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

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

PPI-API 데이터 보강 — plannedStartTime · compl…: 입력: 이 문서는 이렇게 읽으면 됩니다, 주요 처리 단계: 단일 아동 경로 흐름 ( enrichUserLessonsWithPpiApi…, 결과: 읽을 때 주의할 함정 흐름
동작 흐름 요약
  1. 입력: 이 문서는 이렇게 읽으면 됩니다
  2. 주요 처리 단계: 단일 아동 경로 흐름 ( enrichUserLessonsWithPpiApi…
  3. 결과: 읽을 때 주의할 함정
문서 읽는 법 · 설명식

이 문서는 이렇게 읽으면 됩니다

PPI-API 데이터 보강 — plannedStartTime·completionStatus 코드레벨 동작 흐름의 핵심을 설명식으로 먼저 안내합니다. 기술적 결론과 원문 근거는 아래 본문에 보존되어 있습니다.

핵심 흐름 펼쳐 보기
  1. 비유와 핵심 질문으로 먼저 전체 구조를 잡습니다.
  2. 실제 컴포넌트·파일·데이터 흐름을 따라 내려갑니다.
  3. 코드 라인과 주의사항에서 구현 근거를 확인합니다.
  • 세 보강 경로
  • 공통 join — resolvePlannedClass
  • 단일 아동 경로 흐름 (enrichUserLessonsWithPpiApiData)
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 항목