prepare(" SELECT name, rank_name, sys_comp_code FROM edu_users WHERE member_id = ? AND sys_comp_code = ? LIMIT 1 "); $stmtUser->execute([$memberId, $sysCompCode]); $userRow = $stmtUser->fetch(); $userName = $userRow['name'] ?? '사용자'; $userRank = $userRow['rank_name'] ?? ''; // ── 3. 법정교육 카테고리/챕터 고정 매핑 (관리자 코드관리 기준) ─────────────── $fixedLegalNameMap = [ 'C01' => '개인정보보호', 'C02' => '직장내 괴롭힘 예방', 'C03' => '장애인 인식 개선', 'C04' => '성희롱 예방 교육', 'C05' => '퇴직금 교육', // 'C06' => '산업안전보건', ]; $normalizeLegalGroupCode = static function ($value) { $code = strtoupper(trim((string)$value)); if (preg_match('/^CA200(C\d{2})$/', $code, $m)) { return $m[1]; } if (preg_match('/^C\d{2}$/', $code)) { return $code; } return ''; }; $legalCodes = array_keys($fixedLegalNameMap); $legalRawCodes = []; foreach ($legalCodes as $code) { $legalRawCodes[] = $code; $legalRawCodes[] = 'CA200' . $code; } $legalRawCodes = array_values(array_unique($legalRawCodes)); // 카테고리명 조회 (edu_codes 테이블) $phCodes = implode(',', array_fill(0, count($legalRawCodes), '?')); $stmtCodes = $pdo->prepare(" SELECT base_code, code_name FROM edu_codes WHERE base_code IN ($phCodes) ORDER BY base_code "); $stmtCodes->execute($legalRawCodes); $codeNameMap = []; foreach ($stmtCodes->fetchAll() as $row) { $codeNameMap[$row['base_code']] = $row['code_name']; } // ── 4. 콘텐츠 + 시청 이력 조회 ─────────────────────────────────────────── $stmtContents = $pdo->prepare(" SELECT c.content_id, c.title, c.content_url, c.description, c.category_group, c.end_date, c.sort_order, COALESCE(lh.content_tm, 0) AS content_tm, COALESCE(lh.watch_tm, 0) AS watch_tm, COALESCE(lh.all_tm, 0) AS all_tm, lh.completed_at FROM edu_contents c LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id AND lh.member_id = ? AND lh.sys_comp_code = ? WHERE c.is_active = 1 AND c.category_code = 'CA10003' AND c.category_group IN ($phCodes) ORDER BY c.category_group, c.sort_order ASC, c.content_id ASC "); $stmtContents->execute(array_merge([$memberId, $sysCompCode], $legalRawCodes)); $rows = $stmtContents->fetchAll(); // ── 5. 디버그 정보 준비 ────────────────────────────────────────────── $debugData = [ 'WHERE_category_code' => 'CA10003', 'WHERE_category_group_IN' => $legalRawCodes, 'rows_count' => count($rows), 'chapter_configs' => [], ]; foreach ($legalCodes as $code) { $debugData['chapter_configs'][$code] = [ 'normalize_from' => [$code, 'CA200' . $code], ]; } // ── 6. 챕터별 그룹화 ───────────────────────────────────────────────────── $chapterMap = []; foreach ($legalCodes as $idx => $code) { $chapterMap[$code] = [ 'id' => $idx + 1, 'code' => $code, 'name' => $fixedLegalNameMap[$code] ?? ($codeNameMap[$code] ?? $code), 'lessons' => [], 'completed' => true, // 아래에서 미완료 레슨 있으면 false 'min_end_date' => null, ]; } foreach ($rows as $row) { $code = $normalizeLegalGroupCode($row['category_group'] ?? ''); if ($code === '' || !isset($chapterMap[$code])) { continue; } $contentTm = (int)$row['content_tm']; $watchTm = (int)$row['watch_tm']; $allTm = (int)$row['all_tm']; $completedAt = $row['completed_at']; $hasHistory = ($contentTm > 0 || $watchTm > 0 || !empty($completedAt)); $completed = (!empty($completedAt)); $status = !$hasHistory ? 'not-started' : ($completed ? 'completed' : 'in-progress'); $lessonEndDate = !empty($row['end_date']) ? substr($row['end_date'], 0, 10) : null; $chapterMap[$code]['lessons'][] = [ 'content_id' => $row['content_id'], 'title' => $row['title'], 'url' => $row['content_url'], 'description' => $row['description'] ?? '', 'content_tm' => $contentTm, 'watch_tm' => $watchTm, 'all_tm' => $allTm, 'completed' => $completed, 'status' => $status, 'end_date' => $lessonEndDate, 'sort_order' => (int)($row['sort_order'] ?? 0), ]; if ($lessonEndDate !== null) { if ($chapterMap[$code]['min_end_date'] === null || $lessonEndDate < $chapterMap[$code]['min_end_date']) { $chapterMap[$code]['min_end_date'] = $lessonEndDate; } } if (!$completed) { $chapterMap[$code]['completed'] = false; } } // 영상이 없는 챕터는 미완료 처리 foreach ($chapterMap as &$ch) { if (empty($ch['lessons'])) { $ch['completed'] = false; } } unset($ch); $chapters = array_values($chapterMap); // lessons를 sort_order 기준으로 재정렬 (동률 시 content_id ASC) foreach ($chapters as &$ch) { usort($ch['lessons'], static function ($a, $b) { $diff = ($a['sort_order'] ?? 0) <=> ($b['sort_order'] ?? 0); return $diff !== 0 ? $diff : ($a['content_id'] <=> $b['content_id']); }); } unset($ch); // ── 6. 전체 진도율 계산 ────────────────────────────────────────────────── $totalLessons = 0; $completedLessons = 0; foreach ($chapters as $ch) { foreach ($ch['lessons'] as $lesson) { $totalLessons++; if ($lesson['completed']) { $completedLessons++; } } } $progressRate = $totalLessons > 0 ? (int)round($completedLessons / $totalLessons * 100) : 0; // ── 7. D-day 계산 (edu_contents.end_date 최솟값 기준, 없으면 당해 12월 31일) ── $minEndDate = null; foreach ($chapters as $ch) { if (!empty($ch['min_end_date'])) { if ($minEndDate === null || $ch['min_end_date'] < $minEndDate) { $minEndDate = $ch['min_end_date']; } } } $deadline = $minEndDate ? new DateTime($minEndDate) : new DateTime(date('Y') . '-12-31'); $today = new DateTime(date('Y-m-d')); $diff = $today->diff($deadline); $dDay = ($today <= $deadline) ? (int)$diff->days : 0; $deadlineStr = $deadline->format('n월 j일'); // ── 8. JS 주입용 JSON 생성 ──────────────────────────────────────────────── $chaptersJson = json_encode($chapters, JSON_UNESCAPED_UNICODE); // window.learningConfigData 형식: { chapterId: { completed, lessons:[{completed}] } } $configData = []; foreach ($chapters as $ch) { $chapterConfig = [ 'completed' => $ch['completed'], 'lessons' => array_map( fn($l) => ['completed' => $l['completed']], $ch['lessons'] ), ]; // 프론트 코드 호환을 위해 code/id 키 모두 제공 $configData[$ch['code']] = $chapterConfig; $configData[(string)$ch['id']] = $chapterConfig; } $configJson = json_encode($configData, JSON_UNESCAPED_UNICODE);