Prices, quotas and site URLs were copied by hand into the edge functions, admin, desktop and the landing site, and the copies disagreed (Payple billed 9,900/29,900 KRW, admin labels said 12,900/24,900 KRW and $9.9/$19.9, the site said 2,900/8,900 KRW). - packages/core/src/plan-catalog.ts is the single source for PLAN_PRICE_KRW (Free 0 / Pro 2,900 / Pro+ 8,900 a month) and PLAN_QUOTA. - packages/core/src/web-urls.ts is the single source for the public origin, the /app web-app base path, SITE_URLS and billingUrl(). - Deno cannot bundle packages/core, so scripts/ci/sync-core-contract.mjs generates _shared/core-contract.generated.ts; `npm run contract:check` fails on drift (same pattern as version:sync). - Payple checkout, renewal and webhook amount checks now bill the catalog price, so existing subscribers move to the new price at their next renewal. quota.ts, team-contract.ts and the tests read the generated values. - Admin MRR/ARR is computed in KRW from the catalog; license labels, the release link and desktop PREMIUM_LLM limits derive from core; the site imports prices and quotas directly. Policy: docs/REFACTOR_POLICY.md Wave 3, W3-1 and W3-2.
89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
// src/shared/constants.ts
|
|
import type { LicenseTier } from './types'
|
|
import { PLAN_QUOTA, type PlanQuotaFeature, type PlanQuotaPeriod } from './plan-catalog'
|
|
|
|
/** 타이밍 상수 (Speakly 리버스엔지니어링 기반) */
|
|
export const TIMING = {
|
|
/** 더블프레스 감지 간격 (ms) */
|
|
DOUBLE_PRESS_DURATION: 300,
|
|
|
|
/** 최소 녹음 시간 (ms) — 이하 자동 취소 */
|
|
MIN_AUDIO_DURATION: 700,
|
|
|
|
/** 녹음 후 STT 대기 시간 (ms) */
|
|
POST_RECORDING_WAIT: 4000,
|
|
|
|
/** 녹음 후 STT 대기 (버퍼 있을 때) (ms) */
|
|
POST_RECORDING_WAIT_BUFFERED: 6000,
|
|
|
|
/** STT 아이들 타임아웃 (ms) */
|
|
STT_IDLE_TIMEOUT: 30000,
|
|
|
|
/** 절대 최대 대기 시간 (ms) */
|
|
ABSOLUTE_MAX_WAIT: 120000,
|
|
|
|
/** 오디오 레벨 전송 간격 (ms) */
|
|
AUDIO_LEVEL_INTERVAL: 100,
|
|
|
|
/** 서비스 종료 타임아웃 (ms) */
|
|
SERVICE_DESTROY_TIMEOUT: 3000,
|
|
|
|
/** 사이드카 헬스체크 지연 (ms) */
|
|
SIDECAR_HEALTH_DELAY: 3000,
|
|
|
|
/** LLM 요청 타임아웃 (ms) */
|
|
LLM_REQUEST_TIMEOUT: 30000
|
|
} as const
|
|
|
|
/** 웨이브 바 상수 (RecordingTip) */
|
|
export const WAVE_BAR = {
|
|
COUNT: 9,
|
|
ANIMATION_INTERVAL: 100,
|
|
|
|
/** 코사인 분포 가중치 (Speakly 패턴) */
|
|
COS_WEIGHTS: Array.from({ length: 9 }, (_, i) => Math.cos((i - 4) * (Math.PI / 9)))
|
|
} as const
|
|
|
|
/** 오디오 포맷 */
|
|
export const AUDIO_FORMAT = {
|
|
SAMPLE_RATE: 16000,
|
|
CHANNELS: 1,
|
|
BIT_DEPTH: 16,
|
|
BYTES_PER_SAMPLE: 2
|
|
} as const
|
|
|
|
/** Premium 모델별 쿼터 표시 행. 값의 정본은 plan-catalog.ts `PLAN_QUOTA`(서버 quota.ts도 같은 값을 생성 사본으로 읽는다). */
|
|
export interface PremiumModelQuota {
|
|
readonly model: PlanQuotaFeature
|
|
readonly i18nKey: string
|
|
readonly limit: number // -1 = 무제한
|
|
readonly period: PlanQuotaPeriod
|
|
}
|
|
|
|
const PREMIUM_MODEL_ROWS: readonly { model: PlanQuotaFeature; i18nKey: string }[] = [
|
|
{ model: 'llm_haiku', i18nKey: 'license.modelHaiku' },
|
|
{ model: 'llm_sonnet', i18nKey: 'license.modelSonnet' },
|
|
{ model: 'llm_opus', i18nKey: 'license.modelOpus' },
|
|
]
|
|
|
|
function premiumModelLimits(tier: LicenseTier): readonly PremiumModelQuota[] {
|
|
// limit 0(사용 불가) 모델은 표시하지 않는다 — Free 는 Haiku 한 줄만 남는다.
|
|
return PREMIUM_MODEL_ROWS
|
|
.map(({ model, i18nKey }) => ({ model, i18nKey, ...PLAN_QUOTA[tier][model] }))
|
|
.filter((row) => row.limit !== 0)
|
|
}
|
|
|
|
export const PREMIUM_MODEL_LIMITS: Record<LicenseTier, readonly PremiumModelQuota[]> = {
|
|
free: premiumModelLimits('free'),
|
|
pro: premiumModelLimits('pro'),
|
|
pro_plus: premiumModelLimits('pro_plus'),
|
|
team: premiumModelLimits('team'),
|
|
enterprise: premiumModelLimits('enterprise'),
|
|
}
|
|
|
|
/** 윈도우 크기 */
|
|
export const WINDOW_SIZE = {
|
|
MAIN: { width: 1104, height: 816 },
|
|
RECORDING_TIP: { width: 280, height: 80 },
|
|
RESULT_POPUP: { width: 400, height: 200 }
|
|
} as const
|