memi-board 0.18.0 → 0.19.3

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