@mandujs/core 0.11.0 → 0.12.1
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/README.ko.md +81 -4
- package/README.md +81 -0
- package/package.json +10 -9
- package/src/constants.ts +15 -0
- package/src/content/content-layer.ts +314 -0
- package/src/content/content.test.ts +433 -0
- package/src/content/data-store.ts +245 -0
- package/src/content/digest.ts +133 -0
- package/src/content/index.ts +164 -0
- package/src/content/loader-context.ts +172 -0
- package/src/content/loaders/api.ts +216 -0
- package/src/content/loaders/file.ts +169 -0
- package/src/content/loaders/glob.ts +252 -0
- package/src/content/loaders/index.ts +34 -0
- package/src/content/loaders/types.ts +137 -0
- package/src/content/meta-store.ts +209 -0
- package/src/content/types.ts +282 -0
- package/src/content/watcher.ts +135 -0
package/README.ko.md
CHANGED
|
@@ -25,11 +25,16 @@ bun add @mandujs/core
|
|
|
25
25
|
|
|
26
26
|
```
|
|
27
27
|
@mandujs/core
|
|
28
|
-
├──
|
|
29
|
-
├── generator/ # 코드 생성
|
|
28
|
+
├── router/ # 파일 시스템 기반 라우팅
|
|
30
29
|
├── guard/ # 아키텍처 검사 및 자동 수정
|
|
31
|
-
├── runtime/ #
|
|
32
|
-
|
|
30
|
+
├── runtime/ # 서버, SSR, 스트리밍
|
|
31
|
+
├── filling/ # 핸들러 체인 API
|
|
32
|
+
├── contract/ # 타입 안전 API 계약
|
|
33
|
+
├── content/ # Content Layer - 빌드 타임 콘텐츠 로딩 🆕
|
|
34
|
+
├── bundler/ # 클라이언트 번들링, HMR
|
|
35
|
+
├── client/ # Island 하이드레이션, 클라이언트 라우터
|
|
36
|
+
├── brain/ # Doctor, Watcher, 아키텍처 분석
|
|
37
|
+
└── change/ # 트랜잭션 & 히스토리
|
|
33
38
|
```
|
|
34
39
|
|
|
35
40
|
## Spec 모듈
|
|
@@ -133,6 +138,73 @@ if (!result.passed) {
|
|
|
133
138
|
| `COMPONENT_NOT_FOUND` | 컴포넌트 파일 없음 | ❌ |
|
|
134
139
|
| `SLOT_NOT_FOUND` | slot 파일 없음 | ✅ |
|
|
135
140
|
|
|
141
|
+
## Content Layer 🆕
|
|
142
|
+
|
|
143
|
+
Astro에서 영감받은 빌드 타임 콘텐츠 로딩 시스템.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// content.config.ts
|
|
147
|
+
import { defineContentConfig, glob, file, api } from "@mandujs/core/content";
|
|
148
|
+
import { z } from "zod";
|
|
149
|
+
|
|
150
|
+
const postSchema = z.object({
|
|
151
|
+
title: z.string(),
|
|
152
|
+
date: z.coerce.date(),
|
|
153
|
+
tags: z.array(z.string()).default([]),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export default defineContentConfig({
|
|
157
|
+
collections: {
|
|
158
|
+
// Markdown 파일 (프론트매터 지원)
|
|
159
|
+
posts: {
|
|
160
|
+
loader: glob({ pattern: "content/posts/**/*.md" }),
|
|
161
|
+
schema: postSchema,
|
|
162
|
+
},
|
|
163
|
+
// 단일 JSON/YAML 파일
|
|
164
|
+
settings: {
|
|
165
|
+
loader: file({ path: "data/settings.json" }),
|
|
166
|
+
},
|
|
167
|
+
// 외부 API
|
|
168
|
+
products: {
|
|
169
|
+
loader: api({
|
|
170
|
+
url: "https://api.example.com/products",
|
|
171
|
+
cacheTTL: 3600,
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 콘텐츠 조회
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { getCollection, getEntry } from "@mandujs/core/content";
|
|
182
|
+
|
|
183
|
+
// 전체 컬렉션 조회
|
|
184
|
+
const posts = await getCollection("posts");
|
|
185
|
+
|
|
186
|
+
// 단일 엔트리 조회
|
|
187
|
+
const post = await getEntry("posts", "hello-world");
|
|
188
|
+
console.log(post?.data.title, post?.body);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 내장 로더
|
|
192
|
+
|
|
193
|
+
| 로더 | 설명 | 예시 |
|
|
194
|
+
|------|------|------|
|
|
195
|
+
| `file()` | 단일 파일 (JSON, YAML, TOML) | `file({ path: "data/config.json" })` |
|
|
196
|
+
| `glob()` | 패턴 매칭 (Markdown, JSON) | `glob({ pattern: "content/**/*.md" })` |
|
|
197
|
+
| `api()` | HTTP API (캐싱 지원) | `api({ url: "https://...", cacheTTL: 3600 })` |
|
|
198
|
+
|
|
199
|
+
### 주요 기능
|
|
200
|
+
|
|
201
|
+
- **Digest 기반 캐싱**: 변경된 파일만 재파싱
|
|
202
|
+
- **Zod 검증**: 스키마 기반 타입 안전 콘텐츠
|
|
203
|
+
- **프론트매터 지원**: Markdown YAML 프론트매터
|
|
204
|
+
- **Dev 모드 감시**: 콘텐츠 변경 시 자동 리로드
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
136
208
|
## Contract 모듈
|
|
137
209
|
|
|
138
210
|
Zod 기반 계약(Contract) 정의 및 타입 안전 클라이언트 생성.
|
|
@@ -209,6 +281,11 @@ import type {
|
|
|
209
281
|
GuardViolation,
|
|
210
282
|
GenerateResult,
|
|
211
283
|
AutoCorrectResult,
|
|
284
|
+
// Content Layer
|
|
285
|
+
DataEntry,
|
|
286
|
+
ContentConfig,
|
|
287
|
+
CollectionConfig,
|
|
288
|
+
Loader,
|
|
212
289
|
} from "@mandujs/core";
|
|
213
290
|
```
|
|
214
291
|
|
package/README.md
CHANGED
|
@@ -81,6 +81,8 @@ const handlers = Mandu.handler(userContract, {
|
|
|
81
81
|
├── runtime/ # Server, SSR, streaming
|
|
82
82
|
├── filling/ # Handler chain API (Mandu.filling())
|
|
83
83
|
├── contract/ # Type-safe API contracts
|
|
84
|
+
├── content/ # Content Layer - build-time content loading 🆕
|
|
85
|
+
│ └── loaders # file(), glob(), api() loaders
|
|
84
86
|
├── bundler/ # Client bundling, HMR
|
|
85
87
|
├── client/ # Island hydration, client router
|
|
86
88
|
├── brain/ # Doctor, Watcher, Architecture analyzer
|
|
@@ -453,6 +455,78 @@ function Navigation() {
|
|
|
453
455
|
|
|
454
456
|
---
|
|
455
457
|
|
|
458
|
+
## Content Layer 🆕
|
|
459
|
+
|
|
460
|
+
Astro-inspired build-time content loading system.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// content.config.ts
|
|
464
|
+
import { defineContentConfig, glob, file, api } from "@mandujs/core/content";
|
|
465
|
+
import { z } from "zod";
|
|
466
|
+
|
|
467
|
+
const postSchema = z.object({
|
|
468
|
+
title: z.string(),
|
|
469
|
+
date: z.coerce.date(),
|
|
470
|
+
tags: z.array(z.string()).default([]),
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
export default defineContentConfig({
|
|
474
|
+
collections: {
|
|
475
|
+
// Markdown files with frontmatter
|
|
476
|
+
posts: {
|
|
477
|
+
loader: glob({ pattern: "content/posts/**/*.md" }),
|
|
478
|
+
schema: postSchema,
|
|
479
|
+
},
|
|
480
|
+
// Single JSON/YAML file
|
|
481
|
+
settings: {
|
|
482
|
+
loader: file({ path: "data/settings.json" }),
|
|
483
|
+
},
|
|
484
|
+
// External API
|
|
485
|
+
products: {
|
|
486
|
+
loader: api({
|
|
487
|
+
url: "https://api.example.com/products",
|
|
488
|
+
headers: () => ({ Authorization: `Bearer ${process.env.API_KEY}` }),
|
|
489
|
+
cacheTTL: 3600,
|
|
490
|
+
}),
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Querying Content
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { getCollection, getEntry } from "@mandujs/core/content";
|
|
500
|
+
|
|
501
|
+
// Get all entries
|
|
502
|
+
const posts = await getCollection("posts");
|
|
503
|
+
posts.forEach(post => {
|
|
504
|
+
console.log(post.id, post.data.title);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Get single entry
|
|
508
|
+
const post = await getEntry("posts", "hello-world");
|
|
509
|
+
console.log(post?.data.title, post?.body);
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Built-in Loaders
|
|
513
|
+
|
|
514
|
+
| Loader | Description | Example |
|
|
515
|
+
|--------|-------------|---------|
|
|
516
|
+
| `file()` | Single file (JSON, YAML, TOML) | `file({ path: "data/config.json" })` |
|
|
517
|
+
| `glob()` | Pattern matching (Markdown, JSON) | `glob({ pattern: "content/**/*.md" })` |
|
|
518
|
+
| `api()` | HTTP API with caching | `api({ url: "https://...", cacheTTL: 3600 })` |
|
|
519
|
+
|
|
520
|
+
### Features
|
|
521
|
+
|
|
522
|
+
- **Digest-based caching**: Only re-parse changed files
|
|
523
|
+
- **Zod validation**: Type-safe content with schema validation
|
|
524
|
+
- **Frontmatter support**: YAML frontmatter in Markdown files
|
|
525
|
+
- **Dev mode watching**: Auto-reload on content changes
|
|
526
|
+
- **Incremental updates**: Efficient builds with change detection
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
456
530
|
## Brain (AI Assistant)
|
|
457
531
|
|
|
458
532
|
Doctor and architecture analyzer.
|
|
@@ -551,6 +625,13 @@ import type {
|
|
|
551
625
|
|
|
552
626
|
// Filling
|
|
553
627
|
ManduContext,
|
|
628
|
+
|
|
629
|
+
// Content Layer
|
|
630
|
+
DataEntry,
|
|
631
|
+
ContentConfig,
|
|
632
|
+
CollectionConfig,
|
|
633
|
+
Loader,
|
|
634
|
+
LoaderContext,
|
|
554
635
|
} from "@mandujs/core";
|
|
555
636
|
```
|
|
556
637
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -13,12 +13,6 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"src/**/*"
|
|
15
15
|
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"test": "bun test tests/streaming-ssr && bun test tests/hydration tests/typing src",
|
|
18
|
-
"test:hydration": "bun test tests/hydration",
|
|
19
|
-
"test:streaming": "bun test tests/streaming-ssr",
|
|
20
|
-
"test:watch": "bun test --watch"
|
|
21
|
-
},
|
|
22
16
|
"devDependencies": {
|
|
23
17
|
"@happy-dom/global-registrator": "^15.0.0"
|
|
24
18
|
},
|
|
@@ -35,7 +29,7 @@
|
|
|
35
29
|
"directory": "packages/core"
|
|
36
30
|
},
|
|
37
31
|
"author": "konamgil",
|
|
38
|
-
"license": "
|
|
32
|
+
"license": "MPL-2.0",
|
|
39
33
|
"publishConfig": {
|
|
40
34
|
"access": "public"
|
|
41
35
|
},
|
|
@@ -49,8 +43,15 @@
|
|
|
49
43
|
},
|
|
50
44
|
"dependencies": {
|
|
51
45
|
"chokidar": "^5.0.0",
|
|
46
|
+
"fast-glob": "^3.3.2",
|
|
52
47
|
"glob": "^13.0.0",
|
|
53
48
|
"minimatch": "^10.1.1",
|
|
54
49
|
"ollama": "^0.6.3"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"test": "bun test tests/streaming-ssr && bun test tests/hydration tests/typing src",
|
|
53
|
+
"test:hydration": "bun test tests/hydration",
|
|
54
|
+
"test:streaming": "bun test tests/streaming-ssr",
|
|
55
|
+
"test:watch": "bun test --watch"
|
|
55
56
|
}
|
|
56
|
-
}
|
|
57
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -23,3 +23,18 @@ export const LIMITS = {
|
|
|
23
23
|
ROUTER_PATTERN_CACHE: 200,
|
|
24
24
|
ROUTER_PREFETCH_CACHE: 500,
|
|
25
25
|
} as const;
|
|
26
|
+
|
|
27
|
+
export const CONTENT = {
|
|
28
|
+
/** 로더 기본 타임아웃 (ms) */
|
|
29
|
+
LOADER_TIMEOUT: 10000,
|
|
30
|
+
/** 데이터 스토어 파일 경로 */
|
|
31
|
+
STORE_FILE: ".mandu/content-store.json",
|
|
32
|
+
/** 메타 스토어 파일 경로 */
|
|
33
|
+
META_FILE: ".mandu/content-meta.json",
|
|
34
|
+
/** 저장 디바운스 (ms) */
|
|
35
|
+
DEBOUNCE_SAVE: 500,
|
|
36
|
+
/** 기본 콘텐츠 디렉토리 */
|
|
37
|
+
DEFAULT_DIR: "content",
|
|
38
|
+
/** API 로더 기본 캐시 TTL (초) */
|
|
39
|
+
API_CACHE_TTL: 3600,
|
|
40
|
+
} as const;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContentLayer - 콘텐츠 레이어 메인 클래스
|
|
3
|
+
*
|
|
4
|
+
* 빌드 타임에 모든 컬렉션을 로드하고 관리
|
|
5
|
+
* getCollection(), getEntry() API 제공
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ContentConfig,
|
|
10
|
+
CollectionConfig,
|
|
11
|
+
DataEntry,
|
|
12
|
+
ManduContentConfig,
|
|
13
|
+
ContentWatcher,
|
|
14
|
+
RenderedContent,
|
|
15
|
+
} from "./types";
|
|
16
|
+
import { LoaderError } from "./types";
|
|
17
|
+
import { ContentDataStore, createDataStore } from "./data-store";
|
|
18
|
+
import { ContentMetaStore, createMetaStore } from "./meta-store";
|
|
19
|
+
import { createLoaderContext, createSimpleMarkdownRenderer } from "./loader-context";
|
|
20
|
+
import { CONTENT } from "../constants";
|
|
21
|
+
import type { ZodSchema } from "zod";
|
|
22
|
+
import * as path from "path";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ContentLayer 옵션
|
|
26
|
+
*/
|
|
27
|
+
export interface ContentLayerOptions {
|
|
28
|
+
/** 콘텐츠 설정 */
|
|
29
|
+
contentConfig: ContentConfig;
|
|
30
|
+
/** Mandu 설정 */
|
|
31
|
+
manduConfig: ManduContentConfig;
|
|
32
|
+
/** Markdown 렌더러 (선택) */
|
|
33
|
+
markdownRenderer?: (content: string) => Promise<RenderedContent>;
|
|
34
|
+
/** 파일 감시자 (dev 모드) */
|
|
35
|
+
watcher?: ContentWatcher;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ContentLayer 클래스
|
|
40
|
+
*
|
|
41
|
+
* 빌드 타임 콘텐츠 로딩 및 관리
|
|
42
|
+
*/
|
|
43
|
+
export class ContentLayer {
|
|
44
|
+
private contentConfig: ContentConfig;
|
|
45
|
+
private manduConfig: ManduContentConfig;
|
|
46
|
+
private dataStore: ContentDataStore;
|
|
47
|
+
private metaStore: ContentMetaStore;
|
|
48
|
+
private markdownRenderer: (content: string) => Promise<RenderedContent>;
|
|
49
|
+
private watcher?: ContentWatcher;
|
|
50
|
+
private loaded: boolean = false;
|
|
51
|
+
private loading: Promise<void> | null = null;
|
|
52
|
+
|
|
53
|
+
constructor(options: ContentLayerOptions) {
|
|
54
|
+
const { contentConfig, manduConfig, markdownRenderer, watcher } = options;
|
|
55
|
+
|
|
56
|
+
this.contentConfig = contentConfig;
|
|
57
|
+
this.manduConfig = manduConfig;
|
|
58
|
+
this.watcher = watcher;
|
|
59
|
+
this.markdownRenderer = markdownRenderer ?? createSimpleMarkdownRenderer();
|
|
60
|
+
|
|
61
|
+
// 스토어 초기화
|
|
62
|
+
const storeFile = path.join(manduConfig.root, CONTENT.STORE_FILE);
|
|
63
|
+
const metaFile = path.join(manduConfig.root, CONTENT.META_FILE);
|
|
64
|
+
|
|
65
|
+
this.dataStore = createDataStore({
|
|
66
|
+
filePath: storeFile,
|
|
67
|
+
autoSave: true,
|
|
68
|
+
saveDebounce: CONTENT.DEBOUNCE_SAVE,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.metaStore = createMetaStore({
|
|
72
|
+
filePath: metaFile,
|
|
73
|
+
autoSave: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 모든 컬렉션 로드
|
|
79
|
+
*/
|
|
80
|
+
async load(): Promise<void> {
|
|
81
|
+
// 이미 로딩 중이면 기다림
|
|
82
|
+
if (this.loading) {
|
|
83
|
+
return this.loading;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 이미 로드됨
|
|
87
|
+
if (this.loaded) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.loading = this.doLoad();
|
|
92
|
+
await this.loading;
|
|
93
|
+
this.loading = null;
|
|
94
|
+
this.loaded = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async doLoad(): Promise<void> {
|
|
98
|
+
// 캐시에서 로드
|
|
99
|
+
await Promise.all([this.dataStore.load(), this.metaStore.load()]);
|
|
100
|
+
|
|
101
|
+
const collections = Object.entries(this.contentConfig.collections);
|
|
102
|
+
|
|
103
|
+
// 병렬로 모든 컬렉션 로드
|
|
104
|
+
await Promise.all(
|
|
105
|
+
collections.map(([name, config]) => this.loadCollection(name, config))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// 저장
|
|
109
|
+
await Promise.all([this.dataStore.save(), this.metaStore.save()]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 단일 컬렉션 로드
|
|
114
|
+
*/
|
|
115
|
+
private async loadCollection(name: string, config: CollectionConfig): Promise<void> {
|
|
116
|
+
const { loader, schema } = config;
|
|
117
|
+
|
|
118
|
+
// 로더 스키마와 컬렉션 스키마 병합 (컬렉션 우선)
|
|
119
|
+
let finalSchema: ZodSchema | undefined = schema;
|
|
120
|
+
if (!finalSchema && loader.schema) {
|
|
121
|
+
finalSchema =
|
|
122
|
+
typeof loader.schema === "function" ? await loader.schema() : loader.schema;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// LoaderContext 생성
|
|
126
|
+
const context = createLoaderContext({
|
|
127
|
+
collection: name,
|
|
128
|
+
store: this.dataStore.getStore(name),
|
|
129
|
+
meta: this.metaStore.getStore(name),
|
|
130
|
+
config: this.manduConfig,
|
|
131
|
+
schema: finalSchema,
|
|
132
|
+
markdownRenderer: this.markdownRenderer,
|
|
133
|
+
watcher: this.watcher,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// 타임아웃 처리
|
|
138
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
139
|
+
setTimeout(
|
|
140
|
+
() => reject(new LoaderError(`Loader "${loader.name}" timed out`, name)),
|
|
141
|
+
CONTENT.LOADER_TIMEOUT
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await Promise.race([loader.load(context), timeoutPromise]);
|
|
146
|
+
|
|
147
|
+
context.logger.info(`Loaded ${context.store.size()} entries`);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
150
|
+
context.logger.error(`Failed to load: ${message}`);
|
|
151
|
+
|
|
152
|
+
if (error instanceof LoaderError) {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw new LoaderError(`Failed to load collection "${name}": ${message}`, name);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 단일 컬렉션 다시 로드
|
|
162
|
+
*/
|
|
163
|
+
async reloadCollection(name: string): Promise<void> {
|
|
164
|
+
const config = this.contentConfig.collections[name];
|
|
165
|
+
if (!config) {
|
|
166
|
+
throw new Error(`Collection "${name}" not found`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await this.loadCollection(name, config);
|
|
170
|
+
await this.dataStore.save();
|
|
171
|
+
await this.metaStore.save();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 컬렉션의 모든 엔트리 조회
|
|
176
|
+
*/
|
|
177
|
+
getCollection<T = Record<string, unknown>>(
|
|
178
|
+
collection: string
|
|
179
|
+
): Array<DataEntry<T>> {
|
|
180
|
+
this.ensureLoaded();
|
|
181
|
+
|
|
182
|
+
const store = this.dataStore.getStore(collection);
|
|
183
|
+
return store.values<T>();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 컬렉션에서 단일 엔트리 조회
|
|
188
|
+
*/
|
|
189
|
+
getEntry<T = Record<string, unknown>>(
|
|
190
|
+
collection: string,
|
|
191
|
+
id: string
|
|
192
|
+
): DataEntry<T> | undefined {
|
|
193
|
+
this.ensureLoaded();
|
|
194
|
+
|
|
195
|
+
const store = this.dataStore.getStore(collection);
|
|
196
|
+
return store.get<T>(id);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 컬렉션 존재 여부 확인
|
|
201
|
+
*/
|
|
202
|
+
hasCollection(collection: string): boolean {
|
|
203
|
+
return collection in this.contentConfig.collections;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 모든 컬렉션 이름 조회
|
|
208
|
+
*/
|
|
209
|
+
getCollectionNames(): string[] {
|
|
210
|
+
return Object.keys(this.contentConfig.collections);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 컬렉션 통계
|
|
215
|
+
*/
|
|
216
|
+
getStats(): {
|
|
217
|
+
collections: number;
|
|
218
|
+
totalEntries: number;
|
|
219
|
+
byCollection: Record<string, number>;
|
|
220
|
+
} {
|
|
221
|
+
this.ensureLoaded();
|
|
222
|
+
|
|
223
|
+
const byCollection: Record<string, number> = {};
|
|
224
|
+
let totalEntries = 0;
|
|
225
|
+
|
|
226
|
+
for (const name of this.getCollectionNames()) {
|
|
227
|
+
const store = this.dataStore.getStore(name);
|
|
228
|
+
const count = store.size();
|
|
229
|
+
byCollection[name] = count;
|
|
230
|
+
totalEntries += count;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
collections: Object.keys(byCollection).length,
|
|
235
|
+
totalEntries,
|
|
236
|
+
byCollection,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 로드 확인
|
|
242
|
+
*/
|
|
243
|
+
private ensureLoaded(): void {
|
|
244
|
+
if (!this.loaded) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"ContentLayer not loaded. Call await contentLayer.load() first."
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 리소스 정리
|
|
253
|
+
*/
|
|
254
|
+
dispose(): void {
|
|
255
|
+
this.dataStore.dispose();
|
|
256
|
+
this.watcher?.close();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* ContentLayer 팩토리
|
|
262
|
+
*/
|
|
263
|
+
export function createContentLayer(options: ContentLayerOptions): ContentLayer {
|
|
264
|
+
return new ContentLayer(options);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// 전역 ContentLayer 인스턴스 (싱글톤)
|
|
269
|
+
// ============================================================================
|
|
270
|
+
|
|
271
|
+
let globalContentLayer: ContentLayer | null = null;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 전역 ContentLayer 설정
|
|
275
|
+
*/
|
|
276
|
+
export function setGlobalContentLayer(layer: ContentLayer): void {
|
|
277
|
+
globalContentLayer = layer;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 전역 ContentLayer 조회
|
|
282
|
+
*/
|
|
283
|
+
export function getGlobalContentLayer(): ContentLayer | null {
|
|
284
|
+
return globalContentLayer;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 컬렉션 조회 (전역 ContentLayer 사용)
|
|
289
|
+
*/
|
|
290
|
+
export async function getCollection<T = Record<string, unknown>>(
|
|
291
|
+
collection: string
|
|
292
|
+
): Promise<Array<DataEntry<T>>> {
|
|
293
|
+
if (!globalContentLayer) {
|
|
294
|
+
throw new Error("ContentLayer not initialized. Call setGlobalContentLayer() first.");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await globalContentLayer.load();
|
|
298
|
+
return globalContentLayer.getCollection<T>(collection);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 단일 엔트리 조회 (전역 ContentLayer 사용)
|
|
303
|
+
*/
|
|
304
|
+
export async function getEntry<T = Record<string, unknown>>(
|
|
305
|
+
collection: string,
|
|
306
|
+
id: string
|
|
307
|
+
): Promise<DataEntry<T> | undefined> {
|
|
308
|
+
if (!globalContentLayer) {
|
|
309
|
+
throw new Error("ContentLayer not initialized. Call setGlobalContentLayer() first.");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await globalContentLayer.load();
|
|
313
|
+
return globalContentLayer.getEntry<T>(collection, id);
|
|
314
|
+
}
|