memi-board 0.39.1 → 0.40.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,11 @@ function Gn(e) {
2256
2256
  //#endregion
2257
2257
  //#region src/data/versionHistory.ts
2258
2258
  var Kn = [
2259
+ {
2260
+ version: "0.40.0",
2261
+ date: "2026-08-27",
2262
+ highlights: ["AI 글쓰기 도우미에서 원하는 문체를 직접 입력해 바꿀 수 있어요 (예: 경상도 사투리 스타일로)"]
2263
+ },
2259
2264
  {
2260
2265
  version: "0.39.1",
2261
2266
  date: "2026-08-27",
@@ -2975,7 +2980,7 @@ function tr(e, t) {
2975
2980
  }
2976
2981
  //#endregion
2977
2982
  //#region src/composables/useMemiBoardWritingAssistant.ts
2978
- var nr = 10, rr = 864e5, ir = {
2983
+ var nr = 10, rr = 864e5, ir = 200, ar = {
2979
2984
  proofread: "맞춤법, 띄어쓰기, 문법 오류만 고쳐라. 의미와 말투는 바꾸지 마라.",
2980
2985
  polish: "뜻과 작성자의 말투를 유지하면서 더 자연스럽고 읽기 좋은 문장으로 다듬어라.",
2981
2986
  continue: "앞의 맥락과 말투에 맞는 내용을 과장 없이 한두 문단 이어서 작성하라.",
@@ -2984,13 +2989,19 @@ var nr = 10, rr = 864e5, ir = {
2984
2989
  "translate-ko": "자연스러운 한국어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.",
2985
2990
  title: "본문을 대표하는 간결한 한국어 제목 하나를 만들어라. 따옴표나 설명 없이 제목만 출력하라."
2986
2991
  };
2987
- function ar(e) {
2992
+ function or(e) {
2993
+ if (e.action !== "custom") return ar[e.action];
2994
+ let t = e.customInstruction?.trim().slice(0, ir);
2995
+ if (!t) throw Error("어떻게 바꿀지 지시문을 입력해 주세요.");
2996
+ return `뜻은 유지하면서 다음 스타일 지시를 반영해 다시 써라: ${t}`;
2997
+ }
2998
+ function sr(e) {
2988
2999
  return e.trim().replace(/^```(?:html|text)?\s*/i, "").replace(/\s*```$/, "").trim();
2989
3000
  }
2990
- function or(e) {
3001
+ function cr(e) {
2991
3002
  return !e || typeof e != "object" ? null : "toMillis" in e && typeof e.toMillis == "function" ? e.toMillis() : null;
2992
3003
  }
2993
- function sr() {
3004
+ function lr() {
2994
3005
  let e = le(), t = Q(), n = Z(), r = u();
2995
3006
  async function i() {
2996
3007
  let e = n.value?.uid;
@@ -2998,7 +3009,7 @@ function sr() {
2998
3009
  await oe(t, async (n) => {
2999
3010
  let r = x(t, _(), e), i = await n.get(r);
3000
3011
  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;
3012
+ 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
3013
  if (l && s >= 10) {
3003
3014
  let e = o + rr, t = Math.max(1, Math.ceil((e - c) / 36e5));
3004
3015
  throw Error(`AI 글쓰기 도우미는 하루 10회까지 사용할 수 있습니다. 약 ${t}시간 후 다시 이용해 주세요.`);
@@ -3011,43 +3022,43 @@ function sr() {
3011
3022
  });
3012
3023
  }
3013
3024
  async function a(t) {
3025
+ let n = or(t);
3026
+ if (!t.content.trim()) throw Error("AI로 다듬을 내용을 먼저 입력해 주세요.");
3014
3027
  await i();
3015
- let n = r.moderation ?? {}, a = we(e, {
3028
+ let a = r.moderation ?? {}, o = we(e, {
3016
3029
  backend: new Se(),
3017
- useLimitedUseAppCheckTokens: n.useLimitedUseAppCheckTokens !== !1
3018
- }), o = Te(a, {
3019
- model: n.model ?? "gemini-3.5-flash-lite",
3030
+ useLimitedUseAppCheckTokens: a.useLimitedUseAppCheckTokens !== !1
3031
+ }), s = Te(o, {
3032
+ model: a.model ?? "gemini-3.5-flash-lite",
3020
3033
  systemInstruction: [
3021
3034
  "너는 게시판 글쓰기 편집 도우미다.",
3022
3035
  "사용자가 요청한 편집 결과만 출력하고 설명, 인사, 마크다운 코드 펜스를 붙이지 마라.",
3023
3036
  t.action === "title" || t.selection ? "출력은 일반 텍스트로 작성하라." : "입력의 HTML 구조와 링크, 이미지 태그를 보존하고 결과도 HTML 조각으로 출력하라."
3024
3037
  ].join("\n"),
3025
3038
  generationConfig: {
3026
- temperature: t.action === "continue" || t.action === "title" ? .6 : .2,
3039
+ temperature: t.action === "continue" || t.action === "title" || t.action === "custom" ? .6 : .2,
3027
3040
  maxOutputTokens: 2048
3028
3041
  }
3029
- }), s = t.content.trim();
3030
- if (!s) throw Error("AI로 다듬을 내용을 먼저 입력해 주세요.");
3031
- let c = [
3032
- `작업: ${ir[t.action]}`,
3042
+ }), c = t.content.trim(), l = [
3043
+ `작업: ${n}`,
3033
3044
  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;
3045
+ `본문:\n${c.slice(0, 12e3)}`
3046
+ ].filter(Boolean).join("\n\n"), u = sr((await s.generateContent(l)).response.text());
3047
+ if (!u) throw Error("AI가 결과를 만들지 못했습니다. 다시 시도해 주세요.");
3048
+ return u;
3038
3049
  }
3039
3050
  return { assist: a };
3040
3051
  }
3041
3052
  //#endregion
3042
3053
  //#region src/utils/renderMarkdown.ts
3043
- function cr(e) {
3054
+ function ur(e) {
3044
3055
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3045
3056
  }
3046
- function lr(e) {
3057
+ function dr(e) {
3047
3058
  let t = e;
3048
3059
  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
3060
  }
3050
- function ur(e) {
3061
+ function fr(e) {
3051
3062
  if (!e?.trim()) return "";
3052
3063
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
3053
3064
  function c() {
@@ -3056,7 +3067,7 @@ function ur(e) {
3056
3067
  return;
3057
3068
  }
3058
3069
  let e = o;
3059
- n.push(`<${e}>${s.map((e) => `<li>${lr(e)}</li>`).join("")}</${e}>`), o = null, s = [];
3070
+ n.push(`<${e}>${s.map((e) => `<li>${dr(e)}</li>`).join("")}</${e}>`), o = null, s = [];
3060
3071
  }
3061
3072
  for (; r < t.length;) {
3062
3073
  let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
@@ -3065,7 +3076,7 @@ function ur(e) {
3065
3076
  continue;
3066
3077
  }
3067
3078
  if (i) {
3068
- a.push(cr(e)), r++;
3079
+ a.push(ur(e)), r++;
3069
3080
  continue;
3070
3081
  }
3071
3082
  if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(e)) {
@@ -3076,24 +3087,24 @@ function ur(e) {
3076
3087
  if (u) {
3077
3088
  c();
3078
3089
  let e = u[1].length;
3079
- n.push(`<h${e} class="font-semibold mt-4 mb-2">${lr(cr(u[2]))}</h${e}>`), r++;
3090
+ n.push(`<h${e} class="font-semibold mt-4 mb-2">${dr(ur(u[2]))}</h${e}>`), r++;
3080
3091
  continue;
3081
3092
  }
3082
3093
  if (e.startsWith("> ")) {
3083
3094
  c();
3084
3095
  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>`);
3096
+ for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(ur((t[r] ?? "").slice(2))), r++;
3097
+ n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${dr(e.join("<br />"))}</blockquote>`);
3087
3098
  continue;
3088
3099
  }
3089
3100
  let d = e.match(/^[-*+]\s+(.+)$/);
3090
3101
  if (d) {
3091
- o && o !== "ul" && c(), o = "ul", s.push(cr(d[1])), r++;
3102
+ o && o !== "ul" && c(), o = "ul", s.push(ur(d[1])), r++;
3092
3103
  continue;
3093
3104
  }
3094
3105
  let f = e.match(/^\d+\.\s+(.+)$/);
3095
3106
  if (f) {
3096
- o && o !== "ol" && c(), o = "ol", s.push(cr(f[1])), r++;
3107
+ o && o !== "ol" && c(), o = "ol", s.push(ur(f[1])), r++;
3097
3108
  continue;
3098
3109
  }
3099
3110
  if (!e.trim()) {
@@ -3105,13 +3116,13 @@ function ur(e) {
3105
3116
  for (; r < t.length;) {
3106
3117
  let e = t[r] ?? "";
3107
3118
  if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
3108
- p.push(cr(e)), r++;
3119
+ p.push(ur(e)), r++;
3109
3120
  }
3110
- n.push(`<p class="my-2 leading-relaxed">${lr(p.join("<br />"))}</p>`);
3121
+ n.push(`<p class="my-2 leading-relaxed">${dr(p.join("<br />"))}</p>`);
3111
3122
  }
3112
3123
  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
3124
  }
3114
3125
  //#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 };
3126
+ 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
3127
 
3117
3128
  //# sourceMappingURL=index.js.map