memi-board 0.4.13 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/composables/useMemiBoardPosts.d.ts +8 -0
- package/dist/composables/useMemiBoardSettings.d.ts +5 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +361 -266
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/Attachments.vue +1 -1
- package/dist/runtime/components/Detail.vue +109 -40
- package/dist/runtime/components/Editor.vue +62 -23
- package/dist/runtime/components/List.vue +19 -49
- package/dist/runtime/components/ListDefault.vue +41 -0
- package/dist/runtime/components/ListImage.vue +41 -0
- package/dist/runtime/components/ListVideo.vue +44 -0
- package/dist/runtime/components/Settings.vue +238 -0
- package/dist/types.d.ts +12 -2
- package/dist/utils/postPreview.d.ts +6 -0
- package/docs/firestore.rules.example +13 -5
- package/docs/storage.rules.example +2 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { computed as e, reactive as t, ref as n, watch as r } from "vue";
|
|
2
|
-
import { useCollection as i, useCurrentUser as a,
|
|
3
|
-
import { collection as
|
|
4
|
-
import { GoogleAuthProvider as
|
|
5
|
-
import { deleteObject as
|
|
6
|
-
import { GoogleAIBackend as
|
|
2
|
+
import { useCollection as i, useCurrentUser as a, useFirebaseApp as o, useFirebaseAuth as s, useFirestore as c } from "vuefire";
|
|
3
|
+
import { collection as l, deleteDoc as u, deleteField as d, doc as f, endBefore as p, getDoc as m, getDocs as h, increment as g, limit as _, limitToLast as v, orderBy as y, query as b, serverTimestamp as x, setDoc as S, startAfter as C, updateDoc as w, where as T, writeBatch as E } from "firebase/firestore";
|
|
4
|
+
import { GoogleAuthProvider as D, OAuthProvider as ee, createUserWithEmailAndPassword as O, signInWithEmailAndPassword as te, signInWithPopup as k, signOut as ne, updateProfile as re } from "firebase/auth";
|
|
5
|
+
import { deleteObject as A, getDownloadURL as j, getStorage as M, listAll as N, ref as P, uploadBytes as ie, uploadBytesResumable as F } from "firebase/storage";
|
|
6
|
+
import { GoogleAIBackend as ae, Schema as I, getAI as oe, getGenerativeModel as se } from "firebase/ai";
|
|
7
7
|
//#region src/config.ts
|
|
8
|
-
var
|
|
9
|
-
function
|
|
8
|
+
var L = "__MEMI_BOARD_CONFIG__";
|
|
9
|
+
function R() {
|
|
10
10
|
let e = globalThis;
|
|
11
|
-
return e[
|
|
11
|
+
return e[L] || (e[L] = t({
|
|
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[L];
|
|
24
24
|
}
|
|
25
|
-
function
|
|
26
|
-
let t =
|
|
25
|
+
function ce(e) {
|
|
26
|
+
let t = R();
|
|
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 z() {
|
|
30
|
+
return R();
|
|
31
31
|
}
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/utils/moderation-strike.ts
|
|
34
|
-
var le = 3,
|
|
35
|
-
function
|
|
34
|
+
var le = 3, B = 864e5;
|
|
35
|
+
function V(e, t, n = Date.now(), r = B) {
|
|
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 H(e, t, n = Date.now(), r = 3, i = B) {
|
|
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,10 +48,10 @@ 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 U(e, t, n = Date.now(), r = 3, i = B) {
|
|
52
|
+
return H(e, t, n, r, i) != null;
|
|
53
53
|
}
|
|
54
|
-
function
|
|
54
|
+
function W(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}시간 후 해제`;
|
|
@@ -70,168 +70,168 @@ function ue(e) {
|
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region src/composables/useMemiBoardAuth.ts
|
|
73
|
-
var
|
|
73
|
+
var G = "__MEMI_BOARD_AUTH_STATE__";
|
|
74
74
|
function de() {
|
|
75
75
|
let e = globalThis;
|
|
76
|
-
return e[
|
|
76
|
+
return e[G] || (e[G] = {
|
|
77
77
|
role: n("user"),
|
|
78
78
|
rolePending: n(!0),
|
|
79
79
|
rawBlockCount: n(0),
|
|
80
80
|
blockAtMs: n(null),
|
|
81
81
|
watchStarted: !1
|
|
82
|
-
}), e[
|
|
82
|
+
}), e[G];
|
|
83
83
|
}
|
|
84
|
-
function
|
|
85
|
-
let t =
|
|
84
|
+
function K() {
|
|
85
|
+
let t = z(), n = de(), i = c(), o = s(), l = a(), { role: u, rolePending: d, rawBlockCount: p, blockAtMs: h } = n;
|
|
86
86
|
function _() {
|
|
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
90
|
function v() {
|
|
91
91
|
let e = t.moderation?.blockBanDecayMs;
|
|
92
|
-
return typeof e == "number" && e > 0 ? e :
|
|
92
|
+
return typeof e == "number" && e > 0 ? e : B;
|
|
93
93
|
}
|
|
94
94
|
function y(e) {
|
|
95
|
-
return
|
|
95
|
+
return f(i, `${t.collectionPrefix}Users`, e);
|
|
96
96
|
}
|
|
97
|
-
function
|
|
97
|
+
function b(e) {
|
|
98
98
|
if (!e) {
|
|
99
|
-
|
|
99
|
+
p.value = 0, h.value = null;
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
p.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ue(e.moderationBlockAt);
|
|
103
103
|
}
|
|
104
|
-
async function
|
|
104
|
+
async function C(e) {
|
|
105
105
|
let t = y(e.uid), n = await m(t);
|
|
106
|
-
if (!n.exists()) return await
|
|
106
|
+
if (!n.exists()) return await S(t, {
|
|
107
107
|
role: "user",
|
|
108
108
|
displayName: e.displayName,
|
|
109
|
-
updatedAt:
|
|
110
|
-
}),
|
|
109
|
+
updatedAt: x()
|
|
110
|
+
}), b({ moderationBlockCount: 0 }), "user";
|
|
111
111
|
let r = n.data();
|
|
112
|
-
return
|
|
112
|
+
return b(r), await w(t, {
|
|
113
113
|
displayName: e.displayName,
|
|
114
|
-
updatedAt:
|
|
114
|
+
updatedAt: x()
|
|
115
115
|
}), r.role ?? "user";
|
|
116
116
|
}
|
|
117
|
-
async function
|
|
118
|
-
let e =
|
|
117
|
+
async function T() {
|
|
118
|
+
let e = l.value?.uid;
|
|
119
119
|
if (!e) {
|
|
120
|
-
|
|
120
|
+
b(void 0);
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
let t = await m(y(e));
|
|
124
|
-
|
|
124
|
+
b(t.exists() ? t.data() : void 0);
|
|
125
125
|
}
|
|
126
|
-
n.watchStarted || (n.watchStarted = !0, r(
|
|
126
|
+
n.watchStarted || (n.watchStarted = !0, r(l, async (e) => {
|
|
127
127
|
if (d.value = !0, !e) {
|
|
128
|
-
u.value = "user",
|
|
128
|
+
u.value = "user", b(void 0), d.value = !1;
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
try {
|
|
132
|
-
u.value = await
|
|
132
|
+
u.value = await C(e);
|
|
133
133
|
} catch {
|
|
134
134
|
u.value = "user";
|
|
135
135
|
} finally {
|
|
136
136
|
d.value = !1;
|
|
137
137
|
}
|
|
138
138
|
}, { immediate: !0 }));
|
|
139
|
-
let E = e(() => !!
|
|
139
|
+
let E = e(() => !!l.value), A = e(() => u.value === "admin"), j = e(() => V(p.value, h.value, Date.now(), v())), M = e(() => {
|
|
140
140
|
let e = _();
|
|
141
|
-
return e <= 0 ? !1 :
|
|
142
|
-
}),
|
|
141
|
+
return e <= 0 ? !1 : U(p.value, h.value, Date.now(), e, v());
|
|
142
|
+
}), N = e(() => {
|
|
143
143
|
let e = _();
|
|
144
144
|
if (e <= 0) return null;
|
|
145
|
-
let t =
|
|
146
|
-
return t == null ? null :
|
|
147
|
-
}),
|
|
148
|
-
if (!
|
|
149
|
-
let e =
|
|
145
|
+
let t = H(p.value, h.value, Date.now(), e, v());
|
|
146
|
+
return t == null ? null : W(t);
|
|
147
|
+
}), P = e(() => {
|
|
148
|
+
if (!M.value) return null;
|
|
149
|
+
let e = N.value;
|
|
150
150
|
return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
|
|
151
151
|
});
|
|
152
|
-
async function
|
|
153
|
-
let e =
|
|
152
|
+
async function ie() {
|
|
153
|
+
let e = l.value?.uid, t = _();
|
|
154
154
|
if (!e || t <= 0) return {
|
|
155
155
|
effectiveCount: 0,
|
|
156
156
|
restricted: !1,
|
|
157
157
|
messageSuffix: ""
|
|
158
158
|
};
|
|
159
159
|
let n = y(e), r = await m(n);
|
|
160
|
-
r.exists() ? await
|
|
160
|
+
r.exists() ? await w(n, {
|
|
161
161
|
moderationBlockCount: g(1),
|
|
162
|
-
moderationBlockAt:
|
|
163
|
-
updatedAt:
|
|
164
|
-
}) : await
|
|
162
|
+
moderationBlockAt: x(),
|
|
163
|
+
updatedAt: x()
|
|
164
|
+
}) : await S(n, {
|
|
165
165
|
role: "user",
|
|
166
|
-
displayName:
|
|
166
|
+
displayName: l.value?.displayName ?? null,
|
|
167
167
|
moderationBlockCount: 1,
|
|
168
|
-
moderationBlockAt:
|
|
169
|
-
updatedAt:
|
|
168
|
+
moderationBlockAt: x(),
|
|
169
|
+
updatedAt: x()
|
|
170
170
|
});
|
|
171
171
|
let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
|
|
172
|
-
|
|
173
|
-
let o =
|
|
174
|
-
if (
|
|
175
|
-
let e =
|
|
176
|
-
|
|
172
|
+
p.value = i, h.value = a;
|
|
173
|
+
let o = V(i, a, a, v()), s = U(i, a, a, t, v()), c = ` (경고 ${o}/${t})`;
|
|
174
|
+
if (s) {
|
|
175
|
+
let e = H(i, a, a, t, v()), n = e == null ? null : W(e, a);
|
|
176
|
+
c = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
|
|
177
177
|
}
|
|
178
178
|
return {
|
|
179
179
|
effectiveCount: o,
|
|
180
|
-
restricted:
|
|
181
|
-
messageSuffix:
|
|
180
|
+
restricted: s,
|
|
181
|
+
messageSuffix: c
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
|
-
function
|
|
184
|
+
function F() {
|
|
185
185
|
if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
|
|
186
186
|
return o;
|
|
187
187
|
}
|
|
188
|
-
async function
|
|
189
|
-
return
|
|
188
|
+
async function ae() {
|
|
189
|
+
return k(F(), new D());
|
|
190
190
|
}
|
|
191
|
-
async function
|
|
192
|
-
let e = new
|
|
193
|
-
return e.addScope("email"), e.addScope("name"),
|
|
191
|
+
async function I() {
|
|
192
|
+
let e = new ee("apple.com");
|
|
193
|
+
return e.addScope("email"), e.addScope("name"), k(F(), e);
|
|
194
194
|
}
|
|
195
|
-
async function
|
|
196
|
-
return
|
|
195
|
+
async function oe(e, t) {
|
|
196
|
+
return te(F(), e, t);
|
|
197
197
|
}
|
|
198
|
-
async function
|
|
199
|
-
let r = await
|
|
200
|
-
return n && await
|
|
198
|
+
async function se(e, t, n) {
|
|
199
|
+
let r = await O(F(), e, t);
|
|
200
|
+
return n && await re(r.user, { displayName: n }), r;
|
|
201
201
|
}
|
|
202
|
-
async function
|
|
203
|
-
await
|
|
202
|
+
async function L() {
|
|
203
|
+
await ne(F());
|
|
204
204
|
}
|
|
205
|
-
function
|
|
206
|
-
return E.value && (
|
|
205
|
+
function R(e) {
|
|
206
|
+
return E.value && (l.value?.uid === e.authorUid || A.value);
|
|
207
207
|
}
|
|
208
|
-
function
|
|
209
|
-
return E.value && (
|
|
208
|
+
function ce(e) {
|
|
209
|
+
return E.value && (l.value?.uid === e.authorUid || A.value);
|
|
210
210
|
}
|
|
211
211
|
return {
|
|
212
|
-
user:
|
|
212
|
+
user: l,
|
|
213
213
|
isSignedIn: E,
|
|
214
|
-
isAdmin:
|
|
214
|
+
isAdmin: A,
|
|
215
215
|
rolePending: d,
|
|
216
|
-
moderationBlockCount:
|
|
217
|
-
isWriteRestricted:
|
|
218
|
-
restrictedUntilLabel:
|
|
219
|
-
restrictedMessage:
|
|
220
|
-
signInWithGoogle:
|
|
221
|
-
signInWithApple:
|
|
222
|
-
signInWithEmail:
|
|
223
|
-
signUpWithEmail:
|
|
224
|
-
signOut:
|
|
225
|
-
canEdit:
|
|
226
|
-
canDelete:
|
|
227
|
-
canDeleteComment:
|
|
228
|
-
recordContentModerationBlock:
|
|
229
|
-
refreshBoardUser:
|
|
216
|
+
moderationBlockCount: j,
|
|
217
|
+
isWriteRestricted: M,
|
|
218
|
+
restrictedUntilLabel: N,
|
|
219
|
+
restrictedMessage: P,
|
|
220
|
+
signInWithGoogle: ae,
|
|
221
|
+
signInWithApple: I,
|
|
222
|
+
signInWithEmail: oe,
|
|
223
|
+
signUpWithEmail: se,
|
|
224
|
+
signOut: L,
|
|
225
|
+
canEdit: R,
|
|
226
|
+
canDelete: R,
|
|
227
|
+
canDeleteComment: ce,
|
|
228
|
+
recordContentModerationBlock: ie,
|
|
229
|
+
refreshBoardUser: T
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
232
|
//#endregion
|
|
233
233
|
//#region src/utils/slugify.ts
|
|
234
|
-
function
|
|
234
|
+
function q(e) {
|
|
235
235
|
return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
236
236
|
}
|
|
237
237
|
function fe(e, t = 60) {
|
|
@@ -256,28 +256,53 @@ function me(e) {
|
|
|
256
256
|
return null;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
-
function
|
|
259
|
+
function J(e) {
|
|
260
260
|
return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
|
|
261
261
|
}
|
|
262
262
|
//#endregion
|
|
263
|
+
//#region src/utils/postPreview.ts
|
|
264
|
+
function Y(e) {
|
|
265
|
+
return e.replace(/&/g, "&").replace(/"/g, "\"").replace(/'/g, "'");
|
|
266
|
+
}
|
|
267
|
+
function he(e) {
|
|
268
|
+
try {
|
|
269
|
+
let t = new URL(Y(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
|
|
270
|
+
if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
|
|
271
|
+
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;
|
|
272
|
+
} catch {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function ge(e, t = []) {
|
|
277
|
+
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 ? Y(r[1]) : a, s = o ? he(o) : void 0, c = n ? Y(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
|
|
278
|
+
return {
|
|
279
|
+
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),
|
|
280
|
+
...c ? { previewImage: c } : {},
|
|
281
|
+
...o ? { videoUrl: o } : {}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
263
285
|
//#region src/composables/useMemiBoardPosts.ts
|
|
264
|
-
async function
|
|
265
|
-
let t = await
|
|
286
|
+
async function X(e) {
|
|
287
|
+
let t = await N(e).catch(() => ({
|
|
266
288
|
items: [],
|
|
267
289
|
prefixes: []
|
|
268
290
|
}));
|
|
269
|
-
await Promise.all([...t.items.map((e) =>
|
|
291
|
+
await Promise.all([...t.items.map((e) => A(e)), ...t.prefixes.map((e) => X(e))]);
|
|
270
292
|
}
|
|
271
|
-
async function
|
|
293
|
+
async function _e(e, t) {
|
|
272
294
|
let n = [...new Set([...t].filter(Boolean))];
|
|
273
|
-
await Promise.all(n.map((t) =>
|
|
295
|
+
await Promise.all(n.map((t) => A(P(e, t)).catch(() => {})));
|
|
274
296
|
}
|
|
275
|
-
function
|
|
276
|
-
let e =
|
|
277
|
-
|
|
297
|
+
function ve() {
|
|
298
|
+
let e = z(), t = c(), n = o(), r = () => e.collectionPrefix, i = () => l(t, `${r()}Posts`), a = (e) => f(t, `${r()}Posts`, e), s = (e) => f(t, `${r()}Posts`, e, "body", "main"), g = (e) => l(t, `${r()}Posts`, e, "comments");
|
|
299
|
+
function D() {
|
|
300
|
+
return f(i()).id;
|
|
301
|
+
}
|
|
302
|
+
async function ee(e = {}) {
|
|
278
303
|
let t = e.pageSize ?? 20, n = [];
|
|
279
|
-
e.category && n.push(
|
|
280
|
-
let r = await h(
|
|
304
|
+
e.category && n.push(T("category", "==", e.category)), n.push(y("createdAt", "desc")), e.cursor && n.push(C(e.cursor)), n.push(_(t + 1));
|
|
305
|
+
let r = await h(b(i(), ...n)), a = r.docs.slice(0, t);
|
|
281
306
|
return {
|
|
282
307
|
posts: a.map((e) => ({
|
|
283
308
|
id: e.id,
|
|
@@ -287,19 +312,43 @@ function ge() {
|
|
|
287
312
|
hasMore: r.docs.length > t
|
|
288
313
|
};
|
|
289
314
|
}
|
|
290
|
-
async function
|
|
291
|
-
let [t, n] = await Promise.all([m(a(e)), m(
|
|
315
|
+
async function O(e) {
|
|
316
|
+
let [t, n] = await Promise.all([m(a(e)), m(s(e))]);
|
|
292
317
|
return t.exists() ? {
|
|
293
318
|
id: t.id,
|
|
294
319
|
...t.data(),
|
|
295
320
|
content: n.exists() ? n.data().content ?? "" : ""
|
|
296
321
|
} : null;
|
|
297
322
|
}
|
|
298
|
-
async function te(e) {
|
|
299
|
-
let
|
|
300
|
-
|
|
301
|
-
|
|
323
|
+
async function te(e, t) {
|
|
324
|
+
let n = (await h(b(i(), T("category", "==", e), T("slug", "==", t), _(1)))).docs[0];
|
|
325
|
+
return n ? O(n.id) : null;
|
|
326
|
+
}
|
|
327
|
+
async function k(e) {
|
|
328
|
+
if (!e.category || !e.createdAt) return {
|
|
329
|
+
previous: null,
|
|
330
|
+
next: null
|
|
331
|
+
};
|
|
332
|
+
let t = [T("category", "==", e.category), y("createdAt", "desc")], [n, r] = await Promise.all([h(b(i(), ...t, C(e.createdAt), _(1))), h(b(i(), ...t, p(e.createdAt), v(1)))]), a = (e) => {
|
|
333
|
+
let t = e.docs[0];
|
|
334
|
+
return t ? {
|
|
335
|
+
id: t.id,
|
|
336
|
+
...t.data()
|
|
337
|
+
} : null;
|
|
338
|
+
};
|
|
339
|
+
return {
|
|
340
|
+
previous: a(n),
|
|
341
|
+
next: a(r)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function ne(e) {
|
|
345
|
+
let t = q(e.title) || "post", n = t, r = 1;
|
|
346
|
+
for (; !(await h(b(i(), T("category", "==", e.category || ""), T("slug", "==", n), _(1)))).empty;) r++, n = `${t}-${r}`;
|
|
347
|
+
let o = e.postId || D(), c = ge(e.content, e.attachments);
|
|
348
|
+
return await S(a(o), {
|
|
349
|
+
slug: n,
|
|
302
350
|
title: e.title,
|
|
351
|
+
...c,
|
|
303
352
|
tags: e.tags ?? [],
|
|
304
353
|
...e.category ? { category: e.category } : {},
|
|
305
354
|
attachments: e.attachments ?? [],
|
|
@@ -309,62 +358,69 @@ function ge() {
|
|
|
309
358
|
authorPhoto: e.authorPhoto,
|
|
310
359
|
moderationStatus: "approved",
|
|
311
360
|
moderationModel: e.moderationModel ?? null,
|
|
312
|
-
createdAt:
|
|
313
|
-
updatedAt:
|
|
314
|
-
}),
|
|
361
|
+
createdAt: x(),
|
|
362
|
+
updatedAt: x()
|
|
363
|
+
}), await S(s(o), { content: e.content }), n;
|
|
315
364
|
}
|
|
316
|
-
async function
|
|
317
|
-
|
|
365
|
+
async function re(e, t) {
|
|
366
|
+
let n = ge(t.content, t.attachments);
|
|
367
|
+
await Promise.all([w(a(e), {
|
|
318
368
|
title: t.title,
|
|
369
|
+
...n,
|
|
370
|
+
previewImage: n.previewImage ? n.previewImage : d(),
|
|
371
|
+
videoUrl: n.videoUrl ? n.videoUrl : d(),
|
|
319
372
|
tags: t.tags ?? [],
|
|
320
|
-
category: t.category ? t.category :
|
|
373
|
+
category: t.category ? t.category : d(),
|
|
321
374
|
attachments: t.attachments ?? [],
|
|
322
|
-
updatedAt:
|
|
323
|
-
}),
|
|
375
|
+
updatedAt: x()
|
|
376
|
+
}), S(s(e), { content: t.content }, { merge: !0 })]);
|
|
324
377
|
}
|
|
325
|
-
async function
|
|
326
|
-
let [i,
|
|
378
|
+
async function A(e) {
|
|
379
|
+
let [i, o] = await Promise.all([m(a(e)), m(s(e))]);
|
|
327
380
|
if (!i.exists()) return;
|
|
328
|
-
let
|
|
329
|
-
for (let e = 0; e <
|
|
330
|
-
let n =
|
|
331
|
-
|
|
381
|
+
let c = i.data(), l = o.exists() ? String(o.data().content ?? "") : "", d = await h(g(e));
|
|
382
|
+
for (let e = 0; e < d.docs.length; e += 450) {
|
|
383
|
+
let n = E(t);
|
|
384
|
+
d.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
332
385
|
}
|
|
333
|
-
let
|
|
334
|
-
for (let t of
|
|
335
|
-
|
|
336
|
-
let n =
|
|
386
|
+
let f = M(n), p = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
|
|
387
|
+
for (let t of c.attachments ?? []) if (t?.path) {
|
|
388
|
+
p.add(t.path);
|
|
389
|
+
let n = J(t.path);
|
|
337
390
|
n && n !== e && _.add(n);
|
|
338
391
|
}
|
|
339
|
-
for (let t of pe(
|
|
392
|
+
for (let t of pe(l)) {
|
|
340
393
|
let n = me(t);
|
|
341
394
|
if (!n) continue;
|
|
342
|
-
|
|
395
|
+
p.add(n);
|
|
343
396
|
let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
|
|
344
|
-
r !== n && r.includes("/images/thumbnails/") &&
|
|
397
|
+
r !== n && r.includes("/images/thumbnails/") && p.add(r);
|
|
345
398
|
let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
|
|
346
|
-
i && !n.includes("/thumbnails/") &&
|
|
347
|
-
let a =
|
|
399
|
+
i && !n.includes("/thumbnails/") && p.add(`${i[1]}thumbnails/${i[2]}.jpg`);
|
|
400
|
+
let a = J(n);
|
|
348
401
|
a && a !== e && _.add(a);
|
|
349
402
|
}
|
|
350
|
-
await
|
|
351
|
-
for (let e of _) await
|
|
352
|
-
await
|
|
403
|
+
await u(s(e)), await X(P(f, `${r()}/posts/${e}`));
|
|
404
|
+
for (let e of _) await X(P(f, `${r()}/posts/${e}`));
|
|
405
|
+
await _e(f, p), await u(a(e));
|
|
353
406
|
}
|
|
354
407
|
return {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
408
|
+
createPostId: D,
|
|
409
|
+
getPosts: ee,
|
|
410
|
+
getPost: O,
|
|
411
|
+
getPostBySlug: te,
|
|
412
|
+
getAdjacentPosts: k,
|
|
413
|
+
createPost: ne,
|
|
414
|
+
updatePost: re,
|
|
415
|
+
deletePost: A
|
|
360
416
|
};
|
|
361
417
|
}
|
|
362
418
|
//#endregion
|
|
363
419
|
//#region src/composables/useMemiBoardComments.ts
|
|
364
|
-
function
|
|
365
|
-
let n =
|
|
420
|
+
function ye(t) {
|
|
421
|
+
let n = z(), r = c(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => b(l(r, `${a()}Posts`, o.value, "comments"), y("createdAt", "asc"))), u = i(s);
|
|
366
422
|
async function d(e) {
|
|
367
|
-
let t =
|
|
423
|
+
let t = E(r), n = a(), i = f(l(r, `${n}Posts`, o.value, "comments"));
|
|
368
424
|
t.set(i, {
|
|
369
425
|
postId: o.value,
|
|
370
426
|
body: e.body,
|
|
@@ -372,22 +428,22 @@ function _e(t) {
|
|
|
372
428
|
authorName: e.authorName,
|
|
373
429
|
authorPhoto: e.authorPhoto,
|
|
374
430
|
moderationStatus: "approved",
|
|
375
|
-
createdAt:
|
|
376
|
-
}), t.update(
|
|
431
|
+
createdAt: x()
|
|
432
|
+
}), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
|
|
377
433
|
}
|
|
378
|
-
async function
|
|
379
|
-
let t =
|
|
380
|
-
t.delete(
|
|
434
|
+
async function p(e) {
|
|
435
|
+
let t = E(r), n = a();
|
|
436
|
+
t.delete(f(r, `${n}Posts`, o.value, "comments", e)), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
|
|
381
437
|
}
|
|
382
438
|
return {
|
|
383
|
-
comments:
|
|
439
|
+
comments: u,
|
|
384
440
|
addComment: d,
|
|
385
|
-
deleteComment:
|
|
441
|
+
deleteComment: p
|
|
386
442
|
};
|
|
387
443
|
}
|
|
388
444
|
//#endregion
|
|
389
445
|
//#region src/utils/moderation-prompt.ts
|
|
390
|
-
var
|
|
446
|
+
var be = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", xe = {
|
|
391
447
|
abuse: "욕설·비속어·모욕",
|
|
392
448
|
spam: "스팸·광고",
|
|
393
449
|
adult: "선정적·성인 내용",
|
|
@@ -395,17 +451,17 @@ var ve = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
|
|
|
395
451
|
other: "부적절한 내용",
|
|
396
452
|
none: ""
|
|
397
453
|
};
|
|
398
|
-
function
|
|
454
|
+
function Z(e) {
|
|
399
455
|
if (e.via === "local" && e.localHit) {
|
|
400
456
|
let t = e.localHit.trim();
|
|
401
457
|
return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
|
|
402
458
|
}
|
|
403
459
|
let t = (e.reason || "").trim();
|
|
404
460
|
if (t) return t;
|
|
405
|
-
let n = e.category && e.category !== "none" ?
|
|
461
|
+
let n = e.category && e.category !== "none" ? xe[e.category] || e.category : "";
|
|
406
462
|
return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
|
|
407
463
|
}
|
|
408
|
-
var
|
|
464
|
+
var Se = [
|
|
409
465
|
"씨발",
|
|
410
466
|
"시발",
|
|
411
467
|
"시이발",
|
|
@@ -431,10 +487,10 @@ var be = [
|
|
|
431
487
|
"bitch",
|
|
432
488
|
"asshole"
|
|
433
489
|
];
|
|
434
|
-
function
|
|
490
|
+
function Ce(e) {
|
|
435
491
|
return new RegExp(e.join("|"), "i");
|
|
436
492
|
}
|
|
437
|
-
var
|
|
493
|
+
var we = /* @__PURE__ */ new Set([
|
|
438
494
|
"none",
|
|
439
495
|
"abuse",
|
|
440
496
|
"spam",
|
|
@@ -442,7 +498,7 @@ var Se = /* @__PURE__ */ new Set([
|
|
|
442
498
|
"violence",
|
|
443
499
|
"other"
|
|
444
500
|
]);
|
|
445
|
-
function
|
|
501
|
+
function Te(e) {
|
|
446
502
|
let t = e.trim();
|
|
447
503
|
if (!t) return null;
|
|
448
504
|
let n;
|
|
@@ -460,7 +516,7 @@ function Ce(e) {
|
|
|
460
516
|
if (typeof n != "object" || !n) return null;
|
|
461
517
|
let r = n;
|
|
462
518
|
if (typeof r.flagged != "boolean") return null;
|
|
463
|
-
let i = typeof r.category == "string" &&
|
|
519
|
+
let i = typeof r.category == "string" && we.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
464
520
|
return {
|
|
465
521
|
flagged: r.flagged,
|
|
466
522
|
category: i,
|
|
@@ -469,20 +525,20 @@ function Ce(e) {
|
|
|
469
525
|
}
|
|
470
526
|
//#endregion
|
|
471
527
|
//#region src/composables/useMemiBoardModeration.ts
|
|
472
|
-
var
|
|
528
|
+
var Ee = {
|
|
473
529
|
flagged: !1,
|
|
474
530
|
category: "none",
|
|
475
531
|
reason: "",
|
|
476
532
|
via: "empty"
|
|
477
533
|
};
|
|
478
|
-
function
|
|
479
|
-
let e =
|
|
480
|
-
function
|
|
534
|
+
function De() {
|
|
535
|
+
let e = z().moderation ?? {}, t = o(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = K(), a = Ce([...Se, ...e.localBlocklist ?? []]);
|
|
536
|
+
function s(e) {
|
|
481
537
|
let t = e.match(a);
|
|
482
538
|
return t ? {
|
|
483
539
|
flagged: !0,
|
|
484
540
|
category: "abuse",
|
|
485
|
-
reason:
|
|
541
|
+
reason: Z({
|
|
486
542
|
via: "local",
|
|
487
543
|
localHit: t[0] || "",
|
|
488
544
|
category: "abuse"
|
|
@@ -491,21 +547,21 @@ function we() {
|
|
|
491
547
|
} : null;
|
|
492
548
|
}
|
|
493
549
|
function c() {
|
|
494
|
-
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i =
|
|
495
|
-
backend: new
|
|
550
|
+
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = oe(t, {
|
|
551
|
+
backend: new ae(),
|
|
496
552
|
useLimitedUseAppCheckTokens: r
|
|
497
553
|
});
|
|
498
|
-
return
|
|
554
|
+
return se(i, {
|
|
499
555
|
model: n,
|
|
500
|
-
systemInstruction:
|
|
556
|
+
systemInstruction: be,
|
|
501
557
|
generationConfig: {
|
|
502
558
|
temperature: 0,
|
|
503
559
|
maxOutputTokens: 256,
|
|
504
560
|
responseMimeType: "application/json",
|
|
505
|
-
responseSchema:
|
|
506
|
-
flagged:
|
|
507
|
-
category:
|
|
508
|
-
reason:
|
|
561
|
+
responseSchema: I.object({ properties: {
|
|
562
|
+
flagged: I.boolean(),
|
|
563
|
+
category: I.string(),
|
|
564
|
+
reason: I.string()
|
|
509
565
|
} })
|
|
510
566
|
}
|
|
511
567
|
});
|
|
@@ -526,7 +582,7 @@ function we() {
|
|
|
526
582
|
};
|
|
527
583
|
}
|
|
528
584
|
function u(e, t = "") {
|
|
529
|
-
let n =
|
|
585
|
+
let n = Z({
|
|
530
586
|
reason: e.reason,
|
|
531
587
|
category: e.category,
|
|
532
588
|
via: e.via
|
|
@@ -554,22 +610,22 @@ function we() {
|
|
|
554
610
|
};
|
|
555
611
|
let i = t.trim();
|
|
556
612
|
if (!i) return {
|
|
557
|
-
...
|
|
613
|
+
...Ee,
|
|
558
614
|
via: "empty"
|
|
559
615
|
};
|
|
560
|
-
let a =
|
|
616
|
+
let a = s(i);
|
|
561
617
|
if (a) return d(a);
|
|
562
618
|
if (e.enabled === !1) return {
|
|
563
|
-
...
|
|
619
|
+
...Ee,
|
|
564
620
|
via: "disabled"
|
|
565
621
|
};
|
|
566
622
|
try {
|
|
567
|
-
let e =
|
|
623
|
+
let e = Te((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
|
|
568
624
|
if (!e) return l();
|
|
569
625
|
let t = {
|
|
570
626
|
flagged: e.flagged,
|
|
571
627
|
category: e.category,
|
|
572
|
-
reason:
|
|
628
|
+
reason: Z({
|
|
573
629
|
reason: e.reason,
|
|
574
630
|
category: e.category,
|
|
575
631
|
via: "ai"
|
|
@@ -589,14 +645,14 @@ function we() {
|
|
|
589
645
|
via: "restricted"
|
|
590
646
|
};
|
|
591
647
|
if (e.enabled === !1 || !e.moderateImages) return {
|
|
592
|
-
...
|
|
648
|
+
...Ee,
|
|
593
649
|
via: "disabled"
|
|
594
650
|
};
|
|
595
651
|
try {
|
|
596
652
|
let e = await new Promise((e, n) => {
|
|
597
653
|
let r = new FileReader();
|
|
598
654
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
599
|
-
}), n =
|
|
655
|
+
}), n = Te((await c().generateContent([{ inlineData: {
|
|
600
656
|
mimeType: t.type || "image/jpeg",
|
|
601
657
|
data: e
|
|
602
658
|
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
@@ -604,7 +660,7 @@ function we() {
|
|
|
604
660
|
let r = {
|
|
605
661
|
flagged: n.flagged,
|
|
606
662
|
category: n.category,
|
|
607
|
-
reason:
|
|
663
|
+
reason: Z({
|
|
608
664
|
reason: n.reason,
|
|
609
665
|
category: n.category,
|
|
610
666
|
via: "ai"
|
|
@@ -623,7 +679,7 @@ function we() {
|
|
|
623
679
|
}
|
|
624
680
|
//#endregion
|
|
625
681
|
//#region src/utils/compressImage.ts
|
|
626
|
-
async function
|
|
682
|
+
async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
627
683
|
return new Promise((r, i) => {
|
|
628
684
|
let a = new Image(), o = URL.createObjectURL(e);
|
|
629
685
|
a.onload = () => {
|
|
@@ -643,11 +699,11 @@ async function Te(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
|
643
699
|
}
|
|
644
700
|
//#endregion
|
|
645
701
|
//#region src/composables/useMemiBoardStorage.ts
|
|
646
|
-
var
|
|
647
|
-
function
|
|
648
|
-
let e =
|
|
702
|
+
var ke = 5242880;
|
|
703
|
+
function Ae() {
|
|
704
|
+
let e = z(), t = o(), n = () => e.collectionPrefix;
|
|
649
705
|
function r(e, r, i) {
|
|
650
|
-
let a =
|
|
706
|
+
let a = M(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = P(a, o), c = F(s, e, {
|
|
651
707
|
contentType: e.type || "application/octet-stream",
|
|
652
708
|
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
|
|
653
709
|
});
|
|
@@ -656,7 +712,7 @@ function De() {
|
|
|
656
712
|
c.on("state_changed", (e) => {
|
|
657
713
|
i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
|
|
658
714
|
}, n, async () => {
|
|
659
|
-
let n = await
|
|
715
|
+
let n = await j(s);
|
|
660
716
|
t({
|
|
661
717
|
name: e.name,
|
|
662
718
|
url: n,
|
|
@@ -670,111 +726,150 @@ function De() {
|
|
|
670
726
|
};
|
|
671
727
|
}
|
|
672
728
|
async function i(e) {
|
|
673
|
-
let n =
|
|
674
|
-
await
|
|
729
|
+
let n = M(t);
|
|
730
|
+
await A(P(n, e.path)).catch(() => {});
|
|
675
731
|
}
|
|
676
732
|
async function a(e, r) {
|
|
677
733
|
if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
|
|
678
|
-
if (e.size > 5242880) throw Error(`이미지는 ${
|
|
679
|
-
let i =
|
|
680
|
-
await
|
|
681
|
-
let d = await
|
|
734
|
+
if (e.size > 5242880) throw Error(`이미지는 ${ke / 1024 / 1024}MB 이하여야 합니다.`);
|
|
735
|
+
let i = M(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 = P(i, l);
|
|
736
|
+
await ie(u, e, { contentType: e.type || "image/png" });
|
|
737
|
+
let d = await j(u), f = await Oe(e, {
|
|
682
738
|
maxWidth: 400,
|
|
683
739
|
quality: .8
|
|
684
|
-
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m =
|
|
685
|
-
return await
|
|
740
|
+
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = P(i, p);
|
|
741
|
+
return await ie(m, f, { contentType: "image/jpeg" }), {
|
|
686
742
|
originalUrl: d,
|
|
687
743
|
originalPath: l,
|
|
688
|
-
thumbnailUrl: await
|
|
744
|
+
thumbnailUrl: await j(m),
|
|
689
745
|
thumbnailPath: p
|
|
690
746
|
};
|
|
691
747
|
}
|
|
692
|
-
async function
|
|
693
|
-
let n =
|
|
694
|
-
await Promise.all([
|
|
748
|
+
async function s(e) {
|
|
749
|
+
let n = M(t);
|
|
750
|
+
await Promise.all([A(P(n, e.originalPath)).catch(() => {}), A(P(n, e.thumbnailPath)).catch(() => {})]);
|
|
695
751
|
}
|
|
696
752
|
return {
|
|
697
753
|
uploadAttachment: r,
|
|
698
754
|
deleteAttachment: i,
|
|
699
755
|
uploadEditorImage: a,
|
|
700
|
-
deleteEditorImage:
|
|
756
|
+
deleteEditorImage: s
|
|
701
757
|
};
|
|
702
758
|
}
|
|
703
759
|
//#endregion
|
|
704
760
|
//#region src/composables/useMemiBoardSettings.ts
|
|
705
|
-
var
|
|
761
|
+
var je = [
|
|
706
762
|
{
|
|
707
763
|
id: "free",
|
|
708
|
-
label: "자유"
|
|
764
|
+
label: "자유",
|
|
765
|
+
listView: "default",
|
|
766
|
+
order: 0
|
|
709
767
|
},
|
|
710
768
|
{
|
|
711
769
|
id: "notice",
|
|
712
|
-
label: "공지"
|
|
770
|
+
label: "공지",
|
|
771
|
+
listView: "default",
|
|
772
|
+
order: 1
|
|
713
773
|
},
|
|
714
774
|
{
|
|
715
775
|
id: "question",
|
|
716
|
-
label: "질문"
|
|
776
|
+
label: "질문",
|
|
777
|
+
listView: "default",
|
|
778
|
+
order: 2
|
|
717
779
|
}
|
|
718
780
|
];
|
|
719
|
-
function
|
|
720
|
-
let t =
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
!
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
})
|
|
734
|
-
|
|
735
|
-
|
|
781
|
+
function Me() {
|
|
782
|
+
let t = z(), n = c(), { isSignedIn: r } = K(), a = e(() => f(n, `${t.collectionPrefix}Settings`, "config")), o = e(() => l(n, `${t.collectionPrefix}Settings`, "config", "categories")), s = e(() => b(o.value, y("order", "asc"))), { data: d, pending: p } = i(s), m = e(() => d.value.map((e, t) => ({
|
|
783
|
+
...e,
|
|
784
|
+
id: e.id,
|
|
785
|
+
order: typeof e.order == "number" ? e.order : t,
|
|
786
|
+
listView: e.listView ?? "default"
|
|
787
|
+
})));
|
|
788
|
+
function h(e) {
|
|
789
|
+
if (e) return m.value.find((t) => t.id === e)?.label ?? e;
|
|
790
|
+
}
|
|
791
|
+
async function g(e, t = e.order ?? 0) {
|
|
792
|
+
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
793
|
+
let n = e.id.trim(), i = e.label.trim();
|
|
794
|
+
if (!n || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
|
|
795
|
+
await S(a.value, { updatedAt: x() }, { merge: !0 }), await S(f(o.value, n), {
|
|
796
|
+
label: i,
|
|
797
|
+
listView: e.listView ?? "default",
|
|
798
|
+
order: t,
|
|
799
|
+
updatedAt: x()
|
|
800
|
+
}, { merge: !0 });
|
|
801
|
+
}
|
|
802
|
+
async function _(e) {
|
|
803
|
+
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
804
|
+
let t = E(n);
|
|
805
|
+
t.set(a.value, { updatedAt: x() }, { merge: !0 }), e.forEach((e, n) => {
|
|
806
|
+
let r = e.id.trim(), i = e.label.trim();
|
|
807
|
+
if (!r || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
|
|
808
|
+
t.set(f(o.value, r), {
|
|
809
|
+
label: i,
|
|
810
|
+
listView: e.listView ?? "default",
|
|
811
|
+
order: n,
|
|
812
|
+
updatedAt: x()
|
|
813
|
+
}, { merge: !0 });
|
|
814
|
+
}), await t.commit();
|
|
815
|
+
}
|
|
816
|
+
async function v(e) {
|
|
817
|
+
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
818
|
+
await u(f(o.value, e));
|
|
819
|
+
}
|
|
820
|
+
async function C() {
|
|
821
|
+
return Promise.resolve();
|
|
822
|
+
}
|
|
823
|
+
async function w(e) {
|
|
736
824
|
let t = e.trim();
|
|
737
825
|
if (!t) throw Error("카테고리 이름을 입력해 주세요.");
|
|
738
|
-
if (!
|
|
739
|
-
|
|
740
|
-
let n = d.value.find((e) => e.label === t || e.id === K(t));
|
|
826
|
+
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
827
|
+
let n = m.value.find((e) => e.label === t);
|
|
741
828
|
if (n) return n.id;
|
|
742
|
-
let
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
a,
|
|
762
|
-
i,
|
|
763
|
-
u
|
|
764
|
-
], async ([e, t, n]) => {
|
|
765
|
-
n || c.value || !e && !t || await h().catch(() => {});
|
|
766
|
-
}, { immediate: !0 }), {
|
|
767
|
-
categories: d,
|
|
768
|
-
categoryLabel: f,
|
|
769
|
-
settingsPending: u,
|
|
770
|
-
ensureSettings: h,
|
|
771
|
-
addCategory: g,
|
|
772
|
-
DEFAULT_CATEGORIES: Z
|
|
829
|
+
let i = q(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
|
|
830
|
+
for (; s.has(a);) a = `${i}-${o++}`;
|
|
831
|
+
return await g({
|
|
832
|
+
id: a,
|
|
833
|
+
label: t,
|
|
834
|
+
listView: "default",
|
|
835
|
+
order: m.value.length
|
|
836
|
+
}), a;
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
categories: m,
|
|
840
|
+
categoryLabel: h,
|
|
841
|
+
settingsPending: p,
|
|
842
|
+
ensureSettings: C,
|
|
843
|
+
addCategory: w,
|
|
844
|
+
saveCategory: g,
|
|
845
|
+
saveCategories: _,
|
|
846
|
+
deleteCategory: v,
|
|
847
|
+
DEFAULT_CATEGORIES: je
|
|
773
848
|
};
|
|
774
849
|
}
|
|
775
850
|
//#endregion
|
|
776
851
|
//#region src/data/versionHistory.ts
|
|
777
|
-
var
|
|
852
|
+
var Ne = [
|
|
853
|
+
{
|
|
854
|
+
version: "0.5.0",
|
|
855
|
+
date: "2026-08-04",
|
|
856
|
+
highlights: [
|
|
857
|
+
"게시판마다 일반·이미지·영상 목록 화면을 선택할 수 있어요",
|
|
858
|
+
"YouTube 링크를 붙여넣으면 본문에 영상으로 표시돼요",
|
|
859
|
+
"게시글 아래에서 이전 글·목록·다음 글로 이동할 수 있어요",
|
|
860
|
+
"관리자가 카테고리를 각각 저장하고 관리할 수 있어요"
|
|
861
|
+
]
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
version: "0.4.15",
|
|
865
|
+
date: "2026-08-03",
|
|
866
|
+
highlights: ["설정 화면이 설치된 프로젝트에서 정상적으로 열리도록 고쳤어요"]
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
version: "0.4.14",
|
|
870
|
+
date: "2026-08-03",
|
|
871
|
+
highlights: ["관리자가 카테고리 이름과 순서를 한 화면에서 관리할 수 있어요", "카테고리마다 일반·이미지·영상 목록 방식을 선택할 수 있어요"]
|
|
872
|
+
},
|
|
778
873
|
{
|
|
779
874
|
version: "0.2.2",
|
|
780
875
|
date: "2026-07-31",
|
|
@@ -801,7 +896,7 @@ var ke = [
|
|
|
801
896
|
];
|
|
802
897
|
//#endregion
|
|
803
898
|
//#region src/utils/formatDate.ts
|
|
804
|
-
function
|
|
899
|
+
function Pe(e) {
|
|
805
900
|
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
806
901
|
}
|
|
807
902
|
//#endregion
|
|
@@ -813,7 +908,7 @@ function $(e) {
|
|
|
813
908
|
let t = e;
|
|
814
909
|
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;
|
|
815
910
|
}
|
|
816
|
-
function
|
|
911
|
+
function Fe(e) {
|
|
817
912
|
if (!e?.trim()) return "";
|
|
818
913
|
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
819
914
|
function c() {
|
|
@@ -878,6 +973,6 @@ function je(e) {
|
|
|
878
973
|
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");
|
|
879
974
|
}
|
|
880
975
|
//#endregion
|
|
881
|
-
export {
|
|
976
|
+
export { B as DEFAULT_BLOCK_BAN_DECAY_MS, le as DEFAULT_BLOCK_BAN_THRESHOLD, je as DEFAULT_CATEGORIES, ke as EDITOR_IMAGE_MAX_BYTES, Oe as compressImage, ce as configureMemiBoard, V as effectiveModerationBlockCount, pe as extractEditorImageUrls, Pe as formatDate, W as formatRestrictedUntilLabel, U as isModerationWriteRestricted, H as moderationWriteRestrictedUntilMs, J as postNamespaceFromStoragePath, Fe as renderMarkdownToHtml, q as slugify, me as storagePathFromDownloadUrl, K as useMemiBoardAuth, ye as useMemiBoardComments, z as useMemiBoardConfig, De as useMemiBoardModeration, ve as useMemiBoardPosts, Me as useMemiBoardSettings, Ae as useMemiBoardStorage, Ne as versionHistory };
|
|
882
977
|
|
|
883
978
|
//# sourceMappingURL=index.js.map
|