memi-board 0.22.0 → 0.28.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.
Files changed (48) hide show
  1. package/README.md +26 -40
  2. package/dist/composables/useMemiBoardAuthorMemo.d.ts +19 -0
  3. package/dist/composables/useMemiBoardComments.d.ts +6 -1
  4. package/dist/composables/useMemiBoardSettings.d.ts +2 -2
  5. package/dist/composables/useMemiBoardStorage.d.ts +3 -2
  6. package/dist/composables/useMemiBoardUserComments.d.ts +19 -0
  7. package/dist/composables/useMemiBoardUserPosts.d.ts +4 -2
  8. package/dist/composables/useMemiBoardUserProfile.d.ts +10 -0
  9. package/dist/composables/useMemiBoardUsers.d.ts +5 -4
  10. package/dist/composables/useMemiBoardViews.d.ts +1 -1
  11. package/dist/config.d.ts +8 -2
  12. package/dist/{heic2any-BXb_U1zK.js → heic2any-BHVEj2CD.js} +2 -2
  13. package/dist/{heic2any-BXb_U1zK.js.map → heic2any-BHVEj2CD.js.map} +1 -1
  14. package/dist/index.d.ts +5 -5
  15. package/dist/index.js +870 -906
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.mjs +35 -29
  18. package/dist/module.mjs.map +1 -1
  19. package/dist/runtime/components/Attachments.vue +2 -3
  20. package/dist/runtime/components/AuthorMenu.vue +129 -21
  21. package/dist/runtime/components/AvatarCropModal.vue +108 -0
  22. package/dist/runtime/components/CategorySettings.vue +1 -1
  23. package/dist/runtime/components/CommentItem.vue +8 -7
  24. package/dist/runtime/components/Detail.vue +14 -7
  25. package/dist/runtime/components/Editor.vue +8 -4
  26. package/dist/runtime/components/List.vue +6 -0
  27. package/dist/runtime/components/ListDefault.vue +32 -29
  28. package/dist/runtime/components/ListDense.vue +86 -97
  29. package/dist/runtime/components/ListVideo.vue +13 -8
  30. package/dist/runtime/components/Profile.vue +93 -0
  31. package/dist/runtime/components/ProfileEditModal.vue +190 -0
  32. package/dist/runtime/components/Settings.vue +1 -1
  33. package/dist/runtime/components/UserCommentList.vue +109 -0
  34. package/dist/runtime/components/UserPostList.vue +10 -1
  35. package/dist/runtime/components/Users.vue +1 -1
  36. package/dist/slugify-CDMIDDCN.js +122 -0
  37. package/dist/slugify-CDMIDDCN.js.map +1 -0
  38. package/dist/storage.d.ts +9 -0
  39. package/dist/storage.js +148 -0
  40. package/dist/storage.js.map +1 -0
  41. package/dist/types.d.ts +41 -10
  42. package/dist/utils/boardPaths.d.ts +30 -15
  43. package/docs/firebase-setup.md +3 -4
  44. package/docs/firestore.indexes.json.example +136 -0
  45. package/docs/firestore.rules.example +128 -72
  46. package/docs/storage.rules.example +22 -12
  47. package/package.json +7 -2
  48. package/dist/rolldown-runtime-DtPi1Y-2.js +0 -13
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/utils/boardPaths.ts","../src/config.ts","../src/utils/boardSeo.ts","../src/composables/fetchPublicSeo.ts","../src/utils/moderation-strike.ts","../src/composables/useMemiBoardAuth.ts","../src/utils/slugify.ts","../src/utils/extractEditorImageUrls.ts","../src/utils/storagePath.ts","../src/utils/postBody.ts","../src/utils/postPreview.ts","../src/composables/useMemiBoardSettings.ts","../src/composables/useMemiBoardPosts.ts","../src/composables/useMemiBoardUserPosts.ts","../src/composables/useMemiBoardComments.ts","../src/composables/useMemiBoardLikes.ts","../src/composables/useMemiBoardViews.ts","../src/utils/moderation-prompt.ts","../src/composables/useMemiBoardModeration.ts","../src/utils/compressImage.ts","../src/composables/useMemiBoardStorage.ts","../src/composables/useMemiBoardUsers.ts","../src/data/versionHistory.ts","../node_modules/.pnpm/dayjs@1.11.21/node_modules/dayjs/dayjs.min.js","../node_modules/.pnpm/dayjs@1.11.21/node_modules/dayjs/plugin/relativeTime.js","../node_modules/.pnpm/dayjs@1.11.21/node_modules/dayjs/locale/ko.js","../src/utils/formatDate.ts","../src/utils/renderMarkdown.ts"],"sourcesContent":["/**\n * Firestore / Storage 경로\n *\n * ```\n * memiBoardUsers/{uid}\n *\n * memiBoards/{boardId} // 목록용 메타(optional stub + order)\n * memiBoards/{boardId}/settings/config // 보드 설정 (구 category + boardSettings)\n * memiBoards/{boardId}/posts/{postId}\n * memiBoards/{boardId}/posts/{postId}/body/main\n * memiBoards/{boardId}/posts/{postId}/comments|likes\n * ```\n *\n * Storage: `memiBoards/{boardId}/posts/{postId}/...`\n *\n * 카테고리 개념 없음 — 예전 category id 가 곧 boardId.\n */\nimport {\n collection,\n doc,\n type CollectionReference,\n type DocumentReference,\n type Firestore,\n} from 'firebase/firestore'\n\nexport const DEFAULT_BOARDS_COLLECTION = 'memiBoards'\nexport const DEFAULT_USERS_COLLECTION = 'memiBoardUsers'\n\nexport interface BoardPathConfig {\n boardsCollection: string\n usersCollection: string\n}\n\nexport function resolveBoardPathConfig(input: {\n boardsCollection?: string\n usersCollection?: string\n}): BoardPathConfig {\n return {\n boardsCollection: input.boardsCollection?.trim() || DEFAULT_BOARDS_COLLECTION,\n usersCollection: input.usersCollection?.trim() || DEFAULT_USERS_COLLECTION,\n }\n}\n\nexport function boardsCol(db: Firestore, cfg: BoardPathConfig): CollectionReference {\n return collection(db, cfg.boardsCollection)\n}\n\nexport function boardDoc(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n): DocumentReference {\n return doc(db, cfg.boardsCollection, boardId)\n}\n\n/** 구 boardSettings — 보드 단위 설정 문서 */\nexport function boardSettingsDoc(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n): DocumentReference {\n return doc(db, cfg.boardsCollection, boardId, 'settings', 'config')\n}\n\nexport function boardPostsCol(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n): CollectionReference {\n return collection(db, cfg.boardsCollection, boardId, 'posts')\n}\n\nexport function boardPostDoc(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n postId: string,\n): DocumentReference {\n return doc(db, cfg.boardsCollection, boardId, 'posts', postId)\n}\n\nexport function boardPostBodyDoc(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n postId: string,\n): DocumentReference {\n return doc(db, cfg.boardsCollection, boardId, 'posts', postId, 'body', 'main')\n}\n\nexport function boardPostCommentsCol(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n postId: string,\n): CollectionReference {\n return collection(db, cfg.boardsCollection, boardId, 'posts', postId, 'comments')\n}\n\nexport function boardPostCommentDoc(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n postId: string,\n commentId: string,\n): DocumentReference {\n return doc(db, cfg.boardsCollection, boardId, 'posts', postId, 'comments', commentId)\n}\n\nexport function boardPostLikeDoc(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n postId: string,\n uid: string,\n): DocumentReference {\n return doc(db, cfg.boardsCollection, boardId, 'posts', postId, 'likes', uid)\n}\n\nexport function boardUsersCol(db: Firestore, cfg: BoardPathConfig): CollectionReference {\n return collection(db, cfg.usersCollection)\n}\n\nexport function boardUserDoc(\n db: Firestore,\n cfg: BoardPathConfig,\n uid: string,\n): DocumentReference {\n return doc(db, cfg.usersCollection, uid)\n}\n\nexport function boardStorageRoot(cfg: BoardPathConfig, boardId: string): string {\n return `${cfg.boardsCollection}/${boardId}`\n}\n\nexport function boardPostStorageFolder(\n cfg: BoardPathConfig,\n boardId: string,\n postId: string,\n): string {\n return `${boardStorageRoot(cfg, boardId)}/posts/${postId}`\n}\n\nexport function boardSsrKey(cfg: BoardPathConfig, suffix: string): string {\n return `${cfg.boardsCollection}/${cfg.usersCollection}/${suffix}`\n}\n","import { reactive } from 'vue'\nimport {\n DEFAULT_BOARDS_COLLECTION,\n DEFAULT_USERS_COLLECTION,\n resolveBoardPathConfig,\n type BoardPathConfig,\n} from './utils/boardPaths'\n\nexport interface MemiBoardAuthOptions {\n providers?: Array<'google' | 'apple' | 'emailPassword'>\n}\n\nexport interface MemiBoardModerationOptions {\n enabled?: boolean\n model?: string\n localBlocklist?: string[]\n onError?: 'allow' | 'block'\n moderateImages?: boolean\n useLimitedUseAppCheckTokens?: boolean\n blockBanThreshold?: number\n blockBanDecayMs?: number\n}\n\nexport interface MemiBoardSeoOptions {\n enabled?: boolean\n siteName?: string\n siteUrl?: string\n defaultOgImage?: string\n /**\n * 호스트 게시판 base path (trailing slash 없음). 기본 '/board'\n * canonical: {base}, {base}/{boardId}, {base}/{boardId}/{slug}\n */\n basePath?: string\n}\n\nexport interface MemiBoardConfig {\n /** 루트 컬렉션. 기본 `memiBoards` */\n boardsCollection: string\n /** 역할 문서 컬렉션. 기본 `memiBoardUsers` */\n usersCollection: string\n auth: MemiBoardAuthOptions\n moderation: MemiBoardModerationOptions\n seo: MemiBoardSeoOptions\n}\n\nconst GLOBAL_KEY = '__MEMI_BOARD_CONFIG__' as const\n\nfunction getSharedConfig(): MemiBoardConfig {\n const g = globalThis as typeof globalThis & {\n [GLOBAL_KEY]?: MemiBoardConfig\n }\n if (!g[GLOBAL_KEY]) {\n g[GLOBAL_KEY] = reactive<MemiBoardConfig>({\n boardsCollection: DEFAULT_BOARDS_COLLECTION,\n usersCollection: DEFAULT_USERS_COLLECTION,\n auth: { providers: ['google', 'apple'] },\n moderation: {\n enabled: true,\n model: 'gemini-3.5-flash-lite',\n onError: 'allow',\n moderateImages: false,\n localBlocklist: [],\n blockBanThreshold: 3,\n blockBanDecayMs: 24 * 60 * 60 * 1000,\n },\n seo: {\n enabled: true,\n siteName: 'Board',\n siteUrl: '',\n defaultOgImage: '',\n basePath: '/board',\n },\n })\n }\n return g[GLOBAL_KEY]\n}\n\nexport function configureMemiBoard(options: Partial<MemiBoardConfig>): void {\n const config = getSharedConfig()\n if (options.boardsCollection != null && options.boardsCollection !== '') {\n config.boardsCollection = options.boardsCollection.trim()\n }\n if (options.usersCollection != null && options.usersCollection !== '') {\n config.usersCollection = options.usersCollection.trim()\n }\n if (options.auth) Object.assign(config.auth, options.auth)\n if (options.moderation) Object.assign(config.moderation, options.moderation)\n if (options.seo) Object.assign(config.seo, options.seo)\n}\n\nexport function useMemiBoardConfig(): MemiBoardConfig {\n return getSharedConfig()\n}\n\nexport function useBoardPathConfig(): BoardPathConfig {\n return resolveBoardPathConfig(useMemiBoardConfig())\n}\n","/** 게시판 SEO 순수 헬퍼 — Nuxt 의존 없음 */\n\nexport type BoardPostSeoPayload = {\n id: string\n slug: string\n category: string\n categoryLabel: string\n title: string\n summary: string\n previewImage: string | null\n authorName: string | null\n createdAt: string | null\n}\n\nexport type BoardListSeoPayload = {\n kind: 'all' | 'category'\n category: string | null\n categoryLabel: string\n description: string\n ogImage: string | null\n recentTitles: string[]\n}\n\nexport function asHttpUrl(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const s = value.trim()\n return /^https?:\\/\\//i.test(s) ? s : null\n}\n\n/**\n * 게시글 previewImage(원본) → 공유 미리보기용 썸네일 URL.\n * Storage: `.../images/{file}` → `.../images/thumbnails/{basename}.jpg`\n * (uploadEditorImage 와 동일 규칙, 공개 read 시 `?alt=media` 만으로 접근)\n * Firebase 가 아니면 원본 유지. 이미 thumbnails 경로면 토큰만 제거.\n */\nexport function toBoardOgImageUrl(value: unknown): string | null {\n const s = asHttpUrl(value)\n if (!s) return null\n if (!/firebasestorage\\.googleapis\\.com/i.test(s)) return s\n\n try {\n const u = new URL(s)\n // /v0/b/{bucket}/o/{encodedPath}\n const parts = u.pathname.split('/')\n const bucket = parts[3]\n const oIdx = parts.indexOf('o')\n if (!bucket || oIdx < 0 || !parts[oIdx + 1]) return s\n\n const path = decodeURIComponent(parts.slice(oIdx + 1).join('/'))\n let thumbPath = path\n if (!path.includes('/images/thumbnails/')) {\n const next = path.replace(\n /\\/images\\/([^/]+)$/,\n (_full, file: string) => {\n const base = String(file).replace(/\\.[^.]+$/, '')\n return `/images/thumbnails/${base}.jpg`\n },\n )\n if (next === path) return s\n thumbPath = next\n }\n\n // 토큰 없이 공개 read — 크롤러·카톡 캐시에 유리\n return `${u.origin}/v0/b/${bucket}/o/${encodeURIComponent(thumbPath)}?alt=media`\n }\n catch {\n return s\n }\n}\n\nexport function toAbsoluteUrl(pathOrUrl: string, origin: string, fallbackPath = ''): string {\n const base = origin.replace(/\\/$/, '')\n const s = pathOrUrl.trim()\n if (!s) {\n if (fallbackPath) return toAbsoluteUrl(fallbackPath, origin)\n return base\n }\n if (/^https?:\\/\\//i.test(s)) return s\n return `${base}${s.startsWith('/') ? s : `/${s}`}`\n}\n\nexport function boardPostOgTitle(\n title: string,\n categoryLabel: string | undefined,\n siteName: string,\n /** 제목 없을 때(이미지 보드) 본문 요약 사용 */\n summary?: string,\n): string {\n const fromSummary = summary?.trim().slice(0, 40)\n const t = title.trim() || fromSummary || '사진'\n const cat = categoryLabel?.trim()\n const site = siteName.trim() || 'Board'\n return cat ? `${t} · ${cat} | ${site}` : `${t} | ${site}`\n}\n\nexport function boardPostOgDescription(opts: {\n summary?: string\n categoryLabel?: string\n authorName?: string | null\n}): string {\n const bits: string[] = []\n if (opts.summary?.trim()) bits.push(opts.summary.trim().slice(0, 140))\n else bits.push('게시판 글')\n if (opts.categoryLabel?.trim()) bits.push(opts.categoryLabel.trim())\n if (opts.authorName?.trim()) bits.push(opts.authorName.trim())\n return bits.join(' · ').slice(0, 200)\n}\n\nexport function boardListOgTitle(categoryLabel: string | null | undefined, siteName: string): string {\n const site = siteName.trim() || 'Board'\n const cat = categoryLabel?.trim()\n if (cat && cat !== '전체') return `${cat} 게시판 | ${site}`\n return `게시판 | ${site}`\n}\n\nexport function boardListOgDescription(opts: {\n categoryLabel?: string | null\n description?: string\n recentTitles?: string[]\n}): string {\n if (opts.description?.trim()) return opts.description.trim().slice(0, 200)\n const cat = opts.categoryLabel?.trim()\n const head = cat && cat !== '전체'\n ? `${cat} 게시판 — 소식을 확인하세요.`\n : '게시판 — 공지와 이야기를 확인하세요.'\n const titles = (opts.recentTitles || []).filter(Boolean).slice(0, 3)\n if (!titles.length) return head.slice(0, 200)\n return `${head} ${titles.map(t => `「${t}」`).join(' ')}`.slice(0, 200)\n}\n\nexport function normalizeBasePath(basePath: string | undefined): string {\n const raw = (basePath || '/board').trim() || '/board'\n const withSlash = raw.startsWith('/') ? raw : `/${raw}`\n return withSlash.replace(/\\/$/, '') || '/board'\n}\n","/**\n * 공개 SEO용 Firestore 직조회.\n * listed === true && isPublished === true 만. boardId = 예전 category 경로 세그먼트.\n */\nimport {\n getDoc,\n getDocs,\n limit as fbLimit,\n orderBy,\n query,\n where,\n type Firestore,\n} from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport {\n boardPostsCol,\n boardSettingsDoc,\n resolveBoardPathConfig,\n type BoardPathConfig,\n} from '../utils/boardPaths'\nimport {\n toBoardOgImageUrl,\n type BoardListSeoPayload,\n type BoardPostSeoPayload,\n} from '../utils/boardSeo'\n\nexport type PublicSeoDb = {\n db: Firestore\n paths: BoardPathConfig\n}\n\nexport function resolvePublicSeoDb(): PublicSeoDb {\n return {\n db: useFirestore(),\n paths: useBoardPathConfig(),\n }\n}\n\nasync function loadBoardMeta(ctx: PublicSeoDb, boardId: string): Promise<{\n label: string\n description: string\n hidden: boolean\n}> {\n try {\n const snap = await getDoc(boardSettingsDoc(ctx.db, ctx.paths, boardId))\n if (!snap.exists()) {\n // 부모 문서 fallback\n const parent = await getDoc(\n // lazy import path via settings only\n boardSettingsDoc(ctx.db, ctx.paths, boardId),\n )\n if (!parent.exists()) {\n return { label: boardId, description: '', hidden: false }\n }\n }\n const d = snap.data() ?? {}\n return {\n label: typeof d.label === 'string' && d.label.trim() ? d.label.trim() : boardId,\n description: typeof d.description === 'string' ? d.description.trim() : '',\n hidden: d.visibility === 'hidden',\n }\n }\n catch {\n return { label: boardId, description: '', hidden: false }\n }\n}\n\nfunction createdAtIso(value: unknown): string | null {\n if (value && typeof value === 'object' && 'toDate' in value) {\n try {\n const d = (value as { toDate: () => Date }).toDate()\n return d instanceof Date ? d.toISOString() : null\n }\n catch {\n return null\n }\n }\n return null\n}\n\n/** 공개 글 1건 (boardId + slug) */\nexport async function fetchPublicPostForSeo(\n boardId: string,\n slug: string,\n ctx?: PublicSeoDb,\n): Promise<BoardPostSeoPayload | null> {\n const b = boardId.trim()\n const s = slug.trim()\n if (!b || !s) return null\n\n const store = ctx ?? resolvePublicSeoDb()\n store.paths = resolveBoardPathConfig(store.paths)\n const meta = await loadBoardMeta(store, b)\n if (meta.hidden) return null\n\n try {\n const snap = await getDocs(query(\n boardPostsCol(store.db, store.paths, b),\n where('slug', '==', s),\n where('listed', '==', true),\n where('isPublished', '==', true),\n fbLimit(1),\n ))\n const row = snap.docs[0]\n if (!row) return null\n const d = row.data() ?? {}\n const title = typeof d.title === 'string' ? d.title.trim() : ''\n if (!title) return null\n return {\n id: row.id,\n slug: typeof d.slug === 'string' ? d.slug : s,\n category: b,\n categoryLabel: meta.label,\n title,\n summary: typeof d.summary === 'string' ? d.summary.trim().slice(0, 200) : '',\n previewImage: toBoardOgImageUrl(d.previewImage),\n authorName: typeof d.authorName === 'string' && d.authorName.trim() ? d.authorName.trim() : null,\n createdAt: createdAtIso(d.createdAt),\n }\n }\n catch (e) {\n if (import.meta.dev) console.warn('[memi-board] fetchPublicPostForSeo failed', e)\n return null\n }\n}\n\n/** 전체 보드 목록 또는 단일 보드 SEO */\nexport async function fetchPublicListForSeo(\n boardId?: string | null,\n ctx?: PublicSeoDb,\n): Promise<BoardListSeoPayload> {\n const b = boardId?.trim() || null\n const store = ctx ?? resolvePublicSeoDb()\n store.paths = resolveBoardPathConfig(store.paths)\n\n if (b) {\n const meta = await loadBoardMeta(store, b)\n if (meta.hidden) {\n return {\n kind: 'category',\n category: b,\n categoryLabel: meta.label,\n description: '',\n ogImage: null,\n recentTitles: [],\n }\n }\n const { recentTitles, ogImage } = await loadRecentListed(store, b)\n return {\n kind: 'category',\n category: b,\n categoryLabel: meta.label,\n description: meta.description || `${meta.label} 게시판`,\n ogImage,\n recentTitles,\n }\n }\n\n return {\n kind: 'all',\n category: null,\n categoryLabel: '전체',\n description: '게시판 — 공지와 커뮤니티 소식을 확인하세요.',\n ogImage: null,\n recentTitles: [],\n }\n}\n\nasync function loadRecentListed(ctx: PublicSeoDb, boardId: string): Promise<{\n recentTitles: string[]\n ogImage: string | null\n}> {\n const recentTitles: string[] = []\n let ogImage: string | null = null\n try {\n const snap = await getDocs(query(\n boardPostsCol(ctx.db, ctx.paths, boardId),\n where('listed', '==', true),\n where('isPublished', '==', true),\n orderBy('createdAt', 'desc'),\n fbLimit(12),\n ))\n for (const row of snap.docs) {\n const d = row.data() ?? {}\n const title = typeof d.title === 'string' ? d.title.trim() : ''\n if (title && recentTitles.length < 5) recentTitles.push(title)\n if (!ogImage) ogImage = toBoardOgImageUrl(d.previewImage)\n }\n }\n catch (e) {\n if (import.meta.dev) console.warn('[memi-board] loadRecentListed failed', e)\n }\n return { recentTitles, ogImage }\n}\n","/**\n * 게시판 검열 차단 누적 → 일시 이용 제한 (호스트 contentWarning 과 동일 모델).\n * - 콘텐츠 차단(로컬/AI flagged) 시 +1\n * - 유효 경고 ≥ threshold(기본 3) → 글/댓글 작성 불가\n * - 24h마다 lazy 차감 (moderationBlockAt 기준)\n */\n\nexport const DEFAULT_BLOCK_BAN_THRESHOLD = 3\nexport const DEFAULT_BLOCK_BAN_DECAY_MS = 24 * 60 * 60 * 1000\n\nexport function effectiveModerationBlockCount(\n count: number | null | undefined,\n blockAtMs: number | null | undefined,\n nowMs: number = Date.now(),\n decayMs: number = DEFAULT_BLOCK_BAN_DECAY_MS,\n): number {\n const c = Math.max(0, Math.floor(Number(count) || 0))\n if (c <= 0) return 0\n const at = typeof blockAtMs === 'number' && Number.isFinite(blockAtMs) ? blockAtMs : null\n if (at == null) return c\n if (nowMs <= at) return c\n const decayed = Math.floor((nowMs - at) / decayMs)\n return Math.max(0, c - decayed)\n}\n\n/** 제한 해제 시각(ms). 제한 없으면 null. */\nexport function moderationWriteRestrictedUntilMs(\n count: number | null | undefined,\n blockAtMs: number | null | undefined,\n nowMs: number = Date.now(),\n threshold: number = DEFAULT_BLOCK_BAN_THRESHOLD,\n decayMs: number = DEFAULT_BLOCK_BAN_DECAY_MS,\n): number | null {\n const c = Math.max(0, Math.floor(Number(count) || 0))\n if (c < threshold) return null\n const at = typeof blockAtMs === 'number' && Number.isFinite(blockAtMs) ? blockAtMs : null\n if (at == null) return nowMs\n // 유효 경고가 threshold-1 이하가 되는 시점\n const until = at + (c - (threshold - 1)) * decayMs\n return until > nowMs ? until : null\n}\n\nexport function isModerationWriteRestricted(\n count: number | null | undefined,\n blockAtMs: number | null | undefined,\n nowMs: number = Date.now(),\n threshold: number = DEFAULT_BLOCK_BAN_THRESHOLD,\n decayMs: number = DEFAULT_BLOCK_BAN_DECAY_MS,\n): boolean {\n return moderationWriteRestrictedUntilMs(count, blockAtMs, nowMs, threshold, decayMs) != null\n}\n\nexport function formatRestrictedUntilLabel(untilMs: number, nowMs: number = Date.now()): string {\n const left = Math.max(0, untilMs - nowMs)\n const hours = Math.ceil(left / (60 * 60 * 1000))\n if (hours <= 0) return '곧 해제'\n if (hours < 24) return `약 ${hours}시간 후 해제`\n const days = Math.floor(hours / 24)\n const remH = hours % 24\n if (remH === 0) return `약 ${days}일 후 해제`\n return `약 ${days}일 ${remH}시간 후 해제`\n}\n\nexport function toBlockAtMs(value: unknown): number | null {\n if (value == null) return null\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'object' && value !== null && 'toMillis' in value\n && typeof (value as { toMillis: () => number }).toMillis === 'function') {\n return (value as { toMillis: () => number }).toMillis()\n }\n if (typeof value === 'object' && value !== null && 'seconds' in value) {\n const s = Number((value as { seconds: number }).seconds)\n if (Number.isFinite(s)) return s * 1000\n }\n return null\n}\n","import { ref, computed, watch } from 'vue'\nimport type { Ref, ComputedRef } from 'vue'\nimport { useFirestore, useFirebaseAuth, useCurrentUser } from 'vuefire'\nimport { getDoc, setDoc, updateDoc, increment, serverTimestamp } from 'firebase/firestore'\nimport {\n GoogleAuthProvider,\n OAuthProvider,\n signInWithPopup,\n signInWithEmailAndPassword,\n createUserWithEmailAndPassword,\n signOut as firebaseSignOut,\n updateProfile,\n} from 'firebase/auth'\nimport type { User } from 'firebase/auth'\nimport { useMemiBoardConfig, useBoardPathConfig } from '../config'\nimport { boardUserDoc } from '../utils/boardPaths'\nimport type { PostModel, CommentModel, BoardUserRole } from '../types'\nimport {\n DEFAULT_BLOCK_BAN_DECAY_MS,\n DEFAULT_BLOCK_BAN_THRESHOLD,\n effectiveModerationBlockCount,\n formatRestrictedUntilLabel,\n isModerationWriteRestricted,\n moderationWriteRestrictedUntilMs,\n toBlockAtMs,\n} from '../utils/moderation-strike'\n\nexport interface UseMemiBoardAuthReturn {\n user: ReturnType<typeof useCurrentUser>\n isSignedIn: ComputedRef<boolean>\n isAdmin: ComputedRef<boolean>\n isStaff: ComputedRef<boolean>\n canManageContent: ComputedRef<boolean>\n rolePending: Ref<boolean>\n /** 유효 검열 차단 누적 (lazy decay 반영) */\n moderationBlockCount: ComputedRef<number>\n /** 글·댓글 작성 제한 중 */\n isWriteRestricted: ComputedRef<boolean>\n restrictedUntilLabel: ComputedRef<string | null>\n restrictedMessage: ComputedRef<string | null>\n signInWithGoogle: () => ReturnType<typeof signInWithPopup>\n signInWithApple: () => ReturnType<typeof signInWithPopup>\n signInWithEmail: (email: string, password: string) => ReturnType<typeof signInWithEmailAndPassword>\n signUpWithEmail: (email: string, password: string, displayName?: string) => ReturnType<typeof createUserWithEmailAndPassword>\n signOut: () => Promise<void>\n canEdit: (post: Pick<PostModel, 'authorUid'>) => boolean\n canDelete: (post: Pick<PostModel, 'authorUid'>) => boolean\n canEditComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean\n canDeleteComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean\n /** 콘텐츠 검열 차단 1회 기록 후 메시지 조각 반환 */\n recordContentModerationBlock: () => Promise<{\n effectiveCount: number\n restricted: boolean\n messageSuffix: string\n }>\n refreshBoardUser: () => Promise<void>\n}\n\nconst AUTH_STATE_KEY = '__MEMI_BOARD_AUTH_STATE__' as const\n\ntype BoardAuthShared = {\n role: Ref<BoardUserRole>\n rolePending: Ref<boolean>\n rawBlockCount: Ref<number>\n blockAtMs: Ref<number | null>\n watchStarted: boolean\n}\n\nfunction getSharedAuthState(): BoardAuthShared {\n const g = globalThis as typeof globalThis & { [AUTH_STATE_KEY]?: BoardAuthShared }\n if (!g[AUTH_STATE_KEY]) {\n g[AUTH_STATE_KEY] = {\n role: ref<BoardUserRole>('user'),\n rolePending: ref(true),\n rawBlockCount: ref(0),\n blockAtMs: ref<number | null>(null),\n watchStarted: false,\n }\n }\n return g[AUTH_STATE_KEY]\n}\n\n/**\n * 인증 상태 + 게시판 역할(role) + 검열 차단 누적.\n * role·경고 카운트는 globalThis 공유 (여러 composable 호출이 같은 상태).\n */\nexport function useMemiBoardAuth(): UseMemiBoardAuthReturn {\n const config = useMemiBoardConfig()\n const shared = getSharedAuthState()\n const db = useFirestore()\n const auth = useFirebaseAuth()\n const user = useCurrentUser()\n\n const { role, rolePending, rawBlockCount, blockAtMs } = shared\n\n function banThreshold(): number {\n const t = config.moderation?.blockBanThreshold\n if (t === 0) return 0\n return typeof t === 'number' && t > 0 ? Math.floor(t) : DEFAULT_BLOCK_BAN_THRESHOLD\n }\n\n function banDecayMs(): number {\n const d = config.moderation?.blockBanDecayMs\n return typeof d === 'number' && d > 0 ? d : DEFAULT_BLOCK_BAN_DECAY_MS\n }\n\n function roleDocRef(uid: string) {\n return boardUserDoc(db, useBoardPathConfig(), uid)\n }\n\n function applyUserData(data: Record<string, unknown> | undefined) {\n if (!data) {\n rawBlockCount.value = 0\n blockAtMs.value = null\n return\n }\n rawBlockCount.value = Math.max(0, Math.floor(Number(data.moderationBlockCount) || 0))\n blockAtMs.value = toBlockAtMs(data.moderationBlockAt)\n }\n\n async function ensureUserDoc(firebaseUser: User): Promise<BoardUserRole> {\n const ref = roleDocRef(firebaseUser.uid)\n const snap = await getDoc(ref)\n if (!snap.exists()) {\n await setDoc(ref, {\n role: 'user',\n displayName: firebaseUser.displayName,\n email: firebaseUser.email,\n photoURL: firebaseUser.photoURL,\n updatedAt: serverTimestamp(),\n })\n applyUserData({ moderationBlockCount: 0 })\n return 'user'\n }\n const data = snap.data() as Record<string, unknown>\n applyUserData(data)\n await updateDoc(ref, {\n displayName: firebaseUser.displayName,\n email: firebaseUser.email,\n photoURL: firebaseUser.photoURL,\n updatedAt: serverTimestamp(),\n })\n return (['admin', 'staff', 'user'].includes(String(data.role)) ? data.role : 'user') as BoardUserRole\n }\n\n async function refreshBoardUser() {\n const uid = user.value?.uid\n if (!uid) {\n applyUserData(undefined)\n return\n }\n const snap = await getDoc(roleDocRef(uid))\n applyUserData(snap.exists() ? snap.data() as Record<string, unknown> : undefined)\n }\n\n if (!shared.watchStarted) {\n shared.watchStarted = true\n watch(\n user,\n async (current) => {\n rolePending.value = true\n if (!current) {\n role.value = 'user'\n applyUserData(undefined)\n rolePending.value = false\n return\n }\n try {\n role.value = await ensureUserDoc(current)\n }\n catch (cause) {\n // permission-denied 시 문서 미생성 — rules 미배포·isBoardAdmin get 실패 등\n console.error('[memi-board] ensureUserDoc failed', cause)\n role.value = 'user'\n applyUserData(undefined)\n }\n finally {\n rolePending.value = false\n }\n },\n { immediate: true },\n )\n }\n\n const isSignedIn = computed(() => !!user.value)\n const isAdmin = computed(() => role.value === 'admin')\n const isStaff = computed(() => role.value === 'staff')\n const canManageContent = computed(() => isAdmin.value || isStaff.value)\n\n const moderationBlockCount = computed(() =>\n effectiveModerationBlockCount(\n rawBlockCount.value,\n blockAtMs.value,\n Date.now(),\n banDecayMs(),\n ),\n )\n\n const isWriteRestricted = computed(() => {\n const th = banThreshold()\n if (th <= 0) return false\n return isModerationWriteRestricted(\n rawBlockCount.value,\n blockAtMs.value,\n Date.now(),\n th,\n banDecayMs(),\n )\n })\n\n const restrictedUntilLabel = computed(() => {\n const th = banThreshold()\n if (th <= 0) return null\n const until = moderationWriteRestrictedUntilMs(\n rawBlockCount.value,\n blockAtMs.value,\n Date.now(),\n th,\n banDecayMs(),\n )\n return until != null ? formatRestrictedUntilLabel(until) : null\n })\n\n const restrictedMessage = computed(() => {\n if (!isWriteRestricted.value) return null\n const until = restrictedUntilLabel.value\n return until\n ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${until})`\n : '콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.'\n })\n\n async function recordContentModerationBlock() {\n const uid = user.value?.uid\n const threshold = banThreshold()\n if (!uid || threshold <= 0) {\n return { effectiveCount: 0, restricted: false, messageSuffix: '' }\n }\n\n const ref = roleDocRef(uid)\n const snap = await getDoc(ref)\n if (!snap.exists()) {\n await setDoc(ref, {\n role: 'user',\n displayName: user.value?.displayName ?? null,\n moderationBlockCount: 1,\n moderationBlockAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n }\n else {\n await updateDoc(ref, {\n moderationBlockCount: increment(1),\n moderationBlockAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n }\n\n // increment 직후 getDoc 이 로컬 캐시로 바로 반영됨\n const nextCount = (snap.exists()\n ? Math.max(0, Math.floor(Number((snap.data() as Record<string, unknown>).moderationBlockCount) || 0))\n : 0) + 1\n const nextAt = Date.now()\n rawBlockCount.value = nextCount\n blockAtMs.value = nextAt\n\n const effectiveCount = effectiveModerationBlockCount(\n nextCount,\n nextAt,\n nextAt,\n banDecayMs(),\n )\n const restricted = isModerationWriteRestricted(\n nextCount,\n nextAt,\n nextAt,\n threshold,\n banDecayMs(),\n )\n // 앞 사유 문장과 이어 붙일 접미 (공백·구분자 포함)\n let messageSuffix = ` (경고 ${effectiveCount}/${threshold})`\n if (restricted) {\n const until = moderationWriteRestrictedUntilMs(\n nextCount,\n nextAt,\n nextAt,\n threshold,\n banDecayMs(),\n )\n const untilLabel = until != null ? formatRestrictedUntilLabel(until, nextAt) : null\n messageSuffix = untilLabel\n ? ` 경고 ${effectiveCount}회가 되어 글·댓글 작성이 제한됩니다 (${untilLabel}).`\n : ` 경고 ${effectiveCount}회가 되어 글·댓글 작성이 제한됩니다.`\n }\n return { effectiveCount, restricted, messageSuffix }\n }\n\n function requireAuth() {\n if (!auth) throw new Error('[memi-board] Firebase Auth가 초기화되지 않았습니다.')\n return auth\n }\n\n async function signInWithGoogle() {\n return signInWithPopup(requireAuth(), new GoogleAuthProvider())\n }\n\n async function signInWithApple() {\n const provider = new OAuthProvider('apple.com')\n provider.addScope('email')\n provider.addScope('name')\n return signInWithPopup(requireAuth(), provider)\n }\n\n async function signInWithEmail(email: string, password: string) {\n return signInWithEmailAndPassword(requireAuth(), email, password)\n }\n\n async function signUpWithEmail(email: string, password: string, displayName?: string) {\n const credential = await createUserWithEmailAndPassword(requireAuth(), email, password)\n if (displayName) await updateProfile(credential.user, { displayName })\n return credential\n }\n\n async function signOut() {\n await firebaseSignOut(requireAuth())\n }\n\n function canEdit(post: Pick<PostModel, 'authorUid'>) {\n return isSignedIn.value && (user.value?.uid === post.authorUid || canManageContent.value)\n }\n\n function canEditComment(comment: Pick<CommentModel, 'authorUid'>) {\n return isSignedIn.value && user.value?.uid === comment.authorUid\n }\n\n function canDeleteComment(comment: Pick<CommentModel, 'authorUid'>) {\n return isSignedIn.value && (user.value?.uid === comment.authorUid || canManageContent.value)\n }\n\n return {\n user,\n isSignedIn,\n isAdmin,\n isStaff,\n canManageContent,\n rolePending,\n moderationBlockCount,\n isWriteRestricted,\n restrictedUntilLabel,\n restrictedMessage,\n signInWithGoogle,\n signInWithApple,\n signInWithEmail,\n signUpWithEmail,\n signOut,\n canEdit,\n canDelete: canEdit,\n canEditComment,\n canDeleteComment,\n recordContentModerationBlock,\n refreshBoardUser,\n }\n}\n","/**\n * 제목 문자열을 URL-friendly slug로 변환\n * e.g. \"게시판 모듈 만들기!!\" → \"게시판-모듈-만들기\"\n */\nexport function slugify(text: string): string {\n return text\n .trim()\n .replace(/\\s+/g, '-') // 공백(연속 포함) → 하이픈\n .replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, '') // 허용 문자 외 제거\n .replace(/-+/g, '-') // 연속 하이픈 → 단일 하이픈\n .replace(/^-+|-+$/g, '') // 앞뒤 하이픈 제거\n}\n\n/** Storage 경로/파일명에 안전한 문자열로 변환 (한글은 유지) */\nexport function safeFileName(name: string, maxLength = 60): string {\n return name.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, '-').slice(0, maxLength)\n}\n","/**\n * 에디터 본문(markdown 또는 html)에서 이미지 URL 추출.\n * 고아 파일 정리 스케줄러·저장 시 참조 비교에 사용.\n */\nexport function extractEditorImageUrls(content: string): string[] {\n if (!content) return []\n const urls = new Set<string>()\n\n for (const m of content.matchAll(/!\\[[^\\]]*]\\((https?:\\/\\/[^)\\s]+)\\)/g)) {\n if (m[1]) urls.add(m[1].replaceAll('&amp;', '&'))\n }\n for (const m of content.matchAll(/<img[^>]*\\ssrc=[\"'](https?:\\/\\/[^\"']+)[\"']/gi)) {\n if (m[1]) urls.add(m[1].replaceAll('&amp;', '&'))\n }\n\n return [...urls]\n}\n","/**\n * Firebase Storage download URL → object path.\n * 예: .../o/board%2Fposts%2Fid%2Fimages%2Fx.png?alt=media → board/posts/id/images/x.png\n */\nexport function storagePathFromDownloadUrl(url: string): string | null {\n try {\n const m = url.match(/\\/o\\/([^?]+)/)\n if (!m?.[1]) return null\n return decodeURIComponent(m[1])\n }\n catch {\n return null\n }\n}\n\n/** path 에서 posts/{ns}/... 의 ns 추출 */\nexport function postNamespaceFromStoragePath(path: string): string | null {\n const m = path.match(/(?:^|\\/)posts\\/([^/]+)\\//)\n return m?.[1] ?? null\n}\n","/**\n * 게시글 본문 HTML 검증·파생 필드.\n * - 일반 보드: 글자 필수 (이미지만 불가)\n * - 이미지 리스트뷰 보드: 글자 + 이미지 필수, 제목은 본문에서 자동 생성\n */\n\n/** 미디어·태그를 제거하고 보이는 글자만 남긴다. */\nexport function plainTextFromHtml(html: string): string {\n return String(html || '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<img\\b[^>]*>/gi, ' ')\n .replace(/<iframe\\b[^>]*>[\\s\\S]*?<\\/iframe>/gi, ' ')\n .replace(/<video\\b[\\s\\S]*?<\\/video>/gi, ' ')\n .replace(/<audio\\b[\\s\\S]*?<\\/audio>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/&nbsp;/gi, ' ')\n .replace(/&#160;/g, ' ')\n .replace(/&[a-z]+;/gi, ' ')\n .replace(/&#\\d+;/g, ' ')\n // zero-width / soft hyphen 등\n .replace(/[\\u200B-\\u200D\\uFEFF\\u00AD]/g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/**\n * 이미지 보드 등 제목 미입력 시 본문 plain 앞부분을 title 로 쓴다.\n * summary(160) 보다 짧게 — 목록·탭·OG 한 줄용.\n */\nexport function titleFromBody(html: string, maxLength = 40): string {\n const plain = plainTextFromHtml(html)\n if (!plain) return ''\n if (plain.length <= maxLength) return plain\n // 단어 중간 자르기 완화: 마지막 공백 근처에서 끊기\n const slice = plain.slice(0, maxLength)\n const sp = slice.lastIndexOf(' ')\n const cut = sp >= Math.floor(maxLength * 0.6) ? slice.slice(0, sp) : slice\n return `${cut.trimEnd()}…`\n}\n\n/** 본문에 실제 글자가 있는지 (이미지·유튜브만이면 false) */\nexport function hasBodyText(html: string): boolean {\n return plainTextFromHtml(html).length > 0\n}\n\n/** 본문 HTML 또는 이미지 첨부 중 하나라도 있으면 true */\nexport function hasBodyImage(\n html: string,\n attachments?: Array<{ type?: string } | null> | null,\n): boolean {\n if (/<img\\b/i.test(String(html || ''))) return true\n return (attachments || []).some(a => !!a?.type?.startsWith('image/'))\n}\n\nexport function isContentEmpty(html: string): boolean {\n return !hasBodyText(html)\n}\n","import type { Attachment } from '../types'\n\nfunction decodeHtmlAttribute(value: string): string {\n return value.replace(/&amp;/g, '&').replace(/&quot;/g, '\"').replace(/&#39;/g, \"'\")\n}\n\n/** YouTube watch/embed/shorts/youtu.be URL 에서 video id 추출. 아니면 undefined. */\nexport function youtubeId(value: string | null | undefined): string | undefined {\n if (!value) return undefined\n try {\n const url = new URL(decodeHtmlAttribute(value))\n const host = url.hostname.toLowerCase().replace(/^www\\./, '')\n if (host === 'youtu.be') return url.pathname.split('/').filter(Boolean)[0]\n if (host.endsWith('youtube.com') || host === 'youtube-nocookie.com') {\n if (url.pathname.startsWith('/embed/')) return url.pathname.split('/')[2]\n if (url.pathname.startsWith('/shorts/')) return url.pathname.split('/')[2]\n return url.searchParams.get('v') || undefined\n }\n }\n catch {\n return undefined\n }\n}\n\n/** 영상 목록 카드용 커버: YouTube 썸네일 우선, 없으면 previewImage/이미지 첨부. */\nexport function videoListCoverUrl(post: {\n videoUrl?: string\n previewImage?: string\n attachments?: Attachment[]\n}): string | undefined {\n const id = youtubeId(post.videoUrl)\n if (id) return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`\n return post.previewImage\n || post.attachments?.find(item => item.type.startsWith('image/'))?.url\n}\n\nexport function buildPostPreview(content: string, attachments: Attachment[] = []): {\n summary: string\n previewImage?: string\n videoUrl?: string\n} {\n const imageMatch = content.match(/<img\\b[^>]*\\bsrc=[\"']([^\"']+)[\"']/i)\n const iframeMatch = content.match(/<iframe\\b[^>]*\\bsrc=[\"']([^\"']+)[\"']/i)\n const attachmentImage = attachments.find(item => item.type.startsWith('image/'))?.url\n const attachmentVideo = attachments.find(item => item.type.startsWith('video/'))?.url\n const videoUrl = iframeMatch ? decodeHtmlAttribute(iframeMatch[1]!) : attachmentVideo\n const videoId = videoUrl ? youtubeId(videoUrl) : undefined\n const previewImage = imageMatch\n ? decodeHtmlAttribute(imageMatch[1]!)\n : attachmentImage || (videoId ? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` : undefined)\n const summary = content\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 160)\n\n return { summary, ...(previewImage ? { previewImage } : {}), ...(videoUrl ? { videoUrl } : {}) }\n}\n","import { computed } from 'vue'\nimport { useCollection, useFirestore } from 'vuefire'\nimport {\n deleteDoc,\n getDocs,\n limit as fbLimit,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n startAfter,\n writeBatch,\n} from 'firebase/firestore'\nimport type { QueryDocumentSnapshot } from 'firebase/firestore'\nimport { useBoardPathConfig } from '../config'\nimport {\n boardDoc,\n boardPostsCol,\n boardSettingsDoc,\n boardsCol,\n boardSsrKey,\n} from '../utils/boardPaths'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport { slugify } from '../utils/slugify'\nimport type { BoardModel, BoardVisibility } from '../types'\n\n/** 필요할 때 호스트가 명시적으로 시드할 수 있는 예시 보드. 자동 적용하지 않는다. */\nexport const DEFAULT_BOARDS: BoardModel[] = [\n { id: 'free', label: '자유', listView: 'default', writeRole: 'user', commentWriteRole: 'user', visibility: 'public', order: 0 },\n { id: 'notice', label: '공지', listView: 'default', writeRole: 'admin', commentWriteRole: 'user', visibility: 'public', order: 1 },\n { id: 'question', label: '질문', listView: 'default', writeRole: 'user', commentWriteRole: 'user', visibility: 'public', order: 2 },\n]\n\n/** @deprecated DEFAULT_BOARDS */\nexport const DEFAULT_CATEGORIES = DEFAULT_BOARDS\n\nfunction normalizeVisibility(value: unknown): BoardVisibility {\n return value === 'hidden' ? 'hidden' : 'public'\n}\n\nfunction mapBoardDoc(id: string, data: Record<string, unknown>, index: number): BoardModel {\n return {\n id,\n label: typeof data.label === 'string' && data.label.trim() ? data.label.trim() : id,\n order: typeof data.order === 'number' ? data.order : index,\n description: (typeof data.description === 'string' ? data.description : '') || '',\n visibility: normalizeVisibility(data.visibility),\n listView: (data.listView as BoardModel['listView']) ?? 'default',\n writeRole: (data.writeRole as BoardModel['writeRole']) ?? 'user',\n commentWriteRole: (data.commentWriteRole as BoardModel['commentWriteRole']) ?? 'user',\n allowedStaffUids: Array.isArray(data.allowedStaffUids) ? data.allowedStaffUids as string[] : [],\n }\n}\n\n/**\n * 보드 목록·설정.\n * - 목록: memiBoards 컬렉션 (부모 문서에 메타 복제)\n * - 설정 원본: memiBoards/{boardId}/settings/config\n */\nexport function useMemiBoardSettings() {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const { isSignedIn } = useMemiBoardAuth()\n\n const boardsQuery = computed(() => query(boardsCol(db, cfg()), orderBy('order', 'asc')))\n const { data: boardDocs, pending: settingsPending } = useCollection(boardsQuery, {\n ssrKey: boardSsrKey(cfg(), 'boards'),\n })\n\n const boards = computed<BoardModel[]>(() =>\n boardDocs.value.map((item, index) =>\n mapBoardDoc(item.id, item as unknown as Record<string, unknown>, index),\n ),\n )\n\n /** @deprecated boards — 카테고리 개념 제거 */\n const categories = boards\n\n const publicBoards = computed(() =>\n boards.value.filter(board => board.visibility !== 'hidden'),\n )\n /** @deprecated publicBoards */\n const publicCategories = publicBoards\n\n function boardLabel(id: string | undefined): string | undefined {\n if (!id) return undefined\n return boards.value.find(board => board.id === id)?.label ?? id\n }\n /** @deprecated boardLabel */\n const categoryLabel = boardLabel\n\n function boardDescription(id: string | undefined): string | undefined {\n if (!id) return undefined\n const value = boards.value.find(board => board.id === id)?.description?.trim()\n return value || undefined\n }\n /** @deprecated boardDescription */\n const categoryDescription = boardDescription\n\n function boardVisibility(id: string | undefined): BoardVisibility {\n if (!id) return 'public'\n return boards.value.find(board => board.id === id)?.visibility ?? 'public'\n }\n /** @deprecated boardVisibility */\n const categoryVisibility = boardVisibility\n\n function isBoardHidden(id: string | undefined): boolean {\n return boardVisibility(id) === 'hidden'\n }\n /** @deprecated isBoardHidden */\n const isCategoryHidden = isBoardHidden\n\n function getBoard(id: string | undefined): BoardModel | undefined {\n if (!id) return undefined\n return boards.value.find(board => board.id === id)\n }\n\n /** 숨김 전환 시 해당 보드 글 listed 일괄 맞춤 */\n async function syncPostsListedForBoard(boardId: string, listed: boolean): Promise<void> {\n const postsCol = boardPostsCol(db, cfg(), boardId)\n let cursor: QueryDocumentSnapshot | undefined\n for (;;) {\n const page = await getDocs(query(\n postsCol,\n orderBy('__name__'),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(400),\n ))\n if (page.empty) break\n const batch = writeBatch(db)\n for (const item of page.docs) {\n batch.update(item.ref, { listed })\n }\n await batch.commit()\n cursor = page.docs[page.docs.length - 1]\n if (page.docs.length < 400) break\n }\n }\n\n function settingsPayload(board: BoardModel, order: number) {\n return {\n label: board.label.trim(),\n description: (board.description ?? '').trim(),\n visibility: normalizeVisibility(board.visibility),\n listView: board.listView ?? 'default',\n writeRole: board.writeRole ?? 'user',\n commentWriteRole: board.commentWriteRole ?? 'user',\n allowedStaffUids: board.allowedStaffUids ?? [],\n order,\n updatedAt: serverTimestamp(),\n }\n }\n\n async function saveBoard(board: BoardModel, order = board.order ?? 0): Promise<void> {\n if (!isSignedIn.value) throw new Error('로그인이 필요합니다.')\n const id = board.id.trim()\n const label = board.label.trim()\n if (!id || !label) throw new Error('보드 ID와 이름을 입력해 주세요.')\n const visibility = normalizeVisibility(board.visibility)\n const previous = boards.value.find(item => item.id === id)\n const previousVisibility = previous?.visibility ?? 'public'\n const payload = settingsPayload({ ...board, label, visibility }, order)\n const c = cfg()\n // 부모 문서(목록) + settings/config(설정 원본)\n await setDoc(boardDoc(db, c, id), payload, { merge: true })\n await setDoc(boardSettingsDoc(db, c, id), payload, { merge: true })\n\n if (previousVisibility !== visibility || visibility === 'public') {\n await syncPostsListedForBoard(id, visibility !== 'hidden')\n }\n }\n\n /** @deprecated saveBoard */\n const saveCategory = saveBoard\n\n async function saveBoards(next: BoardModel[]): Promise<void> {\n if (!isSignedIn.value) throw new Error('로그인이 필요합니다.')\n const previousById = new Map(boards.value.map(item => [item.id, item.visibility ?? 'public']))\n const batch = writeBatch(db)\n const c = cfg()\n const visibilityChanges: Array<{ id: string, listed: boolean }> = []\n next.forEach((board, order) => {\n const id = board.id.trim()\n const label = board.label.trim()\n if (!id || !label) throw new Error('보드 ID와 이름을 입력해 주세요.')\n const visibility = normalizeVisibility(board.visibility)\n const payload = settingsPayload({ ...board, label, visibility }, order)\n batch.set(boardDoc(db, c, id), payload, { merge: true })\n batch.set(boardSettingsDoc(db, c, id), payload, { merge: true })\n const prev = previousById.get(id) ?? 'public'\n if (prev !== visibility) visibilityChanges.push({ id, listed: visibility !== 'hidden' })\n })\n await batch.commit()\n for (const change of visibilityChanges) {\n await syncPostsListedForBoard(change.id, change.listed)\n }\n }\n\n /** @deprecated saveBoards */\n const saveCategories = saveBoards\n\n async function deleteBoard(id: string): Promise<void> {\n if (!isSignedIn.value) throw new Error('로그인이 필요합니다.')\n const c = cfg()\n // 설정·부모 문서만 삭제 (posts 는 호스트/관리 작업으로 별도 정리)\n await deleteDoc(boardSettingsDoc(db, c, id)).catch(() => {})\n await deleteDoc(boardDoc(db, c, id))\n }\n\n /** @deprecated deleteBoard */\n const deleteCategory = deleteBoard\n\n async function ensureSettings(): Promise<void> {\n return Promise.resolve()\n }\n\n async function addBoard(label: string): Promise<string> {\n const trimmed = label.trim()\n if (!trimmed) throw new Error('보드 이름을 입력해 주세요.')\n if (!isSignedIn.value) throw new Error('로그인이 필요합니다.')\n const existing = boards.value.find(board => board.label === trimmed)\n if (existing) return existing.id\n\n const base = slugify(trimmed) || `board-${Date.now()}`\n let id = base\n let suffix = 2\n const used = new Set(boards.value.map(board => board.id))\n while (used.has(id)) id = `${base}-${suffix++}`\n await saveBoard({\n id,\n label: trimmed,\n visibility: 'public',\n listView: 'default',\n writeRole: 'user',\n commentWriteRole: 'user',\n order: boards.value.length,\n })\n return id\n }\n\n /** @deprecated addBoard */\n const addCategory = addBoard\n\n return {\n boards,\n publicBoards,\n boardLabel,\n boardDescription,\n boardVisibility,\n isBoardHidden,\n getBoard,\n categories,\n publicCategories,\n categoryLabel,\n categoryDescription,\n categoryVisibility,\n isCategoryHidden,\n settingsPending,\n ensureSettings,\n addBoard,\n addCategory,\n saveBoard,\n saveCategory,\n saveBoards,\n saveCategories,\n deleteBoard,\n deleteCategory,\n DEFAULT_BOARDS,\n DEFAULT_CATEGORIES,\n }\n}\n","import { computed, onScopeDispose, ref, toValue, watch } from 'vue'\nimport type { MaybeRefOrGetter, Ref } from 'vue'\nimport {\n doc,\n documentId,\n getDoc,\n getDocs,\n onSnapshot,\n setDoc,\n updateDoc,\n deleteDoc,\n query,\n where,\n orderBy,\n limit as fbLimit,\n limitToLast,\n startAfter,\n endBefore,\n serverTimestamp,\n writeBatch,\n deleteField,\n} from 'firebase/firestore'\nimport type { QueryConstraint, Unsubscribe } from 'firebase/firestore'\nimport type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore'\nimport { getStorage, ref as storageRef, listAll, deleteObject } from 'firebase/storage'\nimport type { StorageReference } from 'firebase/storage'\nimport { useDocument, useFirebaseApp, useFirestore } from 'vuefire'\nimport { slugify } from '../utils/slugify'\nimport { extractEditorImageUrls } from '../utils/extractEditorImageUrls'\nimport { postNamespaceFromStoragePath, storagePathFromDownloadUrl } from '../utils/storagePath'\nimport { hasBodyImage, hasBodyText, titleFromBody } from '../utils/postBody'\nimport { useBoardPathConfig } from '../config'\nimport { buildPostPreview } from '../utils/postPreview'\nimport {\n boardPostBodyDoc,\n boardPostCommentsCol,\n boardPostDoc,\n boardPostStorageFolder,\n boardPostsCol,\n} from '../utils/boardPaths'\nimport { useMemiBoardSettings } from './useMemiBoardSettings'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport type { Attachment, PostDetail, PostModel } from '../types'\n\nasync function deleteStorageFolder(folderRef: StorageReference): Promise<void> {\n const list = await listAll(folderRef).catch(() => ({ items: [], prefixes: [] }))\n await Promise.all([\n ...list.items.map(item => deleteObject(item)),\n ...list.prefixes.map(prefix => deleteStorageFolder(prefix)),\n ])\n}\n\nasync function deleteStoragePaths(storage: ReturnType<typeof getStorage>, paths: Iterable<string>): Promise<void> {\n const unique = [...new Set([...paths].filter(Boolean))]\n await Promise.all(\n unique.map(path => deleteObject(storageRef(storage, path)).catch(() => {})),\n )\n}\n\nexport interface CreatePostInput {\n postId?: string\n title: string\n content: string\n tags?: string[]\n attachments?: Attachment[]\n authorUid: string\n authorName: string | null\n authorPhoto: string | null\n moderationModel?: string\n}\n\nexport interface UpdatePostInput {\n title: string\n content: string\n tags?: string[]\n attachments?: Attachment[]\n}\n\nexport type GetPostBySlugResult =\n | { status: 'ok', post: PostDetail }\n | { status: 'not-found' }\n | { status: 'permission-denied' }\n | { status: 'error', message: string }\n\nexport function isFirestorePermissionDenied(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false\n const code = 'code' in error ? String((error as { code?: string }).code) : ''\n return code === 'permission-denied' || code === 'firestore/permission-denied'\n}\n\nfunction resolveBoardId(boardId: MaybeRefOrGetter<string>): string {\n const id = toValue(boardId)?.trim()\n if (!id) throw new Error('[memi-board] boardId 가 필요합니다.')\n return id\n}\n\n/** 목록은 posts 메타만, 본문(body/main)은 상세에서만. */\nexport function useMemiBoardPosts(boardId: MaybeRefOrGetter<string>) {\n const db = useFirestore()\n const app = useFirebaseApp()\n const { isBoardHidden, getBoard } = useMemiBoardSettings()\n const cfg = () => useBoardPathConfig()\n const bid = () => resolveBoardId(boardId)\n\n const postsCol = () => boardPostsCol(db, cfg(), bid())\n const postDoc = (id: string) => boardPostDoc(db, cfg(), bid(), id)\n const bodyDoc = (id: string) => boardPostBodyDoc(db, cfg(), bid(), id)\n const commentsCol = (id: string) => boardPostCommentsCol(db, cfg(), bid(), id)\n\n function listedForBoard(): boolean {\n return !isBoardHidden(bid())\n }\n\n function isImageListBoard(): boolean {\n return getBoard(bid())?.listView === 'image'\n }\n\n function assertPostContent(\n input: { title?: string, content: string, attachments?: Attachment[] },\n ) {\n const imageBoard = isImageListBoard()\n if (!hasBodyText(input.content)) {\n throw new Error(\n imageBoard\n ? '내용을 입력해 주세요.'\n : '본문에 글자를 입력해 주세요. 이미지나 첨부만으로는 게시할 수 없습니다.',\n )\n }\n if (imageBoard) {\n if (!hasBodyImage(input.content, input.attachments)) {\n throw new Error('사진을 올려 주세요.')\n }\n return\n }\n if (!input.title?.trim()) {\n throw new Error('제목을 입력해 주세요.')\n }\n }\n\n function createPostId(): string {\n return doc(postsCol()).id\n }\n\n async function getPosts(opts: {\n pageSize?: number\n cursor?: QueryDocumentSnapshot<DocumentData>\n publicOnly?: boolean\n } = {}) {\n const pageSize = opts.pageSize ?? 20\n const constraints: QueryConstraint[] = [where('isPublished', '==', true)]\n if (opts.publicOnly !== false) {\n constraints.push(where('listed', '==', true))\n }\n constraints.push(orderBy('createdAt', 'desc'))\n if (opts.cursor) constraints.push(startAfter(opts.cursor))\n constraints.push(fbLimit(pageSize + 1))\n\n const snapshot = await getDocs(query(postsCol(), ...constraints))\n const docs = snapshot.docs.slice(0, pageSize)\n return {\n posts: docs.map(d => ({ id: d.id, ...d.data() }) as PostModel),\n cursor: docs[docs.length - 1] as QueryDocumentSnapshot<DocumentData> | undefined,\n hasMore: snapshot.docs.length > pageSize,\n }\n }\n\n async function getPost(id: string): Promise<PostDetail | null> {\n const [metaSnap, bodySnap] = await Promise.all([getDoc(postDoc(id)), getDoc(bodyDoc(id))])\n if (!metaSnap.exists()) return null\n return {\n id: metaSnap.id,\n ...metaSnap.data(),\n content: bodySnap.exists() ? ((bodySnap.data() as { content: string }).content ?? '') : '',\n } as PostDetail\n }\n\n /** 보드 내 slug 로 조회 */\n async function getPostBySlug(slug: string): Promise<GetPostBySlugResult> {\n try {\n const snapshot = await getDocs(query(\n postsCol(),\n where('slug', '==', slug.trim()),\n fbLimit(1),\n ))\n const meta = snapshot.docs[0]\n if (!meta) return { status: 'not-found' }\n try {\n const post = await getPost(meta.id)\n if (!post) return { status: 'not-found' }\n return { status: 'ok', post }\n }\n catch (cause) {\n if (isFirestorePermissionDenied(cause)) return { status: 'permission-denied' }\n return { status: 'error', message: cause instanceof Error ? cause.message : String(cause) }\n }\n }\n catch (cause) {\n if (isFirestorePermissionDenied(cause)) return { status: 'permission-denied' }\n return { status: 'error', message: cause instanceof Error ? cause.message : String(cause) }\n }\n }\n\n async function getAdjacentPosts(current: PostModel): Promise<{\n previous: PostModel | null\n next: PostModel | null\n }> {\n if (!current.createdAt) return { previous: null, next: null }\n try {\n const base: QueryConstraint[] = [\n where('isPublished', '==', true),\n ...((current.listed !== false) ? [where('listed', '==', true)] : []),\n orderBy('createdAt', 'desc'),\n ]\n const [previousSnapshot, nextSnapshot] = await Promise.all([\n getDocs(query(postsCol(), ...base, startAfter(current.createdAt), fbLimit(1))),\n getDocs(query(postsCol(), ...base, endBefore(current.createdAt), limitToLast(1))),\n ])\n const mapPost = (snapshot: typeof previousSnapshot): PostModel | null => {\n const item = snapshot.docs[0]\n return item ? ({ id: item.id, ...item.data() } as PostModel) : null\n }\n return {\n previous: mapPost(previousSnapshot),\n next: mapPost(nextSnapshot),\n }\n }\n catch (cause) {\n if (import.meta.dev) console.warn('[memi-board] getAdjacentPosts failed', cause)\n return { previous: null, next: null }\n }\n }\n\n async function createPost(input: CreatePostInput): Promise<string> {\n assertPostContent(input)\n const id = input.postId || createPostId()\n const imageBoard = isImageListBoard()\n let slug: string\n let title: string\n if (imageBoard) {\n slug = id\n title = titleFromBody(input.content)\n }\n else {\n title = input.title.trim()\n const baseSlug = slugify(title) || 'post'\n slug = baseSlug\n let counter = 1\n while (!(await getDocs(query(\n postsCol(),\n where('slug', '==', slug),\n fbLimit(1),\n ))).empty) {\n counter++\n slug = `${baseSlug}-${counter}`\n }\n }\n\n const preview = buildPostPreview(input.content, input.attachments)\n const listed = listedForBoard()\n await setDoc(postDoc(id), {\n slug,\n title,\n ...preview,\n tags: input.tags ?? [],\n attachments: input.attachments ?? [],\n commentCount: 0,\n likeCount: 0,\n viewCount: 0,\n listed,\n isPublished: false,\n authorUid: input.authorUid,\n authorName: input.authorName,\n authorPhoto: input.authorPhoto,\n moderationStatus: 'approved',\n moderationModel: input.moderationModel ?? null,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n await setDoc(bodyDoc(id), { content: input.content })\n return slug\n }\n\n async function updatePost(id: string, input: UpdatePostInput): Promise<void> {\n assertPostContent(input)\n const imageBoard = isImageListBoard()\n const title = imageBoard ? titleFromBody(input.content) : input.title.trim()\n const preview = buildPostPreview(input.content, input.attachments)\n const listed = listedForBoard()\n await Promise.all([\n updateDoc(postDoc(id), {\n title,\n ...preview,\n previewImage: preview.previewImage ? preview.previewImage : deleteField(),\n videoUrl: preview.videoUrl ? preview.videoUrl : deleteField(),\n tags: input.tags ?? [],\n listed,\n attachments: input.attachments ?? [],\n updatedAt: serverTimestamp(),\n }),\n setDoc(bodyDoc(id), { content: input.content }, { merge: true }),\n ])\n }\n\n async function publishPost(id: string): Promise<void> {\n await updateDoc(postDoc(id), {\n isPublished: true,\n publishedAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n }\n\n async function deletePost(id: string): Promise<void> {\n const [metaSnap, bodySnap] = await Promise.all([\n getDoc(postDoc(id)),\n getDoc(bodyDoc(id)),\n ])\n if (!metaSnap.exists()) return\n\n const meta = metaSnap.data() as PostModel\n const bodyContent = bodySnap.exists()\n ? String((bodySnap.data() as { content?: string }).content ?? '')\n : ''\n\n const commentsSnap = await getDocs(commentsCol(id))\n for (let offset = 0; offset < commentsSnap.docs.length; offset += 450) {\n const batch = writeBatch(db)\n commentsSnap.docs.slice(offset, offset + 450).forEach(d => batch.delete(d.ref))\n await batch.commit()\n }\n\n const storage = getStorage(app)\n const extraPaths = new Set<string>()\n const extraNamespaces = new Set<string>()\n\n for (const att of meta.attachments ?? []) {\n if (att?.path) {\n extraPaths.add(att.path)\n const ns = postNamespaceFromStoragePath(att.path)\n if (ns && ns !== id) extraNamespaces.add(ns)\n }\n }\n\n for (const url of extractEditorImageUrls(bodyContent)) {\n const path = storagePathFromDownloadUrl(url)\n if (!path) continue\n extraPaths.add(path)\n const thumbGuess = path\n .replace(/\\/images\\/([^/]+)$/, '/images/thumbnails/$1')\n .replace(/\\.[^.]+$/, '.jpg')\n if (thumbGuess !== path && thumbGuess.includes('/images/thumbnails/')) {\n extraPaths.add(thumbGuess)\n }\n const m = path.match(/^(.*\\/images\\/)([^/]+)\\.[^.]+$/)\n if (m && !path.includes('/thumbnails/')) {\n extraPaths.add(`${m[1]}thumbnails/${m[2]}.jpg`)\n }\n const ns = postNamespaceFromStoragePath(path)\n if (ns && ns !== id) extraNamespaces.add(ns)\n }\n\n await deleteDoc(bodyDoc(id))\n const board = bid()\n await deleteStorageFolder(storageRef(storage, boardPostStorageFolder(cfg(), board, id)))\n for (const ns of extraNamespaces) {\n await deleteStorageFolder(storageRef(storage, boardPostStorageFolder(cfg(), board, ns)))\n }\n await deleteStoragePaths(storage, extraPaths)\n await deleteDoc(postDoc(id))\n }\n\n return {\n createPostId,\n getPosts,\n getPost,\n getPostBySlug,\n getAdjacentPosts,\n createPost,\n updatePost,\n publishPost,\n deletePost,\n }\n}\n\nconst POST_PAGE_SIZE = 10\n\nexport function useMemiBoardPostList(\n boardId: MaybeRefOrGetter<string>,\n options: { pageSize?: number, publicOnly?: boolean | Ref<boolean> } = {},\n) {\n const db = useFirestore()\n const { isAdmin, isStaff, user } = useMemiBoardAuth()\n const cfg = () => useBoardPathConfig()\n const pageSize = options.pageSize ?? POST_PAGE_SIZE\n const uidValue = computed(() => user.value?.uid ?? '')\n\n const boardIdValue = computed(() => resolveBoardId(boardId))\n const postsCol = () => boardPostsCol(db, cfg(), boardIdValue.value)\n\n const publicOnlyValue = computed(() => {\n if (options.publicOnly === undefined) {\n return !(isAdmin.value || isStaff.value)\n }\n return typeof options.publicOnly === 'boolean' ? options.publicOnly : options.publicOnly.value\n })\n\n const headPosts = ref<PostModel[]>([])\n const olderPosts = ref<PostModel[]>([])\n /** 본인의 미게시 초안 — 목록 쿼리(isPublished==true)엔 안 걸리므로 별도 구독해 얹는다. */\n const ownDraftPosts = ref<PostModel[]>([])\n const headPending = ref(true)\n const hasMore = ref(true)\n const loadingMore = ref(false)\n const loadError = ref('')\n let headTailCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let olderCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stopHeadSubscription: Unsubscribe | undefined\n let stopOwnDraftSubscription: Unsubscribe | undefined\n\n function createdAtMs(post: PostModel): number {\n return post.createdAt?.toMillis?.() ?? 0\n }\n\n const posts = computed(() => {\n const byId = new Map<string, PostModel>()\n for (const post of [...olderPosts.value, ...headPosts.value, ...ownDraftPosts.value]) {\n if (post.id) byId.set(post.id, post)\n }\n return [...byId.values()].sort((a, b) =>\n createdAtMs(b) - createdAtMs(a) || String(b.id).localeCompare(String(a.id)),\n )\n })\n\n function baseConstraints(): QueryConstraint[] {\n const constraints: QueryConstraint[] = [where('isPublished', '==', true)]\n if (publicOnlyValue.value) constraints.push(where('listed', '==', true))\n return constraints\n }\n\n function startHeadSubscription() {\n stopHeadSubscription?.()\n stopHeadSubscription = onSnapshot(query(\n postsCol(),\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n fbLimit(pageSize),\n ), (snapshot) => {\n headPosts.value = snapshot.docs.map(item => ({ id: item.id, ...item.data() }) as PostModel)\n headTailCursor = snapshot.docs.at(-1) ?? null\n if (!olderCursor) hasMore.value = snapshot.docs.length >= pageSize\n headPending.value = false\n }, (cause) => {\n console.error('[memi-board:postList] onSnapshot failed', cause)\n loadError.value = (cause as Error).message || String(cause)\n headPending.value = false\n })\n }\n\n function startOwnDraftSubscription() {\n stopOwnDraftSubscription?.()\n const uid = uidValue.value\n if (!uid) {\n ownDraftPosts.value = []\n return\n }\n stopOwnDraftSubscription = onSnapshot(query(\n postsCol(),\n where('authorUid', '==', uid),\n where('isPublished', '==', false),\n orderBy('createdAt', 'desc'),\n fbLimit(20),\n ), (snapshot) => {\n ownDraftPosts.value = snapshot.docs.map(item => ({ id: item.id, ...item.data() }) as PostModel)\n }, (cause) => {\n console.error('[memi-board:postList] own draft onSnapshot failed', cause)\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loadingMore.value || !hasMore.value || headPending.value) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const cursor = olderCursor ?? headTailCursor\n const constraints = [\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(pageSize + 1),\n ]\n const snapshot = await getDocs(query(postsCol(), ...constraints))\n const pageDocs = snapshot.docs.slice(0, pageSize)\n const page = pageDocs.map(item => ({ id: item.id, ...item.data() }) as PostModel)\n const existingIds = new Set([...olderPosts.value, ...headPosts.value].map(post => post.id))\n olderPosts.value.push(...page.filter(post => !existingIds.has(post.id)))\n olderCursor = pageDocs.at(-1) ?? olderCursor\n hasMore.value = snapshot.docs.length > pageSize\n }\n catch (e) {\n loadError.value = (e as Error).message || String(e)\n console.error('[memi-board:postList] loadMore failed', e)\n }\n finally {\n loadingMore.value = false\n }\n }\n\n function reset() {\n stopHeadSubscription?.()\n headPosts.value = []\n olderPosts.value = []\n headTailCursor = null\n olderCursor = null\n hasMore.value = true\n loadError.value = ''\n headPending.value = true\n startHeadSubscription()\n startOwnDraftSubscription()\n }\n\n watch([boardIdValue, publicOnlyValue, uidValue], reset, { immediate: true })\n onScopeDispose(() => {\n stopHeadSubscription?.()\n stopOwnDraftSubscription?.()\n })\n\n return {\n posts,\n postsPending: headPending,\n hasMore,\n loadingMore,\n loadError,\n loadMore,\n }\n}\n\nexport function useMemiBoardPost(\n boardId: MaybeRefOrGetter<string>,\n postId: string | Ref<string>,\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n\n const bid = computed(() => resolveBoardId(boardId))\n const id = computed(() => (typeof postId === 'string' ? postId : postId.value))\n const postRef = computed(() => boardPostDoc(db, cfg(), bid.value, id.value))\n const bodyRef = computed(() => boardPostBodyDoc(db, cfg(), bid.value, id.value))\n\n const { data: meta, pending: metaPending } = useDocument<PostModel>(postRef)\n const { data: body, pending: bodyPending } = useDocument<{ content: string }>(bodyRef)\n\n const post = computed<PostDetail | null>(() => {\n if (!meta.value) return null\n return { ...meta.value, id: id.value, content: body.value?.content ?? '' } as PostDetail\n })\n const pending = computed(() => metaPending.value || bodyPending.value)\n\n return { post, pending }\n}\n","import { computed, onScopeDispose, ref, toValue, watch } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\nimport {\n collectionGroup,\n documentId,\n getDocs,\n limit as fbLimit,\n onSnapshot,\n orderBy,\n query,\n startAfter,\n where,\n} from 'firebase/firestore'\nimport type { DocumentData, QueryConstraint, QueryDocumentSnapshot, Unsubscribe } from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport type { UserPostModel } from '../types'\n\nconst USER_POST_PAGE_SIZE = 10\n\n/**\n * 작성자 uid로 모든 보드를 가로질러 글을 모은다 (collectionGroup 'posts').\n * 공개 글만 — canReadPost rules와 같은 기준(listed && isPublished)이라\n * 본인이 아니면 숨김 보드·미게시 글은 애초에 안 온다.\n */\nexport function useMemiBoardUserPosts(\n uid: MaybeRefOrGetter<string>,\n options: { pageSize?: number } = {},\n) {\n const db = useFirestore()\n const pageSize = options.pageSize ?? USER_POST_PAGE_SIZE\n const uidValue = computed(() => toValue(uid)?.trim() || '')\n\n const headPosts = ref<UserPostModel[]>([])\n const olderPosts = ref<UserPostModel[]>([])\n const headPending = ref(true)\n const hasMore = ref(true)\n const loadingMore = ref(false)\n const loadError = ref('')\n let headTailCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let olderCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stopHeadSubscription: Unsubscribe | undefined\n\n function mapDoc(item: QueryDocumentSnapshot<DocumentData>): UserPostModel {\n const boardId = item.ref.parent.parent?.id ?? ''\n return { id: item.id, boardId, ...item.data() } as UserPostModel\n }\n\n function createdAtMs(post: UserPostModel): number {\n return post.createdAt?.toMillis?.() ?? 0\n }\n\n const posts = computed(() => {\n const byId = new Map<string, UserPostModel>()\n for (const post of [...olderPosts.value, ...headPosts.value]) {\n if (post.id) byId.set(post.id, post)\n }\n return [...byId.values()].sort((a, b) =>\n createdAtMs(b) - createdAtMs(a) || String(b.id).localeCompare(String(a.id)),\n )\n })\n\n function baseConstraints(): QueryConstraint[] {\n return [\n where('authorUid', '==', uidValue.value),\n where('isPublished', '==', true),\n where('listed', '==', true),\n ]\n }\n\n function startHeadSubscription() {\n stopHeadSubscription?.()\n if (!uidValue.value) {\n headPending.value = false\n return\n }\n stopHeadSubscription = onSnapshot(query(\n collectionGroup(db, 'posts'),\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n fbLimit(pageSize),\n ), (snapshot) => {\n headPosts.value = snapshot.docs.map(mapDoc)\n headTailCursor = snapshot.docs.at(-1) ?? null\n if (!olderCursor) hasMore.value = snapshot.docs.length >= pageSize\n headPending.value = false\n }, (cause) => {\n console.error('[memi-board:userPosts] onSnapshot failed', cause)\n loadError.value = (cause as Error).message || String(cause)\n headPending.value = false\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loadingMore.value || !hasMore.value || headPending.value || !uidValue.value) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const cursor = olderCursor ?? headTailCursor\n const constraints = [\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(pageSize + 1),\n ]\n const snapshot = await getDocs(query(collectionGroup(db, 'posts'), ...constraints))\n const pageDocs = snapshot.docs.slice(0, pageSize)\n const page = pageDocs.map(mapDoc)\n const existingIds = new Set([...olderPosts.value, ...headPosts.value].map(post => post.id))\n olderPosts.value.push(...page.filter(post => !existingIds.has(post.id)))\n olderCursor = pageDocs.at(-1) ?? olderCursor\n hasMore.value = snapshot.docs.length > pageSize\n }\n catch (e) {\n loadError.value = (e as Error).message || String(e)\n console.error('[memi-board:userPosts] loadMore failed', e)\n }\n finally {\n loadingMore.value = false\n }\n }\n\n function reset() {\n stopHeadSubscription?.()\n headPosts.value = []\n olderPosts.value = []\n headTailCursor = null\n olderCursor = null\n hasMore.value = true\n loadError.value = ''\n headPending.value = true\n startHeadSubscription()\n }\n\n watch(uidValue, reset, { immediate: true })\n onScopeDispose(() => stopHeadSubscription?.())\n\n return {\n posts,\n postsPending: headPending,\n hasMore,\n loadingMore,\n loadError,\n loadMore,\n }\n}\n\nexport type { UserPostModel }\n","import { computed, onScopeDispose, ref, toValue, watch } from 'vue'\nimport type { MaybeRefOrGetter, Ref } from 'vue'\nimport type { DocumentData, QueryDocumentSnapshot, Unsubscribe } from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport {\n collection,\n doc,\n documentId,\n getDocs,\n increment,\n limit,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n startAfter,\n updateDoc,\n where,\n writeBatch,\n} from 'firebase/firestore'\nimport { useBoardPathConfig } from '../config'\nimport {\n boardPostCommentDoc,\n boardPostCommentsCol,\n boardPostDoc,\n} from '../utils/boardPaths'\nimport type { CommentModel } from '../types'\n\nexport interface AddCommentInput {\n body: string\n authorUid: string\n authorName: string | null\n authorPhoto: string | null\n}\n\nexport interface AddReplyInput extends AddCommentInput {\n parent: CommentModel\n}\n\nconst COMMENT_PAGE_SIZE = 10\nconst REPLY_PAGE_SIZE = 5\nexport const COMMENT_BODY_MAX_LENGTH = 1_000\n\nfunction normalizedCommentBody(body: string): string {\n const trimmed = body.trim()\n if (!trimmed) throw new Error('댓글 내용을 입력해 주세요.')\n if (trimmed.length > COMMENT_BODY_MAX_LENGTH) {\n throw new Error(`댓글은 ${COMMENT_BODY_MAX_LENGTH.toLocaleString()}자까지 작성할 수 있습니다.`)\n }\n return trimmed\n}\n\n/** 10개씩 순차 조회한 뒤 마지막 페이지에서만 최신 1개를 실시간 구독한다. */\nexport function useMemiBoardComments(\n boardId: MaybeRefOrGetter<string>,\n postId: MaybeRefOrGetter<string>,\n options: { subscribe?: boolean } = {},\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n\n const bid = computed(() => toValue(boardId))\n const id = computed(() => toValue(postId))\n\n // limit(1) 리스너는 최신 댓글 알림 용도다. 쿼리 결과 교체로 기존 댓글이\n // 사라지지 않도록 수신 문서를 별도 배열에 누적한다.\n const liveComments = ref<CommentModel[]>([])\n // parentId 도입 전 댓글은 실시간 10개 슬롯과 분리해 최초 1회만 읽는다.\n const legacyComments = ref<CommentModel[]>([])\n const legacyPending = ref(options.subscribe !== false)\n const pagedComments = ref<CommentModel[]>([])\n const loadingMore = ref(false)\n const hasMore = ref(options.subscribe !== false)\n const initialPending = ref(options.subscribe !== false)\n let pageCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let liveEnableTimer: ReturnType<typeof setTimeout> | undefined\n let stopLiveSubscription: Unsubscribe | undefined\n\n function createdAtMs(comment: CommentModel): number {\n return comment.createdAt?.toMillis?.() ?? 0\n }\n\n const comments = computed(() => {\n const byId = new Map<string, CommentModel>()\n for (const comment of [...liveComments.value, ...legacyComments.value, ...pagedComments.value]) {\n if (comment.id) byId.set(comment.id, comment)\n }\n return [...byId.values()].sort((a, b) =>\n createdAtMs(a) - createdAtMs(b) || String(a.id).localeCompare(String(b.id)),\n )\n })\n\n watch([id, bid], async ([currentId]) => {\n pagedComments.value = []\n liveComments.value = []\n legacyComments.value = []\n pageCursor = null\n clearTimeout(liveEnableTimer)\n liveEnableTimer = undefined\n stopLiveSubscription?.()\n stopLiveSubscription = undefined\n hasMore.value = options.subscribe !== false\n if (options.subscribe === false) return\n\n legacyPending.value = true\n initialPending.value = true\n try {\n const snapshotPromise = getDocs(query(\n boardPostCommentsCol(db, cfg(), bid.value, currentId),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ))\n await Promise.all([snapshotPromise.then((snapshot) => {\n if (id.value !== currentId) return\n const allComments = snapshot.docs\n .map(item => ({ id: item.id, ...item.data() } as CommentModel))\n legacyComments.value = allComments.filter(comment => comment.parentId === undefined)\n }), loadMore()])\n if (id.value !== currentId) return\n }\n finally {\n if (id.value === currentId) {\n legacyPending.value = false\n initialPending.value = false\n }\n }\n }, { immediate: true })\n\n function startLiveSubscription() {\n stopLiveSubscription?.()\n stopLiveSubscription = onSnapshot(query(\n boardPostCommentsCol(db, cfg(), bid.value, id.value),\n where('parentId', '==', null),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n limit(1),\n ), (snapshot) => {\n const byId = new Map(liveComments.value.map(comment => [comment.id, comment]))\n for (const change of snapshot.docChanges()) {\n if (change.type === 'removed') continue\n byId.set(change.doc.id, { id: change.doc.id, ...change.doc.data() } as CommentModel)\n }\n liveComments.value = [...byId.values()]\n }, (cause) => {\n console.error('[memi-board:comments] onSnapshot failed', cause)\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loadingMore.value || !hasMore.value) return\n loadingMore.value = true\n try {\n const constraints = [\n where('parentId', '==', null),\n orderBy('createdAt', 'asc'),\n orderBy(documentId(), 'asc'),\n ...(pageCursor ? [startAfter(pageCursor)] : []),\n limit(COMMENT_PAGE_SIZE + 1),\n ]\n const snapshot = await getDocs(query(\n boardPostCommentsCol(db, cfg(), bid.value, id.value),\n ...constraints,\n ))\n const pageDocs = snapshot.docs.slice(0, COMMENT_PAGE_SIZE)\n const page = pageDocs.map(item => ({ id: item.id, ...item.data() } as CommentModel))\n const existing = new Set(pagedComments.value.map(comment => comment.id))\n pagedComments.value.push(...page.filter(comment => !existing.has(comment.id)))\n pageCursor = pageDocs.at(-1) ?? pageCursor\n if (snapshot.size <= COMMENT_PAGE_SIZE) {\n hasMore.value = false\n liveEnableTimer = setTimeout(() => {\n liveEnableTimer = undefined\n startLiveSubscription()\n }, 1_000)\n }\n }\n finally {\n loadingMore.value = false\n }\n }\n\n onScopeDispose(() => {\n clearTimeout(liveEnableTimer)\n stopLiveSubscription?.()\n })\n\n async function addComment(input: AddCommentInput): Promise<void> {\n const body = normalizedCommentBody(input.body)\n const batch = writeBatch(db)\n const commentRef = doc(boardPostCommentsCol(db, cfg(), bid.value, id.value))\n batch.set(commentRef, {\n postId: id.value,\n body,\n authorUid: input.authorUid,\n authorName: input.authorName,\n authorPhoto: input.authorPhoto,\n moderationStatus: 'approved',\n parentId: null,\n rootId: commentRef.id,\n depth: 0,\n replyToUid: null,\n replyToName: null,\n replyCount: 0,\n isReply: false,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n batch.update(boardPostDoc(db, cfg(), bid.value, id.value), { commentCount: increment(1) })\n await batch.commit()\n }\n\n async function addReply(input: AddReplyInput): Promise<void> {\n const body = normalizedCommentBody(input.body)\n if (!input.parent.id) throw new Error('답글 대상 댓글을 찾을 수 없습니다.')\n const batch = writeBatch(db)\n const commentsCol = boardPostCommentsCol(db, cfg(), bid.value, id.value)\n const replyRef = doc(commentsCol)\n const rootId = input.parent.parentId == null\n ? input.parent.id\n : input.parent.rootId\n if (!rootId) throw new Error('댓글 스레드를 찾을 수 없습니다.')\n const depth = Math.min((input.parent.depth ?? 0) + 1, 2)\n\n batch.set(replyRef, {\n postId: id.value,\n body,\n authorUid: input.authorUid,\n authorName: input.authorName,\n authorPhoto: input.authorPhoto,\n moderationStatus: 'approved',\n parentId: input.parent.id,\n rootId,\n depth,\n replyToUid: input.parent.authorUid,\n replyToName: input.parent.authorName,\n replyCount: 0,\n isReply: true,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n batch.update(doc(commentsCol, rootId), { replyCount: increment(1) })\n batch.update(boardPostDoc(db, cfg(), bid.value, id.value), { commentCount: increment(1) })\n await batch.commit()\n }\n\n async function deleteComment(comment: CommentModel): Promise<void> {\n if (!comment.id) return\n if (comment.parentId == null && (comment.replyCount ?? 0) > 0) {\n throw new Error('답글이 있는 댓글은 현재 삭제할 수 없습니다.')\n }\n const batch = writeBatch(db)\n batch.delete(boardPostCommentDoc(db, cfg(), bid.value, id.value, comment.id))\n if (comment.parentId && comment.rootId) {\n batch.update(boardPostCommentDoc(db, cfg(), bid.value, id.value, comment.rootId), { replyCount: increment(-1) })\n }\n batch.update(boardPostDoc(db, cfg(), bid.value, id.value), { commentCount: increment(-1) })\n await batch.commit()\n pagedComments.value = pagedComments.value.filter(item => item.id !== comment.id)\n legacyComments.value = legacyComments.value.filter(item => item.id !== comment.id)\n liveComments.value = liveComments.value.filter(item => item.id !== comment.id)\n }\n\n async function updateComment(commentId: string, body: string): Promise<void> {\n const trimmed = normalizedCommentBody(body)\n await updateDoc(boardPostCommentDoc(db, cfg(), bid.value, id.value, commentId), {\n body: trimmed,\n updatedAt: serverTimestamp(),\n })\n }\n\n async function setCommentBlinded(commentId: string, isBlinded: boolean, moderatorUid: string): Promise<void> {\n await updateDoc(boardPostCommentDoc(db, cfg(), bid.value, id.value, commentId), {\n isBlinded,\n moderatedAt: serverTimestamp(),\n moderatedBy: moderatorUid,\n })\n }\n\n return {\n comments,\n commentsPending: computed(() => initialPending.value || legacyPending.value),\n hasMore,\n loadingMore,\n loadMore,\n addComment,\n addReply,\n updateComment,\n setCommentBlinded,\n deleteComment,\n }\n}\n\n/** 펼친 스레드의 답글을 5개씩 일회성 조회한다. */\nexport function useMemiBoardReplies(boardId: string, postId: string, rootId: string) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const replies = ref<CommentModel[]>([])\n const loading = ref(false)\n const loaded = ref(false)\n const hasMore = ref(true)\n let cursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stopLiveSubscription: Unsubscribe | undefined\n\n function sortReplies(list: CommentModel[]) {\n return list.sort((a, b) =>\n (a.createdAt?.toMillis?.() ?? 0) - (b.createdAt?.toMillis?.() ?? 0)\n || String(a.id).localeCompare(String(b.id)),\n )\n }\n\n function startLiveSubscription() {\n if (stopLiveSubscription) return\n stopLiveSubscription = onSnapshot(query(\n boardPostCommentsCol(db, cfg(), boardId, postId),\n where('rootId', '==', rootId),\n where('isReply', '==', true),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n limit(1),\n ), (snapshot) => {\n const byId = new Map(replies.value.map(reply => [reply.id, reply]))\n for (const change of snapshot.docChanges()) {\n if (change.type === 'removed') continue\n byId.set(change.doc.id, { id: change.doc.id, ...change.doc.data() } as CommentModel)\n }\n replies.value = sortReplies([...byId.values()])\n }, (cause) => {\n console.error('[memi-board:replies] onSnapshot failed', cause)\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loading.value || !hasMore.value) return\n loading.value = true\n try {\n const constraints = [\n where('rootId', '==', rootId),\n where('isReply', '==', true),\n orderBy('createdAt', 'asc'),\n orderBy(documentId(), 'asc'),\n ...(cursor ? [startAfter(cursor)] : []),\n limit(REPLY_PAGE_SIZE + 1),\n ]\n const snapshot = await getDocs(query(\n boardPostCommentsCol(db, cfg(), boardId, postId),\n ...constraints,\n ))\n const existing = new Set(replies.value.map(reply => reply.id))\n const pageDocs = snapshot.docs.slice(0, REPLY_PAGE_SIZE)\n const page = pageDocs\n .map(item => ({ id: item.id, ...item.data() } as CommentModel))\n .filter(reply => !existing.has(reply.id))\n replies.value = sortReplies([...replies.value, ...page])\n cursor = pageDocs.at(-1) ?? cursor\n loaded.value = true\n if (snapshot.size <= REPLY_PAGE_SIZE) {\n hasMore.value = false\n startLiveSubscription()\n }\n }\n finally {\n loading.value = false\n }\n }\n\n function reset() {\n stopLiveSubscription?.()\n stopLiveSubscription = undefined\n replies.value = []\n cursor = null\n loaded.value = false\n hasMore.value = true\n }\n\n async function refresh(): Promise<void> {\n reset()\n await loadMore()\n }\n\n onScopeDispose(() => stopLiveSubscription?.())\n\n return { replies, loading, loaded, hasMore, loadMore, refresh }\n}\n","import { onScopeDispose, ref, watch } from 'vue'\nimport type { Unsubscribe } from 'firebase/firestore'\nimport { useFirestore, useCurrentUser } from 'vuefire'\nimport {\n increment,\n onSnapshot,\n runTransaction,\n serverTimestamp,\n} from 'firebase/firestore'\nimport { useBoardPathConfig } from '../config'\nimport { boardPostDoc, boardPostLikeDoc } from '../utils/boardPaths'\n\nexport function useMemiBoardLikes(boardId: string, postId: string) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const user = useCurrentUser()\n\n const isLiked = ref(false)\n const likePending = ref(false)\n let stopSubscription: Unsubscribe | undefined\n\n function likeDocRef(uid: string) {\n return boardPostLikeDoc(db, cfg(), boardId, postId, uid)\n }\n\n watch(user, (current) => {\n stopSubscription?.()\n stopSubscription = undefined\n if (!current) {\n isLiked.value = false\n return\n }\n stopSubscription = onSnapshot(likeDocRef(current.uid), (snap) => {\n isLiked.value = snap.exists()\n }, (cause) => {\n console.error('[memi-board:likes] onSnapshot failed', cause)\n })\n }, { immediate: true })\n\n onScopeDispose(() => stopSubscription?.())\n\n async function toggleLike(): Promise<boolean> {\n const uid = user.value?.uid\n if (!uid) throw new Error('로그인이 필요합니다.')\n if (likePending.value) return isLiked.value\n likePending.value = true\n try {\n const likeRef = likeDocRef(uid)\n const postRef = boardPostDoc(db, cfg(), boardId, postId)\n return await runTransaction(db, async (tx) => {\n const likeSnap = await tx.get(likeRef)\n if (likeSnap.exists()) {\n tx.delete(likeRef)\n tx.update(postRef, { likeCount: increment(-1) })\n return false\n }\n tx.set(likeRef, { uid, createdAt: serverTimestamp() })\n tx.update(postRef, { likeCount: increment(1) })\n return true\n })\n }\n finally {\n likePending.value = false\n }\n }\n\n return { isLiked, likePending, toggleLike }\n}\n","import { increment, updateDoc } from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { boardPostDoc } from '../utils/boardPaths'\n\n/**\n * 게시글 조회수 (보드 단위 posts).\n */\nexport function useMemiBoardViews() {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n\n function sessionKey(boardId: string, postId: string): string {\n const { boardsCollection } = cfg()\n return `memi-board:viewed:${boardsCollection}:${boardId}:${postId}`\n }\n\n function hasRecordedView(boardId: string, postId: string): boolean {\n if (typeof sessionStorage === 'undefined') return false\n try {\n return sessionStorage.getItem(sessionKey(boardId, postId)) === '1'\n }\n catch {\n return false\n }\n }\n\n function markRecorded(boardId: string, postId: string): void {\n if (typeof sessionStorage === 'undefined') return\n try {\n sessionStorage.setItem(sessionKey(boardId, postId), '1')\n }\n catch {\n // ignore\n }\n }\n\n async function recordView(boardId: string, postId: string): Promise<void> {\n const b = boardId?.trim()\n const id = postId?.trim()\n if (!b || !id) return\n if (hasRecordedView(b, id)) return\n markRecorded(b, id)\n try {\n await updateDoc(boardPostDoc(db, cfg(), b, id), {\n viewCount: increment(1),\n })\n }\n catch (error) {\n try {\n sessionStorage.removeItem(sessionKey(b, id))\n }\n catch {\n // ignore\n }\n if (import.meta.dev) {\n console.warn('[memi-board] recordView failed', error)\n }\n }\n }\n\n return {\n recordView,\n hasRecordedView,\n }\n}\n","export const MODERATION_SYSTEM = `당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"`\n\n/** 사용자 안내용 카테고리 라벨 */\nexport const MODERATION_CATEGORY_LABELS: Record<string, string> = {\n abuse: '욕설·비속어·모욕',\n spam: '스팸·광고',\n adult: '선정적·성인 내용',\n violence: '폭력·위협',\n other: '부적절한 내용',\n none: '',\n}\n\n/** 차단 사유를 사용자에게 읽기 쉽게 정리 */\nexport function formatModerationUserReason(opts: {\n reason?: string\n category?: string\n via?: string\n localHit?: string\n}): string {\n if (opts.via === 'local' && opts.localHit) {\n const hit = opts.localHit.trim()\n // 짧은 금칙어만 표시 (너무 길면 유형만)\n if (hit.length > 0 && hit.length <= 12) {\n return `「${hit}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.`\n }\n return '욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.'\n }\n\n const raw = (opts.reason || '').trim()\n if (raw) return raw\n\n const cat = opts.category && opts.category !== 'none'\n ? MODERATION_CATEGORY_LABELS[opts.category] || opts.category\n : ''\n if (cat) return `${cat}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.`\n return '게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.'\n}\n\n/** API 없이도 1차로 걸러내는 한국어/영어 비속어 (부분 문자열 매칭) */\nexport const DEFAULT_LOCAL_BLOCKLIST = [\n '씨발', '시발', '시이발', '씨팔', '시팔', '병신', '븅신', '지랄', '좆', '존나',\n '개새끼', '개새', '새끼', '꺼져', '닥쳐', '씹', '니미', '느금', '애미', '애비',\n 'fuck', 'shit', 'bitch', 'asshole',\n]\n\nexport function buildLocalBlockRegex(patterns: string[]): RegExp {\n return new RegExp(patterns.join('|'), 'i')\n}\n\nconst CATEGORIES = new Set(['none', 'abuse', 'spam', 'adult', 'violence', 'other'])\n\nexport function parseModerationJson(raw: string): { flagged: boolean, category: string, reason: string } | null {\n const trimmed = raw.trim()\n if (!trimmed) return null\n\n let parsed: unknown\n try {\n parsed = JSON.parse(trimmed)\n }\n catch {\n const match = trimmed.match(/\\{[\\s\\S]*\\}/)\n if (!match) return null\n try {\n parsed = JSON.parse(match[0])\n }\n catch {\n return null\n }\n }\n\n if (typeof parsed !== 'object' || parsed === null) return null\n const obj = parsed as Record<string, unknown>\n if (typeof obj.flagged !== 'boolean') return null\n\n const category = typeof obj.category === 'string' && CATEGORIES.has(obj.category) ? obj.category : (obj.flagged ? 'other' : 'none')\n const reason = typeof obj.reason === 'string' ? obj.reason : ''\n return { flagged: obj.flagged, category, reason }\n}\n","import { getAI, getGenerativeModel, GoogleAIBackend, Schema } from 'firebase/ai'\nimport { useFirebaseApp } from 'vuefire'\nimport {\n buildLocalBlockRegex,\n DEFAULT_LOCAL_BLOCKLIST,\n formatModerationUserReason,\n MODERATION_SYSTEM,\n parseModerationJson,\n} from '../utils/moderation-prompt'\nimport { useMemiBoardConfig } from '../config'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport type { ModerationResult, ModerationVia } from '../types'\n\nconst APPROVED: ModerationResult = { flagged: false, category: 'none', reason: '', via: 'empty' }\n\n/**\n * 글쓰기 전 블로킹 검열.\n * 이용 제한 확인 → 로컬 비속어 → Firebase AI Logic(Gemini).\n * 콘텐츠 차단(local/ai) 시 board users 경고 누적.\n */\nexport function useMemiBoardModeration() {\n const config = useMemiBoardConfig()\n const moderation = config.moderation ?? {}\n const app = useFirebaseApp()\n const {\n isWriteRestricted,\n restrictedMessage,\n recordContentModerationBlock,\n } = useMemiBoardAuth()\n\n const localBlockRe = buildLocalBlockRegex([...DEFAULT_LOCAL_BLOCKLIST, ...(moderation.localBlocklist ?? [])])\n\n function localCheck(text: string): ModerationResult | null {\n const match = text.match(localBlockRe)\n if (match) {\n const hit = match[0] || ''\n return {\n flagged: true,\n category: 'abuse',\n reason: formatModerationUserReason({ via: 'local', localHit: hit, category: 'abuse' }),\n via: 'local',\n }\n }\n return null\n }\n\n function getModerationModel() {\n const modelName = moderation.model ?? 'gemini-3.5-flash-lite'\n const useLimited = moderation.useLimitedUseAppCheckTokens !== false\n const ai = getAI(app, {\n backend: new GoogleAIBackend(),\n useLimitedUseAppCheckTokens: useLimited,\n })\n return getGenerativeModel(ai, {\n model: modelName,\n systemInstruction: MODERATION_SYSTEM,\n generationConfig: {\n temperature: 0,\n maxOutputTokens: 256,\n responseMimeType: 'application/json',\n responseSchema: Schema.object({\n properties: {\n flagged: Schema.boolean(),\n category: Schema.string(),\n reason: Schema.string(),\n },\n }),\n },\n })\n }\n\n function errorResult(): ModerationResult {\n if (moderation.onError === 'block') {\n return {\n flagged: true,\n category: 'other',\n reason: '내용 검토에 실패해 글을 등록할 수 없습니다. 잠시 후 다시 시도해 주세요.',\n error: true,\n via: 'ai-error-block',\n }\n }\n return {\n flagged: false,\n category: 'none',\n reason: '',\n error: true,\n via: 'ai-error-allow',\n }\n }\n\n /** 사유 + 경고 횟수를 한 문장으로 */\n function withUserFacingReason(result: ModerationResult, messageSuffix = ''): ModerationResult {\n const why = formatModerationUserReason({\n reason: result.reason,\n category: result.category,\n via: result.via,\n })\n // 예: 「시발」 같은 표현은… (경고 2/3)\n const reason = messageSuffix ? `${why}${messageSuffix}` : why\n return { ...result, reason }\n }\n\n /** 콘텐츠 위반(local/ai)만 경고 누적. API 오류·이용제한 자체는 제외. */\n async function applyStrikeIfContentBlock(result: ModerationResult): Promise<ModerationResult> {\n if (!result.flagged || result.error) return result\n if (result.via !== 'local' && result.via !== 'ai') return result\n try {\n const { messageSuffix } = await recordContentModerationBlock()\n return withUserFacingReason(result, messageSuffix)\n }\n catch (e) {\n console.warn('[memi-board] record moderation strike failed', e instanceof Error ? e.message : e)\n return withUserFacingReason(result)\n }\n }\n\n async function checkText(text: string): Promise<ModerationResult> {\n if (isWriteRestricted.value) {\n return {\n flagged: true,\n category: 'other',\n reason: restrictedMessage.value\n || '콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.',\n via: 'restricted',\n }\n }\n\n const trimmed = text.trim()\n if (!trimmed) {\n return { ...APPROVED, via: 'empty' as ModerationVia }\n }\n\n const local = localCheck(trimmed)\n if (local) return applyStrikeIfContentBlock(local)\n\n if (moderation.enabled === false) {\n return { ...APPROVED, via: 'disabled' }\n }\n\n try {\n const model = getModerationModel()\n const result = await model.generateContent(`심사할 텍스트:\\n\"\"\"${trimmed.slice(0, 4000)}\"\"\"`)\n const raw = result.response.text()\n const parsed = parseModerationJson(raw)\n if (!parsed) return errorResult()\n const out: ModerationResult = {\n flagged: parsed.flagged,\n category: parsed.category as ModerationResult['category'],\n reason: formatModerationUserReason({\n reason: parsed.reason,\n category: parsed.category,\n via: 'ai',\n }),\n via: 'ai',\n }\n if (out.flagged) return applyStrikeIfContentBlock(out)\n return out\n }\n catch (e) {\n console.warn('[memi-board] moderation AI failed', e instanceof Error ? e.message : e)\n return errorResult()\n }\n }\n\n async function checkImage(file: File): Promise<ModerationResult> {\n if (isWriteRestricted.value) {\n return {\n flagged: true,\n category: 'other',\n reason: restrictedMessage.value\n || '콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.',\n via: 'restricted',\n }\n }\n\n if (moderation.enabled === false || !moderation.moderateImages) {\n return { ...APPROVED, via: 'disabled' }\n }\n\n try {\n const base64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '')\n reader.onerror = reject\n reader.readAsDataURL(file)\n })\n\n const model = getModerationModel()\n const result = await model.generateContent([\n { inlineData: { mimeType: file.type || 'image/jpeg', data: base64 } },\n '이 이미지를 심사해 주세요.',\n ])\n const raw = result.response.text()\n const parsed = parseModerationJson(raw)\n if (!parsed) return errorResult()\n const out: ModerationResult = {\n flagged: parsed.flagged,\n category: parsed.category as ModerationResult['category'],\n reason: formatModerationUserReason({\n reason: parsed.reason,\n category: parsed.category,\n via: 'ai',\n }),\n via: 'ai',\n }\n if (out.flagged) return applyStrikeIfContentBlock(out)\n return out\n }\n catch (e) {\n console.warn('[memi-board] moderation AI image failed', e instanceof Error ? e.message : e)\n return errorResult()\n }\n }\n\n return { checkText, checkImage }\n}\n","/**\n * Canvas API로 썸네일용 이미지 압축 (shineb 와 동일).\n */\nexport async function compressImage(\n file: File | Blob,\n { maxWidth = 400, quality = 0.8 }: { maxWidth?: number, quality?: number } = {},\n): Promise<Blob> {\n return new Promise((resolve, reject) => {\n const img = new Image()\n const objectUrl = URL.createObjectURL(file)\n\n img.onload = () => {\n URL.revokeObjectURL(objectUrl)\n\n const ratio = Math.min(1, maxWidth / img.naturalWidth)\n const canvas = document.createElement('canvas')\n canvas.width = Math.round(img.naturalWidth * ratio)\n canvas.height = Math.round(img.naturalHeight * ratio)\n\n const ctx = canvas.getContext('2d')\n if (!ctx) {\n reject(new Error('Canvas를 사용할 수 없습니다'))\n return\n }\n ctx.drawImage(img, 0, 0, canvas.width, canvas.height)\n\n canvas.toBlob(\n blob => (blob ? resolve(blob) : reject(new Error('압축 실패'))),\n 'image/jpeg',\n quality,\n )\n }\n\n img.onerror = () => {\n URL.revokeObjectURL(objectUrl)\n reject(new Error('이미지 로드 실패'))\n }\n\n img.src = objectUrl\n })\n}\n","import {\n getStorage,\n ref as storageRef,\n uploadBytes,\n uploadBytesResumable,\n getDownloadURL,\n deleteObject,\n} from 'firebase/storage'\nimport type { UploadTask } from 'firebase/storage'\nimport { useFirebaseApp } from 'vuefire'\nimport { safeFileName } from '../utils/slugify'\nimport { compressImage } from '../utils/compressImage'\nimport { useBoardPathConfig } from '../config'\nimport { boardPostStorageFolder } from '../utils/boardPaths'\nimport type { Attachment, EditorImageEntry } from '../types'\n\n/** 에디터 이미지 최대 크기 (바이트) */\nexport const EDITOR_IMAGE_MAX_BYTES = 5 * 1024 * 1024\n/** 휴대폰 원본 이미지 선택 허용 크기. 큰 이미지는 업로드 전에 최적화한다. */\nexport const EDITOR_IMAGE_SOURCE_MAX_BYTES = 25 * 1024 * 1024\n\nfunction isHeicLike(file: File): boolean {\n const type = (file.type || '').toLowerCase()\n const name = file.name.toLowerCase()\n return type === 'image/heic'\n || type === 'image/heif'\n || name.endsWith('.heic')\n || name.endsWith('.heif')\n}\n\n/** HEIC/HEIF → JPEG (heic2any, 동적 import) */\nasync function heicToJpegBlob(file: File, quality = 0.85): Promise<Blob> {\n const { default: heic2any } = await import('heic2any')\n const result = await heic2any({\n blob: file,\n toType: 'image/jpeg',\n quality,\n })\n const blob = Array.isArray(result) ? result[0] : result\n if (!(blob instanceof Blob)) {\n throw new Error('HEIC 변환 결과가 비어 있습니다.')\n }\n return blob\n}\n\nasync function resizeJpegIfNeeded(blob: Blob): Promise<Blob> {\n if (blob.size <= EDITOR_IMAGE_MAX_BYTES) return blob\n let next = await compressImage(blob, { maxWidth: 2560, quality: 0.85 })\n if (next.size > EDITOR_IMAGE_MAX_BYTES) {\n next = await compressImage(blob, { maxWidth: 2048, quality: 0.75 })\n }\n if (next.size > EDITOR_IMAGE_MAX_BYTES) {\n throw new Error('압축 후에도 이미지가 5MB를 초과합니다. 더 작은 이미지를 선택해 주세요.')\n }\n return next\n}\n\n/**\n * 에디터 업로드용 정규화.\n * - 5MB 초과 → 리사이즈\n * - HEIC/HEIF → heic2any 로 JPEG 변환 후 필요 시 리사이즈\n */\nasync function optimizeEditorImage(file: File): Promise<{ blob: File | Blob, compressed: boolean }> {\n if (file.size > EDITOR_IMAGE_SOURCE_MAX_BYTES) {\n throw new Error(`원본 이미지는 ${EDITOR_IMAGE_SOURCE_MAX_BYTES / 1024 / 1024}MB 이하여야 합니다.`)\n }\n\n if (isHeicLike(file)) {\n try {\n const jpeg = await heicToJpegBlob(file, 0.85)\n const blob = await resizeJpegIfNeeded(jpeg)\n return { blob, compressed: true }\n }\n catch (cause) {\n if (cause instanceof Error && cause.message.includes('압축 후에도')) throw cause\n // Safari 등 canvas 디코드가 되는 환경 폴백\n try {\n const blob = await resizeJpegIfNeeded(await compressImage(file, { maxWidth: 2560, quality: 0.85 }))\n return { blob, compressed: true }\n }\n catch {\n console.error('[memi-board] HEIC convert failed', cause)\n throw new Error('HEIC 사진을 변환하지 못했습니다. 잠시 후 다시 시도하거나 JPG로 저장해 올려 주세요.')\n }\n }\n }\n\n if (file.size <= EDITOR_IMAGE_MAX_BYTES) {\n return { blob: file, compressed: false }\n }\n\n try {\n const blob = await resizeJpegIfNeeded(file)\n return { blob, compressed: true }\n }\n catch (cause) {\n if (cause instanceof Error && cause.message.includes('압축 후에도')) throw cause\n throw new Error('이 이미지 형식은 브라우저에서 최적화할 수 없습니다. JPG, PNG 또는 WebP로 변환해 주세요.')\n }\n}\n\nexport function useMemiBoardStorage() {\n const cfg = () => useBoardPathConfig()\n const app = useFirebaseApp()\n\n /** postId는 작성 화면 진입 시 미리 생성한 Firestore 자동 ID를 사용한다. */\n function uploadAttachment(\n file: File,\n boardId: string,\n postId: string,\n onProgress?: (ratio: number) => void,\n ): { promise: Promise<Attachment>, cancel: () => void } {\n const storage = getStorage(app)\n const path = `${boardPostStorageFolder(cfg(), boardId, postId)}/attachments/${Date.now()}-${safeFileName(file.name)}`\n const fileRef = storageRef(storage, path)\n const task: UploadTask = uploadBytesResumable(fileRef, file, {\n contentType: file.type || 'application/octet-stream',\n contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(file.name)}`,\n })\n\n const promise = new Promise<Attachment>((resolve, reject) => {\n task.on(\n 'state_changed',\n (snapshot) => {\n if (onProgress && snapshot.totalBytes > 0) {\n onProgress(snapshot.bytesTransferred / snapshot.totalBytes)\n }\n },\n reject,\n async () => {\n const url = await getDownloadURL(fileRef)\n resolve({ name: file.name, url, path, size: file.size, type: file.type })\n },\n )\n })\n\n return { promise, cancel: () => task.cancel() }\n }\n\n async function deleteAttachment(attachment: Pick<Attachment, 'path'>): Promise<void> {\n const storage = getStorage(app)\n await deleteObject(storageRef(storage, attachment.path)).catch(() => {})\n }\n\n /**\n * 에디터 본문 이미지: 원본 + 400px JPEG 썸네일.\n * Storage:\n * `memiBoards/{boardId}/posts/{postId}/images/{ts}-{name}.ext`\n * `memiBoards/{boardId}/posts/{postId}/images/thumbnails/{ts}-{name}.jpg`\n */\n async function uploadEditorImage(file: File, boardId: string, postId: string): Promise<EditorImageEntry> {\n if (!file.type.startsWith('image/')) {\n throw new Error('이미지 파일만 업로드할 수 있습니다.')\n }\n const optimized = await optimizeEditorImage(file)\n\n const storage = getStorage(app)\n const ext = (optimized.compressed ? 'jpg' : (file.name.split('.').pop() || file.type.split('/')[1] || 'png'))\n .replace(/[^a-zA-Z0-9]/g, '')\n .slice(0, 8) || 'png'\n const safeName = safeFileName(file.name.replace(/\\.[^.]+$/, '') || 'image', 40)\n const baseName = `${Date.now()}-${safeName || 'image'}`\n const ns = postId || `new-${Date.now()}`\n const folder = boardPostStorageFolder(cfg(), boardId, ns)\n\n const originalPath = `${folder}/images/${baseName}.${ext}`\n const originalRef = storageRef(storage, originalPath)\n await uploadBytes(originalRef, optimized.blob, {\n contentType: optimized.blob.type || 'image/jpeg',\n })\n const originalUrl = await getDownloadURL(originalRef)\n\n const thumbBlob = await compressImage(optimized.blob, { maxWidth: 400, quality: 0.8 })\n const thumbnailPath = `${folder}/images/thumbnails/${baseName}.jpg`\n const thumbRef = storageRef(storage, thumbnailPath)\n await uploadBytes(thumbRef, thumbBlob, { contentType: 'image/jpeg' })\n const thumbnailUrl = await getDownloadURL(thumbRef)\n\n return { originalUrl, originalPath, thumbnailUrl, thumbnailPath }\n }\n\n /** 원본·썸네일 쌍 삭제 (고아 정리·첨부 삭제용) */\n async function deleteEditorImage(entry: Pick<EditorImageEntry, 'originalPath' | 'thumbnailPath'>): Promise<void> {\n const storage = getStorage(app)\n await Promise.all([\n deleteObject(storageRef(storage, entry.originalPath)).catch(() => {}),\n deleteObject(storageRef(storage, entry.thumbnailPath)).catch(() => {}),\n ])\n }\n\n return {\n uploadAttachment,\n deleteAttachment,\n uploadEditorImage,\n deleteEditorImage,\n }\n}\n","import { computed, toValue } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\nimport { serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useCollection, useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { boardSsrKey, boardUserDoc, boardUsersCol } from '../utils/boardPaths'\nimport type { BoardUserModel, BoardUserRole } from '../types'\n\nexport const BOARD_USER_ROLES: Array<{\n value: BoardUserRole\n label: string\n description: string\n}> = [\n { value: 'admin', label: '관리자', description: '사용자 역할, 게시판 설정과 모든 콘텐츠를 관리합니다.' },\n { value: 'staff', label: '스태프', description: '모든 게시글과 댓글을 관리하지만 사용자 역할과 게시판 설정은 바꿀 수 없습니다.' },\n { value: 'user', label: '사용자', description: '글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다.' },\n]\n\n/**\n * 전체 사용자 목록은 관리자만 읽을 수 있다(rules). enabled가 false인 동안은\n * 쿼리 자체를 시작하지 않는다 — 그냥 호출만 하면 권한 확인 전에 구독이 걸려서\n * 관리자가 아니거나 아직 role을 확인 중일 때 permission-denied로 화면이 죽는다.\n *\n * 경로: memiBoards/{boardId}/users/{uid}\n */\nexport function useMemiBoardUsers(options: { enabled?: MaybeRefOrGetter<boolean> } = {}) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const usersQuery = computed(() => (toValue(options.enabled) ?? true) ? boardUsersCol(db, cfg()) : null)\n const users = useCollection<BoardUserModel>(usersQuery, {\n ssrKey: boardSsrKey(cfg(), 'users-management'),\n })\n\n async function updateUserRole(uid: string, role: BoardUserRole) {\n if (!BOARD_USER_ROLES.some(item => item.value === role)) throw new Error('지원하지 않는 역할입니다.')\n await updateDoc(boardUserDoc(db, cfg(), uid), { role, updatedAt: serverTimestamp() })\n }\n\n return { users, usersPending: users.pending, updateUserRole }\n}\n","export interface VersionHistoryEntry {\n version: string\n date: string\n highlights: string[]\n note?: string\n}\n\n/**\n * MemiBoardVersionHistory 컴포넌트가 그대로 보여주는 데이터.\n * 코드 수준의 자세한 변경 내역은 이 레포의 CHANGELOG.md를 참고 — 새 버전을 릴리스할 때 이 배열도 함께 갱신한다.\n */\nexport const versionHistory: VersionHistoryEntry[] = [\n {\n version: '0.21.1',\n date: '2026-08-07',\n highlights: [\n '관리자가 쓴 댓글에 답글을 달 때 오류가 나던 문제를 고쳤어요',\n ],\n },\n {\n version: '0.21.0',\n date: '2026-08-06',\n highlights: [\n '카테고리 대신 게시판(보드)만 있어요 — 예전 카테고리 이름이 곧 보드 ID예요',\n '글은 memiBoards/{보드}/posts, 설정은 settings, 회원 역할은 memiBoardUsers 에 모아요',\n ],\n },\n {\n version: '0.20.0',\n date: '2026-08-06',\n highlights: [\n '게시판 데이터를 memiBoards/{게시판ID}/… 아래에 모았어요 (여러 게시판을 같은 프로젝트에 둘 수 있어요)',\n '설정·글·사용자 경로가 게시판마다 분리돼요 — 호스트는 boardId만 맞추면 돼요',\n ],\n },\n {\n version: '0.19.3',\n date: '2026-08-06',\n highlights: [\n '이미지 목록 게시판은 인스타처럼 제목 없이 사진+내용만 올리면 돼요',\n '사진을 여러 장 올리고 끌어 순서를 바꿀 수 있어요 (첫 장이 대표)',\n ],\n },\n {\n version: '0.19.2',\n date: '2026-08-06',\n highlights: [\n '이미지만 넣고 글 없이 올리는 건 막았어요 — 본문에 글자를 넣어야 해요',\n ],\n },\n {\n version: '0.19.1',\n date: '2026-08-06',\n highlights: [\n '공유 미리보기 이미지를 가벼운 썸네일로 쓰도록 바꿨어요 (카톡 등에 더 잘 맞아요)',\n ],\n },\n {\n version: '0.19.0',\n date: '2026-08-06',\n highlights: [\n '게시판 목록·글 공유 미리보기(제목·설명·대표 이미지)를 모듈만으로 붙일 수 있어요',\n '호스트가 따로 SEO API를 만들 필요 없이, 공개 글 기준으로 미리보기가 채워져요',\n ],\n },\n {\n version: '0.18.0',\n date: '2026-08-06',\n highlights: [\n '게시판을 보임/숨김으로 나눌 수 있어요 — 숨김은 일기장처럼 권한이 있는 사람만 볼 수 있어요',\n '권한이 없는 글·게시판은 로딩에 멈추지 않고 안내 메시지로 알려 줘요',\n '글 붙여넣기로 넣은 이미지가 파일명과 함께 두 번 들어가던 문제를 고쳤어요',\n ],\n },\n {\n version: '0.17.0',\n date: '2026-08-06',\n highlights: [\n '게시글 조회수가 생겼어요 — 로그인하지 않아도 조회가 쌓여요',\n '목록과 상세에서 조회수를 바로 확인할 수 있어요',\n '댓글 수는 모든 목록 보기에서 제목 옆에 같은 방식으로 보여요',\n ],\n },\n {\n version: '0.16.0',\n date: '2026-08-06',\n highlights: [\n '조밀한 목록 보기가 생겼어요 — 한 줄로 빠르게 훑을 수 있어요',\n '이미지 목록에서는 사진 위에 제목과 좋아요가 겹쳐 보여요',\n '영상 목록에서는 유튜브가 있으면 유튜브 썸네일을 먼저 보여 줘요',\n '시간·설명 툴팁이 마우스만 올려도 바로 떠요',\n ],\n },\n {\n version: '0.15.0',\n date: '2026-08-06',\n highlights: [\n '게시판마다 짧은 설명을 적어 두고, 제목에 마우스를 올리면 볼 수 있어요',\n '일반 목록 카드 여백과 썸네일 배치를 더 단정하게 다듬었어요',\n ],\n },\n {\n version: '0.14.1',\n date: '2026-08-06',\n highlights: [\n '일반 리스트뷰에 정사각형 썸네일이 생기고, 작성자·시간·좋아요·댓글수를 오른쪽에 보기 좋게 정리했어요',\n '카테고리 안에서는 같은 카테고리 뱃지가 매 글마다 반복 표시되지 않아요',\n ],\n },\n {\n version: '0.14.0',\n date: '2026-08-06',\n highlights: [\n '게시판 설정에서 글쓰기·댓글쓰기 권한과 리스트뷰를 카드로 골라요',\n '목록 위에서 일반·이미지·영상 보기를 아이콘으로 바로 바꿔볼 수 있어요',\n ],\n },\n {\n version: '0.13.0',\n date: '2026-08-06',\n highlights: [\n '게시판마다 담당 스태프를 따로 지정할 수 있어요',\n '댓글쓰기 권한도 게시판마다 다르게 정할 수 있어요',\n '유튜브 영상이 모바일과 여러 주소 형식에서도 잘 보여요',\n ],\n },\n {\n version: '0.11.0',\n date: '2026-08-05',\n highlights: [\n '게시글에 좋아요 버튼이 생겼어요',\n '글을 보는 동안 다른 사람의 좋아요·댓글 수가 화면에 바로 나타나요',\n '작성 시간을 몇 분 전처럼 쉽게 보고, 올리면 정확한 시각을 확인할 수 있어요',\n ],\n },\n {\n version: '0.10.0',\n date: '2026-08-05',\n highlights: [\n '게시판 설치와 연결 설정이 한결 단순해졌어요',\n '게시판 아래에서 제작 정보와 지금까지의 버전 이야기를 볼 수 있어요',\n ],\n },\n {\n version: '0.9.0',\n date: '2026-08-04',\n highlights: [\n '댓글에 답글을 달고 대화 흐름을 볼 수 있어요',\n '댓글을 이어서 불러오고, 새 댓글은 실시간으로 받아요',\n ],\n },\n {\n version: '0.8.0',\n date: '2026-08-04',\n highlights: [\n '최근 댓글은 실시간으로 보고 이전 댓글은 이어서 불러올 수 있어요',\n '댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요',\n ],\n },\n {\n version: '0.7.0',\n date: '2026-08-04',\n highlights: [\n '카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요',\n '게시판 설정 화면의 SSR hydration 경고를 해결했어요',\n ],\n },\n {\n version: '0.6.0',\n date: '2026-08-04',\n highlights: [\n '관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요',\n '스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요',\n ],\n },\n {\n version: '0.5.0',\n date: '2026-08-04',\n highlights: [\n '게시판마다 일반·이미지·영상 목록을 고르고, 유튜브를 본문에 넣을 수 있어요',\n '이전 글·목록·다음 글 이동과 카테고리 관리를 지원해요',\n ],\n },\n {\n version: '0.2.0',\n date: '2026-07-31',\n highlights: [\n '글쓰기·댓글·첨부·로그인 권한으로 게시판을 바로 시작할 수 있어요',\n '부적절한 글·댓글은 AI 검열로 막을 수 있어요',\n ],\n note: '실제 보안은 호스트 Firebase 규칙 등록이 필요합니다.',\n },\n]\n","!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define(e):(t=\"undefined\"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){\"use strict\";var t=1e3,e=6e4,n=36e5,r=\"millisecond\",i=\"second\",s=\"minute\",u=\"hour\",a=\"day\",o=\"week\",c=\"month\",f=\"quarter\",h=\"year\",d=\"date\",l=\"Invalid Date\",$=/^(\\d{4})[-/]?(\\d{1,2})?[-/]?(\\d{0,2})[Tt\\s]*(\\d{1,2})?:?(\\d{1,2})?:?(\\d{1,2})?[.:]?(\\d+)?$/,y=/\\[([^\\]]+)]|YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:\"en\",weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),ordinal:function(t){var e=[\"th\",\"st\",\"nd\",\"rd\"],n=t%100;return\"[\"+t+(e[(n-20)%10]||e[n]||e[0])+\"]\"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:\"\"+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?\"+\":\"-\")+m(r,2,\"0\")+\":\"+m(i,2,\"0\")},m:function t(e,n){if(e.date()<n.date())return-t(n,e);var r=12*(n.year()-e.year())+(n.month()-e.month()),i=e.clone().add(r,c),s=n-i<0,u=e.clone().add(r+(s?-1:1),c);return+(-(r+(n-i)/(s?i-u:u-i))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(t){return{M:c,y:h,w:o,d:a,D:d,h:u,m:s,s:i,ms:r,Q:f}[t]||String(t||\"\").toLowerCase().replace(/s$/,\"\")},u:function(t){return void 0===t}},g=\"en\",D={};D[g]=M;var p=\"$isDayjsObject\",S=function(t){return t instanceof _||!(!t||!t[p])},w=function t(e,n,r){var i;if(!e)return g;if(\"string\"==typeof e){var s=e.toLowerCase();D[s]&&(i=s),n&&(D[s]=n,i=s);var u=e.split(\"-\");if(!i&&u.length>1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},O=function(t,e){if(S(t))return t.clone();var n=\"object\"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},b=v;b.l=w,b.i=S,b.w=function(t,e){return O(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=w(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[p]=!0}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.u(e))return new Date;if(e instanceof Date)return new Date(e);if(\"string\"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||\"0\").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return b},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=O(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return O(t)<this.startOf(e)},m.isBefore=function(t,e){return this.endOf(e)<O(t)},m.$g=function(t,e,n){return b.u(t)?this[e]:this.set(n,t)},m.unix=function(){return Math.floor(this.valueOf()/1e3)},m.valueOf=function(){return this.$d.getTime()},m.startOf=function(t,e){var n=this,r=!!b.u(e)||e,f=b.p(t),l=function(t,e){var i=b.w(n.$u?Date.UTC(n.$y,e,t):new Date(n.$y,e,t),n);return r?i:i.endOf(a)},$=function(t,e){return b.w(n.toDate()[t].apply(n.toDate(\"s\"),(r?[0,0,0,0]:[23,59,59,999]).slice(e)),n)},y=this.$W,M=this.$M,m=this.$D,v=\"set\"+(this.$u?\"UTC\":\"\");switch(f){case h:return r?l(1,0):l(31,11);case c:return r?l(1,M):l(0,M+1);case o:var g=this.$locale().weekStart||0,D=(y<g?y+7:y)-g;return l(r?m-D:m+(6-D),M);case a:case d:return $(v+\"Hours\",0);case u:return $(v+\"Minutes\",1);case s:return $(v+\"Seconds\",2);case i:return $(v+\"Milliseconds\",3);default:return this.clone()}},m.endOf=function(t){return this.startOf(t,!1)},m.$set=function(t,e){var n,o=b.p(t),f=\"set\"+(this.$u?\"UTC\":\"\"),l=(n={},n[a]=f+\"Date\",n[d]=f+\"Date\",n[c]=f+\"Month\",n[h]=f+\"FullYear\",n[u]=f+\"Hours\",n[s]=f+\"Minutes\",n[i]=f+\"Seconds\",n[r]=f+\"Milliseconds\",n)[o],$=o===a?this.$D+(e-this.$W):e;if(o===c||o===h){var y=this.clone().set(d,1);y.$d[l]($),y.init(),this.$d=y.set(d,Math.min(this.$D,y.daysInMonth())).$d}else l&&this.$d[l]($);return this.init(),this},m.set=function(t,e){return this.clone().$set(t,e)},m.get=function(t){return this[b.p(t)]()},m.add=function(r,f){var d,l=this;r=Number(r);var $=b.p(f),y=function(t){var e=O(l);return b.w(e.date(e.date()+Math.round(t*r)),l)};if($===c)return this.set(c,this.$M+r);if($===h)return this.set(h,this.$y+r);if($===a)return y(1);if($===o)return y(7);var M=(d={},d[s]=e,d[u]=n,d[i]=t,d)[$]||1,m=this.$d.getTime()+r*M;return b.w(m,this)},m.subtract=function(t,e){return this.add(-1*t,e)},m.format=function(t){var e=this,n=this.$locale();if(!this.isValid())return n.invalidDate||l;var r=t||\"YYYY-MM-DDTHH:mm:ssZ\",i=b.z(this),s=this.$H,u=this.$m,a=this.$M,o=n.weekdays,c=n.months,f=n.meridiem,h=function(t,n,i,s){return t&&(t[n]||t(e,r))||i[n].slice(0,s)},d=function(t){return b.s(s%12||12,t,\"0\")},$=f||function(t,e,n){var r=t<12?\"AM\":\"PM\";return n?r.toLowerCase():r};return r.replace(y,(function(t,r){return r||function(t){switch(t){case\"YY\":return String(e.$y).slice(-2);case\"YYYY\":return b.s(e.$y,4,\"0\");case\"M\":return a+1;case\"MM\":return b.s(a+1,2,\"0\");case\"MMM\":return h(n.monthsShort,a,c,3);case\"MMMM\":return h(c,a);case\"D\":return e.$D;case\"DD\":return b.s(e.$D,2,\"0\");case\"d\":return String(e.$W);case\"dd\":return h(n.weekdaysMin,e.$W,o,2);case\"ddd\":return h(n.weekdaysShort,e.$W,o,3);case\"dddd\":return o[e.$W];case\"H\":return String(s);case\"HH\":return b.s(s,2,\"0\");case\"h\":return d(1);case\"hh\":return d(2);case\"a\":return $(s,u,!0);case\"A\":return $(s,u,!1);case\"m\":return String(u);case\"mm\":return b.s(u,2,\"0\");case\"s\":return String(e.$s);case\"ss\":return b.s(e.$s,2,\"0\");case\"SSS\":return b.s(e.$ms,3,\"0\");case\"Z\":return i}return null}(t)||i.replace(\":\",\"\")}))},m.utcOffset=function(){return 15*-Math.round(this.$d.getTimezoneOffset()/15)},m.diff=function(r,d,l){var $,y=this,M=b.p(d),m=O(r),v=(m.utcOffset()-this.utcOffset())*e,g=this-m,D=function(){return b.m(y,m)};switch(M){case h:$=D()/12;break;case c:$=D();break;case f:$=D()/3;break;case o:$=(g-v)/6048e5;break;case a:$=(g-v)/864e5;break;case u:$=g/n;break;case s:$=g/e;break;case i:$=g/t;break;default:$=g}return l?$:b.a($)},m.daysInMonth=function(){return this.endOf(c).$D},m.$locale=function(){return D[this.$L]},m.locale=function(t,e){if(!t)return this.$L;var n=this.clone(),r=w(t,e,!0);return r&&(n.$L=r),n},m.clone=function(){return b.w(this.$d,this)},m.toDate=function(){return new Date(this.valueOf())},m.toJSON=function(){return this.isValid()?this.toISOString():null},m.toISOString=function(){return this.$d.toISOString()},m.toString=function(){return this.$d.toUTCString()},M}(),Y=_.prototype;return O.prototype=Y,[[\"$ms\",r],[\"$s\",i],[\"$m\",s],[\"$H\",u],[\"$W\",a],[\"$M\",c],[\"$y\",h],[\"$D\",d]].forEach((function(t){Y[t[1]]=function(e){return this.$g(e,t[0],t[1])}})),O.extend=function(t,e){return t.$i||(t(e,_,O),t.$i=!0),O},O.locale=w,O.isDayjs=S,O.unix=function(t){return O(1e3*t)},O.en=D[g],O.Ls=D,O.p={},O}));","!function(r,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define(e):(r=\"undefined\"!=typeof globalThis?globalThis:r||self).dayjs_plugin_relativeTime=e()}(this,(function(){\"use strict\";return function(r,e,t){r=r||{};var n=e.prototype,o={future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"};function i(r,e,t,o){return n.fromToBase(r,e,t,o)}t.en.relativeTime=o,n.fromToBase=function(e,n,i,d,u){for(var f,a,s,l=i.$locale().relativeTime||o,h=r.thresholds||[{l:\"s\",r:44,d:\"second\"},{l:\"m\",r:89},{l:\"mm\",r:44,d:\"minute\"},{l:\"h\",r:89},{l:\"hh\",r:21,d:\"hour\"},{l:\"d\",r:35},{l:\"dd\",r:25,d:\"day\"},{l:\"M\",r:45},{l:\"MM\",r:10,d:\"month\"},{l:\"y\",r:17},{l:\"yy\",d:\"year\"}],m=h.length,c=0;c<m;c+=1){var y=h[c];y.d&&(f=d?t(e).diff(i,y.d,!0):i.diff(e,y.d,!0));var p=(r.rounding||Math.round)(Math.abs(f));if(s=f>0,p<=y.r||!y.r){p<=1&&c>0&&(y=h[c-1]);var v=l[y.l];u&&(p=u(\"\"+p)),a=\"string\"==typeof v?v.replace(\"%d\",p):v(p,n,y.l,s);break}}if(n)return a;var M=s?l.future:l.past;return\"function\"==typeof M?M(a):M.replace(\"%s\",a)},n.to=function(r,e){return i(r,e,this,!0)},n.from=function(r,e){return i(r,e,this)};var d=function(r){return r.$u?t.utc():t()};n.toNow=function(r){return this.to(d(this),r)},n.fromNow=function(r){return this.from(d(this),r)}}}));","!function(e,_){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=_(require(\"dayjs\")):\"function\"==typeof define&&define.amd?define([\"dayjs\"],_):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).dayjs_locale_ko=_(e.dayjs)}(this,(function(e){\"use strict\";function _(e){return e&&\"object\"==typeof e&&\"default\"in e?e:{default:e}}var d=_(e),t={name:\"ko\",weekdays:\"일요일_월요일_화요일_수요일_목요일_금요일_토요일\".split(\"_\"),weekdaysShort:\"일_월_화_수_목_금_토\".split(\"_\"),weekdaysMin:\"일_월_화_수_목_금_토\".split(\"_\"),months:\"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월\".split(\"_\"),monthsShort:\"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월\".split(\"_\"),ordinal:function(e){return e+\"일\"},formats:{LT:\"A h:mm\",LTS:\"A h:mm:ss\",L:\"YYYY.MM.DD.\",LL:\"YYYY년 MMMM D일\",LLL:\"YYYY년 MMMM D일 A h:mm\",LLLL:\"YYYY년 MMMM D일 dddd A h:mm\",l:\"YYYY.MM.DD.\",ll:\"YYYY년 MMMM D일\",lll:\"YYYY년 MMMM D일 A h:mm\",llll:\"YYYY년 MMMM D일 dddd A h:mm\"},meridiem:function(e){return e<12?\"오전\":\"오후\"},relativeTime:{future:\"%s 후\",past:\"%s 전\",s:\"몇 초\",m:\"1분\",mm:\"%d분\",h:\"한 시간\",hh:\"%d시간\",d:\"하루\",dd:\"%d일\",M:\"한 달\",MM:\"%d달\",y:\"일 년\",yy:\"%d년\"}};return d.default.locale(t,null,!0),t}));","import type { Timestamp } from 'firebase/firestore'\nimport dayjs from 'dayjs'\nimport relativeTime from 'dayjs/plugin/relativeTime'\nimport 'dayjs/locale/ko'\n\ndayjs.extend(relativeTime)\ndayjs.locale('ko')\n\nexport function formatDate(ts: Timestamp | undefined): string {\n return ts?.toDate ? ts.toDate().toLocaleDateString('ko-KR') : ''\n}\n\nexport function formatRelativeDate(ts: Timestamp | undefined, now = Date.now()): string {\n return ts?.toDate ? dayjs(ts.toDate()).from(dayjs(now)) : '방금 전'\n}\n\nexport function formatFullDate(ts: Timestamp | undefined): string {\n return ts?.toDate ? dayjs(ts.toDate()).format('YYYY년 M월 D일 HH:mm:ss') : '시간 확인 중'\n}\n\n/** 생성·수정 시각이 같거나 수정 시각이 없으면 한 줄, 실제 수정됐으면 두 줄. */\nexport function formatTimestampDetails(createdAt: Timestamp | undefined, updatedAt?: Timestamp): string[] {\n const created = formatFullDate(createdAt)\n const createdMs = createdAt?.toMillis?.()\n const updatedMs = updatedAt?.toMillis?.()\n if (updatedMs == null || createdMs == null || createdMs === updatedMs) return [created]\n return [`생성시간: ${created}`, `수정시간: ${formatFullDate(updatedAt)}`]\n}\n","/**\n * 게시글 상세용 가벼운 마크다운 → HTML (TipTap 없이).\n * 목록 이동 시 UEditor destroy/recreate 로 나는\n * \"Adding different instances of a keyed plugin\" 를 피한다.\n * 신뢰된 작성자 콘텐츠 전제 — 기본 이스케이프 후 허용 패턴만 복원.\n */\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\n/** 인라인 마크다운 (이미 escape 된 문자열에 적용) */\nfunction formatInline(s: string): string {\n let out = s\n // code\n out = out.replace(/`([^`]+)`/g, '<code>$1</code>')\n // images ![alt](url) — url 은 http(s) 만\n out = out.replace(\n /!\\[([^\\]]*)\\]\\((https?:\\/\\/[^)\\s]+)\\)/g,\n '<img src=\"$2\" alt=\"$1\" class=\"max-w-full h-auto rounded-lg my-3\" loading=\"lazy\" />',\n )\n // links [text](url)\n out = out.replace(\n /\\[([^\\]]+)\\]\\((https?:\\/\\/[^)\\s]+)\\)/g,\n '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary underline\">$1</a>',\n )\n // bold ** ** 먼저\n out = out.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n // italic * * (bold 처리 후 남은 단일 *)\n out = out.replace(/\\*([^*]+)\\*/g, '<em>$1</em>')\n // strikethrough ~~\n out = out.replace(/~~([^~]+)~~/g, '<del>$1</del>')\n return out\n}\n\n/**\n * GFM-ish 최소 변환. 코드 펜스·목록·제목·인용·hr·단락.\n */\nexport function renderMarkdownToHtml(markdown: string): string {\n if (!markdown?.trim()) return ''\n\n const lines = markdown.replace(/\\r\\n/g, '\\n').split('\\n')\n const blocks: string[] = []\n let i = 0\n let inCode = false\n let codeLang = ''\n let codeBuf: string[] = []\n let listType: 'ul' | 'ol' | null = null\n let listItems: string[] = []\n\n function flushList() {\n if (!listType || !listItems.length) {\n listType = null\n listItems = []\n return\n }\n const tag = listType\n blocks.push(`<${tag}>${listItems.map(li => `<li>${formatInline(li)}</li>`).join('')}</${tag}>`)\n listType = null\n listItems = []\n }\n\n while (i < lines.length) {\n const line = lines[i] ?? ''\n\n // fenced code\n const fence = line.match(/^```(\\w*)\\s*$/)\n if (fence) {\n if (inCode) {\n blocks.push(\n `<pre class=\"overflow-x-auto rounded-lg bg-muted p-3 text-sm\"><code>${codeBuf.join('\\n')}</code></pre>`,\n )\n codeBuf = []\n inCode = false\n codeLang = ''\n }\n else {\n flushList()\n inCode = true\n codeLang = fence[1] || ''\n void codeLang\n }\n i++\n continue\n }\n if (inCode) {\n codeBuf.push(escapeHtml(line))\n i++\n continue\n }\n\n // hr\n if (/^(-{3,}|\\*{3,}|_{3,})\\s*$/.test(line)) {\n flushList()\n blocks.push('<hr class=\"my-4 border-default\" />')\n i++\n continue\n }\n\n // heading\n const h = line.match(/^(#{1,3})\\s+(.+)$/)\n if (h) {\n flushList()\n const level = h[1]!.length\n blocks.push(`<h${level} class=\"font-semibold mt-4 mb-2\">${formatInline(escapeHtml(h[2]!))}</h${level}>`)\n i++\n continue\n }\n\n // blockquote\n if (line.startsWith('> ')) {\n flushList()\n const quoteLines: string[] = []\n while (i < lines.length && (lines[i] ?? '').startsWith('> ')) {\n quoteLines.push(escapeHtml((lines[i] ?? '').slice(2)))\n i++\n }\n blocks.push(\n `<blockquote class=\"border-l-4 border-default pl-3 text-muted my-3\">${formatInline(quoteLines.join('<br />'))}</blockquote>`,\n )\n continue\n }\n\n // unordered list\n const ul = line.match(/^[-*+]\\s+(.+)$/)\n if (ul) {\n if (listType && listType !== 'ul') flushList()\n listType = 'ul'\n listItems.push(escapeHtml(ul[1]!))\n i++\n continue\n }\n\n // ordered list\n const ol = line.match(/^\\d+\\.\\s+(.+)$/)\n if (ol) {\n if (listType && listType !== 'ol') flushList()\n listType = 'ol'\n listItems.push(escapeHtml(ol[1]!))\n i++\n continue\n }\n\n // empty line\n if (!line.trim()) {\n flushList()\n i++\n continue\n }\n\n // paragraph (merge consecutive non-empty)\n flushList()\n const para: string[] = []\n while (i < lines.length) {\n const l = lines[i] ?? ''\n if (!l.trim()) break\n if (/^(#{1,3}\\s|```|[-*+]\\s|\\d+\\.\\s|> |(-{3,}|\\*{3,})\\s*$)/.test(l)) break\n para.push(escapeHtml(l))\n i++\n }\n blocks.push(`<p class=\"my-2 leading-relaxed\">${formatInline(para.join('<br />'))}</p>`)\n }\n\n flushList()\n if (inCode && codeBuf.length) {\n blocks.push(\n `<pre class=\"overflow-x-auto rounded-lg bg-muted p-3 text-sm\"><code>${codeBuf.join('\\n')}</code></pre>`,\n )\n }\n\n return blocks.join('\\n')\n}\n"],"x_google_ignoreList":[23,24,25],"mappings":"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,YAAY,CACV,yCACA,wCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,0CACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,gDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,kDACA,iDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,4CACA,mCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,4DACA,yCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,uCACA,yCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,4BACA,uCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,6BACA,+BACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wCACA,6BACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,gDACA,oCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,gDACA,iDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,8CACA,gCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wCACA,4BACF;EACA,MAAM;CACR;AACF;CChMA,CAAC,SAAS,GAAE,GAAE;EAAC,AAAU,OAAO,KAAjB,YAA8C,MAApB,SAA2B,EAAO,UAAQ,EAAE,IAAc,OAAO,UAAnB,cAA2B,OAAO,MAAI,OAAO,CAAC,I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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/utils/boardSeo.ts","../src/composables/fetchPublicSeo.ts","../src/utils/moderation-strike.ts","../src/composables/useMemiBoardAuth.ts","../src/utils/extractEditorImageUrls.ts","../src/utils/storagePath.ts","../src/utils/postBody.ts","../src/utils/postPreview.ts","../src/composables/useMemiBoardSettings.ts","../src/composables/useMemiBoardPosts.ts","../src/composables/useMemiBoardUserPosts.ts","../src/composables/useMemiBoardUserComments.ts","../src/composables/useMemiBoardAuthorMemo.ts","../src/composables/useMemiBoardComments.ts","../src/composables/useMemiBoardLikes.ts","../src/composables/useMemiBoardViews.ts","../src/utils/moderation-prompt.ts","../src/composables/useMemiBoardModeration.ts","../src/composables/useMemiBoardUsers.ts","../src/composables/useMemiBoardUserProfile.ts","../src/data/versionHistory.ts","../node_modules/.pnpm/dayjs@1.11.21/node_modules/dayjs/dayjs.min.js","../node_modules/.pnpm/dayjs@1.11.21/node_modules/dayjs/plugin/relativeTime.js","../node_modules/.pnpm/dayjs@1.11.21/node_modules/dayjs/locale/ko.js","../src/utils/formatDate.ts","../src/utils/renderMarkdown.ts"],"sourcesContent":["/** 게시판 SEO 순수 헬퍼 — Nuxt 의존 없음 */\n\nexport type BoardPostSeoPayload = {\n id: string\n slug: string\n category: string\n categoryLabel: string\n title: string\n summary: string\n previewImage: string | null\n authorName: string | null\n createdAt: string | null\n}\n\nexport type BoardListSeoPayload = {\n kind: 'all' | 'category'\n category: string | null\n categoryLabel: string\n description: string\n ogImage: string | null\n recentTitles: string[]\n}\n\nexport function asHttpUrl(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const s = value.trim()\n return /^https?:\\/\\//i.test(s) ? s : null\n}\n\n/**\n * 게시글 previewImage(원본) → 공유 미리보기용 썸네일 URL.\n * Storage: `.../images/{file}` → `.../images/thumbnails/{basename}.jpg`\n * (uploadEditorImage 와 동일 규칙, 공개 read 시 `?alt=media` 만으로 접근)\n * Firebase 가 아니면 원본 유지. 이미 thumbnails 경로면 토큰만 제거.\n */\nexport function toBoardOgImageUrl(value: unknown): string | null {\n const s = asHttpUrl(value)\n if (!s) return null\n if (!/firebasestorage\\.googleapis\\.com/i.test(s)) return s\n\n try {\n const u = new URL(s)\n // /v0/b/{bucket}/o/{encodedPath}\n const parts = u.pathname.split('/')\n const bucket = parts[3]\n const oIdx = parts.indexOf('o')\n if (!bucket || oIdx < 0 || !parts[oIdx + 1]) return s\n\n const path = decodeURIComponent(parts.slice(oIdx + 1).join('/'))\n let thumbPath = path\n if (!path.includes('/images/thumbnails/')) {\n const next = path.replace(\n /\\/images\\/([^/]+)$/,\n (_full, file: string) => {\n const base = String(file).replace(/\\.[^.]+$/, '')\n return `/images/thumbnails/${base}.jpg`\n },\n )\n if (next === path) return s\n thumbPath = next\n }\n\n // 토큰 없이 공개 read — 크롤러·카톡 캐시에 유리\n return `${u.origin}/v0/b/${bucket}/o/${encodeURIComponent(thumbPath)}?alt=media`\n }\n catch {\n return s\n }\n}\n\nexport function toAbsoluteUrl(pathOrUrl: string, origin: string, fallbackPath = ''): string {\n const base = origin.replace(/\\/$/, '')\n const s = pathOrUrl.trim()\n if (!s) {\n if (fallbackPath) return toAbsoluteUrl(fallbackPath, origin)\n return base\n }\n if (/^https?:\\/\\//i.test(s)) return s\n return `${base}${s.startsWith('/') ? s : `/${s}`}`\n}\n\nexport function boardPostOgTitle(\n title: string,\n categoryLabel: string | undefined,\n siteName: string,\n /** 제목 없을 때(이미지 보드) 본문 요약 사용 */\n summary?: string,\n): string {\n const fromSummary = summary?.trim().slice(0, 40)\n const t = title.trim() || fromSummary || '사진'\n const cat = categoryLabel?.trim()\n const site = siteName.trim() || 'Board'\n return cat ? `${t} · ${cat} | ${site}` : `${t} | ${site}`\n}\n\nexport function boardPostOgDescription(opts: {\n summary?: string\n categoryLabel?: string\n authorName?: string | null\n}): string {\n const bits: string[] = []\n if (opts.summary?.trim()) bits.push(opts.summary.trim().slice(0, 140))\n else bits.push('게시판 글')\n if (opts.categoryLabel?.trim()) bits.push(opts.categoryLabel.trim())\n if (opts.authorName?.trim()) bits.push(opts.authorName.trim())\n return bits.join(' · ').slice(0, 200)\n}\n\nexport function boardListOgTitle(categoryLabel: string | null | undefined, siteName: string): string {\n const site = siteName.trim() || 'Board'\n const cat = categoryLabel?.trim()\n if (cat && cat !== '전체') return `${cat} 게시판 | ${site}`\n return `게시판 | ${site}`\n}\n\nexport function boardListOgDescription(opts: {\n categoryLabel?: string | null\n description?: string\n recentTitles?: string[]\n}): string {\n if (opts.description?.trim()) return opts.description.trim().slice(0, 200)\n const cat = opts.categoryLabel?.trim()\n const head = cat && cat !== '전체'\n ? `${cat} 게시판 — 소식을 확인하세요.`\n : '게시판 — 공지와 이야기를 확인하세요.'\n const titles = (opts.recentTitles || []).filter(Boolean).slice(0, 3)\n if (!titles.length) return head.slice(0, 200)\n return `${head} ${titles.map(t => `「${t}」`).join(' ')}`.slice(0, 200)\n}\n\nexport function normalizeBasePath(basePath: string | undefined): string {\n const raw = (basePath || '/board').trim() || '/board'\n const withSlash = raw.startsWith('/') ? raw : `/${raw}`\n return withSlash.replace(/\\/$/, '') || '/board'\n}\n","/**\n * 공개 SEO용 Firestore 직조회.\n * listed === true && isPublished === true 만. boardId = 예전 category 경로 세그먼트.\n */\nimport {\n getDoc,\n getDocs,\n limit as fbLimit,\n orderBy,\n query,\n where,\n type Firestore,\n} from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport {\n postsCol,\n settingsDoc,\n resolveBoardPathConfig,\n type BoardPathConfig,\n} from '../utils/boardPaths'\nimport {\n toBoardOgImageUrl,\n type BoardListSeoPayload,\n type BoardPostSeoPayload,\n} from '../utils/boardSeo'\n\nexport type PublicSeoDb = {\n db: Firestore\n paths: BoardPathConfig\n}\n\nexport function resolvePublicSeoDb(): PublicSeoDb {\n return {\n db: useFirestore(),\n paths: useBoardPathConfig(),\n }\n}\n\nasync function loadBoardMeta(ctx: PublicSeoDb, boardId: string): Promise<{\n label: string\n description: string\n hidden: boolean\n}> {\n try {\n const snap = await getDoc(settingsDoc(ctx.db, ctx.paths, boardId))\n if (!snap.exists()) {\n return { label: boardId, description: '', hidden: false }\n }\n const d = snap.data() ?? {}\n return {\n label: typeof d.label === 'string' && d.label.trim() ? d.label.trim() : boardId,\n description: typeof d.description === 'string' ? d.description.trim() : '',\n hidden: d.visibility === 'hidden',\n }\n }\n catch {\n return { label: boardId, description: '', hidden: false }\n }\n}\n\nfunction createdAtIso(value: unknown): string | null {\n if (value && typeof value === 'object' && 'toDate' in value) {\n try {\n const d = (value as { toDate: () => Date }).toDate()\n return d instanceof Date ? d.toISOString() : null\n }\n catch {\n return null\n }\n }\n return null\n}\n\n/** 공개 글 1건 (boardId + slug) */\nexport async function fetchPublicPostForSeo(\n boardId: string,\n slug: string,\n ctx?: PublicSeoDb,\n): Promise<BoardPostSeoPayload | null> {\n const b = boardId.trim()\n const s = slug.trim()\n if (!b || !s) return null\n\n const store = ctx ?? resolvePublicSeoDb()\n store.paths = resolveBoardPathConfig(store.paths)\n const meta = await loadBoardMeta(store, b)\n if (meta.hidden) return null\n\n try {\n const snap = await getDocs(query(\n postsCol(store.db, store.paths),\n where('boardId', '==', b),\n where('slug', '==', s),\n where('listed', '==', true),\n where('isPublished', '==', true),\n fbLimit(1),\n ))\n const row = snap.docs[0]\n if (!row) return null\n const d = row.data() ?? {}\n const title = typeof d.title === 'string' ? d.title.trim() : ''\n if (!title) return null\n return {\n id: row.id,\n slug: typeof d.slug === 'string' ? d.slug : s,\n category: b,\n categoryLabel: meta.label,\n title,\n summary: typeof d.summary === 'string' ? d.summary.trim().slice(0, 200) : '',\n previewImage: toBoardOgImageUrl(d.previewImage),\n authorName: typeof d.authorName === 'string' && d.authorName.trim() ? d.authorName.trim() : null,\n createdAt: createdAtIso(d.createdAt),\n }\n }\n catch (e) {\n if (import.meta.dev) console.warn('[memi-board] fetchPublicPostForSeo failed', e)\n return null\n }\n}\n\n/** 전체 보드 목록 또는 단일 보드 SEO */\nexport async function fetchPublicListForSeo(\n boardId?: string | null,\n ctx?: PublicSeoDb,\n): Promise<BoardListSeoPayload> {\n const b = boardId?.trim() || null\n const store = ctx ?? resolvePublicSeoDb()\n store.paths = resolveBoardPathConfig(store.paths)\n\n if (b) {\n const meta = await loadBoardMeta(store, b)\n if (meta.hidden) {\n return {\n kind: 'category',\n category: b,\n categoryLabel: meta.label,\n description: '',\n ogImage: null,\n recentTitles: [],\n }\n }\n const { recentTitles, ogImage } = await loadRecentListed(store, b)\n return {\n kind: 'category',\n category: b,\n categoryLabel: meta.label,\n description: meta.description || `${meta.label} 게시판`,\n ogImage,\n recentTitles,\n }\n }\n\n return {\n kind: 'all',\n category: null,\n categoryLabel: '전체',\n description: '게시판 — 공지와 커뮤니티 소식을 확인하세요.',\n ogImage: null,\n recentTitles: [],\n }\n}\n\nasync function loadRecentListed(ctx: PublicSeoDb, boardId: string): Promise<{\n recentTitles: string[]\n ogImage: string | null\n}> {\n const recentTitles: string[] = []\n let ogImage: string | null = null\n try {\n const snap = await getDocs(query(\n postsCol(ctx.db, ctx.paths),\n where('boardId', '==', boardId),\n where('listed', '==', true),\n where('isPublished', '==', true),\n orderBy('createdAt', 'desc'),\n fbLimit(12),\n ))\n for (const row of snap.docs) {\n const d = row.data() ?? {}\n const title = typeof d.title === 'string' ? d.title.trim() : ''\n if (title && recentTitles.length < 5) recentTitles.push(title)\n if (!ogImage) ogImage = toBoardOgImageUrl(d.previewImage)\n }\n }\n catch (e) {\n if (import.meta.dev) console.warn('[memi-board] loadRecentListed failed', e)\n }\n return { recentTitles, ogImage }\n}\n","/**\n * 게시판 검열 차단 누적 → 일시 이용 제한 (호스트 contentWarning 과 동일 모델).\n * - 콘텐츠 차단(로컬/AI flagged) 시 +1\n * - 유효 경고 ≥ threshold(기본 3) → 글/댓글 작성 불가\n * - 24h마다 lazy 차감 (moderationBlockAt 기준)\n */\n\nexport const DEFAULT_BLOCK_BAN_THRESHOLD = 3\nexport const DEFAULT_BLOCK_BAN_DECAY_MS = 24 * 60 * 60 * 1000\n\nexport function effectiveModerationBlockCount(\n count: number | null | undefined,\n blockAtMs: number | null | undefined,\n nowMs: number = Date.now(),\n decayMs: number = DEFAULT_BLOCK_BAN_DECAY_MS,\n): number {\n const c = Math.max(0, Math.floor(Number(count) || 0))\n if (c <= 0) return 0\n const at = typeof blockAtMs === 'number' && Number.isFinite(blockAtMs) ? blockAtMs : null\n if (at == null) return c\n if (nowMs <= at) return c\n const decayed = Math.floor((nowMs - at) / decayMs)\n return Math.max(0, c - decayed)\n}\n\n/** 제한 해제 시각(ms). 제한 없으면 null. */\nexport function moderationWriteRestrictedUntilMs(\n count: number | null | undefined,\n blockAtMs: number | null | undefined,\n nowMs: number = Date.now(),\n threshold: number = DEFAULT_BLOCK_BAN_THRESHOLD,\n decayMs: number = DEFAULT_BLOCK_BAN_DECAY_MS,\n): number | null {\n const c = Math.max(0, Math.floor(Number(count) || 0))\n if (c < threshold) return null\n const at = typeof blockAtMs === 'number' && Number.isFinite(blockAtMs) ? blockAtMs : null\n if (at == null) return nowMs\n // 유효 경고가 threshold-1 이하가 되는 시점\n const until = at + (c - (threshold - 1)) * decayMs\n return until > nowMs ? until : null\n}\n\nexport function isModerationWriteRestricted(\n count: number | null | undefined,\n blockAtMs: number | null | undefined,\n nowMs: number = Date.now(),\n threshold: number = DEFAULT_BLOCK_BAN_THRESHOLD,\n decayMs: number = DEFAULT_BLOCK_BAN_DECAY_MS,\n): boolean {\n return moderationWriteRestrictedUntilMs(count, blockAtMs, nowMs, threshold, decayMs) != null\n}\n\nexport function formatRestrictedUntilLabel(untilMs: number, nowMs: number = Date.now()): string {\n const left = Math.max(0, untilMs - nowMs)\n const hours = Math.ceil(left / (60 * 60 * 1000))\n if (hours <= 0) return '곧 해제'\n if (hours < 24) return `약 ${hours}시간 후 해제`\n const days = Math.floor(hours / 24)\n const remH = hours % 24\n if (remH === 0) return `약 ${days}일 후 해제`\n return `약 ${days}일 ${remH}시간 후 해제`\n}\n\nexport function toBlockAtMs(value: unknown): number | null {\n if (value == null) return null\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'object' && value !== null && 'toMillis' in value\n && typeof (value as { toMillis: () => number }).toMillis === 'function') {\n return (value as { toMillis: () => number }).toMillis()\n }\n if (typeof value === 'object' && value !== null && 'seconds' in value) {\n const s = Number((value as { seconds: number }).seconds)\n if (Number.isFinite(s)) return s * 1000\n }\n return null\n}\n","import { ref, computed, watch } from 'vue'\nimport type { Ref, ComputedRef } from 'vue'\nimport { useFirestore, useFirebaseAuth, useCurrentUser } from 'vuefire'\nimport { getDoc, setDoc, updateDoc, increment, serverTimestamp } from 'firebase/firestore'\nimport {\n GoogleAuthProvider,\n OAuthProvider,\n signInWithPopup,\n signInWithEmailAndPassword,\n createUserWithEmailAndPassword,\n signOut as firebaseSignOut,\n updateProfile,\n} from 'firebase/auth'\nimport type { User } from 'firebase/auth'\nimport { useMemiBoardConfig, useBoardPathConfig } from '../config'\nimport { boardUserDoc, boardUserPrivateDoc } from '../utils/boardPaths'\nimport type { PostModel, CommentModel, BoardUserRole } from '../types'\nimport {\n DEFAULT_BLOCK_BAN_DECAY_MS,\n DEFAULT_BLOCK_BAN_THRESHOLD,\n effectiveModerationBlockCount,\n formatRestrictedUntilLabel,\n isModerationWriteRestricted,\n moderationWriteRestrictedUntilMs,\n toBlockAtMs,\n} from '../utils/moderation-strike'\n\nexport interface UseMemiBoardAuthReturn {\n user: ReturnType<typeof useCurrentUser>\n isSignedIn: ComputedRef<boolean>\n isAdmin: ComputedRef<boolean>\n isStaff: ComputedRef<boolean>\n canManageContent: ComputedRef<boolean>\n rolePending: Ref<boolean>\n /** 유효 검열 차단 누적 (lazy decay 반영) */\n moderationBlockCount: ComputedRef<number>\n /** 글·댓글 작성 제한 중 */\n isWriteRestricted: ComputedRef<boolean>\n restrictedUntilLabel: ComputedRef<string | null>\n restrictedMessage: ComputedRef<string | null>\n signInWithGoogle: () => ReturnType<typeof signInWithPopup>\n signInWithApple: () => ReturnType<typeof signInWithPopup>\n signInWithEmail: (email: string, password: string) => ReturnType<typeof signInWithEmailAndPassword>\n signUpWithEmail: (email: string, password: string, displayName?: string) => ReturnType<typeof createUserWithEmailAndPassword>\n signOut: () => Promise<void>\n canEdit: (post: Pick<PostModel, 'authorUid'>) => boolean\n canDelete: (post: Pick<PostModel, 'authorUid'>) => boolean\n canEditComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean\n canDeleteComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean\n /** 콘텐츠 검열 차단 1회 기록 후 메시지 조각 반환 */\n recordContentModerationBlock: () => Promise<{\n effectiveCount: number\n restricted: boolean\n messageSuffix: string\n }>\n refreshBoardUser: () => Promise<void>\n}\n\nconst AUTH_STATE_KEY = '__MEMI_BOARD_AUTH_STATE__' as const\n\ntype BoardAuthShared = {\n role: Ref<BoardUserRole>\n rolePending: Ref<boolean>\n rawBlockCount: Ref<number>\n blockAtMs: Ref<number | null>\n watchStarted: boolean\n}\n\nfunction getSharedAuthState(): BoardAuthShared {\n const g = globalThis as typeof globalThis & { [AUTH_STATE_KEY]?: BoardAuthShared }\n if (!g[AUTH_STATE_KEY]) {\n g[AUTH_STATE_KEY] = {\n role: ref<BoardUserRole>('user'),\n rolePending: ref(true),\n rawBlockCount: ref(0),\n blockAtMs: ref<number | null>(null),\n watchStarted: false,\n }\n }\n return g[AUTH_STATE_KEY]\n}\n\n/**\n * 인증 상태 + 게시판 역할(role) + 검열 차단 누적.\n * role·경고 카운트는 globalThis 공유 (여러 composable 호출이 같은 상태).\n */\nexport function useMemiBoardAuth(): UseMemiBoardAuthReturn {\n const config = useMemiBoardConfig()\n const shared = getSharedAuthState()\n const db = useFirestore()\n const auth = useFirebaseAuth()\n const user = useCurrentUser()\n\n const { role, rolePending, rawBlockCount, blockAtMs } = shared\n\n function banThreshold(): number {\n const t = config.moderation?.blockBanThreshold\n if (t === 0) return 0\n return typeof t === 'number' && t > 0 ? Math.floor(t) : DEFAULT_BLOCK_BAN_THRESHOLD\n }\n\n function banDecayMs(): number {\n const d = config.moderation?.blockBanDecayMs\n return typeof d === 'number' && d > 0 ? d : DEFAULT_BLOCK_BAN_DECAY_MS\n }\n\n function roleDocRef(uid: string) {\n return boardUserDoc(db, useBoardPathConfig(), uid)\n }\n\n function applyUserData(data: Record<string, unknown> | undefined) {\n if (!data) {\n rawBlockCount.value = 0\n blockAtMs.value = null\n return\n }\n rawBlockCount.value = Math.max(0, Math.floor(Number(data.moderationBlockCount) || 0))\n blockAtMs.value = toBlockAtMs(data.moderationBlockAt)\n }\n\n async function ensureUserDoc(firebaseUser: User): Promise<BoardUserRole> {\n const ref = roleDocRef(firebaseUser.uid)\n const privateRef = boardUserPrivateDoc(db, useBoardPathConfig(), firebaseUser.uid)\n const snap = await getDoc(ref)\n if (!snap.exists()) {\n await Promise.all([\n setDoc(ref, {\n role: 'user',\n displayName: firebaseUser.displayName,\n photoURL: firebaseUser.photoURL,\n joinedAt: serverTimestamp(),\n lastVisitAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n }),\n setDoc(privateRef, { email: firebaseUser.email }),\n ])\n applyUserData({ moderationBlockCount: 0 })\n return 'user'\n }\n const data = snap.data() as Record<string, unknown>\n applyUserData(data)\n await Promise.all([\n updateDoc(ref, {\n displayName: firebaseUser.displayName,\n photoURL: firebaseUser.photoURL,\n lastVisitAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n }),\n setDoc(privateRef, { email: firebaseUser.email }, { merge: true }),\n ])\n return (['admin', 'staff', 'user'].includes(String(data.role)) ? data.role : 'user') as BoardUserRole\n }\n\n async function refreshBoardUser() {\n const uid = user.value?.uid\n if (!uid) {\n applyUserData(undefined)\n return\n }\n const snap = await getDoc(roleDocRef(uid))\n applyUserData(snap.exists() ? snap.data() as Record<string, unknown> : undefined)\n }\n\n if (!shared.watchStarted) {\n shared.watchStarted = true\n watch(\n user,\n async (current) => {\n rolePending.value = true\n if (!current) {\n role.value = 'user'\n applyUserData(undefined)\n rolePending.value = false\n return\n }\n try {\n role.value = await ensureUserDoc(current)\n }\n catch (cause) {\n // permission-denied 시 문서 미생성 — rules 미배포·isBoardAdmin get 실패 등\n console.error('[memi-board] ensureUserDoc failed', cause)\n role.value = 'user'\n applyUserData(undefined)\n }\n finally {\n rolePending.value = false\n }\n },\n { immediate: true },\n )\n }\n\n const isSignedIn = computed(() => !!user.value)\n const isAdmin = computed(() => role.value === 'admin')\n const isStaff = computed(() => role.value === 'staff')\n const canManageContent = computed(() => isAdmin.value || isStaff.value)\n\n const moderationBlockCount = computed(() =>\n effectiveModerationBlockCount(\n rawBlockCount.value,\n blockAtMs.value,\n Date.now(),\n banDecayMs(),\n ),\n )\n\n const isWriteRestricted = computed(() => {\n const th = banThreshold()\n if (th <= 0) return false\n return isModerationWriteRestricted(\n rawBlockCount.value,\n blockAtMs.value,\n Date.now(),\n th,\n banDecayMs(),\n )\n })\n\n const restrictedUntilLabel = computed(() => {\n const th = banThreshold()\n if (th <= 0) return null\n const until = moderationWriteRestrictedUntilMs(\n rawBlockCount.value,\n blockAtMs.value,\n Date.now(),\n th,\n banDecayMs(),\n )\n return until != null ? formatRestrictedUntilLabel(until) : null\n })\n\n const restrictedMessage = computed(() => {\n if (!isWriteRestricted.value) return null\n const until = restrictedUntilLabel.value\n return until\n ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${until})`\n : '콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.'\n })\n\n async function recordContentModerationBlock() {\n const uid = user.value?.uid\n const threshold = banThreshold()\n if (!uid || threshold <= 0) {\n return { effectiveCount: 0, restricted: false, messageSuffix: '' }\n }\n\n const ref = roleDocRef(uid)\n const snap = await getDoc(ref)\n if (!snap.exists()) {\n await setDoc(ref, {\n role: 'user',\n displayName: user.value?.displayName ?? null,\n moderationBlockCount: 1,\n moderationBlockAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n }\n else {\n await updateDoc(ref, {\n moderationBlockCount: increment(1),\n moderationBlockAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n }\n\n // increment 직후 getDoc 이 로컬 캐시로 바로 반영됨\n const nextCount = (snap.exists()\n ? Math.max(0, Math.floor(Number((snap.data() as Record<string, unknown>).moderationBlockCount) || 0))\n : 0) + 1\n const nextAt = Date.now()\n rawBlockCount.value = nextCount\n blockAtMs.value = nextAt\n\n const effectiveCount = effectiveModerationBlockCount(\n nextCount,\n nextAt,\n nextAt,\n banDecayMs(),\n )\n const restricted = isModerationWriteRestricted(\n nextCount,\n nextAt,\n nextAt,\n threshold,\n banDecayMs(),\n )\n // 앞 사유 문장과 이어 붙일 접미 (공백·구분자 포함)\n let messageSuffix = ` (경고 ${effectiveCount}/${threshold})`\n if (restricted) {\n const until = moderationWriteRestrictedUntilMs(\n nextCount,\n nextAt,\n nextAt,\n threshold,\n banDecayMs(),\n )\n const untilLabel = until != null ? formatRestrictedUntilLabel(until, nextAt) : null\n messageSuffix = untilLabel\n ? ` 경고 ${effectiveCount}회가 되어 글·댓글 작성이 제한됩니다 (${untilLabel}).`\n : ` 경고 ${effectiveCount}회가 되어 글·댓글 작성이 제한됩니다.`\n }\n return { effectiveCount, restricted, messageSuffix }\n }\n\n function requireAuth() {\n if (!auth) throw new Error('[memi-board] Firebase Auth가 초기화되지 않았습니다.')\n return auth\n }\n\n async function signInWithGoogle() {\n return signInWithPopup(requireAuth(), new GoogleAuthProvider())\n }\n\n async function signInWithApple() {\n const provider = new OAuthProvider('apple.com')\n provider.addScope('email')\n provider.addScope('name')\n return signInWithPopup(requireAuth(), provider)\n }\n\n async function signInWithEmail(email: string, password: string) {\n return signInWithEmailAndPassword(requireAuth(), email, password)\n }\n\n async function signUpWithEmail(email: string, password: string, displayName?: string) {\n const credential = await createUserWithEmailAndPassword(requireAuth(), email, password)\n if (displayName) await updateProfile(credential.user, { displayName })\n return credential\n }\n\n async function signOut() {\n await firebaseSignOut(requireAuth())\n }\n\n function canEdit(post: Pick<PostModel, 'authorUid'>) {\n return isSignedIn.value && (user.value?.uid === post.authorUid || canManageContent.value)\n }\n\n function canEditComment(comment: Pick<CommentModel, 'authorUid'>) {\n return isSignedIn.value && user.value?.uid === comment.authorUid\n }\n\n function canDeleteComment(comment: Pick<CommentModel, 'authorUid'>) {\n return isSignedIn.value && (user.value?.uid === comment.authorUid || canManageContent.value)\n }\n\n return {\n user,\n isSignedIn,\n isAdmin,\n isStaff,\n canManageContent,\n rolePending,\n moderationBlockCount,\n isWriteRestricted,\n restrictedUntilLabel,\n restrictedMessage,\n signInWithGoogle,\n signInWithApple,\n signInWithEmail,\n signUpWithEmail,\n signOut,\n canEdit,\n canDelete: canEdit,\n canEditComment,\n canDeleteComment,\n recordContentModerationBlock,\n refreshBoardUser,\n }\n}\n","/**\n * 에디터 본문(markdown 또는 html)에서 이미지 URL 추출.\n * 고아 파일 정리 스케줄러·저장 시 참조 비교에 사용.\n */\nexport function extractEditorImageUrls(content: string): string[] {\n if (!content) return []\n const urls = new Set<string>()\n\n for (const m of content.matchAll(/!\\[[^\\]]*]\\((https?:\\/\\/[^)\\s]+)\\)/g)) {\n if (m[1]) urls.add(m[1].replaceAll('&amp;', '&'))\n }\n for (const m of content.matchAll(/<img[^>]*\\ssrc=[\"'](https?:\\/\\/[^\"']+)[\"']/gi)) {\n if (m[1]) urls.add(m[1].replaceAll('&amp;', '&'))\n }\n\n return [...urls]\n}\n","/**\n * Firebase Storage download URL → object path.\n * 예: .../o/board%2Fposts%2Fid%2Fimages%2Fx.png?alt=media → board/posts/id/images/x.png\n */\nexport function storagePathFromDownloadUrl(url: string): string | null {\n try {\n const m = url.match(/\\/o\\/([^?]+)/)\n if (!m?.[1]) return null\n return decodeURIComponent(m[1])\n }\n catch {\n return null\n }\n}\n\n/** path 에서 posts/{ns}/... 의 ns 추출 */\nexport function postNamespaceFromStoragePath(path: string): string | null {\n const m = path.match(/(?:^|\\/)posts\\/([^/]+)\\//)\n return m?.[1] ?? null\n}\n","/**\n * 게시글 본문 HTML 검증·파생 필드.\n * - 일반 보드: 글자 필수 (이미지만 불가)\n * - 이미지 리스트뷰 보드: 글자 + 이미지 필수, 제목은 본문에서 자동 생성\n */\n\n/** 미디어·태그를 제거하고 보이는 글자만 남긴다. */\nexport function plainTextFromHtml(html: string): string {\n return String(html || '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<img\\b[^>]*>/gi, ' ')\n .replace(/<iframe\\b[^>]*>[\\s\\S]*?<\\/iframe>/gi, ' ')\n .replace(/<video\\b[\\s\\S]*?<\\/video>/gi, ' ')\n .replace(/<audio\\b[\\s\\S]*?<\\/audio>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/&nbsp;/gi, ' ')\n .replace(/&#160;/g, ' ')\n .replace(/&[a-z]+;/gi, ' ')\n .replace(/&#\\d+;/g, ' ')\n // zero-width / soft hyphen 등\n .replace(/[\\u200B-\\u200D\\uFEFF\\u00AD]/g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/**\n * 이미지 보드 등 제목 미입력 시 본문 plain 앞부분을 title 로 쓴다.\n * summary(160) 보다 짧게 — 목록·탭·OG 한 줄용.\n */\nexport function titleFromBody(html: string, maxLength = 40): string {\n const plain = plainTextFromHtml(html)\n if (!plain) return ''\n if (plain.length <= maxLength) return plain\n // 단어 중간 자르기 완화: 마지막 공백 근처에서 끊기\n const slice = plain.slice(0, maxLength)\n const sp = slice.lastIndexOf(' ')\n const cut = sp >= Math.floor(maxLength * 0.6) ? slice.slice(0, sp) : slice\n return `${cut.trimEnd()}…`\n}\n\n/** 본문에 실제 글자가 있는지 (이미지·유튜브만이면 false) */\nexport function hasBodyText(html: string): boolean {\n return plainTextFromHtml(html).length > 0\n}\n\n/** 본문 HTML 또는 이미지 첨부 중 하나라도 있으면 true */\nexport function hasBodyImage(\n html: string,\n attachments?: Array<{ type?: string } | null> | null,\n): boolean {\n if (/<img\\b/i.test(String(html || ''))) return true\n return (attachments || []).some(a => !!a?.type?.startsWith('image/'))\n}\n\nexport function isContentEmpty(html: string): boolean {\n return !hasBodyText(html)\n}\n","import type { Attachment } from '../types'\n\nfunction decodeHtmlAttribute(value: string): string {\n return value.replace(/&amp;/g, '&').replace(/&quot;/g, '\"').replace(/&#39;/g, \"'\")\n}\n\n/** YouTube watch/embed/shorts/youtu.be URL 에서 video id 추출. 아니면 undefined. */\nexport function youtubeId(value: string | null | undefined): string | undefined {\n if (!value) return undefined\n try {\n const url = new URL(decodeHtmlAttribute(value))\n const host = url.hostname.toLowerCase().replace(/^www\\./, '')\n if (host === 'youtu.be') return url.pathname.split('/').filter(Boolean)[0]\n if (host.endsWith('youtube.com') || host === 'youtube-nocookie.com') {\n if (url.pathname.startsWith('/embed/')) return url.pathname.split('/')[2]\n if (url.pathname.startsWith('/shorts/')) return url.pathname.split('/')[2]\n return url.searchParams.get('v') || undefined\n }\n }\n catch {\n return undefined\n }\n}\n\n/** 영상 목록 카드용 커버: YouTube 썸네일 우선, 없으면 previewImage/이미지 첨부. */\nexport function videoListCoverUrl(post: {\n videoUrl?: string\n previewImage?: string\n attachments?: Attachment[]\n}): string | undefined {\n const id = youtubeId(post.videoUrl)\n if (id) return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`\n return post.previewImage\n || post.attachments?.find(item => item.type.startsWith('image/'))?.url\n}\n\nexport function buildPostPreview(content: string, attachments: Attachment[] = []): {\n summary: string\n previewImage?: string\n videoUrl?: string\n} {\n const imageMatch = content.match(/<img\\b[^>]*\\bsrc=[\"']([^\"']+)[\"']/i)\n const iframeMatch = content.match(/<iframe\\b[^>]*\\bsrc=[\"']([^\"']+)[\"']/i)\n const attachmentImage = attachments.find(item => item.type.startsWith('image/'))?.url\n const attachmentVideo = attachments.find(item => item.type.startsWith('video/'))?.url\n const videoUrl = iframeMatch ? decodeHtmlAttribute(iframeMatch[1]!) : attachmentVideo\n const videoId = videoUrl ? youtubeId(videoUrl) : undefined\n const previewImage = imageMatch\n ? decodeHtmlAttribute(imageMatch[1]!)\n : attachmentImage || (videoId ? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` : undefined)\n // 문단·<br> 경계를 줄바꿈으로 살려둔다 — 안 그러면 두 줄로 쓴 글도 한 줄로 뭉개져서\n // 목록의 2줄 클램프(line-clamp-2)가 \"줄바꿈해서 2줄\"이 아니라 \"넘쳐서 2줄\"에만 반응한다.\n const summary = content\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|li|h[1-6]|blockquote)>/gi, '\\n')\n .replace(/<[^>]+>/g, ' ')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&')\n .replace(/[^\\S\\n]+/g, ' ')\n .replace(/ *\\n */g, '\\n')\n .replace(/\\n{2,}/g, '\\n')\n .trim()\n .slice(0, 160)\n\n return { summary, ...(previewImage ? { previewImage } : {}), ...(videoUrl ? { videoUrl } : {}) }\n}\n","import { computed } from 'vue'\nimport { useCollection, useFirestore } from 'vuefire'\nimport {\n deleteDoc,\n getDocs,\n limit as fbLimit,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n startAfter,\n where,\n writeBatch,\n} from 'firebase/firestore'\nimport type { QueryDocumentSnapshot } from 'firebase/firestore'\nimport { useBoardPathConfig } from '../config'\nimport {\n postsCol,\n settingsCol,\n settingsDoc,\n boardSsrKey,\n} from '../utils/boardPaths'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport { slugify } from '../utils/slugify'\nimport type { BoardModel, BoardVisibility } from '../types'\n\n/** 필요할 때 호스트가 명시적으로 시드할 수 있는 예시 보드. 자동 적용하지 않는다. */\nexport const DEFAULT_BOARDS: BoardModel[] = [\n { id: 'free', label: '자유', listView: 'default', writeRole: 'user', commentWriteRole: 'user', visibility: 'public', order: 0 },\n { id: 'notice', label: '공지', listView: 'default', writeRole: 'admin', commentWriteRole: 'user', visibility: 'public', order: 1 },\n { id: 'question', label: '질문', listView: 'default', writeRole: 'user', commentWriteRole: 'user', visibility: 'public', order: 2 },\n]\n\n/** @deprecated DEFAULT_BOARDS */\nexport const DEFAULT_CATEGORIES = DEFAULT_BOARDS\n\nfunction normalizeVisibility(value: unknown): BoardVisibility {\n return value === 'hidden' ? 'hidden' : 'public'\n}\n\nfunction mapBoardDoc(id: string, data: Record<string, unknown>, index: number): BoardModel {\n return {\n id,\n label: typeof data.label === 'string' && data.label.trim() ? data.label.trim() : id,\n order: typeof data.order === 'number' ? data.order : index,\n description: (typeof data.description === 'string' ? data.description : '') || '',\n visibility: normalizeVisibility(data.visibility),\n listView: (data.listView as BoardModel['listView']) ?? 'default',\n writeRole: (data.writeRole as BoardModel['writeRole']) ?? 'user',\n commentWriteRole: (data.commentWriteRole as BoardModel['commentWriteRole']) ?? 'user',\n allowedStaffUids: Array.isArray(data.allowedStaffUids) ? data.allowedStaffUids as string[] : [],\n }\n}\n\n/**\n * 보드 목록·설정.\n * memiBoardSettings/{boardId} 하나가 목록 렌더링과 권한 검사를 모두 겸한다\n * (구버전은 부모 문서 + settings/config 이중 문서였다).\n */\nexport function useMemiBoardSettings() {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const { isSignedIn } = useMemiBoardAuth()\n\n const boardsQuery = computed(() => query(settingsCol(db, cfg()), orderBy('order', 'asc')))\n const { data: boardDocs, pending: settingsPending } = useCollection(boardsQuery, {\n ssrKey: boardSsrKey(cfg(), 'boards'),\n })\n\n const boards = computed<BoardModel[]>(() =>\n boardDocs.value.map((item, index) =>\n mapBoardDoc(item.id, item as unknown as Record<string, unknown>, index),\n ),\n )\n\n /** @deprecated boards — 카테고리 개념 제거 */\n const categories = boards\n\n const publicBoards = computed(() =>\n boards.value.filter(board => board.visibility !== 'hidden'),\n )\n /** @deprecated publicBoards */\n const publicCategories = publicBoards\n\n function boardLabel(id: string | undefined): string | undefined {\n if (!id) return undefined\n return boards.value.find(board => board.id === id)?.label ?? id\n }\n /** @deprecated boardLabel */\n const categoryLabel = boardLabel\n\n function boardDescription(id: string | undefined): string | undefined {\n if (!id) return undefined\n const value = boards.value.find(board => board.id === id)?.description?.trim()\n return value || undefined\n }\n /** @deprecated boardDescription */\n const categoryDescription = boardDescription\n\n function boardVisibility(id: string | undefined): BoardVisibility {\n if (!id) return 'public'\n return boards.value.find(board => board.id === id)?.visibility ?? 'public'\n }\n /** @deprecated boardVisibility */\n const categoryVisibility = boardVisibility\n\n function isBoardHidden(id: string | undefined): boolean {\n return boardVisibility(id) === 'hidden'\n }\n /** @deprecated isBoardHidden */\n const isCategoryHidden = isBoardHidden\n\n function getBoard(id: string | undefined): BoardModel | undefined {\n if (!id) return undefined\n return boards.value.find(board => board.id === id)\n }\n\n /** 숨김 전환 시 해당 보드 글 listed 일괄 맞춤 */\n async function syncPostsListedForBoard(boardId: string, listed: boolean): Promise<void> {\n let cursor: QueryDocumentSnapshot | undefined\n for (;;) {\n const page = await getDocs(query(\n postsCol(db, cfg()),\n where('boardId', '==', boardId),\n orderBy('__name__'),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(400),\n ))\n if (page.empty) break\n const batch = writeBatch(db)\n for (const item of page.docs) {\n batch.update(item.ref, { listed })\n }\n await batch.commit()\n cursor = page.docs[page.docs.length - 1]\n if (page.docs.length < 400) break\n }\n }\n\n function settingsPayload(board: BoardModel, order: number) {\n return {\n label: board.label.trim(),\n description: (board.description ?? '').trim(),\n visibility: normalizeVisibility(board.visibility),\n listView: board.listView ?? 'default',\n writeRole: board.writeRole ?? 'user',\n commentWriteRole: board.commentWriteRole ?? 'user',\n allowedStaffUids: board.allowedStaffUids ?? [],\n order,\n updatedAt: serverTimestamp(),\n }\n }\n\n async function saveBoard(board: BoardModel, order = board.order ?? 0): Promise<void> {\n if (!isSignedIn.value) throw new Error('로그인이 필요합니다.')\n const id = board.id.trim()\n const label = board.label.trim()\n if (!id || !label) throw new Error('보드 ID와 이름을 입력해 주세요.')\n const visibility = normalizeVisibility(board.visibility)\n const previous = boards.value.find(item => item.id === id)\n const previousVisibility = previous?.visibility ?? 'public'\n const payload = settingsPayload({ ...board, label, visibility }, order)\n await setDoc(settingsDoc(db, cfg(), id), payload, { merge: true })\n\n if (previousVisibility !== visibility || visibility === 'public') {\n await syncPostsListedForBoard(id, visibility !== 'hidden')\n }\n }\n\n /** @deprecated saveBoard */\n const saveCategory = saveBoard\n\n async function saveBoards(next: BoardModel[]): Promise<void> {\n if (!isSignedIn.value) throw new Error('로그인이 필요합니다.')\n const previousById = new Map(boards.value.map(item => [item.id, item.visibility ?? 'public']))\n const batch = writeBatch(db)\n const visibilityChanges: Array<{ id: string, listed: boolean }> = []\n next.forEach((board, order) => {\n const id = board.id.trim()\n const label = board.label.trim()\n if (!id || !label) throw new Error('보드 ID와 이름을 입력해 주세요.')\n const visibility = normalizeVisibility(board.visibility)\n const payload = settingsPayload({ ...board, label, visibility }, order)\n batch.set(settingsDoc(db, cfg(), id), payload, { merge: true })\n const prev = previousById.get(id) ?? 'public'\n if (prev !== visibility) visibilityChanges.push({ id, listed: visibility !== 'hidden' })\n })\n await batch.commit()\n for (const change of visibilityChanges) {\n await syncPostsListedForBoard(change.id, change.listed)\n }\n }\n\n /** @deprecated saveBoards */\n const saveCategories = saveBoards\n\n async function deleteBoard(id: string): Promise<void> {\n if (!isSignedIn.value) throw new Error('로그인이 필요합니다.')\n // 설정 문서만 삭제 (posts 는 호스트/관리 작업으로 별도 정리)\n await deleteDoc(settingsDoc(db, cfg(), id))\n }\n\n /** @deprecated deleteBoard */\n const deleteCategory = deleteBoard\n\n async function ensureSettings(): Promise<void> {\n return Promise.resolve()\n }\n\n async function addBoard(label: string): Promise<string> {\n const trimmed = label.trim()\n if (!trimmed) throw new Error('보드 이름을 입력해 주세요.')\n if (!isSignedIn.value) throw new Error('로그인이 필요합니다.')\n const existing = boards.value.find(board => board.label === trimmed)\n if (existing) return existing.id\n\n const base = slugify(trimmed) || `board-${Date.now()}`\n let id = base\n let suffix = 2\n const used = new Set(boards.value.map(board => board.id))\n while (used.has(id)) id = `${base}-${suffix++}`\n await saveBoard({\n id,\n label: trimmed,\n visibility: 'public',\n listView: 'default',\n writeRole: 'user',\n commentWriteRole: 'user',\n order: boards.value.length,\n })\n return id\n }\n\n /** @deprecated addBoard */\n const addCategory = addBoard\n\n return {\n boards,\n publicBoards,\n boardLabel,\n boardDescription,\n boardVisibility,\n isBoardHidden,\n getBoard,\n categories,\n publicCategories,\n categoryLabel,\n categoryDescription,\n categoryVisibility,\n isCategoryHidden,\n settingsPending,\n ensureSettings,\n addBoard,\n addCategory,\n saveBoard,\n saveCategory,\n saveBoards,\n saveCategories,\n deleteBoard,\n deleteCategory,\n DEFAULT_BOARDS,\n DEFAULT_CATEGORIES,\n }\n}\n","import { computed, onScopeDispose, ref, toValue, watch } from 'vue'\nimport type { MaybeRefOrGetter, Ref } from 'vue'\nimport {\n doc,\n documentId,\n getDoc,\n getDocs,\n onSnapshot,\n setDoc,\n updateDoc,\n deleteDoc,\n query,\n where,\n orderBy,\n limit as fbLimit,\n limitToLast,\n startAfter,\n endBefore,\n serverTimestamp,\n writeBatch,\n deleteField,\n} from 'firebase/firestore'\nimport type { QueryConstraint, Unsubscribe } from 'firebase/firestore'\nimport type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore'\nimport { getStorage, ref as storageRef, listAll, deleteObject } from 'firebase/storage'\nimport type { StorageReference } from 'firebase/storage'\nimport { useDocument, useFirebaseApp, useFirestore } from 'vuefire'\nimport { slugify } from '../utils/slugify'\nimport { extractEditorImageUrls } from '../utils/extractEditorImageUrls'\nimport { postNamespaceFromStoragePath, storagePathFromDownloadUrl } from '../utils/storagePath'\nimport { hasBodyImage, hasBodyText, titleFromBody } from '../utils/postBody'\nimport { useBoardPathConfig } from '../config'\nimport { buildPostPreview } from '../utils/postPreview'\nimport {\n postBodyDoc,\n commentsCol,\n postDoc,\n postStorageFolder,\n postsCol,\n} from '../utils/boardPaths'\nimport { useMemiBoardSettings } from './useMemiBoardSettings'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport type { Attachment, PostDetail, PostModel } from '../types'\n\nasync function deleteStorageFolder(folderRef: StorageReference): Promise<void> {\n const list = await listAll(folderRef).catch(() => ({ items: [], prefixes: [] }))\n await Promise.all([\n ...list.items.map(item => deleteObject(item)),\n ...list.prefixes.map(prefix => deleteStorageFolder(prefix)),\n ])\n}\n\nasync function deleteStoragePaths(storage: ReturnType<typeof getStorage>, paths: Iterable<string>): Promise<void> {\n const unique = [...new Set([...paths].filter(Boolean))]\n await Promise.all(\n unique.map(path => deleteObject(storageRef(storage, path)).catch(() => {})),\n )\n}\n\nexport interface CreatePostInput {\n postId?: string\n title: string\n content: string\n tags?: string[]\n attachments?: Attachment[]\n authorUid: string\n authorName: string | null\n authorPhoto: string | null\n moderationModel?: string\n}\n\nexport interface UpdatePostInput {\n title: string\n content: string\n tags?: string[]\n attachments?: Attachment[]\n}\n\nexport type GetPostBySlugResult =\n | { status: 'ok', post: PostDetail }\n | { status: 'not-found' }\n | { status: 'permission-denied' }\n | { status: 'error', message: string }\n\nexport function isFirestorePermissionDenied(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false\n const code = 'code' in error ? String((error as { code?: string }).code) : ''\n return code === 'permission-denied' || code === 'firestore/permission-denied'\n}\n\nfunction resolveBoardId(boardId: MaybeRefOrGetter<string>): string {\n const id = toValue(boardId)?.trim()\n if (!id) throw new Error('[memi-board] boardId 가 필요합니다.')\n return id\n}\n\n/** 목록은 posts 메타만, 본문(body/main)은 상세에서만. */\nexport function useMemiBoardPosts(boardId: MaybeRefOrGetter<string>) {\n const db = useFirestore()\n const app = useFirebaseApp()\n const { isBoardHidden, getBoard } = useMemiBoardSettings()\n const cfg = () => useBoardPathConfig()\n const bid = () => resolveBoardId(boardId)\n\n const postsColRef = () => postsCol(db, cfg())\n const postDocRef = (id: string) => postDoc(db, cfg(), id)\n const bodyDocRef = (id: string) => postBodyDoc(db, cfg(), id)\n const commentsColRef = () => commentsCol(db, cfg())\n\n function listedForBoard(): boolean {\n return !isBoardHidden(bid())\n }\n\n function isImageListBoard(): boolean {\n return getBoard(bid())?.listView === 'image'\n }\n\n function assertPostContent(\n input: { title?: string, content: string, attachments?: Attachment[] },\n ) {\n const imageBoard = isImageListBoard()\n if (!hasBodyText(input.content)) {\n throw new Error(\n imageBoard\n ? '내용을 입력해 주세요.'\n : '본문에 글자를 입력해 주세요. 이미지나 첨부만으로는 게시할 수 없습니다.',\n )\n }\n if (imageBoard) {\n if (!hasBodyImage(input.content, input.attachments)) {\n throw new Error('사진을 올려 주세요.')\n }\n return\n }\n if (!input.title?.trim()) {\n throw new Error('제목을 입력해 주세요.')\n }\n }\n\n function createPostId(): string {\n return doc(postsColRef()).id\n }\n\n async function getPosts(opts: {\n pageSize?: number\n cursor?: QueryDocumentSnapshot<DocumentData>\n publicOnly?: boolean\n } = {}) {\n const pageSize = opts.pageSize ?? 20\n const constraints: QueryConstraint[] = [\n where('boardId', '==', bid()),\n where('isPublished', '==', true),\n ]\n if (opts.publicOnly !== false) {\n constraints.push(where('listed', '==', true))\n }\n constraints.push(orderBy('createdAt', 'desc'))\n if (opts.cursor) constraints.push(startAfter(opts.cursor))\n constraints.push(fbLimit(pageSize + 1))\n\n const snapshot = await getDocs(query(postsColRef(), ...constraints))\n const docs = snapshot.docs.slice(0, pageSize)\n return {\n posts: docs.map(d => ({ id: d.id, ...d.data() }) as PostModel),\n cursor: docs[docs.length - 1] as QueryDocumentSnapshot<DocumentData> | undefined,\n hasMore: snapshot.docs.length > pageSize,\n }\n }\n\n async function getPost(id: string): Promise<PostDetail | null> {\n const [metaSnap, bodySnap] = await Promise.all([getDoc(postDocRef(id)), getDoc(bodyDocRef(id))])\n if (!metaSnap.exists()) return null\n return {\n id: metaSnap.id,\n ...metaSnap.data(),\n content: bodySnap.exists() ? ((bodySnap.data() as { content: string }).content ?? '') : '',\n } as PostDetail\n }\n\n /** 보드 내 slug 로 조회 */\n async function getPostBySlug(slug: string): Promise<GetPostBySlugResult> {\n try {\n const snapshot = await getDocs(query(\n postsColRef(),\n where('boardId', '==', bid()),\n where('slug', '==', slug.trim()),\n fbLimit(1),\n ))\n const meta = snapshot.docs[0]\n if (!meta) return { status: 'not-found' }\n try {\n const post = await getPost(meta.id)\n if (!post) return { status: 'not-found' }\n return { status: 'ok', post }\n }\n catch (cause) {\n if (isFirestorePermissionDenied(cause)) return { status: 'permission-denied' }\n return { status: 'error', message: cause instanceof Error ? cause.message : String(cause) }\n }\n }\n catch (cause) {\n if (isFirestorePermissionDenied(cause)) return { status: 'permission-denied' }\n return { status: 'error', message: cause instanceof Error ? cause.message : String(cause) }\n }\n }\n\n async function getAdjacentPosts(current: PostModel): Promise<{\n previous: PostModel | null\n next: PostModel | null\n }> {\n if (!current.createdAt) return { previous: null, next: null }\n try {\n const base: QueryConstraint[] = [\n where('boardId', '==', bid()),\n where('isPublished', '==', true),\n ...((current.listed !== false) ? [where('listed', '==', true)] : []),\n orderBy('createdAt', 'desc'),\n ]\n const [previousSnapshot, nextSnapshot] = await Promise.all([\n getDocs(query(postsColRef(), ...base, startAfter(current.createdAt), fbLimit(1))),\n getDocs(query(postsColRef(), ...base, endBefore(current.createdAt), limitToLast(1))),\n ])\n const mapPost = (snapshot: typeof previousSnapshot): PostModel | null => {\n const item = snapshot.docs[0]\n return item ? ({ id: item.id, ...item.data() } as PostModel) : null\n }\n return {\n previous: mapPost(previousSnapshot),\n next: mapPost(nextSnapshot),\n }\n }\n catch (cause) {\n if (import.meta.dev) console.warn('[memi-board] getAdjacentPosts failed', cause)\n return { previous: null, next: null }\n }\n }\n\n async function createPost(input: CreatePostInput): Promise<string> {\n assertPostContent(input)\n const id = input.postId || createPostId()\n const imageBoard = isImageListBoard()\n let slug: string\n let title: string\n if (imageBoard) {\n slug = id\n title = titleFromBody(input.content)\n }\n else {\n title = input.title.trim()\n const baseSlug = slugify(title) || 'post'\n slug = baseSlug\n let counter = 1\n while (!(await getDocs(query(\n postsColRef(),\n where('boardId', '==', bid()),\n where('slug', '==', slug),\n fbLimit(1),\n ))).empty) {\n counter++\n slug = `${baseSlug}-${counter}`\n }\n }\n\n const preview = buildPostPreview(input.content, input.attachments)\n const listed = listedForBoard()\n await setDoc(postDocRef(id), {\n boardId: bid(),\n slug,\n title,\n ...preview,\n tags: input.tags ?? [],\n attachments: input.attachments ?? [],\n commentCount: 0,\n likeCount: 0,\n viewCount: 0,\n listed,\n isPublished: false,\n authorUid: input.authorUid,\n authorName: input.authorName,\n authorPhoto: input.authorPhoto,\n moderationStatus: 'approved',\n moderationModel: input.moderationModel ?? null,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n await setDoc(bodyDocRef(id), { content: input.content })\n return slug\n }\n\n async function updatePost(id: string, input: UpdatePostInput): Promise<void> {\n assertPostContent(input)\n const imageBoard = isImageListBoard()\n const title = imageBoard ? titleFromBody(input.content) : input.title.trim()\n const preview = buildPostPreview(input.content, input.attachments)\n const listed = listedForBoard()\n await Promise.all([\n updateDoc(postDocRef(id), {\n title,\n ...preview,\n previewImage: preview.previewImage ? preview.previewImage : deleteField(),\n videoUrl: preview.videoUrl ? preview.videoUrl : deleteField(),\n tags: input.tags ?? [],\n listed,\n attachments: input.attachments ?? [],\n updatedAt: serverTimestamp(),\n }),\n setDoc(bodyDocRef(id), { content: input.content }, { merge: true }),\n ])\n }\n\n async function publishPost(id: string): Promise<void> {\n await updateDoc(postDocRef(id), {\n isPublished: true,\n publishedAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n }\n\n async function deletePost(id: string): Promise<void> {\n const [metaSnap, bodySnap] = await Promise.all([\n getDoc(postDocRef(id)),\n getDoc(bodyDocRef(id)),\n ])\n if (!metaSnap.exists()) return\n\n const meta = metaSnap.data() as PostModel\n const bodyContent = bodySnap.exists()\n ? String((bodySnap.data() as { content?: string }).content ?? '')\n : ''\n\n const commentsSnap = await getDocs(query(commentsColRef(), where('postId', '==', id)))\n for (let offset = 0; offset < commentsSnap.docs.length; offset += 450) {\n const batch = writeBatch(db)\n commentsSnap.docs.slice(offset, offset + 450).forEach(d => batch.delete(d.ref))\n await batch.commit()\n }\n\n const storage = getStorage(app)\n const extraPaths = new Set<string>()\n const extraNamespaces = new Set<string>()\n\n for (const att of meta.attachments ?? []) {\n if (att?.path) {\n extraPaths.add(att.path)\n const ns = postNamespaceFromStoragePath(att.path)\n if (ns && ns !== id) extraNamespaces.add(ns)\n }\n }\n\n for (const url of extractEditorImageUrls(bodyContent)) {\n const path = storagePathFromDownloadUrl(url)\n if (!path) continue\n extraPaths.add(path)\n const thumbGuess = path\n .replace(/\\/images\\/([^/]+)$/, '/images/thumbnails/$1')\n .replace(/\\.[^.]+$/, '.jpg')\n if (thumbGuess !== path && thumbGuess.includes('/images/thumbnails/')) {\n extraPaths.add(thumbGuess)\n }\n const m = path.match(/^(.*\\/images\\/)([^/]+)\\.[^.]+$/)\n if (m && !path.includes('/thumbnails/')) {\n extraPaths.add(`${m[1]}thumbnails/${m[2]}.jpg`)\n }\n const ns = postNamespaceFromStoragePath(path)\n if (ns && ns !== id) extraNamespaces.add(ns)\n }\n\n await deleteDoc(bodyDocRef(id))\n await deleteStorageFolder(storageRef(storage, postStorageFolder(cfg(), id)))\n for (const ns of extraNamespaces) {\n await deleteStorageFolder(storageRef(storage, postStorageFolder(cfg(), ns)))\n }\n await deleteStoragePaths(storage, extraPaths)\n await deleteDoc(postDocRef(id))\n }\n\n return {\n createPostId,\n getPosts,\n getPost,\n getPostBySlug,\n getAdjacentPosts,\n createPost,\n updatePost,\n publishPost,\n deletePost,\n }\n}\n\nconst POST_PAGE_SIZE = 10\n\nexport function useMemiBoardPostList(\n boardId: MaybeRefOrGetter<string>,\n options: { pageSize?: number, publicOnly?: boolean | Ref<boolean> } = {},\n) {\n const db = useFirestore()\n const { isAdmin, isStaff, user } = useMemiBoardAuth()\n const cfg = () => useBoardPathConfig()\n const pageSize = options.pageSize ?? POST_PAGE_SIZE\n const uidValue = computed(() => user.value?.uid ?? '')\n\n const boardIdValue = computed(() => resolveBoardId(boardId))\n const postsColRef = () => postsCol(db, cfg())\n\n const publicOnlyValue = computed(() => {\n if (options.publicOnly === undefined) {\n return !(isAdmin.value || isStaff.value)\n }\n return typeof options.publicOnly === 'boolean' ? options.publicOnly : options.publicOnly.value\n })\n\n const headPosts = ref<PostModel[]>([])\n const olderPosts = ref<PostModel[]>([])\n /** 본인의 미게시 초안 — 목록 쿼리(isPublished==true)엔 안 걸리므로 별도 구독해 얹는다. */\n const ownDraftPosts = ref<PostModel[]>([])\n const headPending = ref(true)\n const hasMore = ref(true)\n const loadingMore = ref(false)\n const loadError = ref('')\n let headTailCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let olderCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stopHeadSubscription: Unsubscribe | undefined\n let stopOwnDraftSubscription: Unsubscribe | undefined\n\n function createdAtMs(post: PostModel): number {\n return post.createdAt?.toMillis?.() ?? 0\n }\n\n const posts = computed(() => {\n const byId = new Map<string, PostModel>()\n for (const post of [...olderPosts.value, ...headPosts.value, ...ownDraftPosts.value]) {\n if (post.id) byId.set(post.id, post)\n }\n return [...byId.values()].sort((a, b) =>\n createdAtMs(b) - createdAtMs(a) || String(b.id).localeCompare(String(a.id)),\n )\n })\n\n function baseConstraints(): QueryConstraint[] {\n const constraints: QueryConstraint[] = [\n where('boardId', '==', boardIdValue.value),\n where('isPublished', '==', true),\n ]\n if (publicOnlyValue.value) constraints.push(where('listed', '==', true))\n return constraints\n }\n\n function startHeadSubscription() {\n stopHeadSubscription?.()\n stopHeadSubscription = onSnapshot(query(\n postsColRef(),\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n fbLimit(pageSize),\n ), (snapshot) => {\n headPosts.value = snapshot.docs.map(item => ({ id: item.id, ...item.data() }) as PostModel)\n headTailCursor = snapshot.docs.at(-1) ?? null\n if (!olderCursor) hasMore.value = snapshot.docs.length >= pageSize\n headPending.value = false\n }, (cause) => {\n console.error('[memi-board:postList] onSnapshot failed', cause)\n loadError.value = (cause as Error).message || String(cause)\n headPending.value = false\n })\n }\n\n function startOwnDraftSubscription() {\n stopOwnDraftSubscription?.()\n const uid = uidValue.value\n if (!uid) {\n ownDraftPosts.value = []\n return\n }\n stopOwnDraftSubscription = onSnapshot(query(\n postsColRef(),\n where('boardId', '==', boardIdValue.value),\n where('authorUid', '==', uid),\n where('isPublished', '==', false),\n orderBy('createdAt', 'desc'),\n fbLimit(20),\n ), (snapshot) => {\n ownDraftPosts.value = snapshot.docs.map(item => ({ id: item.id, ...item.data() }) as PostModel)\n }, (cause) => {\n console.error('[memi-board:postList] own draft onSnapshot failed', cause)\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loadingMore.value || !hasMore.value || headPending.value) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const cursor = olderCursor ?? headTailCursor\n const constraints = [\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(pageSize + 1),\n ]\n const snapshot = await getDocs(query(postsColRef(), ...constraints))\n const pageDocs = snapshot.docs.slice(0, pageSize)\n const page = pageDocs.map(item => ({ id: item.id, ...item.data() }) as PostModel)\n const existingIds = new Set([...olderPosts.value, ...headPosts.value].map(post => post.id))\n olderPosts.value.push(...page.filter(post => !existingIds.has(post.id)))\n olderCursor = pageDocs.at(-1) ?? olderCursor\n hasMore.value = snapshot.docs.length > pageSize\n }\n catch (e) {\n loadError.value = (e as Error).message || String(e)\n console.error('[memi-board:postList] loadMore failed', e)\n }\n finally {\n loadingMore.value = false\n }\n }\n\n function reset() {\n stopHeadSubscription?.()\n headPosts.value = []\n olderPosts.value = []\n headTailCursor = null\n olderCursor = null\n hasMore.value = true\n loadError.value = ''\n headPending.value = true\n startHeadSubscription()\n startOwnDraftSubscription()\n }\n\n watch([boardIdValue, publicOnlyValue, uidValue], reset, { immediate: true })\n onScopeDispose(() => {\n stopHeadSubscription?.()\n stopOwnDraftSubscription?.()\n })\n\n return {\n posts,\n postsPending: headPending,\n hasMore,\n loadingMore,\n loadError,\n loadMore,\n }\n}\n\nexport function useMemiBoardPost(\n boardId: MaybeRefOrGetter<string>,\n postId: string | Ref<string>,\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n\n const bid = computed(() => resolveBoardId(boardId))\n const id = computed(() => (typeof postId === 'string' ? postId : postId.value))\n const postRef = computed(() => postDoc(db, cfg(), id.value))\n const bodyRef = computed(() => postBodyDoc(db, cfg(), id.value))\n\n const { data: meta, pending: metaPending } = useDocument<PostModel>(postRef)\n const { data: body, pending: bodyPending } = useDocument<{ content: string }>(bodyRef)\n\n const post = computed<PostDetail | null>(() => {\n // boardId 는 이제 문서 필드라, URL의 boardId 세그먼트와 실제 글이 다른 보드에\n // 속하면(잘못된 URL 등) 상세를 보여주지 않는다.\n if (!meta.value || meta.value.boardId !== bid.value) return null\n return { ...meta.value, id: id.value, content: body.value?.content ?? '' } as PostDetail\n })\n const pending = computed(() => metaPending.value || bodyPending.value)\n\n return { post, pending }\n}\n","import { computed, onScopeDispose, ref, toValue, watch } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\nimport {\n documentId,\n getDocs,\n limit as fbLimit,\n onSnapshot,\n orderBy,\n query,\n startAfter,\n where,\n} from 'firebase/firestore'\nimport type { DocumentData, QueryConstraint, QueryDocumentSnapshot, Unsubscribe } from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { postsCol } from '../utils/boardPaths'\nimport type { UserPostModel } from '../types'\n\nconst USER_POST_PAGE_SIZE = 10\n\n/**\n * 작성자 uid로 모든 보드를 가로질러 글을 모은다.\n * memiBoardPosts 가 flat 컬렉션이라 일반 where('authorUid',...) 쿼리로 충분 —\n * collectionGroup 이 필요했던 건 구버전이 보드별 posts 서브컬렉션이었을 때뿐.\n * 공개 글만 — canReadPost rules와 같은 기준(listed && isPublished)이라\n * 본인이 아니면 숨김 보드·미게시 글은 애초에 안 온다.\n */\nexport function useMemiBoardUserPosts(\n uid: MaybeRefOrGetter<string>,\n options: { pageSize?: number } = {},\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const postsColRef = () => postsCol(db, cfg())\n const pageSize = options.pageSize ?? USER_POST_PAGE_SIZE\n const uidValue = computed(() => toValue(uid)?.trim() || '')\n\n const headPosts = ref<UserPostModel[]>([])\n const olderPosts = ref<UserPostModel[]>([])\n const headPending = ref(true)\n const hasMore = ref(true)\n const loadingMore = ref(false)\n const loadError = ref('')\n let headTailCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let olderCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stopHeadSubscription: Unsubscribe | undefined\n\n function mapDoc(item: QueryDocumentSnapshot<DocumentData>): UserPostModel {\n return { id: item.id, ...item.data() } as UserPostModel\n }\n\n function createdAtMs(post: UserPostModel): number {\n return post.createdAt?.toMillis?.() ?? 0\n }\n\n const posts = computed(() => {\n const byId = new Map<string, UserPostModel>()\n for (const post of [...olderPosts.value, ...headPosts.value]) {\n if (post.id) byId.set(post.id, post)\n }\n return [...byId.values()].sort((a, b) =>\n createdAtMs(b) - createdAtMs(a) || String(b.id).localeCompare(String(a.id)),\n )\n })\n\n function baseConstraints(): QueryConstraint[] {\n return [\n where('authorUid', '==', uidValue.value),\n where('isPublished', '==', true),\n where('listed', '==', true),\n ]\n }\n\n function startHeadSubscription() {\n stopHeadSubscription?.()\n if (!uidValue.value) {\n headPending.value = false\n return\n }\n stopHeadSubscription = onSnapshot(query(\n postsColRef(),\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n fbLimit(pageSize),\n ), (snapshot) => {\n headPosts.value = snapshot.docs.map(mapDoc)\n headTailCursor = snapshot.docs.at(-1) ?? null\n if (!olderCursor) hasMore.value = snapshot.docs.length >= pageSize\n headPending.value = false\n }, (cause) => {\n console.error('[memi-board:userPosts] onSnapshot failed', cause)\n loadError.value = (cause as Error).message || String(cause)\n headPending.value = false\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loadingMore.value || !hasMore.value || headPending.value || !uidValue.value) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const cursor = olderCursor ?? headTailCursor\n const constraints = [\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(pageSize + 1),\n ]\n const snapshot = await getDocs(query(postsColRef(), ...constraints))\n const pageDocs = snapshot.docs.slice(0, pageSize)\n const page = pageDocs.map(mapDoc)\n const existingIds = new Set([...olderPosts.value, ...headPosts.value].map(post => post.id))\n olderPosts.value.push(...page.filter(post => !existingIds.has(post.id)))\n olderCursor = pageDocs.at(-1) ?? olderCursor\n hasMore.value = snapshot.docs.length > pageSize\n }\n catch (e) {\n loadError.value = (e as Error).message || String(e)\n console.error('[memi-board:userPosts] loadMore failed', e)\n }\n finally {\n loadingMore.value = false\n }\n }\n\n function reset() {\n stopHeadSubscription?.()\n headPosts.value = []\n olderPosts.value = []\n headTailCursor = null\n olderCursor = null\n hasMore.value = true\n loadError.value = ''\n headPending.value = true\n startHeadSubscription()\n }\n\n watch(uidValue, reset, { immediate: true })\n onScopeDispose(() => stopHeadSubscription?.())\n\n return {\n posts,\n postsPending: headPending,\n hasMore,\n loadingMore,\n loadError,\n loadMore,\n }\n}\n\nexport type { UserPostModel }\n","import { computed, onScopeDispose, ref, toValue, watch } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\nimport {\n documentId,\n getDocs,\n limit as fbLimit,\n onSnapshot,\n orderBy,\n query,\n startAfter,\n where,\n} from 'firebase/firestore'\nimport type { DocumentData, QueryConstraint, QueryDocumentSnapshot, Unsubscribe } from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { commentsCol } from '../utils/boardPaths'\nimport type { CommentModel } from '../types'\n\nconst USER_COMMENT_PAGE_SIZE = 10\n\n/**\n * 작성자 uid로 모든 보드를 가로질러 댓글을 모은다. memiBoardComments 읽기 규칙이\n * 더 이상 부모 글을 get()하지 않고 항상 허용이라(누구나 볼 수 있어야 하는 요구사항),\n * useMemiBoardUserPosts와 동일하게 authorUid 동등 필터만으로 충분하다 — 부모 글이\n * 비공개·삭제·블라인드 상태여도 댓글 자체는 여기서 그대로 보이고, 실제 글로\n * 이동했을 때 그 글의 접근 상태에 따라 표현하는 건 호스트(클릭 시 이동하는 페이지)의 몫이다.\n */\nexport function useMemiBoardUserComments(\n uid: MaybeRefOrGetter<string>,\n options: { pageSize?: number } = {},\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const commentsColRef = () => commentsCol(db, cfg())\n const pageSize = options.pageSize ?? USER_COMMENT_PAGE_SIZE\n const uidValue = computed(() => toValue(uid)?.trim() || '')\n\n const headComments = ref<CommentModel[]>([])\n const olderComments = ref<CommentModel[]>([])\n const headPending = ref(true)\n const hasMore = ref(true)\n const loadingMore = ref(false)\n const loadError = ref('')\n let headTailCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let olderCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stopHeadSubscription: Unsubscribe | undefined\n\n function mapDoc(item: QueryDocumentSnapshot<DocumentData>): CommentModel {\n return { id: item.id, ...item.data() } as CommentModel\n }\n\n function createdAtMs(comment: CommentModel): number {\n return comment.createdAt?.toMillis?.() ?? 0\n }\n\n const comments = computed(() => {\n const byId = new Map<string, CommentModel>()\n for (const comment of [...olderComments.value, ...headComments.value]) {\n if (comment.id) byId.set(comment.id, comment)\n }\n return [...byId.values()].sort((a, b) =>\n createdAtMs(b) - createdAtMs(a) || String(b.id).localeCompare(String(a.id)),\n )\n })\n\n function baseConstraints(): QueryConstraint[] {\n return [where('authorUid', '==', uidValue.value)]\n }\n\n function startHeadSubscription() {\n stopHeadSubscription?.()\n if (!uidValue.value) {\n headPending.value = false\n return\n }\n stopHeadSubscription = onSnapshot(query(\n commentsColRef(),\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n fbLimit(pageSize),\n ), (snapshot) => {\n headComments.value = snapshot.docs.map(mapDoc)\n headTailCursor = snapshot.docs.at(-1) ?? null\n if (!olderCursor) hasMore.value = snapshot.docs.length >= pageSize\n headPending.value = false\n }, (cause) => {\n console.error('[memi-board:userComments] onSnapshot failed', cause)\n loadError.value = (cause as Error).message || String(cause)\n headPending.value = false\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loadingMore.value || !hasMore.value || headPending.value || !uidValue.value) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const cursor = olderCursor ?? headTailCursor\n const constraints = [\n ...baseConstraints(),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(pageSize + 1),\n ]\n const snapshot = await getDocs(query(commentsColRef(), ...constraints))\n const pageDocs = snapshot.docs.slice(0, pageSize)\n const page = pageDocs.map(mapDoc)\n const existingIds = new Set([...olderComments.value, ...headComments.value].map(comment => comment.id))\n olderComments.value.push(...page.filter(comment => !existingIds.has(comment.id)))\n olderCursor = pageDocs.at(-1) ?? olderCursor\n hasMore.value = snapshot.docs.length > pageSize\n }\n catch (e) {\n loadError.value = (e as Error).message || String(e)\n console.error('[memi-board:userComments] loadMore failed', e)\n }\n finally {\n loadingMore.value = false\n }\n }\n\n function reset() {\n stopHeadSubscription?.()\n headComments.value = []\n olderComments.value = []\n headTailCursor = null\n olderCursor = null\n hasMore.value = true\n loadError.value = ''\n headPending.value = true\n startHeadSubscription()\n }\n\n watch(uidValue, reset, { immediate: true })\n onScopeDispose(() => stopHeadSubscription?.())\n\n return {\n comments,\n commentsPending: headPending,\n hasMore,\n loadingMore,\n loadError,\n loadMore,\n }\n}\n","import { computed, reactive, toValue } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\nimport { deleteDoc, getDoc, serverTimestamp, setDoc } from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { authorMemoDoc } from '../utils/boardPaths'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport type { AuthorMemoSentiment } from '../types'\n\nexport const AUTHOR_MEMO_MAX_LENGTH = 100\n\ninterface MemoEntry {\n text: string\n sentiment: AuthorMemoSentiment | null\n loaded: boolean\n pending: boolean\n saving: boolean\n error: string\n}\n\n/**\n * 같은 작성자가 목록에 여러 번 나오면 AuthorMenu 인스턴스도 여러 개 생기는데,\n * 각자 따로 상태를 들고 있으면 한쪽에서 저장해도 다른 쪽 아이콘·내용이 그대로다.\n * targetUid(+viewerUid)로 캐시를 키잉해서 모든 인스턴스가 같은 reactive 상태를\n * 공유하게 한다 — 하나가 저장하면 화면에 있는 나머지도 즉시 반응한다.\n */\nconst memoCache = new Map<string, MemoEntry>()\n\nfunction getEntry(key: string): MemoEntry {\n let entry = memoCache.get(key)\n if (!entry) {\n entry = reactive({ text: '', sentiment: null, loaded: false, pending: false, saving: false, error: '' })\n memoCache.set(key, entry)\n }\n return entry\n}\n\n/**\n * 다른 작성자에 대해 나만 보는 개인 메모 + 좋음/나쁨 평가. memiBoardUsers/{내uid}/memos/{targetUid}에\n * 저장되고, 문서 ID가 targetUid라 대상당 메모가 항상 하나다(재저장 시 덮어씀).\n * 다른 사용자·관리자는 규칙상 아예 못 읽으므로 비속어 필터 등 별도 검증이 없다.\n */\nexport function useMemiBoardAuthorMemo(targetUid: MaybeRefOrGetter<string>) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const { user } = useMemiBoardAuth()\n\n const targetUidValue = computed(() => toValue(targetUid))\n const cacheKey = computed(() => `${user.value?.uid ?? ''}:${targetUidValue.value}`)\n const memoRef = computed(() => {\n const viewerUid = user.value?.uid\n const target = targetUidValue.value\n if (!viewerUid || !target) return null\n return authorMemoDoc(db, cfg(), viewerUid, target)\n })\n\n const text = computed({\n get: () => getEntry(cacheKey.value).text,\n set: (value: string) => { getEntry(cacheKey.value).text = value },\n })\n const sentiment = computed(() => getEntry(cacheKey.value).sentiment)\n const loaded = computed(() => getEntry(cacheKey.value).loaded)\n const pending = computed(() => getEntry(cacheKey.value).pending)\n const saving = computed(() => getEntry(cacheKey.value).saving)\n const error = computed(() => getEntry(cacheKey.value).error)\n\n async function load(): Promise<void> {\n const ref_ = memoRef.value\n if (!ref_) return\n const entry = getEntry(cacheKey.value)\n entry.pending = true\n entry.error = ''\n try {\n const snapshot = await getDoc(ref_)\n const data = snapshot.data()\n entry.text = data ? ((data.text as string) ?? '') : ''\n entry.sentiment = data ? ((data.sentiment as AuthorMemoSentiment) ?? null) : null\n entry.loaded = true\n }\n catch (e) {\n entry.error = (e as Error).message || String(e)\n console.error('[memi-board:authorMemo] load failed', e)\n }\n finally {\n entry.pending = false\n }\n }\n\n /** 좋음/나쁨 버튼이 곧 저장 버튼이다 — 평가 없이 텍스트만 저장하는 경로는 없다. */\n async function save(nextText: string, nextSentiment: AuthorMemoSentiment): Promise<void> {\n const ref_ = memoRef.value\n if (!ref_) return\n const trimmed = nextText.trim()\n if (trimmed.length > AUTHOR_MEMO_MAX_LENGTH) {\n throw new Error(`메모는 ${AUTHOR_MEMO_MAX_LENGTH.toLocaleString()}자까지 작성할 수 있습니다.`)\n }\n const entry = getEntry(cacheKey.value)\n entry.saving = true\n entry.error = ''\n try {\n await setDoc(ref_, { text: trimmed, sentiment: nextSentiment, updatedAt: serverTimestamp() })\n entry.text = trimmed\n entry.sentiment = nextSentiment\n entry.loaded = true\n }\n catch (e) {\n entry.error = (e as Error).message || String(e)\n console.error('[memi-board:authorMemo] save failed', e)\n throw e\n }\n finally {\n entry.saving = false\n }\n }\n\n async function remove(): Promise<void> {\n const ref_ = memoRef.value\n if (!ref_) return\n const entry = getEntry(cacheKey.value)\n entry.saving = true\n entry.error = ''\n try {\n await deleteDoc(ref_)\n entry.text = ''\n entry.sentiment = null\n entry.loaded = true\n }\n catch (e) {\n entry.error = (e as Error).message || String(e)\n console.error('[memi-board:authorMemo] remove failed', e)\n throw e\n }\n finally {\n entry.saving = false\n }\n }\n\n return { text, sentiment, loaded, pending, saving, error, load, save, remove }\n}\n","import { computed, onScopeDispose, ref, toValue, watch } from 'vue'\nimport type { MaybeRefOrGetter, Ref } from 'vue'\nimport type { DocumentData, QueryDocumentSnapshot, Unsubscribe } from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport {\n doc,\n documentId,\n getDocs,\n increment,\n limit,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n startAfter,\n updateDoc,\n where,\n writeBatch,\n} from 'firebase/firestore'\nimport { useBoardPathConfig } from '../config'\nimport {\n commentDoc,\n commentsCol,\n postDoc,\n} from '../utils/boardPaths'\nimport type { CommentModel } from '../types'\n\nexport interface AddCommentInput {\n body: string\n authorUid: string\n authorName: string | null\n authorPhoto: string | null\n}\n\nexport interface AddReplyInput extends AddCommentInput {\n parent: CommentModel\n}\n\nconst COMMENT_PAGE_SIZE = 10\nconst REPLY_PAGE_SIZE = 5\nexport const COMMENT_BODY_MAX_LENGTH = 1_000\n\nfunction normalizedCommentBody(body: string): string {\n const trimmed = body.trim()\n if (!trimmed) throw new Error('댓글 내용을 입력해 주세요.')\n if (trimmed.length > COMMENT_BODY_MAX_LENGTH) {\n throw new Error(`댓글은 ${COMMENT_BODY_MAX_LENGTH.toLocaleString()}자까지 작성할 수 있습니다.`)\n }\n return trimmed\n}\n\n/** 10개씩 순차 조회한 뒤 마지막 페이지에서만 최신 1개를 실시간 구독한다. */\nexport function useMemiBoardComments(\n boardId: MaybeRefOrGetter<string>,\n postId: MaybeRefOrGetter<string>,\n options: { subscribe?: boolean } = {},\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const commentsColRef = () => commentsCol(db, cfg())\n\n const bid = computed(() => toValue(boardId))\n const id = computed(() => toValue(postId))\n\n // limit(1) 리스너는 최신 댓글 알림 용도다. 쿼리 결과 교체로 기존 댓글이\n // 사라지지 않도록 수신 문서를 별도 배열에 누적한다.\n const liveComments = ref<CommentModel[]>([])\n // parentId 도입 전 댓글은 실시간 10개 슬롯과 분리해 최초 1회만 읽는다.\n const legacyComments = ref<CommentModel[]>([])\n const legacyPending = ref(options.subscribe !== false)\n const pagedComments = ref<CommentModel[]>([])\n const loadingMore = ref(false)\n const hasMore = ref(options.subscribe !== false)\n const initialPending = ref(options.subscribe !== false)\n let pageCursor: QueryDocumentSnapshot<DocumentData> | null = null\n let liveEnableTimer: ReturnType<typeof setTimeout> | undefined\n let stopLiveSubscription: Unsubscribe | undefined\n\n function createdAtMs(comment: CommentModel): number {\n return comment.createdAt?.toMillis?.() ?? 0\n }\n\n const comments = computed(() => {\n const byId = new Map<string, CommentModel>()\n for (const comment of [...liveComments.value, ...legacyComments.value, ...pagedComments.value]) {\n if (comment.id) byId.set(comment.id, comment)\n }\n return [...byId.values()].sort((a, b) =>\n createdAtMs(a) - createdAtMs(b) || String(a.id).localeCompare(String(b.id)),\n )\n })\n\n watch([id, bid], async ([currentId]) => {\n pagedComments.value = []\n liveComments.value = []\n legacyComments.value = []\n pageCursor = null\n clearTimeout(liveEnableTimer)\n liveEnableTimer = undefined\n stopLiveSubscription?.()\n stopLiveSubscription = undefined\n hasMore.value = options.subscribe !== false\n if (options.subscribe === false) return\n\n legacyPending.value = true\n initialPending.value = true\n try {\n const snapshotPromise = getDocs(query(\n commentsColRef(),\n where('postId', '==', currentId),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ))\n await Promise.all([snapshotPromise.then((snapshot) => {\n if (id.value !== currentId) return\n const allComments = snapshot.docs\n .map(item => ({ id: item.id, ...item.data() } as CommentModel))\n legacyComments.value = allComments.filter(comment => comment.parentId === undefined)\n }), loadMore()])\n if (id.value !== currentId) return\n }\n finally {\n if (id.value === currentId) {\n legacyPending.value = false\n initialPending.value = false\n }\n }\n }, { immediate: true })\n\n function startLiveSubscription() {\n stopLiveSubscription?.()\n stopLiveSubscription = onSnapshot(query(\n commentsColRef(),\n where('postId', '==', id.value),\n where('parentId', '==', null),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n limit(1),\n ), (snapshot) => {\n const byId = new Map(liveComments.value.map(comment => [comment.id, comment]))\n for (const change of snapshot.docChanges()) {\n if (change.type === 'removed') continue\n byId.set(change.doc.id, { id: change.doc.id, ...change.doc.data() } as CommentModel)\n }\n liveComments.value = [...byId.values()]\n }, (cause) => {\n console.error('[memi-board:comments] onSnapshot failed', cause)\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loadingMore.value || !hasMore.value) return\n loadingMore.value = true\n try {\n const constraints = [\n where('postId', '==', id.value),\n where('parentId', '==', null),\n orderBy('createdAt', 'asc'),\n orderBy(documentId(), 'asc'),\n ...(pageCursor ? [startAfter(pageCursor)] : []),\n limit(COMMENT_PAGE_SIZE + 1),\n ]\n const snapshot = await getDocs(query(\n commentsColRef(),\n ...constraints,\n ))\n const pageDocs = snapshot.docs.slice(0, COMMENT_PAGE_SIZE)\n const page = pageDocs.map(item => ({ id: item.id, ...item.data() } as CommentModel))\n const existing = new Set(pagedComments.value.map(comment => comment.id))\n pagedComments.value.push(...page.filter(comment => !existing.has(comment.id)))\n pageCursor = pageDocs.at(-1) ?? pageCursor\n if (snapshot.size <= COMMENT_PAGE_SIZE) {\n hasMore.value = false\n liveEnableTimer = setTimeout(() => {\n liveEnableTimer = undefined\n startLiveSubscription()\n }, 1_000)\n }\n }\n finally {\n loadingMore.value = false\n }\n }\n\n onScopeDispose(() => {\n clearTimeout(liveEnableTimer)\n stopLiveSubscription?.()\n })\n\n async function addComment(input: AddCommentInput): Promise<void> {\n const body = normalizedCommentBody(input.body)\n const batch = writeBatch(db)\n const commentRef = doc(commentsColRef())\n batch.set(commentRef, {\n postId: id.value,\n boardId: bid.value,\n body,\n authorUid: input.authorUid,\n authorName: input.authorName,\n authorPhoto: input.authorPhoto,\n moderationStatus: 'approved',\n parentId: null,\n rootId: commentRef.id,\n depth: 0,\n replyToUid: null,\n replyToName: null,\n replyCount: 0,\n isReply: false,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n batch.update(postDoc(db, cfg(), id.value), { commentCount: increment(1) })\n await batch.commit()\n }\n\n async function addReply(input: AddReplyInput): Promise<void> {\n const body = normalizedCommentBody(input.body)\n if (!input.parent.id) throw new Error('답글 대상 댓글을 찾을 수 없습니다.')\n const batch = writeBatch(db)\n const replyRef = doc(commentsColRef())\n const rootId = input.parent.parentId == null\n ? input.parent.id\n : input.parent.rootId\n if (!rootId) throw new Error('댓글 스레드를 찾을 수 없습니다.')\n const depth = Math.min((input.parent.depth ?? 0) + 1, 2)\n\n batch.set(replyRef, {\n postId: id.value,\n boardId: bid.value,\n body,\n authorUid: input.authorUid,\n authorName: input.authorName,\n authorPhoto: input.authorPhoto,\n moderationStatus: 'approved',\n parentId: input.parent.id,\n rootId,\n depth,\n replyToUid: input.parent.authorUid,\n replyToName: input.parent.authorName,\n replyCount: 0,\n isReply: true,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n batch.update(commentDoc(db, cfg(), rootId), { replyCount: increment(1) })\n batch.update(postDoc(db, cfg(), id.value), { commentCount: increment(1) })\n await batch.commit()\n }\n\n async function deleteComment(comment: CommentModel): Promise<void> {\n if (!comment.id) return\n if (comment.parentId == null && (comment.replyCount ?? 0) > 0) {\n throw new Error('답글이 있는 댓글은 현재 삭제할 수 없습니다.')\n }\n const batch = writeBatch(db)\n batch.delete(commentDoc(db, cfg(), comment.id))\n if (comment.parentId && comment.rootId) {\n batch.update(commentDoc(db, cfg(), comment.rootId), { replyCount: increment(-1) })\n }\n batch.update(postDoc(db, cfg(), id.value), { commentCount: increment(-1) })\n await batch.commit()\n pagedComments.value = pagedComments.value.filter(item => item.id !== comment.id)\n legacyComments.value = legacyComments.value.filter(item => item.id !== comment.id)\n liveComments.value = liveComments.value.filter(item => item.id !== comment.id)\n }\n\n async function updateComment(commentId: string, body: string): Promise<void> {\n const trimmed = normalizedCommentBody(body)\n await updateDoc(commentDoc(db, cfg(), commentId), {\n body: trimmed,\n updatedAt: serverTimestamp(),\n })\n }\n\n async function setCommentBlinded(commentId: string, isBlinded: boolean, moderatorUid: string): Promise<void> {\n await updateDoc(commentDoc(db, cfg(), commentId), {\n isBlinded,\n moderatedAt: serverTimestamp(),\n moderatedBy: moderatorUid,\n })\n }\n\n return {\n comments,\n commentsPending: computed(() => initialPending.value || legacyPending.value),\n hasMore,\n loadingMore,\n loadMore,\n addComment,\n addReply,\n updateComment,\n setCommentBlinded,\n deleteComment,\n }\n}\n\n/**\n * 펼친 스레드의 답글을 5개씩 일회성 조회한다.\n * rootId 는 전역 고유(댓글 auto-ID)라 postId 필터 없이도 정확히 한 스레드로 스코핑된다.\n */\nexport function useMemiBoardReplies(boardId: string, postId: string, rootId: string) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const commentsColRef = () => commentsCol(db, cfg())\n const replies = ref<CommentModel[]>([])\n const loading = ref(false)\n const loaded = ref(false)\n const hasMore = ref(true)\n let cursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stopLiveSubscription: Unsubscribe | undefined\n\n function sortReplies(list: CommentModel[]) {\n return list.sort((a, b) =>\n (a.createdAt?.toMillis?.() ?? 0) - (b.createdAt?.toMillis?.() ?? 0)\n || String(a.id).localeCompare(String(b.id)),\n )\n }\n\n function startLiveSubscription() {\n if (stopLiveSubscription) return\n stopLiveSubscription = onSnapshot(query(\n commentsColRef(),\n where('rootId', '==', rootId),\n where('isReply', '==', true),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n limit(1),\n ), (snapshot) => {\n const byId = new Map(replies.value.map(reply => [reply.id, reply]))\n for (const change of snapshot.docChanges()) {\n if (change.type === 'removed') continue\n byId.set(change.doc.id, { id: change.doc.id, ...change.doc.data() } as CommentModel)\n }\n replies.value = sortReplies([...byId.values()])\n }, (cause) => {\n console.error('[memi-board:replies] onSnapshot failed', cause)\n })\n }\n\n async function loadMore(): Promise<void> {\n if (loading.value || !hasMore.value) return\n loading.value = true\n try {\n const constraints = [\n where('rootId', '==', rootId),\n where('isReply', '==', true),\n orderBy('createdAt', 'asc'),\n orderBy(documentId(), 'asc'),\n ...(cursor ? [startAfter(cursor)] : []),\n limit(REPLY_PAGE_SIZE + 1),\n ]\n const snapshot = await getDocs(query(\n commentsColRef(),\n ...constraints,\n ))\n const existing = new Set(replies.value.map(reply => reply.id))\n const pageDocs = snapshot.docs.slice(0, REPLY_PAGE_SIZE)\n const page = pageDocs\n .map(item => ({ id: item.id, ...item.data() } as CommentModel))\n .filter(reply => !existing.has(reply.id))\n replies.value = sortReplies([...replies.value, ...page])\n cursor = pageDocs.at(-1) ?? cursor\n loaded.value = true\n if (snapshot.size <= REPLY_PAGE_SIZE) {\n hasMore.value = false\n startLiveSubscription()\n }\n }\n finally {\n loading.value = false\n }\n }\n\n function reset() {\n stopLiveSubscription?.()\n stopLiveSubscription = undefined\n replies.value = []\n cursor = null\n loaded.value = false\n hasMore.value = true\n }\n\n async function refresh(): Promise<void> {\n reset()\n await loadMore()\n }\n\n onScopeDispose(() => stopLiveSubscription?.())\n\n return { replies, loading, loaded, hasMore, loadMore, refresh }\n}\n","import { onScopeDispose, ref, watch } from 'vue'\nimport type { Unsubscribe } from 'firebase/firestore'\nimport { useFirestore, useCurrentUser } from 'vuefire'\nimport {\n increment,\n onSnapshot,\n runTransaction,\n serverTimestamp,\n} from 'firebase/firestore'\nimport { useBoardPathConfig } from '../config'\nimport { likeDoc, postDoc } from '../utils/boardPaths'\n\nexport function useMemiBoardLikes(boardId: string, postId: string) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const user = useCurrentUser()\n\n const isLiked = ref(false)\n const likePending = ref(false)\n let stopSubscription: Unsubscribe | undefined\n\n function likeDocRef(uid: string) {\n return likeDoc(db, cfg(), postId, uid)\n }\n\n watch(user, (current) => {\n stopSubscription?.()\n stopSubscription = undefined\n if (!current) {\n isLiked.value = false\n return\n }\n stopSubscription = onSnapshot(likeDocRef(current.uid), (snap) => {\n isLiked.value = snap.exists()\n }, (cause) => {\n console.error('[memi-board:likes] onSnapshot failed', cause)\n })\n }, { immediate: true })\n\n onScopeDispose(() => stopSubscription?.())\n\n async function toggleLike(): Promise<boolean> {\n const uid = user.value?.uid\n if (!uid) throw new Error('로그인이 필요합니다.')\n if (likePending.value) return isLiked.value\n likePending.value = true\n try {\n const likeRef = likeDocRef(uid)\n const postRef = postDoc(db, cfg(), postId)\n return await runTransaction(db, async (tx) => {\n const likeSnap = await tx.get(likeRef)\n if (likeSnap.exists()) {\n tx.delete(likeRef)\n tx.update(postRef, { likeCount: increment(-1) })\n return false\n }\n tx.set(likeRef, { uid, postId, boardId, createdAt: serverTimestamp() })\n tx.update(postRef, { likeCount: increment(1) })\n return true\n })\n }\n finally {\n likePending.value = false\n }\n }\n\n return { isLiked, likePending, toggleLike }\n}\n","import { increment, updateDoc } from 'firebase/firestore'\nimport { useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { postDoc } from '../utils/boardPaths'\n\n/**\n * 게시글 조회수 (memiBoardPosts, flat).\n */\nexport function useMemiBoardViews() {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n\n function sessionKey(boardId: string, postId: string): string {\n const { postsCollection } = cfg()\n return `memi-board:viewed:${postsCollection}:${boardId}:${postId}`\n }\n\n function hasRecordedView(boardId: string, postId: string): boolean {\n if (typeof sessionStorage === 'undefined') return false\n try {\n return sessionStorage.getItem(sessionKey(boardId, postId)) === '1'\n }\n catch {\n return false\n }\n }\n\n function markRecorded(boardId: string, postId: string): void {\n if (typeof sessionStorage === 'undefined') return\n try {\n sessionStorage.setItem(sessionKey(boardId, postId), '1')\n }\n catch {\n // ignore\n }\n }\n\n async function recordView(boardId: string, postId: string): Promise<void> {\n const b = boardId?.trim()\n const id = postId?.trim()\n if (!b || !id) return\n if (hasRecordedView(b, id)) return\n markRecorded(b, id)\n try {\n await updateDoc(postDoc(db, cfg(), id), {\n viewCount: increment(1),\n })\n }\n catch (error) {\n try {\n sessionStorage.removeItem(sessionKey(b, id))\n }\n catch {\n // ignore\n }\n if (import.meta.dev) {\n console.warn('[memi-board] recordView failed', error)\n }\n }\n }\n\n return {\n recordView,\n hasRecordedView,\n }\n}\n","export const MODERATION_SYSTEM = `당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"`\n\n/** 사용자 안내용 카테고리 라벨 */\nexport const MODERATION_CATEGORY_LABELS: Record<string, string> = {\n abuse: '욕설·비속어·모욕',\n spam: '스팸·광고',\n adult: '선정적·성인 내용',\n violence: '폭력·위협',\n other: '부적절한 내용',\n none: '',\n}\n\n/** 차단 사유를 사용자에게 읽기 쉽게 정리 */\nexport function formatModerationUserReason(opts: {\n reason?: string\n category?: string\n via?: string\n localHit?: string\n}): string {\n if (opts.via === 'local' && opts.localHit) {\n const hit = opts.localHit.trim()\n // 짧은 금칙어만 표시 (너무 길면 유형만)\n if (hit.length > 0 && hit.length <= 12) {\n return `「${hit}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.`\n }\n return '욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.'\n }\n\n const raw = (opts.reason || '').trim()\n if (raw) return raw\n\n const cat = opts.category && opts.category !== 'none'\n ? MODERATION_CATEGORY_LABELS[opts.category] || opts.category\n : ''\n if (cat) return `${cat}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.`\n return '게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.'\n}\n\n/** API 없이도 1차로 걸러내는 한국어/영어 비속어 (부분 문자열 매칭) */\nexport const DEFAULT_LOCAL_BLOCKLIST = [\n '씨발', '시발', '시이발', '씨팔', '시팔', '병신', '븅신', '지랄', '좆', '존나',\n '개새끼', '개새', '새끼', '꺼져', '닥쳐', '씹', '니미', '느금', '애미', '애비',\n 'fuck', 'shit', 'bitch', 'asshole',\n]\n\nexport function buildLocalBlockRegex(patterns: string[]): RegExp {\n return new RegExp(patterns.join('|'), 'i')\n}\n\nconst CATEGORIES = new Set(['none', 'abuse', 'spam', 'adult', 'violence', 'other'])\n\nexport function parseModerationJson(raw: string): { flagged: boolean, category: string, reason: string } | null {\n const trimmed = raw.trim()\n if (!trimmed) return null\n\n let parsed: unknown\n try {\n parsed = JSON.parse(trimmed)\n }\n catch {\n const match = trimmed.match(/\\{[\\s\\S]*\\}/)\n if (!match) return null\n try {\n parsed = JSON.parse(match[0])\n }\n catch {\n return null\n }\n }\n\n if (typeof parsed !== 'object' || parsed === null) return null\n const obj = parsed as Record<string, unknown>\n if (typeof obj.flagged !== 'boolean') return null\n\n const category = typeof obj.category === 'string' && CATEGORIES.has(obj.category) ? obj.category : (obj.flagged ? 'other' : 'none')\n const reason = typeof obj.reason === 'string' ? obj.reason : ''\n return { flagged: obj.flagged, category, reason }\n}\n","import { getAI, getGenerativeModel, GoogleAIBackend, Schema } from 'firebase/ai'\nimport { useFirebaseApp } from 'vuefire'\nimport {\n buildLocalBlockRegex,\n DEFAULT_LOCAL_BLOCKLIST,\n formatModerationUserReason,\n MODERATION_SYSTEM,\n parseModerationJson,\n} from '../utils/moderation-prompt'\nimport { useMemiBoardConfig } from '../config'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport type { ModerationResult, ModerationVia } from '../types'\n\nconst APPROVED: ModerationResult = { flagged: false, category: 'none', reason: '', via: 'empty' }\n\n/**\n * 글쓰기 전 블로킹 검열.\n * 이용 제한 확인 → 로컬 비속어 → Firebase AI Logic(Gemini).\n * 콘텐츠 차단(local/ai) 시 board users 경고 누적.\n */\nexport function useMemiBoardModeration() {\n const config = useMemiBoardConfig()\n const moderation = config.moderation ?? {}\n const app = useFirebaseApp()\n const {\n isWriteRestricted,\n restrictedMessage,\n recordContentModerationBlock,\n } = useMemiBoardAuth()\n\n const localBlockRe = buildLocalBlockRegex([...DEFAULT_LOCAL_BLOCKLIST, ...(moderation.localBlocklist ?? [])])\n\n function localCheck(text: string): ModerationResult | null {\n const match = text.match(localBlockRe)\n if (match) {\n const hit = match[0] || ''\n return {\n flagged: true,\n category: 'abuse',\n reason: formatModerationUserReason({ via: 'local', localHit: hit, category: 'abuse' }),\n via: 'local',\n }\n }\n return null\n }\n\n function getModerationModel() {\n const modelName = moderation.model ?? 'gemini-3.5-flash-lite'\n const useLimited = moderation.useLimitedUseAppCheckTokens !== false\n const ai = getAI(app, {\n backend: new GoogleAIBackend(),\n useLimitedUseAppCheckTokens: useLimited,\n })\n return getGenerativeModel(ai, {\n model: modelName,\n systemInstruction: MODERATION_SYSTEM,\n generationConfig: {\n temperature: 0,\n maxOutputTokens: 256,\n responseMimeType: 'application/json',\n responseSchema: Schema.object({\n properties: {\n flagged: Schema.boolean(),\n category: Schema.string(),\n reason: Schema.string(),\n },\n }),\n },\n })\n }\n\n function errorResult(): ModerationResult {\n if (moderation.onError === 'block') {\n return {\n flagged: true,\n category: 'other',\n reason: '내용 검토에 실패해 글을 등록할 수 없습니다. 잠시 후 다시 시도해 주세요.',\n error: true,\n via: 'ai-error-block',\n }\n }\n return {\n flagged: false,\n category: 'none',\n reason: '',\n error: true,\n via: 'ai-error-allow',\n }\n }\n\n /** 사유 + 경고 횟수를 한 문장으로 */\n function withUserFacingReason(result: ModerationResult, messageSuffix = ''): ModerationResult {\n const why = formatModerationUserReason({\n reason: result.reason,\n category: result.category,\n via: result.via,\n })\n // 예: 「시발」 같은 표현은… (경고 2/3)\n const reason = messageSuffix ? `${why}${messageSuffix}` : why\n return { ...result, reason }\n }\n\n /** 콘텐츠 위반(local/ai)만 경고 누적. API 오류·이용제한 자체는 제외. */\n async function applyStrikeIfContentBlock(result: ModerationResult): Promise<ModerationResult> {\n if (!result.flagged || result.error) return result\n if (result.via !== 'local' && result.via !== 'ai') return result\n try {\n const { messageSuffix } = await recordContentModerationBlock()\n return withUserFacingReason(result, messageSuffix)\n }\n catch (e) {\n console.warn('[memi-board] record moderation strike failed', e instanceof Error ? e.message : e)\n return withUserFacingReason(result)\n }\n }\n\n async function checkText(text: string): Promise<ModerationResult> {\n if (isWriteRestricted.value) {\n return {\n flagged: true,\n category: 'other',\n reason: restrictedMessage.value\n || '콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.',\n via: 'restricted',\n }\n }\n\n const trimmed = text.trim()\n if (!trimmed) {\n return { ...APPROVED, via: 'empty' as ModerationVia }\n }\n\n const local = localCheck(trimmed)\n if (local) return applyStrikeIfContentBlock(local)\n\n if (moderation.enabled === false) {\n return { ...APPROVED, via: 'disabled' }\n }\n\n try {\n const model = getModerationModel()\n const result = await model.generateContent(`심사할 텍스트:\\n\"\"\"${trimmed.slice(0, 4000)}\"\"\"`)\n const raw = result.response.text()\n const parsed = parseModerationJson(raw)\n if (!parsed) return errorResult()\n const out: ModerationResult = {\n flagged: parsed.flagged,\n category: parsed.category as ModerationResult['category'],\n reason: formatModerationUserReason({\n reason: parsed.reason,\n category: parsed.category,\n via: 'ai',\n }),\n via: 'ai',\n }\n if (out.flagged) return applyStrikeIfContentBlock(out)\n return out\n }\n catch (e) {\n console.warn('[memi-board] moderation AI failed', e instanceof Error ? e.message : e)\n return errorResult()\n }\n }\n\n async function checkImage(file: File): Promise<ModerationResult> {\n if (isWriteRestricted.value) {\n return {\n flagged: true,\n category: 'other',\n reason: restrictedMessage.value\n || '콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.',\n via: 'restricted',\n }\n }\n\n if (moderation.enabled === false || !moderation.moderateImages) {\n return { ...APPROVED, via: 'disabled' }\n }\n\n try {\n const base64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '')\n reader.onerror = reject\n reader.readAsDataURL(file)\n })\n\n const model = getModerationModel()\n const result = await model.generateContent([\n { inlineData: { mimeType: file.type || 'image/jpeg', data: base64 } },\n '이 이미지를 심사해 주세요.',\n ])\n const raw = result.response.text()\n const parsed = parseModerationJson(raw)\n if (!parsed) return errorResult()\n const out: ModerationResult = {\n flagged: parsed.flagged,\n category: parsed.category as ModerationResult['category'],\n reason: formatModerationUserReason({\n reason: parsed.reason,\n category: parsed.category,\n via: 'ai',\n }),\n via: 'ai',\n }\n if (out.flagged) return applyStrikeIfContentBlock(out)\n return out\n }\n catch (e) {\n console.warn('[memi-board] moderation AI image failed', e instanceof Error ? e.message : e)\n return errorResult()\n }\n }\n\n return { checkText, checkImage }\n}\n","import { computed, toValue } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\nimport { serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useCollection, useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { boardSsrKey, boardUserDoc, boardUsersCol } from '../utils/boardPaths'\nimport type { BoardUserModel, BoardUserRole } from '../types'\n\nexport const BOARD_USER_ROLES: Array<{\n value: BoardUserRole\n label: string\n description: string\n}> = [\n { value: 'admin', label: '관리자', description: '사용자 역할, 게시판 설정과 모든 콘텐츠를 관리합니다.' },\n { value: 'staff', label: '스태프', description: '모든 게시글과 댓글을 관리하지만 사용자 역할과 게시판 설정은 바꿀 수 없습니다.' },\n { value: 'user', label: '사용자', description: '글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다.' },\n]\n\n/**\n * memiBoardUsers/{uid}는 이메일만 빼면 공개 프로필이라 목록 읽기 자체는 누구나 할 수\n * 있다(rules) — 이 컴포저블은 관리 화면(역할 변경)에서만 쓰라고 있는 것뿐이다.\n * enabled가 false인 동안은 쿼리 자체를 시작하지 않는다 — role 확인이 끝나기 전에\n * 미리 구독을 걸어두면 화면이 불필요하게 로딩 상태로 오래 머문다.\n *\n * 경로: memiBoardUsers/{uid} (flat)\n */\nexport function useMemiBoardUsers(options: { enabled?: MaybeRefOrGetter<boolean> } = {}) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const usersQuery = computed(() => (toValue(options.enabled) ?? true) ? boardUsersCol(db, cfg()) : null)\n const users = useCollection<BoardUserModel>(usersQuery, {\n ssrKey: boardSsrKey(cfg(), 'users-management'),\n })\n\n async function updateUserRole(uid: string, role: BoardUserRole) {\n if (!BOARD_USER_ROLES.some(item => item.value === role)) throw new Error('지원하지 않는 역할입니다.')\n await updateDoc(boardUserDoc(db, cfg(), uid), { role, updatedAt: serverTimestamp() })\n }\n\n return { users, usersPending: users.pending, updateUserRole }\n}\n","import { computed, toValue } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\nimport { useDocument, useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { boardUserDoc } from '../utils/boardPaths'\nimport type { BoardUserModel } from '../types'\n\n/**\n * 다른 사용자의 공개 프로필. memiBoardUsers/{uid}는 이메일만 빼면 전부 공개라\n * 누구나 조회할 수 있다(rules) — email 필드는 애초에 이 문서에 없다.\n */\nexport function useMemiBoardUserProfile(uid: MaybeRefOrGetter<string>) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const uidValue = computed(() => toValue(uid))\n const profileRef = computed(() => {\n const value = uidValue.value\n return value ? boardUserDoc(db, cfg(), value) : null\n })\n\n const { data: profile, pending } = useDocument<BoardUserModel>(profileRef)\n\n return { profile, profilePending: pending }\n}\n","export interface VersionHistoryEntry {\n version: string\n date: string\n highlights: string[]\n note?: string\n}\n\n/**\n * MemiBoardVersionHistory 컴포넌트가 그대로 보여주는 데이터.\n * 코드 수준의 자세한 변경 내역은 이 레포의 CHANGELOG.md를 참고 — 새 버전을 릴리스할 때 이 배열도 함께 갱신한다.\n */\nexport const versionHistory: VersionHistoryEntry[] = [\n {\n version: '0.27.0',\n date: '2026-08-12',\n highlights: [\n '일반 목록 디자인을 다듬었어요 — 사진이 있는 글은 제목과 작성자 사이에 동그란 미리보기로 보여요',\n '조밀 보기를 커뮤니티 게시판처럼 한 줄로 다시 짰어요 — 시간·작성자·조회·추천·게시판을 한 줄에, 사진 있으면 오른쪽에 작은 미리보기가 붙어요',\n '두 줄 이상으로 쓴 글은 목록 요약에서도 줄바꿈이 그대로 보여요',\n ],\n },\n {\n version: '0.26.0',\n date: '2026-08-12',\n highlights: [\n '다른 사람 글을 보다가 그 사람에 대해 나만 보는 메모를 남길 수 있어요 — 좋음/나쁨 표시도 같이 남겨요',\n '메모는 작성자 이름 옆 아이콘을 눌러서 쓰고, 다른 사람은(관리자 포함) 절대 못 봐요',\n ],\n },\n {\n version: '0.25.0',\n date: '2026-08-12',\n highlights: [\n '\"작성글 보기\"처럼 \"작성한 댓글 보기\"도 생겼어요 — 여러 게시판에 흩어진 내 댓글을 한 곳에서 모아 봐요',\n ],\n note: '이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요.',\n },\n {\n version: '0.24.0',\n date: '2026-08-11',\n highlights: [\n '사진 올리는 코드를 분리해서, 게시판이 더 안정적으로 빌드되고 열려요',\n ],\n },\n {\n version: '0.23.0',\n date: '2026-08-11',\n highlights: [\n '글·댓글·좋아요를 게시판 아래 중첩시키지 않고 memiBoardPosts/memiBoardComments/memiBoardLikes로 따로 모아요 — 보드 ID는 각 글의 필드로 들어가요',\n '작성자별 글 모음이 더 가벼워졌어요 — 여러 게시판을 뒤지는 대신 한 번에 찾아요',\n '\"내가 좋아요한 글\" 같은 기능을 나중에 붙이기 쉬워졌어요',\n '날짜에 마우스를 올려야 보이던 작성 시각을 이제 눌러서 볼 수 있어요 — 폰에서도 똑같이 동작해요',\n ],\n note: '기존 게시판 데이터와 호환되지 않아요 — 새로 설치하는 것처럼 다시 시작해야 해요.',\n },\n {\n version: '0.21.1',\n date: '2026-08-07',\n highlights: [\n '관리자가 쓴 댓글에 답글을 달 때 오류가 나던 문제를 고쳤어요',\n ],\n },\n {\n version: '0.21.0',\n date: '2026-08-06',\n highlights: [\n '카테고리 대신 게시판(보드)만 있어요 — 예전 카테고리 이름이 곧 보드 ID예요',\n '글은 memiBoards/{보드}/posts, 설정은 settings, 회원 역할은 memiBoardUsers 에 모아요',\n ],\n },\n {\n version: '0.20.0',\n date: '2026-08-06',\n highlights: [\n '게시판 데이터를 memiBoards/{게시판ID}/… 아래에 모았어요 (여러 게시판을 같은 프로젝트에 둘 수 있어요)',\n '설정·글·사용자 경로가 게시판마다 분리돼요 — 호스트는 boardId만 맞추면 돼요',\n ],\n },\n {\n version: '0.19.3',\n date: '2026-08-06',\n highlights: [\n '이미지 목록 게시판은 인스타처럼 제목 없이 사진+내용만 올리면 돼요',\n '사진을 여러 장 올리고 끌어 순서를 바꿀 수 있어요 (첫 장이 대표)',\n ],\n },\n {\n version: '0.19.2',\n date: '2026-08-06',\n highlights: [\n '이미지만 넣고 글 없이 올리는 건 막았어요 — 본문에 글자를 넣어야 해요',\n ],\n },\n {\n version: '0.19.1',\n date: '2026-08-06',\n highlights: [\n '공유 미리보기 이미지를 가벼운 썸네일로 쓰도록 바꿨어요 (카톡 등에 더 잘 맞아요)',\n ],\n },\n {\n version: '0.19.0',\n date: '2026-08-06',\n highlights: [\n '게시판 목록·글 공유 미리보기(제목·설명·대표 이미지)를 모듈만으로 붙일 수 있어요',\n '호스트가 따로 SEO API를 만들 필요 없이, 공개 글 기준으로 미리보기가 채워져요',\n ],\n },\n {\n version: '0.18.0',\n date: '2026-08-06',\n highlights: [\n '게시판을 보임/숨김으로 나눌 수 있어요 — 숨김은 일기장처럼 권한이 있는 사람만 볼 수 있어요',\n '권한이 없는 글·게시판은 로딩에 멈추지 않고 안내 메시지로 알려 줘요',\n '글 붙여넣기로 넣은 이미지가 파일명과 함께 두 번 들어가던 문제를 고쳤어요',\n ],\n },\n {\n version: '0.17.0',\n date: '2026-08-06',\n highlights: [\n '게시글 조회수가 생겼어요 — 로그인하지 않아도 조회가 쌓여요',\n '목록과 상세에서 조회수를 바로 확인할 수 있어요',\n '댓글 수는 모든 목록 보기에서 제목 옆에 같은 방식으로 보여요',\n ],\n },\n {\n version: '0.16.0',\n date: '2026-08-06',\n highlights: [\n '조밀한 목록 보기가 생겼어요 — 한 줄로 빠르게 훑을 수 있어요',\n '이미지 목록에서는 사진 위에 제목과 좋아요가 겹쳐 보여요',\n '영상 목록에서는 유튜브가 있으면 유튜브 썸네일을 먼저 보여 줘요',\n '시간·설명 툴팁이 마우스만 올려도 바로 떠요',\n ],\n },\n {\n version: '0.15.0',\n date: '2026-08-06',\n highlights: [\n '게시판마다 짧은 설명을 적어 두고, 제목에 마우스를 올리면 볼 수 있어요',\n '일반 목록 카드 여백과 썸네일 배치를 더 단정하게 다듬었어요',\n ],\n },\n {\n version: '0.14.1',\n date: '2026-08-06',\n highlights: [\n '일반 리스트뷰에 정사각형 썸네일이 생기고, 작성자·시간·좋아요·댓글수를 오른쪽에 보기 좋게 정리했어요',\n '카테고리 안에서는 같은 카테고리 뱃지가 매 글마다 반복 표시되지 않아요',\n ],\n },\n {\n version: '0.14.0',\n date: '2026-08-06',\n highlights: [\n '게시판 설정에서 글쓰기·댓글쓰기 권한과 리스트뷰를 카드로 골라요',\n '목록 위에서 일반·이미지·영상 보기를 아이콘으로 바로 바꿔볼 수 있어요',\n ],\n },\n {\n version: '0.13.0',\n date: '2026-08-06',\n highlights: [\n '게시판마다 담당 스태프를 따로 지정할 수 있어요',\n '댓글쓰기 권한도 게시판마다 다르게 정할 수 있어요',\n '유튜브 영상이 모바일과 여러 주소 형식에서도 잘 보여요',\n ],\n },\n {\n version: '0.11.0',\n date: '2026-08-05',\n highlights: [\n '게시글에 좋아요 버튼이 생겼어요',\n '글을 보는 동안 다른 사람의 좋아요·댓글 수가 화면에 바로 나타나요',\n '작성 시간을 몇 분 전처럼 쉽게 보고, 올리면 정확한 시각을 확인할 수 있어요',\n ],\n },\n {\n version: '0.10.0',\n date: '2026-08-05',\n highlights: [\n '게시판 설치와 연결 설정이 한결 단순해졌어요',\n '게시판 아래에서 제작 정보와 지금까지의 버전 이야기를 볼 수 있어요',\n ],\n },\n {\n version: '0.9.0',\n date: '2026-08-04',\n highlights: [\n '댓글에 답글을 달고 대화 흐름을 볼 수 있어요',\n '댓글을 이어서 불러오고, 새 댓글은 실시간으로 받아요',\n ],\n },\n {\n version: '0.8.0',\n date: '2026-08-04',\n highlights: [\n '최근 댓글은 실시간으로 보고 이전 댓글은 이어서 불러올 수 있어요',\n '댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요',\n ],\n },\n {\n version: '0.7.0',\n date: '2026-08-04',\n highlights: [\n '카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요',\n '게시판 설정 화면의 SSR hydration 경고를 해결했어요',\n ],\n },\n {\n version: '0.6.0',\n date: '2026-08-04',\n highlights: [\n '관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요',\n '스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요',\n ],\n },\n {\n version: '0.5.0',\n date: '2026-08-04',\n highlights: [\n '게시판마다 일반·이미지·영상 목록을 고르고, 유튜브를 본문에 넣을 수 있어요',\n '이전 글·목록·다음 글 이동과 카테고리 관리를 지원해요',\n ],\n },\n {\n version: '0.2.0',\n date: '2026-07-31',\n highlights: [\n '글쓰기·댓글·첨부·로그인 권한으로 게시판을 바로 시작할 수 있어요',\n '부적절한 글·댓글은 AI 검열로 막을 수 있어요',\n ],\n note: '실제 보안은 호스트 Firebase 규칙 등록이 필요합니다.',\n },\n]\n","!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define(e):(t=\"undefined\"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){\"use strict\";var t=1e3,e=6e4,n=36e5,r=\"millisecond\",i=\"second\",s=\"minute\",u=\"hour\",a=\"day\",o=\"week\",c=\"month\",f=\"quarter\",h=\"year\",d=\"date\",l=\"Invalid Date\",$=/^(\\d{4})[-/]?(\\d{1,2})?[-/]?(\\d{0,2})[Tt\\s]*(\\d{1,2})?:?(\\d{1,2})?:?(\\d{1,2})?[.:]?(\\d+)?$/,y=/\\[([^\\]]+)]|YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:\"en\",weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),ordinal:function(t){var e=[\"th\",\"st\",\"nd\",\"rd\"],n=t%100;return\"[\"+t+(e[(n-20)%10]||e[n]||e[0])+\"]\"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:\"\"+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?\"+\":\"-\")+m(r,2,\"0\")+\":\"+m(i,2,\"0\")},m:function t(e,n){if(e.date()<n.date())return-t(n,e);var r=12*(n.year()-e.year())+(n.month()-e.month()),i=e.clone().add(r,c),s=n-i<0,u=e.clone().add(r+(s?-1:1),c);return+(-(r+(n-i)/(s?i-u:u-i))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(t){return{M:c,y:h,w:o,d:a,D:d,h:u,m:s,s:i,ms:r,Q:f}[t]||String(t||\"\").toLowerCase().replace(/s$/,\"\")},u:function(t){return void 0===t}},g=\"en\",D={};D[g]=M;var p=\"$isDayjsObject\",S=function(t){return t instanceof _||!(!t||!t[p])},w=function t(e,n,r){var i;if(!e)return g;if(\"string\"==typeof e){var s=e.toLowerCase();D[s]&&(i=s),n&&(D[s]=n,i=s);var u=e.split(\"-\");if(!i&&u.length>1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},O=function(t,e){if(S(t))return t.clone();var n=\"object\"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},b=v;b.l=w,b.i=S,b.w=function(t,e){return O(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=w(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[p]=!0}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.u(e))return new Date;if(e instanceof Date)return new Date(e);if(\"string\"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||\"0\").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return b},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=O(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return O(t)<this.startOf(e)},m.isBefore=function(t,e){return this.endOf(e)<O(t)},m.$g=function(t,e,n){return b.u(t)?this[e]:this.set(n,t)},m.unix=function(){return Math.floor(this.valueOf()/1e3)},m.valueOf=function(){return this.$d.getTime()},m.startOf=function(t,e){var n=this,r=!!b.u(e)||e,f=b.p(t),l=function(t,e){var i=b.w(n.$u?Date.UTC(n.$y,e,t):new Date(n.$y,e,t),n);return r?i:i.endOf(a)},$=function(t,e){return b.w(n.toDate()[t].apply(n.toDate(\"s\"),(r?[0,0,0,0]:[23,59,59,999]).slice(e)),n)},y=this.$W,M=this.$M,m=this.$D,v=\"set\"+(this.$u?\"UTC\":\"\");switch(f){case h:return r?l(1,0):l(31,11);case c:return r?l(1,M):l(0,M+1);case o:var g=this.$locale().weekStart||0,D=(y<g?y+7:y)-g;return l(r?m-D:m+(6-D),M);case a:case d:return $(v+\"Hours\",0);case u:return $(v+\"Minutes\",1);case s:return $(v+\"Seconds\",2);case i:return $(v+\"Milliseconds\",3);default:return this.clone()}},m.endOf=function(t){return this.startOf(t,!1)},m.$set=function(t,e){var n,o=b.p(t),f=\"set\"+(this.$u?\"UTC\":\"\"),l=(n={},n[a]=f+\"Date\",n[d]=f+\"Date\",n[c]=f+\"Month\",n[h]=f+\"FullYear\",n[u]=f+\"Hours\",n[s]=f+\"Minutes\",n[i]=f+\"Seconds\",n[r]=f+\"Milliseconds\",n)[o],$=o===a?this.$D+(e-this.$W):e;if(o===c||o===h){var y=this.clone().set(d,1);y.$d[l]($),y.init(),this.$d=y.set(d,Math.min(this.$D,y.daysInMonth())).$d}else l&&this.$d[l]($);return this.init(),this},m.set=function(t,e){return this.clone().$set(t,e)},m.get=function(t){return this[b.p(t)]()},m.add=function(r,f){var d,l=this;r=Number(r);var $=b.p(f),y=function(t){var e=O(l);return b.w(e.date(e.date()+Math.round(t*r)),l)};if($===c)return this.set(c,this.$M+r);if($===h)return this.set(h,this.$y+r);if($===a)return y(1);if($===o)return y(7);var M=(d={},d[s]=e,d[u]=n,d[i]=t,d)[$]||1,m=this.$d.getTime()+r*M;return b.w(m,this)},m.subtract=function(t,e){return this.add(-1*t,e)},m.format=function(t){var e=this,n=this.$locale();if(!this.isValid())return n.invalidDate||l;var r=t||\"YYYY-MM-DDTHH:mm:ssZ\",i=b.z(this),s=this.$H,u=this.$m,a=this.$M,o=n.weekdays,c=n.months,f=n.meridiem,h=function(t,n,i,s){return t&&(t[n]||t(e,r))||i[n].slice(0,s)},d=function(t){return b.s(s%12||12,t,\"0\")},$=f||function(t,e,n){var r=t<12?\"AM\":\"PM\";return n?r.toLowerCase():r};return r.replace(y,(function(t,r){return r||function(t){switch(t){case\"YY\":return String(e.$y).slice(-2);case\"YYYY\":return b.s(e.$y,4,\"0\");case\"M\":return a+1;case\"MM\":return b.s(a+1,2,\"0\");case\"MMM\":return h(n.monthsShort,a,c,3);case\"MMMM\":return h(c,a);case\"D\":return e.$D;case\"DD\":return b.s(e.$D,2,\"0\");case\"d\":return String(e.$W);case\"dd\":return h(n.weekdaysMin,e.$W,o,2);case\"ddd\":return h(n.weekdaysShort,e.$W,o,3);case\"dddd\":return o[e.$W];case\"H\":return String(s);case\"HH\":return b.s(s,2,\"0\");case\"h\":return d(1);case\"hh\":return d(2);case\"a\":return $(s,u,!0);case\"A\":return $(s,u,!1);case\"m\":return String(u);case\"mm\":return b.s(u,2,\"0\");case\"s\":return String(e.$s);case\"ss\":return b.s(e.$s,2,\"0\");case\"SSS\":return b.s(e.$ms,3,\"0\");case\"Z\":return i}return null}(t)||i.replace(\":\",\"\")}))},m.utcOffset=function(){return 15*-Math.round(this.$d.getTimezoneOffset()/15)},m.diff=function(r,d,l){var $,y=this,M=b.p(d),m=O(r),v=(m.utcOffset()-this.utcOffset())*e,g=this-m,D=function(){return b.m(y,m)};switch(M){case h:$=D()/12;break;case c:$=D();break;case f:$=D()/3;break;case o:$=(g-v)/6048e5;break;case a:$=(g-v)/864e5;break;case u:$=g/n;break;case s:$=g/e;break;case i:$=g/t;break;default:$=g}return l?$:b.a($)},m.daysInMonth=function(){return this.endOf(c).$D},m.$locale=function(){return D[this.$L]},m.locale=function(t,e){if(!t)return this.$L;var n=this.clone(),r=w(t,e,!0);return r&&(n.$L=r),n},m.clone=function(){return b.w(this.$d,this)},m.toDate=function(){return new Date(this.valueOf())},m.toJSON=function(){return this.isValid()?this.toISOString():null},m.toISOString=function(){return this.$d.toISOString()},m.toString=function(){return this.$d.toUTCString()},M}(),Y=_.prototype;return O.prototype=Y,[[\"$ms\",r],[\"$s\",i],[\"$m\",s],[\"$H\",u],[\"$W\",a],[\"$M\",c],[\"$y\",h],[\"$D\",d]].forEach((function(t){Y[t[1]]=function(e){return this.$g(e,t[0],t[1])}})),O.extend=function(t,e){return t.$i||(t(e,_,O),t.$i=!0),O},O.locale=w,O.isDayjs=S,O.unix=function(t){return O(1e3*t)},O.en=D[g],O.Ls=D,O.p={},O}));","!function(r,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define(e):(r=\"undefined\"!=typeof globalThis?globalThis:r||self).dayjs_plugin_relativeTime=e()}(this,(function(){\"use strict\";return function(r,e,t){r=r||{};var n=e.prototype,o={future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"};function i(r,e,t,o){return n.fromToBase(r,e,t,o)}t.en.relativeTime=o,n.fromToBase=function(e,n,i,d,u){for(var f,a,s,l=i.$locale().relativeTime||o,h=r.thresholds||[{l:\"s\",r:44,d:\"second\"},{l:\"m\",r:89},{l:\"mm\",r:44,d:\"minute\"},{l:\"h\",r:89},{l:\"hh\",r:21,d:\"hour\"},{l:\"d\",r:35},{l:\"dd\",r:25,d:\"day\"},{l:\"M\",r:45},{l:\"MM\",r:10,d:\"month\"},{l:\"y\",r:17},{l:\"yy\",d:\"year\"}],m=h.length,c=0;c<m;c+=1){var y=h[c];y.d&&(f=d?t(e).diff(i,y.d,!0):i.diff(e,y.d,!0));var p=(r.rounding||Math.round)(Math.abs(f));if(s=f>0,p<=y.r||!y.r){p<=1&&c>0&&(y=h[c-1]);var v=l[y.l];u&&(p=u(\"\"+p)),a=\"string\"==typeof v?v.replace(\"%d\",p):v(p,n,y.l,s);break}}if(n)return a;var M=s?l.future:l.past;return\"function\"==typeof M?M(a):M.replace(\"%s\",a)},n.to=function(r,e){return i(r,e,this,!0)},n.from=function(r,e){return i(r,e,this)};var d=function(r){return r.$u?t.utc():t()};n.toNow=function(r){return this.to(d(this),r)},n.fromNow=function(r){return this.from(d(this),r)}}}));","!function(e,_){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=_(require(\"dayjs\")):\"function\"==typeof define&&define.amd?define([\"dayjs\"],_):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).dayjs_locale_ko=_(e.dayjs)}(this,(function(e){\"use strict\";function _(e){return e&&\"object\"==typeof e&&\"default\"in e?e:{default:e}}var d=_(e),t={name:\"ko\",weekdays:\"일요일_월요일_화요일_수요일_목요일_금요일_토요일\".split(\"_\"),weekdaysShort:\"일_월_화_수_목_금_토\".split(\"_\"),weekdaysMin:\"일_월_화_수_목_금_토\".split(\"_\"),months:\"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월\".split(\"_\"),monthsShort:\"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월\".split(\"_\"),ordinal:function(e){return e+\"일\"},formats:{LT:\"A h:mm\",LTS:\"A h:mm:ss\",L:\"YYYY.MM.DD.\",LL:\"YYYY년 MMMM D일\",LLL:\"YYYY년 MMMM D일 A h:mm\",LLLL:\"YYYY년 MMMM D일 dddd A h:mm\",l:\"YYYY.MM.DD.\",ll:\"YYYY년 MMMM D일\",lll:\"YYYY년 MMMM D일 A h:mm\",llll:\"YYYY년 MMMM D일 dddd A h:mm\"},meridiem:function(e){return e<12?\"오전\":\"오후\"},relativeTime:{future:\"%s 후\",past:\"%s 전\",s:\"몇 초\",m:\"1분\",mm:\"%d분\",h:\"한 시간\",hh:\"%d시간\",d:\"하루\",dd:\"%d일\",M:\"한 달\",MM:\"%d달\",y:\"일 년\",yy:\"%d년\"}};return d.default.locale(t,null,!0),t}));","import type { Timestamp } from 'firebase/firestore'\nimport dayjs from 'dayjs'\nimport relativeTime from 'dayjs/plugin/relativeTime'\nimport 'dayjs/locale/ko'\n\ndayjs.extend(relativeTime)\ndayjs.locale('ko')\n\nexport function formatDate(ts: Timestamp | undefined): string {\n return ts?.toDate ? ts.toDate().toLocaleDateString('ko-KR') : ''\n}\n\nexport function formatRelativeDate(ts: Timestamp | undefined, now = Date.now()): string {\n return ts?.toDate ? dayjs(ts.toDate()).from(dayjs(now)) : '방금 전'\n}\n\nexport function formatFullDate(ts: Timestamp | undefined): string {\n return ts?.toDate ? dayjs(ts.toDate()).format('YYYY년 M월 D일 HH:mm:ss') : '시간 확인 중'\n}\n\n/** 생성·수정 시각이 같거나 수정 시각이 없으면 한 줄, 실제 수정됐으면 두 줄. */\nexport function formatTimestampDetails(createdAt: Timestamp | undefined, updatedAt?: Timestamp): string[] {\n const created = formatFullDate(createdAt)\n const createdMs = createdAt?.toMillis?.()\n const updatedMs = updatedAt?.toMillis?.()\n if (updatedMs == null || createdMs == null || createdMs === updatedMs) return [created]\n return [`생성시간: ${created}`, `수정시간: ${formatFullDate(updatedAt)}`]\n}\n","/**\n * 게시글 상세용 가벼운 마크다운 → HTML (TipTap 없이).\n * 목록 이동 시 UEditor destroy/recreate 로 나는\n * \"Adding different instances of a keyed plugin\" 를 피한다.\n * 신뢰된 작성자 콘텐츠 전제 — 기본 이스케이프 후 허용 패턴만 복원.\n */\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\n/** 인라인 마크다운 (이미 escape 된 문자열에 적용) */\nfunction formatInline(s: string): string {\n let out = s\n // code\n out = out.replace(/`([^`]+)`/g, '<code>$1</code>')\n // images ![alt](url) — url 은 http(s) 만\n out = out.replace(\n /!\\[([^\\]]*)\\]\\((https?:\\/\\/[^)\\s]+)\\)/g,\n '<img src=\"$2\" alt=\"$1\" class=\"max-w-full h-auto rounded-lg my-3\" loading=\"lazy\" />',\n )\n // links [text](url)\n out = out.replace(\n /\\[([^\\]]+)\\]\\((https?:\\/\\/[^)\\s]+)\\)/g,\n '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary underline\">$1</a>',\n )\n // bold ** ** 먼저\n out = out.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n // italic * * (bold 처리 후 남은 단일 *)\n out = out.replace(/\\*([^*]+)\\*/g, '<em>$1</em>')\n // strikethrough ~~\n out = out.replace(/~~([^~]+)~~/g, '<del>$1</del>')\n return out\n}\n\n/**\n * GFM-ish 최소 변환. 코드 펜스·목록·제목·인용·hr·단락.\n */\nexport function renderMarkdownToHtml(markdown: string): string {\n if (!markdown?.trim()) return ''\n\n const lines = markdown.replace(/\\r\\n/g, '\\n').split('\\n')\n const blocks: string[] = []\n let i = 0\n let inCode = false\n let codeLang = ''\n let codeBuf: string[] = []\n let listType: 'ul' | 'ol' | null = null\n let listItems: string[] = []\n\n function flushList() {\n if (!listType || !listItems.length) {\n listType = null\n listItems = []\n return\n }\n const tag = listType\n blocks.push(`<${tag}>${listItems.map(li => `<li>${formatInline(li)}</li>`).join('')}</${tag}>`)\n listType = null\n listItems = []\n }\n\n while (i < lines.length) {\n const line = lines[i] ?? ''\n\n // fenced code\n const fence = line.match(/^```(\\w*)\\s*$/)\n if (fence) {\n if (inCode) {\n blocks.push(\n `<pre class=\"overflow-x-auto rounded-lg bg-muted p-3 text-sm\"><code>${codeBuf.join('\\n')}</code></pre>`,\n )\n codeBuf = []\n inCode = false\n codeLang = ''\n }\n else {\n flushList()\n inCode = true\n codeLang = fence[1] || ''\n void codeLang\n }\n i++\n continue\n }\n if (inCode) {\n codeBuf.push(escapeHtml(line))\n i++\n continue\n }\n\n // hr\n if (/^(-{3,}|\\*{3,}|_{3,})\\s*$/.test(line)) {\n flushList()\n blocks.push('<hr class=\"my-4 border-default\" />')\n i++\n continue\n }\n\n // heading\n const h = line.match(/^(#{1,3})\\s+(.+)$/)\n if (h) {\n flushList()\n const level = h[1]!.length\n blocks.push(`<h${level} class=\"font-semibold mt-4 mb-2\">${formatInline(escapeHtml(h[2]!))}</h${level}>`)\n i++\n continue\n }\n\n // blockquote\n if (line.startsWith('> ')) {\n flushList()\n const quoteLines: string[] = []\n while (i < lines.length && (lines[i] ?? '').startsWith('> ')) {\n quoteLines.push(escapeHtml((lines[i] ?? '').slice(2)))\n i++\n }\n blocks.push(\n `<blockquote class=\"border-l-4 border-default pl-3 text-muted my-3\">${formatInline(quoteLines.join('<br />'))}</blockquote>`,\n )\n continue\n }\n\n // unordered list\n const ul = line.match(/^[-*+]\\s+(.+)$/)\n if (ul) {\n if (listType && listType !== 'ul') flushList()\n listType = 'ul'\n listItems.push(escapeHtml(ul[1]!))\n i++\n continue\n }\n\n // ordered list\n const ol = line.match(/^\\d+\\.\\s+(.+)$/)\n if (ol) {\n if (listType && listType !== 'ol') flushList()\n listType = 'ol'\n listItems.push(escapeHtml(ol[1]!))\n i++\n continue\n }\n\n // empty line\n if (!line.trim()) {\n flushList()\n i++\n continue\n }\n\n // paragraph (merge consecutive non-empty)\n flushList()\n const para: string[] = []\n while (i < lines.length) {\n const l = lines[i] ?? ''\n if (!l.trim()) break\n if (/^(#{1,3}\\s|```|[-*+]\\s|\\d+\\.\\s|> |(-{3,}|\\*{3,})\\s*$)/.test(l)) break\n para.push(escapeHtml(l))\n i++\n }\n blocks.push(`<p class=\"my-2 leading-relaxed\">${formatInline(para.join('<br />'))}</p>`)\n }\n\n flushList()\n if (inCode && codeBuf.length) {\n blocks.push(\n `<pre class=\"overflow-x-auto rounded-lg bg-muted p-3 text-sm\"><code>${codeBuf.join('\\n')}</code></pre>`,\n )\n }\n\n return blocks.join('\\n')\n}\n"],"x_google_ignoreList":[21,22,23],"mappings":"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ACA,gDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,yCACA,wCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,0CACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,gDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,kDACA,iDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,4CACA,mCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,4DACA,yCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,uCACA,yCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,4BACA,uCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,6BACA,+BACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wCACA,6BACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,gDACA,oCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,gDACA,iDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,8CACA,gCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wCACA,4BACF;EACA,MAAM;CACR;AACF;CC3OA,CAAC,SAAS,GAAE,GAAE;EAAC,AAAU,OAAO,KAAjB,YAA8C,MAApB,SAA2B,EAAO,UAAQ,EAAE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"}