spoclip-kit 2.5.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,14 +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,
30
+ calculateGymPartnerRegistrationGoldTotal: () => calculateGymPartnerRegistrationGoldTotal,
27
31
  calculateOriginalDownloadCostWon: () => calculateOriginalDownloadCostWon,
28
32
  color: () => color,
29
33
  colorV2: () => colorV2,
30
34
  isDownloadDurationAllowed: () => isDownloadDurationAllowed,
35
+ isGymGoldChargeKrwAllowed: () => isGymGoldChargeKrwAllowed,
31
36
  mobileTypo: () => mobileTypo,
32
37
  typo: () => typo
33
38
  });
@@ -76,6 +81,49 @@ function calculateOriginalDownloadCostWon(durationSeconds, options = {}) {
76
81
  return Math.ceil(Number(rawCost.toFixed(6)));
77
82
  }
78
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
+
79
127
  // src/libs/gym-registration-gold.ts
80
128
  var GYM_PARTNER_REGISTRATION_GOLD_POLICY = {
81
129
  goldPerDay: 350,
@@ -97,6 +145,20 @@ function calculateGymPartnerRegistrationGold(input) {
97
145
  goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay
98
146
  };
99
147
  }
148
+ function calculateGymPartnerRegistrationGoldTotal(periods) {
149
+ const items = periods.map((period) => {
150
+ const result = calculateGymPartnerRegistrationGold(period);
151
+ return {
152
+ ...period,
153
+ ...result
154
+ };
155
+ });
156
+ return {
157
+ totalDays: items.reduce((sum, item) => sum + item.days, 0),
158
+ totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),
159
+ items
160
+ };
161
+ }
100
162
  function parseKstDateString(input, fieldName) {
101
163
  const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);
102
164
  if (dateOnlyMatch === null) {
@@ -360,14 +422,19 @@ var mobileTypo = {
360
422
  };
361
423
  // Annotate the CommonJS export names for ESM import in node:
362
424
  0 && (module.exports = {
425
+ GYM_GOLD_CHARGE_POLICY,
363
426
  GYM_PARTNER_REGISTRATION_GOLD_POLICY,
364
427
  MIN_DOWNLOAD_SECONDS,
365
428
  ORIGINAL_DOWNLOAD_COST_POLICY,
429
+ assertValidGymGoldChargeKrw,
430
+ calculateGymGoldCharge,
366
431
  calculateGymPartnerRegistrationGold,
432
+ calculateGymPartnerRegistrationGoldTotal,
367
433
  calculateOriginalDownloadCostWon,
368
434
  color,
369
435
  colorV2,
370
436
  isDownloadDurationAllowed,
437
+ isGymGoldChargeKrwAllowed,
371
438
  mobileTypo,
372
439
  typo
373
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\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\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, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, OriginalDownloadCostOptions, calculateGymPartnerRegistrationGold, 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, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, OriginalDownloadCostOptions, calculateGymPartnerRegistrationGold, 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,
@@ -62,6 +105,20 @@ function calculateGymPartnerRegistrationGold(input) {
62
105
  goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay
63
106
  };
64
107
  }
108
+ function calculateGymPartnerRegistrationGoldTotal(periods) {
109
+ const items = periods.map((period) => {
110
+ const result = calculateGymPartnerRegistrationGold(period);
111
+ return {
112
+ ...period,
113
+ ...result
114
+ };
115
+ });
116
+ return {
117
+ totalDays: items.reduce((sum, item) => sum + item.days, 0),
118
+ totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),
119
+ items
120
+ };
121
+ }
65
122
  function parseKstDateString(input, fieldName) {
66
123
  const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);
67
124
  if (dateOnlyMatch === null) {
@@ -324,14 +381,19 @@ var mobileTypo = {
324
381
  }
325
382
  };
326
383
  export {
384
+ GYM_GOLD_CHARGE_POLICY,
327
385
  GYM_PARTNER_REGISTRATION_GOLD_POLICY,
328
386
  MIN_DOWNLOAD_SECONDS,
329
387
  ORIGINAL_DOWNLOAD_COST_POLICY,
388
+ assertValidGymGoldChargeKrw,
389
+ calculateGymGoldCharge,
330
390
  calculateGymPartnerRegistrationGold,
391
+ calculateGymPartnerRegistrationGoldTotal,
331
392
  calculateOriginalDownloadCostWon,
332
393
  color,
333
394
  colorV2,
334
395
  isDownloadDurationAllowed,
396
+ isGymGoldChargeKrwAllowed,
335
397
  mobileTypo,
336
398
  typo
337
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\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\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,12 +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,
30
+ calculateGymPartnerRegistrationGoldTotal: () => calculateGymPartnerRegistrationGoldTotal,
27
31
  calculateOriginalDownloadCostWon: () => calculateOriginalDownloadCostWon,
28
- isDownloadDurationAllowed: () => isDownloadDurationAllowed
32
+ isDownloadDurationAllowed: () => isDownloadDurationAllowed,
33
+ isGymGoldChargeKrwAllowed: () => isGymGoldChargeKrwAllowed
29
34
  });
30
35
  module.exports = __toCommonJS(libs_exports);
31
36
 
@@ -72,6 +77,49 @@ function calculateOriginalDownloadCostWon(durationSeconds, options = {}) {
72
77
  return Math.ceil(Number(rawCost.toFixed(6)));
73
78
  }
74
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
+
75
123
  // src/libs/gym-registration-gold.ts
76
124
  var GYM_PARTNER_REGISTRATION_GOLD_POLICY = {
77
125
  goldPerDay: 350,
@@ -93,6 +141,20 @@ function calculateGymPartnerRegistrationGold(input) {
93
141
  goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay
94
142
  };
95
143
  }
144
+ function calculateGymPartnerRegistrationGoldTotal(periods) {
145
+ const items = periods.map((period) => {
146
+ const result = calculateGymPartnerRegistrationGold(period);
147
+ return {
148
+ ...period,
149
+ ...result
150
+ };
151
+ });
152
+ return {
153
+ totalDays: items.reduce((sum, item) => sum + item.days, 0),
154
+ totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),
155
+ items
156
+ };
157
+ }
96
158
  function parseKstDateString(input, fieldName) {
97
159
  const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);
98
160
  if (dateOnlyMatch === null) {
@@ -112,11 +174,16 @@ function toUtcDateKey(date) {
112
174
  }
113
175
  // Annotate the CommonJS export names for ESM import in node:
114
176
  0 && (module.exports = {
177
+ GYM_GOLD_CHARGE_POLICY,
115
178
  GYM_PARTNER_REGISTRATION_GOLD_POLICY,
116
179
  MIN_DOWNLOAD_SECONDS,
117
180
  ORIGINAL_DOWNLOAD_COST_POLICY,
181
+ assertValidGymGoldChargeKrw,
182
+ calculateGymGoldCharge,
118
183
  calculateGymPartnerRegistrationGold,
184
+ calculateGymPartnerRegistrationGoldTotal,
119
185
  calculateOriginalDownloadCostWon,
120
- isDownloadDurationAllowed
186
+ isDownloadDurationAllowed,
187
+ isGymGoldChargeKrwAllowed
121
188
  });
122
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\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\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEO,IAAM,gCAAgC;AAAA,EAC3C,qBAAqB;AAAA,EACrB,aAAa;AAAA,EACb,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMvB,qBAAqB;AAAA,IACnB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,mBAAmB;AAAA,IACjB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AACF;AAMO,IAAM,uBAAuB;AAM7B,SAAS,0BAA0B,iBAAkC;AAC1E,SACE,OAAO,SAAS,eAAe,KAAK,mBAAmB;AAE3D;AAYO,SAAS,iCACd,iBACA,UAAuC,CAAC,GAChC;AACR,MAAI,CAAC,OAAO,SAAS,eAAe,KAAK,mBAAmB,EAAG,QAAO;AAEtE,QAAM,EAAE,MAAM,YAAY,IAAI;AAI9B,QAAM,cACJ,SAAS,SACL,8BAA8B,kBAAkB,IAAI,IACpD;AACN,QAAM,kBAAkB,KAAK,IAAI,GAAG,kBAAkB,WAAW;AACjE,MAAI,mBAAmB,EAAG,QAAO;AAEjC,QAAM,qBACJ,SAAS,SACL,8BAA8B,oBAAoB,IAAI,IAAI,KAC1D,8BAA8B,cAC9B,8BAA8B;AAEpC,QAAM,qBAAqB,cACvB,IAAI,8BAA8B,wBAClC;AAIJ,QAAM,UAAU,kBAAkB,qBAAqB;AACvD,SAAO,KAAK,KAAK,OAAO,QAAQ,QAAQ,CAAC,CAAC,CAAC;AAC7C;;;ACtFO,IAAM,uCAAuC;AAAA,EAClD,YAAY;AAAA,EACZ,UAAU;AACZ;AAwBA,IAAM,oBAAoB;AAC1B,IAAM,aAAa,KAAK,KAAK,KAAK;AAE3B,SAAS,oCACd,OACkC;AAClC,QAAM,YAAY,mBAAmB,MAAM,WAAW,WAAW;AACjE,QAAM,UAAU,mBAAmB,MAAM,SAAS,SAAS;AAE3D,QAAM,eAAe,aAAa,SAAS;AAC3C,QAAM,aAAa,aAAa,OAAO;AAEvC,MAAI,aAAa,cAAc;AAC7B,UAAM,IAAI,WAAW,uCAAuC;AAAA,EAC9D;AAEA,QAAM,OAAO,KAAK,OAAO,aAAa,gBAAgB,UAAU,IAAI;AAEpE,SAAO;AAAA,IACL;AAAA,IACA,YAAY,OAAO,qCAAqC;AAAA,EAC1D;AACF;AAEA,SAAS,mBACP,OACA,WACc;AACd,QAAM,gBAAgB,kBAAkB,KAAK,KAAK;AAClD,MAAI,kBAAkB,MAAM;AAC1B,UAAM,IAAI,WAAW,GAAG,SAAS,4BAA4B;AAAA,EAC/D;AAEA,QAAM,OAAO,OAAO,cAAc,CAAC,CAAC;AACpC,QAAM,QAAQ,OAAO,cAAc,CAAC,CAAC;AACrC,QAAM,MAAM,OAAO,cAAc,CAAC,CAAC;AACnC,QAAM,UAAU,IAAI,KAAK,KAAK,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;AAEvD,MACE,QAAQ,eAAe,MAAM,QAC7B,QAAQ,YAAY,IAAI,MAAM,SAC9B,QAAQ,WAAW,MAAM,KACzB;AACA,UAAM,IAAI,WAAW,GAAG,SAAS,uBAAuB;AAAA,EAC1D;AAEA,SAAO,EAAE,MAAM,OAAO,IAAI;AAC5B;AAEA,SAAS,aAAa,MAA4B;AAChD,SAAO,KAAK,IAAI,KAAK,MAAM,KAAK,QAAQ,GAAG,KAAK,GAAG;AACrD;","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";
@@ -65,6 +99,14 @@ interface GymPartnerRegistrationGoldResult {
65
99
  days: number;
66
100
  goldAmount: number;
67
101
  }
102
+ interface GymPartnerRegistrationGoldTotalItem extends GymPartnerRegistrationGoldInput, GymPartnerRegistrationGoldResult {
103
+ }
104
+ interface GymPartnerRegistrationGoldTotalResult {
105
+ totalDays: number;
106
+ totalGoldAmount: number;
107
+ items: GymPartnerRegistrationGoldTotalItem[];
108
+ }
68
109
  declare function calculateGymPartnerRegistrationGold(input: GymPartnerRegistrationGoldInput): GymPartnerRegistrationGoldResult;
110
+ declare function calculateGymPartnerRegistrationGoldTotal(periods: GymPartnerRegistrationGoldInput[]): GymPartnerRegistrationGoldTotalResult;
69
111
 
70
- export { GYM_PARTNER_REGISTRATION_GOLD_POLICY, type GymPartnerRegistrationGoldInput, type GymPartnerRegistrationGoldResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, type OriginalDownloadCostOptions, calculateGymPartnerRegistrationGold, 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";
@@ -65,6 +99,14 @@ interface GymPartnerRegistrationGoldResult {
65
99
  days: number;
66
100
  goldAmount: number;
67
101
  }
102
+ interface GymPartnerRegistrationGoldTotalItem extends GymPartnerRegistrationGoldInput, GymPartnerRegistrationGoldResult {
103
+ }
104
+ interface GymPartnerRegistrationGoldTotalResult {
105
+ totalDays: number;
106
+ totalGoldAmount: number;
107
+ items: GymPartnerRegistrationGoldTotalItem[];
108
+ }
68
109
  declare function calculateGymPartnerRegistrationGold(input: GymPartnerRegistrationGoldInput): GymPartnerRegistrationGoldResult;
110
+ declare function calculateGymPartnerRegistrationGoldTotal(periods: GymPartnerRegistrationGoldInput[]): GymPartnerRegistrationGoldTotalResult;
69
111
 
70
- export { GYM_PARTNER_REGISTRATION_GOLD_POLICY, type GymPartnerRegistrationGoldInput, type GymPartnerRegistrationGoldResult, MIN_DOWNLOAD_SECONDS, ORIGINAL_DOWNLOAD_COST_POLICY, type OriginalDownloadCostOptions, calculateGymPartnerRegistrationGold, 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,
@@ -62,6 +105,20 @@ function calculateGymPartnerRegistrationGold(input) {
62
105
  goldAmount: days * GYM_PARTNER_REGISTRATION_GOLD_POLICY.goldPerDay
63
106
  };
64
107
  }
108
+ function calculateGymPartnerRegistrationGoldTotal(periods) {
109
+ const items = periods.map((period) => {
110
+ const result = calculateGymPartnerRegistrationGold(period);
111
+ return {
112
+ ...period,
113
+ ...result
114
+ };
115
+ });
116
+ return {
117
+ totalDays: items.reduce((sum, item) => sum + item.days, 0),
118
+ totalGoldAmount: items.reduce((sum, item) => sum + item.goldAmount, 0),
119
+ items
120
+ };
121
+ }
65
122
  function parseKstDateString(input, fieldName) {
66
123
  const dateOnlyMatch = DATE_ONLY_PATTERN.exec(input);
67
124
  if (dateOnlyMatch === null) {
@@ -80,11 +137,16 @@ function toUtcDateKey(date) {
80
137
  return Date.UTC(date.year, date.month - 1, date.day);
81
138
  }
82
139
  export {
140
+ GYM_GOLD_CHARGE_POLICY,
83
141
  GYM_PARTNER_REGISTRATION_GOLD_POLICY,
84
142
  MIN_DOWNLOAD_SECONDS,
85
143
  ORIGINAL_DOWNLOAD_COST_POLICY,
144
+ assertValidGymGoldChargeKrw,
145
+ calculateGymGoldCharge,
86
146
  calculateGymPartnerRegistrationGold,
147
+ calculateGymPartnerRegistrationGoldTotal,
87
148
  calculateOriginalDownloadCostWon,
88
- isDownloadDurationAllowed
149
+ isDownloadDurationAllowed,
150
+ isGymGoldChargeKrwAllowed
89
151
  };
90
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\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\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":";AAEO,IAAM,gCAAgC;AAAA,EAC3C,qBAAqB;AAAA,EACrB,aAAa;AAAA,EACb,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMvB,qBAAqB;AAAA,IACnB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,mBAAmB;AAAA,IACjB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AACF;AAMO,IAAM,uBAAuB;AAM7B,SAAS,0BAA0B,iBAAkC;AAC1E,SACE,OAAO,SAAS,eAAe,KAAK,mBAAmB;AAE3D;AAYO,SAAS,iCACd,iBACA,UAAuC,CAAC,GAChC;AACR,MAAI,CAAC,OAAO,SAAS,eAAe,KAAK,mBAAmB,EAAG,QAAO;AAEtE,QAAM,EAAE,MAAM,YAAY,IAAI;AAI9B,QAAM,cACJ,SAAS,SACL,8BAA8B,kBAAkB,IAAI,IACpD;AACN,QAAM,kBAAkB,KAAK,IAAI,GAAG,kBAAkB,WAAW;AACjE,MAAI,mBAAmB,EAAG,QAAO;AAEjC,QAAM,qBACJ,SAAS,SACL,8BAA8B,oBAAoB,IAAI,IAAI,KAC1D,8BAA8B,cAC9B,8BAA8B;AAEpC,QAAM,qBAAqB,cACvB,IAAI,8BAA8B,wBAClC;AAIJ,QAAM,UAAU,kBAAkB,qBAAqB;AACvD,SAAO,KAAK,KAAK,OAAO,QAAQ,QAAQ,CAAC,CAAC,CAAC;AAC7C;;;ACtFO,IAAM,uCAAuC;AAAA,EAClD,YAAY;AAAA,EACZ,UAAU;AACZ;AAwBA,IAAM,oBAAoB;AAC1B,IAAM,aAAa,KAAK,KAAK,KAAK;AAE3B,SAAS,oCACd,OACkC;AAClC,QAAM,YAAY,mBAAmB,MAAM,WAAW,WAAW;AACjE,QAAM,UAAU,mBAAmB,MAAM,SAAS,SAAS;AAE3D,QAAM,eAAe,aAAa,SAAS;AAC3C,QAAM,aAAa,aAAa,OAAO;AAEvC,MAAI,aAAa,cAAc;AAC7B,UAAM,IAAI,WAAW,uCAAuC;AAAA,EAC9D;AAEA,QAAM,OAAO,KAAK,OAAO,aAAa,gBAAgB,UAAU,IAAI;AAEpE,SAAO;AAAA,IACL;AAAA,IACA,YAAY,OAAO,qCAAqC;AAAA,EAC1D;AACF;AAEA,SAAS,mBACP,OACA,WACc;AACd,QAAM,gBAAgB,kBAAkB,KAAK,KAAK;AAClD,MAAI,kBAAkB,MAAM;AAC1B,UAAM,IAAI,WAAW,GAAG,SAAS,4BAA4B;AAAA,EAC/D;AAEA,QAAM,OAAO,OAAO,cAAc,CAAC,CAAC;AACpC,QAAM,QAAQ,OAAO,cAAc,CAAC,CAAC;AACrC,QAAM,MAAM,OAAO,cAAc,CAAC,CAAC;AACnC,QAAM,UAAU,IAAI,KAAK,KAAK,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;AAEvD,MACE,QAAQ,eAAe,MAAM,QAC7B,QAAQ,YAAY,IAAI,MAAM,SAC9B,QAAQ,WAAW,MAAM,KACzB;AACA,UAAM,IAAI,WAAW,GAAG,SAAS,uBAAuB;AAAA,EAC1D;AAEA,SAAO,EAAE,MAAM,OAAO,IAAI;AAC5B;AAEA,SAAS,aAAa,MAA4B;AAChD,SAAO,KAAK,IAAI,KAAK,MAAM,KAAK,QAAQ,GAAG,KAAK,GAAG;AACrD;","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.5.0",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "description": "TypeScript utility library for internal use",
6
6
  "files": [