@spfn/cms 0.1.0-alpha.76 → 0.1.0-alpha.77
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/api.d.ts +2 -2
- package/dist/client.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/server/entities/index.d.ts +0 -1
- package/dist/server/entities/index.js +36 -69
- package/dist/server/entities/index.js.map +1 -1
- package/dist/server/generators/index.js +38 -70
- package/dist/server/generators/index.js.map +1 -1
- package/dist/server/repositories/index.js +36 -68
- package/dist/server/repositories/index.js.map +1 -1
- package/dist/server/routes/labels/[id]/index.js +36 -68
- package/dist/server/routes/labels/[id]/index.js.map +1 -1
- package/dist/server/routes/labels/[labelId]/admin/index.js +36 -68
- package/dist/server/routes/labels/[labelId]/admin/index.js.map +1 -1
- package/dist/server/routes/labels/[labelId]/publish/index.js +36 -68
- package/dist/server/routes/labels/[labelId]/publish/index.js.map +1 -1
- package/dist/server/routes/labels/[labelId]/versions/index.js +58 -164
- package/dist/server/routes/labels/[labelId]/versions/index.js.map +1 -1
- package/dist/server/routes/labels/by-key/[key]/index.js +36 -68
- package/dist/server/routes/labels/by-key/[key]/index.js.map +1 -1
- package/dist/server/routes/labels/index.js +36 -68
- package/dist/server/routes/labels/index.js.map +1 -1
- package/dist/server/routes/published-cache/index.js +36 -68
- package/dist/server/routes/published-cache/index.js.map +1 -1
- package/dist/server/routes/values/[labelId]/[version]/index.js +36 -68
- package/dist/server/routes/values/[labelId]/[version]/index.js.map +1 -1
- package/dist/server/routes/values/[labelId]/index.js +36 -68
- package/dist/server/routes/values/[labelId]/index.js.map +1 -1
- package/dist/server.d.ts +0 -1
- package/dist/server.js +40 -73
- package/dist/server.js.map +1 -1
- package/migrations/0003_rare_runaways.sql +1 -0
- package/migrations/meta/0003_snapshot.json +563 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/dist/server/entities/cms-label-versions.d.ts +0 -174
- package/dist/server/entities/cms-label-versions.js +0 -76
- package/dist/server/entities/cms-label-versions.js.map +0 -1
package/dist/api.d.ts
CHANGED
|
@@ -212,7 +212,6 @@ declare const cmsApi: {
|
|
|
212
212
|
readonly getAdminLabel: (options: {
|
|
213
213
|
params: GetAdminLabelParams;
|
|
214
214
|
}) => Promise<{
|
|
215
|
-
status: "published" | "default-only" | "unpublished" | "modified";
|
|
216
215
|
label: {
|
|
217
216
|
section: string;
|
|
218
217
|
description: string | null;
|
|
@@ -242,6 +241,7 @@ declare const cmsApi: {
|
|
|
242
241
|
labelId: number;
|
|
243
242
|
breakpoint: string | null;
|
|
244
243
|
}[];
|
|
244
|
+
status: "published" | "default-only" | "unpublished" | "modified";
|
|
245
245
|
} | {
|
|
246
246
|
error: string;
|
|
247
247
|
}>;
|
|
@@ -252,7 +252,6 @@ declare const cmsApi: {
|
|
|
252
252
|
version: number;
|
|
253
253
|
publishedAt: string;
|
|
254
254
|
publishedBy: string | null;
|
|
255
|
-
notes: string | null;
|
|
256
255
|
values: {
|
|
257
256
|
locale: string;
|
|
258
257
|
id: number;
|
|
@@ -260,6 +259,7 @@ declare const cmsApi: {
|
|
|
260
259
|
createdAt: string;
|
|
261
260
|
breakpoint: string | null;
|
|
262
261
|
}[];
|
|
262
|
+
notes: string | null;
|
|
263
263
|
}[];
|
|
264
264
|
} | {
|
|
265
265
|
error: string;
|
package/dist/client.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ import 'drizzle-orm/pg-core';
|
|
|
9
9
|
import './server/entities/cms-label-values.js';
|
|
10
10
|
import './server/entities/cms-draft-cache.js';
|
|
11
11
|
import './server/entities/cms-published-cache.js';
|
|
12
|
-
import './server/entities/cms-label-versions.js';
|
|
13
12
|
import './server/entities/cms-audit-logs.js';
|
|
14
13
|
import './label-sync-generator-B0EmvtWM.js';
|
|
15
14
|
import '@spfn/core/codegen';
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ import 'drizzle-orm/pg-core';
|
|
|
7
7
|
import './server/entities/cms-label-values.js';
|
|
8
8
|
import './server/entities/cms-draft-cache.js';
|
|
9
9
|
import './server/entities/cms-published-cache.js';
|
|
10
|
-
import './server/entities/cms-label-versions.js';
|
|
11
10
|
import './server/entities/cms-audit-logs.js';
|
|
12
11
|
import './label-sync-generator-B0EmvtWM.js';
|
|
13
12
|
import '@spfn/core/codegen';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { CmsLabel, NewCmsLabel, cmsLabels } from './cms-labels.js';
|
|
2
2
|
export { CmsLabelValue, NewCmsLabelValue, cmsLabelValues } from './cms-label-values.js';
|
|
3
|
-
export { CmsLabelVersion, NewCmsLabelVersion, cmsLabelVersions } from './cms-label-versions.js';
|
|
4
3
|
export { CmsDraftCache, NewCmsDraftCache, cmsDraftCache } from './cms-draft-cache.js';
|
|
5
4
|
export { CmsPublishedCache, NewCmsPublishedCache, cmsPublishedCache } from './cms-published-cache.js';
|
|
6
5
|
export { CmsAuditLog, NewCmsAuditLog, cmsAuditLogs } from './cms-audit-logs.js';
|
|
@@ -74,53 +74,21 @@ var cmsLabelValues = schema2.table("label_values", {
|
|
|
74
74
|
index2("cms_label_values_locale_idx").on(table.locale)
|
|
75
75
|
]);
|
|
76
76
|
|
|
77
|
-
// src/server/entities/cms-
|
|
78
|
-
import { serial as serial3,
|
|
77
|
+
// src/server/entities/cms-draft-cache.ts
|
|
78
|
+
import { serial as serial3, text as text3, jsonb as jsonb2, timestamp as timestamp3, index as index3, unique as unique2 } from "drizzle-orm/pg-core";
|
|
79
79
|
import { createFunctionSchema as createFunctionSchema3 } from "@spfn/core/db";
|
|
80
80
|
var schema3 = createFunctionSchema3("@spfn/cms");
|
|
81
|
-
var
|
|
81
|
+
var cmsDraftCache = schema3.table("draft_cache", {
|
|
82
82
|
// Primary Key
|
|
83
83
|
id: serial3("id").primaryKey(),
|
|
84
|
-
// Foreign Key: cms_labels
|
|
85
|
-
labelId: integer3("label_id").notNull().references(() => cmsLabels.id, { onDelete: "cascade" }),
|
|
86
|
-
// 버전 번호
|
|
87
|
-
version: integer3("version").notNull(),
|
|
88
|
-
// 버전 상태
|
|
89
|
-
status: text3("status").notNull(),
|
|
90
|
-
// "draft" | "published" | "archived"
|
|
91
|
-
// 발행 정보
|
|
92
|
-
publishedAt: timestamp3("published_at", { withTimezone: true }),
|
|
93
|
-
publishedBy: text3("published_by"),
|
|
94
|
-
// 버전 노트 (변경사항 설명)
|
|
95
|
-
notes: text3("notes"),
|
|
96
|
-
// 버전 생성자
|
|
97
|
-
createdBy: text3("created_by"),
|
|
98
|
-
// 생성 시각
|
|
99
|
-
createdAt: timestamp3("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
100
|
-
}, (table) => [
|
|
101
|
-
// UNIQUE 제약: 각 라벨의 버전 번호는 고유
|
|
102
|
-
unique2("cms_label_versions_label_version_unique").on(table.labelId, table.version),
|
|
103
|
-
// 인덱스: labelId로 버전 목록 조회 최적화
|
|
104
|
-
index3("cms_label_versions_label_id_idx").on(table.labelId),
|
|
105
|
-
// 인덱스: status 필터링 최적화
|
|
106
|
-
index3("cms_label_versions_status_idx").on(table.status)
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
// src/server/entities/cms-draft-cache.ts
|
|
110
|
-
import { serial as serial4, text as text4, jsonb as jsonb2, timestamp as timestamp4, index as index4, unique as unique3 } from "drizzle-orm/pg-core";
|
|
111
|
-
import { createFunctionSchema as createFunctionSchema4 } from "@spfn/core/db";
|
|
112
|
-
var schema4 = createFunctionSchema4("@spfn/cms");
|
|
113
|
-
var cmsDraftCache = schema4.table("draft_cache", {
|
|
114
|
-
// Primary Key
|
|
115
|
-
id: serial4("id").primaryKey(),
|
|
116
84
|
// 섹션 (페이지 단위)
|
|
117
|
-
section:
|
|
85
|
+
section: text3("section").notNull(),
|
|
118
86
|
// "home" | "why-futureplay" | "team" | "our-companies" | "apply"
|
|
119
87
|
// 언어
|
|
120
|
-
locale:
|
|
88
|
+
locale: text3("locale").notNull(),
|
|
121
89
|
// "ko" | "en" | "ja"
|
|
122
90
|
// 사용자 ID (핵심 필드!)
|
|
123
|
-
userId:
|
|
91
|
+
userId: text3("user_id").notNull(),
|
|
124
92
|
// 각 관리자의 독립적인 작업 공간
|
|
125
93
|
// Draft 콘텐츠 (JSONB)
|
|
126
94
|
content: jsonb2("content").notNull(),
|
|
@@ -131,28 +99,28 @@ var cmsDraftCache = schema4.table("draft_cache", {
|
|
|
131
99
|
// ...
|
|
132
100
|
// }
|
|
133
101
|
// 최종 수정 시각
|
|
134
|
-
updatedAt:
|
|
102
|
+
updatedAt: timestamp3("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
135
103
|
}, (table) => [
|
|
136
104
|
// UNIQUE 제약: section + locale + userId 조합은 유일
|
|
137
|
-
|
|
105
|
+
unique2("cms_draft_cache_unique").on(table.section, table.locale, table.userId),
|
|
138
106
|
// 인덱스: section으로 조회 최적화
|
|
139
|
-
|
|
107
|
+
index3("cms_draft_cache_section_idx").on(table.section),
|
|
140
108
|
// 인덱스: userId로 사용자의 모든 draft 조회 최적화
|
|
141
|
-
|
|
109
|
+
index3("cms_draft_cache_user_idx").on(table.userId)
|
|
142
110
|
]);
|
|
143
111
|
|
|
144
112
|
// src/server/entities/cms-published-cache.ts
|
|
145
|
-
import { serial as
|
|
146
|
-
import { createFunctionSchema as
|
|
147
|
-
var
|
|
148
|
-
var cmsPublishedCache =
|
|
113
|
+
import { serial as serial4, text as text4, jsonb as jsonb3, integer as integer3, timestamp as timestamp4, index as index4, unique as unique3 } from "drizzle-orm/pg-core";
|
|
114
|
+
import { createFunctionSchema as createFunctionSchema4 } from "@spfn/core/db";
|
|
115
|
+
var schema4 = createFunctionSchema4("@spfn/cms");
|
|
116
|
+
var cmsPublishedCache = schema4.table("published_cache", {
|
|
149
117
|
// Primary Key
|
|
150
|
-
id:
|
|
118
|
+
id: serial4("id").primaryKey(),
|
|
151
119
|
// 섹션 (페이지 단위)
|
|
152
|
-
section:
|
|
120
|
+
section: text4("section").notNull(),
|
|
153
121
|
// "home" | "why-futureplay" | "team" | "our-companies" | "apply"
|
|
154
122
|
// 언어
|
|
155
|
-
locale:
|
|
123
|
+
locale: text4("locale").notNull(),
|
|
156
124
|
// "ko" | "en" | "ja"
|
|
157
125
|
// 캐시된 콘텐츠 (JSONB)
|
|
158
126
|
content: jsonb3("content").notNull(),
|
|
@@ -163,32 +131,32 @@ var cmsPublishedCache = schema5.table("published_cache", {
|
|
|
163
131
|
// ...
|
|
164
132
|
// }
|
|
165
133
|
// 발행 정보
|
|
166
|
-
publishedAt:
|
|
167
|
-
publishedBy:
|
|
134
|
+
publishedAt: timestamp4("published_at", { withTimezone: true }).notNull(),
|
|
135
|
+
publishedBy: text4("published_by"),
|
|
168
136
|
// 캐시 버전 (클라이언트 캐싱용)
|
|
169
|
-
version:
|
|
137
|
+
version: integer3("version").notNull().default(1)
|
|
170
138
|
}, (table) => [
|
|
171
139
|
// UNIQUE 제약: section + locale 조합은 유일
|
|
172
|
-
|
|
140
|
+
unique3("cms_published_cache_unique").on(table.section, table.locale),
|
|
173
141
|
// 인덱스: section으로 조회 최적화
|
|
174
|
-
|
|
142
|
+
index4("cms_published_cache_section_idx").on(table.section)
|
|
175
143
|
]);
|
|
176
144
|
|
|
177
145
|
// src/server/entities/cms-audit-logs.ts
|
|
178
|
-
import { serial as
|
|
179
|
-
import { createFunctionSchema as
|
|
180
|
-
var
|
|
181
|
-
var cmsAuditLogs =
|
|
146
|
+
import { serial as serial5, integer as integer4, text as text5, jsonb as jsonb4, timestamp as timestamp5, index as index5 } from "drizzle-orm/pg-core";
|
|
147
|
+
import { createFunctionSchema as createFunctionSchema5 } from "@spfn/core/db";
|
|
148
|
+
var schema5 = createFunctionSchema5("@spfn/cms");
|
|
149
|
+
var cmsAuditLogs = schema5.table("audit_logs", {
|
|
182
150
|
// Primary Key
|
|
183
|
-
id:
|
|
151
|
+
id: serial5("id").primaryKey(),
|
|
184
152
|
// Foreign Key: cms_labels (nullable - 라벨 삭제 시 로그는 유지)
|
|
185
|
-
labelId:
|
|
153
|
+
labelId: integer4("label_id").references(() => cmsLabels.id, { onDelete: "set null" }),
|
|
186
154
|
// 작업 유형
|
|
187
|
-
action:
|
|
155
|
+
action: text5("action").notNull(),
|
|
188
156
|
// "create" | "update" | "publish" | "unpublish" | "archive" | "delete" | "rollback" | "duplicate"
|
|
189
157
|
// 사용자 정보
|
|
190
|
-
userId:
|
|
191
|
-
userName:
|
|
158
|
+
userId: text5("user_id").notNull(),
|
|
159
|
+
userName: text5("user_name"),
|
|
192
160
|
// 변경 내용 (before/after)
|
|
193
161
|
changes: jsonb4("changes"),
|
|
194
162
|
// { before: {...}, after: {...} }
|
|
@@ -196,22 +164,21 @@ var cmsAuditLogs = schema6.table("audit_logs", {
|
|
|
196
164
|
metadata: jsonb4("metadata"),
|
|
197
165
|
// { version: number, ip: string, userAgent: string, ... }
|
|
198
166
|
// 작업 시각
|
|
199
|
-
createdAt:
|
|
167
|
+
createdAt: timestamp5("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
200
168
|
}, (table) => [
|
|
201
169
|
// 인덱스: labelId로 이력 조회 최적화
|
|
202
|
-
|
|
170
|
+
index5("cms_audit_logs_label_id_idx").on(table.labelId),
|
|
203
171
|
// 인덱스: userId로 사용자 활동 조회 최적화
|
|
204
|
-
|
|
172
|
+
index5("cms_audit_logs_user_id_idx").on(table.userId),
|
|
205
173
|
// 인덱스: action 필터링 최적화
|
|
206
|
-
|
|
174
|
+
index5("cms_audit_logs_action_idx").on(table.action),
|
|
207
175
|
// 인덱스: 시간순 조회 최적화
|
|
208
|
-
|
|
176
|
+
index5("cms_audit_logs_created_at_idx").on(table.createdAt)
|
|
209
177
|
]);
|
|
210
178
|
export {
|
|
211
179
|
cmsAuditLogs,
|
|
212
180
|
cmsDraftCache,
|
|
213
181
|
cmsLabelValues,
|
|
214
|
-
cmsLabelVersions,
|
|
215
182
|
cmsLabels,
|
|
216
183
|
cmsPublishedCache
|
|
217
184
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/server/entities/cms-labels.ts","../../../src/server/entities/cms-label-values.ts","../../../src/server/entities/cms-label-versions.ts","../../../src/server/entities/cms-draft-cache.ts","../../../src/server/entities/cms-published-cache.ts","../../../src/server/entities/cms-audit-logs.ts"],"sourcesContent":["/**\n * CMS Labels Entity\n *\n * 라벨의 메타데이터와 현재 발행 상태를 관리합니다.\n * - 라벨 식별 (id, key)\n * - 섹션 분류 (section)\n * - 타입 정의 (type)\n * - 발행 상태 (publishedVersion)\n */\n\nimport { index, integer, serial, text, timestamp } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsLabels = schema.table('labels', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // 라벨 식별자\n key: text('key').notNull().unique(),\n // 예: \"home.hero.title\", \"why-futureplay.hero.subtitle\"\n // 구조: {section}.{component}.{property}\n\n // 섹션 분류 (페이지 단위)\n section: text('section').notNull(),\n // 예: \"home\", \"why-futureplay\", \"team\"\n\n // 값 타입\n type: text('type').notNull(),\n // \"text\" | \"image\" | \"video\" | \"file\" | \"object\"\n\n // 기본값\n defaultValue: text('default_value'),\n // 라벨의 기본값 (sync 시 설정)\n\n // 설명\n description: text('description'),\n // 라벨에 대한 설명 (optional)\n\n // 현재 발행된 버전 번호\n publishedVersion: integer('published_version'),\n // null = 미발행 상태\n // 1, 2, 3... = 발행된 버전 번호\n\n // 생성자 추적\n createdBy: text('created_by'),\n\n // 타임스탬프\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // 인덱스: 섹션별 조회 최적화\n index('cms_labels_section_idx').on(table.section),\n\n // 인덱스: key로 조회 최적화 (unique 제약으로 자동 생성되지만 명시)\n index('cms_labels_key_idx').on(table.key),\n]);\n\n// 타입 추론\nexport type CmsLabel = typeof cmsLabels.$inferSelect;\nexport type NewCmsLabel = typeof cmsLabels.$inferInsert;","/**\n * CMS Label Values Entity\n *\n * 라벨의 실제 값을 저장합니다.\n * - 다국어 지원 (locale)\n * - 반응형 지원 (breakpoint)\n * - 버전 관리 (version)\n * - JSONB로 유연한 값 저장\n */\n\nimport { serial, integer, text, jsonb, timestamp, index, unique } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\nimport { cmsLabels } from '@/server/entities/cms-labels';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsLabelValues = schema.table('label_values', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // Foreign Key: cms_labels\n labelId: integer('label_id')\n .notNull()\n .references(() => cmsLabels.id, { onDelete: 'cascade' }),\n\n // 버전 번호 (null = draft, number = published version)\n version: integer('version'),\n\n // 언어 코드\n locale: text('locale').notNull().default('ko'),\n // \"ko\" | \"en\" | \"ja\"\n\n // 반응형 브레이크포인트\n breakpoint: text('breakpoint'),\n // null = 기본값 (모든 화면 크기)\n // \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n\n // 실제 값 (JSONB)\n value: jsonb('value').notNull(),\n // LabelValue 타입:\n // - TextValue: { type: \"text\", content: string }\n // - ImageValue: { type: \"image\", url: string, alt?: string, width?: number, height?: number }\n // - VideoValue: { type: \"video\", url: string, thumbnail?: string, duration?: number }\n // - FileValue: { type: \"file\", url: string, filename: string, size?: number }\n // - ObjectValue: { type: \"object\", fields: Record<string, LabelValue> }\n\n // 생성 시각\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // UNIQUE 제약: 같은 버전에서 locale + breakpoint 조합은 유일\n unique('cms_label_values_locale_breakpoint_unique')\n .on(table.labelId, table.version, table.locale, table.breakpoint),\n\n // 인덱스: labelId + version 복합 조회 최적화\n index('cms_label_values_label_version_idx')\n .on(table.labelId, table.version),\n\n // 인덱스: locale 필터링 최적화\n index('cms_label_values_locale_idx').on(table.locale),\n]);\n\n// 타입 추론\nexport type CmsLabelValue = typeof cmsLabelValues.$inferSelect;\nexport type NewCmsLabelValue = typeof cmsLabelValues.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 텍스트 값 저장\n * await db.insert(cmsLabelValues).values({\n * labelId: 1,\n * version: 1,\n * locale: 'ko',\n * breakpoint: null,\n * value: {\n * type: 'text',\n * content: '미래를 만드는 기업'\n * }\n * });\n *\n * // 반응형 이미지 저장 (모바일용)\n * await db.insert(cmsLabelValues).values({\n * labelId: 2,\n * version: 1,\n * locale: 'ko',\n * breakpoint: 'sm',\n * value: {\n * type: 'image',\n * url: '/uploads/hero-mobile.jpg',\n * alt: 'Hero Image',\n * width: 640,\n * height: 480\n * }\n * });\n *\n * // 특정 버전의 한국어 값 조회\n * const values = await db.select()\n * .from(cmsLabelValues)\n * .where(and(\n * eq(cmsLabelValues.labelId, 1),\n * eq(cmsLabelValues.version, 2),\n * eq(cmsLabelValues.locale, 'ko')\n * ));\n *\n * // Object 타입 값 저장 (재귀 구조)\n * await db.insert(cmsLabelValues).values({\n * labelId: 3,\n * version: 1,\n * locale: 'ko',\n * value: {\n * type: 'object',\n * fields: {\n * title: { type: 'text', content: '특징 1' },\n * icon: { type: 'image', url: '/icons/feature1.svg', alt: 'Icon' },\n * description: { type: 'text', content: '상세 설명...' }\n * }\n * }\n * });\n */","/**\n * CMS Label Versions Entity\n *\n * 라벨의 버전 메타데이터를 관리합니다.\n * - 버전별 상태 (draft/published/archived)\n * - 발행 정보 (publishedAt, publishedBy)\n * - 버전 노트\n */\n\nimport { serial, integer, text, timestamp, index, unique } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\nimport { cmsLabels } from '@/server/entities/cms-labels';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsLabelVersions = schema.table('label_versions', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // Foreign Key: cms_labels\n labelId: integer('label_id')\n .notNull()\n .references(() => cmsLabels.id, { onDelete: 'cascade' }),\n\n // 버전 번호\n version: integer('version').notNull(),\n\n // 버전 상태\n status: text('status').notNull(),\n // \"draft\" | \"published\" | \"archived\"\n\n // 발행 정보\n publishedAt: timestamp('published_at', { withTimezone: true }),\n publishedBy: text('published_by'),\n\n // 버전 노트 (변경사항 설명)\n notes: text('notes'),\n\n // 버전 생성자\n createdBy: text('created_by'),\n\n // 생성 시각\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // UNIQUE 제약: 각 라벨의 버전 번호는 고유\n unique('cms_label_versions_label_version_unique')\n .on(table.labelId, table.version),\n\n // 인덱스: labelId로 버전 목록 조회 최적화\n index('cms_label_versions_label_id_idx').on(table.labelId),\n\n // 인덱스: status 필터링 최적화\n index('cms_label_versions_status_idx').on(table.status),\n]);\n\n// 타입 추론\nexport type CmsLabelVersion = typeof cmsLabelVersions.$inferSelect;\nexport type NewCmsLabelVersion = typeof cmsLabelVersions.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 새 버전 생성 (draft)\n * await db.insert(cmsLabelVersions).values({\n * labelId: 1,\n * version: 2,\n * status: 'draft',\n * notes: '문구 개선',\n * createdBy: 'editor@futureplay.com'\n * });\n *\n * // 버전 발행\n * await db.update(cmsLabelVersions)\n * .set({\n * status: 'published',\n * publishedAt: new Date(),\n * publishedBy: 'admin@futureplay.com'\n * })\n * .where(and(\n * eq(cmsLabelVersions.labelId, 1),\n * eq(cmsLabelVersions.version, 2)\n * ));\n *\n * // 라벨의 모든 버전 조회\n * const versions = await db.select()\n * .from(cmsLabelVersions)\n * .where(eq(cmsLabelVersions.labelId, 1))\n * .orderBy(desc(cmsLabelVersions.version));\n *\n * // 발행된 버전만 조회\n * const published = await db.select()\n * .from(cmsLabelVersions)\n * .where(eq(cmsLabelVersions.status, 'published'))\n * .orderBy(desc(cmsLabelVersions.publishedAt));\n */","/**\n * CMS Draft Cache Entity\n *\n * 관리자별 Draft 콘텐츠를 캐싱합니다.\n * - 사용자별 격리 (userId)\n * - 실시간 미리보기 지원\n * - 동시 편집 가능\n *\n * 핵심 기능:\n * - 여러 관리자가 같은 섹션을 동시에 편집\n * - 각자의 변경사항은 자신의 미리보기에만 표시\n * - 충돌 없이 안전하게 작업\n */\n\nimport { serial, text, jsonb, timestamp, index, unique } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsDraftCache = schema.table('draft_cache', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // 섹션 (페이지 단위)\n section: text('section').notNull(),\n // \"home\" | \"why-futureplay\" | \"team\" | \"our-companies\" | \"apply\"\n\n // 언어\n locale: text('locale').notNull(),\n // \"ko\" | \"en\" | \"ja\"\n\n // 사용자 ID (핵심 필드!)\n userId: text('user_id').notNull(),\n // 각 관리자의 독립적인 작업 공간\n\n // Draft 콘텐츠 (JSONB)\n content: jsonb('content').notNull(),\n // Record<string, LabelValue>\n // {\n // \"home.hero.title\": { type: \"text\", content: \"수정 중...\" },\n // \"home.hero.subtitle\": { type: \"text\", content: \"새로운 문구\" },\n // ...\n // }\n\n // 최종 수정 시각\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // UNIQUE 제약: section + locale + userId 조합은 유일\n unique('cms_draft_cache_unique')\n .on(table.section, table.locale, table.userId),\n\n // 인덱스: section으로 조회 최적화\n index('cms_draft_cache_section_idx').on(table.section),\n\n // 인덱스: userId로 사용자의 모든 draft 조회 최적화\n index('cms_draft_cache_user_idx').on(table.userId),\n]);\n\n// 타입 추론\nexport type CmsDraftCache = typeof cmsDraftCache.$inferSelect;\nexport type NewCmsDraftCache = typeof cmsDraftCache.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // Draft 초기화 (편집 시작)\n * await db.insert(cmsDraftCache)\n * .values({\n * section: 'home',\n * locale: 'ko',\n * userId: 'user-a@futureplay.com',\n * content: publishedContent // 발행 버전 복사\n * });\n *\n * // Draft 업데이트 (값 수정 시)\n * const cache = await db.select()\n * .from(cmsDraftCache)\n * .where(and(\n * eq(cmsDraftCache.section, 'home'),\n * eq(cmsDraftCache.locale, 'ko'),\n * eq(cmsDraftCache.userId, userId)\n * ))\n * .limit(1);\n *\n * const updatedContent = {\n * ...cache[0].content,\n * 'home.hero.title': newValue // 부분 업데이트\n * };\n *\n * await db.update(cmsDraftCache)\n * .set({ content: updatedContent, updatedAt: new Date() })\n * .where(eq(cmsDraftCache.id, cache[0].id));\n *\n * // Draft 조회 (미리보기)\n * const draft = await db.select()\n * .from(cmsDraftCache)\n * .where(and(\n * eq(cmsDraftCache.section, 'home'),\n * eq(cmsDraftCache.locale, 'ko'),\n * eq(cmsDraftCache.userId, session.user.id)\n * ))\n * .limit(1);\n *\n * // 사용자의 모든 작업 중인 섹션 조회\n * const userDrafts = await db.select()\n * .from(cmsDraftCache)\n * .where(eq(cmsDraftCache.userId, userId))\n * .orderBy(desc(cmsDraftCache.updatedAt));\n *\n * // 오래된 Draft 정리 (30일 이상)\n * const stale = await db.delete(cmsDraftCache)\n * .where(lt(\n * cmsDraftCache.updatedAt,\n * new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)\n * ))\n * .returning();\n *\n * // Draft 폐기 (변경사항 버리기)\n * await db.delete(cmsDraftCache)\n * .where(and(\n * eq(cmsDraftCache.section, 'home'),\n * eq(cmsDraftCache.locale, 'ko'),\n * eq(cmsDraftCache.userId, userId)\n * ));\n */","/**\n * CMS Published Cache Entity\n *\n * 발행된 콘텐츠를 섹션+언어 단위로 캐싱합니다.\n * - 초고속 읽기 성능 (5ms)\n * - 단일 쿼리로 섹션 전체 로드\n * - JSONB로 즉시 사용 가능한 데이터\n *\n * 성능 비교:\n * - 정규화 테이블 JOIN: 87ms\n * - 캐시 테이블: 5ms (17배 빠름!)\n */\n\nimport { serial, text, jsonb, integer, timestamp, index, unique } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsPublishedCache = schema.table('published_cache', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // 섹션 (페이지 단위)\n section: text('section').notNull(),\n // \"home\" | \"why-futureplay\" | \"team\" | \"our-companies\" | \"apply\"\n\n // 언어\n locale: text('locale').notNull(),\n // \"ko\" | \"en\" | \"ja\"\n\n // 캐시된 콘텐츠 (JSONB)\n content: jsonb('content').notNull(),\n // Record<string, LabelValue>\n // {\n // \"home.hero.title\": { type: \"text\", content: \"...\" },\n // \"home.hero.image\": { type: \"image\", url: \"...\", alt: \"...\" },\n // ...\n // }\n\n // 발행 정보\n publishedAt: timestamp('published_at', { withTimezone: true }).notNull(),\n publishedBy: text('published_by'),\n\n // 캐시 버전 (클라이언트 캐싱용)\n version: integer('version').notNull().default(1),\n}, (table) => [\n // UNIQUE 제약: section + locale 조합은 유일\n unique('cms_published_cache_unique').on(table.section, table.locale),\n\n // 인덱스: section으로 조회 최적화\n index('cms_published_cache_section_idx').on(table.section),\n]);\n\n// 타입 추론\nexport type CmsPublishedCache = typeof cmsPublishedCache.$inferSelect;\nexport type NewCmsPublishedCache = typeof cmsPublishedCache.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 캐시 생성/업데이트 (UPSERT)\n * await db.insert(cmsPublishedCache)\n * .values({\n * section: 'home',\n * locale: 'ko',\n * content: {\n * 'home.hero.title': {\n * type: 'text',\n * content: '미래를 만드는 기업'\n * },\n * 'home.hero.image': {\n * type: 'image',\n * url: '/uploads/hero.jpg',\n * alt: 'Hero',\n * width: 1920,\n * height: 1080\n * }\n * },\n * publishedAt: new Date(),\n * publishedBy: 'admin@futureplay.com'\n * })\n * .onConflictDoUpdate({\n * target: [cmsPublishedCache.section, cmsPublishedCache.locale],\n * set: {\n * content: sql`EXCLUDED.content`,\n * publishedAt: sql`EXCLUDED.published_at`,\n * publishedBy: sql`EXCLUDED.published_by`,\n * version: sql`${cmsPublishedCache.version} + 1`\n * }\n * });\n *\n * // 캐시 조회 (초고속!)\n * const cache = await db.select()\n * .from(cmsPublishedCache)\n * .where(and(\n * eq(cmsPublishedCache.section, 'home'),\n * eq(cmsPublishedCache.locale, 'ko')\n * ))\n * .limit(1);\n *\n * const labels = cache[0].content; // 즉시 사용 가능!\n *\n * // 섹션의 모든 언어 캐시 조회\n * const allLocales = await db.select()\n * .from(cmsPublishedCache)\n * .where(eq(cmsPublishedCache.section, 'home'));\n *\n * // 오래된 캐시 감지\n * const stale = await db.select()\n * .from(cmsPublishedCache)\n * .where(lt(\n * cmsPublishedCache.publishedAt,\n * new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)\n * ));\n */","/**\n * CMS Audit Logs Entity\n *\n * CMS의 모든 변경사항을 추적합니다.\n * - 누가 (userId, userName)\n * - 언제 (createdAt)\n * - 무엇을 (action, changes)\n * - 왜 (metadata)\n */\n\nimport { serial, integer, text, jsonb, timestamp, index } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\nimport { cmsLabels } from '@/server/entities/cms-labels';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsAuditLogs = schema.table('audit_logs', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // Foreign Key: cms_labels (nullable - 라벨 삭제 시 로그는 유지)\n labelId: integer('label_id')\n .references(() => cmsLabels.id, { onDelete: 'set null' }),\n\n // 작업 유형\n action: text('action').notNull(),\n // \"create\" | \"update\" | \"publish\" | \"unpublish\" | \"archive\" | \"delete\" | \"rollback\" | \"duplicate\"\n\n // 사용자 정보\n userId: text('user_id').notNull(),\n userName: text('user_name'),\n\n // 변경 내용 (before/after)\n changes: jsonb('changes'),\n // { before: {...}, after: {...} }\n\n // 추가 메타데이터\n metadata: jsonb('metadata'),\n // { version: number, ip: string, userAgent: string, ... }\n\n // 작업 시각\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // 인덱스: labelId로 이력 조회 최적화\n index('cms_audit_logs_label_id_idx').on(table.labelId),\n\n // 인덱스: userId로 사용자 활동 조회 최적화\n index('cms_audit_logs_user_id_idx').on(table.userId),\n\n // 인덱스: action 필터링 최적화\n index('cms_audit_logs_action_idx').on(table.action),\n\n // 인덱스: 시간순 조회 최적화\n index('cms_audit_logs_created_at_idx').on(table.createdAt),\n]);\n\n// 타입 추론\nexport type CmsAuditLog = typeof cmsAuditLogs.$inferSelect;\nexport type NewCmsAuditLog = typeof cmsAuditLogs.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 라벨 생성 로그\n * await db.insert(cmsAuditLogs).values({\n * labelId: 1,\n * action: 'create',\n * userId: 'user123',\n * userName: '김철수',\n * changes: {\n * before: null,\n * after: {\n * key: 'home.hero.title',\n * section: 'home',\n * type: 'text'\n * }\n * },\n * metadata: {\n * ip: '192.168.1.1',\n * userAgent: 'Mozilla/5.0...'\n * }\n * });\n *\n * // 발행 로그\n * await db.insert(cmsAuditLogs).values({\n * labelId: 1,\n * action: 'publish',\n * userId: 'admin123',\n * userName: '관리자',\n * changes: {\n * before: { status: 'draft', publishedVersion: null },\n * after: { status: 'published', publishedVersion: 2 }\n * },\n * metadata: {\n * version: 2,\n * notes: '신규 브랜딩 적용'\n * }\n * });\n *\n * // 라벨별 이력 조회\n * const logs = await db.select()\n * .from(cmsAuditLogs)\n * .where(eq(cmsAuditLogs.labelId, 1))\n * .orderBy(desc(cmsAuditLogs.createdAt))\n * .limit(20);\n *\n * // 사용자별 활동 조회\n * const userActivity = await db.select()\n * .from(cmsAuditLogs)\n * .where(eq(cmsAuditLogs.userId, 'user123'))\n * .orderBy(desc(cmsAuditLogs.createdAt));\n *\n * // 최근 24시간 변경 이력\n * const recent = await db.select()\n * .from(cmsAuditLogs)\n * .where(gte(cmsAuditLogs.createdAt, new Date(Date.now() - 24 * 60 * 60 * 1000)))\n * .orderBy(desc(cmsAuditLogs.createdAt));\n */"],"mappings":";AAUA,SAAS,OAAO,SAAS,QAAQ,MAAM,iBAAiB;AACxD,SAAS,4BAA4B;AAGrC,IAAM,SAAS,qBAAqB,WAAW;AAExC,IAAM,YAAY,OAAO,MAAM,UAAU;AAAA;AAAA,EAE5C,IAAI,OAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,KAAK,KAAK,KAAK,EAAE,QAAQ,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA,EAKlC,SAAS,KAAK,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA,EAIjC,MAAM,KAAK,MAAM,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI3B,cAAc,KAAK,eAAe;AAAA;AAAA;AAAA,EAIlC,aAAa,KAAK,aAAa;AAAA;AAAA;AAAA,EAI/B,kBAAkB,QAAQ,mBAAmB;AAAA;AAAA;AAAA;AAAA,EAK7C,WAAW,KAAK,YAAY;AAAA;AAAA,EAG5B,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AAAA,EAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEV,MAAM,wBAAwB,EAAE,GAAG,MAAM,OAAO;AAAA;AAAA,EAGhD,MAAM,oBAAoB,EAAE,GAAG,MAAM,GAAG;AAC5C,CAAC;;;AChDD,SAAS,UAAAA,SAAQ,WAAAC,UAAS,QAAAC,OAAM,OAAO,aAAAC,YAAW,SAAAC,QAAO,cAAc;AACvE,SAAS,wBAAAC,6BAA4B;AAIrC,IAAMC,UAASC,sBAAqB,WAAW;AAExC,IAAM,iBAAiBD,QAAO,MAAM,gBAAgB;AAAA;AAAA,EAEvD,IAAIE,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,SAAQ,UAAU,EACtB,QAAQ,EACR,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA;AAAA,EAG3D,SAASA,SAAQ,SAAS;AAAA;AAAA,EAG1B,QAAQC,MAAK,QAAQ,EAAE,QAAQ,EAAE,QAAQ,IAAI;AAAA;AAAA;AAAA,EAI7C,YAAYA,MAAK,YAAY;AAAA;AAAA;AAAA;AAAA,EAK7B,OAAO,MAAM,OAAO,EAAE,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS9B,WAAWC,WAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEV,OAAO,2CAA2C,EAC7C,GAAG,MAAM,SAAS,MAAM,SAAS,MAAM,QAAQ,MAAM,UAAU;AAAA;AAAA,EAGpEC,OAAM,oCAAoC,EACrC,GAAG,MAAM,SAAS,MAAM,OAAO;AAAA;AAAA,EAGpCA,OAAM,6BAA6B,EAAE,GAAG,MAAM,MAAM;AACxD,CAAC;;;ACnDD,SAAS,UAAAC,SAAQ,WAAAC,UAAS,QAAAC,OAAM,aAAAC,YAAW,SAAAC,QAAO,UAAAC,eAAc;AAChE,SAAS,wBAAAC,6BAA4B;AAIrC,IAAMC,UAASC,sBAAqB,WAAW;AAExC,IAAM,mBAAmBD,QAAO,MAAM,kBAAkB;AAAA;AAAA,EAE3D,IAAIE,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,SAAQ,UAAU,EAC1B,QAAQ,EACR,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA;AAAA,EAGvD,SAASA,SAAQ,SAAS,EAAE,QAAQ;AAAA;AAAA,EAGpC,QAAQC,MAAK,QAAQ,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI/B,aAAaC,WAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC;AAAA,EAC7D,aAAaD,MAAK,cAAc;AAAA;AAAA,EAGhC,OAAOA,MAAK,OAAO;AAAA;AAAA,EAGnB,WAAWA,MAAK,YAAY;AAAA;AAAA,EAG5B,WAAWC,WAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEVC,QAAO,yCAAyC,EAC/C,GAAG,MAAM,SAAS,MAAM,OAAO;AAAA;AAAA,EAGhCC,OAAM,iCAAiC,EAAE,GAAG,MAAM,OAAO;AAAA;AAAA,EAGzDA,OAAM,+BAA+B,EAAE,GAAG,MAAM,MAAM;AAC1D,CAAC;;;ACxCD,SAAS,UAAAC,SAAQ,QAAAC,OAAM,SAAAC,QAAO,aAAAC,YAAW,SAAAC,QAAO,UAAAC,eAAc;AAC9D,SAAS,wBAAAC,6BAA4B;AAGrC,IAAMC,UAASD,sBAAqB,WAAW;AAExC,IAAM,gBAAgBC,QAAO,MAAM,eAAe;AAAA;AAAA,EAErD,IAAIP,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,MAAK,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA,EAIjC,QAAQA,MAAK,QAAQ,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI/B,QAAQA,MAAK,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA,EAIhC,SAASC,OAAM,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASlC,WAAWC,WAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEVE,QAAO,wBAAwB,EAC9B,GAAG,MAAM,SAAS,MAAM,QAAQ,MAAM,MAAM;AAAA;AAAA,EAG7CD,OAAM,6BAA6B,EAAE,GAAG,MAAM,OAAO;AAAA;AAAA,EAGrDA,OAAM,0BAA0B,EAAE,GAAG,MAAM,MAAM;AACrD,CAAC;;;AC5CD,SAAS,UAAAI,SAAQ,QAAAC,OAAM,SAAAC,QAAO,WAAAC,UAAS,aAAAC,YAAW,SAAAC,QAAO,UAAAC,eAAc;AACvE,SAAS,wBAAAC,6BAA4B;AAGrC,IAAMC,UAASD,sBAAqB,WAAW;AAExC,IAAM,oBAAoBC,QAAO,MAAM,mBAAmB;AAAA;AAAA,EAE7D,IAAIR,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,MAAK,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA,EAIjC,QAAQA,MAAK,QAAQ,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI/B,SAASC,OAAM,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASlC,aAAaE,WAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ;AAAA,EACvE,aAAaH,MAAK,cAAc;AAAA;AAAA,EAGhC,SAASE,SAAQ,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC;AACnD,GAAG,CAAC,UAAU;AAAA;AAAA,EAEVG,QAAO,4BAA4B,EAAE,GAAG,MAAM,SAAS,MAAM,MAAM;AAAA;AAAA,EAGnED,OAAM,iCAAiC,EAAE,GAAG,MAAM,OAAO;AAC7D,CAAC;;;AC1CD,SAAS,UAAAI,SAAQ,WAAAC,UAAS,QAAAC,OAAM,SAAAC,QAAO,aAAAC,YAAW,SAAAC,cAAa;AAC/D,SAAS,wBAAAC,6BAA4B;AAIrC,IAAMC,UAASC,sBAAqB,WAAW;AAExC,IAAM,eAAeD,QAAO,MAAM,cAAc;AAAA;AAAA,EAEnD,IAAIE,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,SAAQ,UAAU,EAC1B,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,WAAW,CAAC;AAAA;AAAA,EAGxD,QAAQC,MAAK,QAAQ,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI/B,QAAQA,MAAK,SAAS,EAAE,QAAQ;AAAA,EAChC,UAAUA,MAAK,WAAW;AAAA;AAAA,EAG1B,SAASC,OAAM,SAAS;AAAA;AAAA;AAAA,EAIxB,UAAUA,OAAM,UAAU;AAAA;AAAA;AAAA,EAI1B,WAAWC,WAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEVC,OAAM,6BAA6B,EAAE,GAAG,MAAM,OAAO;AAAA;AAAA,EAGrDA,OAAM,4BAA4B,EAAE,GAAG,MAAM,MAAM;AAAA;AAAA,EAGnDA,OAAM,2BAA2B,EAAE,GAAG,MAAM,MAAM;AAAA;AAAA,EAGlDA,OAAM,+BAA+B,EAAE,GAAG,MAAM,SAAS;AAC7D,CAAC;","names":["serial","integer","text","timestamp","index","createFunctionSchema","schema","createFunctionSchema","serial","integer","text","timestamp","index","serial","integer","text","timestamp","index","unique","createFunctionSchema","schema","createFunctionSchema","serial","integer","text","timestamp","unique","index","serial","text","jsonb","timestamp","index","unique","createFunctionSchema","schema","serial","text","jsonb","integer","timestamp","index","unique","createFunctionSchema","schema","serial","integer","text","jsonb","timestamp","index","createFunctionSchema","schema","createFunctionSchema","serial","integer","text","jsonb","timestamp","index"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/server/entities/cms-labels.ts","../../../src/server/entities/cms-label-values.ts","../../../src/server/entities/cms-draft-cache.ts","../../../src/server/entities/cms-published-cache.ts","../../../src/server/entities/cms-audit-logs.ts"],"sourcesContent":["/**\n * CMS Labels Entity\n *\n * 라벨의 메타데이터와 현재 발행 상태를 관리합니다.\n * - 라벨 식별 (id, key)\n * - 섹션 분류 (section)\n * - 타입 정의 (type)\n * - 발행 상태 (publishedVersion)\n */\n\nimport { index, integer, serial, text, timestamp } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsLabels = schema.table('labels', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // 라벨 식별자\n key: text('key').notNull().unique(),\n // 예: \"home.hero.title\", \"why-futureplay.hero.subtitle\"\n // 구조: {section}.{component}.{property}\n\n // 섹션 분류 (페이지 단위)\n section: text('section').notNull(),\n // 예: \"home\", \"why-futureplay\", \"team\"\n\n // 값 타입\n type: text('type').notNull(),\n // \"text\" | \"image\" | \"video\" | \"file\" | \"object\"\n\n // 기본값\n defaultValue: text('default_value'),\n // 라벨의 기본값 (sync 시 설정)\n\n // 설명\n description: text('description'),\n // 라벨에 대한 설명 (optional)\n\n // 현재 발행된 버전 번호\n publishedVersion: integer('published_version'),\n // null = 미발행 상태\n // 1, 2, 3... = 발행된 버전 번호\n\n // 생성자 추적\n createdBy: text('created_by'),\n\n // 타임스탬프\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // 인덱스: 섹션별 조회 최적화\n index('cms_labels_section_idx').on(table.section),\n\n // 인덱스: key로 조회 최적화 (unique 제약으로 자동 생성되지만 명시)\n index('cms_labels_key_idx').on(table.key),\n]);\n\n// 타입 추론\nexport type CmsLabel = typeof cmsLabels.$inferSelect;\nexport type NewCmsLabel = typeof cmsLabels.$inferInsert;","/**\n * CMS Label Values Entity\n *\n * 라벨의 실제 값을 저장합니다.\n * - 다국어 지원 (locale)\n * - 반응형 지원 (breakpoint)\n * - 버전 관리 (version)\n * - JSONB로 유연한 값 저장\n */\n\nimport { serial, integer, text, jsonb, timestamp, index, unique } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\nimport { cmsLabels } from '@/server/entities/cms-labels';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsLabelValues = schema.table('label_values', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // Foreign Key: cms_labels\n labelId: integer('label_id')\n .notNull()\n .references(() => cmsLabels.id, { onDelete: 'cascade' }),\n\n // 버전 번호 (null = draft, number = published version)\n version: integer('version'),\n\n // 언어 코드\n locale: text('locale').notNull().default('ko'),\n // \"ko\" | \"en\" | \"ja\"\n\n // 반응형 브레이크포인트\n breakpoint: text('breakpoint'),\n // null = 기본값 (모든 화면 크기)\n // \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n\n // 실제 값 (JSONB)\n value: jsonb('value').notNull(),\n // LabelValue 타입:\n // - TextValue: { type: \"text\", content: string }\n // - ImageValue: { type: \"image\", url: string, alt?: string, width?: number, height?: number }\n // - VideoValue: { type: \"video\", url: string, thumbnail?: string, duration?: number }\n // - FileValue: { type: \"file\", url: string, filename: string, size?: number }\n // - ObjectValue: { type: \"object\", fields: Record<string, LabelValue> }\n\n // 생성 시각\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // UNIQUE 제약: 같은 버전에서 locale + breakpoint 조합은 유일\n unique('cms_label_values_locale_breakpoint_unique')\n .on(table.labelId, table.version, table.locale, table.breakpoint),\n\n // 인덱스: labelId + version 복합 조회 최적화\n index('cms_label_values_label_version_idx')\n .on(table.labelId, table.version),\n\n // 인덱스: locale 필터링 최적화\n index('cms_label_values_locale_idx').on(table.locale),\n]);\n\n// 타입 추론\nexport type CmsLabelValue = typeof cmsLabelValues.$inferSelect;\nexport type NewCmsLabelValue = typeof cmsLabelValues.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 텍스트 값 저장\n * await db.insert(cmsLabelValues).values({\n * labelId: 1,\n * version: 1,\n * locale: 'ko',\n * breakpoint: null,\n * value: {\n * type: 'text',\n * content: '미래를 만드는 기업'\n * }\n * });\n *\n * // 반응형 이미지 저장 (모바일용)\n * await db.insert(cmsLabelValues).values({\n * labelId: 2,\n * version: 1,\n * locale: 'ko',\n * breakpoint: 'sm',\n * value: {\n * type: 'image',\n * url: '/uploads/hero-mobile.jpg',\n * alt: 'Hero Image',\n * width: 640,\n * height: 480\n * }\n * });\n *\n * // 특정 버전의 한국어 값 조회\n * const values = await db.select()\n * .from(cmsLabelValues)\n * .where(and(\n * eq(cmsLabelValues.labelId, 1),\n * eq(cmsLabelValues.version, 2),\n * eq(cmsLabelValues.locale, 'ko')\n * ));\n *\n * // Object 타입 값 저장 (재귀 구조)\n * await db.insert(cmsLabelValues).values({\n * labelId: 3,\n * version: 1,\n * locale: 'ko',\n * value: {\n * type: 'object',\n * fields: {\n * title: { type: 'text', content: '특징 1' },\n * icon: { type: 'image', url: '/icons/feature1.svg', alt: 'Icon' },\n * description: { type: 'text', content: '상세 설명...' }\n * }\n * }\n * });\n */","/**\n * CMS Draft Cache Entity\n *\n * 관리자별 Draft 콘텐츠를 캐싱합니다.\n * - 사용자별 격리 (userId)\n * - 실시간 미리보기 지원\n * - 동시 편집 가능\n *\n * 핵심 기능:\n * - 여러 관리자가 같은 섹션을 동시에 편집\n * - 각자의 변경사항은 자신의 미리보기에만 표시\n * - 충돌 없이 안전하게 작업\n */\n\nimport { serial, text, jsonb, timestamp, index, unique } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsDraftCache = schema.table('draft_cache', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // 섹션 (페이지 단위)\n section: text('section').notNull(),\n // \"home\" | \"why-futureplay\" | \"team\" | \"our-companies\" | \"apply\"\n\n // 언어\n locale: text('locale').notNull(),\n // \"ko\" | \"en\" | \"ja\"\n\n // 사용자 ID (핵심 필드!)\n userId: text('user_id').notNull(),\n // 각 관리자의 독립적인 작업 공간\n\n // Draft 콘텐츠 (JSONB)\n content: jsonb('content').notNull(),\n // Record<string, LabelValue>\n // {\n // \"home.hero.title\": { type: \"text\", content: \"수정 중...\" },\n // \"home.hero.subtitle\": { type: \"text\", content: \"새로운 문구\" },\n // ...\n // }\n\n // 최종 수정 시각\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // UNIQUE 제약: section + locale + userId 조합은 유일\n unique('cms_draft_cache_unique')\n .on(table.section, table.locale, table.userId),\n\n // 인덱스: section으로 조회 최적화\n index('cms_draft_cache_section_idx').on(table.section),\n\n // 인덱스: userId로 사용자의 모든 draft 조회 최적화\n index('cms_draft_cache_user_idx').on(table.userId),\n]);\n\n// 타입 추론\nexport type CmsDraftCache = typeof cmsDraftCache.$inferSelect;\nexport type NewCmsDraftCache = typeof cmsDraftCache.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // Draft 초기화 (편집 시작)\n * await db.insert(cmsDraftCache)\n * .values({\n * section: 'home',\n * locale: 'ko',\n * userId: 'user-a@futureplay.com',\n * content: publishedContent // 발행 버전 복사\n * });\n *\n * // Draft 업데이트 (값 수정 시)\n * const cache = await db.select()\n * .from(cmsDraftCache)\n * .where(and(\n * eq(cmsDraftCache.section, 'home'),\n * eq(cmsDraftCache.locale, 'ko'),\n * eq(cmsDraftCache.userId, userId)\n * ))\n * .limit(1);\n *\n * const updatedContent = {\n * ...cache[0].content,\n * 'home.hero.title': newValue // 부분 업데이트\n * };\n *\n * await db.update(cmsDraftCache)\n * .set({ content: updatedContent, updatedAt: new Date() })\n * .where(eq(cmsDraftCache.id, cache[0].id));\n *\n * // Draft 조회 (미리보기)\n * const draft = await db.select()\n * .from(cmsDraftCache)\n * .where(and(\n * eq(cmsDraftCache.section, 'home'),\n * eq(cmsDraftCache.locale, 'ko'),\n * eq(cmsDraftCache.userId, session.user.id)\n * ))\n * .limit(1);\n *\n * // 사용자의 모든 작업 중인 섹션 조회\n * const userDrafts = await db.select()\n * .from(cmsDraftCache)\n * .where(eq(cmsDraftCache.userId, userId))\n * .orderBy(desc(cmsDraftCache.updatedAt));\n *\n * // 오래된 Draft 정리 (30일 이상)\n * const stale = await db.delete(cmsDraftCache)\n * .where(lt(\n * cmsDraftCache.updatedAt,\n * new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)\n * ))\n * .returning();\n *\n * // Draft 폐기 (변경사항 버리기)\n * await db.delete(cmsDraftCache)\n * .where(and(\n * eq(cmsDraftCache.section, 'home'),\n * eq(cmsDraftCache.locale, 'ko'),\n * eq(cmsDraftCache.userId, userId)\n * ));\n */","/**\n * CMS Published Cache Entity\n *\n * 발행된 콘텐츠를 섹션+언어 단위로 캐싱합니다.\n * - 초고속 읽기 성능 (5ms)\n * - 단일 쿼리로 섹션 전체 로드\n * - JSONB로 즉시 사용 가능한 데이터\n *\n * 성능 비교:\n * - 정규화 테이블 JOIN: 87ms\n * - 캐시 테이블: 5ms (17배 빠름!)\n */\n\nimport { serial, text, jsonb, integer, timestamp, index, unique } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsPublishedCache = schema.table('published_cache', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // 섹션 (페이지 단위)\n section: text('section').notNull(),\n // \"home\" | \"why-futureplay\" | \"team\" | \"our-companies\" | \"apply\"\n\n // 언어\n locale: text('locale').notNull(),\n // \"ko\" | \"en\" | \"ja\"\n\n // 캐시된 콘텐츠 (JSONB)\n content: jsonb('content').notNull(),\n // Record<string, LabelValue>\n // {\n // \"home.hero.title\": { type: \"text\", content: \"...\" },\n // \"home.hero.image\": { type: \"image\", url: \"...\", alt: \"...\" },\n // ...\n // }\n\n // 발행 정보\n publishedAt: timestamp('published_at', { withTimezone: true }).notNull(),\n publishedBy: text('published_by'),\n\n // 캐시 버전 (클라이언트 캐싱용)\n version: integer('version').notNull().default(1),\n}, (table) => [\n // UNIQUE 제약: section + locale 조합은 유일\n unique('cms_published_cache_unique').on(table.section, table.locale),\n\n // 인덱스: section으로 조회 최적화\n index('cms_published_cache_section_idx').on(table.section),\n]);\n\n// 타입 추론\nexport type CmsPublishedCache = typeof cmsPublishedCache.$inferSelect;\nexport type NewCmsPublishedCache = typeof cmsPublishedCache.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 캐시 생성/업데이트 (UPSERT)\n * await db.insert(cmsPublishedCache)\n * .values({\n * section: 'home',\n * locale: 'ko',\n * content: {\n * 'home.hero.title': {\n * type: 'text',\n * content: '미래를 만드는 기업'\n * },\n * 'home.hero.image': {\n * type: 'image',\n * url: '/uploads/hero.jpg',\n * alt: 'Hero',\n * width: 1920,\n * height: 1080\n * }\n * },\n * publishedAt: new Date(),\n * publishedBy: 'admin@futureplay.com'\n * })\n * .onConflictDoUpdate({\n * target: [cmsPublishedCache.section, cmsPublishedCache.locale],\n * set: {\n * content: sql`EXCLUDED.content`,\n * publishedAt: sql`EXCLUDED.published_at`,\n * publishedBy: sql`EXCLUDED.published_by`,\n * version: sql`${cmsPublishedCache.version} + 1`\n * }\n * });\n *\n * // 캐시 조회 (초고속!)\n * const cache = await db.select()\n * .from(cmsPublishedCache)\n * .where(and(\n * eq(cmsPublishedCache.section, 'home'),\n * eq(cmsPublishedCache.locale, 'ko')\n * ))\n * .limit(1);\n *\n * const labels = cache[0].content; // 즉시 사용 가능!\n *\n * // 섹션의 모든 언어 캐시 조회\n * const allLocales = await db.select()\n * .from(cmsPublishedCache)\n * .where(eq(cmsPublishedCache.section, 'home'));\n *\n * // 오래된 캐시 감지\n * const stale = await db.select()\n * .from(cmsPublishedCache)\n * .where(lt(\n * cmsPublishedCache.publishedAt,\n * new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)\n * ));\n */","/**\n * CMS Audit Logs Entity\n *\n * CMS의 모든 변경사항을 추적합니다.\n * - 누가 (userId, userName)\n * - 언제 (createdAt)\n * - 무엇을 (action, changes)\n * - 왜 (metadata)\n */\n\nimport { serial, integer, text, jsonb, timestamp, index } from 'drizzle-orm/pg-core';\nimport { createFunctionSchema } from '@spfn/core/db';\nimport { cmsLabels } from '@/server/entities/cms-labels';\n\n// Create isolated schema for @spfn/cms\nconst schema = createFunctionSchema('@spfn/cms');\n\nexport const cmsAuditLogs = schema.table('audit_logs', {\n // Primary Key\n id: serial('id').primaryKey(),\n\n // Foreign Key: cms_labels (nullable - 라벨 삭제 시 로그는 유지)\n labelId: integer('label_id')\n .references(() => cmsLabels.id, { onDelete: 'set null' }),\n\n // 작업 유형\n action: text('action').notNull(),\n // \"create\" | \"update\" | \"publish\" | \"unpublish\" | \"archive\" | \"delete\" | \"rollback\" | \"duplicate\"\n\n // 사용자 정보\n userId: text('user_id').notNull(),\n userName: text('user_name'),\n\n // 변경 내용 (before/after)\n changes: jsonb('changes'),\n // { before: {...}, after: {...} }\n\n // 추가 메타데이터\n metadata: jsonb('metadata'),\n // { version: number, ip: string, userAgent: string, ... }\n\n // 작업 시각\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}, (table) => [\n // 인덱스: labelId로 이력 조회 최적화\n index('cms_audit_logs_label_id_idx').on(table.labelId),\n\n // 인덱스: userId로 사용자 활동 조회 최적화\n index('cms_audit_logs_user_id_idx').on(table.userId),\n\n // 인덱스: action 필터링 최적화\n index('cms_audit_logs_action_idx').on(table.action),\n\n // 인덱스: 시간순 조회 최적화\n index('cms_audit_logs_created_at_idx').on(table.createdAt),\n]);\n\n// 타입 추론\nexport type CmsAuditLog = typeof cmsAuditLogs.$inferSelect;\nexport type NewCmsAuditLog = typeof cmsAuditLogs.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 라벨 생성 로그\n * await db.insert(cmsAuditLogs).values({\n * labelId: 1,\n * action: 'create',\n * userId: 'user123',\n * userName: '김철수',\n * changes: {\n * before: null,\n * after: {\n * key: 'home.hero.title',\n * section: 'home',\n * type: 'text'\n * }\n * },\n * metadata: {\n * ip: '192.168.1.1',\n * userAgent: 'Mozilla/5.0...'\n * }\n * });\n *\n * // 발행 로그\n * await db.insert(cmsAuditLogs).values({\n * labelId: 1,\n * action: 'publish',\n * userId: 'admin123',\n * userName: '관리자',\n * changes: {\n * before: { status: 'draft', publishedVersion: null },\n * after: { status: 'published', publishedVersion: 2 }\n * },\n * metadata: {\n * version: 2,\n * notes: '신규 브랜딩 적용'\n * }\n * });\n *\n * // 라벨별 이력 조회\n * const logs = await db.select()\n * .from(cmsAuditLogs)\n * .where(eq(cmsAuditLogs.labelId, 1))\n * .orderBy(desc(cmsAuditLogs.createdAt))\n * .limit(20);\n *\n * // 사용자별 활동 조회\n * const userActivity = await db.select()\n * .from(cmsAuditLogs)\n * .where(eq(cmsAuditLogs.userId, 'user123'))\n * .orderBy(desc(cmsAuditLogs.createdAt));\n *\n * // 최근 24시간 변경 이력\n * const recent = await db.select()\n * .from(cmsAuditLogs)\n * .where(gte(cmsAuditLogs.createdAt, new Date(Date.now() - 24 * 60 * 60 * 1000)))\n * .orderBy(desc(cmsAuditLogs.createdAt));\n */"],"mappings":"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","names":["serial","integer","text","timestamp","index","createFunctionSchema","schema","createFunctionSchema","serial","integer","text","timestamp","index","serial","text","jsonb","timestamp","index","unique","createFunctionSchema","schema","serial","text","jsonb","integer","timestamp","index","unique","createFunctionSchema","schema","serial","integer","text","jsonb","timestamp","index","createFunctionSchema","schema","createFunctionSchema","serial","integer","text","jsonb","timestamp","index"]}
|
|
@@ -115,53 +115,21 @@ var cmsLabelValues = schema2.table("label_values", {
|
|
|
115
115
|
index2("cms_label_values_locale_idx").on(table.locale)
|
|
116
116
|
]);
|
|
117
117
|
|
|
118
|
-
// src/server/entities/cms-
|
|
119
|
-
import { serial as serial3,
|
|
118
|
+
// src/server/entities/cms-draft-cache.ts
|
|
119
|
+
import { serial as serial3, text as text3, jsonb as jsonb2, timestamp as timestamp3, index as index3, unique as unique2 } from "drizzle-orm/pg-core";
|
|
120
120
|
import { createFunctionSchema as createFunctionSchema3 } from "@spfn/core/db";
|
|
121
121
|
var schema3 = createFunctionSchema3("@spfn/cms");
|
|
122
|
-
var
|
|
122
|
+
var cmsDraftCache = schema3.table("draft_cache", {
|
|
123
123
|
// Primary Key
|
|
124
124
|
id: serial3("id").primaryKey(),
|
|
125
|
-
// Foreign Key: cms_labels
|
|
126
|
-
labelId: integer3("label_id").notNull().references(() => cmsLabels.id, { onDelete: "cascade" }),
|
|
127
|
-
// 버전 번호
|
|
128
|
-
version: integer3("version").notNull(),
|
|
129
|
-
// 버전 상태
|
|
130
|
-
status: text3("status").notNull(),
|
|
131
|
-
// "draft" | "published" | "archived"
|
|
132
|
-
// 발행 정보
|
|
133
|
-
publishedAt: timestamp3("published_at", { withTimezone: true }),
|
|
134
|
-
publishedBy: text3("published_by"),
|
|
135
|
-
// 버전 노트 (변경사항 설명)
|
|
136
|
-
notes: text3("notes"),
|
|
137
|
-
// 버전 생성자
|
|
138
|
-
createdBy: text3("created_by"),
|
|
139
|
-
// 생성 시각
|
|
140
|
-
createdAt: timestamp3("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
141
|
-
}, (table) => [
|
|
142
|
-
// UNIQUE 제약: 각 라벨의 버전 번호는 고유
|
|
143
|
-
unique2("cms_label_versions_label_version_unique").on(table.labelId, table.version),
|
|
144
|
-
// 인덱스: labelId로 버전 목록 조회 최적화
|
|
145
|
-
index3("cms_label_versions_label_id_idx").on(table.labelId),
|
|
146
|
-
// 인덱스: status 필터링 최적화
|
|
147
|
-
index3("cms_label_versions_status_idx").on(table.status)
|
|
148
|
-
]);
|
|
149
|
-
|
|
150
|
-
// src/server/entities/cms-draft-cache.ts
|
|
151
|
-
import { serial as serial4, text as text4, jsonb as jsonb2, timestamp as timestamp4, index as index4, unique as unique3 } from "drizzle-orm/pg-core";
|
|
152
|
-
import { createFunctionSchema as createFunctionSchema4 } from "@spfn/core/db";
|
|
153
|
-
var schema4 = createFunctionSchema4("@spfn/cms");
|
|
154
|
-
var cmsDraftCache = schema4.table("draft_cache", {
|
|
155
|
-
// Primary Key
|
|
156
|
-
id: serial4("id").primaryKey(),
|
|
157
125
|
// 섹션 (페이지 단위)
|
|
158
|
-
section:
|
|
126
|
+
section: text3("section").notNull(),
|
|
159
127
|
// "home" | "why-futureplay" | "team" | "our-companies" | "apply"
|
|
160
128
|
// 언어
|
|
161
|
-
locale:
|
|
129
|
+
locale: text3("locale").notNull(),
|
|
162
130
|
// "ko" | "en" | "ja"
|
|
163
131
|
// 사용자 ID (핵심 필드!)
|
|
164
|
-
userId:
|
|
132
|
+
userId: text3("user_id").notNull(),
|
|
165
133
|
// 각 관리자의 독립적인 작업 공간
|
|
166
134
|
// Draft 콘텐츠 (JSONB)
|
|
167
135
|
content: jsonb2("content").notNull(),
|
|
@@ -172,28 +140,28 @@ var cmsDraftCache = schema4.table("draft_cache", {
|
|
|
172
140
|
// ...
|
|
173
141
|
// }
|
|
174
142
|
// 최종 수정 시각
|
|
175
|
-
updatedAt:
|
|
143
|
+
updatedAt: timestamp3("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
176
144
|
}, (table) => [
|
|
177
145
|
// UNIQUE 제약: section + locale + userId 조합은 유일
|
|
178
|
-
|
|
146
|
+
unique2("cms_draft_cache_unique").on(table.section, table.locale, table.userId),
|
|
179
147
|
// 인덱스: section으로 조회 최적화
|
|
180
|
-
|
|
148
|
+
index3("cms_draft_cache_section_idx").on(table.section),
|
|
181
149
|
// 인덱스: userId로 사용자의 모든 draft 조회 최적화
|
|
182
|
-
|
|
150
|
+
index3("cms_draft_cache_user_idx").on(table.userId)
|
|
183
151
|
]);
|
|
184
152
|
|
|
185
153
|
// src/server/entities/cms-published-cache.ts
|
|
186
|
-
import { serial as
|
|
187
|
-
import { createFunctionSchema as
|
|
188
|
-
var
|
|
189
|
-
var cmsPublishedCache =
|
|
154
|
+
import { serial as serial4, text as text4, jsonb as jsonb3, integer as integer3, timestamp as timestamp4, index as index4, unique as unique3 } from "drizzle-orm/pg-core";
|
|
155
|
+
import { createFunctionSchema as createFunctionSchema4 } from "@spfn/core/db";
|
|
156
|
+
var schema4 = createFunctionSchema4("@spfn/cms");
|
|
157
|
+
var cmsPublishedCache = schema4.table("published_cache", {
|
|
190
158
|
// Primary Key
|
|
191
|
-
id:
|
|
159
|
+
id: serial4("id").primaryKey(),
|
|
192
160
|
// 섹션 (페이지 단위)
|
|
193
|
-
section:
|
|
161
|
+
section: text4("section").notNull(),
|
|
194
162
|
// "home" | "why-futureplay" | "team" | "our-companies" | "apply"
|
|
195
163
|
// 언어
|
|
196
|
-
locale:
|
|
164
|
+
locale: text4("locale").notNull(),
|
|
197
165
|
// "ko" | "en" | "ja"
|
|
198
166
|
// 캐시된 콘텐츠 (JSONB)
|
|
199
167
|
content: jsonb3("content").notNull(),
|
|
@@ -204,32 +172,32 @@ var cmsPublishedCache = schema5.table("published_cache", {
|
|
|
204
172
|
// ...
|
|
205
173
|
// }
|
|
206
174
|
// 발행 정보
|
|
207
|
-
publishedAt:
|
|
208
|
-
publishedBy:
|
|
175
|
+
publishedAt: timestamp4("published_at", { withTimezone: true }).notNull(),
|
|
176
|
+
publishedBy: text4("published_by"),
|
|
209
177
|
// 캐시 버전 (클라이언트 캐싱용)
|
|
210
|
-
version:
|
|
178
|
+
version: integer3("version").notNull().default(1)
|
|
211
179
|
}, (table) => [
|
|
212
180
|
// UNIQUE 제약: section + locale 조합은 유일
|
|
213
|
-
|
|
181
|
+
unique3("cms_published_cache_unique").on(table.section, table.locale),
|
|
214
182
|
// 인덱스: section으로 조회 최적화
|
|
215
|
-
|
|
183
|
+
index4("cms_published_cache_section_idx").on(table.section)
|
|
216
184
|
]);
|
|
217
185
|
|
|
218
186
|
// src/server/entities/cms-audit-logs.ts
|
|
219
|
-
import { serial as
|
|
220
|
-
import { createFunctionSchema as
|
|
221
|
-
var
|
|
222
|
-
var cmsAuditLogs =
|
|
187
|
+
import { serial as serial5, integer as integer4, text as text5, jsonb as jsonb4, timestamp as timestamp5, index as index5 } from "drizzle-orm/pg-core";
|
|
188
|
+
import { createFunctionSchema as createFunctionSchema5 } from "@spfn/core/db";
|
|
189
|
+
var schema5 = createFunctionSchema5("@spfn/cms");
|
|
190
|
+
var cmsAuditLogs = schema5.table("audit_logs", {
|
|
223
191
|
// Primary Key
|
|
224
|
-
id:
|
|
192
|
+
id: serial5("id").primaryKey(),
|
|
225
193
|
// Foreign Key: cms_labels (nullable - 라벨 삭제 시 로그는 유지)
|
|
226
|
-
labelId:
|
|
194
|
+
labelId: integer4("label_id").references(() => cmsLabels.id, { onDelete: "set null" }),
|
|
227
195
|
// 작업 유형
|
|
228
|
-
action:
|
|
196
|
+
action: text5("action").notNull(),
|
|
229
197
|
// "create" | "update" | "publish" | "unpublish" | "archive" | "delete" | "rollback" | "duplicate"
|
|
230
198
|
// 사용자 정보
|
|
231
|
-
userId:
|
|
232
|
-
userName:
|
|
199
|
+
userId: text5("user_id").notNull(),
|
|
200
|
+
userName: text5("user_name"),
|
|
233
201
|
// 변경 내용 (before/after)
|
|
234
202
|
changes: jsonb4("changes"),
|
|
235
203
|
// { before: {...}, after: {...} }
|
|
@@ -237,16 +205,16 @@ var cmsAuditLogs = schema6.table("audit_logs", {
|
|
|
237
205
|
metadata: jsonb4("metadata"),
|
|
238
206
|
// { version: number, ip: string, userAgent: string, ... }
|
|
239
207
|
// 작업 시각
|
|
240
|
-
createdAt:
|
|
208
|
+
createdAt: timestamp5("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
241
209
|
}, (table) => [
|
|
242
210
|
// 인덱스: labelId로 이력 조회 최적화
|
|
243
|
-
|
|
211
|
+
index5("cms_audit_logs_label_id_idx").on(table.labelId),
|
|
244
212
|
// 인덱스: userId로 사용자 활동 조회 최적화
|
|
245
|
-
|
|
213
|
+
index5("cms_audit_logs_user_id_idx").on(table.userId),
|
|
246
214
|
// 인덱스: action 필터링 최적화
|
|
247
|
-
|
|
215
|
+
index5("cms_audit_logs_action_idx").on(table.action),
|
|
248
216
|
// 인덱스: 시간순 조회 최적화
|
|
249
|
-
|
|
217
|
+
index5("cms_audit_logs_created_at_idx").on(table.createdAt)
|
|
250
218
|
]);
|
|
251
219
|
|
|
252
220
|
// src/server/repositories/cms-labels.repository.ts
|
|
@@ -574,13 +542,13 @@ async function updatePublishedCache(section) {
|
|
|
574
542
|
localesSet.add("ko");
|
|
575
543
|
}
|
|
576
544
|
});
|
|
577
|
-
const
|
|
545
|
+
const timestamp6 = /* @__PURE__ */ new Date();
|
|
578
546
|
for (const locale of localesSet) {
|
|
579
547
|
await cmsPublishedCacheRepository.upsert({
|
|
580
548
|
section,
|
|
581
549
|
locale,
|
|
582
550
|
content: labelsByLocale[locale] || {},
|
|
583
|
-
publishedAt:
|
|
551
|
+
publishedAt: timestamp6,
|
|
584
552
|
publishedBy: "system"
|
|
585
553
|
});
|
|
586
554
|
}
|