memi-board 0.3.1 → 0.4.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,12 +1,11 @@
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, doc as f, getDoc as p, getDocs as m, increment as h, limit as g, orderBy as _, query as v, serverTimestamp as y, setDoc as b, startAfter as x, updateDoc as S, writeBatch as C } from "firebase/firestore";
4
+ import { GoogleAuthProvider as w, OAuthProvider as T, createUserWithEmailAndPassword as E, signInWithEmailAndPassword as D, signInWithPopup as O, signOut as k, updateProfile as ee } from "firebase/auth";
5
+ import { deleteObject as A, getDownloadURL as j, getStorage as M, listAll as N, ref as P, uploadBytesResumable as F } from "firebase/storage";
6
+ import { GoogleAIBackend as I, Schema as L, getAI as R, getGenerativeModel as z } from "firebase/ai";
8
7
  //#region src/config.ts
9
- var U = d({
8
+ var B = t({
10
9
  collectionPrefix: "memiBoard",
11
10
  auth: { providers: ["google", "apple"] },
12
11
  moderation: {
@@ -17,111 +16,111 @@ var U = d({
17
16
  localBlocklist: []
18
17
  }
19
18
  });
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);
19
+ function V(e) {
20
+ e.collectionPrefix && (B.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(B.auth, e.auth), e.moderation && Object.assign(B.moderation, e.moderation);
22
21
  }
23
- function W() {
24
- return U;
22
+ function H() {
23
+ return B;
25
24
  }
26
25
  //#endregion
27
26
  //#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, {
27
+ function U() {
28
+ let t = H(), i = l(), o = c(), s = a(), u = n("user"), d = n(!0);
29
+ function m(e) {
30
+ return f(i, `${t.collectionPrefix}Users`, e);
31
+ }
32
+ async function h(e) {
33
+ let t = m(e.uid), n = await p(t);
34
+ return n.exists() ? (await S(t, {
36
35
  displayName: e.displayName,
37
- updatedAt: I()
38
- }), n.data().role ?? "user") : (await L(t, {
36
+ updatedAt: y()
37
+ }), n.data().role ?? "user") : (await b(t, {
39
38
  role: "user",
40
39
  displayName: e.displayName,
41
- updatedAt: I()
40
+ updatedAt: y()
42
41
  }), "user");
43
42
  }
44
- v(i, async (e) => {
45
- if (o.value = !0, !e) {
46
- a.value = "user", o.value = !1;
43
+ r(s, async (e) => {
44
+ if (d.value = !0, !e) {
45
+ u.value = "user", d.value = !1;
47
46
  return;
48
47
  }
49
48
  try {
50
- a.value = await c(e);
49
+ u.value = await h(e);
51
50
  } catch {
52
- a.value = "user";
51
+ u.value = "user";
53
52
  } finally {
54
- o.value = !1;
53
+ d.value = !1;
55
54
  }
56
55
  }, { 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;
56
+ let g = e(() => !!s.value), _ = e(() => u.value === "admin");
57
+ function v() {
58
+ if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
59
+ return o;
61
60
  }
62
- async function p() {
63
- return ae(d(), new te());
61
+ async function x() {
62
+ return O(v(), new w());
64
63
  }
65
- async function m() {
66
- let e = new ne("apple.com");
67
- return e.addScope("email"), e.addScope("name"), ae(d(), e);
64
+ async function C() {
65
+ let e = new T("apple.com");
66
+ return e.addScope("email"), e.addScope("name"), O(v(), e);
68
67
  }
69
- async function h(e, t) {
70
- return ie(d(), e, t);
68
+ async function A(e, t) {
69
+ return D(v(), e, t);
71
70
  }
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;
71
+ async function j(e, t, n) {
72
+ let r = await E(v(), e, t);
73
+ return n && await ee(r.user, { displayName: n }), r;
75
74
  }
76
- async function _() {
77
- await oe(d());
75
+ async function M() {
76
+ await k(v());
78
77
  }
79
- function y(e) {
80
- return l.value && (i.value?.uid === e.authorUid || u.value);
78
+ function N(e) {
79
+ return g.value && (s.value?.uid === e.authorUid || _.value);
81
80
  }
82
- function b(e) {
83
- return l.value && (i.value?.uid === e.authorUid || u.value);
81
+ function P(e) {
82
+ return g.value && (s.value?.uid === e.authorUid || _.value);
84
83
  }
85
84
  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
85
+ user: s,
86
+ isSignedIn: g,
87
+ isAdmin: _,
88
+ rolePending: d,
89
+ signInWithGoogle: x,
90
+ signInWithApple: C,
91
+ signInWithEmail: A,
92
+ signUpWithEmail: j,
93
+ signOut: M,
94
+ canEdit: N,
95
+ canDelete: N,
96
+ canDeleteComment: P
98
97
  };
99
98
  }
100
99
  //#endregion
101
100
  //#region src/utils/slugify.ts
102
- function _e(e) {
101
+ function W(e) {
103
102
  return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
104
103
  }
105
- function ve(e, t = 60) {
104
+ function G(e, t = 60) {
106
105
  return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
107
106
  }
108
107
  //#endregion
109
108
  //#region src/composables/useMemiBoardPosts.ts
110
109
  async function K(e) {
111
- let t = await ue(e).catch(() => ({
110
+ let t = await N(e).catch(() => ({
112
111
  items: [],
113
112
  prefixes: []
114
113
  }));
115
- await Promise.all([...t.items.map((e) => ce(e)), ...t.prefixes.map((e) => K(e))]);
114
+ await Promise.all([...t.items.map((e) => A(e)), ...t.prefixes.map((e) => K(e))]);
116
115
  }
117
116
  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 = {}) {
117
+ let e = H(), t = l(), n = s(), r = e.collectionPrefix, i = () => u(t, `${r}Posts`), a = (e) => f(t, `${r}Posts`, e), o = (e) => f(t, `${r}Posts`, e, "body", "main"), c = (e) => u(t, `${r}Posts`, e, "comments");
118
+ async function h(e = {}) {
120
119
  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);
120
+ _("createdAt", "desc"),
121
+ x(e.cursor),
122
+ g(t + 1)
123
+ ] : [_("createdAt", "desc"), g(t + 1)], r = await m(v(i(), ...n)), a = r.docs.slice(0, t);
125
124
  return {
126
125
  posts: a.map((e) => ({
127
126
  id: e.id,
@@ -131,18 +130,18 @@ function q() {
131
130
  hasMore: r.docs.length > t
132
131
  };
133
132
  }
134
- async function l(e) {
135
- let [t, n] = await Promise.all([A(a(e)), A(o(e))]);
133
+ async function w(e) {
134
+ let [t, n] = await Promise.all([p(a(e)), p(o(e))]);
136
135
  return t.exists() ? {
137
136
  id: t.id,
138
137
  ...t.data(),
139
138
  content: n.exists() ? n.data().content ?? "" : ""
140
139
  } : null;
141
140
  }
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), {
141
+ async function T(e) {
142
+ let t = W(e.title) || "post", n = t, r = 1;
143
+ for (; (await p(a(n))).exists();) r++, n = `${t}-${r}`;
144
+ return await Promise.all([b(a(n), {
146
145
  title: e.title,
147
146
  tags: e.tags ?? [],
148
147
  ...e.category ? { category: e.category } : {},
@@ -153,65 +152,65 @@ function q() {
153
152
  authorPhoto: e.authorPhoto,
154
153
  moderationStatus: "approved",
155
154
  moderationModel: e.moderationModel ?? null,
156
- createdAt: I(),
157
- updatedAt: I()
158
- }), L(o(n), { content: e.content })]), n;
155
+ createdAt: y(),
156
+ updatedAt: y()
157
+ }), b(o(n), { content: e.content })]), n;
159
158
  }
160
- async function d(e, t) {
161
- await Promise.all([R(a(e), {
159
+ async function E(e, t) {
160
+ await Promise.all([S(a(e), {
162
161
  title: t.title,
163
162
  tags: t.tags ?? [],
164
163
  ...t.category ? { category: t.category } : {},
165
164
  attachments: t.attachments ?? [],
166
- updatedAt: I()
167
- }), L(o(e), { content: t.content })]);
165
+ updatedAt: y()
166
+ }), b(o(e), { content: t.content })]);
168
167
  }
169
- async function f(e) {
170
- let i = await j(s(e));
168
+ async function D(e) {
169
+ let i = await m(c(e));
171
170
  for (let e = 0; e < i.docs.length; e += 450) {
172
- let n = z(t);
171
+ let n = C(t);
173
172
  i.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
174
173
  }
175
- let c = B(n);
176
- await O(o(e)), await K(V(c, `${r}/posts/${e}`)), await O(a(e));
174
+ let s = M(n);
175
+ await d(o(e)), await K(P(s, `${r}/posts/${e}`)), await d(a(e));
177
176
  }
178
177
  return {
179
- getPosts: c,
180
- getPost: l,
181
- createPost: u,
182
- updatePost: d,
183
- deletePost: f
178
+ getPosts: h,
179
+ getPost: w,
180
+ createPost: T,
181
+ updatePost: E,
182
+ deletePost: D
184
183
  };
185
184
  }
186
185
  //#endregion
187
186
  //#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"));
187
+ function J(t) {
188
+ let n = H(), r = l(), a = n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => v(u(r, `${a}Posts`, o.value, "comments"), _("createdAt", "asc"))), c = i(s);
189
+ async function d(e) {
190
+ let t = C(r), n = f(u(r, `${a}Posts`, o.value, "comments"));
192
191
  t.set(n, {
193
- postId: a.value,
192
+ postId: o.value,
194
193
  body: e.body,
195
194
  authorUid: e.authorUid,
196
195
  authorName: e.authorName,
197
196
  authorPhoto: e.authorPhoto,
198
197
  moderationStatus: "approved",
199
- createdAt: I()
200
- }), t.update(k(r, `${i}Posts`, a.value), { commentCount: M(1) }), await t.commit();
198
+ createdAt: y()
199
+ }), t.update(f(r, `${a}Posts`, o.value), { commentCount: h(1) }), await t.commit();
201
200
  }
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();
201
+ async function p(e) {
202
+ let t = C(r);
203
+ t.delete(f(r, `${a}Posts`, o.value, "comments", e)), t.update(f(r, `${a}Posts`, o.value), { commentCount: h(-1) }), await t.commit();
205
204
  }
206
205
  return {
207
- comments: s,
208
- addComment: c,
209
- deleteComment: l
206
+ comments: c,
207
+ addComment: d,
208
+ deleteComment: p
210
209
  };
211
210
  }
212
211
  //#endregion
213
212
  //#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 = [
213
+ var Y = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", X = [
215
214
  "씨발",
216
215
  "시발",
217
216
  "씨팔",
@@ -235,10 +234,10 @@ var ye = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
235
234
  "bitch",
236
235
  "asshole"
237
236
  ];
238
- function xe(e) {
237
+ function te(e) {
239
238
  return new RegExp(e.join("|"), "i");
240
239
  }
241
- var Se = /* @__PURE__ */ new Set([
240
+ var ne = /* @__PURE__ */ new Set([
242
241
  "none",
243
242
  "abuse",
244
243
  "spam",
@@ -246,7 +245,7 @@ var Se = /* @__PURE__ */ new Set([
246
245
  "violence",
247
246
  "other"
248
247
  ]);
249
- function Ce(e) {
248
+ function Z(e) {
250
249
  let t = e.trim();
251
250
  if (!t) return null;
252
251
  let n;
@@ -264,7 +263,7 @@ function Ce(e) {
264
263
  if (typeof n != "object" || !n) return null;
265
264
  let r = n;
266
265
  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 : "";
266
+ let i = typeof r.category == "string" && ne.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
268
267
  return {
269
268
  flagged: r.flagged,
270
269
  category: i,
@@ -273,13 +272,13 @@ function Ce(e) {
273
272
  }
274
273
  //#endregion
275
274
  //#region src/composables/useMemiBoardModeration.ts
276
- var Y = {
275
+ var Q = {
277
276
  flagged: !1,
278
277
  category: "none",
279
278
  reason: ""
280
279
  };
281
- function X() {
282
- let e = W().moderation ?? {}, t = w(), n = xe([...be, ...e.localBlocklist ?? []]);
280
+ function re() {
281
+ let e = H().moderation ?? {}, t = s(), n = te([...X, ...e.localBlocklist ?? []]);
283
282
  function r(e) {
284
283
  return n.test(e) ? {
285
284
  flagged: !0,
@@ -288,21 +287,21 @@ function X() {
288
287
  } : null;
289
288
  }
290
289
  function i() {
291
- let n = pe(t, {
292
- backend: new fe(),
290
+ let n = R(t, {
291
+ backend: new I(),
293
292
  useLimitedUseAppCheckTokens: !0
294
293
  });
295
- return me(n, {
294
+ return z(n, {
296
295
  model: e.model ?? "gemini-2.5-flash",
297
- systemInstruction: ye,
296
+ systemInstruction: Y,
298
297
  generationConfig: {
299
298
  temperature: 0,
300
299
  maxOutputTokens: 256,
301
300
  responseMimeType: "application/json",
302
- responseSchema: H.object({ properties: {
303
- flagged: H.boolean(),
304
- category: H.string(),
305
- reason: H.string()
301
+ responseSchema: L.object({ properties: {
302
+ flagged: L.boolean(),
303
+ category: L.string(),
304
+ reason: L.string()
306
305
  } })
307
306
  }
308
307
  });
@@ -314,18 +313,18 @@ function X() {
314
313
  reason: "검열 서비스에 일시적으로 연결할 수 없어 제출이 보류되었습니다. 잠시 후 다시 시도해 주세요.",
315
314
  error: !0
316
315
  } : {
317
- ...Y,
316
+ ...Q,
318
317
  error: !0
319
318
  };
320
319
  }
321
320
  async function o(t) {
322
321
  let n = t.trim();
323
- if (!n) return Y;
322
+ if (!n) return Q;
324
323
  let o = r(n);
325
324
  if (o) return o;
326
- if (e.enabled === !1) return Y;
325
+ if (e.enabled === !1) return Q;
327
326
  try {
328
- let e = Ce((await i().generateContent(`심사할 텍스트:\n"""${n.slice(0, 4e3)}"""`)).response.text());
327
+ let e = Z((await i().generateContent(`심사할 텍스트:\n"""${n.slice(0, 4e3)}"""`)).response.text());
329
328
  return e ? {
330
329
  flagged: e.flagged,
331
330
  category: e.category,
@@ -335,13 +334,13 @@ function X() {
335
334
  return console.warn("[memi-board] AI 검열 호출 실패 → onError 정책 적용", e), a();
336
335
  }
337
336
  }
338
- async function s(t) {
339
- if (e.enabled === !1 || !e.moderateImages) return Y;
337
+ async function c(t) {
338
+ if (e.enabled === !1 || !e.moderateImages) return Q;
340
339
  try {
341
340
  let e = await new Promise((e, n) => {
342
341
  let r = new FileReader();
343
342
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
344
- }), n = Ce((await i().generateContent([{ inlineData: {
343
+ }), n = Z((await i().generateContent([{ inlineData: {
345
344
  mimeType: t.type || "image/jpeg",
346
345
  data: e
347
346
  } }, "이 이미지를 심사해 주세요."])).response.text());
@@ -356,15 +355,15 @@ function X() {
356
355
  }
357
356
  return {
358
357
  checkText: o,
359
- checkImage: s
358
+ checkImage: c
360
359
  };
361
360
  }
362
361
  //#endregion
363
362
  //#region src/composables/useMemiBoardStorage.ts
364
- function we() {
365
- let e = W(), t = w(), n = e.collectionPrefix;
363
+ function ie() {
364
+ let e = H(), t = s(), n = e.collectionPrefix;
366
365
  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, {
366
+ let a = M(t), o = `${n}/posts/${r}/attachments/${Date.now()}-${G(e.name)}`, s = P(a, o), c = F(s, e, {
368
367
  contentType: e.type || "application/octet-stream",
369
368
  contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
370
369
  });
@@ -373,7 +372,7 @@ function we() {
373
372
  c.on("state_changed", (e) => {
374
373
  i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
375
374
  }, n, async () => {
376
- let n = await le(s);
375
+ let n = await j(s);
377
376
  t({
378
377
  name: e.name,
379
378
  url: n,
@@ -387,8 +386,8 @@ function we() {
387
386
  };
388
387
  }
389
388
  async function i(e) {
390
- let n = B(t);
391
- await ce(V(n, e.path)).catch(() => {});
389
+ let n = M(t);
390
+ await A(P(n, e.path)).catch(() => {});
392
391
  }
393
392
  return {
394
393
  uploadAttachment: r,
@@ -397,7 +396,7 @@ function we() {
397
396
  }
398
397
  //#endregion
399
398
  //#region src/composables/useMemiBoardSettings.ts
400
- var Te = [
399
+ var $ = [
401
400
  {
402
401
  id: "free",
403
402
  label: "자유"
@@ -411,664 +410,30 @@ var Te = [
411
410
  label: "질문"
412
411
  }
413
412
  ];
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;
413
+ function ae() {
414
+ let t = H(), n = l(), { isAdmin: i } = U(), a = t.collectionPrefix, s = f(n, `${a}Settings`, "config"), { data: c, pending: u } = o(s), d = e(() => {
415
+ let e = c.value?.categories;
416
+ return e && e.length > 0 ? e : $;
418
417
  });
419
- function l(e) {
420
- if (e) return c.value.find((t) => t.id === e)?.label ?? e;
421
- }
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()
418
+ function m(e) {
419
+ if (e) return d.value.find((t) => t.id === e)?.label ?? e;
420
+ }
421
+ return r([i, u], async ([e, t]) => {
422
+ if (!e || t || c.value) return;
423
+ let n = await p(s).catch(() => null);
424
+ !n || n.exists() || await b(s, {
425
+ categories: $,
426
+ updatedAt: y()
428
427
  }).catch(() => {});
429
428
  }, { immediate: !0 }), {
430
- categories: c,
431
- categoryLabel: l,
432
- settingsPending: s
429
+ categories: d,
430
+ categoryLabel: m,
431
+ settingsPending: u
433
432
  };
434
433
  }
435
434
  //#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 = [
435
+ //#region src/data/versionHistory.ts
436
+ var oe = [
1072
437
  {
1073
438
  version: "0.2.2",
1074
439
  date: "2026-07-31",
@@ -1092,30 +457,13 @@ var Ee = { class: "flex flex-col gap-2" }, De = {
1092
457
  date: "2026-07-27",
1093
458
  highlights: ["프로젝트 최초 생성 (아직 게시판 기능은 없는 빈 Nuxt 4 프로젝트)"]
1094
459
  }
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
- });
460
+ ];
461
+ //#endregion
462
+ //#region src/utils/formatDate.ts
463
+ function se(e) {
464
+ return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
465
+ }
1118
466
  //#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 };
467
+ export { V as configureMemiBoard, se as formatDate, U as useMemiBoardAuth, J as useMemiBoardComments, H as useMemiBoardConfig, re as useMemiBoardModeration, q as useMemiBoardPosts, ae as useMemiBoardSettings, ie as useMemiBoardStorage, oe as versionHistory };
1120
468
 
1121
469
  //# sourceMappingURL=index.js.map