@spfn/cms 0.1.0-alpha.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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +490 -0
  3. package/dist/actions.d.ts +9 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +11 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/client.d.ts +138 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +62 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/cms.config.d.ts +77 -0
  12. package/dist/cms.config.d.ts.map +1 -0
  13. package/dist/cms.config.js +111 -0
  14. package/dist/cms.config.js.map +1 -0
  15. package/dist/entities/cms-audit-logs.d.ts +213 -0
  16. package/dist/entities/cms-audit-logs.d.ts.map +1 -0
  17. package/dist/entities/cms-audit-logs.js +103 -0
  18. package/dist/entities/cms-audit-logs.js.map +1 -0
  19. package/dist/entities/cms-draft-cache.d.ts +188 -0
  20. package/dist/entities/cms-draft-cache.d.ts.map +1 -0
  21. package/dist/entities/cms-draft-cache.js +112 -0
  22. package/dist/entities/cms-draft-cache.js.map +1 -0
  23. package/dist/entities/cms-label-values.d.ts +192 -0
  24. package/dist/entities/cms-label-values.d.ts.map +1 -0
  25. package/dist/entities/cms-label-values.js +105 -0
  26. package/dist/entities/cms-label-values.js.map +1 -0
  27. package/dist/entities/cms-label-versions.d.ts +207 -0
  28. package/dist/entities/cms-label-versions.d.ts.map +1 -0
  29. package/dist/entities/cms-label-versions.js +80 -0
  30. package/dist/entities/cms-label-versions.js.map +1 -0
  31. package/dist/entities/cms-labels.d.ts +189 -0
  32. package/dist/entities/cms-labels.d.ts.map +1 -0
  33. package/dist/entities/cms-labels.js +48 -0
  34. package/dist/entities/cms-labels.js.map +1 -0
  35. package/dist/entities/cms-published-cache.d.ts +199 -0
  36. package/dist/entities/cms-published-cache.d.ts.map +1 -0
  37. package/dist/entities/cms-published-cache.js +103 -0
  38. package/dist/entities/cms-published-cache.js.map +1 -0
  39. package/dist/entities/index.d.ts +10 -0
  40. package/dist/entities/index.d.ts.map +1 -0
  41. package/dist/entities/index.js +10 -0
  42. package/dist/entities/index.js.map +1 -0
  43. package/dist/generators/index.d.ts +19 -0
  44. package/dist/generators/index.d.ts.map +1 -0
  45. package/dist/generators/index.js +19 -0
  46. package/dist/generators/index.js.map +1 -0
  47. package/dist/generators/label-sync-generator.d.ts +33 -0
  48. package/dist/generators/label-sync-generator.d.ts.map +1 -0
  49. package/dist/generators/label-sync-generator.js +86 -0
  50. package/dist/generators/label-sync-generator.js.map +1 -0
  51. package/dist/helpers/locale.actions.d.ts +132 -0
  52. package/dist/helpers/locale.actions.d.ts.map +1 -0
  53. package/dist/helpers/locale.actions.js +210 -0
  54. package/dist/helpers/locale.actions.js.map +1 -0
  55. package/dist/helpers/locale.constants.d.ts +10 -0
  56. package/dist/helpers/locale.constants.d.ts.map +1 -0
  57. package/dist/helpers/locale.constants.js +10 -0
  58. package/dist/helpers/locale.constants.js.map +1 -0
  59. package/dist/helpers/locale.d.ts +17 -0
  60. package/dist/helpers/locale.d.ts.map +1 -0
  61. package/dist/helpers/locale.js +20 -0
  62. package/dist/helpers/locale.js.map +1 -0
  63. package/dist/helpers/sync.d.ts +41 -0
  64. package/dist/helpers/sync.d.ts.map +1 -0
  65. package/dist/helpers/sync.js +309 -0
  66. package/dist/helpers/sync.js.map +1 -0
  67. package/dist/index.d.ts +20 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +24 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/init.d.ts +31 -0
  72. package/dist/init.d.ts.map +1 -0
  73. package/dist/init.js +36 -0
  74. package/dist/init.js.map +1 -0
  75. package/dist/labels/helpers.d.ts +31 -0
  76. package/dist/labels/helpers.d.ts.map +1 -0
  77. package/dist/labels/helpers.js +60 -0
  78. package/dist/labels/helpers.js.map +1 -0
  79. package/dist/labels/index.d.ts +7 -0
  80. package/dist/labels/index.d.ts.map +1 -0
  81. package/dist/labels/index.js +7 -0
  82. package/dist/labels/index.js.map +1 -0
  83. package/dist/repositories/cms-draft-cache.repository.d.ts +62 -0
  84. package/dist/repositories/cms-draft-cache.repository.d.ts.map +1 -0
  85. package/dist/repositories/cms-draft-cache.repository.js +56 -0
  86. package/dist/repositories/cms-draft-cache.repository.js.map +1 -0
  87. package/dist/repositories/cms-label-values.repository.d.ts +32 -0
  88. package/dist/repositories/cms-label-values.repository.d.ts.map +1 -0
  89. package/dist/repositories/cms-label-values.repository.js +72 -0
  90. package/dist/repositories/cms-label-values.repository.js.map +1 -0
  91. package/dist/repositories/cms-labels.repository.d.ts +53 -0
  92. package/dist/repositories/cms-labels.repository.d.ts.map +1 -0
  93. package/dist/repositories/cms-labels.repository.js +77 -0
  94. package/dist/repositories/cms-labels.repository.js.map +1 -0
  95. package/dist/repositories/cms-published-cache.repository.d.ts +53 -0
  96. package/dist/repositories/cms-published-cache.repository.d.ts.map +1 -0
  97. package/dist/repositories/cms-published-cache.repository.js +54 -0
  98. package/dist/repositories/cms-published-cache.repository.js.map +1 -0
  99. package/dist/repositories/index.d.ts +8 -0
  100. package/dist/repositories/index.d.ts.map +1 -0
  101. package/dist/repositories/index.js +9 -0
  102. package/dist/repositories/index.js.map +1 -0
  103. package/dist/routes/labels/[id]/contract.d.ts +68 -0
  104. package/dist/routes/labels/[id]/contract.d.ts.map +1 -0
  105. package/dist/routes/labels/[id]/contract.js +84 -0
  106. package/dist/routes/labels/[id]/contract.js.map +1 -0
  107. package/dist/routes/labels/[id]/index.d.ts +10 -0
  108. package/dist/routes/labels/[id]/index.d.ts.map +1 -0
  109. package/dist/routes/labels/[id]/index.js +96 -0
  110. package/dist/routes/labels/[id]/index.js.map +1 -0
  111. package/dist/routes/labels/by-key/[key]/contract.d.ts +24 -0
  112. package/dist/routes/labels/by-key/[key]/contract.d.ts.map +1 -0
  113. package/dist/routes/labels/by-key/[key]/contract.js +28 -0
  114. package/dist/routes/labels/by-key/[key]/contract.js.map +1 -0
  115. package/dist/routes/labels/by-key/[key]/index.d.ts +8 -0
  116. package/dist/routes/labels/by-key/[key]/index.d.ts.map +1 -0
  117. package/dist/routes/labels/by-key/[key]/index.js +32 -0
  118. package/dist/routes/labels/by-key/[key]/index.js.map +1 -0
  119. package/dist/routes/labels/contract.d.ts +59 -0
  120. package/dist/routes/labels/contract.d.ts.map +1 -0
  121. package/dist/routes/labels/contract.js +75 -0
  122. package/dist/routes/labels/contract.js.map +1 -0
  123. package/dist/routes/labels/index.d.ts +10 -0
  124. package/dist/routes/labels/index.d.ts.map +1 -0
  125. package/dist/routes/labels/index.js +73 -0
  126. package/dist/routes/labels/index.js.map +1 -0
  127. package/dist/routes/published-cache/contract.d.ts +25 -0
  128. package/dist/routes/published-cache/contract.d.ts.map +1 -0
  129. package/dist/routes/published-cache/contract.js +35 -0
  130. package/dist/routes/published-cache/contract.js.map +1 -0
  131. package/dist/routes/published-cache/index.d.ts +8 -0
  132. package/dist/routes/published-cache/index.d.ts.map +1 -0
  133. package/dist/routes/published-cache/index.js +33 -0
  134. package/dist/routes/published-cache/index.js.map +1 -0
  135. package/dist/routes/sync/contract.d.ts +33 -0
  136. package/dist/routes/sync/contract.d.ts.map +1 -0
  137. package/dist/routes/sync/contract.js +34 -0
  138. package/dist/routes/sync/contract.js.map +1 -0
  139. package/dist/routes/sync/index.d.ts +13 -0
  140. package/dist/routes/sync/index.d.ts.map +1 -0
  141. package/dist/routes/sync/index.js +241 -0
  142. package/dist/routes/sync/index.js.map +1 -0
  143. package/dist/routes/values/[labelId]/[version]/contract.d.ts +29 -0
  144. package/dist/routes/values/[labelId]/[version]/contract.d.ts.map +1 -0
  145. package/dist/routes/values/[labelId]/[version]/contract.js +33 -0
  146. package/dist/routes/values/[labelId]/[version]/contract.js.map +1 -0
  147. package/dist/routes/values/[labelId]/[version]/index.d.ts +8 -0
  148. package/dist/routes/values/[labelId]/[version]/index.d.ts.map +1 -0
  149. package/dist/routes/values/[labelId]/[version]/index.js +45 -0
  150. package/dist/routes/values/[labelId]/[version]/index.js.map +1 -0
  151. package/dist/routes/values/[labelId]/contract.d.ts +38 -0
  152. package/dist/routes/values/[labelId]/contract.d.ts.map +1 -0
  153. package/dist/routes/values/[labelId]/contract.js +59 -0
  154. package/dist/routes/values/[labelId]/contract.js.map +1 -0
  155. package/dist/routes/values/[labelId]/index.d.ts +8 -0
  156. package/dist/routes/values/[labelId]/index.d.ts.map +1 -0
  157. package/dist/routes/values/[labelId]/index.js +42 -0
  158. package/dist/routes/values/[labelId]/index.js.map +1 -0
  159. package/dist/server.d.ts +99 -0
  160. package/dist/server.d.ts.map +1 -0
  161. package/dist/server.js +256 -0
  162. package/dist/server.js.map +1 -0
  163. package/dist/store.d.ts +87 -0
  164. package/dist/store.d.ts.map +1 -0
  165. package/dist/store.js +205 -0
  166. package/dist/store.js.map +1 -0
  167. package/dist/sync.d.ts +11 -0
  168. package/dist/sync.d.ts.map +1 -0
  169. package/dist/sync.js +179 -0
  170. package/dist/sync.js.map +1 -0
  171. package/dist/types.d.ts +74 -0
  172. package/dist/types.d.ts.map +1 -0
  173. package/dist/types.js +7 -0
  174. package/dist/types.js.map +1 -0
  175. package/package.json +95 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 INFLIKE Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,490 @@
1
+ # @spfn/cms
2
+
3
+ Content Management System for Next.js with JSON-based labels and automatic database synchronization.
4
+
5
+ ## Features
6
+
7
+ - 📁 **JSON file-based labels** - Simple file structure for label management
8
+ - 🔄 **Auto-sync to database** on server startup and during development
9
+ - 🌐 **Multi-language support** (i18n)
10
+ - 🍪 **Cookie-based locale management** - Automatic locale detection and persistence
11
+ - 📦 **Folder-based structure** for better organization
12
+ - 🔥 **Hot reload** during development
13
+ - 💾 **Published cache** for optimal performance
14
+ - ⚡ **Server Actions** for client-side locale management
15
+ - 🛠️ **Built on Drizzle ORM**
16
+
17
+ ## Installation
18
+
19
+ ### Recommended: Using SPFN CLI (Automatic Database Setup)
20
+
21
+ ```bash
22
+ pnpm spfn add @spfn/cms
23
+ ```
24
+
25
+ This command will:
26
+ 1. ✅ Install the package
27
+ 2. ✅ Discover CMS database schemas automatically
28
+ 3. ✅ Generate migrations for 6 CMS tables
29
+ 4. ✅ Apply migrations to your database
30
+ 5. ✅ Show setup guide
31
+
32
+ **Tables created:**
33
+ - `cms_labels` - Label definitions (10 columns, 2 indexes)
34
+ - `cms_label_values` - Label values per locale (7 columns, 2 indexes, 1 FK)
35
+ - `cms_label_versions` - Version history (9 columns, 2 indexes, 1 FK)
36
+ - `cms_draft_cache` - Draft content cache (6 columns, 2 indexes)
37
+ - `cms_published_cache` - Published content cache (7 columns, 1 index)
38
+ - `cms_audit_logs` - Change audit trail (8 columns, 4 indexes, 1 FK)
39
+
40
+ ### Manual Installation
41
+
42
+ ```bash
43
+ pnpm add @spfn/cms
44
+ ```
45
+
46
+ Then run database migrations:
47
+
48
+ ```bash
49
+ pnpm spfn db generate # Generate migrations
50
+ pnpm spfn db migrate # Apply migrations
51
+ ```
52
+
53
+ **Note:** Manual installation requires that you have `DATABASE_URL` configured in your `.env.local` file.
54
+
55
+ ## Quick Start
56
+
57
+ ### 1. Create Label Files
58
+
59
+ Create JSON files organized by sections and categories:
60
+
61
+ ```
62
+ src/cms/labels/
63
+ layout/ ← Section name
64
+ nav.json ← Category
65
+ footer.json
66
+ home/
67
+ hero.json
68
+ features.json
69
+ ```
70
+
71
+ **Example:** `src/cms/labels/layout/nav.json`
72
+
73
+ ```json
74
+ {
75
+ "about": {
76
+ "key": "layout.nav.about",
77
+ "defaultValue": "About",
78
+ "description": "Navigation link for About page"
79
+ },
80
+ "services": {
81
+ "key": "layout.nav.services",
82
+ "defaultValue": "Services",
83
+ "description": "Navigation link for Services page"
84
+ },
85
+ "team": {
86
+ "key": "layout.nav.team",
87
+ "defaultValue": "Team"
88
+ }
89
+ }
90
+ ```
91
+
92
+ **Multi-language example:** `src/cms/labels/home/hero.json`
93
+
94
+ ```json
95
+ {
96
+ "title": {
97
+ "key": "home.hero.title",
98
+ "defaultValue": {
99
+ "ko": "혁신적인 솔루션",
100
+ "en": "Innovative Solutions"
101
+ }
102
+ },
103
+ "subtitle": {
104
+ "key": "home.hero.subtitle",
105
+ "defaultValue": {
106
+ "ko": "비즈니스 성장을 위한 최고의 파트너",
107
+ "en": "Your Best Partner for Business Growth"
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ **Variable substitution:** `src/cms/labels/layout/footer.json`
114
+
115
+ ```json
116
+ {
117
+ "copyright": {
118
+ "key": "layout.footer.copyright",
119
+ "defaultValue": "© {year} Company. All rights reserved."
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### 2. Enable Auto-Sync on Server Startup
125
+
126
+ Configure `src/server/server.config.ts`:
127
+
128
+ ```typescript
129
+ import type { ServerConfig } from '@spfn/core/server';
130
+ import { initLabelSync } from '@spfn/cms';
131
+
132
+ export default {
133
+ beforeRoutes: async (app) => {
134
+ await initLabelSync({
135
+ verbose: true,
136
+ labelsDir: 'src/cms/labels' // Optional, this is the default
137
+ });
138
+ },
139
+ } satisfies ServerConfig;
140
+ ```
141
+
142
+ ### 3. Enable Auto-Sync During Development
143
+
144
+ Your `.spfnrc.json` should include:
145
+
146
+ ```json
147
+ {
148
+ "codegen": {
149
+ "generators": [
150
+ {
151
+ "name": "@spfn/cms:label-sync",
152
+ "enabled": true
153
+ }
154
+ ]
155
+ }
156
+ }
157
+ ```
158
+
159
+ This is automatically configured when you run `pnpm spfn add @spfn/cms`.
160
+
161
+ ### 4. Use Labels in Your App
162
+
163
+ **Server Component:**
164
+
165
+ ```typescript
166
+ import { getSection } from '@spfn/cms/server';
167
+
168
+ export default async function HomePage() {
169
+ const { t } = await getSection('layout', 'ko');
170
+
171
+ return <h1>{t('nav.team')}</h1>;
172
+ }
173
+ ```
174
+
175
+ **With variable substitution:**
176
+
177
+ ```typescript
178
+ const { t } = await getSection('layout');
179
+ const copyright = t('footer.copyright', undefined, {
180
+ year: new Date().getFullYear()
181
+ });
182
+ // → "© 2025 Company. All rights reserved."
183
+ ```
184
+
185
+ **Client Component:**
186
+
187
+ ```typescript
188
+ 'use client';
189
+ import { useSection } from '@spfn/cms/client';
190
+
191
+ export default function Nav() {
192
+ const { t, loading } = useSection('layout', { autoLoad: true });
193
+
194
+ if (loading) return <div>Loading...</div>;
195
+
196
+ return (
197
+ <nav>
198
+ <a>{t('nav.about')}</a>
199
+ <a>{t('nav.services')}</a>
200
+ </nav>
201
+ );
202
+ }
203
+ ```
204
+
205
+ ## File Structure
206
+
207
+ ```
208
+ src/cms/labels/
209
+ layout/ # Section: layout
210
+ nav.json # Category: nav
211
+ footer.json # Category: footer
212
+ home/ # Section: home
213
+ hero.json # Category: hero
214
+ features.json # Category: features
215
+ ```
216
+
217
+ **How it maps:**
218
+ - Folder name = Section name
219
+ - JSON file name = Category name (for organization only)
220
+ - Inside JSON: `key` field defines the actual label key
221
+
222
+ Example:
223
+ ```
224
+ src/cms/labels/layout/nav.json:
225
+ key: "layout.nav.team" → t('nav.team') in code
226
+ ```
227
+
228
+ ## JSON Label Format
229
+
230
+ ```typescript
231
+ {
232
+ "labelName": {
233
+ "key": "section.category.name", // Required: Unique identifier
234
+ "defaultValue": "Text" | {...}, // Required: String or i18n object
235
+ "description": "Optional description" // Optional: For documentation
236
+ }
237
+ }
238
+ ```
239
+
240
+ **Single language:**
241
+ ```json
242
+ {
243
+ "welcome": {
244
+ "key": "home.welcome",
245
+ "defaultValue": "Welcome"
246
+ }
247
+ }
248
+ ```
249
+
250
+ **Multi-language:**
251
+ ```json
252
+ {
253
+ "welcome": {
254
+ "key": "home.welcome",
255
+ "defaultValue": {
256
+ "ko": "환영합니다",
257
+ "en": "Welcome",
258
+ "ja": "ようこそ"
259
+ }
260
+ }
261
+ }
262
+ ```
263
+
264
+ **Variable placeholders:**
265
+ ```json
266
+ {
267
+ "greeting": {
268
+ "key": "home.greeting",
269
+ "defaultValue": "Hello, {name}!"
270
+ }
271
+ }
272
+ ```
273
+
274
+ Usage:
275
+ ```typescript
276
+ t('greeting', undefined, { name: 'John' })
277
+ // → "Hello, John!"
278
+ ```
279
+
280
+ ## Configuration
281
+
282
+ ### Environment Variables
283
+
284
+ Configure CMS behavior via environment variables in `.env.local`:
285
+
286
+ ```bash
287
+ # Default locale (default: 'en')
288
+ SPFN_CMS_DEFAULT_LOCALE=ko
289
+
290
+ # Supported locales, comma-separated (default: 'en,ko')
291
+ SPFN_CMS_SUPPORTED_LOCALES=en,ko,ja
292
+
293
+ # Auto-detect browser language (default: true)
294
+ SPFN_CMS_DETECT_BROWSER_LANGUAGE=true
295
+ ```
296
+
297
+ ### Runtime Configuration
298
+
299
+ Override configuration at runtime (mainly for testing):
300
+
301
+ ```typescript
302
+ import { configureCms, getCmsConfig } from '@spfn/cms';
303
+
304
+ // Get current configuration
305
+ const config = getCmsConfig();
306
+ console.log(config.defaultLocale); // 'ko'
307
+ console.log(config.supportedLocales); // ['ko', 'en']
308
+
309
+ // Update configuration
310
+ configureCms({
311
+ defaultLocale: 'en',
312
+ supportedLocales: ['en', 'ko', 'ja'],
313
+ detectBrowserLanguage: false
314
+ });
315
+ ```
316
+
317
+ ## Locale Management
318
+
319
+ ### Automatic Locale Detection
320
+
321
+ The CMS automatically manages user locale with the following priority:
322
+
323
+ 1. **Cookie** - User's explicitly selected locale (persisted)
324
+ 2. **Browser Language** - Auto-detected from `Accept-Language` header (if enabled)
325
+ 3. **Default Locale** - System default from environment variables
326
+
327
+ ### Server Actions (`@spfn/cms/actions`)
328
+
329
+ Use Server Actions for locale management in both server and client components:
330
+
331
+ **Get current locale:**
332
+
333
+ ```typescript
334
+ // Server Component
335
+ import { getLocale } from '@spfn/cms/actions';
336
+
337
+ export default async function RootLayout({ children }) {
338
+ const locale = await getLocale();
339
+
340
+ return <html lang={locale}>{children}</html>;
341
+ }
342
+ ```
343
+
344
+ ```typescript
345
+ // Client Component
346
+ 'use client';
347
+ import { getLocale } from '@spfn/cms/actions';
348
+ import { useEffect, useState } from 'react';
349
+
350
+ export default function LanguageSwitcher() {
351
+ const [locale, setLocale] = useState('');
352
+
353
+ useEffect(() => {
354
+ getLocale().then(setLocale);
355
+ }, []);
356
+
357
+ return <div>Current: {locale}</div>;
358
+ }
359
+ ```
360
+
361
+ **Change locale:**
362
+
363
+ ```typescript
364
+ import { setLocale } from '@spfn/cms/actions';
365
+
366
+ async function changeLanguage(newLocale: string) {
367
+ await setLocale(newLocale);
368
+ window.location.reload(); // Reload to apply changes
369
+ }
370
+ ```
371
+
372
+ **Get supported locales:**
373
+
374
+ ```typescript
375
+ import { getLocales } from '@spfn/cms/actions';
376
+
377
+ const locales = await getLocales(); // ['ko', 'en', 'ja']
378
+ ```
379
+
380
+ ### Auto-detect Locale in Server Components
381
+
382
+ When `locale` is not specified, `getSection()` automatically uses the detected locale:
383
+
384
+ ```typescript
385
+ import { getSection } from '@spfn/cms/server';
386
+
387
+ // Auto-detects locale from cookie → browser → default
388
+ const { t } = await getSection('home');
389
+
390
+ // Or explicitly specify locale
391
+ const { t: tEn } = await getSection('home', 'en');
392
+ ```
393
+
394
+ ## Documentation
395
+
396
+ - **[Label Auto-Sync Guide](./LABEL_SYNC_GUIDE.md)** - Detailed configuration guide
397
+ - **[Examples](./examples/)** - Usage examples
398
+
399
+ ## Architecture
400
+
401
+ ```
402
+ JSON Files (src/cms/labels/**/*.json)
403
+
404
+ loadLabelsFromJson()
405
+
406
+ ┌─────────────────────┐
407
+ │ LabelSyncGenerator │ ← File watcher (development)
408
+ │ initLabelSync() │ ← Server startup
409
+ └─────────────────────┘
410
+
411
+ syncAll()
412
+
413
+ ┌─────────────────────┐
414
+ │ PostgreSQL DB │
415
+ │ - cms_labels │
416
+ │ - published_cache │ ⭐ Used by API
417
+ └─────────────────────┘
418
+ ↓ HTTP API
419
+ ┌─────────────────────┐
420
+ │ Application │
421
+ │ - getSection() │
422
+ │ - useSection() │
423
+ └─────────────────────┘
424
+ ```
425
+
426
+ ## API Reference
427
+
428
+ ### Server-side API
429
+
430
+ - `getSection(section, locale?)` - Get section labels (auto-detects locale if not specified)
431
+ - `getSections(sections, locale?)` - Get multiple sections (auto-detects locale if not specified)
432
+ - `initLabelSync(options?)` - Sync labels on server startup
433
+
434
+ ### Server Actions API (`@spfn/cms/actions`)
435
+
436
+ Available for both server and client components:
437
+
438
+ - `getLocale()` - Get current locale (cookie → browser → default)
439
+ - `setLocale(locale)` - Set locale (saves to cookie)
440
+ - `getLocales()` - Get supported locale list
441
+ - `LOCALE_COOKIE_KEY` - Locale cookie key constant
442
+
443
+ ### Configuration API
444
+
445
+ - `getCmsConfig()` - Get current CMS configuration
446
+ - `configureCms(config)` - Update configuration (runtime)
447
+ - `resetCmsConfig()` - Reset configuration to defaults
448
+
449
+ ### Client-side API (`@spfn/cms/client`)
450
+
451
+ - `useSection(section, options?)` - Section labels hook
452
+ - `useSections(sections)` - Multiple sections hook
453
+ - `useCmsStore()` - CMS store hook
454
+ - `cmsApi` - CMS API client
455
+ - `InitCms` - Client initialization component
456
+
457
+ ### Sync API
458
+
459
+ - `loadLabelsFromJson(labelsDir)` - Load labels from JSON files
460
+ - `syncAll(sections, options?)` - Sync all sections
461
+ - `syncSection(definition, options?)` - Sync specific section
462
+
463
+ ### Codegen Integration
464
+
465
+ - `createLabelSyncGenerator(config?)` - Generator factory
466
+ - `LabelSyncGenerator` - Generator class
467
+
468
+ ## Development Workflow
469
+
470
+ 1. **Create/Edit JSON files** in `src/cms/labels/`
471
+ 2. **Auto-sync happens** (if dev server is running)
472
+ 3. **Labels immediately available** via `getSection()` or `useSection()`
473
+
474
+ **Example:**
475
+
476
+ ```bash
477
+ # Terminal 1: Start dev server
478
+ pnpm dev
479
+
480
+ # Terminal 2: Edit label file
481
+ echo '{"test": {"key": "layout.test", "defaultValue": "Test"}}' > src/cms/labels/layout/test.json
482
+
483
+ # Auto-sync triggers
484
+ # ✅ Label sync completed
485
+ # Created: 1
486
+ ```
487
+
488
+ ## License
489
+
490
+ MIT
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @spfn/cms/actions
3
+ *
4
+ * Server Actions
5
+ * 서버/클라이언트 컴포넌트 양쪽에서 사용 가능한 Server Actions
6
+ */
7
+ export { getLocale, setLocale, getLocales, } from './helpers/locale.actions.js';
8
+ export { LOCALE_COOKIE_KEY } from './helpers/locale.constants.js';
9
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACH,SAAS,EACT,SAAS,EACT,UAAU,GACb,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @spfn/cms/actions
3
+ *
4
+ * Server Actions
5
+ * 서버/클라이언트 컴포넌트 양쪽에서 사용 가능한 Server Actions
6
+ */
7
+ // Locale Server Actions
8
+ export { getLocale, setLocale, getLocales, } from './helpers/locale.actions.js';
9
+ // Locale Constants
10
+ export { LOCALE_COOKIE_KEY } from './helpers/locale.constants.js';
11
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wBAAwB;AACxB,OAAO,EACH,SAAS,EACT,SAAS,EACT,UAAU,GACb,MAAM,6BAA6B,CAAC;AAErC,mBAAmB;AACnB,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @spfn/cms/client
3
+ *
4
+ * Client Components Only
5
+ * 클라이언트 컴포넌트 전용 (브라우저에서 실행)
6
+ */
7
+ import type { InferContract } from '@spfn/core';
8
+ import { getLabelsContract, createLabelContract } from './routes/labels/contract';
9
+ import { getLabelContract, updateLabelContract, deleteLabelContract } from './routes/labels/[id]/contract';
10
+ import { getPublishedCacheContract } from './routes/published-cache/contract';
11
+ /**
12
+ * CMS API Client
13
+ */
14
+ export declare const cmsApi: {
15
+ /**
16
+ * Labels API
17
+ */
18
+ readonly labels: {
19
+ /**
20
+ * GET /cms/labels
21
+ * 라벨 목록 조회 (섹션 필터, 페이지네이션)
22
+ */
23
+ readonly list: (options?: {
24
+ query?: InferContract<typeof getLabelsContract>["query"];
25
+ }) => Promise<{
26
+ limit: number;
27
+ offset: number;
28
+ labels: {
29
+ section: string;
30
+ id: number;
31
+ key: string;
32
+ type: string;
33
+ publishedVersion: number | null;
34
+ createdBy: string | null;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ }[];
38
+ total: number;
39
+ }>;
40
+ /**
41
+ * GET /cms/labels/:id
42
+ * 특정 라벨 조회
43
+ */
44
+ readonly getById: (options: {
45
+ params: InferContract<typeof getLabelContract>["params"];
46
+ }) => Promise<{
47
+ section: string;
48
+ id: number;
49
+ key: string;
50
+ type: string;
51
+ publishedVersion: number | null;
52
+ createdBy: string | null;
53
+ createdAt: string;
54
+ updatedAt: string;
55
+ } | {
56
+ error: string;
57
+ }>;
58
+ /**
59
+ * POST /cms/labels
60
+ * 새 라벨 생성
61
+ */
62
+ readonly create: (options: {
63
+ body: InferContract<typeof createLabelContract>["body"];
64
+ }) => Promise<{
65
+ section: string;
66
+ id: number;
67
+ key: string;
68
+ type: string;
69
+ publishedVersion: number | null;
70
+ createdBy: string | null;
71
+ createdAt: string;
72
+ updatedAt: string;
73
+ } | {
74
+ key?: string | undefined;
75
+ error: string;
76
+ }>;
77
+ /**
78
+ * PATCH /cms/labels/:id
79
+ * 라벨 업데이트
80
+ */
81
+ readonly update: (options: {
82
+ params: InferContract<typeof updateLabelContract>["params"];
83
+ body: InferContract<typeof updateLabelContract>["body"];
84
+ }) => Promise<{
85
+ section: string;
86
+ id: number;
87
+ key: string;
88
+ type: string;
89
+ publishedVersion: number | null;
90
+ createdBy: string | null;
91
+ createdAt: string;
92
+ updatedAt: string;
93
+ } | {
94
+ error: string;
95
+ }>;
96
+ /**
97
+ * DELETE /cms/labels/:id
98
+ * 라벨 삭제
99
+ */
100
+ readonly delete: (options: {
101
+ params: InferContract<typeof deleteLabelContract>["params"];
102
+ }) => Promise<{
103
+ id: number;
104
+ success: boolean;
105
+ } | {
106
+ error: string;
107
+ }>;
108
+ };
109
+ /**
110
+ * Published Cache API
111
+ */
112
+ readonly publishedCache: {
113
+ /**
114
+ * GET /cms/published-cache
115
+ * 발행된 콘텐츠 캐시 조회
116
+ */
117
+ readonly get: (options: {
118
+ query: InferContract<typeof getPublishedCacheContract>["query"];
119
+ }) => Promise<{
120
+ section: string;
121
+ locale: string;
122
+ content: {
123
+ [x: string]: any;
124
+ };
125
+ version: number;
126
+ publishedAt: string | null;
127
+ }[] | {
128
+ error: string;
129
+ }>;
130
+ };
131
+ };
132
+ /**
133
+ * Type exports
134
+ */
135
+ export type CmsApi = typeof cmsApi;
136
+ export { useCmsStore, useSection, useSections } from './store';
137
+ export { InitCms } from './init';
138
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EACH,iBAAiB,EACjB,mBAAmB,EACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACH,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAE9E;;GAEG;AACH,eAAO,MAAM,MAAM;IACf;;OAEG;;QAEC;;;WAGG;kCACc;YAAE,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,iBAAiB,CAAC,CAAC,OAAO,CAAC,CAAA;SAAE;;;;;;;;;;;;;;;QAG7E;;;WAGG;oCACgB;YAAE,MAAM,EAAE,aAAa,CAAC,OAAO,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAA;SAAE;;;;;;;;;;;;QAG/E;;;WAGG;mCACe;YAAE,IAAI,EAAE,aAAa,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;SAAE;;;;;;;;;;;;;QAG7E;;;WAGG;mCACe;YACd,MAAM,EAAE,aAAa,CAAC,OAAO,mBAAmB,CAAC,CAAC,QAAQ,CAAC,CAAC;YAC5D,IAAI,EAAE,aAAa,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;SAC3D;;;;;;;;;;;;QAGD;;;WAGG;mCACe;YAAE,MAAM,EAAE,aAAa,CAAC,OAAO,mBAAmB,CAAC,CAAC,QAAQ,CAAC,CAAA;SAAE;;;;;;;IAIrF;;OAEG;;QAEC;;;WAGG;gCACY;YAAE,KAAK,EAAE,aAAa,CAAC,OAAO,yBAAyB,CAAC,CAAC,OAAO,CAAC,CAAA;SAAE;;;;;;;;;;;;CAGhF,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC;AAGnC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC"}