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

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
@@ -239,7 +239,7 @@ var cmsLabelsRepository = new CmsLabelsRepository();
239
239
 
240
240
  // src/server/repositories/cms-label-values.repository.ts
241
241
  import { BaseRepository as BaseRepository2 } from "@spfn/core/db";
242
- import { eq as eq2, and, isNull, gte, lte, inArray as inArray2 } from "drizzle-orm";
242
+ import { eq as eq2, and, or, isNull, gte, lte, inArray as inArray2, sql } from "drizzle-orm";
243
243
  var CmsLabelValuesRepository = class extends BaseRepository2 {
244
244
  /**
245
245
  * 특정 라벨의 특정 버전 값들 조회
@@ -314,6 +314,90 @@ var CmsLabelValuesRepository = class extends BaseRepository2 {
314
314
  }
315
315
  return results;
316
316
  }
317
+ /**
318
+ * 여러 라벨의 Draft 값들을 한 번에 조회 (version = null) — publish N+1 제거
319
+ * Read replica 사용
320
+ */
321
+ async findDraftsByLabelIds(labelIds) {
322
+ if (labelIds.length === 0) {
323
+ return [];
324
+ }
325
+ return this.readDb.select().from(cmsLabelValues).where(
326
+ and(
327
+ inArray2(cmsLabelValues.labelId, labelIds),
328
+ isNull(cmsLabelValues.version)
329
+ )
330
+ );
331
+ }
332
+ /**
333
+ * 새 published 버전 값들을 한 번에 multi-row INSERT (publish 배치용).
334
+ * unique(labelId, version, locale, breakpoint) 충돌 시 value 갱신(멱등).
335
+ * Write primary 사용
336
+ */
337
+ async insertPublishedValues(rows) {
338
+ if (rows.length === 0) {
339
+ return;
340
+ }
341
+ await this.db.insert(cmsLabelValues).values(rows).onConflictDoUpdate({
342
+ target: [
343
+ cmsLabelValues.labelId,
344
+ cmsLabelValues.version,
345
+ cmsLabelValues.locale,
346
+ cmsLabelValues.breakpoint
347
+ ],
348
+ set: { value: sql`excluded.value` }
349
+ });
350
+ }
351
+ /**
352
+ * 여러 라벨의 Draft 값들을 한 번에 삭제 (version = null) — publish 배치용
353
+ * Write primary 사용
354
+ */
355
+ async deleteDraftsByLabelIds(labelIds) {
356
+ if (labelIds.length === 0) {
357
+ return;
358
+ }
359
+ await this.db.delete(cmsLabelValues).where(
360
+ and(
361
+ inArray2(cmsLabelValues.labelId, labelIds),
362
+ isNull(cmsLabelValues.version)
363
+ )
364
+ );
365
+ }
366
+ /**
367
+ * 특정 (labelId, locale) 쌍들의 Draft(breakpoint = null) 일괄 삭제.
368
+ * saveSectionDraft의 delete-then-insert 배치용 — 저장 대상 locale만 교체한다.
369
+ * Write primary 사용
370
+ */
371
+ async deleteDraftsByLabelLocales(pairs) {
372
+ if (pairs.length === 0) {
373
+ return;
374
+ }
375
+ await this.db.delete(cmsLabelValues).where(
376
+ and(
377
+ isNull(cmsLabelValues.version),
378
+ isNull(cmsLabelValues.breakpoint),
379
+ or(
380
+ ...pairs.map(
381
+ (p) => and(
382
+ eq2(cmsLabelValues.labelId, p.labelId),
383
+ eq2(cmsLabelValues.locale, p.locale)
384
+ )
385
+ )
386
+ )
387
+ )
388
+ );
389
+ }
390
+ /**
391
+ * Draft 값들을 한 번에 multi-row INSERT (saveSectionDraft 배치용).
392
+ * 호출 전에 같은 (labelId, locale) draft를 삭제한 상태를 전제로 한다.
393
+ * Write primary 사용
394
+ */
395
+ async insertDraftValues(rows) {
396
+ if (rows.length === 0) {
397
+ return;
398
+ }
399
+ await this.db.insert(cmsLabelValues).values(rows);
400
+ }
317
401
  /**
318
402
  * 특정 버전의 모든 값 삭제
319
403
  * Write primary 사용
@@ -349,22 +433,22 @@ var CmsLabelValuesRepository = class extends BaseRepository2 {
349
433
  return /* @__PURE__ */ new Map();
350
434
  }
351
435
  const allValues = await this.readDb.select().from(cmsLabelValues).where(
352
- and(
353
- inArray2(
354
- cmsLabelValues.labelId,
355
- labelVersions.map((lv) => lv.labelId)
436
+ or(
437
+ ...labelVersions.map(
438
+ (lv) => and(
439
+ eq2(cmsLabelValues.labelId, lv.labelId),
440
+ eq2(cmsLabelValues.version, lv.version)
441
+ )
356
442
  )
357
443
  )
358
444
  );
359
- const versionMap = new Map(labelVersions.map((lv) => [lv.labelId, lv.version]));
360
445
  const resultMap = /* @__PURE__ */ new Map();
361
446
  for (const value of allValues) {
362
- const expectedVersion = versionMap.get(value.labelId);
363
- if (expectedVersion !== void 0 && value.version === expectedVersion) {
364
- if (!resultMap.has(value.labelId)) {
365
- resultMap.set(value.labelId, []);
366
- }
367
- resultMap.get(value.labelId).push(value);
447
+ const list = resultMap.get(value.labelId);
448
+ if (list) {
449
+ list.push(value);
450
+ } else {
451
+ resultMap.set(value.labelId, [value]);
368
452
  }
369
453
  }
370
454
  return resultMap;
@@ -419,7 +503,7 @@ var cmsLabelValuesRepository = new CmsLabelValuesRepository();
419
503
 
420
504
  // src/server/repositories/cms-published-cache.repository.ts
421
505
  import { BaseRepository as BaseRepository3 } from "@spfn/core/db";
422
- import { eq as eq3, and as and2, sql, inArray as inArray3 } from "drizzle-orm";
506
+ import { eq as eq3, and as and2, sql as sql2, inArray as inArray3 } from "drizzle-orm";
423
507
  var CmsPublishedCacheRepository = class extends BaseRepository3 {
424
508
  /**
425
509
  * 섹션 + 언어로 발행된 캐시 조회
@@ -445,7 +529,7 @@ var CmsPublishedCacheRepository = class extends BaseRepository3 {
445
529
  content: data.content,
446
530
  publishedAt: data.publishedAt,
447
531
  publishedBy: data.publishedBy,
448
- version: sql`${cmsPublishedCache.version} + 1`
532
+ version: sql2`${cmsPublishedCache.version} + 1`
449
533
  // 버전 증가로 클라이언트 캐시 무효화
450
534
  }
451
535
  }).returning();
@@ -495,9 +579,11 @@ var cmsPublishedCacheRepository = new CmsPublishedCacheRepository();
495
579
  // src/server/routes/admin.routes.ts
496
580
  import { Type } from "@sinclair/typebox";
497
581
  import { defineRouter, route } from "@spfn/core/route";
582
+ import { authenticate, requireRole } from "@spfn/auth/server";
498
583
 
499
584
  // src/server/services/publish.service.ts
500
585
  import { logger } from "@spfn/core/logger";
586
+ import { runInTransaction } from "@spfn/core/db";
501
587
  var publishLogger = logger.child("@spfn/cms:publish");
502
588
  async function getSectionLabels(section, locales) {
503
589
  publishLogger.debug("getSectionLabels", { section, locales });
@@ -506,13 +592,20 @@ async function getSectionLabels(section, locales) {
506
592
  return { section, locales, labels: [] };
507
593
  }
508
594
  const labelIds = labels.map((l) => l.id);
509
- const draftValues = await Promise.all(
510
- labelIds.map((id4) => cmsLabelValuesRepository.findDraftsByLabelId(id4))
511
- );
595
+ const allDrafts = await cmsLabelValuesRepository.findDraftsByLabelIds(labelIds);
596
+ const draftsByLabel = /* @__PURE__ */ new Map();
597
+ for (const draft of allDrafts) {
598
+ const list = draftsByLabel.get(draft.labelId);
599
+ if (list) {
600
+ list.push(draft);
601
+ } else {
602
+ draftsByLabel.set(draft.labelId, [draft]);
603
+ }
604
+ }
512
605
  const labelVersions = labels.filter((l) => l.publishedVersion !== null).map((l) => ({ labelId: l.id, version: l.publishedVersion }));
513
606
  const publishedValuesMap = labelVersions.length > 0 ? await cmsLabelValuesRepository.findByLabelVersions(labelVersions) : /* @__PURE__ */ new Map();
514
- const result = labels.map((label, index4) => {
515
- const drafts = draftValues[index4];
607
+ const result = labels.map((label) => {
608
+ const drafts = draftsByLabel.get(label.id) || [];
516
609
  const published = publishedValuesMap.get(label.id) || [];
517
610
  const draftRecord = drafts.length > 0 ? drafts.reduce((acc, d) => {
518
611
  const value = d.value;
@@ -541,18 +634,26 @@ async function getSectionLabels(section, locales) {
541
634
  }
542
635
  async function saveSectionDraft(section, labels) {
543
636
  publishLogger.debug("saveSectionDraft", { section, labelCount: labels.length });
544
- let updated = 0;
637
+ const pairs = [];
638
+ const rows = [];
545
639
  for (const { id: id4, values } of labels) {
546
640
  for (const [locale, value] of Object.entries(values)) {
547
- await cmsLabelValuesRepository.upsert({
641
+ pairs.push({ labelId: id4, locale });
642
+ rows.push({
548
643
  labelId: id4,
549
644
  version: null,
550
645
  locale,
551
646
  value: { type: "text", content: value }
552
647
  });
553
- updated++;
554
648
  }
555
649
  }
650
+ if (rows.length > 0) {
651
+ await runInTransaction(async () => {
652
+ await cmsLabelValuesRepository.deleteDraftsByLabelLocales(pairs);
653
+ await cmsLabelValuesRepository.insertDraftValues(rows);
654
+ });
655
+ }
656
+ const updated = rows.length;
556
657
  publishLogger.info("Draft saved", { section, updated });
557
658
  return { updated };
558
659
  }
@@ -562,17 +663,30 @@ async function publishSection(section, locales) {
562
663
  if (labels.length === 0) {
563
664
  return { published: 0, version: 0, labels: [] };
564
665
  }
666
+ const labelIds = labels.map((l) => l.id);
667
+ const allDrafts = await cmsLabelValuesRepository.findDraftsByLabelIds(labelIds);
668
+ const draftsByLabel = /* @__PURE__ */ new Map();
669
+ for (const draft of allDrafts) {
670
+ const list = draftsByLabel.get(draft.labelId);
671
+ if (list) {
672
+ list.push(draft);
673
+ } else {
674
+ draftsByLabel.set(draft.labelId, [draft]);
675
+ }
676
+ }
677
+ const newRows = [];
678
+ const versionUpdates = [];
565
679
  const publishedLabels = [];
566
680
  let maxVersion = 0;
567
681
  for (const label of labels) {
568
- const drafts = await cmsLabelValuesRepository.findDraftsByLabelId(label.id);
569
- if (drafts.length === 0) {
682
+ const drafts = draftsByLabel.get(label.id);
683
+ if (!drafts || drafts.length === 0) {
570
684
  continue;
571
685
  }
572
686
  const newVersion = (label.publishedVersion || 0) + 1;
573
687
  maxVersion = Math.max(maxVersion, newVersion);
574
688
  for (const draft of drafts) {
575
- await cmsLabelValuesRepository.upsert({
689
+ newRows.push({
576
690
  labelId: label.id,
577
691
  version: newVersion,
578
692
  locale: draft.locale,
@@ -580,13 +694,20 @@ async function publishSection(section, locales) {
580
694
  value: draft.value
581
695
  });
582
696
  }
583
- await cmsLabelValuesRepository.deleteByVersion(label.id, null);
584
- await cmsLabelsRepository.updateById(label.id, {
585
- publishedVersion: newVersion
586
- });
697
+ versionUpdates.push({ id: label.id, version: newVersion });
587
698
  publishedLabels.push(label.key);
588
699
  }
589
700
  if (publishedLabels.length > 0) {
701
+ const publishedLabelIds = versionUpdates.map((v) => v.id);
702
+ await runInTransaction(async () => {
703
+ await cmsLabelValuesRepository.insertPublishedValues(newRows);
704
+ await cmsLabelValuesRepository.deleteDraftsByLabelIds(publishedLabelIds);
705
+ for (const update of versionUpdates) {
706
+ await cmsLabelsRepository.updateById(update.id, {
707
+ publishedVersion: update.version
708
+ });
709
+ }
710
+ });
590
711
  await rebuildSectionCache(section, locales);
591
712
  }
592
713
  publishLogger.info("Section published", {
@@ -643,6 +764,7 @@ async function rebuildSectionCache(section, locales) {
643
764
  }
644
765
 
645
766
  // src/server/routes/admin.routes.ts
767
+ var ADMIN_GUARD = [authenticate, requireRole("admin", "superadmin")];
646
768
  var getSectionLabelsRoute = route.get("/_cms/admin/sections/:section/labels").input({
647
769
  params: Type.Object({
648
770
  section: Type.String()
@@ -651,7 +773,7 @@ var getSectionLabelsRoute = route.get("/_cms/admin/sections/:section/labels").in
651
773
  locales: Type.Optional(Type.String())
652
774
  // comma-separated: "en,ko"
653
775
  })
654
- }).handler(async (c) => {
776
+ }).use(ADMIN_GUARD).handler(async (c) => {
655
777
  const { params, query } = await c.data();
656
778
  const { section } = params;
657
779
  const locales = query.locales?.split(",") || ["en"];
@@ -669,7 +791,7 @@ var saveSectionDraftRoute = route.put("/_cms/admin/sections/:section/draft").inp
669
791
  })
670
792
  )
671
793
  })
672
- }).handler(async (c) => {
794
+ }).use(ADMIN_GUARD).handler(async (c) => {
673
795
  const { params, body } = await c.data();
674
796
  const { section } = params;
675
797
  const { labels } = body;
@@ -683,7 +805,7 @@ var publishSectionRoute = route.post("/_cms/admin/sections/:section/publish").in
683
805
  body: Type.Object({
684
806
  locales: Type.Array(Type.String())
685
807
  })
686
- }).handler(async (c) => {
808
+ }).use(ADMIN_GUARD).handler(async (c) => {
687
809
  const { params, body } = await c.data();
688
810
  const { section } = params;
689
811
  const { locales } = body;
@@ -694,7 +816,7 @@ var resetSectionDraftRoute = route.delete("/_cms/admin/sections/:section/draft")
694
816
  params: Type.Object({
695
817
  section: Type.String()
696
818
  })
697
- }).handler(async (c) => {
819
+ }).use(ADMIN_GUARD).handler(async (c) => {
698
820
  const { params } = await c.data();
699
821
  const { section } = params;
700
822
  const result = await resetSectionDraft(section);
@@ -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, SQL, isNull, gte, lte, inArray } 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 * 특정 버전의 모든 값 삭제\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 // 모든 label의 publishedVersion 값들을 한 번에 조회\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n inArray(\n cmsLabelValues.labelId,\n labelVersions.map(lv => lv.labelId),\n ),\n ),\n );\n\n // labelId와 version으로 필터링하여 Map 생성\n const versionMap = new Map(labelVersions.map(lv => [lv.labelId, lv.version]));\n const resultMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n const expectedVersion = versionMap.get(value.labelId);\n\n // 해당 labelId의 version이 일치하는 경우만 포함\n if (expectedVersion !== undefined && value.version === expectedVersion)\n {\n if (!resultMap.has(value.labelId))\n {\n resultMap.set(value.labelId, []);\n }\n resultMap.get(value.labelId)!.push(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 {\n getSectionLabels,\n saveSectionDraft,\n publishSection,\n resetSectionDraft,\n} from '../services/publish.service';\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 .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 .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 .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 .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 {\n cmsLabelsRepository,\n cmsLabelValuesRepository,\n cmsPublishedCacheRepository,\n} from '../repositories';\nimport { type CmsLabelValue } 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 값 조회 (version: null)\n const labelIds = labels.map(l => l.id);\n const draftValues = await Promise.all(\n labelIds.map(id => cmsLabelValuesRepository.findDraftsByLabelId(id)),\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, index) =>\n {\n const drafts = draftValues[index];\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 let updated = 0;\n\n for (const { id, values } of labels)\n {\n // 각 locale별로 Draft 저장 (version: null)\n for (const [locale, value] of Object.entries(values))\n {\n await cmsLabelValuesRepository.upsert({\n labelId: id,\n version: null,\n locale,\n value: { type: 'text', content: value },\n });\n updated++;\n }\n }\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 const publishedLabels: string[] = [];\n let maxVersion = 0;\n\n for (const label of labels)\n {\n // 2. Draft 값 조회\n const drafts = await cmsLabelValuesRepository.findDraftsByLabelId(label.id);\n\n if (drafts.length === 0)\n {\n continue;\n }\n\n // 3. 새 버전 번호 생성\n const newVersion = (label.publishedVersion || 0) + 1;\n maxVersion = Math.max(maxVersion, newVersion);\n\n // 4. Draft 값을 새 버전으로 복사\n for (const draft of drafts)\n {\n await cmsLabelValuesRepository.upsert({\n labelId: label.id,\n version: newVersion,\n locale: draft.locale,\n breakpoint: draft.breakpoint,\n value: draft.value,\n });\n }\n\n // 4.5. Draft 삭제 (version: null)\n await cmsLabelValuesRepository.deleteByVersion(label.id, null);\n\n // 5. cms_labels.publishedVersion 업데이트\n await cmsLabelsRepository.updateById(label.id, {\n publishedVersion: newVersion,\n });\n\n publishedLabels.push(label.key);\n }\n\n // 6. cms_published_cache 갱신\n if (publishedLabels.length > 0)\n {\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","inArray","BaseRepository","and","eq","inArray","id","index","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 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfn/cms",
3
- "version": "0.2.0-beta.22",
3
+ "version": "0.2.0-beta.24",
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",
@@ -71,6 +71,7 @@
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@spfn/core": ">=0.2.0-beta.54",
74
+ "@spfn/auth": ">=0.2.0-beta.70",
74
75
  "drizzle-orm": ">=0.44.7",
75
76
  "next": "^15.0.0 || ^16.0.0",
76
77
  "react": "^18.0.0 || ^19.0.0"
@@ -78,7 +79,7 @@
78
79
  "dependencies": {
79
80
  "@sinclair/typebox": "^0.34.0",
80
81
  "jiti": "^2.6.1",
81
- "lodash-es": "^4.17.21",
82
+ "lodash-es": "^4.18.1",
82
83
  "zustand": "^5.0.8"
83
84
  },
84
85
  "devDependencies": {
@@ -95,8 +96,9 @@
95
96
  "tsx": "^4.20.6",
96
97
  "typescript": "^5.3.3",
97
98
  "vitest": "^4.0.6",
98
- "@spfn/core": "0.2.0-beta.55",
99
- "spfn": "0.2.0-beta.51"
99
+ "@spfn/auth": "0.2.0-beta.72",
100
+ "spfn": "0.2.0-beta.52",
101
+ "@spfn/core": "0.2.0-beta.56"
100
102
  },
101
103
  "scripts": {
102
104
  "build": "pnpm check:circular && tsup",