@spfn/cms 0.2.0-beta.24 → 0.2.0-beta.26

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/server.js CHANGED
@@ -708,7 +708,12 @@ async function publishSection(section, locales) {
708
708
  });
709
709
  }
710
710
  });
711
- await rebuildSectionCache(section, locales);
711
+ const newVersionById = new Map(versionUpdates.map((v) => [v.id, v.version]));
712
+ const labelsAfterPublish = labels.map((l) => ({
713
+ ...l,
714
+ publishedVersion: newVersionById.get(l.id) ?? l.publishedVersion
715
+ }));
716
+ await rebuildSectionCache(section, locales, labelsAfterPublish);
712
717
  }
713
718
  publishLogger.info("Section published", {
714
719
  section,
@@ -735,9 +740,9 @@ async function resetSectionDraft(section) {
735
740
  publishLogger.info("Draft reset", { section, reset });
736
741
  return { reset };
737
742
  }
738
- async function rebuildSectionCache(section, locales) {
743
+ async function rebuildSectionCache(section, locales, preloadedLabels) {
739
744
  publishLogger.debug("rebuildSectionCache", { section, locales });
740
- const labels = await cmsLabelsRepository.findBySection(section);
745
+ const labels = preloadedLabels ?? await cmsLabelsRepository.findBySection(section);
741
746
  const labelVersions = labels.filter((l) => l.publishedVersion !== null).map((l) => ({ labelId: l.id, version: l.publishedVersion }));
742
747
  if (labelVersions.length === 0) {
743
748
  return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/server/routes/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-published-cache.ts","../src/server/repositories/cms-label-values.repository.ts","../src/server/repositories/cms-published-cache.repository.ts","../src/server/routes/admin.routes.ts","../src/server/services/publish.service.ts","../src/server/services/sync.service.ts","../src/lib/helpers.ts"],"sourcesContent":["import '@spfn/cms/config';\n\nexport { cmsAppRouter } from './server/routes';\nexport { syncLabels } from './server/services';\n","/**\n * CMS App Router\n *\n * 모든 CMS 라우트를 통합하는 메인 라우터\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { defineRouter, route } from '@spfn/core/route';\nimport { cmsPublishedCacheRepository } from '../repositories';\nimport {\n getSectionLabelsRoute,\n saveSectionDraftRoute,\n publishSectionRoute,\n resetSectionDraftRoute,\n} from './admin.routes';\n\nexport const getLabelCache = route.get('/_cms/labels/cache')\n .skip(['auth'])\n .input({\n query: Type.Object({\n sections: Type.Array(Type.String()),\n locale: Type.Optional(Type.String()),\n }),\n })\n .handler(async (c) =>\n {\n const { query } = await c.data();\n const { sections, locale = 'en' } = query;\n\n // 단일 쿼리로 모든 섹션 조회 (N+1 방지)\n const results = await cmsPublishedCacheRepository.findBySections(sections, locale);\n\n // Record<section, content> 형태로 변환\n return results.reduce((acc, item) => \n {\n acc[item.section] = item.content;\n\n return acc;\n }, {} as Record<string, any>);\n });\n\nexport const cmsAppRouter = defineRouter({\n getLabelCache,\n // Admin routes\n getSectionLabels: getSectionLabelsRoute,\n saveSectionDraft: saveSectionDraftRoute,\n publishSection: publishSectionRoute,\n resetSectionDraft: resetSectionDraftRoute,\n});\n\nexport type AppRouter = typeof cmsAppRouter;\n\n// Re-export admin router for standalone use\nexport { cmsAdminRouter } from './admin.routes';\nexport type { CmsAdminRouter } from './admin.routes';\n","/**\n * CMS Labels Repository\n *\n * 라벨 메타데이터 관리를 위한 Repository\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { asc, count as drizzleCount, eq, inArray } from 'drizzle-orm';\nimport { type CmsLabel, cmsLabels, type NewCmsLabel } from '../entities';\n\n/**\n * CMS Labels Repository 클래스\n *\n * BaseRepository를 상속받아 다음 기능을 제공:\n * - 자동 트랜잭션 컨텍스트 감지 및 사용\n * - Read/Write 연결 분리 (replica 활용)\n * - 타입 안전성\n */\nexport class CmsLabelsRepository extends BaseRepository\n{\n /**\n * 라벨 목록 조회\n * Read replica 사용\n */\n async findMany(options?: {\n section?: string;\n }): Promise<CmsLabel[]>\n {\n const { section } = options || {};\n\n let query = this.readDb\n .select()\n .from(cmsLabels)\n .orderBy(asc(cmsLabels.key)); // key 오름차순 정렬 (JSON 파일의 순서 유지)\n\n if (section)\n {\n query = query.where(eq(cmsLabels.section, section)) as typeof query;\n }\n\n return query;\n }\n\n /**\n * 전체 라벨 수 조회\n * Read replica 사용\n */\n async count(section?: string): Promise<number>\n {\n const query = this.readDb\n .select({ count: drizzleCount() })\n .from(cmsLabels);\n\n const result = section\n ? await query.where(eq(cmsLabels.section, section))\n : await query;\n\n return result[0]?.count ?? 0;\n }\n\n /**\n * ID로 라벨 조회\n * Read replica 사용\n */\n async findById(id: number): Promise<CmsLabel | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.id, id))\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * Key로 라벨 조회\n * Read replica 사용\n */\n async findByKey(key: string): Promise<CmsLabel | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.key, key))\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * 섹션으로 모든 라벨 조회\n * Read replica 사용\n */\n async findBySection(section: string): Promise<CmsLabel[]>\n {\n return this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.section, section))\n .orderBy(asc(cmsLabels.key)); // key 오름차순 정렬 (JSON 파일의 순서 유지)\n }\n\n /**\n * 라벨 생성\n * Write primary 사용\n */\n async create(data: NewCmsLabel): Promise<CmsLabel>\n {\n const result = await this.db\n .insert(cmsLabels)\n .values(data)\n .returning();\n\n return result[0];\n }\n\n /**\n * 라벨 수정\n * Write primary 사용\n */\n async updateById(id: number, data: Partial<NewCmsLabel>): Promise<CmsLabel | null>\n {\n const result = await this.db\n .update(cmsLabels)\n .set({ ...data, updatedAt: new Date() })\n .where(eq(cmsLabels.id, id))\n .returning();\n\n return result[0] ?? null;\n }\n\n /**\n * 라벨 삭제\n * Write primary 사용\n */\n async deleteById(id: number): Promise<CmsLabel | null>\n {\n const result = await this.db\n .delete(cmsLabels)\n .where(eq(cmsLabels.id, id))\n .returning();\n\n return result[0] ?? null;\n }\n\n /**\n * 여러 key로 라벨 조회\n * Read replica 사용\n */\n async findByKeys(keys: string[]): Promise<CmsLabel[]>\n {\n if (keys.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsLabels)\n .where(inArray(cmsLabels.key, keys));\n }\n\n /**\n * 여러 라벨 한번에 생성\n * Write primary 사용\n */\n async bulkCreate(data: NewCmsLabel[]): Promise<CmsLabel[]>\n {\n if (data.length === 0)\n {\n return [];\n }\n\n return this.db\n .insert(cmsLabels)\n .values(data)\n .returning();\n }\n\n /**\n * 여러 라벨 한번에 수정 (key 기준)\n * Write primary 사용\n *\n * @param updates - Array of { key, data } objects\n */\n async bulkUpdateByKeys(updates: Array<{ key: string; data: Partial<NewCmsLabel> }>): Promise<void>\n {\n if (updates.length === 0)\n {\n return;\n }\n\n // Drizzle doesn't support bulk update directly, so we need to do it one by one\n // But we can do it in a single transaction context (handled by BaseRepository)\n for (const { key, data } of updates)\n {\n await this.db\n .update(cmsLabels)\n .set({ ...data, updatedAt: new Date() })\n .where(eq(cmsLabels.key, key));\n }\n }\n\n /**\n * 여러 라벨 한번에 삭제 (key 기준)\n * Write primary 사용\n */\n async bulkDeleteByKeys(keys: string[]): Promise<CmsLabel[]>\n {\n if (keys.length === 0)\n {\n return [];\n }\n\n return this.db\n .delete(cmsLabels)\n .where(inArray(cmsLabels.key, keys))\n .returning();\n }\n}\n\n// Default instance export\nexport const cmsLabelsRepository = new CmsLabelsRepository();\n","/**\n * CMS Labels Entity\n *\n * 라벨의 메타데이터와 현재 발행 상태를 관리합니다.\n * - 라벨 식별 (id, key)\n * - 섹션 분류 (section)\n * - 타입 정의 (type)\n * - 발행 상태 (publishedVersion)\n */\n\nimport { index, integer, text } from 'drizzle-orm/pg-core';\nimport { id, timestamps, typedJsonb } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\n\nexport const cmsLabels = cmsSchema.table('labels', {\n // Primary Key\n id: id(),\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: typedJsonb<Record<string, any>>('default_value'),\n // 라벨의 기본값 (sync 시 설정)\n // 예: { en: \"Welcome\", ko: \"환영합니다\" } 또는 단일 값\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 ...timestamps(),\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","/**\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 { createSchema } from '@spfn/core/db';\n\nexport const cmsSchema = createSchema('@spfn/cms');\n","/**\n * CMS Label Values Entity\n *\n * 라벨의 실제 값을 저장합니다.\n * - 다국어 지원 (locale)\n * - 반응형 지원 (breakpoint)\n * - 버전 관리 (version)\n * - JSONB로 유연한 값 저장\n */\n\nimport { integer, text, index, unique } from 'drizzle-orm/pg-core';\nimport { id, utcTimestamp, typedJsonb, foreignKey } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\nimport { cmsLabels } from './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: id(),\n\n // Foreign Key: cms_labels\n labelId: foreignKey('label', () => cmsLabels.id, { onDelete: 'cascade' }),\n\n // 버전 번호 (null = draft, number = published version)\n version: integer('version'),\n\n // 언어 코드\n locale: text('locale').notNull().default('en'),\n // \"ko\" | \"en\" | \"ja\"\n\n // 반응형 브레이크포인트\n breakpoint: text('breakpoint'),\n // null = 기본값 (모든 화면 크기)\n // \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n\n // 실제 값 (JSONB)\n value: typedJsonb<Record<string, any>>('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: utcTimestamp('created_at').defaultNow().notNull(),\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","/**\n * CMS Published Cache Entity\n *\n * 발행된 콘텐츠를 섹션+언어 단위로 캐싱합니다.\n * - 초고속 읽기 성능 (5ms)\n * - 단일 쿼리로 섹션 전체 로드\n * - JSONB로 즉시 사용 가능한 데이터\n *\n * 성능 비교:\n * - 정규화 테이블 JOIN: 87ms\n * - 캐시 테이블: 5ms (17배 빠름!)\n */\n\nimport { text, integer, index, unique } from 'drizzle-orm/pg-core';\nimport { id, publishingFields, typedJsonb } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\nexport const cmsPublishedCache = cmsSchema.table('published_cache', {\n // Primary Key\n id: id(),\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: typedJsonb<Record<string, any>>('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 ...publishingFields(),\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","/**\n * CMS Label Values Repository\n *\n * 라벨 값 관리를 위한 Repository\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { eq, and, or, SQL, isNull, gte, lte, inArray, sql } from 'drizzle-orm';\nimport { cmsLabelValues, type CmsLabelValue, type NewCmsLabelValue } from '../entities';\n\n/**\n * 버전 히스토리 타입\n */\nexport interface VersionHistory\n{\n version: number;\n publishedAt: string;\n publishedBy: null;\n notes: null;\n values: Array<{\n id: number;\n locale: string;\n breakpoint: string | null;\n value: any;\n createdAt: string;\n }>;\n}\n\n/**\n * CMS Label Values Repository 클래스\n */\nexport class CmsLabelValuesRepository extends BaseRepository\n{\n /**\n * 특정 라벨의 특정 버전 값들 조회\n * Read replica 사용\n */\n async 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 this.readDb\n .select()\n .from(cmsLabelValues)\n .where(and(...conditions));\n }\n\n /**\n * 값 저장 (upsert)\n * - version: null → Draft 저장 (덮어쓰기)\n * - version: number → Published 버전 생성 (불변)\n * Write primary 사용\n */\n async upsert(data: NewCmsLabelValue & { labelId: number }): 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 existingResult = await this.db\n .select()\n .from(cmsLabelValues)\n .where(\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 .limit(1);\n\n const existing = existingResult[0];\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 this.db\n .update(cmsLabelValues)\n .set({ value: data.value })\n .where(eq(cmsLabelValues.id, existing.id))\n .returning();\n\n return updated[0];\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 const inserted = await this.db\n .insert(cmsLabelValues)\n .values(data)\n .returning();\n\n return inserted[0];\n }\n }\n\n /**\n * Draft 값들 조회 (version = null)\n * Read replica 사용\n */\n async findDraftsByLabelId(labelId: number): Promise<CmsLabelValue[]>\n {\n return this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n isNull(cmsLabelValues.version),\n ),\n );\n }\n\n /**\n * 여러 값 일괄 저장\n * Write primary 사용\n */\n async upsertMany(values: (NewCmsLabelValue & { labelId: number })[]): Promise<CmsLabelValue[]>\n {\n const results = [];\n for (const value of values)\n {\n const result = await this.upsert(value);\n results.push(result);\n }\n\n return results;\n }\n\n /**\n * 여러 라벨의 Draft 값들을 한 번에 조회 (version = null) — publish N+1 제거\n * Read replica 사용\n */\n async findDraftsByLabelIds(labelIds: number[]): Promise<CmsLabelValue[]>\n {\n if (labelIds.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n inArray(cmsLabelValues.labelId, labelIds),\n isNull(cmsLabelValues.version),\n ),\n );\n }\n\n /**\n * 새 published 버전 값들을 한 번에 multi-row INSERT (publish 배치용).\n * unique(labelId, version, locale, breakpoint) 충돌 시 value 갱신(멱등).\n * Write primary 사용\n */\n async insertPublishedValues(rows: (NewCmsLabelValue & { labelId: number })[]): Promise<void>\n {\n if (rows.length === 0)\n {\n return;\n }\n\n await this.db\n .insert(cmsLabelValues)\n .values(rows)\n .onConflictDoUpdate({\n target: [\n cmsLabelValues.labelId,\n cmsLabelValues.version,\n cmsLabelValues.locale,\n cmsLabelValues.breakpoint,\n ],\n set: { value: sql`excluded.value` },\n });\n }\n\n /**\n * 여러 라벨의 Draft 값들을 한 번에 삭제 (version = null) — publish 배치용\n * Write primary 사용\n */\n async deleteDraftsByLabelIds(labelIds: number[]): Promise<void>\n {\n if (labelIds.length === 0)\n {\n return;\n }\n\n await this.db\n .delete(cmsLabelValues)\n .where(\n and(\n inArray(cmsLabelValues.labelId, labelIds),\n isNull(cmsLabelValues.version),\n ),\n );\n }\n\n /**\n * 특정 (labelId, locale) 쌍들의 Draft(breakpoint = null) 일괄 삭제.\n * saveSectionDraft의 delete-then-insert 배치용 — 저장 대상 locale만 교체한다.\n * Write primary 사용\n */\n async deleteDraftsByLabelLocales(pairs: Array<{ labelId: number; locale: string }>): Promise<void>\n {\n if (pairs.length === 0)\n {\n return;\n }\n\n await this.db\n .delete(cmsLabelValues)\n .where(\n and(\n isNull(cmsLabelValues.version),\n isNull(cmsLabelValues.breakpoint),\n or(\n ...pairs.map(p =>\n and(\n eq(cmsLabelValues.labelId, p.labelId),\n eq(cmsLabelValues.locale, p.locale),\n ),\n ),\n ),\n ),\n );\n }\n\n /**\n * Draft 값들을 한 번에 multi-row INSERT (saveSectionDraft 배치용).\n * 호출 전에 같은 (labelId, locale) draft를 삭제한 상태를 전제로 한다.\n * Write primary 사용\n */\n async insertDraftValues(rows: (NewCmsLabelValue & { labelId: number })[]): Promise<void>\n {\n if (rows.length === 0)\n {\n return;\n }\n\n await this.db.insert(cmsLabelValues).values(rows);\n }\n\n /**\n * 특정 버전의 모든 값 삭제\n * Write primary 사용\n */\n async deleteByVersion(labelId: number, version: number | null): Promise<CmsLabelValue[]>\n {\n const versionCondition = version === null\n ? isNull(cmsLabelValues.version)\n : eq(cmsLabelValues.version, version);\n\n return this.db\n .delete(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n versionCondition,\n ),\n )\n .returning();\n }\n\n /**\n * 여러 라벨의 publishedVersion 값들을 한 번에 조회 (N+1 문제 해결)\n * Read replica 사용\n *\n * @param labelVersions - { labelId, version } 배열\n * @returns labelId를 키로 하는 Map<labelId, CmsLabelValue[]>\n *\n * @example\n * ```typescript\n * const result = await findByLabelVersions([\n * { labelId: 1, version: 5 },\n * { labelId: 2, version: 3 }\n * ]);\n * // result.get(1) -> label 1의 version 5 값들\n * // result.get(2) -> label 2의 version 3 값들\n * ```\n */\n async findByLabelVersions(\n labelVersions: Array<{ labelId: number; version: number }>,\n ): Promise<Map<number, CmsLabelValue[]>>\n {\n if (labelVersions.length === 0)\n {\n return new Map();\n }\n\n // (labelId, version) 쌍을 SQL에서 직접 필터 — 예전엔 labelId만으로 모든\n // 버전을 끌어와 JS로 걸렀다. published 버전은 불변·누적이라 v50을 요청하면\n // v1..50을 전부 전송하는 read amplification이 발생했다. composite index\n // (labelId, version)를 타도록 OR(AND(labelId, version))로 좁힌다.\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n or(\n ...labelVersions.map(lv =>\n and(\n eq(cmsLabelValues.labelId, lv.labelId),\n eq(cmsLabelValues.version, lv.version),\n ),\n ),\n ),\n );\n\n const resultMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n const list = resultMap.get(value.labelId);\n if (list)\n {\n list.push(value);\n }\n else\n {\n resultMap.set(value.labelId, [value]);\n }\n }\n\n return resultMap;\n }\n\n /**\n * 라벨의 버전 히스토리 조회 (1 ~ maxVersion)\n * 한 번의 쿼리로 모든 버전을 조회하고 version별로 그룹화\n * Read replica 사용\n */\n async findVersionHistoryByLabelId(\n labelId: number,\n maxVersion: number,\n ): Promise<VersionHistory[]>\n {\n // 모든 버전의 값을 한 번에 조회\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n gte(cmsLabelValues.version, 1),\n lte(cmsLabelValues.version, maxVersion),\n ),\n )\n .orderBy(cmsLabelValues.version, cmsLabelValues.locale);\n\n // version별로 그룹화\n const versionMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n if (value.version === null) continue; // null 버전은 제외\n\n if (!versionMap.has(value.version))\n {\n versionMap.set(value.version, []);\n }\n versionMap.get(value.version)!.push(value);\n }\n\n // VersionHistory 형식으로 변환\n const versions: VersionHistory[] = [];\n\n for (let version = 1; version <= maxVersion; version++)\n {\n const values = versionMap.get(version);\n\n if (values && values.length > 0)\n {\n versions.push({\n version,\n publishedAt: values[0].createdAt.toISOString(),\n publishedBy: null, // label_values에는 publishedBy 정보가 없음\n notes: null, // label_values에는 notes 정보가 없음\n values: values.map(v => ({\n id: v.id,\n locale: v.locale,\n breakpoint: v.breakpoint,\n value: v.value,\n createdAt: v.createdAt.toISOString(),\n })),\n });\n }\n }\n\n // 버전 내림차순 정렬 (최신 버전이 먼저)\n versions.sort((a, b) => b.version - a.version);\n\n return versions;\n }\n}\n\n// Default instance export\nexport const cmsLabelValuesRepository = new CmsLabelValuesRepository();\n","/**\n * CMS Published Cache Repository\n *\n * 발행된 콘텐츠 캐시 관리 (초고속 조회)\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { eq, and, sql, inArray } from 'drizzle-orm';\nimport { cmsPublishedCache, type CmsPublishedCache, type NewCmsPublishedCache } from '../entities';\n\n/**\n * CMS Published Cache Repository 클래스\n */\nexport class CmsPublishedCacheRepository extends BaseRepository\n{\n /**\n * 섹션 + 언어로 발행된 캐시 조회\n * Read replica 사용\n */\n async findBySection(section: string, locale: string = 'en'): Promise<CmsPublishedCache | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale),\n ),\n )\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * 캐시 생성 또는 업데이트 (UPSERT)\n * Write primary 사용\n */\n async upsert(data: NewCmsPublishedCache): Promise<CmsPublishedCache>\n {\n const result = await this.db\n .insert(cmsPublishedCache)\n .values(data)\n .onConflictDoUpdate({\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 .returning();\n\n return result[0];\n }\n\n /**\n * 여러 섹션의 캐시를 한 번에 조회 (N+1 방지)\n * Read replica 사용\n */\n async findBySections(sections: string[], locale: string = 'en'): Promise<CmsPublishedCache[]>\n {\n if (sections.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(\n and(\n inArray(cmsPublishedCache.section, sections),\n eq(cmsPublishedCache.locale, locale),\n ),\n );\n }\n\n /**\n * 섹션별 모든 언어 캐시 조회\n * Read replica 사용\n */\n async findAllLanguages(section: string): Promise<CmsPublishedCache[]>\n {\n return this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(eq(cmsPublishedCache.section, section));\n }\n\n /**\n * 캐시 삭제\n * Write primary 사용\n */\n async deleteBySection(section: string, locale?: string): Promise<void>\n {\n if (locale)\n {\n await this.db\n .delete(cmsPublishedCache)\n .where(\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale),\n ),\n );\n }\n else\n {\n await this.db\n .delete(cmsPublishedCache)\n .where(eq(cmsPublishedCache.section, section));\n }\n }\n}\n\n// Default instance export\nexport const cmsPublishedCacheRepository = new CmsPublishedCacheRepository();\n","/**\n * CMS Admin Routes\n *\n * 관리자용 라벨 관리 API\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { defineRouter, route } from '@spfn/core/route';\nimport { authenticate, requireRole } from '@spfn/auth/server';\nimport {\n getSectionLabels,\n saveSectionDraft,\n publishSection,\n resetSectionDraft,\n} from '../services/publish.service';\n\n// These routes rewrite/publish site-wide CMS content, so they must be gated.\n// Authenticate AND require an admin-tier role at the package level — do NOT rely on\n// the consumer's global auth for authorization (fail closed). requireRole is an\n// exact match, so both admin tiers are listed explicitly.\nconst ADMIN_GUARD = [authenticate, requireRole('admin', 'superadmin')];\n\n/**\n * 섹션의 모든 라벨 조회 (테이블 뷰용)\n *\n * GET /_cms/admin/sections/:section/labels?locales=en,ko\n */\nexport const getSectionLabelsRoute = route.get('/_cms/admin/sections/:section/labels')\n .input({\n params: Type.Object({\n section: Type.String(),\n }),\n query: Type.Object({\n locales: Type.Optional(Type.String()), // comma-separated: \"en,ko\"\n }),\n })\n .use(ADMIN_GUARD)\n .handler(async (c) =>\n {\n const { params, query } = await c.data();\n const { section } = params;\n const locales = query.locales?.split(',') || ['en'];\n\n return getSectionLabels(section, locales);\n });\n\n/**\n * 섹션 라벨 일괄 Draft 저장\n *\n * PUT /_cms/admin/sections/:section/draft\n */\nexport const saveSectionDraftRoute = route.put('/_cms/admin/sections/:section/draft')\n .input({\n params: Type.Object({\n section: Type.String(),\n }),\n body: Type.Object({\n labels: Type.Array(\n Type.Object({\n id: Type.Number(),\n values: Type.Record(Type.String(), Type.String()),\n }),\n ),\n }),\n })\n .use(ADMIN_GUARD)\n .handler(async (c) =>\n {\n const { params, body } = await c.data();\n const { section } = params;\n const { labels } = body;\n\n const result = await saveSectionDraft(section, labels);\n\n return { success: true, ...result };\n });\n\n/**\n * 섹션 전체 발행\n *\n * POST /_cms/admin/sections/:section/publish\n */\nexport const publishSectionRoute = route.post('/_cms/admin/sections/:section/publish')\n .input({\n params: Type.Object({\n section: Type.String(),\n }),\n body: Type.Object({\n locales: Type.Array(Type.String()),\n }),\n })\n .use(ADMIN_GUARD)\n .handler(async (c) =>\n {\n const { params, body } = await c.data();\n const { section } = params;\n const { locales } = body;\n\n const result = await publishSection(section, locales);\n\n return { success: true, ...result };\n });\n\n/**\n * 섹션 Draft 초기화\n *\n * DELETE /_cms/admin/sections/:section/draft\n */\nexport const resetSectionDraftRoute = route.delete('/_cms/admin/sections/:section/draft')\n .input({\n params: Type.Object({\n section: Type.String(),\n }),\n })\n .use(ADMIN_GUARD)\n .handler(async (c) =>\n {\n const { params } = await c.data();\n const { section } = params;\n\n const result = await resetSectionDraft(section);\n\n return { success: true, ...result };\n });\n\n/**\n * Admin Router\n */\nexport const cmsAdminRouter = defineRouter({\n getSectionLabels: getSectionLabelsRoute,\n saveSectionDraft: saveSectionDraftRoute,\n publishSection: publishSectionRoute,\n resetSectionDraft: resetSectionDraftRoute,\n});\n\nexport type CmsAdminRouter = typeof cmsAdminRouter;\n","/**\n * CMS Publish Service\n *\n * 라벨 Draft 저장 및 발행 관리\n */\n\nimport { logger } from '@spfn/core/logger';\nimport { runInTransaction } from '@spfn/core/db';\nimport {\n cmsLabelsRepository,\n cmsLabelValuesRepository,\n cmsPublishedCacheRepository,\n} from '../repositories';\nimport { type CmsLabelValue, type NewCmsLabelValue } from '../entities';\n\nconst publishLogger = logger.child('@spfn/cms:publish');\n\n/**\n * 섹션의 모든 라벨 조회 (Admin 테이블 뷰용)\n */\nexport async function getSectionLabels(\n section: string,\n locales: string[],\n): Promise<{\n section: string;\n locales: string[];\n labels: Array<{\n id: number;\n key: string;\n description: string | null;\n defaultValue: Record<string, string>;\n draft: Record<string, string> | null;\n published: Record<string, string> | null;\n hasDraft: boolean;\n }>;\n}>\n{\n publishLogger.debug('getSectionLabels', { section, locales });\n\n // 1. 섹션의 모든 라벨 메타데이터 조회\n const labels = await cmsLabelsRepository.findBySection(section);\n\n if (labels.length === 0)\n {\n return { section, locales, labels: [] };\n }\n\n // 2. 모든 라벨의 Draft 값을 한 번에 조회 (라벨당 1쿼리 → 1쿼리, N+1/풀고갈 제거)\n const labelIds = labels.map(l => l.id);\n const allDrafts = await cmsLabelValuesRepository.findDraftsByLabelIds(labelIds);\n\n const draftsByLabel = new Map<number, CmsLabelValue[]>();\n for (const draft of allDrafts)\n {\n const list = draftsByLabel.get(draft.labelId);\n if (list)\n {\n list.push(draft);\n }\n else\n {\n draftsByLabel.set(draft.labelId, [draft]);\n }\n }\n\n // 3. 각 라벨의 Published 값 조회\n const labelVersions = labels\n .filter(l => l.publishedVersion !== null)\n .map(l => ({ labelId: l.id, version: l.publishedVersion! }));\n\n const publishedValuesMap = labelVersions.length > 0\n ? await cmsLabelValuesRepository.findByLabelVersions(labelVersions)\n : new Map();\n\n // 4. 결과 조합\n const result = labels.map((label) =>\n {\n const drafts = draftsByLabel.get(label.id) || [];\n const published = publishedValuesMap.get(label.id) || [];\n\n // Draft를 locale별 Record로 변환\n const draftRecord: Record<string, string> | null = drafts.length > 0\n ? drafts.reduce((acc, d) =>\n {\n const value = d.value as any;\n acc[d.locale] = value?.content ?? value;\n\n return acc;\n }, {} as Record<string, string>)\n : null;\n\n // Published를 locale별 Record로 변환\n let publishedRecord: Record<string, string> | null = null;\n if (published.length > 0)\n {\n publishedRecord = {};\n for (const p of published as CmsLabelValue[])\n {\n const value = p.value as any;\n publishedRecord[p.locale] = value?.content ?? value;\n }\n }\n\n return {\n id: label.id,\n key: label.key,\n description: label.description,\n defaultValue: (label.defaultValue as Record<string, string>) || {},\n draft: draftRecord,\n published: publishedRecord,\n hasDraft: drafts.length > 0,\n };\n });\n\n return { section, locales, labels: result };\n}\n\n/**\n * 섹션 라벨 일괄 Draft 저장\n */\nexport async function saveSectionDraft(\n section: string,\n labels: Array<{\n id: number;\n values: Record<string, string>;\n }>,\n): Promise<{ updated: number }>\n{\n publishLogger.debug('saveSectionDraft', { section, labelCount: labels.length });\n\n // 저장 대상 (labelId, locale) 쌍과 새 draft 행을 모은다. 라벨×locale마다\n // upsert(SELECT+write)를 직렬로 돌리던 것을, 해당 쌍의 draft만 한 번에 삭제하고\n // 새 값들을 한 번에 INSERT하는 배치로 바꾼다 (트랜잭션으로 원자적).\n const pairs: Array<{ labelId: number; locale: string }> = [];\n const rows: (NewCmsLabelValue & { labelId: number })[] = [];\n\n for (const { id, values } of labels)\n {\n for (const [locale, value] of Object.entries(values))\n {\n pairs.push({ labelId: id, locale });\n rows.push({\n labelId: id,\n version: null,\n locale,\n value: { type: 'text', content: value },\n });\n }\n }\n\n if (rows.length > 0)\n {\n await runInTransaction(async () =>\n {\n await cmsLabelValuesRepository.deleteDraftsByLabelLocales(pairs);\n await cmsLabelValuesRepository.insertDraftValues(rows);\n });\n }\n\n const updated = rows.length;\n\n publishLogger.info('Draft saved', { section, updated });\n\n return { updated };\n}\n\n/**\n * 섹션 전체 발행\n *\n * 1. Draft가 있는 라벨들의 Draft 값을 새 버전으로 복사\n * 2. cms_labels.publishedVersion 업데이트\n * 3. cms_published_cache 갱신\n */\nexport async function publishSection(\n section: string,\n locales: string[],\n): Promise<{\n published: number;\n version: number;\n labels: string[];\n}>\n{\n publishLogger.debug('publishSection', { section, locales });\n\n // 1. 섹션의 모든 라벨 조회\n const labels = await cmsLabelsRepository.findBySection(section);\n\n if (labels.length === 0)\n {\n return { published: 0, version: 0, labels: [] };\n }\n\n // 2. 모든 라벨의 Draft 값을 한 번에 조회 (라벨당 SELECT → 1 SELECT, N+1 제거)\n const labelIds = labels.map(l => l.id);\n const allDrafts = await cmsLabelValuesRepository.findDraftsByLabelIds(labelIds);\n\n const draftsByLabel = new Map<number, CmsLabelValue[]>();\n for (const draft of allDrafts)\n {\n const list = draftsByLabel.get(draft.labelId);\n if (list)\n {\n list.push(draft);\n }\n else\n {\n draftsByLabel.set(draft.labelId, [draft]);\n }\n }\n\n // 3. 새 버전 행/업데이트를 모은다 (DB는 아래 트랜잭션에서 배치로)\n const newRows: (NewCmsLabelValue & { labelId: number })[] = [];\n const versionUpdates: Array<{ id: number; version: number }> = [];\n const publishedLabels: string[] = [];\n let maxVersion = 0;\n\n for (const label of labels)\n {\n const drafts = draftsByLabel.get(label.id);\n if (!drafts || drafts.length === 0)\n {\n continue;\n }\n\n const newVersion = (label.publishedVersion || 0) + 1;\n maxVersion = Math.max(maxVersion, newVersion);\n\n for (const draft of drafts)\n {\n newRows.push({\n labelId: label.id,\n version: newVersion,\n locale: draft.locale,\n breakpoint: draft.breakpoint,\n value: draft.value,\n });\n }\n\n versionUpdates.push({ id: label.id, version: newVersion });\n publishedLabels.push(label.key);\n }\n\n // 4. 발행은 원자적으로 — 새 버전 multi-row INSERT → draft 일괄 삭제 →\n // publishedVersion 업데이트. (라벨당 1+2D+1+1 직렬 왕복 → 소수의 배치 쿼리)\n if (publishedLabels.length > 0)\n {\n const publishedLabelIds = versionUpdates.map(v => v.id);\n\n await runInTransaction(async () =>\n {\n await cmsLabelValuesRepository.insertPublishedValues(newRows);\n await cmsLabelValuesRepository.deleteDraftsByLabelIds(publishedLabelIds);\n\n for (const update of versionUpdates)\n {\n await cmsLabelsRepository.updateById(update.id, {\n publishedVersion: update.version,\n });\n }\n });\n\n // 5. cms_published_cache 갱신\n await rebuildSectionCache(section, locales);\n }\n\n publishLogger.info('Section published', {\n section,\n published: publishedLabels.length,\n version: maxVersion,\n });\n\n return {\n published: publishedLabels.length,\n version: maxVersion,\n labels: publishedLabels,\n };\n}\n\n/**\n * 섹션 Draft 초기화 (삭제)\n */\nexport async function resetSectionDraft(section: string): Promise<{ reset: number }>\n{\n publishLogger.debug('resetSectionDraft', { section });\n\n // 1. 섹션의 모든 라벨 조회\n const labels = await cmsLabelsRepository.findBySection(section);\n\n let reset = 0;\n\n for (const label of labels)\n {\n // 2. Draft 값 삭제 (version: null)\n const drafts = await cmsLabelValuesRepository.findDraftsByLabelId(label.id);\n\n if (drafts.length > 0)\n {\n await cmsLabelValuesRepository.deleteByVersion(label.id, null as any);\n reset += drafts.length;\n }\n }\n\n publishLogger.info('Draft reset', { section, reset });\n\n return { reset };\n}\n\n/**\n * 섹션의 Published Cache 재구축\n */\nasync function rebuildSectionCache(section: string, locales: string[]): Promise<void>\n{\n publishLogger.debug('rebuildSectionCache', { section, locales });\n\n // 1. 섹션의 모든 라벨 조회\n const labels = await cmsLabelsRepository.findBySection(section);\n\n // 2. Published 값이 있는 라벨들 조회\n const labelVersions = labels\n .filter(l => l.publishedVersion !== null)\n .map(l => ({ labelId: l.id, version: l.publishedVersion! }));\n\n if (labelVersions.length === 0)\n {\n return;\n }\n\n const publishedValuesMap = await cmsLabelValuesRepository.findByLabelVersions(labelVersions);\n\n // 3. locale별로 cache 구축\n for (const locale of locales)\n {\n const content: Record<string, any> = {};\n\n for (const label of labels)\n {\n const values = publishedValuesMap.get(label.id) || [];\n const localeValue = values.find(v => v.locale === locale);\n\n if (localeValue)\n {\n // key에서 section prefix 제거하여 저장\n // 예: \"home.hero.title\" → content[\"home.hero.title\"] = value\n content[label.key] = localeValue.value;\n }\n }\n\n // 4. cache upsert\n await cmsPublishedCacheRepository.upsert({\n section,\n locale,\n content,\n publishedAt: new Date(),\n publishedBy: 'system',\n });\n }\n\n publishLogger.debug('Cache rebuilt', { section, locales });\n}\n","/**\n * CMS Label Synchronization Service\n *\n * Synchronizes labels defined in code with database\n */\n\nimport { isEqual } from 'lodash-es';\nimport { type SyncOptions, type SyncResult } from '../../lib/types';\nimport { type FlatLabelWithDescription, flattenLabels } from '../../lib/helpers';\nimport { cmsLabelsRepository } from '../repositories';\nimport { type NewCmsLabel } from '../entities';\n\ntype DbLabelMap = Record<string, { values: Record<string, string>; description?: string | null }>;\n\n/**\n * Compare current DB labels with new labels\n *\n * @param dbLabels - Labels currently in database\n * @param codeLabels - Labels from code (flattened)\n * @returns Comparison result with added/removed/updated labels\n */\nfunction compareLabels(dbLabels: DbLabelMap, codeLabels: FlatLabelWithDescription): SyncResult\n{\n const added: string[] = [];\n const removed: string[] = [];\n const updated: string[] = [];\n const unchanged: string[] = [];\n\n const dbKeys = Object.keys(dbLabels);\n const codeKeys = Object.keys(codeLabels);\n\n // Check for added and updated labels\n for (const key of codeKeys)\n {\n if (!(key in dbLabels))\n {\n // New label\n added.push(key);\n }\n else\n {\n // Check if values or description changed\n const dbItem = dbLabels[key];\n const codeItem = codeLabels[key];\n\n const valuesChanged = !isEqual(dbItem.values, codeItem.values);\n const descChanged = (dbItem.description ?? undefined) !== codeItem.description;\n\n if (valuesChanged || descChanged)\n {\n updated.push(key);\n }\n else\n {\n unchanged.push(key);\n }\n }\n }\n\n // Check for removed labels\n for (const key of dbKeys)\n {\n if (!(key in codeLabels))\n {\n removed.push(key);\n }\n }\n\n return {\n added,\n removed,\n updated,\n unchanged,\n };\n}\n\n/**\n * Sync labels with database\n *\n * @param labels - Single label definition or array of label definitions\n * @param options - Sync options\n * @returns Sync result\n *\n * @example\n * ```typescript\n * // Single definition\n * await syncLabels(labelsDefinition);\n *\n * // Multiple definitions\n * await syncLabels([homeLabels, aboutLabels, commonLabels]);\n * ```\n */\nexport async function syncLabels<T extends Record<string, any>>(\n labels: T | T[],\n options?: SyncOptions,\n): Promise<SyncResult>\n{\n const { removeOrphaned = false, dryRun = false } = options || {};\n\n // 1. Merge multiple label definitions into one (if array provided)\n const mergedLabels = Array.isArray(labels)\n ? Object.assign({}, ...labels)\n : labels;\n\n // 2. Flatten code labels\n const codeLabels = flattenLabels(mergedLabels);\n\n // 3. Fetch current labels from DB\n const dbLabels = await cmsLabelsRepository.findMany();\n const dbLabelMap: DbLabelMap = {};\n\n for (const label of dbLabels)\n {\n if (label.defaultValue)\n {\n dbLabelMap[label.key] = {\n values: label.defaultValue as Record<string, string>,\n description: label.description,\n };\n }\n }\n\n // 4. Compare changes\n const result = compareLabels(dbLabelMap, codeLabels);\n\n // 5. Return result if dry run\n if (dryRun)\n {\n return result;\n }\n\n // 6. Create new labels\n if (result.added.length > 0)\n {\n const toCreate: NewCmsLabel[] = result.added.map(key => ({\n key,\n section: extractSection(key),\n type: 'text',\n defaultValue: codeLabels[key].values,\n description: codeLabels[key].description,\n }));\n\n await cmsLabelsRepository.bulkCreate(toCreate);\n }\n\n // 7. Update changed labels\n if (result.updated.length > 0)\n {\n const updates = result.updated.map(key => ({\n key,\n data: {\n defaultValue: codeLabels[key].values,\n description: codeLabels[key].description,\n },\n }));\n\n await cmsLabelsRepository.bulkUpdateByKeys(updates);\n }\n\n // 8. Remove deleted labels (only if option is true)\n if (removeOrphaned && result.removed.length > 0)\n {\n await cmsLabelsRepository.bulkDeleteByKeys(result.removed);\n }\n\n return result;\n}\n\n/**\n * Extract section from key\n * Example: \"home.hero.title\" -> \"home\"\n */\nfunction extractSection(key: string): string\n{\n const parts = key.split('.');\n\n return parts[0] || key;\n}\n","/**\n * CMS Helper Functions\n */\n\nexport type FlatLabel = Record<string, Record<string, string>>;\n\nexport type FlatLabelItem = {\n values: Record<string, string>;\n description?: string;\n};\n\nexport type FlatLabelWithDescription = Record<string, FlatLabelItem>;\n\n/**\n * Flatten nested label structure into dot notation\n *\n * @param labels - Nested label object\n * @param prefix - Key prefix for recursion\n * @returns Flattened label structure with description\n *\n * @example\n * ```typescript\n * const nested = {\n * home: {\n * hero: {\n * title: {\n * en: \"Welcome\",\n * ko: \"환영합니다\",\n * $description: \"Main hero title\"\n * }\n * }\n * }\n * };\n *\n * const flat = flattenLabels(nested);\n * // {\n * // \"home.hero.title\": {\n * // values: { en: \"Welcome\", ko: \"환영합니다\" },\n * // description: \"Main hero title\"\n * // }\n * // }\n * ```\n */\nexport function flattenLabels<T extends Record<string, any>>(\n labels: T,\n prefix = '',\n): FlatLabelWithDescription\n{\n const result: FlatLabelWithDescription = {};\n\n if (!labels || typeof labels !== 'object')\n {\n return result;\n }\n\n const obj = labels as Record<string, unknown>;\n\n for (const [key, value] of Object.entries(obj))\n {\n const newKey = prefix ? `${prefix}.${key}` : key;\n\n if (!value || typeof value !== 'object')\n {\n continue;\n }\n\n const valueObj = value as Record<string, unknown>;\n\n // Check if this is a leaf node (locale values: { en: \"...\", ko: \"...\", $description?: \"...\" })\n // All values must be strings (including $description)\n const isLeaf = Object.values(valueObj).every(v => typeof v === 'string');\n\n if (isLeaf)\n {\n const { $description, ...localeValues } = valueObj as Record<string, string>;\n\n result[newKey] = {\n values: localeValues,\n description: $description,\n };\n }\n else\n {\n // Recursively flatten nested structure\n Object.assign(result, flattenLabels(value, newKey));\n }\n }\n\n return result;\n}\n\n/**\n * Set a value in nested object using dot notation path\n *\n * @param target - Target object to modify\n * @param path - Dot notation path (e.g., \"home.hero.title\")\n * @param value - Value to set\n *\n * @example\n * ```typescript\n * const obj = {};\n * setNestedValue(obj, \"home.hero.title\", \"Welcome\");\n * // obj = { home: { hero: { title: \"Welcome\" } } }\n * ```\n */\nexport function setNestedValue(target: any, path: string, value: any): void\n{\n const parts = path.split('.');\n let current = target;\n\n for (let i = 0; i < parts.length - 1; i++)\n {\n const part = parts[i];\n if (!current[part])\n {\n current[part] = {};\n }\n current = current[part];\n }\n\n // Set the leaf value\n const lastPart = parts[parts.length - 1];\n current[lastPart] = value;\n}\n\n/**\n * Unflatten dot notation keys back to nested structure\n *\n * @param flat - Flattened label object\n * @returns Nested label structure\n *\n * @example\n * ```typescript\n * const flat = {\n * \"home.hero.title\": { en: \"Welcome\", ko: \"환영합니다\" },\n * \"home.hero.subtitle\": { en: \"Subtitle\", ko: \"부제목\" }\n * };\n *\n * const nested = unflattenLabels(flat);\n * // {\n * // home: {\n * // hero: {\n * // title: { en: \"Welcome\", ko: \"환영합니다\" },\n * // subtitle: { en: \"Subtitle\", ko: \"부제목\" }\n * // }\n * // }\n * // }\n * ```\n */\nexport function unflattenLabels(flat: FlatLabel): Record<string, any>\n{\n const result: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(flat))\n {\n setNestedValue(result, key, value);\n }\n\n return result;\n}\n"],"mappings":"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","names":["Type","defineRouter","route","integer","text","index","id","typedJsonb","id","integer","text","typedJsonb","index","text","integer","index","unique","id","typedJsonb","id","text","typedJsonb","integer","unique","index","id","BaseRepository","eq","inArray","BaseRepository","eq","inArray","BaseRepository","eq","and","sql","inArray","BaseRepository","and","eq","sql","inArray","id","route","Type","defineRouter"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/server/routes/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-published-cache.ts","../src/server/repositories/cms-label-values.repository.ts","../src/server/repositories/cms-published-cache.repository.ts","../src/server/routes/admin.routes.ts","../src/server/services/publish.service.ts","../src/server/services/sync.service.ts","../src/lib/helpers.ts"],"sourcesContent":["import '@spfn/cms/config';\n\nexport { cmsAppRouter } from './server/routes';\nexport { syncLabels } from './server/services';\n","/**\n * CMS App Router\n *\n * 모든 CMS 라우트를 통합하는 메인 라우터\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { defineRouter, route } from '@spfn/core/route';\nimport { cmsPublishedCacheRepository } from '../repositories';\nimport {\n getSectionLabelsRoute,\n saveSectionDraftRoute,\n publishSectionRoute,\n resetSectionDraftRoute,\n} from './admin.routes';\n\nexport const getLabelCache = route.get('/_cms/labels/cache')\n .skip(['auth'])\n .input({\n query: Type.Object({\n sections: Type.Array(Type.String()),\n locale: Type.Optional(Type.String()),\n }),\n })\n .handler(async (c) =>\n {\n const { query } = await c.data();\n const { sections, locale = 'en' } = query;\n\n // 단일 쿼리로 모든 섹션 조회 (N+1 방지)\n const results = await cmsPublishedCacheRepository.findBySections(sections, locale);\n\n // Record<section, content> 형태로 변환\n return results.reduce((acc, item) => \n {\n acc[item.section] = item.content;\n\n return acc;\n }, {} as Record<string, any>);\n });\n\nexport const cmsAppRouter = defineRouter({\n getLabelCache,\n // Admin routes\n getSectionLabels: getSectionLabelsRoute,\n saveSectionDraft: saveSectionDraftRoute,\n publishSection: publishSectionRoute,\n resetSectionDraft: resetSectionDraftRoute,\n});\n\nexport type AppRouter = typeof cmsAppRouter;\n\n// Re-export admin router for standalone use\nexport { cmsAdminRouter } from './admin.routes';\nexport type { CmsAdminRouter } from './admin.routes';\n","/**\n * CMS Labels Repository\n *\n * 라벨 메타데이터 관리를 위한 Repository\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { asc, count as drizzleCount, eq, inArray } from 'drizzle-orm';\nimport { type CmsLabel, cmsLabels, type NewCmsLabel } from '../entities';\n\n/**\n * CMS Labels Repository 클래스\n *\n * BaseRepository를 상속받아 다음 기능을 제공:\n * - 자동 트랜잭션 컨텍스트 감지 및 사용\n * - Read/Write 연결 분리 (replica 활용)\n * - 타입 안전성\n */\nexport class CmsLabelsRepository extends BaseRepository\n{\n /**\n * 라벨 목록 조회\n * Read replica 사용\n */\n async findMany(options?: {\n section?: string;\n }): Promise<CmsLabel[]>\n {\n const { section } = options || {};\n\n let query = this.readDb\n .select()\n .from(cmsLabels)\n .orderBy(asc(cmsLabels.key)); // key 오름차순 정렬 (JSON 파일의 순서 유지)\n\n if (section)\n {\n query = query.where(eq(cmsLabels.section, section)) as typeof query;\n }\n\n return query;\n }\n\n /**\n * 전체 라벨 수 조회\n * Read replica 사용\n */\n async count(section?: string): Promise<number>\n {\n const query = this.readDb\n .select({ count: drizzleCount() })\n .from(cmsLabels);\n\n const result = section\n ? await query.where(eq(cmsLabels.section, section))\n : await query;\n\n return result[0]?.count ?? 0;\n }\n\n /**\n * ID로 라벨 조회\n * Read replica 사용\n */\n async findById(id: number): Promise<CmsLabel | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.id, id))\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * Key로 라벨 조회\n * Read replica 사용\n */\n async findByKey(key: string): Promise<CmsLabel | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.key, key))\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * 섹션으로 모든 라벨 조회\n * Read replica 사용\n */\n async findBySection(section: string): Promise<CmsLabel[]>\n {\n return this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.section, section))\n .orderBy(asc(cmsLabels.key)); // key 오름차순 정렬 (JSON 파일의 순서 유지)\n }\n\n /**\n * 라벨 생성\n * Write primary 사용\n */\n async create(data: NewCmsLabel): Promise<CmsLabel>\n {\n const result = await this.db\n .insert(cmsLabels)\n .values(data)\n .returning();\n\n return result[0];\n }\n\n /**\n * 라벨 수정\n * Write primary 사용\n */\n async updateById(id: number, data: Partial<NewCmsLabel>): Promise<CmsLabel | null>\n {\n const result = await this.db\n .update(cmsLabels)\n .set({ ...data, updatedAt: new Date() })\n .where(eq(cmsLabels.id, id))\n .returning();\n\n return result[0] ?? null;\n }\n\n /**\n * 라벨 삭제\n * Write primary 사용\n */\n async deleteById(id: number): Promise<CmsLabel | null>\n {\n const result = await this.db\n .delete(cmsLabels)\n .where(eq(cmsLabels.id, id))\n .returning();\n\n return result[0] ?? null;\n }\n\n /**\n * 여러 key로 라벨 조회\n * Read replica 사용\n */\n async findByKeys(keys: string[]): Promise<CmsLabel[]>\n {\n if (keys.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsLabels)\n .where(inArray(cmsLabels.key, keys));\n }\n\n /**\n * 여러 라벨 한번에 생성\n * Write primary 사용\n */\n async bulkCreate(data: NewCmsLabel[]): Promise<CmsLabel[]>\n {\n if (data.length === 0)\n {\n return [];\n }\n\n return this.db\n .insert(cmsLabels)\n .values(data)\n .returning();\n }\n\n /**\n * 여러 라벨 한번에 수정 (key 기준)\n * Write primary 사용\n *\n * @param updates - Array of { key, data } objects\n */\n async bulkUpdateByKeys(updates: Array<{ key: string; data: Partial<NewCmsLabel> }>): Promise<void>\n {\n if (updates.length === 0)\n {\n return;\n }\n\n // Drizzle doesn't support bulk update directly, so we need to do it one by one\n // But we can do it in a single transaction context (handled by BaseRepository)\n for (const { key, data } of updates)\n {\n await this.db\n .update(cmsLabels)\n .set({ ...data, updatedAt: new Date() })\n .where(eq(cmsLabels.key, key));\n }\n }\n\n /**\n * 여러 라벨 한번에 삭제 (key 기준)\n * Write primary 사용\n */\n async bulkDeleteByKeys(keys: string[]): Promise<CmsLabel[]>\n {\n if (keys.length === 0)\n {\n return [];\n }\n\n return this.db\n .delete(cmsLabels)\n .where(inArray(cmsLabels.key, keys))\n .returning();\n }\n}\n\n// Default instance export\nexport const cmsLabelsRepository = new CmsLabelsRepository();\n","/**\n * CMS Labels Entity\n *\n * 라벨의 메타데이터와 현재 발행 상태를 관리합니다.\n * - 라벨 식별 (id, key)\n * - 섹션 분류 (section)\n * - 타입 정의 (type)\n * - 발행 상태 (publishedVersion)\n */\n\nimport { index, integer, text } from 'drizzle-orm/pg-core';\nimport { id, timestamps, typedJsonb } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\n\nexport const cmsLabels = cmsSchema.table('labels', {\n // Primary Key\n id: id(),\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: typedJsonb<Record<string, any>>('default_value'),\n // 라벨의 기본값 (sync 시 설정)\n // 예: { en: \"Welcome\", ko: \"환영합니다\" } 또는 단일 값\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 ...timestamps(),\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","/**\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 { createSchema } from '@spfn/core/db';\n\nexport const cmsSchema = createSchema('@spfn/cms');\n","/**\n * CMS Label Values Entity\n *\n * 라벨의 실제 값을 저장합니다.\n * - 다국어 지원 (locale)\n * - 반응형 지원 (breakpoint)\n * - 버전 관리 (version)\n * - JSONB로 유연한 값 저장\n */\n\nimport { integer, text, index, unique } from 'drizzle-orm/pg-core';\nimport { id, utcTimestamp, typedJsonb, foreignKey } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\nimport { cmsLabels } from './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: id(),\n\n // Foreign Key: cms_labels\n labelId: foreignKey('label', () => cmsLabels.id, { onDelete: 'cascade' }),\n\n // 버전 번호 (null = draft, number = published version)\n version: integer('version'),\n\n // 언어 코드\n locale: text('locale').notNull().default('en'),\n // \"ko\" | \"en\" | \"ja\"\n\n // 반응형 브레이크포인트\n breakpoint: text('breakpoint'),\n // null = 기본값 (모든 화면 크기)\n // \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n\n // 실제 값 (JSONB)\n value: typedJsonb<Record<string, any>>('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: utcTimestamp('created_at').defaultNow().notNull(),\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","/**\n * CMS Published Cache Entity\n *\n * 발행된 콘텐츠를 섹션+언어 단위로 캐싱합니다.\n * - 초고속 읽기 성능 (5ms)\n * - 단일 쿼리로 섹션 전체 로드\n * - JSONB로 즉시 사용 가능한 데이터\n *\n * 성능 비교:\n * - 정규화 테이블 JOIN: 87ms\n * - 캐시 테이블: 5ms (17배 빠름!)\n */\n\nimport { text, integer, index, unique } from 'drizzle-orm/pg-core';\nimport { id, publishingFields, typedJsonb } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\nexport const cmsPublishedCache = cmsSchema.table('published_cache', {\n // Primary Key\n id: id(),\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: typedJsonb<Record<string, any>>('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 ...publishingFields(),\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","/**\n * CMS Label Values Repository\n *\n * 라벨 값 관리를 위한 Repository\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { eq, and, or, SQL, isNull, gte, lte, inArray, sql } from 'drizzle-orm';\nimport { cmsLabelValues, type CmsLabelValue, type NewCmsLabelValue } from '../entities';\n\n/**\n * 버전 히스토리 타입\n */\nexport interface VersionHistory\n{\n version: number;\n publishedAt: string;\n publishedBy: null;\n notes: null;\n values: Array<{\n id: number;\n locale: string;\n breakpoint: string | null;\n value: any;\n createdAt: string;\n }>;\n}\n\n/**\n * CMS Label Values Repository 클래스\n */\nexport class CmsLabelValuesRepository extends BaseRepository\n{\n /**\n * 특정 라벨의 특정 버전 값들 조회\n * Read replica 사용\n */\n async 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 this.readDb\n .select()\n .from(cmsLabelValues)\n .where(and(...conditions));\n }\n\n /**\n * 값 저장 (upsert)\n * - version: null → Draft 저장 (덮어쓰기)\n * - version: number → Published 버전 생성 (불변)\n * Write primary 사용\n */\n async upsert(data: NewCmsLabelValue & { labelId: number }): 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 existingResult = await this.db\n .select()\n .from(cmsLabelValues)\n .where(\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 .limit(1);\n\n const existing = existingResult[0];\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 this.db\n .update(cmsLabelValues)\n .set({ value: data.value })\n .where(eq(cmsLabelValues.id, existing.id))\n .returning();\n\n return updated[0];\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 const inserted = await this.db\n .insert(cmsLabelValues)\n .values(data)\n .returning();\n\n return inserted[0];\n }\n }\n\n /**\n * Draft 값들 조회 (version = null)\n * Read replica 사용\n */\n async findDraftsByLabelId(labelId: number): Promise<CmsLabelValue[]>\n {\n return this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n isNull(cmsLabelValues.version),\n ),\n );\n }\n\n /**\n * 여러 값 일괄 저장\n * Write primary 사용\n */\n async upsertMany(values: (NewCmsLabelValue & { labelId: number })[]): Promise<CmsLabelValue[]>\n {\n const results = [];\n for (const value of values)\n {\n const result = await this.upsert(value);\n results.push(result);\n }\n\n return results;\n }\n\n /**\n * 여러 라벨의 Draft 값들을 한 번에 조회 (version = null) — publish N+1 제거\n * Read replica 사용\n */\n async findDraftsByLabelIds(labelIds: number[]): Promise<CmsLabelValue[]>\n {\n if (labelIds.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n inArray(cmsLabelValues.labelId, labelIds),\n isNull(cmsLabelValues.version),\n ),\n );\n }\n\n /**\n * 새 published 버전 값들을 한 번에 multi-row INSERT (publish 배치용).\n * unique(labelId, version, locale, breakpoint) 충돌 시 value 갱신(멱등).\n * Write primary 사용\n */\n async insertPublishedValues(rows: (NewCmsLabelValue & { labelId: number })[]): Promise<void>\n {\n if (rows.length === 0)\n {\n return;\n }\n\n await this.db\n .insert(cmsLabelValues)\n .values(rows)\n .onConflictDoUpdate({\n target: [\n cmsLabelValues.labelId,\n cmsLabelValues.version,\n cmsLabelValues.locale,\n cmsLabelValues.breakpoint,\n ],\n set: { value: sql`excluded.value` },\n });\n }\n\n /**\n * 여러 라벨의 Draft 값들을 한 번에 삭제 (version = null) — publish 배치용\n * Write primary 사용\n */\n async deleteDraftsByLabelIds(labelIds: number[]): Promise<void>\n {\n if (labelIds.length === 0)\n {\n return;\n }\n\n await this.db\n .delete(cmsLabelValues)\n .where(\n and(\n inArray(cmsLabelValues.labelId, labelIds),\n isNull(cmsLabelValues.version),\n ),\n );\n }\n\n /**\n * 특정 (labelId, locale) 쌍들의 Draft(breakpoint = null) 일괄 삭제.\n * saveSectionDraft의 delete-then-insert 배치용 — 저장 대상 locale만 교체한다.\n * Write primary 사용\n */\n async deleteDraftsByLabelLocales(pairs: Array<{ labelId: number; locale: string }>): Promise<void>\n {\n if (pairs.length === 0)\n {\n return;\n }\n\n await this.db\n .delete(cmsLabelValues)\n .where(\n and(\n isNull(cmsLabelValues.version),\n isNull(cmsLabelValues.breakpoint),\n or(\n ...pairs.map(p =>\n and(\n eq(cmsLabelValues.labelId, p.labelId),\n eq(cmsLabelValues.locale, p.locale),\n ),\n ),\n ),\n ),\n );\n }\n\n /**\n * Draft 값들을 한 번에 multi-row INSERT (saveSectionDraft 배치용).\n * 호출 전에 같은 (labelId, locale) draft를 삭제한 상태를 전제로 한다.\n * Write primary 사용\n */\n async insertDraftValues(rows: (NewCmsLabelValue & { labelId: number })[]): Promise<void>\n {\n if (rows.length === 0)\n {\n return;\n }\n\n await this.db.insert(cmsLabelValues).values(rows);\n }\n\n /**\n * 특정 버전의 모든 값 삭제\n * Write primary 사용\n */\n async deleteByVersion(labelId: number, version: number | null): Promise<CmsLabelValue[]>\n {\n const versionCondition = version === null\n ? isNull(cmsLabelValues.version)\n : eq(cmsLabelValues.version, version);\n\n return this.db\n .delete(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n versionCondition,\n ),\n )\n .returning();\n }\n\n /**\n * 여러 라벨의 publishedVersion 값들을 한 번에 조회 (N+1 문제 해결)\n * Read replica 사용\n *\n * @param labelVersions - { labelId, version } 배열\n * @returns labelId를 키로 하는 Map<labelId, CmsLabelValue[]>\n *\n * @example\n * ```typescript\n * const result = await findByLabelVersions([\n * { labelId: 1, version: 5 },\n * { labelId: 2, version: 3 }\n * ]);\n * // result.get(1) -> label 1의 version 5 값들\n * // result.get(2) -> label 2의 version 3 값들\n * ```\n */\n async findByLabelVersions(\n labelVersions: Array<{ labelId: number; version: number }>,\n ): Promise<Map<number, CmsLabelValue[]>>\n {\n if (labelVersions.length === 0)\n {\n return new Map();\n }\n\n // (labelId, version) 쌍을 SQL에서 직접 필터 — 예전엔 labelId만으로 모든\n // 버전을 끌어와 JS로 걸렀다. published 버전은 불변·누적이라 v50을 요청하면\n // v1..50을 전부 전송하는 read amplification이 발생했다. composite index\n // (labelId, version)를 타도록 OR(AND(labelId, version))로 좁힌다.\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n or(\n ...labelVersions.map(lv =>\n and(\n eq(cmsLabelValues.labelId, lv.labelId),\n eq(cmsLabelValues.version, lv.version),\n ),\n ),\n ),\n );\n\n const resultMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n const list = resultMap.get(value.labelId);\n if (list)\n {\n list.push(value);\n }\n else\n {\n resultMap.set(value.labelId, [value]);\n }\n }\n\n return resultMap;\n }\n\n /**\n * 라벨의 버전 히스토리 조회 (1 ~ maxVersion)\n * 한 번의 쿼리로 모든 버전을 조회하고 version별로 그룹화\n * Read replica 사용\n */\n async findVersionHistoryByLabelId(\n labelId: number,\n maxVersion: number,\n ): Promise<VersionHistory[]>\n {\n // 모든 버전의 값을 한 번에 조회\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n gte(cmsLabelValues.version, 1),\n lte(cmsLabelValues.version, maxVersion),\n ),\n )\n .orderBy(cmsLabelValues.version, cmsLabelValues.locale);\n\n // version별로 그룹화\n const versionMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n if (value.version === null) continue; // null 버전은 제외\n\n if (!versionMap.has(value.version))\n {\n versionMap.set(value.version, []);\n }\n versionMap.get(value.version)!.push(value);\n }\n\n // VersionHistory 형식으로 변환\n const versions: VersionHistory[] = [];\n\n for (let version = 1; version <= maxVersion; version++)\n {\n const values = versionMap.get(version);\n\n if (values && values.length > 0)\n {\n versions.push({\n version,\n publishedAt: values[0].createdAt.toISOString(),\n publishedBy: null, // label_values에는 publishedBy 정보가 없음\n notes: null, // label_values에는 notes 정보가 없음\n values: values.map(v => ({\n id: v.id,\n locale: v.locale,\n breakpoint: v.breakpoint,\n value: v.value,\n createdAt: v.createdAt.toISOString(),\n })),\n });\n }\n }\n\n // 버전 내림차순 정렬 (최신 버전이 먼저)\n versions.sort((a, b) => b.version - a.version);\n\n return versions;\n }\n}\n\n// Default instance export\nexport const cmsLabelValuesRepository = new CmsLabelValuesRepository();\n","/**\n * CMS Published Cache Repository\n *\n * 발행된 콘텐츠 캐시 관리 (초고속 조회)\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { eq, and, sql, inArray } from 'drizzle-orm';\nimport { cmsPublishedCache, type CmsPublishedCache, type NewCmsPublishedCache } from '../entities';\n\n/**\n * CMS Published Cache Repository 클래스\n */\nexport class CmsPublishedCacheRepository extends BaseRepository\n{\n /**\n * 섹션 + 언어로 발행된 캐시 조회\n * Read replica 사용\n */\n async findBySection(section: string, locale: string = 'en'): Promise<CmsPublishedCache | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale),\n ),\n )\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * 캐시 생성 또는 업데이트 (UPSERT)\n * Write primary 사용\n */\n async upsert(data: NewCmsPublishedCache): Promise<CmsPublishedCache>\n {\n const result = await this.db\n .insert(cmsPublishedCache)\n .values(data)\n .onConflictDoUpdate({\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 .returning();\n\n return result[0];\n }\n\n /**\n * 여러 섹션의 캐시를 한 번에 조회 (N+1 방지)\n * Read replica 사용\n */\n async findBySections(sections: string[], locale: string = 'en'): Promise<CmsPublishedCache[]>\n {\n if (sections.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(\n and(\n inArray(cmsPublishedCache.section, sections),\n eq(cmsPublishedCache.locale, locale),\n ),\n );\n }\n\n /**\n * 섹션별 모든 언어 캐시 조회\n * Read replica 사용\n */\n async findAllLanguages(section: string): Promise<CmsPublishedCache[]>\n {\n return this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(eq(cmsPublishedCache.section, section));\n }\n\n /**\n * 캐시 삭제\n * Write primary 사용\n */\n async deleteBySection(section: string, locale?: string): Promise<void>\n {\n if (locale)\n {\n await this.db\n .delete(cmsPublishedCache)\n .where(\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale),\n ),\n );\n }\n else\n {\n await this.db\n .delete(cmsPublishedCache)\n .where(eq(cmsPublishedCache.section, section));\n }\n }\n}\n\n// Default instance export\nexport const cmsPublishedCacheRepository = new CmsPublishedCacheRepository();\n","/**\n * CMS Admin Routes\n *\n * 관리자용 라벨 관리 API\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { defineRouter, route } from '@spfn/core/route';\nimport { authenticate, requireRole } from '@spfn/auth/server';\nimport {\n getSectionLabels,\n saveSectionDraft,\n publishSection,\n resetSectionDraft,\n} from '../services/publish.service';\n\n// These routes rewrite/publish site-wide CMS content, so they must be gated.\n// Authenticate AND require an admin-tier role at the package level — do NOT rely on\n// the consumer's global auth for authorization (fail closed). requireRole is an\n// exact match, so both admin tiers are listed explicitly.\nconst ADMIN_GUARD = [authenticate, requireRole('admin', 'superadmin')];\n\n/**\n * 섹션의 모든 라벨 조회 (테이블 뷰용)\n *\n * GET /_cms/admin/sections/:section/labels?locales=en,ko\n */\nexport const getSectionLabelsRoute = route.get('/_cms/admin/sections/:section/labels')\n .input({\n params: Type.Object({\n section: Type.String(),\n }),\n query: Type.Object({\n locales: Type.Optional(Type.String()), // comma-separated: \"en,ko\"\n }),\n })\n .use(ADMIN_GUARD)\n .handler(async (c) =>\n {\n const { params, query } = await c.data();\n const { section } = params;\n const locales = query.locales?.split(',') || ['en'];\n\n return getSectionLabels(section, locales);\n });\n\n/**\n * 섹션 라벨 일괄 Draft 저장\n *\n * PUT /_cms/admin/sections/:section/draft\n */\nexport const saveSectionDraftRoute = route.put('/_cms/admin/sections/:section/draft')\n .input({\n params: Type.Object({\n section: Type.String(),\n }),\n body: Type.Object({\n labels: Type.Array(\n Type.Object({\n id: Type.Number(),\n values: Type.Record(Type.String(), Type.String()),\n }),\n ),\n }),\n })\n .use(ADMIN_GUARD)\n .handler(async (c) =>\n {\n const { params, body } = await c.data();\n const { section } = params;\n const { labels } = body;\n\n const result = await saveSectionDraft(section, labels);\n\n return { success: true, ...result };\n });\n\n/**\n * 섹션 전체 발행\n *\n * POST /_cms/admin/sections/:section/publish\n */\nexport const publishSectionRoute = route.post('/_cms/admin/sections/:section/publish')\n .input({\n params: Type.Object({\n section: Type.String(),\n }),\n body: Type.Object({\n locales: Type.Array(Type.String()),\n }),\n })\n .use(ADMIN_GUARD)\n .handler(async (c) =>\n {\n const { params, body } = await c.data();\n const { section } = params;\n const { locales } = body;\n\n const result = await publishSection(section, locales);\n\n return { success: true, ...result };\n });\n\n/**\n * 섹션 Draft 초기화\n *\n * DELETE /_cms/admin/sections/:section/draft\n */\nexport const resetSectionDraftRoute = route.delete('/_cms/admin/sections/:section/draft')\n .input({\n params: Type.Object({\n section: Type.String(),\n }),\n })\n .use(ADMIN_GUARD)\n .handler(async (c) =>\n {\n const { params } = await c.data();\n const { section } = params;\n\n const result = await resetSectionDraft(section);\n\n return { success: true, ...result };\n });\n\n/**\n * Admin Router\n */\nexport const cmsAdminRouter = defineRouter({\n getSectionLabels: getSectionLabelsRoute,\n saveSectionDraft: saveSectionDraftRoute,\n publishSection: publishSectionRoute,\n resetSectionDraft: resetSectionDraftRoute,\n});\n\nexport type CmsAdminRouter = typeof cmsAdminRouter;\n","/**\n * CMS Publish Service\n *\n * 라벨 Draft 저장 및 발행 관리\n */\n\nimport { logger } from '@spfn/core/logger';\nimport { runInTransaction } from '@spfn/core/db';\nimport {\n cmsLabelsRepository,\n cmsLabelValuesRepository,\n cmsPublishedCacheRepository,\n} from '../repositories';\nimport { type CmsLabel, type CmsLabelValue, type NewCmsLabelValue } from '../entities';\n\nconst publishLogger = logger.child('@spfn/cms:publish');\n\n/**\n * 섹션의 모든 라벨 조회 (Admin 테이블 뷰용)\n */\nexport async function getSectionLabels(\n section: string,\n locales: string[],\n): Promise<{\n section: string;\n locales: string[];\n labels: Array<{\n id: number;\n key: string;\n description: string | null;\n defaultValue: Record<string, string>;\n draft: Record<string, string> | null;\n published: Record<string, string> | null;\n hasDraft: boolean;\n }>;\n}>\n{\n publishLogger.debug('getSectionLabels', { section, locales });\n\n // 1. 섹션의 모든 라벨 메타데이터 조회\n const labels = await cmsLabelsRepository.findBySection(section);\n\n if (labels.length === 0)\n {\n return { section, locales, labels: [] };\n }\n\n // 2. 모든 라벨의 Draft 값을 한 번에 조회 (라벨당 1쿼리 → 1쿼리, N+1/풀고갈 제거)\n const labelIds = labels.map(l => l.id);\n const allDrafts = await cmsLabelValuesRepository.findDraftsByLabelIds(labelIds);\n\n const draftsByLabel = new Map<number, CmsLabelValue[]>();\n for (const draft of allDrafts)\n {\n const list = draftsByLabel.get(draft.labelId);\n if (list)\n {\n list.push(draft);\n }\n else\n {\n draftsByLabel.set(draft.labelId, [draft]);\n }\n }\n\n // 3. 각 라벨의 Published 값 조회\n const labelVersions = labels\n .filter(l => l.publishedVersion !== null)\n .map(l => ({ labelId: l.id, version: l.publishedVersion! }));\n\n const publishedValuesMap = labelVersions.length > 0\n ? await cmsLabelValuesRepository.findByLabelVersions(labelVersions)\n : new Map();\n\n // 4. 결과 조합\n const result = labels.map((label) =>\n {\n const drafts = draftsByLabel.get(label.id) || [];\n const published = publishedValuesMap.get(label.id) || [];\n\n // Draft를 locale별 Record로 변환\n const draftRecord: Record<string, string> | null = drafts.length > 0\n ? drafts.reduce((acc, d) =>\n {\n const value = d.value as any;\n acc[d.locale] = value?.content ?? value;\n\n return acc;\n }, {} as Record<string, string>)\n : null;\n\n // Published를 locale별 Record로 변환\n let publishedRecord: Record<string, string> | null = null;\n if (published.length > 0)\n {\n publishedRecord = {};\n for (const p of published as CmsLabelValue[])\n {\n const value = p.value as any;\n publishedRecord[p.locale] = value?.content ?? value;\n }\n }\n\n return {\n id: label.id,\n key: label.key,\n description: label.description,\n defaultValue: (label.defaultValue as Record<string, string>) || {},\n draft: draftRecord,\n published: publishedRecord,\n hasDraft: drafts.length > 0,\n };\n });\n\n return { section, locales, labels: result };\n}\n\n/**\n * 섹션 라벨 일괄 Draft 저장\n */\nexport async function saveSectionDraft(\n section: string,\n labels: Array<{\n id: number;\n values: Record<string, string>;\n }>,\n): Promise<{ updated: number }>\n{\n publishLogger.debug('saveSectionDraft', { section, labelCount: labels.length });\n\n // 저장 대상 (labelId, locale) 쌍과 새 draft 행을 모은다. 라벨×locale마다\n // upsert(SELECT+write)를 직렬로 돌리던 것을, 해당 쌍의 draft만 한 번에 삭제하고\n // 새 값들을 한 번에 INSERT하는 배치로 바꾼다 (트랜잭션으로 원자적).\n const pairs: Array<{ labelId: number; locale: string }> = [];\n const rows: (NewCmsLabelValue & { labelId: number })[] = [];\n\n for (const { id, values } of labels)\n {\n for (const [locale, value] of Object.entries(values))\n {\n pairs.push({ labelId: id, locale });\n rows.push({\n labelId: id,\n version: null,\n locale,\n value: { type: 'text', content: value },\n });\n }\n }\n\n if (rows.length > 0)\n {\n await runInTransaction(async () =>\n {\n await cmsLabelValuesRepository.deleteDraftsByLabelLocales(pairs);\n await cmsLabelValuesRepository.insertDraftValues(rows);\n });\n }\n\n const updated = rows.length;\n\n publishLogger.info('Draft saved', { section, updated });\n\n return { updated };\n}\n\n/**\n * 섹션 전체 발행\n *\n * 1. Draft가 있는 라벨들의 Draft 값을 새 버전으로 복사\n * 2. cms_labels.publishedVersion 업데이트\n * 3. cms_published_cache 갱신\n */\nexport async function publishSection(\n section: string,\n locales: string[],\n): Promise<{\n published: number;\n version: number;\n labels: string[];\n}>\n{\n publishLogger.debug('publishSection', { section, locales });\n\n // 1. 섹션의 모든 라벨 조회\n const labels = await cmsLabelsRepository.findBySection(section);\n\n if (labels.length === 0)\n {\n return { published: 0, version: 0, labels: [] };\n }\n\n // 2. 모든 라벨의 Draft 값을 한 번에 조회 (라벨당 SELECT → 1 SELECT, N+1 제거)\n const labelIds = labels.map(l => l.id);\n const allDrafts = await cmsLabelValuesRepository.findDraftsByLabelIds(labelIds);\n\n const draftsByLabel = new Map<number, CmsLabelValue[]>();\n for (const draft of allDrafts)\n {\n const list = draftsByLabel.get(draft.labelId);\n if (list)\n {\n list.push(draft);\n }\n else\n {\n draftsByLabel.set(draft.labelId, [draft]);\n }\n }\n\n // 3. 새 버전 행/업데이트를 모은다 (DB는 아래 트랜잭션에서 배치로)\n const newRows: (NewCmsLabelValue & { labelId: number })[] = [];\n const versionUpdates: Array<{ id: number; version: number }> = [];\n const publishedLabels: string[] = [];\n let maxVersion = 0;\n\n for (const label of labels)\n {\n const drafts = draftsByLabel.get(label.id);\n if (!drafts || drafts.length === 0)\n {\n continue;\n }\n\n const newVersion = (label.publishedVersion || 0) + 1;\n maxVersion = Math.max(maxVersion, newVersion);\n\n for (const draft of drafts)\n {\n newRows.push({\n labelId: label.id,\n version: newVersion,\n locale: draft.locale,\n breakpoint: draft.breakpoint,\n value: draft.value,\n });\n }\n\n versionUpdates.push({ id: label.id, version: newVersion });\n publishedLabels.push(label.key);\n }\n\n // 4. 발행은 원자적으로 — 새 버전 multi-row INSERT → draft 일괄 삭제 →\n // publishedVersion 업데이트. (라벨당 1+2D+1+1 직렬 왕복 → 소수의 배치 쿼리)\n if (publishedLabels.length > 0)\n {\n const publishedLabelIds = versionUpdates.map(v => v.id);\n\n await runInTransaction(async () =>\n {\n await cmsLabelValuesRepository.insertPublishedValues(newRows);\n await cmsLabelValuesRepository.deleteDraftsByLabelIds(publishedLabelIds);\n\n for (const update of versionUpdates)\n {\n await cmsLabelsRepository.updateById(update.id, {\n publishedVersion: update.version,\n });\n }\n });\n\n // 5. cms_published_cache 갱신 — 방금 발행한 새 publishedVersion을 라벨에\n // 머지해 넘긴다(rebuild가 다시 SELECT하지 않게; 발행 안 된 라벨은 기존 버전).\n const newVersionById = new Map(versionUpdates.map(v => [v.id, v.version]));\n const labelsAfterPublish = labels.map(l => ({\n ...l,\n publishedVersion: newVersionById.get(l.id) ?? l.publishedVersion,\n }));\n\n await rebuildSectionCache(section, locales, labelsAfterPublish);\n }\n\n publishLogger.info('Section published', {\n section,\n published: publishedLabels.length,\n version: maxVersion,\n });\n\n return {\n published: publishedLabels.length,\n version: maxVersion,\n labels: publishedLabels,\n };\n}\n\n/**\n * 섹션 Draft 초기화 (삭제)\n */\nexport async function resetSectionDraft(section: string): Promise<{ reset: number }>\n{\n publishLogger.debug('resetSectionDraft', { section });\n\n // 1. 섹션의 모든 라벨 조회\n const labels = await cmsLabelsRepository.findBySection(section);\n\n let reset = 0;\n\n for (const label of labels)\n {\n // 2. Draft 값 삭제 (version: null)\n const drafts = await cmsLabelValuesRepository.findDraftsByLabelId(label.id);\n\n if (drafts.length > 0)\n {\n await cmsLabelValuesRepository.deleteByVersion(label.id, null as any);\n reset += drafts.length;\n }\n }\n\n publishLogger.info('Draft reset', { section, reset });\n\n return { reset };\n}\n\n/**\n * 섹션의 Published Cache 재구축\n */\nasync function rebuildSectionCache(\n section: string,\n locales: string[],\n preloadedLabels?: CmsLabel[],\n): Promise<void>\n{\n publishLogger.debug('rebuildSectionCache', { section, locales });\n\n // 1. 섹션의 모든 라벨 — 호출자가 (방금 갱신된 publishedVersion까지 반영된)\n // 라벨을 갖고 있으면 재사용해 publish당 중복 full-section SELECT를 없앤다.\n const labels = preloadedLabels ?? await cmsLabelsRepository.findBySection(section);\n\n // 2. Published 값이 있는 라벨들 조회\n const labelVersions = labels\n .filter(l => l.publishedVersion !== null)\n .map(l => ({ labelId: l.id, version: l.publishedVersion! }));\n\n if (labelVersions.length === 0)\n {\n return;\n }\n\n const publishedValuesMap = await cmsLabelValuesRepository.findByLabelVersions(labelVersions);\n\n // 3. locale별로 cache 구축\n for (const locale of locales)\n {\n const content: Record<string, any> = {};\n\n for (const label of labels)\n {\n const values = publishedValuesMap.get(label.id) || [];\n const localeValue = values.find(v => v.locale === locale);\n\n if (localeValue)\n {\n // key에서 section prefix 제거하여 저장\n // 예: \"home.hero.title\" → content[\"home.hero.title\"] = value\n content[label.key] = localeValue.value;\n }\n }\n\n // 4. cache upsert\n await cmsPublishedCacheRepository.upsert({\n section,\n locale,\n content,\n publishedAt: new Date(),\n publishedBy: 'system',\n });\n }\n\n publishLogger.debug('Cache rebuilt', { section, locales });\n}\n","/**\n * CMS Label Synchronization Service\n *\n * Synchronizes labels defined in code with database\n */\n\nimport { isEqual } from 'lodash-es';\nimport { type SyncOptions, type SyncResult } from '../../lib/types';\nimport { type FlatLabelWithDescription, flattenLabels } from '../../lib/helpers';\nimport { cmsLabelsRepository } from '../repositories';\nimport { type NewCmsLabel } from '../entities';\n\ntype DbLabelMap = Record<string, { values: Record<string, string>; description?: string | null }>;\n\n/**\n * Compare current DB labels with new labels\n *\n * @param dbLabels - Labels currently in database\n * @param codeLabels - Labels from code (flattened)\n * @returns Comparison result with added/removed/updated labels\n */\nfunction compareLabels(dbLabels: DbLabelMap, codeLabels: FlatLabelWithDescription): SyncResult\n{\n const added: string[] = [];\n const removed: string[] = [];\n const updated: string[] = [];\n const unchanged: string[] = [];\n\n const dbKeys = Object.keys(dbLabels);\n const codeKeys = Object.keys(codeLabels);\n\n // Check for added and updated labels\n for (const key of codeKeys)\n {\n if (!(key in dbLabels))\n {\n // New label\n added.push(key);\n }\n else\n {\n // Check if values or description changed\n const dbItem = dbLabels[key];\n const codeItem = codeLabels[key];\n\n const valuesChanged = !isEqual(dbItem.values, codeItem.values);\n const descChanged = (dbItem.description ?? undefined) !== codeItem.description;\n\n if (valuesChanged || descChanged)\n {\n updated.push(key);\n }\n else\n {\n unchanged.push(key);\n }\n }\n }\n\n // Check for removed labels\n for (const key of dbKeys)\n {\n if (!(key in codeLabels))\n {\n removed.push(key);\n }\n }\n\n return {\n added,\n removed,\n updated,\n unchanged,\n };\n}\n\n/**\n * Sync labels with database\n *\n * @param labels - Single label definition or array of label definitions\n * @param options - Sync options\n * @returns Sync result\n *\n * @example\n * ```typescript\n * // Single definition\n * await syncLabels(labelsDefinition);\n *\n * // Multiple definitions\n * await syncLabels([homeLabels, aboutLabels, commonLabels]);\n * ```\n */\nexport async function syncLabels<T extends Record<string, any>>(\n labels: T | T[],\n options?: SyncOptions,\n): Promise<SyncResult>\n{\n const { removeOrphaned = false, dryRun = false } = options || {};\n\n // 1. Merge multiple label definitions into one (if array provided)\n const mergedLabels = Array.isArray(labels)\n ? Object.assign({}, ...labels)\n : labels;\n\n // 2. Flatten code labels\n const codeLabels = flattenLabels(mergedLabels);\n\n // 3. Fetch current labels from DB\n const dbLabels = await cmsLabelsRepository.findMany();\n const dbLabelMap: DbLabelMap = {};\n\n for (const label of dbLabels)\n {\n if (label.defaultValue)\n {\n dbLabelMap[label.key] = {\n values: label.defaultValue as Record<string, string>,\n description: label.description,\n };\n }\n }\n\n // 4. Compare changes\n const result = compareLabels(dbLabelMap, codeLabels);\n\n // 5. Return result if dry run\n if (dryRun)\n {\n return result;\n }\n\n // 6. Create new labels\n if (result.added.length > 0)\n {\n const toCreate: NewCmsLabel[] = result.added.map(key => ({\n key,\n section: extractSection(key),\n type: 'text',\n defaultValue: codeLabels[key].values,\n description: codeLabels[key].description,\n }));\n\n await cmsLabelsRepository.bulkCreate(toCreate);\n }\n\n // 7. Update changed labels\n if (result.updated.length > 0)\n {\n const updates = result.updated.map(key => ({\n key,\n data: {\n defaultValue: codeLabels[key].values,\n description: codeLabels[key].description,\n },\n }));\n\n await cmsLabelsRepository.bulkUpdateByKeys(updates);\n }\n\n // 8. Remove deleted labels (only if option is true)\n if (removeOrphaned && result.removed.length > 0)\n {\n await cmsLabelsRepository.bulkDeleteByKeys(result.removed);\n }\n\n return result;\n}\n\n/**\n * Extract section from key\n * Example: \"home.hero.title\" -> \"home\"\n */\nfunction extractSection(key: string): string\n{\n const parts = key.split('.');\n\n return parts[0] || key;\n}\n","/**\n * CMS Helper Functions\n */\n\nexport type FlatLabel = Record<string, Record<string, string>>;\n\nexport type FlatLabelItem = {\n values: Record<string, string>;\n description?: string;\n};\n\nexport type FlatLabelWithDescription = Record<string, FlatLabelItem>;\n\n/**\n * Flatten nested label structure into dot notation\n *\n * @param labels - Nested label object\n * @param prefix - Key prefix for recursion\n * @returns Flattened label structure with description\n *\n * @example\n * ```typescript\n * const nested = {\n * home: {\n * hero: {\n * title: {\n * en: \"Welcome\",\n * ko: \"환영합니다\",\n * $description: \"Main hero title\"\n * }\n * }\n * }\n * };\n *\n * const flat = flattenLabels(nested);\n * // {\n * // \"home.hero.title\": {\n * // values: { en: \"Welcome\", ko: \"환영합니다\" },\n * // description: \"Main hero title\"\n * // }\n * // }\n * ```\n */\nexport function flattenLabels<T extends Record<string, any>>(\n labels: T,\n prefix = '',\n): FlatLabelWithDescription\n{\n const result: FlatLabelWithDescription = {};\n\n if (!labels || typeof labels !== 'object')\n {\n return result;\n }\n\n const obj = labels as Record<string, unknown>;\n\n for (const [key, value] of Object.entries(obj))\n {\n const newKey = prefix ? `${prefix}.${key}` : key;\n\n if (!value || typeof value !== 'object')\n {\n continue;\n }\n\n const valueObj = value as Record<string, unknown>;\n\n // Check if this is a leaf node (locale values: { en: \"...\", ko: \"...\", $description?: \"...\" })\n // All values must be strings (including $description)\n const isLeaf = Object.values(valueObj).every(v => typeof v === 'string');\n\n if (isLeaf)\n {\n const { $description, ...localeValues } = valueObj as Record<string, string>;\n\n result[newKey] = {\n values: localeValues,\n description: $description,\n };\n }\n else\n {\n // Recursively flatten nested structure\n Object.assign(result, flattenLabels(value, newKey));\n }\n }\n\n return result;\n}\n\n/**\n * Set a value in nested object using dot notation path\n *\n * @param target - Target object to modify\n * @param path - Dot notation path (e.g., \"home.hero.title\")\n * @param value - Value to set\n *\n * @example\n * ```typescript\n * const obj = {};\n * setNestedValue(obj, \"home.hero.title\", \"Welcome\");\n * // obj = { home: { hero: { title: \"Welcome\" } } }\n * ```\n */\nexport function setNestedValue(target: any, path: string, value: any): void\n{\n const parts = path.split('.');\n let current = target;\n\n for (let i = 0; i < parts.length - 1; i++)\n {\n const part = parts[i];\n if (!current[part])\n {\n current[part] = {};\n }\n current = current[part];\n }\n\n // Set the leaf value\n const lastPart = parts[parts.length - 1];\n current[lastPart] = value;\n}\n\n/**\n * Unflatten dot notation keys back to nested structure\n *\n * @param flat - Flattened label object\n * @returns Nested label structure\n *\n * @example\n * ```typescript\n * const flat = {\n * \"home.hero.title\": { en: \"Welcome\", ko: \"환영합니다\" },\n * \"home.hero.subtitle\": { en: \"Subtitle\", ko: \"부제목\" }\n * };\n *\n * const nested = unflattenLabels(flat);\n * // {\n * // home: {\n * // hero: {\n * // title: { en: \"Welcome\", ko: \"환영합니다\" },\n * // subtitle: { en: \"Subtitle\", ko: \"부제목\" }\n * // }\n * // }\n * // }\n * ```\n */\nexport function unflattenLabels(flat: FlatLabel): Record<string, any>\n{\n const result: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(flat))\n {\n setNestedValue(result, key, value);\n }\n\n return result;\n}\n"],"mappings":"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","names":["Type","defineRouter","route","integer","text","index","id","typedJsonb","id","integer","text","typedJsonb","index","text","integer","index","unique","id","typedJsonb","id","text","typedJsonb","integer","unique","index","id","BaseRepository","eq","inArray","BaseRepository","eq","inArray","BaseRepository","eq","and","sql","inArray","BaseRepository","and","eq","sql","inArray","id","route","Type","defineRouter"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfn/cms",
3
- "version": "0.2.0-beta.24",
3
+ "version": "0.2.0-beta.26",
4
4
  "description": "SPFN CMS - Content Management System with type-safe labels and Next.js integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -96,9 +96,9 @@
96
96
  "tsx": "^4.20.6",
97
97
  "typescript": "^5.3.3",
98
98
  "vitest": "^4.0.6",
99
- "@spfn/auth": "0.2.0-beta.72",
100
- "spfn": "0.2.0-beta.52",
101
- "@spfn/core": "0.2.0-beta.56"
99
+ "@spfn/core": "0.2.0-beta.59",
100
+ "@spfn/auth": "0.2.0-beta.74",
101
+ "spfn": "0.2.0-beta.54"
102
102
  },
103
103
  "scripts": {
104
104
  "build": "pnpm check:circular && tsup",