memi-board 0.7.0 → 0.9.1
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 +2 -0
- package/dist/composables/useMemiBoardComments.d.ts +81 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +462 -282
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/CommentForm.vue +21 -6
- package/dist/runtime/components/CommentItem.vue +73 -0
- package/dist/runtime/components/CommentList.vue +83 -39
- package/dist/runtime/components/CommentSkeleton.vue +12 -0
- package/dist/runtime/components/CommentThread.vue +121 -0
- package/dist/runtime/components/Detail.vue +3 -6
- package/dist/runtime/components/Users.vue +2 -3
- package/dist/types.d.ts +11 -0
- package/docs/firestore.rules.example +65 -4
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { computed as e,
|
|
2
|
-
import { useCollection as
|
|
3
|
-
import { collection as
|
|
4
|
-
import { GoogleAuthProvider as
|
|
5
|
-
import { deleteObject as
|
|
6
|
-
import { GoogleAIBackend as
|
|
1
|
+
import { computed as e, onScopeDispose as t, reactive as n, ref as r, watch as i } from "vue";
|
|
2
|
+
import { useCollection as a, useCurrentUser 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, documentId as m, endBefore as h, getDoc as g, getDocs as _, increment as v, limit as y, limitToLast as b, onSnapshot as x, orderBy as S, query as C, serverTimestamp as w, setDoc as T, startAfter as E, updateDoc as D, where as O, writeBatch as k } from "firebase/firestore";
|
|
4
|
+
import { GoogleAuthProvider as A, OAuthProvider as j, createUserWithEmailAndPassword as M, signInWithEmailAndPassword as N, signInWithPopup as P, signOut as ee, updateProfile as te } from "firebase/auth";
|
|
5
|
+
import { deleteObject as F, getDownloadURL as I, getStorage as L, listAll as ne, ref as R, uploadBytes as z, uploadBytesResumable as re } from "firebase/storage";
|
|
6
|
+
import { GoogleAIBackend as ie, Schema as B, getAI as ae, getGenerativeModel as oe } from "firebase/ai";
|
|
7
7
|
//#region src/config.ts
|
|
8
|
-
var
|
|
9
|
-
function
|
|
8
|
+
var V = "__MEMI_BOARD_CONFIG__";
|
|
9
|
+
function H() {
|
|
10
10
|
let e = globalThis;
|
|
11
|
-
return e[
|
|
11
|
+
return e[V] || (e[V] = n({
|
|
12
12
|
collectionPrefix: "memiBoard",
|
|
13
13
|
auth: { providers: ["google", "apple"] },
|
|
14
14
|
moderation: {
|
|
@@ -20,19 +20,19 @@ function I() {
|
|
|
20
20
|
blockBanThreshold: 3,
|
|
21
21
|
blockBanDecayMs: 864e5
|
|
22
22
|
}
|
|
23
|
-
})), e[
|
|
23
|
+
})), e[V];
|
|
24
24
|
}
|
|
25
|
-
function
|
|
26
|
-
let t =
|
|
25
|
+
function se(e) {
|
|
26
|
+
let t = H();
|
|
27
27
|
e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
|
|
28
28
|
}
|
|
29
|
-
function
|
|
30
|
-
return
|
|
29
|
+
function U() {
|
|
30
|
+
return H();
|
|
31
31
|
}
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/utils/moderation-strike.ts
|
|
34
|
-
var
|
|
35
|
-
function
|
|
34
|
+
var ce = 3, W = 864e5;
|
|
35
|
+
function G(e, t, n = Date.now(), r = W) {
|
|
36
36
|
let i = Math.max(0, Math.floor(Number(e) || 0));
|
|
37
37
|
if (i <= 0) return 0;
|
|
38
38
|
let a = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -40,7 +40,7 @@ function B(e, t, n = Date.now(), r = z) {
|
|
|
40
40
|
let o = Math.floor((n - a) / r);
|
|
41
41
|
return Math.max(0, i - o);
|
|
42
42
|
}
|
|
43
|
-
function
|
|
43
|
+
function K(e, t, n = Date.now(), r = 3, i = W) {
|
|
44
44
|
let a = Math.max(0, Math.floor(Number(e) || 0));
|
|
45
45
|
if (a < r) return null;
|
|
46
46
|
let o = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -48,17 +48,17 @@ function V(e, t, n = Date.now(), r = 3, i = z) {
|
|
|
48
48
|
let s = o + (a - (r - 1)) * i;
|
|
49
49
|
return s > n ? s : null;
|
|
50
50
|
}
|
|
51
|
-
function
|
|
52
|
-
return
|
|
51
|
+
function q(e, t, n = Date.now(), r = 3, i = W) {
|
|
52
|
+
return K(e, t, n, r, i) != null;
|
|
53
53
|
}
|
|
54
|
-
function
|
|
54
|
+
function J(e, t = Date.now()) {
|
|
55
55
|
let n = Math.max(0, e - t), r = Math.ceil(n / 36e5);
|
|
56
56
|
if (r <= 0) return "곧 해제";
|
|
57
57
|
if (r < 24) return `약 ${r}시간 후 해제`;
|
|
58
58
|
let i = Math.floor(r / 24), a = r % 24;
|
|
59
59
|
return a === 0 ? `약 ${i}일 후 해제` : `약 ${i}일 ${a}시간 후 해제`;
|
|
60
60
|
}
|
|
61
|
-
function
|
|
61
|
+
function le(e) {
|
|
62
62
|
if (e == null) return null;
|
|
63
63
|
if (typeof e == "number" && Number.isFinite(e)) return e;
|
|
64
64
|
if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
|
|
@@ -70,186 +70,186 @@ function de(e) {
|
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region src/composables/useMemiBoardAuth.ts
|
|
73
|
-
var
|
|
74
|
-
function
|
|
73
|
+
var Y = "__MEMI_BOARD_AUTH_STATE__";
|
|
74
|
+
function ue() {
|
|
75
75
|
let e = globalThis;
|
|
76
|
-
return e[
|
|
77
|
-
role:
|
|
78
|
-
rolePending:
|
|
79
|
-
rawBlockCount:
|
|
80
|
-
blockAtMs:
|
|
76
|
+
return e[Y] || (e[Y] = {
|
|
77
|
+
role: r("user"),
|
|
78
|
+
rolePending: r(!0),
|
|
79
|
+
rawBlockCount: r(0),
|
|
80
|
+
blockAtMs: r(null),
|
|
81
81
|
watchStarted: !1
|
|
82
|
-
}), e[
|
|
82
|
+
}), e[Y];
|
|
83
83
|
}
|
|
84
|
-
function
|
|
85
|
-
let t =
|
|
86
|
-
function
|
|
84
|
+
function X() {
|
|
85
|
+
let t = U(), n = ue(), r = l(), a = c(), s = o(), { role: u, rolePending: d, rawBlockCount: f, blockAtMs: m } = n;
|
|
86
|
+
function h() {
|
|
87
87
|
let e = t.moderation?.blockBanThreshold;
|
|
88
88
|
return e === 0 ? 0 : typeof e == "number" && e > 0 ? Math.floor(e) : 3;
|
|
89
89
|
}
|
|
90
|
-
function
|
|
90
|
+
function _() {
|
|
91
91
|
let e = t.moderation?.blockBanDecayMs;
|
|
92
|
-
return typeof e == "number" && e > 0 ? e :
|
|
92
|
+
return typeof e == "number" && e > 0 ? e : W;
|
|
93
93
|
}
|
|
94
94
|
function y(e) {
|
|
95
|
-
return
|
|
95
|
+
return p(r, `${t.collectionPrefix}Users`, e);
|
|
96
96
|
}
|
|
97
97
|
function b(e) {
|
|
98
98
|
if (!e) {
|
|
99
|
-
|
|
99
|
+
f.value = 0, m.value = null;
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), m.value = le(e.moderationBlockAt);
|
|
103
103
|
}
|
|
104
|
-
async function
|
|
105
|
-
let t = y(e.uid), n = await
|
|
106
|
-
if (!n.exists()) return await
|
|
104
|
+
async function x(e) {
|
|
105
|
+
let t = y(e.uid), n = await g(t);
|
|
106
|
+
if (!n.exists()) return await T(t, {
|
|
107
107
|
role: "user",
|
|
108
108
|
displayName: e.displayName,
|
|
109
109
|
email: e.email,
|
|
110
110
|
photoURL: e.photoURL,
|
|
111
|
-
updatedAt:
|
|
111
|
+
updatedAt: w()
|
|
112
112
|
}), b({ moderationBlockCount: 0 }), "user";
|
|
113
113
|
let r = n.data();
|
|
114
|
-
return b(r), await
|
|
114
|
+
return b(r), await D(t, {
|
|
115
115
|
displayName: e.displayName,
|
|
116
116
|
email: e.email,
|
|
117
117
|
photoURL: e.photoURL,
|
|
118
|
-
updatedAt:
|
|
118
|
+
updatedAt: w()
|
|
119
119
|
}), [
|
|
120
120
|
"admin",
|
|
121
121
|
"staff",
|
|
122
122
|
"user"
|
|
123
123
|
].includes(String(r.role)) ? r.role : "user";
|
|
124
124
|
}
|
|
125
|
-
async function
|
|
126
|
-
let e =
|
|
125
|
+
async function S() {
|
|
126
|
+
let e = s.value?.uid;
|
|
127
127
|
if (!e) {
|
|
128
128
|
b(void 0);
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
|
-
let t = await
|
|
131
|
+
let t = await g(y(e));
|
|
132
132
|
b(t.exists() ? t.data() : void 0);
|
|
133
133
|
}
|
|
134
|
-
n.watchStarted || (n.watchStarted = !0,
|
|
134
|
+
n.watchStarted || (n.watchStarted = !0, i(s, async (e) => {
|
|
135
135
|
if (d.value = !0, !e) {
|
|
136
136
|
u.value = "user", b(void 0), d.value = !1;
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
try {
|
|
140
|
-
u.value = await
|
|
140
|
+
u.value = await x(e);
|
|
141
141
|
} catch {
|
|
142
142
|
u.value = "user";
|
|
143
143
|
} finally {
|
|
144
144
|
d.value = !1;
|
|
145
145
|
}
|
|
146
146
|
}, { immediate: !0 }));
|
|
147
|
-
let
|
|
148
|
-
let e =
|
|
149
|
-
return e <= 0 ? !1 :
|
|
150
|
-
}),
|
|
151
|
-
let e =
|
|
147
|
+
let C = e(() => !!s.value), E = e(() => u.value === "admin"), O = e(() => u.value === "staff"), k = e(() => E.value || O.value), F = e(() => G(f.value, m.value, Date.now(), _())), I = e(() => {
|
|
148
|
+
let e = h();
|
|
149
|
+
return e <= 0 ? !1 : q(f.value, m.value, Date.now(), e, _());
|
|
150
|
+
}), L = e(() => {
|
|
151
|
+
let e = h();
|
|
152
152
|
if (e <= 0) return null;
|
|
153
|
-
let t =
|
|
154
|
-
return t == null ? null :
|
|
155
|
-
}),
|
|
156
|
-
if (!
|
|
157
|
-
let e =
|
|
153
|
+
let t = K(f.value, m.value, Date.now(), e, _());
|
|
154
|
+
return t == null ? null : J(t);
|
|
155
|
+
}), ne = e(() => {
|
|
156
|
+
if (!I.value) return null;
|
|
157
|
+
let e = L.value;
|
|
158
158
|
return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
|
|
159
159
|
});
|
|
160
|
-
async function
|
|
161
|
-
let e =
|
|
160
|
+
async function R() {
|
|
161
|
+
let e = s.value?.uid, t = h();
|
|
162
162
|
if (!e || t <= 0) return {
|
|
163
163
|
effectiveCount: 0,
|
|
164
164
|
restricted: !1,
|
|
165
165
|
messageSuffix: ""
|
|
166
166
|
};
|
|
167
|
-
let n = y(e), r = await
|
|
168
|
-
r.exists() ? await
|
|
169
|
-
moderationBlockCount:
|
|
170
|
-
moderationBlockAt:
|
|
171
|
-
updatedAt:
|
|
172
|
-
}) : await
|
|
167
|
+
let n = y(e), r = await g(n);
|
|
168
|
+
r.exists() ? await D(n, {
|
|
169
|
+
moderationBlockCount: v(1),
|
|
170
|
+
moderationBlockAt: w(),
|
|
171
|
+
updatedAt: w()
|
|
172
|
+
}) : await T(n, {
|
|
173
173
|
role: "user",
|
|
174
|
-
displayName:
|
|
174
|
+
displayName: s.value?.displayName ?? null,
|
|
175
175
|
moderationBlockCount: 1,
|
|
176
|
-
moderationBlockAt:
|
|
177
|
-
updatedAt:
|
|
176
|
+
moderationBlockAt: w(),
|
|
177
|
+
updatedAt: w()
|
|
178
178
|
});
|
|
179
179
|
let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
|
|
180
|
-
|
|
181
|
-
let o =
|
|
182
|
-
if (
|
|
183
|
-
let e =
|
|
184
|
-
|
|
180
|
+
f.value = i, m.value = a;
|
|
181
|
+
let o = G(i, a, a, _()), c = q(i, a, a, t, _()), l = ` (경고 ${o}/${t})`;
|
|
182
|
+
if (c) {
|
|
183
|
+
let e = K(i, a, a, t, _()), n = e == null ? null : J(e, a);
|
|
184
|
+
l = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
|
|
185
185
|
}
|
|
186
186
|
return {
|
|
187
187
|
effectiveCount: o,
|
|
188
|
-
restricted:
|
|
189
|
-
messageSuffix:
|
|
188
|
+
restricted: c,
|
|
189
|
+
messageSuffix: l
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
-
function
|
|
193
|
-
if (!
|
|
194
|
-
return
|
|
192
|
+
function z() {
|
|
193
|
+
if (!a) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
|
|
194
|
+
return a;
|
|
195
195
|
}
|
|
196
|
-
async function
|
|
197
|
-
return
|
|
196
|
+
async function re() {
|
|
197
|
+
return P(z(), new A());
|
|
198
198
|
}
|
|
199
|
-
async function
|
|
200
|
-
let e = new
|
|
201
|
-
return e.addScope("email"), e.addScope("name"),
|
|
199
|
+
async function ie() {
|
|
200
|
+
let e = new j("apple.com");
|
|
201
|
+
return e.addScope("email"), e.addScope("name"), P(z(), e);
|
|
202
202
|
}
|
|
203
|
-
async function
|
|
204
|
-
return
|
|
203
|
+
async function B(e, t) {
|
|
204
|
+
return N(z(), e, t);
|
|
205
205
|
}
|
|
206
|
-
async function
|
|
207
|
-
let r = await
|
|
208
|
-
return n && await
|
|
206
|
+
async function ae(e, t, n) {
|
|
207
|
+
let r = await M(z(), e, t);
|
|
208
|
+
return n && await te(r.user, { displayName: n }), r;
|
|
209
209
|
}
|
|
210
|
-
async function
|
|
211
|
-
await
|
|
210
|
+
async function oe() {
|
|
211
|
+
await ee(z());
|
|
212
212
|
}
|
|
213
|
-
function
|
|
214
|
-
return
|
|
213
|
+
function V(e) {
|
|
214
|
+
return C.value && (s.value?.uid === e.authorUid || k.value);
|
|
215
215
|
}
|
|
216
|
-
function
|
|
217
|
-
return
|
|
216
|
+
function H(e) {
|
|
217
|
+
return C.value && (s.value?.uid === e.authorUid || k.value);
|
|
218
218
|
}
|
|
219
219
|
return {
|
|
220
|
-
user:
|
|
221
|
-
isSignedIn:
|
|
222
|
-
isAdmin:
|
|
223
|
-
isStaff:
|
|
224
|
-
canManageContent:
|
|
220
|
+
user: s,
|
|
221
|
+
isSignedIn: C,
|
|
222
|
+
isAdmin: E,
|
|
223
|
+
isStaff: O,
|
|
224
|
+
canManageContent: k,
|
|
225
225
|
rolePending: d,
|
|
226
|
-
moderationBlockCount:
|
|
227
|
-
isWriteRestricted:
|
|
228
|
-
restrictedUntilLabel:
|
|
229
|
-
restrictedMessage:
|
|
230
|
-
signInWithGoogle:
|
|
231
|
-
signInWithApple:
|
|
232
|
-
signInWithEmail:
|
|
233
|
-
signUpWithEmail:
|
|
234
|
-
signOut:
|
|
235
|
-
canEdit:
|
|
236
|
-
canDelete:
|
|
237
|
-
canDeleteComment:
|
|
238
|
-
recordContentModerationBlock:
|
|
239
|
-
refreshBoardUser:
|
|
226
|
+
moderationBlockCount: F,
|
|
227
|
+
isWriteRestricted: I,
|
|
228
|
+
restrictedUntilLabel: L,
|
|
229
|
+
restrictedMessage: ne,
|
|
230
|
+
signInWithGoogle: re,
|
|
231
|
+
signInWithApple: ie,
|
|
232
|
+
signInWithEmail: B,
|
|
233
|
+
signUpWithEmail: ae,
|
|
234
|
+
signOut: oe,
|
|
235
|
+
canEdit: V,
|
|
236
|
+
canDelete: V,
|
|
237
|
+
canDeleteComment: H,
|
|
238
|
+
recordContentModerationBlock: R,
|
|
239
|
+
refreshBoardUser: S
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
242
|
//#endregion
|
|
243
243
|
//#region src/utils/slugify.ts
|
|
244
|
-
function
|
|
244
|
+
function de(e) {
|
|
245
245
|
return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
246
246
|
}
|
|
247
|
-
function
|
|
247
|
+
function fe(e, t = 60) {
|
|
248
248
|
return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
|
|
249
249
|
}
|
|
250
250
|
//#endregion
|
|
251
251
|
//#region src/utils/extractEditorImageUrls.ts
|
|
252
|
-
function
|
|
252
|
+
function pe(e) {
|
|
253
253
|
if (!e) return [];
|
|
254
254
|
let t = /* @__PURE__ */ new Set();
|
|
255
255
|
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
@@ -258,7 +258,7 @@ function me(e) {
|
|
|
258
258
|
}
|
|
259
259
|
//#endregion
|
|
260
260
|
//#region src/utils/storagePath.ts
|
|
261
|
-
function
|
|
261
|
+
function me(e) {
|
|
262
262
|
try {
|
|
263
263
|
let t = e.match(/\/o\/([^?]+)/);
|
|
264
264
|
return t?.[1] ? decodeURIComponent(t[1]) : null;
|
|
@@ -266,25 +266,25 @@ function he(e) {
|
|
|
266
266
|
return null;
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
|
-
function
|
|
269
|
+
function he(e) {
|
|
270
270
|
return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
|
|
271
271
|
}
|
|
272
272
|
//#endregion
|
|
273
273
|
//#region src/utils/postPreview.ts
|
|
274
|
-
function
|
|
274
|
+
function ge(e) {
|
|
275
275
|
return e.replace(/&/g, "&").replace(/"/g, "\"").replace(/'/g, "'");
|
|
276
276
|
}
|
|
277
|
-
function
|
|
277
|
+
function _e(e) {
|
|
278
278
|
try {
|
|
279
|
-
let t = new URL(
|
|
279
|
+
let t = new URL(ge(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
|
|
280
280
|
if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
|
|
281
281
|
if (n.endsWith("youtube.com") || n === "youtube-nocookie.com") return t.pathname.startsWith("/embed/") || t.pathname.startsWith("/shorts/") ? t.pathname.split("/")[2] : t.searchParams.get("v") || void 0;
|
|
282
282
|
} catch {
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
|
-
function
|
|
287
|
-
let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ?
|
|
286
|
+
function ve(e, t = []) {
|
|
287
|
+
let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? ge(r[1]) : a, s = o ? _e(o) : void 0, c = n ? ge(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
|
|
288
288
|
return {
|
|
289
289
|
summary: e.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/\s+/g, " ").trim().slice(0, 160),
|
|
290
290
|
...c ? { previewImage: c } : {},
|
|
@@ -293,26 +293,26 @@ function _e(e, t = []) {
|
|
|
293
293
|
}
|
|
294
294
|
//#endregion
|
|
295
295
|
//#region src/composables/useMemiBoardPosts.ts
|
|
296
|
-
async function
|
|
297
|
-
let t = await
|
|
296
|
+
async function ye(e) {
|
|
297
|
+
let t = await ne(e).catch(() => ({
|
|
298
298
|
items: [],
|
|
299
299
|
prefixes: []
|
|
300
300
|
}));
|
|
301
|
-
await Promise.all([...t.items.map((e) =>
|
|
301
|
+
await Promise.all([...t.items.map((e) => F(e)), ...t.prefixes.map((e) => ye(e))]);
|
|
302
302
|
}
|
|
303
|
-
async function
|
|
303
|
+
async function be(e, t) {
|
|
304
304
|
let n = [...new Set([...t].filter(Boolean))];
|
|
305
|
-
await Promise.all(n.map((t) =>
|
|
305
|
+
await Promise.all(n.map((t) => F(R(e, t)).catch(() => {})));
|
|
306
306
|
}
|
|
307
|
-
function
|
|
308
|
-
let e =
|
|
309
|
-
function
|
|
310
|
-
return
|
|
307
|
+
function xe() {
|
|
308
|
+
let e = U(), 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");
|
|
309
|
+
function m() {
|
|
310
|
+
return p(i()).id;
|
|
311
311
|
}
|
|
312
|
-
async function
|
|
312
|
+
async function v(e = {}) {
|
|
313
313
|
let t = e.pageSize ?? 20, n = [];
|
|
314
|
-
e.category && n.push(
|
|
315
|
-
let r = await
|
|
314
|
+
e.category && n.push(O("category", "==", e.category)), n.push(S("createdAt", "desc")), e.cursor && n.push(E(e.cursor)), n.push(y(t + 1));
|
|
315
|
+
let r = await _(C(i(), ...n)), a = r.docs.slice(0, t);
|
|
316
316
|
return {
|
|
317
317
|
posts: a.map((e) => ({
|
|
318
318
|
id: e.id,
|
|
@@ -322,24 +322,24 @@ function ye() {
|
|
|
322
322
|
hasMore: r.docs.length > t
|
|
323
323
|
};
|
|
324
324
|
}
|
|
325
|
-
async function
|
|
326
|
-
let [t, n] = await Promise.all([
|
|
325
|
+
async function x(e) {
|
|
326
|
+
let [t, n] = await Promise.all([g(a(e)), g(o(e))]);
|
|
327
327
|
return t.exists() ? {
|
|
328
328
|
id: t.id,
|
|
329
329
|
...t.data(),
|
|
330
330
|
content: n.exists() ? n.data().content ?? "" : ""
|
|
331
331
|
} : null;
|
|
332
332
|
}
|
|
333
|
-
async function
|
|
334
|
-
let n = (await
|
|
335
|
-
return n ?
|
|
333
|
+
async function A(e, t) {
|
|
334
|
+
let n = (await _(C(i(), O("category", "==", e), O("slug", "==", t), y(1)))).docs[0];
|
|
335
|
+
return n ? x(n.id) : null;
|
|
336
336
|
}
|
|
337
|
-
async function
|
|
337
|
+
async function j(e) {
|
|
338
338
|
if (!e.category || !e.createdAt) return {
|
|
339
339
|
previous: null,
|
|
340
340
|
next: null
|
|
341
341
|
};
|
|
342
|
-
let t = [
|
|
342
|
+
let t = [O("category", "==", e.category), S("createdAt", "desc")], [n, r] = await Promise.all([_(C(i(), ...t, E(e.createdAt), y(1))), _(C(i(), ...t, h(e.createdAt), b(1)))]), a = (e) => {
|
|
343
343
|
let t = e.docs[0];
|
|
344
344
|
return t ? {
|
|
345
345
|
id: t.id,
|
|
@@ -351,11 +351,11 @@ function ye() {
|
|
|
351
351
|
next: a(r)
|
|
352
352
|
};
|
|
353
353
|
}
|
|
354
|
-
async function
|
|
355
|
-
let t =
|
|
356
|
-
for (; !(await
|
|
357
|
-
let
|
|
358
|
-
return await
|
|
354
|
+
async function M(e) {
|
|
355
|
+
let t = de(e.title) || "post", n = t, r = 1;
|
|
356
|
+
for (; !(await _(C(i(), O("category", "==", e.category || ""), O("slug", "==", n), y(1)))).empty;) r++, n = `${t}-${r}`;
|
|
357
|
+
let s = e.postId || m(), c = ve(e.content, e.attachments);
|
|
358
|
+
return await T(a(s), {
|
|
359
359
|
slug: n,
|
|
360
360
|
title: e.title,
|
|
361
361
|
...c,
|
|
@@ -368,92 +368,249 @@ function ye() {
|
|
|
368
368
|
authorPhoto: e.authorPhoto,
|
|
369
369
|
moderationStatus: "approved",
|
|
370
370
|
moderationModel: e.moderationModel ?? null,
|
|
371
|
-
createdAt:
|
|
372
|
-
updatedAt:
|
|
373
|
-
}), await
|
|
371
|
+
createdAt: w(),
|
|
372
|
+
updatedAt: w()
|
|
373
|
+
}), await T(o(s), { content: e.content }), n;
|
|
374
374
|
}
|
|
375
|
-
async function
|
|
376
|
-
let n =
|
|
377
|
-
await Promise.all([
|
|
375
|
+
async function N(e, t) {
|
|
376
|
+
let n = ve(t.content, t.attachments);
|
|
377
|
+
await Promise.all([D(a(e), {
|
|
378
378
|
title: t.title,
|
|
379
379
|
...n,
|
|
380
|
-
previewImage: n.previewImage ? n.previewImage :
|
|
381
|
-
videoUrl: n.videoUrl ? n.videoUrl :
|
|
380
|
+
previewImage: n.previewImage ? n.previewImage : f(),
|
|
381
|
+
videoUrl: n.videoUrl ? n.videoUrl : f(),
|
|
382
382
|
tags: t.tags ?? [],
|
|
383
|
-
category: t.category ? t.category :
|
|
383
|
+
category: t.category ? t.category : f(),
|
|
384
384
|
attachments: t.attachments ?? [],
|
|
385
|
-
updatedAt:
|
|
386
|
-
}),
|
|
385
|
+
updatedAt: w()
|
|
386
|
+
}), T(o(e), { content: t.content }, { merge: !0 })]);
|
|
387
387
|
}
|
|
388
|
-
async function
|
|
389
|
-
let [i,
|
|
388
|
+
async function P(e) {
|
|
389
|
+
let [i, s] = await Promise.all([g(a(e)), g(o(e))]);
|
|
390
390
|
if (!i.exists()) return;
|
|
391
|
-
let
|
|
392
|
-
for (let e = 0; e <
|
|
393
|
-
let n =
|
|
394
|
-
|
|
391
|
+
let l = i.data(), u = s.exists() ? String(s.data().content ?? "") : "", f = await _(c(e));
|
|
392
|
+
for (let e = 0; e < f.docs.length; e += 450) {
|
|
393
|
+
let n = k(t);
|
|
394
|
+
f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
395
395
|
}
|
|
396
|
-
let
|
|
397
|
-
for (let t of
|
|
398
|
-
|
|
399
|
-
let n =
|
|
400
|
-
n && n !== e &&
|
|
396
|
+
let p = L(n), m = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set();
|
|
397
|
+
for (let t of l.attachments ?? []) if (t?.path) {
|
|
398
|
+
m.add(t.path);
|
|
399
|
+
let n = he(t.path);
|
|
400
|
+
n && n !== e && h.add(n);
|
|
401
401
|
}
|
|
402
|
-
for (let t of
|
|
403
|
-
let n =
|
|
402
|
+
for (let t of pe(u)) {
|
|
403
|
+
let n = me(t);
|
|
404
404
|
if (!n) continue;
|
|
405
|
-
|
|
405
|
+
m.add(n);
|
|
406
406
|
let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
|
|
407
|
-
r !== n && r.includes("/images/thumbnails/") &&
|
|
407
|
+
r !== n && r.includes("/images/thumbnails/") && m.add(r);
|
|
408
408
|
let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
|
|
409
|
-
i && !n.includes("/thumbnails/") &&
|
|
410
|
-
let a =
|
|
411
|
-
a && a !== e &&
|
|
409
|
+
i && !n.includes("/thumbnails/") && m.add(`${i[1]}thumbnails/${i[2]}.jpg`);
|
|
410
|
+
let a = he(n);
|
|
411
|
+
a && a !== e && h.add(a);
|
|
412
412
|
}
|
|
413
|
-
await
|
|
414
|
-
for (let e of
|
|
415
|
-
await
|
|
413
|
+
await d(o(e)), await ye(R(p, `${r()}/posts/${e}`));
|
|
414
|
+
for (let e of h) await ye(R(p, `${r()}/posts/${e}`));
|
|
415
|
+
await be(p, m), await d(a(e));
|
|
416
416
|
}
|
|
417
417
|
return {
|
|
418
|
-
createPostId:
|
|
419
|
-
getPosts:
|
|
420
|
-
getPost:
|
|
421
|
-
getPostBySlug:
|
|
422
|
-
getAdjacentPosts:
|
|
423
|
-
createPost:
|
|
424
|
-
updatePost:
|
|
425
|
-
deletePost:
|
|
418
|
+
createPostId: m,
|
|
419
|
+
getPosts: v,
|
|
420
|
+
getPost: x,
|
|
421
|
+
getPostBySlug: A,
|
|
422
|
+
getAdjacentPosts: j,
|
|
423
|
+
createPost: M,
|
|
424
|
+
updatePost: N,
|
|
425
|
+
deletePost: P
|
|
426
426
|
};
|
|
427
427
|
}
|
|
428
428
|
//#endregion
|
|
429
429
|
//#region src/composables/useMemiBoardComments.ts
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
430
|
+
var Se = 10, Ce = 5;
|
|
431
|
+
function we(n, a = {}) {
|
|
432
|
+
let o = U(), s = l(), c = () => o.collectionPrefix, d = e(() => typeof n == "string" ? n : n.value), f = r([]), h = r([]), g = r(a.subscribe !== !1), b = r([]), T = r(!1), D = r(a.subscribe !== !1), A = r(a.subscribe !== !1), j = null, M, N;
|
|
433
|
+
function P(e) {
|
|
434
|
+
return e.createdAt?.toMillis?.() ?? 0;
|
|
435
|
+
}
|
|
436
|
+
let ee = e(() => {
|
|
437
|
+
let e = /* @__PURE__ */ new Map();
|
|
438
|
+
for (let t of [
|
|
439
|
+
...f.value,
|
|
440
|
+
...h.value,
|
|
441
|
+
...b.value
|
|
442
|
+
]) t.id && e.set(t.id, t);
|
|
443
|
+
return [...e.values()].sort((e, t) => P(e) - P(t) || String(e.id).localeCompare(String(t.id)));
|
|
444
|
+
});
|
|
445
|
+
i(d, async (e) => {
|
|
446
|
+
if (b.value = [], f.value = [], h.value = [], j = null, clearTimeout(M), M = void 0, N?.(), N = void 0, D.value = a.subscribe !== !1, a.subscribe !== !1) {
|
|
447
|
+
g.value = !0, A.value = !0;
|
|
448
|
+
try {
|
|
449
|
+
let t = _(C(u(s, `${c()}Posts`, e, "comments"), S("createdAt", "desc"), S(m(), "desc")));
|
|
450
|
+
if (await Promise.all([t.then((t) => {
|
|
451
|
+
if (d.value !== e) return;
|
|
452
|
+
let n = t.docs.map((e) => ({
|
|
453
|
+
id: e.id,
|
|
454
|
+
...e.data()
|
|
455
|
+
}));
|
|
456
|
+
h.value = n.filter((e) => e.parentId === void 0);
|
|
457
|
+
}), F()]), d.value !== e) return;
|
|
458
|
+
} finally {
|
|
459
|
+
d.value === e && (g.value = !1, A.value = !1);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}, { immediate: !0 });
|
|
463
|
+
function te() {
|
|
464
|
+
N?.(), N = x(C(u(s, `${c()}Posts`, d.value, "comments"), O("parentId", "==", null), S("createdAt", "desc"), S(m(), "desc"), y(1)), (e) => {
|
|
465
|
+
let t = new Map(f.value.map((e) => [e.id, e]));
|
|
466
|
+
for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
|
|
467
|
+
id: n.doc.id,
|
|
468
|
+
...n.doc.data()
|
|
469
|
+
});
|
|
470
|
+
f.value = [...t.values()];
|
|
471
|
+
}, (e) => {
|
|
472
|
+
console.error("[memi-board:comments] onSnapshot failed", e);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
async function F() {
|
|
476
|
+
if (!(T.value || !D.value)) {
|
|
477
|
+
T.value = !0;
|
|
478
|
+
try {
|
|
479
|
+
let e = [
|
|
480
|
+
O("parentId", "==", null),
|
|
481
|
+
S("createdAt", "asc"),
|
|
482
|
+
S(m(), "asc"),
|
|
483
|
+
...j ? [E(j)] : [],
|
|
484
|
+
y(11)
|
|
485
|
+
], t = await _(C(u(s, `${c()}Posts`, d.value, "comments"), ...e)), n = t.docs.slice(0, Se), r = n.map((e) => ({
|
|
486
|
+
id: e.id,
|
|
487
|
+
...e.data()
|
|
488
|
+
})), i = new Set(b.value.map((e) => e.id));
|
|
489
|
+
b.value.push(...r.filter((e) => !i.has(e.id))), j = n.at(-1) ?? j, t.size <= Se && (D.value = !1, M = setTimeout(() => {
|
|
490
|
+
M = void 0, te();
|
|
491
|
+
}, 1e3));
|
|
492
|
+
} finally {
|
|
493
|
+
T.value = !1;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
t(() => {
|
|
498
|
+
clearTimeout(M), N?.();
|
|
499
|
+
});
|
|
500
|
+
async function I(e) {
|
|
501
|
+
let t = k(s), n = c(), r = p(u(s, `${n}Posts`, d.value, "comments"));
|
|
502
|
+
t.set(r, {
|
|
503
|
+
postId: d.value,
|
|
504
|
+
body: e.body,
|
|
505
|
+
authorUid: e.authorUid,
|
|
506
|
+
authorName: e.authorName,
|
|
507
|
+
authorPhoto: e.authorPhoto,
|
|
508
|
+
moderationStatus: "approved",
|
|
509
|
+
parentId: null,
|
|
510
|
+
rootId: r.id,
|
|
511
|
+
depth: 0,
|
|
512
|
+
replyToUid: null,
|
|
513
|
+
replyToName: null,
|
|
514
|
+
replyCount: 0,
|
|
515
|
+
isReply: !1,
|
|
516
|
+
createdAt: w()
|
|
517
|
+
}), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(1) }), await t.commit();
|
|
518
|
+
}
|
|
519
|
+
async function L(e) {
|
|
520
|
+
if (!e.parent.id) throw Error("답글 대상 댓글을 찾을 수 없습니다.");
|
|
521
|
+
let t = k(s), n = c(), r = u(s, `${n}Posts`, d.value, "comments"), i = p(r), a = e.parent.parentId == null ? e.parent.id : e.parent.rootId;
|
|
522
|
+
if (!a) throw Error("댓글 스레드를 찾을 수 없습니다.");
|
|
523
|
+
let o = Math.min((e.parent.depth ?? 0) + 1, 2);
|
|
434
524
|
t.set(i, {
|
|
435
|
-
postId:
|
|
525
|
+
postId: d.value,
|
|
436
526
|
body: e.body,
|
|
437
527
|
authorUid: e.authorUid,
|
|
438
528
|
authorName: e.authorName,
|
|
439
529
|
authorPhoto: e.authorPhoto,
|
|
440
530
|
moderationStatus: "approved",
|
|
441
|
-
|
|
442
|
-
|
|
531
|
+
parentId: e.parent.id,
|
|
532
|
+
rootId: a,
|
|
533
|
+
depth: o,
|
|
534
|
+
replyToUid: e.parent.authorUid,
|
|
535
|
+
replyToName: e.parent.authorName,
|
|
536
|
+
replyCount: 0,
|
|
537
|
+
isReply: !0,
|
|
538
|
+
createdAt: w()
|
|
539
|
+
}), t.update(p(r, a), { replyCount: v(1) }), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(1) }), await t.commit();
|
|
443
540
|
}
|
|
444
|
-
async function
|
|
445
|
-
|
|
446
|
-
|
|
541
|
+
async function ne(e) {
|
|
542
|
+
if (!e.id) return;
|
|
543
|
+
if (e.parentId == null && (e.replyCount ?? 0) > 0) throw Error("답글이 있는 댓글은 현재 삭제할 수 없습니다.");
|
|
544
|
+
let t = k(s), n = c();
|
|
545
|
+
t.delete(p(s, `${n}Posts`, d.value, "comments", e.id)), e.parentId && e.rootId && t.update(p(s, `${n}Posts`, d.value, "comments", e.rootId), { replyCount: v(-1) }), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(-1) }), await t.commit(), b.value = b.value.filter((t) => t.id !== e.id), h.value = h.value.filter((t) => t.id !== e.id), f.value = f.value.filter((t) => t.id !== e.id);
|
|
447
546
|
}
|
|
448
547
|
return {
|
|
449
|
-
comments:
|
|
450
|
-
|
|
451
|
-
|
|
548
|
+
comments: ee,
|
|
549
|
+
commentsPending: e(() => A.value || g.value),
|
|
550
|
+
hasMore: D,
|
|
551
|
+
loadingMore: T,
|
|
552
|
+
loadMore: F,
|
|
553
|
+
addComment: I,
|
|
554
|
+
addReply: L,
|
|
555
|
+
deleteComment: ne
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function Te(e, n) {
|
|
559
|
+
let i = U(), a = l(), o = r([]), s = r(!1), c = r(!1), d = r(!0), f = null, p;
|
|
560
|
+
function h(e) {
|
|
561
|
+
return e.sort((e, t) => (e.createdAt?.toMillis?.() ?? 0) - (t.createdAt?.toMillis?.() ?? 0) || String(e.id).localeCompare(String(t.id)));
|
|
562
|
+
}
|
|
563
|
+
function g() {
|
|
564
|
+
p ||= x(C(u(a, `${i.collectionPrefix}Posts`, e, "comments"), O("rootId", "==", n), O("isReply", "==", !0), S("createdAt", "desc"), S(m(), "desc"), y(1)), (e) => {
|
|
565
|
+
let t = new Map(o.value.map((e) => [e.id, e]));
|
|
566
|
+
for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
|
|
567
|
+
id: n.doc.id,
|
|
568
|
+
...n.doc.data()
|
|
569
|
+
});
|
|
570
|
+
o.value = h([...t.values()]);
|
|
571
|
+
}, (e) => {
|
|
572
|
+
console.error("[memi-board:replies] onSnapshot failed", e);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async function v() {
|
|
576
|
+
if (!(s.value || !d.value)) {
|
|
577
|
+
s.value = !0;
|
|
578
|
+
try {
|
|
579
|
+
let t = [
|
|
580
|
+
O("rootId", "==", n),
|
|
581
|
+
O("isReply", "==", !0),
|
|
582
|
+
S("createdAt", "asc"),
|
|
583
|
+
S(m(), "asc"),
|
|
584
|
+
...f ? [E(f)] : [],
|
|
585
|
+
y(6)
|
|
586
|
+
], r = await _(C(u(a, `${i.collectionPrefix}Posts`, e, "comments"), ...t)), s = new Set(o.value.map((e) => e.id)), l = r.docs.slice(0, Ce), p = l.map((e) => ({
|
|
587
|
+
id: e.id,
|
|
588
|
+
...e.data()
|
|
589
|
+
})).filter((e) => !s.has(e.id));
|
|
590
|
+
o.value = h([...o.value, ...p]), f = l.at(-1) ?? f, c.value = !0, r.size <= Ce && (d.value = !1, g());
|
|
591
|
+
} finally {
|
|
592
|
+
s.value = !1;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function b() {
|
|
597
|
+
p?.(), p = void 0, o.value = [], f = null, c.value = !1, d.value = !0;
|
|
598
|
+
}
|
|
599
|
+
async function w() {
|
|
600
|
+
b(), await v();
|
|
601
|
+
}
|
|
602
|
+
return t(() => p?.()), {
|
|
603
|
+
replies: o,
|
|
604
|
+
loading: s,
|
|
605
|
+
loaded: c,
|
|
606
|
+
hasMore: d,
|
|
607
|
+
loadMore: v,
|
|
608
|
+
refresh: w
|
|
452
609
|
};
|
|
453
610
|
}
|
|
454
611
|
//#endregion
|
|
455
612
|
//#region src/utils/moderation-prompt.ts
|
|
456
|
-
var
|
|
613
|
+
var Ee = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", De = {
|
|
457
614
|
abuse: "욕설·비속어·모욕",
|
|
458
615
|
spam: "스팸·광고",
|
|
459
616
|
adult: "선정적·성인 내용",
|
|
@@ -461,17 +618,17 @@ var xe = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
|
|
|
461
618
|
other: "부적절한 내용",
|
|
462
619
|
none: ""
|
|
463
620
|
};
|
|
464
|
-
function
|
|
621
|
+
function Z(e) {
|
|
465
622
|
if (e.via === "local" && e.localHit) {
|
|
466
623
|
let t = e.localHit.trim();
|
|
467
624
|
return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
|
|
468
625
|
}
|
|
469
626
|
let t = (e.reason || "").trim();
|
|
470
627
|
if (t) return t;
|
|
471
|
-
let n = e.category && e.category !== "none" ?
|
|
628
|
+
let n = e.category && e.category !== "none" ? De[e.category] || e.category : "";
|
|
472
629
|
return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
|
|
473
630
|
}
|
|
474
|
-
var
|
|
631
|
+
var Oe = [
|
|
475
632
|
"씨발",
|
|
476
633
|
"시발",
|
|
477
634
|
"시이발",
|
|
@@ -497,10 +654,10 @@ var Ce = [
|
|
|
497
654
|
"bitch",
|
|
498
655
|
"asshole"
|
|
499
656
|
];
|
|
500
|
-
function
|
|
657
|
+
function ke(e) {
|
|
501
658
|
return new RegExp(e.join("|"), "i");
|
|
502
659
|
}
|
|
503
|
-
var
|
|
660
|
+
var Ae = /* @__PURE__ */ new Set([
|
|
504
661
|
"none",
|
|
505
662
|
"abuse",
|
|
506
663
|
"spam",
|
|
@@ -508,7 +665,7 @@ var Te = /* @__PURE__ */ new Set([
|
|
|
508
665
|
"violence",
|
|
509
666
|
"other"
|
|
510
667
|
]);
|
|
511
|
-
function
|
|
668
|
+
function je(e) {
|
|
512
669
|
let t = e.trim();
|
|
513
670
|
if (!t) return null;
|
|
514
671
|
let n;
|
|
@@ -526,7 +683,7 @@ function Ee(e) {
|
|
|
526
683
|
if (typeof n != "object" || !n) return null;
|
|
527
684
|
let r = n;
|
|
528
685
|
if (typeof r.flagged != "boolean") return null;
|
|
529
|
-
let i = typeof r.category == "string" &&
|
|
686
|
+
let i = typeof r.category == "string" && Ae.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
530
687
|
return {
|
|
531
688
|
flagged: r.flagged,
|
|
532
689
|
category: i,
|
|
@@ -535,20 +692,20 @@ function Ee(e) {
|
|
|
535
692
|
}
|
|
536
693
|
//#endregion
|
|
537
694
|
//#region src/composables/useMemiBoardModeration.ts
|
|
538
|
-
var
|
|
695
|
+
var Me = {
|
|
539
696
|
flagged: !1,
|
|
540
697
|
category: "none",
|
|
541
698
|
reason: "",
|
|
542
699
|
via: "empty"
|
|
543
700
|
};
|
|
544
|
-
function
|
|
545
|
-
let e =
|
|
546
|
-
function
|
|
701
|
+
function Ne() {
|
|
702
|
+
let e = U().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = X(), a = ke([...Oe, ...e.localBlocklist ?? []]);
|
|
703
|
+
function o(e) {
|
|
547
704
|
let t = e.match(a);
|
|
548
705
|
return t ? {
|
|
549
706
|
flagged: !0,
|
|
550
707
|
category: "abuse",
|
|
551
|
-
reason:
|
|
708
|
+
reason: Z({
|
|
552
709
|
via: "local",
|
|
553
710
|
localHit: t[0] || "",
|
|
554
711
|
category: "abuse"
|
|
@@ -557,21 +714,21 @@ function De() {
|
|
|
557
714
|
} : null;
|
|
558
715
|
}
|
|
559
716
|
function c() {
|
|
560
|
-
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i =
|
|
561
|
-
backend: new
|
|
717
|
+
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = ae(t, {
|
|
718
|
+
backend: new ie(),
|
|
562
719
|
useLimitedUseAppCheckTokens: r
|
|
563
720
|
});
|
|
564
|
-
return
|
|
721
|
+
return oe(i, {
|
|
565
722
|
model: n,
|
|
566
|
-
systemInstruction:
|
|
723
|
+
systemInstruction: Ee,
|
|
567
724
|
generationConfig: {
|
|
568
725
|
temperature: 0,
|
|
569
726
|
maxOutputTokens: 256,
|
|
570
727
|
responseMimeType: "application/json",
|
|
571
|
-
responseSchema:
|
|
572
|
-
flagged:
|
|
573
|
-
category:
|
|
574
|
-
reason:
|
|
728
|
+
responseSchema: B.object({ properties: {
|
|
729
|
+
flagged: B.boolean(),
|
|
730
|
+
category: B.string(),
|
|
731
|
+
reason: B.string()
|
|
575
732
|
} })
|
|
576
733
|
}
|
|
577
734
|
});
|
|
@@ -592,7 +749,7 @@ function De() {
|
|
|
592
749
|
};
|
|
593
750
|
}
|
|
594
751
|
function u(e, t = "") {
|
|
595
|
-
let n =
|
|
752
|
+
let n = Z({
|
|
596
753
|
reason: e.reason,
|
|
597
754
|
category: e.category,
|
|
598
755
|
via: e.via
|
|
@@ -620,22 +777,22 @@ function De() {
|
|
|
620
777
|
};
|
|
621
778
|
let i = t.trim();
|
|
622
779
|
if (!i) return {
|
|
623
|
-
...
|
|
780
|
+
...Me,
|
|
624
781
|
via: "empty"
|
|
625
782
|
};
|
|
626
|
-
let a =
|
|
783
|
+
let a = o(i);
|
|
627
784
|
if (a) return d(a);
|
|
628
785
|
if (e.enabled === !1) return {
|
|
629
|
-
...
|
|
786
|
+
...Me,
|
|
630
787
|
via: "disabled"
|
|
631
788
|
};
|
|
632
789
|
try {
|
|
633
|
-
let e =
|
|
790
|
+
let e = je((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
|
|
634
791
|
if (!e) return l();
|
|
635
792
|
let t = {
|
|
636
793
|
flagged: e.flagged,
|
|
637
794
|
category: e.category,
|
|
638
|
-
reason:
|
|
795
|
+
reason: Z({
|
|
639
796
|
reason: e.reason,
|
|
640
797
|
category: e.category,
|
|
641
798
|
via: "ai"
|
|
@@ -655,14 +812,14 @@ function De() {
|
|
|
655
812
|
via: "restricted"
|
|
656
813
|
};
|
|
657
814
|
if (e.enabled === !1 || !e.moderateImages) return {
|
|
658
|
-
...
|
|
815
|
+
...Me,
|
|
659
816
|
via: "disabled"
|
|
660
817
|
};
|
|
661
818
|
try {
|
|
662
819
|
let e = await new Promise((e, n) => {
|
|
663
820
|
let r = new FileReader();
|
|
664
821
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
665
|
-
}), n =
|
|
822
|
+
}), n = je((await c().generateContent([{ inlineData: {
|
|
666
823
|
mimeType: t.type || "image/jpeg",
|
|
667
824
|
data: e
|
|
668
825
|
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
@@ -670,7 +827,7 @@ function De() {
|
|
|
670
827
|
let r = {
|
|
671
828
|
flagged: n.flagged,
|
|
672
829
|
category: n.category,
|
|
673
|
-
reason:
|
|
830
|
+
reason: Z({
|
|
674
831
|
reason: n.reason,
|
|
675
832
|
category: n.category,
|
|
676
833
|
via: "ai"
|
|
@@ -689,7 +846,7 @@ function De() {
|
|
|
689
846
|
}
|
|
690
847
|
//#endregion
|
|
691
848
|
//#region src/utils/compressImage.ts
|
|
692
|
-
async function
|
|
849
|
+
async function Pe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
693
850
|
return new Promise((r, i) => {
|
|
694
851
|
let a = new Image(), o = URL.createObjectURL(e);
|
|
695
852
|
a.onload = () => {
|
|
@@ -709,11 +866,11 @@ async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
|
709
866
|
}
|
|
710
867
|
//#endregion
|
|
711
868
|
//#region src/composables/useMemiBoardStorage.ts
|
|
712
|
-
var
|
|
713
|
-
function
|
|
714
|
-
let e =
|
|
869
|
+
var Fe = 5242880;
|
|
870
|
+
function Ie() {
|
|
871
|
+
let e = U(), t = s(), n = () => e.collectionPrefix;
|
|
715
872
|
function r(e, r, i) {
|
|
716
|
-
let a =
|
|
873
|
+
let a = L(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = R(a, o), c = re(s, e, {
|
|
717
874
|
contentType: e.type || "application/octet-stream",
|
|
718
875
|
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
|
|
719
876
|
});
|
|
@@ -722,7 +879,7 @@ function Ae() {
|
|
|
722
879
|
c.on("state_changed", (e) => {
|
|
723
880
|
i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
|
|
724
881
|
}, n, async () => {
|
|
725
|
-
let n = await
|
|
882
|
+
let n = await I(s);
|
|
726
883
|
t({
|
|
727
884
|
name: e.name,
|
|
728
885
|
url: n,
|
|
@@ -736,39 +893,39 @@ function Ae() {
|
|
|
736
893
|
};
|
|
737
894
|
}
|
|
738
895
|
async function i(e) {
|
|
739
|
-
let n =
|
|
740
|
-
await
|
|
896
|
+
let n = L(t);
|
|
897
|
+
await F(R(n, e.path)).catch(() => {});
|
|
741
898
|
}
|
|
742
899
|
async function a(e, r) {
|
|
743
900
|
if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
|
|
744
|
-
if (e.size > 5242880) throw Error(`이미지는 ${
|
|
745
|
-
let i =
|
|
746
|
-
await
|
|
747
|
-
let d = await
|
|
901
|
+
if (e.size > 5242880) throw Error(`이미지는 ${Fe / 1024 / 1024}MB 이하여야 합니다.`);
|
|
902
|
+
let i = L(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = fe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = R(i, l);
|
|
903
|
+
await z(u, e, { contentType: e.type || "image/png" });
|
|
904
|
+
let d = await I(u), f = await Pe(e, {
|
|
748
905
|
maxWidth: 400,
|
|
749
906
|
quality: .8
|
|
750
|
-
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m =
|
|
751
|
-
return await
|
|
907
|
+
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = R(i, p);
|
|
908
|
+
return await z(m, f, { contentType: "image/jpeg" }), {
|
|
752
909
|
originalUrl: d,
|
|
753
910
|
originalPath: l,
|
|
754
|
-
thumbnailUrl: await
|
|
911
|
+
thumbnailUrl: await I(m),
|
|
755
912
|
thumbnailPath: p
|
|
756
913
|
};
|
|
757
914
|
}
|
|
758
|
-
async function
|
|
759
|
-
let n =
|
|
760
|
-
await Promise.all([
|
|
915
|
+
async function o(e) {
|
|
916
|
+
let n = L(t);
|
|
917
|
+
await Promise.all([F(R(n, e.originalPath)).catch(() => {}), F(R(n, e.thumbnailPath)).catch(() => {})]);
|
|
761
918
|
}
|
|
762
919
|
return {
|
|
763
920
|
uploadAttachment: r,
|
|
764
921
|
deleteAttachment: i,
|
|
765
922
|
uploadEditorImage: a,
|
|
766
|
-
deleteEditorImage:
|
|
923
|
+
deleteEditorImage: o
|
|
767
924
|
};
|
|
768
925
|
}
|
|
769
926
|
//#endregion
|
|
770
927
|
//#region src/composables/useMemiBoardSettings.ts
|
|
771
|
-
var
|
|
928
|
+
var Le = [
|
|
772
929
|
{
|
|
773
930
|
id: "free",
|
|
774
931
|
label: "자유",
|
|
@@ -791,8 +948,8 @@ var je = [
|
|
|
791
948
|
order: 2
|
|
792
949
|
}
|
|
793
950
|
];
|
|
794
|
-
function
|
|
795
|
-
let t =
|
|
951
|
+
function Re() {
|
|
952
|
+
let t = U(), n = l(), { isSignedIn: r } = X(), i = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), o = e(() => u(n, `${t.collectionPrefix}Settings`, "config", "categories")), s = e(() => C(o.value, S("order", "asc"))), { data: c, pending: f } = a(s, { ssrKey: `${t.collectionPrefix}Settings/config/categories` }), m = e(() => c.value.map((e, t) => ({
|
|
796
953
|
...e,
|
|
797
954
|
id: e.id,
|
|
798
955
|
order: typeof e.order == "number" ? e.order : t,
|
|
@@ -804,45 +961,45 @@ function Me() {
|
|
|
804
961
|
}
|
|
805
962
|
async function g(e, t = e.order ?? 0) {
|
|
806
963
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
807
|
-
let n = e.id.trim(),
|
|
808
|
-
if (!n || !
|
|
809
|
-
await
|
|
810
|
-
label:
|
|
964
|
+
let n = e.id.trim(), a = e.label.trim();
|
|
965
|
+
if (!n || !a) throw Error("카테고리 ID와 이름을 입력해 주세요.");
|
|
966
|
+
await T(i.value, { updatedAt: w() }, { merge: !0 }), await T(p(o.value, n), {
|
|
967
|
+
label: a,
|
|
811
968
|
listView: e.listView ?? "default",
|
|
812
969
|
writeRole: e.writeRole ?? "user",
|
|
813
970
|
order: t,
|
|
814
|
-
updatedAt:
|
|
971
|
+
updatedAt: w()
|
|
815
972
|
}, { merge: !0 });
|
|
816
973
|
}
|
|
817
974
|
async function _(e) {
|
|
818
975
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
819
|
-
let t =
|
|
820
|
-
t.set(
|
|
976
|
+
let t = k(n);
|
|
977
|
+
t.set(i.value, { updatedAt: w() }, { merge: !0 }), e.forEach((e, n) => {
|
|
821
978
|
let r = e.id.trim(), i = e.label.trim();
|
|
822
979
|
if (!r || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
|
|
823
|
-
t.set(
|
|
980
|
+
t.set(p(o.value, r), {
|
|
824
981
|
label: i,
|
|
825
982
|
listView: e.listView ?? "default",
|
|
826
983
|
writeRole: e.writeRole ?? "user",
|
|
827
984
|
order: n,
|
|
828
|
-
updatedAt:
|
|
985
|
+
updatedAt: w()
|
|
829
986
|
}, { merge: !0 });
|
|
830
987
|
}), await t.commit();
|
|
831
988
|
}
|
|
832
989
|
async function v(e) {
|
|
833
990
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
834
|
-
await
|
|
991
|
+
await d(p(o.value, e));
|
|
835
992
|
}
|
|
836
|
-
async function
|
|
993
|
+
async function y() {
|
|
837
994
|
return Promise.resolve();
|
|
838
995
|
}
|
|
839
|
-
async function
|
|
996
|
+
async function b(e) {
|
|
840
997
|
let t = e.trim();
|
|
841
998
|
if (!t) throw Error("카테고리 이름을 입력해 주세요.");
|
|
842
999
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
843
1000
|
let n = m.value.find((e) => e.label === t);
|
|
844
1001
|
if (n) return n.id;
|
|
845
|
-
let i =
|
|
1002
|
+
let i = de(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
|
|
846
1003
|
for (; s.has(a);) a = `${i}-${o++}`;
|
|
847
1004
|
return await g({
|
|
848
1005
|
id: a,
|
|
@@ -855,18 +1012,18 @@ function Me() {
|
|
|
855
1012
|
return {
|
|
856
1013
|
categories: m,
|
|
857
1014
|
categoryLabel: h,
|
|
858
|
-
settingsPending:
|
|
859
|
-
ensureSettings:
|
|
860
|
-
addCategory:
|
|
1015
|
+
settingsPending: f,
|
|
1016
|
+
ensureSettings: y,
|
|
1017
|
+
addCategory: b,
|
|
861
1018
|
saveCategory: g,
|
|
862
1019
|
saveCategories: _,
|
|
863
1020
|
deleteCategory: v,
|
|
864
|
-
DEFAULT_CATEGORIES:
|
|
1021
|
+
DEFAULT_CATEGORIES: Le
|
|
865
1022
|
};
|
|
866
1023
|
}
|
|
867
1024
|
//#endregion
|
|
868
1025
|
//#region src/composables/useMemiBoardUsers.ts
|
|
869
|
-
var
|
|
1026
|
+
var ze = [
|
|
870
1027
|
{
|
|
871
1028
|
value: "admin",
|
|
872
1029
|
label: "관리자",
|
|
@@ -883,24 +1040,47 @@ var Ne = [
|
|
|
883
1040
|
description: "글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다."
|
|
884
1041
|
}
|
|
885
1042
|
];
|
|
886
|
-
function
|
|
887
|
-
let e =
|
|
888
|
-
async function
|
|
889
|
-
if (!
|
|
890
|
-
await
|
|
1043
|
+
function Be() {
|
|
1044
|
+
let e = U(), t = l(), n = `${e.collectionPrefix}Users`, r = a(u(t, n), { ssrKey: `${n}-management` });
|
|
1045
|
+
async function i(e, r) {
|
|
1046
|
+
if (!ze.some((e) => e.value === r)) throw Error("지원하지 않는 역할입니다.");
|
|
1047
|
+
await D(p(t, n, e), {
|
|
891
1048
|
role: r,
|
|
892
|
-
updatedAt:
|
|
1049
|
+
updatedAt: w()
|
|
893
1050
|
});
|
|
894
1051
|
}
|
|
895
1052
|
return {
|
|
896
1053
|
users: r,
|
|
897
1054
|
usersPending: r.pending,
|
|
898
|
-
updateUserRole:
|
|
1055
|
+
updateUserRole: i
|
|
899
1056
|
};
|
|
900
1057
|
}
|
|
901
1058
|
//#endregion
|
|
902
1059
|
//#region src/data/versionHistory.ts
|
|
903
|
-
var
|
|
1060
|
+
var Ve = [
|
|
1061
|
+
{
|
|
1062
|
+
version: "0.9.1",
|
|
1063
|
+
date: "2026-08-04",
|
|
1064
|
+
highlights: ["npm으로 설치한 프로젝트에서 사용자 관리 화면이 정상적으로 빌드돼요"]
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
version: "0.9.0",
|
|
1068
|
+
date: "2026-08-04",
|
|
1069
|
+
highlights: [
|
|
1070
|
+
"댓글에 답글을 달고 대화 흐름을 최대 2단계로 볼 수 있어요",
|
|
1071
|
+
"댓글 10개·답글 5개 단위로 이어서 보고 마지막부터 새 댓글을 실시간으로 받아요",
|
|
1072
|
+
"더보기 버튼이 화면에 들어오면 로딩 표시와 함께 다음 댓글을 자동으로 불러와요"
|
|
1073
|
+
]
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
version: "0.8.0",
|
|
1077
|
+
date: "2026-08-04",
|
|
1078
|
+
highlights: [
|
|
1079
|
+
"최근 댓글 5개는 실시간으로 보고 이전 댓글은 5개씩 불러올 수 있어요",
|
|
1080
|
+
"댓글 시간이 상대시간으로 표시되고 전체 시간은 툴팁으로 확인할 수 있어요",
|
|
1081
|
+
"댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요"
|
|
1082
|
+
]
|
|
1083
|
+
},
|
|
904
1084
|
{
|
|
905
1085
|
version: "0.7.0",
|
|
906
1086
|
date: "2026-08-04",
|
|
@@ -957,7 +1137,7 @@ var Fe = [
|
|
|
957
1137
|
];
|
|
958
1138
|
//#endregion
|
|
959
1139
|
//#region src/utils/formatDate.ts
|
|
960
|
-
function
|
|
1140
|
+
function He(e) {
|
|
961
1141
|
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
962
1142
|
}
|
|
963
1143
|
//#endregion
|
|
@@ -969,7 +1149,7 @@ function $(e) {
|
|
|
969
1149
|
let t = e;
|
|
970
1150
|
return t = t.replace(/`([^`]+)`/g, "<code>$1</code>"), t = t.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, "<img src=\"$2\" alt=\"$1\" class=\"max-w-full h-auto rounded-lg my-3\" loading=\"lazy\" />"), t = t.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary underline\">$1</a>"), t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"), t = t.replace(/\*([^*]+)\*/g, "<em>$1</em>"), t = t.replace(/~~([^~]+)~~/g, "<del>$1</del>"), t;
|
|
971
1151
|
}
|
|
972
|
-
function
|
|
1152
|
+
function Ue(e) {
|
|
973
1153
|
if (!e?.trim()) return "";
|
|
974
1154
|
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
975
1155
|
function c() {
|
|
@@ -1034,6 +1214,6 @@ function Le(e) {
|
|
|
1034
1214
|
return c(), i && a.length && n.push(`<pre class="overflow-x-auto rounded-lg bg-muted p-3 text-sm"><code>${a.join("\n")}</code></pre>`), n.join("\n");
|
|
1035
1215
|
}
|
|
1036
1216
|
//#endregion
|
|
1037
|
-
export {
|
|
1217
|
+
export { ze as BOARD_USER_ROLES, W as DEFAULT_BLOCK_BAN_DECAY_MS, ce as DEFAULT_BLOCK_BAN_THRESHOLD, Le as DEFAULT_CATEGORIES, Fe as EDITOR_IMAGE_MAX_BYTES, Pe as compressImage, se as configureMemiBoard, G as effectiveModerationBlockCount, pe as extractEditorImageUrls, He as formatDate, J as formatRestrictedUntilLabel, q as isModerationWriteRestricted, K as moderationWriteRestrictedUntilMs, he as postNamespaceFromStoragePath, Ue as renderMarkdownToHtml, de as slugify, me as storagePathFromDownloadUrl, X as useMemiBoardAuth, we as useMemiBoardComments, U as useMemiBoardConfig, Ne as useMemiBoardModeration, xe as useMemiBoardPosts, Te as useMemiBoardReplies, Re as useMemiBoardSettings, Ie as useMemiBoardStorage, Be as useMemiBoardUsers, Ve as versionHistory };
|
|
1038
1218
|
|
|
1039
1219
|
//# sourceMappingURL=index.js.map
|