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 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 ar(e) {
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 or(e) {
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 sr() {
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 = or(a.aiWritingWindowAt), s = Math.max(0, Math.floor(Number(a.aiWritingCount) || 0)), c = Date.now(), l = o != null && c - o < rr;
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 n = r.moderation ?? {}, a = we(e, {
3033
+ let a = r.moderation ?? {}, o = we(e, {
3016
3034
  backend: new Se(),
3017
- useLimitedUseAppCheckTokens: n.useLimitedUseAppCheckTokens !== !1
3018
- }), o = Te(a, {
3019
- model: n.model ?? "gemini-3.5-flash-lite",
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
- }), s = t.content.trim();
3030
- if (!s) throw Error("AI로 다듬을 내용을 먼저 입력해 주세요.");
3031
- let c = [
3032
- `작업: ${ir[t.action]}`,
3047
+ }), c = t.content.trim(), l = [
3048
+ `작업: ${n}`,
3033
3049
  t.title ? `현재 제목: ${t.title}` : "",
3034
- `본문:\n${s.slice(0, 12e3)}`
3035
- ].filter(Boolean).join("\n\n"), l = ar((await o.generateContent(c)).response.text());
3036
- if (!l) throw Error("AI가 결과를 만들지 못했습니다. 다시 시도해 주세요.");
3037
- return l;
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 cr(e) {
3059
+ function ur(e) {
3044
3060
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3045
3061
  }
3046
- function lr(e) {
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 ur(e) {
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>${lr(e)}</li>`).join("")}</${e}>`), o = null, s = [];
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(cr(e)), r++;
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">${lr(cr(u[2]))}</h${e}>`), r++;
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(cr((t[r] ?? "").slice(2))), r++;
3086
- n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${lr(e.join("<br />"))}</blockquote>`);
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(cr(d[1])), r++;
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(cr(f[1])), r++;
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(cr(e)), r++;
3124
+ p.push(ur(e)), r++;
3109
3125
  }
3110
- n.push(`<p class="my-2 leading-relaxed">${lr(p.join("<br />"))}</p>`);
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, ur 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, sr as useMemiBoardWritingAssistant, Kn as versionHistory, xt as videoListCoverUrl, bt as youtubeId };
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