memi-board 0.30.2 → 0.31.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/dist/index.d.ts CHANGED
@@ -30,6 +30,8 @@ export { DEFAULT_BLOCK_BAN_THRESHOLD, DEFAULT_BLOCK_BAN_DECAY_MS, effectiveModer
30
30
  export type { Attachment, EditorImageEntry, ModerationStatus, PostModel, PostDetail, UserPostModel, CommentModel, BoardUserRole, BoardUserModel, BoardUserPrivateModel, AuthorMemoModel, AuthorMemoSentiment, BoardCategory, BoardModel, BoardListView, BoardWriteRole, BoardVisibility, BoardSettingsModel, BoardLikeModel, ModerationResult, } from './types';
31
31
  export { plainTextFromHtml, hasBodyText, hasBodyImage, isContentEmpty, titleFromBody, } from './utils/postBody';
32
32
  export { extractEditorImageUrls } from './utils/extractEditorImageUrls';
33
+ export { useMemiBoardWritingAssistant, WRITING_ASSISTANT_DAILY_LIMIT, } from './composables/useMemiBoardWritingAssistant';
34
+ export type { WritingAssistantAction } from './composables/useMemiBoardWritingAssistant';
33
35
  export { renderMarkdownToHtml } from './utils/renderMarkdown';
34
36
  export { buildPostPreview, youtubeId, videoListCoverUrl } from './utils/postPreview';
35
37
  export { storagePathFromDownloadUrl, postNamespaceFromStoragePath } from './utils/storagePath';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as e, D as t, E as n, O as r, S as i, T as a, _ as o, a as s, b as c, c as l, d as u, f as d, g as f, h as p, i as m, k as h, l as g, m as _, n as v, o as y, p as b, r as x, s as S, u as C, v as w, w as T, x as E, y as D } from "./slugify-CDMIDDCN.js";
1
+ import { C as e, D as t, E as n, O as r, S as i, T as a, _ as o, a as s, b as c, c as l, d as u, f as d, g as f, h as p, i as m, k as h, l as g, m as _, n as v, o as y, p as b, r as x, s as S, u as C, v as w, w as T, x as E, y as D } from "./slugify-B5XHr4SZ.js";
2
2
  import { computed as O, onScopeDispose as k, reactive as A, ref as j, toValue as M, watch as N } from "vue";
3
3
  import { deleteDoc as P, deleteField as F, doc as I, documentId as L, endBefore as ee, getDoc as R, getDocs as z, increment as B, limit as V, limitToLast as te, onSnapshot as H, orderBy as U, query as W, runTransaction as ne, serverTimestamp as G, setDoc as K, startAfter as q, updateDoc as J, where as Y, writeBatch as X } from "firebase/firestore";
4
4
  import { useCollection as re, useCurrentUser as ie, useDocument as ae, useFirebaseApp as oe, useFirebaseAuth as se, useFirestore as Z } from "vuefire";
@@ -168,7 +168,7 @@ async function Ie(e, t) {
168
168
  }
169
169
  //#endregion
170
170
  //#region src/utils/moderation-strike.ts
171
- var Le = 3, Re = 864e5;
171
+ var Le = 10, Re = 864e5;
172
172
  function ze(e, t, n = Date.now(), r = Re) {
173
173
  let i = Math.max(0, Math.floor(Number(e) || 0));
174
174
  if (i <= 0) return 0;
@@ -177,7 +177,7 @@ function ze(e, t, n = Date.now(), r = Re) {
177
177
  let o = Math.floor((n - a) / r);
178
178
  return Math.max(0, i - o);
179
179
  }
180
- function Be(e, t, n = Date.now(), r = 3, i = Re) {
180
+ function Be(e, t, n = Date.now(), r = 10, i = Re) {
181
181
  let a = Math.max(0, Math.floor(Number(e) || 0));
182
182
  if (a < r) return null;
183
183
  let o = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -185,7 +185,7 @@ function Be(e, t, n = Date.now(), r = 3, i = Re) {
185
185
  let s = o + (a - (r - 1)) * i;
186
186
  return s > n ? s : null;
187
187
  }
188
- function Ve(e, t, n = Date.now(), r = 3, i = Re) {
188
+ function Ve(e, t, n = Date.now(), r = 10, i = Re) {
189
189
  return Be(e, t, n, r, i) != null;
190
190
  }
191
191
  function He(e, t = Date.now()) {
@@ -222,7 +222,7 @@ function Ke() {
222
222
  let e = s(), t = Ge(), n = Z(), r = se(), i = ie(), { role: a, rolePending: o, rawBlockCount: c, blockAtMs: l } = t;
223
223
  function u() {
224
224
  let t = e.moderation?.blockBanThreshold;
225
- return t === 0 ? 0 : typeof t == "number" && t > 0 ? Math.floor(t) : 3;
225
+ return t === 0 ? 0 : typeof t == "number" && t > 0 ? Math.floor(t) : 10;
226
226
  }
227
227
  function d() {
228
228
  let t = e.moderation?.blockBanDecayMs;
@@ -1724,6 +1724,12 @@ function Wt(e) {
1724
1724
  //#endregion
1725
1725
  //#region src/data/versionHistory.ts
1726
1726
  var Gt = [
1727
+ {
1728
+ version: "0.31.0",
1729
+ date: "2026-08-14",
1730
+ highlights: ["글쓰기 화면에 AI 도우미가 생겼어요. 맞춤법, 다듬기, 이어서 쓰기, 요약, 번역, 제목을 도와줘요", "AI 도우미는 하루 10번까지 사용할 수 있어요"],
1731
+ note: "호스트는 Firestore 규칙에 AI 글쓰기 횟수 필드를 병합·배포해야 제한이 지켜져요."
1732
+ },
1727
1733
  {
1728
1734
  version: "0.30.2",
1729
1735
  date: "2026-08-14",
@@ -2386,15 +2392,80 @@ function en(e, t) {
2386
2392
  return i == null || r == null || r === i ? [n] : [`생성시간: ${n}`, `수정시간: ${$t(t)}`];
2387
2393
  }
2388
2394
  //#endregion
2395
+ //#region src/composables/useMemiBoardWritingAssistant.ts
2396
+ var tn = 10, nn = 864e5, rn = {
2397
+ proofread: "맞춤법, 띄어쓰기, 문법 오류만 고쳐라. 의미와 말투는 바꾸지 마라.",
2398
+ polish: "뜻과 작성자의 말투를 유지하면서 더 자연스럽고 읽기 좋은 문장으로 다듬어라.",
2399
+ continue: "앞의 맥락과 말투에 맞는 내용을 과장 없이 한두 문단 이어서 작성하라.",
2400
+ summarize: "핵심 정보는 보존하면서 원문의 약 절반 길이로 간결하게 줄여라.",
2401
+ "translate-en": "자연스러운 영어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.",
2402
+ "translate-ko": "자연스러운 한국어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.",
2403
+ title: "본문을 대표하는 간결한 한국어 제목 하나를 만들어라. 따옴표나 설명 없이 제목만 출력하라."
2404
+ };
2405
+ function an(e) {
2406
+ return e.trim().replace(/^```(?:html|text)?\s*/i, "").replace(/\s*```$/, "").trim();
2407
+ }
2408
+ function on(e) {
2409
+ return !e || typeof e != "object" ? null : "toMillis" in e && typeof e.toMillis == "function" ? e.toMillis() : null;
2410
+ }
2411
+ function sn() {
2412
+ let e = oe(), t = Z(), n = ie(), r = s();
2413
+ async function i() {
2414
+ let e = n.value?.uid;
2415
+ if (!e) throw Error("AI 글쓰기 도우미는 로그인 후 사용할 수 있습니다.");
2416
+ await ne(t, async (n) => {
2417
+ let r = b(t, m(), e), i = await n.get(r);
2418
+ if (!i.exists()) throw Error("사용자 정보를 확인한 뒤 다시 시도해 주세요.");
2419
+ let a = i.data(), o = on(a.aiWritingWindowAt), s = Math.max(0, Math.floor(Number(a.aiWritingCount) || 0)), c = Date.now(), l = o != null && c - o < nn;
2420
+ if (l && s >= 10) {
2421
+ let e = o + nn, t = Math.max(1, Math.ceil((e - c) / 36e5));
2422
+ throw Error(`AI 글쓰기 도우미는 하루 10회까지 사용할 수 있습니다. 약 ${t}시간 후 다시 이용해 주세요.`);
2423
+ }
2424
+ n.update(r, {
2425
+ aiWritingCount: l ? s + 1 : 1,
2426
+ aiWritingWindowAt: l ? a.aiWritingWindowAt : G(),
2427
+ updatedAt: G()
2428
+ });
2429
+ });
2430
+ }
2431
+ async function a(t) {
2432
+ await i();
2433
+ let n = r.moderation ?? {}, a = xe(e, {
2434
+ backend: new ye(),
2435
+ useLimitedUseAppCheckTokens: n.useLimitedUseAppCheckTokens !== !1
2436
+ }), o = Se(a, {
2437
+ model: n.model ?? "gemini-3.5-flash-lite",
2438
+ systemInstruction: [
2439
+ "너는 게시판 글쓰기 편집 도우미다.",
2440
+ "사용자가 요청한 편집 결과만 출력하고 설명, 인사, 마크다운 코드 펜스를 붙이지 마라.",
2441
+ t.action === "title" || t.selection ? "출력은 일반 텍스트로 작성하라." : "입력의 HTML 구조와 링크, 이미지 태그를 보존하고 결과도 HTML 조각으로 출력하라."
2442
+ ].join("\n"),
2443
+ generationConfig: {
2444
+ temperature: t.action === "continue" || t.action === "title" ? .6 : .2,
2445
+ maxOutputTokens: 2048
2446
+ }
2447
+ }), s = t.content.trim();
2448
+ if (!s) throw Error("AI로 다듬을 내용을 먼저 입력해 주세요.");
2449
+ let c = [
2450
+ `작업: ${rn[t.action]}`,
2451
+ t.title ? `현재 제목: ${t.title}` : "",
2452
+ `본문:\n${s.slice(0, 12e3)}`
2453
+ ].filter(Boolean).join("\n\n"), l = an((await o.generateContent(c)).response.text());
2454
+ if (!l) throw Error("AI가 결과를 만들지 못했습니다. 다시 시도해 주세요.");
2455
+ return l;
2456
+ }
2457
+ return { assist: a };
2458
+ }
2459
+ //#endregion
2389
2460
  //#region src/utils/renderMarkdown.ts
2390
2461
  function $(e) {
2391
2462
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2392
2463
  }
2393
- function tn(e) {
2464
+ function cn(e) {
2394
2465
  let t = e;
2395
2466
  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;
2396
2467
  }
2397
- function nn(e) {
2468
+ function ln(e) {
2398
2469
  if (!e?.trim()) return "";
2399
2470
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
2400
2471
  function c() {
@@ -2403,7 +2474,7 @@ function nn(e) {
2403
2474
  return;
2404
2475
  }
2405
2476
  let e = o;
2406
- n.push(`<${e}>${s.map((e) => `<li>${tn(e)}</li>`).join("")}</${e}>`), o = null, s = [];
2477
+ n.push(`<${e}>${s.map((e) => `<li>${cn(e)}</li>`).join("")}</${e}>`), o = null, s = [];
2407
2478
  }
2408
2479
  for (; r < t.length;) {
2409
2480
  let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
@@ -2423,14 +2494,14 @@ function nn(e) {
2423
2494
  if (u) {
2424
2495
  c();
2425
2496
  let e = u[1].length;
2426
- n.push(`<h${e} class="font-semibold mt-4 mb-2">${tn($(u[2]))}</h${e}>`), r++;
2497
+ n.push(`<h${e} class="font-semibold mt-4 mb-2">${cn($(u[2]))}</h${e}>`), r++;
2427
2498
  continue;
2428
2499
  }
2429
2500
  if (e.startsWith("> ")) {
2430
2501
  c();
2431
2502
  let e = [];
2432
2503
  for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push($((t[r] ?? "").slice(2))), r++;
2433
- n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${tn(e.join("<br />"))}</blockquote>`);
2504
+ n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${cn(e.join("<br />"))}</blockquote>`);
2434
2505
  continue;
2435
2506
  }
2436
2507
  let d = e.match(/^[-*+]\s+(.+)$/);
@@ -2454,11 +2525,11 @@ function nn(e) {
2454
2525
  if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
2455
2526
  p.push($(e)), r++;
2456
2527
  }
2457
- n.push(`<p class="my-2 leading-relaxed">${tn(p.join("<br />"))}</p>`);
2528
+ n.push(`<p class="my-2 leading-relaxed">${cn(p.join("<br />"))}</p>`);
2458
2529
  }
2459
2530
  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");
2460
2531
  }
2461
2532
  //#endregion
2462
- export { St as AUTHOR_MEMO_MAX_LENGTH, Ht as BOARD_USER_ROLES, Dt as COMMENT_BODY_MAX_LENGTH, Re as DEFAULT_BLOCK_BAN_DECAY_MS, Le as DEFAULT_BLOCK_BAN_THRESHOLD, at as DEFAULT_BOARDS, ot as DEFAULT_CATEGORIES, y as DEFAULT_COMMENTS_COLLECTION, S as DEFAULT_LIKES_COLLECTION, l as DEFAULT_POSTS_COLLECTION, g as DEFAULT_SETTINGS_COLLECTION, C as DEFAULT_USERS_COLLECTION, Ce as asHttpUrl, ke as boardListOgDescription, Oe as boardListOgTitle, De as boardPostOgDescription, Ee as boardPostOgTitle, d as boardSsrKey, b as boardUserDoc, p as boardUsersCol, it as buildPostPreview, f as commentDoc, o as commentsCol, x as configureMemiBoard, ze as effectiveModerationBlockCount, qe as extractEditorImageUrls, Fe as fetchPublicListForSeo, Pe as fetchPublicPostForSeo, Zt as formatDate, $t as formatFullDate, Qt as formatRelativeDate, He as formatRestrictedUntilLabel, en as formatTimestampDetails, $e as hasBodyImage, Qe as hasBodyText, et as isContentEmpty, ft as isFirestorePermissionDenied, Ve as isModerationWriteRestricted, w as likeDoc, D as likeDocId, c as likesCol, Be as moderationWriteRestrictedUntilMs, Ae as normalizeBasePath, Xe as plainTextFromHtml, E as postBodyDoc, i as postDoc, Ye as postNamespaceFromStoragePath, e as postStorageFolder, T as postsCol, nn as renderMarkdownToHtml, a as resolveBoardPathConfig, je as resolvePublicSeoDb, n as settingsCol, t as settingsDoc, v as slugify, Je as storagePathFromDownloadUrl, Ze as titleFromBody, Te as toAbsoluteUrl, we as toBoardOgImageUrl, m as useBoardPathConfig, Ke as useMemiBoardAuth, wt as useMemiBoardAuthorMemo, kt as useMemiBoardComments, s as useMemiBoardConfig, jt as useMemiBoardLikes, Vt as useMemiBoardModeration, _t as useMemiBoardPost, gt as useMemiBoardPostList, mt as useMemiBoardPosts, At as useMemiBoardReplies, lt as useMemiBoardSettings, xt as useMemiBoardUserComments, yt as useMemiBoardUserPosts, Wt as useMemiBoardUserProfile, Ut as useMemiBoardUsers, Mt as useMemiBoardViews, Gt as versionHistory, rt as videoListCoverUrl, nt as youtubeId };
2533
+ export { St as AUTHOR_MEMO_MAX_LENGTH, Ht as BOARD_USER_ROLES, Dt as COMMENT_BODY_MAX_LENGTH, Re as DEFAULT_BLOCK_BAN_DECAY_MS, Le as DEFAULT_BLOCK_BAN_THRESHOLD, at as DEFAULT_BOARDS, ot as DEFAULT_CATEGORIES, y as DEFAULT_COMMENTS_COLLECTION, S as DEFAULT_LIKES_COLLECTION, l as DEFAULT_POSTS_COLLECTION, g as DEFAULT_SETTINGS_COLLECTION, C as DEFAULT_USERS_COLLECTION, tn as WRITING_ASSISTANT_DAILY_LIMIT, Ce as asHttpUrl, ke as boardListOgDescription, Oe as boardListOgTitle, De as boardPostOgDescription, Ee as boardPostOgTitle, d as boardSsrKey, b as boardUserDoc, p as boardUsersCol, it as buildPostPreview, f as commentDoc, o as commentsCol, x as configureMemiBoard, ze as effectiveModerationBlockCount, qe as extractEditorImageUrls, Fe as fetchPublicListForSeo, Pe as fetchPublicPostForSeo, Zt as formatDate, $t as formatFullDate, Qt as formatRelativeDate, He as formatRestrictedUntilLabel, en as formatTimestampDetails, $e as hasBodyImage, Qe as hasBodyText, et as isContentEmpty, ft as isFirestorePermissionDenied, Ve as isModerationWriteRestricted, w as likeDoc, D as likeDocId, c as likesCol, Be as moderationWriteRestrictedUntilMs, Ae as normalizeBasePath, Xe as plainTextFromHtml, E as postBodyDoc, i as postDoc, Ye as postNamespaceFromStoragePath, e as postStorageFolder, T as postsCol, ln as renderMarkdownToHtml, a as resolveBoardPathConfig, je as resolvePublicSeoDb, n as settingsCol, t as settingsDoc, v as slugify, Je as storagePathFromDownloadUrl, Ze as titleFromBody, Te as toAbsoluteUrl, we as toBoardOgImageUrl, m as useBoardPathConfig, Ke as useMemiBoardAuth, wt as useMemiBoardAuthorMemo, kt as useMemiBoardComments, s as useMemiBoardConfig, jt as useMemiBoardLikes, Vt as useMemiBoardModeration, _t as useMemiBoardPost, gt as useMemiBoardPostList, mt as useMemiBoardPosts, At as useMemiBoardReplies, lt as useMemiBoardSettings, xt as useMemiBoardUserComments, yt as useMemiBoardUserPosts, Wt as useMemiBoardUserProfile, Ut as useMemiBoardUsers, Mt as useMemiBoardViews, sn as useMemiBoardWritingAssistant, Gt as versionHistory, rt as videoListCoverUrl, nt as youtubeId };
2463
2534
 
2464
2535
  //# sourceMappingURL=index.js.map