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