PPI-API 데이터 보강 — plannedStartTime · completionStatus 코드레벨 동작 흐름

마지막 업데이트 2026-07-22

PPI-API 보강 enrichLessonsWithPpiApiData enrichUserLessonsWithPpiApiData resolvePlannedClass plannedStartTime batchGetLessons KST 고정 missingTodayClasses 백로그 P0 #2

TL;DR

PPI는 두 개의 진실 소스를 합친다 — DynamoDB Lesson(진도·세션·예외 시간)과 ppi-api classes(정규 스케줄·상태·담당 진행자·그룹). 보강(enrichment)은 이 둘을 join해 각 회차에 completionStatus·classDate/Time·groupId·담당자·그리고 가장 중요한 plannedStartTime(실제 입장 기준 시각)을 붙인다. 이 값이 P0 #1 입장 제어의 모든 시간 판정 입력이다.

호출 주체에 따라 세 경로로 갈린다 — 단일 아동(enrichUserLessonsWithPpiApiData, child/member auth), 관리자 전체(enrichLessonsWithPpiApiData, admin auth), 모니터 대시보드(getMonitorDashboardPlannedLessons). 공통 join 규칙은 resolvePlannedClass가 담당하고, 가장 까다로운 부분은 그룹 vs 1:1 시각 분리local 우선 병합이다.

세 보강 경로

① 단일 아동 (입장)

  • enrichUserLessonsWithPpiApiData lesson-ppi-api-utils.ts:248
  • child auth → getChildScheduledClasses / member auth → getScheduledClasses(+childId 필터)
  • missingTodayClasses 산출 → P0 #1 LESSON_NOT_PROVISIONED
  • 상태: effectiveLessonStatus ?? status

② 관리자 전체

  • enrichLessonsWithPpiApiData :49
  • getAdminClasses (오늘 범위)
  • ppi-api-only 클래스는 synthetic lesson 생성
  • 시간 필터 + includeCompleted + terminal 필터
  • 상태: raw cls.status(effective 아님)

③ 모니터 대시보드

  • getMonitorDashboardPlannedLessons monitor-dashboard-planned-lessons.ts:134
  • GET /api/monitor-dashboard/planned-lessons
  • getAdminClasses + isWithinRange + toPlannedLesson
  • 아동 이름 resolve(getUserProfile)

공통 join — resolvePlannedClass

모든 경로가 쓰는 핵심. lib/planned-class-resolver.ts:33. ppi-api 한 건(date+time+status+groupId?)과 로컬 lesson.scheduledTime을 합쳐 입장 기준 시각을 만든다.

// date(YYYYMMDD)+time(HH:MM) → KST(+09:00) ISO
const classDateTime = parseClassDateTimeToIso(cls.date, cls.time);
// ★ 그룹이면 scheduledTime(1:1 예외 개방) 무시 → 정규 시각 사용
const scheduledTime = cls.groupId ? undefined : lesson?.scheduledTime;
return {
  plannedStartTime: scheduledTime ?? classDateTime,  // 입장 기준 시각
  isTempOpen: !!scheduledTime,                         // 1:1 임시 개방
  completionStatus: cls.status, classDate: cls.date, classTime: cls.time, ...
};
케이스plannedStartTimeisTempOpen
정규 1:1 / 그룹ppi-api classDateTimefalse
1:1 임시 개방 (scheduledTime 있고 groupId 없음)로컬 scheduledTimetrue

단일 아동 경로 흐름 (enrichUserLessonsWithPpiApiData)

1
인증 분기 → ppi-api 호출
:257-301
accessToken 없으면 보강 skip(원본 반환). child면 getChildScheduledClasses(token, userId, today), member면 getScheduledClasses(token, today)childId === userId 필터. status≠200이면 warn + skip.
2
상태·그룹 맵 구성 → 전체 회차 부착
:304-332
statusMap(lessonIndex → effectiveLessonStatus ?? statusgroupIdMap 만들어 모든 enrichedLessonscompletionStatus·classDate/Time·groupId 부착.
3
plannedStartTime 계산 + DynamoDB 폴백
:334-403
각 ppi-api 클래스에 대해 enrichedLessons에서 회차를 찾고, 없으면 getLesson 폴백. 찾으면 resolvePlannedClass로 patch. isLegacyMode면 skip.
↓ 폴백도 실패하면
4
missingTodayClasses 수집
:365-382
ppi-api엔 오늘 수업이 있는데 DynamoDB Lesson 레코드가 양쪽 다 없으면 {lessonIndex, plannedStartTime} push + warn. → P0 #1이 이걸 받아 LESSON_NOT_PROVISIONED로 구분.

관리자 경로의 추가 동작 (enrichLessonsWithPpiApiData)

KST 시각 변환

모든 날짜 계산이 Asia/Seoul 고정이다. 서버 TZ와 무관하게 동작해야 하므로 dayjs.tz를 쓴다. lesson-ppi-api-utils.ts:22-29:

formatDateToYYYYMMDD(date) => dayjs(date).tz("Asia/Seoul").format("YYYYMMDD");
parseClassDateTime(date, time) => dayjs.tz(`${yyyy}-${mm}-${dd} ${time}`, "Asia/Seoul").toDate();

코드 맵 — 파일별 역할

파일역할
lib/lesson-ppi-api-utils.tsenrichUserLessonsWithPpiApiData(단일 아동)·enrichLessonsWithPpiApiData(관리자)·KST 유틸·isTerminalStatus
lib/planned-class-resolver.tsresolvePlannedClass — 그룹/1:1 분기, plannedStartTime·isTempOpen
lib/monitor-dashboard-planned-lessons.tsgetMonitorDashboardPlannedLessons — 모니터 대시보드용, 범위 필터·아동 이름 resolve·toPlannedLesson
lib/ppi-api-client.tsgetAdminClasses·getScheduledClasses·getChildScheduledClasses — ppi-api 호출 래퍼
lib/db-queries.tsgetLesson·batchGetLessons — DynamoDB 조회/배치

읽을 때 주의할 함정

1. 두 enrichment 함수의 상태 소스가 다르다. 단일 아동 경로는 effectiveLessonStatus ?? status를 쓰지만, 관리자 경로는 raw cls.status를 쓴다(:82). 이유: 예약 수업 목록은 오늘 실제 상태가 필요해서, 같은 lessonIndex에 미래 pending 수업이 있어도 오늘 취소된 수업은 취소로 유지돼야 한다. 두 함수를 혼동해 상태가 다르게 나온다고 의심하기 전에 어느 경로인지 확인할 것.

2. 그룹이면 scheduledTime을 버린다. resolvePlannedClasscls.groupId가 있으면 로컬 scheduledTime을 무시하고 정규 classDateTime을 쓴다(planned-class-resolver.ts:38). 그룹 수업에 1:1 임시 개방 시각이 잘못 끼어드는 것을 막는 분기이므로 통합 금지.

3. 관리자 경로의 synthetic lesson은 빈 껍데기. ppi-api-only 클래스로 만든 가짜 Lesson은 activities:[]·createdAt:0이다(:184-198). 목록·시간 판정엔 쓰이지만 진도/세션 데이터는 없으므로, 이 객체로 진도를 읽으려 하면 안 된다.

4. accessToken이 없으면 보강 자체를 건너뛴다. 두 함수 모두 토큰 없으면 원본 lessons를 그대로 반환한다(:259, :55). 이 경우 plannedStartTime·completionStatus가 안 붙어 P0 #1 입장 판정이 "예정된 수업 없음" 경로로 빠질 수 있다.

5. 모든 시각은 KST 고정. formatDateToYYYYMMDD·parseClassDateTime은 서버 TZ가 아닌 Asia/Seoul 기준이다(:22-29). "오늘"의 경계가 서버 로컬 자정이 아니라 KST 자정임을 전제로 디버깅할 것.

6. batchGetLessons는 키 누락을 조용히 생략한다. 관리자 경로는 N건을 BatchGetItem으로 묶는데, 존재하지 않는 키는 결과 맵에 안 들어온다(:157). 단일 아동 경로의 missingTodayClasses처럼 누락을 추적하는 장치가 관리자 경로엔 없으므로, 관리자 목록에서 일부 회차가 빠지면 이 경로를 의심.

관련 문서

수업 입장 제어 — 시간 검증·can-enter (P0 #1) — plannedStartTime·missingTodayClasses의 소비처 누적 경과시간 추적 (P0 #3) — sessionSummary와 함께 회차에 부착되는 데이터 클래스 관리/스케줄링 — scheduledTime(예외 개방)을 만드는 관리자 흐름 운영·관리·인프라 문서화 백로그 — 이 문서는 P0 #2 항목