memi-board 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -28
- package/dist/composables/useMemiBoardSettings.d.ts +10 -4
- package/dist/index.d.ts +3 -9
- package/dist/index.js +216 -828
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +4 -0
- package/dist/module.mjs +75 -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 +287 -0
- package/dist/runtime/components/List.vue +134 -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,127 +1,131 @@
|
|
|
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, deleteField as f, doc as p, getDoc as m, getDocs as h, increment as g, limit as _, orderBy as v, query as y, serverTimestamp as b, setDoc as x, startAfter as S, updateDoc as C, writeBatch as w } from "firebase/firestore";
|
|
4
|
+
import { GoogleAuthProvider as T, OAuthProvider as E, createUserWithEmailAndPassword as D, signInWithEmailAndPassword as O, signInWithPopup as k, signOut as A, updateProfile as j } from "firebase/auth";
|
|
5
|
+
import { deleteObject as M, getDownloadURL as N, getStorage as P, listAll as F, ref as I, uploadBytesResumable as L } from "firebase/storage";
|
|
6
|
+
import { GoogleAIBackend as R, Schema as z, getAI as B, getGenerativeModel as V } from "firebase/ai";
|
|
8
7
|
//#region src/config.ts
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
var H = "__MEMI_BOARD_CONFIG__";
|
|
9
|
+
function U() {
|
|
10
|
+
let e = globalThis;
|
|
11
|
+
return e[H] || (e[H] = t({
|
|
12
|
+
collectionPrefix: "memiBoard",
|
|
13
|
+
auth: { providers: ["google", "apple"] },
|
|
14
|
+
moderation: {
|
|
15
|
+
enabled: !0,
|
|
16
|
+
model: "gemini-2.5-flash",
|
|
17
|
+
onError: "allow",
|
|
18
|
+
moderateImages: !1,
|
|
19
|
+
localBlocklist: []
|
|
20
|
+
}
|
|
21
|
+
})), e[H];
|
|
22
22
|
}
|
|
23
|
-
function W() {
|
|
24
|
-
|
|
23
|
+
function W(e) {
|
|
24
|
+
let t = U();
|
|
25
|
+
e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
|
|
26
|
+
}
|
|
27
|
+
function G() {
|
|
28
|
+
return U();
|
|
25
29
|
}
|
|
26
30
|
//#endregion
|
|
27
31
|
//#region src/composables/useMemiBoardAuth.ts
|
|
28
|
-
function
|
|
29
|
-
let
|
|
30
|
-
function
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
async function
|
|
34
|
-
let t =
|
|
35
|
-
return n.exists() ? (await
|
|
32
|
+
function K() {
|
|
33
|
+
let t = G(), i = l(), o = c(), s = a(), u = n("user"), d = n(!0);
|
|
34
|
+
function f(e) {
|
|
35
|
+
return p(i, `${t.collectionPrefix}Users`, e);
|
|
36
|
+
}
|
|
37
|
+
async function h(e) {
|
|
38
|
+
let t = f(e.uid), n = await m(t);
|
|
39
|
+
return n.exists() ? (await C(t, {
|
|
36
40
|
displayName: e.displayName,
|
|
37
|
-
updatedAt:
|
|
38
|
-
}), n.data().role ?? "user") : (await
|
|
41
|
+
updatedAt: b()
|
|
42
|
+
}), n.data().role ?? "user") : (await x(t, {
|
|
39
43
|
role: "user",
|
|
40
44
|
displayName: e.displayName,
|
|
41
|
-
updatedAt:
|
|
45
|
+
updatedAt: b()
|
|
42
46
|
}), "user");
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
48
|
+
r(s, async (e) => {
|
|
49
|
+
if (d.value = !0, !e) {
|
|
50
|
+
u.value = "user", d.value = !1;
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
49
53
|
try {
|
|
50
|
-
|
|
54
|
+
u.value = await h(e);
|
|
51
55
|
} catch {
|
|
52
|
-
|
|
56
|
+
u.value = "user";
|
|
53
57
|
} finally {
|
|
54
|
-
|
|
58
|
+
d.value = !1;
|
|
55
59
|
}
|
|
56
60
|
}, { immediate: !0 });
|
|
57
|
-
let
|
|
58
|
-
function
|
|
59
|
-
if (!
|
|
60
|
-
return
|
|
61
|
+
let g = e(() => !!s.value), _ = e(() => u.value === "admin");
|
|
62
|
+
function v() {
|
|
63
|
+
if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
|
|
64
|
+
return o;
|
|
61
65
|
}
|
|
62
|
-
async function
|
|
63
|
-
return
|
|
66
|
+
async function y() {
|
|
67
|
+
return k(v(), new T());
|
|
64
68
|
}
|
|
65
|
-
async function
|
|
66
|
-
let e = new
|
|
67
|
-
return e.addScope("email"), e.addScope("name"),
|
|
69
|
+
async function S() {
|
|
70
|
+
let e = new E("apple.com");
|
|
71
|
+
return e.addScope("email"), e.addScope("name"), k(v(), e);
|
|
68
72
|
}
|
|
69
|
-
async function
|
|
70
|
-
return
|
|
73
|
+
async function w(e, t) {
|
|
74
|
+
return O(v(), e, t);
|
|
71
75
|
}
|
|
72
|
-
async function
|
|
73
|
-
let r = await
|
|
74
|
-
return n && await
|
|
76
|
+
async function M(e, t, n) {
|
|
77
|
+
let r = await D(v(), e, t);
|
|
78
|
+
return n && await j(r.user, { displayName: n }), r;
|
|
75
79
|
}
|
|
76
|
-
async function
|
|
77
|
-
await
|
|
80
|
+
async function N() {
|
|
81
|
+
await A(v());
|
|
78
82
|
}
|
|
79
|
-
function
|
|
80
|
-
return
|
|
83
|
+
function P(e) {
|
|
84
|
+
return g.value && (s.value?.uid === e.authorUid || _.value);
|
|
81
85
|
}
|
|
82
|
-
function
|
|
83
|
-
return
|
|
86
|
+
function F(e) {
|
|
87
|
+
return g.value && (s.value?.uid === e.authorUid || _.value);
|
|
84
88
|
}
|
|
85
89
|
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:
|
|
90
|
+
user: s,
|
|
91
|
+
isSignedIn: g,
|
|
92
|
+
isAdmin: _,
|
|
93
|
+
rolePending: d,
|
|
94
|
+
signInWithGoogle: y,
|
|
95
|
+
signInWithApple: S,
|
|
96
|
+
signInWithEmail: w,
|
|
97
|
+
signUpWithEmail: M,
|
|
98
|
+
signOut: N,
|
|
99
|
+
canEdit: P,
|
|
100
|
+
canDelete: P,
|
|
101
|
+
canDeleteComment: F
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
104
|
//#endregion
|
|
101
105
|
//#region src/utils/slugify.ts
|
|
102
|
-
function
|
|
106
|
+
function q(e) {
|
|
103
107
|
return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
104
108
|
}
|
|
105
|
-
function
|
|
109
|
+
function J(e, t = 60) {
|
|
106
110
|
return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
|
|
107
111
|
}
|
|
108
112
|
//#endregion
|
|
109
113
|
//#region src/composables/useMemiBoardPosts.ts
|
|
110
|
-
async function
|
|
111
|
-
let t = await
|
|
114
|
+
async function Y(e) {
|
|
115
|
+
let t = await F(e).catch(() => ({
|
|
112
116
|
items: [],
|
|
113
117
|
prefixes: []
|
|
114
118
|
}));
|
|
115
|
-
await Promise.all([...t.items.map((e) =>
|
|
119
|
+
await Promise.all([...t.items.map((e) => M(e)), ...t.prefixes.map((e) => Y(e))]);
|
|
116
120
|
}
|
|
117
|
-
function
|
|
118
|
-
let e =
|
|
119
|
-
async function
|
|
121
|
+
function X() {
|
|
122
|
+
let e = G(), t = l(), n = s(), r = () => e.collectionPrefix, i = () => u(t, `${r()}Posts`), a = (e) => p(t, `${r()}Posts`, e), o = (e) => p(t, `${r()}Posts`, e, "body", "main"), c = (e) => u(t, `${r()}Posts`, e, "comments");
|
|
123
|
+
async function g(e = {}) {
|
|
120
124
|
let t = e.pageSize ?? 20, n = e.cursor ? [
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
] : [
|
|
125
|
+
v("createdAt", "desc"),
|
|
126
|
+
S(e.cursor),
|
|
127
|
+
_(t + 1)
|
|
128
|
+
] : [v("createdAt", "desc"), _(t + 1)], r = await h(y(i(), ...n)), a = r.docs.slice(0, t);
|
|
125
129
|
return {
|
|
126
130
|
posts: a.map((e) => ({
|
|
127
131
|
id: e.id,
|
|
@@ -131,18 +135,18 @@ function q() {
|
|
|
131
135
|
hasMore: r.docs.length > t
|
|
132
136
|
};
|
|
133
137
|
}
|
|
134
|
-
async function
|
|
135
|
-
let [t, n] = await Promise.all([
|
|
138
|
+
async function T(e) {
|
|
139
|
+
let [t, n] = await Promise.all([m(a(e)), m(o(e))]);
|
|
136
140
|
return t.exists() ? {
|
|
137
141
|
id: t.id,
|
|
138
142
|
...t.data(),
|
|
139
143
|
content: n.exists() ? n.data().content ?? "" : ""
|
|
140
144
|
} : null;
|
|
141
145
|
}
|
|
142
|
-
async function
|
|
143
|
-
let t =
|
|
144
|
-
for (; (await
|
|
145
|
-
return await Promise.all([
|
|
146
|
+
async function E(e) {
|
|
147
|
+
let t = q(e.title) || "post", n = t, r = 1;
|
|
148
|
+
for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
|
|
149
|
+
return await Promise.all([x(a(n), {
|
|
146
150
|
title: e.title,
|
|
147
151
|
tags: e.tags ?? [],
|
|
148
152
|
...e.category ? { category: e.category } : {},
|
|
@@ -153,65 +157,65 @@ function q() {
|
|
|
153
157
|
authorPhoto: e.authorPhoto,
|
|
154
158
|
moderationStatus: "approved",
|
|
155
159
|
moderationModel: e.moderationModel ?? null,
|
|
156
|
-
createdAt:
|
|
157
|
-
updatedAt:
|
|
158
|
-
}),
|
|
160
|
+
createdAt: b(),
|
|
161
|
+
updatedAt: b()
|
|
162
|
+
}), x(o(n), { content: e.content })]), n;
|
|
159
163
|
}
|
|
160
|
-
async function
|
|
161
|
-
await Promise.all([
|
|
164
|
+
async function D(e, t) {
|
|
165
|
+
await Promise.all([C(a(e), {
|
|
162
166
|
title: t.title,
|
|
163
167
|
tags: t.tags ?? [],
|
|
164
|
-
|
|
168
|
+
category: t.category ? t.category : f(),
|
|
165
169
|
attachments: t.attachments ?? [],
|
|
166
|
-
updatedAt:
|
|
167
|
-
}),
|
|
170
|
+
updatedAt: b()
|
|
171
|
+
}), x(o(e), { content: t.content }, { merge: !0 })]);
|
|
168
172
|
}
|
|
169
|
-
async function
|
|
170
|
-
let i = await
|
|
173
|
+
async function O(e) {
|
|
174
|
+
let i = await h(c(e));
|
|
171
175
|
for (let e = 0; e < i.docs.length; e += 450) {
|
|
172
|
-
let n =
|
|
176
|
+
let n = w(t);
|
|
173
177
|
i.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
174
178
|
}
|
|
175
|
-
let
|
|
176
|
-
await
|
|
179
|
+
let s = P(n);
|
|
180
|
+
await d(o(e)), await Y(I(s, `${r()}/posts/${e}`)), await d(a(e));
|
|
177
181
|
}
|
|
178
182
|
return {
|
|
179
|
-
getPosts:
|
|
180
|
-
getPost:
|
|
181
|
-
createPost:
|
|
182
|
-
updatePost:
|
|
183
|
-
deletePost:
|
|
183
|
+
getPosts: g,
|
|
184
|
+
getPost: T,
|
|
185
|
+
createPost: E,
|
|
186
|
+
updatePost: D,
|
|
187
|
+
deletePost: O
|
|
184
188
|
};
|
|
185
189
|
}
|
|
186
190
|
//#endregion
|
|
187
191
|
//#region src/composables/useMemiBoardComments.ts
|
|
188
|
-
function
|
|
189
|
-
let n =
|
|
190
|
-
async function
|
|
191
|
-
let t =
|
|
192
|
-
t.set(
|
|
193
|
-
postId:
|
|
192
|
+
function ee(t) {
|
|
193
|
+
let n = G(), r = l(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => y(u(r, `${a()}Posts`, o.value, "comments"), v("createdAt", "asc"))), c = i(s);
|
|
194
|
+
async function d(e) {
|
|
195
|
+
let t = w(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
|
|
196
|
+
t.set(i, {
|
|
197
|
+
postId: o.value,
|
|
194
198
|
body: e.body,
|
|
195
199
|
authorUid: e.authorUid,
|
|
196
200
|
authorName: e.authorName,
|
|
197
201
|
authorPhoto: e.authorPhoto,
|
|
198
202
|
moderationStatus: "approved",
|
|
199
|
-
createdAt:
|
|
200
|
-
}), t.update(
|
|
203
|
+
createdAt: b()
|
|
204
|
+
}), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
|
|
201
205
|
}
|
|
202
|
-
async function
|
|
203
|
-
let t =
|
|
204
|
-
t.delete(
|
|
206
|
+
async function f(e) {
|
|
207
|
+
let t = w(r), n = a();
|
|
208
|
+
t.delete(p(r, `${n}Posts`, o.value, "comments", e)), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
|
|
205
209
|
}
|
|
206
210
|
return {
|
|
207
|
-
comments:
|
|
208
|
-
addComment:
|
|
209
|
-
deleteComment:
|
|
211
|
+
comments: c,
|
|
212
|
+
addComment: d,
|
|
213
|
+
deleteComment: f
|
|
210
214
|
};
|
|
211
215
|
}
|
|
212
216
|
//#endregion
|
|
213
217
|
//#region src/utils/moderation-prompt.ts
|
|
214
|
-
var
|
|
218
|
+
var te = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category는 상황에 맞게 고르고, reason은 한국어 1문장 (예: \"욕설이 포함되어 있어 게시할 수 없습니다.\")\n- flagged=false면 category는 \"none\", reason은 \"\"", ne = [
|
|
215
219
|
"씨발",
|
|
216
220
|
"시발",
|
|
217
221
|
"씨팔",
|
|
@@ -235,10 +239,10 @@ var ye = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
|
|
|
235
239
|
"bitch",
|
|
236
240
|
"asshole"
|
|
237
241
|
];
|
|
238
|
-
function
|
|
242
|
+
function re(e) {
|
|
239
243
|
return new RegExp(e.join("|"), "i");
|
|
240
244
|
}
|
|
241
|
-
var
|
|
245
|
+
var ie = /* @__PURE__ */ new Set([
|
|
242
246
|
"none",
|
|
243
247
|
"abuse",
|
|
244
248
|
"spam",
|
|
@@ -246,7 +250,7 @@ var Se = /* @__PURE__ */ new Set([
|
|
|
246
250
|
"violence",
|
|
247
251
|
"other"
|
|
248
252
|
]);
|
|
249
|
-
function
|
|
253
|
+
function Z(e) {
|
|
250
254
|
let t = e.trim();
|
|
251
255
|
if (!t) return null;
|
|
252
256
|
let n;
|
|
@@ -264,7 +268,7 @@ function Ce(e) {
|
|
|
264
268
|
if (typeof n != "object" || !n) return null;
|
|
265
269
|
let r = n;
|
|
266
270
|
if (typeof r.flagged != "boolean") return null;
|
|
267
|
-
let i = typeof r.category == "string" &&
|
|
271
|
+
let i = typeof r.category == "string" && ie.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
268
272
|
return {
|
|
269
273
|
flagged: r.flagged,
|
|
270
274
|
category: i,
|
|
@@ -273,13 +277,13 @@ function Ce(e) {
|
|
|
273
277
|
}
|
|
274
278
|
//#endregion
|
|
275
279
|
//#region src/composables/useMemiBoardModeration.ts
|
|
276
|
-
var
|
|
280
|
+
var Q = {
|
|
277
281
|
flagged: !1,
|
|
278
282
|
category: "none",
|
|
279
283
|
reason: ""
|
|
280
284
|
};
|
|
281
|
-
function
|
|
282
|
-
let e =
|
|
285
|
+
function ae() {
|
|
286
|
+
let e = G().moderation ?? {}, t = s(), n = re([...ne, ...e.localBlocklist ?? []]);
|
|
283
287
|
function r(e) {
|
|
284
288
|
return n.test(e) ? {
|
|
285
289
|
flagged: !0,
|
|
@@ -288,21 +292,21 @@ function X() {
|
|
|
288
292
|
} : null;
|
|
289
293
|
}
|
|
290
294
|
function i() {
|
|
291
|
-
let n =
|
|
292
|
-
backend: new
|
|
295
|
+
let n = B(t, {
|
|
296
|
+
backend: new R(),
|
|
293
297
|
useLimitedUseAppCheckTokens: !0
|
|
294
298
|
});
|
|
295
|
-
return
|
|
299
|
+
return V(n, {
|
|
296
300
|
model: e.model ?? "gemini-2.5-flash",
|
|
297
|
-
systemInstruction:
|
|
301
|
+
systemInstruction: te,
|
|
298
302
|
generationConfig: {
|
|
299
303
|
temperature: 0,
|
|
300
304
|
maxOutputTokens: 256,
|
|
301
305
|
responseMimeType: "application/json",
|
|
302
|
-
responseSchema:
|
|
303
|
-
flagged:
|
|
304
|
-
category:
|
|
305
|
-
reason:
|
|
306
|
+
responseSchema: z.object({ properties: {
|
|
307
|
+
flagged: z.boolean(),
|
|
308
|
+
category: z.string(),
|
|
309
|
+
reason: z.string()
|
|
306
310
|
} })
|
|
307
311
|
}
|
|
308
312
|
});
|
|
@@ -314,18 +318,18 @@ function X() {
|
|
|
314
318
|
reason: "검열 서비스에 일시적으로 연결할 수 없어 제출이 보류되었습니다. 잠시 후 다시 시도해 주세요.",
|
|
315
319
|
error: !0
|
|
316
320
|
} : {
|
|
317
|
-
...
|
|
321
|
+
...Q,
|
|
318
322
|
error: !0
|
|
319
323
|
};
|
|
320
324
|
}
|
|
321
325
|
async function o(t) {
|
|
322
326
|
let n = t.trim();
|
|
323
|
-
if (!n) return
|
|
327
|
+
if (!n) return Q;
|
|
324
328
|
let o = r(n);
|
|
325
329
|
if (o) return o;
|
|
326
|
-
if (e.enabled === !1) return
|
|
330
|
+
if (e.enabled === !1) return Q;
|
|
327
331
|
try {
|
|
328
|
-
let e =
|
|
332
|
+
let e = Z((await i().generateContent(`심사할 텍스트:\n"""${n.slice(0, 4e3)}"""`)).response.text());
|
|
329
333
|
return e ? {
|
|
330
334
|
flagged: e.flagged,
|
|
331
335
|
category: e.category,
|
|
@@ -335,13 +339,13 @@ function X() {
|
|
|
335
339
|
return console.warn("[memi-board] AI 검열 호출 실패 → onError 정책 적용", e), a();
|
|
336
340
|
}
|
|
337
341
|
}
|
|
338
|
-
async function
|
|
339
|
-
if (e.enabled === !1 || !e.moderateImages) return
|
|
342
|
+
async function c(t) {
|
|
343
|
+
if (e.enabled === !1 || !e.moderateImages) return Q;
|
|
340
344
|
try {
|
|
341
345
|
let e = await new Promise((e, n) => {
|
|
342
346
|
let r = new FileReader();
|
|
343
347
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
344
|
-
}), n =
|
|
348
|
+
}), n = Z((await i().generateContent([{ inlineData: {
|
|
345
349
|
mimeType: t.type || "image/jpeg",
|
|
346
350
|
data: e
|
|
347
351
|
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
@@ -356,15 +360,15 @@ function X() {
|
|
|
356
360
|
}
|
|
357
361
|
return {
|
|
358
362
|
checkText: o,
|
|
359
|
-
checkImage:
|
|
363
|
+
checkImage: c
|
|
360
364
|
};
|
|
361
365
|
}
|
|
362
366
|
//#endregion
|
|
363
367
|
//#region src/composables/useMemiBoardStorage.ts
|
|
364
|
-
function
|
|
365
|
-
let e =
|
|
368
|
+
function oe() {
|
|
369
|
+
let e = G(), t = s(), n = () => e.collectionPrefix;
|
|
366
370
|
function r(e, r, i) {
|
|
367
|
-
let a =
|
|
371
|
+
let a = P(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${J(e.name)}`, s = I(a, o), c = L(s, e, {
|
|
368
372
|
contentType: e.type || "application/octet-stream",
|
|
369
373
|
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
|
|
370
374
|
});
|
|
@@ -373,7 +377,7 @@ function we() {
|
|
|
373
377
|
c.on("state_changed", (e) => {
|
|
374
378
|
i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
|
|
375
379
|
}, n, async () => {
|
|
376
|
-
let n = await
|
|
380
|
+
let n = await N(s);
|
|
377
381
|
t({
|
|
378
382
|
name: e.name,
|
|
379
383
|
url: n,
|
|
@@ -387,8 +391,8 @@ function we() {
|
|
|
387
391
|
};
|
|
388
392
|
}
|
|
389
393
|
async function i(e) {
|
|
390
|
-
let n =
|
|
391
|
-
await
|
|
394
|
+
let n = P(t);
|
|
395
|
+
await M(I(n, e.path)).catch(() => {});
|
|
392
396
|
}
|
|
393
397
|
return {
|
|
394
398
|
uploadAttachment: r,
|
|
@@ -397,7 +401,7 @@ function we() {
|
|
|
397
401
|
}
|
|
398
402
|
//#endregion
|
|
399
403
|
//#region src/composables/useMemiBoardSettings.ts
|
|
400
|
-
var
|
|
404
|
+
var $ = [
|
|
401
405
|
{
|
|
402
406
|
id: "free",
|
|
403
407
|
label: "자유"
|
|
@@ -411,664 +415,65 @@ var Te = [
|
|
|
411
415
|
label: "질문"
|
|
412
416
|
}
|
|
413
417
|
];
|
|
414
|
-
function
|
|
415
|
-
let
|
|
416
|
-
let e =
|
|
417
|
-
return e && e.length > 0 ? e :
|
|
418
|
+
function se() {
|
|
419
|
+
let t = G(), n = l(), { isSignedIn: i, isAdmin: a } = K(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
|
|
420
|
+
let e = c.value?.categories;
|
|
421
|
+
return e && e.length > 0 ? e : $;
|
|
418
422
|
});
|
|
419
|
-
function
|
|
420
|
-
if (e) return
|
|
423
|
+
function f(e) {
|
|
424
|
+
if (e) return d.value.find((t) => t.id === e)?.label ?? e;
|
|
425
|
+
}
|
|
426
|
+
async function h() {
|
|
427
|
+
if (!i.value) return;
|
|
428
|
+
let e = s.value, t = await m(e).catch(() => null);
|
|
429
|
+
!t || t.exists() || await x(e, {
|
|
430
|
+
categories: $,
|
|
431
|
+
updatedAt: b()
|
|
432
|
+
});
|
|
421
433
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
!
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
434
|
+
async function g(e) {
|
|
435
|
+
let t = e.trim();
|
|
436
|
+
if (!t) throw Error("카테고리 이름을 입력해 주세요.");
|
|
437
|
+
if (!i.value) throw Error("로그인이 필요합니다.");
|
|
438
|
+
await h();
|
|
439
|
+
let n = d.value.find((e) => e.label === t || e.id === q(t));
|
|
440
|
+
if (n) return n.id;
|
|
441
|
+
let r = q(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
|
|
442
|
+
if (a.has(r)) {
|
|
443
|
+
let e = 2;
|
|
444
|
+
for (; a.has(`${r}-${e}`);) e++;
|
|
445
|
+
r = `${r}-${e}`;
|
|
446
|
+
}
|
|
447
|
+
let o = [...d.value, {
|
|
448
|
+
id: r,
|
|
449
|
+
label: t
|
|
450
|
+
}], c = s.value;
|
|
451
|
+
return (await m(c)).exists() ? await C(c, {
|
|
452
|
+
categories: o,
|
|
453
|
+
updatedAt: b()
|
|
454
|
+
}) : await x(c, {
|
|
455
|
+
categories: o,
|
|
456
|
+
updatedAt: b()
|
|
457
|
+
}), r;
|
|
458
|
+
}
|
|
459
|
+
return r([
|
|
460
|
+
a,
|
|
461
|
+
i,
|
|
462
|
+
u
|
|
463
|
+
], async ([e, t, n]) => {
|
|
464
|
+
n || c.value || !e && !t || await h().catch(() => {});
|
|
429
465
|
}, { immediate: !0 }), {
|
|
430
|
-
categories:
|
|
431
|
-
categoryLabel:
|
|
432
|
-
settingsPending:
|
|
466
|
+
categories: d,
|
|
467
|
+
categoryLabel: f,
|
|
468
|
+
settingsPending: u,
|
|
469
|
+
ensureSettings: h,
|
|
470
|
+
addCategory: g,
|
|
471
|
+
DEFAULT_CATEGORIES: $
|
|
433
472
|
};
|
|
434
473
|
}
|
|
435
474
|
//#endregion
|
|
436
|
-
//#region src/
|
|
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 = [
|
|
475
|
+
//#region src/data/versionHistory.ts
|
|
476
|
+
var ce = [
|
|
1072
477
|
{
|
|
1073
478
|
version: "0.2.2",
|
|
1074
479
|
date: "2026-07-31",
|
|
@@ -1092,30 +497,13 @@ var Ee = { class: "flex flex-col gap-2" }, De = {
|
|
|
1092
497
|
date: "2026-07-27",
|
|
1093
498
|
highlights: ["프로젝트 최초 생성 (아직 게시판 기능은 없는 빈 Nuxt 4 프로젝트)"]
|
|
1094
499
|
}
|
|
1095
|
-
]
|
|
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
|
-
});
|
|
500
|
+
];
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/utils/formatDate.ts
|
|
503
|
+
function le(e) {
|
|
504
|
+
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
505
|
+
}
|
|
1118
506
|
//#endregion
|
|
1119
|
-
export { $ as
|
|
507
|
+
export { $ as DEFAULT_CATEGORIES, W as configureMemiBoard, le as formatDate, K as useMemiBoardAuth, ee as useMemiBoardComments, G as useMemiBoardConfig, ae as useMemiBoardModeration, X as useMemiBoardPosts, se as useMemiBoardSettings, oe as useMemiBoardStorage, ce as versionHistory };
|
|
1120
508
|
|
|
1121
509
|
//# sourceMappingURL=index.js.map
|