memi-board 0.39.1 → 0.41.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 -0
- package/dist/composables/useMemiBoardWritingAssistant.d.ts +3 -1
- package/dist/index.js +47 -31
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/Editor.vue +175 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -467,6 +467,12 @@ const router = useRouter()
|
|
|
467
467
|
콘텐츠 차단이 누적되면 `memiBoardUsers/{uid}` 에 `moderationBlockCount` / `moderationBlockAt` 이 기록된다.
|
|
468
468
|
10회면 약 하루 동안 작성 불가(24h마다 1 차감). Rules 예시에서 본인은 **+1만** 가능하고 감소·리셋은 admin.
|
|
469
469
|
|
|
470
|
+
### 관리자 검열 우회
|
|
471
|
+
|
|
472
|
+
글쓰기 폼(`Editor.vue`) 하단, 제출 버튼 위에 `isAdmin`인 사용자에게만 "비속어 필터 무시" 스위치가 보인다. 켜고 제출하면 로컬 금칙어·AI 검열을 모두 건너뛰고 저장한다(`via: 'admin-override'`, 경고 카운트도 쌓이지 않음). 댓글 폼(`CommentForm.vue`)에는 없다.
|
|
473
|
+
|
|
474
|
+
직접 호출할 때는 `useMemiBoardModeration().checkText(text, { skipFilter: true })`로 우회한다. `skipFilter`는 호스트가 넘긴 값 그대로 신뢰하므로, admin 여부 판정은 호출부(호스트)의 책임이다.
|
|
475
|
+
|
|
470
476
|
### AI 글쓰기 도우미 사용 제한
|
|
471
477
|
|
|
472
478
|
에디터의 AI 글쓰기 도우미는 로그인 사용자별 24시간 구간에 최대 **10회** 사용할 수 있다.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export declare const WRITING_ASSISTANT_DAILY_LIMIT = 10;
|
|
2
|
-
export type WritingAssistantAction = 'proofread' | 'polish' | 'continue' | 'summarize' | 'translate-en' | 'translate-ko' | 'title';
|
|
2
|
+
export type WritingAssistantAction = 'proofread' | 'polish' | 'continue' | 'summarize' | 'translate-en' | 'translate-ko' | 'title' | 'custom';
|
|
3
3
|
interface WritingAssistantInput {
|
|
4
4
|
action: WritingAssistantAction;
|
|
5
5
|
content: string;
|
|
6
6
|
title?: string;
|
|
7
7
|
selection?: boolean;
|
|
8
|
+
/** action이 'custom'일 때 사용자가 직접 입력한 지시문 (예: "경상도 사투리 스타일로") */
|
|
9
|
+
customInstruction?: string;
|
|
8
10
|
}
|
|
9
11
|
/** 검열과 동일한 Firebase AI Logic 연결을 사용하는 글쓰기 도우미. */
|
|
10
12
|
export declare function useMemiBoardWritingAssistant(): {
|
package/dist/index.js
CHANGED
|
@@ -2256,6 +2256,16 @@ function Gn(e) {
|
|
|
2256
2256
|
//#endregion
|
|
2257
2257
|
//#region src/data/versionHistory.ts
|
|
2258
2258
|
var Kn = [
|
|
2259
|
+
{
|
|
2260
|
+
version: "0.41.0",
|
|
2261
|
+
date: "2026-08-27",
|
|
2262
|
+
highlights: ["AI 도우미 결과를 바로 적용하지 않고 이전/이후를 비교해보고 적용할 수 있어요"]
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
version: "0.40.0",
|
|
2266
|
+
date: "2026-08-27",
|
|
2267
|
+
highlights: ["AI 글쓰기 도우미에서 원하는 문체를 직접 입력해 바꿀 수 있어요 (예: 경상도 사투리 스타일로)"]
|
|
2268
|
+
},
|
|
2259
2269
|
{
|
|
2260
2270
|
version: "0.39.1",
|
|
2261
2271
|
date: "2026-08-27",
|
|
@@ -2975,7 +2985,7 @@ function tr(e, t) {
|
|
|
2975
2985
|
}
|
|
2976
2986
|
//#endregion
|
|
2977
2987
|
//#region src/composables/useMemiBoardWritingAssistant.ts
|
|
2978
|
-
var nr = 10, rr = 864e5, ir = {
|
|
2988
|
+
var nr = 10, rr = 864e5, ir = 200, ar = {
|
|
2979
2989
|
proofread: "맞춤법, 띄어쓰기, 문법 오류만 고쳐라. 의미와 말투는 바꾸지 마라.",
|
|
2980
2990
|
polish: "뜻과 작성자의 말투를 유지하면서 더 자연스럽고 읽기 좋은 문장으로 다듬어라.",
|
|
2981
2991
|
continue: "앞의 맥락과 말투에 맞는 내용을 과장 없이 한두 문단 이어서 작성하라.",
|
|
@@ -2984,13 +2994,19 @@ var nr = 10, rr = 864e5, ir = {
|
|
|
2984
2994
|
"translate-ko": "자연스러운 한국어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.",
|
|
2985
2995
|
title: "본문을 대표하는 간결한 한국어 제목 하나를 만들어라. 따옴표나 설명 없이 제목만 출력하라."
|
|
2986
2996
|
};
|
|
2987
|
-
function
|
|
2997
|
+
function or(e) {
|
|
2998
|
+
if (e.action !== "custom") return ar[e.action];
|
|
2999
|
+
let t = e.customInstruction?.trim().slice(0, ir);
|
|
3000
|
+
if (!t) throw Error("어떻게 바꿀지 지시문을 입력해 주세요.");
|
|
3001
|
+
return `뜻은 유지하면서 다음 스타일 지시를 반영해 다시 써라: ${t}`;
|
|
3002
|
+
}
|
|
3003
|
+
function sr(e) {
|
|
2988
3004
|
return e.trim().replace(/^```(?:html|text)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
2989
3005
|
}
|
|
2990
|
-
function
|
|
3006
|
+
function cr(e) {
|
|
2991
3007
|
return !e || typeof e != "object" ? null : "toMillis" in e && typeof e.toMillis == "function" ? e.toMillis() : null;
|
|
2992
3008
|
}
|
|
2993
|
-
function
|
|
3009
|
+
function lr() {
|
|
2994
3010
|
let e = le(), t = Q(), n = Z(), r = u();
|
|
2995
3011
|
async function i() {
|
|
2996
3012
|
let e = n.value?.uid;
|
|
@@ -2998,7 +3014,7 @@ function sr() {
|
|
|
2998
3014
|
await oe(t, async (n) => {
|
|
2999
3015
|
let r = x(t, _(), e), i = await n.get(r);
|
|
3000
3016
|
if (!i.exists()) throw Error("사용자 정보를 확인한 뒤 다시 시도해 주세요.");
|
|
3001
|
-
let a = i.data(), o =
|
|
3017
|
+
let a = i.data(), o = cr(a.aiWritingWindowAt), s = Math.max(0, Math.floor(Number(a.aiWritingCount) || 0)), c = Date.now(), l = o != null && c - o < rr;
|
|
3002
3018
|
if (l && s >= 10) {
|
|
3003
3019
|
let e = o + rr, t = Math.max(1, Math.ceil((e - c) / 36e5));
|
|
3004
3020
|
throw Error(`AI 글쓰기 도우미는 하루 10회까지 사용할 수 있습니다. 약 ${t}시간 후 다시 이용해 주세요.`);
|
|
@@ -3011,43 +3027,43 @@ function sr() {
|
|
|
3011
3027
|
});
|
|
3012
3028
|
}
|
|
3013
3029
|
async function a(t) {
|
|
3030
|
+
let n = or(t);
|
|
3031
|
+
if (!t.content.trim()) throw Error("AI로 다듬을 내용을 먼저 입력해 주세요.");
|
|
3014
3032
|
await i();
|
|
3015
|
-
let
|
|
3033
|
+
let a = r.moderation ?? {}, o = we(e, {
|
|
3016
3034
|
backend: new Se(),
|
|
3017
|
-
useLimitedUseAppCheckTokens:
|
|
3018
|
-
}),
|
|
3019
|
-
model:
|
|
3035
|
+
useLimitedUseAppCheckTokens: a.useLimitedUseAppCheckTokens !== !1
|
|
3036
|
+
}), s = Te(o, {
|
|
3037
|
+
model: a.model ?? "gemini-3.5-flash-lite",
|
|
3020
3038
|
systemInstruction: [
|
|
3021
3039
|
"너는 게시판 글쓰기 편집 도우미다.",
|
|
3022
3040
|
"사용자가 요청한 편집 결과만 출력하고 설명, 인사, 마크다운 코드 펜스를 붙이지 마라.",
|
|
3023
3041
|
t.action === "title" || t.selection ? "출력은 일반 텍스트로 작성하라." : "입력의 HTML 구조와 링크, 이미지 태그를 보존하고 결과도 HTML 조각으로 출력하라."
|
|
3024
3042
|
].join("\n"),
|
|
3025
3043
|
generationConfig: {
|
|
3026
|
-
temperature: t.action === "continue" || t.action === "title" ? .6 : .2,
|
|
3044
|
+
temperature: t.action === "continue" || t.action === "title" || t.action === "custom" ? .6 : .2,
|
|
3027
3045
|
maxOutputTokens: 2048
|
|
3028
3046
|
}
|
|
3029
|
-
}),
|
|
3030
|
-
|
|
3031
|
-
let c = [
|
|
3032
|
-
`작업: ${ir[t.action]}`,
|
|
3047
|
+
}), c = t.content.trim(), l = [
|
|
3048
|
+
`작업: ${n}`,
|
|
3033
3049
|
t.title ? `현재 제목: ${t.title}` : "",
|
|
3034
|
-
`본문:\n${
|
|
3035
|
-
].filter(Boolean).join("\n\n"),
|
|
3036
|
-
if (!
|
|
3037
|
-
return
|
|
3050
|
+
`본문:\n${c.slice(0, 12e3)}`
|
|
3051
|
+
].filter(Boolean).join("\n\n"), u = sr((await s.generateContent(l)).response.text());
|
|
3052
|
+
if (!u) throw Error("AI가 결과를 만들지 못했습니다. 다시 시도해 주세요.");
|
|
3053
|
+
return u;
|
|
3038
3054
|
}
|
|
3039
3055
|
return { assist: a };
|
|
3040
3056
|
}
|
|
3041
3057
|
//#endregion
|
|
3042
3058
|
//#region src/utils/renderMarkdown.ts
|
|
3043
|
-
function
|
|
3059
|
+
function ur(e) {
|
|
3044
3060
|
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3045
3061
|
}
|
|
3046
|
-
function
|
|
3062
|
+
function dr(e) {
|
|
3047
3063
|
let t = e;
|
|
3048
3064
|
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;
|
|
3049
3065
|
}
|
|
3050
|
-
function
|
|
3066
|
+
function fr(e) {
|
|
3051
3067
|
if (!e?.trim()) return "";
|
|
3052
3068
|
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
3053
3069
|
function c() {
|
|
@@ -3056,7 +3072,7 @@ function ur(e) {
|
|
|
3056
3072
|
return;
|
|
3057
3073
|
}
|
|
3058
3074
|
let e = o;
|
|
3059
|
-
n.push(`<${e}>${s.map((e) => `<li>${
|
|
3075
|
+
n.push(`<${e}>${s.map((e) => `<li>${dr(e)}</li>`).join("")}</${e}>`), o = null, s = [];
|
|
3060
3076
|
}
|
|
3061
3077
|
for (; r < t.length;) {
|
|
3062
3078
|
let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
|
|
@@ -3065,7 +3081,7 @@ function ur(e) {
|
|
|
3065
3081
|
continue;
|
|
3066
3082
|
}
|
|
3067
3083
|
if (i) {
|
|
3068
|
-
a.push(
|
|
3084
|
+
a.push(ur(e)), r++;
|
|
3069
3085
|
continue;
|
|
3070
3086
|
}
|
|
3071
3087
|
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(e)) {
|
|
@@ -3076,24 +3092,24 @@ function ur(e) {
|
|
|
3076
3092
|
if (u) {
|
|
3077
3093
|
c();
|
|
3078
3094
|
let e = u[1].length;
|
|
3079
|
-
n.push(`<h${e} class="font-semibold mt-4 mb-2">${
|
|
3095
|
+
n.push(`<h${e} class="font-semibold mt-4 mb-2">${dr(ur(u[2]))}</h${e}>`), r++;
|
|
3080
3096
|
continue;
|
|
3081
3097
|
}
|
|
3082
3098
|
if (e.startsWith("> ")) {
|
|
3083
3099
|
c();
|
|
3084
3100
|
let e = [];
|
|
3085
|
-
for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(
|
|
3086
|
-
n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${
|
|
3101
|
+
for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(ur((t[r] ?? "").slice(2))), r++;
|
|
3102
|
+
n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${dr(e.join("<br />"))}</blockquote>`);
|
|
3087
3103
|
continue;
|
|
3088
3104
|
}
|
|
3089
3105
|
let d = e.match(/^[-*+]\s+(.+)$/);
|
|
3090
3106
|
if (d) {
|
|
3091
|
-
o && o !== "ul" && c(), o = "ul", s.push(
|
|
3107
|
+
o && o !== "ul" && c(), o = "ul", s.push(ur(d[1])), r++;
|
|
3092
3108
|
continue;
|
|
3093
3109
|
}
|
|
3094
3110
|
let f = e.match(/^\d+\.\s+(.+)$/);
|
|
3095
3111
|
if (f) {
|
|
3096
|
-
o && o !== "ol" && c(), o = "ol", s.push(
|
|
3112
|
+
o && o !== "ol" && c(), o = "ol", s.push(ur(f[1])), r++;
|
|
3097
3113
|
continue;
|
|
3098
3114
|
}
|
|
3099
3115
|
if (!e.trim()) {
|
|
@@ -3105,13 +3121,13 @@ function ur(e) {
|
|
|
3105
3121
|
for (; r < t.length;) {
|
|
3106
3122
|
let e = t[r] ?? "";
|
|
3107
3123
|
if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
|
|
3108
|
-
p.push(
|
|
3124
|
+
p.push(ur(e)), r++;
|
|
3109
3125
|
}
|
|
3110
|
-
n.push(`<p class="my-2 leading-relaxed">${
|
|
3126
|
+
n.push(`<p class="my-2 leading-relaxed">${dr(p.join("<br />"))}</p>`);
|
|
3111
3127
|
}
|
|
3112
3128
|
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");
|
|
3113
3129
|
}
|
|
3114
3130
|
//#endregion
|
|
3115
|
-
export { Bt as AUTHOR_MEMO_MAX_LENGTH, vn as BOARD_SECTION_COLS, yn as BOARD_SECTION_KINDS, bn as BOARD_SECTION_SORTS, hn as BOARD_USER_ROLES, Gt as COMMENT_BODY_MAX_LENGTH, qe as DEFAULT_BLOCK_BAN_DECAY_MS, Ke as DEFAULT_BLOCK_BAN_THRESHOLD, Ct as DEFAULT_BOARDS, wt as DEFAULT_CATEGORIES, C as DEFAULT_COMMENTS_COLLECTION, E as DEFAULT_LIKES_COLLECTION, f as DEFAULT_POSTS_COLLECTION, b as DEFAULT_REPORTS_COLLECTION, D as DEFAULT_SETTINGS_COLLECTION, p as DEFAULT_USERS_COLLECTION, Qt as REPORT_DETAIL_MAX_LENGTH, $t as REPORT_REASONS, nr as WRITING_ASSISTANT_DAILY_LIMIT, Ee as asHttpUrl, Me as boardListOgDescription, je as boardListOgTitle, Ae as boardPostOgDescription, ke as boardPostOgTitle, w as boardSsrKey, x as boardUserDoc, h as boardUsersCol, St as buildPostPreview, Ve as canManageBoardByRole, He as canWriteCommentByRole, Sn as clampSectionCount, l as commentDoc, O as commentsCol, T as configureMemiBoard, xn as defaultSectionCount, Je as effectiveModerationBlockCount, nt as extractEditorImageUrls, Re as fetchPublicListForSeo, Le as fetchPublicPostForSeo, Qn as formatDate, er as formatFullDate, $n as formatRelativeDate, Ze as formatRestrictedUntilLabel, tr as formatTimestampDetails, _t as hasBodyImage, gt as hasBodyText, Be as isAssignedStaff, vt as isContentEmpty, Ot as isFirestorePermissionDenied, Xe as isModerationWriteRestricted, j as likeDoc, d as likeDocId, A as likesCol, Xt as memiBoardCommentLikesKey, Nn as miniBoardView, On as miniCommentLink, Dn as miniMoreLink, kn as miniPostImage, En as miniPostLink, An as miniPostTitle, Pn as miniPostsView, jn as miniViewGroup, Mn as miniViewLabel, Ye as moderationWriteRestrictedUntilMs, Ne as normalizeBasePath, Hn as normalizeBoardSection, Cn as pairSectionCols, mt as plainTextFromHtml, s as postBodyDoc, t as postDoc, it as postNamespaceFromStoragePath, k as postStorageFolder, c as postsCol,
|
|
3131
|
+
export { Bt as AUTHOR_MEMO_MAX_LENGTH, vn as BOARD_SECTION_COLS, yn as BOARD_SECTION_KINDS, bn as BOARD_SECTION_SORTS, hn as BOARD_USER_ROLES, Gt as COMMENT_BODY_MAX_LENGTH, qe as DEFAULT_BLOCK_BAN_DECAY_MS, Ke as DEFAULT_BLOCK_BAN_THRESHOLD, Ct as DEFAULT_BOARDS, wt as DEFAULT_CATEGORIES, C as DEFAULT_COMMENTS_COLLECTION, E as DEFAULT_LIKES_COLLECTION, f as DEFAULT_POSTS_COLLECTION, b as DEFAULT_REPORTS_COLLECTION, D as DEFAULT_SETTINGS_COLLECTION, p as DEFAULT_USERS_COLLECTION, Qt as REPORT_DETAIL_MAX_LENGTH, $t as REPORT_REASONS, nr as WRITING_ASSISTANT_DAILY_LIMIT, Ee as asHttpUrl, Me as boardListOgDescription, je as boardListOgTitle, Ae as boardPostOgDescription, ke as boardPostOgTitle, w as boardSsrKey, x as boardUserDoc, h as boardUsersCol, St as buildPostPreview, Ve as canManageBoardByRole, He as canWriteCommentByRole, Sn as clampSectionCount, l as commentDoc, O as commentsCol, T as configureMemiBoard, xn as defaultSectionCount, Je as effectiveModerationBlockCount, nt as extractEditorImageUrls, Re as fetchPublicListForSeo, Le as fetchPublicPostForSeo, Qn as formatDate, er as formatFullDate, $n as formatRelativeDate, Ze as formatRestrictedUntilLabel, tr as formatTimestampDetails, _t as hasBodyImage, gt as hasBodyText, Be as isAssignedStaff, vt as isContentEmpty, Ot as isFirestorePermissionDenied, Xe as isModerationWriteRestricted, j as likeDoc, d as likeDocId, A as likesCol, Xt as memiBoardCommentLikesKey, Nn as miniBoardView, On as miniCommentLink, Dn as miniMoreLink, kn as miniPostImage, En as miniPostLink, An as miniPostTitle, Pn as miniPostsView, jn as miniViewGroup, Mn as miniViewLabel, Ye as moderationWriteRestrictedUntilMs, Ne as normalizeBasePath, Hn as normalizeBoardSection, Cn as pairSectionCols, mt as plainTextFromHtml, s as postBodyDoc, t as postDoc, it as postNamespaceFromStoragePath, k as postStorageFolder, c as postsCol, fr as renderMarkdownToHtml, r as reportDoc, n as reportDocId, en as reportReasonLabel, o as reportsCol, y as resolveBoardPathConfig, Pe as resolvePublicSeoDb, wn as sectionColClass, Tn as sectionMinHeight, e as settingsCol, v as settingsDoc, S as slugify, rt as storagePathFromDownloadUrl, ht as titleFromBody, Oe as toAbsoluteUrl, De as toBoardOgImageUrl, _ as useBoardPathConfig, tt as useMemiBoardAuth, Ht as useMemiBoardAuthorMemo, zt as useMemiBoardCommentFeed, Zt as useMemiBoardCommentLikes, qt as useMemiBoardComments, u as useMemiBoardConfig, Yt as useMemiBoardLikes, Wn as useMemiBoardMiniLatest, Gn as useMemiBoardMiniPicked, mn as useMemiBoardModeration, Nt as useMemiBoardPost, Mt as useMemiBoardPostList, At as useMemiBoardPosts, Jt as useMemiBoardReplies, rn as useMemiBoardReportQueue, tn as useMemiBoardReports, Un as useMemiBoardSections, Dt as useMemiBoardSettings, Lt as useMemiBoardUserComments, Ft as useMemiBoardUserPosts, _n as useMemiBoardUserProfile, gn as useMemiBoardUsers, an as useMemiBoardViews, lr as useMemiBoardWritingAssistant, Kn as versionHistory, xt as videoListCoverUrl, bt as youtubeId };
|
|
3116
3132
|
|
|
3117
3133
|
//# sourceMappingURL=index.js.map
|