@spfn/cms 0.1.0-alpha.88 → 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +399 -50
  2. package/dist/actions.d.ts +15 -2
  3. package/dist/actions.js +15 -89
  4. package/dist/actions.js.map +1 -1
  5. package/dist/config.d.ts +39 -0
  6. package/dist/config.js +39 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/errors.d.ts +149 -0
  9. package/dist/errors.js +164 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +107 -85
  12. package/dist/index.js +197 -610
  13. package/dist/index.js.map +1 -1
  14. package/dist/server.d.ts +41 -148
  15. package/dist/server.js +473 -1619
  16. package/dist/server.js.map +1 -1
  17. package/migrations/0000_medical_ozymandias.sql +44 -0
  18. package/migrations/meta/0000_snapshot.json +8 -237
  19. package/migrations/meta/_journal.json +2 -2
  20. package/package.json +26 -35
  21. package/dist/actions-BEFWwQsh.d.ts +0 -195
  22. package/dist/api.d.ts +0 -319
  23. package/dist/api.js +0 -467
  24. package/dist/api.js.map +0 -1
  25. package/dist/client.d.ts +0 -146
  26. package/dist/client.js +0 -1321
  27. package/dist/client.js.map +0 -1
  28. package/dist/index-Dh5FjWzR.d.ts +0 -112
  29. package/dist/label-sync-generator-B0EmvtWM.d.ts +0 -32
  30. package/dist/lib/contracts/labels.d.ts +0 -244
  31. package/dist/lib/contracts/labels.js +0 -269
  32. package/dist/lib/contracts/labels.js.map +0 -1
  33. package/dist/lib/contracts/published-cache.d.ts +0 -48
  34. package/dist/lib/contracts/published-cache.js +0 -49
  35. package/dist/lib/contracts/published-cache.js.map +0 -1
  36. package/dist/lib/contracts/values.d.ts +0 -71
  37. package/dist/lib/contracts/values.js +0 -104
  38. package/dist/lib/contracts/values.js.map +0 -1
  39. package/dist/locale.constants-BNkSdNP1.d.ts +0 -108
  40. package/dist/server/entities/cms-audit-logs.d.ts +0 -158
  41. package/dist/server/entities/cms-audit-logs.js +0 -78
  42. package/dist/server/entities/cms-audit-logs.js.map +0 -1
  43. package/dist/server/entities/cms-draft-cache.d.ts +0 -128
  44. package/dist/server/entities/cms-draft-cache.js +0 -42
  45. package/dist/server/entities/cms-draft-cache.js.map +0 -1
  46. package/dist/server/entities/cms-label-values.d.ts +0 -141
  47. package/dist/server/entities/cms-label-values.js +0 -81
  48. package/dist/server/entities/cms-label-values.js.map +0 -1
  49. package/dist/server/entities/cms-labels.d.ts +0 -192
  50. package/dist/server/entities/cms-labels.js +0 -46
  51. package/dist/server/entities/cms-labels.js.map +0 -1
  52. package/dist/server/entities/cms-published-cache.d.ts +0 -144
  53. package/dist/server/entities/cms-published-cache.js +0 -40
  54. package/dist/server/entities/cms-published-cache.js.map +0 -1
  55. package/dist/server/entities/cms-schema.d.ts +0 -5
  56. package/dist/server/entities/cms-schema.js +0 -7
  57. package/dist/server/entities/cms-schema.js.map +0 -1
  58. package/dist/server/entities/index.d.ts +0 -6
  59. package/dist/server/entities/index.js +0 -181
  60. package/dist/server/entities/index.js.map +0 -1
  61. package/dist/server/generators/index.d.ts +0 -19
  62. package/dist/server/generators/index.js +0 -727
  63. package/dist/server/generators/index.js.map +0 -1
  64. package/dist/server/labels/index.d.ts +0 -1
  65. package/dist/server/labels/index.js +0 -33
  66. package/dist/server/labels/index.js.map +0 -1
  67. package/dist/server/repositories/index.d.ts +0 -212
  68. package/dist/server/repositories/index.js +0 -414
  69. package/dist/server/repositories/index.js.map +0 -1
  70. package/dist/server/routes/labels/[id]/admin/index.js +0 -675
  71. package/dist/server/routes/labels/[id]/admin/index.js.map +0 -1
  72. package/dist/server/routes/labels/[id]/index.js +0 -572
  73. package/dist/server/routes/labels/[id]/index.js.map +0 -1
  74. package/dist/server/routes/labels/[id]/publish/index.js +0 -716
  75. package/dist/server/routes/labels/[id]/publish/index.js.map +0 -1
  76. package/dist/server/routes/labels/[id]/versions/index.js +0 -544
  77. package/dist/server/routes/labels/[id]/versions/index.js.map +0 -1
  78. package/dist/server/routes/labels/_id_/admin/index.d.ts +0 -11
  79. package/dist/server/routes/labels/_id_/index.d.ts +0 -12
  80. package/dist/server/routes/labels/_id_/publish/index.d.ts +0 -11
  81. package/dist/server/routes/labels/_id_/versions/index.d.ts +0 -11
  82. package/dist/server/routes/labels/by-key/[key]/index.js +0 -521
  83. package/dist/server/routes/labels/by-key/[key]/index.js.map +0 -1
  84. package/dist/server/routes/labels/by-key/_key_/index.d.ts +0 -10
  85. package/dist/server/routes/labels/index.d.ts +0 -12
  86. package/dist/server/routes/labels/index.js +0 -680
  87. package/dist/server/routes/labels/index.js.map +0 -1
  88. package/dist/server/routes/published-cache/index.d.ts +0 -11
  89. package/dist/server/routes/published-cache/index.js +0 -333
  90. package/dist/server/routes/published-cache/index.js.map +0 -1
  91. package/dist/server/routes/values/[labelId]/[version]/index.js +0 -453
  92. package/dist/server/routes/values/[labelId]/[version]/index.js.map +0 -1
  93. package/dist/server/routes/values/[labelId]/index.js +0 -448
  94. package/dist/server/routes/values/[labelId]/index.js.map +0 -1
  95. package/dist/server/routes/values/_labelId_/_version_/index.d.ts +0 -10
  96. package/dist/server/routes/values/_labelId_/index.d.ts +0 -10
  97. package/migrations/0000_milky_blockbuster.sql +0 -72
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/server/routes/published-cache/index.ts","../../../../src/server/repositories/cms-labels.repository.ts","../../../../src/server/entities/cms-labels.ts","../../../../src/server/entities/cms-schema.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","../../../../src/lib/contracts/published-cache.ts"],"sourcesContent":["/**\n * CMS Published Cache Routes\n *\n * - GET /cms/published-cache - 발행된 콘텐츠 캐시 조회 (단일 또는 배치)\n * - POST /cms/published-cache - 발행된 콘텐츠 캐시 업데이트/생성 (upsert)\n */\n\nimport { createApp } from '@spfn/core/route';\nimport { cmsPublishedCacheRepository } from '@/server/repositories';\nimport { getPublishedCacheContract, upsertPublishedCacheContract } from '@/lib/contracts/published-cache';\n\nconst app = createApp();\n\n/**\n * GET /cms/published-cache\n * 발행된 섹션 콘텐츠 조회 (단일 또는 여러 섹션)\n */\napp.bind(getPublishedCacheContract, async (c) =>\n{\n const { sections: sectionsParam, locale = 'ko' } = c.query;\n\n // Normalize to array\n const sections = Array.isArray(sectionsParam) ? sectionsParam : [sectionsParam];\n\n // Fetch all sections in parallel\n const results = await Promise.all(\n sections.map(section => cmsPublishedCacheRepository.findBySection(section, locale))\n );\n\n // Map to response format (only include found sections)\n const found = results\n .filter((cache): cache is NonNullable<typeof cache> => cache !== null)\n .map(cache => ({\n section: cache.section,\n locale: cache.locale,\n content: cache.content as Record<string, any>,\n version: cache.version,\n publishedAt: cache.publishedAt?.toISOString() || null,\n }));\n\n return c.json(found);\n});\n\n/**\n * POST /cms/published-cache\n * 발행된 콘텐츠 캐시 업데이트/생성 (upsert)\n */\napp.bind(upsertPublishedCacheContract, async (c) =>\n{\n try\n {\n const { section, locale, content, version } = await c.data();\n\n // Upsert cache\n const result = await cmsPublishedCacheRepository.upsert({\n section,\n locale,\n content,\n version,\n publishedAt: new Date(),\n });\n\n return c.json({\n section: result.section,\n locale: result.locale,\n content: result.content as Record<string, any>,\n version: result.version,\n publishedAt: result.publishedAt?.toISOString() || null,\n });\n }\n catch (error)\n {\n console.error('[upsertPublishedCache] Error:', error);\n return c.json(\n { error: error instanceof Error ? error.message : 'Failed to upsert published cache' },\n 500\n );\n }\n});\n\nexport default app;","/**\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 { cmsSchema } from './cms-schema';\n\nexport const cmsLabels = cmsSchema.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 Schema Definition\n *\n * Creates isolated 'spfn_cms' PostgreSQL schema for CMS tables.\n * Export this schema so drizzle-kit can generate CREATE SCHEMA statement.\n */\nimport { createFunctionSchema } from '@spfn/core/db';\n\nexport const cmsSchema = createFunctionSchema('@spfn/cms');","/**\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 { cmsSchema } from './cms-schema';\nimport { cmsLabels } from '@/server/entities/cms-labels';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\n\nexport const cmsLabelValues = cmsSchema.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 { cmsSchema } from './cms-schema';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\n\nexport const cmsDraftCache = cmsSchema.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 { cmsSchema } from './cms-schema';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\n\nexport const cmsPublishedCache = cmsSchema.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 { cmsSchema } from './cms-schema';\nimport { cmsLabels } from '@/server/entities/cms-labels';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\n\nexport const cmsAuditLogs = cmsSchema.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};","import { Type } from '@sinclair/typebox';\nimport type { RouteContract } from '@spfn/core/route';\n\nconst SectionData = Type.Object({\n section: Type.String(),\n locale: Type.String(),\n content: Type.Record(Type.String(), Type.Any()),\n version: Type.Number(),\n publishedAt: Type.Union([Type.String(), Type.Null()]),\n});\n\n/**\n * GET /_cms/published-cache\n * 발행된 콘텐츠 캐시 조회 (단일 또는 여러 섹션)\n */\nexport const getPublishedCacheContract = {\n method: 'GET' as const,\n path: '/_cms/published-cache',\n query: Type.Object({\n sections: Type.Union([\n Type.String({ description: '단일 섹션 이름 (예: home)' }),\n Type.Array(Type.String(), { description: '여러 섹션 이름 (예: [\"home\", \"footer\"])' })\n ]),\n locale: Type.Optional(Type.String({ default: 'ko', description: '언어 코드' })),\n }),\n response: Type.Union([\n // 성공: 항상 배열로 반환\n Type.Array(SectionData),\n // 에러\n Type.Object({\n error: Type.String()\n })\n ])\n} as const satisfies RouteContract;\n\n/**\n * POST /_cms/published-cache\n * 발행된 콘텐츠 캐시 업데이트/생성 (upsert)\n */\nexport const upsertPublishedCacheContract = {\n method: 'POST' as const,\n path: '/_cms/published-cache',\n body: Type.Object({\n section: Type.String({ description: '섹션 이름 (예: home)' }),\n locale: Type.String({ description: '언어 코드 (예: ko, en, ja)' }),\n content: Type.Record(Type.String(), Type.Any(), { description: '발행할 콘텐츠 (key-value 형태)' }),\n version: Type.Number({ description: '버전 번호' })\n }),\n response: Type.Union([\n SectionData,\n Type.Object({\n error: Type.String()\n })\n ])\n} as const satisfies RouteContract;"],"mappings":";AAOA,SAAS,iBAAiB;;;ACD1B,SAAS,SAAS,YAAY,gBAAgB,UAAU,cAAc,WAAW,WAAW,SAAS,mBAAmB;AACxH,SAAS,WAAW;;;ACGpB,SAAS,OAAO,SAAS,QAAQ,MAAM,iBAAiB;;;ACJxD,SAAS,4BAA4B;AAE9B,IAAM,YAAY,qBAAqB,WAAW;;;ADKlD,IAAM,YAAY,UAAU,MAAM,UAAU;AAAA;AAAA,EAE/C,IAAI,OAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,KAAK,KAAK,KAAK,EAAE,QAAQ,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA,EAKlC,SAAS,KAAK,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA,EAIjC,MAAM,KAAK,MAAM,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI3B,cAAc,KAAK,eAAe;AAAA;AAAA;AAAA,EAIlC,aAAa,KAAK,aAAa;AAAA;AAAA;AAAA,EAI/B,kBAAkB,QAAQ,mBAAmB;AAAA;AAAA;AAAA;AAAA,EAK7C,WAAW,KAAK,YAAY;AAAA;AAAA,EAG5B,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AAAA,EAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEV,MAAM,wBAAwB,EAAE,GAAG,MAAM,OAAO;AAAA;AAAA,EAGhD,MAAM,oBAAoB,EAAE,GAAG,MAAM,GAAG;AAC5C,CAAC;;;AE7CD,SAAS,UAAAA,SAAQ,WAAAC,UAAS,QAAAC,OAAM,OAAO,aAAAC,YAAW,SAAAC,QAAO,cAAc;AAOhE,IAAM,iBAAiB,UAAU,MAAM,gBAAgB;AAAA;AAAA,EAE1D,IAAIC,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,SAAQ,UAAU,EACtB,QAAQ,EACR,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA;AAAA,EAG3D,SAASA,SAAQ,SAAS;AAAA;AAAA,EAG1B,QAAQC,MAAK,QAAQ,EAAE,QAAQ,EAAE,QAAQ,IAAI;AAAA;AAAA;AAAA,EAI7C,YAAYA,MAAK,YAAY;AAAA;AAAA;AAAA;AAAA,EAK7B,OAAO,MAAM,OAAO,EAAE,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS9B,WAAWC,WAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEV,OAAO,2CAA2C,EAC7C,GAAG,MAAM,SAAS,MAAM,SAAS,MAAM,QAAQ,MAAM,UAAU;AAAA;AAAA,EAGpEC,OAAM,oCAAoC,EACrC,GAAG,MAAM,SAAS,MAAM,OAAO;AAAA;AAAA,EAGpCA,OAAM,6BAA6B,EAAE,GAAG,MAAM,MAAM;AACxD,CAAC;;;AC9CD,SAAS,UAAAC,SAAQ,QAAAC,OAAM,SAAAC,QAAO,aAAAC,YAAW,SAAAC,QAAO,UAAAC,eAAc;AAMvD,IAAM,gBAAgB,UAAU,MAAM,eAAe;AAAA;AAAA,EAExD,IAAIC,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,MAAK,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA,EAIjC,QAAQA,MAAK,QAAQ,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI/B,QAAQA,MAAK,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA,EAIhC,SAASC,OAAM,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASlC,WAAWC,WAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEVC,QAAO,wBAAwB,EAC9B,GAAG,MAAM,SAAS,MAAM,QAAQ,MAAM,MAAM;AAAA;AAAA,EAG7CC,OAAM,6BAA6B,EAAE,GAAG,MAAM,OAAO;AAAA;AAAA,EAGrDA,OAAM,0BAA0B,EAAE,GAAG,MAAM,MAAM;AACrD,CAAC;;;AC5CD,SAAS,UAAAC,SAAQ,QAAAC,OAAM,SAAAC,QAAO,WAAAC,UAAS,aAAAC,YAAW,SAAAC,QAAO,UAAAC,eAAc;AAMhE,IAAM,oBAAoB,UAAU,MAAM,mBAAmB;AAAA;AAAA,EAEhE,IAAIC,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,MAAK,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA,EAIjC,QAAQA,MAAK,QAAQ,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI/B,SAASC,OAAM,SAAS,EAAE,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASlC,aAAaC,WAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ;AAAA,EACvE,aAAaF,MAAK,cAAc;AAAA;AAAA,EAGhC,SAASG,SAAQ,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC;AACnD,GAAG,CAAC,UAAU;AAAA;AAAA,EAEVC,QAAO,4BAA4B,EAAE,GAAG,MAAM,SAAS,MAAM,MAAM;AAAA;AAAA,EAGnEC,OAAM,iCAAiC,EAAE,GAAG,MAAM,OAAO;AAC7D,CAAC;;;AC1CD,SAAS,UAAAC,SAAQ,WAAAC,UAAS,QAAAC,OAAM,SAAAC,QAAO,aAAAC,YAAW,SAAAC,cAAa;AAOxD,IAAM,eAAe,UAAU,MAAM,cAAc;AAAA;AAAA,EAEtD,IAAIC,QAAO,IAAI,EAAE,WAAW;AAAA;AAAA,EAG5B,SAASC,SAAQ,UAAU,EAC1B,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,WAAW,CAAC;AAAA;AAAA,EAGxD,QAAQC,MAAK,QAAQ,EAAE,QAAQ;AAAA;AAAA;AAAA,EAI/B,QAAQA,MAAK,SAAS,EAAE,QAAQ;AAAA,EAChC,UAAUA,MAAK,WAAW;AAAA;AAAA,EAG1B,SAASC,OAAM,SAAS;AAAA;AAAA;AAAA,EAIxB,UAAUA,OAAM,UAAU;AAAA;AAAA;AAAA,EAI1B,WAAWC,WAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ,EAAE,WAAW;AACpF,GAAG,CAAC,UAAU;AAAA;AAAA,EAEVC,OAAM,6BAA6B,EAAE,GAAG,MAAM,OAAO;AAAA;AAAA,EAGrDA,OAAM,4BAA4B,EAAE,GAAG,MAAM,MAAM;AAAA;AAAA,EAGnDA,OAAM,2BAA2B,EAAE,GAAG,MAAM,MAAM;AAAA;AAAA,EAGlDA,OAAM,+BAA+B,EAAE,GAAG,MAAM,SAAS;AAC7D,CAAC;;;ACjDD,SAAS,WAAAC,UAAS,UAAU,QAAQ,aAAAC,YAAW,kBAAkB;AACjE,SAAS,IAAI,KAAU,cAAc;;;ACDrC,SAAS,WAAAC,UAAS,YAAAC,WAAU,aAAAC,YAAW,cAAAC,aAAY,UAAU,oBAAoB;AACjF,SAAS,MAAAC,KAAI,OAAAC,MAAK,UAAU;;;ACD5B,SAAS,WAAAC,UAAS,YAAAC,WAAU,aAAAC,YAAW,cAAAC,aAAY,UAAUC,qBAAoB;AACjF,SAAS,MAAAC,KAAI,OAAAC,MAAK,WAAW;AAM7B,eAAsB,cAAc,SAAiB,SAAiB,MACtE;AACI,SAAOC;AAAA,IACH;AAAA,IACAC;AAAA,MACIC,IAAG,kBAAkB,SAAS,OAAO;AAAA,MACrCA,IAAG,kBAAkB,QAAQ,MAAM;AAAA,IACvC;AAAA,EACJ;AACJ;AAKA,eAAsB,OAAO,MAC7B;AACI,SAAOC,cAAa,mBAAmB,MAAM;AAAA,IACzC,QAAQ,CAAC,kBAAkB,SAAS,kBAAkB,MAAM;AAAA,IAC5D,KAAK;AAAA,MACD,SAAS,KAAK;AAAA,MACd,aAAa,KAAK;AAAA,MAClB,aAAa,KAAK;AAAA,MAClB,SAAS,MAAM,kBAAkB,OAAO;AAAA;AAAA,IAC5C;AAAA,EACJ,CAAC;AACL;AAKA,eAAsB,iBAAiB,SACvC;AACI,SAAOC,UAAS,mBAAmB;AAAA,IAC/B,OAAOF,IAAG,kBAAkB,SAAS,OAAO;AAAA,EAChD,CAAC;AACL;AAKA,eAAsB,gBAAgB,SAAiB,QACvD;AACI,MAAI,QACJ;AACI,UAAMG;AAAA,MACF;AAAA,MACAJ;AAAA,QACIC,IAAG,kBAAkB,SAAS,OAAO;AAAA,QACrCA,IAAG,kBAAkB,QAAQ,MAAM;AAAA,MACvC;AAAA,IACJ;AAAA,EACJ,OAEA;AACI,UAAMI;AAAA,MACF;AAAA,MACAJ,IAAG,kBAAkB,SAAS,OAAO;AAAA,IACzC;AAAA,EACJ;AACJ;AAEO,IAAM,8BAA8B;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;;;AC/EA,SAAS,YAAY;AAGrB,IAAM,cAAc,KAAK,OAAO;AAAA,EAC5B,SAAS,KAAK,OAAO;AAAA,EACrB,QAAQ,KAAK,OAAO;AAAA,EACpB,SAAS,KAAK,OAAO,KAAK,OAAO,GAAG,KAAK,IAAI,CAAC;AAAA,EAC9C,SAAS,KAAK,OAAO;AAAA,EACrB,aAAa,KAAK,MAAM,CAAC,KAAK,OAAO,GAAG,KAAK,KAAK,CAAC,CAAC;AACxD,CAAC;AAMM,IAAM,4BAA4B;AAAA,EACrC,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO,KAAK,OAAO;AAAA,IACf,UAAU,KAAK,MAAM;AAAA,MACjB,KAAK,OAAO,EAAE,aAAa,wDAAqB,CAAC;AAAA,MACjD,KAAK,MAAM,KAAK,OAAO,GAAG,EAAE,aAAa,sEAAmC,CAAC;AAAA,IACjF,CAAC;AAAA,IACD,QAAQ,KAAK,SAAS,KAAK,OAAO,EAAE,SAAS,MAAM,aAAa,4BAAQ,CAAC,CAAC;AAAA,EAC9E,CAAC;AAAA,EACD,UAAU,KAAK,MAAM;AAAA;AAAA,IAEjB,KAAK,MAAM,WAAW;AAAA;AAAA,IAEtB,KAAK,OAAO;AAAA,MACR,OAAO,KAAK,OAAO;AAAA,IACvB,CAAC;AAAA,EACL,CAAC;AACL;AAMO,IAAM,+BAA+B;AAAA,EACxC,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM,KAAK,OAAO;AAAA,IACd,SAAS,KAAK,OAAO,EAAE,aAAa,2CAAkB,CAAC;AAAA,IACvD,QAAQ,KAAK,OAAO,EAAE,aAAa,iDAAwB,CAAC;AAAA,IAC5D,SAAS,KAAK,OAAO,KAAK,OAAO,GAAG,KAAK,IAAI,GAAG,EAAE,aAAa,iEAAyB,CAAC;AAAA,IACzF,SAAS,KAAK,OAAO,EAAE,aAAa,4BAAQ,CAAC;AAAA,EACjD,CAAC;AAAA,EACD,UAAU,KAAK,MAAM;AAAA,IACjB;AAAA,IACA,KAAK,OAAO;AAAA,MACR,OAAO,KAAK,OAAO;AAAA,IACvB,CAAC;AAAA,EACL,CAAC;AACL;;;AX3CA,IAAM,MAAM,UAAU;AAMtB,IAAI,KAAK,2BAA2B,OAAO,MAC3C;AACI,QAAM,EAAE,UAAU,eAAe,SAAS,KAAK,IAAI,EAAE;AAGrD,QAAM,WAAW,MAAM,QAAQ,aAAa,IAAI,gBAAgB,CAAC,aAAa;AAG9E,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC1B,SAAS,IAAI,aAAW,4BAA4B,cAAc,SAAS,MAAM,CAAC;AAAA,EACtF;AAGA,QAAM,QAAQ,QACT,OAAO,CAAC,UAA8C,UAAU,IAAI,EACpE,IAAI,YAAU;AAAA,IACX,SAAS,MAAM;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,aAAa,MAAM,aAAa,YAAY,KAAK;AAAA,EACrD,EAAE;AAEN,SAAO,EAAE,KAAK,KAAK;AACvB,CAAC;AAMD,IAAI,KAAK,8BAA8B,OAAO,MAC9C;AACI,MACA;AACI,UAAM,EAAE,SAAS,QAAQ,SAAS,QAAQ,IAAI,MAAM,EAAE,KAAK;AAG3D,UAAM,SAAS,MAAM,4BAA4B,OAAO;AAAA,MACpD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa,oBAAI,KAAK;AAAA,IAC1B,CAAC;AAED,WAAO,EAAE,KAAK;AAAA,MACV,SAAS,OAAO;AAAA,MAChB,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO;AAAA,MAChB,SAAS,OAAO;AAAA,MAChB,aAAa,OAAO,aAAa,YAAY,KAAK;AAAA,IACtD,CAAC;AAAA,EACL,SACO,OACP;AACI,YAAQ,MAAM,iCAAiC,KAAK;AACpD,WAAO,EAAE;AAAA,MACL,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,mCAAmC;AAAA,MACrF;AAAA,IACJ;AAAA,EACJ;AACJ,CAAC;AAED,IAAO,0BAAQ;","names":["serial","integer","text","timestamp","index","serial","integer","text","timestamp","index","serial","text","jsonb","timestamp","index","unique","serial","text","jsonb","timestamp","unique","index","serial","text","jsonb","integer","timestamp","index","unique","serial","text","jsonb","timestamp","integer","unique","index","serial","integer","text","jsonb","timestamp","index","serial","integer","text","jsonb","timestamp","index","findOne","updateOne","findOne","findMany","deleteOne","deleteMany","eq","and","findOne","findMany","deleteOne","deleteMany","upsertHelper","eq","and","findOne","and","eq","upsertHelper","findMany","deleteOne","deleteMany"]}
@@ -1,453 +0,0 @@
1
- // src/server/routes/values/[labelId]/[version]/index.ts
2
- import { createApp } from "@spfn/core/route";
3
-
4
- // src/server/repositories/cms-labels.repository.ts
5
- import { findOne, findMany as findManyHelper, create as createHelper, updateOne, deleteOne, count as countHelper } from "@spfn/core/db";
6
- import { asc } from "drizzle-orm";
7
-
8
- // src/server/entities/cms-labels.ts
9
- import { index, integer, serial, text, timestamp } from "drizzle-orm/pg-core";
10
-
11
- // src/server/entities/cms-schema.ts
12
- import { createFunctionSchema } from "@spfn/core/db";
13
- var cmsSchema = createFunctionSchema("@spfn/cms");
14
-
15
- // src/server/entities/cms-labels.ts
16
- var cmsLabels = cmsSchema.table("labels", {
17
- // Primary Key
18
- id: serial("id").primaryKey(),
19
- // 라벨 식별자
20
- key: text("key").notNull().unique(),
21
- // 예: "home.hero.title", "why-futureplay.hero.subtitle"
22
- // 구조: {section}.{component}.{property}
23
- // 섹션 분류 (페이지 단위)
24
- section: text("section").notNull(),
25
- // 예: "home", "why-futureplay", "team"
26
- // 값 타입
27
- type: text("type").notNull(),
28
- // "text" | "image" | "video" | "file" | "object"
29
- // 기본값
30
- defaultValue: text("default_value"),
31
- // 라벨의 기본값 (sync 시 설정)
32
- // 설명
33
- description: text("description"),
34
- // 라벨에 대한 설명 (optional)
35
- // 현재 발행된 버전 번호
36
- publishedVersion: integer("published_version"),
37
- // null = 미발행 상태
38
- // 1, 2, 3... = 발행된 버전 번호
39
- // 생성자 추적
40
- createdBy: text("created_by"),
41
- // 타임스탬프
42
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
43
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
44
- }, (table) => [
45
- // 인덱스: 섹션별 조회 최적화
46
- index("cms_labels_section_idx").on(table.section),
47
- // 인덱스: key로 조회 최적화 (unique 제약으로 자동 생성되지만 명시)
48
- index("cms_labels_key_idx").on(table.key)
49
- ]);
50
-
51
- // src/server/entities/cms-label-values.ts
52
- import { serial as serial2, integer as integer2, text as text2, jsonb, timestamp as timestamp2, index as index2, unique } from "drizzle-orm/pg-core";
53
- var cmsLabelValues = cmsSchema.table("label_values", {
54
- // Primary Key
55
- id: serial2("id").primaryKey(),
56
- // Foreign Key: cms_labels
57
- labelId: integer2("label_id").notNull().references(() => cmsLabels.id, { onDelete: "cascade" }),
58
- // 버전 번호 (null = draft, number = published version)
59
- version: integer2("version"),
60
- // 언어 코드
61
- locale: text2("locale").notNull().default("ko"),
62
- // "ko" | "en" | "ja"
63
- // 반응형 브레이크포인트
64
- breakpoint: text2("breakpoint"),
65
- // null = 기본값 (모든 화면 크기)
66
- // "sm" | "md" | "lg" | "xl" | "2xl"
67
- // 실제 값 (JSONB)
68
- value: jsonb("value").notNull(),
69
- // LabelValue 타입:
70
- // - TextValue: { type: "text", content: string }
71
- // - ImageValue: { type: "image", url: string, alt?: string, width?: number, height?: number }
72
- // - VideoValue: { type: "video", url: string, thumbnail?: string, duration?: number }
73
- // - FileValue: { type: "file", url: string, filename: string, size?: number }
74
- // - ObjectValue: { type: "object", fields: Record<string, LabelValue> }
75
- // 생성 시각
76
- createdAt: timestamp2("created_at", { withTimezone: true }).notNull().defaultNow()
77
- }, (table) => [
78
- // UNIQUE 제약: 같은 버전에서 locale + breakpoint 조합은 유일
79
- unique("cms_label_values_locale_breakpoint_unique").on(table.labelId, table.version, table.locale, table.breakpoint),
80
- // 인덱스: labelId + version 복합 조회 최적화
81
- index2("cms_label_values_label_version_idx").on(table.labelId, table.version),
82
- // 인덱스: locale 필터링 최적화
83
- index2("cms_label_values_locale_idx").on(table.locale)
84
- ]);
85
-
86
- // src/server/entities/cms-draft-cache.ts
87
- import { serial as serial3, text as text3, jsonb as jsonb2, timestamp as timestamp3, index as index3, unique as unique2 } from "drizzle-orm/pg-core";
88
- var cmsDraftCache = cmsSchema.table("draft_cache", {
89
- // Primary Key
90
- id: serial3("id").primaryKey(),
91
- // 섹션 (페이지 단위)
92
- section: text3("section").notNull(),
93
- // "home" | "why-futureplay" | "team" | "our-companies" | "apply"
94
- // 언어
95
- locale: text3("locale").notNull(),
96
- // "ko" | "en" | "ja"
97
- // 사용자 ID (핵심 필드!)
98
- userId: text3("user_id").notNull(),
99
- // 각 관리자의 독립적인 작업 공간
100
- // Draft 콘텐츠 (JSONB)
101
- content: jsonb2("content").notNull(),
102
- // Record<string, LabelValue>
103
- // {
104
- // "home.hero.title": { type: "text", content: "수정 중..." },
105
- // "home.hero.subtitle": { type: "text", content: "새로운 문구" },
106
- // ...
107
- // }
108
- // 최종 수정 시각
109
- updatedAt: timestamp3("updated_at", { withTimezone: true }).notNull().defaultNow()
110
- }, (table) => [
111
- // UNIQUE 제약: section + locale + userId 조합은 유일
112
- unique2("cms_draft_cache_unique").on(table.section, table.locale, table.userId),
113
- // 인덱스: section으로 조회 최적화
114
- index3("cms_draft_cache_section_idx").on(table.section),
115
- // 인덱스: userId로 사용자의 모든 draft 조회 최적화
116
- index3("cms_draft_cache_user_idx").on(table.userId)
117
- ]);
118
-
119
- // src/server/entities/cms-published-cache.ts
120
- import { serial as serial4, text as text4, jsonb as jsonb3, integer as integer3, timestamp as timestamp4, index as index4, unique as unique3 } from "drizzle-orm/pg-core";
121
- var cmsPublishedCache = cmsSchema.table("published_cache", {
122
- // Primary Key
123
- id: serial4("id").primaryKey(),
124
- // 섹션 (페이지 단위)
125
- section: text4("section").notNull(),
126
- // "home" | "why-futureplay" | "team" | "our-companies" | "apply"
127
- // 언어
128
- locale: text4("locale").notNull(),
129
- // "ko" | "en" | "ja"
130
- // 캐시된 콘텐츠 (JSONB)
131
- content: jsonb3("content").notNull(),
132
- // Record<string, LabelValue>
133
- // {
134
- // "home.hero.title": { type: "text", content: "..." },
135
- // "home.hero.image": { type: "image", url: "...", alt: "..." },
136
- // ...
137
- // }
138
- // 발행 정보
139
- publishedAt: timestamp4("published_at", { withTimezone: true }).notNull(),
140
- publishedBy: text4("published_by"),
141
- // 캐시 버전 (클라이언트 캐싱용)
142
- version: integer3("version").notNull().default(1)
143
- }, (table) => [
144
- // UNIQUE 제약: section + locale 조합은 유일
145
- unique3("cms_published_cache_unique").on(table.section, table.locale),
146
- // 인덱스: section으로 조회 최적화
147
- index4("cms_published_cache_section_idx").on(table.section)
148
- ]);
149
-
150
- // src/server/entities/cms-audit-logs.ts
151
- import { serial as serial5, integer as integer4, text as text5, jsonb as jsonb4, timestamp as timestamp5, index as index5 } from "drizzle-orm/pg-core";
152
- var cmsAuditLogs = cmsSchema.table("audit_logs", {
153
- // Primary Key
154
- id: serial5("id").primaryKey(),
155
- // Foreign Key: cms_labels (nullable - 라벨 삭제 시 로그는 유지)
156
- labelId: integer4("label_id").references(() => cmsLabels.id, { onDelete: "set null" }),
157
- // 작업 유형
158
- action: text5("action").notNull(),
159
- // "create" | "update" | "publish" | "unpublish" | "archive" | "delete" | "rollback" | "duplicate"
160
- // 사용자 정보
161
- userId: text5("user_id").notNull(),
162
- userName: text5("user_name"),
163
- // 변경 내용 (before/after)
164
- changes: jsonb4("changes"),
165
- // { before: {...}, after: {...} }
166
- // 추가 메타데이터
167
- metadata: jsonb4("metadata"),
168
- // { version: number, ip: string, userAgent: string, ... }
169
- // 작업 시각
170
- createdAt: timestamp5("created_at", { withTimezone: true }).notNull().defaultNow()
171
- }, (table) => [
172
- // 인덱스: labelId로 이력 조회 최적화
173
- index5("cms_audit_logs_label_id_idx").on(table.labelId),
174
- // 인덱스: userId로 사용자 활동 조회 최적화
175
- index5("cms_audit_logs_user_id_idx").on(table.userId),
176
- // 인덱스: action 필터링 최적화
177
- index5("cms_audit_logs_action_idx").on(table.action),
178
- // 인덱스: 시간순 조회 최적화
179
- index5("cms_audit_logs_created_at_idx").on(table.createdAt)
180
- ]);
181
-
182
- // src/server/repositories/cms-labels.repository.ts
183
- async function findMany(options) {
184
- const { section } = options || {};
185
- return findManyHelper(cmsLabels, {
186
- where: section ? { section } : void 0,
187
- orderBy: asc(cmsLabels.key)
188
- // key 오름차순 정렬 (JSON 파일의 순서 유지)
189
- });
190
- }
191
- async function count(section) {
192
- return countHelper(cmsLabels, section ? { section } : void 0);
193
- }
194
- async function findById(id) {
195
- return findOne(cmsLabels, { id });
196
- }
197
- async function findByKey(key) {
198
- return findOne(cmsLabels, { key });
199
- }
200
- async function findBySection(section) {
201
- return findManyHelper(cmsLabels, {
202
- where: { section },
203
- orderBy: asc(cmsLabels.key)
204
- // key 오름차순 정렬 (JSON 파일의 순서 유지)
205
- });
206
- }
207
- async function create(data) {
208
- return createHelper(cmsLabels, data);
209
- }
210
- async function updateById(id, data) {
211
- return updateOne(cmsLabels, { id }, { ...data, updatedAt: /* @__PURE__ */ new Date() });
212
- }
213
- async function deleteById(id) {
214
- return deleteOne(cmsLabels, { id });
215
- }
216
- var cmsLabelsRepository = {
217
- findMany,
218
- count,
219
- findById,
220
- findByKey,
221
- findBySection,
222
- create,
223
- updateById,
224
- deleteById
225
- };
226
-
227
- // src/server/repositories/cms-label-values.repository.ts
228
- import { findOne as findOne2, findMany as findMany2, create as create2, updateOne as updateOne2, deleteMany } from "@spfn/core/db";
229
- import { eq, and, isNull } from "drizzle-orm";
230
- async function findByLabelIdAndVersion(labelId, version, options) {
231
- const { locale, breakpoint } = options || {};
232
- const conditions = [
233
- eq(cmsLabelValues.labelId, labelId),
234
- eq(cmsLabelValues.version, version)
235
- ];
236
- if (locale) {
237
- conditions.push(eq(cmsLabelValues.locale, locale));
238
- }
239
- if (breakpoint !== void 0) {
240
- conditions.push(
241
- breakpoint === null ? isNull(cmsLabelValues.breakpoint) : eq(cmsLabelValues.breakpoint, breakpoint)
242
- );
243
- }
244
- return findMany2(cmsLabelValues, {
245
- where: and(...conditions)
246
- });
247
- }
248
- async function upsert(data) {
249
- const versionCondition = data.version === null || data.version === void 0 ? isNull(cmsLabelValues.version) : eq(cmsLabelValues.version, data.version);
250
- const existing = await findOne2(
251
- cmsLabelValues,
252
- and(
253
- eq(cmsLabelValues.labelId, data.labelId),
254
- versionCondition,
255
- eq(cmsLabelValues.locale, data.locale || "ko"),
256
- data.breakpoint ? eq(cmsLabelValues.breakpoint, data.breakpoint) : isNull(cmsLabelValues.breakpoint)
257
- )
258
- );
259
- if (existing) {
260
- if (data.version === null || data.version === void 0) {
261
- const updated = await updateOne2(
262
- cmsLabelValues,
263
- { id: existing.id },
264
- { value: data.value }
265
- );
266
- return updated;
267
- } else {
268
- throw new Error(`Published version ${data.version} already exists and cannot be overwritten`);
269
- }
270
- } else {
271
- return create2(cmsLabelValues, data);
272
- }
273
- }
274
- async function findDraftsByLabelId(labelId) {
275
- return findMany2(cmsLabelValues, {
276
- where: and(
277
- eq(cmsLabelValues.labelId, labelId),
278
- isNull(cmsLabelValues.version)
279
- )
280
- });
281
- }
282
- async function upsertMany(values) {
283
- const results = [];
284
- for (const value of values) {
285
- const result = await upsert(value);
286
- results.push(result);
287
- }
288
- return results;
289
- }
290
- async function deleteByVersion(labelId, version) {
291
- return deleteMany(
292
- cmsLabelValues,
293
- and(
294
- eq(cmsLabelValues.labelId, labelId),
295
- eq(cmsLabelValues.version, version)
296
- )
297
- );
298
- }
299
- var cmsLabelValuesRepository = {
300
- findByLabelIdAndVersion,
301
- findDraftsByLabelId,
302
- upsert,
303
- upsertMany,
304
- deleteByVersion
305
- };
306
-
307
- // src/server/repositories/cms-draft-cache.repository.ts
308
- import { findOne as findOne3, findMany as findMany3, deleteOne as deleteOne2, deleteMany as deleteMany2, upsert as upsertHelper } from "@spfn/core/db";
309
- import { eq as eq2, and as and2, lt } from "drizzle-orm";
310
-
311
- // src/server/repositories/cms-published-cache.repository.ts
312
- import { findOne as findOne4, findMany as findMany4, deleteOne as deleteOne3, deleteMany as deleteMany3, upsert as upsertHelper2 } from "@spfn/core/db";
313
- import { eq as eq3, and as and3, sql } from "drizzle-orm";
314
-
315
- // src/lib/contracts/values.ts
316
- import { Type } from "@sinclair/typebox";
317
- var LabelValueSchema = Type.Object({
318
- type: Type.Union([
319
- Type.Literal("text"),
320
- Type.Literal("image"),
321
- Type.Literal("video"),
322
- Type.Literal("file"),
323
- Type.Literal("object")
324
- ]),
325
- content: Type.Optional(Type.String()),
326
- // text type
327
- url: Type.Optional(Type.String()),
328
- // image, video, file types (required for these types but optional in schema)
329
- alt: Type.Optional(Type.String()),
330
- // image type
331
- width: Type.Optional(Type.Number()),
332
- // image type
333
- height: Type.Optional(Type.Number()),
334
- // image type
335
- thumbnail: Type.Optional(Type.String()),
336
- // video type
337
- duration: Type.Optional(Type.Number()),
338
- // video type
339
- filename: Type.Optional(Type.String()),
340
- // file type
341
- size: Type.Optional(Type.Number()),
342
- // file type
343
- fields: Type.Optional(Type.Any())
344
- // object type - recursive structure
345
- });
346
- var saveValuesContract = {
347
- method: "POST",
348
- path: "/_cms/values/:labelId",
349
- params: Type.Object({
350
- labelId: Type.String({ description: "\uB77C\uBCA8 ID" })
351
- }),
352
- body: Type.Object({
353
- version: Type.Union([
354
- Type.Null({ description: "Draft \uC800\uC7A5 (\uB36E\uC5B4\uC4F0\uAE30)" }),
355
- Type.Number({ description: "\uBC84\uC804 \uBC88\uD638 (\uBD88\uBCC0)", minimum: 1 })
356
- ]),
357
- values: Type.Array(
358
- Type.Object({
359
- locale: Type.String({ description: "\uC5B8\uC5B4 \uCF54\uB4DC (ko, en, ja)", default: "ko" }),
360
- breakpoint: Type.Optional(Type.Union([
361
- Type.Literal("sm"),
362
- Type.Literal("md"),
363
- Type.Literal("lg"),
364
- Type.Literal("xl"),
365
- Type.Literal("2xl"),
366
- Type.Null()
367
- ], { description: "\uBC18\uC751\uD615 \uBE0C\uB808\uC774\uD06C\uD3EC\uC778\uD2B8" })),
368
- value: LabelValueSchema
369
- // 모든 라벨 값은 객체 형태 (type 필드 필수)
370
- })
371
- )
372
- }),
373
- response: Type.Union([
374
- Type.Object({
375
- success: Type.Boolean(),
376
- saved: Type.Number(),
377
- version: Type.Union([Type.Null(), Type.Number()])
378
- }),
379
- Type.Object({
380
- error: Type.String()
381
- })
382
- ])
383
- };
384
- var getValuesContract = {
385
- method: "GET",
386
- path: "/_cms/values/:labelId/:version",
387
- params: Type.Object({
388
- labelId: Type.String({ description: "\uB77C\uBCA8 ID" }),
389
- version: Type.String({ description: "\uBC84\uC804 \uBC88\uD638" })
390
- }),
391
- query: Type.Object({
392
- locale: Type.Optional(Type.String({ description: "\uC5B8\uC5B4 \uCF54\uB4DC (ko, en, ja)" })),
393
- breakpoint: Type.Optional(Type.String({ description: "\uBC18\uC751\uD615 \uBE0C\uB808\uC774\uD06C\uD3EC\uC778\uD2B8" }))
394
- }),
395
- response: Type.Union([
396
- Type.Object({
397
- labelId: Type.Number(),
398
- version: Type.Number(),
399
- values: Type.Array(
400
- Type.Object({
401
- id: Type.Number(),
402
- locale: Type.String(),
403
- breakpoint: Type.Union([Type.String(), Type.Null()]),
404
- value: Type.Any(),
405
- createdAt: Type.String()
406
- })
407
- )
408
- }),
409
- Type.Object({
410
- error: Type.String()
411
- })
412
- ])
413
- };
414
-
415
- // src/server/routes/values/[labelId]/[version]/index.ts
416
- var app = createApp();
417
- app.bind(getValuesContract, async (c) => {
418
- const { labelId: labelIdStr, version: versionStr } = c.params;
419
- const labelId = parseInt(labelIdStr, 10);
420
- const version = parseInt(versionStr, 10);
421
- if (isNaN(labelId) || isNaN(version)) {
422
- return c.json({ error: "Invalid label ID or version" }, 400);
423
- }
424
- const { locale, breakpoint } = c.query;
425
- const label = await cmsLabelsRepository.findById(labelId);
426
- if (!label) {
427
- return c.json({ error: "Label not found" }, 404);
428
- }
429
- const values = await cmsLabelValuesRepository.findByLabelIdAndVersion(
430
- labelId,
431
- version,
432
- {
433
- locale,
434
- breakpoint: breakpoint === "null" ? null : breakpoint
435
- }
436
- );
437
- return c.json({
438
- labelId,
439
- version,
440
- values: values.map((v) => ({
441
- id: v.id,
442
- locale: v.locale,
443
- breakpoint: v.breakpoint,
444
- value: v.value,
445
- createdAt: v.createdAt.toISOString()
446
- }))
447
- });
448
- });
449
- var version_default = app;
450
- export {
451
- version_default as default
452
- };
453
- //# sourceMappingURL=index.js.map