memi-board 0.3.1 → 0.4.2

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,127 +1,131 @@
1
- import { Fragment as e, computed as t, createBlock as n, createCommentVNode as r, createElementBlock as i, createElementVNode as a, createTextVNode as o, createVNode as s, defineComponent as c, onMounted as l, openBlock as u, reactive as d, ref as f, renderList as p, resolveComponent as m, resolveDynamicComponent as h, toDisplayString as g, unref as _, watch as v, withCtx as y, withModifiers as b } from "vue";
2
- import { useCollection as x, useCurrentUser as S, useDocument as C, useFirebaseApp as w, useFirebaseAuth as T, useFirestore as E } from "vuefire";
3
- import { collection as D, deleteDoc as O, doc as k, getDoc as A, getDocs as j, increment as M, limit as N, orderBy as P, query as F, serverTimestamp as I, setDoc as L, startAfter as ee, updateDoc as R, writeBatch as z } from "firebase/firestore";
4
- import { GoogleAuthProvider as te, OAuthProvider as ne, createUserWithEmailAndPassword as re, signInWithEmailAndPassword as ie, signInWithPopup as ae, signOut as oe, updateProfile as se } from "firebase/auth";
5
- import { deleteObject as ce, getDownloadURL as le, getStorage as B, listAll as ue, ref as V, uploadBytesResumable as de } from "firebase/storage";
6
- import { GoogleAIBackend as fe, Schema as H, getAI as pe, getGenerativeModel as me } from "firebase/ai";
7
- import { RouterLink as he } from "vue-router";
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, writeBatch as w } from "firebase/firestore";
4
+ import { GoogleAuthProvider as T, OAuthProvider as E, createUserWithEmailAndPassword as D, signInWithEmailAndPassword as O, signInWithPopup as k, signOut as A, updateProfile as j } from "firebase/auth";
5
+ import { deleteObject as M, getDownloadURL as N, getStorage as P, listAll as F, ref as I, uploadBytesResumable as L } from "firebase/storage";
6
+ import { GoogleAIBackend as R, Schema as z, getAI as B, getGenerativeModel as V } from "firebase/ai";
8
7
  //#region src/config.ts
9
- var U = d({
10
- collectionPrefix: "memiBoard",
11
- auth: { providers: ["google", "apple"] },
12
- moderation: {
13
- enabled: !0,
14
- model: "gemini-2.5-flash",
15
- onError: "allow",
16
- moderateImages: !1,
17
- localBlocklist: []
18
- }
19
- });
20
- function ge(e) {
21
- e.collectionPrefix && (U.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(U.auth, e.auth), e.moderation && Object.assign(U.moderation, e.moderation);
8
+ var H = "__MEMI_BOARD_CONFIG__";
9
+ function U() {
10
+ let e = globalThis;
11
+ return e[H] || (e[H] = t({
12
+ collectionPrefix: "memiBoard",
13
+ auth: { providers: ["google", "apple"] },
14
+ moderation: {
15
+ enabled: !0,
16
+ model: "gemini-2.5-flash",
17
+ onError: "allow",
18
+ moderateImages: !1,
19
+ localBlocklist: []
20
+ }
21
+ })), e[H];
22
22
  }
23
- function W() {
24
- return U;
23
+ function W(e) {
24
+ let t = U();
25
+ e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
26
+ }
27
+ function G() {
28
+ return U();
25
29
  }
26
30
  //#endregion
27
31
  //#region src/composables/useMemiBoardAuth.ts
28
- function G() {
29
- let e = W(), n = E(), r = T(), i = S(), a = f("user"), o = f(!0);
30
- function s(t) {
31
- return k(n, `${e.collectionPrefix}Users`, t);
32
- }
33
- async function c(e) {
34
- let t = s(e.uid), n = await A(t);
35
- return n.exists() ? (await R(t, {
32
+ function K() {
33
+ let t = G(), i = l(), o = c(), s = a(), u = n("user"), d = n(!0);
34
+ function f(e) {
35
+ return p(i, `${t.collectionPrefix}Users`, e);
36
+ }
37
+ async function h(e) {
38
+ let t = f(e.uid), n = await m(t);
39
+ return n.exists() ? (await C(t, {
36
40
  displayName: e.displayName,
37
- updatedAt: I()
38
- }), n.data().role ?? "user") : (await L(t, {
41
+ updatedAt: b()
42
+ }), n.data().role ?? "user") : (await x(t, {
39
43
  role: "user",
40
44
  displayName: e.displayName,
41
- updatedAt: I()
45
+ updatedAt: b()
42
46
  }), "user");
43
47
  }
44
- v(i, async (e) => {
45
- if (o.value = !0, !e) {
46
- a.value = "user", o.value = !1;
48
+ r(s, async (e) => {
49
+ if (d.value = !0, !e) {
50
+ u.value = "user", d.value = !1;
47
51
  return;
48
52
  }
49
53
  try {
50
- a.value = await c(e);
54
+ u.value = await h(e);
51
55
  } catch {
52
- a.value = "user";
56
+ u.value = "user";
53
57
  } finally {
54
- o.value = !1;
58
+ d.value = !1;
55
59
  }
56
60
  }, { immediate: !0 });
57
- let l = t(() => !!i.value), u = t(() => a.value === "admin");
58
- function d() {
59
- if (!r) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
60
- return r;
61
+ let g = e(() => !!s.value), _ = e(() => u.value === "admin");
62
+ function v() {
63
+ if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
64
+ return o;
61
65
  }
62
- async function p() {
63
- return ae(d(), new te());
66
+ async function y() {
67
+ return k(v(), new T());
64
68
  }
65
- async function m() {
66
- let e = new ne("apple.com");
67
- return e.addScope("email"), e.addScope("name"), ae(d(), e);
69
+ async function S() {
70
+ let e = new E("apple.com");
71
+ return e.addScope("email"), e.addScope("name"), k(v(), e);
68
72
  }
69
- async function h(e, t) {
70
- return ie(d(), e, t);
73
+ async function w(e, t) {
74
+ return O(v(), e, t);
71
75
  }
72
- async function g(e, t, n) {
73
- let r = await re(d(), e, t);
74
- return n && await se(r.user, { displayName: n }), r;
76
+ async function M(e, t, n) {
77
+ let r = await D(v(), e, t);
78
+ return n && await j(r.user, { displayName: n }), r;
75
79
  }
76
- async function _() {
77
- await oe(d());
80
+ async function N() {
81
+ await A(v());
78
82
  }
79
- function y(e) {
80
- return l.value && (i.value?.uid === e.authorUid || u.value);
83
+ function P(e) {
84
+ return g.value && (s.value?.uid === e.authorUid || _.value);
81
85
  }
82
- function b(e) {
83
- return l.value && (i.value?.uid === e.authorUid || u.value);
86
+ function F(e) {
87
+ return g.value && (s.value?.uid === e.authorUid || _.value);
84
88
  }
85
89
  return {
86
- user: i,
87
- isSignedIn: l,
88
- isAdmin: u,
89
- rolePending: o,
90
- signInWithGoogle: p,
91
- signInWithApple: m,
92
- signInWithEmail: h,
93
- signUpWithEmail: g,
94
- signOut: _,
95
- canEdit: y,
96
- canDelete: y,
97
- canDeleteComment: b
90
+ user: s,
91
+ isSignedIn: g,
92
+ isAdmin: _,
93
+ rolePending: d,
94
+ signInWithGoogle: y,
95
+ signInWithApple: S,
96
+ signInWithEmail: w,
97
+ signUpWithEmail: M,
98
+ signOut: N,
99
+ canEdit: P,
100
+ canDelete: P,
101
+ canDeleteComment: F
98
102
  };
99
103
  }
100
104
  //#endregion
101
105
  //#region src/utils/slugify.ts
102
- function _e(e) {
106
+ function q(e) {
103
107
  return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
104
108
  }
105
- function ve(e, t = 60) {
109
+ function J(e, t = 60) {
106
110
  return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
107
111
  }
108
112
  //#endregion
109
113
  //#region src/composables/useMemiBoardPosts.ts
110
- async function K(e) {
111
- let t = await ue(e).catch(() => ({
114
+ async function Y(e) {
115
+ let t = await F(e).catch(() => ({
112
116
  items: [],
113
117
  prefixes: []
114
118
  }));
115
- await Promise.all([...t.items.map((e) => ce(e)), ...t.prefixes.map((e) => K(e))]);
119
+ await Promise.all([...t.items.map((e) => M(e)), ...t.prefixes.map((e) => Y(e))]);
116
120
  }
117
- function q() {
118
- let e = W(), t = E(), n = w(), r = e.collectionPrefix, i = () => D(t, `${r}Posts`), a = (e) => k(t, `${r}Posts`, e), o = (e) => k(t, `${r}Posts`, e, "body", "main"), s = (e) => D(t, `${r}Posts`, e, "comments");
119
- async function c(e = {}) {
121
+ function X() {
122
+ let e = G(), t = l(), n = s(), r = () => e.collectionPrefix, i = () => u(t, `${r()}Posts`), a = (e) => p(t, `${r()}Posts`, e), o = (e) => p(t, `${r()}Posts`, e, "body", "main"), c = (e) => u(t, `${r()}Posts`, e, "comments");
123
+ async function g(e = {}) {
120
124
  let t = e.pageSize ?? 20, n = e.cursor ? [
121
- P("createdAt", "desc"),
122
- ee(e.cursor),
123
- N(t + 1)
124
- ] : [P("createdAt", "desc"), N(t + 1)], r = await j(F(i(), ...n)), a = r.docs.slice(0, t);
125
+ v("createdAt", "desc"),
126
+ S(e.cursor),
127
+ _(t + 1)
128
+ ] : [v("createdAt", "desc"), _(t + 1)], r = await h(y(i(), ...n)), a = r.docs.slice(0, t);
125
129
  return {
126
130
  posts: a.map((e) => ({
127
131
  id: e.id,
@@ -131,18 +135,18 @@ function q() {
131
135
  hasMore: r.docs.length > t
132
136
  };
133
137
  }
134
- async function l(e) {
135
- let [t, n] = await Promise.all([A(a(e)), A(o(e))]);
138
+ async function T(e) {
139
+ let [t, n] = await Promise.all([m(a(e)), m(o(e))]);
136
140
  return t.exists() ? {
137
141
  id: t.id,
138
142
  ...t.data(),
139
143
  content: n.exists() ? n.data().content ?? "" : ""
140
144
  } : null;
141
145
  }
142
- async function u(e) {
143
- let t = _e(e.title) || "post", n = t, r = 1;
144
- for (; (await A(a(n))).exists();) r++, n = `${t}-${r}`;
145
- return await Promise.all([L(a(n), {
146
+ async function E(e) {
147
+ let t = q(e.title) || "post", n = t, r = 1;
148
+ for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
149
+ return await Promise.all([x(a(n), {
146
150
  title: e.title,
147
151
  tags: e.tags ?? [],
148
152
  ...e.category ? { category: e.category } : {},
@@ -153,65 +157,65 @@ function q() {
153
157
  authorPhoto: e.authorPhoto,
154
158
  moderationStatus: "approved",
155
159
  moderationModel: e.moderationModel ?? null,
156
- createdAt: I(),
157
- updatedAt: I()
158
- }), L(o(n), { content: e.content })]), n;
160
+ createdAt: b(),
161
+ updatedAt: b()
162
+ }), x(o(n), { content: e.content })]), n;
159
163
  }
160
- async function d(e, t) {
161
- await Promise.all([R(a(e), {
164
+ async function D(e, t) {
165
+ await Promise.all([C(a(e), {
162
166
  title: t.title,
163
167
  tags: t.tags ?? [],
164
- ...t.category ? { category: t.category } : {},
168
+ category: t.category ? t.category : f(),
165
169
  attachments: t.attachments ?? [],
166
- updatedAt: I()
167
- }), L(o(e), { content: t.content })]);
170
+ updatedAt: b()
171
+ }), x(o(e), { content: t.content }, { merge: !0 })]);
168
172
  }
169
- async function f(e) {
170
- let i = await j(s(e));
173
+ async function O(e) {
174
+ let i = await h(c(e));
171
175
  for (let e = 0; e < i.docs.length; e += 450) {
172
- let n = z(t);
176
+ let n = w(t);
173
177
  i.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
174
178
  }
175
- let c = B(n);
176
- await O(o(e)), await K(V(c, `${r}/posts/${e}`)), await O(a(e));
179
+ let s = P(n);
180
+ await d(o(e)), await Y(I(s, `${r()}/posts/${e}`)), await d(a(e));
177
181
  }
178
182
  return {
179
- getPosts: c,
180
- getPost: l,
181
- createPost: u,
182
- updatePost: d,
183
- deletePost: f
183
+ getPosts: g,
184
+ getPost: T,
185
+ createPost: E,
186
+ updatePost: D,
187
+ deletePost: O
184
188
  };
185
189
  }
186
190
  //#endregion
187
191
  //#region src/composables/useMemiBoardComments.ts
188
- function J(e) {
189
- let n = W(), r = E(), i = n.collectionPrefix, a = t(() => typeof e == "string" ? e : e.value), o = t(() => F(D(r, `${i}Posts`, a.value, "comments"), P("createdAt", "asc"))), s = x(o);
190
- async function c(e) {
191
- let t = z(r), n = k(D(r, `${i}Posts`, a.value, "comments"));
192
- t.set(n, {
193
- postId: a.value,
192
+ function ee(t) {
193
+ let n = G(), 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);
194
+ async function d(e) {
195
+ let t = w(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
196
+ t.set(i, {
197
+ postId: o.value,
194
198
  body: e.body,
195
199
  authorUid: e.authorUid,
196
200
  authorName: e.authorName,
197
201
  authorPhoto: e.authorPhoto,
198
202
  moderationStatus: "approved",
199
- createdAt: I()
200
- }), t.update(k(r, `${i}Posts`, a.value), { commentCount: M(1) }), await t.commit();
203
+ createdAt: b()
204
+ }), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
201
205
  }
202
- async function l(e) {
203
- let t = z(r);
204
- t.delete(k(r, `${i}Posts`, a.value, "comments", e)), t.update(k(r, `${i}Posts`, a.value), { commentCount: M(-1) }), await t.commit();
206
+ async function f(e) {
207
+ let t = w(r), n = a();
208
+ t.delete(p(r, `${n}Posts`, o.value, "comments", e)), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
205
209
  }
206
210
  return {
207
- comments: s,
208
- addComment: c,
209
- deleteComment: l
211
+ comments: c,
212
+ addComment: d,
213
+ deleteComment: f
210
214
  };
211
215
  }
212
216
  //#endregion
213
217
  //#region src/utils/moderation-prompt.ts
214
- var ye = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category는 상황에 맞게 고르고, reason은 한국어 1문장 (예: \"욕설이 포함되어 있어 게시할 수 없습니다.\")\n- flagged=false면 category는 \"none\", reason은 \"\"", be = [
218
+ var te = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category는 상황에 맞게 고르고, reason은 한국어 1문장 (예: \"욕설이 포함되어 있어 게시할 수 없습니다.\")\n- flagged=false면 category는 \"none\", reason은 \"\"", ne = [
215
219
  "씨발",
216
220
  "시발",
217
221
  "씨팔",
@@ -235,10 +239,10 @@ var ye = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
235
239
  "bitch",
236
240
  "asshole"
237
241
  ];
238
- function xe(e) {
242
+ function re(e) {
239
243
  return new RegExp(e.join("|"), "i");
240
244
  }
241
- var Se = /* @__PURE__ */ new Set([
245
+ var ie = /* @__PURE__ */ new Set([
242
246
  "none",
243
247
  "abuse",
244
248
  "spam",
@@ -246,7 +250,7 @@ var Se = /* @__PURE__ */ new Set([
246
250
  "violence",
247
251
  "other"
248
252
  ]);
249
- function Ce(e) {
253
+ function Z(e) {
250
254
  let t = e.trim();
251
255
  if (!t) return null;
252
256
  let n;
@@ -264,7 +268,7 @@ function Ce(e) {
264
268
  if (typeof n != "object" || !n) return null;
265
269
  let r = n;
266
270
  if (typeof r.flagged != "boolean") return null;
267
- let i = typeof r.category == "string" && Se.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
271
+ let i = typeof r.category == "string" && ie.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
268
272
  return {
269
273
  flagged: r.flagged,
270
274
  category: i,
@@ -273,13 +277,13 @@ function Ce(e) {
273
277
  }
274
278
  //#endregion
275
279
  //#region src/composables/useMemiBoardModeration.ts
276
- var Y = {
280
+ var Q = {
277
281
  flagged: !1,
278
282
  category: "none",
279
283
  reason: ""
280
284
  };
281
- function X() {
282
- let e = W().moderation ?? {}, t = w(), n = xe([...be, ...e.localBlocklist ?? []]);
285
+ function ae() {
286
+ let e = G().moderation ?? {}, t = s(), n = re([...ne, ...e.localBlocklist ?? []]);
283
287
  function r(e) {
284
288
  return n.test(e) ? {
285
289
  flagged: !0,
@@ -288,21 +292,21 @@ function X() {
288
292
  } : null;
289
293
  }
290
294
  function i() {
291
- let n = pe(t, {
292
- backend: new fe(),
295
+ let n = B(t, {
296
+ backend: new R(),
293
297
  useLimitedUseAppCheckTokens: !0
294
298
  });
295
- return me(n, {
299
+ return V(n, {
296
300
  model: e.model ?? "gemini-2.5-flash",
297
- systemInstruction: ye,
301
+ systemInstruction: te,
298
302
  generationConfig: {
299
303
  temperature: 0,
300
304
  maxOutputTokens: 256,
301
305
  responseMimeType: "application/json",
302
- responseSchema: H.object({ properties: {
303
- flagged: H.boolean(),
304
- category: H.string(),
305
- reason: H.string()
306
+ responseSchema: z.object({ properties: {
307
+ flagged: z.boolean(),
308
+ category: z.string(),
309
+ reason: z.string()
306
310
  } })
307
311
  }
308
312
  });
@@ -314,18 +318,18 @@ function X() {
314
318
  reason: "검열 서비스에 일시적으로 연결할 수 없어 제출이 보류되었습니다. 잠시 후 다시 시도해 주세요.",
315
319
  error: !0
316
320
  } : {
317
- ...Y,
321
+ ...Q,
318
322
  error: !0
319
323
  };
320
324
  }
321
325
  async function o(t) {
322
326
  let n = t.trim();
323
- if (!n) return Y;
327
+ if (!n) return Q;
324
328
  let o = r(n);
325
329
  if (o) return o;
326
- if (e.enabled === !1) return Y;
330
+ if (e.enabled === !1) return Q;
327
331
  try {
328
- let e = Ce((await i().generateContent(`심사할 텍스트:\n"""${n.slice(0, 4e3)}"""`)).response.text());
332
+ let e = Z((await i().generateContent(`심사할 텍스트:\n"""${n.slice(0, 4e3)}"""`)).response.text());
329
333
  return e ? {
330
334
  flagged: e.flagged,
331
335
  category: e.category,
@@ -335,13 +339,13 @@ function X() {
335
339
  return console.warn("[memi-board] AI 검열 호출 실패 → onError 정책 적용", e), a();
336
340
  }
337
341
  }
338
- async function s(t) {
339
- if (e.enabled === !1 || !e.moderateImages) return Y;
342
+ async function c(t) {
343
+ if (e.enabled === !1 || !e.moderateImages) return Q;
340
344
  try {
341
345
  let e = await new Promise((e, n) => {
342
346
  let r = new FileReader();
343
347
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
344
- }), n = Ce((await i().generateContent([{ inlineData: {
348
+ }), n = Z((await i().generateContent([{ inlineData: {
345
349
  mimeType: t.type || "image/jpeg",
346
350
  data: e
347
351
  } }, "이 이미지를 심사해 주세요."])).response.text());
@@ -356,15 +360,15 @@ function X() {
356
360
  }
357
361
  return {
358
362
  checkText: o,
359
- checkImage: s
363
+ checkImage: c
360
364
  };
361
365
  }
362
366
  //#endregion
363
367
  //#region src/composables/useMemiBoardStorage.ts
364
- function we() {
365
- let e = W(), t = w(), n = e.collectionPrefix;
368
+ function oe() {
369
+ let e = G(), t = s(), n = () => e.collectionPrefix;
366
370
  function r(e, r, i) {
367
- let a = B(t), o = `${n}/posts/${r}/attachments/${Date.now()}-${ve(e.name)}`, s = V(a, o), c = de(s, e, {
371
+ let a = P(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${J(e.name)}`, s = I(a, o), c = L(s, e, {
368
372
  contentType: e.type || "application/octet-stream",
369
373
  contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
370
374
  });
@@ -373,7 +377,7 @@ function we() {
373
377
  c.on("state_changed", (e) => {
374
378
  i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
375
379
  }, n, async () => {
376
- let n = await le(s);
380
+ let n = await N(s);
377
381
  t({
378
382
  name: e.name,
379
383
  url: n,
@@ -387,8 +391,8 @@ function we() {
387
391
  };
388
392
  }
389
393
  async function i(e) {
390
- let n = B(t);
391
- await ce(V(n, e.path)).catch(() => {});
394
+ let n = P(t);
395
+ await M(I(n, e.path)).catch(() => {});
392
396
  }
393
397
  return {
394
398
  uploadAttachment: r,
@@ -397,7 +401,7 @@ function we() {
397
401
  }
398
402
  //#endregion
399
403
  //#region src/composables/useMemiBoardSettings.ts
400
- var Te = [
404
+ var $ = [
401
405
  {
402
406
  id: "free",
403
407
  label: "자유"
@@ -411,664 +415,65 @@ var Te = [
411
415
  label: "질문"
412
416
  }
413
417
  ];
414
- function Z() {
415
- let e = W(), n = E(), { isAdmin: r } = G(), i = e.collectionPrefix, a = k(n, `${i}Settings`, "config"), { data: o, pending: s } = C(a), c = t(() => {
416
- let e = o.value?.categories;
417
- return e && e.length > 0 ? e : Te;
418
+ function se() {
419
+ let t = G(), n = l(), { isSignedIn: i, isAdmin: a } = K(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
420
+ let e = c.value?.categories;
421
+ return e && e.length > 0 ? e : $;
418
422
  });
419
- function l(e) {
420
- if (e) return c.value.find((t) => t.id === e)?.label ?? e;
423
+ function f(e) {
424
+ if (e) return d.value.find((t) => t.id === e)?.label ?? e;
425
+ }
426
+ async function h() {
427
+ if (!i.value) return;
428
+ let e = s.value, t = await m(e).catch(() => null);
429
+ !t || t.exists() || await x(e, {
430
+ categories: $,
431
+ updatedAt: b()
432
+ });
421
433
  }
422
- return v([r, s], async ([e, t]) => {
423
- if (!e || t || o.value) return;
424
- let n = await A(a).catch(() => null);
425
- !n || n.exists() || await L(a, {
426
- categories: Te,
427
- updatedAt: I()
428
- }).catch(() => {});
434
+ async function g(e) {
435
+ let t = e.trim();
436
+ if (!t) throw Error("카테고리 이름을 입력해 주세요.");
437
+ if (!i.value) throw Error("로그인이 필요합니다.");
438
+ await h();
439
+ let n = d.value.find((e) => e.label === t || e.id === q(t));
440
+ if (n) return n.id;
441
+ let r = q(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
442
+ if (a.has(r)) {
443
+ let e = 2;
444
+ for (; a.has(`${r}-${e}`);) e++;
445
+ r = `${r}-${e}`;
446
+ }
447
+ let o = [...d.value, {
448
+ id: r,
449
+ label: t
450
+ }], c = s.value;
451
+ return (await m(c)).exists() ? await C(c, {
452
+ categories: o,
453
+ updatedAt: b()
454
+ }) : await x(c, {
455
+ categories: o,
456
+ updatedAt: b()
457
+ }), r;
458
+ }
459
+ return r([
460
+ a,
461
+ i,
462
+ u
463
+ ], async ([e, t, n]) => {
464
+ n || c.value || !e && !t || await h().catch(() => {});
429
465
  }, { immediate: !0 }), {
430
- categories: c,
431
- categoryLabel: l,
432
- settingsPending: s
466
+ categories: d,
467
+ categoryLabel: f,
468
+ settingsPending: u,
469
+ ensureSettings: h,
470
+ addCategory: g,
471
+ DEFAULT_CATEGORIES: $
433
472
  };
434
473
  }
435
474
  //#endregion
436
- //#region src/utils/formatDate.ts
437
- function Q(e) {
438
- return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
439
- }
440
- //#endregion
441
- //#region src/components/List.vue?vue&type=script&setup=true&lang.ts
442
- var Ee = { class: "flex flex-col gap-2" }, De = {
443
- key: 1,
444
- class: "text-sm text-muted text-center py-8"
445
- }, Oe = { class: "flex items-center justify-between gap-4" }, ke = { class: "flex items-center gap-2 min-w-0" }, Ae = { class: "font-medium truncate" }, je = { class: "text-xs text-muted shrink-0" }, Me = { class: "flex items-center gap-3 text-xs text-muted mt-2" }, Ne = { class: "flex items-center gap-1" }, Pe = {
446
- key: 0,
447
- class: "flex items-center gap-1"
448
- }, Fe = /* @__PURE__ */ c({
449
- __name: "List",
450
- props: {
451
- pageSize: { default: 20 },
452
- linkBase: {}
453
- },
454
- emits: ["select"],
455
- setup(t, { emit: c }) {
456
- let d = t, v = c, { getPosts: b } = q(), { categoryLabel: x } = Z(), S = f([]), C = f(void 0), w = f(!1), T = f(!1), E = f(!0);
457
- async function D() {
458
- T.value = !0;
459
- try {
460
- let e = await b({
461
- pageSize: d.pageSize,
462
- cursor: C.value
463
- });
464
- S.value.push(...e.posts), C.value = e.cursor, w.value = e.hasMore;
465
- } finally {
466
- T.value = !1, E.value = !1;
467
- }
468
- }
469
- l(D);
470
- function O(e) {
471
- d.linkBase || v("select", e);
472
- }
473
- return (c, l) => {
474
- let d = m("USkeleton"), f = m("UBadge"), v = m("UIcon"), b = m("UCard"), C = m("UButton");
475
- return u(), i("div", Ee, [
476
- E.value ? (u(), i(e, { key: 0 }, p(3, (e) => s(d, {
477
- key: e,
478
- class: "h-20 w-full"
479
- })), 64)) : S.value.length ? r("", !0) : (u(), i("p", De, " 아직 게시글이 없습니다. ")),
480
- (u(!0), i(e, null, p(S.value, (e) => (u(), n(h(t.linkBase ? _(he) : "button"), {
481
- key: e.id,
482
- to: t.linkBase ? `${t.linkBase}/${e.id}` : void 0,
483
- class: "text-left",
484
- onClick: (t) => O(e)
485
- }, {
486
- default: y(() => [s(b, { class: "hover:bg-elevated/50 transition-colors" }, {
487
- default: y(() => [a("div", Oe, [a("div", ke, [e.category ? (u(), n(f, {
488
- key: 0,
489
- label: _(x)(e.category),
490
- variant: "subtle",
491
- size: "sm"
492
- }, null, 8, ["label"])) : r("", !0), a("h3", Ae, g(e.title), 1)]), a("span", je, g(_(Q)(e.createdAt)), 1)]), a("div", Me, [
493
- a("span", null, g(e.authorName ?? "익명"), 1),
494
- a("span", Ne, [s(v, {
495
- name: "i-lucide-message-circle",
496
- class: "size-3"
497
- }), o(g(e.commentCount), 1)]),
498
- e.attachments?.length ? (u(), i("span", Pe, [s(v, {
499
- name: "i-lucide-paperclip",
500
- class: "size-3"
501
- }), o(g(e.attachments.length), 1)])) : r("", !0)
502
- ])]),
503
- _: 2
504
- }, 1024)]),
505
- _: 2
506
- }, 1032, ["to", "onClick"]))), 128)),
507
- w.value ? (u(), n(C, {
508
- key: 2,
509
- variant: "outline",
510
- color: "neutral",
511
- label: "더 보기",
512
- block: "",
513
- loading: T.value,
514
- onClick: D
515
- }, null, 8, ["loading"])) : r("", !0)
516
- ]);
517
- };
518
- }
519
- }), Ie = { class: "space-y-2" }, Le = ["src"], Re = { class: "flex-1 min-w-0" }, ze = { class: "text-sm font-medium truncate" }, Be = { class: "text-xs text-muted" }, Ve = {
520
- key: 1,
521
- class: "text-xs text-error"
522
- }, He = {
523
- key: 1,
524
- class: "text-xs text-muted"
525
- }, $ = /* @__PURE__ */ c({
526
- __name: "Attachments",
527
- props: {
528
- modelValue: {},
529
- postId: {},
530
- editable: {
531
- type: Boolean,
532
- default: !1
533
- },
534
- maxFiles: { default: 5 }
535
- },
536
- emits: ["update:modelValue"],
537
- setup(t, { emit: o }) {
538
- let c = t, l = o, { uploadAttachment: d, deleteAttachment: h } = we(), _ = f(null), v = f(!1), y = f(0), b = f(null), x = f("");
539
- function S(e) {
540
- return e.startsWith("image/");
541
- }
542
- async function C(e) {
543
- let t = e.target, n = Array.from(t.files ?? []);
544
- if (t.value = "", !n.length) return;
545
- let r = c.maxFiles - c.modelValue.length, i = n.slice(0, r);
546
- v.value = !0, x.value = "";
547
- try {
548
- let e = [];
549
- for (let t of i) {
550
- y.value = 0;
551
- let { promise: n } = d(t, c.postId, (e) => {
552
- y.value = e;
553
- });
554
- e.push(await n);
555
- }
556
- l("update:modelValue", [...c.modelValue, ...e]);
557
- } catch (e) {
558
- x.value = e.message;
559
- } finally {
560
- v.value = !1;
561
- }
562
- }
563
- async function w(e) {
564
- let t = c.modelValue[e];
565
- if (t) {
566
- b.value = e;
567
- try {
568
- await h(t), l("update:modelValue", c.modelValue.filter((t, n) => n !== e));
569
- } finally {
570
- b.value = null;
571
- }
572
- }
573
- }
574
- function T(e) {
575
- return e === "application/pdf" ? "i-lucide-file-text" : e.includes("zip") || e.includes("archive") || e.includes("compressed") ? "i-lucide-archive" : e.includes("spreadsheet") || e.includes("excel") || e.includes("csv") ? "i-lucide-table" : e.includes("presentation") || e.includes("powerpoint") ? "i-lucide-presentation" : S(e) ? "i-lucide-image" : e.includes("video") ? "i-lucide-video" : e.includes("audio") ? "i-lucide-music" : "i-lucide-file";
576
- }
577
- function E(e) {
578
- return e < 1024 ? `${e} B` : e < 1048576 ? `${(e / 1024).toFixed(1)} KB` : `${(e / 1024 / 1024).toFixed(1)} MB`;
579
- }
580
- return (o, c) => {
581
- let l = m("UIcon"), d = m("UButton"), f = m("UProgress");
582
- return u(), i("div", Ie, [(u(!0), i(e, null, p(t.modelValue, (e, r) => (u(), i("div", {
583
- key: e.path,
584
- class: "flex items-center gap-3 px-3 py-2 rounded-lg border border-default"
585
- }, [
586
- S(e.type) ? (u(), i("img", {
587
- key: 0,
588
- src: e.url,
589
- class: "size-10 rounded object-cover shrink-0"
590
- }, null, 8, Le)) : (u(), n(l, {
591
- key: 1,
592
- name: T(e.type),
593
- class: "size-5 shrink-0 text-muted"
594
- }, null, 8, ["name"])),
595
- a("div", Re, [a("p", ze, g(e.name), 1), a("p", Be, g(E(e.size)), 1)]),
596
- t.editable ? (u(), n(d, {
597
- key: 2,
598
- icon: "i-lucide-x",
599
- size: "xs",
600
- color: "error",
601
- variant: "ghost",
602
- loading: b.value === r,
603
- onClick: (e) => w(r)
604
- }, null, 8, ["loading", "onClick"])) : (u(), n(d, {
605
- key: 3,
606
- icon: "i-lucide-download",
607
- size: "xs",
608
- color: "neutral",
609
- variant: "ghost",
610
- to: e.url,
611
- target: "_blank"
612
- }, null, 8, ["to"]))
613
- ]))), 128)), t.editable && t.modelValue.length < t.maxFiles ? (u(), i(e, { key: 0 }, [
614
- a("input", {
615
- ref_key: "fileInput",
616
- ref: _,
617
- type: "file",
618
- multiple: "",
619
- class: "hidden",
620
- onChange: C
621
- }, null, 544),
622
- s(d, {
623
- icon: "i-lucide-paperclip",
624
- label: `파일 첨부 (${t.modelValue.length} / ${t.maxFiles})`,
625
- variant: "outline",
626
- color: "neutral",
627
- size: "sm",
628
- loading: v.value,
629
- onClick: c[0] ||= (e) => _.value?.click()
630
- }, null, 8, ["label", "loading"]),
631
- v.value ? (u(), n(f, {
632
- key: 0,
633
- "model-value": Math.round(y.value * 100)
634
- }, null, 8, ["model-value"])) : r("", !0),
635
- x.value ? (u(), i("p", Ve, g(x.value), 1)) : r("", !0)
636
- ], 64)) : t.editable ? (u(), i("p", He, " 최대 " + g(t.maxFiles) + "개까지 첨부할 수 있습니다. ", 1)) : r("", !0)]);
637
- };
638
- }
639
- }), Ue = { class: "flex justify-between items-center" }, We = {
640
- key: 0,
641
- class: "text-xs text-error"
642
- }, Ge = {
643
- key: 1,
644
- class: "text-sm text-muted"
645
- }, Ke = /* @__PURE__ */ c({
646
- __name: "CommentForm",
647
- props: { postId: {} },
648
- setup(e) {
649
- let t = e, { user: n, isSignedIn: o } = G(), { addComment: c } = J(t.postId), { checkText: l } = X(), d = f(""), p = f(!1), h = f("");
650
- async function v() {
651
- if (h.value = "", d.value.trim()) {
652
- if (!n.value) {
653
- h.value = "로그인이 필요합니다.";
654
- return;
655
- }
656
- p.value = !0;
657
- try {
658
- let e = await l(d.value);
659
- if (e.flagged) {
660
- h.value = e.reason || "작성할 수 없는 내용이 포함되어 있습니다.";
661
- return;
662
- }
663
- await c({
664
- body: d.value,
665
- authorUid: n.value.uid,
666
- authorName: n.value.displayName,
667
- authorPhoto: n.value.photoURL
668
- }), d.value = "";
669
- } catch (e) {
670
- h.value = e.message;
671
- } finally {
672
- p.value = !1;
673
- }
674
- }
675
- }
676
- return (e, t) => {
677
- let n = m("UTextarea"), c = m("UButton");
678
- return _(o) ? (u(), i("form", {
679
- key: 0,
680
- class: "flex flex-col gap-2",
681
- onSubmit: b(v, ["prevent"])
682
- }, [s(n, {
683
- modelValue: d.value,
684
- "onUpdate:modelValue": t[0] ||= (e) => d.value = e,
685
- placeholder: "댓글을 입력하세요",
686
- rows: 2
687
- }, null, 8, ["modelValue"]), a("div", Ue, [
688
- h.value ? (u(), i("p", We, g(h.value), 1)) : r("", !0),
689
- t[1] ||= a("div", { class: "flex-1" }, null, -1),
690
- s(c, {
691
- type: "submit",
692
- size: "sm",
693
- label: "댓글 작성",
694
- loading: p.value,
695
- disabled: !d.value.trim()
696
- }, null, 8, ["loading", "disabled"])
697
- ])], 32)) : (u(), i("p", Ge, " 댓글을 작성하려면 로그인이 필요합니다. "));
698
- };
699
- }
700
- }), qe = { class: "flex flex-col gap-3" }, Je = {
701
- key: 0,
702
- class: "text-sm text-muted"
703
- }, Ye = { class: "flex-1 min-w-0" }, Xe = { class: "flex items-center gap-2" }, Ze = { class: "text-sm font-medium" }, Qe = { class: "text-xs text-muted" }, $e = { class: "text-sm whitespace-pre-wrap break-words" }, et = /* @__PURE__ */ c({
704
- __name: "CommentList",
705
- props: { postId: {} },
706
- setup(t) {
707
- let { comments: o, deleteComment: c } = J(t.postId), { canDeleteComment: l } = G(), d = f(null);
708
- async function h(e) {
709
- d.value = e;
710
- try {
711
- await c(e);
712
- } finally {
713
- d.value = null;
714
- }
715
- }
716
- return (t, c) => {
717
- let f = m("UAvatar"), v = m("UButton");
718
- return u(), i("div", qe, [_(o).length ? r("", !0) : (u(), i("p", Je, " 아직 댓글이 없습니다. ")), (u(!0), i(e, null, p(_(o), (e) => (u(), i("div", {
719
- key: e.id,
720
- class: "flex items-start gap-3"
721
- }, [
722
- s(f, {
723
- src: e.authorPhoto ?? void 0,
724
- alt: e.authorName ?? "익명",
725
- size: "sm"
726
- }, null, 8, ["src", "alt"]),
727
- a("div", Ye, [a("div", Xe, [a("span", Ze, g(e.authorName ?? "익명"), 1), a("span", Qe, g(_(Q)(e.createdAt)), 1)]), a("p", $e, g(e.body), 1)]),
728
- _(l)(e) ? (u(), n(v, {
729
- key: 0,
730
- icon: "i-lucide-trash-2",
731
- size: "xs",
732
- variant: "ghost",
733
- color: "error",
734
- loading: d.value === e.id,
735
- onClick: (t) => h(e.id)
736
- }, null, 8, ["loading", "onClick"])) : r("", !0)
737
- ]))), 128))]);
738
- };
739
- }
740
- }), tt = {
741
- key: 0,
742
- class: "flex flex-col gap-3"
743
- }, nt = {
744
- key: 1,
745
- class: "text-sm text-muted"
746
- }, rt = {
747
- key: 2,
748
- class: "flex flex-col gap-6"
749
- }, it = { class: "flex flex-col gap-2" }, at = { class: "flex items-start justify-between gap-4" }, ot = { class: "flex items-center gap-2 min-w-0" }, st = { class: "text-2xl font-semibold" }, ct = {
750
- key: 0,
751
- class: "flex gap-2 shrink-0"
752
- }, lt = { class: "flex items-center gap-3 text-sm text-muted" }, ut = {
753
- key: 0,
754
- class: "flex gap-1"
755
- }, dt = { class: "whitespace-pre-wrap break-words leading-relaxed" }, ft = { class: "flex flex-col gap-4 border-t border-default pt-4" }, pt = { class: "text-sm font-medium text-muted" }, mt = /* @__PURE__ */ c({
756
- __name: "Detail",
757
- props: { postId: {} },
758
- emits: ["deleted", "edit"],
759
- setup(t, { emit: c }) {
760
- let d = t, h = c, { getPost: b, deletePost: x } = q(), { canEdit: S, canDelete: C } = G(), { categoryLabel: w } = Z(), { comments: T } = J(d.postId), E = f(null), D = f(!0), O = f(!1), k = f(!1);
761
- async function A() {
762
- D.value = !0, k.value = !1;
763
- let e = await b(d.postId);
764
- e || (k.value = !0), E.value = e, D.value = !1;
765
- }
766
- l(A), v(() => d.postId, A);
767
- async function j() {
768
- if (E.value && window.confirm("이 게시글을 삭제하시겠습니까? 되돌릴 수 없습니다.")) {
769
- O.value = !0;
770
- try {
771
- await x(d.postId), h("deleted");
772
- } finally {
773
- O.value = !1;
774
- }
775
- }
776
- }
777
- return (c, l) => {
778
- let d = m("USkeleton"), f = m("UBadge"), v = m("UButton"), b = m("UAvatar");
779
- return D.value ? (u(), i("div", tt, [s(d, { class: "h-8 w-2/3" }), s(d, { class: "h-32 w-full" })])) : k.value ? (u(), i("p", nt, " 게시글을 찾을 수 없습니다. ")) : E.value ? (u(), i("div", rt, [
780
- a("header", it, [
781
- a("div", at, [a("div", ot, [E.value.category ? (u(), n(f, {
782
- key: 0,
783
- label: _(w)(E.value.category),
784
- variant: "subtle"
785
- }, null, 8, ["label"])) : r("", !0), a("h1", st, g(E.value.title), 1)]), _(S)(E.value) || _(C)(E.value) ? (u(), i("div", ct, [_(S)(E.value) ? (u(), n(v, {
786
- key: 0,
787
- icon: "i-lucide-pencil",
788
- size: "sm",
789
- variant: "outline",
790
- color: "neutral",
791
- label: "수정",
792
- onClick: l[0] ||= (e) => h("edit", t.postId)
793
- })) : r("", !0), _(C)(E.value) ? (u(), n(v, {
794
- key: 1,
795
- icon: "i-lucide-trash-2",
796
- size: "sm",
797
- variant: "outline",
798
- color: "error",
799
- label: "삭제",
800
- loading: O.value,
801
- onClick: j
802
- }, null, 8, ["loading"])) : r("", !0)])) : r("", !0)]),
803
- a("div", lt, [
804
- s(b, {
805
- src: E.value.authorPhoto ?? void 0,
806
- alt: E.value.authorName ?? "익명",
807
- size: "xs"
808
- }, null, 8, ["src", "alt"]),
809
- a("span", null, g(E.value.authorName ?? "익명"), 1),
810
- a("span", null, g(_(Q)(E.value.createdAt)), 1)
811
- ]),
812
- E.value.tags?.length ? (u(), i("div", ut, [(u(!0), i(e, null, p(E.value.tags, (e) => (u(), n(f, {
813
- key: e,
814
- variant: "subtle",
815
- color: "neutral",
816
- size: "sm"
817
- }, {
818
- default: y(() => [o(g(e), 1)]),
819
- _: 2
820
- }, 1024))), 128))])) : r("", !0)
821
- ]),
822
- a("p", dt, g(E.value.content), 1),
823
- E.value.attachments?.length ? (u(), n($, {
824
- key: 0,
825
- "model-value": E.value.attachments,
826
- "post-id": t.postId
827
- }, null, 8, ["model-value", "post-id"])) : r("", !0),
828
- a("section", ft, [
829
- a("h2", pt, " 댓글 " + g(_(T).length), 1),
830
- s(Ke, { "post-id": t.postId }, null, 8, ["post-id"]),
831
- s(et, { "post-id": t.postId }, null, 8, ["post-id"])
832
- ])
833
- ])) : r("", !0);
834
- };
835
- }
836
- }), ht = {
837
- key: 0,
838
- class: "flex flex-col gap-3"
839
- }, gt = {
840
- key: 0,
841
- class: "text-sm text-error"
842
- }, _t = { class: "flex gap-2" }, vt = /* @__PURE__ */ c({
843
- __name: "Editor",
844
- props: { postId: {} },
845
- emits: ["saved", "cancel"],
846
- setup(e, { emit: n }) {
847
- let o = e, c = n, { user: d } = G(), { getPost: p, createPost: h, updatePost: _ } = q(), { checkText: v } = X(), { categories: y } = Z(), x = f(""), S = f(""), C = f(""), w = f(void 0), T = f([]), E = t(() => y.value.map((e) => ({
848
- label: e.label,
849
- value: e.id
850
- }))), D = f(!!o.postId), O = f(!1), k = f(""), A = f(o.postId ?? `new-${Date.now()}`);
851
- l(async () => {
852
- if (o.postId) try {
853
- let e = await p(o.postId);
854
- if (!e) {
855
- k.value = "게시글을 찾을 수 없습니다.";
856
- return;
857
- }
858
- x.value = e.title, S.value = e.content, C.value = (e.tags ?? []).join(", "), w.value = e.category, T.value = e.attachments ?? [];
859
- } finally {
860
- D.value = !1;
861
- }
862
- });
863
- async function j() {
864
- if (k.value = "", !x.value.trim() || !S.value.trim()) {
865
- k.value = "제목과 내용을 입력해 주세요.";
866
- return;
867
- }
868
- if (!d.value) {
869
- k.value = "로그인이 필요합니다.";
870
- return;
871
- }
872
- O.value = !0;
873
- try {
874
- let e = await v(`${x.value}\n${S.value}`);
875
- if (e.flagged) {
876
- k.value = e.reason || "게시할 수 없는 내용이 포함되어 있습니다.";
877
- return;
878
- }
879
- let t = C.value.split(",").map((e) => e.trim()).filter(Boolean);
880
- if (o.postId) await _(o.postId, {
881
- title: x.value,
882
- content: S.value,
883
- tags: t,
884
- category: w.value,
885
- attachments: T.value
886
- }), c("saved", o.postId);
887
- else {
888
- let e = await h({
889
- title: x.value,
890
- content: S.value,
891
- tags: t,
892
- category: w.value,
893
- attachments: T.value,
894
- authorUid: d.value.uid,
895
- authorName: d.value.displayName,
896
- authorPhoto: d.value.photoURL
897
- });
898
- c("saved", e);
899
- }
900
- } catch (e) {
901
- k.value = e.message;
902
- } finally {
903
- O.value = !1;
904
- }
905
- }
906
- return (t, n) => {
907
- let o = m("USkeleton"), l = m("USelect"), d = m("UInput"), f = m("UTextarea"), p = m("UButton");
908
- return D.value ? (u(), i("div", ht, [s(o, { class: "h-10 w-full" }), s(o, { class: "h-40 w-full" })])) : (u(), i("form", {
909
- key: 1,
910
- class: "flex flex-col gap-4",
911
- onSubmit: b(j, ["prevent"])
912
- }, [
913
- s(l, {
914
- modelValue: w.value,
915
- "onUpdate:modelValue": n[0] ||= (e) => w.value = e,
916
- items: E.value,
917
- placeholder: "카테고리 선택",
918
- class: "w-40"
919
- }, null, 8, ["modelValue", "items"]),
920
- s(d, {
921
- modelValue: x.value,
922
- "onUpdate:modelValue": n[1] ||= (e) => x.value = e,
923
- placeholder: "제목",
924
- size: "lg",
925
- required: ""
926
- }, null, 8, ["modelValue"]),
927
- s(f, {
928
- modelValue: S.value,
929
- "onUpdate:modelValue": n[2] ||= (e) => S.value = e,
930
- placeholder: "내용을 입력하세요",
931
- rows: 12,
932
- required: ""
933
- }, null, 8, ["modelValue"]),
934
- s(d, {
935
- modelValue: C.value,
936
- "onUpdate:modelValue": n[3] ||= (e) => C.value = e,
937
- placeholder: "태그 (쉼표로 구분)"
938
- }, null, 8, ["modelValue"]),
939
- s($, {
940
- modelValue: T.value,
941
- "onUpdate:modelValue": n[4] ||= (e) => T.value = e,
942
- "post-id": A.value,
943
- editable: ""
944
- }, null, 8, ["modelValue", "post-id"]),
945
- k.value ? (u(), i("p", gt, g(k.value), 1)) : r("", !0),
946
- a("div", _t, [s(p, {
947
- type: "submit",
948
- loading: O.value,
949
- label: e.postId ? "수정 완료" : "게시하기"
950
- }, null, 8, ["loading", "label"]), s(p, {
951
- variant: "ghost",
952
- color: "neutral",
953
- label: "취소",
954
- onClick: n[5] ||= (e) => c("cancel")
955
- })])
956
- ], 32));
957
- };
958
- }
959
- }), yt = { class: "flex flex-col gap-4 max-w-sm" }, bt = {
960
- key: 0,
961
- class: "flex items-center gap-2 text-xs text-muted"
962
- }, xt = {
963
- key: 3,
964
- class: "text-sm text-error"
965
- }, St = /* @__PURE__ */ c({
966
- __name: "SignIn",
967
- setup(c) {
968
- let l = W(), d = t(() => l.auth?.providers ?? ["google", "apple"]), p = t(() => d.value.includes("google") || d.value.includes("apple")), { signInWithGoogle: h, signInWithApple: _, signInWithEmail: v, signUpWithEmail: y } = G(), x = f("signin"), S = f(""), C = f(""), w = f(""), T = f(!1), E = f("");
969
- async function D() {
970
- T.value = !0, E.value = "";
971
- try {
972
- await h();
973
- } catch (e) {
974
- E.value = e.message;
975
- } finally {
976
- T.value = !1;
977
- }
978
- }
979
- async function O() {
980
- T.value = !0, E.value = "";
981
- try {
982
- await _();
983
- } catch (e) {
984
- E.value = e.message;
985
- } finally {
986
- T.value = !1;
987
- }
988
- }
989
- async function k() {
990
- T.value = !0, E.value = "";
991
- try {
992
- x.value === "signin" ? await v(S.value, C.value) : await y(S.value, C.value, w.value || void 0);
993
- } catch (e) {
994
- E.value = e.message;
995
- } finally {
996
- T.value = !1;
997
- }
998
- }
999
- return (t, c) => {
1000
- let l = m("UButton"), f = m("UInput");
1001
- return u(), i("div", yt, [
1002
- d.value.includes("google") ? (u(), n(l, {
1003
- key: 0,
1004
- icon: "i-simple-icons-google",
1005
- color: "neutral",
1006
- variant: "outline",
1007
- label: "Google로 로그인",
1008
- block: "",
1009
- loading: T.value,
1010
- onClick: D
1011
- }, null, 8, ["loading"])) : r("", !0),
1012
- d.value.includes("apple") ? (u(), n(l, {
1013
- key: 1,
1014
- icon: "i-simple-icons-apple",
1015
- color: "neutral",
1016
- variant: "outline",
1017
- label: "Apple로 로그인",
1018
- block: "",
1019
- loading: T.value,
1020
- onClick: O
1021
- }, null, 8, ["loading"])) : r("", !0),
1022
- d.value.includes("emailPassword") ? (u(), i(e, { key: 2 }, [
1023
- p.value ? (u(), i("div", bt, [...c[4] ||= [
1024
- a("div", { class: "flex-1 border-t border-default" }, null, -1),
1025
- o(" 또는 ", -1),
1026
- a("div", { class: "flex-1 border-t border-default" }, null, -1)
1027
- ]])) : r("", !0),
1028
- a("form", {
1029
- class: "flex flex-col gap-3",
1030
- onSubmit: b(k, ["prevent"])
1031
- }, [
1032
- x.value === "signup" ? (u(), n(f, {
1033
- key: 0,
1034
- modelValue: w.value,
1035
- "onUpdate:modelValue": c[0] ||= (e) => w.value = e,
1036
- placeholder: "이름"
1037
- }, null, 8, ["modelValue"])) : r("", !0),
1038
- s(f, {
1039
- modelValue: S.value,
1040
- "onUpdate:modelValue": c[1] ||= (e) => S.value = e,
1041
- type: "email",
1042
- placeholder: "이메일",
1043
- required: ""
1044
- }, null, 8, ["modelValue"]),
1045
- s(f, {
1046
- modelValue: C.value,
1047
- "onUpdate:modelValue": c[2] ||= (e) => C.value = e,
1048
- type: "password",
1049
- placeholder: "비밀번호",
1050
- required: ""
1051
- }, null, 8, ["modelValue"]),
1052
- s(l, {
1053
- type: "submit",
1054
- block: "",
1055
- loading: T.value,
1056
- label: x.value === "signin" ? "로그인" : "회원가입"
1057
- }, null, 8, ["loading", "label"])
1058
- ], 32),
1059
- s(l, {
1060
- variant: "link",
1061
- size: "sm",
1062
- color: "neutral",
1063
- label: x.value === "signin" ? "계정이 없으신가요? 회원가입" : "이미 계정이 있으신가요? 로그인",
1064
- onClick: c[3] ||= (e) => x.value = x.value === "signin" ? "signup" : "signin"
1065
- }, null, 8, ["label"])
1066
- ], 64)) : r("", !0),
1067
- E.value ? (u(), i("p", xt, g(E.value), 1)) : r("", !0)
1068
- ]);
1069
- };
1070
- }
1071
- }), Ct = [
475
+ //#region src/data/versionHistory.ts
476
+ var ce = [
1072
477
  {
1073
478
  version: "0.2.2",
1074
479
  date: "2026-07-31",
@@ -1092,30 +497,13 @@ var Ee = { class: "flex flex-col gap-2" }, De = {
1092
497
  date: "2026-07-27",
1093
498
  highlights: ["프로젝트 최초 생성 (아직 게시판 기능은 없는 빈 Nuxt 4 프로젝트)"]
1094
499
  }
1095
- ], wt = { class: "flex flex-col gap-6" }, Tt = { class: "flex items-center gap-2" }, Et = { class: "text-sm text-muted" }, Dt = { class: "list-disc list-inside text-sm leading-relaxed space-y-1" }, Ot = {
1096
- key: 0,
1097
- class: "text-xs text-muted"
1098
- }, kt = /* @__PURE__ */ c({
1099
- __name: "VersionHistory",
1100
- setup(t) {
1101
- return (t, n) => {
1102
- let o = m("UBadge");
1103
- return u(), i("div", wt, [(u(!0), i(e, null, p(_(Ct), (t) => (u(), i("div", {
1104
- key: t.version,
1105
- class: "flex flex-col gap-2"
1106
- }, [
1107
- a("div", Tt, [s(o, {
1108
- label: `v${t.version}`,
1109
- color: "primary",
1110
- variant: "subtle"
1111
- }, null, 8, ["label"]), a("span", Et, g(t.date), 1)]),
1112
- a("ul", Dt, [(u(!0), i(e, null, p(t.highlights, (e) => (u(), i("li", { key: e }, g(e), 1))), 128))]),
1113
- t.note ? (u(), i("p", Ot, g(t.note), 1)) : r("", !0)
1114
- ]))), 128))]);
1115
- };
1116
- }
1117
- });
500
+ ];
501
+ //#endregion
502
+ //#region src/utils/formatDate.ts
503
+ function le(e) {
504
+ return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
505
+ }
1118
506
  //#endregion
1119
- export { $ as MemiBoardAttachments, Ke as MemiBoardCommentForm, et as MemiBoardCommentList, mt as MemiBoardDetail, vt as MemiBoardEditor, Fe as MemiBoardList, St as MemiBoardSignIn, kt as MemiBoardVersionHistory, ge as configureMemiBoard, G as useMemiBoardAuth, J as useMemiBoardComments, W as useMemiBoardConfig, X as useMemiBoardModeration, q as useMemiBoardPosts, Z as useMemiBoardSettings, we as useMemiBoardStorage, Ct as versionHistory };
507
+ export { $ as DEFAULT_CATEGORIES, W as configureMemiBoard, le as formatDate, K as useMemiBoardAuth, ee as useMemiBoardComments, G as useMemiBoardConfig, ae as useMemiBoardModeration, X as useMemiBoardPosts, se as useMemiBoardSettings, oe as useMemiBoardStorage, ce as versionHistory };
1120
508
 
1121
509
  //# sourceMappingURL=index.js.map