memi-board 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/utils/boardSeo.ts","../src/composables/fetchPublicSeo.ts","../src/utils/boardAccess.ts","../src/composables/boardLookup.ts","../src/utils/moderation-strike.ts","../src/composables/useMemiBoardAuth.ts","../src/utils/imeSafeSubmit.ts","../src/utils/extractEditorImageUrls.ts","../src/utils/storagePath.ts","../src/utils/deletePostCascade.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/useMemiBoardCommentFeed.ts","../src/composables/useMemiBoardAuthorMemo.ts","../src/composables/useMemiBoardComments.ts","../src/composables/useMemiBoardLikes.ts","../src/composables/useMemiBoardReports.ts","../src/composables/useMemiBoardViews.ts","../src/utils/moderation-prompt.ts","../src/composables/useMemiBoardModeration.ts","../src/composables/useMemiBoardUsers.ts","../src/composables/useMemiBoardUserProfile.ts","../src/utils/section.ts","../src/composables/useMemiBoardSections.ts","../src/composables/useMemiBoardMiniPosts.ts","../src/data/versionHistory.ts","../src/composables/useMemiBoardWritingAssistant.ts","../src/composables/useMemiBoardAutoTranslate.ts","../src/utils/renderMarkdown.ts"],"sourcesContent":["/** 게시판 SEO 순수 헬퍼 — Nuxt 의존 없음 */\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\n\n/** '전체' 라벨(현재 언어 또는 예전 한국어 값)이면 전체 목록으로 본다. */\nfunction isAllCategoryLabel(label: string | undefined, locale?: MemiBoardLocale): boolean {\n if (!label) return true\n return label === '전체' || label === translate(locale, 'seo.allCategory')\n}\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 locale?: MemiBoardLocale,\n): string {\n const fromSummary = summary?.trim().slice(0, 40)\n const t = title.trim() || fromSummary || translate(locale, 'seo.photo')\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}, locale?: MemiBoardLocale): string {\n const bits: string[] = []\n if (opts.summary?.trim()) bits.push(opts.summary.trim().slice(0, 140))\n else bits.push(translate(locale, 'seo.postDescriptionFallback'))\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(\n categoryLabel: string | null | undefined,\n siteName: string,\n locale?: MemiBoardLocale,\n): string {\n const site = siteName.trim() || 'Board'\n const cat = categoryLabel?.trim()\n if (!isAllCategoryLabel(cat, locale)) return translate(locale, 'seo.listTitle', { name: cat, site })\n return translate(locale, 'seo.listTitleAll', { site })\n}\n\nexport function boardListOgDescription(opts: {\n categoryLabel?: string | null\n description?: string\n recentTitles?: string[]\n}, locale?: MemiBoardLocale): string {\n if (opts.description?.trim()) return opts.description.trim().slice(0, 200)\n const cat = opts.categoryLabel?.trim()\n const head = !isAllCategoryLabel(cat, locale)\n ? translate(locale, 'seo.listDescription', { name: cat })\n : translate(locale, 'seo.listDescriptionAll')\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'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { loadMemiBoardLocale, translate } from '../i18n/translate'\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 /** 기본 문구(설명·'전체' 라벨) 언어. 생략 시 한국어. */\n locale?: MemiBoardLocale,\n): Promise<BoardListSeoPayload> {\n if (locale) await loadMemiBoardLocale(locale)\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 || translate(locale, 'seo.boardDescription', { name: meta.label }),\n ogImage,\n recentTitles,\n }\n }\n\n return {\n kind: 'all',\n category: null,\n categoryLabel: translate(locale, 'seo.allCategory'),\n description: translate(locale, 'seo.allDescription'),\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","import type { BoardVisibility, BoardWriteRole } from '../types'\n\n/** 스태프가 이 보드 담당인지. 목록에 있는 uid만. 비어 있으면 담당 없음(관리자만). */\nexport function isAssignedStaff(\n uid: string | undefined,\n board: { allowedStaffUids?: string[] } | null | undefined,\n): boolean {\n if (!uid || !board) return false\n return (board.allowedStaffUids ?? []).includes(uid)\n}\n\nexport function canManageBoardByRole(\n uid: string | undefined,\n isAdmin: boolean,\n isStaff: boolean,\n board: { allowedStaffUids?: string[] } | null | undefined,\n): boolean {\n if (!uid) return false\n if (isAdmin) return true\n if (!isStaff) return false\n return isAssignedStaff(uid, board)\n}\n\n/**\n * 댓글 작성 권한. 숨김 보드는 commentWriteRole 과 무관하게 관리자·담당 스태프만.\n * board 가 아직 없으면 공개 보드(일반 이상)와 같이 취급한다.\n */\nexport function canWriteCommentByRole(\n uid: string | undefined,\n isAdmin: boolean,\n isStaff: boolean,\n board: {\n visibility?: BoardVisibility\n commentWriteRole?: BoardWriteRole\n allowedStaffUids?: string[]\n } | null | undefined,\n): boolean {\n if (!uid) return false\n if (board?.visibility === 'hidden') {\n return canManageBoardByRole(uid, isAdmin, isStaff, board)\n }\n const required = board?.commentWriteRole ?? 'user'\n if (required === 'admin') return isAdmin\n if (required === 'staff') return canManageBoardByRole(uid, isAdmin, isStaff, board)\n return true\n}","type BoardLookup = (boardId: string) => { allowedStaffUids?: string[] } | undefined\n\nlet lookup: BoardLookup | undefined\n\nexport function registerBoardLookup(fn: BoardLookup) {\n lookup = fn\n}\n\nexport function resolveBoardLookup(boardId: string) {\n return lookup?.(boardId)\n}","import type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\n\n/**\n * 게시판 검열 차단 누적 → 일시 이용 제한 (호스트 contentWarning 과 동일 모델).\n * - 콘텐츠 차단(로컬/AI flagged) 시 +1\n * - 유효 경고 ≥ threshold(기본 10) → 글/댓글 작성 불가\n * - 24h마다 lazy 차감 (moderationBlockAt 기준)\n */\n\nexport const DEFAULT_BLOCK_BAN_THRESHOLD = 10\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(\n untilMs: number,\n nowMs: number = Date.now(),\n locale?: MemiBoardLocale,\n): string {\n const left = Math.max(0, untilMs - nowMs)\n const hours = Math.ceil(left / (60 * 60 * 1000))\n if (hours <= 0) return translate(locale, 'moderationStrike.liftSoon')\n if (hours < 24) return translate(locale, 'moderationStrike.liftInHours', { hours })\n const days = Math.floor(hours / 24)\n const remH = hours % 24\n if (remH === 0) return translate(locale, 'moderationStrike.liftInDays', { days })\n return translate(locale, 'moderationStrike.liftInDaysHours', { days, hours: 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 { canManageBoardByRole } from '../utils/boardAccess'\nimport { resolveBoardLookup } from './boardLookup'\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'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\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 canManageBoard: (boardId: string | undefined | null) => 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' | 'boardId'>) => boolean\n canDelete: (post: Pick<PostModel, 'authorUid' | 'boardId'>) => boolean\n canEditComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean\n canDeleteComment: (comment: Pick<CommentModel, 'authorUid' | 'boardId'>) => 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 { t, locale } = useMemiBoardI18n()\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 const hasName = typeof data.displayName === 'string' && data.displayName.trim()\n const hasPhoto = typeof data.photoURL === 'string' && data.photoURL.trim()\n const visit: Record<string, unknown> = {\n lastVisitAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n }\n // 이미 있는 이름·사진은 프로필이 주인. Auth(구글/애플) 값으로 되돌리지 않는다.\n if (!hasName && firebaseUser.displayName) visit.displayName = firebaseUser.displayName\n if (!hasPhoto && firebaseUser.photoURL) visit.photoURL = firebaseUser.photoURL\n await Promise.all([\n updateDoc(ref, visit),\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, Date.now(), locale.value) : null\n })\n\n const restrictedMessage = computed(() => {\n if (!isWriteRestricted.value) return null\n const until = restrictedUntilLabel.value\n return until\n ? t('auth.writeRestrictedUntil', { until })\n : t('auth.writeRestricted')\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 = t('auth.strikeSuffix', { count: 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, locale.value) : null\n messageSuffix = untilLabel\n ? t('auth.strikeRestrictedUntil', { count: effectiveCount, until: untilLabel })\n : t('auth.strikeRestricted', { count: effectiveCount })\n }\n return { effectiveCount, restricted, messageSuffix }\n }\n\n function requireAuth() {\n if (!auth) throw new Error(t('auth.notInitialized'))\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 canManageBoard(boardId: string | undefined | null) {\n return canManageBoardByRole(\n user.value?.uid,\n isAdmin.value,\n isStaff.value,\n boardId ? resolveBoardLookup(boardId) : undefined,\n )\n }\n\n function canEdit(post: Pick<PostModel, 'authorUid' | 'boardId'>) {\n return isSignedIn.value && (user.value?.uid === post.authorUid || canManageBoard(post.boardId))\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' | 'boardId'>) {\n return isSignedIn.value && (user.value?.uid === comment.authorUid || canManageBoard(comment.boardId))\n }\n\n return {\n user,\n isSignedIn,\n isAdmin,\n isStaff,\n canManageContent,\n canManageBoard,\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 * macOS Safari: 한글 조합 중(마지막 글자 미확정)에 제출 버튼을 누르면 첫 click 이 조합 확정에 먹혀\n * 두 번 눌러야 넘어간다. 마우스는 pointerdown 에서 조합을 확정(blur)하고 바로 제출, 뒤따르는 click 은 무시.\n * 터치는 스크롤과 구분해야 하므로 기존 click 그대로.\n *\n * 사용: `<UButton type=\"submit\" @pointerdown=\"imeSafeSubmitPointerDown\" @click=\"imeSafeSubmitClick\" />`\n * 버튼이 속한 form 을 requestSubmit 하므로 form 의 `@submit.prevent` 핸들러가 그대로 불린다.\n */\nconst suppressClickUntil = new WeakMap<HTMLButtonElement, number>()\n\nfunction submitButtonOf(e: Event): HTMLButtonElement | null {\n const target = e.currentTarget\n if (target instanceof HTMLButtonElement) return target\n return target instanceof Element ? target.closest('button') : null\n}\n\nexport function imeSafeSubmitPointerDown(e: PointerEvent): void {\n if (e.pointerType !== 'mouse' || e.button !== 0) return\n const button = submitButtonOf(e)\n const form = button?.form\n if (!button || !form || button.disabled) return\n suppressClickUntil.set(button, Date.now() + 1000)\n const active = document.activeElement\n if (active instanceof HTMLElement) active.blur()\n // blur 로 확정된 마지막 글자가 v-model 에 반영된 뒤 제출\n setTimeout(() => form.requestSubmit(button), 0)\n}\n\nexport function imeSafeSubmitClick(e: MouseEvent): void {\n const button = submitButtonOf(e)\n if (!button) return\n const until = suppressClickUntil.get(button)\n if (until && Date.now() < until) {\n e.preventDefault()\n suppressClickUntil.delete(button)\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 parsed = new URL(url)\n const host = parsed.hostname.toLowerCase()\n const isFirebaseStorage = host === 'firebasestorage.googleapis.com'\n || host.endsWith('.firebasestorage.app')\n if (!isFirebaseStorage) return null\n const m = parsed.pathname.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","import {\n deleteDoc,\n getDoc,\n getDocs,\n query,\n where,\n writeBatch,\n type Firestore,\n type Query,\n} from 'firebase/firestore'\nimport {\n deleteObject,\n listAll,\n ref as storageRef,\n type FirebaseStorage,\n type StorageReference,\n} from 'firebase/storage'\nimport type { BoardPathConfig } from './boardPaths'\nimport {\n commentsCol,\n likesCol,\n postBodyDoc,\n postDoc,\n postStorageFolder,\n reportsCol,\n} from './boardPaths'\nimport { extractEditorImageUrls } from './extractEditorImageUrls'\nimport { postNamespaceFromStoragePath, storagePathFromDownloadUrl } from './storagePath'\nimport type { PostModel } from '../types'\n\nasync function deleteStorageFolder(folderRef: StorageReference): Promise<void> {\n try {\n const list = await listAll(folderRef)\n await Promise.all([\n ...list.items.map(item => deleteObject(item).catch(() => {})),\n ...list.prefixes.map(prefix => deleteStorageFolder(prefix)),\n ])\n }\n catch {\n // 이 경로에 list 규칙이 없으면 건너뛴다. 호출 쪽에서 하위 폴더를 따로 비운다.\n }\n}\n\n/** 규칙이 매칭하는 images/·attachments/ 만 비운다. 부모 폴더 listAll 은 호스트 규칙에서 403 이 난다. */\nasync function deletePostStorageTree(storage: FirebaseStorage, folder: string): Promise<void> {\n await deleteStorageFolder(storageRef(storage, `${folder}/images`))\n await deleteStorageFolder(storageRef(storage, `${folder}/attachments`))\n}\n\nasync function deleteStoragePaths(storage: FirebaseStorage, paths: Iterable<string>): Promise<void> {\n const unique = [...new Set([...paths].filter(Boolean))]\n await Promise.all(unique.map(path => deleteObject(storageRef(storage, path)).catch(() => {})))\n}\n\nconst DELETE_BATCH_SIZE = 450\n\nasync function deleteQueryDocs(db: Firestore, docsQuery: Query): Promise<void> {\n const snapshot = await getDocs(docsQuery)\n for (let offset = 0; offset < snapshot.docs.length; offset += DELETE_BATCH_SIZE) {\n const batch = writeBatch(db)\n snapshot.docs.slice(offset, offset + DELETE_BATCH_SIZE).forEach(item => batch.delete(item.ref))\n await batch.commit()\n }\n}\n\n/** 글에 달린 본문·댓글 좋아요를 모두 지운다. 둘 다 postId 필드를 가진다. */\nexport async function deleteLikesForPost(\n db: Firestore,\n cfg: BoardPathConfig,\n postId: string,\n): Promise<void> {\n await deleteQueryDocs(db, query(likesCol(db, cfg), where('postId', '==', postId)))\n}\n\n/** 글에 달린 신고를 모두 지운다. */\nexport async function deleteReportsForPost(\n db: Firestore,\n cfg: BoardPathConfig,\n postId: string,\n): Promise<void> {\n await deleteQueryDocs(db, query(reportsCol(db, cfg), where('postId', '==', postId)))\n}\n\n/** 댓글 하나에 달린 좋아요만 지운다. */\nexport async function deleteLikesForComment(\n db: Firestore,\n cfg: BoardPathConfig,\n commentId: string,\n): Promise<void> {\n await deleteQueryDocs(db, query(likesCol(db, cfg), where('commentId', '==', commentId)))\n}\n\n/** 글 하나의 댓글(답글 포함), 좋아요, 본문, 첨부 Storage, 메타를 순서대로 삭제한다. */\nexport async function deletePostCascade(\n db: Firestore,\n storage: FirebaseStorage,\n cfg: BoardPathConfig,\n id: string,\n): Promise<void> {\n const metaRef = postDoc(db, cfg, id)\n const bodyRef = postBodyDoc(db, cfg, id)\n const [metaSnap, bodySnap] = await Promise.all([getDoc(metaRef), getDoc(bodyRef)])\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 // 답글도 같은 flat 컬렉션에서 같은 postId를 가지므로 함께 삭제된다.\n await deleteQueryDocs(db, query(commentsCol(db, cfg), where('postId', '==', id)))\n await deleteLikesForPost(db, cfg, id)\n await deleteReportsForPost(db, cfg, id)\n\n const extraPaths = new Set<string>()\n const extraNamespaces = new Set<string>()\n for (const attachment of meta.attachments ?? []) {\n if (!attachment?.path) continue\n extraPaths.add(attachment.path)\n const namespace = postNamespaceFromStoragePath(attachment.path)\n if (namespace && namespace !== id) extraNamespaces.add(namespace)\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/')) extraPaths.add(thumbGuess)\n const match = path.match(/^(.*\\/images\\/)([^/]+)\\.[^.]+$/)\n if (match && !path.includes('/thumbnails/')) extraPaths.add(`${match[1]}thumbnails/${match[2]}.jpg`)\n const namespace = postNamespaceFromStoragePath(path)\n if (namespace && namespace !== id) extraNamespaces.add(namespace)\n }\n\n await deleteDoc(bodyRef)\n await deletePostStorageTree(storage, postStorageFolder(cfg, id))\n for (const namespace of extraNamespaces) {\n await deletePostStorageTree(storage, postStorageFolder(cfg, namespace))\n }\n await deleteStoragePaths(storage, extraPaths)\n await deleteDoc(metaRef)\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, useFirebaseApp, useFirestore } from 'vuefire'\nimport { getStorage } from 'firebase/storage'\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 commentsCol,\n postsCol,\n settingsCol,\n settingsDoc,\n boardSsrKey,\n} from '../utils/boardPaths'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport { registerBoardLookup } from './boardLookup'\nimport { canManageBoardByRole } from '../utils/boardAccess'\nimport { slugify } from '../utils/slugify'\nimport { deletePostCascade } from '../utils/deletePostCascade'\nimport { normalizeMemiBoardLocale } from '../i18n/locales'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\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 editorType: (data.editorType as BoardModel['editorType'])\n ?? (data.listView === 'image' ? 'image' : '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 locale: normalizeMemiBoardLocale(data.locale) ?? '',\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 app = useFirebaseApp()\n const { isSignedIn, isAdmin, isStaff, user } = useMemiBoardAuth()\n const { t } = useMemiBoardI18n()\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 registerBoardLookup(id => getBoard(id))\n\n function canManageBoard(boardId: string | undefined | null) {\n return canManageBoardByRole(user.value?.uid, isAdmin.value, isStaff.value, boardId ? getBoard(boardId) : undefined)\n }\n\n const managedBoards = computed(() => boards.value.filter(board => canManageBoard(board.id)))\n\n /** 숨김 전환 시 해당 보드 글·댓글 listed 일괄 맞춤. 값이 이미 같으면 건너뛴다. */\n async function syncListedForCol(\n col: ReturnType<typeof postsCol>,\n boardId: string,\n listed: boolean,\n ): Promise<void> {\n let cursor: QueryDocumentSnapshot | undefined\n for (;;) {\n const page = await getDocs(query(\n col,\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 let writes = 0\n for (const item of page.docs) {\n if (item.data().listed === listed) continue\n batch.update(item.ref, { listed })\n writes++\n }\n if (writes) await batch.commit()\n cursor = page.docs[page.docs.length - 1]\n if (page.docs.length < 400) break\n }\n }\n\n async function syncListedForBoard(boardId: string, listed: boolean): Promise<void> {\n await syncListedForCol(postsCol(db, cfg()), boardId, listed)\n await syncListedForCol(commentsCol(db, cfg()), boardId, listed)\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 editorType: board.editorType ?? (board.listView === 'image' ? 'image' : 'default'),\n writeRole: board.writeRole ?? 'user',\n commentWriteRole: board.commentWriteRole ?? 'user',\n allowedStaffUids: board.allowedStaffUids ?? [],\n locale: normalizeMemiBoardLocale(board.locale) ?? '',\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(t('boardSettings.signInRequired'))\n const id = board.id.trim()\n const label = board.label.trim()\n if (!id || !label) throw new Error(t('boardSettings.idAndNameRequired'))\n const visibility = normalizeVisibility(board.visibility)\n const payload = settingsPayload({ ...board, label, visibility }, order)\n await setDoc(settingsDoc(db, cfg(), id), payload, { merge: true })\n await syncListedForBoard(id, visibility !== 'hidden')\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(t('boardSettings.signInRequired'))\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(t('boardSettings.idAndNameRequired'))\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 || visibility === 'hidden') {\n visibilityChanges.push({ id, listed: visibility !== 'hidden' })\n }\n })\n await batch.commit()\n for (const change of visibilityChanges) {\n await syncListedForBoard(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(t('boardSettings.signInRequired'))\n const boardId = id.trim()\n if (!boardId) throw new Error(t('boardSettings.deleteIdRequired'))\n\n // 설정은 마지막에 삭제한다. 중간 실패 시 게시판을 남겨 두어 관리자가 재시도할 수 있다.\n for (;;) {\n const page = await getDocs(query(\n postsCol(db, cfg()),\n where('boardId', '==', boardId),\n fbLimit(25),\n ))\n if (page.empty) break\n for (const post of page.docs) {\n await deletePostCascade(db, getStorage(app), cfg(), post.id)\n }\n }\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(t('boardSettings.nameRequired'))\n if (!isSignedIn.value) throw new Error(t('boardSettings.signInRequired'))\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 editorType: '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 canManageBoard,\n managedBoards,\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 writeBatch,\n query,\n where,\n orderBy,\n limit as fbLimit,\n limitToLast,\n startAfter,\n endBefore,\n serverTimestamp,\n deleteField,\n} from 'firebase/firestore'\nimport type { QueryConstraint, Unsubscribe } from 'firebase/firestore'\nimport type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore'\nimport { getStorage } from 'firebase/storage'\nimport { useDocument, useFirebaseApp, useFirestore } from 'vuefire'\nimport { slugify } from '../utils/slugify'\nimport { deletePostCascade } from '../utils/deletePostCascade'\nimport { hasBodyImage, hasBodyText, titleFromBody } from '../utils/postBody'\nimport { useBoardPathConfig } from '../config'\nimport { buildPostPreview } from '../utils/postPreview'\nimport {\n postBodyDoc,\n postDoc,\n postsCol,\n} from '../utils/boardPaths'\nimport { useMemiBoardSettings } from './useMemiBoardSettings'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\nimport type { Attachment, PostDetail, PostModel } from '../types'\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 /** resolveUniqueSlug 로 미리 구한 slug. 없으면 createPost 가 직접 구한다. */\n slug?: 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 type ResolvePostIdResult =\n | { status: 'ok', id: string }\n | { status: 'not-found' }\n | { status: 'permission-denied' }\n | { status: 'error', message: string }\n\n/**\n * 방금 만든 글의 `boardId/slug` → id.\n * 저장 직후 상세(미리보기)로 갈 때 slug 재조회 왕복을 건너뛴다. slug 는 바뀌지 않는다.\n */\nconst createdPostIds = new Map<string, 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>, locale?: MemiBoardLocale): string {\n const id = toValue(boardId)?.trim()\n if (!id) throw new Error(translate(locale, 'posts.boardIdRequired'))\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 { t, locale } = useMemiBoardI18n()\n const cfg = () => useBoardPathConfig()\n const bid = () => resolveBoardId(boardId, locale.value)\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\n function listedForBoard(): boolean {\n return !isBoardHidden(bid())\n }\n\n function isImageEditorBoard(): boolean {\n return getBoard(bid())?.editorType === 'image'\n }\n\n function assertPostContent(\n input: { title?: string, content: string, attachments?: Attachment[] },\n ) {\n const imageBoard = isImageEditorBoard()\n const hasText = hasBodyText(input.content)\n const hasImage = hasBodyImage(input.content, input.attachments)\n if (imageBoard) {\n if (!hasImage) {\n throw new Error(t('posts.photoRequired'))\n }\n return\n }\n if (!hasText && !hasImage) {\n throw new Error(t('posts.bodyRequired'))\n }\n if (!input.title?.trim()) {\n throw new Error(t('posts.titleRequired'))\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 /**\n * slug → 글 id 만. 상세·수정 페이지는 id 만 필요하므로 본문까지 읽는 getPostBySlug 보다 왕복이 적다.\n * 이 탭에서 방금 만든 글이면 조회 없이 바로 돌려준다.\n */\n async function resolvePostIdBySlug(slug: string): Promise<ResolvePostIdResult> {\n const s = slug.trim()\n const cached = createdPostIds.get(`${bid()}/${s}`)\n if (cached) return { status: 'ok', id: cached }\n try {\n const snapshot = await getDocs(query(\n postsColRef(),\n where('boardId', '==', bid()),\n where('slug', '==', s),\n fbLimit(1),\n ))\n const row = snapshot.docs[0]\n return row ? { status: 'ok', id: row.id } : { status: 'not-found' }\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 /** 보드 안에서 겹치지 않는 slug. 검열과 동시에 돌릴 수 있게 createPost 밖으로 노출한다. */\n async function resolveUniqueSlug(title: string): Promise<string> {\n const baseSlug = slugify(title.trim()) || 'post'\n let 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 return slug\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 = isImageEditorBoard()\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 slug = input.slug?.trim() || await resolveUniqueSlug(title)\n }\n\n const preview = buildPostPreview(input.content, input.attachments)\n const listed = listedForBoard()\n // 메타·본문을 한 번에 커밋 — 순차 setDoc 두 번보다 왕복 1회 적다.\n const batch = writeBatch(db)\n batch.set(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 batch.set(bodyDocRef(id), { content: input.content })\n await batch.commit()\n createdPostIds.set(`${bid()}/${slug}`, id)\n return slug\n }\n\n async function updatePost(id: string, input: UpdatePostInput): Promise<void> {\n assertPostContent(input)\n const imageBoard = isImageEditorBoard()\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 await deletePostCascade(db, getStorage(app), cfg(), id)\n for (const [key, value] of createdPostIds) {\n if (value === id) createdPostIds.delete(key)\n }\n }\n\n return {\n createPostId,\n getPosts,\n getPost,\n getPostBySlug,\n resolvePostIdBySlug,\n resolveUniqueSlug,\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, user, canManageBoard } = 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 || canManageBoard(boardIdValue.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로 모든 보드를 가로질러 공개(listed) 댓글만 모은다.\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 [\n where('authorUid', '==', uidValue.value),\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 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, 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 COMMENT_FEED_PAGE_SIZE = 10\n\nexport type CommentFeedSort = 'latest' | 'likes'\n\n/**\n * 보드를 가로질러 공개(listed) 댓글만 모은다. 숨김 보드 댓글은 제외.\n * 최신순은 createdAt, 좋아요순은 likeCount.\n * likeCount 가 없는 예전 댓글은 좋아요순 쿼리에 나오지 않는다(표시 시 0).\n */\nexport function useMemiBoardCommentFeed(\n sort: MaybeRefOrGetter<CommentFeedSort> = 'latest',\n options: { pageSize?: MaybeRefOrGetter<number> } = {},\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const commentsColRef = () => commentsCol(db, cfg())\n const pageSize = computed(() => {\n const n = Math.floor(Number(toValue(options.pageSize)))\n return Number.isFinite(n) && n > 0 ? n : COMMENT_FEED_PAGE_SIZE\n })\n const sortValue = computed<CommentFeedSort>(() => {\n const value = toValue(sort)\n return value === 'likes' ? 'likes' : 'latest'\n })\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 function compare(a: CommentModel, b: CommentModel): number {\n if (sortValue.value === 'likes') {\n const likes = (b.likeCount ?? 0) - (a.likeCount ?? 0)\n if (likes) return likes\n }\n return createdAtMs(b) - createdAtMs(a) || String(b.id).localeCompare(String(a.id))\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(compare)\n })\n\n function orderConstraints(): QueryConstraint[] {\n if (sortValue.value === 'likes') {\n return [\n where('listed', '==', true),\n orderBy('likeCount', 'desc'),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ]\n }\n return [\n where('listed', '==', true),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ]\n }\n\n function startHeadSubscription() {\n stopHeadSubscription?.()\n stopHeadSubscription = onSnapshot(query(\n commentsColRef(),\n ...orderConstraints(),\n fbLimit(pageSize.value),\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.value\n headPending.value = false\n }, (cause) => {\n console.error('[memi-board:commentFeed] 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) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const cursor = olderCursor ?? headTailCursor\n const snapshot = await getDocs(query(\n commentsColRef(),\n ...orderConstraints(),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(pageSize.value + 1),\n ))\n const pageDocs = snapshot.docs.slice(0, pageSize.value)\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.value\n }\n catch (e) {\n loadError.value = (e as Error).message || String(e)\n console.error('[memi-board:commentFeed] 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([sortValue, pageSize], reset, { immediate: true })\n onScopeDispose(() => stopHeadSubscription?.())\n\n return {\n comments,\n commentsPending: headPending,\n hasMore,\n loadingMore,\n loadError,\n loadMore,\n sort: sortValue,\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 { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\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 const { t, formatNumber } = useMemiBoardI18n()\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(t('authorMemo.tooLong', { max: formatNumber(AUTHOR_MEMO_MAX_LENGTH) }))\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 { deleteLikesForComment } from '../utils/deletePostCascade'\nimport { useMemiBoardSettings } from './useMemiBoardSettings'\nimport type { CommentModel } from '../types'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\nimport type { MemiBoardI18n } from '../i18n/useMemiBoardI18n'\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, { t, formatNumber }: Pick<MemiBoardI18n, 't' | 'formatNumber'>): string {\n const trimmed = body.trim()\n if (!trimmed) throw new Error(t('comments.bodyRequired'))\n if (trimmed.length > COMMENT_BODY_MAX_LENGTH) {\n throw new Error(t('comments.bodyTooLong', { max: formatNumber(COMMENT_BODY_MAX_LENGTH) }))\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 { isBoardHidden } = useMemiBoardSettings()\n const i18n = useMemiBoardI18n()\n const { t } = i18n\n const commentsColRef = () => commentsCol(db, cfg())\n\n const bid = computed(() => toValue(boardId))\n const id = computed(() => toValue(postId))\n const listedForBoard = () => !isBoardHidden(bid.value)\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, i18n)\n const batch = writeBatch(db)\n const commentRef = doc(commentsColRef())\n batch.set(commentRef, {\n postId: id.value,\n boardId: bid.value,\n listed: listedForBoard(),\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 likeCount: 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, i18n)\n if (!input.parent.id) throw new Error(t('comments.replyTargetNotFound'))\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(t('comments.threadNotFound'))\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 listed: listedForBoard(),\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 likeCount: 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(t('comments.hasRepliesCannotDelete'))\n }\n await deleteLikesForComment(db, cfg(), comment.id)\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, i18n)\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 pid = postId.trim()\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('postId', '==', pid),\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('postId', '==', pid),\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 { computed, onScopeDispose, ref, toValue, watch, type InjectionKey, type MaybeRefOrGetter } from 'vue'\nimport type { Unsubscribe } from 'firebase/firestore'\nimport { useFirestore, useCurrentUser } from 'vuefire'\nimport {\n increment,\n onSnapshot,\n query,\n runTransaction,\n serverTimestamp,\n where,\n} from 'firebase/firestore'\nimport { useBoardPathConfig } from '../config'\nimport { commentDoc, likeDoc, likesCol, postDoc } from '../utils/boardPaths'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport function useMemiBoardLikes(boardId: string, postId: string) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const user = useCurrentUser()\n const { t } = useMemiBoardI18n()\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(t('likes.signInRequired'))\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\nexport interface MemiBoardCommentLikesApi {\n isLiked: (commentId: string | undefined) => boolean\n isPending: (commentId: string | undefined) => boolean\n toggleLike: (commentId: string) => Promise<boolean>\n}\n\nexport const memiBoardCommentLikesKey: InjectionKey<MemiBoardCommentLikesApi> = Symbol.for('memi-board:comment-likes')\n\n/** 글 하나에서 내가 누른 댓글 좋아요를 쿼리 1번으로 구독한다. 댓글마다 리스너를 달지 않는다. */\nexport function useMemiBoardCommentLikes(\n boardId: MaybeRefOrGetter<string>,\n postId: MaybeRefOrGetter<string>,\n): MemiBoardCommentLikesApi {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const user = useCurrentUser()\n const { t } = useMemiBoardI18n()\n const bid = computed(() => toValue(boardId))\n const pid = computed(() => toValue(postId))\n\n const likedIds = ref(new Set<string>())\n const pendingId = ref<string | null>(null)\n let stopSubscription: Unsubscribe | undefined\n\n watch([user, pid], ([current, currentPostId]) => {\n stopSubscription?.()\n stopSubscription = undefined\n likedIds.value = new Set()\n if (!current || !currentPostId) return\n\n stopSubscription = onSnapshot(\n query(\n likesCol(db, cfg()),\n where('uid', '==', current.uid),\n where('postId', '==', currentPostId),\n where('target', '==', 'comment'),\n ),\n (snapshot) => {\n const next = new Set<string>()\n for (const item of snapshot.docs) {\n const commentId = item.data().commentId\n if (typeof commentId === 'string' && commentId) next.add(commentId)\n }\n likedIds.value = next\n },\n (cause) => {\n console.error('[memi-board:comment-likes] onSnapshot failed', cause)\n },\n )\n }, { immediate: true })\n\n onScopeDispose(() => stopSubscription?.())\n\n function isLiked(commentId: string | undefined): boolean {\n return !!commentId && likedIds.value.has(commentId)\n }\n\n function isPending(commentId: string | undefined): boolean {\n return !!commentId && pendingId.value === commentId\n }\n\n async function toggleLike(commentId: string): Promise<boolean> {\n const uid = user.value?.uid\n if (!uid) throw new Error(t('likes.signInRequired'))\n if (!commentId) return false\n if (pendingId.value) return isLiked(commentId)\n pendingId.value = commentId\n try {\n const likeRef = likeDoc(db, cfg(), commentId, uid)\n const commentRef = commentDoc(db, cfg(), commentId)\n const nextLiked = await runTransaction(db, async (tx) => {\n const likeSnap = await tx.get(likeRef)\n const commentSnap = await tx.get(commentRef)\n if (!commentSnap.exists()) throw new Error(t('likes.commentNotFound'))\n const current = Number(commentSnap.data()?.likeCount ?? 0)\n if (likeSnap.exists()) {\n tx.delete(likeRef)\n tx.update(commentRef, { likeCount: Math.max(0, current - 1) })\n return false\n }\n tx.set(likeRef, {\n uid,\n postId: pid.value,\n boardId: bid.value,\n commentId,\n target: 'comment',\n createdAt: serverTimestamp(),\n })\n tx.update(commentRef, { likeCount: current + 1 })\n return true\n })\n const next = new Set(likedIds.value)\n if (nextLiked) next.add(commentId)\n else next.delete(commentId)\n likedIds.value = next\n return nextLiked\n }\n finally {\n pendingId.value = null\n }\n }\n\n return { isLiked, isPending, toggleLike }\n}\n","import { computed, onScopeDispose, ref, toValue, watch, type MaybeRefOrGetter } from 'vue'\nimport type { Unsubscribe } from 'firebase/firestore'\nimport {\n documentId,\n getDocs,\n limit as fbLimit,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n startAfter,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore'\nimport { useCurrentUser, useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport { reportDoc, reportsCol } from '../utils/boardPaths'\nimport type { BoardReportModel, BoardReportReason, BoardReportStatus } from '../types'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport const REPORT_DETAIL_MAX_LENGTH = 200\n/**\n * 신고 사유 목록. `label` 은 호환용 한국어 원문이다 — 화면에는 `reportReasonLabel(value, locale)` 로\n * 현재 언어 문구를 쓴다.\n */\nexport const REPORT_REASONS: { value: BoardReportReason, label: string }[] = [\n { value: 'spam', label: '스팸·광고' },\n { value: 'abuse', label: '욕설·혐오·괴롭힘' },\n { value: 'adult', label: '음란물' },\n { value: 'illegal', label: '불법 정보' },\n { value: 'other', label: '기타' },\n]\n\nexport function reportReasonLabel(reason: BoardReportReason | string | undefined, locale?: MemiBoardLocale) {\n if (REPORT_REASONS.some(item => item.value === reason)) return translate(locale, `reports.reason.${reason}`)\n return reason || translate(locale, 'reports.fallbackLabel')\n}\n\nexport function useMemiBoardReports(boardId: string, postId: string) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const user = useCurrentUser()\n const { t } = useMemiBoardI18n()\n\n const hasReported = ref(false)\n const pending = ref(false)\n let stop: Unsubscribe | undefined\n\n watch(user, (current) => {\n stop?.()\n stop = undefined\n if (!current) {\n hasReported.value = false\n return\n }\n stop = onSnapshot(reportDoc(db, cfg(), postId, current.uid), (snap) => {\n hasReported.value = snap.exists()\n }, (cause) => {\n console.error('[memi-board:reports] onSnapshot failed', cause)\n })\n }, { immediate: true })\n\n onScopeDispose(() => stop?.())\n\n async function submitReport(input: {\n reason: BoardReportReason\n detail?: string\n postTitle?: string\n authorUid?: string\n authorName?: string | null\n }): Promise<void> {\n const uid = user.value?.uid\n if (!uid) throw new Error(t('reports.signInRequired'))\n if (pending.value) return\n if (input.authorUid && input.authorUid === uid) throw new Error(t('reports.cannotReportOwn'))\n const reason = REPORT_REASONS.some(item => item.value === input.reason) ? input.reason : null\n if (!reason) throw new Error(t('reports.reasonRequired'))\n const detail = (input.detail || '').trim()\n if (detail.length > REPORT_DETAIL_MAX_LENGTH) {\n throw new Error(t('reports.detailTooLong', { max: REPORT_DETAIL_MAX_LENGTH }))\n }\n\n pending.value = true\n try {\n await setDoc(reportDoc(db, cfg(), postId, uid), {\n uid,\n target: 'post',\n postId,\n boardId,\n reason,\n detail,\n status: 'open',\n postTitle: (input.postTitle || '').trim().slice(0, 200),\n authorUid: input.authorUid || '',\n authorName: input.authorName ?? null,\n createdAt: serverTimestamp(),\n })\n hasReported.value = true\n }\n finally {\n pending.value = false\n }\n }\n\n return { hasReported, pending, submitReport }\n}\n\nconst QUEUE_PAGE_SIZE = 20\n\nexport function useMemiBoardReportQueue(options: { pageSize?: number, boardId?: MaybeRefOrGetter<string> } = {}) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const { canManageContent, canManageBoard } = useMemiBoardAuth()\n const pageSize = options.pageSize ?? QUEUE_PAGE_SIZE\n const boardFilter = computed(() => toValue(options.boardId)?.trim() || '')\n\n const reports = ref<BoardReportModel[]>([])\n const pending = ref(true)\n const loadingMore = ref(false)\n const hasMore = ref(false)\n const loadError = ref('')\n let cursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stop: Unsubscribe | undefined\n\n function mapDoc(item: QueryDocumentSnapshot<DocumentData>): BoardReportModel {\n return { id: item.id, ...item.data() } as BoardReportModel\n }\n\n function startHead() {\n stop?.()\n if (!canManageContent.value) {\n reports.value = []\n pending.value = false\n return\n }\n pending.value = true\n const constraints = boardFilter.value\n ? [where('status', '==', 'open'), where('boardId', '==', boardFilter.value)]\n : [where('status', '==', 'open'), where('target', '==', 'post')]\n stop = onSnapshot(query(\n reportsCol(db, cfg()),\n ...constraints,\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n fbLimit(pageSize),\n ), (snapshot) => {\n reports.value = snapshot.docs.map(mapDoc).filter(item => canManageBoard(item.boardId))\n cursor = snapshot.docs.at(-1) ?? null\n hasMore.value = snapshot.docs.length >= pageSize\n pending.value = false\n }, (cause) => {\n console.error('[memi-board:report-queue] onSnapshot failed', cause)\n loadError.value = (cause as Error).message || String(cause)\n pending.value = false\n })\n }\n\n watch([canManageContent, boardFilter], startHead, { immediate: true })\n onScopeDispose(() => stop?.())\n\n async function loadMore() {\n if (loadingMore.value || !hasMore.value || !cursor || !canManageContent.value) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const constraints = boardFilter.value\n ? [where('status', '==', 'open'), where('boardId', '==', boardFilter.value)]\n : [where('status', '==', 'open'), where('target', '==', 'post')]\n const snapshot = await getDocs(query(\n reportsCol(db, cfg()),\n ...constraints,\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n startAfter(cursor),\n fbLimit(pageSize + 1),\n ))\n const pageDocs = snapshot.docs.slice(0, pageSize)\n const existing = new Set(reports.value.map(item => item.id))\n reports.value.push(...pageDocs.map(mapDoc).filter(item => !existing.has(item.id) && canManageBoard(item.boardId)))\n cursor = pageDocs.at(-1) ?? cursor\n hasMore.value = snapshot.docs.length > pageSize\n }\n catch (cause) {\n loadError.value = (cause as Error).message || String(cause)\n }\n finally {\n loadingMore.value = false\n }\n }\n\n async function resolveReport(report: BoardReportModel, status: Exclude<BoardReportStatus, 'open'>) {\n if (!report.id || !canManageBoard(report.boardId)) return\n await updateDoc(reportDoc(db, cfg(), report.postId, report.uid), {\n status,\n reviewedAt: serverTimestamp(),\n reviewedBy: useCurrentUser().value?.uid || '',\n })\n }\n\n return {\n reports,\n pending,\n loadingMore,\n hasMore,\n loadError,\n loadMore,\n dismissReport: (report: BoardReportModel) => resolveReport(report, 'dismissed'),\n actionReport: (report: BoardReportModel) => resolveReport(report, 'actioned'),\n }\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","import { memiBoardLocaleInfo, type MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\n\n/** 한국어 보드용 원문 프롬프트 (ko). */\nexport 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/** 프롬프트에 함께 적는 언어 영어 이름 (모델이 언어를 헷갈리지 않게). */\nconst LOCALE_ENGLISH_NAMES: Record<MemiBoardLocale, string> = {\n ko: 'Korean',\n en: 'English',\n ja: 'Japanese',\n de: 'German',\n fr: 'French',\n es: 'Spanish',\n pt: 'Portuguese',\n zh: 'Simplified Chinese',\n ar: 'Arabic',\n id: 'Indonesian',\n}\n\n/** 예: \"Japanese (日本語)\", \"English\" */\nexport function memiBoardLanguageLabel(locale: MemiBoardLocale): string {\n const english = LOCALE_ENGLISH_NAMES[locale] ?? 'English'\n const native = memiBoardLocaleInfo(locale).name\n return english === native ? english : `${english} (${native})`\n}\n\n/** ko 외 모든 언어용 영어 프롬프트. 엄격도·출력 형식은 한국어 원문과 동일. */\nfunction buildEnglishModerationPrompt(locale: MemiBoardLocale): string {\n const language = memiBoardLanguageLabel(locale)\n return `You are a strict moderator reviewing content for a message board.\nIf the text below contains profanity, vulgar slang, sexual harassment, hate speech, threats, spam, or repetitive/flooding advertisements, you MUST reject it (flagged=true).\n\nThe content may be written in ANY language, or a mix of languages — not only the board's language.\nJudge every language equally strictly, including slang, abbreviations, romanizations, deliberate misspellings,\ninserted spaces/symbols, and other obfuscation (e.g. in Korean, Japanese, Chinese, Arabic, Spanish, etc.).\n\nflagged=true examples:\n- Profanity/vulgar slang: fuck, shit, bitch, asshole, f*ck, sh1t /\n Korean: 씨발, 시발, ㅅㅂ, 병신, ㅂㅅ, 지랄, 좆, 개새끼, 존나 /\n Japanese: クソ, 死ね, ちんこ / Chinese: 操你妈, 傻逼, 他妈的 /\n Spanish: puta, mierda, pendejo / German: Scheiße, Hurensohn / French: putain, connard /\n Portuguese: porra, caralho / Indonesian: anjing, bangsat, kontol / Arabic: كس, شرموطة, etc.\n- Sexual insults, hate speech, threats\n- Flooding advertisements/spam unrelated to the board's topic\n\nflagged=false:\n- Normal posts/comments that fit the board's topic (strong opinions and legitimate criticism are allowed)\n\nOutput JSON only:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- If flagged=true, choose a category, and write reason as ONE concrete sentence in ${language} explaining **what the problem is**\n (e.g. \"It contains profanity, so it can't be posted.\", \"It contains advertising/spam, so it can't be posted.\" — but written in ${language}).\n Do not quote the offensive words at length; clearly state the type (profanity/hate/spam, etc.).\n- If flagged=false, category is \"none\" and reason is \"\"`\n}\n\n/**\n * 보드 언어별 검열 시스템 프롬프트.\n * ko 는 한국어 원문(MODERATION_SYSTEM) 그대로, 그 외는 영어 프롬프트 + reason 을 보드 언어로 쓰도록 지시.\n */\nexport function buildModerationPrompt(locale: MemiBoardLocale = 'ko'): string {\n return locale === 'ko' ? MODERATION_SYSTEM : buildEnglishModerationPrompt(locale)\n}\n\n/** 텍스트 검열 요청 본문 */\nexport function buildModerationTextPrompt(text: string, locale: MemiBoardLocale = 'ko'): string {\n const body = `\"\"\"${text.slice(0, 4000)}\"\"\"`\n return locale === 'ko' ? `심사할 텍스트:\\n${body}` : `Text to review:\\n${body}`\n}\n\n/** 이미지 검열 요청 문구 */\nexport function buildModerationImagePrompt(locale: MemiBoardLocale = 'ko'): string {\n return locale === 'ko' ? '이 이미지를 심사해 주세요.' : 'Please review this image.'\n}\n\n/** 사용자 안내용 카테고리 라벨 (ko 원문. 다른 언어는 moderationCategoryLabel 사용) */\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 moderationCategoryLabel(category: string, locale: MemiBoardLocale = 'ko'): string {\n if (!category || category === 'none') return ''\n if (!(category in MODERATION_CATEGORY_LABELS)) return category\n return translate(locale, `moderationPrompt.categories.${category}`)\n}\n\n/** 차단 사유를 사용자에게 읽기 쉽게 정리 */\nexport function formatModerationUserReason(opts: {\n reason?: string\n category?: string\n via?: string\n localHit?: string\n /** 안내 문구 언어 (기본 ko) */\n locale?: MemiBoardLocale\n}): string {\n const locale = opts.locale ?? 'ko'\n if (opts.via === 'local' && opts.localHit) {\n const hit = opts.localHit.trim()\n // 짧은 금칙어만 표시 (너무 길면 유형만)\n if (hit.length > 0 && hit.length <= 12) {\n return translate(locale, 'moderationPrompt.localHit', { hit })\n }\n return translate(locale, 'moderationPrompt.localGeneric')\n }\n\n const raw = (opts.reason || '').trim()\n if (raw) return raw\n\n const cat = opts.category ? moderationCategoryLabel(opts.category, locale) : ''\n if (cat) return translate(locale, 'moderationPrompt.categoryBlocked', { category: cat })\n return translate(locale, 'moderationPrompt.genericBlocked')\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 { computed, toValue, type MaybeRefOrGetter } from 'vue'\nimport { useFirebaseApp } from 'vuefire'\nimport {\n buildLocalBlockRegex,\n DEFAULT_LOCAL_BLOCKLIST,\n buildModerationImagePrompt,\n buildModerationPrompt,\n buildModerationTextPrompt,\n formatModerationUserReason,\n parseModerationJson,\n} from '../utils/moderation-prompt'\nimport { useMemiBoardConfig } from '../config'\nimport { normalizeMemiBoardLocale, type MemiBoardLocale } from '../i18n/locales'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport { translate } from '../i18n/translate'\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(options: {\n /** 보드 언어. 같은 컴포넌트에서 boardLocale 을 provide 했다면 그 locale 을 넘긴다(자기 자신에게 inject 되지 않음). */\n locale?: MaybeRefOrGetter<string | undefined | null>\n} = {}) {\n const promptI18n = useMemiBoardI18n()\n const promptLocale = computed<MemiBoardLocale>(() => normalizeMemiBoardLocale(toValue(options.locale)) ?? promptI18n.locale.value)\n const config = useMemiBoardConfig()\n const moderation = config.moderation ?? {}\n const app = useFirebaseApp()\n const t = (key: string) => translate(promptLocale.value, key)\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: buildModerationPrompt(promptLocale.value),\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: t('moderation.reviewFailed'),\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, opts?: { skipFilter?: boolean }): Promise<ModerationResult> {\n if (isWriteRestricted.value) {\n return {\n flagged: true,\n category: 'other',\n reason: restrictedMessage.value\n || t('moderation.writeRestricted'),\n via: 'restricted',\n }\n }\n\n const trimmed = text.trim()\n if (!trimmed) {\n return { ...APPROVED, via: 'empty' as ModerationVia }\n }\n\n if (opts?.skipFilter) {\n return { ...APPROVED, via: 'admin-override' }\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(buildModerationTextPrompt(trimmed, promptLocale.value))\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 || t('moderation.writeRestricted'),\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 buildModerationImagePrompt(promptLocale.value),\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'\nimport { translate } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\n/**\n * 역할 목록. label·description 은 기본 언어(ko) 문구다 —\n * 화면에서는 t('common.role.<value>') / t('boardUsers.roleDescription.<value>') 로 현재 언어 문구를 쓴다.\n */\nexport const BOARD_USER_ROLES: Array<{\n value: BoardUserRole\n label: string\n description: string\n}> = (['admin', 'staff', 'user'] as const).map(value => ({\n value,\n label: translate('ko', `common.role.${value}`),\n description: translate('ko', `boardUsers.roleDescription.${value}`),\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 { t } = useMemiBoardI18n()\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(t('boardUsers.unsupportedRole'))\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","import type { BoardListView, BoardSection, BoardSectionCols, BoardSectionHeight, BoardSectionKind, BoardSectionSort, CommentModel, PostModel } from '../types'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\n\nexport const BOARD_SECTION_COLS: BoardSectionCols[] = [3, 4, 5, 6, 7, 8, 9, 12]\n\n/** 섹션 종류 선택지. locale 을 넘기면 해당 언어 라벨(기본 ko). */\nexport function boardSectionKinds(locale?: MemiBoardLocale): { value: BoardSectionKind, label: string }[] {\n return [\n { value: 'list', label: translate(locale, 'sectionUtil.kind.list') },\n { value: 'post', label: translate(locale, 'sectionUtil.kind.post') },\n { value: 'comments', label: translate(locale, 'sectionUtil.kind.comments') },\n ]\n}\n\n/** 섹션 정렬 선택지. locale 을 넘기면 해당 언어 라벨(기본 ko). */\nexport function boardSectionSorts(locale?: MemiBoardLocale): { value: BoardSectionSort, label: string }[] {\n return [\n { value: 'latest', label: translate(locale, 'sectionUtil.sort.latest') },\n { value: 'likes', label: translate(locale, 'sectionUtil.sort.likes') },\n ]\n}\n\n/** @deprecated 한국어 고정 라벨. 언어별 라벨은 boardSectionKinds(locale) 사용 */\nexport const BOARD_SECTION_KINDS = boardSectionKinds('ko')\n/** @deprecated 한국어 고정 라벨. 언어별 라벨은 boardSectionSorts(locale) 사용 */\nexport const BOARD_SECTION_SORTS = boardSectionSorts('ko')\n\nexport function defaultSectionCount(kind: BoardSectionKind) {\n return kind === 'list' ? 5 : 4\n}\n\nexport function clampSectionCount(kind: BoardSectionKind, value: unknown) {\n const n = Math.floor(Number(value))\n if (!Number.isFinite(n)) return defaultSectionCount(kind)\n return Math.min(kind === 'list' ? 20 : 10, Math.max(1, n))\n}\n\nexport function pairSectionCols(cols: BoardSectionCols): BoardSectionCols | null {\n if (cols === 12) return null\n return (12 - cols) as BoardSectionCols\n}\n\nexport function sectionColClass(cols: BoardSectionCols): string {\n return {\n 3: 'col-span-12 md:col-span-3',\n 4: 'col-span-12 md:col-span-4',\n 5: 'col-span-12 md:col-span-5',\n 6: 'col-span-12 md:col-span-6',\n 7: 'col-span-12 md:col-span-7',\n 8: 'col-span-12 md:col-span-8',\n 9: 'col-span-12 md:col-span-9',\n 12: 'col-span-12',\n }[cols]\n}\n\nexport function sectionMinHeight(height: BoardSectionHeight) {\n if (height === 'sm') return 'min-h-40'\n if (height === 'lg') return 'min-h-80'\n return 'min-h-56'\n}\n\nexport function miniPostLink(post: Pick<PostModel, 'boardId' | 'slug' | 'category'>) {\n const boardId = post.boardId || post.category || 'free'\n if (!post.slug) return `/board/${boardId}`\n return `/board/${boardId}/${post.slug}`\n}\n\nexport function miniMoreLink(section: Pick<BoardSection, 'kind' | 'boardId' | 'sort'>) {\n if (section.kind === 'comments') {\n return section.sort === 'likes' ? '/boardComments?sort=likes' : '/boardComments'\n }\n if (section.kind === 'list' && section.boardId) return `/board/${section.boardId}`\n return '/board'\n}\n\nexport function miniCommentLink(comment: Pick<CommentModel, 'boardId' | 'postId'>) {\n if (!comment.boardId || !comment.postId) return undefined\n return `/board/${comment.boardId}/post/${comment.postId}`\n}\n\nexport function miniPostImage(post: PostModel): string | undefined {\n return post.previewImage\n || post.attachments?.find(item => item.type.startsWith('image/'))?.url\n}\n\nexport function miniPostTitle(post: PostModel, locale?: MemiBoardLocale) {\n return post.title?.trim() || post.summary || translate(locale, 'sectionUtil.untitledPost')\n}\n\nexport type MiniViewGroup = 'list' | 'media'\n\nexport function miniViewGroup(view: BoardListView): MiniViewGroup {\n return view === 'image' || view === 'video' ? 'media' : 'list'\n}\n\nexport function miniViewLabel(view: BoardListView, locale?: MemiBoardLocale) {\n return translate(locale, miniViewGroup(view) === 'media' ? 'sectionUtil.view.media' : 'sectionUtil.view.list')\n}\n\nexport function miniBoardView(\n boardId: string | null | undefined,\n getBoard: (id: string) => { listView?: BoardListView } | undefined,\n): BoardListView {\n if (!boardId) return 'default'\n return getBoard(boardId)?.listView ?? 'default'\n}\n\nexport function miniPostsView(\n posts: Array<{ boardId?: string }>,\n getBoard: (id: string) => { listView?: BoardListView } | undefined,\n): BoardListView {\n const views = posts.map(post => miniBoardView(post.boardId, getBoard))\n if (!views.length) return 'default'\n if (views.every(view => view === views[0])) return views[0]!\n const image = views.filter(view => view === 'image').length\n const video = views.filter(view => view === 'video').length\n if (image >= views.length / 2) return 'image'\n if (video >= views.length / 2) return 'video'\n return 'default'\n}","import { computed } from 'vue'\nimport { doc, serverTimestamp, setDoc } from 'firebase/firestore'\nimport { useDocument, useFirestore } from 'vuefire'\nimport type { BoardSection, BoardSectionCols, BoardSectionHeight, BoardSectionKind, BoardSectionLayout, BoardSectionSort } from '../types'\nimport { BOARD_SECTION_COLS, clampSectionCount } from '../utils/section'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport const DEFAULT_BOARD_SECTIONS: BoardSection[] = []\n\nfunction asCols(raw: Record<string, unknown>): BoardSectionCols {\n const cols = Number(raw.cols)\n if ((BOARD_SECTION_COLS as number[]).includes(cols)) return cols as BoardSectionCols\n const width = raw.width\n if (width === 'xs') return 3\n if (width === 'sm') return 4\n if (width === 'lg' || Number(raw.span) === 2) return 12\n return 6\n}\n\nfunction asHeight(raw: Record<string, unknown>): BoardSectionHeight {\n const height = raw.height ?? raw.size\n return height === 'sm' || height === 'lg' ? height : 'md'\n}\n\nfunction asKind(raw: Record<string, unknown>): BoardSectionKind {\n if (raw.kind === 'post' || raw.kind === 'comments') return raw.kind\n return 'list'\n}\n\nfunction asSort(raw: Record<string, unknown>): BoardSectionSort {\n return raw.sort === 'likes' ? 'likes' : 'latest'\n}\n\nfunction asBoardId(raw: Record<string, unknown>): string | null {\n const id = typeof raw.boardId === 'string' ? raw.boardId.trim() : ''\n return id || null\n}\n\nfunction asPostIds(raw: Record<string, unknown>, count: number): string[] {\n const source = Array.isArray(raw.postIds) ? raw.postIds : []\n const ids: string[] = []\n for (const value of source) {\n const id = String(value || '').trim()\n if (!id || ids.includes(id)) continue\n ids.push(id)\n if (ids.length >= count) break\n }\n return ids\n}\n\nfunction defaultSectionTitle(order: number, locale?: MemiBoardLocale) {\n return translate(locale, 'sections.defaultTitle', { n: order + 1 })\n}\n\nexport function normalizeBoardSection(raw: Record<string, unknown>, order: number, locale?: MemiBoardLocale): BoardSection {\n const kind = asKind(raw)\n const count = clampSectionCount(kind, raw.count)\n return {\n id: String(raw.id || `box-${order + 1}`),\n title: String(raw.title || defaultSectionTitle(order, locale)),\n showTitle: raw.showTitle !== false,\n kind,\n boardId: asBoardId(raw),\n count,\n postIds: asPostIds(raw, count),\n sort: asSort(raw),\n cols: asCols(raw),\n height: asHeight(raw),\n order,\n }\n}\n\n/** 호스트 `settings/homeLayout` 문서를 읽는다. 기존 메인 레이아웃 경로를 유지한다. */\nexport function useMemiBoardSections() {\n const { locale } = useMemiBoardI18n()\n const db = useFirestore()\n const settingsRef = doc(db, 'settings', 'homeLayout')\n const { data: settings, pending } = useDocument<BoardSectionLayout>(settingsRef)\n\n const sections = computed(() => {\n const source = Array.isArray(settings.value?.blocks) ? settings.value.blocks : DEFAULT_BOARD_SECTIONS\n return source.map((block, index) => normalizeBoardSection(block as unknown as Record<string, unknown>, index, locale.value))\n })\n\n async function saveSections(next: BoardSection[]) {\n await setDoc(settingsRef, {\n columns: 12,\n blocks: next.map((block, order) => {\n const raw = block as unknown as Record<string, unknown>\n const kind = asKind(raw)\n const count = clampSectionCount(kind, block.count)\n return {\n id: block.id,\n title: block.title.trim() || defaultSectionTitle(order, locale.value),\n showTitle: block.showTitle !== false,\n kind,\n boardId: asBoardId(raw),\n count,\n postIds: asPostIds(raw, count),\n sort: asSort(raw),\n cols: asCols(raw),\n height: asHeight(raw),\n order,\n }\n }),\n updatedAt: serverTimestamp(),\n })\n }\n\n const hasSavedLayout = computed(() => Array.isArray(settings.value?.blocks))\n\n return {\n settings,\n pending,\n sections,\n blocks: sections,\n hasSavedLayout,\n saveSections,\n saveBlocks: saveSections,\n }\n}","import { computed, toValue, type MaybeRefOrGetter } from 'vue'\nimport {\n documentId,\n limit as fbLimit,\n orderBy,\n query,\n where,\n type QueryConstraint,\n} from 'firebase/firestore'\nimport { useCollection, useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { boardSsrKey, postsCol } from '../utils/boardPaths'\nimport type { PostModel } from '../types'\n\n/**\n * SSR과 클라이언트 첫 렌더의 pending 상태가 갈리면(서버는 즉시 완료, 클라이언트는\n * 로딩 중) MiniList.vue가 서버에선 빈 상태(<p>), 클라이언트에선 스켈레톤(<div>)을\n * 그려 노드 자체가 달라지는 하이드레이션 mismatch가 난다. 쿼리가 where/orderBy/limit을\n * 쓰는 복합 쿼리라 ssrKey 없인 vuefire가 경로를 못 찾는 것도 같은 원인이라, 두 컴포저블\n * 모두 서버에서도 쿼리를 실행하고 ssrKey로 하이드레이션시킨다.\n */\nexport function useMemiBoardMiniLatest(\n boardId: MaybeRefOrGetter<string | null>,\n count: MaybeRefOrGetter<number>,\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const boardKey = toValue(boardId)?.trim() || 'all'\n const postsQuery = computed(() => {\n const take = Math.max(1, Math.floor(Number(toValue(count)) || 5))\n const board = toValue(boardId)?.trim() || ''\n const constraints: QueryConstraint[] = [\n where('isPublished', '==', true),\n where('listed', '==', true),\n ]\n if (board) constraints.unshift(where('boardId', '==', board))\n return query(\n postsCol(db, cfg()),\n ...constraints,\n orderBy('createdAt', 'desc'),\n fbLimit(take),\n )\n })\n const { data, pending, error } = useCollection<PostModel>(postsQuery, {\n ssrKey: boardSsrKey(cfg(), `mini-list/${boardKey}`),\n })\n const posts = computed(() => data.value ?? [])\n return { posts, pending, error }\n}\n\nexport function useMemiBoardMiniPicked(postIds: MaybeRefOrGetter<string[]>) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const ids = computed(() =>\n [...new Set(toValue(postIds).map(id => id.trim()).filter(Boolean))].slice(0, 10),\n )\n const idsKey = ids.value.join(',') || 'none'\n const postsQuery = computed(() => {\n if (!ids.value.length) return null\n return query(\n postsCol(db, cfg()),\n where(documentId(), 'in', ids.value),\n )\n })\n const { data, pending, error } = useCollection<PostModel>(postsQuery, {\n ssrKey: boardSsrKey(cfg(), `mini-picked/${idsKey}`),\n })\n const posts = computed(() => {\n const byId = new Map((data.value ?? []).map(post => [post.id, post]))\n const ordered: PostModel[] = []\n for (const id of ids.value) {\n const item = byId.get(id)\n if (item && item.isPublished !== false) ordered.push(item)\n }\n return ordered\n })\n return { posts, pending, error }\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.45.0',\n date: '2026-10-01',\n highlights: [\n '게시판을 10개 언어로 볼 수 있어요 (한국어·영어·일본어·독일어·프랑스어·스페인어·포르투갈어·중국어·아랍어·인도네시아어)',\n '게시판 설정에서 게시판마다 언어를 고정하거나 사이트 언어를 따르게 할 수 있어요',\n ],\n },\n {\n version: '0.44.1',\n date: '2026-10-01',\n highlights: [\n 'Mac Safari에서 한글을 입력하고 바로 등록 버튼을 누를 때 두 번 눌러야 하던 문제를 고쳤어요',\n ],\n },\n {\n version: '0.44.0',\n date: '2026-10-01',\n highlights: [\n '글을 저장하고 미리보기가 뜨기까지 기다리는 시간이 줄었어요 (특히 iPhone·Mac Safari)',\n ],\n },\n {\n version: '0.42.0',\n date: '2026-08-27',\n highlights: [\n '글쓰기에서 문단 옆 + 버튼이나 / 입력으로 제목·목록·이미지 같은 블록을 빠르게 넣을 수 있어요',\n '문단 메뉴에서 블록을 다른 형식으로 바꾸거나 복제·이동·삭제할 수 있어요',\n ],\n },\n {\n version: '0.41.0',\n date: '2026-08-27',\n highlights: [\n 'AI 도우미 결과를 바로 적용하지 않고 이전/이후를 비교해보고 적용할 수 있어요',\n ],\n },\n {\n version: '0.40.0',\n date: '2026-08-27',\n highlights: [\n 'AI 글쓰기 도우미에서 원하는 문체를 직접 입력해 바꿀 수 있어요 (예: 경상도 사투리 스타일로)',\n ],\n },\n {\n version: '0.39.1',\n date: '2026-08-27',\n highlights: [\n '관리자는 글쓰기 하단 스위치로 비속어 필터를 끄고 글을 올릴 수 있어요',\n ],\n },\n {\n version: '0.39.0',\n date: '2026-08-21',\n highlights: [\n '사진 글에서 원하는 사진을 대표로 고를 수 있어요',\n '목록 모양과 글쓰기 폼을 각각 선택할 수 있어요',\n ],\n },\n {\n version: '0.38.1',\n date: '2026-08-21',\n highlights: [\n '내가 쓴 글을 지울 때 첨부 사진 때문에 막히던 문제를 고쳤어요',\n ],\n note: '호스트는 Storage 예시 규칙의 memiBoardPosts/{postId}/{allPaths=**} 목록 블록을 넣고 배포해야 해요.',\n },\n {\n version: '0.38.0',\n date: '2026-08-21',\n highlights: [\n '숨김 게시판에는 댓글을 거의 쓰지 않아요. 필요하면 담당 스태프만 남길 수 있어요',\n '숨김 게시판 댓글은 공개 댓글 모음에 나오지 않아요',\n ],\n note: '호스트는 Firestore 규칙·댓글 listed 인덱스를 다시 배포해야 해요. 공개 보드는 한 번 저장하면 예전 댓글이 목록에 다시 채워져요.',\n },\n {\n version: '0.36.0',\n date: '2026-08-19',\n highlights: [\n '게시물을 신고할 수 있어요. 담당 스태프가 내용을 확인해요',\n '스태프는 지정된 게시판만 관리해요',\n ],\n note: '호스트는 Firestore 신고 규칙·인덱스를 배포해야 해요.',\n },\n {\n version: '0.35.0',\n date: '2026-08-19',\n highlights: [\n '메인에 게시목록·게시물·댓글모음 섹션을 올릴 수 있어요',\n '댓글모음은 최신순이나 좋아요순으로 네 개부터 보여요',\n ],\n },\n {\n version: '0.34.0',\n date: '2026-08-19',\n highlights: [\n '댓글을 최신순이나 좋아요순으로 모아 볼 수 있어요',\n ],\n note: '호스트는 좋아요순 목록용 Firestore 인덱스(likeCount + createdAt)를 배포해야 해요.',\n },\n {\n version: '0.33.0',\n date: '2026-08-19',\n highlights: [\n '댓글과 답글에도 좋아요를 누를 수 있어요',\n '글을 지우면 댓글 좋아요도 함께 삭제돼요',\n ],\n note: '호스트는 Firestore 규칙과 좋아요 인덱스를 병합·배포해야 댓글 좋아요가 동작해요.',\n },\n {\n version: '0.32.0',\n date: '2026-08-14',\n highlights: [\n '글쓰기에서 사진 파일을 올리거나 외부 이미지 주소를 넣을 수 있어요',\n '게시판을 지우면 그 안의 글·댓글·이미지도 함께 삭제돼요',\n ],\n },\n {\n version: '0.31.0',\n date: '2026-08-14',\n highlights: [\n '글쓰기 화면에 AI 도우미가 생겼어요. 맞춤법, 다듬기, 이어서 쓰기, 요약, 번역, 제목을 도와줘요',\n 'AI 도우미는 하루 10번까지 사용할 수 있어요',\n ],\n note: '호스트는 Firestore 규칙에 AI 글쓰기 횟수 필드를 병합·배포해야 제한이 지켜져요.',\n },\n {\n version: '0.30.2',\n date: '2026-08-14',\n highlights: [\n '일반 목록 썸네일을 조금 작은 라운드 사각으로 바꿨어요',\n '글쓰기 도구 모음을 서식 메뉴로 줄여 좁은 화면에서도 잘리지 않아요',\n '다시 로그인해도 바꿔 둔 프로필 이름과 사진이 유지돼요',\n ],\n },\n {\n version: '0.30.1',\n date: '2026-08-13',\n highlights: [\n '게시물 상단의 목록 버튼을 간결한 왼쪽 화살표로 바꿨어요',\n '이제 main 브랜치에 push하면 npm 배포가 자동으로 진행돼요',\n ],\n },\n {\n version: '0.30.0',\n date: '2026-08-13',\n highlights: [\n '본문 글 없이 사진만 첨부해도 글을 올릴 수 있어요',\n '목록은 게시판 설정에서 정한 보기 방식을 사용하고, 테스트용 보기 전환 버튼은 숨겼어요',\n '글쓰기 중 사진 업로드 권한과 Storage 버킷 설정 안내를 보강했어요',\n ],\n note: '이미지 업로드가 거부되면 호스트의 Storage 규칙과 firebase.json 버킷 설정을 최신 예시로 확인해 주세요.',\n },\n {\n version: '0.29.1',\n date: '2026-08-13',\n highlights: [\n '프로필 이름이나 사진을 바꾸면 예전에 작성한 글과 댓글의 작성자 정보도 함께 바뀌어요',\n '나를 대상으로 작성된 답글의 표시 이름도 새 프로필 이름으로 맞춰져요',\n ],\n note: '이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요.',\n },\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","import { getAI, getGenerativeModel, GoogleAIBackend } from 'firebase/ai'\nimport { runTransaction, serverTimestamp } from 'firebase/firestore'\nimport { computed, toValue, type MaybeRefOrGetter } from 'vue'\nimport { useCurrentUser, useFirebaseApp, useFirestore } from 'vuefire'\nimport { useBoardPathConfig, useMemiBoardConfig } from '../config'\nimport { boardUserDoc } from '../utils/boardPaths'\nimport { memiBoardLanguageLabel } from '../utils/moderation-prompt'\nimport { normalizeMemiBoardLocale, type MemiBoardLocale } from '../i18n/locales'\nimport { translate, type MemiBoardMessageParams } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport const WRITING_ASSISTANT_DAILY_LIMIT = 10\nconst WRITING_ASSISTANT_WINDOW_MS = 24 * 60 * 60 * 1000\n\nexport type WritingAssistantAction =\n | 'proofread'\n | 'polish'\n | 'continue'\n | 'summarize'\n | 'translate-en'\n | 'translate-ko'\n | 'title'\n | 'custom'\n\nconst CUSTOM_INSTRUCTION_MAX_LENGTH = 200\n\ninterface WritingAssistantInput {\n action: WritingAssistantAction\n content: string\n title?: string\n selection?: boolean\n /** action이 'custom'일 때 사용자가 직접 입력한 지시문 (예: \"경상도 사투리 스타일로\") */\n customInstruction?: string\n}\n\ntype BuiltInAction = Exclude<WritingAssistantAction, 'custom'>\n\n/** 한국어 보드용 원문 지시문 (ko) */\nconst INSTRUCTIONS: Record<BuiltInAction, string> = {\n proofread: '맞춤법, 띄어쓰기, 문법 오류만 고쳐라. 의미와 말투는 바꾸지 마라.',\n polish: '뜻과 작성자의 말투를 유지하면서 더 자연스럽고 읽기 좋은 문장으로 다듬어라.',\n continue: '앞의 맥락과 말투에 맞는 내용을 과장 없이 한두 문단 이어서 작성하라.',\n summarize: '핵심 정보는 보존하면서 원문의 약 절반 길이로 간결하게 줄여라.',\n 'translate-en': '자연스러운 영어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.',\n 'translate-ko': '자연스러운 한국어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.',\n title: '본문을 대표하는 간결한 한국어 제목 하나를 만들어라. 따옴표나 설명 없이 제목만 출력하라.',\n}\n\n/** ko 외 언어용 영어 지시문. 결과는 보드 언어로 쓰도록 language 를 넣는다. */\nfunction englishInstructions(language: string): Record<BuiltInAction, string> {\n return {\n proofread: 'Fix only spelling, spacing, and grammar errors. Do not change the meaning or tone.',\n polish: 'Refine the text into more natural, readable sentences while keeping the meaning and the author\\'s tone.',\n continue: 'Continue writing one or two paragraphs that match the preceding context and tone, without exaggeration.',\n summarize: 'Shorten the text concisely to about half of its original length while preserving the key information.',\n 'translate-en': 'Translate into natural English. Keep proper nouns and technical terms accurate.',\n 'translate-ko': 'Translate into natural Korean. Keep proper nouns and technical terms accurate.',\n title: `Create one concise title in ${language} that represents the body. Output only the title, without quotes or explanation.`,\n }\n}\n\ntype Translator = (key: string, params?: MemiBoardMessageParams) => string\n\nfunction resolveInstruction(input: WritingAssistantInput, locale: MemiBoardLocale, t: Translator): string {\n const ko = locale === 'ko'\n const language = memiBoardLanguageLabel(locale)\n if (input.action !== 'custom') {\n if (ko) return INSTRUCTIONS[input.action]\n const base = englishInstructions(language)[input.action]\n // 번역 작업은 대상 언어가 정해져 있으므로 보드 언어 지시를 덧붙이지 않는다.\n if (input.action === 'translate-en' || input.action === 'translate-ko' || input.action === 'title') return base\n return `${base} Write the result in the same language as the original text; if unclear, write in ${language}.`\n }\n const custom = input.customInstruction?.trim().slice(0, CUSTOM_INSTRUCTION_MAX_LENGTH)\n if (!custom) throw new Error(t('writingAssistant.customRequired'))\n return ko\n ? `뜻은 유지하면서 다음 스타일 지시를 반영해 다시 써라: ${custom}`\n : `Rewrite the text applying the following style instruction while keeping the meaning: ${custom}`\n}\n\nfunction cleanModelOutput(value: string): string {\n return value.trim()\n .replace(/^```(?:html|text)?\\s*/i, '')\n .replace(/\\s*```$/, '')\n .trim()\n}\n\nfunction timestampMs(value: unknown): number | null {\n if (!value || typeof value !== 'object') return null\n if ('toMillis' in value && typeof (value as { toMillis: () => number }).toMillis === 'function') {\n return (value as { toMillis: () => number }).toMillis()\n }\n return null\n}\n\nexport interface UseMemiBoardWritingAssistantOptions {\n /**\n * 보드 언어. 같은 컴포넌트에서 useMemiBoardI18n({ boardLocale }) 로 provide 한 값은\n * 자기 자신에게 inject 되지 않으므로, 그 경우 여기로 넘긴다(예: Editor 의 locale).\n */\n locale?: MaybeRefOrGetter<string | undefined | null>\n}\n\n/** 검열과 동일한 Firebase AI Logic 연결을 사용하는 글쓰기 도우미. */\nexport function useMemiBoardWritingAssistant(options: UseMemiBoardWritingAssistantOptions = {}) {\n const i18n = useMemiBoardI18n()\n const locale = computed<MemiBoardLocale>(() => normalizeMemiBoardLocale(toValue(options.locale)) ?? i18n.locale.value)\n const t: Translator = (key, params) => translate(locale.value, key, params)\n const app = useFirebaseApp()\n const db = useFirestore()\n const user = useCurrentUser()\n const config = useMemiBoardConfig()\n\n async function reserveDailyUse(): Promise<void> {\n const uid = user.value?.uid\n if (!uid) throw new Error(t('writingAssistant.loginRequired'))\n\n await runTransaction(db, async (transaction) => {\n const ref = boardUserDoc(db, useBoardPathConfig(), uid)\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists()) throw new Error(t('writingAssistant.userMissing'))\n\n const data = snapshot.data() as Record<string, unknown>\n const previousAt = timestampMs(data.aiWritingWindowAt)\n const previousCount = Math.max(0, Math.floor(Number(data.aiWritingCount) || 0))\n const now = Date.now()\n const activeWindow = previousAt != null && now - previousAt < WRITING_ASSISTANT_WINDOW_MS\n if (activeWindow && previousCount >= WRITING_ASSISTANT_DAILY_LIMIT) {\n const resetAt = previousAt + WRITING_ASSISTANT_WINDOW_MS\n const hours = Math.max(1, Math.ceil((resetAt - now) / (60 * 60 * 1000)))\n throw new Error(t('writingAssistant.dailyLimit', { limit: WRITING_ASSISTANT_DAILY_LIMIT, hours }))\n }\n\n transaction.update(ref, {\n aiWritingCount: activeWindow ? previousCount + 1 : 1,\n aiWritingWindowAt: activeWindow ? data.aiWritingWindowAt : serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n })\n }\n\n async function assist(input: WritingAssistantInput): Promise<string> {\n const lang = locale.value\n const ko = lang === 'ko'\n const instruction = resolveInstruction(input, lang, t)\n if (!input.content.trim()) throw new Error(t('writingAssistant.contentRequired'))\n await reserveDailyUse()\n const moderation = config.moderation ?? {}\n const ai = getAI(app, {\n backend: new GoogleAIBackend(),\n useLimitedUseAppCheckTokens: moderation.useLimitedUseAppCheckTokens !== false,\n })\n const model = getGenerativeModel(ai, {\n model: moderation.model ?? 'gemini-3.5-flash-lite',\n systemInstruction: (ko\n ? [\n '너는 게시판 글쓰기 편집 도우미다.',\n '사용자가 요청한 편집 결과만 출력하고 설명, 인사, 마크다운 코드 펜스를 붙이지 마라.',\n input.action === 'title' || input.selection\n ? '출력은 일반 텍스트로 작성하라.'\n : '입력의 HTML 구조와 링크, 이미지 태그를 보존하고 결과도 HTML 조각으로 출력하라.',\n ]\n : [\n 'You are an editing assistant for writing message board posts.',\n 'Output only the requested edit result. Do not add explanations, greetings, or markdown code fences.',\n input.action === 'title' || input.selection\n ? 'Write the output as plain text.'\n : 'Preserve the HTML structure, links, and image tags of the input, and output the result as an HTML fragment as well.',\n ]).join('\\n'),\n generationConfig: {\n temperature: input.action === 'continue' || input.action === 'title' || input.action === 'custom' ? 0.6 : 0.2,\n maxOutputTokens: 2048,\n },\n })\n const source = input.content.trim()\n const prompt = (ko\n ? [\n `작업: ${instruction}`,\n input.title ? `현재 제목: ${input.title}` : '',\n `본문:\\n${source.slice(0, 12000)}`,\n ]\n : [\n `Task: ${instruction}`,\n input.title ? `Current title: ${input.title}` : '',\n `Body:\\n${source.slice(0, 12000)}`,\n ]).filter(Boolean).join('\\n\\n')\n const result = await model.generateContent(prompt)\n const output = cleanModelOutput(result.response.text())\n if (!output) throw new Error(t('writingAssistant.emptyResult'))\n return output\n }\n\n return { assist }\n}\n","import { getAI, getGenerativeModel, GoogleAIBackend, Schema } from 'firebase/ai'\nimport { useFirebaseApp } from 'vuefire'\nimport { useMemiBoardConfig } from '../config'\nimport {\n DEFAULT_MEMI_BOARD_LOCALE,\n MEMI_BOARD_LOCALES,\n normalizeMemiBoardLocale,\n type MemiBoardLocale,\n} from '../i18n/locales'\nimport { memiBoardLanguageLabel } from '../utils/moderation-prompt'\n\n/** 번역할 항목 하나. fields 의 키(label, description 등)별로 번역 결과가 돌아온다. */\nexport interface MemiBoardTranslateEntry {\n id: string\n fields: Record<string, string>\n /** 짧은 이름의 뜻을 잡아 주는 참고 정보(링크 주소, 상위 메뉴 이름 등). 번역하지 않는다. */\n context?: string\n}\n\n/** 언어별 번역 결과. `{ en: { label: 'About' }, ja: { label: '…' } }` */\nexport type MemiBoardLocalizedFields = Partial<Record<MemiBoardLocale, Record<string, string>>>\n\nexport interface MemiBoardTranslateOptions {\n /** 원문 언어. 기본 'ko' */\n source?: MemiBoardLocale\n /** 번역할 언어. 기본: 원문을 뺀 지원 언어 전부 */\n targets?: MemiBoardLocale[]\n /** 무엇을 번역하는지 한 줄 설명(예: 'website navigation menu labels'). 문체를 맞추는 데 쓴다. */\n purpose?: string\n}\n\nconst REQUEST_TIMEOUT_MS = 30_000\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`translation timed out after ${ms / 1000}s`)), ms)\n promise.then(\n (value) => { clearTimeout(timer); resolve(value) },\n (error) => { clearTimeout(timer); reject(error) },\n )\n })\n}\n\n/**\n * 짧은 UI 문구(메뉴 이름, 게시판 이름 등)를 Firebase AI Logic 으로 여러 언어로 번역한다.\n * 자동으로 저장하지 않는다 — 호출한 쪽이 결과를 보여 주고 저장 여부를 정한다.\n *\n * 항목 하나당 요청 하나. 응답 스키마를 `{ [locale]: { [field]: string } }` 로 고정해\n * 출력 길이가 일정하고, 모델이 같은 줄을 반복하다 토큰 한도에 걸리는 일이 없다.\n */\nexport function useMemiBoardAutoTranslate() {\n const app = useFirebaseApp()\n const { moderation } = useMemiBoardConfig()\n\n function getModel(targets: MemiBoardLocale[], fields: string[]) {\n const ai = getAI(app, {\n backend: new GoogleAIBackend(),\n useLimitedUseAppCheckTokens: moderation.useLimitedUseAppCheckTokens !== false,\n })\n const fieldSchema = Schema.object({\n properties: Object.fromEntries(fields.map(field => [field, Schema.string()])),\n })\n return getGenerativeModel(ai, {\n model: moderation.model ?? 'gemini-3.5-flash-lite',\n systemInstruction: [\n 'You translate short user-interface strings for a website.',\n 'Return natural, concise UI wording a native speaker would expect in a menu or title — not literal word-for-word translations.',\n 'Keep proper nouns, brand names, product names and game titles as they are unless they have a well-known localized name.',\n 'Use the context only to understand meaning; never translate or output it.',\n ].join('\\n'),\n generationConfig: {\n temperature: 0.2,\n maxOutputTokens: 2048,\n responseMimeType: 'application/json',\n responseSchema: Schema.object({\n properties: Object.fromEntries(targets.map(code => [code, fieldSchema])),\n }),\n },\n })\n }\n\n async function translateOne(\n entry: MemiBoardTranslateEntry,\n source: MemiBoardLocale,\n targets: MemiBoardLocale[],\n purpose: string,\n ): Promise<MemiBoardLocalizedFields> {\n const fields = Object.keys(entry.fields)\n const prompt = [\n `Purpose: ${purpose}`,\n `Source language: ${memiBoardLanguageLabel(source)} (${source})`,\n `Translate into: ${targets.map(code => `${code} = ${memiBoardLanguageLabel(code)}`).join(', ')}`,\n entry.context ? `Context (do not translate): ${entry.context}` : '',\n 'Text (JSON):',\n JSON.stringify(entry.fields),\n ].filter(Boolean).join('\\n')\n\n const result = await withTimeout(getModel(targets, fields).generateContent(prompt), REQUEST_TIMEOUT_MS)\n const parsed = JSON.parse(result.response.text()) as Record<string, Record<string, unknown> | undefined>\n\n const out: MemiBoardLocalizedFields = {}\n for (const code of targets) {\n const row = parsed?.[code]\n if (!row) continue\n const picked: Record<string, string> = {}\n for (const field of fields) {\n const text = typeof row[field] === 'string' ? (row[field] as string).trim() : ''\n if (text) picked[field] = text\n }\n if (Object.keys(picked).length) out[code] = picked\n }\n return out\n }\n\n /**\n * entries 를 targets 언어로 번역해 `{ [id]: { [locale]: { [field]: text } } }` 로 돌려준다.\n * 빈 필드는 번역하지 않는다. 하나라도 실패하면 reject 한다(30초 제한).\n */\n async function translateEntries(\n entries: MemiBoardTranslateEntry[],\n options: MemiBoardTranslateOptions = {},\n ): Promise<Record<string, MemiBoardLocalizedFields>> {\n const source = options.source ?? DEFAULT_MEMI_BOARD_LOCALE\n const targets = (options.targets ?? MEMI_BOARD_LOCALES.map(item => item.code))\n .filter(code => code !== source)\n const purpose = options.purpose ?? 'website UI labels'\n\n const cleaned = entries\n .map(entry => ({\n ...entry,\n fields: Object.fromEntries(\n Object.entries(entry.fields).map(([key, value]) => [key, value.trim()]).filter(([, value]) => value),\n ),\n }))\n .filter(entry => Object.keys(entry.fields).length)\n if (!cleaned.length || !targets.length) return {}\n\n const results = await Promise.all(cleaned.map(entry => translateOne(entry, source, targets, purpose)))\n return Object.fromEntries(cleaned.map((entry, index) => [entry.id, results[index]!]))\n }\n\n return { translateEntries }\n}\n\n/**\n * 저장된 번역에서 현재 언어 값을 고른다. 현재 언어 → en → 원문 순.\n * `localizedText(item.i18n, locale, 'label', item.label)`\n */\nexport function localizedText(\n i18n: MemiBoardLocalizedFields | null | undefined,\n locale: string | null | undefined,\n field: string,\n fallback: string,\n): string {\n const code = normalizeMemiBoardLocale(locale)\n if (!code || code === DEFAULT_MEMI_BOARD_LOCALE) return fallback\n return i18n?.[code]?.[field] || i18n?.en?.[field] || fallback\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"],"mappings":"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AgB,MAAO,EAAQ,KAAK,CAAI;GAC3D;GACA,OAAO;EACT,CACS;EAAO;EAAS;CAAM;AACjC;;;AClEA,IAAa,KAAwC;CACnD;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wEACA,8CACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,0DACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,yDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,0DACA,0CACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,8CACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,yCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,+BACA,4BACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,qCACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,iDACA,8BACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,oCACA,oBACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,kCACA,8BACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,6BACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,0BACA,wBACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,yCACA,iCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,4DACA,4BACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,mCACA,uCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,mDACA,wCACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,+DACA,kDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,kEACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;GACA;EACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,oCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,gDACA,qEACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,qEACA,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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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/utils/boardSeo.ts","../src/composables/fetchPublicSeo.ts","../src/utils/boardAccess.ts","../src/composables/boardLookup.ts","../src/utils/moderation-strike.ts","../src/composables/useMemiBoardAuth.ts","../src/utils/imeSafeSubmit.ts","../src/utils/findReadablePost.ts","../src/utils/extractEditorImageUrls.ts","../src/utils/storagePath.ts","../src/utils/deletePostCascade.ts","../src/utils/postBody.ts","../src/utils/postPreview.ts","../src/utils/saveBoardVisibility.ts","../src/composables/useMemiBoardSettings.ts","../src/composables/useMemiBoardPosts.ts","../src/composables/useMemiBoardUserPosts.ts","../src/composables/useMemiBoardUserComments.ts","../src/composables/useMemiBoardCommentFeed.ts","../src/composables/useMemiBoardAuthorMemo.ts","../src/utils/subscribeCommentRange.ts","../src/composables/useMemiBoardComments.ts","../src/composables/useMemiBoardLikes.ts","../src/composables/useMemiBoardReports.ts","../src/composables/useMemiBoardViews.ts","../src/utils/moderation-prompt.ts","../src/composables/useMemiBoardModeration.ts","../src/composables/useMemiBoardUsers.ts","../src/composables/useMemiBoardUserProfile.ts","../src/utils/section.ts","../src/composables/useMemiBoardSections.ts","../src/composables/useMemiBoardMiniPosts.ts","../src/data/versionHistory.ts","../src/composables/useMemiBoardWritingAssistant.ts","../src/composables/useMemiBoardAutoTranslate.ts","../src/utils/renderMarkdown.ts"],"sourcesContent":["/** 게시판 SEO 순수 헬퍼 — Nuxt 의존 없음 */\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\n\n/** '전체' 라벨(현재 언어 또는 예전 한국어 값)이면 전체 목록으로 본다. */\nfunction isAllCategoryLabel(label: string | undefined, locale?: MemiBoardLocale): boolean {\n if (!label) return true\n return label === '전체' || label === translate(locale, 'seo.allCategory')\n}\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 locale?: MemiBoardLocale,\n): string {\n const fromSummary = summary?.trim().slice(0, 40)\n const t = title.trim() || fromSummary || translate(locale, 'seo.photo')\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}, locale?: MemiBoardLocale): string {\n const bits: string[] = []\n if (opts.summary?.trim()) bits.push(opts.summary.trim().slice(0, 140))\n else bits.push(translate(locale, 'seo.postDescriptionFallback'))\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(\n categoryLabel: string | null | undefined,\n siteName: string,\n locale?: MemiBoardLocale,\n): string {\n const site = siteName.trim() || 'Board'\n const cat = categoryLabel?.trim()\n if (!isAllCategoryLabel(cat, locale)) return translate(locale, 'seo.listTitle', { name: cat, site })\n return translate(locale, 'seo.listTitleAll', { site })\n}\n\nexport function boardListOgDescription(opts: {\n categoryLabel?: string | null\n description?: string\n recentTitles?: string[]\n}, locale?: MemiBoardLocale): string {\n if (opts.description?.trim()) return opts.description.trim().slice(0, 200)\n const cat = opts.categoryLabel?.trim()\n const head = !isAllCategoryLabel(cat, locale)\n ? translate(locale, 'seo.listDescription', { name: cat })\n : translate(locale, 'seo.listDescriptionAll')\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'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { loadMemiBoardLocale, translate } from '../i18n/translate'\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 /** 기본 문구(설명·'전체' 라벨) 언어. 생략 시 한국어. */\n locale?: MemiBoardLocale,\n): Promise<BoardListSeoPayload> {\n if (locale) await loadMemiBoardLocale(locale)\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 || translate(locale, 'seo.boardDescription', { name: meta.label }),\n ogImage,\n recentTitles,\n }\n }\n\n return {\n kind: 'all',\n category: null,\n categoryLabel: translate(locale, 'seo.allCategory'),\n description: translate(locale, 'seo.allDescription'),\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","import type { BoardVisibility, BoardWriteRole } from '../types'\n\n/** 스태프가 이 보드 담당인지. 목록에 있는 uid만. 비어 있으면 담당 없음(관리자만). */\nexport function isAssignedStaff(\n uid: string | undefined,\n board: { allowedStaffUids?: string[]; allowedManagerUids?: string[] } | null | undefined,\n): boolean {\n if (!uid || !board) return false\n return [...(board.allowedStaffUids ?? []), ...(board.allowedManagerUids ?? [])].includes(uid)\n}\n\nexport function canManageBoardByRole(\n uid: string | undefined,\n isAdmin: boolean,\n isStaff: boolean,\n board: { allowedStaffUids?: string[]; allowedManagerUids?: string[] } | null | undefined,\n): boolean {\n if (!uid) return false\n if (isAdmin) return true\n if (!isStaff) return false\n return isAssignedStaff(uid, board)\n}\n\n/**\n * 댓글 작성 권한. 숨김 보드는 commentWriteRole 과 무관하게 관리자·담당 스태프만.\n * board 가 아직 없으면 공개 보드(일반 이상)와 같이 취급한다.\n */\nexport function canWriteCommentByRole(\n uid: string | undefined,\n isAdmin: boolean,\n isStaff: boolean,\n board: {\n visibility?: BoardVisibility\n commentWriteRole?: BoardWriteRole\n allowedStaffUids?: string[]; allowedManagerUids?: string[]\n } | null | undefined,\n): boolean {\n if (!uid) return false\n if (board?.visibility === 'hidden') {\n return canManageBoardByRole(uid, isAdmin, isStaff, board)\n }\n const required = board?.commentWriteRole ?? 'user'\n if (required === 'admin') return isAdmin\n if (required === 'staff') return canManageBoardByRole(uid, isAdmin, isStaff, board)\n return true\n}","type BoardLookup = (boardId: string) => { allowedStaffUids?: string[]; allowedManagerUids?: string[] } | undefined\n\nlet lookup: BoardLookup | undefined\n\nexport function registerBoardLookup(fn: BoardLookup) {\n lookup = fn\n}\n\nexport function resolveBoardLookup(boardId: string) {\n return lookup?.(boardId)\n}","import type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\n\n/**\n * 게시판 검열 차단 누적 → 일시 이용 제한 (호스트 contentWarning 과 동일 모델).\n * - 콘텐츠 차단(로컬/AI flagged) 시 +1\n * - 유효 경고 ≥ threshold(기본 10) → 글/댓글 작성 불가\n * - 24h마다 lazy 차감 (moderationBlockAt 기준)\n */\n\nexport const DEFAULT_BLOCK_BAN_THRESHOLD = 10\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(\n untilMs: number,\n nowMs: number = Date.now(),\n locale?: MemiBoardLocale,\n): string {\n const left = Math.max(0, untilMs - nowMs)\n const hours = Math.ceil(left / (60 * 60 * 1000))\n if (hours <= 0) return translate(locale, 'moderationStrike.liftSoon')\n if (hours < 24) return translate(locale, 'moderationStrike.liftInHours', { hours })\n const days = Math.floor(hours / 24)\n const remH = hours % 24\n if (remH === 0) return translate(locale, 'moderationStrike.liftInDays', { days })\n return translate(locale, 'moderationStrike.liftInDaysHours', { days, hours: 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 { canManageBoardByRole } from '../utils/boardAccess'\nimport { resolveBoardLookup } from './boardLookup'\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'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport interface UseMemiBoardAuthReturn {\n user: ReturnType<typeof useCurrentUser>\n isSignedIn: ComputedRef<boolean>\n role: Ref<BoardUserRole>\n isOwner: ComputedRef<boolean>\n isManager: ComputedRef<boolean>\n canConfigureBoard: (boardId: string | undefined | null) => boolean\n isAdmin: ComputedRef<boolean>\n isStaff: ComputedRef<boolean>\n canManageContent: ComputedRef<boolean>\n canManageBoard: (boardId: string | undefined | null) => 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' | 'boardId'>) => boolean\n canDelete: (post: Pick<PostModel, 'authorUid' | 'boardId'>) => boolean\n canEditComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean\n canDeleteComment: (comment: Pick<CommentModel, 'authorUid' | 'boardId'>) => 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 { t, locale } = useMemiBoardI18n()\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 role.value = ['owner', 'admin', 'manager', 'staff', 'user'].includes(String(data?.role)) ? data?.role as BoardUserRole : 'user'\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 const hasName = typeof data.displayName === 'string' && data.displayName.trim()\n const hasPhoto = typeof data.photoURL === 'string' && data.photoURL.trim()\n const visit: Record<string, unknown> = {\n lastVisitAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n }\n // 이미 있는 이름·사진은 프로필이 주인. Auth(구글/애플) 값으로 되돌리지 않는다.\n if (!hasName && firebaseUser.displayName) visit.displayName = firebaseUser.displayName\n if (!hasPhoto && firebaseUser.photoURL) visit.photoURL = firebaseUser.photoURL\n await Promise.all([\n updateDoc(ref, visit),\n setDoc(privateRef, { email: firebaseUser.email }, { merge: true }),\n ])\n return (['owner', 'admin', 'manager', '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 isOwner = computed(() => role.value === 'owner')\n const isManager = computed(() => role.value === 'manager')\n const isAdmin = computed(() => isOwner.value || role.value === 'admin')\n const isStaff = computed(() => isManager.value || 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, Date.now(), locale.value) : null\n })\n\n const restrictedMessage = computed(() => {\n if (!isWriteRestricted.value) return null\n const until = restrictedUntilLabel.value\n return until\n ? t('auth.writeRestrictedUntil', { until })\n : t('auth.writeRestricted')\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 = t('auth.strikeSuffix', { count: 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, locale.value) : null\n messageSuffix = untilLabel\n ? t('auth.strikeRestrictedUntil', { count: effectiveCount, until: untilLabel })\n : t('auth.strikeRestricted', { count: effectiveCount })\n }\n return { effectiveCount, restricted, messageSuffix }\n }\n\n function requireAuth() {\n if (!auth) throw new Error(t('auth.notInitialized'))\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 canManageBoard(boardId: string | undefined | null) {\n return canManageBoardByRole(\n user.value?.uid,\n isAdmin.value,\n isStaff.value,\n boardId ? resolveBoardLookup(boardId) : undefined,\n )\n }\n\n function canConfigureBoard(boardId: string | undefined | null) {\n if (!user.value) return false\n if (isAdmin.value) return true\n return isManager.value && !!boardId\n && (resolveBoardLookup(boardId)?.allowedManagerUids ?? []).includes(user.value.uid)\n }\n\n function canEdit(post: Pick<PostModel, 'authorUid' | 'boardId'>) {\n return isSignedIn.value && (user.value?.uid === post.authorUid || canManageBoard(post.boardId))\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' | 'boardId'>) {\n return isSignedIn.value && (user.value?.uid === comment.authorUid || canManageBoard(comment.boardId))\n }\n\n return {\n user,\n role,\n isOwner,\n isManager,\n canConfigureBoard,\n isSignedIn,\n isAdmin,\n isStaff,\n canManageContent,\n canManageBoard,\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 * macOS Safari: 한글 조합 중(마지막 글자 미확정)에 제출 버튼을 누르면 첫 click 이 조합 확정에 먹혀\n * 두 번 눌러야 넘어간다. 마우스는 pointerdown 에서 조합을 확정(blur)하고 바로 제출, 뒤따르는 click 은 무시.\n * 터치는 스크롤과 구분해야 하므로 기존 click 그대로.\n *\n * 사용: `<UButton type=\"submit\" @pointerdown=\"imeSafeSubmitPointerDown\" @click=\"imeSafeSubmitClick\" />`\n * 버튼이 속한 form 을 requestSubmit 하므로 form 의 `@submit.prevent` 핸들러가 그대로 불린다.\n */\nconst suppressClickUntil = new WeakMap<HTMLButtonElement, number>()\n\nfunction submitButtonOf(e: Event): HTMLButtonElement | null {\n const target = e.currentTarget\n if (target instanceof HTMLButtonElement) return target\n return target instanceof Element ? target.closest('button') : null\n}\n\nexport function imeSafeSubmitPointerDown(e: PointerEvent): void {\n if (e.pointerType !== 'mouse' || e.button !== 0) return\n const button = submitButtonOf(e)\n const form = button?.form\n if (!button || !form || button.disabled) return\n suppressClickUntil.set(button, Date.now() + 1000)\n const active = document.activeElement\n if (active instanceof HTMLElement) active.blur()\n // blur 로 확정된 마지막 글자가 v-model 에 반영된 뒤 제출\n setTimeout(() => form.requestSubmit(button), 0)\n}\n\nexport function imeSafeSubmitClick(e: MouseEvent): void {\n const button = submitButtonOf(e)\n if (!button) return\n const until = suppressClickUntil.get(button)\n if (until && Date.now() < until) {\n e.preventDefault()\n suppressClickUntil.delete(button)\n }\n}\n","import { getDocs, limit, query, where, type Firestore } from 'firebase/firestore'\nimport { postsCol, type BoardPathConfig } from './boardPaths'\n\n/** Query only documents the caller can read; rules cannot filter private drafts. */\nexport async function findReadablePostBySlug(\n db: Firestore,\n cfg: BoardPathConfig,\n boardId: string,\n slug: string,\n uid: string | undefined,\n canManage: boolean,\n) {\n const constraints = [where('boardId', '==', boardId), where('slug', '==', slug)]\n if (canManage) {\n return (await getDocs(query(postsCol(db, cfg), ...constraints, limit(1)))).docs[0]\n }\n const snapshots = await Promise.all([\n getDocs(query(postsCol(db, cfg), ...constraints,\n where('listed', '==', true), where('isPublished', '==', true), limit(1))),\n ...(uid ? [getDocs(query(postsCol(db, cfg), ...constraints,\n where('authorUid', '==', uid), limit(1)))] : []),\n ])\n return snapshots.flatMap(snapshot => snapshot.docs)[0]\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 parsed = new URL(url)\n const host = parsed.hostname.toLowerCase()\n const isFirebaseStorage = host === 'firebasestorage.googleapis.com'\n || host.endsWith('.firebasestorage.app')\n if (!isFirebaseStorage) return null\n const m = parsed.pathname.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","import {\n deleteDoc,\n getDoc,\n getDocs,\n query,\n where,\n writeBatch,\n type Firestore,\n type Query,\n} from 'firebase/firestore'\nimport {\n deleteObject,\n listAll,\n ref as storageRef,\n type FirebaseStorage,\n type StorageReference,\n} from 'firebase/storage'\nimport type { BoardPathConfig } from './boardPaths'\nimport {\n commentsCol,\n likesCol,\n postBodyDoc,\n postDoc,\n postStorageFolder,\n} from './boardPaths'\nimport { extractEditorImageUrls } from './extractEditorImageUrls'\nimport { postNamespaceFromStoragePath, storagePathFromDownloadUrl } from './storagePath'\nimport type { PostModel } from '../types'\n\nasync function deleteStorageFolder(folderRef: StorageReference): Promise<void> {\n try {\n const list = await listAll(folderRef)\n await Promise.all([\n ...list.items.map(item => deleteObject(item).catch(() => {})),\n ...list.prefixes.map(prefix => deleteStorageFolder(prefix)),\n ])\n }\n catch {\n // 이 경로에 list 규칙이 없으면 건너뛴다. 호출 쪽에서 하위 폴더를 따로 비운다.\n }\n}\n\n/** 규칙이 매칭하는 images/·attachments/ 만 비운다. 부모 폴더 listAll 은 호스트 규칙에서 403 이 난다. */\nasync function deletePostStorageTree(storage: FirebaseStorage, folder: string): Promise<void> {\n await deleteStorageFolder(storageRef(storage, `${folder}/images`))\n await deleteStorageFolder(storageRef(storage, `${folder}/attachments`))\n}\n\nasync function deleteStoragePaths(storage: FirebaseStorage, paths: Iterable<string>): Promise<void> {\n const unique = [...new Set([...paths].filter(Boolean))]\n await Promise.all(unique.map(path => deleteObject(storageRef(storage, path)).catch(() => {})))\n}\n\nconst DELETE_BATCH_SIZE = 450\n\nasync function deleteQueryDocs(db: Firestore, docsQuery: Query): Promise<void> {\n const snapshot = await getDocs(docsQuery)\n for (let offset = 0; offset < snapshot.docs.length; offset += DELETE_BATCH_SIZE) {\n const batch = writeBatch(db)\n snapshot.docs.slice(offset, offset + DELETE_BATCH_SIZE).forEach(item => batch.delete(item.ref))\n await batch.commit()\n }\n}\n\n/** 글에 달린 본문·댓글 좋아요를 모두 지운다. 둘 다 postId 필드를 가진다. */\nexport async function deleteLikesForPost(\n db: Firestore,\n cfg: BoardPathConfig,\n postId: string,\n): Promise<void> {\n await deleteQueryDocs(db, query(likesCol(db, cfg), where('postId', '==', postId)))\n}\n\n/** 댓글 하나에 달린 좋아요만 지운다. */\nexport async function deleteLikesForComment(\n db: Firestore,\n cfg: BoardPathConfig,\n commentId: string,\n): Promise<void> {\n await deleteQueryDocs(db, query(likesCol(db, cfg), where('commentId', '==', commentId), where('target', '==', 'comment')))\n}\n\n/** 글 하나의 댓글(답글 포함), 좋아요, 본문, 첨부 Storage, 메타를 순서대로 삭제한다. */\nexport async function deletePostCascade(\n db: Firestore,\n storage: FirebaseStorage,\n cfg: BoardPathConfig,\n id: string,\n): Promise<void> {\n const metaRef = postDoc(db, cfg, id)\n const bodyRef = postBodyDoc(db, cfg, id)\n const [metaSnap, bodySnap] = await Promise.all([getDoc(metaRef), getDoc(bodyRef)])\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 // 답글도 같은 flat 컬렉션에서 같은 postId를 가지므로 함께 삭제된다.\n await deleteQueryDocs(db, query(commentsCol(db, cfg), where('postId', '==', id)))\n await deleteLikesForPost(db, cfg, id)\n // Reports are moderation/audit records. Keep them after the post is deleted.\n\n const extraPaths = new Set<string>()\n const extraNamespaces = new Set<string>()\n for (const attachment of meta.attachments ?? []) {\n if (!attachment?.path) continue\n extraPaths.add(attachment.path)\n const namespace = postNamespaceFromStoragePath(attachment.path)\n if (namespace && namespace !== id) extraNamespaces.add(namespace)\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/')) extraPaths.add(thumbGuess)\n const match = path.match(/^(.*\\/images\\/)([^/]+)\\.[^.]+$/)\n if (match && !path.includes('/thumbnails/')) extraPaths.add(`${match[1]}thumbnails/${match[2]}.jpg`)\n const namespace = postNamespaceFromStoragePath(path)\n if (namespace && namespace !== id) extraNamespaces.add(namespace)\n }\n\n await deleteDoc(bodyRef)\n await deletePostStorageTree(storage, postStorageFolder(cfg, id))\n for (const namespace of extraNamespaces) {\n await deletePostStorageTree(storage, postStorageFolder(cfg, namespace))\n }\n await deleteStoragePaths(storage, extraPaths)\n await deleteDoc(metaRef)\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 {\n getDocs,\n limit as fbLimit,\n orderBy,\n query,\n runTransaction,\n serverTimestamp,\n startAfter,\n where,\n writeBatch,\n type Firestore,\n type QueryDocumentSnapshot,\n type DocumentData,\n} from 'firebase/firestore'\nimport { commentsCol, postsCol, settingsDoc, type BoardPathConfig } from './boardPaths'\n\nfunction normalizeVisibility(value: unknown) { return value === 'hidden' ? 'hidden' : 'public' }\n\n/** 숨김 전환 시 해당 보드 글·댓글 listed 일괄 맞춤. 값이 이미 같으면 건너뛴다. */\nasync function syncListedForCol(\n db: Firestore,\n col: ReturnType<typeof postsCol>,\n boardId: string,\n listed: boolean,\n): Promise<void> {\n let cursor: QueryDocumentSnapshot | undefined\n for (;;) {\n const page = await getDocs(query(\n col,\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 let writes = 0\n for (const item of page.docs) {\n if (item.data().listed === listed) continue\n batch.update(item.ref, { listed })\n writes++\n }\n if (writes) await batch.commit()\n cursor = page.docs[page.docs.length - 1]\n if (page.docs.length < 400) break\n }\n}\n\nasync function syncListedForBoard(db: Firestore, cfg: BoardPathConfig, boardId: string, listed: boolean): Promise<void> {\n await syncListedForCol(db, postsCol(db, cfg), boardId, listed)\n await syncListedForCol(db, commentsCol(db, cfg), boardId, listed)\n}\n\nexport async function saveBoardWithVisibility(db: Firestore, cfg: BoardPathConfig, id: string, payload: DocumentData): Promise<void> {\n const ref = settingsDoc(db, cfg, id)\n const visibility = normalizeVisibility(payload.visibility)\n // Lock creation before paging through content. A failed transition remains\n // explicit and can be resumed by saving the same visibility again.\n const transition = await runTransaction(db, async (transaction) => {\n const snapshot = await transaction.get(ref)\n const previous = snapshot.data()\n const pending = previous?.visibilityTransition || ''\n if (pending && pending !== visibility) {\n throw new Error('Finish the pending visibility change before changing it again.')\n }\n if (!snapshot.exists() || (!pending && normalizeVisibility(previous?.visibility) === visibility)) {\n transaction.set(ref, payload, { merge: true })\n return false\n }\n // Unhiding is authorized before publishing any content. Hiding is only\n // marked complete AFTER all posts and comments have been hidden.\n transaction.set(ref, {\n ...payload,\n visibility: visibility === 'public' ? 'public' : normalizeVisibility(previous?.visibility),\n visibilityTransition: visibility,\n }, { merge: true })\n return true\n })\n if (!transition) return\n await syncListedForBoard(db, cfg, id, visibility !== 'hidden')\n await runTransaction(db, async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (snapshot.data()?.visibilityTransition !== visibility) {\n throw new Error('The visibility transition changed. Reload and retry.')\n }\n transaction.update(ref, { visibility, visibilityTransition: '', updatedAt: serverTimestamp() })\n })\n}\n","import { computed } from 'vue'\nimport { useCollection, useFirebaseApp, useFirestore } from 'vuefire'\nimport { getStorage } from 'firebase/storage'\nimport {\n deleteDoc,\n getDocs,\n limit as fbLimit,\n orderBy,\n query,\n serverTimestamp,\n where,\n} 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 { registerBoardLookup } from './boardLookup'\nimport { canManageBoardByRole } from '../utils/boardAccess'\nimport { slugify } from '../utils/slugify'\nimport { deletePostCascade } from '../utils/deletePostCascade'\nimport { saveBoardWithVisibility } from '../utils/saveBoardVisibility'\nimport { normalizeMemiBoardLocale } from '../i18n/locales'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\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 visibilityTransition: data.visibilityTransition === 'hidden' || data.visibilityTransition === 'public'\n ? data.visibilityTransition : undefined,\n listView: (data.listView as BoardModel['listView']) ?? 'default',\n editorType: (data.editorType as BoardModel['editorType'])\n ?? (data.listView === 'image' ? 'image' : '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 allowedManagerUids: Array.isArray(data.allowedManagerUids) ? data.allowedManagerUids as string[] : [],\n locale: normalizeMemiBoardLocale(data.locale) ?? '',\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 app = useFirebaseApp()\n const { isSignedIn, isAdmin, isStaff, user } = useMemiBoardAuth()\n const { t } = useMemiBoardI18n()\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 registerBoardLookup(id => getBoard(id))\n\n function canManageBoard(boardId: string | undefined | null) {\n return canManageBoardByRole(user.value?.uid, isAdmin.value, isStaff.value, boardId ? getBoard(boardId) : undefined)\n }\n\n const managedBoards = computed(() => boards.value.filter(board => canManageBoard(board.id)))\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 editorType: board.editorType ?? (board.listView === 'image' ? 'image' : 'default'),\n writeRole: board.writeRole ?? 'user',\n commentWriteRole: board.commentWriteRole ?? 'user',\n allowedStaffUids: board.allowedStaffUids ?? [],\n allowedManagerUids: board.allowedManagerUids ?? [],\n locale: normalizeMemiBoardLocale(board.locale) ?? '',\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(t('boardSettings.signInRequired'))\n const id = board.id.trim()\n const label = board.label.trim()\n if (!id || !label) throw new Error(t('boardSettings.idAndNameRequired'))\n const visibility = normalizeVisibility(board.visibility)\n const payload = settingsPayload({ ...board, label, visibility }, order)\n\n await saveBoardWithVisibility(db, cfg(), id, payload)\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(t('boardSettings.signInRequired'))\n // Validate the whole input before starting any writes.\n for (const board of next) {\n if (!board.id.trim() || !board.label.trim()) throw new Error(t('boardSettings.idAndNameRequired'))\n }\n for (const [order, board] of next.entries()) await saveBoard(board, order)\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(t('boardSettings.signInRequired'))\n const boardId = id.trim()\n if (!boardId) throw new Error(t('boardSettings.deleteIdRequired'))\n\n // 설정은 마지막에 삭제한다. 중간 실패 시 게시판을 남겨 두어 관리자가 재시도할 수 있다.\n for (;;) {\n const page = await getDocs(query(\n postsCol(db, cfg()),\n where('boardId', '==', boardId),\n fbLimit(25),\n ))\n if (page.empty) break\n for (const post of page.docs) {\n await deletePostCascade(db, getStorage(app), cfg(), post.id)\n }\n }\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(t('boardSettings.nameRequired'))\n if (!isSignedIn.value) throw new Error(t('boardSettings.signInRequired'))\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 editorType: '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 canManageBoard,\n managedBoards,\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 writeBatch,\n query,\n where,\n orderBy,\n limit as fbLimit,\n limitToLast,\n startAfter,\n endBefore,\n serverTimestamp,\n deleteField,\n} from 'firebase/firestore'\nimport type { QueryConstraint, Unsubscribe } from 'firebase/firestore'\nimport type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore'\nimport { getStorage } from 'firebase/storage'\nimport { useDocument, useFirebaseApp, useFirestore } from 'vuefire'\nimport { slugify } from '../utils/slugify'\nimport { findReadablePostBySlug } from '../utils/findReadablePost'\nimport { deletePostCascade } from '../utils/deletePostCascade'\nimport { hasBodyImage, hasBodyText, titleFromBody } from '../utils/postBody'\nimport { useBoardPathConfig } from '../config'\nimport { buildPostPreview } from '../utils/postPreview'\nimport {\n postBodyDoc,\n postDoc,\n postsCol,\n} from '../utils/boardPaths'\nimport { useMemiBoardSettings } from './useMemiBoardSettings'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\nimport type { Attachment, PostDetail, PostModel } from '../types'\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 /** resolveUniqueSlug 로 미리 구한 slug. 없으면 createPost 가 직접 구한다. */\n slug?: 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 type ResolvePostIdResult =\n | { status: 'ok', id: string }\n | { status: 'not-found' }\n | { status: 'permission-denied' }\n | { status: 'error', message: string }\n\n/**\n * 방금 만든 글의 `boardId/slug` → id.\n * 저장 직후 상세(미리보기)로 갈 때 slug 재조회 왕복을 건너뛴다. slug 는 바뀌지 않는다.\n */\nconst createdPostIds = new Map<string, 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>, locale?: MemiBoardLocale): string {\n const id = toValue(boardId)?.trim()\n if (!id) throw new Error(translate(locale, 'posts.boardIdRequired'))\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 { user, canManageBoard } = useMemiBoardAuth()\n const { t, locale } = useMemiBoardI18n()\n const cfg = () => useBoardPathConfig()\n const bid = () => resolveBoardId(boardId, locale.value)\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\n function listedForBoard(): boolean {\n return !isBoardHidden(bid())\n }\n\n function isImageEditorBoard(): boolean {\n return getBoard(bid())?.editorType === 'image'\n }\n\n function assertPostContent(\n input: { title?: string, content: string, attachments?: Attachment[] },\n ) {\n const imageBoard = isImageEditorBoard()\n const hasText = hasBodyText(input.content)\n const hasImage = hasBodyImage(input.content, input.attachments)\n if (imageBoard) {\n if (!hasImage) {\n throw new Error(t('posts.photoRequired'))\n }\n return\n }\n if (!hasText && !hasImage) {\n throw new Error(t('posts.bodyRequired'))\n }\n if (!input.title?.trim()) {\n throw new Error(t('posts.titleRequired'))\n }\n }\n\n function createPostId(): string {\n return doc(postsColRef()).id\n }\n\n function findBySlug(slug: string) {\n return findReadablePostBySlug(db, cfg(), bid(), slug, user.value?.uid, canManageBoard(bid()))\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 meta = await findBySlug(slug.trim())\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 /**\n * slug → 글 id 만. 상세·수정 페이지는 id 만 필요하므로 본문까지 읽는 getPostBySlug 보다 왕복이 적다.\n * 이 탭에서 방금 만든 글이면 조회 없이 바로 돌려준다.\n */\n async function resolvePostIdBySlug(slug: string): Promise<ResolvePostIdResult> {\n const s = slug.trim()\n const cached = createdPostIds.get(`${bid()}/${s}`)\n if (cached) return { status: 'ok', id: cached }\n try {\n const row = await findBySlug(s)\n return row ? { status: 'ok', id: row.id }\n : { status: isBoardHidden(bid()) ? 'permission-denied' : 'not-found' }\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 /** Auto-ID suffix also prevents collisions with unreadable drafts and concurrent writers. */\n async function resolveUniqueSlug(title: string): Promise<string> {\n const baseSlug = slugify(title.trim()) || 'post'\n return `${baseSlug}-${createPostId()}`\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 = isImageEditorBoard()\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 slug = input.slug?.trim() || await resolveUniqueSlug(title)\n }\n\n const preview = buildPostPreview(input.content, input.attachments)\n const listed = listedForBoard()\n // 메타·본문을 한 번에 커밋 — 순차 setDoc 두 번보다 왕복 1회 적다.\n const batch = writeBatch(db)\n batch.set(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 batch.set(bodyDocRef(id), { content: input.content })\n await batch.commit()\n createdPostIds.set(`${bid()}/${slug}`, id)\n return slug\n }\n\n async function updatePost(id: string, input: UpdatePostInput): Promise<void> {\n assertPostContent(input)\n const imageBoard = isImageEditorBoard()\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 await deletePostCascade(db, getStorage(app), cfg(), id)\n for (const [key, value] of createdPostIds) {\n if (value === id) createdPostIds.delete(key)\n }\n }\n\n return {\n createPostId,\n getPosts,\n getPost,\n getPostBySlug,\n resolvePostIdBySlug,\n resolveUniqueSlug,\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, user, canManageBoard } = 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 || canManageBoard(boardIdValue.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로 모든 보드를 가로질러 공개(listed) 댓글만 모은다.\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 [\n where('authorUid', '==', uidValue.value),\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 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, 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 COMMENT_FEED_PAGE_SIZE = 10\n\nexport type CommentFeedSort = 'latest' | 'likes'\n\n/**\n * 보드를 가로질러 공개(listed) 댓글만 모은다. 숨김 보드 댓글은 제외.\n * 최신순은 createdAt, 좋아요순은 likeCount.\n * likeCount 가 없는 예전 댓글은 좋아요순 쿼리에 나오지 않는다(표시 시 0).\n */\nexport function useMemiBoardCommentFeed(\n sort: MaybeRefOrGetter<CommentFeedSort> = 'latest',\n options: { pageSize?: MaybeRefOrGetter<number> } = {},\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const commentsColRef = () => commentsCol(db, cfg())\n const pageSize = computed(() => {\n const n = Math.floor(Number(toValue(options.pageSize)))\n return Number.isFinite(n) && n > 0 ? n : COMMENT_FEED_PAGE_SIZE\n })\n const sortValue = computed<CommentFeedSort>(() => {\n const value = toValue(sort)\n return value === 'likes' ? 'likes' : 'latest'\n })\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 function compare(a: CommentModel, b: CommentModel): number {\n if (sortValue.value === 'likes') {\n const likes = (b.likeCount ?? 0) - (a.likeCount ?? 0)\n if (likes) return likes\n }\n return createdAtMs(b) - createdAtMs(a) || String(b.id).localeCompare(String(a.id))\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(compare)\n })\n\n function orderConstraints(): QueryConstraint[] {\n if (sortValue.value === 'likes') {\n return [\n where('listed', '==', true),\n orderBy('likeCount', 'desc'),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ]\n }\n return [\n where('listed', '==', true),\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n ]\n }\n\n function startHeadSubscription() {\n stopHeadSubscription?.()\n stopHeadSubscription = onSnapshot(query(\n commentsColRef(),\n ...orderConstraints(),\n fbLimit(pageSize.value),\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.value\n headPending.value = false\n }, (cause) => {\n console.error('[memi-board:commentFeed] 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) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const cursor = olderCursor ?? headTailCursor\n const snapshot = await getDocs(query(\n commentsColRef(),\n ...orderConstraints(),\n ...(cursor ? [startAfter(cursor)] : []),\n fbLimit(pageSize.value + 1),\n ))\n const pageDocs = snapshot.docs.slice(0, pageSize.value)\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.value\n }\n catch (e) {\n loadError.value = (e as Error).message || String(e)\n console.error('[memi-board:commentFeed] 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([sortValue, pageSize], reset, { immediate: true })\n onScopeDispose(() => stopHeadSubscription?.())\n\n return {\n comments,\n commentsPending: headPending,\n hasMore,\n loadingMore,\n loadError,\n loadMore,\n sort: sortValue,\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 { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\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 const { t, formatNumber } = useMemiBoardI18n()\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(t('authorMemo.tooLong', { max: formatNumber(AUTHOR_MEMO_MAX_LENGTH) }))\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 {\n documentId, endAt, onSnapshot, orderBy, query, where,\n type Firestore, type QueryDocumentSnapshot,\n} from 'firebase/firestore'\nimport { commentsCol, type BoardPathConfig } from './boardPaths'\nimport type { CommentModel } from '../types'\n\n/** Keep the loaded range live; after the final page, include newly added rows. */\nexport function subscribeCommentRange(\n db: Firestore,\n cfg: BoardPathConfig,\n postId: string,\n options: { rootId?: string, cursor: QueryDocumentSnapshot | null, hasMore: boolean },\n receive: (comments: CommentModel[]) => void,\n onError: (cause: Error) => void,\n) {\n return onSnapshot(query(\n commentsCol(db, cfg),\n where('postId', '==', postId),\n ...(options.rootId\n ? [where('rootId', '==', options.rootId), where('isReply', '==', true)]\n : [where('parentId', '==', null)]),\n orderBy('createdAt', 'asc'),\n orderBy(documentId(), 'asc'),\n ...(options.hasMore && options.cursor ? [endAt(options.cursor)] : []),\n ), snapshot => receive(snapshot.docs.map(item => ({ ...item.data(), id: item.id }) as CommentModel)), onError)\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 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 { deleteLikesForComment } from '../utils/deletePostCascade'\nimport { subscribeCommentRange } from '../utils/subscribeCommentRange'\nimport { useMemiBoardSettings } from './useMemiBoardSettings'\nimport type { CommentModel } from '../types'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\nimport type { MemiBoardI18n } from '../i18n/useMemiBoardI18n'\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, { t, formatNumber }: Pick<MemiBoardI18n, 't' | 'formatNumber'>): string {\n const trimmed = body.trim()\n if (!trimmed) throw new Error(t('comments.bodyRequired'))\n if (trimmed.length > COMMENT_BODY_MAX_LENGTH) {\n throw new Error(t('comments.bodyTooLong', { max: formatNumber(COMMENT_BODY_MAX_LENGTH) }))\n }\n return trimmed\n}\n\n/** 10개씩 순차 조회하고 불러온 범위의 수정·좋아요·삭제를 실시간 구독한다. */\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 { isBoardHidden } = useMemiBoardSettings()\n const i18n = useMemiBoardI18n()\n const { t } = i18n\n const commentsColRef = () => commentsCol(db, cfg())\n\n const bid = computed(() => toValue(boardId))\n const id = computed(() => toValue(postId))\n const listedForBoard = () => !isBoardHidden(bid.value)\n\n // parentId 도입 전 댓글은 범위 구독과 분리해 최초 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 epoch = 0\n let subscriptionVersion = 0\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 [...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 const currentEpoch = ++epoch\n subscriptionVersion++\n loadingMore.value = false\n pagedComments.value = []\n legacyComments.value = []\n pageCursor = null\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 (currentEpoch !== epoch) 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 (currentEpoch !== epoch) return\n }\n finally {\n if (currentEpoch === epoch) {\n legacyPending.value = false\n initialPending.value = false\n }\n }\n }, { immediate: true })\n\n function startLiveSubscription() {\n stopLiveSubscription?.()\n const version = ++subscriptionVersion\n stopLiveSubscription = subscribeCommentRange(db, cfg(), id.value, {\n cursor: pageCursor, hasMore: hasMore.value,\n }, (items) => {\n if (version === subscriptionVersion) pagedComments.value = items\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 const currentEpoch = epoch\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 if (currentEpoch !== epoch) return\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 hasMore.value = snapshot.size > COMMENT_PAGE_SIZE\n startLiveSubscription()\n }\n finally {\n if (currentEpoch === epoch) loadingMore.value = false\n }\n }\n\n onScopeDispose(() => {\n epoch++\n subscriptionVersion++\n stopLiveSubscription?.()\n })\n\n async function addComment(input: AddCommentInput): Promise<void> {\n const body = normalizedCommentBody(input.body, i18n)\n const batch = writeBatch(db)\n const commentRef = doc(commentsColRef())\n batch.set(commentRef, {\n postId: id.value,\n boardId: bid.value,\n listed: listedForBoard(),\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 likeCount: 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, i18n)\n if (!input.parent.id) throw new Error(t('comments.replyTargetNotFound'))\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(t('comments.threadNotFound'))\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 listed: listedForBoard(),\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 likeCount: 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(t('comments.hasRepliesCannotDelete'))\n }\n await deleteLikesForComment(db, cfg(), comment.id)\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 }\n\n async function updateComment(commentId: string, body: string): Promise<void> {\n const trimmed = normalizedCommentBody(body, i18n)\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 */\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 pid = postId.trim()\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 let epoch = 0\n let subscriptionVersion = 0\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 stopLiveSubscription?.()\n const version = ++subscriptionVersion\n stopLiveSubscription = subscribeCommentRange(db, cfg(), pid, {\n rootId, cursor, hasMore: hasMore.value,\n }, (items) => {\n if (version === subscriptionVersion) replies.value = items\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 const currentEpoch = epoch\n loading.value = true\n try {\n const constraints = [\n where('postId', '==', pid),\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 if (currentEpoch !== epoch) return\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 hasMore.value = snapshot.size > REPLY_PAGE_SIZE\n startLiveSubscription()\n }\n finally {\n if (currentEpoch === epoch) loading.value = false\n }\n }\n\n function reset() {\n epoch++\n subscriptionVersion++\n loading.value = false\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(() => { epoch++; subscriptionVersion++; stopLiveSubscription?.() })\n\n return { replies, loading, loaded, hasMore, loadMore, refresh }\n}\n","import { computed, onScopeDispose, ref, toValue, watch, type InjectionKey, type MaybeRefOrGetter } from 'vue'\nimport type { Unsubscribe } from 'firebase/firestore'\nimport { useFirestore, useCurrentUser } from 'vuefire'\nimport {\n increment,\n onSnapshot,\n query,\n runTransaction,\n serverTimestamp,\n where,\n} from 'firebase/firestore'\nimport { useBoardPathConfig } from '../config'\nimport { commentDoc, likeDoc, likesCol, postDoc } from '../utils/boardPaths'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport function useMemiBoardLikes(boardId: string, postId: string) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const user = useCurrentUser()\n const { t } = useMemiBoardI18n()\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(t('likes.signInRequired'))\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\nexport interface MemiBoardCommentLikesApi {\n isLiked: (commentId: string | undefined) => boolean\n isPending: (commentId: string | undefined) => boolean\n toggleLike: (commentId: string) => Promise<boolean>\n}\n\nexport const memiBoardCommentLikesKey: InjectionKey<MemiBoardCommentLikesApi> = Symbol.for('memi-board:comment-likes')\n\n/** 글 하나에서 내가 누른 댓글 좋아요를 쿼리 1번으로 구독한다. 댓글마다 리스너를 달지 않는다. */\nexport function useMemiBoardCommentLikes(\n boardId: MaybeRefOrGetter<string>,\n postId: MaybeRefOrGetter<string>,\n): MemiBoardCommentLikesApi {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const user = useCurrentUser()\n const { t } = useMemiBoardI18n()\n const bid = computed(() => toValue(boardId))\n const pid = computed(() => toValue(postId))\n\n const likedIds = ref(new Set<string>())\n const pendingId = ref<string | null>(null)\n let stopSubscription: Unsubscribe | undefined\n\n watch([user, pid], ([current, currentPostId]) => {\n stopSubscription?.()\n stopSubscription = undefined\n likedIds.value = new Set()\n if (!current || !currentPostId) return\n\n stopSubscription = onSnapshot(\n query(\n likesCol(db, cfg()),\n where('uid', '==', current.uid),\n where('postId', '==', currentPostId),\n where('target', '==', 'comment'),\n ),\n (snapshot) => {\n const next = new Set<string>()\n for (const item of snapshot.docs) {\n const commentId = item.data().commentId\n if (typeof commentId === 'string' && commentId) next.add(commentId)\n }\n likedIds.value = next\n },\n (cause) => {\n console.error('[memi-board:comment-likes] onSnapshot failed', cause)\n },\n )\n }, { immediate: true })\n\n onScopeDispose(() => stopSubscription?.())\n\n function isLiked(commentId: string | undefined): boolean {\n return !!commentId && likedIds.value.has(commentId)\n }\n\n function isPending(commentId: string | undefined): boolean {\n return !!commentId && pendingId.value === commentId\n }\n\n async function toggleLike(commentId: string): Promise<boolean> {\n const uid = user.value?.uid\n if (!uid) throw new Error(t('likes.signInRequired'))\n if (!commentId) return false\n if (pendingId.value) return isLiked(commentId)\n pendingId.value = commentId\n try {\n const likeRef = likeDoc(db, cfg(), commentId, uid)\n const commentRef = commentDoc(db, cfg(), commentId)\n const nextLiked = await runTransaction(db, async (tx) => {\n const likeSnap = await tx.get(likeRef)\n const commentSnap = await tx.get(commentRef)\n if (!commentSnap.exists()) throw new Error(t('likes.commentNotFound'))\n const current = Number(commentSnap.data()?.likeCount ?? 0)\n if (likeSnap.exists()) {\n tx.delete(likeRef)\n tx.update(commentRef, { likeCount: Math.max(0, current - 1) })\n return false\n }\n tx.set(likeRef, {\n uid,\n postId: pid.value,\n boardId: bid.value,\n commentId,\n target: 'comment',\n createdAt: serverTimestamp(),\n })\n tx.update(commentRef, { likeCount: current + 1 })\n return true\n })\n const next = new Set(likedIds.value)\n if (nextLiked) next.add(commentId)\n else next.delete(commentId)\n likedIds.value = next\n return nextLiked\n }\n finally {\n pendingId.value = null\n }\n }\n\n return { isLiked, isPending, toggleLike }\n}\n","import { computed, onScopeDispose, ref, toValue, watch, type MaybeRefOrGetter } from 'vue'\nimport type { Unsubscribe } from 'firebase/firestore'\nimport {\n documentId,\n getDocs,\n limit as fbLimit,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n startAfter,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore'\nimport { useCurrentUser, useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport { reportDoc, reportsCol } from '../utils/boardPaths'\nimport type { BoardReportModel, BoardReportReason, BoardReportStatus } from '../types'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport const REPORT_DETAIL_MAX_LENGTH = 200\n/**\n * 신고 사유 목록. `label` 은 호환용 한국어 원문이다 — 화면에는 `reportReasonLabel(value, locale)` 로\n * 현재 언어 문구를 쓴다.\n */\nexport const REPORT_REASONS: { value: BoardReportReason, label: string }[] = [\n { value: 'spam', label: '스팸·광고' },\n { value: 'abuse', label: '욕설·혐오·괴롭힘' },\n { value: 'adult', label: '음란물' },\n { value: 'illegal', label: '불법 정보' },\n { value: 'other', label: '기타' },\n]\n\nexport function reportReasonLabel(reason: BoardReportReason | string | undefined, locale?: MemiBoardLocale) {\n if (REPORT_REASONS.some(item => item.value === reason)) return translate(locale, `reports.reason.${reason}`)\n return reason || translate(locale, 'reports.fallbackLabel')\n}\n\nexport function useMemiBoardReports(boardId: string, postId: string) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const user = useCurrentUser()\n const { t } = useMemiBoardI18n()\n\n const hasReported = ref(false)\n const pending = ref(false)\n let stop: Unsubscribe | undefined\n\n watch(user, (current) => {\n stop?.()\n stop = undefined\n if (!current) {\n hasReported.value = false\n return\n }\n stop = onSnapshot(reportDoc(db, cfg(), postId, current.uid), (snap) => {\n hasReported.value = snap.exists()\n }, (cause) => {\n console.error('[memi-board:reports] onSnapshot failed', cause)\n })\n }, { immediate: true })\n\n onScopeDispose(() => stop?.())\n\n async function submitReport(input: {\n reason: BoardReportReason\n detail?: string\n postTitle?: string\n authorUid?: string\n authorName?: string | null\n }): Promise<void> {\n const uid = user.value?.uid\n if (!uid) throw new Error(t('reports.signInRequired'))\n if (pending.value) return\n if (input.authorUid && input.authorUid === uid) throw new Error(t('reports.cannotReportOwn'))\n const reason = REPORT_REASONS.some(item => item.value === input.reason) ? input.reason : null\n if (!reason) throw new Error(t('reports.reasonRequired'))\n const detail = (input.detail || '').trim()\n if (detail.length > REPORT_DETAIL_MAX_LENGTH) {\n throw new Error(t('reports.detailTooLong', { max: REPORT_DETAIL_MAX_LENGTH }))\n }\n\n pending.value = true\n try {\n await setDoc(reportDoc(db, cfg(), postId, uid), {\n uid,\n target: 'post',\n postId,\n boardId,\n reason,\n detail,\n status: 'open',\n postTitle: (input.postTitle || '').trim().slice(0, 200),\n authorUid: input.authorUid || '',\n authorName: input.authorName ?? null,\n createdAt: serverTimestamp(),\n })\n hasReported.value = true\n }\n finally {\n pending.value = false\n }\n }\n\n return { hasReported, pending, submitReport }\n}\n\nconst QUEUE_PAGE_SIZE = 20\n\nexport function useMemiBoardReportQueue(options: { pageSize?: number, boardId?: MaybeRefOrGetter<string> } = {}) {\n const cfg = () => useBoardPathConfig()\n const db = useFirestore()\n const { isAdmin, canManageContent, canManageBoard } = useMemiBoardAuth()\n const pageSize = options.pageSize ?? QUEUE_PAGE_SIZE\n const boardFilter = computed(() => toValue(options.boardId)?.trim() || '')\n\n const reports = ref<BoardReportModel[]>([])\n const pending = ref(true)\n const loadingMore = ref(false)\n const hasMore = ref(false)\n const loadError = ref('')\n let cursor: QueryDocumentSnapshot<DocumentData> | null = null\n let stop: Unsubscribe | undefined\n\n function mapDoc(item: QueryDocumentSnapshot<DocumentData>): BoardReportModel {\n return { id: item.id, ...item.data() } as BoardReportModel\n }\n\n function startHead() {\n stop?.()\n if (!canManageContent.value || (!isAdmin.value && !boardFilter.value)) {\n reports.value = []\n pending.value = false\n return\n }\n pending.value = true\n const constraints = boardFilter.value\n ? [where('status', '==', 'open'), where('boardId', '==', boardFilter.value)]\n : [where('status', '==', 'open'), where('target', '==', 'post')]\n stop = onSnapshot(query(\n reportsCol(db, cfg()),\n ...constraints,\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n fbLimit(pageSize),\n ), (snapshot) => {\n reports.value = snapshot.docs.map(mapDoc).filter(item => canManageBoard(item.boardId))\n cursor = snapshot.docs.at(-1) ?? null\n hasMore.value = snapshot.docs.length >= pageSize\n pending.value = false\n }, (cause) => {\n console.error('[memi-board:report-queue] onSnapshot failed', cause)\n loadError.value = (cause as Error).message || String(cause)\n pending.value = false\n })\n }\n\n watch([canManageContent, boardFilter], startHead, { immediate: true })\n onScopeDispose(() => stop?.())\n\n async function loadMore() {\n if (loadingMore.value || !hasMore.value || !cursor || !canManageContent.value) return\n loadingMore.value = true\n loadError.value = ''\n try {\n const constraints = boardFilter.value\n ? [where('status', '==', 'open'), where('boardId', '==', boardFilter.value)]\n : [where('status', '==', 'open'), where('target', '==', 'post')]\n const snapshot = await getDocs(query(\n reportsCol(db, cfg()),\n ...constraints,\n orderBy('createdAt', 'desc'),\n orderBy(documentId(), 'desc'),\n startAfter(cursor),\n fbLimit(pageSize + 1),\n ))\n const pageDocs = snapshot.docs.slice(0, pageSize)\n const existing = new Set(reports.value.map(item => item.id))\n reports.value.push(...pageDocs.map(mapDoc).filter(item => !existing.has(item.id) && canManageBoard(item.boardId)))\n cursor = pageDocs.at(-1) ?? cursor\n hasMore.value = snapshot.docs.length > pageSize\n }\n catch (cause) {\n loadError.value = (cause as Error).message || String(cause)\n }\n finally {\n loadingMore.value = false\n }\n }\n\n async function resolveReport(report: BoardReportModel, status: Exclude<BoardReportStatus, 'open'>) {\n if (!report.id || !canManageBoard(report.boardId)) return\n await updateDoc(reportDoc(db, cfg(), report.postId, report.uid), {\n status,\n reviewedAt: serverTimestamp(),\n reviewedBy: useCurrentUser().value?.uid || '',\n })\n }\n\n return {\n reports,\n pending,\n loadingMore,\n hasMore,\n loadError,\n loadMore,\n dismissReport: (report: BoardReportModel) => resolveReport(report, 'dismissed'),\n actionReport: (report: BoardReportModel) => resolveReport(report, 'actioned'),\n }\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","import { memiBoardLocaleInfo, type MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\n\n/** 한국어 보드용 원문 프롬프트 (ko). */\nexport 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/** 프롬프트에 함께 적는 언어 영어 이름 (모델이 언어를 헷갈리지 않게). */\nconst LOCALE_ENGLISH_NAMES: Record<MemiBoardLocale, string> = {\n ko: 'Korean',\n en: 'English',\n ja: 'Japanese',\n de: 'German',\n fr: 'French',\n es: 'Spanish',\n pt: 'Portuguese',\n zh: 'Simplified Chinese',\n ar: 'Arabic',\n id: 'Indonesian',\n}\n\n/** 예: \"Japanese (日本語)\", \"English\" */\nexport function memiBoardLanguageLabel(locale: MemiBoardLocale): string {\n const english = LOCALE_ENGLISH_NAMES[locale] ?? 'English'\n const native = memiBoardLocaleInfo(locale).name\n return english === native ? english : `${english} (${native})`\n}\n\n/** ko 외 모든 언어용 영어 프롬프트. 엄격도·출력 형식은 한국어 원문과 동일. */\nfunction buildEnglishModerationPrompt(locale: MemiBoardLocale): string {\n const language = memiBoardLanguageLabel(locale)\n return `You are a strict moderator reviewing content for a message board.\nIf the text below contains profanity, vulgar slang, sexual harassment, hate speech, threats, spam, or repetitive/flooding advertisements, you MUST reject it (flagged=true).\n\nThe content may be written in ANY language, or a mix of languages — not only the board's language.\nJudge every language equally strictly, including slang, abbreviations, romanizations, deliberate misspellings,\ninserted spaces/symbols, and other obfuscation (e.g. in Korean, Japanese, Chinese, Arabic, Spanish, etc.).\n\nflagged=true examples:\n- Profanity/vulgar slang: fuck, shit, bitch, asshole, f*ck, sh1t /\n Korean: 씨발, 시발, ㅅㅂ, 병신, ㅂㅅ, 지랄, 좆, 개새끼, 존나 /\n Japanese: クソ, 死ね, ちんこ / Chinese: 操你妈, 傻逼, 他妈的 /\n Spanish: puta, mierda, pendejo / German: Scheiße, Hurensohn / French: putain, connard /\n Portuguese: porra, caralho / Indonesian: anjing, bangsat, kontol / Arabic: كس, شرموطة, etc.\n- Sexual insults, hate speech, threats\n- Flooding advertisements/spam unrelated to the board's topic\n\nflagged=false:\n- Normal posts/comments that fit the board's topic (strong opinions and legitimate criticism are allowed)\n\nOutput JSON only:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- If flagged=true, choose a category, and write reason as ONE concrete sentence in ${language} explaining **what the problem is**\n (e.g. \"It contains profanity, so it can't be posted.\", \"It contains advertising/spam, so it can't be posted.\" — but written in ${language}).\n Do not quote the offensive words at length; clearly state the type (profanity/hate/spam, etc.).\n- If flagged=false, category is \"none\" and reason is \"\"`\n}\n\n/**\n * 보드 언어별 검열 시스템 프롬프트.\n * ko 는 한국어 원문(MODERATION_SYSTEM) 그대로, 그 외는 영어 프롬프트 + reason 을 보드 언어로 쓰도록 지시.\n */\nexport function buildModerationPrompt(locale: MemiBoardLocale = 'ko'): string {\n return locale === 'ko' ? MODERATION_SYSTEM : buildEnglishModerationPrompt(locale)\n}\n\n/** 텍스트 검열 요청 본문 */\nexport function buildModerationTextPrompt(text: string, locale: MemiBoardLocale = 'ko'): string {\n const body = `\"\"\"${text.slice(0, 4000)}\"\"\"`\n return locale === 'ko' ? `심사할 텍스트:\\n${body}` : `Text to review:\\n${body}`\n}\n\n/** 이미지 검열 요청 문구 */\nexport function buildModerationImagePrompt(locale: MemiBoardLocale = 'ko'): string {\n return locale === 'ko' ? '이 이미지를 심사해 주세요.' : 'Please review this image.'\n}\n\n/** 사용자 안내용 카테고리 라벨 (ko 원문. 다른 언어는 moderationCategoryLabel 사용) */\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 moderationCategoryLabel(category: string, locale: MemiBoardLocale = 'ko'): string {\n if (!category || category === 'none') return ''\n if (!(category in MODERATION_CATEGORY_LABELS)) return category\n return translate(locale, `moderationPrompt.categories.${category}`)\n}\n\n/** 차단 사유를 사용자에게 읽기 쉽게 정리 */\nexport function formatModerationUserReason(opts: {\n reason?: string\n category?: string\n via?: string\n localHit?: string\n /** 안내 문구 언어 (기본 ko) */\n locale?: MemiBoardLocale\n}): string {\n const locale = opts.locale ?? 'ko'\n if (opts.via === 'local' && opts.localHit) {\n const hit = opts.localHit.trim()\n // 짧은 금칙어만 표시 (너무 길면 유형만)\n if (hit.length > 0 && hit.length <= 12) {\n return translate(locale, 'moderationPrompt.localHit', { hit })\n }\n return translate(locale, 'moderationPrompt.localGeneric')\n }\n\n const raw = (opts.reason || '').trim()\n if (raw) return raw\n\n const cat = opts.category ? moderationCategoryLabel(opts.category, locale) : ''\n if (cat) return translate(locale, 'moderationPrompt.categoryBlocked', { category: cat })\n return translate(locale, 'moderationPrompt.genericBlocked')\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 { computed, toValue, type MaybeRefOrGetter } from 'vue'\nimport { useFirebaseApp } from 'vuefire'\nimport {\n buildLocalBlockRegex,\n DEFAULT_LOCAL_BLOCKLIST,\n buildModerationImagePrompt,\n buildModerationPrompt,\n buildModerationTextPrompt,\n formatModerationUserReason,\n parseModerationJson,\n} from '../utils/moderation-prompt'\nimport { useMemiBoardConfig } from '../config'\nimport { normalizeMemiBoardLocale, type MemiBoardLocale } from '../i18n/locales'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\nimport { useMemiBoardAuth } from './useMemiBoardAuth'\nimport { translate } from '../i18n/translate'\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(options: {\n /** 보드 언어. 같은 컴포넌트에서 boardLocale 을 provide 했다면 그 locale 을 넘긴다(자기 자신에게 inject 되지 않음). */\n locale?: MaybeRefOrGetter<string | undefined | null>\n} = {}) {\n const promptI18n = useMemiBoardI18n()\n const promptLocale = computed<MemiBoardLocale>(() => normalizeMemiBoardLocale(toValue(options.locale)) ?? promptI18n.locale.value)\n const config = useMemiBoardConfig()\n const moderation = config.moderation ?? {}\n const app = useFirebaseApp()\n const t = (key: string) => translate(promptLocale.value, key)\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: buildModerationPrompt(promptLocale.value),\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: t('moderation.reviewFailed'),\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, opts?: { skipFilter?: boolean }): Promise<ModerationResult> {\n if (isWriteRestricted.value) {\n return {\n flagged: true,\n category: 'other',\n reason: restrictedMessage.value\n || t('moderation.writeRestricted'),\n via: 'restricted',\n }\n }\n\n const trimmed = text.trim()\n if (!trimmed) {\n return { ...APPROVED, via: 'empty' as ModerationVia }\n }\n\n if (opts?.skipFilter) {\n return { ...APPROVED, via: 'admin-override' }\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(buildModerationTextPrompt(trimmed, promptLocale.value))\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 || t('moderation.writeRestricted'),\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 buildModerationImagePrompt(promptLocale.value),\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'\nimport { translate } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\n/**\n * 역할 목록. label·description 은 기본 언어(ko) 문구다 —\n * 화면에서는 t('common.role.<value>') / t('boardUsers.roleDescription.<value>') 로 현재 언어 문구를 쓴다.\n */\nexport const BOARD_USER_ROLES: Array<{\n value: BoardUserRole\n label: string\n description: string\n}> = (['owner', 'admin', 'manager', 'staff', 'user'] as const).map(value => ({\n value,\n label: translate('ko', `common.role.${value}`),\n description: translate('ko', `boardUsers.roleDescription.${value}`),\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 { t } = useMemiBoardI18n()\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(t('boardUsers.unsupportedRole'))\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","import type { BoardListView, BoardSection, BoardSectionCols, BoardSectionHeight, BoardSectionKind, BoardSectionSort, CommentModel, PostModel } from '../types'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\n\nexport const BOARD_SECTION_COLS: BoardSectionCols[] = [3, 4, 5, 6, 7, 8, 9, 12]\n\n/** 섹션 종류 선택지. locale 을 넘기면 해당 언어 라벨(기본 ko). */\nexport function boardSectionKinds(locale?: MemiBoardLocale): { value: BoardSectionKind, label: string }[] {\n return [\n { value: 'list', label: translate(locale, 'sectionUtil.kind.list') },\n { value: 'post', label: translate(locale, 'sectionUtil.kind.post') },\n { value: 'comments', label: translate(locale, 'sectionUtil.kind.comments') },\n ]\n}\n\n/** 섹션 정렬 선택지. locale 을 넘기면 해당 언어 라벨(기본 ko). */\nexport function boardSectionSorts(locale?: MemiBoardLocale): { value: BoardSectionSort, label: string }[] {\n return [\n { value: 'latest', label: translate(locale, 'sectionUtil.sort.latest') },\n { value: 'likes', label: translate(locale, 'sectionUtil.sort.likes') },\n ]\n}\n\n/** @deprecated 한국어 고정 라벨. 언어별 라벨은 boardSectionKinds(locale) 사용 */\nexport const BOARD_SECTION_KINDS = boardSectionKinds('ko')\n/** @deprecated 한국어 고정 라벨. 언어별 라벨은 boardSectionSorts(locale) 사용 */\nexport const BOARD_SECTION_SORTS = boardSectionSorts('ko')\n\nexport function defaultSectionCount(kind: BoardSectionKind) {\n return kind === 'list' ? 5 : 4\n}\n\nexport function clampSectionCount(kind: BoardSectionKind, value: unknown) {\n const n = Math.floor(Number(value))\n if (!Number.isFinite(n)) return defaultSectionCount(kind)\n return Math.min(kind === 'list' ? 20 : 10, Math.max(1, n))\n}\n\nexport function pairSectionCols(cols: BoardSectionCols): BoardSectionCols | null {\n if (cols === 12) return null\n return (12 - cols) as BoardSectionCols\n}\n\nexport function sectionColClass(cols: BoardSectionCols): string {\n return {\n 3: 'col-span-12 md:col-span-3',\n 4: 'col-span-12 md:col-span-4',\n 5: 'col-span-12 md:col-span-5',\n 6: 'col-span-12 md:col-span-6',\n 7: 'col-span-12 md:col-span-7',\n 8: 'col-span-12 md:col-span-8',\n 9: 'col-span-12 md:col-span-9',\n 12: 'col-span-12',\n }[cols]\n}\n\nexport function sectionMinHeight(height: BoardSectionHeight) {\n if (height === 'sm') return 'min-h-40'\n if (height === 'lg') return 'min-h-80'\n return 'min-h-56'\n}\n\nexport function miniPostLink(post: Pick<PostModel, 'boardId' | 'slug' | 'category'>) {\n const boardId = post.boardId || post.category || 'free'\n if (!post.slug) return `/board/${boardId}`\n return `/board/${boardId}/${post.slug}`\n}\n\nexport function miniMoreLink(section: Pick<BoardSection, 'kind' | 'boardId' | 'sort'>) {\n if (section.kind === 'comments') {\n return section.sort === 'likes' ? '/boardComments?sort=likes' : '/boardComments'\n }\n if (section.kind === 'list' && section.boardId) return `/board/${section.boardId}`\n return '/board'\n}\n\nexport function miniCommentLink(comment: Pick<CommentModel, 'boardId' | 'postId'>) {\n if (!comment.boardId || !comment.postId) return undefined\n return `/board/${comment.boardId}/post/${comment.postId}`\n}\n\nexport function miniPostImage(post: PostModel): string | undefined {\n return post.previewImage\n || post.attachments?.find(item => item.type.startsWith('image/'))?.url\n}\n\nexport function miniPostTitle(post: PostModel, locale?: MemiBoardLocale) {\n return post.title?.trim() || post.summary || translate(locale, 'sectionUtil.untitledPost')\n}\n\nexport type MiniViewGroup = 'list' | 'media'\n\nexport function miniViewGroup(view: BoardListView): MiniViewGroup {\n return view === 'image' || view === 'video' ? 'media' : 'list'\n}\n\nexport function miniViewLabel(view: BoardListView, locale?: MemiBoardLocale) {\n return translate(locale, miniViewGroup(view) === 'media' ? 'sectionUtil.view.media' : 'sectionUtil.view.list')\n}\n\nexport function miniBoardView(\n boardId: string | null | undefined,\n getBoard: (id: string) => { listView?: BoardListView } | undefined,\n): BoardListView {\n if (!boardId) return 'default'\n return getBoard(boardId)?.listView ?? 'default'\n}\n\nexport function miniPostsView(\n posts: Array<{ boardId?: string }>,\n getBoard: (id: string) => { listView?: BoardListView } | undefined,\n): BoardListView {\n const views = posts.map(post => miniBoardView(post.boardId, getBoard))\n if (!views.length) return 'default'\n if (views.every(view => view === views[0])) return views[0]!\n const image = views.filter(view => view === 'image').length\n const video = views.filter(view => view === 'video').length\n if (image >= views.length / 2) return 'image'\n if (video >= views.length / 2) return 'video'\n return 'default'\n}","import { computed } from 'vue'\nimport { doc, serverTimestamp, setDoc } from 'firebase/firestore'\nimport { useDocument, useFirestore } from 'vuefire'\nimport type { BoardSection, BoardSectionCols, BoardSectionHeight, BoardSectionKind, BoardSectionLayout, BoardSectionSort } from '../types'\nimport { BOARD_SECTION_COLS, clampSectionCount } from '../utils/section'\nimport type { MemiBoardLocale } from '../i18n/locales'\nimport { translate } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport const DEFAULT_BOARD_SECTIONS: BoardSection[] = []\n\nfunction asCols(raw: Record<string, unknown>): BoardSectionCols {\n const cols = Number(raw.cols)\n if ((BOARD_SECTION_COLS as number[]).includes(cols)) return cols as BoardSectionCols\n const width = raw.width\n if (width === 'xs') return 3\n if (width === 'sm') return 4\n if (width === 'lg' || Number(raw.span) === 2) return 12\n return 6\n}\n\nfunction asHeight(raw: Record<string, unknown>): BoardSectionHeight {\n const height = raw.height ?? raw.size\n return height === 'sm' || height === 'lg' ? height : 'md'\n}\n\nfunction asKind(raw: Record<string, unknown>): BoardSectionKind {\n if (raw.kind === 'post' || raw.kind === 'comments') return raw.kind\n return 'list'\n}\n\nfunction asSort(raw: Record<string, unknown>): BoardSectionSort {\n return raw.sort === 'likes' ? 'likes' : 'latest'\n}\n\nfunction asBoardId(raw: Record<string, unknown>): string | null {\n const id = typeof raw.boardId === 'string' ? raw.boardId.trim() : ''\n return id || null\n}\n\nfunction asPostIds(raw: Record<string, unknown>, count: number): string[] {\n const source = Array.isArray(raw.postIds) ? raw.postIds : []\n const ids: string[] = []\n for (const value of source) {\n const id = String(value || '').trim()\n if (!id || ids.includes(id)) continue\n ids.push(id)\n if (ids.length >= count) break\n }\n return ids\n}\n\nfunction defaultSectionTitle(order: number, locale?: MemiBoardLocale) {\n return translate(locale, 'sections.defaultTitle', { n: order + 1 })\n}\n\nexport function normalizeBoardSection(raw: Record<string, unknown>, order: number, locale?: MemiBoardLocale): BoardSection {\n const kind = asKind(raw)\n const count = clampSectionCount(kind, raw.count)\n return {\n id: String(raw.id || `box-${order + 1}`),\n title: String(raw.title || defaultSectionTitle(order, locale)),\n showTitle: raw.showTitle !== false,\n kind,\n boardId: asBoardId(raw),\n count,\n postIds: asPostIds(raw, count),\n sort: asSort(raw),\n cols: asCols(raw),\n height: asHeight(raw),\n order,\n }\n}\n\n/** 호스트 `settings/homeLayout` 문서를 읽는다. 기존 메인 레이아웃 경로를 유지한다. */\nexport function useMemiBoardSections() {\n const { locale } = useMemiBoardI18n()\n const db = useFirestore()\n const settingsRef = doc(db, 'settings', 'homeLayout')\n const { data: settings, pending } = useDocument<BoardSectionLayout>(settingsRef)\n\n const sections = computed(() => {\n const source = Array.isArray(settings.value?.blocks) ? settings.value.blocks : DEFAULT_BOARD_SECTIONS\n return source.map((block, index) => normalizeBoardSection(block as unknown as Record<string, unknown>, index, locale.value))\n })\n\n async function saveSections(next: BoardSection[]) {\n await setDoc(settingsRef, {\n columns: 12,\n blocks: next.map((block, order) => {\n const raw = block as unknown as Record<string, unknown>\n const kind = asKind(raw)\n const count = clampSectionCount(kind, block.count)\n return {\n id: block.id,\n title: block.title.trim() || defaultSectionTitle(order, locale.value),\n showTitle: block.showTitle !== false,\n kind,\n boardId: asBoardId(raw),\n count,\n postIds: asPostIds(raw, count),\n sort: asSort(raw),\n cols: asCols(raw),\n height: asHeight(raw),\n order,\n }\n }),\n updatedAt: serverTimestamp(),\n })\n }\n\n const hasSavedLayout = computed(() => Array.isArray(settings.value?.blocks))\n\n return {\n settings,\n pending,\n sections,\n blocks: sections,\n hasSavedLayout,\n saveSections,\n saveBlocks: saveSections,\n }\n}","import { computed, toValue, type MaybeRefOrGetter } from 'vue'\nimport {\n documentId,\n limit as fbLimit,\n orderBy,\n query,\n where,\n type QueryConstraint,\n} from 'firebase/firestore'\nimport { useCollection, useFirestore } from 'vuefire'\nimport { useBoardPathConfig } from '../config'\nimport { boardSsrKey, postsCol } from '../utils/boardPaths'\nimport type { PostModel } from '../types'\n\n/**\n * SSR과 클라이언트 첫 렌더의 pending 상태가 갈리면(서버는 즉시 완료, 클라이언트는\n * 로딩 중) MiniList.vue가 서버에선 빈 상태(<p>), 클라이언트에선 스켈레톤(<div>)을\n * 그려 노드 자체가 달라지는 하이드레이션 mismatch가 난다. 쿼리가 where/orderBy/limit을\n * 쓰는 복합 쿼리라 ssrKey 없인 vuefire가 경로를 못 찾는 것도 같은 원인이라, 두 컴포저블\n * 모두 서버에서도 쿼리를 실행하고 ssrKey로 하이드레이션시킨다.\n */\nexport function useMemiBoardMiniLatest(\n boardId: MaybeRefOrGetter<string | null>,\n count: MaybeRefOrGetter<number>,\n) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const boardKey = toValue(boardId)?.trim() || 'all'\n const postsQuery = computed(() => {\n const take = Math.max(1, Math.floor(Number(toValue(count)) || 5))\n const board = toValue(boardId)?.trim() || ''\n const constraints: QueryConstraint[] = [\n where('isPublished', '==', true),\n where('listed', '==', true),\n ]\n if (board) constraints.unshift(where('boardId', '==', board))\n return query(\n postsCol(db, cfg()),\n ...constraints,\n orderBy('createdAt', 'desc'),\n fbLimit(take),\n )\n })\n const { data, pending, error } = useCollection<PostModel>(postsQuery, {\n ssrKey: boardSsrKey(cfg(), `mini-list/${boardKey}`),\n })\n const posts = computed(() => data.value ?? [])\n return { posts, pending, error }\n}\n\nexport function useMemiBoardMiniPicked(postIds: MaybeRefOrGetter<string[]>) {\n const db = useFirestore()\n const cfg = () => useBoardPathConfig()\n const ids = computed(() =>\n [...new Set(toValue(postIds).map(id => id.trim()).filter(Boolean))].slice(0, 10),\n )\n const idsKey = ids.value.join(',') || 'none'\n const postsQuery = computed(() => {\n if (!ids.value.length) return null\n return query(\n postsCol(db, cfg()),\n where(documentId(), 'in', ids.value),\n )\n })\n const { data, pending, error } = useCollection<PostModel>(postsQuery, {\n ssrKey: boardSsrKey(cfg(), `mini-picked/${idsKey}`),\n })\n const posts = computed(() => {\n const byId = new Map((data.value ?? []).map(post => [post.id, post]))\n const ordered: PostModel[] = []\n for (const id of ids.value) {\n const item = byId.get(id)\n if (item && item.isPublished !== false) ordered.push(item)\n }\n return ordered\n })\n return { posts, pending, error }\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.46.0',\n date: '2026-10-21',\n highlights: [\n '일반 사용자·스태프·매니저·관리자·최고 관리자, 다섯 가지 역할을 지원해요',\n '게시판별 매니저가 설정과 스태프 배정을 관리할 수 있어요',\n '서비스에 맞게 역할 이름을 바꿀 수 있어요',\n ],\n },\n {\n version: '0.45.1',\n date: '2026-10-15',\n highlights: [\n '일반 계정 글쓰기·초안 조회와 태그 입력을 고쳤어요',\n '댓글·답글 수정과 좋아요가 다른 사용자에게 바로 반영돼요',\n '글 삭제·첨부파일 보호·게시판 공개 범위 변경을 보강했어요',\n ],\n },\n {\n version: '0.45.0',\n date: '2026-10-01',\n highlights: [\n '게시판을 10개 언어로 볼 수 있어요 (한국어·영어·일본어·독일어·프랑스어·스페인어·포르투갈어·중국어·아랍어·인도네시아어)',\n '게시판 설정에서 게시판마다 언어를 고정하거나 사이트 언어를 따르게 할 수 있어요',\n ],\n },\n {\n version: '0.44.1',\n date: '2026-10-01',\n highlights: [\n 'Mac Safari에서 한글을 입력하고 바로 등록 버튼을 누를 때 두 번 눌러야 하던 문제를 고쳤어요',\n ],\n },\n {\n version: '0.44.0',\n date: '2026-10-01',\n highlights: [\n '글을 저장하고 미리보기가 뜨기까지 기다리는 시간이 줄었어요 (특히 iPhone·Mac Safari)',\n ],\n },\n {\n version: '0.42.0',\n date: '2026-08-27',\n highlights: [\n '글쓰기에서 문단 옆 + 버튼이나 / 입력으로 제목·목록·이미지 같은 블록을 빠르게 넣을 수 있어요',\n '문단 메뉴에서 블록을 다른 형식으로 바꾸거나 복제·이동·삭제할 수 있어요',\n ],\n },\n {\n version: '0.41.0',\n date: '2026-08-27',\n highlights: [\n 'AI 도우미 결과를 바로 적용하지 않고 이전/이후를 비교해보고 적용할 수 있어요',\n ],\n },\n {\n version: '0.40.0',\n date: '2026-08-27',\n highlights: [\n 'AI 글쓰기 도우미에서 원하는 문체를 직접 입력해 바꿀 수 있어요 (예: 경상도 사투리 스타일로)',\n ],\n },\n {\n version: '0.39.1',\n date: '2026-08-27',\n highlights: [\n '관리자는 글쓰기 하단 스위치로 비속어 필터를 끄고 글을 올릴 수 있어요',\n ],\n },\n {\n version: '0.39.0',\n date: '2026-08-21',\n highlights: [\n '사진 글에서 원하는 사진을 대표로 고를 수 있어요',\n '목록 모양과 글쓰기 폼을 각각 선택할 수 있어요',\n ],\n },\n {\n version: '0.38.1',\n date: '2026-08-21',\n highlights: [\n '내가 쓴 글을 지울 때 첨부 사진 때문에 막히던 문제를 고쳤어요',\n ],\n note: '호스트는 Storage 예시 규칙의 memiBoardPosts/{postId}/{allPaths=**} 목록 블록을 넣고 배포해야 해요.',\n },\n {\n version: '0.38.0',\n date: '2026-08-21',\n highlights: [\n '숨김 게시판에는 댓글을 거의 쓰지 않아요. 필요하면 담당 스태프만 남길 수 있어요',\n '숨김 게시판 댓글은 공개 댓글 모음에 나오지 않아요',\n ],\n note: '호스트는 Firestore 규칙·댓글 listed 인덱스를 다시 배포해야 해요. 공개 보드는 한 번 저장하면 예전 댓글이 목록에 다시 채워져요.',\n },\n {\n version: '0.36.0',\n date: '2026-08-19',\n highlights: [\n '게시물을 신고할 수 있어요. 담당 스태프가 내용을 확인해요',\n '스태프는 지정된 게시판만 관리해요',\n ],\n note: '호스트는 Firestore 신고 규칙·인덱스를 배포해야 해요.',\n },\n {\n version: '0.35.0',\n date: '2026-08-19',\n highlights: [\n '메인에 게시목록·게시물·댓글모음 섹션을 올릴 수 있어요',\n '댓글모음은 최신순이나 좋아요순으로 네 개부터 보여요',\n ],\n },\n {\n version: '0.34.0',\n date: '2026-08-19',\n highlights: [\n '댓글을 최신순이나 좋아요순으로 모아 볼 수 있어요',\n ],\n note: '호스트는 좋아요순 목록용 Firestore 인덱스(likeCount + createdAt)를 배포해야 해요.',\n },\n {\n version: '0.33.0',\n date: '2026-08-19',\n highlights: [\n '댓글과 답글에도 좋아요를 누를 수 있어요',\n '글을 지우면 댓글 좋아요도 함께 삭제돼요',\n ],\n note: '호스트는 Firestore 규칙과 좋아요 인덱스를 병합·배포해야 댓글 좋아요가 동작해요.',\n },\n {\n version: '0.32.0',\n date: '2026-08-14',\n highlights: [\n '글쓰기에서 사진 파일을 올리거나 외부 이미지 주소를 넣을 수 있어요',\n '게시판을 지우면 그 안의 글·댓글·이미지도 함께 삭제돼요',\n ],\n },\n {\n version: '0.31.0',\n date: '2026-08-14',\n highlights: [\n '글쓰기 화면에 AI 도우미가 생겼어요. 맞춤법, 다듬기, 이어서 쓰기, 요약, 번역, 제목을 도와줘요',\n 'AI 도우미는 하루 10번까지 사용할 수 있어요',\n ],\n note: '호스트는 Firestore 규칙에 AI 글쓰기 횟수 필드를 병합·배포해야 제한이 지켜져요.',\n },\n {\n version: '0.30.2',\n date: '2026-08-14',\n highlights: [\n '일반 목록 썸네일을 조금 작은 라운드 사각으로 바꿨어요',\n '글쓰기 도구 모음을 서식 메뉴로 줄여 좁은 화면에서도 잘리지 않아요',\n '다시 로그인해도 바꿔 둔 프로필 이름과 사진이 유지돼요',\n ],\n },\n {\n version: '0.30.1',\n date: '2026-08-13',\n highlights: [\n '게시물 상단의 목록 버튼을 간결한 왼쪽 화살표로 바꿨어요',\n '이제 main 브랜치에 push하면 npm 배포가 자동으로 진행돼요',\n ],\n },\n {\n version: '0.30.0',\n date: '2026-08-13',\n highlights: [\n '본문 글 없이 사진만 첨부해도 글을 올릴 수 있어요',\n '목록은 게시판 설정에서 정한 보기 방식을 사용하고, 테스트용 보기 전환 버튼은 숨겼어요',\n '글쓰기 중 사진 업로드 권한과 Storage 버킷 설정 안내를 보강했어요',\n ],\n note: '이미지 업로드가 거부되면 호스트의 Storage 규칙과 firebase.json 버킷 설정을 최신 예시로 확인해 주세요.',\n },\n {\n version: '0.29.1',\n date: '2026-08-13',\n highlights: [\n '프로필 이름이나 사진을 바꾸면 예전에 작성한 글과 댓글의 작성자 정보도 함께 바뀌어요',\n '나를 대상으로 작성된 답글의 표시 이름도 새 프로필 이름으로 맞춰져요',\n ],\n note: '이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요.',\n },\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","import { getAI, getGenerativeModel, GoogleAIBackend } from 'firebase/ai'\nimport { runTransaction, serverTimestamp } from 'firebase/firestore'\nimport { computed, toValue, type MaybeRefOrGetter } from 'vue'\nimport { useCurrentUser, useFirebaseApp, useFirestore } from 'vuefire'\nimport { useBoardPathConfig, useMemiBoardConfig } from '../config'\nimport { boardUserDoc } from '../utils/boardPaths'\nimport { memiBoardLanguageLabel } from '../utils/moderation-prompt'\nimport { normalizeMemiBoardLocale, type MemiBoardLocale } from '../i18n/locales'\nimport { translate, type MemiBoardMessageParams } from '../i18n/translate'\nimport { useMemiBoardI18n } from '../i18n/useMemiBoardI18n'\n\nexport const WRITING_ASSISTANT_DAILY_LIMIT = 10\nconst WRITING_ASSISTANT_WINDOW_MS = 24 * 60 * 60 * 1000\n\nexport type WritingAssistantAction =\n | 'proofread'\n | 'polish'\n | 'continue'\n | 'summarize'\n | 'translate-en'\n | 'translate-ko'\n | 'title'\n | 'custom'\n\nconst CUSTOM_INSTRUCTION_MAX_LENGTH = 200\n\ninterface WritingAssistantInput {\n action: WritingAssistantAction\n content: string\n title?: string\n selection?: boolean\n /** action이 'custom'일 때 사용자가 직접 입력한 지시문 (예: \"경상도 사투리 스타일로\") */\n customInstruction?: string\n}\n\ntype BuiltInAction = Exclude<WritingAssistantAction, 'custom'>\n\n/** 한국어 보드용 원문 지시문 (ko) */\nconst INSTRUCTIONS: Record<BuiltInAction, string> = {\n proofread: '맞춤법, 띄어쓰기, 문법 오류만 고쳐라. 의미와 말투는 바꾸지 마라.',\n polish: '뜻과 작성자의 말투를 유지하면서 더 자연스럽고 읽기 좋은 문장으로 다듬어라.',\n continue: '앞의 맥락과 말투에 맞는 내용을 과장 없이 한두 문단 이어서 작성하라.',\n summarize: '핵심 정보는 보존하면서 원문의 약 절반 길이로 간결하게 줄여라.',\n 'translate-en': '자연스러운 영어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.',\n 'translate-ko': '자연스러운 한국어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.',\n title: '본문을 대표하는 간결한 한국어 제목 하나를 만들어라. 따옴표나 설명 없이 제목만 출력하라.',\n}\n\n/** ko 외 언어용 영어 지시문. 결과는 보드 언어로 쓰도록 language 를 넣는다. */\nfunction englishInstructions(language: string): Record<BuiltInAction, string> {\n return {\n proofread: 'Fix only spelling, spacing, and grammar errors. Do not change the meaning or tone.',\n polish: 'Refine the text into more natural, readable sentences while keeping the meaning and the author\\'s tone.',\n continue: 'Continue writing one or two paragraphs that match the preceding context and tone, without exaggeration.',\n summarize: 'Shorten the text concisely to about half of its original length while preserving the key information.',\n 'translate-en': 'Translate into natural English. Keep proper nouns and technical terms accurate.',\n 'translate-ko': 'Translate into natural Korean. Keep proper nouns and technical terms accurate.',\n title: `Create one concise title in ${language} that represents the body. Output only the title, without quotes or explanation.`,\n }\n}\n\ntype Translator = (key: string, params?: MemiBoardMessageParams) => string\n\nfunction resolveInstruction(input: WritingAssistantInput, locale: MemiBoardLocale, t: Translator): string {\n const ko = locale === 'ko'\n const language = memiBoardLanguageLabel(locale)\n if (input.action !== 'custom') {\n if (ko) return INSTRUCTIONS[input.action]\n const base = englishInstructions(language)[input.action]\n // 번역 작업은 대상 언어가 정해져 있으므로 보드 언어 지시를 덧붙이지 않는다.\n if (input.action === 'translate-en' || input.action === 'translate-ko' || input.action === 'title') return base\n return `${base} Write the result in the same language as the original text; if unclear, write in ${language}.`\n }\n const custom = input.customInstruction?.trim().slice(0, CUSTOM_INSTRUCTION_MAX_LENGTH)\n if (!custom) throw new Error(t('writingAssistant.customRequired'))\n return ko\n ? `뜻은 유지하면서 다음 스타일 지시를 반영해 다시 써라: ${custom}`\n : `Rewrite the text applying the following style instruction while keeping the meaning: ${custom}`\n}\n\nfunction cleanModelOutput(value: string): string {\n return value.trim()\n .replace(/^```(?:html|text)?\\s*/i, '')\n .replace(/\\s*```$/, '')\n .trim()\n}\n\nfunction timestampMs(value: unknown): number | null {\n if (!value || typeof value !== 'object') return null\n if ('toMillis' in value && typeof (value as { toMillis: () => number }).toMillis === 'function') {\n return (value as { toMillis: () => number }).toMillis()\n }\n return null\n}\n\nexport interface UseMemiBoardWritingAssistantOptions {\n /**\n * 보드 언어. 같은 컴포넌트에서 useMemiBoardI18n({ boardLocale }) 로 provide 한 값은\n * 자기 자신에게 inject 되지 않으므로, 그 경우 여기로 넘긴다(예: Editor 의 locale).\n */\n locale?: MaybeRefOrGetter<string | undefined | null>\n}\n\n/** 검열과 동일한 Firebase AI Logic 연결을 사용하는 글쓰기 도우미. */\nexport function useMemiBoardWritingAssistant(options: UseMemiBoardWritingAssistantOptions = {}) {\n const i18n = useMemiBoardI18n()\n const locale = computed<MemiBoardLocale>(() => normalizeMemiBoardLocale(toValue(options.locale)) ?? i18n.locale.value)\n const t: Translator = (key, params) => translate(locale.value, key, params)\n const app = useFirebaseApp()\n const db = useFirestore()\n const user = useCurrentUser()\n const config = useMemiBoardConfig()\n\n async function reserveDailyUse(): Promise<void> {\n const uid = user.value?.uid\n if (!uid) throw new Error(t('writingAssistant.loginRequired'))\n\n await runTransaction(db, async (transaction) => {\n const ref = boardUserDoc(db, useBoardPathConfig(), uid)\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists()) throw new Error(t('writingAssistant.userMissing'))\n\n const data = snapshot.data() as Record<string, unknown>\n const previousAt = timestampMs(data.aiWritingWindowAt)\n const previousCount = Math.max(0, Math.floor(Number(data.aiWritingCount) || 0))\n const now = Date.now()\n const activeWindow = previousAt != null && now - previousAt < WRITING_ASSISTANT_WINDOW_MS\n if (activeWindow && previousCount >= WRITING_ASSISTANT_DAILY_LIMIT) {\n const resetAt = previousAt + WRITING_ASSISTANT_WINDOW_MS\n const hours = Math.max(1, Math.ceil((resetAt - now) / (60 * 60 * 1000)))\n throw new Error(t('writingAssistant.dailyLimit', { limit: WRITING_ASSISTANT_DAILY_LIMIT, hours }))\n }\n\n transaction.update(ref, {\n aiWritingCount: activeWindow ? previousCount + 1 : 1,\n aiWritingWindowAt: activeWindow ? data.aiWritingWindowAt : serverTimestamp(),\n updatedAt: serverTimestamp(),\n })\n })\n }\n\n async function assist(input: WritingAssistantInput): Promise<string> {\n const lang = locale.value\n const ko = lang === 'ko'\n const instruction = resolveInstruction(input, lang, t)\n if (!input.content.trim()) throw new Error(t('writingAssistant.contentRequired'))\n await reserveDailyUse()\n const moderation = config.moderation ?? {}\n const ai = getAI(app, {\n backend: new GoogleAIBackend(),\n useLimitedUseAppCheckTokens: moderation.useLimitedUseAppCheckTokens !== false,\n })\n const model = getGenerativeModel(ai, {\n model: moderation.model ?? 'gemini-3.5-flash-lite',\n systemInstruction: (ko\n ? [\n '너는 게시판 글쓰기 편집 도우미다.',\n '사용자가 요청한 편집 결과만 출력하고 설명, 인사, 마크다운 코드 펜스를 붙이지 마라.',\n input.action === 'title' || input.selection\n ? '출력은 일반 텍스트로 작성하라.'\n : '입력의 HTML 구조와 링크, 이미지 태그를 보존하고 결과도 HTML 조각으로 출력하라.',\n ]\n : [\n 'You are an editing assistant for writing message board posts.',\n 'Output only the requested edit result. Do not add explanations, greetings, or markdown code fences.',\n input.action === 'title' || input.selection\n ? 'Write the output as plain text.'\n : 'Preserve the HTML structure, links, and image tags of the input, and output the result as an HTML fragment as well.',\n ]).join('\\n'),\n generationConfig: {\n temperature: input.action === 'continue' || input.action === 'title' || input.action === 'custom' ? 0.6 : 0.2,\n maxOutputTokens: 2048,\n },\n })\n const source = input.content.trim()\n const prompt = (ko\n ? [\n `작업: ${instruction}`,\n input.title ? `현재 제목: ${input.title}` : '',\n `본문:\\n${source.slice(0, 12000)}`,\n ]\n : [\n `Task: ${instruction}`,\n input.title ? `Current title: ${input.title}` : '',\n `Body:\\n${source.slice(0, 12000)}`,\n ]).filter(Boolean).join('\\n\\n')\n const result = await model.generateContent(prompt)\n const output = cleanModelOutput(result.response.text())\n if (!output) throw new Error(t('writingAssistant.emptyResult'))\n return output\n }\n\n return { assist }\n}\n","import { getAI, getGenerativeModel, GoogleAIBackend, Schema } from 'firebase/ai'\nimport { useFirebaseApp } from 'vuefire'\nimport { useMemiBoardConfig } from '../config'\nimport {\n DEFAULT_MEMI_BOARD_LOCALE,\n MEMI_BOARD_LOCALES,\n normalizeMemiBoardLocale,\n type MemiBoardLocale,\n} from '../i18n/locales'\nimport { memiBoardLanguageLabel } from '../utils/moderation-prompt'\n\n/** 번역할 항목 하나. fields 의 키(label, description 등)별로 번역 결과가 돌아온다. */\nexport interface MemiBoardTranslateEntry {\n id: string\n fields: Record<string, string>\n /** 짧은 이름의 뜻을 잡아 주는 참고 정보(링크 주소, 상위 메뉴 이름 등). 번역하지 않는다. */\n context?: string\n}\n\n/** 언어별 번역 결과. `{ en: { label: 'About' }, ja: { label: '…' } }` */\nexport type MemiBoardLocalizedFields = Partial<Record<MemiBoardLocale, Record<string, string>>>\n\nexport interface MemiBoardTranslateOptions {\n /** 원문 언어. 기본 'ko' */\n source?: MemiBoardLocale\n /** 번역할 언어. 기본: 원문을 뺀 지원 언어 전부 */\n targets?: MemiBoardLocale[]\n /** 무엇을 번역하는지 한 줄 설명(예: 'website navigation menu labels'). 문체를 맞추는 데 쓴다. */\n purpose?: string\n}\n\nconst REQUEST_TIMEOUT_MS = 30_000\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`translation timed out after ${ms / 1000}s`)), ms)\n promise.then(\n (value) => { clearTimeout(timer); resolve(value) },\n (error) => { clearTimeout(timer); reject(error) },\n )\n })\n}\n\n/**\n * 짧은 UI 문구(메뉴 이름, 게시판 이름 등)를 Firebase AI Logic 으로 여러 언어로 번역한다.\n * 자동으로 저장하지 않는다 — 호출한 쪽이 결과를 보여 주고 저장 여부를 정한다.\n *\n * 항목 하나당 요청 하나. 응답 스키마를 `{ [locale]: { [field]: string } }` 로 고정해\n * 출력 길이가 일정하고, 모델이 같은 줄을 반복하다 토큰 한도에 걸리는 일이 없다.\n */\nexport function useMemiBoardAutoTranslate() {\n const app = useFirebaseApp()\n const { moderation } = useMemiBoardConfig()\n\n function getModel(targets: MemiBoardLocale[], fields: string[]) {\n const ai = getAI(app, {\n backend: new GoogleAIBackend(),\n useLimitedUseAppCheckTokens: moderation.useLimitedUseAppCheckTokens !== false,\n })\n const fieldSchema = Schema.object({\n properties: Object.fromEntries(fields.map(field => [field, Schema.string()])),\n })\n return getGenerativeModel(ai, {\n model: moderation.model ?? 'gemini-3.5-flash-lite',\n systemInstruction: [\n 'You translate short user-interface strings for a website.',\n 'Return natural, concise UI wording a native speaker would expect in a menu or title — not literal word-for-word translations.',\n 'Keep proper nouns, brand names, product names and game titles as they are unless they have a well-known localized name.',\n 'Use the context only to understand meaning; never translate or output it.',\n ].join('\\n'),\n generationConfig: {\n temperature: 0.2,\n maxOutputTokens: 2048,\n responseMimeType: 'application/json',\n responseSchema: Schema.object({\n properties: Object.fromEntries(targets.map(code => [code, fieldSchema])),\n }),\n },\n })\n }\n\n async function translateOne(\n entry: MemiBoardTranslateEntry,\n source: MemiBoardLocale,\n targets: MemiBoardLocale[],\n purpose: string,\n ): Promise<MemiBoardLocalizedFields> {\n const fields = Object.keys(entry.fields)\n const prompt = [\n `Purpose: ${purpose}`,\n `Source language: ${memiBoardLanguageLabel(source)} (${source})`,\n `Translate into: ${targets.map(code => `${code} = ${memiBoardLanguageLabel(code)}`).join(', ')}`,\n entry.context ? `Context (do not translate): ${entry.context}` : '',\n 'Text (JSON):',\n JSON.stringify(entry.fields),\n ].filter(Boolean).join('\\n')\n\n const result = await withTimeout(getModel(targets, fields).generateContent(prompt), REQUEST_TIMEOUT_MS)\n const parsed = JSON.parse(result.response.text()) as Record<string, Record<string, unknown> | undefined>\n\n const out: MemiBoardLocalizedFields = {}\n for (const code of targets) {\n const row = parsed?.[code]\n if (!row) continue\n const picked: Record<string, string> = {}\n for (const field of fields) {\n const text = typeof row[field] === 'string' ? (row[field] as string).trim() : ''\n if (text) picked[field] = text\n }\n if (Object.keys(picked).length) out[code] = picked\n }\n return out\n }\n\n /**\n * entries 를 targets 언어로 번역해 `{ [id]: { [locale]: { [field]: text } } }` 로 돌려준다.\n * 빈 필드는 번역하지 않는다. 하나라도 실패하면 reject 한다(30초 제한).\n */\n async function translateEntries(\n entries: MemiBoardTranslateEntry[],\n options: MemiBoardTranslateOptions = {},\n ): Promise<Record<string, MemiBoardLocalizedFields>> {\n const source = options.source ?? DEFAULT_MEMI_BOARD_LOCALE\n const targets = (options.targets ?? MEMI_BOARD_LOCALES.map(item => item.code))\n .filter(code => code !== source)\n const purpose = options.purpose ?? 'website UI labels'\n\n const cleaned = entries\n .map(entry => ({\n ...entry,\n fields: Object.fromEntries(\n Object.entries(entry.fields).map(([key, value]) => [key, value.trim()]).filter(([, value]) => value),\n ),\n }))\n .filter(entry => Object.keys(entry.fields).length)\n if (!cleaned.length || !targets.length) return {}\n\n const results = await Promise.all(cleaned.map(entry => translateOne(entry, source, targets, purpose)))\n return Object.fromEntries(cleaned.map((entry, index) => [entry.id, results[index]!]))\n }\n\n return { translateEntries }\n}\n\n/**\n * 저장된 번역에서 현재 언어 값을 고른다. 현재 언어 → en → 원문 순.\n * `localizedText(item.i18n, locale, 'label', item.label)`\n */\nexport function localizedText(\n i18n: MemiBoardLocalizedFields | null | undefined,\n locale: string | null | undefined,\n field: string,\n fallback: string,\n): string {\n const code = normalizeMemiBoardLocale(locale)\n if (!code || code === DEFAULT_MEMI_BOARD_LOCALE) return fallback\n return i18n?.[code]?.[field] || i18n?.en?.[field] || fallback\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"],"mappings":"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GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wEACA,8CACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,0DACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,yDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,0DACA,0CACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,8CACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,yCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,+BACA,4BACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,qCACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,iDACA,8BACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,oCACA,oBACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,kCACA,8BACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,6BACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,0BACA,wBACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,yCACA,iCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,4DACA,4BACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,mCACA,uCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,mDACA,wCACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;EACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,+DACA,kDACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,kEACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,wCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY;GACV;GACA;GACA;GACA;EACF;EACA,MAAM;CACR;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,oCACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,gDACA,qEACF;CACF;CACA;EACE,SAAS;EACT,MAAM;EACN,YAAY,CACV,qEACA,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,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"}