@spfn/cms 0.1.0-alpha.79 → 0.1.0-alpha.80
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 +20 -22
- package/dist/api.js +11 -14
- package/dist/api.js.map +1 -1
- package/dist/client.js +11 -14
- package/dist/client.js.map +1 -1
- package/dist/lib/contracts/labels.d.ts +10 -14
- package/dist/lib/contracts/labels.js +9 -13
- package/dist/lib/contracts/labels.js.map +1 -1
- package/dist/lib/contracts/values.js +2 -1
- package/dist/lib/contracts/values.js.map +1 -1
- package/dist/server/generators/index.js +2 -4
- package/dist/server/generators/index.js.map +1 -1
- package/dist/server/repositories/index.d.ts +1 -3
- package/dist/server/repositories/index.js +2 -4
- package/dist/server/repositories/index.js.map +1 -1
- package/dist/server/routes/labels/{[labelId] → [id]}/admin/index.js +47 -23
- package/dist/server/routes/labels/[id]/admin/index.js.map +1 -0
- package/dist/server/routes/labels/[id]/index.js +11 -17
- package/dist/server/routes/labels/[id]/index.js.map +1 -1
- package/dist/server/routes/labels/{[labelId] → [id]}/publish/index.js +37 -166
- package/dist/server/routes/labels/[id]/publish/index.js.map +1 -0
- package/dist/server/routes/labels/{[labelId] → [id]}/versions/index.js +15 -21
- package/dist/server/routes/labels/[id]/versions/index.js.map +1 -0
- package/dist/server/routes/labels/{_labelId_ → _id_}/admin/index.d.ts +1 -1
- package/dist/server/routes/labels/{_labelId_ → _id_}/publish/index.d.ts +1 -1
- package/dist/server/routes/labels/{_labelId_ → _id_}/versions/index.d.ts +1 -1
- package/dist/server/routes/labels/by-key/[key]/index.js +11 -17
- package/dist/server/routes/labels/by-key/[key]/index.js.map +1 -1
- package/dist/server/routes/labels/index.js +14 -24
- package/dist/server/routes/labels/index.js.map +1 -1
- package/dist/server/routes/published-cache/index.js.map +1 -1
- package/dist/server/routes/values/[labelId]/[version]/index.js +4 -5
- package/dist/server/routes/values/[labelId]/[version]/index.js.map +1 -1
- package/dist/server/routes/values/[labelId]/index.js +4 -5
- package/dist/server/routes/values/[labelId]/index.js.map +1 -1
- package/dist/server.js +14 -16
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/server/routes/labels/[labelId]/admin/index.js.map +0 -1
- package/dist/server/routes/labels/[labelId]/publish/index.js.map +0 -1
- package/dist/server/routes/labels/[labelId]/versions/index.js.map +0 -1
|
@@ -182,13 +182,11 @@ var cmsAuditLogs = schema5.table("audit_logs", {
|
|
|
182
182
|
|
|
183
183
|
// src/server/repositories/cms-labels.repository.ts
|
|
184
184
|
async function findMany(options) {
|
|
185
|
-
const { section
|
|
185
|
+
const { section } = options || {};
|
|
186
186
|
return findManyHelper(cmsLabels, {
|
|
187
187
|
where: section ? { section } : void 0,
|
|
188
|
-
orderBy: asc(cmsLabels.key)
|
|
188
|
+
orderBy: asc(cmsLabels.key)
|
|
189
189
|
// key 오름차순 정렬 (JSON 파일의 순서 유지)
|
|
190
|
-
limit,
|
|
191
|
-
offset
|
|
192
190
|
});
|
|
193
191
|
}
|
|
194
192
|
async function count(section) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/server/repositories/cms-labels.repository.ts","../../../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","../../../src/server/repositories/cms-label-values.repository.ts","../../../src/server/repositories/cms-draft-cache.repository.ts","../../../src/server/repositories/cms-published-cache.repository.ts"],"sourcesContent":["/**\n * CMS Labels Repository\n *\n * 라벨 메타데이터 관리를 위한 Repository\n */\n\nimport { findOne, findMany as findManyHelper, create as createHelper, updateOne, deleteOne, count as countHelper } from '@spfn/core/db';\nimport { asc } from 'drizzle-orm';\nimport { cmsLabels, type CmsLabel, type NewCmsLabel } from '@/server/entities';\n\n/**\n * 라벨 목록 조회 (페이지네이션)\n */\nexport async function findMany(options?: {\n section?: string;\n limit?: number;\n offset?: number;\n}): Promise<CmsLabel[]>\n{\n const { section, limit = 20, offset = 0 } = options || {};\n\n return findManyHelper(cmsLabels, {\n where: section ? { section } : undefined,\n orderBy: asc(cmsLabels.key), // key 오름차순 정렬 (JSON 파일의 순서 유지)\n limit,\n offset\n });\n}\n\n/**\n * 전체 라벨 수 조회\n */\nexport async function count(section?: string): Promise<number>\n{\n return countHelper(cmsLabels, section ? { section } : undefined);\n}\n\n/**\n * ID로 라벨 조회\n */\nexport async function findById(id: number): Promise<CmsLabel | null>\n{\n return findOne(cmsLabels, { id });\n}\n\n/**\n * Key로 라벨 조회\n */\nexport async function findByKey(key: string): Promise<CmsLabel | null>\n{\n return findOne(cmsLabels, { key });\n}\n\n/**\n * 섹션으로 모든 라벨 조회\n */\nexport async function findBySection(section: string): Promise<CmsLabel[]>\n{\n return findManyHelper(cmsLabels, {\n where: { section },\n orderBy: asc(cmsLabels.key), // key 오름차순 정렬 (JSON 파일의 순서 유지)\n });\n}\n\n/**\n * 라벨 생성\n */\nexport async function create(data: NewCmsLabel): Promise<CmsLabel>\n{\n return createHelper(cmsLabels, data);\n}\n\n/**\n * 라벨 수정\n */\nexport async function updateById(id: number, data: Partial<NewCmsLabel>): Promise<CmsLabel | null>\n{\n return updateOne(cmsLabels, { id }, { ...data, updatedAt: new Date() });\n}\n\n/**\n * 라벨 삭제\n */\nexport async function deleteById(id: number): Promise<CmsLabel | null>\n{\n return deleteOne(cmsLabels, { id });\n}\n\n// Legacy export for backward compatibility\nexport const cmsLabelsRepository = {\n findMany,\n count,\n findById,\n findByKey,\n findBySection,\n create,\n updateById,\n deleteById\n};","/**\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 */","/**\n * CMS Label Values Repository\n *\n * 라벨 값 관리를 위한 Repository\n */\n\nimport { findOne, findMany, create, updateOne, deleteMany } from '@spfn/core/db';\nimport { eq, and, SQL, isNull } from 'drizzle-orm';\nimport { cmsLabelValues, type CmsLabelValue, type NewCmsLabelValue } from '@/server/entities';\n\n/**\n * 특정 라벨의 특정 버전 값들 조회\n */\nexport async function findByLabelIdAndVersion(\n labelId: number,\n version: number,\n options?: {\n locale?: string;\n breakpoint?: string | null;\n }\n): Promise<CmsLabelValue[]>\n{\n const { locale, breakpoint } = options || {};\n\n const conditions: SQL[] = [\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n ];\n\n if (locale)\n {\n conditions.push(eq(cmsLabelValues.locale, locale));\n }\n\n if (breakpoint !== undefined)\n {\n conditions.push(\n breakpoint === null\n ? isNull(cmsLabelValues.breakpoint)\n : eq(cmsLabelValues.breakpoint, breakpoint)\n );\n }\n\n return findMany(cmsLabelValues, {\n where: and(...conditions)\n });\n}\n\n/**\n * 값 저장 (upsert)\n * - version: null → Draft 저장 (덮어쓰기)\n * - version: number → Published 버전 생성 (불변)\n */\nexport async function upsert(data: NewCmsLabelValue): Promise<CmsLabelValue>\n{\n // 기존 값이 있는지 확인\n const versionCondition = data.version === null || data.version === undefined\n ? isNull(cmsLabelValues.version)\n : eq(cmsLabelValues.version, data.version as number);\n\n const existing = await findOne(\n cmsLabelValues,\n and(\n eq(cmsLabelValues.labelId, data.labelId),\n versionCondition,\n eq(cmsLabelValues.locale, data.locale || 'ko'),\n data.breakpoint\n ? eq(cmsLabelValues.breakpoint, data.breakpoint)\n : isNull(cmsLabelValues.breakpoint)\n )\n );\n\n if (existing)\n {\n // UPDATE (only for drafts with version: null)\n if (data.version === null || data.version === undefined)\n {\n const updated = await updateOne(\n cmsLabelValues,\n { id: existing.id },\n { value: data.value }\n );\n return updated!;\n }\n else\n {\n // Published versions are immutable - this shouldn't happen\n throw new Error(`Published version ${data.version} already exists and cannot be overwritten`);\n }\n }\n else\n {\n // INSERT (both draft and new published versions)\n return create(cmsLabelValues, data);\n }\n}\n\n/**\n * Draft 값들 조회 (version = null)\n */\nexport async function findDraftsByLabelId(labelId: number): Promise<CmsLabelValue[]>\n{\n return findMany(cmsLabelValues, {\n where: and(\n eq(cmsLabelValues.labelId, labelId),\n isNull(cmsLabelValues.version)\n )\n });\n}\n\n/**\n * 여러 값 일괄 저장\n */\nexport async function upsertMany(values: NewCmsLabelValue[]): Promise<CmsLabelValue[]>\n{\n const results = [];\n for (const value of values)\n {\n const result = await upsert(value);\n results.push(result);\n }\n return results;\n}\n\n/**\n * 특정 버전의 모든 값 삭제\n */\nexport async function deleteByVersion(labelId: number, version: number): Promise<CmsLabelValue[]>\n{\n return deleteMany(\n cmsLabelValues,\n and(\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n )\n );\n}\n\n// Legacy export for backward compatibility\nexport const cmsLabelValuesRepository = {\n findByLabelIdAndVersion,\n findDraftsByLabelId,\n upsert,\n upsertMany,\n deleteByVersion\n};","/**\n * CMS Draft Cache Repository\n *\n * 관리자별 초안 캐시 관리 (동시 편집 지원)\n */\n\nimport { findOne, findMany, deleteOne, deleteMany, upsert as upsertHelper } from '@spfn/core/db';\nimport { eq, and, lt } from 'drizzle-orm';\nimport { cmsDraftCache, type NewCmsDraftCache } from '@/server/entities';\n\n/**\n * 섹션 + 언어 + 사용자로 초안 캐시 조회\n */\nexport async function findByUser(section: string, locale: string, userId: string)\n{\n return findOne(\n cmsDraftCache,\n and(\n eq(cmsDraftCache.section, section),\n eq(cmsDraftCache.locale, locale),\n eq(cmsDraftCache.userId, userId)\n )\n );\n}\n\n/**\n * 초안 캐시 생성 또는 업데이트 (UPSERT)\n */\nexport async function upsert(data: NewCmsDraftCache)\n{\n return upsertHelper(cmsDraftCache, data, {\n target: [cmsDraftCache.section, cmsDraftCache.locale, cmsDraftCache.userId],\n set: {\n content: data.content,\n updatedAt: new Date(),\n }\n });\n}\n\n/**\n * 특정 사용자의 모든 초안 조회\n */\nexport async function findAllByUser(userId: string)\n{\n return findMany(cmsDraftCache, {\n where: eq(cmsDraftCache.userId, userId)\n });\n}\n\n/**\n * 초안 삭제\n */\nexport async function deleteByUser(section: string, locale: string, userId: string)\n{\n await deleteOne(\n cmsDraftCache,\n and(\n eq(cmsDraftCache.section, section),\n eq(cmsDraftCache.locale, locale),\n eq(cmsDraftCache.userId, userId)\n )\n );\n}\n\n/**\n * 오래된 초안 정리 (30일 이상 미사용)\n */\nexport async function cleanupOldDrafts(daysOld: number = 30)\n{\n const cutoffDate = new Date();\n cutoffDate.setDate(cutoffDate.getDate() - daysOld);\n\n return deleteMany(\n cmsDraftCache,\n lt(cmsDraftCache.updatedAt, cutoffDate)\n );\n}\n\nexport const cmsDraftCacheRepository = {\n findByUser,\n upsert,\n findAllByUser,\n deleteByUser,\n cleanupOldDrafts,\n};","/**\n * CMS Published Cache Repository\n *\n * 발행된 콘텐츠 캐시 관리 (초고속 조회)\n */\n\nimport { findOne, findMany, deleteOne, deleteMany, upsert as upsertHelper } from '@spfn/core/db';\nimport { eq, and, sql } from 'drizzle-orm';\nimport { cmsPublishedCache, type NewCmsPublishedCache } from '@/server/entities';\n\n/**\n * 섹션 + 언어로 발행된 캐시 조회\n */\nexport async function findBySection(section: string, locale: string = 'ko')\n{\n return findOne(\n cmsPublishedCache,\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale)\n )\n );\n}\n\n/**\n * 캐시 생성 또는 업데이트 (UPSERT)\n */\nexport async function upsert(data: NewCmsPublishedCache)\n{\n return upsertHelper(cmsPublishedCache, data, {\n target: [cmsPublishedCache.section, cmsPublishedCache.locale],\n set: {\n content: data.content,\n publishedAt: data.publishedAt,\n publishedBy: data.publishedBy,\n version: sql`${cmsPublishedCache.version} + 1`, // 버전 증가로 클라이언트 캐시 무효화\n }\n });\n}\n\n/**\n * 섹션별 모든 언어 캐시 조회\n */\nexport async function findAllLanguages(section: string)\n{\n return findMany(cmsPublishedCache, {\n where: eq(cmsPublishedCache.section, section)\n });\n}\n\n/**\n * 캐시 삭제\n */\nexport async function deleteBySection(section: string, locale?: string)\n{\n if (locale)\n {\n await deleteOne(\n cmsPublishedCache,\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale)\n )\n );\n }\n else\n {\n await deleteMany(\n cmsPublishedCache,\n eq(cmsPublishedCache.section, section)\n );\n }\n}\n\nexport const cmsPublishedCacheRepository = {\n findBySection,\n upsert,\n findAllLanguages,\n deleteBySection,\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","findOne","findMany","create","updateOne","findMany","findOne","updateOne","create","findOne","findMany","deleteOne","deleteMany","eq","and","findOne","and","eq","upsert","findMany","deleteOne","deleteMany","findOne","findMany","deleteOne","deleteMany","upsertHelper","eq","and","findBySection","findOne","and","eq","upsert","upsertHelper","findMany","deleteOne","deleteMany"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/server/repositories/cms-labels.repository.ts","../../../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","../../../src/server/repositories/cms-label-values.repository.ts","../../../src/server/repositories/cms-draft-cache.repository.ts","../../../src/server/repositories/cms-published-cache.repository.ts"],"sourcesContent":["/**\n * CMS Labels Repository\n *\n * 라벨 메타데이터 관리를 위한 Repository\n */\n\nimport { findOne, findMany as findManyHelper, create as createHelper, updateOne, deleteOne, count as countHelper } from '@spfn/core/db';\nimport { asc } from 'drizzle-orm';\nimport { cmsLabels, type CmsLabel, type NewCmsLabel } from '@/server/entities';\n\n/**\n * 라벨 목록 조회\n */\nexport async function findMany(options?: {\n section?: string;\n}): Promise<CmsLabel[]>\n{\n const { section } = options || {};\n\n return findManyHelper(cmsLabels, {\n where: section ? { section } : undefined,\n orderBy: asc(cmsLabels.key), // key 오름차순 정렬 (JSON 파일의 순서 유지)\n });\n}\n\n/**\n * 전체 라벨 수 조회\n */\nexport async function count(section?: string): Promise<number>\n{\n return countHelper(cmsLabels, section ? { section } : undefined);\n}\n\n/**\n * ID로 라벨 조회\n */\nexport async function findById(id: number): Promise<CmsLabel | null>\n{\n return findOne(cmsLabels, { id });\n}\n\n/**\n * Key로 라벨 조회\n */\nexport async function findByKey(key: string): Promise<CmsLabel | null>\n{\n return findOne(cmsLabels, { key });\n}\n\n/**\n * 섹션으로 모든 라벨 조회\n */\nexport async function findBySection(section: string): Promise<CmsLabel[]>\n{\n return findManyHelper(cmsLabels, {\n where: { section },\n orderBy: asc(cmsLabels.key), // key 오름차순 정렬 (JSON 파일의 순서 유지)\n });\n}\n\n/**\n * 라벨 생성\n */\nexport async function create(data: NewCmsLabel): Promise<CmsLabel>\n{\n return createHelper(cmsLabels, data);\n}\n\n/**\n * 라벨 수정\n */\nexport async function updateById(id: number, data: Partial<NewCmsLabel>): Promise<CmsLabel | null>\n{\n return updateOne(cmsLabels, { id }, { ...data, updatedAt: new Date() });\n}\n\n/**\n * 라벨 삭제\n */\nexport async function deleteById(id: number): Promise<CmsLabel | null>\n{\n return deleteOne(cmsLabels, { id });\n}\n\n// Legacy export for backward compatibility\nexport const cmsLabelsRepository = {\n findMany,\n count,\n findById,\n findByKey,\n findBySection,\n create,\n updateById,\n deleteById\n};","/**\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 */","/**\n * CMS Label Values Repository\n *\n * 라벨 값 관리를 위한 Repository\n */\n\nimport { findOne, findMany, create, updateOne, deleteMany } from '@spfn/core/db';\nimport { eq, and, SQL, isNull } from 'drizzle-orm';\nimport { cmsLabelValues, type CmsLabelValue, type NewCmsLabelValue } from '@/server/entities';\n\n/**\n * 특정 라벨의 특정 버전 값들 조회\n */\nexport async function findByLabelIdAndVersion(\n labelId: number,\n version: number,\n options?: {\n locale?: string;\n breakpoint?: string | null;\n }\n): Promise<CmsLabelValue[]>\n{\n const { locale, breakpoint } = options || {};\n\n const conditions: SQL[] = [\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n ];\n\n if (locale)\n {\n conditions.push(eq(cmsLabelValues.locale, locale));\n }\n\n if (breakpoint !== undefined)\n {\n conditions.push(\n breakpoint === null\n ? isNull(cmsLabelValues.breakpoint)\n : eq(cmsLabelValues.breakpoint, breakpoint)\n );\n }\n\n return findMany(cmsLabelValues, {\n where: and(...conditions)\n });\n}\n\n/**\n * 값 저장 (upsert)\n * - version: null → Draft 저장 (덮어쓰기)\n * - version: number → Published 버전 생성 (불변)\n */\nexport async function upsert(data: NewCmsLabelValue): Promise<CmsLabelValue>\n{\n // 기존 값이 있는지 확인\n const versionCondition = data.version === null || data.version === undefined\n ? isNull(cmsLabelValues.version)\n : eq(cmsLabelValues.version, data.version as number);\n\n const existing = await findOne(\n cmsLabelValues,\n and(\n eq(cmsLabelValues.labelId, data.labelId),\n versionCondition,\n eq(cmsLabelValues.locale, data.locale || 'ko'),\n data.breakpoint\n ? eq(cmsLabelValues.breakpoint, data.breakpoint)\n : isNull(cmsLabelValues.breakpoint)\n )\n );\n\n if (existing)\n {\n // UPDATE (only for drafts with version: null)\n if (data.version === null || data.version === undefined)\n {\n const updated = await updateOne(\n cmsLabelValues,\n { id: existing.id },\n { value: data.value }\n );\n return updated!;\n }\n else\n {\n // Published versions are immutable - this shouldn't happen\n throw new Error(`Published version ${data.version} already exists and cannot be overwritten`);\n }\n }\n else\n {\n // INSERT (both draft and new published versions)\n return create(cmsLabelValues, data);\n }\n}\n\n/**\n * Draft 값들 조회 (version = null)\n */\nexport async function findDraftsByLabelId(labelId: number): Promise<CmsLabelValue[]>\n{\n return findMany(cmsLabelValues, {\n where: and(\n eq(cmsLabelValues.labelId, labelId),\n isNull(cmsLabelValues.version)\n )\n });\n}\n\n/**\n * 여러 값 일괄 저장\n */\nexport async function upsertMany(values: NewCmsLabelValue[]): Promise<CmsLabelValue[]>\n{\n const results = [];\n for (const value of values)\n {\n const result = await upsert(value);\n results.push(result);\n }\n return results;\n}\n\n/**\n * 특정 버전의 모든 값 삭제\n */\nexport async function deleteByVersion(labelId: number, version: number): Promise<CmsLabelValue[]>\n{\n return deleteMany(\n cmsLabelValues,\n and(\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n )\n );\n}\n\n// Legacy export for backward compatibility\nexport const cmsLabelValuesRepository = {\n findByLabelIdAndVersion,\n findDraftsByLabelId,\n upsert,\n upsertMany,\n deleteByVersion\n};","/**\n * CMS Draft Cache Repository\n *\n * 관리자별 초안 캐시 관리 (동시 편집 지원)\n */\n\nimport { findOne, findMany, deleteOne, deleteMany, upsert as upsertHelper } from '@spfn/core/db';\nimport { eq, and, lt } from 'drizzle-orm';\nimport { cmsDraftCache, type NewCmsDraftCache } from '@/server/entities';\n\n/**\n * 섹션 + 언어 + 사용자로 초안 캐시 조회\n */\nexport async function findByUser(section: string, locale: string, userId: string)\n{\n return findOne(\n cmsDraftCache,\n and(\n eq(cmsDraftCache.section, section),\n eq(cmsDraftCache.locale, locale),\n eq(cmsDraftCache.userId, userId)\n )\n );\n}\n\n/**\n * 초안 캐시 생성 또는 업데이트 (UPSERT)\n */\nexport async function upsert(data: NewCmsDraftCache)\n{\n return upsertHelper(cmsDraftCache, data, {\n target: [cmsDraftCache.section, cmsDraftCache.locale, cmsDraftCache.userId],\n set: {\n content: data.content,\n updatedAt: new Date(),\n }\n });\n}\n\n/**\n * 특정 사용자의 모든 초안 조회\n */\nexport async function findAllByUser(userId: string)\n{\n return findMany(cmsDraftCache, {\n where: eq(cmsDraftCache.userId, userId)\n });\n}\n\n/**\n * 초안 삭제\n */\nexport async function deleteByUser(section: string, locale: string, userId: string)\n{\n await deleteOne(\n cmsDraftCache,\n and(\n eq(cmsDraftCache.section, section),\n eq(cmsDraftCache.locale, locale),\n eq(cmsDraftCache.userId, userId)\n )\n );\n}\n\n/**\n * 오래된 초안 정리 (30일 이상 미사용)\n */\nexport async function cleanupOldDrafts(daysOld: number = 30)\n{\n const cutoffDate = new Date();\n cutoffDate.setDate(cutoffDate.getDate() - daysOld);\n\n return deleteMany(\n cmsDraftCache,\n lt(cmsDraftCache.updatedAt, cutoffDate)\n );\n}\n\nexport const cmsDraftCacheRepository = {\n findByUser,\n upsert,\n findAllByUser,\n deleteByUser,\n cleanupOldDrafts,\n};","/**\n * CMS Published Cache Repository\n *\n * 발행된 콘텐츠 캐시 관리 (초고속 조회)\n */\n\nimport { findOne, findMany, deleteOne, deleteMany, upsert as upsertHelper } from '@spfn/core/db';\nimport { eq, and, sql } from 'drizzle-orm';\nimport { cmsPublishedCache, type NewCmsPublishedCache } from '@/server/entities';\n\n/**\n * 섹션 + 언어로 발행된 캐시 조회\n */\nexport async function findBySection(section: string, locale: string = 'ko')\n{\n return findOne(\n cmsPublishedCache,\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale)\n )\n );\n}\n\n/**\n * 캐시 생성 또는 업데이트 (UPSERT)\n */\nexport async function upsert(data: NewCmsPublishedCache)\n{\n return upsertHelper(cmsPublishedCache, data, {\n target: [cmsPublishedCache.section, cmsPublishedCache.locale],\n set: {\n content: data.content,\n publishedAt: data.publishedAt,\n publishedBy: data.publishedBy,\n version: sql`${cmsPublishedCache.version} + 1`, // 버전 증가로 클라이언트 캐시 무효화\n }\n });\n}\n\n/**\n * 섹션별 모든 언어 캐시 조회\n */\nexport async function findAllLanguages(section: string)\n{\n return findMany(cmsPublishedCache, {\n where: eq(cmsPublishedCache.section, section)\n });\n}\n\n/**\n * 캐시 삭제\n */\nexport async function deleteBySection(section: string, locale?: string)\n{\n if (locale)\n {\n await deleteOne(\n cmsPublishedCache,\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale)\n )\n );\n }\n else\n {\n await deleteMany(\n cmsPublishedCache,\n eq(cmsPublishedCache.section, section)\n );\n }\n}\n\nexport const cmsPublishedCacheRepository = {\n findBySection,\n upsert,\n findAllLanguages,\n deleteBySection,\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","findOne","findMany","create","updateOne","findMany","findOne","updateOne","create","findOne","findMany","deleteOne","deleteMany","eq","and","findOne","and","eq","upsert","findMany","deleteOne","deleteMany","findOne","findMany","deleteOne","deleteMany","upsertHelper","eq","and","findBySection","findOne","and","eq","upsert","upsertHelper","findMany","deleteOne","deleteMany"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/server/routes/labels/[
|
|
1
|
+
// src/server/routes/labels/[id]/admin/index.ts
|
|
2
2
|
import { createApp } from "@spfn/core/route";
|
|
3
3
|
|
|
4
4
|
// src/lib/contracts/labels.ts
|
|
@@ -8,9 +8,7 @@ var getLabelsContract = {
|
|
|
8
8
|
path: "/_cms/labels",
|
|
9
9
|
query: Type.Object({
|
|
10
10
|
section: Type.Optional(Type.String({ description: "\uC139\uC158\uC73C\uB85C \uD544\uD130\uB9C1 (\uC608: home, why-futureplay)" })),
|
|
11
|
-
|
|
12
|
-
offset: Type.Optional(Type.Number({ minimum: 0, default: 0, description: "\uC2DC\uC791 \uC624\uD504\uC14B" })),
|
|
13
|
-
includeDefaultValues: Type.Optional(Type.Boolean({ description: "\uAE30\uBCF8\uAC12 \uD3EC\uD568 \uC5EC\uBD80", default: false }))
|
|
11
|
+
includeDefaultValues: Type.Optional(Type.Boolean({ description: "\uAE30\uBCF8\uAC12 \uD3EC\uD568 \uC5EC\uBD80" }))
|
|
14
12
|
}),
|
|
15
13
|
response: Type.Object({
|
|
16
14
|
labels: Type.Array(Type.Object({
|
|
@@ -25,9 +23,7 @@ var getLabelsContract = {
|
|
|
25
23
|
updatedAt: Type.String(),
|
|
26
24
|
defaultValue: Type.Optional(Type.Any({ description: "\uB77C\uBCA8 \uC815\uC758 \uD30C\uC77C\uC758 \uAE30\uBCF8\uAC12" }))
|
|
27
25
|
})),
|
|
28
|
-
total: Type.Number()
|
|
29
|
-
limit: Type.Number(),
|
|
30
|
-
offset: Type.Number()
|
|
26
|
+
total: Type.Number()
|
|
31
27
|
})
|
|
32
28
|
};
|
|
33
29
|
var createLabelContract = {
|
|
@@ -166,9 +162,9 @@ var getLabelByKeyContract = {
|
|
|
166
162
|
};
|
|
167
163
|
var publishLabelContract = {
|
|
168
164
|
method: "POST",
|
|
169
|
-
path: "/_cms/labels/:
|
|
165
|
+
path: "/_cms/labels/:id/publish",
|
|
170
166
|
params: Type.Object({
|
|
171
|
-
|
|
167
|
+
id: Type.String({ description: "\uB77C\uBCA8 ID" })
|
|
172
168
|
}),
|
|
173
169
|
body: Type.Object({
|
|
174
170
|
notes: Type.Optional(Type.String({ description: "\uBC1C\uD589 \uB178\uD2B8 (\uBC84\uC804 \uC124\uBA85)" })),
|
|
@@ -177,7 +173,7 @@ var publishLabelContract = {
|
|
|
177
173
|
response: Type.Union([
|
|
178
174
|
Type.Object({
|
|
179
175
|
success: Type.Boolean(),
|
|
180
|
-
|
|
176
|
+
id: Type.Number(),
|
|
181
177
|
version: Type.Number(),
|
|
182
178
|
message: Type.String()
|
|
183
179
|
}),
|
|
@@ -188,9 +184,9 @@ var publishLabelContract = {
|
|
|
188
184
|
};
|
|
189
185
|
var getAdminLabelContract = {
|
|
190
186
|
method: "GET",
|
|
191
|
-
path: "/_cms/labels/:
|
|
187
|
+
path: "/_cms/labels/:id/admin",
|
|
192
188
|
params: Type.Object({
|
|
193
|
-
|
|
189
|
+
id: Type.String({ description: "\uB77C\uBCA8 ID" })
|
|
194
190
|
}),
|
|
195
191
|
response: Type.Union([
|
|
196
192
|
Type.Object({
|
|
@@ -237,9 +233,9 @@ var getAdminLabelContract = {
|
|
|
237
233
|
};
|
|
238
234
|
var getLabelVersionsContract = {
|
|
239
235
|
method: "GET",
|
|
240
|
-
path: "/_cms/labels/:
|
|
236
|
+
path: "/_cms/labels/:id/versions",
|
|
241
237
|
params: Type.Object({
|
|
242
|
-
|
|
238
|
+
id: Type.String({ description: "\uB77C\uBCA8 ID" })
|
|
243
239
|
}),
|
|
244
240
|
response: Type.Union([
|
|
245
241
|
Type.Object({
|
|
@@ -447,13 +443,11 @@ var cmsAuditLogs = schema5.table("audit_logs", {
|
|
|
447
443
|
|
|
448
444
|
// src/server/repositories/cms-labels.repository.ts
|
|
449
445
|
async function findMany(options) {
|
|
450
|
-
const { section
|
|
446
|
+
const { section } = options || {};
|
|
451
447
|
return findManyHelper(cmsLabels, {
|
|
452
448
|
where: section ? { section } : void 0,
|
|
453
|
-
orderBy: asc(cmsLabels.key)
|
|
449
|
+
orderBy: asc(cmsLabels.key)
|
|
454
450
|
// key 오름차순 정렬 (JSON 파일의 순서 유지)
|
|
455
|
-
limit,
|
|
456
|
-
offset
|
|
457
451
|
});
|
|
458
452
|
}
|
|
459
453
|
async function count(section) {
|
|
@@ -572,9 +566,33 @@ var cmsLabelValuesRepository = {
|
|
|
572
566
|
deleteByVersion
|
|
573
567
|
};
|
|
574
568
|
|
|
575
|
-
// src/server/routes/labels/[
|
|
569
|
+
// src/server/routes/labels/[id]/admin/index.ts
|
|
576
570
|
var app = createApp();
|
|
577
|
-
function
|
|
571
|
+
function compareValues(draftValues, publishedValues) {
|
|
572
|
+
const draftMap = new Map(
|
|
573
|
+
draftValues.map((v) => [
|
|
574
|
+
`${v.locale}:${v.breakpoint || "default"}`,
|
|
575
|
+
v.value
|
|
576
|
+
])
|
|
577
|
+
);
|
|
578
|
+
const publishedMap = new Map(
|
|
579
|
+
publishedValues.map((v) => [
|
|
580
|
+
`${v.locale}:${v.breakpoint || "default"}`,
|
|
581
|
+
v.value
|
|
582
|
+
])
|
|
583
|
+
);
|
|
584
|
+
if (draftMap.size !== publishedMap.size) {
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
for (const [key, draftValue] of draftMap.entries()) {
|
|
588
|
+
const publishedValue = publishedMap.get(key);
|
|
589
|
+
if (!publishedValue || JSON.stringify(draftValue) !== JSON.stringify(publishedValue)) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
function calculateStatus(hasDraft, hasPublished, draftValues, publishedValues) {
|
|
578
596
|
if (!hasPublished && !hasDraft) {
|
|
579
597
|
return "default-only";
|
|
580
598
|
}
|
|
@@ -584,12 +602,16 @@ function calculateStatus(hasDraft, hasPublished) {
|
|
|
584
602
|
if (hasPublished && !hasDraft) {
|
|
585
603
|
return "published";
|
|
586
604
|
}
|
|
605
|
+
if (draftValues && publishedValues) {
|
|
606
|
+
const hasActualChanges = compareValues(draftValues, publishedValues);
|
|
607
|
+
return hasActualChanges ? "modified" : "published";
|
|
608
|
+
}
|
|
587
609
|
return "modified";
|
|
588
610
|
}
|
|
589
611
|
app.bind(getAdminLabelContract, async (c) => {
|
|
590
|
-
const {
|
|
612
|
+
const { id } = c.params;
|
|
591
613
|
try {
|
|
592
|
-
const label = await cmsLabelsRepository.findById(parseInt(
|
|
614
|
+
const label = await cmsLabelsRepository.findById(parseInt(id));
|
|
593
615
|
if (!label) {
|
|
594
616
|
return c.json(
|
|
595
617
|
{ error: "Label not found" },
|
|
@@ -606,7 +628,9 @@ app.bind(getAdminLabelContract, async (c) => {
|
|
|
606
628
|
}
|
|
607
629
|
const status = calculateStatus(
|
|
608
630
|
draftValues.length > 0,
|
|
609
|
-
publishedValues.length > 0
|
|
631
|
+
publishedValues.length > 0,
|
|
632
|
+
draftValues,
|
|
633
|
+
publishedValues
|
|
610
634
|
);
|
|
611
635
|
return c.json({
|
|
612
636
|
label: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../src/server/routes/labels/%5Bid%5D/admin/index.ts","../../../../../../src/lib/contracts/labels.ts","../../../../../../src/server/repositories/cms-labels.repository.ts","../../../../../../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","../../../../../../src/server/repositories/cms-label-values.repository.ts"],"sourcesContent":["/**\n * CMS Label Admin Route\n *\n * 관리자용 라벨 조회 API (Draft + Published + Status)\n * - GET /labels/:id/admin - 라벨 상세 정보 조회 (final: /_cms/labels/:id/admin)\n */\n\nimport { createApp } from '@spfn/core/route';\nimport { getAdminLabelContract } from '@/lib/contracts/labels';\nimport { cmsLabelsRepository } from '@/server/repositories/cms-labels.repository';\nimport { cmsLabelValuesRepository } from '@/server/repositories/cms-label-values.repository';\nimport type { CmsLabelValue } from '@/server/entities/cms-label-values';\n\nconst app = createApp();\n\n/**\n * Draft와 Published 값들을 비교하여 실제 변경이 있는지 확인\n *\n * @param draftValues - Draft 값 목록\n * @param publishedValues - Published 값 목록\n * @returns 변경 사항이 있으면 true, 없으면 false\n */\nfunction compareValues(\n draftValues: CmsLabelValue[],\n publishedValues: CmsLabelValue[]\n): boolean\n{\n // locale + breakpoint 조합별로 값 매핑\n const draftMap = new Map(\n draftValues.map(v => [\n `${v.locale}:${v.breakpoint || 'default'}`,\n v.value\n ])\n );\n\n const publishedMap = new Map(\n publishedValues.map(v => [\n `${v.locale}:${v.breakpoint || 'default'}`,\n v.value\n ])\n );\n\n // 개수가 다르면 변경됨\n if (draftMap.size !== publishedMap.size)\n {\n return true;\n }\n\n // 각 값 깊은 비교 (JSON 직렬화로 비교)\n for (const [key, draftValue] of draftMap.entries())\n {\n const publishedValue = publishedMap.get(key);\n\n // Published에 해당 키가 없거나 값이 다르면 변경됨\n if (!publishedValue || JSON.stringify(draftValue) !== JSON.stringify(publishedValue))\n {\n return true;\n }\n }\n\n // 모든 값이 동일함\n return false;\n}\n\n/**\n * 라벨 상태 계산\n *\n * @param hasDraft - Draft가 있는지\n * @param hasPublished - Published가 있는지\n * @param draftValues - Draft 값 목록 (있는 경우)\n * @param publishedValues - Published 값 목록 (있는 경우)\n * @returns 상태 ('default-only' | 'unpublished' | 'published' | 'modified')\n */\nfunction calculateStatus(\n hasDraft: boolean,\n hasPublished: boolean,\n draftValues?: CmsLabelValue[],\n publishedValues?: CmsLabelValue[]\n): 'default-only' | 'unpublished' | 'published' | 'modified'\n{\n if (!hasPublished && !hasDraft)\n {\n return 'default-only';\n }\n\n if (!hasPublished && hasDraft)\n {\n return 'unpublished';\n }\n\n if (hasPublished && !hasDraft)\n {\n return 'published';\n }\n\n // hasPublished && hasDraft\n // Draft와 Published 내용을 실제로 비교하여 변경 여부 확인\n if (draftValues && publishedValues)\n {\n const hasActualChanges = compareValues(draftValues, publishedValues);\n return hasActualChanges ? 'modified' : 'published';\n }\n\n // 안전을 위해 기본값은 'modified'\n return 'modified';\n}\n\n/**\n * GET /labels/:id/admin\n * 관리자용 라벨 상세 조회\n */\napp.bind(getAdminLabelContract, async (c) =>\n{\n const { id } = c.params;\n\n try\n {\n // 라벨 메타데이터 조회\n const label = await cmsLabelsRepository.findById(parseInt(id));\n if (!label)\n {\n return c.json(\n { error: 'Label not found' },\n 404\n );\n }\n\n // Draft 값들 조회 (version = null)\n const draftValues = await cmsLabelValuesRepository.findDraftsByLabelId(label.id);\n\n // Published 값들 조회 (publishedVersion이 있는 경우)\n let publishedValues: any[] = [];\n if (label.publishedVersion !== null)\n {\n publishedValues = await cmsLabelValuesRepository.findByLabelIdAndVersion(\n label.id,\n label.publishedVersion\n );\n }\n\n // 상태 계산 (실제 값 비교 포함)\n const status = calculateStatus(\n draftValues.length > 0,\n publishedValues.length > 0,\n draftValues,\n publishedValues\n );\n\n return c.json({\n label: {\n id: label.id,\n key: label.key,\n section: label.section,\n type: label.type,\n description: label.description,\n publishedVersion: label.publishedVersion,\n createdBy: label.createdBy,\n createdAt: label.createdAt.toISOString(),\n updatedAt: label.updatedAt.toISOString()\n },\n draft: draftValues.map(v => ({\n id: v.id,\n labelId: v.labelId,\n version: null,\n locale: v.locale,\n breakpoint: v.breakpoint,\n value: v.value,\n createdAt: v.createdAt.toISOString()\n })),\n published: publishedValues.map(v => ({\n id: v.id,\n labelId: v.labelId,\n version: v.version as number,\n locale: v.locale,\n breakpoint: v.breakpoint,\n value: v.value,\n createdAt: v.createdAt.toISOString()\n })),\n status\n });\n }\n catch (error)\n {\n const err = error as Error;\n return c.json(\n { error: err.message },\n 500\n );\n }\n});\n\nexport default app;","import { Type } from '@sinclair/typebox';\nimport type { RouteContract } from '@spfn/core/route';\n\n/**\n * CMS Labels Contracts\n *\n * 라벨 메타데이터 관리 API\n */\n\n/**\n * GET /_cms/labels - 라벨 목록 조회\n */\nexport const getLabelsContract = {\n method: 'GET' as const,\n path: '/_cms/labels',\n query: Type.Object({\n section: Type.Optional(Type.String({ description: '섹션으로 필터링 (예: home, why-futureplay)' })),\n includeDefaultValues: Type.Optional(Type.Boolean({ description: '기본값 포함 여부' }))\n }),\n response: Type.Object({\n labels: Type.Array(Type.Object({\n id: Type.Number(),\n key: Type.String(),\n section: Type.String(),\n type: Type.String(),\n description: Type.Union([Type.String(), Type.Null()], { description: '라벨 설명' }),\n publishedVersion: Type.Union([Type.Number(), Type.Null()]),\n createdBy: Type.Union([Type.String(), Type.Null()]),\n createdAt: Type.String(),\n updatedAt: Type.String(),\n defaultValue: Type.Optional(Type.Any({ description: '라벨 정의 파일의 기본값' }))\n })),\n total: Type.Number()\n })\n} as const satisfies RouteContract;\n\n/**\n * POST /_cms/labels - 새 라벨 생성\n */\nexport const createLabelContract = {\n method: 'POST' as const,\n path: '/_cms/labels',\n body: Type.Object({\n key: Type.String({\n description: '고유 키 (예: home.hero.title)',\n pattern: '^[a-z0-9-]+\\\\.[a-z0-9-]+\\\\.[a-z0-9-]+$'\n }),\n section: Type.String({\n description: '섹션 이름 (예: home, why-futureplay)',\n pattern: '^[a-z0-9-]+$'\n }),\n type: Type.Union([\n Type.Literal('text'),\n Type.Literal('image'),\n Type.Literal('video'),\n Type.Literal('file'),\n Type.Literal('object')\n ], { description: '값 타입' }),\n createdBy: Type.Optional(Type.String({ description: '생성자 ID' }))\n }),\n response: Type.Union([\n Type.Object({\n id: Type.Number(),\n key: Type.String(),\n section: Type.String(),\n type: Type.String(),\n publishedVersion: Type.Union([Type.Number(), Type.Null()]),\n createdBy: Type.Union([Type.String(), Type.Null()]),\n createdAt: Type.String(),\n updatedAt: Type.String()\n }),\n Type.Object({\n error: Type.String(),\n key: Type.Optional(Type.String())\n })\n ])\n} as const satisfies RouteContract;\n\n/**\n * GET /_cms/labels/:id - 라벨 단건 조회\n */\nexport const getLabelContract = {\n method: 'GET' as const,\n path: '/_cms/labels/:id',\n params: Type.Object({\n id: Type.String({ description: '라벨 ID' })\n }),\n response: Type.Union([\n Type.Object({\n id: Type.Number(),\n key: Type.String(),\n section: Type.String(),\n type: Type.String(),\n description: Type.Union([Type.String(), Type.Null()]),\n publishedVersion: Type.Union([Type.Number(), Type.Null()]),\n createdBy: Type.Union([Type.String(), Type.Null()]),\n createdAt: Type.String(),\n updatedAt: Type.String()\n }),\n Type.Object({\n error: Type.String()\n })\n ])\n} as const satisfies RouteContract;\n\n/**\n * PATCH /_cms/labels/:id - 라벨 메타데이터 수정\n */\nexport const updateLabelContract = {\n method: 'PATCH' as const,\n path: '/_cms/labels/:id',\n params: Type.Object({\n id: Type.String({ description: '라벨 ID' })\n }),\n body: Type.Object({\n section: Type.Optional(Type.String({ description: '섹션 변경' })),\n type: Type.Optional(Type.Union([\n Type.Literal('text'),\n Type.Literal('image'),\n Type.Literal('video'),\n Type.Literal('file'),\n Type.Literal('object')\n ]))\n }),\n response: Type.Union([\n Type.Object({\n id: Type.Number(),\n key: Type.String(),\n section: Type.String(),\n type: Type.String(),\n description: Type.Union([Type.String(), Type.Null()]),\n publishedVersion: Type.Union([Type.Number(), Type.Null()]),\n createdBy: Type.Union([Type.String(), Type.Null()]),\n createdAt: Type.String(),\n updatedAt: Type.String()\n }),\n Type.Object({\n error: Type.String()\n })\n ])\n} as const satisfies RouteContract;\n\n/**\n * DELETE /_cms/labels/:id - 라벨 삭제\n */\nexport const deleteLabelContract = {\n method: 'DELETE' as const,\n path: '/_cms/labels/:id',\n params: Type.Object({\n id: Type.String({ description: '라벨 ID' })\n }),\n response: Type.Union([\n Type.Object({\n success: Type.Boolean(),\n id: Type.Number()\n }),\n Type.Object({\n error: Type.String()\n })\n ])\n} as const satisfies RouteContract;\n\n/**\n * GET /_cms/labels/by-key/:key - Key로 라벨 조회\n */\nexport const getLabelByKeyContract = {\n method: 'GET' as const,\n path: '/_cms/labels/by-key/:key',\n params: Type.Object({\n key: Type.String({ description: '라벨 Key (예: home.hero.title)' })\n }),\n response: Type.Union([\n Type.Object({\n id: Type.Number(),\n key: Type.String(),\n section: Type.String(),\n type: Type.String(),\n description: Type.Union([Type.String(), Type.Null()]),\n publishedVersion: Type.Union([Type.Number(), Type.Null()]),\n createdBy: Type.Union([Type.String(), Type.Null()]),\n createdAt: Type.String(),\n updatedAt: Type.String()\n }),\n Type.Object({\n error: Type.String(),\n key: Type.Optional(Type.String())\n })\n ])\n} as const satisfies RouteContract;\n\n/**\n * POST /_cms/labels/:id/publish - 라벨 발행 (Draft → Published)\n */\nexport const publishLabelContract = {\n method: 'POST' as const,\n path: '/_cms/labels/:id/publish',\n params: Type.Object({\n id: Type.String({ description: '라벨 ID' })\n }),\n body: Type.Object({\n notes: Type.Optional(Type.String({ description: '발행 노트 (버전 설명)' })),\n publishedBy: Type.Optional(Type.String({ description: '발행자 ID' }))\n }),\n response: Type.Union([\n Type.Object({\n success: Type.Boolean(),\n id: Type.Number(),\n version: Type.Number(),\n message: Type.String()\n }),\n Type.Object({\n error: Type.String()\n })\n ])\n} as const satisfies RouteContract;\n\n/**\n * GET /_cms/labels/:id/admin - 관리자용 라벨 조회 (Draft + Published + Status)\n */\nexport const getAdminLabelContract = {\n method: 'GET' as const,\n path: '/_cms/labels/:id/admin',\n params: Type.Object({\n id: Type.String({ description: '라벨 ID' })\n }),\n response: Type.Union([\n Type.Object({\n label: Type.Object({\n id: Type.Number(),\n key: Type.String(),\n section: Type.String(),\n type: Type.String(),\n description: Type.Union([Type.String(), Type.Null()]),\n publishedVersion: Type.Union([Type.Number(), Type.Null()]),\n createdBy: Type.Union([Type.String(), Type.Null()]),\n createdAt: Type.String(),\n updatedAt: Type.String()\n }),\n draft: Type.Array(Type.Object({\n id: Type.Number(),\n labelId: Type.Number(),\n version: Type.Null(),\n locale: Type.String(),\n breakpoint: Type.Union([Type.String(), Type.Null()]),\n value: Type.Any(),\n createdAt: Type.String()\n })),\n published: Type.Array(Type.Object({\n id: Type.Number(),\n labelId: Type.Number(),\n version: Type.Number(),\n locale: Type.String(),\n breakpoint: Type.Union([Type.String(), Type.Null()]),\n value: Type.Any(),\n createdAt: Type.String()\n })),\n status: Type.Union([\n Type.Literal('default-only'),\n Type.Literal('unpublished'),\n Type.Literal('published'),\n Type.Literal('modified')\n ])\n }),\n Type.Object({\n error: Type.String()\n })\n ])\n} as const satisfies RouteContract;\n\n/**\n * GET /_cms/labels/:id/versions - 라벨 버전 히스토리 조회\n */\nexport const getLabelVersionsContract = {\n method: 'GET' as const,\n path: '/_cms/labels/:id/versions',\n params: Type.Object({\n id: Type.String({ description: '라벨 ID' })\n }),\n response: Type.Union([\n Type.Object({\n versions: Type.Array(Type.Object({\n version: Type.Number({ description: '버전 번호' }),\n publishedAt: Type.String({ description: '발행 시각 (ISO 8601)' }),\n publishedBy: Type.Union([Type.String(), Type.Null()], { description: '발행자 ID' }),\n notes: Type.Union([Type.String(), Type.Null()], { description: '발행 노트' }),\n values: Type.Array(Type.Object({\n id: Type.Number(),\n locale: Type.String(),\n breakpoint: Type.Union([Type.String(), Type.Null()]),\n value: Type.Any(),\n createdAt: Type.String()\n }))\n }))\n }),\n Type.Object({\n error: Type.String()\n })\n ])\n} as const satisfies RouteContract;","/**\n * CMS Labels Repository\n *\n * 라벨 메타데이터 관리를 위한 Repository\n */\n\nimport { findOne, findMany as findManyHelper, create as createHelper, updateOne, deleteOne, count as countHelper } from '@spfn/core/db';\nimport { asc } from 'drizzle-orm';\nimport { cmsLabels, type CmsLabel, type NewCmsLabel } from '@/server/entities';\n\n/**\n * 라벨 목록 조회\n */\nexport async function findMany(options?: {\n section?: string;\n}): Promise<CmsLabel[]>\n{\n const { section } = options || {};\n\n return findManyHelper(cmsLabels, {\n where: section ? { section } : undefined,\n orderBy: asc(cmsLabels.key), // key 오름차순 정렬 (JSON 파일의 순서 유지)\n });\n}\n\n/**\n * 전체 라벨 수 조회\n */\nexport async function count(section?: string): Promise<number>\n{\n return countHelper(cmsLabels, section ? { section } : undefined);\n}\n\n/**\n * ID로 라벨 조회\n */\nexport async function findById(id: number): Promise<CmsLabel | null>\n{\n return findOne(cmsLabels, { id });\n}\n\n/**\n * Key로 라벨 조회\n */\nexport async function findByKey(key: string): Promise<CmsLabel | null>\n{\n return findOne(cmsLabels, { key });\n}\n\n/**\n * 섹션으로 모든 라벨 조회\n */\nexport async function findBySection(section: string): Promise<CmsLabel[]>\n{\n return findManyHelper(cmsLabels, {\n where: { section },\n orderBy: asc(cmsLabels.key), // key 오름차순 정렬 (JSON 파일의 순서 유지)\n });\n}\n\n/**\n * 라벨 생성\n */\nexport async function create(data: NewCmsLabel): Promise<CmsLabel>\n{\n return createHelper(cmsLabels, data);\n}\n\n/**\n * 라벨 수정\n */\nexport async function updateById(id: number, data: Partial<NewCmsLabel>): Promise<CmsLabel | null>\n{\n return updateOne(cmsLabels, { id }, { ...data, updatedAt: new Date() });\n}\n\n/**\n * 라벨 삭제\n */\nexport async function deleteById(id: number): Promise<CmsLabel | null>\n{\n return deleteOne(cmsLabels, { id });\n}\n\n// Legacy export for backward compatibility\nexport const cmsLabelsRepository = {\n findMany,\n count,\n findById,\n findByKey,\n findBySection,\n create,\n updateById,\n deleteById\n};","/**\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 */","/**\n * CMS Label Values Repository\n *\n * 라벨 값 관리를 위한 Repository\n */\n\nimport { findOne, findMany, create, updateOne, deleteMany } from '@spfn/core/db';\nimport { eq, and, SQL, isNull } from 'drizzle-orm';\nimport { cmsLabelValues, type CmsLabelValue, type NewCmsLabelValue } from '@/server/entities';\n\n/**\n * 특정 라벨의 특정 버전 값들 조회\n */\nexport async function findByLabelIdAndVersion(\n labelId: number,\n version: number,\n options?: {\n locale?: string;\n breakpoint?: string | null;\n }\n): Promise<CmsLabelValue[]>\n{\n const { locale, breakpoint } = options || {};\n\n const conditions: SQL[] = [\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n ];\n\n if (locale)\n {\n conditions.push(eq(cmsLabelValues.locale, locale));\n }\n\n if (breakpoint !== undefined)\n {\n conditions.push(\n breakpoint === null\n ? isNull(cmsLabelValues.breakpoint)\n : eq(cmsLabelValues.breakpoint, breakpoint)\n );\n }\n\n return findMany(cmsLabelValues, {\n where: and(...conditions)\n });\n}\n\n/**\n * 값 저장 (upsert)\n * - version: null → Draft 저장 (덮어쓰기)\n * - version: number → Published 버전 생성 (불변)\n */\nexport async function upsert(data: NewCmsLabelValue): Promise<CmsLabelValue>\n{\n // 기존 값이 있는지 확인\n const versionCondition = data.version === null || data.version === undefined\n ? isNull(cmsLabelValues.version)\n : eq(cmsLabelValues.version, data.version as number);\n\n const existing = await findOne(\n cmsLabelValues,\n and(\n eq(cmsLabelValues.labelId, data.labelId),\n versionCondition,\n eq(cmsLabelValues.locale, data.locale || 'ko'),\n data.breakpoint\n ? eq(cmsLabelValues.breakpoint, data.breakpoint)\n : isNull(cmsLabelValues.breakpoint)\n )\n );\n\n if (existing)\n {\n // UPDATE (only for drafts with version: null)\n if (data.version === null || data.version === undefined)\n {\n const updated = await updateOne(\n cmsLabelValues,\n { id: existing.id },\n { value: data.value }\n );\n return updated!;\n }\n else\n {\n // Published versions are immutable - this shouldn't happen\n throw new Error(`Published version ${data.version} already exists and cannot be overwritten`);\n }\n }\n else\n {\n // INSERT (both draft and new published versions)\n return create(cmsLabelValues, data);\n }\n}\n\n/**\n * Draft 값들 조회 (version = null)\n */\nexport async function findDraftsByLabelId(labelId: number): Promise<CmsLabelValue[]>\n{\n return findMany(cmsLabelValues, {\n where: and(\n eq(cmsLabelValues.labelId, labelId),\n isNull(cmsLabelValues.version)\n )\n });\n}\n\n/**\n * 여러 값 일괄 저장\n */\nexport async function upsertMany(values: NewCmsLabelValue[]): Promise<CmsLabelValue[]>\n{\n const results = [];\n for (const value of values)\n {\n const result = await upsert(value);\n results.push(result);\n }\n return results;\n}\n\n/**\n * 특정 버전의 모든 값 삭제\n */\nexport async function deleteByVersion(labelId: number, version: number): Promise<CmsLabelValue[]>\n{\n return deleteMany(\n cmsLabelValues,\n and(\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n )\n );\n}\n\n// Legacy export for backward compatibility\nexport const cmsLabelValuesRepository = {\n findByLabelIdAndVersion,\n findDraftsByLabelId,\n upsert,\n upsertMany,\n deleteByVersion\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","findOne","findMany","create","updateOne","findMany","findOne","updateOne","create"]}
|
|
@@ -186,13 +186,11 @@ var cmsAuditLogs = schema5.table("audit_logs", {
|
|
|
186
186
|
|
|
187
187
|
// src/server/repositories/cms-labels.repository.ts
|
|
188
188
|
async function findMany(options) {
|
|
189
|
-
const { section
|
|
189
|
+
const { section } = options || {};
|
|
190
190
|
return findManyHelper(cmsLabels, {
|
|
191
191
|
where: section ? { section } : void 0,
|
|
192
|
-
orderBy: asc(cmsLabels.key)
|
|
192
|
+
orderBy: asc(cmsLabels.key)
|
|
193
193
|
// key 오름차순 정렬 (JSON 파일의 순서 유지)
|
|
194
|
-
limit,
|
|
195
|
-
offset
|
|
196
194
|
});
|
|
197
195
|
}
|
|
198
196
|
async function count(section) {
|
|
@@ -250,9 +248,7 @@ var getLabelsContract = {
|
|
|
250
248
|
path: "/_cms/labels",
|
|
251
249
|
query: Type.Object({
|
|
252
250
|
section: Type.Optional(Type.String({ description: "\uC139\uC158\uC73C\uB85C \uD544\uD130\uB9C1 (\uC608: home, why-futureplay)" })),
|
|
253
|
-
|
|
254
|
-
offset: Type.Optional(Type.Number({ minimum: 0, default: 0, description: "\uC2DC\uC791 \uC624\uD504\uC14B" })),
|
|
255
|
-
includeDefaultValues: Type.Optional(Type.Boolean({ description: "\uAE30\uBCF8\uAC12 \uD3EC\uD568 \uC5EC\uBD80", default: false }))
|
|
251
|
+
includeDefaultValues: Type.Optional(Type.Boolean({ description: "\uAE30\uBCF8\uAC12 \uD3EC\uD568 \uC5EC\uBD80" }))
|
|
256
252
|
}),
|
|
257
253
|
response: Type.Object({
|
|
258
254
|
labels: Type.Array(Type.Object({
|
|
@@ -267,9 +263,7 @@ var getLabelsContract = {
|
|
|
267
263
|
updatedAt: Type.String(),
|
|
268
264
|
defaultValue: Type.Optional(Type.Any({ description: "\uB77C\uBCA8 \uC815\uC758 \uD30C\uC77C\uC758 \uAE30\uBCF8\uAC12" }))
|
|
269
265
|
})),
|
|
270
|
-
total: Type.Number()
|
|
271
|
-
limit: Type.Number(),
|
|
272
|
-
offset: Type.Number()
|
|
266
|
+
total: Type.Number()
|
|
273
267
|
})
|
|
274
268
|
};
|
|
275
269
|
var createLabelContract = {
|
|
@@ -408,9 +402,9 @@ var getLabelByKeyContract = {
|
|
|
408
402
|
};
|
|
409
403
|
var publishLabelContract = {
|
|
410
404
|
method: "POST",
|
|
411
|
-
path: "/_cms/labels/:
|
|
405
|
+
path: "/_cms/labels/:id/publish",
|
|
412
406
|
params: Type.Object({
|
|
413
|
-
|
|
407
|
+
id: Type.String({ description: "\uB77C\uBCA8 ID" })
|
|
414
408
|
}),
|
|
415
409
|
body: Type.Object({
|
|
416
410
|
notes: Type.Optional(Type.String({ description: "\uBC1C\uD589 \uB178\uD2B8 (\uBC84\uC804 \uC124\uBA85)" })),
|
|
@@ -419,7 +413,7 @@ var publishLabelContract = {
|
|
|
419
413
|
response: Type.Union([
|
|
420
414
|
Type.Object({
|
|
421
415
|
success: Type.Boolean(),
|
|
422
|
-
|
|
416
|
+
id: Type.Number(),
|
|
423
417
|
version: Type.Number(),
|
|
424
418
|
message: Type.String()
|
|
425
419
|
}),
|
|
@@ -430,9 +424,9 @@ var publishLabelContract = {
|
|
|
430
424
|
};
|
|
431
425
|
var getAdminLabelContract = {
|
|
432
426
|
method: "GET",
|
|
433
|
-
path: "/_cms/labels/:
|
|
427
|
+
path: "/_cms/labels/:id/admin",
|
|
434
428
|
params: Type.Object({
|
|
435
|
-
|
|
429
|
+
id: Type.String({ description: "\uB77C\uBCA8 ID" })
|
|
436
430
|
}),
|
|
437
431
|
response: Type.Union([
|
|
438
432
|
Type.Object({
|
|
@@ -479,9 +473,9 @@ var getAdminLabelContract = {
|
|
|
479
473
|
};
|
|
480
474
|
var getLabelVersionsContract = {
|
|
481
475
|
method: "GET",
|
|
482
|
-
path: "/_cms/labels/:
|
|
476
|
+
path: "/_cms/labels/:id/versions",
|
|
483
477
|
params: Type.Object({
|
|
484
|
-
|
|
478
|
+
id: Type.String({ description: "\uB77C\uBCA8 ID" })
|
|
485
479
|
}),
|
|
486
480
|
response: Type.Union([
|
|
487
481
|
Type.Object({
|