@spfn/cms 0.1.0-alpha.72 → 0.1.0-alpha.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/actions.js +1 -1
  2. package/dist/actions.js.map +1 -1
  3. package/dist/api.d.ts +77 -6
  4. package/dist/api.js +88 -8
  5. package/dist/api.js.map +1 -1
  6. package/dist/client.js +88 -8
  7. package/dist/client.js.map +1 -1
  8. package/dist/lib/contracts/labels.d.ts +66 -1
  9. package/dist/lib/contracts/labels.js +72 -0
  10. package/dist/lib/contracts/labels.js.map +1 -1
  11. package/dist/server/generators/index.js.map +1 -1
  12. package/dist/server/repositories/index.d.ts +5 -0
  13. package/dist/server/repositories/index.js +9 -0
  14. package/dist/server/repositories/index.js.map +1 -1
  15. package/dist/server/routes/labels/[id]/index.js +70 -0
  16. package/dist/server/routes/labels/[id]/index.js.map +1 -1
  17. package/dist/server/routes/labels/[labelId]/admin/index.js +652 -0
  18. package/dist/server/routes/labels/[labelId]/admin/index.js.map +1 -0
  19. package/dist/server/routes/labels/[labelId]/publish/index.js +846 -0
  20. package/dist/server/routes/labels/[labelId]/publish/index.js.map +1 -0
  21. package/dist/server/routes/labels/_labelId_/admin/index.d.ts +11 -0
  22. package/dist/server/routes/labels/_labelId_/publish/index.d.ts +11 -0
  23. package/dist/server/routes/labels/by-key/[key]/index.js +70 -0
  24. package/dist/server/routes/labels/by-key/[key]/index.js.map +1 -1
  25. package/dist/server/routes/labels/index.js +70 -0
  26. package/dist/server/routes/labels/index.js.map +1 -1
  27. package/dist/server/routes/published-cache/index.js.map +1 -1
  28. package/dist/server/routes/values/[labelId]/[version]/index.js +9 -0
  29. package/dist/server/routes/values/[labelId]/[version]/index.js.map +1 -1
  30. package/dist/server/routes/values/[labelId]/index.js +9 -0
  31. package/dist/server/routes/values/[labelId]/index.js.map +1 -1
  32. package/dist/server.js +10 -1
  33. package/dist/server.js.map +1 -1
  34. package/package.json +3 -3
package/dist/actions.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/server/helpers/locale.actions.ts
2
- import { cookies, headers } from "next/headers";
2
+ import { cookies, headers } from "next/headers.js";
3
3
 
4
4
  // src/server/config/cms.config.ts
5
5
  function getEnvVar(key, defaultValue) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/helpers/locale.actions.ts","../src/server/config/cms.config.ts","../src/lib/constants/locale.constants.ts"],"sourcesContent":["\"use server\";\n\n/**\n * Locale Management Server Actions\n *\n * Server Actions으로 구현된 locale 관리 함수\n * - 서버 컴포넌트: 일반 함수 호출로 동작\n * - 클라이언트 컴포넌트: Server Action으로 자동 처리\n */\n\nimport { cookies, headers } from 'next/headers';\nimport { getCmsConfig } from '@/server/config/cms.config';\nimport {\n LOCALE_COOKIE_KEY,\n getLocaleInfo,\n type LocaleInfo,\n} from '@/lib/constants/locale.constants';\n\n/**\n * 브라우저 언어 감지\n *\n * Accept-Language 헤더에서 지원하는 언어를 찾습니다.\n *\n * @returns 감지된 언어 코드 또는 null\n */\nasync function detectBrowserLanguage(): Promise<string | null>\n{\n try\n {\n const headersList = await headers();\n const acceptLanguage = headersList.get('accept-language');\n\n if (!acceptLanguage)\n {\n return null;\n }\n\n // \"ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\" 형식 파싱\n const languages = acceptLanguage\n .split(',')\n .map(lang =>\n {\n const [code] = lang.split(';');\n return code.split('-')[0].trim();\n });\n\n const config = getCmsConfig();\n\n // 지원하는 언어 중 첫 번째 매칭되는 언어 반환\n for (const lang of languages)\n {\n if (config.supportedLocales.includes(lang))\n {\n return lang;\n }\n }\n\n return null;\n }\n catch (error)\n {\n // 헤더 접근 실패 시\n return null;\n }\n}\n\n/**\n * 현재 locale 가져오기 (Server Action)\n *\n * 서버/클라이언트 컴포넌트 모두에서 사용 가능\n *\n * 우선순위:\n * 1. 쿠키 (사용자가 명시적으로 선택한 언어)\n * 2. 브라우저 언어 감지 (설정에서 활성화된 경우)\n * 3. 시스템 기본 언어 (CMS 설정)\n *\n * @returns 현재 locale (예: 'ko', 'en')\n *\n * @example\n * ```tsx\n * // Server Component\n * import { getLocale } from '@spfn/cms/actions';\n *\n * export default async function Page()\n * {\n * const locale = await getLocale();\n * return <div>Current locale: {locale}</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Client Component\n * 'use client';\n * import { getLocale } from '@spfn/cms/client';\n *\n * export default function LanguageSwitcher()\n * {\n * const [locale, setLocale] = useState('');\n *\n * useEffect(() => {\n * getLocale().then(setLocale);\n * }, []);\n *\n * return <div>Current locale: {locale}</div>;\n * }\n * ```\n */\nexport async function getLocale(): Promise<string>\n{\n const config = getCmsConfig();\n\n // 1순위: 쿠키 (사용자가 명시적으로 선택한 언어)\n const cookieStore = await cookies();\n const cookieLocale = cookieStore.get(LOCALE_COOKIE_KEY)?.value;\n\n if (cookieLocale && config.supportedLocales.includes(cookieLocale))\n {\n return cookieLocale;\n }\n\n // 2순위: 브라우저 언어 감지 (설정에서 활성화된 경우)\n if (config.detectBrowserLanguage)\n {\n const browserLang = await detectBrowserLanguage();\n if (browserLang)\n {\n return browserLang;\n }\n }\n\n // 3순위: 시스템 기본 언어\n return config.defaultLocale;\n}\n\n/**\n * Locale 설정하기 (Server Action)\n *\n * 서버/클라이언트 컴포넌트 모두에서 사용 가능\n * 쿠키에 locale을 저장합니다.\n *\n * @param locale - 설정할 locale (예: 'ko', 'en')\n * @throws {Error} 지원하지 않는 locale인 경우\n *\n * @example\n * ```tsx\n * // Server Component (Server Action)\n * import { setLocale } from '@spfn/cms/actions';\n *\n * export default async function Page()\n * {\n * await setLocale('en');\n * return <div>Locale changed</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Client Component (Server Action)\n * 'use client';\n * import { setLocale } from '@spfn/cms/client';\n *\n * export default function LanguageSwitcher()\n * {\n * const handleChange = async (newLocale: string) =>\n * {\n * await setLocale(newLocale);\n * window.location.reload(); // 페이지 새로고침\n * };\n *\n * return (\n * <button onClick={() => handleChange('en')}>\n * Switch to English\n * </button>\n * );\n * }\n * ```\n */\nexport async function setLocale(locale: string): Promise<void>\n{\n const config = getCmsConfig();\n\n // 유효성 검사\n if (!config.supportedLocales.includes(locale))\n {\n throw new Error(\n `Unsupported locale: ${locale}. Supported locales: ${config.supportedLocales.join(', ')}`\n );\n }\n\n const cookieStore = await cookies();\n\n cookieStore.set(LOCALE_COOKIE_KEY, locale, {\n path: '/',\n maxAge: 60 * 60 * 24 * 365, // 1년\n sameSite: 'lax',\n });\n}\n\n/**\n * 지원하는 locale 목록 가져오기 (Server Action)\n *\n * 서버/클라이언트 컴포넌트 모두에서 사용 가능\n *\n * @returns 지원하는 locale 배열 (예: ['ko', 'en', 'ja'])\n *\n * @example\n * ```tsx\n * // Server Component\n * import { getLocales } from '@spfn/cms/actions';\n *\n * export default async function Page()\n * {\n * const locales = await getLocales();\n * return <div>Supported: {locales.join(', ')}</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Client Component\n * 'use client';\n * import { getLocales } from '@spfn/cms/client';\n *\n * export default function LanguageSwitcher()\n * {\n * const [locales, setLocales] = useState<string[]>([]);\n *\n * useEffect(() => {\n * getLocales().then(setLocales);\n * }, []);\n *\n * return (\n * <div>\n * {locales.map(locale => (\n * <button key={locale}>{locale}</button>\n * ))}\n * </div>\n * );\n * }\n * ```\n */\nexport async function getLocales(): Promise<string[]>\n{\n const config = getCmsConfig();\n return config.supportedLocales;\n}\n\n/**\n * 현재 locale과 상세 정보 함께 가져오기 (Server Action)\n *\n * locale 코드와 함께 국가 코드, 국기, 전화번호 코드 등의 상세 정보를 반환합니다.\n *\n * @returns Locale 코드와 LocaleInfo 객체\n *\n * @example\n * ```tsx\n * // Server Component\n * import { getLocaleWithInfo } from '@spfn/cms/actions';\n *\n * export default async function Page()\n * {\n * const { locale, info } = await getLocaleWithInfo();\n *\n * return (\n * <div>\n * <span>{info?.flag}</span>\n * <span>{info?.nativeName}</span>\n * <span>{info?.dialCode}</span>\n * </div>\n * );\n * }\n * ```\n */\nexport async function getLocaleWithInfo(): Promise<{\n locale: string;\n info: LocaleInfo | undefined;\n}>\n{\n const locale = await getLocale();\n const info = getLocaleInfo(locale);\n\n return { locale, info };\n}\n\n/**\n * 지원하는 모든 locale과 상세 정보 가져오기 (Server Action)\n *\n * 시스템이 지원하는 모든 locale의 상세 정보를 배열로 반환합니다.\n * 언어 선택 UI를 만들 때 유용합니다.\n *\n * @returns LocaleInfo 배열\n *\n * @example\n * ```tsx\n * // Server Component\n * import { getLocalesWithInfo } from '@spfn/cms/actions';\n *\n * export default async function LanguageSelector()\n * {\n * const locales = await getLocalesWithInfo();\n *\n * return (\n * <select>\n * {locales.map(info => (\n * <option key={info.locale} value={info.locale}>\n * {info.flag} {info.nativeName}\n * </option>\n * ))}\n * </select>\n * );\n * }\n * ```\n */\nexport async function getLocalesWithInfo(): Promise<LocaleInfo[]>\n{\n const config = getCmsConfig();\n const supportedLocales = config.supportedLocales;\n\n return supportedLocales\n .map(locale => getLocaleInfo(locale))\n .filter((info): info is LocaleInfo => info !== undefined);\n}","/**\n * CMS Configuration Module\n *\n * 환경변수 기반 CMS 설정 관리\n * - SPFN_CMS_DEFAULT_LOCALE: 기본 언어 (기본값: 'ko')\n * - SPFN_CMS_SUPPORTED_LOCALES: 지원 언어 목록, 쉼표로 구분 (기본값: 'ko,en')\n * - SPFN_CMS_DETECT_BROWSER_LANGUAGE: 브라우저 언어 자동 감지 (기본값: 'false')\n */\n\n/**\n * CMS 설정 타입\n */\nexport interface CmsConfig\n{\n /**\n * 기본 언어 코드\n * @example 'ko', 'en', 'ja'\n */\n defaultLocale: string;\n\n /**\n * 지원하는 언어 목록\n * @example ['ko', 'en', 'ja']\n */\n supportedLocales: string[];\n\n /**\n * 브라우저 언어 자동 감지 여부\n * @default true\n */\n detectBrowserLanguage: boolean;\n}\n\n/**\n * 환경변수 읽기 헬퍼\n */\nfunction getEnvVar(key: string, defaultValue: string): string\n{\n return process.env[key] || defaultValue;\n}\n\n/**\n * 환경변수에서 boolean 읽기\n */\nfunction getEnvBoolean(key: string, defaultValue: boolean): boolean\n{\n const value = process.env[key];\n if (value === undefined) return defaultValue;\n return value === 'true' || value === '1';\n}\n\n/**\n * 환경변수에서 설정 로드\n */\nfunction loadConfigFromEnv(): CmsConfig\n{\n const defaultLocale = getEnvVar('SPFN_CMS_DEFAULT_LOCALE', 'en');\n const supportedLocalesStr = getEnvVar('SPFN_CMS_SUPPORTED_LOCALES', 'en,ko');\n const detectBrowserLanguage = getEnvBoolean('SPFN_CMS_DETECT_BROWSER_LANGUAGE', true);\n\n const supportedLocales = supportedLocalesStr\n .split(',')\n .map(locale => locale.trim())\n .filter(locale => locale.length > 0);\n\n // 기본 언어가 지원 목록에 없으면 추가\n if (!supportedLocales.includes(defaultLocale))\n {\n supportedLocales.unshift(defaultLocale);\n }\n\n return {\n defaultLocale,\n supportedLocales,\n detectBrowserLanguage,\n };\n}\n\n/**\n * 현재 설정 (환경변수에서 초기화)\n */\nlet currentConfig: CmsConfig = loadConfigFromEnv();\n\n/**\n * CMS 설정 조회\n *\n * @returns 현재 CMS 설정\n *\n * @example\n * ```tsx\n * import { getCmsConfig } from '@spfn/cms';\n *\n * const config = getCmsConfig();\n * console.log(config.defaultLocale); // 'ko'\n * console.log(config.supportedLocales); // ['ko', 'en']\n * ```\n */\nexport function getCmsConfig(): Readonly<CmsConfig>\n{\n return currentConfig;\n}\n\n/**\n * CMS 설정 변경 (런타임 오버라이드)\n *\n * 환경변수 설정을 런타임에 오버라이드합니다.\n * 주로 테스트나 특수한 경우에 사용됩니다.\n *\n * @param config - 변경할 설정 (부분 업데이트 가능)\n *\n * @example\n * ```tsx\n * import { configureCms } from '@spfn/cms';\n *\n * // 앱 초기화 시 (선택적)\n * configureCms({\n * defaultLocale: 'en',\n * supportedLocales: ['en', 'ko', 'ja'],\n * detectBrowserLanguage: true,\n * });\n * ```\n */\nexport function configureCms(config: Partial<CmsConfig>): void\n{\n currentConfig = {\n ...currentConfig,\n ...config,\n };\n\n // 기본 언어가 지원 목록에 있는지 확인\n if (config.defaultLocale && !currentConfig.supportedLocales.includes(config.defaultLocale))\n {\n console.warn(\n `[CMS Config] Default locale '${config.defaultLocale}' not in supported locales, adding automatically.`,\n `Supported locales: [${currentConfig.supportedLocales.join(', ')}]`\n );\n\n currentConfig.supportedLocales.unshift(config.defaultLocale);\n }\n}\n\n/**\n * 설정 초기화 (환경변수에서 재로드)\n *\n * @example\n * ```tsx\n * import { resetCmsConfig } from '@spfn/cms';\n *\n * // 환경변수 설정으로 되돌리기\n * resetCmsConfig();\n * ```\n */\nexport function resetCmsConfig(): void\n{\n currentConfig = loadConfigFromEnv();\n}","/**\n * Locale Constants\n *\n * Server/Client 양쪽에서 사용 가능한 locale 관련 상수\n */\n\n/**\n * Locale 쿠키 키\n */\nexport const LOCALE_COOKIE_KEY = 'spfn-locale';\n\n/**\n * 지원하는 Locale 타입 (Type-safe)\n */\nexport type SupportedLocale =\n // 아시아-태평양\n | 'ko' // 한국어\n | 'ja' // 일본어\n | 'zh' // 중국어 (간체)\n | 'zh-TW' // 중국어 (번체, 대만)\n | 'zh-HK' // 중국어 (홍콩)\n | 'hi' // 힌디어\n | 'th' // 태국어\n | 'vi' // 베트남어\n | 'id' // 인도네시아어\n | 'ms' // 말레이어\n // 영어권\n | 'en' // 영어 (미국)\n | 'en-GB' // 영어 (영국)\n | 'en-CA' // 영어 (캐나다)\n | 'en-AU' // 영어 (호주)\n | 'en-NZ' // 영어 (뉴질랜드)\n // 서유럽\n | 'es' // 스페인어 (스페인)\n | 'es-MX' // 스페인어 (멕시코)\n | 'es-AR' // 스페인어 (아르헨티나)\n | 'es-CO' // 스페인어 (콜롬비아)\n | 'fr' // 프랑스어\n | 'de' // 독일어\n | 'it' // 이탈리아어\n | 'pt' // 포르투갈어\n | 'nl' // 네덜란드어\n // 북유럽\n | 'sv' // 스웨덴어\n | 'no' // 노르웨이어\n | 'da' // 덴마크어\n | 'fi' // 핀란드어\n // 동유럽\n | 'ru' // 러시아어\n | 'pl' // 폴란드어\n | 'uk' // 우크라이나어\n | 'cs' // 체코어\n | 'hu' // 헝가리어\n | 'ro' // 루마니아어\n | 'bg' // 불가리아어\n | 'hr' // 크로아티아어\n | 'sr' // 세르비아어\n | 'sk' // 슬로바키아어\n | 'sl' // 슬로베니아어\n | 'lt' // 리투아니아어\n | 'lv' // 라트비아어\n | 'et' // 에스토니아어\n // 남유럽\n | 'el' // 그리스어\n // 중동\n | 'tr' // 터키어\n | 'ar' // 아랍어\n | 'fa' // 페르시아어\n | 'he' // 히브리어\n // 아프리카\n | 'sw'; // 스와힐리어\n\n/**\n * 국가/지역 정보 타입\n */\nexport interface LocaleInfo\n{\n /** Locale 코드 (ISO 639-1) */\n locale: SupportedLocale;\n /** 국가 코드 (ISO 3166-1 alpha-2) */\n countryCode: string;\n /** 국기 이모지 (HTML/React용) */\n flag: string;\n /** 전화번호 국가 코드 */\n dialCode: string;\n /** 네이티브 이름 (현지어) */\n nativeName: string;\n /** 영어 이름 */\n englishName: string;\n /** RTL (Right-to-Left) 여부 */\n rtl?: boolean;\n /** 통화 코드 (ISO 4217) */\n currencyCode?: string;\n /** 날짜 형식 예시 */\n dateFormat?: string;\n}\n\n/**\n * 사전 정의된 Locale 정보 맵\n *\n * 주요 언어/국가 정보를 포함합니다.\n * 프로젝트에 맞게 추가/수정 가능합니다.\n */\nexport const LOCALE_INFO_MAP: Record<SupportedLocale, LocaleInfo> = {\n // 한국어\n ko: {\n locale: 'ko',\n countryCode: 'KR',\n flag: '&#x1F1F0;&#x1F1F7;',\n dialCode: '+82',\n nativeName: '한국어',\n englishName: 'Korean',\n currencyCode: 'KRW',\n dateFormat: 'YYYY.MM.DD',\n },\n\n // 영어 (미국)\n en: {\n locale: 'en',\n countryCode: 'US',\n flag: '&#x1F1FA;&#x1F1F8;',\n dialCode: '+1',\n nativeName: 'English',\n englishName: 'English',\n currencyCode: 'USD',\n dateFormat: 'MM/DD/YYYY',\n },\n\n // 일본어\n ja: {\n locale: 'ja',\n countryCode: 'JP',\n flag: '&#x1F1EF;&#x1F1F5;',\n dialCode: '+81',\n nativeName: '日本語',\n englishName: 'Japanese',\n currencyCode: 'JPY',\n dateFormat: 'YYYY/MM/DD',\n },\n\n // 중국어 (간체)\n zh: {\n locale: 'zh',\n countryCode: 'CN',\n flag: '&#x1F1E8;&#x1F1F3;',\n dialCode: '+86',\n nativeName: '简体中文',\n englishName: 'Chinese (Simplified)',\n currencyCode: 'CNY',\n dateFormat: 'YYYY-MM-DD',\n },\n\n // 중국어 (번체, 대만)\n 'zh-TW': {\n locale: 'zh-TW',\n countryCode: 'TW',\n flag: '&#x1F1F9;&#x1F1FC;',\n dialCode: '+886',\n nativeName: '繁體中文',\n englishName: 'Chinese (Traditional)',\n currencyCode: 'TWD',\n dateFormat: 'YYYY/MM/DD',\n },\n\n // 스페인어\n es: {\n locale: 'es',\n countryCode: 'ES',\n flag: '&#x1F1EA;&#x1F1F8;',\n dialCode: '+34',\n nativeName: 'Español',\n englishName: 'Spanish',\n currencyCode: 'EUR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 프랑스어\n fr: {\n locale: 'fr',\n countryCode: 'FR',\n flag: '&#x1F1EB;&#x1F1F7;',\n dialCode: '+33',\n nativeName: 'Français',\n englishName: 'French',\n currencyCode: 'EUR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 독일어\n de: {\n locale: 'de',\n countryCode: 'DE',\n flag: '&#x1F1E9;&#x1F1EA;',\n dialCode: '+49',\n nativeName: 'Deutsch',\n englishName: 'German',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 이탈리아어\n it: {\n locale: 'it',\n countryCode: 'IT',\n flag: '&#x1F1EE;&#x1F1F9;',\n dialCode: '+39',\n nativeName: 'Italiano',\n englishName: 'Italian',\n currencyCode: 'EUR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 포르투갈어 (브라질)\n pt: {\n locale: 'pt',\n countryCode: 'BR',\n flag: '&#x1F1E7;&#x1F1F7;',\n dialCode: '+55',\n nativeName: 'Português',\n englishName: 'Portuguese',\n currencyCode: 'BRL',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 러시아어\n ru: {\n locale: 'ru',\n countryCode: 'RU',\n flag: '&#x1F1F7;&#x1F1FA;',\n dialCode: '+7',\n nativeName: 'Русский',\n englishName: 'Russian',\n currencyCode: 'RUB',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 아랍어\n ar: {\n locale: 'ar',\n countryCode: 'SA',\n flag: '&#x1F1F8;&#x1F1E6;',\n dialCode: '+966',\n nativeName: 'العربية',\n englishName: 'Arabic',\n rtl: true,\n currencyCode: 'SAR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 힌디어\n hi: {\n locale: 'hi',\n countryCode: 'IN',\n flag: '&#x1F1EE;&#x1F1F3;',\n dialCode: '+91',\n nativeName: 'हिन्दी',\n englishName: 'Hindi',\n currencyCode: 'INR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 태국어\n th: {\n locale: 'th',\n countryCode: 'TH',\n flag: '&#x1F1F9;&#x1F1ED;',\n dialCode: '+66',\n nativeName: 'ไทย',\n englishName: 'Thai',\n currencyCode: 'THB',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 베트남어\n vi: {\n locale: 'vi',\n countryCode: 'VN',\n flag: '&#x1F1FB;&#x1F1F3;',\n dialCode: '+84',\n nativeName: 'Tiếng Việt',\n englishName: 'Vietnamese',\n currencyCode: 'VND',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 인도네시아어\n id: {\n locale: 'id',\n countryCode: 'ID',\n flag: '&#x1F1EE;&#x1F1E9;',\n dialCode: '+62',\n nativeName: 'Bahasa Indonesia',\n englishName: 'Indonesian',\n currencyCode: 'IDR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 터키어\n tr: {\n locale: 'tr',\n countryCode: 'TR',\n flag: '&#x1F1F9;&#x1F1F7;',\n dialCode: '+90',\n nativeName: 'Türkçe',\n englishName: 'Turkish',\n currencyCode: 'TRY',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 폴란드어\n pl: {\n locale: 'pl',\n countryCode: 'PL',\n flag: '&#x1F1F5;&#x1F1F1;',\n dialCode: '+48',\n nativeName: 'Polski',\n englishName: 'Polish',\n currencyCode: 'PLN',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 네덜란드어\n nl: {\n locale: 'nl',\n countryCode: 'NL',\n flag: '&#x1F1F3;&#x1F1F1;',\n dialCode: '+31',\n nativeName: 'Nederlands',\n englishName: 'Dutch',\n currencyCode: 'EUR',\n dateFormat: 'DD-MM-YYYY',\n },\n\n // 중국어 (홍콩)\n 'zh-HK': {\n locale: 'zh-HK',\n countryCode: 'HK',\n flag: '&#x1F1ED;&#x1F1F0;',\n dialCode: '+852',\n nativeName: '繁體中文 (香港)',\n englishName: 'Chinese (Hong Kong)',\n currencyCode: 'HKD',\n dateFormat: 'YYYY/MM/DD',\n },\n\n // 말레이어\n ms: {\n locale: 'ms',\n countryCode: 'MY',\n flag: '&#x1F1F2;&#x1F1FE;',\n dialCode: '+60',\n nativeName: 'Bahasa Melayu',\n englishName: 'Malay',\n currencyCode: 'MYR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 영어 (영국)\n 'en-GB': {\n locale: 'en-GB',\n countryCode: 'GB',\n flag: '&#x1F1EC;&#x1F1E7;',\n dialCode: '+44',\n nativeName: 'English (UK)',\n englishName: 'English (United Kingdom)',\n currencyCode: 'GBP',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 영어 (캐나다)\n 'en-CA': {\n locale: 'en-CA',\n countryCode: 'CA',\n flag: '&#x1F1E8;&#x1F1E6;',\n dialCode: '+1',\n nativeName: 'English (Canada)',\n englishName: 'English (Canada)',\n currencyCode: 'CAD',\n dateFormat: 'YYYY-MM-DD',\n },\n\n // 영어 (호주)\n 'en-AU': {\n locale: 'en-AU',\n countryCode: 'AU',\n flag: '&#x1F1E6;&#x1F1FA;',\n dialCode: '+61',\n nativeName: 'English (Australia)',\n englishName: 'English (Australia)',\n currencyCode: 'AUD',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 영어 (뉴질랜드)\n 'en-NZ': {\n locale: 'en-NZ',\n countryCode: 'NZ',\n flag: '&#x1F1F3;&#x1F1FF;',\n dialCode: '+64',\n nativeName: 'English (New Zealand)',\n englishName: 'English (New Zealand)',\n currencyCode: 'NZD',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스페인어 (멕시코)\n 'es-MX': {\n locale: 'es-MX',\n countryCode: 'MX',\n flag: '&#x1F1F2;&#x1F1FD;',\n dialCode: '+52',\n nativeName: 'Español (México)',\n englishName: 'Spanish (Mexico)',\n currencyCode: 'MXN',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스페인어 (아르헨티나)\n 'es-AR': {\n locale: 'es-AR',\n countryCode: 'AR',\n flag: '&#x1F1E6;&#x1F1F7;',\n dialCode: '+54',\n nativeName: 'Español (Argentina)',\n englishName: 'Spanish (Argentina)',\n currencyCode: 'ARS',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스페인어 (콜롬비아)\n 'es-CO': {\n locale: 'es-CO',\n countryCode: 'CO',\n flag: '&#x1F1E8;&#x1F1F4;',\n dialCode: '+57',\n nativeName: 'Español (Colombia)',\n englishName: 'Spanish (Colombia)',\n currencyCode: 'COP',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스웨덴어\n sv: {\n locale: 'sv',\n countryCode: 'SE',\n flag: '&#x1F1F8;&#x1F1EA;',\n dialCode: '+46',\n nativeName: 'Svenska',\n englishName: 'Swedish',\n currencyCode: 'SEK',\n dateFormat: 'YYYY-MM-DD',\n },\n\n // 노르웨이어\n no: {\n locale: 'no',\n countryCode: 'NO',\n flag: '&#x1F1F3;&#x1F1F4;',\n dialCode: '+47',\n nativeName: 'Norsk',\n englishName: 'Norwegian',\n currencyCode: 'NOK',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 덴마크어\n da: {\n locale: 'da',\n countryCode: 'DK',\n flag: '&#x1F1E9;&#x1F1F0;',\n dialCode: '+45',\n nativeName: 'Dansk',\n englishName: 'Danish',\n currencyCode: 'DKK',\n dateFormat: 'DD-MM-YYYY',\n },\n\n // 핀란드어\n fi: {\n locale: 'fi',\n countryCode: 'FI',\n flag: '&#x1F1EB;&#x1F1EE;',\n dialCode: '+358',\n nativeName: 'Suomi',\n englishName: 'Finnish',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 우크라이나어\n uk: {\n locale: 'uk',\n countryCode: 'UA',\n flag: '&#x1F1FA;&#x1F1E6;',\n dialCode: '+380',\n nativeName: 'Українська',\n englishName: 'Ukrainian',\n currencyCode: 'UAH',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 체코어\n cs: {\n locale: 'cs',\n countryCode: 'CZ',\n flag: '&#x1F1E8;&#x1F1FF;',\n dialCode: '+420',\n nativeName: 'Čeština',\n englishName: 'Czech',\n currencyCode: 'CZK',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 헝가리어\n hu: {\n locale: 'hu',\n countryCode: 'HU',\n flag: '&#x1F1ED;&#x1F1FA;',\n dialCode: '+36',\n nativeName: 'Magyar',\n englishName: 'Hungarian',\n currencyCode: 'HUF',\n dateFormat: 'YYYY.MM.DD.',\n },\n\n // 루마니아어\n ro: {\n locale: 'ro',\n countryCode: 'RO',\n flag: '&#x1F1F7;&#x1F1F4;',\n dialCode: '+40',\n nativeName: 'Română',\n englishName: 'Romanian',\n currencyCode: 'RON',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 불가리아어\n bg: {\n locale: 'bg',\n countryCode: 'BG',\n flag: '&#x1F1E7;&#x1F1EC;',\n dialCode: '+359',\n nativeName: 'Български',\n englishName: 'Bulgarian',\n currencyCode: 'BGN',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 크로아티아어\n hr: {\n locale: 'hr',\n countryCode: 'HR',\n flag: '&#x1F1ED;&#x1F1F7;',\n dialCode: '+385',\n nativeName: 'Hrvatski',\n englishName: 'Croatian',\n currencyCode: 'HRK',\n dateFormat: 'DD.MM.YYYY.',\n },\n\n // 세르비아어\n sr: {\n locale: 'sr',\n countryCode: 'RS',\n flag: '&#x1F1F7;&#x1F1F8;',\n dialCode: '+381',\n nativeName: 'Српски',\n englishName: 'Serbian',\n currencyCode: 'RSD',\n dateFormat: 'DD.MM.YYYY.',\n },\n\n // 슬로바키아어\n sk: {\n locale: 'sk',\n countryCode: 'SK',\n flag: '&#x1F1F8;&#x1F1F0;',\n dialCode: '+421',\n nativeName: 'Slovenčina',\n englishName: 'Slovak',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 슬로베니아어\n sl: {\n locale: 'sl',\n countryCode: 'SI',\n flag: '&#x1F1F8;&#x1F1EE;',\n dialCode: '+386',\n nativeName: 'Slovenščina',\n englishName: 'Slovenian',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 리투아니아어\n lt: {\n locale: 'lt',\n countryCode: 'LT',\n flag: '&#x1F1F1;&#x1F1F9;',\n dialCode: '+370',\n nativeName: 'Lietuvių',\n englishName: 'Lithuanian',\n currencyCode: 'EUR',\n dateFormat: 'YYYY-MM-DD',\n },\n\n // 라트비아어\n lv: {\n locale: 'lv',\n countryCode: 'LV',\n flag: '&#x1F1F1;&#x1F1FB;',\n dialCode: '+371',\n nativeName: 'Latviešu',\n englishName: 'Latvian',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY.',\n },\n\n // 에스토니아어\n et: {\n locale: 'et',\n countryCode: 'EE',\n flag: '&#x1F1EA;&#x1F1EA;',\n dialCode: '+372',\n nativeName: 'Eesti',\n englishName: 'Estonian',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 그리스어\n el: {\n locale: 'el',\n countryCode: 'GR',\n flag: '&#x1F1EC;&#x1F1F7;',\n dialCode: '+30',\n nativeName: 'Ελληνικά',\n englishName: 'Greek',\n currencyCode: 'EUR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 페르시아어\n fa: {\n locale: 'fa',\n countryCode: 'IR',\n flag: '&#x1F1EE;&#x1F1F7;',\n dialCode: '+98',\n nativeName: 'فارسی',\n englishName: 'Persian',\n rtl: true,\n currencyCode: 'IRR',\n dateFormat: 'YYYY/MM/DD',\n },\n\n // 히브리어\n he: {\n locale: 'he',\n countryCode: 'IL',\n flag: '&#x1F1EE;&#x1F1F1;',\n dialCode: '+972',\n nativeName: 'עברית',\n englishName: 'Hebrew',\n rtl: true,\n currencyCode: 'ILS',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스와힐리어\n sw: {\n locale: 'sw',\n countryCode: 'KE',\n flag: '&#x1F1F0;&#x1F1EA;',\n dialCode: '+254',\n nativeName: 'Kiswahili',\n englishName: 'Swahili',\n currencyCode: 'KES',\n dateFormat: 'DD/MM/YYYY',\n },\n};\n\n/**\n * Locale 정보 가져오기\n *\n * @param locale - Locale 코드 (예: 'ko', 'en', 'ja')\n * @returns LocaleInfo 또는 undefined\n *\n * @example\n * ```typescript\n * const koInfo = getLocaleInfo('ko');\n * console.log(koInfo.flag); // 🇰🇷\n * console.log(koInfo.dialCode); // +82\n * ```\n */\nexport function getLocaleInfo(locale: string): LocaleInfo | undefined\n{\n return LOCALE_INFO_MAP[locale as SupportedLocale];\n}\n\n/**\n * 지원하는 모든 Locale 목록 가져오기\n *\n * @returns Locale 코드 배열\n */\nexport function getSupportedLocales(): SupportedLocale[]\n{\n return Object.keys(LOCALE_INFO_MAP) as SupportedLocale[];\n}\n\n/**\n * 국기 이모지만 가져오기\n *\n * @param locale - Locale 코드\n * @returns 국기 이모지 또는 빈 문자열\n *\n * @example\n * ```typescript\n * getFlag('ko'); // 🇰🇷\n * getFlag('en'); // 🇺🇸\n * ```\n */\nexport function getFlag(locale: string): string\n{\n return LOCALE_INFO_MAP[locale as SupportedLocale]?.flag ?? '';\n}\n\n/**\n * 전화번호 국가 코드 가져오기\n *\n * @param locale - Locale 코드\n * @returns 전화번호 코드 또는 빈 문자열\n *\n * @example\n * ```typescript\n * getDialCode('ko'); // +82\n * getDialCode('en'); // +1\n * ```\n */\nexport function getDialCode(locale: string): string\n{\n return LOCALE_INFO_MAP[locale as SupportedLocale]?.dialCode ?? '';\n}\n\n/**\n * RTL (Right-to-Left) 여부 확인\n *\n * @param locale - Locale 코드\n * @returns RTL 여부\n *\n * @example\n * ```typescript\n * isRTL('ar'); // true (Arabic)\n * isRTL('ko'); // false (Korean)\n * ```\n */\nexport function isRTL(locale: string): boolean\n{\n return LOCALE_INFO_MAP[locale as SupportedLocale]?.rtl ?? false;\n}"],"mappings":";AAUA,SAAS,SAAS,eAAe;;;AC0BjC,SAAS,UAAU,KAAa,cAChC;AACI,SAAO,QAAQ,IAAI,GAAG,KAAK;AAC/B;AAKA,SAAS,cAAc,KAAa,cACpC;AACI,QAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,UAAU,UAAU,UAAU;AACzC;AAKA,SAAS,oBACT;AACI,QAAM,gBAAgB,UAAU,2BAA2B,IAAI;AAC/D,QAAM,sBAAsB,UAAU,8BAA8B,OAAO;AAC3E,QAAMA,yBAAwB,cAAc,oCAAoC,IAAI;AAEpF,QAAM,mBAAmB,oBACpB,MAAM,GAAG,EACT,IAAI,YAAU,OAAO,KAAK,CAAC,EAC3B,OAAO,YAAU,OAAO,SAAS,CAAC;AAGvC,MAAI,CAAC,iBAAiB,SAAS,aAAa,GAC5C;AACI,qBAAiB,QAAQ,aAAa;AAAA,EAC1C;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA,uBAAAA;AAAA,EACJ;AACJ;AAKA,IAAI,gBAA2B,kBAAkB;AAgB1C,SAAS,eAChB;AACI,SAAO;AACX;;;AC3FO,IAAM,oBAAoB;;;AFgBjC,eAAe,wBACf;AACI,MACA;AACI,UAAM,cAAc,MAAM,QAAQ;AAClC,UAAM,iBAAiB,YAAY,IAAI,iBAAiB;AAExD,QAAI,CAAC,gBACL;AACI,aAAO;AAAA,IACX;AAGA,UAAM,YAAY,eACb,MAAM,GAAG,EACT,IAAI,UACL;AACI,YAAM,CAAC,IAAI,IAAI,KAAK,MAAM,GAAG;AAC7B,aAAO,KAAK,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,IACnC,CAAC;AAEL,UAAM,SAAS,aAAa;AAG5B,eAAW,QAAQ,WACnB;AACI,UAAI,OAAO,iBAAiB,SAAS,IAAI,GACzC;AACI,eAAO;AAAA,MACX;AAAA,IACJ;AAEA,WAAO;AAAA,EACX,SACO,OACP;AAEI,WAAO;AAAA,EACX;AACJ;AA4CA,eAAsB,YACtB;AACI,QAAM,SAAS,aAAa;AAG5B,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,eAAe,YAAY,IAAI,iBAAiB,GAAG;AAEzD,MAAI,gBAAgB,OAAO,iBAAiB,SAAS,YAAY,GACjE;AACI,WAAO;AAAA,EACX;AAGA,MAAI,OAAO,uBACX;AACI,UAAM,cAAc,MAAM,sBAAsB;AAChD,QAAI,aACJ;AACI,aAAO;AAAA,IACX;AAAA,EACJ;AAGA,SAAO,OAAO;AAClB;AA6CA,eAAsB,UAAU,QAChC;AACI,QAAM,SAAS,aAAa;AAG5B,MAAI,CAAC,OAAO,iBAAiB,SAAS,MAAM,GAC5C;AACI,UAAM,IAAI;AAAA,MACN,uBAAuB,MAAM,wBAAwB,OAAO,iBAAiB,KAAK,IAAI,CAAC;AAAA,IAC3F;AAAA,EACJ;AAEA,QAAM,cAAc,MAAM,QAAQ;AAElC,cAAY,IAAI,mBAAmB,QAAQ;AAAA,IACvC,MAAM;AAAA,IACN,QAAQ,KAAK,KAAK,KAAK;AAAA;AAAA,IACvB,UAAU;AAAA,EACd,CAAC;AACL;AA6CA,eAAsB,aACtB;AACI,QAAM,SAAS,aAAa;AAC5B,SAAO,OAAO;AAClB;","names":["detectBrowserLanguage"]}
1
+ {"version":3,"sources":["../src/server/helpers/locale.actions.ts","../src/server/config/cms.config.ts","../src/lib/constants/locale.constants.ts"],"sourcesContent":["\"use server\";\n\n/**\n * Locale Management Server Actions\n *\n * Server Actions으로 구현된 locale 관리 함수\n * - 서버 컴포넌트: 일반 함수 호출로 동작\n * - 클라이언트 컴포넌트: Server Action으로 자동 처리\n */\n\nimport { cookies, headers } from 'next/headers.js';\nimport { getCmsConfig } from '@/server/config/cms.config';\nimport {\n LOCALE_COOKIE_KEY,\n getLocaleInfo,\n type LocaleInfo,\n} from '@/lib/constants/locale.constants';\n\n/**\n * 브라우저 언어 감지\n *\n * Accept-Language 헤더에서 지원하는 언어를 찾습니다.\n *\n * @returns 감지된 언어 코드 또는 null\n */\nasync function detectBrowserLanguage(): Promise<string | null>\n{\n try\n {\n const headersList = await headers();\n const acceptLanguage = headersList.get('accept-language');\n\n if (!acceptLanguage)\n {\n return null;\n }\n\n // \"ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\" 형식 파싱\n const languages = acceptLanguage\n .split(',')\n .map(lang =>\n {\n const [code] = lang.split(';');\n return code.split('-')[0].trim();\n });\n\n const config = getCmsConfig();\n\n // 지원하는 언어 중 첫 번째 매칭되는 언어 반환\n for (const lang of languages)\n {\n if (config.supportedLocales.includes(lang))\n {\n return lang;\n }\n }\n\n return null;\n }\n catch (error)\n {\n // 헤더 접근 실패 시\n return null;\n }\n}\n\n/**\n * 현재 locale 가져오기 (Server Action)\n *\n * 서버/클라이언트 컴포넌트 모두에서 사용 가능\n *\n * 우선순위:\n * 1. 쿠키 (사용자가 명시적으로 선택한 언어)\n * 2. 브라우저 언어 감지 (설정에서 활성화된 경우)\n * 3. 시스템 기본 언어 (CMS 설정)\n *\n * @returns 현재 locale (예: 'ko', 'en')\n *\n * @example\n * ```tsx\n * // Server Component\n * import { getLocale } from '@spfn/cms/actions';\n *\n * export default async function Page()\n * {\n * const locale = await getLocale();\n * return <div>Current locale: {locale}</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Client Component\n * 'use client';\n * import { getLocale } from '@spfn/cms/client';\n *\n * export default function LanguageSwitcher()\n * {\n * const [locale, setLocale] = useState('');\n *\n * useEffect(() => {\n * getLocale().then(setLocale);\n * }, []);\n *\n * return <div>Current locale: {locale}</div>;\n * }\n * ```\n */\nexport async function getLocale(): Promise<string>\n{\n const config = getCmsConfig();\n\n // 1순위: 쿠키 (사용자가 명시적으로 선택한 언어)\n const cookieStore = await cookies();\n const cookieLocale = cookieStore.get(LOCALE_COOKIE_KEY)?.value;\n\n if (cookieLocale && config.supportedLocales.includes(cookieLocale))\n {\n return cookieLocale;\n }\n\n // 2순위: 브라우저 언어 감지 (설정에서 활성화된 경우)\n if (config.detectBrowserLanguage)\n {\n const browserLang = await detectBrowserLanguage();\n if (browserLang)\n {\n return browserLang;\n }\n }\n\n // 3순위: 시스템 기본 언어\n return config.defaultLocale;\n}\n\n/**\n * Locale 설정하기 (Server Action)\n *\n * 서버/클라이언트 컴포넌트 모두에서 사용 가능\n * 쿠키에 locale을 저장합니다.\n *\n * @param locale - 설정할 locale (예: 'ko', 'en')\n * @throws {Error} 지원하지 않는 locale인 경우\n *\n * @example\n * ```tsx\n * // Server Component (Server Action)\n * import { setLocale } from '@spfn/cms/actions';\n *\n * export default async function Page()\n * {\n * await setLocale('en');\n * return <div>Locale changed</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Client Component (Server Action)\n * 'use client';\n * import { setLocale } from '@spfn/cms/client';\n *\n * export default function LanguageSwitcher()\n * {\n * const handleChange = async (newLocale: string) =>\n * {\n * await setLocale(newLocale);\n * window.location.reload(); // 페이지 새로고침\n * };\n *\n * return (\n * <button onClick={() => handleChange('en')}>\n * Switch to English\n * </button>\n * );\n * }\n * ```\n */\nexport async function setLocale(locale: string): Promise<void>\n{\n const config = getCmsConfig();\n\n // 유효성 검사\n if (!config.supportedLocales.includes(locale))\n {\n throw new Error(\n `Unsupported locale: ${locale}. Supported locales: ${config.supportedLocales.join(', ')}`\n );\n }\n\n const cookieStore = await cookies();\n\n cookieStore.set(LOCALE_COOKIE_KEY, locale, {\n path: '/',\n maxAge: 60 * 60 * 24 * 365, // 1년\n sameSite: 'lax',\n });\n}\n\n/**\n * 지원하는 locale 목록 가져오기 (Server Action)\n *\n * 서버/클라이언트 컴포넌트 모두에서 사용 가능\n *\n * @returns 지원하는 locale 배열 (예: ['ko', 'en', 'ja'])\n *\n * @example\n * ```tsx\n * // Server Component\n * import { getLocales } from '@spfn/cms/actions';\n *\n * export default async function Page()\n * {\n * const locales = await getLocales();\n * return <div>Supported: {locales.join(', ')}</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Client Component\n * 'use client';\n * import { getLocales } from '@spfn/cms/client';\n *\n * export default function LanguageSwitcher()\n * {\n * const [locales, setLocales] = useState<string[]>([]);\n *\n * useEffect(() => {\n * getLocales().then(setLocales);\n * }, []);\n *\n * return (\n * <div>\n * {locales.map(locale => (\n * <button key={locale}>{locale}</button>\n * ))}\n * </div>\n * );\n * }\n * ```\n */\nexport async function getLocales(): Promise<string[]>\n{\n const config = getCmsConfig();\n return config.supportedLocales;\n}\n\n/**\n * 현재 locale과 상세 정보 함께 가져오기 (Server Action)\n *\n * locale 코드와 함께 국가 코드, 국기, 전화번호 코드 등의 상세 정보를 반환합니다.\n *\n * @returns Locale 코드와 LocaleInfo 객체\n *\n * @example\n * ```tsx\n * // Server Component\n * import { getLocaleWithInfo } from '@spfn/cms/actions';\n *\n * export default async function Page()\n * {\n * const { locale, info } = await getLocaleWithInfo();\n *\n * return (\n * <div>\n * <span>{info?.flag}</span>\n * <span>{info?.nativeName}</span>\n * <span>{info?.dialCode}</span>\n * </div>\n * );\n * }\n * ```\n */\nexport async function getLocaleWithInfo(): Promise<{\n locale: string;\n info: LocaleInfo | undefined;\n}>\n{\n const locale = await getLocale();\n const info = getLocaleInfo(locale);\n\n return { locale, info };\n}\n\n/**\n * 지원하는 모든 locale과 상세 정보 가져오기 (Server Action)\n *\n * 시스템이 지원하는 모든 locale의 상세 정보를 배열로 반환합니다.\n * 언어 선택 UI를 만들 때 유용합니다.\n *\n * @returns LocaleInfo 배열\n *\n * @example\n * ```tsx\n * // Server Component\n * import { getLocalesWithInfo } from '@spfn/cms/actions';\n *\n * export default async function LanguageSelector()\n * {\n * const locales = await getLocalesWithInfo();\n *\n * return (\n * <select>\n * {locales.map(info => (\n * <option key={info.locale} value={info.locale}>\n * {info.flag} {info.nativeName}\n * </option>\n * ))}\n * </select>\n * );\n * }\n * ```\n */\nexport async function getLocalesWithInfo(): Promise<LocaleInfo[]>\n{\n const config = getCmsConfig();\n const supportedLocales = config.supportedLocales;\n\n return supportedLocales\n .map(locale => getLocaleInfo(locale))\n .filter((info): info is LocaleInfo => info !== undefined);\n}","/**\n * CMS Configuration Module\n *\n * 환경변수 기반 CMS 설정 관리\n * - SPFN_CMS_DEFAULT_LOCALE: 기본 언어 (기본값: 'ko')\n * - SPFN_CMS_SUPPORTED_LOCALES: 지원 언어 목록, 쉼표로 구분 (기본값: 'ko,en')\n * - SPFN_CMS_DETECT_BROWSER_LANGUAGE: 브라우저 언어 자동 감지 (기본값: 'false')\n */\n\n/**\n * CMS 설정 타입\n */\nexport interface CmsConfig\n{\n /**\n * 기본 언어 코드\n * @example 'ko', 'en', 'ja'\n */\n defaultLocale: string;\n\n /**\n * 지원하는 언어 목록\n * @example ['ko', 'en', 'ja']\n */\n supportedLocales: string[];\n\n /**\n * 브라우저 언어 자동 감지 여부\n * @default true\n */\n detectBrowserLanguage: boolean;\n}\n\n/**\n * 환경변수 읽기 헬퍼\n */\nfunction getEnvVar(key: string, defaultValue: string): string\n{\n return process.env[key] || defaultValue;\n}\n\n/**\n * 환경변수에서 boolean 읽기\n */\nfunction getEnvBoolean(key: string, defaultValue: boolean): boolean\n{\n const value = process.env[key];\n if (value === undefined) return defaultValue;\n return value === 'true' || value === '1';\n}\n\n/**\n * 환경변수에서 설정 로드\n */\nfunction loadConfigFromEnv(): CmsConfig\n{\n const defaultLocale = getEnvVar('SPFN_CMS_DEFAULT_LOCALE', 'en');\n const supportedLocalesStr = getEnvVar('SPFN_CMS_SUPPORTED_LOCALES', 'en,ko');\n const detectBrowserLanguage = getEnvBoolean('SPFN_CMS_DETECT_BROWSER_LANGUAGE', true);\n\n const supportedLocales = supportedLocalesStr\n .split(',')\n .map(locale => locale.trim())\n .filter(locale => locale.length > 0);\n\n // 기본 언어가 지원 목록에 없으면 추가\n if (!supportedLocales.includes(defaultLocale))\n {\n supportedLocales.unshift(defaultLocale);\n }\n\n return {\n defaultLocale,\n supportedLocales,\n detectBrowserLanguage,\n };\n}\n\n/**\n * 현재 설정 (환경변수에서 초기화)\n */\nlet currentConfig: CmsConfig = loadConfigFromEnv();\n\n/**\n * CMS 설정 조회\n *\n * @returns 현재 CMS 설정\n *\n * @example\n * ```tsx\n * import { getCmsConfig } from '@spfn/cms';\n *\n * const config = getCmsConfig();\n * console.log(config.defaultLocale); // 'ko'\n * console.log(config.supportedLocales); // ['ko', 'en']\n * ```\n */\nexport function getCmsConfig(): Readonly<CmsConfig>\n{\n return currentConfig;\n}\n\n/**\n * CMS 설정 변경 (런타임 오버라이드)\n *\n * 환경변수 설정을 런타임에 오버라이드합니다.\n * 주로 테스트나 특수한 경우에 사용됩니다.\n *\n * @param config - 변경할 설정 (부분 업데이트 가능)\n *\n * @example\n * ```tsx\n * import { configureCms } from '@spfn/cms';\n *\n * // 앱 초기화 시 (선택적)\n * configureCms({\n * defaultLocale: 'en',\n * supportedLocales: ['en', 'ko', 'ja'],\n * detectBrowserLanguage: true,\n * });\n * ```\n */\nexport function configureCms(config: Partial<CmsConfig>): void\n{\n currentConfig = {\n ...currentConfig,\n ...config,\n };\n\n // 기본 언어가 지원 목록에 있는지 확인\n if (config.defaultLocale && !currentConfig.supportedLocales.includes(config.defaultLocale))\n {\n console.warn(\n `[CMS Config] Default locale '${config.defaultLocale}' not in supported locales, adding automatically.`,\n `Supported locales: [${currentConfig.supportedLocales.join(', ')}]`\n );\n\n currentConfig.supportedLocales.unshift(config.defaultLocale);\n }\n}\n\n/**\n * 설정 초기화 (환경변수에서 재로드)\n *\n * @example\n * ```tsx\n * import { resetCmsConfig } from '@spfn/cms';\n *\n * // 환경변수 설정으로 되돌리기\n * resetCmsConfig();\n * ```\n */\nexport function resetCmsConfig(): void\n{\n currentConfig = loadConfigFromEnv();\n}","/**\n * Locale Constants\n *\n * Server/Client 양쪽에서 사용 가능한 locale 관련 상수\n */\n\n/**\n * Locale 쿠키 키\n */\nexport const LOCALE_COOKIE_KEY = 'spfn-locale';\n\n/**\n * 지원하는 Locale 타입 (Type-safe)\n */\nexport type SupportedLocale =\n // 아시아-태평양\n | 'ko' // 한국어\n | 'ja' // 일본어\n | 'zh' // 중국어 (간체)\n | 'zh-TW' // 중국어 (번체, 대만)\n | 'zh-HK' // 중국어 (홍콩)\n | 'hi' // 힌디어\n | 'th' // 태국어\n | 'vi' // 베트남어\n | 'id' // 인도네시아어\n | 'ms' // 말레이어\n // 영어권\n | 'en' // 영어 (미국)\n | 'en-GB' // 영어 (영국)\n | 'en-CA' // 영어 (캐나다)\n | 'en-AU' // 영어 (호주)\n | 'en-NZ' // 영어 (뉴질랜드)\n // 서유럽\n | 'es' // 스페인어 (스페인)\n | 'es-MX' // 스페인어 (멕시코)\n | 'es-AR' // 스페인어 (아르헨티나)\n | 'es-CO' // 스페인어 (콜롬비아)\n | 'fr' // 프랑스어\n | 'de' // 독일어\n | 'it' // 이탈리아어\n | 'pt' // 포르투갈어\n | 'nl' // 네덜란드어\n // 북유럽\n | 'sv' // 스웨덴어\n | 'no' // 노르웨이어\n | 'da' // 덴마크어\n | 'fi' // 핀란드어\n // 동유럽\n | 'ru' // 러시아어\n | 'pl' // 폴란드어\n | 'uk' // 우크라이나어\n | 'cs' // 체코어\n | 'hu' // 헝가리어\n | 'ro' // 루마니아어\n | 'bg' // 불가리아어\n | 'hr' // 크로아티아어\n | 'sr' // 세르비아어\n | 'sk' // 슬로바키아어\n | 'sl' // 슬로베니아어\n | 'lt' // 리투아니아어\n | 'lv' // 라트비아어\n | 'et' // 에스토니아어\n // 남유럽\n | 'el' // 그리스어\n // 중동\n | 'tr' // 터키어\n | 'ar' // 아랍어\n | 'fa' // 페르시아어\n | 'he' // 히브리어\n // 아프리카\n | 'sw'; // 스와힐리어\n\n/**\n * 국가/지역 정보 타입\n */\nexport interface LocaleInfo\n{\n /** Locale 코드 (ISO 639-1) */\n locale: SupportedLocale;\n /** 국가 코드 (ISO 3166-1 alpha-2) */\n countryCode: string;\n /** 국기 이모지 (HTML/React용) */\n flag: string;\n /** 전화번호 국가 코드 */\n dialCode: string;\n /** 네이티브 이름 (현지어) */\n nativeName: string;\n /** 영어 이름 */\n englishName: string;\n /** RTL (Right-to-Left) 여부 */\n rtl?: boolean;\n /** 통화 코드 (ISO 4217) */\n currencyCode?: string;\n /** 날짜 형식 예시 */\n dateFormat?: string;\n}\n\n/**\n * 사전 정의된 Locale 정보 맵\n *\n * 주요 언어/국가 정보를 포함합니다.\n * 프로젝트에 맞게 추가/수정 가능합니다.\n */\nexport const LOCALE_INFO_MAP: Record<SupportedLocale, LocaleInfo> = {\n // 한국어\n ko: {\n locale: 'ko',\n countryCode: 'KR',\n flag: '&#x1F1F0;&#x1F1F7;',\n dialCode: '+82',\n nativeName: '한국어',\n englishName: 'Korean',\n currencyCode: 'KRW',\n dateFormat: 'YYYY.MM.DD',\n },\n\n // 영어 (미국)\n en: {\n locale: 'en',\n countryCode: 'US',\n flag: '&#x1F1FA;&#x1F1F8;',\n dialCode: '+1',\n nativeName: 'English',\n englishName: 'English',\n currencyCode: 'USD',\n dateFormat: 'MM/DD/YYYY',\n },\n\n // 일본어\n ja: {\n locale: 'ja',\n countryCode: 'JP',\n flag: '&#x1F1EF;&#x1F1F5;',\n dialCode: '+81',\n nativeName: '日本語',\n englishName: 'Japanese',\n currencyCode: 'JPY',\n dateFormat: 'YYYY/MM/DD',\n },\n\n // 중국어 (간체)\n zh: {\n locale: 'zh',\n countryCode: 'CN',\n flag: '&#x1F1E8;&#x1F1F3;',\n dialCode: '+86',\n nativeName: '简体中文',\n englishName: 'Chinese (Simplified)',\n currencyCode: 'CNY',\n dateFormat: 'YYYY-MM-DD',\n },\n\n // 중국어 (번체, 대만)\n 'zh-TW': {\n locale: 'zh-TW',\n countryCode: 'TW',\n flag: '&#x1F1F9;&#x1F1FC;',\n dialCode: '+886',\n nativeName: '繁體中文',\n englishName: 'Chinese (Traditional)',\n currencyCode: 'TWD',\n dateFormat: 'YYYY/MM/DD',\n },\n\n // 스페인어\n es: {\n locale: 'es',\n countryCode: 'ES',\n flag: '&#x1F1EA;&#x1F1F8;',\n dialCode: '+34',\n nativeName: 'Español',\n englishName: 'Spanish',\n currencyCode: 'EUR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 프랑스어\n fr: {\n locale: 'fr',\n countryCode: 'FR',\n flag: '&#x1F1EB;&#x1F1F7;',\n dialCode: '+33',\n nativeName: 'Français',\n englishName: 'French',\n currencyCode: 'EUR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 독일어\n de: {\n locale: 'de',\n countryCode: 'DE',\n flag: '&#x1F1E9;&#x1F1EA;',\n dialCode: '+49',\n nativeName: 'Deutsch',\n englishName: 'German',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 이탈리아어\n it: {\n locale: 'it',\n countryCode: 'IT',\n flag: '&#x1F1EE;&#x1F1F9;',\n dialCode: '+39',\n nativeName: 'Italiano',\n englishName: 'Italian',\n currencyCode: 'EUR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 포르투갈어 (브라질)\n pt: {\n locale: 'pt',\n countryCode: 'BR',\n flag: '&#x1F1E7;&#x1F1F7;',\n dialCode: '+55',\n nativeName: 'Português',\n englishName: 'Portuguese',\n currencyCode: 'BRL',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 러시아어\n ru: {\n locale: 'ru',\n countryCode: 'RU',\n flag: '&#x1F1F7;&#x1F1FA;',\n dialCode: '+7',\n nativeName: 'Русский',\n englishName: 'Russian',\n currencyCode: 'RUB',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 아랍어\n ar: {\n locale: 'ar',\n countryCode: 'SA',\n flag: '&#x1F1F8;&#x1F1E6;',\n dialCode: '+966',\n nativeName: 'العربية',\n englishName: 'Arabic',\n rtl: true,\n currencyCode: 'SAR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 힌디어\n hi: {\n locale: 'hi',\n countryCode: 'IN',\n flag: '&#x1F1EE;&#x1F1F3;',\n dialCode: '+91',\n nativeName: 'हिन्दी',\n englishName: 'Hindi',\n currencyCode: 'INR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 태국어\n th: {\n locale: 'th',\n countryCode: 'TH',\n flag: '&#x1F1F9;&#x1F1ED;',\n dialCode: '+66',\n nativeName: 'ไทย',\n englishName: 'Thai',\n currencyCode: 'THB',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 베트남어\n vi: {\n locale: 'vi',\n countryCode: 'VN',\n flag: '&#x1F1FB;&#x1F1F3;',\n dialCode: '+84',\n nativeName: 'Tiếng Việt',\n englishName: 'Vietnamese',\n currencyCode: 'VND',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 인도네시아어\n id: {\n locale: 'id',\n countryCode: 'ID',\n flag: '&#x1F1EE;&#x1F1E9;',\n dialCode: '+62',\n nativeName: 'Bahasa Indonesia',\n englishName: 'Indonesian',\n currencyCode: 'IDR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 터키어\n tr: {\n locale: 'tr',\n countryCode: 'TR',\n flag: '&#x1F1F9;&#x1F1F7;',\n dialCode: '+90',\n nativeName: 'Türkçe',\n englishName: 'Turkish',\n currencyCode: 'TRY',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 폴란드어\n pl: {\n locale: 'pl',\n countryCode: 'PL',\n flag: '&#x1F1F5;&#x1F1F1;',\n dialCode: '+48',\n nativeName: 'Polski',\n englishName: 'Polish',\n currencyCode: 'PLN',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 네덜란드어\n nl: {\n locale: 'nl',\n countryCode: 'NL',\n flag: '&#x1F1F3;&#x1F1F1;',\n dialCode: '+31',\n nativeName: 'Nederlands',\n englishName: 'Dutch',\n currencyCode: 'EUR',\n dateFormat: 'DD-MM-YYYY',\n },\n\n // 중국어 (홍콩)\n 'zh-HK': {\n locale: 'zh-HK',\n countryCode: 'HK',\n flag: '&#x1F1ED;&#x1F1F0;',\n dialCode: '+852',\n nativeName: '繁體中文 (香港)',\n englishName: 'Chinese (Hong Kong)',\n currencyCode: 'HKD',\n dateFormat: 'YYYY/MM/DD',\n },\n\n // 말레이어\n ms: {\n locale: 'ms',\n countryCode: 'MY',\n flag: '&#x1F1F2;&#x1F1FE;',\n dialCode: '+60',\n nativeName: 'Bahasa Melayu',\n englishName: 'Malay',\n currencyCode: 'MYR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 영어 (영국)\n 'en-GB': {\n locale: 'en-GB',\n countryCode: 'GB',\n flag: '&#x1F1EC;&#x1F1E7;',\n dialCode: '+44',\n nativeName: 'English (UK)',\n englishName: 'English (United Kingdom)',\n currencyCode: 'GBP',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 영어 (캐나다)\n 'en-CA': {\n locale: 'en-CA',\n countryCode: 'CA',\n flag: '&#x1F1E8;&#x1F1E6;',\n dialCode: '+1',\n nativeName: 'English (Canada)',\n englishName: 'English (Canada)',\n currencyCode: 'CAD',\n dateFormat: 'YYYY-MM-DD',\n },\n\n // 영어 (호주)\n 'en-AU': {\n locale: 'en-AU',\n countryCode: 'AU',\n flag: '&#x1F1E6;&#x1F1FA;',\n dialCode: '+61',\n nativeName: 'English (Australia)',\n englishName: 'English (Australia)',\n currencyCode: 'AUD',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 영어 (뉴질랜드)\n 'en-NZ': {\n locale: 'en-NZ',\n countryCode: 'NZ',\n flag: '&#x1F1F3;&#x1F1FF;',\n dialCode: '+64',\n nativeName: 'English (New Zealand)',\n englishName: 'English (New Zealand)',\n currencyCode: 'NZD',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스페인어 (멕시코)\n 'es-MX': {\n locale: 'es-MX',\n countryCode: 'MX',\n flag: '&#x1F1F2;&#x1F1FD;',\n dialCode: '+52',\n nativeName: 'Español (México)',\n englishName: 'Spanish (Mexico)',\n currencyCode: 'MXN',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스페인어 (아르헨티나)\n 'es-AR': {\n locale: 'es-AR',\n countryCode: 'AR',\n flag: '&#x1F1E6;&#x1F1F7;',\n dialCode: '+54',\n nativeName: 'Español (Argentina)',\n englishName: 'Spanish (Argentina)',\n currencyCode: 'ARS',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스페인어 (콜롬비아)\n 'es-CO': {\n locale: 'es-CO',\n countryCode: 'CO',\n flag: '&#x1F1E8;&#x1F1F4;',\n dialCode: '+57',\n nativeName: 'Español (Colombia)',\n englishName: 'Spanish (Colombia)',\n currencyCode: 'COP',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스웨덴어\n sv: {\n locale: 'sv',\n countryCode: 'SE',\n flag: '&#x1F1F8;&#x1F1EA;',\n dialCode: '+46',\n nativeName: 'Svenska',\n englishName: 'Swedish',\n currencyCode: 'SEK',\n dateFormat: 'YYYY-MM-DD',\n },\n\n // 노르웨이어\n no: {\n locale: 'no',\n countryCode: 'NO',\n flag: '&#x1F1F3;&#x1F1F4;',\n dialCode: '+47',\n nativeName: 'Norsk',\n englishName: 'Norwegian',\n currencyCode: 'NOK',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 덴마크어\n da: {\n locale: 'da',\n countryCode: 'DK',\n flag: '&#x1F1E9;&#x1F1F0;',\n dialCode: '+45',\n nativeName: 'Dansk',\n englishName: 'Danish',\n currencyCode: 'DKK',\n dateFormat: 'DD-MM-YYYY',\n },\n\n // 핀란드어\n fi: {\n locale: 'fi',\n countryCode: 'FI',\n flag: '&#x1F1EB;&#x1F1EE;',\n dialCode: '+358',\n nativeName: 'Suomi',\n englishName: 'Finnish',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 우크라이나어\n uk: {\n locale: 'uk',\n countryCode: 'UA',\n flag: '&#x1F1FA;&#x1F1E6;',\n dialCode: '+380',\n nativeName: 'Українська',\n englishName: 'Ukrainian',\n currencyCode: 'UAH',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 체코어\n cs: {\n locale: 'cs',\n countryCode: 'CZ',\n flag: '&#x1F1E8;&#x1F1FF;',\n dialCode: '+420',\n nativeName: 'Čeština',\n englishName: 'Czech',\n currencyCode: 'CZK',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 헝가리어\n hu: {\n locale: 'hu',\n countryCode: 'HU',\n flag: '&#x1F1ED;&#x1F1FA;',\n dialCode: '+36',\n nativeName: 'Magyar',\n englishName: 'Hungarian',\n currencyCode: 'HUF',\n dateFormat: 'YYYY.MM.DD.',\n },\n\n // 루마니아어\n ro: {\n locale: 'ro',\n countryCode: 'RO',\n flag: '&#x1F1F7;&#x1F1F4;',\n dialCode: '+40',\n nativeName: 'Română',\n englishName: 'Romanian',\n currencyCode: 'RON',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 불가리아어\n bg: {\n locale: 'bg',\n countryCode: 'BG',\n flag: '&#x1F1E7;&#x1F1EC;',\n dialCode: '+359',\n nativeName: 'Български',\n englishName: 'Bulgarian',\n currencyCode: 'BGN',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 크로아티아어\n hr: {\n locale: 'hr',\n countryCode: 'HR',\n flag: '&#x1F1ED;&#x1F1F7;',\n dialCode: '+385',\n nativeName: 'Hrvatski',\n englishName: 'Croatian',\n currencyCode: 'HRK',\n dateFormat: 'DD.MM.YYYY.',\n },\n\n // 세르비아어\n sr: {\n locale: 'sr',\n countryCode: 'RS',\n flag: '&#x1F1F7;&#x1F1F8;',\n dialCode: '+381',\n nativeName: 'Српски',\n englishName: 'Serbian',\n currencyCode: 'RSD',\n dateFormat: 'DD.MM.YYYY.',\n },\n\n // 슬로바키아어\n sk: {\n locale: 'sk',\n countryCode: 'SK',\n flag: '&#x1F1F8;&#x1F1F0;',\n dialCode: '+421',\n nativeName: 'Slovenčina',\n englishName: 'Slovak',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 슬로베니아어\n sl: {\n locale: 'sl',\n countryCode: 'SI',\n flag: '&#x1F1F8;&#x1F1EE;',\n dialCode: '+386',\n nativeName: 'Slovenščina',\n englishName: 'Slovenian',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 리투아니아어\n lt: {\n locale: 'lt',\n countryCode: 'LT',\n flag: '&#x1F1F1;&#x1F1F9;',\n dialCode: '+370',\n nativeName: 'Lietuvių',\n englishName: 'Lithuanian',\n currencyCode: 'EUR',\n dateFormat: 'YYYY-MM-DD',\n },\n\n // 라트비아어\n lv: {\n locale: 'lv',\n countryCode: 'LV',\n flag: '&#x1F1F1;&#x1F1FB;',\n dialCode: '+371',\n nativeName: 'Latviešu',\n englishName: 'Latvian',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY.',\n },\n\n // 에스토니아어\n et: {\n locale: 'et',\n countryCode: 'EE',\n flag: '&#x1F1EA;&#x1F1EA;',\n dialCode: '+372',\n nativeName: 'Eesti',\n englishName: 'Estonian',\n currencyCode: 'EUR',\n dateFormat: 'DD.MM.YYYY',\n },\n\n // 그리스어\n el: {\n locale: 'el',\n countryCode: 'GR',\n flag: '&#x1F1EC;&#x1F1F7;',\n dialCode: '+30',\n nativeName: 'Ελληνικά',\n englishName: 'Greek',\n currencyCode: 'EUR',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 페르시아어\n fa: {\n locale: 'fa',\n countryCode: 'IR',\n flag: '&#x1F1EE;&#x1F1F7;',\n dialCode: '+98',\n nativeName: 'فارسی',\n englishName: 'Persian',\n rtl: true,\n currencyCode: 'IRR',\n dateFormat: 'YYYY/MM/DD',\n },\n\n // 히브리어\n he: {\n locale: 'he',\n countryCode: 'IL',\n flag: '&#x1F1EE;&#x1F1F1;',\n dialCode: '+972',\n nativeName: 'עברית',\n englishName: 'Hebrew',\n rtl: true,\n currencyCode: 'ILS',\n dateFormat: 'DD/MM/YYYY',\n },\n\n // 스와힐리어\n sw: {\n locale: 'sw',\n countryCode: 'KE',\n flag: '&#x1F1F0;&#x1F1EA;',\n dialCode: '+254',\n nativeName: 'Kiswahili',\n englishName: 'Swahili',\n currencyCode: 'KES',\n dateFormat: 'DD/MM/YYYY',\n },\n};\n\n/**\n * Locale 정보 가져오기\n *\n * @param locale - Locale 코드 (예: 'ko', 'en', 'ja')\n * @returns LocaleInfo 또는 undefined\n *\n * @example\n * ```typescript\n * const koInfo = getLocaleInfo('ko');\n * console.log(koInfo.flag); // 🇰🇷\n * console.log(koInfo.dialCode); // +82\n * ```\n */\nexport function getLocaleInfo(locale: string): LocaleInfo | undefined\n{\n return LOCALE_INFO_MAP[locale as SupportedLocale];\n}\n\n/**\n * 지원하는 모든 Locale 목록 가져오기\n *\n * @returns Locale 코드 배열\n */\nexport function getSupportedLocales(): SupportedLocale[]\n{\n return Object.keys(LOCALE_INFO_MAP) as SupportedLocale[];\n}\n\n/**\n * 국기 이모지만 가져오기\n *\n * @param locale - Locale 코드\n * @returns 국기 이모지 또는 빈 문자열\n *\n * @example\n * ```typescript\n * getFlag('ko'); // 🇰🇷\n * getFlag('en'); // 🇺🇸\n * ```\n */\nexport function getFlag(locale: string): string\n{\n return LOCALE_INFO_MAP[locale as SupportedLocale]?.flag ?? '';\n}\n\n/**\n * 전화번호 국가 코드 가져오기\n *\n * @param locale - Locale 코드\n * @returns 전화번호 코드 또는 빈 문자열\n *\n * @example\n * ```typescript\n * getDialCode('ko'); // +82\n * getDialCode('en'); // +1\n * ```\n */\nexport function getDialCode(locale: string): string\n{\n return LOCALE_INFO_MAP[locale as SupportedLocale]?.dialCode ?? '';\n}\n\n/**\n * RTL (Right-to-Left) 여부 확인\n *\n * @param locale - Locale 코드\n * @returns RTL 여부\n *\n * @example\n * ```typescript\n * isRTL('ar'); // true (Arabic)\n * isRTL('ko'); // false (Korean)\n * ```\n */\nexport function isRTL(locale: string): boolean\n{\n return LOCALE_INFO_MAP[locale as SupportedLocale]?.rtl ?? false;\n}"],"mappings":";AAUA,SAAS,SAAS,eAAe;;;AC0BjC,SAAS,UAAU,KAAa,cAChC;AACI,SAAO,QAAQ,IAAI,GAAG,KAAK;AAC/B;AAKA,SAAS,cAAc,KAAa,cACpC;AACI,QAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,UAAU,UAAU,UAAU;AACzC;AAKA,SAAS,oBACT;AACI,QAAM,gBAAgB,UAAU,2BAA2B,IAAI;AAC/D,QAAM,sBAAsB,UAAU,8BAA8B,OAAO;AAC3E,QAAMA,yBAAwB,cAAc,oCAAoC,IAAI;AAEpF,QAAM,mBAAmB,oBACpB,MAAM,GAAG,EACT,IAAI,YAAU,OAAO,KAAK,CAAC,EAC3B,OAAO,YAAU,OAAO,SAAS,CAAC;AAGvC,MAAI,CAAC,iBAAiB,SAAS,aAAa,GAC5C;AACI,qBAAiB,QAAQ,aAAa;AAAA,EAC1C;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA,uBAAAA;AAAA,EACJ;AACJ;AAKA,IAAI,gBAA2B,kBAAkB;AAgB1C,SAAS,eAChB;AACI,SAAO;AACX;;;AC3FO,IAAM,oBAAoB;;;AFgBjC,eAAe,wBACf;AACI,MACA;AACI,UAAM,cAAc,MAAM,QAAQ;AAClC,UAAM,iBAAiB,YAAY,IAAI,iBAAiB;AAExD,QAAI,CAAC,gBACL;AACI,aAAO;AAAA,IACX;AAGA,UAAM,YAAY,eACb,MAAM,GAAG,EACT,IAAI,UACL;AACI,YAAM,CAAC,IAAI,IAAI,KAAK,MAAM,GAAG;AAC7B,aAAO,KAAK,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,IACnC,CAAC;AAEL,UAAM,SAAS,aAAa;AAG5B,eAAW,QAAQ,WACnB;AACI,UAAI,OAAO,iBAAiB,SAAS,IAAI,GACzC;AACI,eAAO;AAAA,MACX;AAAA,IACJ;AAEA,WAAO;AAAA,EACX,SACO,OACP;AAEI,WAAO;AAAA,EACX;AACJ;AA4CA,eAAsB,YACtB;AACI,QAAM,SAAS,aAAa;AAG5B,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,eAAe,YAAY,IAAI,iBAAiB,GAAG;AAEzD,MAAI,gBAAgB,OAAO,iBAAiB,SAAS,YAAY,GACjE;AACI,WAAO;AAAA,EACX;AAGA,MAAI,OAAO,uBACX;AACI,UAAM,cAAc,MAAM,sBAAsB;AAChD,QAAI,aACJ;AACI,aAAO;AAAA,IACX;AAAA,EACJ;AAGA,SAAO,OAAO;AAClB;AA6CA,eAAsB,UAAU,QAChC;AACI,QAAM,SAAS,aAAa;AAG5B,MAAI,CAAC,OAAO,iBAAiB,SAAS,MAAM,GAC5C;AACI,UAAM,IAAI;AAAA,MACN,uBAAuB,MAAM,wBAAwB,OAAO,iBAAiB,KAAK,IAAI,CAAC;AAAA,IAC3F;AAAA,EACJ;AAEA,QAAM,cAAc,MAAM,QAAQ;AAElC,cAAY,IAAI,mBAAmB,QAAQ;AAAA,IACvC,MAAM;AAAA,IACN,QAAQ,KAAK,KAAK,KAAK;AAAA;AAAA,IACvB,UAAU;AAAA,EACd,CAAC;AACL;AA6CA,eAAsB,aACtB;AACI,QAAM,SAAS,aAAa;AAC5B,SAAO,OAAO;AAClB;","names":["detectBrowserLanguage"]}
package/dist/api.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { InferContract } from '@spfn/core';
2
2
  import { saveValuesContract, getValuesContract } from './lib/contracts/values.js';
3
3
  import { getPublishedCacheContract, upsertPublishedCacheContract } from './lib/contracts/published-cache.js';
4
- import { getLabelByKeyContract, getLabelsContract, createLabelContract, getLabelContract, updateLabelContract, deleteLabelContract } from './lib/contracts/labels.js';
4
+ import { getAdminLabelContract, publishLabelContract, getLabelByKeyContract, getLabelsContract, createLabelContract, getLabelContract, updateLabelContract, deleteLabelContract } from './lib/contracts/labels.js';
5
5
  export { client } from '@spfn/core/client';
6
6
  import '@sinclair/typebox';
7
7
 
@@ -11,7 +11,7 @@ import '@sinclair/typebox';
11
11
  * Generated by @spfn/core codegen
12
12
  * DO NOT EDIT MANUALLY
13
13
  *
14
- * @generated 2025-11-02T14:38:21.625Z
14
+ * @generated 2025-11-03T05:36:08.366Z
15
15
  */
16
16
 
17
17
  type SaveValuesResponse = InferContract<typeof saveValuesContract>['response'];
@@ -27,7 +27,7 @@ type GetValuesParams = InferContract<typeof getValuesContract>['params'];
27
27
  * Generated by @spfn/core codegen
28
28
  * DO NOT EDIT MANUALLY
29
29
  *
30
- * @generated 2025-11-02T14:38:21.624Z
30
+ * @generated 2025-11-03T05:36:08.365Z
31
31
  */
32
32
 
33
33
  type GetPublishedCacheResponse = InferContract<typeof getPublishedCacheContract>['response'];
@@ -41,7 +41,32 @@ type UpsertPublishedCacheBody = InferContract<typeof upsertPublishedCacheContrac
41
41
  * Generated by @spfn/core codegen
42
42
  * DO NOT EDIT MANUALLY
43
43
  *
44
- * @generated 2025-11-02T14:38:21.624Z
44
+ * @generated 2025-11-03T05:36:08.365Z
45
+ */
46
+
47
+ type GetAdminLabelResponse = InferContract<typeof getAdminLabelContract>['response'];
48
+ type GetAdminLabelParams = InferContract<typeof getAdminLabelContract>['params'];
49
+
50
+ /**
51
+ * Auto-generated API Client
52
+ *
53
+ * Generated by @spfn/core codegen
54
+ * DO NOT EDIT MANUALLY
55
+ *
56
+ * @generated 2025-11-03T05:36:08.364Z
57
+ */
58
+
59
+ type PublishLabelResponse = InferContract<typeof publishLabelContract>['response'];
60
+ type PublishLabelParams = InferContract<typeof publishLabelContract>['params'];
61
+ type PublishLabelBody = InferContract<typeof publishLabelContract>['body'];
62
+
63
+ /**
64
+ * Auto-generated API Client
65
+ *
66
+ * Generated by @spfn/core codegen
67
+ * DO NOT EDIT MANUALLY
68
+ *
69
+ * @generated 2025-11-03T05:36:08.364Z
45
70
  */
46
71
 
47
72
  type GetLabelByKeyResponse = InferContract<typeof getLabelByKeyContract>['response'];
@@ -53,7 +78,7 @@ type GetLabelByKeyParams = InferContract<typeof getLabelByKeyContract>['params']
53
78
  * Generated by @spfn/core codegen
54
79
  * DO NOT EDIT MANUALLY
55
80
  *
56
- * @generated 2025-11-02T14:38:21.623Z
81
+ * @generated 2025-11-03T05:36:08.363Z
57
82
  */
58
83
 
59
84
  type GetLabelsResponse = InferContract<typeof getLabelsContract>['response'];
@@ -157,6 +182,52 @@ declare const cmsApi: {
157
182
  key?: string | undefined;
158
183
  error: string;
159
184
  }>;
185
+ readonly publishLabel: (options: {
186
+ params: PublishLabelParams;
187
+ body: PublishLabelBody;
188
+ }) => Promise<{
189
+ version: number;
190
+ labelId: number;
191
+ success: boolean;
192
+ message: string;
193
+ } | {
194
+ error: string;
195
+ }>;
196
+ readonly getAdminLabel: (options: {
197
+ params: GetAdminLabelParams;
198
+ }) => Promise<{
199
+ status: "published" | "default-only" | "unpublished" | "modified";
200
+ label: {
201
+ section: string;
202
+ id: number;
203
+ key: string;
204
+ type: string;
205
+ publishedVersion: number | null;
206
+ createdBy: string | null;
207
+ createdAt: string;
208
+ updatedAt: string;
209
+ };
210
+ draft: {
211
+ locale: string;
212
+ version: null;
213
+ id: number;
214
+ value: any;
215
+ createdAt: string;
216
+ labelId: number;
217
+ breakpoint: string | null;
218
+ }[];
219
+ published: {
220
+ locale: string;
221
+ version: number;
222
+ id: number;
223
+ value: any;
224
+ createdAt: string;
225
+ labelId: number;
226
+ breakpoint: string | null;
227
+ }[];
228
+ } | {
229
+ error: string;
230
+ }>;
160
231
  readonly getPublishedCache: (options: {
161
232
  query?: GetPublishedCacheQuery;
162
233
  }) => Promise<{
@@ -211,4 +282,4 @@ declare const cmsApi: {
211
282
  }>;
212
283
  };
213
284
 
214
- export { type CreateLabelBody, type CreateLabelResponse, type DeleteLabelParams, type DeleteLabelResponse, type GetLabelByKeyParams, type GetLabelByKeyResponse, type GetLabelParams, type GetLabelResponse, type GetLabelsQuery, type GetLabelsResponse, type GetPublishedCacheQuery, type GetPublishedCacheResponse, type GetValuesParams, type GetValuesQuery, type GetValuesResponse, type SaveValuesBody, type SaveValuesParams, type SaveValuesResponse, type UpdateLabelBody, type UpdateLabelParams, type UpdateLabelResponse, type UpsertPublishedCacheBody, type UpsertPublishedCacheResponse, cmsApi };
285
+ export { type CreateLabelBody, type CreateLabelResponse, type DeleteLabelParams, type DeleteLabelResponse, type GetAdminLabelParams, type GetAdminLabelResponse, type GetLabelByKeyParams, type GetLabelByKeyResponse, type GetLabelParams, type GetLabelResponse, type GetLabelsQuery, type GetLabelsResponse, type GetPublishedCacheQuery, type GetPublishedCacheResponse, type GetValuesParams, type GetValuesQuery, type GetValuesResponse, type PublishLabelBody, type PublishLabelParams, type PublishLabelResponse, type SaveValuesBody, type SaveValuesParams, type SaveValuesResponse, type UpdateLabelBody, type UpdateLabelParams, type UpdateLabelResponse, type UpsertPublishedCacheBody, type UpsertPublishedCacheResponse, cmsApi };
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/api/index.ts
2
- import { client as client5 } from "@spfn/core/client";
2
+ import { client as client7 } from "@spfn/core/client";
3
3
 
4
4
  // src/api/cms-labels.ts
5
5
  import { client } from "@spfn/core/client";
@@ -163,6 +163,76 @@ var getLabelByKeyContract = {
163
163
  })
164
164
  ])
165
165
  };
166
+ var publishLabelContract = {
167
+ method: "POST",
168
+ path: "/_cms/labels/:labelId/publish",
169
+ params: Type.Object({
170
+ labelId: Type.String({ description: "\uB77C\uBCA8 ID" })
171
+ }),
172
+ body: Type.Object({
173
+ notes: Type.Optional(Type.String({ description: "\uBC1C\uD589 \uB178\uD2B8 (\uBC84\uC804 \uC124\uBA85)" })),
174
+ publishedBy: Type.Optional(Type.String({ description: "\uBC1C\uD589\uC790 ID" }))
175
+ }),
176
+ response: Type.Union([
177
+ Type.Object({
178
+ success: Type.Boolean(),
179
+ labelId: Type.Number(),
180
+ version: Type.Number(),
181
+ message: Type.String()
182
+ }),
183
+ Type.Object({
184
+ error: Type.String()
185
+ })
186
+ ])
187
+ };
188
+ var getAdminLabelContract = {
189
+ method: "GET",
190
+ path: "/_cms/labels/:labelId/admin",
191
+ params: Type.Object({
192
+ labelId: Type.String({ description: "\uB77C\uBCA8 ID" })
193
+ }),
194
+ response: Type.Union([
195
+ Type.Object({
196
+ label: Type.Object({
197
+ id: Type.Number(),
198
+ key: Type.String(),
199
+ section: Type.String(),
200
+ type: Type.String(),
201
+ publishedVersion: Type.Union([Type.Number(), Type.Null()]),
202
+ createdBy: Type.Union([Type.String(), Type.Null()]),
203
+ createdAt: Type.String(),
204
+ updatedAt: Type.String()
205
+ }),
206
+ draft: Type.Array(Type.Object({
207
+ id: Type.Number(),
208
+ labelId: Type.Number(),
209
+ version: Type.Null(),
210
+ locale: Type.String(),
211
+ breakpoint: Type.Union([Type.String(), Type.Null()]),
212
+ value: Type.Any(),
213
+ createdAt: Type.String()
214
+ })),
215
+ published: Type.Array(Type.Object({
216
+ id: Type.Number(),
217
+ labelId: Type.Number(),
218
+ version: Type.Number(),
219
+ locale: Type.String(),
220
+ breakpoint: Type.Union([Type.String(), Type.Null()]),
221
+ value: Type.Any(),
222
+ createdAt: Type.String()
223
+ })),
224
+ status: Type.Union([
225
+ Type.Literal("default-only"),
226
+ Type.Literal("unpublished"),
227
+ Type.Literal("published"),
228
+ Type.Literal("modified")
229
+ ])
230
+ }),
231
+ Type.Object({
232
+ error: Type.String()
233
+ })
234
+ ])
235
+ };
166
236
 
167
237
  // src/api/cms-labels.ts
168
238
  var getLabels = (options) => client.call(getLabelsContract, options);
@@ -175,8 +245,16 @@ var deleteLabel = (options) => client.call(deleteLabelContract, options);
175
245
  import { client as client2 } from "@spfn/core/client";
176
246
  var getLabelByKey = (options) => client2.call(getLabelByKeyContract, options);
177
247
 
178
- // src/api/cms-published-cache.ts
248
+ // src/api/cms-labels-publish.ts
179
249
  import { client as client3 } from "@spfn/core/client";
250
+ var publishLabel = (options) => client3.call(publishLabelContract, options);
251
+
252
+ // src/api/cms-labels-admin.ts
253
+ import { client as client4 } from "@spfn/core/client";
254
+ var getAdminLabel = (options) => client4.call(getAdminLabelContract, options);
255
+
256
+ // src/api/cms-published-cache.ts
257
+ import { client as client5 } from "@spfn/core/client";
180
258
 
181
259
  // src/lib/contracts/published-cache.ts
182
260
  import { Type as Type2 } from "@sinclair/typebox";
@@ -224,11 +302,11 @@ var upsertPublishedCacheContract = {
224
302
  };
225
303
 
226
304
  // src/api/cms-published-cache.ts
227
- var getPublishedCache = (options) => client3.call(getPublishedCacheContract, options);
228
- var upsertPublishedCache = (options) => client3.call(upsertPublishedCacheContract, options);
305
+ var getPublishedCache = (options) => client5.call(getPublishedCacheContract, options);
306
+ var upsertPublishedCache = (options) => client5.call(upsertPublishedCacheContract, options);
229
307
 
230
308
  // src/api/cms-values.ts
231
- import { client as client4 } from "@spfn/core/client";
309
+ import { client as client6 } from "@spfn/core/client";
232
310
 
233
311
  // src/lib/contracts/values.ts
234
312
  import { Type as Type3 } from "@sinclair/typebox";
@@ -330,8 +408,8 @@ var getValuesContract = {
330
408
  };
331
409
 
332
410
  // src/api/cms-values.ts
333
- var saveValues = (options) => client4.call(saveValuesContract, options);
334
- var getValues = (options) => client4.call(getValuesContract, options);
411
+ var saveValues = (options) => client6.call(saveValuesContract, options);
412
+ var getValues = (options) => client6.call(getValuesContract, options);
335
413
 
336
414
  // src/api/index.ts
337
415
  var cmsApi = {
@@ -341,13 +419,15 @@ var cmsApi = {
341
419
  updateLabel,
342
420
  deleteLabel,
343
421
  getLabelByKey,
422
+ publishLabel,
423
+ getAdminLabel,
344
424
  getPublishedCache,
345
425
  upsertPublishedCache,
346
426
  saveValues,
347
427
  getValues
348
428
  };
349
429
  export {
350
- client5 as client,
430
+ client7 as client,
351
431
  cmsApi
352
432
  };
353
433
  //# sourceMappingURL=api.js.map