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/README.md +31 -28
- package/dist/index.d.ts +2 -8
- package/dist/index.js +162 -814
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +4 -0
- package/dist/module.mjs +62 -0
- package/dist/module.mjs.map +1 -0
- package/dist/runtime/components/Attachments.vue +173 -0
- package/dist/runtime/components/CommentForm.vue +83 -0
- package/dist/runtime/components/CommentList.vue +64 -0
- package/dist/runtime/components/Detail.vue +156 -0
- package/dist/runtime/components/Editor.vue +168 -0
- package/dist/runtime/components/List.vue +121 -0
- package/dist/runtime/components/SignIn.vue +146 -0
- package/dist/runtime/components/VersionHistory.vue +36 -0
- package/package.json +35 -9
- package/dist/components/Attachments.vue.d.ts +0 -17
- package/dist/components/CommentForm.vue.d.ts +0 -5
- package/dist/components/CommentList.vue.d.ts +0 -5
- package/dist/components/Detail.vue.d.ts +0 -11
- package/dist/components/Editor.vue.d.ts +0 -12
- package/dist/components/List.vue.d.ts +0 -14
- package/dist/components/SignIn.vue.d.ts +0 -2
- package/dist/components/VersionHistory.vue.d.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useCollection as
|
|
3
|
-
import { collection as
|
|
4
|
-
import { GoogleAuthProvider as
|
|
5
|
-
import { deleteObject as
|
|
6
|
-
import { GoogleAIBackend as
|
|
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
|
|
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
|
|
21
|
-
e.collectionPrefix && (
|
|
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
|
|
24
|
-
return
|
|
22
|
+
function H() {
|
|
23
|
+
return B;
|
|
25
24
|
}
|
|
26
25
|
//#endregion
|
|
27
26
|
//#region src/composables/useMemiBoardAuth.ts
|
|
28
|
-
function
|
|
29
|
-
let
|
|
30
|
-
function
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
async function
|
|
34
|
-
let t =
|
|
35
|
-
return n.exists() ? (await
|
|
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:
|
|
38
|
-
}), n.data().role ?? "user") : (await
|
|
36
|
+
updatedAt: y()
|
|
37
|
+
}), n.data().role ?? "user") : (await b(t, {
|
|
39
38
|
role: "user",
|
|
40
39
|
displayName: e.displayName,
|
|
41
|
-
updatedAt:
|
|
40
|
+
updatedAt: y()
|
|
42
41
|
}), "user");
|
|
43
42
|
}
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
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
|
-
|
|
49
|
+
u.value = await h(e);
|
|
51
50
|
} catch {
|
|
52
|
-
|
|
51
|
+
u.value = "user";
|
|
53
52
|
} finally {
|
|
54
|
-
|
|
53
|
+
d.value = !1;
|
|
55
54
|
}
|
|
56
55
|
}, { immediate: !0 });
|
|
57
|
-
let
|
|
58
|
-
function
|
|
59
|
-
if (!
|
|
60
|
-
return
|
|
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
|
|
63
|
-
return
|
|
61
|
+
async function x() {
|
|
62
|
+
return O(v(), new w());
|
|
64
63
|
}
|
|
65
|
-
async function
|
|
66
|
-
let e = new
|
|
67
|
-
return e.addScope("email"), e.addScope("name"),
|
|
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
|
|
70
|
-
return
|
|
68
|
+
async function A(e, t) {
|
|
69
|
+
return D(v(), e, t);
|
|
71
70
|
}
|
|
72
|
-
async function
|
|
73
|
-
let r = await
|
|
74
|
-
return n && await
|
|
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
|
|
75
|
+
async function M() {
|
|
76
|
+
await k(v());
|
|
78
77
|
}
|
|
79
|
-
function
|
|
80
|
-
return
|
|
78
|
+
function N(e) {
|
|
79
|
+
return g.value && (s.value?.uid === e.authorUid || _.value);
|
|
81
80
|
}
|
|
82
|
-
function
|
|
83
|
-
return
|
|
81
|
+
function P(e) {
|
|
82
|
+
return g.value && (s.value?.uid === e.authorUid || _.value);
|
|
84
83
|
}
|
|
85
84
|
return {
|
|
86
|
-
user:
|
|
87
|
-
isSignedIn:
|
|
88
|
-
isAdmin:
|
|
89
|
-
rolePending:
|
|
90
|
-
signInWithGoogle:
|
|
91
|
-
signInWithApple:
|
|
92
|
-
signInWithEmail:
|
|
93
|
-
signUpWithEmail:
|
|
94
|
-
signOut:
|
|
95
|
-
canEdit:
|
|
96
|
-
canDelete:
|
|
97
|
-
canDeleteComment:
|
|
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
|
|
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
|
|
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
|
|
110
|
+
let t = await N(e).catch(() => ({
|
|
112
111
|
items: [],
|
|
113
112
|
prefixes: []
|
|
114
113
|
}));
|
|
115
|
-
await Promise.all([...t.items.map((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 =
|
|
119
|
-
async function
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
] : [
|
|
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
|
|
135
|
-
let [t, n] = await Promise.all([
|
|
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
|
|
143
|
-
let t =
|
|
144
|
-
for (; (await
|
|
145
|
-
return await Promise.all([
|
|
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:
|
|
157
|
-
updatedAt:
|
|
158
|
-
}),
|
|
155
|
+
createdAt: y(),
|
|
156
|
+
updatedAt: y()
|
|
157
|
+
}), b(o(n), { content: e.content })]), n;
|
|
159
158
|
}
|
|
160
|
-
async function
|
|
161
|
-
await Promise.all([
|
|
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:
|
|
167
|
-
}),
|
|
165
|
+
updatedAt: y()
|
|
166
|
+
}), b(o(e), { content: t.content })]);
|
|
168
167
|
}
|
|
169
|
-
async function
|
|
170
|
-
let i = await
|
|
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 =
|
|
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
|
|
176
|
-
await
|
|
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:
|
|
180
|
-
getPost:
|
|
181
|
-
createPost:
|
|
182
|
-
updatePost:
|
|
183
|
-
deletePost:
|
|
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(
|
|
189
|
-
let n =
|
|
190
|
-
async function
|
|
191
|
-
let t =
|
|
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:
|
|
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:
|
|
200
|
-
}), t.update(
|
|
198
|
+
createdAt: y()
|
|
199
|
+
}), t.update(f(r, `${a}Posts`, o.value), { commentCount: h(1) }), await t.commit();
|
|
201
200
|
}
|
|
202
|
-
async function
|
|
203
|
-
let t =
|
|
204
|
-
t.delete(
|
|
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:
|
|
208
|
-
addComment:
|
|
209
|
-
deleteComment:
|
|
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
|
|
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
|
|
237
|
+
function te(e) {
|
|
239
238
|
return new RegExp(e.join("|"), "i");
|
|
240
239
|
}
|
|
241
|
-
var
|
|
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
|
|
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" &&
|
|
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
|
|
275
|
+
var Q = {
|
|
277
276
|
flagged: !1,
|
|
278
277
|
category: "none",
|
|
279
278
|
reason: ""
|
|
280
279
|
};
|
|
281
|
-
function
|
|
282
|
-
let e =
|
|
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 =
|
|
292
|
-
backend: new
|
|
290
|
+
let n = R(t, {
|
|
291
|
+
backend: new I(),
|
|
293
292
|
useLimitedUseAppCheckTokens: !0
|
|
294
293
|
});
|
|
295
|
-
return
|
|
294
|
+
return z(n, {
|
|
296
295
|
model: e.model ?? "gemini-2.5-flash",
|
|
297
|
-
systemInstruction:
|
|
296
|
+
systemInstruction: Y,
|
|
298
297
|
generationConfig: {
|
|
299
298
|
temperature: 0,
|
|
300
299
|
maxOutputTokens: 256,
|
|
301
300
|
responseMimeType: "application/json",
|
|
302
|
-
responseSchema:
|
|
303
|
-
flagged:
|
|
304
|
-
category:
|
|
305
|
-
reason:
|
|
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
|
-
...
|
|
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
|
|
322
|
+
if (!n) return Q;
|
|
324
323
|
let o = r(n);
|
|
325
324
|
if (o) return o;
|
|
326
|
-
if (e.enabled === !1) return
|
|
325
|
+
if (e.enabled === !1) return Q;
|
|
327
326
|
try {
|
|
328
|
-
let e =
|
|
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
|
|
339
|
-
if (e.enabled === !1 || !e.moderateImages) return
|
|
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 =
|
|
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:
|
|
358
|
+
checkImage: c
|
|
360
359
|
};
|
|
361
360
|
}
|
|
362
361
|
//#endregion
|
|
363
362
|
//#region src/composables/useMemiBoardStorage.ts
|
|
364
|
-
function
|
|
365
|
-
let e =
|
|
363
|
+
function ie() {
|
|
364
|
+
let e = H(), t = s(), n = e.collectionPrefix;
|
|
366
365
|
function r(e, r, i) {
|
|
367
|
-
let a =
|
|
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
|
|
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 =
|
|
391
|
-
await
|
|
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
|
|
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
|
|
415
|
-
let
|
|
416
|
-
let e =
|
|
417
|
-
return e && e.length > 0 ? e :
|
|
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
|
|
420
|
-
if (e) return
|
|
421
|
-
}
|
|
422
|
-
return
|
|
423
|
-
if (!e || t ||
|
|
424
|
-
let n = await
|
|
425
|
-
!n || n.exists() || await
|
|
426
|
-
categories:
|
|
427
|
-
updatedAt:
|
|
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:
|
|
431
|
-
categoryLabel:
|
|
432
|
-
settingsPending:
|
|
429
|
+
categories: d,
|
|
430
|
+
categoryLabel: m,
|
|
431
|
+
settingsPending: u
|
|
433
432
|
};
|
|
434
433
|
}
|
|
435
434
|
//#endregion
|
|
436
|
-
//#region src/
|
|
437
|
-
|
|
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
|
-
]
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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 {
|
|
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
|