memi-board 0.22.0 → 0.23.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.
@@ -5,7 +5,6 @@ import { useMemiBoardStorage } from 'memi-board/runtime'
5
5
 
6
6
  const props = withDefaults(defineProps<{
7
7
  modelValue: Attachment[]
8
- boardId: string
9
8
  /** 새 글 작성 중에도 미리 생성한 Firestore 자동 ID를 넘긴다. */
10
9
  postId: string
11
10
  editable?: boolean
@@ -44,7 +43,7 @@ async function onFileInputChange(e: Event) {
44
43
  const uploaded: Attachment[] = []
45
44
  for (const file of toUpload) {
46
45
  uploadProgress.value = 0
47
- const { promise } = uploadAttachment(file, props.boardId, props.postId, (ratio) => {
46
+ const { promise } = uploadAttachment(file, props.postId, (ratio) => {
48
47
  uploadProgress.value = ratio
49
48
  })
50
49
  uploaded.push(await promise)
@@ -130,18 +130,19 @@ async function toggleBlind() {
130
130
  <div class="min-w-0 flex-1">
131
131
  <div class="flex items-center gap-2">
132
132
  <span class="text-sm font-medium">{{ comment.authorName ?? '익명' }}</span>
133
- <UTooltip
134
- :delay-duration="0"
135
- :text="timestampDetails"
136
- :ui="{ content: 'h-auto w-max py-2', text: 'whitespace-pre-line overflow-visible' }"
137
- >
133
+ <UPopover :content="{ side: 'top' }" :ui="{ content: 'h-auto w-max' }">
138
134
  <time
139
135
  :datetime="date?.toISOString()"
140
- class="cursor-help text-xs text-muted"
136
+ class="cursor-pointer text-xs text-muted"
141
137
  >
142
138
  {{ relativeDate }}
143
139
  </time>
144
- </UTooltip>
140
+ <template #content>
141
+ <p class="whitespace-pre-line px-3 py-2 text-xs">
142
+ {{ timestampDetails }}
143
+ </p>
144
+ </template>
145
+ </UPopover>
145
146
  </div>
146
147
  <form
147
148
  v-if="editing"
@@ -254,18 +254,19 @@ const contentHtml = computed(() => {
254
254
  <UIcon name="i-lucide-eye" class="size-3.5" />
255
255
  {{ post.viewCount ?? 0 }}
256
256
  </span>
257
- <UTooltip
258
- :delay-duration="0"
259
- :text="formatTimestampDetails(post.createdAt, post.updatedAt).join('\n')"
260
- :ui="{ content: 'h-auto w-max py-2', text: 'whitespace-pre-line overflow-visible' }"
261
- >
257
+ <UPopover :content="{ side: 'top' }" :ui="{ content: 'h-auto w-max' }">
262
258
  <time
263
259
  :datetime="post.createdAt?.toDate?.().toISOString()"
264
- class="cursor-help"
260
+ class="cursor-pointer"
265
261
  >
266
262
  {{ formatRelativeDate(post.createdAt, now) }}
267
263
  </time>
268
- </UTooltip>
264
+ <template #content>
265
+ <p class="whitespace-pre-line px-3 py-2 text-xs">
266
+ {{ formatTimestampDetails(post.createdAt, post.updatedAt).join('\n') }}
267
+ </p>
268
+ </template>
269
+ </UPopover>
269
270
  </div>
270
271
  <div v-if="canEdit(post) || canDelete(post)" class="flex gap-2">
271
272
  <UButton
@@ -231,7 +231,7 @@ async function doUploadImage(file: File): Promise<EditorImageEntry> {
231
231
  if (file.size > EDITOR_IMAGE_SOURCE_MAX_BYTES) {
232
232
  throw new Error(`원본 이미지는 ${EDITOR_IMAGE_SOURCE_MAX_BYTES / 1024 / 1024}MB 이하여야 합니다.`)
233
233
  }
234
- const entry = await uploadEditorImage(file, resolvedBoardId.value, imageNamespace())
234
+ const entry = await uploadEditorImage(file, imageNamespace())
235
235
  uploadedEditorImages.value.push(entry)
236
236
  return entry
237
237
  }
@@ -907,7 +907,6 @@ async function handleSubmit() {
907
907
  <MemiBoardAttachments
908
908
  v-if="!isImageListView"
909
909
  v-model="attachments"
910
- :board-id="resolvedBoardId"
911
910
  :post-id="attachmentNamespace"
912
911
  editable
913
912
  />
@@ -36,8 +36,8 @@ function image(post: PostModel): string | undefined {
36
36
  class="flex min-w-0 flex-1 text-left transition-colors hover:bg-elevated/50"
37
37
  @click="!postTo(post) && emit('select', post)"
38
38
  >
39
- <!-- 카드 좌·상·하 밀착, 가로는 고정 / 세로는 행 높이 전체 -->
40
- <div class="w-[4.5rem] shrink-0 self-stretch bg-elevated">
39
+ <!-- 카드 좌·상 밀착, 항상 정사각형 — 텍스트가 길어져도 썸네일 비율은 유지 -->
40
+ <div class="size-[4.5rem] shrink-0 self-start bg-elevated">
41
41
  <img
42
42
  v-if="image(post)"
43
43
  :src="image(post)"
@@ -46,7 +46,7 @@ function image(post: PostModel): string | undefined {
46
46
  >
47
47
  <div
48
48
  v-else
49
- class="flex h-full min-h-[4.5rem] w-full items-center justify-center text-muted"
49
+ class="flex h-full w-full items-center justify-center text-muted"
50
50
  >
51
51
  <UIcon name="i-lucide-image" class="size-6" />
52
52
  </div>
@@ -103,18 +103,19 @@ function image(post: PostModel): string | undefined {
103
103
  truncate
104
104
  class="max-w-full"
105
105
  />
106
- <UTooltip
107
- :delay-duration="0"
108
- :text="formatTimestampDetails(post.createdAt, post.updatedAt).join('\n')"
109
- :ui="{ content: 'h-auto w-max py-2', text: 'whitespace-pre-line overflow-visible' }"
110
- >
106
+ <UPopover :content="{ side: 'top' }" :ui="{ content: 'h-auto w-max' }">
111
107
  <time
112
108
  :datetime="post.createdAt?.toDate?.().toISOString()"
113
- class="cursor-help text-xs leading-snug text-muted"
109
+ class="cursor-pointer text-xs leading-snug text-muted"
114
110
  >
115
111
  {{ formatRelativeDate(post.createdAt, now) }}
116
112
  </time>
117
- </UTooltip>
113
+ <template #content>
114
+ <p class="whitespace-pre-line px-3 py-2 text-xs">
115
+ {{ formatTimestampDetails(post.createdAt, post.updatedAt).join('\n') }}
116
+ </p>
117
+ </template>
118
+ </UPopover>
118
119
  <div class="flex flex-wrap items-center justify-end gap-x-2 gap-y-0.5 text-xs leading-snug text-muted">
119
120
  <span class="inline-flex items-center gap-0.5 tabular-nums">
120
121
  <UIcon name="i-lucide-eye" class="size-3 shrink-0" />{{ post.viewCount ?? 0 }}
@@ -93,18 +93,19 @@ function thumb(post: PostModel): string | undefined {
93
93
  />
94
94
  </span>
95
95
  <span class="text-default/40" aria-hidden="true">·</span>
96
- <UTooltip
97
- :delay-duration="0"
98
- :text="formatTimestampDetails(post.createdAt, post.updatedAt).join('\n')"
99
- :ui="{ content: 'h-auto w-max py-2', text: 'whitespace-pre-line overflow-visible' }"
100
- >
96
+ <UPopover :content="{ side: 'top' }" :ui="{ content: 'h-auto w-max' }">
101
97
  <time
102
98
  :datetime="post.createdAt?.toDate?.().toISOString()"
103
- class="cursor-help tabular-nums"
99
+ class="cursor-pointer tabular-nums"
104
100
  >
105
101
  {{ formatRelativeDate(post.createdAt, now) }}
106
102
  </time>
107
- </UTooltip>
103
+ <template #content>
104
+ <p class="whitespace-pre-line px-3 py-2 text-xs">
105
+ {{ formatTimestampDetails(post.createdAt, post.updatedAt).join('\n') }}
106
+ </p>
107
+ </template>
108
+ </UPopover>
108
109
  <span class="text-default/40" aria-hidden="true">·</span>
109
110
  <span class="inline-flex items-center gap-0.5 tabular-nums">
110
111
  <UIcon name="i-lucide-eye" class="size-3" />
@@ -75,18 +75,19 @@ function cover(post: PostModel): string | undefined {
75
75
  <UIcon name="i-lucide-eye" class="size-3" />
76
76
  {{ post.viewCount ?? 0 }}
77
77
  </span>
78
- <UTooltip
79
- :delay-duration="0"
80
- :text="formatTimestampDetails(post.createdAt, post.updatedAt).join('\n')"
81
- :ui="{ content: 'h-auto w-max py-2', text: 'whitespace-pre-line overflow-visible' }"
82
- >
78
+ <UPopover :content="{ side: 'top' }" :ui="{ content: 'h-auto w-max' }">
83
79
  <time
84
80
  :datetime="post.createdAt?.toDate?.().toISOString()"
85
- class="cursor-help"
81
+ class="cursor-pointer"
86
82
  >
87
83
  {{ formatRelativeDate(post.createdAt, now) }}
88
84
  </time>
89
- </UTooltip>
85
+ <template #content>
86
+ <p class="whitespace-pre-line px-3 py-2 text-xs">
87
+ {{ formatTimestampDetails(post.createdAt, post.updatedAt).join('\n') }}
88
+ </p>
89
+ </template>
90
+ </UPopover>
90
91
  </div>
91
92
  </div>
92
93
  </div>
package/dist/types.d.ts CHANGED
@@ -8,7 +8,7 @@ export interface Attachment {
8
8
  }
9
9
  /**
10
10
  * 에디터 본문 이미지 업로드 결과 (원본 + 썸네일).
11
- * Storage: `memiBoards/{boardId}/posts/{postId}/images/...` · `.../images/thumbnails/...`
11
+ * Storage: `memiBoardPosts/{postId}/images/...` · `.../images/thumbnails/...`
12
12
  * 본문 markdown 에는 originalUrl 만 넣고, 썸네일은 목록 카드 등에 활용 가능.
13
13
  * 수정 중 버려진 파일은 호스트 스케줄러로 정리 (본문에 없는 path).
14
14
  */
@@ -22,6 +22,8 @@ export interface EditorImageEntry {
22
22
  export type ModerationStatus = 'approved';
23
23
  export interface PostModel {
24
24
  id?: string;
25
+ /** 어느 보드(memiBoardSettings/{boardId})에 속한 글인지 — flat 컬렉션이라 경로 대신 필드로 스코핑한다. */
26
+ boardId: string;
25
27
  /** 카테고리 내 URL 식별자. 최초 작성 시 생성되고 제목 수정 시에도 유지된다. */
26
28
  slug: string;
27
29
  title: string;
@@ -32,14 +34,14 @@ export interface PostModel {
32
34
  videoUrl?: string;
33
35
  tags?: string[];
34
36
  /**
35
- * @deprecated 카테고리 개념 제거 — 글은 memiBoards/{boardId}/posts 에만 존재.
37
+ * @deprecated 카테고리 개념 제거 — boardId 로 대체.
36
38
  * 레거시 읽기 호환용으로만 남을 수 있음.
37
39
  */
38
40
  category?: string;
39
41
  attachments?: Attachment[];
40
42
  /** 클라이언트 batch로 증감하는 UI 편의 필드 — 보안 판단에 쓰지 않는다. */
41
43
  commentCount: number;
42
- /** 클라이언트 트랜잭션으로 증감하는 UI 편의 필드 — 보안 판단에 쓰지 않는다. 실제 좋아요 여부는 likes/{uid} 서브컬렉션이 기준. */
44
+ /** 클라이언트 트랜잭션으로 증감하는 UI 편의 필드 — 보안 판단에 쓰지 않는다. 실제 좋아요 여부는 memiBoardLikes 컬렉션이 기준. */
43
45
  likeCount: number;
44
46
  /**
45
47
  * 조회수. 로그인 없이 +1 가능(rules 가 viewCount 단독 증가만 허용).
@@ -69,13 +71,17 @@ export interface PostModel {
69
71
  export interface PostDetail extends PostModel {
70
72
  content: string;
71
73
  }
72
- /** 보드 경계를 넘는 작성자별 글 목록(예: /board-user/{uid})에서만 쓰는 형태 — 어느 보드 글인지 함께 담는다. */
73
- export interface UserPostModel extends PostModel {
74
- boardId: string;
75
- }
74
+ /**
75
+ * @deprecated PostModel 에 boardId 가 이미 있어 더 이상 별도 타입이 필요 없다.
76
+ * 보드 경계를 넘는 작성자별 글 목록(예: /board-user/{uid}) 등에서 PostModel 을 그대로 쓰면 된다.
77
+ */
78
+ export type UserPostModel = PostModel;
76
79
  export interface CommentModel {
77
80
  id?: string;
81
+ /** 속한 글. flat 컬렉션이라 경로 대신 필드로 스코핑한다. */
78
82
  postId: string;
83
+ /** 부모 글의 boardId — 생성 시 클라이언트가 채우고, rules 가 부모 글과 일치하는지 검증한다. */
84
+ boardId: string;
79
85
  body: string;
80
86
  authorUid: string;
81
87
  authorName: string | null;
@@ -116,7 +122,7 @@ export type BoardWriteRole = 'user' | 'staff' | 'admin';
116
122
  export type BoardVisibility = 'public' | 'hidden';
117
123
  /**
118
124
  * 보드 1개 메타 (= 예전 BoardCategory).
119
- * id 가 곧 boardId — memiBoards/{id}/settings/config 에 저장.
125
+ * id 가 곧 boardId — memiBoardSettings/{id} 에 저장.
120
126
  */
121
127
  export interface BoardModel {
122
128
  /** boardId (예: 'notice', 'free') */
@@ -141,10 +147,18 @@ export interface BoardModel {
141
147
  }
142
148
  /** @deprecated BoardModel 사용 — 카테고리 개념 제거, id = boardId */
143
149
  export type BoardCategory = BoardModel;
144
- /** memiBoards/{boardId}/settings/config */
150
+ /** memiBoardSettings/{boardId} */
145
151
  export interface BoardSettingsModel extends Omit<BoardModel, 'id'> {
146
152
  updatedAt?: Timestamp;
147
153
  }
154
+ /** memiBoardLikes/{postId}_{uid} — "내가 좋아요한 글" 등 uid 기준 교차 조회용 flat 컬렉션. */
155
+ export interface BoardLikeModel {
156
+ id?: string;
157
+ uid: string;
158
+ postId: string;
159
+ boardId: string;
160
+ createdAt: Timestamp;
161
+ }
148
162
  /** 검열이 어느 단계에서 끝났는지 (호스트/로깅용, UI 노출 없음) */
149
163
  export type ModerationVia = 'empty' | 'disabled' | 'local' | 'ai' | 'restricted' | 'ai-error-allow' | 'ai-error-block';
150
164
  export interface ModerationResult {
@@ -1,26 +1,37 @@
1
1
  import { CollectionReference, DocumentReference, Firestore } from 'firebase/firestore';
2
- export declare const DEFAULT_BOARDS_COLLECTION = "memiBoards";
2
+ export declare const DEFAULT_POSTS_COLLECTION = "memiBoardPosts";
3
+ export declare const DEFAULT_COMMENTS_COLLECTION = "memiBoardComments";
4
+ export declare const DEFAULT_LIKES_COLLECTION = "memiBoardLikes";
5
+ export declare const DEFAULT_SETTINGS_COLLECTION = "memiBoardSettings";
3
6
  export declare const DEFAULT_USERS_COLLECTION = "memiBoardUsers";
4
7
  export interface BoardPathConfig {
5
- boardsCollection: string;
8
+ postsCollection: string;
9
+ commentsCollection: string;
10
+ likesCollection: string;
11
+ settingsCollection: string;
6
12
  usersCollection: string;
7
13
  }
8
14
  export declare function resolveBoardPathConfig(input: {
9
- boardsCollection?: string;
15
+ postsCollection?: string;
16
+ commentsCollection?: string;
17
+ likesCollection?: string;
18
+ settingsCollection?: string;
10
19
  usersCollection?: string;
11
20
  }): BoardPathConfig;
12
- export declare function boardsCol(db: Firestore, cfg: BoardPathConfig): CollectionReference;
13
- export declare function boardDoc(db: Firestore, cfg: BoardPathConfig, boardId: string): DocumentReference;
14
- /** 구 boardSettings — 보드 단위 설정 문서 */
15
- export declare function boardSettingsDoc(db: Firestore, cfg: BoardPathConfig, boardId: string): DocumentReference;
16
- export declare function boardPostsCol(db: Firestore, cfg: BoardPathConfig, boardId: string): CollectionReference;
17
- export declare function boardPostDoc(db: Firestore, cfg: BoardPathConfig, boardId: string, postId: string): DocumentReference;
18
- export declare function boardPostBodyDoc(db: Firestore, cfg: BoardPathConfig, boardId: string, postId: string): DocumentReference;
19
- export declare function boardPostCommentsCol(db: Firestore, cfg: BoardPathConfig, boardId: string, postId: string): CollectionReference;
20
- export declare function boardPostCommentDoc(db: Firestore, cfg: BoardPathConfig, boardId: string, postId: string, commentId: string): DocumentReference;
21
- export declare function boardPostLikeDoc(db: Firestore, cfg: BoardPathConfig, boardId: string, postId: string, uid: string): DocumentReference;
21
+ /** 보드 설정 목록/문서 — 구 memiBoards/{boardId} stub + settings/config 통합 */
22
+ export declare function settingsCol(db: Firestore, cfg: BoardPathConfig): CollectionReference;
23
+ export declare function settingsDoc(db: Firestore, cfg: BoardPathConfig, boardId: string): DocumentReference;
24
+ export declare function postsCol(db: Firestore, cfg: BoardPathConfig): CollectionReference;
25
+ export declare function postDoc(db: Firestore, cfg: BoardPathConfig, postId: string): DocumentReference;
26
+ export declare function postBodyDoc(db: Firestore, cfg: BoardPathConfig, postId: string): DocumentReference;
27
+ export declare function commentsCol(db: Firestore, cfg: BoardPathConfig): CollectionReference;
28
+ export declare function commentDoc(db: Firestore, cfg: BoardPathConfig, commentId: string): DocumentReference;
29
+ export declare function likesCol(db: Firestore, cfg: BoardPathConfig): CollectionReference;
30
+ /** 좋아요 문서ID 관례: `${postId}_${uid}` — 유저당 글당 좋아요 1개, 토글은 create-or-delete */
31
+ export declare function likeDocId(postId: string, uid: string): string;
32
+ export declare function likeDoc(db: Firestore, cfg: BoardPathConfig, postId: string, uid: string): DocumentReference;
22
33
  export declare function boardUsersCol(db: Firestore, cfg: BoardPathConfig): CollectionReference;
23
34
  export declare function boardUserDoc(db: Firestore, cfg: BoardPathConfig, uid: string): DocumentReference;
24
- export declare function boardStorageRoot(cfg: BoardPathConfig, boardId: string): string;
25
- export declare function boardPostStorageFolder(cfg: BoardPathConfig, boardId: string, postId: string): string;
35
+ /** postId 가 전역 고유이므로 boardId 프리픽스 불필요 */
36
+ export declare function postStorageFolder(cfg: BoardPathConfig, postId: string): string;
26
37
  export declare function boardSsrKey(cfg: BoardPathConfig, suffix: string): string;
@@ -94,13 +94,12 @@ AI Logic / `firebaseml` 이 **Enforce** 이면 App Check 토큰 없이 검열 AP
94
94
  4. **프로덕션:** 디버그 토큰 사용 금지. reCAPTCHA 도메인에 실제 호스트 도메인 등록.
95
95
  5. (선택) Firestore/Storage Enforce — 켜면 토큰 없는 요청 거부. AI Logic 과 별개 토글이다.
96
96
 
97
- ## 9. Firestore 복합 인덱스 (카테고리 필터 사용 시)
97
+ ## 9. Firestore 복합 인덱스
98
98
 
99
- `MemiBoardList` 에 `category` prop 을 넘기면 `category + createdAt` 복합 인덱스가 필요하다.
100
- 콘솔 에러의 인덱스 생성 링크를 따르거나, 호스트 `firestore.indexes.json` 에 추가 후:
99
+ `memiBoardPosts`/`memiBoardComments`/`memiBoardLikes` 는 모두 최상위 flat 컬렉션이고, 보드/글 단위 스코핑은 `boardId`/`postId` 필드에 대한 `where` 동등 필터로 한다. [docs/firestore.indexes.json.example](firestore.indexes.json.example) 를 호스트 `firestore.indexes.json` 에 병합 후:
101
100
 
102
101
  ```bash
103
102
  firebase deploy --only firestore:indexes
104
103
  ```
105
104
 
106
- 컬렉션 ID: 서브컬렉션 `posts` (collection group `posts`). 경로 예: `memiBoards/{boardId}/posts`.
105
+ 콘솔 에러에 인덱스 생성 링크가 뜨면 그걸 따라도 된다 — 위 예시 파일은 패키지가 실제로 실행하는 쿼리 기준으로 미리 정리해 둔 것이다. 컬렉션 이름을 커스터마이즈했다면(`memiBoard.postsCollection` 등) `collectionGroup` 값도 맞춰 바꾼다.
@@ -0,0 +1,127 @@
1
+ {
2
+ "indexes": [
3
+ {
4
+ "collectionGroup": "memiBoardPosts",
5
+ "queryScope": "COLLECTION",
6
+ "fields": [
7
+ { "fieldPath": "boardId", "order": "ASCENDING" },
8
+ { "fieldPath": "isPublished", "order": "ASCENDING" },
9
+ { "fieldPath": "listed", "order": "ASCENDING" },
10
+ { "fieldPath": "createdAt", "order": "DESCENDING" },
11
+ { "fieldPath": "__name__", "order": "DESCENDING" }
12
+ ]
13
+ },
14
+ {
15
+ "collectionGroup": "memiBoardPosts",
16
+ "queryScope": "COLLECTION",
17
+ "fields": [
18
+ { "fieldPath": "boardId", "order": "ASCENDING" },
19
+ { "fieldPath": "isPublished", "order": "ASCENDING" },
20
+ { "fieldPath": "createdAt", "order": "DESCENDING" },
21
+ { "fieldPath": "__name__", "order": "DESCENDING" }
22
+ ]
23
+ },
24
+ {
25
+ "collectionGroup": "memiBoardPosts",
26
+ "queryScope": "COLLECTION",
27
+ "fields": [
28
+ { "fieldPath": "boardId", "order": "ASCENDING" },
29
+ { "fieldPath": "isPublished", "order": "ASCENDING" },
30
+ { "fieldPath": "listed", "order": "ASCENDING" },
31
+ { "fieldPath": "createdAt", "order": "ASCENDING" },
32
+ { "fieldPath": "__name__", "order": "ASCENDING" }
33
+ ]
34
+ },
35
+ {
36
+ "collectionGroup": "memiBoardPosts",
37
+ "queryScope": "COLLECTION",
38
+ "fields": [
39
+ { "fieldPath": "boardId", "order": "ASCENDING" },
40
+ { "fieldPath": "isPublished", "order": "ASCENDING" },
41
+ { "fieldPath": "createdAt", "order": "ASCENDING" },
42
+ { "fieldPath": "__name__", "order": "ASCENDING" }
43
+ ]
44
+ },
45
+ {
46
+ "collectionGroup": "memiBoardPosts",
47
+ "queryScope": "COLLECTION",
48
+ "fields": [
49
+ { "fieldPath": "boardId", "order": "ASCENDING" },
50
+ { "fieldPath": "authorUid", "order": "ASCENDING" },
51
+ { "fieldPath": "isPublished", "order": "ASCENDING" },
52
+ { "fieldPath": "createdAt", "order": "DESCENDING" },
53
+ { "fieldPath": "__name__", "order": "DESCENDING" }
54
+ ]
55
+ },
56
+ {
57
+ "collectionGroup": "memiBoardPosts",
58
+ "queryScope": "COLLECTION",
59
+ "fields": [
60
+ { "fieldPath": "authorUid", "order": "ASCENDING" },
61
+ { "fieldPath": "isPublished", "order": "ASCENDING" },
62
+ { "fieldPath": "listed", "order": "ASCENDING" },
63
+ { "fieldPath": "createdAt", "order": "DESCENDING" },
64
+ { "fieldPath": "__name__", "order": "DESCENDING" }
65
+ ]
66
+ },
67
+ {
68
+ "collectionGroup": "memiBoardComments",
69
+ "queryScope": "COLLECTION",
70
+ "fields": [
71
+ { "fieldPath": "postId", "order": "ASCENDING" },
72
+ { "fieldPath": "createdAt", "order": "DESCENDING" },
73
+ { "fieldPath": "__name__", "order": "DESCENDING" }
74
+ ]
75
+ },
76
+ {
77
+ "collectionGroup": "memiBoardComments",
78
+ "queryScope": "COLLECTION",
79
+ "fields": [
80
+ { "fieldPath": "postId", "order": "ASCENDING" },
81
+ { "fieldPath": "parentId", "order": "ASCENDING" },
82
+ { "fieldPath": "createdAt", "order": "DESCENDING" },
83
+ { "fieldPath": "__name__", "order": "DESCENDING" }
84
+ ]
85
+ },
86
+ {
87
+ "collectionGroup": "memiBoardComments",
88
+ "queryScope": "COLLECTION",
89
+ "fields": [
90
+ { "fieldPath": "postId", "order": "ASCENDING" },
91
+ { "fieldPath": "parentId", "order": "ASCENDING" },
92
+ { "fieldPath": "createdAt", "order": "ASCENDING" },
93
+ { "fieldPath": "__name__", "order": "ASCENDING" }
94
+ ]
95
+ },
96
+ {
97
+ "collectionGroup": "memiBoardComments",
98
+ "queryScope": "COLLECTION",
99
+ "fields": [
100
+ { "fieldPath": "rootId", "order": "ASCENDING" },
101
+ { "fieldPath": "isReply", "order": "ASCENDING" },
102
+ { "fieldPath": "createdAt", "order": "DESCENDING" },
103
+ { "fieldPath": "__name__", "order": "DESCENDING" }
104
+ ]
105
+ },
106
+ {
107
+ "collectionGroup": "memiBoardComments",
108
+ "queryScope": "COLLECTION",
109
+ "fields": [
110
+ { "fieldPath": "rootId", "order": "ASCENDING" },
111
+ { "fieldPath": "isReply", "order": "ASCENDING" },
112
+ { "fieldPath": "createdAt", "order": "ASCENDING" },
113
+ { "fieldPath": "__name__", "order": "ASCENDING" }
114
+ ]
115
+ },
116
+ {
117
+ "collectionGroup": "memiBoardLikes",
118
+ "queryScope": "COLLECTION",
119
+ "fields": [
120
+ { "fieldPath": "uid", "order": "ASCENDING" },
121
+ { "fieldPath": "createdAt", "order": "DESCENDING" },
122
+ { "fieldPath": "__name__", "order": "DESCENDING" }
123
+ ]
124
+ }
125
+ ],
126
+ "fieldOverrides": []
127
+ }