memi-board 0.11.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +163 -17
  2. package/dist/composables/fetchPublicSeo.d.ts +12 -0
  3. package/dist/composables/useMemiBoardPosts.d.ts +18 -2
  4. package/dist/composables/useMemiBoardSettings.d.ts +5 -1
  5. package/dist/composables/useMemiBoardUsers.d.ts +9 -1
  6. package/dist/composables/useMemiBoardViews.d.ts +11 -0
  7. package/dist/config.d.ts +23 -0
  8. package/dist/index.d.ts +10 -3
  9. package/dist/index.js +922 -626
  10. package/dist/index.js.map +1 -1
  11. package/dist/module.mjs +44 -13
  12. package/dist/module.mjs.map +1 -1
  13. package/dist/runtime/components/CategorySettings.vue +108 -12
  14. package/dist/runtime/components/CommentForm.vue +26 -5
  15. package/dist/runtime/components/CommentItem.vue +1 -0
  16. package/dist/runtime/components/CommentList.vue +2 -1
  17. package/dist/runtime/components/CommentThread.vue +2 -1
  18. package/dist/runtime/components/Detail.vue +25 -6
  19. package/dist/runtime/components/Editor.vue +36 -11
  20. package/dist/runtime/components/Footer.vue +1 -1
  21. package/dist/runtime/components/List.vue +85 -5
  22. package/dist/runtime/components/ListDefault.vue +81 -24
  23. package/dist/runtime/components/ListDense.vue +112 -0
  24. package/dist/runtime/components/ListImage.vue +35 -22
  25. package/dist/runtime/components/ListVideo.vue +31 -15
  26. package/dist/runtime/components/ListViewSwitch.vue +30 -0
  27. package/dist/runtime/components/OptionCards.vue +30 -0
  28. package/dist/runtime/components/Settings.vue +86 -11
  29. package/dist/runtime/components/Users.vue +1 -1
  30. package/dist/runtime/composables/useMemiBoardSeo.ts +247 -0
  31. package/dist/types.d.ts +28 -1
  32. package/dist/utils/boardSeo.d.ts +35 -0
  33. package/dist/utils/postPreview.d.ts +8 -0
  34. package/docs/firestore.rules.example +85 -16
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # memi-board
2
2
 
3
- Nuxt 4 + Nuxt UI + Firebase 프로젝트에 **클라이언트 전용 게시판**을 붙이는 npm 패키지.
3
+ Nuxt 4 + Nuxt UI + Firebase 프로젝트에 게시판을 붙이는 npm 패키지.
4
4
 
5
- - 게시글 CRUD · 댓글 · Storage 첨부 · 카테고리
5
+ - 게시글 CRUD · 댓글 · Storage 첨부 · 카테고리 · 조회수
6
6
  - 작성자/관리자 권한 (Firestore Rules)
7
7
  - 로컬 비속어 + Firebase AI Logic(Gemini) 검열 (선택)
8
+ - **공유 미리보기(OG)** — 호스트 SEO API 없이 목록·상세 메타 (`useMemiBoard*Seo`)
8
9
 
9
- **서버 / Firebase Admin 불필요.** 권한은 Rules, 검열은 브라우저(우회 가능 — 아래 한계 참고).
10
+ **서버 / Firebase Admin 불필요** (권한은 Rules, 검열은 브라우저 — 우회 가능, 아래 한계 참고).
11
+ 공개 목록·글 상세만 SSR로 OG를 채우고, 쓰기·설정은 CSR로 두면 된다.
10
12
 
11
13
  같은 스택(Nuxt 4 · `@nuxt/ui` · `nuxt-vuefire` · Firebase)이면 **다른 호스트 앱에도 그대로 재사용** 가능하다. Firebase 프로젝트는 호스트마다 달라도 되고, 같은 프로젝트를 공유해도 `collectionPrefix`로 컬렉션만 나누면 된다.
12
14
 
@@ -36,7 +38,7 @@ Nuxt 4 + Nuxt UI + Firebase 프로젝트에 **클라이언트 전용 게시판**
36
38
  ├── nuxt-vuefire + firebaseConfig ← 인증·Firestore
37
39
  ├── (선택) App Check 플러그인 ← AI Logic Enforce 시 필수에 가까움
38
40
  ├── memi-board ← Nuxt 모듈 + 런타임 설정
39
- ├── pages/board/* ← 라우트는 호스트 소유
41
+ ├── pages/board/* ← 라우트는 호스트 소유 (+ SEO 한 줄)
40
42
  └── firestore.rules + storage.rules ← docs 예시 병합 후 배포
41
43
  ```
42
44
 
@@ -50,7 +52,14 @@ pnpm add memi-board @nuxt/ui nuxt-vuefire vuefire firebase dayjs
50
52
 
51
53
  `vuefire` / `firebase` / `nuxt-vuefire` / `dayjs`는 **호스트에 직접 설치**한다. TipTap은 `@nuxt/ui`와 `memi-board`가 관리하므로 호스트에서 별도로 설치하거나 버전을 맞출 필요가 없다.
52
54
 
53
- ### 1. modules + vuefire
55
+ ---
56
+
57
+ ## 부모(호스트) 앱 설정
58
+
59
+ 호스트가 하는 일만 정리한다. **호스트 전용 SEO API·서버 라우트는 필요 없다.**
60
+ Firestore `listed === true` 공개 글을 패키지가 직접 읽고 `useSeoMeta`로 OG를 채운다.
61
+
62
+ ### 1. `nuxt.config.ts` — modules + `memiBoard` + vuefire + routeRules
54
63
 
55
64
  ```ts
56
65
  // nuxt.config.ts
@@ -60,6 +69,7 @@ export default defineNuxtConfig({
60
69
  'nuxt-vuefire',
61
70
  'memi-board',
62
71
  ],
72
+
63
73
  memiBoard: {
64
74
  // Firestore: {prefix}Posts, {prefix}Users, {prefix}Settings
65
75
  collectionPrefix: 'board',
@@ -71,8 +81,27 @@ export default defineNuxtConfig({
71
81
  moderateImages: false,
72
82
  useLimitedUseAppCheckTokens: false,
73
83
  // localBlocklist: ['금칙어'],
84
+ // blockBanThreshold: 3,
85
+ // blockBanDecayMs: 24 * 60 * 60 * 1000,
86
+ },
87
+ // 공유 미리보기(OG) — 호스트 API 없이 패키지 composable 사용
88
+ seo: {
89
+ enabled: true,
90
+ siteName: 'My App', // og:site_name · title 접미사
91
+ siteUrl: process.env.NUXT_PUBLIC_SITE_URL
92
+ || 'https://example.com', // 절대 URL origin (trailing slash 없음)
93
+ defaultOgImage: '/images/og-default.jpg', // 글에 previewImage 없을 때
94
+ basePath: '/board', // canonical 경로 prefix
74
95
  },
75
96
  },
97
+
98
+ // (선택) 호스트 전역 siteUrl — SEO가 memiBoard.seo.siteUrl 다음으로 참고할 수 있음
99
+ runtimeConfig: {
100
+ public: {
101
+ siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'https://example.com',
102
+ },
103
+ },
104
+
76
105
  vuefire: {
77
106
  // 콘솔 → 프로젝트 설정 → 내 앱 → 웹 앱 SDK snippet 과 동일해야 함
78
107
  // (appId 가 다른 웹 앱이면 App Check / AI Logic 이 실패한다)
@@ -86,18 +115,112 @@ export default defineNuxtConfig({
86
115
  },
87
116
  auth: { enabled: true },
88
117
  },
118
+
89
119
  routeRules: {
90
- // 게시판은 Firebase Auth/Firestore를 사용하는 클라이언트 전용 화면이다.
91
- '/board/**': { ssr: false },
92
- '/board-settings': { ssr: false },
120
+ // 공개 목록·글 상세: SSR 유지(기본) → 카톡/OG 크롤러가 HTML 메타를 읽음
121
+ // 쓰기·설정만 CSR
122
+ '/board/**/new': { ssr: false },
123
+ '/board/**/**/edit': { ssr: false },
124
+ '/board/version-history': { ssr: false },
125
+ '/board-settings/**': { ssr: false },
93
126
  '/board-users': { ssr: false },
94
127
  },
95
128
  })
96
129
  ```
97
130
 
98
- `memiBoard` 옵션은 Nuxt 모듈이 런타임 플러그인으로 자동 전달한다. `app/plugins/memi-board.ts`를 만들지 않는다.
131
+ `memiBoard` 옵션은 Nuxt 모듈이 런타임 플러그인으로 자동 전달한다. **`app/plugins/memi-board.ts`를 만들지 않는다.**
132
+
133
+ #### `memiBoard` 옵션 요약
99
134
 
100
- ### 2. Tailwind `@source` (필수)
135
+ | 키 | 기본 | 설명 |
136
+ |----|------|------|
137
+ | `collectionPrefix` | `'memiBoard'` | Firestore/Storage 접두 (`board` → `boardPosts` …) |
138
+ | `auth.providers` | `['google','apple']` | 로그인 공급자 |
139
+ | `moderation.*` | (아래 AI 절) | 로컬·Gemini 검열 |
140
+ | `seo.enabled` | `true` | `false`면 SEO composable no-op |
141
+ | `seo.siteName` | `'Board'` | OG 사이트명·title 접미사 |
142
+ | `seo.siteUrl` | `''` | 절대 URL origin. 비우면 `runtimeConfig.public.siteUrl` → 요청 origin 순 |
143
+ | `seo.defaultOgImage` | `''` | 대표 이미지 없을 때 (상대경로 또는 `https://…`) |
144
+ | `seo.basePath` | `'/board'` | canonical: `{base}`, `{base}/{category}`, `{base}/{category}/{slug}` |
145
+
146
+ #### routeRules 원칙
147
+
148
+ | 경로 | SSR | 이유 |
149
+ |------|-----|------|
150
+ | `/board`, `/board/{category}`, `/board/{category}/{slug}` | **on** (기본) | OG/카톡 미리보기 |
151
+ | `/board/**/new`, `…/edit`, 설정·유저 관리 | **off** | 로그인·권한 UI, SEO 불필요 |
152
+
153
+ > 예전 가이드의 `/board/**: { ssr: false }` 전부 CSR은 **공유 미리보기가 비게 된다.** 공개 읽기 경로는 SSR을 켠다.
154
+
155
+ ### 2. 페이지에서 SEO 한 줄 (auto-import)
156
+
157
+ 모듈이 `useMemiBoardListSeo` / `useMemiBoardPostSeo`를 auto-import한다.
158
+ **호스트 `/server/api/board/**` SEO 엔드포인트는 만들지 않는다.**
159
+
160
+ ```vue
161
+ <!-- app/pages/board/index.vue — 전체 목록 -->
162
+ <script setup lang="ts">
163
+ await useMemiBoardListSeo({ path: '/board' })
164
+ // … MemiBoardList 등
165
+ </script>
166
+ ```
167
+
168
+ ```vue
169
+ <!-- app/pages/board/[category]/index.vue — 카테고리 목록 -->
170
+ <script setup lang="ts">
171
+ const route = useRoute()
172
+ const categoryId = computed(() => String(route.params.category || ''))
173
+
174
+ await useMemiBoardListSeo({
175
+ category: categoryId,
176
+ path: computed(() => `/board/${encodeURIComponent(categoryId.value)}`),
177
+ })
178
+ </script>
179
+ ```
180
+
181
+ ```vue
182
+ <!-- app/pages/board/[category]/[id]/index.vue — 글 상세 (id 자리 = slug) -->
183
+ <script setup lang="ts">
184
+ const route = useRoute()
185
+ const categoryId = computed(() => String(route.params.category || ''))
186
+ const id = computed(() => String(route.params.id || ''))
187
+
188
+ const { post: postSeo } = await useMemiBoardPostSeo({
189
+ category: categoryId,
190
+ slug: id,
191
+ path: computed(() =>
192
+ `/board/${encodeURIComponent(categoryId.value)}/${encodeURIComponent(id.value)}`,
193
+ ),
194
+ })
195
+ // postSeo: id · title · previewImage 등 — 상세 로드와 공유 가능
196
+ </script>
197
+ ```
198
+
199
+ | composable | 대상 | OG 요약 |
200
+ |------------|------|---------|
201
+ | `useMemiBoardListSeo` | 목록 | 카테고리 label·description, 최근 공개 글 이미지 |
202
+ | `useMemiBoardPostSeo` | 상세 | 제목·요약·작성자, **`previewImage` → og:image** |
203
+
204
+ - 공개(`listed`) 글만 조회. **숨김 게시판**은 메타를 비우고 `noindex` 쪽에 가깝게 동작한다.
205
+ - 글 상세 `og:type` = `article`, 목록 = `website`.
206
+ - 로컬 확인:
207
+ `curl -sL 'http://localhost:3000/board/…' | grep og:title`
208
+ 또는 브라우저 **페이지 소스 보기** (`view-source:`). DevTools Elements만 보면 하이드레이션 이후와 헷갈릴 수 있다.
209
+ - 카톡·슬랙 미리보기는 **배포 URL**로 확인 (localhost 크롤 불가).
210
+
211
+ ### 3. 부모 설정 체크리스트
212
+
213
+ - [ ] `modules`에 `@nuxt/ui`, `nuxt-vuefire`, `memi-board`
214
+ - [ ] `memiBoard.collectionPrefix` · `auth` · `moderation` · **`seo`**
215
+ - [ ] `seo.siteUrl` / `seo.siteName` / `seo.defaultOgImage` / `seo.basePath`
216
+ - [ ] 공개 board 경로 **SSR on**, 쓰기·설정만 `ssr: false`
217
+ - [ ] 목록·상세 페이지에 `await useMemiBoard*Seo(…)` 한 줄
218
+ - [ ] 호스트 SEO API **만들지 않음**
219
+ - [ ] CSS `@source` (아래)
220
+ - [ ] Rules·indexes **deploy**
221
+ - [ ] 최초 admin + 첫 카테고리
222
+
223
+ ### 4. Tailwind `@source` (필수)
101
224
 
102
225
  패키지 SFC 안의 클래스가 purge되지 않도록:
103
226
 
@@ -108,7 +231,7 @@ export default defineNuxtConfig({
108
231
  @source "../../../node_modules/memi-board/dist/runtime/components";
109
232
  ```
110
233
 
111
- ### 3. Security Rules + 인덱스 배포
234
+ ### 5. Security Rules + 인덱스 배포
112
235
 
113
236
  1. [docs/firestore.rules.example](docs/firestore.rules.example) → 호스트 `firestore.rules`에 병합
114
237
  (`memiBoard*` 이름을 쓰는 `collectionPrefix`에 맞게 치환)
@@ -142,7 +265,7 @@ export default defineNuxtConfig({
142
265
 
143
266
  > ⚠️ npm 패키지 tarball에도 `docs/` 가 포함된다(0.4.7+). 구버전이면 GitHub의 `docs/`를 보면 된다.
144
267
 
145
- ### 4. 최초 관리자와 카테고리 만들기
268
+ ### 6. 최초 관리자와 카테고리 만들기
146
269
 
147
270
  Rules는 일반 사용자가 스스로 관리자가 되는 것을 막는다. 따라서 최초 한 번은 다음 순서가 필요하다.
148
271
 
@@ -270,7 +393,11 @@ const router = useRouter()
270
393
  본문 HTML에는 원본 URL. 수정 중 버려진 파일은 호스트 스케줄러로 정리 (`extractEditorImageUrls` 로 본문 참조 비교).
271
394
 
272
395
  **composables:**
273
- `useMemiBoardAuth`, `useMemiBoardPosts`, `useMemiBoardComments`, `useMemiBoardStorage`, `useMemiBoardModeration`, `useMemiBoardSettings`, `useMemiBoardUsers`
396
+ `useMemiBoardAuth`, `useMemiBoardPosts`, `useMemiBoardComments`, `useMemiBoardStorage`, `useMemiBoardModeration`, `useMemiBoardSettings`, `useMemiBoardUsers`, `useMemiBoardViews`
397
+ **SEO (auto-import, 호스트 파이프라인):**
398
+ `useMemiBoardListSeo`, `useMemiBoardPostSeo`
399
+ **SEO fetch (필요 시):**
400
+ `fetchPublicListForSeo`, `fetchPublicPostForSeo`, `resolvePublicSeoDb`
274
401
 
275
402
  댓글은 시간순으로 10개씩 cursor 조회하며, 실제 `댓글 더보기` 버튼이 화면에 들어오면 1초 후 자동으로 다음 페이지를 읽는다. 기존 댓글을 모두 읽은 뒤에만 최신 댓글 1개를 실시간 구독한다. 대댓글은 5개씩 조회하고 모두 읽은 뒤 최신 답글 1개를 실시간 구독하며, 화면 깊이는 최대 2단계로 표시한다.
276
403
 
@@ -361,11 +488,13 @@ export default defineNuxtPlugin(() => {
361
488
  - [ ] 동일 스택: Nuxt 4 + `@nuxt/ui` 4 + `nuxt-vuefire` + Firebase
362
489
  - [ ] `pnpm add memi-board @nuxt/ui nuxt-vuefire vuefire firebase dayjs`
363
490
  - [ ] `modules`에 `@nuxt/ui`, `nuxt-vuefire`, `memi-board`
364
- - [ ] 게시판·관리 경로에 `ssr: false`
365
- - [ ] `nuxt.config.ts`의 `memiBoard` 설정 (`collectionPrefix` 충돌 없게)
491
+ - [ ] `memiBoard` (`collectionPrefix` · `auth` · `moderation` · **`seo`**)
492
+ - [ ] 공개 목록·상세 **SSR on**, 쓰기·설정만 `ssr: false` (전 경로 CSR 금지)
493
+ - [ ] 목록·상세 페이지에 `await useMemiBoardListSeo` / `useMemiBoardPostSeo`
494
+ - [ ] 호스트 SEO API 없음
366
495
  - [ ] CSS `@source` …/memi-board/dist/runtime/components
367
496
  - [ ] Rules 예시 병합 + **deploy**
368
- - [ ] 카테고리 필터 사용 시 indexes deploy
497
+ - [ ] 카테고리·listed 쿼리 사용 시 indexes deploy
369
498
  - [ ] 페이지·미들웨어 직접 작성 (또는 playground 복사)
370
499
  - [ ] 최초 로그인 후 Firebase Console에서 `{prefix}Users/{uid}.role = 'admin'`
371
500
  - [ ] 설정 페이지에서 첫 카테고리 생성
@@ -401,7 +530,15 @@ pnpm build
401
530
  pnpm add link:../memi-board
402
531
  ```
403
532
 
404
- `src/components` 수정은 Nuxt 모듈이 링크 소스를 직접 읽으므로 즉시 반영된다. `src/module.ts` 또는 core composable을 수정하면 `pnpm build`를 다시 실행하고 호스트 개발 서버를 재시작한다. 모듈이 Vue·Vue Router·VueFire·Firebase·TipTap dedupe와 `dayjs` 최적화를 자동 적용한다. 배포 전에는 의존성을 **npm 버전**으로 되돌린다.
533
+ | 수정 대상 | 반영 |
534
+ |-----------|------|
535
+ | `src/components/*` | 모듈이 링크 소스 직독 → **즉시** (dev HMR) |
536
+ | `useMemiBoard*Seo` (`src/composables/useMemiBoardSeo.ts`) | 호스트 Nuxt가 소스 컴파일 → **즉시** (dev 재시작이 필요할 수 있음) |
537
+ | `src/module.ts` · 그 외 core composable (`dist`) | **`pnpm build`** 후 호스트 dev 재시작 |
538
+
539
+ SEO composable은 `#imports`로 호스트 Nuxt 인스턴스를 쓴다. `dist/index.js`에 `nuxt/app`을 넣지 않으므로 link 시 Nuxt 이중 로딩(E1001)이 나지 않는다.
540
+
541
+ 모듈이 Vue·Vue Router·VueFire·Firebase·TipTap dedupe와 `dayjs` 최적화를 자동 적용한다. 배포 전에는 의존성을 **npm 버전**으로 되돌린다.
405
542
 
406
543
  링크 상태에서 새 Tailwind 클래스를 바로 시험하려면 호스트 CSS에 소스 경로를 하나 더 둔다.
407
544
 
@@ -410,6 +547,15 @@ pnpm add link:../memi-board
410
547
  @source "../../../node_modules/memi-board/dist/runtime/components";
411
548
  ```
412
549
 
550
+ ### OG 메타가 비어 있거나 카톡 미리보기가 안 될 때
551
+
552
+ 1. 공개 경로가 `ssr: false`로 꺼져 있지 않은지 (`routeRules`)
553
+ 2. 페이지에 `await useMemiBoardListSeo` / `useMemiBoardPostSeo` 호출 여부
554
+ 3. `memiBoard.seo.siteUrl` · `siteName` · `defaultOgImage`
555
+ 4. `curl` / view-source 에 `og:title` 이 있는지 (Elements만 보지 말 것)
556
+ 5. 글이 `listed` / 숨김 게시판이 아닌지 (숨김은 noindex)
557
+ 6. 배포 URL로 재스크랩 (카톡·FB 캐시)
558
+
413
559
  ## 로컬 개발 (이 리포)
414
560
 
415
561
  ```bash
@@ -0,0 +1,12 @@
1
+ import { Firestore } from 'firebase/firestore';
2
+ import { BoardListSeoPayload, BoardPostSeoPayload } from '../utils/boardSeo';
3
+ export type PublicSeoDb = {
4
+ db: Firestore;
5
+ prefix: string;
6
+ };
7
+ /** setup 동기 구간에서만 호출 — useFirestore + config 캡처 */
8
+ export declare function resolvePublicSeoDb(): PublicSeoDb;
9
+ /** 공개 글 1건 (category + slug). 없거나 숨김이면 null. */
10
+ export declare function fetchPublicPostForSeo(category: string, slug: string, ctx?: PublicSeoDb): Promise<BoardPostSeoPayload | null>;
11
+ /** 전체 또는 카테고리 공개 목록 SEO 페이로드. */
12
+ export declare function fetchPublicListForSeo(category?: string | null, ctx?: PublicSeoDb): Promise<BoardListSeoPayload>;
@@ -21,6 +21,19 @@ export interface UpdatePostInput {
21
21
  category?: string;
22
22
  attachments?: Attachment[];
23
23
  }
24
+ /** getPostBySlug 결과 — not-found 와 permission-denied 를 UI 에서 구분한다. */
25
+ export type GetPostBySlugResult = {
26
+ status: 'ok';
27
+ post: PostDetail;
28
+ } | {
29
+ status: 'not-found';
30
+ } | {
31
+ status: 'permission-denied';
32
+ } | {
33
+ status: 'error';
34
+ message: string;
35
+ };
36
+ export declare function isFirestorePermissionDenied(error: unknown): boolean;
24
37
  /** 목록 조회는 posts/{id} 메타만 읽고, 본문(body/main)은 상세 조회 시에만 읽는다. */
25
38
  export declare function useMemiBoardPosts(): {
26
39
  createPostId: () => string;
@@ -29,13 +42,15 @@ export declare function useMemiBoardPosts(): {
29
42
  cursor?: QueryDocumentSnapshot<DocumentData>;
30
43
  /** 지정 시 해당 카테고리 글만 (boardSettings 의 category id) */
31
44
  category?: string;
45
+ /** true 면 listed==true 만 (비권한 목록). staff 전체 보기는 false */
46
+ publicOnly?: boolean;
32
47
  }) => Promise<{
33
48
  posts: PostModel[];
34
49
  cursor: QueryDocumentSnapshot<DocumentData> | undefined;
35
50
  hasMore: boolean;
36
51
  }>;
37
52
  getPost: (id: string) => Promise<PostDetail | null>;
38
- getPostBySlug: (category: string, slug: string) => Promise<PostDetail | null>;
53
+ getPostBySlug: (category: string, slug: string) => Promise<GetPostBySlugResult>;
39
54
  getAdjacentPosts: (current: PostModel) => Promise<{
40
55
  previous: PostModel | null;
41
56
  next: PostModel | null;
@@ -54,7 +69,8 @@ export declare function useMemiBoardPosts(): {
54
69
  * 이 정도 트래픽 폭이면 실사용에서 발생 가능성이 낮아 별도 보정 로직은 두지 않았다.
55
70
  */
56
71
  export declare function useMemiBoardPostList(category?: string | Ref<string | undefined>, options?: {
57
- pageSize?: number;
72
+ pageSize?: number; /** staff 등 숨김 글 포함 시 false. 기본 true */
73
+ publicOnly?: boolean | Ref<boolean>;
58
74
  }): {
59
75
  posts: import('vue').ComputedRef<PostModel[]>;
60
76
  postsPending: Ref<boolean, boolean>;
@@ -1,10 +1,14 @@
1
- import { BoardCategory } from '../types';
1
+ import { BoardCategory, BoardVisibility } from '../types';
2
2
  /** 필요할 때 호스트가 명시적으로 사용할 수 있는 예시 카테고리. 자동 적용하지 않는다. */
3
3
  export declare const DEFAULT_CATEGORIES: BoardCategory[];
4
4
  /** 게시판 카테고리 — `{prefix}Settings/config/categories/{categoryId}` 개별 문서. */
5
5
  export declare function useMemiBoardSettings(): {
6
6
  categories: import('vue').ComputedRef<BoardCategory[]>;
7
+ publicCategories: import('vue').ComputedRef<BoardCategory[]>;
7
8
  categoryLabel: (id: string | undefined) => string | undefined;
9
+ categoryDescription: (id: string | undefined) => string | undefined;
10
+ categoryVisibility: (id: string | undefined) => BoardVisibility;
11
+ isCategoryHidden: (id: string | undefined) => boolean;
8
12
  settingsPending: import('vue').Ref<boolean, boolean>;
9
13
  ensureSettings: () => Promise<void>;
10
14
  addCategory: (label: string) => Promise<string>;
@@ -1,10 +1,18 @@
1
+ import { MaybeRefOrGetter } from 'vue';
1
2
  import { BoardUserModel, BoardUserRole } from '../types';
2
3
  export declare const BOARD_USER_ROLES: Array<{
3
4
  value: BoardUserRole;
4
5
  label: string;
5
6
  description: string;
6
7
  }>;
7
- export declare function useMemiBoardUsers(): {
8
+ /**
9
+ * 전체 사용자 목록은 관리자만 읽을 수 있다(rules). enabled가 false인 동안은
10
+ * 쿼리 자체를 시작하지 않는다 — 그냥 호출만 하면 권한 확인 전에 구독이 걸려서
11
+ * 관리자가 아니거나 아직 role을 확인 중일 때 permission-denied로 화면이 죽는다.
12
+ */
13
+ export declare function useMemiBoardUsers(options?: {
14
+ enabled?: MaybeRefOrGetter<boolean>;
15
+ }): {
8
16
  users: import('vuefire')._RefFirestore<import('vuefire').VueFirestoreQueryData<BoardUserModel>>;
9
17
  usersPending: import('vue').Ref<boolean, boolean>;
10
18
  updateUserRole: (uid: string, role: BoardUserRole) => Promise<void>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 게시글 조회수.
3
+ *
4
+ * - 로그인 불필요: rules 가 viewCount +1 만 허용하면 익명도 증가 가능.
5
+ * - 신뢰 수준은 likeCount/commentCount 와 같은 클라이언트 편의 카운터.
6
+ * - 같은 브라우저·세션에서 같은 글을 반복 열어 폭증하지 않도록 sessionStorage 로 1회만 기록.
7
+ */
8
+ export declare function useMemiBoardViews(): {
9
+ recordView: (postId: string) => Promise<void>;
10
+ hasRecordedView: (postId: string) => boolean;
11
+ };
package/dist/config.d.ts CHANGED
@@ -17,10 +17,33 @@ export interface MemiBoardModerationOptions {
17
17
  /** 경고 1회 차감 간격(ms). 기본 24시간 */
18
18
  blockBanDecayMs?: number;
19
19
  }
20
+ /**
21
+ * 게시판 SEO (호스트 API 없이 Firestore 직조회 + useSeoMeta).
22
+ * 호스트는 siteUrl/siteName 과 공개 board 경로 SSR on 만 맞추면 된다.
23
+ */
24
+ export interface MemiBoardSeoOptions {
25
+ /** false 면 useMemiBoard*Seo 가 no-op. 기본 true */
26
+ enabled?: boolean;
27
+ /** og:site_name · title 접미사. 예: Loop Waiting */
28
+ siteName?: string;
29
+ /**
30
+ * 절대 URL용 origin. 예: https://loopwait.com
31
+ * 비우면 useRequestURL / NUXT_PUBLIC_SITE_URL 순으로 추론.
32
+ */
33
+ siteUrl?: string;
34
+ /** 글에 previewImage 없을 때. 상대경로 또는 https */
35
+ defaultOgImage?: string;
36
+ /**
37
+ * 호스트 게시판 base path (trailing slash 없음). 기본 '/board'
38
+ * canonical: {base}, {base}/{category}, {base}/{category}/{slug}
39
+ */
40
+ basePath?: string;
41
+ }
20
42
  export interface MemiBoardConfig {
21
43
  collectionPrefix: string;
22
44
  auth: MemiBoardAuthOptions;
23
45
  moderation: MemiBoardModerationOptions;
46
+ seo: MemiBoardSeoOptions;
24
47
  }
25
48
  /**
26
49
  * 앱 부팅 시(플러그인 등에서) 한 번 호출한다. 모든 요청/세션이 같은 값을 보므로
package/dist/index.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  /** Runtime API — composables / config / types only. UI SFC는 루트 Nuxt 모듈이 등록. */
2
2
  export { configureMemiBoard, useMemiBoardConfig } from './config';
3
- export type { MemiBoardConfig, MemiBoardAuthOptions, MemiBoardModerationOptions } from './config';
3
+ export type { MemiBoardConfig, MemiBoardAuthOptions, MemiBoardModerationOptions, MemiBoardSeoOptions, } from './config';
4
+ export { fetchPublicPostForSeo, fetchPublicListForSeo, resolvePublicSeoDb, } from './composables/fetchPublicSeo';
5
+ export type { PublicSeoDb } from './composables/fetchPublicSeo';
6
+ export { boardPostOgTitle, boardPostOgDescription, boardListOgTitle, boardListOgDescription, toAbsoluteUrl, asHttpUrl, normalizeBasePath, } from './utils/boardSeo';
7
+ export type { BoardPostSeoPayload, BoardListSeoPayload } from './utils/boardSeo';
4
8
  export { useMemiBoardAuth } from './composables/useMemiBoardAuth';
5
9
  export { useMemiBoardPosts, useMemiBoardPostList, useMemiBoardPost } from './composables/useMemiBoardPosts';
6
- export type { CreatePostInput, UpdatePostInput } from './composables/useMemiBoardPosts';
10
+ export type { CreatePostInput, UpdatePostInput, GetPostBySlugResult } from './composables/useMemiBoardPosts';
11
+ export { isFirestorePermissionDenied } from './composables/useMemiBoardPosts';
7
12
  export { COMMENT_BODY_MAX_LENGTH, useMemiBoardComments, useMemiBoardReplies } from './composables/useMemiBoardComments';
8
13
  export type { AddCommentInput, AddReplyInput } from './composables/useMemiBoardComments';
9
14
  export { useMemiBoardLikes } from './composables/useMemiBoardLikes';
15
+ export { useMemiBoardViews } from './composables/useMemiBoardViews';
10
16
  export { useMemiBoardModeration } from './composables/useMemiBoardModeration';
11
17
  export { useMemiBoardStorage } from './composables/useMemiBoardStorage';
12
18
  export { useMemiBoardSettings, DEFAULT_CATEGORIES } from './composables/useMemiBoardSettings';
@@ -16,9 +22,10 @@ export type { VersionHistoryEntry } from './data/versionHistory';
16
22
  export { formatDate, formatFullDate, formatRelativeDate, formatTimestampDetails } from './utils/formatDate';
17
23
  export { slugify } from './utils/slugify';
18
24
  export { DEFAULT_BLOCK_BAN_THRESHOLD, DEFAULT_BLOCK_BAN_DECAY_MS, effectiveModerationBlockCount, isModerationWriteRestricted, moderationWriteRestrictedUntilMs, formatRestrictedUntilLabel, } from './utils/moderation-strike';
19
- export type { Attachment, EditorImageEntry, ModerationStatus, PostModel, PostDetail, CommentModel, BoardUserRole, BoardUserModel, BoardCategory, BoardListView, BoardWriteRole, BoardSettingsModel, ModerationResult, } from './types';
25
+ export type { Attachment, EditorImageEntry, ModerationStatus, PostModel, PostDetail, CommentModel, BoardUserRole, BoardUserModel, BoardCategory, BoardListView, BoardWriteRole, BoardVisibility, BoardSettingsModel, ModerationResult, } from './types';
20
26
  export { compressImage } from './utils/compressImage';
21
27
  export { extractEditorImageUrls } from './utils/extractEditorImageUrls';
22
28
  export { renderMarkdownToHtml } from './utils/renderMarkdown';
29
+ export { buildPostPreview, youtubeId, videoListCoverUrl } from './utils/postPreview';
23
30
  export { storagePathFromDownloadUrl, postNamespaceFromStoragePath } from './utils/storagePath';
24
31
  export { EDITOR_IMAGE_MAX_BYTES, EDITOR_IMAGE_SOURCE_MAX_BYTES } from './composables/useMemiBoardStorage';