@spfn/cms 0.1.0-alpha.9 → 0.2.0-beta.10

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 (171) hide show
  1. package/README.md +436 -342
  2. package/dist/actions.d.ts +12 -6
  3. package/dist/actions.js +25 -10
  4. package/dist/actions.js.map +1 -1
  5. package/dist/config.d.ts +40 -0
  6. package/dist/config.js +39 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/errors.d.ts +149 -0
  9. package/dist/errors.js +164 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +199 -20
  12. package/dist/index.js +202 -23
  13. package/dist/index.js.map +1 -1
  14. package/dist/server.d.ts +99 -81
  15. package/dist/server.js +842 -256
  16. package/dist/server.js.map +1 -1
  17. package/migrations/0000_medical_ozymandias.sql +54 -0
  18. package/migrations/meta/0000_snapshot.json +336 -0
  19. package/migrations/meta/_journal.json +13 -0
  20. package/package.json +56 -46
  21. package/dist/actions.d.ts.map +0 -1
  22. package/dist/client.d.ts +0 -138
  23. package/dist/client.d.ts.map +0 -1
  24. package/dist/client.js +0 -62
  25. package/dist/client.js.map +0 -1
  26. package/dist/cms.config.d.ts +0 -77
  27. package/dist/cms.config.d.ts.map +0 -1
  28. package/dist/cms.config.js +0 -111
  29. package/dist/cms.config.js.map +0 -1
  30. package/dist/entities/cms-audit-logs.d.ts +0 -213
  31. package/dist/entities/cms-audit-logs.d.ts.map +0 -1
  32. package/dist/entities/cms-audit-logs.js +0 -103
  33. package/dist/entities/cms-audit-logs.js.map +0 -1
  34. package/dist/entities/cms-draft-cache.d.ts +0 -188
  35. package/dist/entities/cms-draft-cache.d.ts.map +0 -1
  36. package/dist/entities/cms-draft-cache.js +0 -112
  37. package/dist/entities/cms-draft-cache.js.map +0 -1
  38. package/dist/entities/cms-label-values.d.ts +0 -192
  39. package/dist/entities/cms-label-values.d.ts.map +0 -1
  40. package/dist/entities/cms-label-values.js +0 -105
  41. package/dist/entities/cms-label-values.js.map +0 -1
  42. package/dist/entities/cms-label-versions.d.ts +0 -207
  43. package/dist/entities/cms-label-versions.d.ts.map +0 -1
  44. package/dist/entities/cms-label-versions.js +0 -80
  45. package/dist/entities/cms-label-versions.js.map +0 -1
  46. package/dist/entities/cms-labels.d.ts +0 -189
  47. package/dist/entities/cms-labels.d.ts.map +0 -1
  48. package/dist/entities/cms-labels.js +0 -48
  49. package/dist/entities/cms-labels.js.map +0 -1
  50. package/dist/entities/cms-published-cache.d.ts +0 -199
  51. package/dist/entities/cms-published-cache.d.ts.map +0 -1
  52. package/dist/entities/cms-published-cache.js +0 -103
  53. package/dist/entities/cms-published-cache.js.map +0 -1
  54. package/dist/entities/index.d.ts +0 -10
  55. package/dist/entities/index.d.ts.map +0 -1
  56. package/dist/entities/index.js +0 -10
  57. package/dist/entities/index.js.map +0 -1
  58. package/dist/generators/index.d.ts +0 -19
  59. package/dist/generators/index.d.ts.map +0 -1
  60. package/dist/generators/index.js +0 -19
  61. package/dist/generators/index.js.map +0 -1
  62. package/dist/generators/label-sync-generator.d.ts +0 -33
  63. package/dist/generators/label-sync-generator.d.ts.map +0 -1
  64. package/dist/generators/label-sync-generator.js +0 -86
  65. package/dist/generators/label-sync-generator.js.map +0 -1
  66. package/dist/helpers/locale.actions.d.ts +0 -132
  67. package/dist/helpers/locale.actions.d.ts.map +0 -1
  68. package/dist/helpers/locale.actions.js +0 -210
  69. package/dist/helpers/locale.actions.js.map +0 -1
  70. package/dist/helpers/locale.constants.d.ts +0 -10
  71. package/dist/helpers/locale.constants.d.ts.map +0 -1
  72. package/dist/helpers/locale.constants.js +0 -10
  73. package/dist/helpers/locale.constants.js.map +0 -1
  74. package/dist/helpers/locale.d.ts +0 -17
  75. package/dist/helpers/locale.d.ts.map +0 -1
  76. package/dist/helpers/locale.js +0 -20
  77. package/dist/helpers/locale.js.map +0 -1
  78. package/dist/helpers/sync.d.ts +0 -41
  79. package/dist/helpers/sync.d.ts.map +0 -1
  80. package/dist/helpers/sync.js +0 -309
  81. package/dist/helpers/sync.js.map +0 -1
  82. package/dist/index.d.ts.map +0 -1
  83. package/dist/init.d.ts +0 -31
  84. package/dist/init.d.ts.map +0 -1
  85. package/dist/init.js +0 -36
  86. package/dist/init.js.map +0 -1
  87. package/dist/labels/helpers.d.ts +0 -31
  88. package/dist/labels/helpers.d.ts.map +0 -1
  89. package/dist/labels/helpers.js +0 -60
  90. package/dist/labels/helpers.js.map +0 -1
  91. package/dist/labels/index.d.ts +0 -7
  92. package/dist/labels/index.d.ts.map +0 -1
  93. package/dist/labels/index.js +0 -7
  94. package/dist/labels/index.js.map +0 -1
  95. package/dist/repositories/cms-draft-cache.repository.d.ts +0 -62
  96. package/dist/repositories/cms-draft-cache.repository.d.ts.map +0 -1
  97. package/dist/repositories/cms-draft-cache.repository.js +0 -56
  98. package/dist/repositories/cms-draft-cache.repository.js.map +0 -1
  99. package/dist/repositories/cms-label-values.repository.d.ts +0 -32
  100. package/dist/repositories/cms-label-values.repository.d.ts.map +0 -1
  101. package/dist/repositories/cms-label-values.repository.js +0 -72
  102. package/dist/repositories/cms-label-values.repository.js.map +0 -1
  103. package/dist/repositories/cms-labels.repository.d.ts +0 -53
  104. package/dist/repositories/cms-labels.repository.d.ts.map +0 -1
  105. package/dist/repositories/cms-labels.repository.js +0 -77
  106. package/dist/repositories/cms-labels.repository.js.map +0 -1
  107. package/dist/repositories/cms-published-cache.repository.d.ts +0 -53
  108. package/dist/repositories/cms-published-cache.repository.d.ts.map +0 -1
  109. package/dist/repositories/cms-published-cache.repository.js +0 -54
  110. package/dist/repositories/cms-published-cache.repository.js.map +0 -1
  111. package/dist/repositories/index.d.ts +0 -8
  112. package/dist/repositories/index.d.ts.map +0 -1
  113. package/dist/repositories/index.js +0 -9
  114. package/dist/repositories/index.js.map +0 -1
  115. package/dist/routes/labels/[id]/contract.d.ts +0 -68
  116. package/dist/routes/labels/[id]/contract.d.ts.map +0 -1
  117. package/dist/routes/labels/[id]/contract.js +0 -84
  118. package/dist/routes/labels/[id]/contract.js.map +0 -1
  119. package/dist/routes/labels/[id]/index.d.ts +0 -10
  120. package/dist/routes/labels/[id]/index.d.ts.map +0 -1
  121. package/dist/routes/labels/[id]/index.js +0 -96
  122. package/dist/routes/labels/[id]/index.js.map +0 -1
  123. package/dist/routes/labels/by-key/[key]/contract.d.ts +0 -24
  124. package/dist/routes/labels/by-key/[key]/contract.d.ts.map +0 -1
  125. package/dist/routes/labels/by-key/[key]/contract.js +0 -28
  126. package/dist/routes/labels/by-key/[key]/contract.js.map +0 -1
  127. package/dist/routes/labels/by-key/[key]/index.d.ts +0 -8
  128. package/dist/routes/labels/by-key/[key]/index.d.ts.map +0 -1
  129. package/dist/routes/labels/by-key/[key]/index.js +0 -32
  130. package/dist/routes/labels/by-key/[key]/index.js.map +0 -1
  131. package/dist/routes/labels/contract.d.ts +0 -59
  132. package/dist/routes/labels/contract.d.ts.map +0 -1
  133. package/dist/routes/labels/contract.js +0 -75
  134. package/dist/routes/labels/contract.js.map +0 -1
  135. package/dist/routes/labels/index.d.ts +0 -10
  136. package/dist/routes/labels/index.d.ts.map +0 -1
  137. package/dist/routes/labels/index.js +0 -73
  138. package/dist/routes/labels/index.js.map +0 -1
  139. package/dist/routes/published-cache/contract.d.ts +0 -25
  140. package/dist/routes/published-cache/contract.d.ts.map +0 -1
  141. package/dist/routes/published-cache/contract.js +0 -35
  142. package/dist/routes/published-cache/contract.js.map +0 -1
  143. package/dist/routes/published-cache/index.d.ts +0 -8
  144. package/dist/routes/published-cache/index.d.ts.map +0 -1
  145. package/dist/routes/published-cache/index.js +0 -33
  146. package/dist/routes/published-cache/index.js.map +0 -1
  147. package/dist/routes/values/[labelId]/[version]/contract.d.ts +0 -29
  148. package/dist/routes/values/[labelId]/[version]/contract.d.ts.map +0 -1
  149. package/dist/routes/values/[labelId]/[version]/contract.js +0 -33
  150. package/dist/routes/values/[labelId]/[version]/contract.js.map +0 -1
  151. package/dist/routes/values/[labelId]/[version]/index.d.ts +0 -8
  152. package/dist/routes/values/[labelId]/[version]/index.d.ts.map +0 -1
  153. package/dist/routes/values/[labelId]/[version]/index.js +0 -45
  154. package/dist/routes/values/[labelId]/[version]/index.js.map +0 -1
  155. package/dist/routes/values/[labelId]/contract.d.ts +0 -38
  156. package/dist/routes/values/[labelId]/contract.d.ts.map +0 -1
  157. package/dist/routes/values/[labelId]/contract.js +0 -59
  158. package/dist/routes/values/[labelId]/contract.js.map +0 -1
  159. package/dist/routes/values/[labelId]/index.d.ts +0 -8
  160. package/dist/routes/values/[labelId]/index.d.ts.map +0 -1
  161. package/dist/routes/values/[labelId]/index.js +0 -42
  162. package/dist/routes/values/[labelId]/index.js.map +0 -1
  163. package/dist/server.d.ts.map +0 -1
  164. package/dist/store.d.ts +0 -87
  165. package/dist/store.d.ts.map +0 -1
  166. package/dist/store.js +0 -205
  167. package/dist/store.js.map +0 -1
  168. package/dist/types.d.ts +0 -74
  169. package/dist/types.d.ts.map +0 -1
  170. package/dist/types.js +0 -7
  171. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -1,490 +1,584 @@
1
1
  # @spfn/cms
2
2
 
3
- Content Management System for Next.js with JSON-based labels and automatic database synchronization.
3
+ Type-safe Content Management System for Next.js with automatic database synchronization and published cache.
4
4
 
5
5
  ## Features
6
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**
7
+ - 🎯 **Type-safe labels** - Full TypeScript support with autocomplete
8
+ - 🔄 **Auto-sync** - Database synchronization on server startup
9
+ - 🌐 **Multi-language** - Type-checked locale support
10
+ - 🍪 **Smart locale detection** - Cookie-based with automatic fallback
11
+ - 💾 **Published cache** - 17x faster queries (5ms vs 87ms)
12
+ - 🎨 **Nested structure** - Organize labels hierarchically
13
+ - 🔧 **Template variables** - Dynamic content with `{placeholder}` syntax
14
+ - 📝 **Draft & versioning** - Version control and audit logs
16
15
 
17
16
  ## Installation
18
17
 
19
- ### Recommended: Using SPFN CLI (Automatic Database Setup)
20
-
21
18
  ```bash
22
19
  pnpm spfn add @spfn/cms
23
20
  ```
24
21
 
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
22
+ ## Quick Start
31
23
 
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)
24
+ ### 1. Define Labels & Configuration
39
25
 
40
- ### Manual Installation
26
+ ```typescript
27
+ // labels.ts
28
+ import { defineLabelConfig, defineLabels, createCmsClient } from '@spfn/cms';
29
+ import { getLocale } from '@spfn/cms/actions';
41
30
 
42
- ```bash
43
- pnpm add @spfn/cms
44
- ```
31
+ // Configure locales
32
+ export const labelConfig = defineLabelConfig({
33
+ locales: ['en', 'ko'] as const,
34
+ defaultLocale: 'ko',
35
+ fallbackLocale: 'en'
36
+ });
45
37
 
46
- Then run database migrations:
38
+ // Define labels with nested structure
39
+ export const labelsDefinition = defineLabels({
40
+ home: {
41
+ hero: {
42
+ title: { en: "Welcome", ko: "환영합니다" },
43
+ subtitle: { en: "Start your journey", ko: "여정을 시작하세요" },
44
+ greeting: { en: "Hello {name}!", ko: "{name}님 안녕하세요!" }
45
+ },
46
+ cta: { en: "Get Started", ko: "시작하기" }
47
+ },
48
+ about: {
49
+ title: { en: "About Us", ko: "회사 소개" }
50
+ }
51
+ });
47
52
 
48
- ```bash
49
- pnpm spfn db generate # Generate migrations
50
- pnpm spfn db migrate # Apply migrations
53
+ // Create client with API, getLabel, getLabels, and format
54
+ export const { api, getLabel, getLabels, format } = createCmsClient(
55
+ labelsDefinition,
56
+ {
57
+ defaultLocale: labelConfig.defaultLocale,
58
+ fallbackLocale: labelConfig.fallbackLocale,
59
+ getLocale: () => getLocale(labelConfig.defaultLocale),
60
+ }
61
+ );
51
62
  ```
52
63
 
53
- **Note:** Manual installation requires that you have `DATABASE_URL` configured in your `.env.local` file.
64
+ ### 2. Enable Auto-Sync
54
65
 
55
- ## Quick Start
66
+ ```typescript
67
+ // server.config.ts
68
+ import { defineServerConfig } from '@spfn/core/server';
69
+ import { syncLabels } from '@spfn/cms/server';
70
+ import { labelsDefinition } from './labels';
56
71
 
57
- ### 1. Create Label Files
58
72
 
59
- Create JSON files organized by sections and categories:
73
+ // Option 1: Single definition
74
+ export default defineServerConfig()
75
+ .lifecycle({
76
+ afterInfrastructure: async () => {
77
+ await syncLabels(labelsDefinition);
78
+ }
79
+ })
80
+ .build();
60
81
 
61
- ```
62
- src/cms/labels/
63
- layout/ ← Section name
64
- nav.json ← Category
65
- footer.json
66
- home/
67
- hero.json
68
- features.json
82
+ // Option 2: Multiple definitions (organized in separate files)
83
+ import { homeLabels } from './labels/home';
84
+ import { aboutLabels } from './labels/about';
85
+
86
+ export default defineServerConfig()
87
+ .lifecycle({
88
+ afterInfrastructure: async () => {
89
+ await syncLabels([homeLabels, aboutLabels]);
90
+ }
91
+ })
92
+ .build();
69
93
  ```
70
94
 
71
- **Example:** `src/cms/labels/layout/nav.json`
95
+ ### 3. Use in Your App
72
96
 
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
- }
97
+ **Server Component (Single Section):**
98
+
99
+ ```typescript
100
+ import { getLabel, format } from '@/labels';
101
+
102
+ export default async function HomePage() {
103
+ // Single section - direct access without section name
104
+ const label = await getLabel('home');
105
+
106
+ return (
107
+ <div>
108
+ <h1>{label.hero.title}</h1>
109
+ <p>{label.hero.subtitle}</p>
110
+ <button>{label.cta}</button>
111
+
112
+ {/* Template variables */}
113
+ <p>{format(label.hero.greeting, { name: 'John' })}</p>
114
+ {/* Output: "John님 안녕하세요!" */}
115
+ </div>
116
+ );
89
117
  }
90
118
  ```
91
119
 
92
- **Multi-language example:** `src/cms/labels/home/hero.json`
120
+ **Server Component (Multiple Sections):**
93
121
 
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
- }
122
+ ```typescript
123
+ import { getLabels } from '@/labels';
124
+
125
+ export default async function MultiSectionPage() {
126
+ // Multiple sections - with section names as keys
127
+ const labels = await getLabels(['home', 'about']);
128
+
129
+ return (
130
+ <div>
131
+ <h1>{labels.home.hero.title}</h1>
132
+ <p>{labels.about.title}</p>
133
+ </div>
134
+ );
110
135
  }
111
136
  ```
112
137
 
113
- **Variable substitution:** `src/cms/labels/layout/footer.json`
138
+ **Locale Management:**
114
139
 
115
- ```json
116
- {
117
- "copyright": {
118
- "key": "layout.footer.copyright",
119
- "defaultValue": {year} Company. All rights reserved."
120
- }
140
+ ```typescript
141
+ 'use client';
142
+ import { setLocale } from '@spfn/cms/actions';
143
+
144
+ export function LanguageSwitcher() {
145
+ return (
146
+ <div>
147
+ <button onClick={() => setLocale('ko')}>한국어</button>
148
+ <button onClick={() => setLocale('en')}>English</button>
149
+ </div>
150
+ );
121
151
  }
122
152
  ```
123
153
 
124
- ### 2. Enable Auto-Sync on Server Startup
154
+ ## Key Features
155
+
156
+ ### 🎯 Type Safety
125
157
 
126
- Configure `src/server/server.config.ts`:
158
+ **Section Name Validation:**
127
159
 
128
160
  ```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;
161
+ // Valid section names are enforced at compile time
162
+ const label = await getLabel('home'); // OK
163
+ await getLabel('homee'); // ❌ Compile error: 'homee' is not a valid section
164
+
165
+ // getLabel returns direct access (no section wrapper)
166
+ const homeLabel = await getLabel('home');
167
+ homeLabel.hero.title; // OK - direct access
168
+ homeLabel.cta; // OK
169
+
170
+ // ✅ getLabels requires array and returns sections with names
171
+ const labels = await getLabels(['home', 'about']);
172
+ labels.home.hero.title; // OK
173
+ labels.about.title; // OK
174
+ labels.contact.email; // ❌ Compile error: 'contact' was not requested
140
175
  ```
141
176
 
142
- ### 3. Enable Auto-Sync During Development
177
+ **Property Access Validation:**
143
178
 
144
- Your `.spfnrc.json` should include:
179
+ ```typescript
180
+ // Single section - direct access
181
+ const label = await getLabel('signup');
145
182
 
146
- ```json
147
- {
148
- "codegen": {
149
- "generators": [
150
- {
151
- "name": "@spfn/cms:label-sync",
152
- "enabled": true
153
- }
154
- ]
155
- }
156
- }
157
- ```
183
+ // ✅ IDE autocomplete works perfectly
184
+ label.title; // OK - direct access
185
+ label.userName; // OK
186
+ label.email; // OK
158
187
 
159
- This is automatically configured when you run `pnpm spfn add @spfn/cms`.
188
+ // Typos are caught at compile time
189
+ label.titlee; // ❌ Compile error
190
+ label.userName; // ❌ Compile error (typo)
160
191
 
161
- ### 4. Use Labels in Your App
192
+ // Multiple sections - with section names
193
+ const labels = await getLabels(['home', 'about']);
194
+ labels.home.hero.title; // OK
195
+ labels.about.title; // OK
196
+ ```
162
197
 
163
- **Server Component:**
198
+ **API Distinction:**
164
199
 
165
200
  ```typescript
166
- import { getSection } from '@spfn/cms/server';
201
+ // getLabel: Single section, direct access
202
+ const signup = await getLabel('signup');
203
+ signup.title; // ✅ Direct access (cleaner!)
167
204
 
168
- export default async function HomePage() {
169
- const { t } = await getSection('layout', 'ko');
170
-
171
- return <h1>{t('nav.team')}</h1>;
172
- }
205
+ // getLabels: Multiple sections, section names as keys
206
+ const multi = await getLabels(['home', 'signup']);
207
+ multi.home.title; // ✅ With section name
208
+ multi.signup.userName; // ✅ With section name
173
209
  ```
174
210
 
175
- **With variable substitution:**
211
+ ### 🎨 Nested Structure
176
212
 
177
213
  ```typescript
178
- const { t } = await getSection('layout');
179
- const copyright = t('footer.copyright', undefined, {
180
- year: new Date().getFullYear()
214
+ defineLabels({
215
+ features: {
216
+ analytics: {
217
+ title: { en: "Analytics", ko: "분석" },
218
+ description: { en: "Track metrics", ko: "지표 추적" }
219
+ },
220
+ security: {
221
+ title: { en: "Security", ko: "보안" }
222
+ }
223
+ }
181
224
  });
182
- // → "© 2025 Company. All rights reserved."
225
+
226
+ // Single section - direct access with nesting
227
+ const label = await getLabel('features');
228
+ label.analytics.title; // "분석" (auto locale, direct access!)
229
+ label.analytics.description; // "지표 추적"
230
+ label.security.title; // "보안"
183
231
  ```
184
232
 
185
- **Client Component:**
233
+ ### 🔧 Template Variables
186
234
 
187
235
  ```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 });
236
+ defineLabels({
237
+ home: {
238
+ welcome: {
239
+ en: "Welcome {name}, you have {count} messages",
240
+ ko: "{name}님, {count}개의 메시지가 있습니다"
241
+ }
242
+ }
243
+ });
193
244
 
194
- if (loading) return <div>Loading...</div>;
245
+ const label = await getLabel('home');
246
+ const text = label.welcome; // Direct access!
195
247
 
196
- return (
197
- <nav>
198
- <a>{t('nav.about')}</a>
199
- <a>{t('nav.services')}</a>
200
- </nav>
201
- );
202
- }
248
+ format(text, { name: "John", count: 5 });
249
+ // Output: "John님, 5개의 메시지가 있습니다"
203
250
  ```
204
251
 
205
- ## File Structure
252
+ ### 🍪 Smart Locale Detection
206
253
 
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
- ```
254
+ Automatic locale detection with priority:
255
+ 1. User's cookie (`cms-locale`)
256
+ 2. Config's `defaultLocale`
257
+ 3. Final fallback: `'en'`
258
+
259
+ ```typescript
260
+ // User sets locale (saved to cookie)
261
+ await setLocale('ko');
216
262
 
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
263
+ // Automatically uses 'ko' locale
264
+ const label = await getLabel('home');
265
+ label.hero.title; // "환영합니다" (Korean)
221
266
 
222
- Example:
223
- ```
224
- src/cms/labels/layout/nav.json:
225
- key: "layout.nav.team" → t('nav.team') in code
267
+ // Switch to English
268
+ await setLocale('en');
269
+ const label2 = await getLabel('home');
270
+ label2.hero.title; // "Welcome" (English)
226
271
  ```
227
272
 
228
- ## JSON Label Format
273
+ ### 🔄 Auto-Sync
274
+
275
+ Labels synchronize automatically on server startup:
276
+ - ✅ Creates new labels
277
+ - ✅ Updates changed labels (deep equality check)
278
+ - ✅ Skips unchanged labels (performance)
279
+ - ✅ Rebuilds published cache
280
+ - ⚠️ Optionally removes orphaned labels
281
+
282
+ ## API Reference
283
+
284
+ ### createCmsClient()
285
+
286
+ Factory function to create CMS client with API, getLabel, getLabels, and format utilities.
229
287
 
230
288
  ```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
- }
289
+ const { api, getLabel, getLabels, format } = createCmsClient(labelsDefinition, {
290
+ defaultLocale: labelConfig.defaultLocale,
291
+ fallbackLocale: labelConfig.fallbackLocale,
292
+ getLocale: () => getLocale(labelConfig.defaultLocale),
293
+ });
238
294
  ```
239
295
 
240
- **Single language:**
241
- ```json
242
- {
243
- "welcome": {
244
- "key": "home.welcome",
245
- "defaultValue": "Welcome"
246
- }
247
- }
248
- ```
296
+ **Returns:**
297
+ - `api` - API client for CMS routes
298
+ - `getLabel(section)` - Fetch a single section (direct access)
299
+ - `getLabels(sections)` - Fetch multiple sections (with section names)
300
+ - `format(template, vars)` - Template variable substitution
249
301
 
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
- ```
302
+ ### getLabel()
263
303
 
264
- **Variable placeholders:**
265
- ```json
266
- {
267
- "greeting": {
268
- "key": "home.greeting",
269
- "defaultValue": "Hello, {name}!"
270
- }
271
- }
272
- ```
304
+ Fetch a **single section** from published cache with direct access (no section name wrapper).
273
305
 
274
- Usage:
275
306
  ```typescript
276
- t('greeting', undefined, { name: 'John' })
277
- // "Hello, John!"
307
+ // Single section - direct access
308
+ const label = await getLabel('home');
309
+ label.hero.title; // Direct access without 'home.' prefix
310
+ label.cta;
278
311
  ```
279
312
 
280
- ## Configuration
313
+ **Use when:**
314
+ - You need labels from only ONE section
315
+ - You want cleaner, direct access to properties
316
+ - Most common use case for individual pages
281
317
 
282
- ### Environment Variables
318
+ **Returns:** Labels directly without section name wrapper
283
319
 
284
- Configure CMS behavior via environment variables in `.env.local`:
320
+ **Features:**
321
+ - Auto locale detection (cookie → defaultLocale → 'en')
322
+ - Direct property access (no section name)
323
+ - Type-safe with IDE autocomplete
324
+ - Merges published cache with defaults
285
325
 
286
- ```bash
287
- # Default locale (default: 'en')
288
- SPFN_CMS_DEFAULT_LOCALE=ko
326
+ ### getLabels()
289
327
 
290
- # Supported locales, comma-separated (default: 'en,ko')
291
- SPFN_CMS_SUPPORTED_LOCALES=en,ko,ja
328
+ Fetch **multiple sections** from published cache with section names as keys.
292
329
 
293
- # Auto-detect browser language (default: true)
294
- SPFN_CMS_DETECT_BROWSER_LANGUAGE=true
330
+ ```typescript
331
+ // Multiple sections - with section names
332
+ const labels = await getLabels(['home', 'about']);
333
+ labels.home.hero.title; // Access via section name
334
+ labels.about.title;
295
335
  ```
296
336
 
297
- ### Runtime Configuration
337
+ **Use when:**
338
+ - You need labels from MULTIPLE sections
339
+ - You're building a page that uses multiple label groups
340
+
341
+ **Returns:** Object with section names as keys
342
+
343
+ **Features:**
344
+ - Auto locale detection (cookie → defaultLocale → 'en')
345
+ - Section filtering (only processes requested sections)
346
+ - Type-safe: only requested sections available
347
+ - Merges published cache with defaults
298
348
 
299
- Override configuration at runtime (mainly for testing):
349
+ **Performance:**
350
+ - Only requested sections are processed (not entire labelsDefinition)
351
+ - 10x faster when requesting specific sections
352
+ - Reduces CPU and memory usage proportionally
353
+
354
+ ### format()
355
+
356
+ Replace template variables in strings.
300
357
 
301
358
  ```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
- });
359
+ format("Hello {name}!", { name: "John" }); // "Hello John!"
360
+ format("{count} items", { count: 5 }); // "5 items"
315
361
  ```
316
362
 
317
- ## Locale Management
363
+ **Syntax:** `{variableName}` - Supports strings and numbers
364
+
365
+ ### setLocale() / getLocale()
318
366
 
319
- ### Automatic Locale Detection
367
+ Server actions for locale management (cookie-based).
320
368
 
321
- The CMS automatically manages user locale with the following priority:
369
+ ```typescript
370
+ // Set user's preferred locale
371
+ await setLocale('ko'); // Saves to 'cms-locale' cookie
322
372
 
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
373
+ // Get current locale
374
+ const locale = await getLocale(defaultLocale); // Returns: cookie defaultLocale → 'en'
375
+ ```
326
376
 
327
- ### Server Actions (`@spfn/cms/actions`)
377
+ **Cookie settings:**
378
+ - Name: `cms-locale`
379
+ - Max age: 1 year
380
+ - HttpOnly, Secure (production), SameSite: lax
328
381
 
329
- Use Server Actions for locale management in both server and client components:
382
+ ### syncLabels()
330
383
 
331
- **Get current locale:**
384
+ Synchronize labels to database (server-side only).
332
385
 
333
386
  ```typescript
334
- // Server Component
335
- import { getLocale } from '@spfn/cms/actions';
387
+ await syncLabels(labelsDefinition, {
388
+ removeOrphaned: false, // Delete labels not in code
389
+ dryRun: false // Preview changes without applying
390
+ });
391
+ ```
336
392
 
337
- export default async function RootLayout({ children }) {
338
- const locale = await getLocale();
393
+ **Returns:** `{ added, updated, removed, unchanged }`
339
394
 
340
- return <html lang={locale}>{children}</html>;
341
- }
342
- ```
395
+ ## Admin API
343
396
 
344
- ```typescript
345
- // Client Component
346
- 'use client';
347
- import { getLocale } from '@spfn/cms/actions';
348
- import { useEffect, useState } from 'react';
397
+ CMS 라벨을 관리하기 위한 Admin API를 제공합니다. 섹션별 테이블 뷰로 라벨을 조회/수정/발행할 수 있습니다.
349
398
 
350
- export default function LanguageSwitcher() {
351
- const [locale, setLocale] = useState('');
399
+ ### Admin Routes
352
400
 
353
- useEffect(() => {
354
- getLocale().then(setLocale);
355
- }, []);
401
+ `cmsAppRouter`에 포함된 Admin 라우트들:
356
402
 
357
- return <div>Current: {locale}</div>;
358
- }
359
- ```
403
+ | Route | Method | Description |
404
+ |-------|--------|-------------|
405
+ | `getSectionLabels` | GET | 섹션의 모든 라벨 조회 (Draft/Published 상태 포함) |
406
+ | `saveSectionDraft` | POST | 섹션 라벨 일괄 Draft 저장 |
407
+ | `publishSection` | POST | 섹션 전체 발행 (Draft → Published) |
408
+ | `resetSectionDraft` | DELETE | 섹션 Draft 초기화 |
409
+
410
+ ### Admin UI 구현 예제
360
411
 
361
- **Change locale:**
412
+ **1. API Client 설정:**
362
413
 
363
414
  ```typescript
364
- import { setLocale } from '@spfn/cms/actions';
415
+ // labels.ts
416
+ import { createCmsClient } from '@spfn/cms';
365
417
 
366
- async function changeLanguage(newLocale: string) {
367
- await setLocale(newLocale);
368
- window.location.reload(); // Reload to apply changes
369
- }
418
+ export const { api: cmsApi } = createCmsClient(labelsDefinition, {
419
+ defaultLocale: labelConfig.defaultLocale,
420
+ fallbackLocale: labelConfig.fallbackLocale,
421
+ getLocale: () => getLocale(labelConfig.defaultLocale),
422
+ });
370
423
  ```
371
424
 
372
- **Get supported locales:**
425
+ **2. 섹션 라벨 조회:**
373
426
 
374
427
  ```typescript
375
- import { getLocales } from '@spfn/cms/actions';
428
+ // 섹션의 모든 라벨을 Draft/Published 상태와 함께 조회
429
+ const data = await cmsApi.getSectionLabels.call({
430
+ params: { section: 'home' },
431
+ query: { locales: 'en,ko' }, // 콤마로 구분
432
+ });
376
433
 
377
- const locales = await getLocales(); // ['ko', 'en', 'ja']
434
+ // 반환값
435
+ {
436
+ section: 'home',
437
+ locales: ['en', 'ko'],
438
+ labels: [
439
+ {
440
+ id: 1,
441
+ key: 'home.hero.title',
442
+ defaultValue: { en: 'Welcome', ko: '환영합니다' },
443
+ draft: { en: 'Welcome!', ko: '환영합니다!' } | null,
444
+ published: { en: 'Welcome', ko: '환영합니다' } | null,
445
+ hasDraft: true
446
+ },
447
+ // ...
448
+ ]
449
+ }
378
450
  ```
379
451
 
380
- ### Auto-detect Locale in Server Components
381
-
382
- When `locale` is not specified, `getSection()` automatically uses the detected locale:
452
+ **3. Draft 저장:**
383
453
 
384
454
  ```typescript
385
- import { getSection } from '@spfn/cms/server';
455
+ // 수정된 라벨들을 Draft로 저장
456
+ await cmsApi.saveSectionDraft.call({
457
+ params: { section: 'home' },
458
+ body: {
459
+ labels: [
460
+ { id: 1, values: { en: 'Welcome!', ko: '환영합니다!' } },
461
+ { id: 2, values: { ko: '새로운 부제목' } }, // 특정 locale만 수정 가능
462
+ ]
463
+ },
464
+ });
465
+ ```
386
466
 
387
- // Auto-detects locale from cookie → browser → default
388
- const { t } = await getSection('home');
467
+ **4. 섹션 발행:**
468
+
469
+ ```typescript
470
+ // Draft가 있는 모든 라벨을 Published로 발행
471
+ const result = await cmsApi.publishSection.call({
472
+ params: { section: 'home' },
473
+ body: { locales: ['en', 'ko'] },
474
+ });
389
475
 
390
- // Or explicitly specify locale
391
- const { t: tEn } = await getSection('home', 'en');
476
+ // 반환값
477
+ {
478
+ published: 2, // 발행된 라벨 수
479
+ version: 3, // 최대 버전 번호
480
+ labels: ['home.hero.title', 'home.hero.subtitle'] // 발행된 라벨 키
481
+ }
392
482
  ```
393
483
 
394
- ## Documentation
484
+ **5. Draft 초기화:**
395
485
 
396
- - **[Label Auto-Sync Guide](./LABEL_SYNC_GUIDE.md)** - Detailed configuration guide
397
- - **[Examples](./examples/)** - Usage examples
486
+ ```typescript
487
+ // 섹션의 모든 Draft 삭제 (Published 값으로 복원)
488
+ await cmsApi.resetSectionDraft.call({
489
+ params: { section: 'home' },
490
+ });
491
+ ```
398
492
 
399
- ## Architecture
493
+ ### Workflow
400
494
 
401
495
  ```
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
- └─────────────────────┘
496
+ ┌─────────────┐ saveSectionDraft ┌─────────────┐
497
+ │ Default │ ──────────────────────► │ Draft │
498
+ (코드 정의) │ │ (version:null)│
499
+ └─────────────┘ └──────┬──────┘
500
+
501
+ publishSection
502
+ ◄───────────────────────────────┘
503
+
504
+
505
+ ┌─────────────┐
506
+ │ Published │
507
+ │ (version:N) │
508
+ └─────────────┘
424
509
  ```
425
510
 
426
- ## API Reference
427
-
428
- ### Server-side API
511
+ **상태 우선순위:** Draft > Published > Default
429
512
 
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
513
+ ### 상태 표시
433
514
 
434
- ### Server Actions API (`@spfn/cms/actions`)
515
+ | 상태 | 의미 | UI 표시 예 |
516
+ |------|------|-----------|
517
+ | Default | 코드에 정의된 기본값만 존재 | 회색 |
518
+ | Draft | 저장되었으나 미발행 | 노란색 |
519
+ | Published | 발행되어 실제 서비스에 반영 | 초록색 |
520
+ | Edited | UI에서 수정 중 (미저장) | 파란색 |
435
521
 
436
- Available for both server and client components:
522
+ ## Architecture
437
523
 
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
524
+ ### Database Schema
442
525
 
443
- ### Configuration API
526
+ ```
527
+ cms_labels (metadata)
528
+ ├─ id, key, section, type, defaultValue
529
+ └─ publishedVersion
444
530
 
445
- - `getCmsConfig()` - Get current CMS configuration
446
- - `configureCms(config)` - Update configuration (runtime)
447
- - `resetCmsConfig()` - Reset configuration to defaults
531
+ cms_label_values (actual content)
532
+ ├─ labelId, version, locale, breakpoint
533
+ └─ value (JSONB)
448
534
 
449
- ### Client-side API (`@spfn/cms/client`)
535
+ cms_published_cache (performance)
536
+ ├─ section, locale, content (JSONB)
537
+ └─ version (for cache invalidation)
538
+ ```
450
539
 
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
540
+ ### Query Flow
456
541
 
457
- ### Sync API
542
+ 1. **getLabels()** → published_cache (single query, 5ms)
543
+ 2. **Fallback** → bindLocale(defaults)
544
+ 3. **Merge** → cache overrides defaults
545
+ 4. **Return** → type-safe nested object
458
546
 
459
- - `loadLabelsFromJson(labelsDir)` - Load labels from JSON files
460
- - `syncAll(sections, options?)` - Sync all sections
461
- - `syncSection(definition, options?)` - Sync specific section
547
+ ## Performance
462
548
 
463
- ### Codegen Integration
549
+ - **Published cache:** 5ms (vs 87ms with JOINs) - 17x faster
550
+ - **N+1 prevention:** Bulk section queries with `inArray()`
551
+ - **Section filtering:** Only requested sections processed (10x faster for selective access)
552
+ - **Unchanged labels:** Skipped during sync (deep equality check)
553
+ - **Client caching:** Version-based invalidation
464
554
 
465
- - `createLabelSyncGenerator(config?)` - Generator factory
466
- - `LabelSyncGenerator` - Generator class
555
+ ### Example: Large Scale
467
556
 
468
- ## Development Workflow
557
+ ```typescript
558
+ // 10 sections with 100 labels each = 1,000 total labels
559
+ const labelsDefinition = {
560
+ home: { /* 100 labels */ },
561
+ about: { /* 100 labels */ },
562
+ products: { /* 100 labels */ },
563
+ // ... 7 more sections
564
+ };
469
565
 
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()`
566
+ // Only request 'home' - direct access
567
+ const label = await getLabel('home');
473
568
 
474
- **Example:**
569
+ // ✅ Performance: Processes only 100 labels (10% of total)
570
+ // ❌ Without filtering: Would process all 1,000 labels
571
+ ```
475
572
 
476
- ```bash
477
- # Terminal 1: Start dev server
478
- pnpm dev
573
+ **Benefits:**
574
+ - CPU usage: 10x reduction (100 vs 1,000 labels)
575
+ - Memory usage: 10x reduction
576
+ - Response time: Proportionally faster
479
577
 
480
- # Terminal 2: Edit label file
481
- echo '{"test": {"key": "layout.test", "defaultValue": "Test"}}' > src/cms/labels/layout/test.json
578
+ ## Development Status
482
579
 
483
- # Auto-sync triggers
484
- # ✅ Label sync completed
485
- # Created: 1
486
- ```
580
+ This package is currently in alpha. APIs may change.
487
581
 
488
582
  ## License
489
583
 
490
- MIT
584
+ MIT