기술 참조 및 공식

완전한 수학적 구현

구현 가이드

이 페이지는 모든 Swim Analytics 지표에 대한 복사-붙여넣기 가능한 공식과 단계별 계산 방법을 제공합니다. 맞춤형 구현, 검증 또는 더 깊은 이해를 위해 사용하세요.

⚠️ 구현 참고 사항

  • 계산을 위해 모든 시간은 초 단위로 변환해야 합니다
  • 수영 페이스는 역수입니다 (높은 % = 느린 페이스)
  • 항상 합리적인 범위 내에서 입력을 검증하세요
  • 예외 상황을 처리하세요 (0으로 나누기, 음수 값)

수동 계산을 피하고 싶으십니까? 우리의 글을 읽어보세요훈련 부하 가이드또는 무료 Swim Analytics 앱을 사용하세요.

핵심 성과 지표 (Core Performance Metrics)

임계 수영 속도 (CSS, Critical Swim Speed)

공식:

CSS (m/s) = (D₂ - D₁) / (T₂ - T₁)
CSS 페이스/100m (초) = (T₄₀₀ - T₂₀₀) / 2

🧪 대화형 계산기 - 공식 테스트

CSS 페이스 (100m 당):
1:49
계산 단계:
CSS (m/s) = (400 - 200) / (368 - 150) = 0.917 m/s
페이스/100m = 100 / 0.917 = 109 초 = 1:49

JavaScript 구현:

function calculateCSS(distance1, time1, distance2, time2) {
  // 필요한 경우 시간을 초 단위로 변환
  const t1 = typeof time1 === 'string' ? timeToSeconds(time1) : time1;
  const t2 = typeof time2 === 'string' ? timeToSeconds(time2) : time2;

  // CSS를 m/s 단위로 계산
  const css_ms = (distance2 - distance1) / (t2 - t1);

  // 100m 당 페이스를 초 단위로 계산
  const pace_per_100m = 100 / css_ms;

  // mm:ss 형식으로 변환
  const minutes = Math.floor(pace_per_100m / 60);
  const seconds = Math.round(pace_per_100m % 60);

  return {
    css_ms: css_ms,
    pace_seconds: pace_per_100m,
    pace_formatted: `${minutes}:${seconds.toString().padStart(2, '0')}`
  };
}

// 사용 예시:
const result = calculateCSS(200, 150, 400, 368);
// 반환값: { css_ms: 0.917, pace_seconds: 109, pace_formatted: "1:49" }

수영 훈련 스트레스 점수 (sTSS)

전체 공식:

sTSS = (IF³) × 지속 시간 (시간) × 100
IF = NSS / FTP
NSS = 총 거리 / 총 시간 (m/min)

🧪 대화형 계산기 - 공식 테스트

계산된 sTSS:
55
계산 단계:
NSS = 3000m / 55분 = 54.5 m/min
FTP = 100 / (93/60) = 64.5 m/min
IF = 54.5 / 64.5 = 0.845
sTSS = 0.845³ × (55/60) × 100 = 55

JavaScript 구현:

function calculateSTSS(distance, timeMinutes, ftpMetersPerMin) {
  // 정규화된 수영 속도 (NSS) 계산
  const nss = distance / timeMinutes;

  // 강도 계수 (Intensity Factor) 계산
  const intensityFactor = nss / ftpMetersPerMin;

  // 시간 계산
  const hours = timeMinutes / 60;

  // 세제곱 강도 계수를 사용하여 sTSS 계산
  const stss = Math.pow(intensityFactor, 3) * hours * 100;

  return Math.round(stss);
}

// 사용 예시:
const stss = calculateSTSS(3000, 55, 64.5);
// 반환값: 55

// 도우미: CSS를 FTP로 변환
function cssToFTP(cssPacePer100mSeconds) {
  // FTP (m/min) = 100m / (분 단위 페이스)
  return 100 / (cssPacePer100mSeconds / 60);
}

// 예시: CSS 1:33 (93초)
const ftp = cssToFTP(93); // 반환값: 64.5 m/min

SWOLF

공식:

SWOLF = 랩 타임 (초) + 스트로크 수
SWOLF₂₅ = (시간 × 25/수영장 길이) + (스트로크 × 25/수영장 길이)

🧪 대화형 계산기 - 공식 테스트

SWOLF 점수:
35
계산:
SWOLF = 20s + 15 스트로크 = 35

JavaScript 구현:

function calculateSWOLF(timeSeconds, strokeCount) {
  return timeSeconds + strokeCount;
}

function calculateNormalizedSWOLF(timeSeconds, strokeCount, poolLength) {
  const normalizedTime = timeSeconds * (25 / poolLength);
  const normalizedStrokes = strokeCount * (25 / poolLength);
  return normalizedTime + normalizedStrokes;
}

// 예시:
const swolf = calculateSWOLF(20, 15);
// 반환값: 35

const swolf50m = calculateNormalizedSWOLF(40, 30, 50);
// 반환값: 35 (25m로 정규화됨)

수영 역학 (Stroke Mechanics)

스트로크 속도 (SR, Stroke Rate)

공식:

SR = 60 / 사이클 시간 (초)
SR = (스트로크 수 / 시간 (초)) × 60

🧪 대화형 계산기 - 공식 테스트

스트로크 속도 (SPM):
72
계산:
SR = (30 / 25) × 60 = 72 SPM

JavaScript 구현:

function calculateStrokeRate(strokeCount, timeSeconds) {
  return (strokeCount / timeSeconds) * 60;
}

// 예시:
const sr = calculateStrokeRate(30, 25);
// 반환값: 72 SPM

스트로크 당 거리 (DPS, Distance Per Stroke)

공식:

DPS = 거리 / 스트로크 수
DPS = 거리 / (SR / 60)

JavaScript 구현:

function calculateDPS(distance, strokeCount, pushoffDistance = 0) {
  const effectiveDistance = distance - pushoffDistance;
  return effectiveDistance / strokeCount;
}

// 예시 (25m 수영장, 5m 푸시 오프):
const dps = calculateDPS(25, 12, 5);
// 반환값: 1.67 m/스트로크

// 여러 랩의 경우:
const dps100m = calculateDPS(100, 48, 4 * 5);
// 반환값: 1.67 m/스트로크 (4 랩 × 5m 푸시 오프)

SR과 DPS를 이용한 속도

공식:

속도 (m/s) = (SR / 60) × DPS

JavaScript 구현:

function calculateVelocity(strokeRate, dps) {
  return (strokeRate / 60) * dps;
}

// 예시:
const velocity = calculateVelocity(70, 1.6);
// 반환값: 1.87 m/s

스트로크 지수 (SI, Stroke Index)

공식:

SI = 속도 (m/s) × DPS (m/스트로크)

JavaScript 구현:

function calculateStrokeIndex(velocity, dps) {
  return velocity * dps;
}

// 예시:
const si = calculateStrokeIndex(1.5, 1.7);
// 반환값: 2.55

성과 관리 차트 (PMC)

CTL, ATL, TSB 계산

공식:

CTL 오늘 = CTL 어제 + (TSS 오늘 - CTL 어제) × (1/42)
ATL 오늘 = ATL 어제 + (TSS 오늘 - ATL 어제) × (1/7)
TSB = CTL 어제 - ATL 어제

JavaScript 구현:

function updateCTL(previousCTL, todayTSS) {
  return previousCTL + (todayTSS - previousCTL) * (1/42);
}

function updateATL(previousATL, todayTSS) {
  return previousATL + (todayTSS - previousATL) * (1/7);
}

function calculateTSB(yesterdayCTL, yesterdayATL) {
  return yesterdayCTL - yesterdayATL;
}

// 일련의 운동에 대한 PMC 계산
function calculatePMC(workouts) {
  let ctl = 0, atl = 0;
  const results = [];

  workouts.forEach(workout => {
    ctl = updateCTL(ctl, workout.tss);
    atl = updateATL(atl, workout.tss);
    const tsb = calculateTSB(ctl, atl);

    results.push({
      date: workout.date,
      tss: workout.tss,
      ctl: Math.round(ctl * 10) / 10,
      atl: Math.round(atl * 10) / 10,
      tsb: Math.round(tsb * 10) / 10
    });
  });

  return results;
}

// 사용 예시:
const workouts = [
  { date: '2025-01-01', tss: 50 },
  { date: '2025-01-02', tss: 60 },
  { date: '2025-01-03', tss: 45 },
  // ... 추가 운동
];

const pmc = calculatePMC(workouts);
// 반환값: 각 날짜의 CTL, ATL, TSB가 포함된 배열

고급 계산

다중 거리에서의 CSS (회귀 분석 방법)

JavaScript 구현:

function calculateCSSRegression(distances, times) {
  // 선형 회귀: 거리 = a + b*시간
  const n = distances.length;
  const sumX = times.reduce((a, b) => a + b, 0);
  const sumY = distances.reduce((a, b) => a + b, 0);
  const sumXY = times.reduce((sum, x, i) => sum + x * distances[i], 0);
  const sumXX = times.reduce((sum, x) => sum + x * x, 0);

  const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
  const intercept = (sumY - slope * sumX) / n;

  return {
    css: slope, // 임계 수영 속도 (m/s)
    anaerobic_capacity: intercept // 무산소 운동 용량 (m)
  };
}

// 다중 테스트 거리 예시:
const distances = [100, 200, 400, 800];
const times = [65, 150, 340, 720]; // 초 단위
const result = calculateCSSRegression(distances, times);
// 반환값: { css: 1.18, anaerobic_capacity: 15.3 }

페이스로부터의 강도 계수

JavaScript 구현:

function calculateIntensityFactor(actualPace100m, thresholdPace100m) {
  // 페이스를 속도(m/s)로 변환
  const actualSpeed = 100 / actualPace100m;
  const thresholdSpeed = 100 / thresholdPace100m;
  return actualSpeed / thresholdSpeed;
}

// 예시:
const if_value = calculateIntensityFactor(110, 93);
// 반환값: 0.845 (임계값의 84.5%로 수영)

페이스 일관성 분석

JavaScript 구현:

function analyzePaceConsistency(laps) {
  const paces = laps.map(lap => lap.distance / lap.time);
  const avgPace = paces.reduce((a, b) => a + b) / paces.length;

  const variance = paces.reduce((sum, pace) =>
    sum + Math.pow(pace - avgPace, 2), 0) / paces.length;
  const stdDev = Math.sqrt(variance);
  const coefficientOfVariation = (stdDev / avgPace) * 100;

  return {
    avgPace,
    stdDev,
    coefficientOfVariation,
    consistency: coefficientOfVariation < 5 ? "우수함" :
                 coefficientOfVariation < 10 ? "좋음" :
                 coefficientOfVariation < 15 ? "보통" : "가변적"
  };
}

// 예시:
const laps = [
  { distance: 100, time: 70 },
  { distance: 100, time: 72 },
  { distance: 100, time: 71 },
  // ...
];
const analysis = analyzePaceConsistency(laps);
// 반환값: { avgPace: 1.41, stdDev: 0.02, coefficientOfVariation: 1.4, consistency: "우수함" }

스트로크 수로 피로 감지

JavaScript 구현:

function detectFatigue(laps) {
  const firstThird = laps.slice(0, Math.floor(laps.length/3));
  const lastThird = laps.slice(-Math.floor(laps.length/3));

  const firstThirdAvg = firstThird.reduce((sum, lap) =>
    sum + lap.strokeCount, 0) / firstThird.length;
  const lastThirdAvg = lastThird.reduce((sum, lap) =>
    sum + lap.strokeCount, 0) / lastThird.length;

  const strokeCountIncrease = ((lastThirdAvg - firstThirdAvg) / firstThirdAvg) * 100;

  return {
    firstThirdAvg: Math.round(firstThirdAvg * 10) / 10,
    lastThirdAvg: Math.round(lastThirdAvg * 10) / 10,
    percentIncrease: Math.round(strokeCountIncrease * 10) / 10,
    fatigueLevel: strokeCountIncrease < 5 ? "최소" :
                  strokeCountIncrease < 10 ? "보통" :
                  strokeCountIncrease < 20 ? "상당함" : "심각함"
  };
}

// 예시:
const laps = [
  { strokeCount: 14 }, { strokeCount: 14 }, { strokeCount: 15 },
  { strokeCount: 15 }, { strokeCount: 16 }, { strokeCount: 16 },
  { strokeCount: 17 }, { strokeCount: 18 }, { strokeCount: 18 }
];
const fatigue = detectFatigue(laps);
// 반환값: { firstThirdAvg: 14.3, lastThirdAvg: 17.7, percentIncrease: 23.8, fatigueLevel: "심각함" }

데이터 검증

운동 데이터 품질 확인

JavaScript 구현:

function validateWorkoutData(workout) {
  const issues = [];

  // 합리적인 페이스 범위 확인 (1:00-5:00 / 100m)
  const avgPace = (workout.totalTime / workout.totalDistance) * 100;
  if (avgPace < 60 || avgPace > 300) {
    issues.push(`비정상적인 평균 페이스: ${Math.round(avgPace)}s / 100m`);
  }

  // 합리적인 스트로크 수 확인 (10-50 / 25m)
  const avgStrokesPer25m = (workout.totalStrokes / workout.totalDistance) * 25;
  if (avgStrokesPer25m < 10 || avgStrokesPer25m > 50) {
    issues.push(`비정상적인 스트로크 수: ${Math.round(avgStrokesPer25m)} / 25m`);
  }

  // 합리적인 스트로크 속도 확인 (30-150 SPM)
  const avgSR = calculateStrokeRate(workout.totalStrokes, workout.totalTime);
  if (avgSR < 30 || avgSR > 150) {
    issues.push(`비정상적인 스트로크 속도: ${Math.round(avgSR)} SPM`);
  }

  // 누락된 랩 확인 (시간 공백)
  if (workout.laps && workout.laps.length > 1) {
    for (let i = 1; i < workout.laps.length; i++) {
      const gap = workout.laps[i].startTime -
                  (workout.laps[i-1].startTime + workout.laps[i-1].duration);
      if (gap > 300) { // 5분 공백
        issues.push(`랩 ${i}와 ${i+1} 사이에서 큰 공백이 감지됨`);
      }
    }
  }

  return {
    isValid: issues.length === 0,
    issues: issues
  };
}