@spfn/cms 0.1.0-alpha.6 → 0.1.0-alpha.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +140 -6
- package/dist/actions.js +97 -10
- package/dist/actions.js.map +1 -1
- package/dist/client.d.ts +43 -9
- package/dist/client.js +406 -56
- package/dist/client.js.map +1 -1
- package/dist/contracts/labels.d.ts +149 -0
- package/dist/contracts/labels.js +166 -0
- package/dist/contracts/labels.js.map +1 -0
- package/dist/contracts/published-cache.d.ts +25 -0
- package/dist/contracts/published-cache.js +32 -0
- package/dist/contracts/published-cache.js.map +1 -0
- package/dist/contracts/values.d.ts +69 -0
- package/dist/contracts/values.js +100 -0
- package/dist/contracts/values.js.map +1 -0
- package/dist/entities/cms-audit-logs.d.ts +15 -70
- package/dist/entities/cms-audit-logs.js +75 -100
- package/dist/entities/cms-audit-logs.js.map +1 -1
- package/dist/entities/cms-draft-cache.d.ts +13 -73
- package/dist/entities/cms-draft-cache.js +35 -109
- package/dist/entities/cms-draft-cache.js.map +1 -1
- package/dist/entities/cms-label-values.d.ts +14 -65
- package/dist/entities/cms-label-values.js +78 -102
- package/dist/entities/cms-label-values.js.map +1 -1
- package/dist/entities/cms-label-versions.d.ts +16 -49
- package/dist/entities/cms-label-versions.js +73 -77
- package/dist/entities/cms-label-versions.js.map +1 -1
- package/dist/entities/cms-labels.d.ts +17 -14
- package/dist/entities/cms-labels.js +39 -45
- package/dist/entities/cms-labels.js.map +1 -1
- package/dist/entities/cms-published-cache.d.ts +14 -69
- package/dist/entities/cms-published-cache.js +33 -100
- package/dist/entities/cms-published-cache.js.map +1 -1
- package/dist/entities/index.d.ts +7 -10
- package/dist/entities/index.js +217 -9
- package/dist/entities/index.js.map +1 -1
- package/dist/generators/index.d.ts +8 -7
- package/dist/generators/index.js +657 -17
- package/dist/generators/index.js.map +1 -1
- package/dist/index.d.ts +134 -20
- package/dist/index.js +1115 -23
- package/dist/index.js.map +1 -1
- package/dist/{generators/label-sync-generator.d.ts → label-sync-generator-lQrcVfja.d.ts} +9 -6
- package/dist/labels/index.d.ts +31 -4
- package/dist/labels/index.js +31 -6
- package/dist/labels/index.js.map +1 -1
- package/dist/repositories/index.d.ts +205 -6
- package/dist/repositories/index.js +435 -8
- package/dist/repositories/index.js.map +1 -1
- package/dist/routes/labels/[id]/index.js +499 -89
- package/dist/routes/labels/[id]/index.js.map +1 -1
- package/dist/routes/labels/{[id] → _id_}/index.d.ts +5 -3
- package/dist/routes/labels/by-key/[key]/index.js +453 -29
- package/dist/routes/labels/by-key/[key]/index.js.map +1 -1
- package/dist/routes/labels/by-key/_key_/index.d.ts +10 -0
- package/dist/routes/labels/index.d.ts +5 -3
- package/dist/routes/labels/index.js +488 -68
- package/dist/routes/labels/index.js.map +1 -1
- package/dist/routes/published-cache/index.d.ts +5 -3
- package/dist/routes/published-cache/index.js +325 -30
- package/dist/routes/published-cache/index.js.map +1 -1
- package/dist/routes/values/[labelId]/[version]/index.js +467 -41
- package/dist/routes/values/[labelId]/[version]/index.js.map +1 -1
- package/dist/routes/values/[labelId]/index.js +463 -39
- package/dist/routes/values/[labelId]/index.js.map +1 -1
- package/dist/routes/values/_labelId_/_version_/index.d.ts +10 -0
- package/dist/routes/values/_labelId_/index.d.ts +10 -0
- package/dist/server.d.ts +17 -7
- package/dist/server.js +263 -252
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +8 -14
- package/dist/store.js +396 -198
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +14 -7
- package/dist/types.js +0 -6
- package/dist/types.js.map +1 -1
- package/migrations/0000_condemned_centennial.sql +89 -0
- package/migrations/meta/0000_snapshot.json +687 -0
- package/migrations/meta/_journal.json +13 -0
- package/package.json +33 -16
- package/dist/actions.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/cms.config.d.ts +0 -77
- package/dist/cms.config.d.ts.map +0 -1
- package/dist/cms.config.js +0 -111
- package/dist/cms.config.js.map +0 -1
- package/dist/entities/cms-audit-logs.d.ts.map +0 -1
- package/dist/entities/cms-draft-cache.d.ts.map +0 -1
- package/dist/entities/cms-label-values.d.ts.map +0 -1
- package/dist/entities/cms-label-versions.d.ts.map +0 -1
- package/dist/entities/cms-labels.d.ts.map +0 -1
- package/dist/entities/cms-published-cache.d.ts.map +0 -1
- package/dist/entities/index.d.ts.map +0 -1
- package/dist/generators/index.d.ts.map +0 -1
- package/dist/generators/label-sync-generator.d.ts.map +0 -1
- package/dist/generators/label-sync-generator.js +0 -87
- package/dist/generators/label-sync-generator.js.map +0 -1
- package/dist/helpers/locale.actions.d.ts +0 -132
- package/dist/helpers/locale.actions.d.ts.map +0 -1
- package/dist/helpers/locale.actions.js +0 -210
- package/dist/helpers/locale.actions.js.map +0 -1
- package/dist/helpers/locale.constants.d.ts +0 -10
- package/dist/helpers/locale.constants.d.ts.map +0 -1
- package/dist/helpers/locale.constants.js +0 -10
- package/dist/helpers/locale.constants.js.map +0 -1
- package/dist/helpers/locale.d.ts +0 -17
- package/dist/helpers/locale.d.ts.map +0 -1
- package/dist/helpers/locale.js +0 -20
- package/dist/helpers/locale.js.map +0 -1
- package/dist/helpers/sync.d.ts +0 -41
- package/dist/helpers/sync.d.ts.map +0 -1
- package/dist/helpers/sync.js +0 -309
- package/dist/helpers/sync.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/init.d.ts +0 -31
- package/dist/init.d.ts.map +0 -1
- package/dist/init.js +0 -36
- package/dist/init.js.map +0 -1
- package/dist/labels/helpers.d.ts +0 -31
- package/dist/labels/helpers.d.ts.map +0 -1
- package/dist/labels/helpers.js +0 -60
- package/dist/labels/helpers.js.map +0 -1
- package/dist/labels/index.d.ts.map +0 -1
- package/dist/repositories/cms-draft-cache.repository.d.ts +0 -62
- package/dist/repositories/cms-draft-cache.repository.d.ts.map +0 -1
- package/dist/repositories/cms-draft-cache.repository.js +0 -56
- package/dist/repositories/cms-draft-cache.repository.js.map +0 -1
- package/dist/repositories/cms-label-values.repository.d.ts +0 -32
- package/dist/repositories/cms-label-values.repository.d.ts.map +0 -1
- package/dist/repositories/cms-label-values.repository.js +0 -72
- package/dist/repositories/cms-label-values.repository.js.map +0 -1
- package/dist/repositories/cms-labels.repository.d.ts +0 -53
- package/dist/repositories/cms-labels.repository.d.ts.map +0 -1
- package/dist/repositories/cms-labels.repository.js +0 -77
- package/dist/repositories/cms-labels.repository.js.map +0 -1
- package/dist/repositories/cms-published-cache.repository.d.ts +0 -53
- package/dist/repositories/cms-published-cache.repository.d.ts.map +0 -1
- package/dist/repositories/cms-published-cache.repository.js +0 -54
- package/dist/repositories/cms-published-cache.repository.js.map +0 -1
- package/dist/repositories/index.d.ts.map +0 -1
- package/dist/routes/labels/[id]/contract.d.ts +0 -68
- package/dist/routes/labels/[id]/contract.d.ts.map +0 -1
- package/dist/routes/labels/[id]/contract.js +0 -84
- package/dist/routes/labels/[id]/contract.js.map +0 -1
- package/dist/routes/labels/[id]/index.d.ts.map +0 -1
- package/dist/routes/labels/by-key/[key]/contract.d.ts +0 -24
- package/dist/routes/labels/by-key/[key]/contract.d.ts.map +0 -1
- package/dist/routes/labels/by-key/[key]/contract.js +0 -28
- package/dist/routes/labels/by-key/[key]/contract.js.map +0 -1
- package/dist/routes/labels/by-key/[key]/index.d.ts +0 -8
- package/dist/routes/labels/by-key/[key]/index.d.ts.map +0 -1
- package/dist/routes/labels/contract.d.ts +0 -59
- package/dist/routes/labels/contract.d.ts.map +0 -1
- package/dist/routes/labels/contract.js +0 -75
- package/dist/routes/labels/contract.js.map +0 -1
- package/dist/routes/labels/index.d.ts.map +0 -1
- package/dist/routes/published-cache/contract.d.ts +0 -25
- package/dist/routes/published-cache/contract.d.ts.map +0 -1
- package/dist/routes/published-cache/contract.js +0 -35
- package/dist/routes/published-cache/contract.js.map +0 -1
- package/dist/routes/published-cache/index.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/[version]/contract.d.ts +0 -29
- package/dist/routes/values/[labelId]/[version]/contract.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/[version]/contract.js +0 -33
- package/dist/routes/values/[labelId]/[version]/contract.js.map +0 -1
- package/dist/routes/values/[labelId]/[version]/index.d.ts +0 -8
- package/dist/routes/values/[labelId]/[version]/index.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/contract.d.ts +0 -38
- package/dist/routes/values/[labelId]/contract.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/contract.js +0 -59
- package/dist/routes/values/[labelId]/contract.js.map +0 -1
- package/dist/routes/values/[labelId]/index.d.ts +0 -8
- package/dist/routes/values/[labelId]/index.d.ts.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/store.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,24 +1,1116 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
// src/cms.config.ts
|
|
2
|
+
function getEnvVar(key, defaultValue) {
|
|
3
|
+
return process.env[key] || defaultValue;
|
|
4
|
+
}
|
|
5
|
+
function getEnvBoolean(key, defaultValue) {
|
|
6
|
+
const value = process.env[key];
|
|
7
|
+
if (value === void 0) return defaultValue;
|
|
8
|
+
return value === "true" || value === "1";
|
|
9
|
+
}
|
|
10
|
+
function loadConfigFromEnv() {
|
|
11
|
+
const defaultLocale = getEnvVar("SPFN_CMS_DEFAULT_LOCALE", "en");
|
|
12
|
+
const supportedLocalesStr = getEnvVar("SPFN_CMS_SUPPORTED_LOCALES", "en,ko");
|
|
13
|
+
const detectBrowserLanguage2 = getEnvBoolean("SPFN_CMS_DETECT_BROWSER_LANGUAGE", true);
|
|
14
|
+
const supportedLocales = supportedLocalesStr.split(",").map((locale) => locale.trim()).filter((locale) => locale.length > 0);
|
|
15
|
+
if (!supportedLocales.includes(defaultLocale)) {
|
|
16
|
+
supportedLocales.unshift(defaultLocale);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
defaultLocale,
|
|
20
|
+
supportedLocales,
|
|
21
|
+
detectBrowserLanguage: detectBrowserLanguage2
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
var currentConfig = loadConfigFromEnv();
|
|
25
|
+
function getCmsConfig() {
|
|
26
|
+
return currentConfig;
|
|
27
|
+
}
|
|
28
|
+
function configureCms(config) {
|
|
29
|
+
currentConfig = {
|
|
30
|
+
...currentConfig,
|
|
31
|
+
...config
|
|
32
|
+
};
|
|
33
|
+
if (config.defaultLocale && !currentConfig.supportedLocales.includes(config.defaultLocale)) {
|
|
34
|
+
console.warn(
|
|
35
|
+
`[CMS Config] Default locale '${config.defaultLocale}' not in supported locales, adding automatically.`,
|
|
36
|
+
`Supported locales: [${currentConfig.supportedLocales.join(", ")}]`
|
|
37
|
+
);
|
|
38
|
+
currentConfig.supportedLocales.unshift(config.defaultLocale);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function resetCmsConfig() {
|
|
42
|
+
currentConfig = loadConfigFromEnv();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/server.ts
|
|
46
|
+
import { cache } from "react";
|
|
47
|
+
import { client } from "@spfn/core/client";
|
|
48
|
+
|
|
49
|
+
// src/contracts/published-cache.ts
|
|
50
|
+
import { Type } from "@sinclair/typebox";
|
|
51
|
+
var SectionData = Type.Object({
|
|
52
|
+
section: Type.String(),
|
|
53
|
+
locale: Type.String(),
|
|
54
|
+
content: Type.Record(Type.String(), Type.Any()),
|
|
55
|
+
version: Type.Number(),
|
|
56
|
+
publishedAt: Type.Union([Type.String(), Type.Null()])
|
|
57
|
+
});
|
|
58
|
+
var getPublishedCacheContract = {
|
|
59
|
+
method: "GET",
|
|
60
|
+
path: "/cms/published-cache",
|
|
61
|
+
query: Type.Object({
|
|
62
|
+
sections: Type.Union([
|
|
63
|
+
Type.String({ description: "\uB2E8\uC77C \uC139\uC158 \uC774\uB984 (\uC608: home)" }),
|
|
64
|
+
Type.Array(Type.String(), { description: '\uC5EC\uB7EC \uC139\uC158 \uC774\uB984 (\uC608: ["home", "footer"])' })
|
|
65
|
+
]),
|
|
66
|
+
locale: Type.Optional(Type.String({ default: "ko", description: "\uC5B8\uC5B4 \uCF54\uB4DC" }))
|
|
67
|
+
}),
|
|
68
|
+
response: Type.Union([
|
|
69
|
+
// 성공: 항상 배열로 반환
|
|
70
|
+
Type.Array(SectionData),
|
|
71
|
+
// 에러
|
|
72
|
+
Type.Object({
|
|
73
|
+
error: Type.String()
|
|
74
|
+
})
|
|
75
|
+
])
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/helpers/locale.actions.ts
|
|
79
|
+
import { cookies, headers } from "next/headers.js";
|
|
80
|
+
|
|
81
|
+
// src/helpers/locale.constants.ts
|
|
82
|
+
var LOCALE_COOKIE_KEY = "spfn-locale";
|
|
83
|
+
|
|
84
|
+
// src/helpers/locale.actions.ts
|
|
85
|
+
async function detectBrowserLanguage() {
|
|
86
|
+
try {
|
|
87
|
+
const headersList = await headers();
|
|
88
|
+
const acceptLanguage = headersList.get("accept-language");
|
|
89
|
+
if (!acceptLanguage) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const languages = acceptLanguage.split(",").map((lang) => {
|
|
93
|
+
const [code] = lang.split(";");
|
|
94
|
+
return code.split("-")[0].trim();
|
|
95
|
+
});
|
|
96
|
+
const config = getCmsConfig();
|
|
97
|
+
for (const lang of languages) {
|
|
98
|
+
if (config.supportedLocales.includes(lang)) {
|
|
99
|
+
return lang;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function getLocale() {
|
|
108
|
+
const config = getCmsConfig();
|
|
109
|
+
const cookieStore = await cookies();
|
|
110
|
+
const cookieLocale = cookieStore.get(LOCALE_COOKIE_KEY)?.value;
|
|
111
|
+
if (cookieLocale && config.supportedLocales.includes(cookieLocale)) {
|
|
112
|
+
return cookieLocale;
|
|
113
|
+
}
|
|
114
|
+
if (config.detectBrowserLanguage) {
|
|
115
|
+
const browserLang = await detectBrowserLanguage();
|
|
116
|
+
if (browserLang) {
|
|
117
|
+
return browserLang;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return config.defaultLocale;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/server.ts
|
|
124
|
+
function replaceVariables(text7, replace) {
|
|
125
|
+
return text7.replace(/\{(\w+)}/g, (match, key) => {
|
|
126
|
+
const value = replace[key];
|
|
127
|
+
return value !== void 0 ? String(value) : match;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
var getSection = cache(async (section, locale) => {
|
|
131
|
+
const actualLocale = locale ?? await getLocale();
|
|
132
|
+
try {
|
|
133
|
+
const response = await client.call(
|
|
134
|
+
getPublishedCacheContract,
|
|
135
|
+
{
|
|
136
|
+
query: { sections: section, locale: actualLocale }
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
if ("error" in response) {
|
|
140
|
+
console.warn(`[CMS] ${response.error}`);
|
|
141
|
+
const sectionData2 = {
|
|
142
|
+
section,
|
|
143
|
+
locale: actualLocale,
|
|
144
|
+
content: {},
|
|
145
|
+
version: 0,
|
|
146
|
+
publishedAt: null
|
|
147
|
+
};
|
|
148
|
+
const t2 = (_key, defaultValue) => defaultValue ?? "";
|
|
149
|
+
return { t: t2, data: sectionData2 };
|
|
150
|
+
}
|
|
151
|
+
const found = response[0];
|
|
152
|
+
if (!found) {
|
|
153
|
+
const sectionData2 = {
|
|
154
|
+
section,
|
|
155
|
+
locale: actualLocale,
|
|
156
|
+
content: {},
|
|
157
|
+
version: 0,
|
|
158
|
+
publishedAt: null
|
|
159
|
+
};
|
|
160
|
+
const t2 = (_key, defaultValue) => defaultValue ?? "";
|
|
161
|
+
return { t: t2, data: sectionData2 };
|
|
162
|
+
}
|
|
163
|
+
const sectionData = {
|
|
164
|
+
section: found.section,
|
|
165
|
+
locale: found.locale,
|
|
166
|
+
content: found.content || {},
|
|
167
|
+
version: found.version,
|
|
168
|
+
publishedAt: found.publishedAt
|
|
169
|
+
};
|
|
170
|
+
const t = (key, defaultValue, replace) => {
|
|
171
|
+
const fullKey = `${section}.${key}`;
|
|
172
|
+
let value = sectionData.content[fullKey];
|
|
173
|
+
if (value === void 0 || value === null) {
|
|
174
|
+
value = defaultValue ?? "";
|
|
175
|
+
}
|
|
176
|
+
if (typeof value !== "string") {
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
if (replace) {
|
|
180
|
+
value = replaceVariables(value, replace);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
t,
|
|
186
|
+
data: sectionData
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(`[CMS] Failed to fetch section "${section}":`, error);
|
|
190
|
+
const sectionData = {
|
|
191
|
+
section,
|
|
192
|
+
locale: actualLocale,
|
|
193
|
+
content: {},
|
|
194
|
+
version: 0,
|
|
195
|
+
publishedAt: null
|
|
196
|
+
};
|
|
197
|
+
const t = (_key, defaultValue) => defaultValue ?? "";
|
|
198
|
+
return { t, data: sectionData };
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
var getSections = cache(async (sections, locale) => {
|
|
202
|
+
const actualLocale = locale ?? await getLocale();
|
|
203
|
+
try {
|
|
204
|
+
const response = await client.call(
|
|
205
|
+
getPublishedCacheContract,
|
|
206
|
+
{
|
|
207
|
+
query: { sections, locale: actualLocale }
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
if ("error" in response) {
|
|
211
|
+
console.warn(`[CMS] ${response.error}`);
|
|
212
|
+
const sectionsMap2 = {};
|
|
213
|
+
sections.forEach((section) => {
|
|
214
|
+
sectionsMap2[section] = {
|
|
215
|
+
t: (_key, defaultValue) => defaultValue ?? "",
|
|
216
|
+
data: {
|
|
217
|
+
section,
|
|
218
|
+
locale: actualLocale,
|
|
219
|
+
content: {},
|
|
220
|
+
version: 0,
|
|
221
|
+
publishedAt: null
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
return sectionsMap2;
|
|
226
|
+
}
|
|
227
|
+
const sectionsMap = {};
|
|
228
|
+
sections.forEach((section) => {
|
|
229
|
+
sectionsMap[section] = {
|
|
230
|
+
t: (_key, defaultValue) => defaultValue ?? "",
|
|
231
|
+
data: {
|
|
232
|
+
section,
|
|
233
|
+
locale: actualLocale,
|
|
234
|
+
content: {},
|
|
235
|
+
version: 0,
|
|
236
|
+
publishedAt: null
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
response.forEach((sectionData) => {
|
|
241
|
+
const createTranslationFn = (section, content) => {
|
|
242
|
+
return (key, defaultValue, replace) => {
|
|
243
|
+
const fullKey = `${section}.${key}`;
|
|
244
|
+
let value = content[fullKey];
|
|
245
|
+
if (value === void 0 || value === null) {
|
|
246
|
+
value = defaultValue ?? "";
|
|
247
|
+
}
|
|
248
|
+
if (typeof value !== "string") {
|
|
249
|
+
return "";
|
|
250
|
+
}
|
|
251
|
+
if (replace) {
|
|
252
|
+
value = replaceVariables(value, replace);
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
sectionsMap[sectionData.section] = {
|
|
258
|
+
t: createTranslationFn(sectionData.section, sectionData.content),
|
|
259
|
+
data: {
|
|
260
|
+
section: sectionData.section,
|
|
261
|
+
locale: sectionData.locale,
|
|
262
|
+
content: sectionData.content,
|
|
263
|
+
version: sectionData.version,
|
|
264
|
+
publishedAt: sectionData.publishedAt
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
return sectionsMap;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(`[CMS] Failed to fetch sections:`, error);
|
|
271
|
+
const sectionsMap = {};
|
|
272
|
+
sections.forEach((section) => {
|
|
273
|
+
sectionsMap[section] = {
|
|
274
|
+
t: (_key, defaultValue) => defaultValue ?? "",
|
|
275
|
+
data: {
|
|
276
|
+
section,
|
|
277
|
+
locale: actualLocale,
|
|
278
|
+
content: {},
|
|
279
|
+
version: 0,
|
|
280
|
+
publishedAt: null
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
return sectionsMap;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// src/repositories/cms-labels.repository.ts
|
|
289
|
+
import { findOne, findMany as findManyHelper, create as createHelper, updateOne, deleteOne, count as countHelper } from "@spfn/core/db";
|
|
290
|
+
import { desc } from "drizzle-orm";
|
|
291
|
+
|
|
292
|
+
// src/entities/cms-labels.ts
|
|
293
|
+
import { index, integer, serial, text, timestamp } from "drizzle-orm/pg-core";
|
|
294
|
+
import { createFunctionSchema } from "@spfn/core/db";
|
|
295
|
+
var schema = createFunctionSchema("@spfn/cms");
|
|
296
|
+
var cmsLabels = schema.table("labels", {
|
|
297
|
+
// Primary Key
|
|
298
|
+
id: serial("id").primaryKey(),
|
|
299
|
+
// 라벨 식별자
|
|
300
|
+
key: text("key").notNull().unique(),
|
|
301
|
+
// 예: "home.hero.title", "why-futureplay.hero.subtitle"
|
|
302
|
+
// 구조: {section}.{component}.{property}
|
|
303
|
+
// 섹션 분류 (페이지 단위)
|
|
304
|
+
section: text("section").notNull(),
|
|
305
|
+
// 예: "home", "why-futureplay", "team"
|
|
306
|
+
// 값 타입
|
|
307
|
+
type: text("type").notNull(),
|
|
308
|
+
// "text" | "image" | "video" | "file" | "object"
|
|
309
|
+
// 기본값
|
|
310
|
+
defaultValue: text("default_value"),
|
|
311
|
+
// 라벨의 기본값 (sync 시 설정)
|
|
312
|
+
// 설명
|
|
313
|
+
description: text("description"),
|
|
314
|
+
// 라벨에 대한 설명 (optional)
|
|
315
|
+
// 현재 발행된 버전 번호
|
|
316
|
+
publishedVersion: integer("published_version"),
|
|
317
|
+
// null = 미발행 상태
|
|
318
|
+
// 1, 2, 3... = 발행된 버전 번호
|
|
319
|
+
// 생성자 추적
|
|
320
|
+
createdBy: text("created_by"),
|
|
321
|
+
// 타임스탬프
|
|
322
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
323
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
324
|
+
}, (table) => [
|
|
325
|
+
// 인덱스: 섹션별 조회 최적화
|
|
326
|
+
index("cms_labels_section_idx").on(table.section),
|
|
327
|
+
// 인덱스: key로 조회 최적화 (unique 제약으로 자동 생성되지만 명시)
|
|
328
|
+
index("cms_labels_key_idx").on(table.key)
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
// src/entities/cms-label-values.ts
|
|
332
|
+
import { serial as serial2, integer as integer2, text as text2, jsonb, timestamp as timestamp2, index as index2, unique } from "drizzle-orm/pg-core";
|
|
333
|
+
import { createFunctionSchema as createFunctionSchema2 } from "@spfn/core/db";
|
|
334
|
+
var schema2 = createFunctionSchema2("@spfn/cms");
|
|
335
|
+
var cmsLabelValues = schema2.table("label_values", {
|
|
336
|
+
// Primary Key
|
|
337
|
+
id: serial2("id").primaryKey(),
|
|
338
|
+
// Foreign Key: cms_labels
|
|
339
|
+
labelId: integer2("label_id").notNull().references(() => cmsLabels.id, { onDelete: "cascade" }),
|
|
340
|
+
// 버전 번호
|
|
341
|
+
version: integer2("version").notNull().default(1),
|
|
342
|
+
// 언어 코드
|
|
343
|
+
locale: text2("locale").notNull().default("ko"),
|
|
344
|
+
// "ko" | "en" | "ja"
|
|
345
|
+
// 반응형 브레이크포인트
|
|
346
|
+
breakpoint: text2("breakpoint"),
|
|
347
|
+
// null = 기본값 (모든 화면 크기)
|
|
348
|
+
// "sm" | "md" | "lg" | "xl" | "2xl"
|
|
349
|
+
// 실제 값 (JSONB)
|
|
350
|
+
value: jsonb("value").notNull(),
|
|
351
|
+
// LabelValue 타입:
|
|
352
|
+
// - TextValue: { type: "text", content: string }
|
|
353
|
+
// - ImageValue: { type: "image", url: string, alt?: string, width?: number, height?: number }
|
|
354
|
+
// - VideoValue: { type: "video", url: string, thumbnail?: string, duration?: number }
|
|
355
|
+
// - FileValue: { type: "file", url: string, filename: string, size?: number }
|
|
356
|
+
// - ObjectValue: { type: "object", fields: Record<string, LabelValue> }
|
|
357
|
+
// 생성 시각
|
|
358
|
+
createdAt: timestamp2("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
359
|
+
}, (table) => [
|
|
360
|
+
// UNIQUE 제약: 같은 버전에서 locale + breakpoint 조합은 유일
|
|
361
|
+
unique("cms_label_values_locale_breakpoint_unique").on(table.labelId, table.version, table.locale, table.breakpoint),
|
|
362
|
+
// 인덱스: labelId + version 복합 조회 최적화
|
|
363
|
+
index2("cms_label_values_label_version_idx").on(table.labelId, table.version),
|
|
364
|
+
// 인덱스: locale 필터링 최적화
|
|
365
|
+
index2("cms_label_values_locale_idx").on(table.locale)
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
// src/entities/cms-label-versions.ts
|
|
369
|
+
import { serial as serial3, integer as integer3, text as text3, timestamp as timestamp3, index as index3, unique as unique2 } from "drizzle-orm/pg-core";
|
|
370
|
+
import { createFunctionSchema as createFunctionSchema3 } from "@spfn/core/db";
|
|
371
|
+
var schema3 = createFunctionSchema3("@spfn/cms");
|
|
372
|
+
var cmsLabelVersions = schema3.table("label_versions", {
|
|
373
|
+
// Primary Key
|
|
374
|
+
id: serial3("id").primaryKey(),
|
|
375
|
+
// Foreign Key: cms_labels
|
|
376
|
+
labelId: integer3("label_id").notNull().references(() => cmsLabels.id, { onDelete: "cascade" }),
|
|
377
|
+
// 버전 번호
|
|
378
|
+
version: integer3("version").notNull(),
|
|
379
|
+
// 버전 상태
|
|
380
|
+
status: text3("status").notNull(),
|
|
381
|
+
// "draft" | "published" | "archived"
|
|
382
|
+
// 발행 정보
|
|
383
|
+
publishedAt: timestamp3("published_at", { withTimezone: true }),
|
|
384
|
+
publishedBy: text3("published_by"),
|
|
385
|
+
// 버전 노트 (변경사항 설명)
|
|
386
|
+
notes: text3("notes"),
|
|
387
|
+
// 버전 생성자
|
|
388
|
+
createdBy: text3("created_by"),
|
|
389
|
+
// 생성 시각
|
|
390
|
+
createdAt: timestamp3("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
391
|
+
}, (table) => [
|
|
392
|
+
// UNIQUE 제약: 각 라벨의 버전 번호는 고유
|
|
393
|
+
unique2("cms_label_versions_label_version_unique").on(table.labelId, table.version),
|
|
394
|
+
// 인덱스: labelId로 버전 목록 조회 최적화
|
|
395
|
+
index3("cms_label_versions_label_id_idx").on(table.labelId),
|
|
396
|
+
// 인덱스: status 필터링 최적화
|
|
397
|
+
index3("cms_label_versions_status_idx").on(table.status)
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
// src/entities/cms-draft-cache.ts
|
|
401
|
+
import { serial as serial4, text as text4, jsonb as jsonb2, timestamp as timestamp4, index as index4, unique as unique3 } from "drizzle-orm/pg-core";
|
|
402
|
+
import { createFunctionSchema as createFunctionSchema4 } from "@spfn/core/db";
|
|
403
|
+
var schema4 = createFunctionSchema4("@spfn/cms");
|
|
404
|
+
var cmsDraftCache = schema4.table("draft_cache", {
|
|
405
|
+
// Primary Key
|
|
406
|
+
id: serial4("id").primaryKey(),
|
|
407
|
+
// 섹션 (페이지 단위)
|
|
408
|
+
section: text4("section").notNull(),
|
|
409
|
+
// "home" | "why-futureplay" | "team" | "our-companies" | "apply"
|
|
410
|
+
// 언어
|
|
411
|
+
locale: text4("locale").notNull(),
|
|
412
|
+
// "ko" | "en" | "ja"
|
|
413
|
+
// 사용자 ID (핵심 필드!)
|
|
414
|
+
userId: text4("user_id").notNull(),
|
|
415
|
+
// 각 관리자의 독립적인 작업 공간
|
|
416
|
+
// Draft 콘텐츠 (JSONB)
|
|
417
|
+
content: jsonb2("content").notNull(),
|
|
418
|
+
// Record<string, LabelValue>
|
|
419
|
+
// {
|
|
420
|
+
// "home.hero.title": { type: "text", content: "수정 중..." },
|
|
421
|
+
// "home.hero.subtitle": { type: "text", content: "새로운 문구" },
|
|
422
|
+
// ...
|
|
423
|
+
// }
|
|
424
|
+
// 최종 수정 시각
|
|
425
|
+
updatedAt: timestamp4("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
426
|
+
}, (table) => [
|
|
427
|
+
// UNIQUE 제약: section + locale + userId 조합은 유일
|
|
428
|
+
unique3("cms_draft_cache_unique").on(table.section, table.locale, table.userId),
|
|
429
|
+
// 인덱스: section으로 조회 최적화
|
|
430
|
+
index4("cms_draft_cache_section_idx").on(table.section),
|
|
431
|
+
// 인덱스: userId로 사용자의 모든 draft 조회 최적화
|
|
432
|
+
index4("cms_draft_cache_user_idx").on(table.userId)
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
// src/entities/cms-published-cache.ts
|
|
436
|
+
import { serial as serial5, text as text5, jsonb as jsonb3, integer as integer4, timestamp as timestamp5, index as index5, unique as unique4 } from "drizzle-orm/pg-core";
|
|
437
|
+
import { createFunctionSchema as createFunctionSchema5 } from "@spfn/core/db";
|
|
438
|
+
var schema5 = createFunctionSchema5("@spfn/cms");
|
|
439
|
+
var cmsPublishedCache = schema5.table("published_cache", {
|
|
440
|
+
// Primary Key
|
|
441
|
+
id: serial5("id").primaryKey(),
|
|
442
|
+
// 섹션 (페이지 단위)
|
|
443
|
+
section: text5("section").notNull(),
|
|
444
|
+
// "home" | "why-futureplay" | "team" | "our-companies" | "apply"
|
|
445
|
+
// 언어
|
|
446
|
+
locale: text5("locale").notNull(),
|
|
447
|
+
// "ko" | "en" | "ja"
|
|
448
|
+
// 캐시된 콘텐츠 (JSONB)
|
|
449
|
+
content: jsonb3("content").notNull(),
|
|
450
|
+
// Record<string, LabelValue>
|
|
451
|
+
// {
|
|
452
|
+
// "home.hero.title": { type: "text", content: "..." },
|
|
453
|
+
// "home.hero.image": { type: "image", url: "...", alt: "..." },
|
|
454
|
+
// ...
|
|
455
|
+
// }
|
|
456
|
+
// 발행 정보
|
|
457
|
+
publishedAt: timestamp5("published_at", { withTimezone: true }).notNull(),
|
|
458
|
+
publishedBy: text5("published_by"),
|
|
459
|
+
// 캐시 버전 (클라이언트 캐싱용)
|
|
460
|
+
version: integer4("version").notNull().default(1)
|
|
461
|
+
}, (table) => [
|
|
462
|
+
// UNIQUE 제약: section + locale 조합은 유일
|
|
463
|
+
unique4("cms_published_cache_unique").on(table.section, table.locale),
|
|
464
|
+
// 인덱스: section으로 조회 최적화
|
|
465
|
+
index5("cms_published_cache_section_idx").on(table.section)
|
|
466
|
+
]);
|
|
467
|
+
|
|
468
|
+
// src/entities/cms-audit-logs.ts
|
|
469
|
+
import { serial as serial6, integer as integer5, text as text6, jsonb as jsonb4, timestamp as timestamp6, index as index6 } from "drizzle-orm/pg-core";
|
|
470
|
+
import { createFunctionSchema as createFunctionSchema6 } from "@spfn/core/db";
|
|
471
|
+
var schema6 = createFunctionSchema6("@spfn/cms");
|
|
472
|
+
var cmsAuditLogs = schema6.table("audit_logs", {
|
|
473
|
+
// Primary Key
|
|
474
|
+
id: serial6("id").primaryKey(),
|
|
475
|
+
// Foreign Key: cms_labels (nullable - 라벨 삭제 시 로그는 유지)
|
|
476
|
+
labelId: integer5("label_id").references(() => cmsLabels.id, { onDelete: "set null" }),
|
|
477
|
+
// 작업 유형
|
|
478
|
+
action: text6("action").notNull(),
|
|
479
|
+
// "create" | "update" | "publish" | "unpublish" | "archive" | "delete" | "rollback" | "duplicate"
|
|
480
|
+
// 사용자 정보
|
|
481
|
+
userId: text6("user_id").notNull(),
|
|
482
|
+
userName: text6("user_name"),
|
|
483
|
+
// 변경 내용 (before/after)
|
|
484
|
+
changes: jsonb4("changes"),
|
|
485
|
+
// { before: {...}, after: {...} }
|
|
486
|
+
// 추가 메타데이터
|
|
487
|
+
metadata: jsonb4("metadata"),
|
|
488
|
+
// { version: number, ip: string, userAgent: string, ... }
|
|
489
|
+
// 작업 시각
|
|
490
|
+
createdAt: timestamp6("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
491
|
+
}, (table) => [
|
|
492
|
+
// 인덱스: labelId로 이력 조회 최적화
|
|
493
|
+
index6("cms_audit_logs_label_id_idx").on(table.labelId),
|
|
494
|
+
// 인덱스: userId로 사용자 활동 조회 최적화
|
|
495
|
+
index6("cms_audit_logs_user_id_idx").on(table.userId),
|
|
496
|
+
// 인덱스: action 필터링 최적화
|
|
497
|
+
index6("cms_audit_logs_action_idx").on(table.action),
|
|
498
|
+
// 인덱스: 시간순 조회 최적화
|
|
499
|
+
index6("cms_audit_logs_created_at_idx").on(table.createdAt)
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
// src/repositories/cms-labels.repository.ts
|
|
503
|
+
async function findMany(options) {
|
|
504
|
+
const { section, limit = 20, offset = 0 } = options || {};
|
|
505
|
+
return findManyHelper(cmsLabels, {
|
|
506
|
+
where: section ? { section } : void 0,
|
|
507
|
+
orderBy: desc(cmsLabels.updatedAt),
|
|
508
|
+
limit,
|
|
509
|
+
offset
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
async function count(section) {
|
|
513
|
+
return countHelper(cmsLabels, section ? { section } : void 0);
|
|
514
|
+
}
|
|
515
|
+
async function findById(id) {
|
|
516
|
+
return findOne(cmsLabels, { id });
|
|
517
|
+
}
|
|
518
|
+
async function findByKey(key) {
|
|
519
|
+
return findOne(cmsLabels, { key });
|
|
520
|
+
}
|
|
521
|
+
async function findBySection(section) {
|
|
522
|
+
return findManyHelper(cmsLabels, {
|
|
523
|
+
where: { section },
|
|
524
|
+
orderBy: desc(cmsLabels.updatedAt)
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function create(data) {
|
|
528
|
+
return createHelper(cmsLabels, data);
|
|
529
|
+
}
|
|
530
|
+
async function updateById(id, data) {
|
|
531
|
+
return updateOne(cmsLabels, { id }, { ...data, updatedAt: /* @__PURE__ */ new Date() });
|
|
532
|
+
}
|
|
533
|
+
async function deleteById(id) {
|
|
534
|
+
return deleteOne(cmsLabels, { id });
|
|
535
|
+
}
|
|
536
|
+
var cmsLabelsRepository = {
|
|
537
|
+
findMany,
|
|
538
|
+
count,
|
|
539
|
+
findById,
|
|
540
|
+
findByKey,
|
|
541
|
+
findBySection,
|
|
542
|
+
create,
|
|
543
|
+
updateById,
|
|
544
|
+
deleteById
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// src/repositories/cms-label-values.repository.ts
|
|
548
|
+
import { findOne as findOne2, findMany as findMany2, create as create2, updateOne as updateOne2, deleteMany } from "@spfn/core/db";
|
|
549
|
+
import { eq, and, isNull } from "drizzle-orm";
|
|
550
|
+
async function findByLabelIdAndVersion(labelId, version, options) {
|
|
551
|
+
const { locale, breakpoint } = options || {};
|
|
552
|
+
const conditions = [
|
|
553
|
+
eq(cmsLabelValues.labelId, labelId),
|
|
554
|
+
eq(cmsLabelValues.version, version)
|
|
555
|
+
];
|
|
556
|
+
if (locale) {
|
|
557
|
+
conditions.push(eq(cmsLabelValues.locale, locale));
|
|
558
|
+
}
|
|
559
|
+
if (breakpoint !== void 0) {
|
|
560
|
+
conditions.push(
|
|
561
|
+
breakpoint === null ? isNull(cmsLabelValues.breakpoint) : eq(cmsLabelValues.breakpoint, breakpoint)
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return findMany2(cmsLabelValues, {
|
|
565
|
+
where: and(...conditions)
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
async function upsert(data) {
|
|
569
|
+
const existing = await findOne2(
|
|
570
|
+
cmsLabelValues,
|
|
571
|
+
and(
|
|
572
|
+
eq(cmsLabelValues.labelId, data.labelId),
|
|
573
|
+
eq(cmsLabelValues.version, data.version ?? 1),
|
|
574
|
+
eq(cmsLabelValues.locale, data.locale || "ko"),
|
|
575
|
+
data.breakpoint ? eq(cmsLabelValues.breakpoint, data.breakpoint) : isNull(cmsLabelValues.breakpoint)
|
|
576
|
+
)
|
|
577
|
+
);
|
|
578
|
+
if (existing) {
|
|
579
|
+
const updated = await updateOne2(
|
|
580
|
+
cmsLabelValues,
|
|
581
|
+
{ id: existing.id },
|
|
582
|
+
{ value: data.value }
|
|
583
|
+
);
|
|
584
|
+
return updated;
|
|
585
|
+
} else {
|
|
586
|
+
return create2(cmsLabelValues, data);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function upsertMany(values) {
|
|
590
|
+
const results = [];
|
|
591
|
+
for (const value of values) {
|
|
592
|
+
const result = await upsert(value);
|
|
593
|
+
results.push(result);
|
|
594
|
+
}
|
|
595
|
+
return results;
|
|
596
|
+
}
|
|
597
|
+
async function deleteByVersion(labelId, version) {
|
|
598
|
+
return deleteMany(
|
|
599
|
+
cmsLabelValues,
|
|
600
|
+
and(
|
|
601
|
+
eq(cmsLabelValues.labelId, labelId),
|
|
602
|
+
eq(cmsLabelValues.version, version)
|
|
603
|
+
)
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
var cmsLabelValuesRepository = {
|
|
607
|
+
findByLabelIdAndVersion,
|
|
608
|
+
upsert,
|
|
609
|
+
upsertMany,
|
|
610
|
+
deleteByVersion
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/repositories/cms-draft-cache.repository.ts
|
|
614
|
+
import { findOne as findOne3, findMany as findMany3, deleteOne as deleteOne2, deleteMany as deleteMany2, upsert as upsertHelper } from "@spfn/core/db";
|
|
615
|
+
import { eq as eq2, and as and2, lt } from "drizzle-orm";
|
|
616
|
+
async function findByUser(section, locale, userId) {
|
|
617
|
+
return findOne3(
|
|
618
|
+
cmsDraftCache,
|
|
619
|
+
and2(
|
|
620
|
+
eq2(cmsDraftCache.section, section),
|
|
621
|
+
eq2(cmsDraftCache.locale, locale),
|
|
622
|
+
eq2(cmsDraftCache.userId, userId)
|
|
623
|
+
)
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
async function upsert2(data) {
|
|
627
|
+
return upsertHelper(cmsDraftCache, data, {
|
|
628
|
+
target: [cmsDraftCache.section, cmsDraftCache.locale, cmsDraftCache.userId],
|
|
629
|
+
set: {
|
|
630
|
+
content: data.content,
|
|
631
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
async function findAllByUser(userId) {
|
|
636
|
+
return findMany3(cmsDraftCache, {
|
|
637
|
+
where: eq2(cmsDraftCache.userId, userId)
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
async function deleteByUser(section, locale, userId) {
|
|
641
|
+
await deleteOne2(
|
|
642
|
+
cmsDraftCache,
|
|
643
|
+
and2(
|
|
644
|
+
eq2(cmsDraftCache.section, section),
|
|
645
|
+
eq2(cmsDraftCache.locale, locale),
|
|
646
|
+
eq2(cmsDraftCache.userId, userId)
|
|
647
|
+
)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
async function cleanupOldDrafts(daysOld = 30) {
|
|
651
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
652
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
653
|
+
return deleteMany2(
|
|
654
|
+
cmsDraftCache,
|
|
655
|
+
lt(cmsDraftCache.updatedAt, cutoffDate)
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
var cmsDraftCacheRepository = {
|
|
659
|
+
findByUser,
|
|
660
|
+
upsert: upsert2,
|
|
661
|
+
findAllByUser,
|
|
662
|
+
deleteByUser,
|
|
663
|
+
cleanupOldDrafts
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/repositories/cms-published-cache.repository.ts
|
|
667
|
+
import { findOne as findOne4, findMany as findMany4, deleteOne as deleteOne3, deleteMany as deleteMany3, upsert as upsertHelper2 } from "@spfn/core/db";
|
|
668
|
+
import { eq as eq3, and as and3, sql } from "drizzle-orm";
|
|
669
|
+
async function findBySection2(section, locale = "ko") {
|
|
670
|
+
return findOne4(
|
|
671
|
+
cmsPublishedCache,
|
|
672
|
+
and3(
|
|
673
|
+
eq3(cmsPublishedCache.section, section),
|
|
674
|
+
eq3(cmsPublishedCache.locale, locale)
|
|
675
|
+
)
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
async function upsert3(data) {
|
|
679
|
+
return upsertHelper2(cmsPublishedCache, data, {
|
|
680
|
+
target: [cmsPublishedCache.section, cmsPublishedCache.locale],
|
|
681
|
+
set: {
|
|
682
|
+
content: data.content,
|
|
683
|
+
publishedAt: data.publishedAt,
|
|
684
|
+
publishedBy: data.publishedBy,
|
|
685
|
+
version: sql`${cmsPublishedCache.version} + 1`
|
|
686
|
+
// 버전 증가로 클라이언트 캐시 무효화
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
async function findAllLanguages(section) {
|
|
691
|
+
return findMany4(cmsPublishedCache, {
|
|
692
|
+
where: eq3(cmsPublishedCache.section, section)
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async function deleteBySection(section, locale) {
|
|
696
|
+
if (locale) {
|
|
697
|
+
await deleteOne3(
|
|
698
|
+
cmsPublishedCache,
|
|
699
|
+
and3(
|
|
700
|
+
eq3(cmsPublishedCache.section, section),
|
|
701
|
+
eq3(cmsPublishedCache.locale, locale)
|
|
702
|
+
)
|
|
703
|
+
);
|
|
704
|
+
} else {
|
|
705
|
+
await deleteMany3(
|
|
706
|
+
cmsPublishedCache,
|
|
707
|
+
eq3(cmsPublishedCache.section, section)
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
var cmsPublishedCacheRepository = {
|
|
712
|
+
findBySection: findBySection2,
|
|
713
|
+
upsert: upsert3,
|
|
714
|
+
findAllLanguages,
|
|
715
|
+
deleteBySection
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// src/helpers/sync.ts
|
|
719
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
720
|
+
import { basename, extname, join } from "path";
|
|
721
|
+
|
|
722
|
+
// src/labels/helpers.ts
|
|
723
|
+
function flattenLabels(labels) {
|
|
724
|
+
const result = [];
|
|
725
|
+
function isLabelDefinition(obj) {
|
|
726
|
+
return "key" in obj && "defaultValue" in obj && typeof obj.key === "string" && (typeof obj.defaultValue === "string" || typeof obj.defaultValue === "object");
|
|
727
|
+
}
|
|
728
|
+
function traverse(obj) {
|
|
729
|
+
if (isLabelDefinition(obj)) {
|
|
730
|
+
result.push({
|
|
731
|
+
key: obj.key,
|
|
732
|
+
defaultValue: obj.defaultValue,
|
|
733
|
+
description: obj.description
|
|
734
|
+
});
|
|
735
|
+
} else {
|
|
736
|
+
Object.values(obj).forEach((value) => {
|
|
737
|
+
if (typeof value === "object" && value !== null) {
|
|
738
|
+
traverse(value);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
traverse(labels);
|
|
744
|
+
return result;
|
|
745
|
+
}
|
|
746
|
+
function extractLabels(definition) {
|
|
747
|
+
return flattenLabels(definition.labels);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/helpers/sync.ts
|
|
751
|
+
async function syncAll(sections, options = {}) {
|
|
752
|
+
const results = [];
|
|
753
|
+
for (const definition of sections) {
|
|
754
|
+
const result = await syncSection(definition, options);
|
|
755
|
+
results.push(result);
|
|
756
|
+
}
|
|
757
|
+
return results;
|
|
758
|
+
}
|
|
759
|
+
function loadLabelsFromJson(labelsDir) {
|
|
760
|
+
const sections = [];
|
|
761
|
+
if (!existsSync(labelsDir)) {
|
|
762
|
+
console.warn(`[CMS] Labels directory not found: ${labelsDir}`);
|
|
763
|
+
console.warn(`[CMS] Expected directory structure:`);
|
|
764
|
+
console.warn(`[CMS] ${labelsDir}/`);
|
|
765
|
+
console.warn(`[CMS] \u251C\u2500\u2500 common/ # Section directory`);
|
|
766
|
+
console.warn(`[CMS] \u2502 \u251C\u2500\u2500 messages.json`);
|
|
767
|
+
console.warn(`[CMS] \u2502 \u2514\u2500\u2500 errors.json`);
|
|
768
|
+
console.warn(`[CMS] \u2514\u2500\u2500 home/ # Section directory`);
|
|
769
|
+
console.warn(`[CMS] \u2514\u2500\u2500 hero.json`);
|
|
770
|
+
return sections;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const entries = readdirSync(labelsDir);
|
|
774
|
+
if (entries.length === 0) {
|
|
775
|
+
console.warn(`[CMS] Labels directory is empty: ${labelsDir}`);
|
|
776
|
+
console.warn(`[CMS] Create section directories with JSON files inside`);
|
|
777
|
+
return sections;
|
|
778
|
+
}
|
|
779
|
+
const jsonFiles = entries.filter((e) => extname(e) === ".json");
|
|
780
|
+
if (jsonFiles.length > 0) {
|
|
781
|
+
console.warn(`[CMS] Found JSON files directly in ${labelsDir}:`);
|
|
782
|
+
jsonFiles.forEach((f) => console.warn(`[CMS] - ${f} (will be ignored)`));
|
|
783
|
+
console.warn(`[CMS] JSON files should be inside section directories`);
|
|
784
|
+
console.warn(`[CMS] Example: Move ${jsonFiles[0]} to ${labelsDir}/${basename(jsonFiles[0], ".json")}/${jsonFiles[0]}`);
|
|
785
|
+
}
|
|
786
|
+
for (const entry of entries) {
|
|
787
|
+
const sectionPath = join(labelsDir, entry);
|
|
788
|
+
const stat = statSync(sectionPath);
|
|
789
|
+
if (stat.isDirectory()) {
|
|
790
|
+
const sectionName = entry;
|
|
791
|
+
const labels = loadSectionLabels(sectionPath);
|
|
792
|
+
if (Object.keys(labels).length > 0) {
|
|
793
|
+
sections.push({ section: sectionName, labels });
|
|
794
|
+
} else {
|
|
795
|
+
console.warn(`[CMS] Section directory "${sectionName}" has no valid JSON files`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (sections.length === 0) {
|
|
800
|
+
console.warn(`[CMS] No valid section directories found in ${labelsDir}`);
|
|
801
|
+
}
|
|
802
|
+
} catch (error) {
|
|
803
|
+
console.warn(`[CMS] Could not scan labels directory: ${labelsDir}`);
|
|
804
|
+
console.error(`[CMS] Error:`, error);
|
|
805
|
+
}
|
|
806
|
+
return sections;
|
|
807
|
+
}
|
|
808
|
+
function loadSectionLabels(sectionPath) {
|
|
809
|
+
const labels = {};
|
|
810
|
+
try {
|
|
811
|
+
const files = readdirSync(sectionPath);
|
|
812
|
+
for (const file of files) {
|
|
813
|
+
if (extname(file) === ".json") {
|
|
814
|
+
const filePath = join(sectionPath, file);
|
|
815
|
+
const categoryName = basename(file, ".json");
|
|
816
|
+
try {
|
|
817
|
+
const content = readFileSync(filePath, "utf-8");
|
|
818
|
+
labels[categoryName] = JSON.parse(content);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
console.warn(`[CMS] Failed to parse ${filePath}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
} catch (error) {
|
|
825
|
+
console.warn(`[CMS] Could not read section directory: ${sectionPath}`);
|
|
826
|
+
}
|
|
827
|
+
return labels;
|
|
828
|
+
}
|
|
829
|
+
async function syncSection(definition, options = {}) {
|
|
830
|
+
const {
|
|
831
|
+
dryRun = false,
|
|
832
|
+
updateExisting = false,
|
|
833
|
+
removeUnused = false,
|
|
834
|
+
verbose = false
|
|
835
|
+
} = options;
|
|
836
|
+
const { section } = definition;
|
|
837
|
+
const result = {
|
|
838
|
+
section,
|
|
839
|
+
created: 0,
|
|
840
|
+
updated: 0,
|
|
841
|
+
deleted: 0,
|
|
842
|
+
unchanged: 0,
|
|
843
|
+
errors: []
|
|
844
|
+
};
|
|
845
|
+
try {
|
|
846
|
+
const definedLabels = extractLabels(definition);
|
|
847
|
+
const definedKeys = new Set(definedLabels.map((l) => l.key));
|
|
848
|
+
const existingLabels = await cmsLabelsRepository.findBySection(section);
|
|
849
|
+
const existingMap = new Map(existingLabels.map((l) => [l.key, l]));
|
|
850
|
+
if (verbose) {
|
|
851
|
+
console.log(`
|
|
852
|
+
[${section}] Found ${definedLabels.length} labels in definition`);
|
|
853
|
+
console.log(`[${section}] Found ${existingLabels.length} labels in DB`);
|
|
854
|
+
}
|
|
855
|
+
for (const label of definedLabels) {
|
|
856
|
+
const existing = existingMap.get(label.key);
|
|
857
|
+
if (!existing) {
|
|
858
|
+
if (verbose) console.log(` [CREATE] ${label.key}`);
|
|
859
|
+
if (!dryRun) {
|
|
860
|
+
try {
|
|
861
|
+
const defaultValue = typeof label.defaultValue === "object" ? JSON.stringify(label.defaultValue) : label.defaultValue;
|
|
862
|
+
await cmsLabelsRepository.create({
|
|
863
|
+
section,
|
|
864
|
+
key: label.key,
|
|
865
|
+
type: label.type || "text",
|
|
866
|
+
// 라벨 타입 (기본값: 'text')
|
|
867
|
+
defaultValue,
|
|
868
|
+
description: label.description
|
|
869
|
+
});
|
|
870
|
+
} catch (error) {
|
|
871
|
+
result.errors.push({
|
|
872
|
+
key: label.key,
|
|
873
|
+
error: error instanceof Error ? error.message : String(error)
|
|
874
|
+
});
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
result.created++;
|
|
879
|
+
} else if (updateExisting) {
|
|
880
|
+
const newDefaultValue = typeof label.defaultValue === "object" ? JSON.stringify(label.defaultValue) : label.defaultValue;
|
|
881
|
+
const hasChanged = existing.defaultValue !== newDefaultValue;
|
|
882
|
+
if (hasChanged) {
|
|
883
|
+
if (verbose) {
|
|
884
|
+
console.log(` [UPDATE] ${label.key}`);
|
|
885
|
+
console.log(` Old: "${existing.defaultValue}"`);
|
|
886
|
+
console.log(` New: "${newDefaultValue}"`);
|
|
887
|
+
}
|
|
888
|
+
if (!dryRun) {
|
|
889
|
+
try {
|
|
890
|
+
await cmsLabelsRepository.updateById(existing.id, {
|
|
891
|
+
defaultValue: newDefaultValue,
|
|
892
|
+
description: label.description
|
|
893
|
+
});
|
|
894
|
+
} catch (error) {
|
|
895
|
+
result.errors.push({
|
|
896
|
+
key: label.key,
|
|
897
|
+
error: error instanceof Error ? error.message : String(error)
|
|
898
|
+
});
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
result.updated++;
|
|
903
|
+
} else {
|
|
904
|
+
result.unchanged++;
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
result.unchanged++;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (removeUnused) {
|
|
911
|
+
for (const existing of existingLabels) {
|
|
912
|
+
if (!definedKeys.has(existing.key)) {
|
|
913
|
+
if (verbose) console.log(` [DELETE] ${existing.key}`);
|
|
914
|
+
if (!dryRun) {
|
|
915
|
+
try {
|
|
916
|
+
await cmsLabelsRepository.deleteById(existing.id);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
result.errors.push({
|
|
919
|
+
key: existing.key,
|
|
920
|
+
error: error instanceof Error ? error.message : String(error)
|
|
921
|
+
});
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
result.deleted++;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (!dryRun && (result.created > 0 || result.updated > 0 || result.deleted > 0)) {
|
|
930
|
+
if (verbose) console.log(` [CACHE] Updating published cache for section: ${section}`);
|
|
931
|
+
await updatePublishedCache(section);
|
|
932
|
+
}
|
|
933
|
+
} catch (error) {
|
|
934
|
+
result.errors.push({
|
|
935
|
+
key: "__section__",
|
|
936
|
+
error: error instanceof Error ? error.message : String(error)
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
async function updatePublishedCache(section) {
|
|
942
|
+
const labels = await cmsLabelsRepository.findBySection(section);
|
|
943
|
+
const localesSet = /* @__PURE__ */ new Set();
|
|
944
|
+
const labelsByLocale = {};
|
|
945
|
+
labels.forEach((label) => {
|
|
946
|
+
try {
|
|
947
|
+
const parsed = JSON.parse(label.defaultValue || "{}");
|
|
948
|
+
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
949
|
+
Object.keys(parsed).forEach((locale) => localesSet.add(locale));
|
|
950
|
+
Object.entries(parsed).forEach(([locale, value]) => {
|
|
951
|
+
if (!labelsByLocale[locale]) labelsByLocale[locale] = {};
|
|
952
|
+
labelsByLocale[locale][label.key] = value;
|
|
953
|
+
});
|
|
954
|
+
} else {
|
|
955
|
+
if (!labelsByLocale.ko) labelsByLocale.ko = {};
|
|
956
|
+
labelsByLocale.ko[label.key] = label.defaultValue;
|
|
957
|
+
localesSet.add("ko");
|
|
958
|
+
}
|
|
959
|
+
} catch {
|
|
960
|
+
if (!labelsByLocale.ko) labelsByLocale.ko = {};
|
|
961
|
+
labelsByLocale.ko[label.key] = label.defaultValue;
|
|
962
|
+
localesSet.add("ko");
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
const timestamp7 = /* @__PURE__ */ new Date();
|
|
966
|
+
for (const locale of localesSet) {
|
|
967
|
+
await cmsPublishedCacheRepository.upsert({
|
|
968
|
+
section,
|
|
969
|
+
locale,
|
|
970
|
+
content: labelsByLocale[locale] || {},
|
|
971
|
+
publishedAt: timestamp7,
|
|
972
|
+
publishedBy: "system"
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function initLabelSync(options = {}) {
|
|
977
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
978
|
+
const verbose = options.verbose ?? isDevelopment;
|
|
979
|
+
const labelsDir = options.labelsDir ?? "src/cms/labels";
|
|
980
|
+
if (verbose) {
|
|
981
|
+
console.log("\n\u{1F504} Initializing label sync...\n");
|
|
982
|
+
}
|
|
983
|
+
const sections = loadLabelsFromJson(labelsDir);
|
|
984
|
+
if (sections.length === 0) {
|
|
985
|
+
if (verbose) {
|
|
986
|
+
console.log("\u26A0\uFE0F No labels found in", labelsDir);
|
|
987
|
+
console.log("");
|
|
988
|
+
}
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const results = await syncAll(sections, {
|
|
992
|
+
updateExisting: true,
|
|
993
|
+
// 🔄 항상 업데이트 (프로덕션 포함)
|
|
994
|
+
...options,
|
|
995
|
+
verbose
|
|
996
|
+
});
|
|
997
|
+
const totalCreated = results.reduce((sum, r) => sum + r.created, 0);
|
|
998
|
+
const totalUpdated = results.reduce((sum, r) => sum + r.updated, 0);
|
|
999
|
+
const totalUnchanged = results.reduce((sum, r) => sum + r.unchanged, 0);
|
|
1000
|
+
const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
|
|
1001
|
+
if (verbose) {
|
|
1002
|
+
console.log("\u2705 Label sync completed\n");
|
|
1003
|
+
console.log(` Sections: ${results.length}`);
|
|
1004
|
+
console.log(` Created: ${totalCreated}`);
|
|
1005
|
+
console.log(` Updated: ${totalUpdated}`);
|
|
1006
|
+
console.log(` Unchanged: ${totalUnchanged}`);
|
|
1007
|
+
if (totalErrors > 0) {
|
|
1008
|
+
console.log(` Errors: ${totalErrors}
|
|
1009
|
+
`);
|
|
1010
|
+
} else {
|
|
1011
|
+
console.log("");
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (totalErrors > 0) {
|
|
1015
|
+
results.forEach((result) => {
|
|
1016
|
+
result.errors.forEach((error) => {
|
|
1017
|
+
console.error(`[${result.section}] ${error.key}: ${error.error}`);
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/generators/label-sync-generator.ts
|
|
1024
|
+
import { logger } from "@spfn/core";
|
|
1025
|
+
import { join as join2 } from "path";
|
|
1026
|
+
var syncLogger = logger.child("label-sync");
|
|
1027
|
+
var LabelSyncGenerator = class {
|
|
1028
|
+
name = "label-sync";
|
|
1029
|
+
labelsDir;
|
|
1030
|
+
constructor(config = {}) {
|
|
1031
|
+
this.labelsDir = config.labelsDir ?? "src/cms/labels";
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Watch patterns for label definition files
|
|
1035
|
+
*/
|
|
1036
|
+
get watchPatterns() {
|
|
1037
|
+
return [
|
|
1038
|
+
`${this.labelsDir}/**/*.json`
|
|
1039
|
+
];
|
|
1040
|
+
}
|
|
1041
|
+
async generate(options) {
|
|
1042
|
+
if (options.debug) {
|
|
1043
|
+
syncLogger.info("Starting label sync...");
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
const labelsPath = join2(options.cwd, this.labelsDir);
|
|
1047
|
+
const sections = loadLabelsFromJson(labelsPath);
|
|
1048
|
+
if (sections.length === 0) {
|
|
1049
|
+
syncLogger.warn(`No labels found in ${labelsPath}`);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
syncLogger.info(`Found ${sections.length} sections`);
|
|
1053
|
+
const results = await syncAll(sections, {
|
|
1054
|
+
verbose: options.debug ?? false,
|
|
1055
|
+
updateExisting: true
|
|
1056
|
+
// 항상 업데이트 (프로덕션 포함)
|
|
1057
|
+
});
|
|
1058
|
+
const totalCreated = results.reduce((sum, r) => sum + r.created, 0);
|
|
1059
|
+
const totalUpdated = results.reduce((sum, r) => sum + r.updated, 0);
|
|
1060
|
+
const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
|
|
1061
|
+
if (options.debug || totalCreated > 0 || totalUpdated > 0) {
|
|
1062
|
+
syncLogger.info("Label sync completed", {
|
|
1063
|
+
sections: results.length,
|
|
1064
|
+
created: totalCreated,
|
|
1065
|
+
updated: totalUpdated,
|
|
1066
|
+
errors: totalErrors
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
if (totalErrors > 0) {
|
|
1070
|
+
results.forEach((result) => {
|
|
1071
|
+
result.errors.forEach((error) => {
|
|
1072
|
+
syncLogger.error(`[${result.section}] ${error.key}: ${error.error}`);
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
syncLogger.error(
|
|
1078
|
+
"Label sync failed",
|
|
1079
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async onFileChange(filePath, event) {
|
|
1084
|
+
syncLogger.info(`Label file ${event}`, { file: filePath });
|
|
1085
|
+
await this.generate({ cwd: process.cwd(), debug: true });
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
function createLabelSyncGenerator(config) {
|
|
1089
|
+
return new LabelSyncGenerator(config);
|
|
1090
|
+
}
|
|
1091
|
+
export {
|
|
1092
|
+
LabelSyncGenerator,
|
|
1093
|
+
cmsAuditLogs,
|
|
1094
|
+
cmsDraftCache,
|
|
1095
|
+
cmsDraftCacheRepository,
|
|
1096
|
+
cmsLabelValues,
|
|
1097
|
+
cmsLabelValuesRepository,
|
|
1098
|
+
cmsLabelVersions,
|
|
1099
|
+
cmsLabels,
|
|
1100
|
+
cmsLabelsRepository,
|
|
1101
|
+
cmsPublishedCache,
|
|
1102
|
+
cmsPublishedCacheRepository,
|
|
1103
|
+
configureCms,
|
|
1104
|
+
createLabelSyncGenerator,
|
|
1105
|
+
extractLabels,
|
|
1106
|
+
flattenLabels,
|
|
1107
|
+
getCmsConfig,
|
|
1108
|
+
getSection,
|
|
1109
|
+
getSections,
|
|
1110
|
+
initLabelSync,
|
|
1111
|
+
loadLabelsFromJson,
|
|
1112
|
+
resetCmsConfig,
|
|
1113
|
+
syncAll,
|
|
1114
|
+
syncSection
|
|
1115
|
+
};
|
|
24
1116
|
//# sourceMappingURL=index.js.map
|