@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
@@ -0,0 +1,199 @@
1
+ /**
2
+ * CMS Published Cache Entity
3
+ *
4
+ * 발행된 콘텐츠를 섹션+언어 단위로 캐싱합니다.
5
+ * - 초고속 읽기 성능 (5ms)
6
+ * - 단일 쿼리로 섹션 전체 로드
7
+ * - JSONB로 즉시 사용 가능한 데이터
8
+ *
9
+ * 성능 비교:
10
+ * - 정규화 테이블 JOIN: 87ms
11
+ * - 캐시 테이블: 5ms (17배 빠름!)
12
+ */
13
+ export declare const cmsPublishedCache: import("drizzle-orm/pg-core").PgTableWithColumns<{
14
+ name: "published_cache";
15
+ schema: string;
16
+ columns: {
17
+ id: import("drizzle-orm/pg-core").PgColumn<{
18
+ name: "id";
19
+ tableName: "published_cache";
20
+ dataType: "number";
21
+ columnType: "PgSerial";
22
+ data: number;
23
+ driverParam: number;
24
+ notNull: true;
25
+ hasDefault: true;
26
+ isPrimaryKey: true;
27
+ isAutoincrement: false;
28
+ hasRuntimeDefault: false;
29
+ enumValues: undefined;
30
+ baseColumn: never;
31
+ identity: undefined;
32
+ generated: undefined;
33
+ }, {}, {}>;
34
+ section: import("drizzle-orm/pg-core").PgColumn<{
35
+ name: "section";
36
+ tableName: "published_cache";
37
+ dataType: "string";
38
+ columnType: "PgText";
39
+ data: string;
40
+ driverParam: string;
41
+ notNull: true;
42
+ hasDefault: false;
43
+ isPrimaryKey: false;
44
+ isAutoincrement: false;
45
+ hasRuntimeDefault: false;
46
+ enumValues: [string, ...string[]];
47
+ baseColumn: never;
48
+ identity: undefined;
49
+ generated: undefined;
50
+ }, {}, {}>;
51
+ locale: import("drizzle-orm/pg-core").PgColumn<{
52
+ name: "locale";
53
+ tableName: "published_cache";
54
+ dataType: "string";
55
+ columnType: "PgText";
56
+ data: string;
57
+ driverParam: string;
58
+ notNull: true;
59
+ hasDefault: false;
60
+ isPrimaryKey: false;
61
+ isAutoincrement: false;
62
+ hasRuntimeDefault: false;
63
+ enumValues: [string, ...string[]];
64
+ baseColumn: never;
65
+ identity: undefined;
66
+ generated: undefined;
67
+ }, {}, {}>;
68
+ content: import("drizzle-orm/pg-core").PgColumn<{
69
+ name: "content";
70
+ tableName: "published_cache";
71
+ dataType: "json";
72
+ columnType: "PgJsonb";
73
+ data: unknown;
74
+ driverParam: unknown;
75
+ notNull: true;
76
+ hasDefault: false;
77
+ isPrimaryKey: false;
78
+ isAutoincrement: false;
79
+ hasRuntimeDefault: false;
80
+ enumValues: undefined;
81
+ baseColumn: never;
82
+ identity: undefined;
83
+ generated: undefined;
84
+ }, {}, {}>;
85
+ publishedAt: import("drizzle-orm/pg-core").PgColumn<{
86
+ name: "published_at";
87
+ tableName: "published_cache";
88
+ dataType: "date";
89
+ columnType: "PgTimestamp";
90
+ data: Date;
91
+ driverParam: string;
92
+ notNull: true;
93
+ hasDefault: false;
94
+ isPrimaryKey: false;
95
+ isAutoincrement: false;
96
+ hasRuntimeDefault: false;
97
+ enumValues: undefined;
98
+ baseColumn: never;
99
+ identity: undefined;
100
+ generated: undefined;
101
+ }, {}, {}>;
102
+ publishedBy: import("drizzle-orm/pg-core").PgColumn<{
103
+ name: "published_by";
104
+ tableName: "published_cache";
105
+ dataType: "string";
106
+ columnType: "PgText";
107
+ data: string;
108
+ driverParam: string;
109
+ notNull: false;
110
+ hasDefault: false;
111
+ isPrimaryKey: false;
112
+ isAutoincrement: false;
113
+ hasRuntimeDefault: false;
114
+ enumValues: [string, ...string[]];
115
+ baseColumn: never;
116
+ identity: undefined;
117
+ generated: undefined;
118
+ }, {}, {}>;
119
+ version: import("drizzle-orm/pg-core").PgColumn<{
120
+ name: "version";
121
+ tableName: "published_cache";
122
+ dataType: "number";
123
+ columnType: "PgInteger";
124
+ data: number;
125
+ driverParam: string | number;
126
+ notNull: true;
127
+ hasDefault: true;
128
+ isPrimaryKey: false;
129
+ isAutoincrement: false;
130
+ hasRuntimeDefault: false;
131
+ enumValues: undefined;
132
+ baseColumn: never;
133
+ identity: undefined;
134
+ generated: undefined;
135
+ }, {}, {}>;
136
+ };
137
+ dialect: "pg";
138
+ }>;
139
+ export type CmsPublishedCache = typeof cmsPublishedCache.$inferSelect;
140
+ export type NewCmsPublishedCache = typeof cmsPublishedCache.$inferInsert;
141
+ /**
142
+ * 사용 예시:
143
+ *
144
+ * // 캐시 생성/업데이트 (UPSERT)
145
+ * await db.insert(cmsPublishedCache)
146
+ * .values({
147
+ * section: 'home',
148
+ * locale: 'ko',
149
+ * content: {
150
+ * 'home.hero.title': {
151
+ * type: 'text',
152
+ * content: '미래를 만드는 기업'
153
+ * },
154
+ * 'home.hero.image': {
155
+ * type: 'image',
156
+ * url: '/uploads/hero.jpg',
157
+ * alt: 'Hero',
158
+ * width: 1920,
159
+ * height: 1080
160
+ * }
161
+ * },
162
+ * publishedAt: new Date(),
163
+ * publishedBy: 'admin@futureplay.com'
164
+ * })
165
+ * .onConflictDoUpdate({
166
+ * target: [cmsPublishedCache.section, cmsPublishedCache.locale],
167
+ * set: {
168
+ * content: sql`EXCLUDED.content`,
169
+ * publishedAt: sql`EXCLUDED.published_at`,
170
+ * publishedBy: sql`EXCLUDED.published_by`,
171
+ * version: sql`${cmsPublishedCache.version} + 1`
172
+ * }
173
+ * });
174
+ *
175
+ * // 캐시 조회 (초고속!)
176
+ * const cache = await db.select()
177
+ * .from(cmsPublishedCache)
178
+ * .where(and(
179
+ * eq(cmsPublishedCache.section, 'home'),
180
+ * eq(cmsPublishedCache.locale, 'ko')
181
+ * ))
182
+ * .limit(1);
183
+ *
184
+ * const labels = cache[0].content; // 즉시 사용 가능!
185
+ *
186
+ * // 섹션의 모든 언어 캐시 조회
187
+ * const allLocales = await db.select()
188
+ * .from(cmsPublishedCache)
189
+ * .where(eq(cmsPublishedCache.section, 'home'));
190
+ *
191
+ * // 오래된 캐시 감지
192
+ * const stale = await db.select()
193
+ * .from(cmsPublishedCache)
194
+ * .where(lt(
195
+ * cmsPublishedCache.publishedAt,
196
+ * new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
197
+ * ));
198
+ */
199
+ //# sourceMappingURL=cms-published-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cms-published-cache.d.ts","sourceRoot":"","sources":["../../src/entities/cms-published-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiC5B,CAAC;AAGH,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAC,YAAY,CAAC;AACtE,MAAM,MAAM,oBAAoB,GAAG,OAAO,iBAAiB,CAAC,YAAY,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * CMS Published Cache Entity
3
+ *
4
+ * 발행된 콘텐츠를 섹션+언어 단위로 캐싱합니다.
5
+ * - 초고속 읽기 성능 (5ms)
6
+ * - 단일 쿼리로 섹션 전체 로드
7
+ * - JSONB로 즉시 사용 가능한 데이터
8
+ *
9
+ * 성능 비교:
10
+ * - 정규화 테이블 JOIN: 87ms
11
+ * - 캐시 테이블: 5ms (17배 빠름!)
12
+ */
13
+ import { serial, text, jsonb, integer, timestamp, index, unique } from 'drizzle-orm/pg-core';
14
+ import { createFunctionSchema } from '@spfn/core/db';
15
+ // Create isolated schema for @spfn/cms
16
+ const schema = createFunctionSchema('@spfn/cms');
17
+ export const cmsPublishedCache = schema.table('published_cache', {
18
+ // Primary Key
19
+ id: serial('id').primaryKey(),
20
+ // 섹션 (페이지 단위)
21
+ section: text('section').notNull(),
22
+ // "home" | "why-futureplay" | "team" | "our-companies" | "apply"
23
+ // 언어
24
+ locale: text('locale').notNull(),
25
+ // "ko" | "en" | "ja"
26
+ // 캐시된 콘텐츠 (JSONB)
27
+ content: jsonb('content').notNull(),
28
+ // Record<string, LabelValue>
29
+ // {
30
+ // "home.hero.title": { type: "text", content: "..." },
31
+ // "home.hero.image": { type: "image", url: "...", alt: "..." },
32
+ // ...
33
+ // }
34
+ // 발행 정보
35
+ publishedAt: timestamp('published_at', { withTimezone: true }).notNull(),
36
+ publishedBy: text('published_by'),
37
+ // 캐시 버전 (클라이언트 캐싱용)
38
+ version: integer('version').notNull().default(1),
39
+ }, (table) => [
40
+ // UNIQUE 제약: section + locale 조합은 유일
41
+ unique('cms_published_cache_unique').on(table.section, table.locale),
42
+ // 인덱스: section으로 조회 최적화
43
+ index('cms_published_cache_section_idx').on(table.section),
44
+ ]);
45
+ /**
46
+ * 사용 예시:
47
+ *
48
+ * // 캐시 생성/업데이트 (UPSERT)
49
+ * await db.insert(cmsPublishedCache)
50
+ * .values({
51
+ * section: 'home',
52
+ * locale: 'ko',
53
+ * content: {
54
+ * 'home.hero.title': {
55
+ * type: 'text',
56
+ * content: '미래를 만드는 기업'
57
+ * },
58
+ * 'home.hero.image': {
59
+ * type: 'image',
60
+ * url: '/uploads/hero.jpg',
61
+ * alt: 'Hero',
62
+ * width: 1920,
63
+ * height: 1080
64
+ * }
65
+ * },
66
+ * publishedAt: new Date(),
67
+ * publishedBy: 'admin@futureplay.com'
68
+ * })
69
+ * .onConflictDoUpdate({
70
+ * target: [cmsPublishedCache.section, cmsPublishedCache.locale],
71
+ * set: {
72
+ * content: sql`EXCLUDED.content`,
73
+ * publishedAt: sql`EXCLUDED.published_at`,
74
+ * publishedBy: sql`EXCLUDED.published_by`,
75
+ * version: sql`${cmsPublishedCache.version} + 1`
76
+ * }
77
+ * });
78
+ *
79
+ * // 캐시 조회 (초고속!)
80
+ * const cache = await db.select()
81
+ * .from(cmsPublishedCache)
82
+ * .where(and(
83
+ * eq(cmsPublishedCache.section, 'home'),
84
+ * eq(cmsPublishedCache.locale, 'ko')
85
+ * ))
86
+ * .limit(1);
87
+ *
88
+ * const labels = cache[0].content; // 즉시 사용 가능!
89
+ *
90
+ * // 섹션의 모든 언어 캐시 조회
91
+ * const allLocales = await db.select()
92
+ * .from(cmsPublishedCache)
93
+ * .where(eq(cmsPublishedCache.section, 'home'));
94
+ *
95
+ * // 오래된 캐시 감지
96
+ * const stale = await db.select()
97
+ * .from(cmsPublishedCache)
98
+ * .where(lt(
99
+ * cmsPublishedCache.publishedAt,
100
+ * new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
101
+ * ));
102
+ */
103
+ //# sourceMappingURL=cms-published-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cms-published-cache.js","sourceRoot":"","sources":["../../src/entities/cms-published-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,uCAAuC;AACvC,MAAM,MAAM,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC;AAEjD,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,KAAK,CAAC,iBAAiB,EAAE;IAC7D,cAAc;IACd,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;IAE7B,cAAc;IACd,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IAClC,iEAAiE;IAEjE,KAAK;IACL,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE;IAChC,qBAAqB;IAErB,kBAAkB;IAClB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IACnC,6BAA6B;IAC7B,IAAI;IACJ,yDAAyD;IACzD,kEAAkE;IAClE,QAAQ;IACR,IAAI;IAEJ,QAAQ;IACR,WAAW,EAAE,SAAS,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACxE,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC;IAEjC,oBAAoB;IACpB,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;CACnD,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;IACV,qCAAqC;IACrC,MAAM,CAAC,4BAA4B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC;IAEpE,wBAAwB;IACxB,KAAK,CAAC,iCAAiC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;CAC7D,CAAC,CAAC;AAMH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CMS Database Entities
3
+ */
4
+ export * from './cms-labels';
5
+ export * from './cms-label-values';
6
+ export * from './cms-label-versions';
7
+ export * from './cms-draft-cache';
8
+ export * from './cms-published-cache';
9
+ export * from './cms-audit-logs';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/entities/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CMS Database Entities
3
+ */
4
+ export * from './cms-labels';
5
+ export * from './cms-label-values';
6
+ export * from './cms-label-versions';
7
+ export * from './cms-draft-cache';
8
+ export * from './cms-published-cache';
9
+ export * from './cms-audit-logs';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/entities/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SPFN CMS Generators Registry
3
+ *
4
+ * Exports generators for use with package-based naming convention
5
+ * e.g., @spfn/cms:label-sync
6
+ */
7
+ import { createLabelSyncGenerator } from './label-sync-generator.js';
8
+ /**
9
+ * Generators registry
10
+ * Maps generator names to their factory functions
11
+ */
12
+ export declare const generators: {
13
+ 'label-sync': typeof createLabelSyncGenerator;
14
+ };
15
+ /**
16
+ * Re-export individual generator factories
17
+ */
18
+ export { createLabelSyncGenerator, LabelSyncGenerator } from './label-sync-generator.js';
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE;;;GAGG;AACH,eAAO,MAAM,UAAU;;CAEtB,CAAC;AAEF;;GAEG;AACH,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SPFN CMS Generators Registry
3
+ *
4
+ * Exports generators for use with package-based naming convention
5
+ * e.g., @spfn/cms:label-sync
6
+ */
7
+ import { createLabelSyncGenerator } from './label-sync-generator.js';
8
+ /**
9
+ * Generators registry
10
+ * Maps generator names to their factory functions
11
+ */
12
+ export const generators = {
13
+ 'label-sync': createLabelSyncGenerator,
14
+ };
15
+ /**
16
+ * Re-export individual generator factories
17
+ */
18
+ export { createLabelSyncGenerator, LabelSyncGenerator } from './label-sync-generator.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACtB,YAAY,EAAE,wBAAwB;CACzC,CAAC;AAEF;;GAEG;AACH,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Label Sync Generator
3
+ *
4
+ * File-based label sync with JSON definitions
5
+ *
6
+ * Structure:
7
+ * cms/labels/
8
+ * layout/ # Section name
9
+ * nav.json # Label definitions
10
+ * footer.json
11
+ * homepage/
12
+ * hero.json
13
+ */
14
+ import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
15
+ export interface LabelSyncGeneratorConfig {
16
+ labelsDir?: string;
17
+ }
18
+ export declare class LabelSyncGenerator implements Generator {
19
+ name: string;
20
+ private labelsDir;
21
+ constructor(config?: LabelSyncGeneratorConfig);
22
+ /**
23
+ * Watch patterns for label definition files
24
+ */
25
+ get watchPatterns(): string[];
26
+ generate(options: GeneratorOptions): Promise<void>;
27
+ onFileChange(filePath: string, event: 'add' | 'change' | 'unlink'): Promise<void>;
28
+ }
29
+ /**
30
+ * Create label sync generator instance
31
+ */
32
+ export declare function createLabelSyncGenerator(config?: LabelSyncGeneratorConfig): Generator;
33
+ //# sourceMappingURL=label-sync-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-sync-generator.d.ts","sourceRoot":"","sources":["../../src/generators/label-sync-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAOtE,MAAM,WAAW,wBAAwB;IAErC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,kBAAmB,YAAW,SAAS;IAEhD,IAAI,SAAgB;IACpB,OAAO,CAAC,SAAS,CAAS;gBAEd,MAAM,GAAE,wBAA6B;IAKjD;;OAEG;IACH,IAAI,aAAa,IAAI,MAAM,EAAE,CAK5B;IAEK,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiElD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAO1F;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,CAAC,EAAE,wBAAwB,GAAG,SAAS,CAGrF"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Label Sync Generator
3
+ *
4
+ * File-based label sync with JSON definitions
5
+ *
6
+ * Structure:
7
+ * cms/labels/
8
+ * layout/ # Section name
9
+ * nav.json # Label definitions
10
+ * footer.json
11
+ * homepage/
12
+ * hero.json
13
+ */
14
+ import { logger } from '@spfn/core';
15
+ import { join } from 'path';
16
+ import { syncAll, loadLabelsFromJson } from '../helpers/sync';
17
+ const syncLogger = logger.child('label-sync');
18
+ export class LabelSyncGenerator {
19
+ constructor(config = {}) {
20
+ this.name = 'label-sync';
21
+ this.labelsDir = config.labelsDir ?? 'src/cms/labels';
22
+ }
23
+ /**
24
+ * Watch patterns for label definition files
25
+ */
26
+ get watchPatterns() {
27
+ return [
28
+ `${this.labelsDir}/**/*.json`,
29
+ ];
30
+ }
31
+ async generate(options) {
32
+ const isDevelopment = process.env.NODE_ENV !== 'production';
33
+ if (options.debug) {
34
+ syncLogger.info('Starting label sync...');
35
+ }
36
+ try {
37
+ const labelsPath = join(options.cwd, this.labelsDir);
38
+ // Load labels from JSON files
39
+ const sections = loadLabelsFromJson(labelsPath);
40
+ if (sections.length === 0) {
41
+ syncLogger.warn(`No labels found in ${labelsPath}`);
42
+ return;
43
+ }
44
+ syncLogger.info(`Found ${sections.length} sections`);
45
+ // Sync all sections
46
+ const results = await syncAll(sections, {
47
+ verbose: options.debug ?? false,
48
+ updateExisting: isDevelopment,
49
+ });
50
+ const totalCreated = results.reduce((sum, r) => sum + r.created, 0);
51
+ const totalUpdated = results.reduce((sum, r) => sum + r.updated, 0);
52
+ const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
53
+ if (options.debug || totalCreated > 0 || totalUpdated > 0) {
54
+ syncLogger.info('Label sync completed', {
55
+ sections: results.length,
56
+ created: totalCreated,
57
+ updated: totalUpdated,
58
+ errors: totalErrors,
59
+ });
60
+ }
61
+ // Log errors if any
62
+ if (totalErrors > 0) {
63
+ results.forEach((result) => {
64
+ result.errors.forEach((error) => {
65
+ syncLogger.error(`[${result.section}] ${error.key}: ${error.error}`);
66
+ });
67
+ });
68
+ }
69
+ }
70
+ catch (error) {
71
+ syncLogger.error('Label sync failed', error instanceof Error ? error : new Error(String(error)));
72
+ }
73
+ }
74
+ async onFileChange(filePath, event) {
75
+ syncLogger.info(`Label file ${event}`, { file: filePath });
76
+ // Re-sync all labels when any label file changes
77
+ await this.generate({ cwd: process.cwd(), debug: true });
78
+ }
79
+ }
80
+ /**
81
+ * Create label sync generator instance
82
+ */
83
+ export function createLabelSyncGenerator(config) {
84
+ return new LabelSyncGenerator(config);
85
+ }
86
+ //# sourceMappingURL=label-sync-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-sync-generator.js","sourceRoot":"","sources":["../../src/generators/label-sync-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * 현재 locale 가져오기 (Server Action)
3
+ *
4
+ * 서버/클라이언트 컴포넌트 모두에서 사용 가능
5
+ *
6
+ * 우선순위:
7
+ * 1. 쿠키 (사용자가 명시적으로 선택한 언어)
8
+ * 2. 브라우저 언어 감지 (설정에서 활성화된 경우)
9
+ * 3. 시스템 기본 언어 (CMS 설정)
10
+ *
11
+ * @returns 현재 locale (예: 'ko', 'en')
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * // Server Component
16
+ * import { getLocale } from '@spfn/cms';
17
+ *
18
+ * export default async function Page()
19
+ * {
20
+ * const locale = await getLocale();
21
+ * return <div>Current locale: {locale}</div>;
22
+ * }
23
+ * ```
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * // Client Component
28
+ * 'use client';
29
+ * import { getLocale } from '@spfn/cms/client';
30
+ *
31
+ * export default function LanguageSwitcher()
32
+ * {
33
+ * const [locale, setLocale] = useState('');
34
+ *
35
+ * useEffect(() => {
36
+ * getLocale().then(setLocale);
37
+ * }, []);
38
+ *
39
+ * return <div>Current locale: {locale}</div>;
40
+ * }
41
+ * ```
42
+ */
43
+ export declare function getLocale(): Promise<string>;
44
+ /**
45
+ * Locale 설정하기 (Server Action)
46
+ *
47
+ * 서버/클라이언트 컴포넌트 모두에서 사용 가능
48
+ * 쿠키에 locale을 저장합니다.
49
+ *
50
+ * @param locale - 설정할 locale (예: 'ko', 'en')
51
+ * @throws {Error} 지원하지 않는 locale인 경우
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * // Server Component (Server Action)
56
+ * import { setLocale } from '@spfn/cms';
57
+ *
58
+ * export default async function Page()
59
+ * {
60
+ * await setLocale('en');
61
+ * return <div>Locale changed</div>;
62
+ * }
63
+ * ```
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * // Client Component (Server Action)
68
+ * 'use client';
69
+ * import { setLocale } from '@spfn/cms/client';
70
+ *
71
+ * export default function LanguageSwitcher()
72
+ * {
73
+ * const handleChange = async (newLocale: string) =>
74
+ * {
75
+ * await setLocale(newLocale);
76
+ * window.location.reload(); // 페이지 새로고침
77
+ * };
78
+ *
79
+ * return (
80
+ * <button onClick={() => handleChange('en')}>
81
+ * Switch to English
82
+ * </button>
83
+ * );
84
+ * }
85
+ * ```
86
+ */
87
+ export declare function setLocale(locale: string): Promise<void>;
88
+ /**
89
+ * 지원하는 locale 목록 가져오기 (Server Action)
90
+ *
91
+ * 서버/클라이언트 컴포넌트 모두에서 사용 가능
92
+ *
93
+ * @returns 지원하는 locale 배열 (예: ['ko', 'en', 'ja'])
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * // Server Component
98
+ * import { getLocales } from '@spfn/cms';
99
+ *
100
+ * export default async function Page()
101
+ * {
102
+ * const locales = await getLocales();
103
+ * return <div>Supported: {locales.join(', ')}</div>;
104
+ * }
105
+ * ```
106
+ *
107
+ * @example
108
+ * ```tsx
109
+ * // Client Component
110
+ * 'use client';
111
+ * import { getLocales } from '@spfn/cms/client';
112
+ *
113
+ * export default function LanguageSwitcher()
114
+ * {
115
+ * const [locales, setLocales] = useState<string[]>([]);
116
+ *
117
+ * useEffect(() => {
118
+ * getLocales().then(setLocales);
119
+ * }, []);
120
+ *
121
+ * return (
122
+ * <div>
123
+ * {locales.map(locale => (
124
+ * <button key={locale}>{locale}</button>
125
+ * ))}
126
+ * </div>
127
+ * );
128
+ * }
129
+ * ```
130
+ */
131
+ export declare function getLocales(): Promise<string[]>;
132
+ //# sourceMappingURL=locale.actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale.actions.d.ts","sourceRoot":"","sources":["../../src/helpers/locale.actions.ts"],"names":[],"mappings":"AA8DA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAyBjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAIpD"}