2026-09-30 PulseUp 작업일지입니다. 오늘은 11건의 작업을 진행했습니다.
지점 심박계 관리 — 기기 ID(ANT 번호)로만 연결, 켜진 심박계에서 선택 등록 (`web-app` › `/admin/heart-rate-devices`)
현상/기능
- 예전 방식(기기명 끝 숫자 매칭)을 없애고 새로운 방식으로만 작동되어 기기 ID 가 심박계 기기 번호와 일치해서 연결되고, 기기명(예. 숫자)으로 사람이 인식해 관리가 편리함
- 심박계를 새로 저장할 때 기존에 등록된 기기는 다시 등록되지 않음
- electron 이 켜진 상태에서 연결된 심박기기 목록을 보여줘 선택 후 저장함
- 예전 데이터 변환은 필요 없어 등록된 기기를 모두 지우고 다시 설정하는 방식으로 처리함
작업
- components/ 에 기기 ID 규칙(normalizeHeartRateDeviceId, isAntDeviceId, isPlaceholderDeviceId, findDuplicateDevice, getDeviceIdIssue)을 추가하고 toSavePayload 가 기기 ID 를 정규화해 전송하도록 함
- components/(신규)를 추가해 펄스박스 ANT 슬롯을 켜진 심박계 행으로 변환하고 등록 여부를 표시함
- components/(신규)를 추가해 연결된 펄스박스 목록과 선택한 펄스박스의 심박 진단을 3초 주기로 조회하도록 함
- components/(신규)를 추가해 슬롯/기기 ID/심박/등록 상태/선택 표를 제공하고 이미 등록된 기기는 선택을 막도록 함
- components/ 에 켜진 심박계에서 선택 버튼과 기기 ID 문제 안내를 추가하고 라벨을 기기 ID(ANT 번호)·기기명(관리 번호)로 변경함
- 를 수정해 기기 목록을 항상 미사용 포함으로 받고, 선택 시 기기 ID 를 채우며 저장 전 ID 문제를 차단하고 삭제 확인 창을 분리함
- 테스트를 추가해 17건 통과를 확인함
심박 기기 ID 단일 연결 규칙 — 서버 (`api-server` › 심박 저장·지점 심박계·방문확인 승인)
현상/기능
- 기기 ID 가 심박계 기기 번호와 일치할 때만 연결되고, 예전 기기명 숫자 매칭 방식은 삭제됨
- 관련 저장 구조를 조정함
- lib/ 에서 기기명 끝 숫자 매칭을 삭제하고 normalizeHeartRateDeviceId 로 만든 키로만 참가자를 찾도록 함
- services/ 를 수정해 심박 저장·실시간 조회가 기기 ID 만으로 회원과 연결되도록 함
- schemas/ 를 수정해 심박계 등록/수정 시 기기 ID 를 ant-{번호}로 정규화하고 형식이 아니면 400을 반환하도록 함
- schemas/ 를 수정해 방문확인 승인 개인 심박계 deviceId 를 정규화·형식 검사하도록 함
- services/ updateDevice 에 syncSlotAssignmentDevice 를 추가해 배정 행 기기 ID·기기명을 동기화하고 바뀌면 캐시를 무효화하도록 함
- , 테스트를 추가·갱신해 36건 통과를 확인함
심박 모니터 이름표 — 기기 ID 로만 등록 기기 찾기 (`electron-app`)
현상/기능
- 예전 기기명 숫자 매칭 방식이 삭제됨
- ant-{번호} 키로만 등록 기기·수업 명단을 찾도록 수정하고 extractNameKey 를 삭제함
- 표시 순서를 회원 이름 → 기기명 → ant-번호 순으로 변경함
- 주석을 정리함
- 기대값을 갱신해 52건 통과를 확인함
회원 개인 심박계·방문확인 승인 기기 ID 형식 검사 (`web-app` › 지점 회원·가입 신청, `mobile-app` › 지점관리자 회원·신청·심박계)
현상/기능
- 다른 등록 화면에서도 잘못된 형식의 기기 ID 는 막힘
- 웹 BranchMembers/components/ 가 지점 전체 기기를 받아 getDeviceIdIssue 로 형식 오류·중복이면 저장을 차단하도록 함
- 웹 BranchMemberApplications/components/ 를 수정해 입력한 기기 ID 가 ANT 형식이 아니면 승인 버튼을 차단하고 ant-{번호}로 정규화해 전송하도록 함
- usePersonalDeviceIdCheck 훅을 추가해 승인 창에서도 이미 등록된 심박계면 빨간 안내와 함께 승인을 차단하고, 확인 중에도 차단하며 불러오기 실패는 노란 안내만 표시하도록 함
- findOwnedDevice, toOwnedDeviceNotice, toPersonalDeviceCheck 를 추가해 회원이 이미 개인 심박계를 가지고 있으면 승인 창에서 노란 안내를 표시하되 승인은 막지 않도록 함
- 앱 more/devices/ 에 웹과 같은 normalizeHeartRateDeviceId·isAntDeviceId·findDuplicateDevice·getDeviceIdIssue 와 toSavePayload 정규화를 추가하고 ANT_DEVICE_ID_PATTERN 을 삭제함
- 앱 members/ 가 형식 오류·중복이면 빨간 안내 후 저장을 차단하도록 함
- 앱 applications/ 가 기기 ID 형식 오류면 승인을 차단하고 정규화해 전송하도록 함
- 앱에도 usePersonalDeviceIdCheck, getDeviceCheckNotices 를 추가해 이미 등록된 번호면 승인을 차단하고 확인 중에도 차단하며 실패 시 노란 안내를 표시하도록 함
- 앱에도 findOwnedDevice·toOwnedDeviceNotice·toPersonalDeviceCheck 를 추가해 회원 소유 개인 심박계가 있으면 노란 안내만 표시하고 승인은 허용하도록 함
- 앱 more/devices/ 의 형식 안내 문구를 정리함
Electron 기동 시 심박 매니저가 재생 서비스를 먼저 읽음 (`electron-app`)
현상/기능
- HeartRateManager 생성 → syncCatalogClassSlot → playService 과정에서 TypeError: Cannot read properties of undefined (reading 'active') 오류가 발생함
- 를 수정해 심박 매니저를 재생 라우터보다 먼저 생성하도록 하고, 세션 조회 시 runtimeRouter 가 없으면 null(수업 없음)을 돌려줘 기동이 멈추지 않도록 함
수강권 만료 알림 중복 로그 (`api-server` › 회원 알림)
현상/기능
- 터미널에 Duplicate entry ... for key 'uq_member_alert_sent' 오류가 수강권 만료 알림(markSent)에서 발생함
- markSent 를 INSERT IGNORE 로 수정해 스케줄러가 같은 만료 알림을 다시 넣어도 이미 보낸 기록은 건너뛰도록 하고 executeQuery 가 중복을 오류로 찍지 않도록 함
- 푸시가 처음 한 번만 나가도록 함
수업을 골라 재생할 때만 심박 저장 — DB·API (`api-server`)
현상/기능
- 월간 프로그램에서 해당일의 예약리스트(예약없음 포함)를 보여주고 예약을 선택해서 재생할 수 있어야 함
- 예약 없음으로 운동할 때는 심박계에 연결된 이름이 없어 심박계를 DB에 저장하지 않아야 함
- 예약된 운동은 예약리스트를 선택해서 재생할 수 있고, 이때는 예약 시 저장된 심박계를 저장해야 함
- 예약리스트는 예약명(시간) 형태로 표시되고, 해당일은 오늘자로 고정되며, 리모컨의 심박 탭을 재생 버튼 누를 때 시간표 출석 화면과 동일한 기능을 해야 함
- 관련 저장 구조를 조정함
- , 를 수정해 save-batch 가 classSlotId 를 받아 해당 수업 배정으로 기기→회원을 정하고 배정된 기기만 class_slot_id 와 함께 저장하도록 함
- 저장 안 함 사유를 NO_CLASS_SLOT, CLASS_SLOT_NOT_FOUND, CLASS_SLOT_BRANCH_MISMATCH 로 구분하고 배정 없는 기기는 unmappedDeviceIds 로 분류함
- (신규)를 추가해 수업별 기기→회원 매핑을 10초 캐시로 조회하고 배정 변경 시 즉시 비우도록 함
- , 에 수업별 심박계 변경/해제 API(PUT·DELETE /class-slots/:id/bookings/:bookingId/heart-rate-device)를 추가함
- 다른 회원이 쓰는 기기는 409, 다른 회원 개인 기기는 400, 취소 예약은 400 으로 처리하도록 함
- 를 수정해 회원 기록에 지점 수업에서 본인 심박이 저장된 운동도 포함하고 is_own_workout, class_slot_id, class_title, class_start_at 필드를 추가함
- (신규), (신규) 테스트를 추가함
재생 세션 수업 ID·수업 명단 모니터 이름, 재생 서비스 분리 (`electron-app`)
현상/기능
- 재생 세션에 수업 ID 가 전달되지 않아 수업 명단 모니터 이름 표시가 되지 않음
- 재생 서비스 파일이 1,200줄을 넘어 길어짐
- (신규)를 에서 분리하고 배치에 수집 시점 세션의 classSlotId 를 담아 전송하도록 함
- 를 수정해 실패 큐 줄마다 세션(수업 ID 포함)을 기록하고 수업이 바뀌는 지점에서 배치를 나누며 구버전 줄에는 지금 수업을 붙이지 않도록 함
- 를 수정해 재생 수업 명단(10초 TTL)의 배정으로 모니터 큰 글씨를 회원 이름으로 표시하고 예약 없음·미배정은 기기 ID 로 표시하도록 함
- , , main/ 에 세션 metadata.classSlotId 전달 로직을 추가함
- 재생 서비스를 분리함(동작 동일, SINGLE/MULTI 서로 import 금지 유지)
- 1,274줄을 567줄로 줄이고 , , , , 로 분리함
- 1,289줄을 571줄로 줄이고 같은 구성의 모드 전용 파일로 분리함
- 쓰지 않던 shouldPrimeIntroStartPreload, 중복 EMOM 재설정 블록, introStartBlocksPlayStart 를 삭제함
- , 테스트를 갱신함
월간 프로그램 재생 전 오늘 수업 선택 (`web-app` › MonthProgram)
현상/기능
- 월간 프로그램에서 해당일의 예약리스트(예약없음 포함)를 보여주고 예약을 선택해서 재생할 수 있어야 함
- 예약리스트는 예약명(시간) 형태로 표시되고 해당일은 오늘자로 고정됨
- (신규)를 추가해 재생 버튼을 누르면 예약 없음과 오늘 수업 목록(수업명(HH:mm))에서 고르도록 하고, 진행 중이거나 30분 안에 시작하는 수업을 기본 선택으로 하며 없으면 예약 없음으로 함
- (+테스트, 신규)를 추가해 오늘 구간, 표시명, 정렬, 기본 선택, payload 변환 로직을 구현함
- 를 수정해 재생 → 수업 선택 → 기존 재생 흐름으로 연결함
- (+테스트)를 수정해 metadata 에 classSlotId(예약 없으면 null), classSlotTitle, classSlotStartAt 을 담도록 함
수업 출석·심박 공용 패널 — 출석 화면·리모컨 심박 탭 (`web-app` › Booking, RemoteControl)
현상/기능
- 리모컨의 심박 탭이 재생 버튼을 누를 때 시간표 출석 화면과 다르게 동작함
- components/ClassSlotAttendance/(신규)를 추가해 useSlotAttendance(조회·즉시 저장), SlotAttendancePanel(배지·임의배정·새로고침·자동배정), SlotAttendanceTable(이름(ID)·전화·출석·심박계), slotAttendanceUtils(+테스트)를 구현함
- 심박계 선택을 바꾸면 바로 저장되도록 하고, 다른 회원 개인 기기는 숨기고 같은 수업 다른 회원이 쓰는 기기는 사용 중으로 비활성 처리함
- 가 공용 패널을 사용하도록 수정하고 운동 선택·일괄 저장 버튼을 제거함
- (신규)를 추가해 리모컨 심박 탭이 세션의 수업 ID로 같은 패널을 표시하고 예약 없음이면 심박 저장 안 됨 안내를 표시하도록 함
- getSessionStatus 에 classSlotId, classSlotTitle, classSlotStartAt 을 추가함
- 에 setBookingHeartRateDevice, clearBookingHeartRateDevice 를 추가함
- 의 slot 타입을 id 만 받도록 완화함
- , , , , types/ 를 삭제함
- types/, 에 수업 정보 필드를 추가하고 지난 예약은 같은 수업 운동과 먼저 잇고 남은 운동 제목은 수업 제목을 우선하도록 함
출석 심박계 탭을 수업 배정 방식으로 (`mobile-app` › 지점관리자 수업예약)
현상/기능
- 출석 심박계 탭이 운동 선택·일괄 반영 방식으로 동작해 수업 배정 방식과 다르게 처리됨
- 운동 목록·참가자 API 를 제거하고 수업별 심박계 변경/해제 API 를 추가함
- 를 수정해 자동배정 → 회원 카드에서 심박계 선택 시 즉시 저장되도록 하고 운동 선택 칩·일괄 반영을 제거함
- , 에 선택지(개인 기기 숨김·사용 중 표시)와 배정 반영 함수를 구현함
- 가 slot.id 를 전달하도록 하고 저장된 배정을 예약 목록·상단 숫자에 바로 반영하도록 함
- 에 수업 정보 필드를 추가하고 같은 수업끼리 먼저 잇도록 함