2026-08-31 PulseUp 작업일지입니다. 오늘은 5건의 작업을 진행했습니다.
지점 심박계 관리 (`/admin/heart-rate-devices`)
현상/기능
- 회원운동예약 흐름이다. 현재 개발된 페이지가 거의 비슷한 내용을 개발되었는데 일부 빠진 부분이 있다
- 전체 흐름을 확인하고 추가/수정할 부분을 정의해. 해당 예약흐름으로 앱개발을 할 예정임을 감안해라
- → 착수 범위: ④ 심박계 자체/개인 구분
작업
- 파일 · 변경
- `src/schemas/` · `owner_type`·`owner_user_id`·`assign_order` 검증 추가. 개인 소유인데 소유회원이 없으면 400
- `src/services/` · `DeviceOwnerType` 타입 export, 프로시저 파라미터 확장, 공용 기기의 소유회원 NULL 정규화
- `src/routes/` · 목록 `owner_type` 쿼리 필터, 등록 시 소유 필드 전달
- `src/services/` · `CAPACITY_BY_DEVICE_CODE` 및 `capacityLimitedByDevice` 설정 해석 추가
- `src/services/` · 두 예약 설정 조합 케이스 추가
- `src/services/` · `DeviceOwnerType` 및 소유 관련 필드 타입 추가
- `src/services/` · `getBookingSettings` 에 `capacityLimitedByDevice` 전달
- `pages/Admin/HeartRateDevices/` · 승인 회원 목록 조회, 구분 필터 상태, 저장 전 소유회원 검증
- `.../components/` · 폼에 `owner_type`·`owner_user_id`·`assign_order` 추가, payload 정규화, `isOwnerSelectionValid`
- `.../components/` · 구분·소유회원·배정순번 입력 추가. 구분 전환 시 반대편 값 초기화
- `.../components/` · `구분`·`소유회원 / 순번` 컬럼과 구분 필터 추가
- `.../components/` · 신규. 승인(active) 지점회원을 소유자 셀렉트 옵션으로 변환
- `.../components/` · 신규. 폼 변환·payload 정규화·소유 검증 9케이스
주요 규칙
- `owner_type='personal'` 이면 `owner_user_id` 필수. 스키마·프로시저 양쪽에서 검증한다
- 한 지점에서 회원 1명은 개인 기기를 1대만 가진다. 자동배정 시 기기를 유일하게 특정하기 위함이다
- 공용 기기는 `assign_order` 오름차순으로 배정한다. `NULL` 은 마지막이다
- 개인 기기는 순번을 쓰지 않는다. 소유 회원에게 고정 배정된다
회원운동예약 흐름 — 전체 정의 및 결정 사항
흐름 단계별 현황
- 단계 · 페이지 · 상태
- ① 회원가입 · `Auth/Register` · 완료
- ② 지점선택 · `Member/BranchApply` · 2026-08-31 완료 (다중선택 UI)
- ③ 회원승인 · `Admin/BranchMemberApplications` · 2026-08-31 완료 (결제·개인심박계 옵션)
- ④ 심박계 등록 · `Admin/HeartRateDevices` · 2026-08-31 완료
- ⑤ 운동예약등록 · `Booking/ClassBookingManagement` · 2026-08-31 완료 (프론트 `min` 검증 추가)
- ⑥ 운동예약 · `Booking/ClassBookingCalendar` · 2026-08-31 완료 (자동배정·이용권 만료 검증)
확정한 정책
- 회원가입은 현재 구현대로 "가입 즉시 로그인 가능"을 유지한다
- 수업 저장 제한은 현행 유지한다. 백엔드 `assertFutureDate` 가 "현재 시각 이후"로 검증한다
- 수업 정원과 지점 공용 심박계 수량 연동은 공통코드로 제어한다
⑥ 심박계 자동배정 구조 — B안 채택
- 채택: `class_slot_heart_rate_assignments` 테이블을 신설해 슬롯 단위 배정을 관리하고
- 미채택: `class_bookings` 에 `device_id`·`slot_number` 컬럼을 추가해 세션으로 승계하는 A안
지점 회원승인·회원관리·수입분석 — 결제 도메인 (`/admin/branch-applications`, `/admin/branch-members`, `/admin/branch-revenue`)
현상/기능
- 결제 도메인 진행
- → 확정 범위: 승인 결제 + 연장결제 화면 + 지점 회원결제정보·회원추이·수입분석 화면(그래프 포함)
- → 결제 입력: 결제방법(현금/카드) + 결제금액
- → 개인심박계: 기기 ID·기기명 직접 입력하되, 비우면 자리표시자로 등록 후 나중에 채움
작업
- 파일 · 변경
- `src/schemas/` · 승인 결제·연장·회원목록·수입분석 스키마 추가
- `src/services/` · `normalizeMembershipPayment` 추가 (승인·연장 공용 검증/정규화)
- `src/services/` · 승인에 결제·심박계 파라미터 전달, 결제 관련 에러코드 3개 추가
- `src/services/` · 신규. 연장 결제·회원목록·결제이력
- `src/services/` · 신규. 월 범위 확정·빈 달 채우기·증감률 (순수 함수)
- `src/services/` · 신규. 월별 매출·만료 추이·회원 수 집계
- `src/routes/` · 승인 body 에 결제·심박계 필드 전달
- `src/routes/` · 신규. `/api/branch/members`
- `src/routes/` · 신규. `/api/branch/revenue`
- `src/` · 라우터 2개 마운트
- `src/services/` · 신규 13케이스
- `src/services/` · 신규 10케이스
- `components/membership/` · 신규. 폼 상태·종료일 자동계산·금액 정리·검증·payload
- `components/membership/` · 신규. 승인/연장 공용 결제 입력 필드
- `components/membership/` · 신규 17케이스
- `pages/Admin/BranchMemberApplications/components/` · 결제 필드 + 개인 심박계 옵션. 종료일 `readOnly` 해제
- `.../components/` · 승인 payload 확장
- `.../components/` + 테스트 · **삭제**. 날짜 계산이 `membershipPaymentModel` 로 이동해 사용처가 없어졌다
- `pages/Admin/BranchMembers/*` · 신규 5파일. 회원 목록·결제이력·연장결제 다이얼로그
- `pages/Admin/BranchRevenue/*` · 신규 5파일. 요약카드·recharts 차트 3종
- `/admin/branch-members`, `/admin/branch-revenue` 라우트 등록
주요 규칙
- 종료일은 시작일 + 등록개월로 자동 계산하되, 직접 고치면 그 값을 그대로 저장한다
- `branch_membership_details` 는 "현재 이용권" 1행이다. 연장하면 새 기간으로 덮어쓰고
- 연장 기본 시작일은 기존 종료일 다음날과 오늘 중 늦은 날이다(기간을 공백 없이 이어 붙인다)
- 개인 심박계 자동 등록 시 기기 ID 를 비우면 `pending-{회원ID}` 자리표시자로 넣고
- 수입분석은 결제가 없는 달도 0 으로 채워 보낸다. 빈 달을 빼면 꺾은선이 실제보다 완만해 보인다
- 만료임박 기준은 서버 필터와 화면 배지 모두 30일이다
알려진 기존 실패 (이번 작업과 무관)
- `(125,43)` TS2345. 이전 작업에서 확인한 기존 오류. → 2026-08-31 해결
- `admin*.` 3종: 라이브 DB 필요(`role` 컬럼 truncate)
- `auth/videos/`: `lib/prisma.js` 부재
- `videos`·`playlists` 라우트가 에서 주석 처리된 상태
회원운동예약 흐름 잔여 항목 일괄 처리
현상/기능
- 남은 항목은 어떤것들이 있지? 순서대로
- 모두 순서대로 작업진행
작업
- 파일 · 변경
- `src/services/` · `canReserveFromMembership` 에 `workoutEndsAt`·`today` 추가, `getRemainingPassDays` 신설
- `src/services/` · 신규. 배정 대상 선택·공용기기 정렬·유효정원 계산 (순수 함수)
- `src/services/` · 신규. 배정/해제/개수조회/슬롯 일괄 동기화
- `src/services/` · 신규. 가 600줄을 넘어 검증 함수 분리
- `src/services/` · 예약/변경/취소에 만료 검증·배정·해제 연결, 목록에 배정 기기 포함
- `src/routes/` · 만료 회원도 슬롯 조회는 허용(예약만 차단), 배정 동기화 라우트 추가
- `src/services/` · 신규 10케이스
- `src/services/` · 만료 검증·잔여일 케이스 추가
- `src/services/` · 배정 기기 필드 및 동기화 API 추가
- `pages/Booking/components/` · `심박계 자동배정` 버튼, 배정 기기 컬럼
- `pages/Booking/components/` · 예약 배정 일괄 반영 버튼
- `src/schemas/` · `cancelPaymentBodySchema` 추가
- `src/services/` · `cancelPayment` 추가, DTO 에 취소·환불 필드 반영
- `src/services/` · 취소 관련 에러코드 3개 추가
- `src/services/` · 월별 매출 집계에서 취소 결제 제외
- `src/routes/` · `POST /payments/:paymentId/cancel` 추가
- `pages/Admin/BranchMembers/components/` · 취소 API·타입 추가
- `pages/Admin/BranchMembers/components/` · 신규. 환불금액·사유 입력
- `pages/Admin/BranchMembers/components/` · 결제별 취소 버튼, 취소 건 표시
- `pages/Admin/BranchMembers/` · 취소 다이얼로그 연결 및 목록·이력 갱신
- 항목 · 결과
- `api-server` 타입체크 · 통과
- `web-app` 타입체크 · 통과
- `api-server` 단위 테스트 · 70건 통과
- `web-app` 전체 테스트 · 172건 통과 (28파일)
주요 규칙
- 만료 회원은 슬롯을 볼 수는 있고 예약만 막는다. 목록 자체를 감추면 연장 결제 유도가 어렵다
- 종료일이 없는 과거 회원은 예약을 허용한다. 데이터가 없다는 이유로 정상 회원을 막지 않는다
- 만료 검증은 예약 트랜잭션 안에서 한 번 더 확인한다. 목록 필터만으로는 동시성에서 새어 나간다
2. ② 지점 다중선택 UI
- 파일 · 변경
- `pages/Member/components/` · 선택 토글·전체선택·선택 유지·지점명 요약 함수 추가
- `pages/Member/components/` · 체크박스 다중선택 + 일괄 신청 버튼으로 교체
- `pages/Member/` · `selectedIds` 상태 및 일괄 신청 처리
- `pages/Member/components/` · `가능` → `예약`, `기간만료` 표시, `이용권` 컬럼 추가
- `src/services/` · `workoutEndsAt`·`remainingDays`·`isExpired` 타입 추가
- `pages/Member/components/` · 다중선택 유틸 케이스 추가
3. ⑤ SlotFormDialog 날짜 하한
- 파일 · 변경
- `pages/Booking/components/` · 시작시각 `min`=현재, 종료시각 `min`=시작시각
4. 앱 API: 내 이용권 / 내 심박계
- 파일 · 변경
- `src/services/` · 신규. `listMyPasses`, `listMyHeartRateDevices`
- `src/routes/` · `GET /my-passes`, `GET /my-heart-rate-devices` 추가
- `web-app/src/services/` · **삭제**. `/heart-rate-data` 대응 서버 라우트가 없는 죽은 코드
- `web-app/tsconfig.json` · 삭제한 파일의 `exclude` 항목 제거
- `api-server/README.md` · 심박계 라우트 목록을 실제 구현에 맞게 정정
주요 규칙
- 결제 행은 지우지 않고 `is_cancelled` 로 표시한다. 누가·언제·왜·얼마를 돌려줬는지 남긴다
- 취소하면 이용권 기간을 "남은 유효 결제 중 가장 최근 것"으로 되돌린다
- 유효 결제가 하나도 남지 않으면 지점 가입을 `cancelled` 로 닫는다
- 환불금액을 생략하면 결제금액 전액을 돌려준 것으로 본다. 부분 환불은 금액을 줄여 입력한다
- 수입분석과 회원목록 결제요약은 `is_cancelled = FALSE` 인 결제만 센다
부수적으로 고친 기존 문제
- 파일 · 내용
- `api-server/src/routes/` · 기존 TS2345 해결. `tsconfig` 의 `strict: false` 때문에 zod 가 객체 키를 전부 optional 로 추론하는 것이 원인이라, 서비스 입력을 명시적으로 옮기는 `toCreateInput` 을 추가했다
- `web-app/.../` · 날짜가 지나면 깨지는 테스트. 고정 날짜(2026-08-25)가 과거가 되어 실패했다. 먼 미래 날짜로 바꾸고 "지난 슬롯은 종료" 케이스를 추가했다
여전히 남은 기존 문제 (이번 작업 범위 밖)
- `npm run lint`: 루트 `.eslintrc.json` 이 `@typescript-eslint/recommended` 를 참조하는데 플러그인이 설치돼 있지 않아 ESLint 가 아예 뜨지 않는다. 별도 정리 필요
- DB·prisma 의존 테스트 실패 4종은 앞 절에 적은 그대로다
방문확인·결제 날짜 입력 (`MembershipPaymentFields`)
현상/기능
- 방문확인, 결제의 달력아이콘이 다크모드에서 안보인다
- 중앙정렬해주고
작업
- 파일 · 변경
- `web-app/src/index.css` · `type="date"`·`datetime-local` 도 month 와 같이 다크모드에서 아이콘을 반전. 아이콘은 세로 중앙
- `web-app/src/components/membership/` · 시작일·종료일 값을 가운데 정렬