Test-based 운동 카운팅 App 알고리즘 개발

2024.12 ~ 2025.03

2024.12 ~ 2025.03

Unit Test데이터 시각화적분 기반 알고리즘

React NativeNext.jsPython

목차

  • 프로젝트 개요
  • 트러블 슈팅
  • A. 펌웨어 의존 카운팅 구조 → 앱 기반 알고리즘 구조 전환
  • 문제
  • 원인 분석
  • 1. 펌웨어 내부에서 카운팅을 수행하는 구조
  • 2. raw 데이터 접근 불가능 구조
  • 3. 알고리즘 배포 및 확장 불가능
  • 해결 방법
  • 1. 카운팅 위치를 펌웨어 → 앱으로 이동
  • 결과
  • 핵심 포인트
  • B. 데이터 시각화 및 넓이 기반 알고리즘
  • 문제
  • 원인 분석
  • 1. 단일 threshold 기반 point 판단 구조
  • 2. 시간과 압력 값을 분리해서 처리하는 구조
  • 3. 운동 파형의 구간 개념 부재
  • 해결 방법
  • 1. point 기반 판단을 구간(segment) 기반 판단으로 변경
  • 2. 데이터 시각화를 통한 파형 기반 설계
  • 3. 음/양 구간을 모두 반영한 넓이 계산
  • 결과
  • 핵심 포인트
  • C. 테스트 시스템 구축 및 영점 보정 및 노이즈 제거 개선
  • 문제
  • 원인 분석
  • 1. 재현 가능한 테스트 환경 부재
  • 2. 기준 값 고정 구조와 영점 변화 미반영
  • 3. 순간적으로 튀는 값과 이전 구간 값이 다음 계산에 남는 구조
  • 해결 방법
  • 1. 로그 재생 기반 테스트 시스템 구축
  • 2. 영점 보정 로직 재설계
  • 3. 노이즈 제거 및 예외 처리 개선
  • 결과
  • 핵심 포인트

Unit Test데이터 시각화적분 기반 알고리즘

React NativeNext.jsPython

목차

  • 프로젝트 개요
  • 트러블 슈팅
  • A. 펌웨어 의존 카운팅 구조 → 앱 기반 알고리즘 구조 전환
  • 문제
  • 원인 분석
  • 1. 펌웨어 내부에서 카운팅을 수행하는 구조
  • 2. raw 데이터 접근 불가능 구조
  • 3. 알고리즘 배포 및 확장 불가능
  • 해결 방법
  • 1. 카운팅 위치를 펌웨어 → 앱으로 이동
  • 결과
  • 핵심 포인트
  • B. 데이터 시각화 및 넓이 기반 알고리즘
  • 문제
  • 원인 분석
  • 1. 단일 threshold 기반 point 판단 구조
  • 2. 시간과 압력 값을 분리해서 처리하는 구조
  • 3. 운동 파형의 구간 개념 부재
  • 해결 방법
  • 1. point 기반 판단을 구간(segment) 기반 판단으로 변경
  • 2. 데이터 시각화를 통한 파형 기반 설계
  • 3. 음/양 구간을 모두 반영한 넓이 계산
  • 결과
  • 핵심 포인트
  • C. 테스트 시스템 구축 및 영점 보정 및 노이즈 제거 개선
  • 문제
  • 원인 분석
  • 1. 재현 가능한 테스트 환경 부재
  • 2. 기준 값 고정 구조와 영점 변화 미반영
  • 3. 순간적으로 튀는 값과 이전 구간 값이 다음 계산에 남는 구조
  • 해결 방법
  • 1. 로그 재생 기반 테스트 시스템 구축
  • 2. 영점 보정 로직 재설계
  • 3. 노이즈 제거 및 예외 처리 개선
  • 결과
  • 핵심 포인트

프로젝트 개요


해당 앱 서비스는 압력 센서가 내장된 하드웨어를 기반으로 푸쉬업, 스쿼트 카운팅 기능을 제공한다.

이 과정에서 하드웨어 의존적인 운동 카운팅 구조를 앱 기반으로 전환하고, 데이터 시각화 및 테스트 시스템을 구축하였다. 이를 통해서 넓이 기반 알고리즘 구현 및 영점 보정, 노이즈 제거 로직 적용하고, 카운팅 알고리즘의 재현율(Recall)을 개선하였다.

1. 카운팅 알고리즘 구조 전환

하드웨어 펌웨어(C++) 기반 카운팅 구조를 앱으로 이전하여 서비스 이후에도 알고리즘 개선이 가능하도록 구조를 재설계하였다.

  • 하드웨어는 raw 데이터 전달, 앱에서 카운팅 수행 구조로 변경
  • 서버 데이터 수집 및 앱 업데이트를 통해 알고리즘 개선이 가능하도록 확장
  • 2. 데이터 시각화 기반 넓이 기반 운동 판단 알고리즘 개발

    센서 데이터와 보조 지표를 그래프로 시각화하여 운동 패턴을 분석하였다.

    이를 기반으로 시간과 압력 값을 결합한 넓이 개념을 도입하여 기존 임계값 기반 알고리즘의 한계를 개선하였다.

  • height × time 기반 수식 설계 및 최적화
  • 양의 넓이 + 음의 넓이 결합 방식으로 치팅 동작 대응
  • 느린 동작 및 빠른 동작 모두 인식 가능하도록 개선
  • 4. 카운팅 알고리즘 테스트 시스템 구축

    알고리즘 검증을 위한 별도의 테스트 환경을 구축하여 재현성과 개발 속도를 개선하였다.

  • 앱 알고리즘 코드(TypeScript)를 그대로 활용하여 테스트할 수 있는 Next.js 기반 시스템 구축
  • 200ms 주기 재현 및 실제 로그 데이터 기반 시뮬레이션
  • 압력 센서 값 그래프 시각화 및 알고리즘 판단 지점 표기를 통한 직관적인 테스트 환경 구현
  • 5. 테스트 시스템 기반 영점 보정 및 노이즈 제거 로직 구현

    테스트 시스템을 기반으로 운동 중 발생하는 기준 값 변화와 노이즈 문제를 해결하기 위한 보정 로직을 설계하였다.

  • 영점 보정 기준 및 로직 설계 및 테스트
  • 영점 보정 중 노이즈 제거를 위해 이동 평균 및 범위 기반 필터링 적용
  • 6. 성능 측정

  • 피실험자 20명을 대상으로 실험 결과 재현율(Recall) 0.95 이상 달성
  • 트러블 슈팅


    A. 펌웨어 의존 카운팅 구조 → 앱 기반 알고리즘 구조 전환


    문제

    기존 카운팅 알고리즘은 하드웨어 내부 펌웨어(C++)에서 수행되며 카운팅 결과 값만 앱으로 전달되는 구조였다.

  • 운동 카운팅 로직이 하드웨어 내부에 고정되어 있어 배포 이후 알고리즘 수정이 불가능
  • 알고리즘 오류가 발생해도 기존 사용자 대상 개선 적용 불가
  • 센서 raw 데이터가 아닌 결과 값만 전달되어 데이터 분석 및 알고리즘 개선이 불가능
  • 서버 또는 앱 단에서 운동 데이터 수집 및 활용이 제한됨
  • 기존 구조는 “카운팅 결과만 전달하는 폐쇄적인 구조”로 구성되어 있어 알고리즘을 개선하거나 확장할 수 없는 상태였다.

    원인 분석

    1. 펌웨어 내부에서 카운팅을 수행하는 구조

  • 센서 데이터를 하드웨어에서 직접 처리하고 결과만 BLE로 전달
  • 앱에서는 이미 계산된 값만 수신
  • 데이터 흐름이 “센서 → 펌웨어 → 결과 → 앱” 구조로 구성되어 있어 앱에서는 판단 과정에 개입할 수 없는 구조였다.

    2. raw 데이터 접근 불가능 구조

    센서 데이터의 전체 흐름이 제거된 상태로 전달되기 때문에 데이터 기반 분석이나 알고리즘 개선이 불가능한 구조였다.

  • 펌웨어에서 필터링 및 판단이 완료된 결과만 전달
  • 앱에서는 weight_data의 변화 과정이나 패턴을 확인할 수 없음
  • 3. 알고리즘 배포 및 확장 불가능

    기존 구조는 알고리즘을 서비스 레벨에서 개선할 수 없는 상태였다.

  • 알고리즘이 펌웨어에 포함되어 있어 수정 시 펌웨어 업데이트 필요
  • 이미 판매된 제품에는 적용 불가능
  • 해결 방법

    1. 카운팅 위치를 펌웨어 → 앱으로 이동

    데이터 흐름을 다음과 같이 변경하였다.

  • 기존: 하드웨어에서 카운팅 수행 후 결과 전달
  • 변경: 하드웨어는 raw 데이터 전달, 앱에서 카운팅 수행
  • 변경된 푸쉬업 Protocol 구조
    변경된 푸쉬업 Protocol 구조

    1.1. 기존 판단 방식 정리

  • 하드웨어 내부에서 threshold 기반 카운팅 수행
  • 앱은 결과 값만 표시
  • 1.2. 변경된 판단 구조

  • 하드웨어는 센서 값을 문자열 형태로 전달
  • 앱에서 해당 값을 파싱하여 직접 처리
  • 앱이 카운팅 로직을 직접 수행하도록 구조를 변경하였다.

    결과

  • 알고리즘을 앱에서 수정 및 배포 가능
  • 사용자에게도 업데이트 형태로 개선 적용 가능
  • raw 데이터 기반 분석 및 알고리즘 고도화 가능
  • 데이터 수집 및 서버 활용 구조 확장 가능
  • 핵심 포인트

    이 개선의 핵심은 데이터 흐름을 재구성한 것에 있다.

  • 결과 전달 구조 → 데이터 전달 구조로 변경
  • 하드웨어 종속 알고리즘 → 앱 기반 독립 알고리즘
  • 폐쇄형 처리 → 데이터 기반 확장 가능 구조
  • 카운팅 결과를 전달하던 구조에서 센서 데이터를 전달하고 앱에서 판단하는 구조로 전환

    B. 데이터 시각화 및 넓이 기반 알고리즘


    문제

    기존 푸쉬업 카운팅은 기준 값을 넘는지 여부를 중심으로 판단하는 구조였고 실제 운동 데이터에서는 이 방식으로 해결되지 않는 문제가 반복되었다.

  • 허들을 낮추면 느리게 수행하는 동작은 잡히지만 반동을 이용한 치팅 동작도 함께 카운팅 됨
  • 허들을 높이면 치팅 동작은 줄어들지만 정상적으로 천천히 수행하는 동작이 카운팅 되지 않음
  • 동일 사용자의 반복 수행에서도 수행 속도와 패턴에 따라 결과가 달라짐
  • 하강 → 상승 → 복귀로 이어지는 실제 운동 흐름이 카운팅 기준에 반영되지 않음
  • 기존 구조는 특정 시점의 값이 기준을 넘는지 여부와 원점 복귀 조건을 조합한 방식이었고 운동 전체 구간이 아닌 단일 이벤트 중심으로 판단하는 한계를 가진 상태였다.

    원인 분석

    1. 단일 threshold 기반 point 판단 구조

    기존 알고리즘은 특정 시점의 값이 기준 값을 초과하는지 여부를 중심으로 상태를 전환하는 구조였다.

    이후 원점 범위로 복귀했는지를 확인하고 일정 시간 유지되면 카운팅을 수행했다.

    판단 기준이 구간이 아닌 특정 시점의 값 초과 여부였기 때문에 운동의 전체 흐름이나 크기를 반영하지 못하는 구조였다.

    2. 시간과 압력 값을 분리해서 처리하는 구조

    기존 구조는 기준 값 초과 여부와 유지 시간을 각각 별도의 조건으로 처리했다.

  • 기준 값 초과 여부 → 운동 시작 판단
  • 유지 시간 → 카운팅 조건
  • 이 구조에서는 “얼마나 크게 움직였는가”와 “얼마 동안 수행했는가”가 하나의 판단 기준으로 결합되지 않았다.

    결과적으로 빠르게 반동을 주는 동작과 천천히 수행하는 동작을 동일한 기준으로 처리하지 못하는 문제가 발생했다.

    3. 운동 파형의 구간 개념 부재

    센서 데이터는 기준 값을 중심으로 하강 후 상승하고 다시 복귀하는 형태의 파형을 가지지만 기존 구조는 이를 하나의 구간으로 처리하지 않았다.

  • 하강 구간(음의 변화)
  • 상승 구간(양의 변화)
  • 이 두 구간이 하나의 운동 사이클로 묶이지 않고 분리된 상태로 처리되었다.

    특히 기준 값 아래로 크게 떨어졌다가 반동으로 올라오는 치팅 패턴을 구분할 수 없었다.

    기존 구조는 운동을 연속된 구간이 아닌 단일 이벤트로 처리하는 방식이었기 때문에 실제 데이터 패턴을 반영하지 못했다.

    해결 방법

    1. point 기반 판단을 구간(segment) 기반 판단으로 변경

    기존 구조는 특정 시점의 값으로 판단했지만 변경 구조는 하나의 운동을 시작부터 종료까지 하나의 구간으로 정의했다.

    데이터를 단일 값이 아닌 구간 단위로 수집하고 해당 구간 전체를 기반으로 판단하도록 변경하였다.

    1.1. 기존 판단 방식 정리

  • 기준 값 초과 시 운동 시작
  • 원점 범위 복귀 시 카운팅 조건 확인
  • 유지 시간 만족 시 카운팅
  • 구간 전체를 계산하지 않고 특정 시점 조건의 조합으로 판단하는 방식이었다.

    1.2. 변경된 판단 구조

  • 하강 → 복귀 → 상승 → 복귀 흐름을 하나의 운동 사이클로 정의
  • 구간 동안 데이터를 배열에 저장
  • 시작 시점과 종료 시점을 기준으로 하나의 segment 구성
  • 판단 단위를 “값”이 아니라 “구간”으로 변경하였다.

    1.3. 판단 기준 재설계

    구간 동안의 데이터로 높이와 시간을 계산하고 이를 곱한 값으로 카운팅을 판단했다.

    판단 기준을 기준 값 초과 여부에서 “구간의 크기 × 수행 시간”으로 변경하였다.

    2. 데이터 시각화를 통한 파형 기반 설계

    로그 데이터를 기반으로 시간 순서대로 weight, 기준 값, 넓이 값을 시각화하여 실제 운동 파형을 분석했다.

    푸쉬업 운동 중 센서 값 시각화 그래프
    푸쉬업 운동 중 센서 값 시각화 그래프

    시각화 결과 다음과 같은 특징을 확인했다.

  • 정상 동작은 하강 → 상승 → 복귀 형태의 연속된 구간으로 나타남
  • 치팅 동작은 기준 값 아래로 크게 튀는 패턴이 반복됨
  • threshold 조정만으로는 두 패턴을 동시에 구분할 수 없음
  • 데이터 파형을 기준으로 알고리즘 구조를 설계하도록 방향을 변경하였다.

    3. 음/양 구간을 모두 반영한 넓이 계산

    초기 구조는 기준 값 위 구간만 계산했지만 이후 하강 구간까지 포함하도록 변경하였다.

  • 상승 구간 → 양의 값
  • 하강 구간 → 음의 값
  • 최종 높이는 두 값을 합산하여 계산하였다.

    운동을 단일 방향이 아닌 하강과 상승을 포함한 하나의 사이클로 처리하도록 변경하였다.

    결과

    기준 값 조정으로 해결되지 않던 치팅 동작과 느린 동작 문제를 동시에 해결할 수 있는 구조를 확보하였다

  • 동일 동작 반복 시 일관된 카운팅 결과 유지
  • threshold 기반 단일 조건 판단에서 벗어나 구간 기반 계산으로 판단 기준 명확화
  • 이후 범위 조정, 시간 가중치, 보정 로직을 추가할 수 있는 구조적 기반이 마련되었다

    핵심 포인트

    이 개선의 핵심은 카운팅 기준을 기준 값 초과 여부에서 운동 구간 전체의 넓이 계산으로 변경한 데 있다.

  • 단일 threshold 기반 point 판단 → 구간(segment) 기반 판단
  • 값 비교 구조 → 시간과 변화량을 결합한 area 계산 구조
  • 단일 방향 판단 → 하강과 상승을 포함한 전체 사이클 판단
  • 단순 기준 값으로 판단하던 구조에서 운동 구간 전체의 면적을 기준으로 판단하는 구조로 전환

    C. 테스트 시스템 구축 및 영점 보정 및 노이즈 제거 개선


    문제

    넓이 기반 알고리즘으로 판단 구조를 바꾼 이후에도 실제 데이터에서는 안정 구간, 튀는 값, 기준 값 변화 때문에 오탐과 미탐이 반복되었다. 테스트를 진행할수록 같은 알고리즘이라도 로그 형태와 사용자 조건에 따라 다른 문제가 드러났다.

  • RN 환경에서는 빌드 시간이 오래 걸리고 실제 기기가 준비되어 있어야 해서 수정 후 바로 재현하기 어려웠다.
  • 운동 1회가 끝난 뒤 데이터가 안정되는 구간에서 카운팅이 발생하는 문제가 있었다.
  • 압력 값이 상승하기 이전에 기록된 최저점이 이후 계산에 남아 중간 값에서도 상승으로 잘못 판단되는 문제가 있었다.
  • 원점이 운동 중 변하는 경우 기존 기준 값으로 계산이 계속 진행되어 카운팅 누락이나 더블 카운트가 발생했다.
  • 여성이나 변화량이 작은 사용자 데이터는 기존 상수 조건을 통과하지 못해 정상 동작이 카운팅되지 않는 경우가 있었다.
  • 넓이 기반 판단 구조를 도입한 것만으로는 충분하지 않았고, 실제 로그를 반복 재생할 수 있는 테스트 환경과 기준 값 변화 및 노이즈를 처리하는 보정 구조가 함께 필요한 상태였다.

    원인 분석

    1. 재현 가능한 테스트 환경 부재

  • 다양한 몸무게를 가진 피실험자 데이터를 모아 검증해야 했지만 RN 환경은 빌드 시간이 크고 실제 기기가 필요해 테스트 반복 비용이 높았다.
  • 문제가 발생한 뒤 수정해도 같은 상황을 다시 재현하기 어려웠다.
  • 알고리즘 개선 작업이 활발하게 진행되는 시점에서 로그를 그대로 재생하며 검증할 수 있는 환경이 없었다.
  • 알고리즘을 수정해도 같은 입력을 다시 넣어 비교할 수 없었기 때문에, 어떤 수정이 실제로 문제를 해결했는지 빠르게 확인하기 어려운 구조였다.

    2. 기준 값 고정 구조와 영점 변화 미반영

  • 운동 중에는 초반에 잡은 영점이 계속 유지되지 않고 변하는 상황이 발생했다.
  • 기존 기준 값으로 이후 데이터를 계속 계산하면 넓이 계산 값이 실제 운동과 어긋났다.
  • 보정이 느슨하면 운동 중간에 잘못된 지점에서 영점이 잡히고, 보정과 카운팅이 같은 흐름에 있으면 더블 카운트가 늘어났다.
  • 운동 중 기준 값이 바뀌는 상황을 별도로 처리하지 못하면 같은 동작이라도 이전 구간의 기준 값이 다음 구간 계산에 남아 오차를 누적시키는 구조였다.

    3. 순간적으로 튀는 값과 이전 구간 값이 다음 계산에 남는 구조

  • 값이 아래로 튀는 경우 최저점이 크게 잡혀 카운팅이 안 되는 문제가 있었다.
  • 오래 유지되지 않는 일시적인 튀는 값이 높이 계산에 과도하게 반영되었다.
  • 압력 값이 상승하기 전에 저장된 최저점이 남아 있어 중간값에서도 상승으로 오판하는 문제가 있었다.
  • 계산 구간이 끝난 뒤 필요한 값만 남기고 정리하지 않으면 이전 구간의 노이즈와 극값이 다음 카운팅 판단에 개입하는 구조였다.

    해결 방법

    1. 로그 재생 기반 테스트 시스템 구축

    테스트 시스템 UI
    테스트 시스템 UI
  • RN 앱 안에서 바로 검증하는 대신 웹 환경에서 알고리즘만 분리해 재생 가능한 테스트 환경을 구성했다.
  • 기존 앱 코드 중 카운팅 로직은 그대로 적용할 수 있도록 환경을 구성하였다.
  • 운동을 수행하는 동안 발생한 로그를 붙여 넣으면 동일한 입력을 같은 주기로 반복 재생할 수 있도록 구성했다.
  • 실제 동작과 같은 200ms 내외 주기로 데이터를 순차 투입하도록 구현했다.
  • 알고리즘을 수정한 뒤 같은 로그를 다시 넣어 결과를 비교할 수 있는 구조로 바꾸어, 문제 재현과 수정 검증을 한 번의 흐름에서 처리할 수 있게 했다.

    1.1. 변경된 테스트 구조

    운동을 수행하는 동안 발생한 로그를 기반으로 입력을 재현할 수 있도록 만들어 알고리즘 수정 결과를 비교 가능한 형태로 바꾸었다.

    1.2. 시각화 및 결과 확인 구조

  • 압력 값 데이터를 시간 순서대로 그래프에 표시했다.
  • 카운팅이 발생한 인덱스를 별도로 기록해 그래프 위에 표시했다.
  • 카운팅 결과 문자열도 함께 누적해 자세 평가 결과를 동시에 확인했다.
  • 카운팅이 발생한 지점과 실제 압력 값 변화가 맞는지 시각적으로 바로 확인할 수 있게 했다.

    2. 영점 보정 로직 재설계

  • 일정 개수의 데이터를 버퍼에 모아 범위 안에 들어오면 기준 값을 다시 설정하도록 구성했다.
  • 초기에는 영점 재설정과 넓이 계산을 같은 흐름에서 처리했지만 더블 카운트와 계산 지연이 늘어나는 문제가 생겨 구조를 분리했다.
  • 이후 보정은 별도로 수행하고, 계산 식에는 기준 값 차이만 보정 값으로 반영하도록 변경했다.
  • 2.1. 초기 영점 보정 방식

    영점을 다시 잡는 구조 자체는 만들었지만 조건이 느슨하면 운동 중간에도 기준 값이 바뀌는 문제가 있었다.

    2.2. 보정값 기반 구조로 변경

    기준 값이 바뀌더라도 수식 계산에는 차이 값만 반영해 현재 구간 계산이 무너지지 않도록 구성했다.

    2.3. 이동 평균 적용

  • 느리게 수행하는 경우 영점이 위쪽으로 계속 이동한 뒤 복구되지 않는 문제가 있어 기준 값 계산에 이동 평균을 적용했다.
  • 보정 조건은 더 까다롭게 만들고, 기준 값 자체는 최근 값 평균으로 계산했다.
  • 기준 값을 단일 측정 값으로 고정하지 않고 평균 값으로 관리해 느린 동작과 큰 흔들림이 동시에 들어와도 기준이 급격히 무너지지 않도록 했다.

    3. 노이즈 제거 및 예외 처리 개선

  • 일시적으로 튀는 값이 최저점으로 남아 카운팅을 방해하는 문제를 해결하기 위해 극값 초기화 시점을 조정했다.
  • 상승 구간이 시작되면 이전에 저장된 최저점을 초기화해 다음 계산에 남지 않도록 변경했다.
  • 넓이 계산 식도 상황에 맞게 조정했다. 스쿼트에서는 안정 구간에서 오탐이 발생해 width ** 1.5를 제거했고, 보정 값도 제외했다.
  • 이전 사이클의 최저점이 다음 상승 판단에 남지 않도록 정리해 중간 값 오판을 줄였다.

  • 변화량이 작은 사용자 데이터도 통과할 수 있도록 무게 기준으로 상수 보정을 적용했다. 푸쉬업은 15000, 20000, 25000 기준으로 조정 값을 나누었고, 스쿼트는 27000 기준으로 조정했다.
  • 기준 상수를 사용자 데이터 범위에 맞게 조정해 여성이나 변화량이 작은 데이터도 정상적으로 처리할 수 있게 했다.

    결과

  • 같은 로그를 200ms 주기로 반복 재생할 수 있는 테스트 시스템을 구축해 수정 후 즉시 재현과 비교가 가능해졌다.
  • 카운팅 지점과 압력 값 파형을 동시에 확인할 수 있어 어떤 값이 오탐과 미탐을 만들었는지 빠르게 파악할 수 있게 되었다.
  • 영점 변화로 인한 누락, 더블 카운트, 기준 값 드리프트 문제가 보정값과 이동 평균 구조로 완화되었다.
  • 일시적인 튀는 값과 이전 구간의 최저점이 다음 계산에 남는 문제를 줄여 안정 구간 오탐과 중간값 오판이 감소했다.
  • 피실험자 20명을 대상으로 실험 결과 푸쉬업, 스쿼트 모두 재현율(Recall) 0.95 이상까지 올릴 수 있었다.
  • 핵심 포인트

    이 개선의 핵심은 알고리즘 수식만 고친 것이 아니라, 같은 입력을 반복 검증할 수 있는 테스트 시스템 구축과 기준 값 변화 및 노이즈를 처리하는 보정 구조를 함께 만든 데 있다.

  • RN 실기기 검증에서 로그 재생 기반 테스트 시스템으로 전환
  • 고정 영점 구조에서 보정 값과 이동 평균을 사용하는 동적 기준 값 구조로 전환
  • 일시적인 튀는 값과 이전 구간 값이 다음 계산에 남지 않도록 초기화와 상수 조정을 분리 적용
  • 테스트 시스템을 기반으로, 넓이 계산식 하나로 문제를 해결하던 구조에서 영점 보정, 노이즈 제거를 함께 다루는 안정화 구조로 전환

    int push_cnt = 0;
    ...
    if (weight_data > push_ref_val * push_range) {
      has_crossed_threshold_pushup = true;
    }
    PUSH=1
    PUSH=2
    push_cnt++;
    pTxCharacteristic->setValue("PUSH=" + intToString(push_cnt));
    PUSH=19612.9
    PUSH=20460.0
    const data = parseNumberValue(ble_value);
    countPush(data);
    if (weight_data > push_ref_val * push_range) {
      has_crossed_threshold_pushup = true;
    }
    if (millis() - push_zero_time >= min_zero_hold_time_push) {
      push_cnt++;
    }
    if (weight_data > push_ref_val * push_range) {
      has_crossed_threshold_pushup = true;
    }
    filteredDataBufferRef.current.push(
      data - standardWeightRef.current
    );
    
    if (!hasCrossedThresholdRef.current) {
      exCrossStartTimeRef.current = Date.now();
      hasCrossedThresholdRef.current = true;
    }
    const height =
      filteredDataBufferRef.current.reduce(
        (sum, value) => sum + value,
        0
      ) / filteredDataBufferRef.current.length;
    
    const width = Date.now() - exCrossStartTimeRef.current;
    
    if (height * width > MINIMUM_AREA) {
      exCountRef.current++;
    }
    const height = highestWeight.current + lowestWeight.current;
    const width = Date.now() - exCrossStartTimeRef.current;
    const interval = setInterval(() => {
      setCurrentLogData(exValues[indexRef.current]);
    
      graphDataRef.current.push({
        time: Date.now() - startedTimeRef.current,
        Weight: exValues[indexRef.current],
      });
    
      setGraphData([...graphDataRef.current]);
    
      ex === "SQUAT"
        ? countSquat(Number(exValues[indexRef.current]))
        : ex === "PUSH"
        ? countPush(Number(exValues[indexRef.current]))
        : countPlank(Number(exValues[indexRef.current]));
    
      indexRef.current++;
    
      if (indexRef.current >= exValues.length) {
        clearInterval(interval);
      }
    }, 200);
    useEffect(() => {
      if (count === 0) return;
      countIndexRef.current.push(indexRef.current);
      setCountIndex([...countIndexRef.current]);
    }, [count]);
    const renderDotByIndex = (props) => {
      const { cx, cy, index } = props;
      if (countIndex.includes(index)) {
        return <circle cx={cx} cy={cy} r={6} fill="red" />;
      }
      return null;
    };
    if (
      standardSetupBufferRef.current[0] > data * 0.95 &&
      standardSetupBufferRef.current[0] < data * 1.05 &&
      standardSetupBufferRef.current[1] > data * 0.95 &&
      standardSetupBufferRef.current[1] < data * 1.05
    ) {
      exLowerLimitRef.current = data;
      exUpperLimitRef.current = data;
      standardWeightRef.current = data;
    }
    if (
      standardSetupBufferRef.current[0] > data * 0.98 &&
      standardSetupBufferRef.current[0] < data * 1.04 &&
      standardSetupBufferRef.current[1] > data * 0.99 &&
      standardSetupBufferRef.current[1] < data * 1.02 &&
      Math.abs(data - standardWeightRef.current) < 2000
    ) {
      correctionValueRef.current = standardWeightRef.current - data;
      standardWeightRef.current = data;
    }
    const height =
      highestWeight.current +
      lowestWeight.current +
      correctionValueRef.current;
    standardWeightStackRef.current.push(data);
    standardWeightRef.current = standardWeightStackRef.current.length
      ? standardWeightStackRef.current.reduce((sum, num) => sum + num, 0) /
        standardWeightStackRef.current.length
      : data;
    if (data > standardWeightRef.current) {
      if (!hasCrossedThresholdRef.current) {
        exCrossStartTimeRef.current = Date.now();
        hasCrossedThresholdRef.current = true;
      }
    
      lowestWeight.current = 0;
    
      if (highestWeight.current < data - standardWeightRef.current)
        highestWeight.current = data - standardWeightRef.current;
    }
    const adjustmentConstant =
      standardWeightRef.current < 27000 ? 0.6 : 1.0;
    
    if (result > SQUAT_MINIMUM_AREA * adjustmentConstant) {
      exCountRef.current++;
    }
    • Sensor → FW(카운팅) → PUSH=1 → App
    • Sensor → FW(데이터 전달) → weight_data → App(카운팅)