spoclip-kit 2.6.0 → 2.7.0

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/index.cjs CHANGED
@@ -20,15 +20,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ GYM_GOLD_CHARGE_POLICY: () => GYM_GOLD_CHARGE_POLICY,
23
24
  GYM_PARTNER_REGISTRATION_GOLD_POLICY: () => GYM_PARTNER_REGISTRATION_GOLD_POLICY,
24
25
  MIN_DOWNLOAD_SECONDS: () => MIN_DOWNLOAD_SECONDS,
25
26
  ORIGINAL_DOWNLOAD_COST_POLICY: () => ORIGINAL_DOWNLOAD_COST_POLICY,
27
+ assertValidGymGoldChargeKrw: () => assertValidGymGoldChargeKrw,
28
+ calculateGymGoldCharge: () => calculateGymGoldCharge,
26
29
  calculateGymPartnerRegistrationGold: () => calculateGymPartnerRegistrationGold,
27
30
  calculateGymPartnerRegistrationGoldTotal: () => calculateGymPartnerRegistrationGoldTotal,
28
31
  calculateOriginalDownloadCostWon: () => calculateOriginalDownloadCostWon,
29
32
  color: () => color,
30
33
  colorV2: () => colorV2,
31
34
  isDownloadDurationAllowed: () => isDownloadDurationAllowed,
35
+ isGymGoldChargeKrwAllowed: () => isGymGoldChargeKrwAllowed,
32
36
  mobileTypo: () => mobileTypo,
33
37
  typo: () => typo
34
38
  });
@@ -77,6 +81,49 @@ function calculateOriginalDownloadCostWon(durationSeconds, options = {}) {
77
81
  return Math.ceil(Number(rawCost.toFixed(6)));
78
82
  }
79
83
 
84
+ // src/libs/gym-gold-charge.ts
85
+ var GYM_GOLD_CHARGE_POLICY = {
86
+ minChargeKrw: 1e4,
87
+ chargeUnitKrw: 1e4,
88
+ bonusRate: 0.2,
89
+ bonusRateBps: 2e3
90
+ };
91
+ function calculateGymGoldCharge(input) {
92
+ assertValidGymGoldChargeKrw(input.chargeKrw);
93
+ const baseGold = input.chargeKrw;
94
+ const bonusGold = calculateBonusGold(input.chargeKrw);
95
+ return {
96
+ chargeKrw: input.chargeKrw,
97
+ baseGold,
98
+ bonusGold,
99
+ totalGold: baseGold + bonusGold
100
+ };
101
+ }
102
+ function isGymGoldChargeKrwAllowed(chargeKrw) {
103
+ return Number.isFinite(chargeKrw) && Number.isInteger(chargeKrw) && chargeKrw >= GYM_GOLD_CHARGE_POLICY.minChargeKrw && chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw === 0;
104
+ }
105
+ function assertValidGymGoldChargeKrw(chargeKrw) {
106
+ if (!Number.isFinite(chargeKrw)) {
107
+ throw new RangeError("chargeKrw must be a finite number");
108
+ }
109
+ if (!Number.isInteger(chargeKrw)) {
110
+ throw new RangeError("chargeKrw must be an integer");
111
+ }
112
+ if (chargeKrw < GYM_GOLD_CHARGE_POLICY.minChargeKrw) {
113
+ throw new RangeError(
114
+ `chargeKrw must be at least ${GYM_GOLD_CHARGE_POLICY.minChargeKrw}`
115
+ );
116
+ }
117
+ if (chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw !== 0) {
118
+ throw new RangeError(
119
+ `chargeKrw must be a multiple of ${GYM_GOLD_CHARGE_POLICY.chargeUnitKrw}`
120
+ );
121
+ }
122
+ }
123
+ function calculateBonusGold(chargeKrw) {
124
+ return Math.floor(chargeKrw * GYM_GOLD_CHARGE_POLICY.bonusRateBps / 1e4);
125
+ }
126
+
80
127
  // src/libs/gym-registration-gold.ts
81
128
  var GYM_PARTNER_REGISTRATION_GOLD_POLICY = {
82
129
  goldPerDay: 350,
@@ -375,15 +422,19 @@ var mobileTypo = {
375
422
  };
376
423
  // Annotate the CommonJS export names for ESM import in node:
377
424
  0 && (module.exports = {
425
+ GYM_GOLD_CHARGE_POLICY,
378
426
  GYM_PARTNER_REGISTRATION_GOLD_POLICY,
379
427
  MIN_DOWNLOAD_SECONDS,
380
428
  ORIGINAL_DOWNLOAD_COST_POLICY,
429
+ assertValidGymGoldChargeKrw,
430
+ calculateGymGoldCharge,
381
431
  calculateGymPartnerRegistrationGold,
382
432
  calculateGymPartnerRegistrationGoldTotal,
383
433
  calculateOriginalDownloadCostWon,
384
434
  color,
385
435
  colorV2,
386
436
  isDownloadDurationAllowed,
437
+ isGymGoldChargeKrwAllowed,
387
438
  mobileTypo,
388
439
  typo
389
440
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/libs/cost.ts","../src/libs/gym-registration-gold.ts","../src/styles/color.ts","../src/styles/typo.ts"],"sourcesContent":["// Main entry point for spoclip-kit\nexport * from './libs';\nexport * from './types';\nexport * from './styles';\n","import type { TicketCode } from '../types/membership';\n\nexport const ORIGINAL_DOWNLOAD_COST_POLICY = {\n baseDurationSeconds: 600,\n baseCostWon: 2000,\n fullVideoDiscountRate: 0.1,\n /**\n * v14 신정책: 멤버십 plan별 분당 KRW 요율.\n * `calculateOriginalDownloadCostWon(duration, { plan })`로 호출 시 적용된다.\n * plan 미지정 시 `baseCostWon/baseDurationSeconds` 기반 단일 단가(레거시 200원/분)로 폴백 — 기존 호출자 호환.\n */\n perPlanKrwPerMinute: {\n FREE: 100,\n PLUS: 50,\n PRO: 25,\n },\n /**\n * 멤버십 plan별 무료 다운로드 구간(초).\n * 다운로드 길이 중 앞 `freeSecondsByPlan[plan]`초는 과금에서 제외하고,\n * 이를 초과한 구간에만 `perPlanKrwPerMinute[plan]` 요율을 적용한다 (초과분 과금).\n * plan 미지정(레거시 단일 단가) 경로에는 무료 구간을 적용하지 않는다.\n */\n freeSecondsByPlan: {\n FREE: 10,\n PLUS: 20,\n PRO: 30,\n },\n} as const;\n\n/**\n * 다운로드 가능한 최소 길이(초).\n * 이보다 짧은 구간은 다운로드 요청 자체를 허용하지 않는다 (server·FE 공용 제약).\n */\nexport const MIN_DOWNLOAD_SECONDS = 10;\n\n/**\n * 다운로드 요청 길이가 허용되는지 검사한다.\n * `durationSeconds >= MIN_DOWNLOAD_SECONDS`일 때만 true.\n */\nexport function isDownloadDurationAllowed(durationSeconds: number): boolean {\n return (\n Number.isFinite(durationSeconds) && durationSeconds >= MIN_DOWNLOAD_SECONDS\n );\n}\n\nexport interface OriginalDownloadCostOptions {\n isFullVideo?: boolean;\n /**\n * 멤버십 plan. 지정 시 `perPlanKrwPerMinute[plan]`을 단가로,\n * `freeSecondsByPlan[plan]`을 무료 구간으로 적용한다.\n * 미지정(undefined) 시 레거시 단일 단가(`baseCostWon/baseDurationSeconds`) + 무료 구간 없음.\n */\n plan?: TicketCode;\n}\n\nexport function calculateOriginalDownloadCostWon(\n durationSeconds: number,\n options: OriginalDownloadCostOptions = {},\n): number {\n if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return 0;\n\n const { plan, isFullVideo } = options;\n\n // plan 지정 시 앞 freeSeconds 구간은 과금에서 제외하고 초과분만 과금한다.\n // 레거시 경로(plan 미지정)는 무료 구간 0으로 기존 동작을 유지한다.\n const freeSeconds =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.freeSecondsByPlan[plan]\n : 0;\n const billableSeconds = Math.max(0, durationSeconds - freeSeconds);\n if (billableSeconds <= 0) return 0;\n\n const unitPricePerSecond =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.perPlanKrwPerMinute[plan] / 60\n : ORIGINAL_DOWNLOAD_COST_POLICY.baseCostWon /\n ORIGINAL_DOWNLOAD_COST_POLICY.baseDurationSeconds;\n\n const discountMultiplier = isFullVideo\n ? 1 - ORIGINAL_DOWNLOAD_COST_POLICY.fullVideoDiscountRate\n : 1;\n\n // 부동소수점 drift(예: 30.000000000000004) 때문에 1원 과다 청구되는 것을 막기 위해\n // 원 단위 미만 노이즈를 보정한 뒤 올림한다.\n const rawCost = billableSeconds * unitPricePerSecond * discountMultiplier;\n return Math.ceil(Number(rawCost.toFixed(6)));\n}\n","export const GYM_PARTNER_REGISTRATION_GOLD_POLICY = {\n goldPerDay: 350,\n timeZone: 'Asia/Seoul',\n} as const;\n\nexport interface GymPartnerRegistrationGoldInput {\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n startDate: string;\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n endDate: string;\n}\n\nexport interface GymPartnerRegistrationGoldResult {\n days: number;\n goldAmount: number;\n}\n\nexport interface GymPartnerRegistrationGoldTotalItem\n extends GymPartnerRegistrationGoldInput,\n GymPartnerRegistrationGoldResult {}\n\nexport interface GymPartnerRegistrationGoldTotalResult {\n totalDays: number;\n totalGoldAmount: number;\n items: GymPartnerRegistrationGoldTotalItem[];\n}\n\ninterface KstDateParts {\n year: number;\n month: number;\n day: number;\n}\n\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport function calculateGymPartnerRegistrationGold(\n input: GymPartnerRegistrationGoldInput,\n): GymPartnerRegistrationGoldResult {\n const startDate = parseKstDateString(input.startDate, 'startDate');\n const endDate = parseKstDateString(input.endDate, 'endDate');\n\n const startDateKey = toUtcDateKey(startDate);\n const endDateKey = toUtcDateKey(endDate);\n\n if (endDateKey < startDateKey) {\n throw new RangeError('endDate must be on or after startDate');\n }\n\n const days = Math.floor((endDateKey - startDateKey) / MS_PER_DAY) + 1;\n\n return {\n days,\n goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay,\n };\n}\n\nexport function calculateGymPartnerRegistrationGoldTotal(\n periods: GymPartnerRegistrationGoldInput[],\n): GymPartnerRegistrationGoldTotalResult {\n const items = periods.map((period) => {\n const result = calculateGymPartnerRegistrationGold(period);\n\n return {\n ...period,\n ...result,\n };\n });\n\n return {\n totalDays: items.reduce((sum, item) => sum + item.days, 0),\n totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),\n items,\n };\n}\n\nfunction parseKstDateString(\n input: string,\n fieldName: 'startDate' | 'endDate',\n): KstDateParts {\n const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);\n if (dateOnlyMatch === null) {\n throw new RangeError(`${fieldName} must be a YYYY-MM-DD date`);\n }\n\n const year = Number(dateOnlyMatch[1]);\n const month = Number(dateOnlyMatch[2]);\n const day = Number(dateOnlyMatch[3]);\n const utcDate = new Date(Date.UTC(year, month - 1, day));\n\n if (\n utcDate.getUTCFullYear() !== year ||\n utcDate.getUTCMonth() + 1 !== month ||\n utcDate.getUTCDate() !== day\n ) {\n throw new RangeError(`${fieldName} must be a valid date`);\n }\n\n return { year, month, day };\n}\n\nfunction toUtcDateKey(date: KstDateParts): number {\n return Date.UTC(date.year, date.month - 1, date.day);\n}\n","const color = {\n primary: '#242535',\n primaryB: '#515265',\n primary2: '#9335FB',\n primary2B: '#6323AA',\n secondary: '#FADD32',\n bgColor: '#F3F4F6',\n error: '#EA2E2E',\n\n white: '#FFFFFF',\n black: '#111111',\n gray1: '#424242',\n gray2: '#616161',\n gray3: '#8E8E8E',\n gray4: '#BDBDBD',\n gray5: '#E0E0E0',\n gray6: '#EDEDED',\n\n sub: '#A3A4BD',\n sub3: '#5561E8',\n sub3b: '#F1F7FF',\n sub4: '#1D1A1A',\n\n point: '#FADD32',\n} as const;\n\n/**\n * @deprecated use color instead (작명 실수 ㅠ)\n */\n\nconst colorV2 = color;\n\nexport { colorV2, color };\n","type TypographyKey =\n | 'h1-24b'\n | 'h1-24m'\n | 'h1-24'\n | 'h2-20b'\n | 'h2-20m'\n | 'h2-20'\n | 'h3-18b'\n | 'h3-18m'\n | 'h3-18'\n | 'b1-16b'\n | 'b1-16m'\n | 'b1-16'\n | 'b2-14b'\n | 'b2-14m'\n | 'b2-14'\n | 'b3-12b'\n | 'b3-12m'\n | 'b3-12'\n | 's1-10b'\n | 's1-10m'\n | 's1-10';\n\nconst typo = {\n 'h1-24b': {\n fontSize: 24,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h1-24m': {\n fontSize: 24,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h1-24': {\n fontSize: 24,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'h2-20b': {\n fontSize: 20,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h2-20m': {\n fontSize: 20,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h2-20': {\n fontSize: 20,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18b': {\n fontSize: 18,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18m': {\n fontSize: 18,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18': {\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16b': {\n fontSize: 16,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16m': {\n fontSize: 16,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16': {\n fontSize: 16,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'b2-14b': {\n fontSize: 14,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b2-14m': {\n fontSize: 14,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b2-14': {\n fontSize: 14,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'b3-12b': {\n fontSize: 12,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b3-12m': {\n fontSize: 12,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b3-12': {\n fontSize: 12,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 's1-10b': {\n fontSize: 10,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 's1-10m': {\n fontSize: 10,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 's1-10': {\n fontSize: 10,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n} as const;\n\nconst transformLineheight = (key: TypographyKey) => {\n return typo[key].lineHeight * typo[key].fontSize;\n};\n\nconst mobileTypo = {\n 'h1-24b': {\n ...typo['h1-24b'],\n lineHeight: transformLineheight('h1-24b'),\n },\n\n 'h1-24m': {\n ...typo['h1-24m'],\n lineHeight: transformLineheight('h1-24m'),\n },\n\n 'h1-24': {\n ...typo['h1-24'],\n lineHeight: transformLineheight('h1-24'),\n },\n\n 'h2-20b': {\n ...typo['h2-20b'],\n lineHeight: transformLineheight('h2-20b'),\n },\n\n 'h2-20m': {\n ...typo['h2-20m'],\n lineHeight: transformLineheight('h2-20m'),\n },\n\n 'h2-20': {\n ...typo['h2-20'],\n lineHeight: transformLineheight('h2-20'),\n },\n\n 'h3-18b': {\n ...typo['h3-18b'],\n lineHeight: transformLineheight('h3-18b'),\n },\n\n 'h3-18m': {\n ...typo['h3-18m'],\n lineHeight: transformLineheight('h3-18m'),\n },\n\n 'h3-18': {\n ...typo['h3-18'],\n lineHeight: transformLineheight('h3-18'),\n },\n\n 'b1-16b': {\n ...typo['b1-16b'],\n lineHeight: transformLineheight('b1-16b'),\n },\n\n 'b1-16m': {\n ...typo['b1-16m'],\n lineHeight: transformLineheight('b1-16m'),\n },\n\n 'b1-16': {\n ...typo['b1-16'],\n lineHeight: transformLineheight('b1-16'),\n },\n\n 'b2-14b': {\n ...typo['b2-14b'],\n lineHeight: transformLineheight('b2-14b'),\n },\n\n 'b2-14m': {\n ...typo['b2-14m'],\n lineHeight: transformLineheight('b2-14m'),\n },\n\n 'b2-14': {\n ...typo['b2-14'],\n lineHeight: transformLineheight('b2-14'),\n },\n\n 'b3-12b': {\n ...typo['b3-12b'],\n lineHeight: transformLineheight('b3-12b'),\n },\n\n 'b3-12m': {\n ...typo['b3-12m'],\n lineHeight: transformLineheight('b3-12m'),\n },\n\n 'b3-12': {\n ...typo['b3-12'],\n lineHeight: transformLineheight('b3-12'),\n },\n\n 's1-10b': {\n ...typo['s1-10b'],\n lineHeight: transformLineheight('s1-10b'),\n },\n\n 's1-10m': {\n ...typo['s1-10m'],\n lineHeight: transformLineheight('s1-10m'),\n },\n\n 's1-10': {\n ...typo['s1-10'],\n lineHeight: transformLineheight('s1-10'),\n },\n} as const;\n\nexport { typo, mobileTypo };\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/libs/cost.ts","../src/libs/gym-gold-charge.ts","../src/libs/gym-registration-gold.ts","../src/styles/color.ts","../src/styles/typo.ts"],"sourcesContent":["// Main entry point for spoclip-kit\nexport * from './libs';\nexport * from './types';\nexport * from './styles';\n","import type { TicketCode } from '../types/membership';\n\nexport const ORIGINAL_DOWNLOAD_COST_POLICY = {\n baseDurationSeconds: 600,\n baseCostWon: 2000,\n fullVideoDiscountRate: 0.1,\n /**\n * v14 신정책: 멤버십 plan별 분당 KRW 요율.\n * `calculateOriginalDownloadCostWon(duration, { plan })`로 호출 시 적용된다.\n * plan 미지정 시 `baseCostWon/baseDurationSeconds` 기반 단일 단가(레거시 200원/분)로 폴백 — 기존 호출자 호환.\n */\n perPlanKrwPerMinute: {\n FREE: 100,\n PLUS: 50,\n PRO: 25,\n },\n /**\n * 멤버십 plan별 무료 다운로드 구간(초).\n * 다운로드 길이 중 앞 `freeSecondsByPlan[plan]`초는 과금에서 제외하고,\n * 이를 초과한 구간에만 `perPlanKrwPerMinute[plan]` 요율을 적용한다 (초과분 과금).\n * plan 미지정(레거시 단일 단가) 경로에는 무료 구간을 적용하지 않는다.\n */\n freeSecondsByPlan: {\n FREE: 10,\n PLUS: 20,\n PRO: 30,\n },\n} as const;\n\n/**\n * 다운로드 가능한 최소 길이(초).\n * 이보다 짧은 구간은 다운로드 요청 자체를 허용하지 않는다 (server·FE 공용 제약).\n */\nexport const MIN_DOWNLOAD_SECONDS = 10;\n\n/**\n * 다운로드 요청 길이가 허용되는지 검사한다.\n * `durationSeconds >= MIN_DOWNLOAD_SECONDS`일 때만 true.\n */\nexport function isDownloadDurationAllowed(durationSeconds: number): boolean {\n return (\n Number.isFinite(durationSeconds) && durationSeconds >= MIN_DOWNLOAD_SECONDS\n );\n}\n\nexport interface OriginalDownloadCostOptions {\n isFullVideo?: boolean;\n /**\n * 멤버십 plan. 지정 시 `perPlanKrwPerMinute[plan]`을 단가로,\n * `freeSecondsByPlan[plan]`을 무료 구간으로 적용한다.\n * 미지정(undefined) 시 레거시 단일 단가(`baseCostWon/baseDurationSeconds`) + 무료 구간 없음.\n */\n plan?: TicketCode;\n}\n\nexport function calculateOriginalDownloadCostWon(\n durationSeconds: number,\n options: OriginalDownloadCostOptions = {},\n): number {\n if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return 0;\n\n const { plan, isFullVideo } = options;\n\n // plan 지정 시 앞 freeSeconds 구간은 과금에서 제외하고 초과분만 과금한다.\n // 레거시 경로(plan 미지정)는 무료 구간 0으로 기존 동작을 유지한다.\n const freeSeconds =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.freeSecondsByPlan[plan]\n : 0;\n const billableSeconds = Math.max(0, durationSeconds - freeSeconds);\n if (billableSeconds <= 0) return 0;\n\n const unitPricePerSecond =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.perPlanKrwPerMinute[plan] / 60\n : ORIGINAL_DOWNLOAD_COST_POLICY.baseCostWon /\n ORIGINAL_DOWNLOAD_COST_POLICY.baseDurationSeconds;\n\n const discountMultiplier = isFullVideo\n ? 1 - ORIGINAL_DOWNLOAD_COST_POLICY.fullVideoDiscountRate\n : 1;\n\n // 부동소수점 drift(예: 30.000000000000004) 때문에 1원 과다 청구되는 것을 막기 위해\n // 원 단위 미만 노이즈를 보정한 뒤 올림한다.\n const rawCost = billableSeconds * unitPricePerSecond * discountMultiplier;\n return Math.ceil(Number(rawCost.toFixed(6)));\n}\n","export const GYM_GOLD_CHARGE_POLICY = {\n minChargeKrw: 10_000,\n chargeUnitKrw: 10_000,\n bonusRate: 0.2,\n bonusRateBps: 2_000,\n} as const;\n\nexport interface GymGoldChargeInput {\n /**\n * Actual KRW amount paid through PortOne.\n */\n chargeKrw: number;\n}\n\nexport interface GymGoldChargeResult {\n /**\n * Actual KRW amount paid through PortOne.\n */\n chargeKrw: number;\n /**\n * 1:1 base Gold granted for paid KRW.\n */\n baseGold: number;\n /**\n * Additional bonus Gold granted by charge policy.\n */\n bonusGold: number;\n /**\n * Final Gold amount credited to the gym balance.\n */\n totalGold: number;\n}\n\nexport function calculateGymGoldCharge(\n input: GymGoldChargeInput,\n): GymGoldChargeResult {\n assertValidGymGoldChargeKrw(input.chargeKrw);\n\n const baseGold = input.chargeKrw;\n const bonusGold = calculateBonusGold(input.chargeKrw);\n\n return {\n chargeKrw: input.chargeKrw,\n baseGold,\n bonusGold,\n totalGold: baseGold + bonusGold,\n };\n}\n\nexport function isGymGoldChargeKrwAllowed(chargeKrw: number): boolean {\n return (\n Number.isFinite(chargeKrw) &&\n Number.isInteger(chargeKrw) &&\n chargeKrw >= GYM_GOLD_CHARGE_POLICY.minChargeKrw &&\n chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw === 0\n );\n}\n\nexport function assertValidGymGoldChargeKrw(chargeKrw: number): void {\n if (!Number.isFinite(chargeKrw)) {\n throw new RangeError('chargeKrw must be a finite number');\n }\n\n if (!Number.isInteger(chargeKrw)) {\n throw new RangeError('chargeKrw must be an integer');\n }\n\n if (chargeKrw < GYM_GOLD_CHARGE_POLICY.minChargeKrw) {\n throw new RangeError(\n `chargeKrw must be at least ${GYM_GOLD_CHARGE_POLICY.minChargeKrw}`,\n );\n }\n\n if (chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw !== 0) {\n throw new RangeError(\n `chargeKrw must be a multiple of ${GYM_GOLD_CHARGE_POLICY.chargeUnitKrw}`,\n );\n }\n}\n\nfunction calculateBonusGold(chargeKrw: number): number {\n return Math.floor((chargeKrw * GYM_GOLD_CHARGE_POLICY.bonusRateBps) / 10_000);\n}\n","export const GYM_PARTNER_REGISTRATION_GOLD_POLICY = {\n goldPerDay: 350,\n timeZone: 'Asia/Seoul',\n} as const;\n\nexport interface GymPartnerRegistrationGoldInput {\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n startDate: string;\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n endDate: string;\n}\n\nexport interface GymPartnerRegistrationGoldResult {\n days: number;\n goldAmount: number;\n}\n\nexport interface GymPartnerRegistrationGoldTotalItem\n extends GymPartnerRegistrationGoldInput,\n GymPartnerRegistrationGoldResult {}\n\nexport interface GymPartnerRegistrationGoldTotalResult {\n totalDays: number;\n totalGoldAmount: number;\n items: GymPartnerRegistrationGoldTotalItem[];\n}\n\ninterface KstDateParts {\n year: number;\n month: number;\n day: number;\n}\n\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport function calculateGymPartnerRegistrationGold(\n input: GymPartnerRegistrationGoldInput,\n): GymPartnerRegistrationGoldResult {\n const startDate = parseKstDateString(input.startDate, 'startDate');\n const endDate = parseKstDateString(input.endDate, 'endDate');\n\n const startDateKey = toUtcDateKey(startDate);\n const endDateKey = toUtcDateKey(endDate);\n\n if (endDateKey < startDateKey) {\n throw new RangeError('endDate must be on or after startDate');\n }\n\n const days = Math.floor((endDateKey - startDateKey) / MS_PER_DAY) + 1;\n\n return {\n days,\n goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay,\n };\n}\n\nexport function calculateGymPartnerRegistrationGoldTotal(\n periods: GymPartnerRegistrationGoldInput[],\n): GymPartnerRegistrationGoldTotalResult {\n const items = periods.map((period) => {\n const result = calculateGymPartnerRegistrationGold(period);\n\n return {\n ...period,\n ...result,\n };\n });\n\n return {\n totalDays: items.reduce((sum, item) => sum + item.days, 0),\n totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),\n items,\n };\n}\n\nfunction parseKstDateString(\n input: string,\n fieldName: 'startDate' | 'endDate',\n): KstDateParts {\n const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);\n if (dateOnlyMatch === null) {\n throw new RangeError(`${fieldName} must be a YYYY-MM-DD date`);\n }\n\n const year = Number(dateOnlyMatch[1]);\n const month = Number(dateOnlyMatch[2]);\n const day = Number(dateOnlyMatch[3]);\n const utcDate = new Date(Date.UTC(year, month - 1, day));\n\n if (\n utcDate.getUTCFullYear() !== year ||\n utcDate.getUTCMonth() + 1 !== month ||\n utcDate.getUTCDate() !== day\n ) {\n throw new RangeError(`${fieldName} must be a valid date`);\n }\n\n return { year, month, day };\n}\n\nfunction toUtcDateKey(date: KstDateParts): number {\n return Date.UTC(date.year, date.month - 1, date.day);\n}\n","const color = {\n primary: '#242535',\n primaryB: '#515265',\n primary2: '#9335FB',\n primary2B: '#6323AA',\n secondary: '#FADD32',\n bgColor: '#F3F4F6',\n error: '#EA2E2E',\n\n white: '#FFFFFF',\n black: '#111111',\n gray1: '#424242',\n gray2: '#616161',\n gray3: '#8E8E8E',\n gray4: '#BDBDBD',\n gray5: '#E0E0E0',\n gray6: '#EDEDED',\n\n sub: '#A3A4BD',\n sub3: '#5561E8',\n sub3b: '#F1F7FF',\n sub4: '#1D1A1A',\n\n point: '#FADD32',\n} as const;\n\n/**\n * @deprecated use color instead (작명 실수 ㅠ)\n */\n\nconst colorV2 = color;\n\nexport { colorV2, color };\n","type TypographyKey =\n | 'h1-24b'\n | 'h1-24m'\n | 'h1-24'\n | 'h2-20b'\n | 'h2-20m'\n | 'h2-20'\n | 'h3-18b'\n | 'h3-18m'\n | 'h3-18'\n | 'b1-16b'\n | 'b1-16m'\n | 'b1-16'\n | 'b2-14b'\n | 'b2-14m'\n | 'b2-14'\n | 'b3-12b'\n | 'b3-12m'\n | 'b3-12'\n | 's1-10b'\n | 's1-10m'\n | 's1-10';\n\nconst typo = {\n 'h1-24b': {\n fontSize: 24,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h1-24m': {\n fontSize: 24,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h1-24': {\n fontSize: 24,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'h2-20b': {\n fontSize: 20,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h2-20m': {\n fontSize: 20,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h2-20': {\n fontSize: 20,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18b': {\n fontSize: 18,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18m': {\n fontSize: 18,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18': {\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16b': {\n fontSize: 16,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16m': {\n fontSize: 16,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16': {\n fontSize: 16,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'b2-14b': {\n fontSize: 14,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b2-14m': {\n fontSize: 14,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b2-14': {\n fontSize: 14,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'b3-12b': {\n fontSize: 12,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b3-12m': {\n fontSize: 12,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b3-12': {\n fontSize: 12,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 's1-10b': {\n fontSize: 10,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 's1-10m': {\n fontSize: 10,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 's1-10': {\n fontSize: 10,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n} as const;\n\nconst transformLineheight = (key: TypographyKey) => {\n return typo[key].lineHeight * typo[key].fontSize;\n};\n\nconst mobileTypo = {\n 'h1-24b': {\n ...typo['h1-24b'],\n lineHeight: transformLineheight('h1-24b'),\n },\n\n 'h1-24m': {\n ...typo['h1-24m'],\n lineHeight: transformLineheight('h1-24m'),\n },\n\n 'h1-24': {\n ...typo['h1-24'],\n lineHeight: transformLineheight('h1-24'),\n },\n\n 'h2-20b': {\n ...typo['h2-20b'],\n lineHeight: transformLineheight('h2-20b'),\n },\n\n 'h2-20m': {\n ...typo['h2-20m'],\n lineHeight: transformLineheight('h2-20m'),\n },\n\n 'h2-20': {\n ...typo['h2-20'],\n lineHeight: transformLineheight('h2-20'),\n },\n\n 'h3-18b': {\n ...typo['h3-18b'],\n lineHeight: transformLineheight('h3-18b'),\n },\n\n 'h3-18m': {\n ...typo['h3-18m'],\n lineHeight: transformLineheight('h3-18m'),\n },\n\n 'h3-18': {\n ...typo['h3-18'],\n lineHeight: transformLineheight('h3-18'),\n },\n\n 'b1-16b': {\n ...typo['b1-16b'],\n lineHeight: transformLineheight('b1-16b'),\n },\n\n 'b1-16m': {\n ...typo['b1-16m'],\n lineHeight: transformLineheight('b1-16m'),\n },\n\n 'b1-16': {\n ...typo['b1-16'],\n lineHeight: transformLineheight('b1-16'),\n },\n\n 'b2-14b': {\n ...typo['b2-14b'],\n lineHeight: transformLineheight('b2-14b'),\n },\n\n 'b2-14m': {\n ...typo['b2-14m'],\n lineHeight: transformLineheight('b2-14m'),\n },\n\n 'b2-14': {\n ...typo['b2-14'],\n lineHeight: transformLineheight('b2-14'),\n },\n\n 'b3-12b': {\n ...typo['b3-12b'],\n lineHeight: transformLineheight('b3-12b'),\n },\n\n 'b3-12m': {\n ...typo['b3-12m'],\n lineHeight: transformLineheight('b3-12m'),\n },\n\n 'b3-12': {\n ...typo['b3-12'],\n lineHeight: transformLineheight('b3-12'),\n },\n\n 's1-10b': {\n ...typo['s1-10b'],\n lineHeight: transformLineheight('s1-10b'),\n },\n\n 's1-10m': {\n ...typo['s1-10m'],\n lineHeight: transformLineheight('s1-10m'),\n },\n\n 's1-10': {\n ...typo['s1-10'],\n lineHeight: transformLineheight('s1-10'),\n },\n} as const;\n\nexport { typo, mobileTypo };\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { GYM_PARTNER_REGISTRATION_GOLD_POLICY, GymPartnerRegistrationGoldInput, GymPartnerRegistrationGoldResult, GymPartnerRegistrationGoldTotalItem, GymPartnerRegistrationGoldTotalResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, OriginalDownloadCostOptions, calculateGymPartnerRegistrationGold, calculateGymPartnerRegistrationGoldTotal, calculateOriginalDownloadCostWon, isDownloadDurationAllowed } from './libs.cjs';
1
+ export { GYM_GOLD_CHARGE_POLICY, GYM_PARTNER_REGISTRATION_GOLD_POLICY, GymGoldChargeInput, GymGoldChargeResult, GymPartnerRegistrationGoldInput, GymPartnerRegistrationGoldResult, GymPartnerRegistrationGoldTotalItem, GymPartnerRegistrationGoldTotalResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, OriginalDownloadCostOptions, assertValidGymGoldChargeKrw, calculateGymGoldCharge, calculateGymPartnerRegistrationGold, calculateGymPartnerRegistrationGoldTotal, calculateOriginalDownloadCostWon, isDownloadDurationAllowed, isGymGoldChargeKrwAllowed } from './libs.cjs';
2
2
  export { T as TicketCode } from './membership-C_ziSHS0.cjs';
3
3
  export { APIErrorResponse, APIResponse, APIResponseBase } from './types.cjs';
4
4
  export { color, colorV2, mobileTypo, typo } from './styles.cjs';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { GYM_PARTNER_REGISTRATION_GOLD_POLICY, GymPartnerRegistrationGoldInput, GymPartnerRegistrationGoldResult, GymPartnerRegistrationGoldTotalItem, GymPartnerRegistrationGoldTotalResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, OriginalDownloadCostOptions, calculateGymPartnerRegistrationGold, calculateGymPartnerRegistrationGoldTotal, calculateOriginalDownloadCostWon, isDownloadDurationAllowed } from './libs.js';
1
+ export { GYM_GOLD_CHARGE_POLICY, GYM_PARTNER_REGISTRATION_GOLD_POLICY, GymGoldChargeInput, GymGoldChargeResult, GymPartnerRegistrationGoldInput, GymPartnerRegistrationGoldResult, GymPartnerRegistrationGoldTotalItem, GymPartnerRegistrationGoldTotalResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, OriginalDownloadCostOptions, assertValidGymGoldChargeKrw, calculateGymGoldCharge, calculateGymPartnerRegistrationGold, calculateGymPartnerRegistrationGoldTotal, calculateOriginalDownloadCostWon, isDownloadDurationAllowed, isGymGoldChargeKrwAllowed } from './libs.js';
2
2
  export { T as TicketCode } from './membership-C_ziSHS0.js';
3
3
  export { APIErrorResponse, APIResponse, APIResponseBase } from './types.js';
4
4
  export { color, colorV2, mobileTypo, typo } from './styles.js';
package/dist/index.js CHANGED
@@ -41,6 +41,49 @@ function calculateOriginalDownloadCostWon(durationSeconds, options = {}) {
41
41
  return Math.ceil(Number(rawCost.toFixed(6)));
42
42
  }
43
43
 
44
+ // src/libs/gym-gold-charge.ts
45
+ var GYM_GOLD_CHARGE_POLICY = {
46
+ minChargeKrw: 1e4,
47
+ chargeUnitKrw: 1e4,
48
+ bonusRate: 0.2,
49
+ bonusRateBps: 2e3
50
+ };
51
+ function calculateGymGoldCharge(input) {
52
+ assertValidGymGoldChargeKrw(input.chargeKrw);
53
+ const baseGold = input.chargeKrw;
54
+ const bonusGold = calculateBonusGold(input.chargeKrw);
55
+ return {
56
+ chargeKrw: input.chargeKrw,
57
+ baseGold,
58
+ bonusGold,
59
+ totalGold: baseGold + bonusGold
60
+ };
61
+ }
62
+ function isGymGoldChargeKrwAllowed(chargeKrw) {
63
+ return Number.isFinite(chargeKrw) && Number.isInteger(chargeKrw) && chargeKrw >= GYM_GOLD_CHARGE_POLICY.minChargeKrw && chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw === 0;
64
+ }
65
+ function assertValidGymGoldChargeKrw(chargeKrw) {
66
+ if (!Number.isFinite(chargeKrw)) {
67
+ throw new RangeError("chargeKrw must be a finite number");
68
+ }
69
+ if (!Number.isInteger(chargeKrw)) {
70
+ throw new RangeError("chargeKrw must be an integer");
71
+ }
72
+ if (chargeKrw < GYM_GOLD_CHARGE_POLICY.minChargeKrw) {
73
+ throw new RangeError(
74
+ `chargeKrw must be at least ${GYM_GOLD_CHARGE_POLICY.minChargeKrw}`
75
+ );
76
+ }
77
+ if (chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw !== 0) {
78
+ throw new RangeError(
79
+ `chargeKrw must be a multiple of ${GYM_GOLD_CHARGE_POLICY.chargeUnitKrw}`
80
+ );
81
+ }
82
+ }
83
+ function calculateBonusGold(chargeKrw) {
84
+ return Math.floor(chargeKrw * GYM_GOLD_CHARGE_POLICY.bonusRateBps / 1e4);
85
+ }
86
+
44
87
  // src/libs/gym-registration-gold.ts
45
88
  var GYM_PARTNER_REGISTRATION_GOLD_POLICY = {
46
89
  goldPerDay: 350,
@@ -338,15 +381,19 @@ var mobileTypo = {
338
381
  }
339
382
  };
340
383
  export {
384
+ GYM_GOLD_CHARGE_POLICY,
341
385
  GYM_PARTNER_REGISTRATION_GOLD_POLICY,
342
386
  MIN_DOWNLOAD_SECONDS,
343
387
  ORIGINAL_DOWNLOAD_COST_POLICY,
388
+ assertValidGymGoldChargeKrw,
389
+ calculateGymGoldCharge,
344
390
  calculateGymPartnerRegistrationGold,
345
391
  calculateGymPartnerRegistrationGoldTotal,
346
392
  calculateOriginalDownloadCostWon,
347
393
  color,
348
394
  colorV2,
349
395
  isDownloadDurationAllowed,
396
+ isGymGoldChargeKrwAllowed,
350
397
  mobileTypo,
351
398
  typo
352
399
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/libs/cost.ts","../src/libs/gym-registration-gold.ts","../src/styles/color.ts","../src/styles/typo.ts"],"sourcesContent":["import type { TicketCode } from '../types/membership';\n\nexport const ORIGINAL_DOWNLOAD_COST_POLICY = {\n baseDurationSeconds: 600,\n baseCostWon: 2000,\n fullVideoDiscountRate: 0.1,\n /**\n * v14 신정책: 멤버십 plan별 분당 KRW 요율.\n * `calculateOriginalDownloadCostWon(duration, { plan })`로 호출 시 적용된다.\n * plan 미지정 시 `baseCostWon/baseDurationSeconds` 기반 단일 단가(레거시 200원/분)로 폴백 — 기존 호출자 호환.\n */\n perPlanKrwPerMinute: {\n FREE: 100,\n PLUS: 50,\n PRO: 25,\n },\n /**\n * 멤버십 plan별 무료 다운로드 구간(초).\n * 다운로드 길이 중 앞 `freeSecondsByPlan[plan]`초는 과금에서 제외하고,\n * 이를 초과한 구간에만 `perPlanKrwPerMinute[plan]` 요율을 적용한다 (초과분 과금).\n * plan 미지정(레거시 단일 단가) 경로에는 무료 구간을 적용하지 않는다.\n */\n freeSecondsByPlan: {\n FREE: 10,\n PLUS: 20,\n PRO: 30,\n },\n} as const;\n\n/**\n * 다운로드 가능한 최소 길이(초).\n * 이보다 짧은 구간은 다운로드 요청 자체를 허용하지 않는다 (server·FE 공용 제약).\n */\nexport const MIN_DOWNLOAD_SECONDS = 10;\n\n/**\n * 다운로드 요청 길이가 허용되는지 검사한다.\n * `durationSeconds >= MIN_DOWNLOAD_SECONDS`일 때만 true.\n */\nexport function isDownloadDurationAllowed(durationSeconds: number): boolean {\n return (\n Number.isFinite(durationSeconds) && durationSeconds >= MIN_DOWNLOAD_SECONDS\n );\n}\n\nexport interface OriginalDownloadCostOptions {\n isFullVideo?: boolean;\n /**\n * 멤버십 plan. 지정 시 `perPlanKrwPerMinute[plan]`을 단가로,\n * `freeSecondsByPlan[plan]`을 무료 구간으로 적용한다.\n * 미지정(undefined) 시 레거시 단일 단가(`baseCostWon/baseDurationSeconds`) + 무료 구간 없음.\n */\n plan?: TicketCode;\n}\n\nexport function calculateOriginalDownloadCostWon(\n durationSeconds: number,\n options: OriginalDownloadCostOptions = {},\n): number {\n if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return 0;\n\n const { plan, isFullVideo } = options;\n\n // plan 지정 시 앞 freeSeconds 구간은 과금에서 제외하고 초과분만 과금한다.\n // 레거시 경로(plan 미지정)는 무료 구간 0으로 기존 동작을 유지한다.\n const freeSeconds =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.freeSecondsByPlan[plan]\n : 0;\n const billableSeconds = Math.max(0, durationSeconds - freeSeconds);\n if (billableSeconds <= 0) return 0;\n\n const unitPricePerSecond =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.perPlanKrwPerMinute[plan] / 60\n : ORIGINAL_DOWNLOAD_COST_POLICY.baseCostWon /\n ORIGINAL_DOWNLOAD_COST_POLICY.baseDurationSeconds;\n\n const discountMultiplier = isFullVideo\n ? 1 - ORIGINAL_DOWNLOAD_COST_POLICY.fullVideoDiscountRate\n : 1;\n\n // 부동소수점 drift(예: 30.000000000000004) 때문에 1원 과다 청구되는 것을 막기 위해\n // 원 단위 미만 노이즈를 보정한 뒤 올림한다.\n const rawCost = billableSeconds * unitPricePerSecond * discountMultiplier;\n return Math.ceil(Number(rawCost.toFixed(6)));\n}\n","export const GYM_PARTNER_REGISTRATION_GOLD_POLICY = {\n goldPerDay: 350,\n timeZone: 'Asia/Seoul',\n} as const;\n\nexport interface GymPartnerRegistrationGoldInput {\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n startDate: string;\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n endDate: string;\n}\n\nexport interface GymPartnerRegistrationGoldResult {\n days: number;\n goldAmount: number;\n}\n\nexport interface GymPartnerRegistrationGoldTotalItem\n extends GymPartnerRegistrationGoldInput,\n GymPartnerRegistrationGoldResult {}\n\nexport interface GymPartnerRegistrationGoldTotalResult {\n totalDays: number;\n totalGoldAmount: number;\n items: GymPartnerRegistrationGoldTotalItem[];\n}\n\ninterface KstDateParts {\n year: number;\n month: number;\n day: number;\n}\n\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport function calculateGymPartnerRegistrationGold(\n input: GymPartnerRegistrationGoldInput,\n): GymPartnerRegistrationGoldResult {\n const startDate = parseKstDateString(input.startDate, 'startDate');\n const endDate = parseKstDateString(input.endDate, 'endDate');\n\n const startDateKey = toUtcDateKey(startDate);\n const endDateKey = toUtcDateKey(endDate);\n\n if (endDateKey < startDateKey) {\n throw new RangeError('endDate must be on or after startDate');\n }\n\n const days = Math.floor((endDateKey - startDateKey) / MS_PER_DAY) + 1;\n\n return {\n days,\n goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay,\n };\n}\n\nexport function calculateGymPartnerRegistrationGoldTotal(\n periods: GymPartnerRegistrationGoldInput[],\n): GymPartnerRegistrationGoldTotalResult {\n const items = periods.map((period) => {\n const result = calculateGymPartnerRegistrationGold(period);\n\n return {\n ...period,\n ...result,\n };\n });\n\n return {\n totalDays: items.reduce((sum, item) => sum + item.days, 0),\n totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),\n items,\n };\n}\n\nfunction parseKstDateString(\n input: string,\n fieldName: 'startDate' | 'endDate',\n): KstDateParts {\n const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);\n if (dateOnlyMatch === null) {\n throw new RangeError(`${fieldName} must be a YYYY-MM-DD date`);\n }\n\n const year = Number(dateOnlyMatch[1]);\n const month = Number(dateOnlyMatch[2]);\n const day = Number(dateOnlyMatch[3]);\n const utcDate = new Date(Date.UTC(year, month - 1, day));\n\n if (\n utcDate.getUTCFullYear() !== year ||\n utcDate.getUTCMonth() + 1 !== month ||\n utcDate.getUTCDate() !== day\n ) {\n throw new RangeError(`${fieldName} must be a valid date`);\n }\n\n return { year, month, day };\n}\n\nfunction toUtcDateKey(date: KstDateParts): number {\n return Date.UTC(date.year, date.month - 1, date.day);\n}\n","const color = {\n primary: '#242535',\n primaryB: '#515265',\n primary2: '#9335FB',\n primary2B: '#6323AA',\n secondary: '#FADD32',\n bgColor: '#F3F4F6',\n error: '#EA2E2E',\n\n white: '#FFFFFF',\n black: '#111111',\n gray1: '#424242',\n gray2: '#616161',\n gray3: '#8E8E8E',\n gray4: '#BDBDBD',\n gray5: '#E0E0E0',\n gray6: '#EDEDED',\n\n sub: '#A3A4BD',\n sub3: '#5561E8',\n sub3b: '#F1F7FF',\n sub4: '#1D1A1A',\n\n point: '#FADD32',\n} as const;\n\n/**\n * @deprecated use color instead (작명 실수 ㅠ)\n */\n\nconst colorV2 = color;\n\nexport { colorV2, color };\n","type TypographyKey =\n | 'h1-24b'\n | 'h1-24m'\n | 'h1-24'\n | 'h2-20b'\n | 'h2-20m'\n | 'h2-20'\n | 'h3-18b'\n | 'h3-18m'\n | 'h3-18'\n | 'b1-16b'\n | 'b1-16m'\n | 'b1-16'\n | 'b2-14b'\n | 'b2-14m'\n | 'b2-14'\n | 'b3-12b'\n | 'b3-12m'\n | 'b3-12'\n | 's1-10b'\n | 's1-10m'\n | 's1-10';\n\nconst typo = {\n 'h1-24b': {\n fontSize: 24,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h1-24m': {\n fontSize: 24,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h1-24': {\n fontSize: 24,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'h2-20b': {\n fontSize: 20,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h2-20m': {\n fontSize: 20,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h2-20': {\n fontSize: 20,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18b': {\n fontSize: 18,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18m': {\n fontSize: 18,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18': {\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16b': {\n fontSize: 16,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16m': {\n fontSize: 16,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16': {\n fontSize: 16,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'b2-14b': {\n fontSize: 14,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b2-14m': {\n fontSize: 14,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b2-14': {\n fontSize: 14,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'b3-12b': {\n fontSize: 12,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b3-12m': {\n fontSize: 12,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b3-12': {\n fontSize: 12,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 's1-10b': {\n fontSize: 10,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 's1-10m': {\n fontSize: 10,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 's1-10': {\n fontSize: 10,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n} as const;\n\nconst transformLineheight = (key: TypographyKey) => {\n return typo[key].lineHeight * typo[key].fontSize;\n};\n\nconst mobileTypo = {\n 'h1-24b': {\n ...typo['h1-24b'],\n lineHeight: transformLineheight('h1-24b'),\n },\n\n 'h1-24m': {\n ...typo['h1-24m'],\n lineHeight: transformLineheight('h1-24m'),\n },\n\n 'h1-24': {\n ...typo['h1-24'],\n lineHeight: transformLineheight('h1-24'),\n },\n\n 'h2-20b': {\n ...typo['h2-20b'],\n lineHeight: transformLineheight('h2-20b'),\n },\n\n 'h2-20m': {\n ...typo['h2-20m'],\n lineHeight: transformLineheight('h2-20m'),\n },\n\n 'h2-20': {\n ...typo['h2-20'],\n lineHeight: transformLineheight('h2-20'),\n },\n\n 'h3-18b': {\n ...typo['h3-18b'],\n lineHeight: transformLineheight('h3-18b'),\n },\n\n 'h3-18m': {\n ...typo['h3-18m'],\n lineHeight: transformLineheight('h3-18m'),\n },\n\n 'h3-18': {\n ...typo['h3-18'],\n lineHeight: transformLineheight('h3-18'),\n },\n\n 'b1-16b': {\n ...typo['b1-16b'],\n lineHeight: transformLineheight('b1-16b'),\n },\n\n 'b1-16m': {\n ...typo['b1-16m'],\n lineHeight: transformLineheight('b1-16m'),\n },\n\n 'b1-16': {\n ...typo['b1-16'],\n lineHeight: transformLineheight('b1-16'),\n },\n\n 'b2-14b': {\n ...typo['b2-14b'],\n lineHeight: transformLineheight('b2-14b'),\n },\n\n 'b2-14m': {\n ...typo['b2-14m'],\n lineHeight: transformLineheight('b2-14m'),\n },\n\n 'b2-14': {\n ...typo['b2-14'],\n lineHeight: transformLineheight('b2-14'),\n },\n\n 'b3-12b': {\n ...typo['b3-12b'],\n lineHeight: transformLineheight('b3-12b'),\n },\n\n 'b3-12m': {\n ...typo['b3-12m'],\n lineHeight: transformLineheight('b3-12m'),\n },\n\n 'b3-12': {\n ...typo['b3-12'],\n lineHeight: transformLineheight('b3-12'),\n },\n\n 's1-10b': {\n ...typo['s1-10b'],\n lineHeight: transformLineheight('s1-10b'),\n },\n\n 's1-10m': {\n ...typo['s1-10m'],\n lineHeight: transformLineheight('s1-10m'),\n },\n\n 's1-10': {\n ...typo['s1-10'],\n lineHeight: transformLineheight('s1-10'),\n },\n} as const;\n\nexport { typo, mobileTypo };\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/libs/cost.ts","../src/libs/gym-gold-charge.ts","../src/libs/gym-registration-gold.ts","../src/styles/color.ts","../src/styles/typo.ts"],"sourcesContent":["import type { TicketCode } from '../types/membership';\n\nexport const ORIGINAL_DOWNLOAD_COST_POLICY = {\n baseDurationSeconds: 600,\n baseCostWon: 2000,\n fullVideoDiscountRate: 0.1,\n /**\n * v14 신정책: 멤버십 plan별 분당 KRW 요율.\n * `calculateOriginalDownloadCostWon(duration, { plan })`로 호출 시 적용된다.\n * plan 미지정 시 `baseCostWon/baseDurationSeconds` 기반 단일 단가(레거시 200원/분)로 폴백 — 기존 호출자 호환.\n */\n perPlanKrwPerMinute: {\n FREE: 100,\n PLUS: 50,\n PRO: 25,\n },\n /**\n * 멤버십 plan별 무료 다운로드 구간(초).\n * 다운로드 길이 중 앞 `freeSecondsByPlan[plan]`초는 과금에서 제외하고,\n * 이를 초과한 구간에만 `perPlanKrwPerMinute[plan]` 요율을 적용한다 (초과분 과금).\n * plan 미지정(레거시 단일 단가) 경로에는 무료 구간을 적용하지 않는다.\n */\n freeSecondsByPlan: {\n FREE: 10,\n PLUS: 20,\n PRO: 30,\n },\n} as const;\n\n/**\n * 다운로드 가능한 최소 길이(초).\n * 이보다 짧은 구간은 다운로드 요청 자체를 허용하지 않는다 (server·FE 공용 제약).\n */\nexport const MIN_DOWNLOAD_SECONDS = 10;\n\n/**\n * 다운로드 요청 길이가 허용되는지 검사한다.\n * `durationSeconds >= MIN_DOWNLOAD_SECONDS`일 때만 true.\n */\nexport function isDownloadDurationAllowed(durationSeconds: number): boolean {\n return (\n Number.isFinite(durationSeconds) && durationSeconds >= MIN_DOWNLOAD_SECONDS\n );\n}\n\nexport interface OriginalDownloadCostOptions {\n isFullVideo?: boolean;\n /**\n * 멤버십 plan. 지정 시 `perPlanKrwPerMinute[plan]`을 단가로,\n * `freeSecondsByPlan[plan]`을 무료 구간으로 적용한다.\n * 미지정(undefined) 시 레거시 단일 단가(`baseCostWon/baseDurationSeconds`) + 무료 구간 없음.\n */\n plan?: TicketCode;\n}\n\nexport function calculateOriginalDownloadCostWon(\n durationSeconds: number,\n options: OriginalDownloadCostOptions = {},\n): number {\n if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return 0;\n\n const { plan, isFullVideo } = options;\n\n // plan 지정 시 앞 freeSeconds 구간은 과금에서 제외하고 초과분만 과금한다.\n // 레거시 경로(plan 미지정)는 무료 구간 0으로 기존 동작을 유지한다.\n const freeSeconds =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.freeSecondsByPlan[plan]\n : 0;\n const billableSeconds = Math.max(0, durationSeconds - freeSeconds);\n if (billableSeconds <= 0) return 0;\n\n const unitPricePerSecond =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.perPlanKrwPerMinute[plan] / 60\n : ORIGINAL_DOWNLOAD_COST_POLICY.baseCostWon /\n ORIGINAL_DOWNLOAD_COST_POLICY.baseDurationSeconds;\n\n const discountMultiplier = isFullVideo\n ? 1 - ORIGINAL_DOWNLOAD_COST_POLICY.fullVideoDiscountRate\n : 1;\n\n // 부동소수점 drift(예: 30.000000000000004) 때문에 1원 과다 청구되는 것을 막기 위해\n // 원 단위 미만 노이즈를 보정한 뒤 올림한다.\n const rawCost = billableSeconds * unitPricePerSecond * discountMultiplier;\n return Math.ceil(Number(rawCost.toFixed(6)));\n}\n","export const GYM_GOLD_CHARGE_POLICY = {\n minChargeKrw: 10_000,\n chargeUnitKrw: 10_000,\n bonusRate: 0.2,\n bonusRateBps: 2_000,\n} as const;\n\nexport interface GymGoldChargeInput {\n /**\n * Actual KRW amount paid through PortOne.\n */\n chargeKrw: number;\n}\n\nexport interface GymGoldChargeResult {\n /**\n * Actual KRW amount paid through PortOne.\n */\n chargeKrw: number;\n /**\n * 1:1 base Gold granted for paid KRW.\n */\n baseGold: number;\n /**\n * Additional bonus Gold granted by charge policy.\n */\n bonusGold: number;\n /**\n * Final Gold amount credited to the gym balance.\n */\n totalGold: number;\n}\n\nexport function calculateGymGoldCharge(\n input: GymGoldChargeInput,\n): GymGoldChargeResult {\n assertValidGymGoldChargeKrw(input.chargeKrw);\n\n const baseGold = input.chargeKrw;\n const bonusGold = calculateBonusGold(input.chargeKrw);\n\n return {\n chargeKrw: input.chargeKrw,\n baseGold,\n bonusGold,\n totalGold: baseGold + bonusGold,\n };\n}\n\nexport function isGymGoldChargeKrwAllowed(chargeKrw: number): boolean {\n return (\n Number.isFinite(chargeKrw) &&\n Number.isInteger(chargeKrw) &&\n chargeKrw >= GYM_GOLD_CHARGE_POLICY.minChargeKrw &&\n chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw === 0\n );\n}\n\nexport function assertValidGymGoldChargeKrw(chargeKrw: number): void {\n if (!Number.isFinite(chargeKrw)) {\n throw new RangeError('chargeKrw must be a finite number');\n }\n\n if (!Number.isInteger(chargeKrw)) {\n throw new RangeError('chargeKrw must be an integer');\n }\n\n if (chargeKrw < GYM_GOLD_CHARGE_POLICY.minChargeKrw) {\n throw new RangeError(\n `chargeKrw must be at least ${GYM_GOLD_CHARGE_POLICY.minChargeKrw}`,\n );\n }\n\n if (chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw !== 0) {\n throw new RangeError(\n `chargeKrw must be a multiple of ${GYM_GOLD_CHARGE_POLICY.chargeUnitKrw}`,\n );\n }\n}\n\nfunction calculateBonusGold(chargeKrw: number): number {\n return Math.floor((chargeKrw * GYM_GOLD_CHARGE_POLICY.bonusRateBps) / 10_000);\n}\n","export const GYM_PARTNER_REGISTRATION_GOLD_POLICY = {\n goldPerDay: 350,\n timeZone: 'Asia/Seoul',\n} as const;\n\nexport interface GymPartnerRegistrationGoldInput {\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n startDate: string;\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n endDate: string;\n}\n\nexport interface GymPartnerRegistrationGoldResult {\n days: number;\n goldAmount: number;\n}\n\nexport interface GymPartnerRegistrationGoldTotalItem\n extends GymPartnerRegistrationGoldInput,\n GymPartnerRegistrationGoldResult {}\n\nexport interface GymPartnerRegistrationGoldTotalResult {\n totalDays: number;\n totalGoldAmount: number;\n items: GymPartnerRegistrationGoldTotalItem[];\n}\n\ninterface KstDateParts {\n year: number;\n month: number;\n day: number;\n}\n\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport function calculateGymPartnerRegistrationGold(\n input: GymPartnerRegistrationGoldInput,\n): GymPartnerRegistrationGoldResult {\n const startDate = parseKstDateString(input.startDate, 'startDate');\n const endDate = parseKstDateString(input.endDate, 'endDate');\n\n const startDateKey = toUtcDateKey(startDate);\n const endDateKey = toUtcDateKey(endDate);\n\n if (endDateKey < startDateKey) {\n throw new RangeError('endDate must be on or after startDate');\n }\n\n const days = Math.floor((endDateKey - startDateKey) / MS_PER_DAY) + 1;\n\n return {\n days,\n goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay,\n };\n}\n\nexport function calculateGymPartnerRegistrationGoldTotal(\n periods: GymPartnerRegistrationGoldInput[],\n): GymPartnerRegistrationGoldTotalResult {\n const items = periods.map((period) => {\n const result = calculateGymPartnerRegistrationGold(period);\n\n return {\n ...period,\n ...result,\n };\n });\n\n return {\n totalDays: items.reduce((sum, item) => sum + item.days, 0),\n totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),\n items,\n };\n}\n\nfunction parseKstDateString(\n input: string,\n fieldName: 'startDate' | 'endDate',\n): KstDateParts {\n const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);\n if (dateOnlyMatch === null) {\n throw new RangeError(`${fieldName} must be a YYYY-MM-DD date`);\n }\n\n const year = Number(dateOnlyMatch[1]);\n const month = Number(dateOnlyMatch[2]);\n const day = Number(dateOnlyMatch[3]);\n const utcDate = new Date(Date.UTC(year, month - 1, day));\n\n if (\n utcDate.getUTCFullYear() !== year ||\n utcDate.getUTCMonth() + 1 !== month ||\n utcDate.getUTCDate() !== day\n ) {\n throw new RangeError(`${fieldName} must be a valid date`);\n }\n\n return { year, month, day };\n}\n\nfunction toUtcDateKey(date: KstDateParts): number {\n return Date.UTC(date.year, date.month - 1, date.day);\n}\n","const color = {\n primary: '#242535',\n primaryB: '#515265',\n primary2: '#9335FB',\n primary2B: '#6323AA',\n secondary: '#FADD32',\n bgColor: '#F3F4F6',\n error: '#EA2E2E',\n\n white: '#FFFFFF',\n black: '#111111',\n gray1: '#424242',\n gray2: '#616161',\n gray3: '#8E8E8E',\n gray4: '#BDBDBD',\n gray5: '#E0E0E0',\n gray6: '#EDEDED',\n\n sub: '#A3A4BD',\n sub3: '#5561E8',\n sub3b: '#F1F7FF',\n sub4: '#1D1A1A',\n\n point: '#FADD32',\n} as const;\n\n/**\n * @deprecated use color instead (작명 실수 ㅠ)\n */\n\nconst colorV2 = color;\n\nexport { colorV2, color };\n","type TypographyKey =\n | 'h1-24b'\n | 'h1-24m'\n | 'h1-24'\n | 'h2-20b'\n | 'h2-20m'\n | 'h2-20'\n | 'h3-18b'\n | 'h3-18m'\n | 'h3-18'\n | 'b1-16b'\n | 'b1-16m'\n | 'b1-16'\n | 'b2-14b'\n | 'b2-14m'\n | 'b2-14'\n | 'b3-12b'\n | 'b3-12m'\n | 'b3-12'\n | 's1-10b'\n | 's1-10m'\n | 's1-10';\n\nconst typo = {\n 'h1-24b': {\n fontSize: 24,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h1-24m': {\n fontSize: 24,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h1-24': {\n fontSize: 24,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'h2-20b': {\n fontSize: 20,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h2-20m': {\n fontSize: 20,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h2-20': {\n fontSize: 20,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18b': {\n fontSize: 18,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18m': {\n fontSize: 18,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'h3-18': {\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16b': {\n fontSize: 16,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16m': {\n fontSize: 16,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b1-16': {\n fontSize: 16,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'b2-14b': {\n fontSize: 14,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b2-14m': {\n fontSize: 14,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b2-14': {\n fontSize: 14,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 'b3-12b': {\n fontSize: 12,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b3-12m': {\n fontSize: 12,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 'b3-12': {\n fontSize: 12,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n\n 's1-10b': {\n fontSize: 10,\n fontWeight: 700,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 's1-10m': {\n fontSize: 10,\n fontWeight: 500,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n 's1-10': {\n fontSize: 10,\n fontWeight: 400,\n lineHeight: 1.5,\n letterSpacing: 0,\n },\n} as const;\n\nconst transformLineheight = (key: TypographyKey) => {\n return typo[key].lineHeight * typo[key].fontSize;\n};\n\nconst mobileTypo = {\n 'h1-24b': {\n ...typo['h1-24b'],\n lineHeight: transformLineheight('h1-24b'),\n },\n\n 'h1-24m': {\n ...typo['h1-24m'],\n lineHeight: transformLineheight('h1-24m'),\n },\n\n 'h1-24': {\n ...typo['h1-24'],\n lineHeight: transformLineheight('h1-24'),\n },\n\n 'h2-20b': {\n ...typo['h2-20b'],\n lineHeight: transformLineheight('h2-20b'),\n },\n\n 'h2-20m': {\n ...typo['h2-20m'],\n lineHeight: transformLineheight('h2-20m'),\n },\n\n 'h2-20': {\n ...typo['h2-20'],\n lineHeight: transformLineheight('h2-20'),\n },\n\n 'h3-18b': {\n ...typo['h3-18b'],\n lineHeight: transformLineheight('h3-18b'),\n },\n\n 'h3-18m': {\n ...typo['h3-18m'],\n lineHeight: transformLineheight('h3-18m'),\n },\n\n 'h3-18': {\n ...typo['h3-18'],\n lineHeight: transformLineheight('h3-18'),\n },\n\n 'b1-16b': {\n ...typo['b1-16b'],\n lineHeight: transformLineheight('b1-16b'),\n },\n\n 'b1-16m': {\n ...typo['b1-16m'],\n lineHeight: transformLineheight('b1-16m'),\n },\n\n 'b1-16': {\n ...typo['b1-16'],\n lineHeight: transformLineheight('b1-16'),\n },\n\n 'b2-14b': {\n ...typo['b2-14b'],\n lineHeight: transformLineheight('b2-14b'),\n },\n\n 'b2-14m': {\n ...typo['b2-14m'],\n lineHeight: transformLineheight('b2-14m'),\n },\n\n 'b2-14': {\n ...typo['b2-14'],\n lineHeight: transformLineheight('b2-14'),\n },\n\n 'b3-12b': {\n ...typo['b3-12b'],\n lineHeight: transformLineheight('b3-12b'),\n },\n\n 'b3-12m': {\n ...typo['b3-12m'],\n lineHeight: transformLineheight('b3-12m'),\n },\n\n 'b3-12': {\n ...typo['b3-12'],\n lineHeight: transformLineheight('b3-12'),\n },\n\n 's1-10b': {\n ...typo['s1-10b'],\n lineHeight: transformLineheight('s1-10b'),\n },\n\n 's1-10m': {\n ...typo['s1-10m'],\n lineHeight: transformLineheight('s1-10m'),\n },\n\n 's1-10': {\n ...typo['s1-10'],\n lineHeight: transformLineheight('s1-10'),\n },\n} as const;\n\nexport { typo, mobileTypo };\n"],"mappings":"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","names":[]}
package/dist/libs.cjs CHANGED
@@ -20,13 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/libs/index.ts
21
21
  var libs_exports = {};
22
22
  __export(libs_exports, {
23
+ GYM_GOLD_CHARGE_POLICY: () => GYM_GOLD_CHARGE_POLICY,
23
24
  GYM_PARTNER_REGISTRATION_GOLD_POLICY: () => GYM_PARTNER_REGISTRATION_GOLD_POLICY,
24
25
  MIN_DOWNLOAD_SECONDS: () => MIN_DOWNLOAD_SECONDS,
25
26
  ORIGINAL_DOWNLOAD_COST_POLICY: () => ORIGINAL_DOWNLOAD_COST_POLICY,
27
+ assertValidGymGoldChargeKrw: () => assertValidGymGoldChargeKrw,
28
+ calculateGymGoldCharge: () => calculateGymGoldCharge,
26
29
  calculateGymPartnerRegistrationGold: () => calculateGymPartnerRegistrationGold,
27
30
  calculateGymPartnerRegistrationGoldTotal: () => calculateGymPartnerRegistrationGoldTotal,
28
31
  calculateOriginalDownloadCostWon: () => calculateOriginalDownloadCostWon,
29
- isDownloadDurationAllowed: () => isDownloadDurationAllowed
32
+ isDownloadDurationAllowed: () => isDownloadDurationAllowed,
33
+ isGymGoldChargeKrwAllowed: () => isGymGoldChargeKrwAllowed
30
34
  });
31
35
  module.exports = __toCommonJS(libs_exports);
32
36
 
@@ -73,6 +77,49 @@ function calculateOriginalDownloadCostWon(durationSeconds, options = {}) {
73
77
  return Math.ceil(Number(rawCost.toFixed(6)));
74
78
  }
75
79
 
80
+ // src/libs/gym-gold-charge.ts
81
+ var GYM_GOLD_CHARGE_POLICY = {
82
+ minChargeKrw: 1e4,
83
+ chargeUnitKrw: 1e4,
84
+ bonusRate: 0.2,
85
+ bonusRateBps: 2e3
86
+ };
87
+ function calculateGymGoldCharge(input) {
88
+ assertValidGymGoldChargeKrw(input.chargeKrw);
89
+ const baseGold = input.chargeKrw;
90
+ const bonusGold = calculateBonusGold(input.chargeKrw);
91
+ return {
92
+ chargeKrw: input.chargeKrw,
93
+ baseGold,
94
+ bonusGold,
95
+ totalGold: baseGold + bonusGold
96
+ };
97
+ }
98
+ function isGymGoldChargeKrwAllowed(chargeKrw) {
99
+ return Number.isFinite(chargeKrw) && Number.isInteger(chargeKrw) && chargeKrw >= GYM_GOLD_CHARGE_POLICY.minChargeKrw && chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw === 0;
100
+ }
101
+ function assertValidGymGoldChargeKrw(chargeKrw) {
102
+ if (!Number.isFinite(chargeKrw)) {
103
+ throw new RangeError("chargeKrw must be a finite number");
104
+ }
105
+ if (!Number.isInteger(chargeKrw)) {
106
+ throw new RangeError("chargeKrw must be an integer");
107
+ }
108
+ if (chargeKrw < GYM_GOLD_CHARGE_POLICY.minChargeKrw) {
109
+ throw new RangeError(
110
+ `chargeKrw must be at least ${GYM_GOLD_CHARGE_POLICY.minChargeKrw}`
111
+ );
112
+ }
113
+ if (chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw !== 0) {
114
+ throw new RangeError(
115
+ `chargeKrw must be a multiple of ${GYM_GOLD_CHARGE_POLICY.chargeUnitKrw}`
116
+ );
117
+ }
118
+ }
119
+ function calculateBonusGold(chargeKrw) {
120
+ return Math.floor(chargeKrw * GYM_GOLD_CHARGE_POLICY.bonusRateBps / 1e4);
121
+ }
122
+
76
123
  // src/libs/gym-registration-gold.ts
77
124
  var GYM_PARTNER_REGISTRATION_GOLD_POLICY = {
78
125
  goldPerDay: 350,
@@ -127,12 +174,16 @@ function toUtcDateKey(date) {
127
174
  }
128
175
  // Annotate the CommonJS export names for ESM import in node:
129
176
  0 && (module.exports = {
177
+ GYM_GOLD_CHARGE_POLICY,
130
178
  GYM_PARTNER_REGISTRATION_GOLD_POLICY,
131
179
  MIN_DOWNLOAD_SECONDS,
132
180
  ORIGINAL_DOWNLOAD_COST_POLICY,
181
+ assertValidGymGoldChargeKrw,
182
+ calculateGymGoldCharge,
133
183
  calculateGymPartnerRegistrationGold,
134
184
  calculateGymPartnerRegistrationGoldTotal,
135
185
  calculateOriginalDownloadCostWon,
136
- isDownloadDurationAllowed
186
+ isDownloadDurationAllowed,
187
+ isGymGoldChargeKrwAllowed
137
188
  });
138
189
  //# sourceMappingURL=libs.cjs.map
package/dist/libs.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/libs/index.ts","../src/libs/cost.ts","../src/libs/gym-registration-gold.ts"],"sourcesContent":["export * from './cost';\nexport * from './gym-registration-gold';\n","import type { TicketCode } from '../types/membership';\n\nexport const ORIGINAL_DOWNLOAD_COST_POLICY = {\n baseDurationSeconds: 600,\n baseCostWon: 2000,\n fullVideoDiscountRate: 0.1,\n /**\n * v14 신정책: 멤버십 plan별 분당 KRW 요율.\n * `calculateOriginalDownloadCostWon(duration, { plan })`로 호출 시 적용된다.\n * plan 미지정 시 `baseCostWon/baseDurationSeconds` 기반 단일 단가(레거시 200원/분)로 폴백 — 기존 호출자 호환.\n */\n perPlanKrwPerMinute: {\n FREE: 100,\n PLUS: 50,\n PRO: 25,\n },\n /**\n * 멤버십 plan별 무료 다운로드 구간(초).\n * 다운로드 길이 중 앞 `freeSecondsByPlan[plan]`초는 과금에서 제외하고,\n * 이를 초과한 구간에만 `perPlanKrwPerMinute[plan]` 요율을 적용한다 (초과분 과금).\n * plan 미지정(레거시 단일 단가) 경로에는 무료 구간을 적용하지 않는다.\n */\n freeSecondsByPlan: {\n FREE: 10,\n PLUS: 20,\n PRO: 30,\n },\n} as const;\n\n/**\n * 다운로드 가능한 최소 길이(초).\n * 이보다 짧은 구간은 다운로드 요청 자체를 허용하지 않는다 (server·FE 공용 제약).\n */\nexport const MIN_DOWNLOAD_SECONDS = 10;\n\n/**\n * 다운로드 요청 길이가 허용되는지 검사한다.\n * `durationSeconds >= MIN_DOWNLOAD_SECONDS`일 때만 true.\n */\nexport function isDownloadDurationAllowed(durationSeconds: number): boolean {\n return (\n Number.isFinite(durationSeconds) && durationSeconds >= MIN_DOWNLOAD_SECONDS\n );\n}\n\nexport interface OriginalDownloadCostOptions {\n isFullVideo?: boolean;\n /**\n * 멤버십 plan. 지정 시 `perPlanKrwPerMinute[plan]`을 단가로,\n * `freeSecondsByPlan[plan]`을 무료 구간으로 적용한다.\n * 미지정(undefined) 시 레거시 단일 단가(`baseCostWon/baseDurationSeconds`) + 무료 구간 없음.\n */\n plan?: TicketCode;\n}\n\nexport function calculateOriginalDownloadCostWon(\n durationSeconds: number,\n options: OriginalDownloadCostOptions = {},\n): number {\n if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return 0;\n\n const { plan, isFullVideo } = options;\n\n // plan 지정 시 앞 freeSeconds 구간은 과금에서 제외하고 초과분만 과금한다.\n // 레거시 경로(plan 미지정)는 무료 구간 0으로 기존 동작을 유지한다.\n const freeSeconds =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.freeSecondsByPlan[plan]\n : 0;\n const billableSeconds = Math.max(0, durationSeconds - freeSeconds);\n if (billableSeconds <= 0) return 0;\n\n const unitPricePerSecond =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.perPlanKrwPerMinute[plan] / 60\n : ORIGINAL_DOWNLOAD_COST_POLICY.baseCostWon /\n ORIGINAL_DOWNLOAD_COST_POLICY.baseDurationSeconds;\n\n const discountMultiplier = isFullVideo\n ? 1 - ORIGINAL_DOWNLOAD_COST_POLICY.fullVideoDiscountRate\n : 1;\n\n // 부동소수점 drift(예: 30.000000000000004) 때문에 1원 과다 청구되는 것을 막기 위해\n // 원 단위 미만 노이즈를 보정한 뒤 올림한다.\n const rawCost = billableSeconds * unitPricePerSecond * discountMultiplier;\n return Math.ceil(Number(rawCost.toFixed(6)));\n}\n","export const GYM_PARTNER_REGISTRATION_GOLD_POLICY = {\n goldPerDay: 350,\n timeZone: 'Asia/Seoul',\n} as const;\n\nexport interface GymPartnerRegistrationGoldInput {\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n startDate: string;\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n endDate: string;\n}\n\nexport interface GymPartnerRegistrationGoldResult {\n days: number;\n goldAmount: number;\n}\n\nexport interface GymPartnerRegistrationGoldTotalItem\n extends GymPartnerRegistrationGoldInput,\n GymPartnerRegistrationGoldResult {}\n\nexport interface GymPartnerRegistrationGoldTotalResult {\n totalDays: number;\n totalGoldAmount: number;\n items: GymPartnerRegistrationGoldTotalItem[];\n}\n\ninterface KstDateParts {\n year: number;\n month: number;\n day: number;\n}\n\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport function calculateGymPartnerRegistrationGold(\n input: GymPartnerRegistrationGoldInput,\n): GymPartnerRegistrationGoldResult {\n const startDate = parseKstDateString(input.startDate, 'startDate');\n const endDate = parseKstDateString(input.endDate, 'endDate');\n\n const startDateKey = toUtcDateKey(startDate);\n const endDateKey = toUtcDateKey(endDate);\n\n if (endDateKey < startDateKey) {\n throw new RangeError('endDate must be on or after startDate');\n }\n\n const days = Math.floor((endDateKey - startDateKey) / MS_PER_DAY) + 1;\n\n return {\n days,\n goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay,\n };\n}\n\nexport function calculateGymPartnerRegistrationGoldTotal(\n periods: GymPartnerRegistrationGoldInput[],\n): GymPartnerRegistrationGoldTotalResult {\n const items = periods.map((period) => {\n const result = calculateGymPartnerRegistrationGold(period);\n\n return {\n ...period,\n ...result,\n };\n });\n\n return {\n totalDays: items.reduce((sum, item) => sum + item.days, 0),\n totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),\n items,\n };\n}\n\nfunction parseKstDateString(\n input: string,\n fieldName: 'startDate' | 'endDate',\n): KstDateParts {\n const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);\n if (dateOnlyMatch === null) {\n throw new RangeError(`${fieldName} must be a YYYY-MM-DD date`);\n }\n\n const year = Number(dateOnlyMatch[1]);\n const month = Number(dateOnlyMatch[2]);\n const day = Number(dateOnlyMatch[3]);\n const utcDate = new Date(Date.UTC(year, month - 1, day));\n\n if (\n utcDate.getUTCFullYear() !== year ||\n utcDate.getUTCMonth() + 1 !== month ||\n utcDate.getUTCDate() !== day\n ) {\n throw new RangeError(`${fieldName} must be a valid date`);\n }\n\n return { year, month, day };\n}\n\nfunction toUtcDateKey(date: KstDateParts): number {\n return Date.UTC(date.year, date.month - 1, date.day);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/libs/index.ts","../src/libs/cost.ts","../src/libs/gym-gold-charge.ts","../src/libs/gym-registration-gold.ts"],"sourcesContent":["export * from './cost';\nexport * from './gym-gold-charge';\nexport * from './gym-registration-gold';\n","import type { TicketCode } from '../types/membership';\n\nexport const ORIGINAL_DOWNLOAD_COST_POLICY = {\n baseDurationSeconds: 600,\n baseCostWon: 2000,\n fullVideoDiscountRate: 0.1,\n /**\n * v14 신정책: 멤버십 plan별 분당 KRW 요율.\n * `calculateOriginalDownloadCostWon(duration, { plan })`로 호출 시 적용된다.\n * plan 미지정 시 `baseCostWon/baseDurationSeconds` 기반 단일 단가(레거시 200원/분)로 폴백 — 기존 호출자 호환.\n */\n perPlanKrwPerMinute: {\n FREE: 100,\n PLUS: 50,\n PRO: 25,\n },\n /**\n * 멤버십 plan별 무료 다운로드 구간(초).\n * 다운로드 길이 중 앞 `freeSecondsByPlan[plan]`초는 과금에서 제외하고,\n * 이를 초과한 구간에만 `perPlanKrwPerMinute[plan]` 요율을 적용한다 (초과분 과금).\n * plan 미지정(레거시 단일 단가) 경로에는 무료 구간을 적용하지 않는다.\n */\n freeSecondsByPlan: {\n FREE: 10,\n PLUS: 20,\n PRO: 30,\n },\n} as const;\n\n/**\n * 다운로드 가능한 최소 길이(초).\n * 이보다 짧은 구간은 다운로드 요청 자체를 허용하지 않는다 (server·FE 공용 제약).\n */\nexport const MIN_DOWNLOAD_SECONDS = 10;\n\n/**\n * 다운로드 요청 길이가 허용되는지 검사한다.\n * `durationSeconds >= MIN_DOWNLOAD_SECONDS`일 때만 true.\n */\nexport function isDownloadDurationAllowed(durationSeconds: number): boolean {\n return (\n Number.isFinite(durationSeconds) && durationSeconds >= MIN_DOWNLOAD_SECONDS\n );\n}\n\nexport interface OriginalDownloadCostOptions {\n isFullVideo?: boolean;\n /**\n * 멤버십 plan. 지정 시 `perPlanKrwPerMinute[plan]`을 단가로,\n * `freeSecondsByPlan[plan]`을 무료 구간으로 적용한다.\n * 미지정(undefined) 시 레거시 단일 단가(`baseCostWon/baseDurationSeconds`) + 무료 구간 없음.\n */\n plan?: TicketCode;\n}\n\nexport function calculateOriginalDownloadCostWon(\n durationSeconds: number,\n options: OriginalDownloadCostOptions = {},\n): number {\n if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return 0;\n\n const { plan, isFullVideo } = options;\n\n // plan 지정 시 앞 freeSeconds 구간은 과금에서 제외하고 초과분만 과금한다.\n // 레거시 경로(plan 미지정)는 무료 구간 0으로 기존 동작을 유지한다.\n const freeSeconds =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.freeSecondsByPlan[plan]\n : 0;\n const billableSeconds = Math.max(0, durationSeconds - freeSeconds);\n if (billableSeconds <= 0) return 0;\n\n const unitPricePerSecond =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.perPlanKrwPerMinute[plan] / 60\n : ORIGINAL_DOWNLOAD_COST_POLICY.baseCostWon /\n ORIGINAL_DOWNLOAD_COST_POLICY.baseDurationSeconds;\n\n const discountMultiplier = isFullVideo\n ? 1 - ORIGINAL_DOWNLOAD_COST_POLICY.fullVideoDiscountRate\n : 1;\n\n // 부동소수점 drift(예: 30.000000000000004) 때문에 1원 과다 청구되는 것을 막기 위해\n // 원 단위 미만 노이즈를 보정한 뒤 올림한다.\n const rawCost = billableSeconds * unitPricePerSecond * discountMultiplier;\n return Math.ceil(Number(rawCost.toFixed(6)));\n}\n","export const GYM_GOLD_CHARGE_POLICY = {\n minChargeKrw: 10_000,\n chargeUnitKrw: 10_000,\n bonusRate: 0.2,\n bonusRateBps: 2_000,\n} as const;\n\nexport interface GymGoldChargeInput {\n /**\n * Actual KRW amount paid through PortOne.\n */\n chargeKrw: number;\n}\n\nexport interface GymGoldChargeResult {\n /**\n * Actual KRW amount paid through PortOne.\n */\n chargeKrw: number;\n /**\n * 1:1 base Gold granted for paid KRW.\n */\n baseGold: number;\n /**\n * Additional bonus Gold granted by charge policy.\n */\n bonusGold: number;\n /**\n * Final Gold amount credited to the gym balance.\n */\n totalGold: number;\n}\n\nexport function calculateGymGoldCharge(\n input: GymGoldChargeInput,\n): GymGoldChargeResult {\n assertValidGymGoldChargeKrw(input.chargeKrw);\n\n const baseGold = input.chargeKrw;\n const bonusGold = calculateBonusGold(input.chargeKrw);\n\n return {\n chargeKrw: input.chargeKrw,\n baseGold,\n bonusGold,\n totalGold: baseGold + bonusGold,\n };\n}\n\nexport function isGymGoldChargeKrwAllowed(chargeKrw: number): boolean {\n return (\n Number.isFinite(chargeKrw) &&\n Number.isInteger(chargeKrw) &&\n chargeKrw >= GYM_GOLD_CHARGE_POLICY.minChargeKrw &&\n chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw === 0\n );\n}\n\nexport function assertValidGymGoldChargeKrw(chargeKrw: number): void {\n if (!Number.isFinite(chargeKrw)) {\n throw new RangeError('chargeKrw must be a finite number');\n }\n\n if (!Number.isInteger(chargeKrw)) {\n throw new RangeError('chargeKrw must be an integer');\n }\n\n if (chargeKrw < GYM_GOLD_CHARGE_POLICY.minChargeKrw) {\n throw new RangeError(\n `chargeKrw must be at least ${GYM_GOLD_CHARGE_POLICY.minChargeKrw}`,\n );\n }\n\n if (chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw !== 0) {\n throw new RangeError(\n `chargeKrw must be a multiple of ${GYM_GOLD_CHARGE_POLICY.chargeUnitKrw}`,\n );\n }\n}\n\nfunction calculateBonusGold(chargeKrw: number): number {\n return Math.floor((chargeKrw * GYM_GOLD_CHARGE_POLICY.bonusRateBps) / 10_000);\n}\n","export const GYM_PARTNER_REGISTRATION_GOLD_POLICY = {\n goldPerDay: 350,\n timeZone: 'Asia/Seoul',\n} as const;\n\nexport interface GymPartnerRegistrationGoldInput {\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n startDate: string;\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n endDate: string;\n}\n\nexport interface GymPartnerRegistrationGoldResult {\n days: number;\n goldAmount: number;\n}\n\nexport interface GymPartnerRegistrationGoldTotalItem\n extends GymPartnerRegistrationGoldInput,\n GymPartnerRegistrationGoldResult {}\n\nexport interface GymPartnerRegistrationGoldTotalResult {\n totalDays: number;\n totalGoldAmount: number;\n items: GymPartnerRegistrationGoldTotalItem[];\n}\n\ninterface KstDateParts {\n year: number;\n month: number;\n day: number;\n}\n\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport function calculateGymPartnerRegistrationGold(\n input: GymPartnerRegistrationGoldInput,\n): GymPartnerRegistrationGoldResult {\n const startDate = parseKstDateString(input.startDate, 'startDate');\n const endDate = parseKstDateString(input.endDate, 'endDate');\n\n const startDateKey = toUtcDateKey(startDate);\n const endDateKey = toUtcDateKey(endDate);\n\n if (endDateKey < startDateKey) {\n throw new RangeError('endDate must be on or after startDate');\n }\n\n const days = Math.floor((endDateKey - startDateKey) / MS_PER_DAY) + 1;\n\n return {\n days,\n goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay,\n };\n}\n\nexport function calculateGymPartnerRegistrationGoldTotal(\n periods: GymPartnerRegistrationGoldInput[],\n): GymPartnerRegistrationGoldTotalResult {\n const items = periods.map((period) => {\n const result = calculateGymPartnerRegistrationGold(period);\n\n return {\n ...period,\n ...result,\n };\n });\n\n return {\n totalDays: items.reduce((sum, item) => sum + item.days, 0),\n totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),\n items,\n };\n}\n\nfunction parseKstDateString(\n input: string,\n fieldName: 'startDate' | 'endDate',\n): KstDateParts {\n const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);\n if (dateOnlyMatch === null) {\n throw new RangeError(`${fieldName} must be a YYYY-MM-DD date`);\n }\n\n const year = Number(dateOnlyMatch[1]);\n const month = Number(dateOnlyMatch[2]);\n const day = Number(dateOnlyMatch[3]);\n const utcDate = new Date(Date.UTC(year, month - 1, day));\n\n if (\n utcDate.getUTCFullYear() !== year ||\n utcDate.getUTCMonth() + 1 !== month ||\n utcDate.getUTCDate() !== day\n ) {\n throw new RangeError(`${fieldName} must be a valid date`);\n }\n\n return { year, month, day };\n}\n\nfunction toUtcDateKey(date: KstDateParts): number {\n return Date.UTC(date.year, date.month - 1, date.day);\n}\n"],"mappings":"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","names":[]}
package/dist/libs.d.cts CHANGED
@@ -47,6 +47,40 @@ interface OriginalDownloadCostOptions {
47
47
  }
48
48
  declare function calculateOriginalDownloadCostWon(durationSeconds: number, options?: OriginalDownloadCostOptions): number;
49
49
 
50
+ declare const GYM_GOLD_CHARGE_POLICY: {
51
+ readonly minChargeKrw: 10000;
52
+ readonly chargeUnitKrw: 10000;
53
+ readonly bonusRate: 0.2;
54
+ readonly bonusRateBps: 2000;
55
+ };
56
+ interface GymGoldChargeInput {
57
+ /**
58
+ * Actual KRW amount paid through PortOne.
59
+ */
60
+ chargeKrw: number;
61
+ }
62
+ interface GymGoldChargeResult {
63
+ /**
64
+ * Actual KRW amount paid through PortOne.
65
+ */
66
+ chargeKrw: number;
67
+ /**
68
+ * 1:1 base Gold granted for paid KRW.
69
+ */
70
+ baseGold: number;
71
+ /**
72
+ * Additional bonus Gold granted by charge policy.
73
+ */
74
+ bonusGold: number;
75
+ /**
76
+ * Final Gold amount credited to the gym balance.
77
+ */
78
+ totalGold: number;
79
+ }
80
+ declare function calculateGymGoldCharge(input: GymGoldChargeInput): GymGoldChargeResult;
81
+ declare function isGymGoldChargeKrwAllowed(chargeKrw: number): boolean;
82
+ declare function assertValidGymGoldChargeKrw(chargeKrw: number): void;
83
+
50
84
  declare const GYM_PARTNER_REGISTRATION_GOLD_POLICY: {
51
85
  readonly goldPerDay: 350;
52
86
  readonly timeZone: "Asia/Seoul";
@@ -75,4 +109,4 @@ interface GymPartnerRegistrationGoldTotalResult {
75
109
  declare function calculateGymPartnerRegistrationGold(input: GymPartnerRegistrationGoldInput): GymPartnerRegistrationGoldResult;
76
110
  declare function calculateGymPartnerRegistrationGoldTotal(periods: GymPartnerRegistrationGoldInput[]): GymPartnerRegistrationGoldTotalResult;
77
111
 
78
- export { GYM_PARTNER_REGISTRATION_GOLD_POLICY, type GymPartnerRegistrationGoldInput, type GymPartnerRegistrationGoldResult, type GymPartnerRegistrationGoldTotalItem, type GymPartnerRegistrationGoldTotalResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, type OriginalDownloadCostOptions, calculateGymPartnerRegistrationGold, calculateGymPartnerRegistrationGoldTotal, calculateOriginalDownloadCostWon, isDownloadDurationAllowed };
112
+ export { GYM_GOLD_CHARGE_POLICY, GYM_PARTNER_REGISTRATION_GOLD_POLICY, type GymGoldChargeInput, type GymGoldChargeResult, type GymPartnerRegistrationGoldInput, type GymPartnerRegistrationGoldResult, type GymPartnerRegistrationGoldTotalItem, type GymPartnerRegistrationGoldTotalResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, type OriginalDownloadCostOptions, assertValidGymGoldChargeKrw, calculateGymGoldCharge, calculateGymPartnerRegistrationGold, calculateGymPartnerRegistrationGoldTotal, calculateOriginalDownloadCostWon, isDownloadDurationAllowed, isGymGoldChargeKrwAllowed };
package/dist/libs.d.ts CHANGED
@@ -47,6 +47,40 @@ interface OriginalDownloadCostOptions {
47
47
  }
48
48
  declare function calculateOriginalDownloadCostWon(durationSeconds: number, options?: OriginalDownloadCostOptions): number;
49
49
 
50
+ declare const GYM_GOLD_CHARGE_POLICY: {
51
+ readonly minChargeKrw: 10000;
52
+ readonly chargeUnitKrw: 10000;
53
+ readonly bonusRate: 0.2;
54
+ readonly bonusRateBps: 2000;
55
+ };
56
+ interface GymGoldChargeInput {
57
+ /**
58
+ * Actual KRW amount paid through PortOne.
59
+ */
60
+ chargeKrw: number;
61
+ }
62
+ interface GymGoldChargeResult {
63
+ /**
64
+ * Actual KRW amount paid through PortOne.
65
+ */
66
+ chargeKrw: number;
67
+ /**
68
+ * 1:1 base Gold granted for paid KRW.
69
+ */
70
+ baseGold: number;
71
+ /**
72
+ * Additional bonus Gold granted by charge policy.
73
+ */
74
+ bonusGold: number;
75
+ /**
76
+ * Final Gold amount credited to the gym balance.
77
+ */
78
+ totalGold: number;
79
+ }
80
+ declare function calculateGymGoldCharge(input: GymGoldChargeInput): GymGoldChargeResult;
81
+ declare function isGymGoldChargeKrwAllowed(chargeKrw: number): boolean;
82
+ declare function assertValidGymGoldChargeKrw(chargeKrw: number): void;
83
+
50
84
  declare const GYM_PARTNER_REGISTRATION_GOLD_POLICY: {
51
85
  readonly goldPerDay: 350;
52
86
  readonly timeZone: "Asia/Seoul";
@@ -75,4 +109,4 @@ interface GymPartnerRegistrationGoldTotalResult {
75
109
  declare function calculateGymPartnerRegistrationGold(input: GymPartnerRegistrationGoldInput): GymPartnerRegistrationGoldResult;
76
110
  declare function calculateGymPartnerRegistrationGoldTotal(periods: GymPartnerRegistrationGoldInput[]): GymPartnerRegistrationGoldTotalResult;
77
111
 
78
- export { GYM_PARTNER_REGISTRATION_GOLD_POLICY, type GymPartnerRegistrationGoldInput, type GymPartnerRegistrationGoldResult, type GymPartnerRegistrationGoldTotalItem, type GymPartnerRegistrationGoldTotalResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, type OriginalDownloadCostOptions, calculateGymPartnerRegistrationGold, calculateGymPartnerRegistrationGoldTotal, calculateOriginalDownloadCostWon, isDownloadDurationAllowed };
112
+ export { GYM_GOLD_CHARGE_POLICY, GYM_PARTNER_REGISTRATION_GOLD_POLICY, type GymGoldChargeInput, type GymGoldChargeResult, type GymPartnerRegistrationGoldInput, type GymPartnerRegistrationGoldResult, type GymPartnerRegistrationGoldTotalItem, type GymPartnerRegistrationGoldTotalResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, type OriginalDownloadCostOptions, assertValidGymGoldChargeKrw, calculateGymGoldCharge, calculateGymPartnerRegistrationGold, calculateGymPartnerRegistrationGoldTotal, calculateOriginalDownloadCostWon, isDownloadDurationAllowed, isGymGoldChargeKrwAllowed };
package/dist/libs.js CHANGED
@@ -41,6 +41,49 @@ function calculateOriginalDownloadCostWon(durationSeconds, options = {}) {
41
41
  return Math.ceil(Number(rawCost.toFixed(6)));
42
42
  }
43
43
 
44
+ // src/libs/gym-gold-charge.ts
45
+ var GYM_GOLD_CHARGE_POLICY = {
46
+ minChargeKrw: 1e4,
47
+ chargeUnitKrw: 1e4,
48
+ bonusRate: 0.2,
49
+ bonusRateBps: 2e3
50
+ };
51
+ function calculateGymGoldCharge(input) {
52
+ assertValidGymGoldChargeKrw(input.chargeKrw);
53
+ const baseGold = input.chargeKrw;
54
+ const bonusGold = calculateBonusGold(input.chargeKrw);
55
+ return {
56
+ chargeKrw: input.chargeKrw,
57
+ baseGold,
58
+ bonusGold,
59
+ totalGold: baseGold + bonusGold
60
+ };
61
+ }
62
+ function isGymGoldChargeKrwAllowed(chargeKrw) {
63
+ return Number.isFinite(chargeKrw) && Number.isInteger(chargeKrw) && chargeKrw >= GYM_GOLD_CHARGE_POLICY.minChargeKrw && chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw === 0;
64
+ }
65
+ function assertValidGymGoldChargeKrw(chargeKrw) {
66
+ if (!Number.isFinite(chargeKrw)) {
67
+ throw new RangeError("chargeKrw must be a finite number");
68
+ }
69
+ if (!Number.isInteger(chargeKrw)) {
70
+ throw new RangeError("chargeKrw must be an integer");
71
+ }
72
+ if (chargeKrw < GYM_GOLD_CHARGE_POLICY.minChargeKrw) {
73
+ throw new RangeError(
74
+ `chargeKrw must be at least ${GYM_GOLD_CHARGE_POLICY.minChargeKrw}`
75
+ );
76
+ }
77
+ if (chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw !== 0) {
78
+ throw new RangeError(
79
+ `chargeKrw must be a multiple of ${GYM_GOLD_CHARGE_POLICY.chargeUnitKrw}`
80
+ );
81
+ }
82
+ }
83
+ function calculateBonusGold(chargeKrw) {
84
+ return Math.floor(chargeKrw * GYM_GOLD_CHARGE_POLICY.bonusRateBps / 1e4);
85
+ }
86
+
44
87
  // src/libs/gym-registration-gold.ts
45
88
  var GYM_PARTNER_REGISTRATION_GOLD_POLICY = {
46
89
  goldPerDay: 350,
@@ -94,12 +137,16 @@ function toUtcDateKey(date) {
94
137
  return Date.UTC(date.year, date.month - 1, date.day);
95
138
  }
96
139
  export {
140
+ GYM_GOLD_CHARGE_POLICY,
97
141
  GYM_PARTNER_REGISTRATION_GOLD_POLICY,
98
142
  MIN_DOWNLOAD_SECONDS,
99
143
  ORIGINAL_DOWNLOAD_COST_POLICY,
144
+ assertValidGymGoldChargeKrw,
145
+ calculateGymGoldCharge,
100
146
  calculateGymPartnerRegistrationGold,
101
147
  calculateGymPartnerRegistrationGoldTotal,
102
148
  calculateOriginalDownloadCostWon,
103
- isDownloadDurationAllowed
149
+ isDownloadDurationAllowed,
150
+ isGymGoldChargeKrwAllowed
104
151
  };
105
152
  //# sourceMappingURL=libs.js.map
package/dist/libs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/libs/cost.ts","../src/libs/gym-registration-gold.ts"],"sourcesContent":["import type { TicketCode } from '../types/membership';\n\nexport const ORIGINAL_DOWNLOAD_COST_POLICY = {\n baseDurationSeconds: 600,\n baseCostWon: 2000,\n fullVideoDiscountRate: 0.1,\n /**\n * v14 신정책: 멤버십 plan별 분당 KRW 요율.\n * `calculateOriginalDownloadCostWon(duration, { plan })`로 호출 시 적용된다.\n * plan 미지정 시 `baseCostWon/baseDurationSeconds` 기반 단일 단가(레거시 200원/분)로 폴백 — 기존 호출자 호환.\n */\n perPlanKrwPerMinute: {\n FREE: 100,\n PLUS: 50,\n PRO: 25,\n },\n /**\n * 멤버십 plan별 무료 다운로드 구간(초).\n * 다운로드 길이 중 앞 `freeSecondsByPlan[plan]`초는 과금에서 제외하고,\n * 이를 초과한 구간에만 `perPlanKrwPerMinute[plan]` 요율을 적용한다 (초과분 과금).\n * plan 미지정(레거시 단일 단가) 경로에는 무료 구간을 적용하지 않는다.\n */\n freeSecondsByPlan: {\n FREE: 10,\n PLUS: 20,\n PRO: 30,\n },\n} as const;\n\n/**\n * 다운로드 가능한 최소 길이(초).\n * 이보다 짧은 구간은 다운로드 요청 자체를 허용하지 않는다 (server·FE 공용 제약).\n */\nexport const MIN_DOWNLOAD_SECONDS = 10;\n\n/**\n * 다운로드 요청 길이가 허용되는지 검사한다.\n * `durationSeconds >= MIN_DOWNLOAD_SECONDS`일 때만 true.\n */\nexport function isDownloadDurationAllowed(durationSeconds: number): boolean {\n return (\n Number.isFinite(durationSeconds) && durationSeconds >= MIN_DOWNLOAD_SECONDS\n );\n}\n\nexport interface OriginalDownloadCostOptions {\n isFullVideo?: boolean;\n /**\n * 멤버십 plan. 지정 시 `perPlanKrwPerMinute[plan]`을 단가로,\n * `freeSecondsByPlan[plan]`을 무료 구간으로 적용한다.\n * 미지정(undefined) 시 레거시 단일 단가(`baseCostWon/baseDurationSeconds`) + 무료 구간 없음.\n */\n plan?: TicketCode;\n}\n\nexport function calculateOriginalDownloadCostWon(\n durationSeconds: number,\n options: OriginalDownloadCostOptions = {},\n): number {\n if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return 0;\n\n const { plan, isFullVideo } = options;\n\n // plan 지정 시 앞 freeSeconds 구간은 과금에서 제외하고 초과분만 과금한다.\n // 레거시 경로(plan 미지정)는 무료 구간 0으로 기존 동작을 유지한다.\n const freeSeconds =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.freeSecondsByPlan[plan]\n : 0;\n const billableSeconds = Math.max(0, durationSeconds - freeSeconds);\n if (billableSeconds <= 0) return 0;\n\n const unitPricePerSecond =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.perPlanKrwPerMinute[plan] / 60\n : ORIGINAL_DOWNLOAD_COST_POLICY.baseCostWon /\n ORIGINAL_DOWNLOAD_COST_POLICY.baseDurationSeconds;\n\n const discountMultiplier = isFullVideo\n ? 1 - ORIGINAL_DOWNLOAD_COST_POLICY.fullVideoDiscountRate\n : 1;\n\n // 부동소수점 drift(예: 30.000000000000004) 때문에 1원 과다 청구되는 것을 막기 위해\n // 원 단위 미만 노이즈를 보정한 뒤 올림한다.\n const rawCost = billableSeconds * unitPricePerSecond * discountMultiplier;\n return Math.ceil(Number(rawCost.toFixed(6)));\n}\n","export const GYM_PARTNER_REGISTRATION_GOLD_POLICY = {\n goldPerDay: 350,\n timeZone: 'Asia/Seoul',\n} as const;\n\nexport interface GymPartnerRegistrationGoldInput {\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n startDate: string;\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n endDate: string;\n}\n\nexport interface GymPartnerRegistrationGoldResult {\n days: number;\n goldAmount: number;\n}\n\nexport interface GymPartnerRegistrationGoldTotalItem\n extends GymPartnerRegistrationGoldInput,\n GymPartnerRegistrationGoldResult {}\n\nexport interface GymPartnerRegistrationGoldTotalResult {\n totalDays: number;\n totalGoldAmount: number;\n items: GymPartnerRegistrationGoldTotalItem[];\n}\n\ninterface KstDateParts {\n year: number;\n month: number;\n day: number;\n}\n\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport function calculateGymPartnerRegistrationGold(\n input: GymPartnerRegistrationGoldInput,\n): GymPartnerRegistrationGoldResult {\n const startDate = parseKstDateString(input.startDate, 'startDate');\n const endDate = parseKstDateString(input.endDate, 'endDate');\n\n const startDateKey = toUtcDateKey(startDate);\n const endDateKey = toUtcDateKey(endDate);\n\n if (endDateKey < startDateKey) {\n throw new RangeError('endDate must be on or after startDate');\n }\n\n const days = Math.floor((endDateKey - startDateKey) / MS_PER_DAY) + 1;\n\n return {\n days,\n goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay,\n };\n}\n\nexport function calculateGymPartnerRegistrationGoldTotal(\n periods: GymPartnerRegistrationGoldInput[],\n): GymPartnerRegistrationGoldTotalResult {\n const items = periods.map((period) => {\n const result = calculateGymPartnerRegistrationGold(period);\n\n return {\n ...period,\n ...result,\n };\n });\n\n return {\n totalDays: items.reduce((sum, item) => sum + item.days, 0),\n totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),\n items,\n };\n}\n\nfunction parseKstDateString(\n input: string,\n fieldName: 'startDate' | 'endDate',\n): KstDateParts {\n const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);\n if (dateOnlyMatch === null) {\n throw new RangeError(`${fieldName} must be a YYYY-MM-DD date`);\n }\n\n const year = Number(dateOnlyMatch[1]);\n const month = Number(dateOnlyMatch[2]);\n const day = Number(dateOnlyMatch[3]);\n const utcDate = new Date(Date.UTC(year, month - 1, day));\n\n if (\n utcDate.getUTCFullYear() !== year ||\n utcDate.getUTCMonth() + 1 !== month ||\n utcDate.getUTCDate() !== day\n ) {\n throw new RangeError(`${fieldName} must be a valid date`);\n }\n\n return { year, month, day };\n}\n\nfunction toUtcDateKey(date: KstDateParts): number {\n return Date.UTC(date.year, date.month - 1, date.day);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/libs/cost.ts","../src/libs/gym-gold-charge.ts","../src/libs/gym-registration-gold.ts"],"sourcesContent":["import type { TicketCode } from '../types/membership';\n\nexport const ORIGINAL_DOWNLOAD_COST_POLICY = {\n baseDurationSeconds: 600,\n baseCostWon: 2000,\n fullVideoDiscountRate: 0.1,\n /**\n * v14 신정책: 멤버십 plan별 분당 KRW 요율.\n * `calculateOriginalDownloadCostWon(duration, { plan })`로 호출 시 적용된다.\n * plan 미지정 시 `baseCostWon/baseDurationSeconds` 기반 단일 단가(레거시 200원/분)로 폴백 — 기존 호출자 호환.\n */\n perPlanKrwPerMinute: {\n FREE: 100,\n PLUS: 50,\n PRO: 25,\n },\n /**\n * 멤버십 plan별 무료 다운로드 구간(초).\n * 다운로드 길이 중 앞 `freeSecondsByPlan[plan]`초는 과금에서 제외하고,\n * 이를 초과한 구간에만 `perPlanKrwPerMinute[plan]` 요율을 적용한다 (초과분 과금).\n * plan 미지정(레거시 단일 단가) 경로에는 무료 구간을 적용하지 않는다.\n */\n freeSecondsByPlan: {\n FREE: 10,\n PLUS: 20,\n PRO: 30,\n },\n} as const;\n\n/**\n * 다운로드 가능한 최소 길이(초).\n * 이보다 짧은 구간은 다운로드 요청 자체를 허용하지 않는다 (server·FE 공용 제약).\n */\nexport const MIN_DOWNLOAD_SECONDS = 10;\n\n/**\n * 다운로드 요청 길이가 허용되는지 검사한다.\n * `durationSeconds >= MIN_DOWNLOAD_SECONDS`일 때만 true.\n */\nexport function isDownloadDurationAllowed(durationSeconds: number): boolean {\n return (\n Number.isFinite(durationSeconds) && durationSeconds >= MIN_DOWNLOAD_SECONDS\n );\n}\n\nexport interface OriginalDownloadCostOptions {\n isFullVideo?: boolean;\n /**\n * 멤버십 plan. 지정 시 `perPlanKrwPerMinute[plan]`을 단가로,\n * `freeSecondsByPlan[plan]`을 무료 구간으로 적용한다.\n * 미지정(undefined) 시 레거시 단일 단가(`baseCostWon/baseDurationSeconds`) + 무료 구간 없음.\n */\n plan?: TicketCode;\n}\n\nexport function calculateOriginalDownloadCostWon(\n durationSeconds: number,\n options: OriginalDownloadCostOptions = {},\n): number {\n if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return 0;\n\n const { plan, isFullVideo } = options;\n\n // plan 지정 시 앞 freeSeconds 구간은 과금에서 제외하고 초과분만 과금한다.\n // 레거시 경로(plan 미지정)는 무료 구간 0으로 기존 동작을 유지한다.\n const freeSeconds =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.freeSecondsByPlan[plan]\n : 0;\n const billableSeconds = Math.max(0, durationSeconds - freeSeconds);\n if (billableSeconds <= 0) return 0;\n\n const unitPricePerSecond =\n plan !== undefined\n ? ORIGINAL_DOWNLOAD_COST_POLICY.perPlanKrwPerMinute[plan] / 60\n : ORIGINAL_DOWNLOAD_COST_POLICY.baseCostWon /\n ORIGINAL_DOWNLOAD_COST_POLICY.baseDurationSeconds;\n\n const discountMultiplier = isFullVideo\n ? 1 - ORIGINAL_DOWNLOAD_COST_POLICY.fullVideoDiscountRate\n : 1;\n\n // 부동소수점 drift(예: 30.000000000000004) 때문에 1원 과다 청구되는 것을 막기 위해\n // 원 단위 미만 노이즈를 보정한 뒤 올림한다.\n const rawCost = billableSeconds * unitPricePerSecond * discountMultiplier;\n return Math.ceil(Number(rawCost.toFixed(6)));\n}\n","export const GYM_GOLD_CHARGE_POLICY = {\n minChargeKrw: 10_000,\n chargeUnitKrw: 10_000,\n bonusRate: 0.2,\n bonusRateBps: 2_000,\n} as const;\n\nexport interface GymGoldChargeInput {\n /**\n * Actual KRW amount paid through PortOne.\n */\n chargeKrw: number;\n}\n\nexport interface GymGoldChargeResult {\n /**\n * Actual KRW amount paid through PortOne.\n */\n chargeKrw: number;\n /**\n * 1:1 base Gold granted for paid KRW.\n */\n baseGold: number;\n /**\n * Additional bonus Gold granted by charge policy.\n */\n bonusGold: number;\n /**\n * Final Gold amount credited to the gym balance.\n */\n totalGold: number;\n}\n\nexport function calculateGymGoldCharge(\n input: GymGoldChargeInput,\n): GymGoldChargeResult {\n assertValidGymGoldChargeKrw(input.chargeKrw);\n\n const baseGold = input.chargeKrw;\n const bonusGold = calculateBonusGold(input.chargeKrw);\n\n return {\n chargeKrw: input.chargeKrw,\n baseGold,\n bonusGold,\n totalGold: baseGold + bonusGold,\n };\n}\n\nexport function isGymGoldChargeKrwAllowed(chargeKrw: number): boolean {\n return (\n Number.isFinite(chargeKrw) &&\n Number.isInteger(chargeKrw) &&\n chargeKrw >= GYM_GOLD_CHARGE_POLICY.minChargeKrw &&\n chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw === 0\n );\n}\n\nexport function assertValidGymGoldChargeKrw(chargeKrw: number): void {\n if (!Number.isFinite(chargeKrw)) {\n throw new RangeError('chargeKrw must be a finite number');\n }\n\n if (!Number.isInteger(chargeKrw)) {\n throw new RangeError('chargeKrw must be an integer');\n }\n\n if (chargeKrw < GYM_GOLD_CHARGE_POLICY.minChargeKrw) {\n throw new RangeError(\n `chargeKrw must be at least ${GYM_GOLD_CHARGE_POLICY.minChargeKrw}`,\n );\n }\n\n if (chargeKrw % GYM_GOLD_CHARGE_POLICY.chargeUnitKrw !== 0) {\n throw new RangeError(\n `chargeKrw must be a multiple of ${GYM_GOLD_CHARGE_POLICY.chargeUnitKrw}`,\n );\n }\n}\n\nfunction calculateBonusGold(chargeKrw: number): number {\n return Math.floor((chargeKrw * GYM_GOLD_CHARGE_POLICY.bonusRateBps) / 10_000);\n}\n","export const GYM_PARTNER_REGISTRATION_GOLD_POLICY = {\n goldPerDay: 350,\n timeZone: 'Asia/Seoul',\n} as const;\n\nexport interface GymPartnerRegistrationGoldInput {\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n startDate: string;\n /**\n * KST calendar date in YYYY-MM-DD format.\n */\n endDate: string;\n}\n\nexport interface GymPartnerRegistrationGoldResult {\n days: number;\n goldAmount: number;\n}\n\nexport interface GymPartnerRegistrationGoldTotalItem\n extends GymPartnerRegistrationGoldInput,\n GymPartnerRegistrationGoldResult {}\n\nexport interface GymPartnerRegistrationGoldTotalResult {\n totalDays: number;\n totalGoldAmount: number;\n items: GymPartnerRegistrationGoldTotalItem[];\n}\n\ninterface KstDateParts {\n year: number;\n month: number;\n day: number;\n}\n\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\nexport function calculateGymPartnerRegistrationGold(\n input: GymPartnerRegistrationGoldInput,\n): GymPartnerRegistrationGoldResult {\n const startDate = parseKstDateString(input.startDate, 'startDate');\n const endDate = parseKstDateString(input.endDate, 'endDate');\n\n const startDateKey = toUtcDateKey(startDate);\n const endDateKey = toUtcDateKey(endDate);\n\n if (endDateKey < startDateKey) {\n throw new RangeError('endDate must be on or after startDate');\n }\n\n const days = Math.floor((endDateKey - startDateKey) / MS_PER_DAY) + 1;\n\n return {\n days,\n goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay,\n };\n}\n\nexport function calculateGymPartnerRegistrationGoldTotal(\n periods: GymPartnerRegistrationGoldInput[],\n): GymPartnerRegistrationGoldTotalResult {\n const items = periods.map((period) => {\n const result = calculateGymPartnerRegistrationGold(period);\n\n return {\n ...period,\n ...result,\n };\n });\n\n return {\n totalDays: items.reduce((sum, item) => sum + item.days, 0),\n totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),\n items,\n };\n}\n\nfunction parseKstDateString(\n input: string,\n fieldName: 'startDate' | 'endDate',\n): KstDateParts {\n const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);\n if (dateOnlyMatch === null) {\n throw new RangeError(`${fieldName} must be a YYYY-MM-DD date`);\n }\n\n const year = Number(dateOnlyMatch[1]);\n const month = Number(dateOnlyMatch[2]);\n const day = Number(dateOnlyMatch[3]);\n const utcDate = new Date(Date.UTC(year, month - 1, day));\n\n if (\n utcDate.getUTCFullYear() !== year ||\n utcDate.getUTCMonth() + 1 !== month ||\n utcDate.getUTCDate() !== day\n ) {\n throw new RangeError(`${fieldName} must be a valid date`);\n }\n\n return { year, month, day };\n}\n\nfunction toUtcDateKey(date: KstDateParts): number {\n return Date.UTC(date.year, date.month - 1, date.day);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoclip-kit",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "description": "TypeScript utility library for internal use",
6
6
  "files": [