..., 'keyword_name'=>...], ... ] (max 3) * $adminKeywords array 관리자 추천 키워드 (max 2) * $allKeywords array 모달용 전체 키워드 목록 * $firstPageVideos array 메인 6개 영상 배열 (JS video 객체 형태) * $totalMin int 총 학습시간(분) * $videosJson string JSON 직렬화 영상 배열 * $myKwJson string JSON 직렬화 내 키워드명 배열 * ───────────────────────────────────────────────────────────────── */ require_once __DIR__ . '/db_conn.php'; require_once __DIR__ . '/api_log.php'; // ════════════════════════════════════════════════════════════════ // 현재 사용자 식별 // TODO: 실제 세션 로그인 구현 후 아래 두 줄을 세션으로 교체 // $memberId = $_SESSION['member_id'] ?? null; // $sysCompCode = $_SESSION['sys_comp_code'] ?? null; // ════════════════════════════════════════════════════════════════ $memberId = 'U001'; $sysCompCode = 'COMP01'; // ──────────────────────────────────────────────────────────────── // 카테고리/서브카테고리 코드 → 한글 변환 맵 (DB 조회 전 fallback 기본값) // DB에서 edu_codes 조회 후 아래 값은 덮어쓰여짐 // ──────────────────────────────────────────────────────────────── $CATEGORY_MAP = [ 'CA10001' => '마이클래스', 'CA10002' => '온보딩', 'CA10003' => '법정교육', 'CA10004' => '리더십', 'CA10005' => '인사이트', 'CA10006' => '비즈트렌드', ]; $SUBCATE_MAP = []; /** * contents 테이블 한 행을 JS video 객체 형태 배열로 변환 * * @param array $row PDO fetchAll 행 데이터 * @param string $type 카드 타입 (main / comment / onboarding 등) * @param string $picker Pick 배지에 표시할 이름 (없으면 '') * @return array */ function mapContentRow(array $row, string $type = 'main', string $picker = '', array $catMap = [], array $subcateMap = []): array { // GROUP_CONCAT 결과 키워드 문자열 → 배열 $kwStr = $row['keywords'] ?? ''; $keywords = $kwStr !== '' ? explode(',', $kwStr) : []; // 학습 진행률 (watch_tm / content_tm × 100, 0~100 클램프) $watchTm = (float)($row['watch_tm'] ?? 0); $contentTm = (float)($row['content_tm'] ?? 0); $gauge = ($contentTm > 0) ? (int)round($watchTm / $contentTm * 100) : 0; $gauge = min(100, max(0, $gauge)); // content_url: 풀URL(https://www.youtube.com/watch?v=ID) 또는 영상ID만 저장된 경우 모두 지원 $raw = trim($row['content_url'] ?? ''); if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) { $videoId = $m[1]; // 풀 URL에서 ID 추출 } elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) { $videoId = $raw; // 정확히 11자리 유효한 YouTube ID } else { $videoId = ''; // 유효하지 않은 값 (example02, 빈값 등) } // thumbnail_url 컬럼이 있으면 우선 사용, 없으면 YouTube API로 생성 $thumbFromDb = trim($row['thumbnail_url'] ?? ''); $url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : ''; $thumbnail = $thumbFromDb !== '' ? $thumbFromDb : ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : ''); // 카테고리 코드 → 한글 $categoryCode = $row['category_code'] ?? ''; $category = $catMap[$categoryCode] ?? $categoryCode; // 서브카테고리 코드 → 한글 $groupCode = $row['category_group'] ?? ''; $subcate = $subcateMap[$groupCode] ?? $groupCode; return [ 'id' => $row['content_id'], 'url' => $url, 'thumbnail' => $thumbnail, 'category' => $category, 'category_code' => $categoryCode, 'subcate' => $subcate, 'bookmark' => (bool)($row['is_bookmarked'] ?? false), 'title' => $row['title'] ?? '', 'picker' => $picker, 'type' => $type, 'keywords' => $keywords, 'gauge' => $gauge, 'watch_tm' => (int)($row['watch_tm'] ?? 0), 'content_tm' => (int)($row['content_tm'] ?? 0), ]; } // ════════════════════════════════════════════════════════════════ // DB 데이터 조회 // ════════════════════════════════════════════════════════════════ $dbSuccess = false; try { $pdo = db_conn(); $pdo->exec("SET NAMES 'utf8mb4'"); // ── 0. edu_codes에서 카테고리/서브카테고리 맵 동적 빌드 ────── $codesRows = $pdo->query(" SELECT group_code, code, code_name FROM edu_codes WHERE is_active = 1 AND group_code IN ('CA100', 'CA200') ")->fetchAll(); foreach ($codesRows as $cr) { $key = $cr['group_code'] . $cr['code']; // e.g. CA10001, CA200L01 if ($cr['group_code'] === 'CA100') { $CATEGORY_MAP[$key] = $cr['code_name']; } else { $SUBCATE_MAP[$key] = $cr['code_name']; } } // ── 1. 사용자 기본 정보 ───────────────────────────────────── $stmtUser = $pdo->prepare(" SELECT name, rank_name FROM edu_users WHERE member_id = :mid AND sys_comp_code = :comp LIMIT 1 "); $stmtUser->execute([':mid' => $memberId, ':comp' => $sysCompCode]); $userRow = $stmtUser->fetch(); $userName = $userRow['name'] ?? '사용자'; $userRank = $userRow['rank_name'] ?? ''; // ── 2. 내 키워드 (최대 3개) ────────────────────────────────── $stmtMyKw = $pdo->prepare(" SELECT keyword_code, keyword_name FROM edu_user_keywords WHERE member_id = :mid AND sys_comp_code = :comp ORDER BY created_at DESC LIMIT 3 "); $stmtMyKw->execute([':mid' => $memberId, ':comp' => $sysCompCode]); $myKeywords = $stmtMyKw->fetchAll(); // ── 3. 관리자(법인) 추천 키워드 (최대 2개) ────────────────── $stmtAdminKw = $pdo->prepare(" SELECT rk.keyword_code, COALESCE(c.code_name, rk.keyword_code) AS keyword_name FROM edu_recommend_keywords rk LEFT JOIN edu_codes c ON c.group_code = 'KW100' AND c.code = rk.keyword_code WHERE rk.sys_comp_code = :comp AND rk.is_active = 1 ORDER BY rk.created_at DESC LIMIT 2 "); $stmtAdminKw->execute([':comp' => $sysCompCode]); $adminKeywords = $stmtAdminKw->fetchAll(); // ── 4. 모달용 전체 키워드 목록 ────────────────────────────── $stmtAllKw = $pdo->query(" SELECT code AS keyword_code, code_name AS keyword_name FROM edu_codes WHERE group_code = 'KW100' AND is_active = 1 ORDER BY code_name "); $allKeywords = $stmtAllKw ? $stmtAllKw->fetchAll() : []; // DB에 코드 데이터 없으면 하드코딩 fallback if (empty($allKeywords)) { $fallbackKwNames = [ '온보딩','성장','코칭','인물','소통','협업','AI','IT테크', '중간관리자','리더십','팔로우십','동기부여','인간관계','스킬업', '피드백','커리어','경영','경제','마인드셋','웰니스','자기개발', ]; $allKeywords = array_map( fn($k) => ['keyword_code' => $k, 'keyword_name' => $k], $fallbackKwNames ); } // ── 5. 영상 슬롯 조합 ──────────────────────────────────────── // [0] Pick (좌상단 고정 - 다른 사람이 추천한 영상) // [1] 관리자 키워드 영상 (좌중단) // [2] 관리자 키워드 영상 (좌하단) // [3] 내 키워드 영상 (우상단) // [4] 내 키워드 영상 (우중단) // [5] 내 키워드 영상 (우하단) $myKwCodes = array_column($myKeywords, 'keyword_code'); $adminKwCodes = array_column($adminKeywords, 'keyword_code'); // 5-1. Pick 영상 (다른 사람이 제안, is_offer=1, 본인 제외) $stmtPick = $pdo->prepare(" SELECT c.*, NULL AS watch_tm, NULL AS content_tm, 0 AS is_bookmarked, GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords, u.name AS picker_name FROM edu_contents c LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id LEFT JOIN edu_content_offer co ON co.offer_id = c.offer_id LEFT JOIN edu_users u ON u.member_id = co.member_id AND u.sys_comp_code = co.sys_comp_code WHERE c.is_offer = 1 AND c.is_active = 1 AND (co.member_id IS NULL OR co.member_id != :mid) GROUP BY c.content_id ORDER BY c.sort_order, RAND() LIMIT 1 "); $stmtPick->execute([':mid' => $memberId]); $pickRow = $stmtPick->fetch(); $pickVideo = $pickRow ? mapContentRow($pickRow, 'main', $pickRow['picker_name'] ?? '동료', $CATEGORY_MAP, $SUBCATE_MAP) : null; // 5-2. 관리자 키워드 관련 영상 (2개) $adminVideos = []; if (!empty($adminKwCodes)) { $ph = implode(',', array_fill(0, count($adminKwCodes), '?')); $stmtAdminV = $pdo->prepare(" SELECT c.*, lh.watch_tm, lh.content_tm, CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked, GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords FROM edu_contents c JOIN edu_content_keywords ck ON ck.content_id = c.content_id AND ck.keyword_code IN ({$ph}) LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id AND lh.member_id = ? AND lh.sys_comp_code = ? LEFT JOIN edu_content_wishlist cw ON cw.content_id = c.content_id AND cw.member_id = ? AND cw.sys_comp_code = ? AND cw.is_active = 1 WHERE c.is_active = 1 GROUP BY c.content_id ORDER BY RAND() LIMIT 2 "); $stmtAdminV->execute(array_merge( $adminKwCodes, [$memberId, $sysCompCode, $memberId, $sysCompCode] )); foreach ($stmtAdminV->fetchAll() as $r) { $adminVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP); } } // 5-3. 내 키워드 관련 영상 (3개) $myVideos = []; if (!empty($myKwCodes)) { $ph2 = implode(',', array_fill(0, count($myKwCodes), '?')); $stmtMyV = $pdo->prepare(" SELECT c.*, lh.watch_tm, lh.content_tm, CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked, GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords FROM edu_contents c JOIN edu_content_keywords ck ON ck.content_id = c.content_id AND ck.keyword_code IN ({$ph2}) LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id AND lh.member_id = ? AND lh.sys_comp_code = ? LEFT JOIN edu_content_wishlist cw ON cw.content_id = c.content_id AND cw.member_id = ? AND cw.sys_comp_code = ? AND cw.is_active = 1 WHERE c.is_active = 1 GROUP BY c.content_id ORDER BY RAND() LIMIT 3 "); $stmtMyV->execute(array_merge( $myKwCodes, [$memberId, $sysCompCode, $memberId, $sysCompCode] )); foreach ($stmtMyV->fetchAll() as $r) { $myVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP); } } // 5-4. 6 슬롯 최종 조합 // ① 키워드 매칭으로 채워진 영상 목록 $slots = [ $pickVideo, $adminVideos[0] ?? null, $adminVideos[1] ?? null, $myVideos[0] ?? null, $myVideos[1] ?? null, $myVideos[2] ?? null, ]; $usedIds = array_values(array_filter(array_map(fn($s) => $s ? $s['id'] : null, $slots))); $slotsLeft = 6 - count(array_filter($slots, fn($s) => $s !== null)); // ② 부족한 슬롯 → edu_contents 에서 직접 조회 (키워드·is_offer 무관, 랜덤) // 키워드를 선택하지 않아도 항상 DB 영상이 표시되도록 함 $extraVideos = []; if ($slotsLeft > 0) { $excClause = !empty($usedIds) ? 'AND c.content_id NOT IN (' . implode(',', array_fill(0, count($usedIds), '?')) . ')' : ''; $stmtExtra = $pdo->prepare(" SELECT c.*, lh.watch_tm, lh.content_tm, 0 AS is_bookmarked, GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords 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 = ? LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id WHERE c.is_active = 1 {$excClause} GROUP BY c.content_id ORDER BY c.sort_order, RAND() LIMIT {$slotsLeft} "); $bindParams = [$memberId, $sysCompCode]; if (!empty($usedIds)) { $bindParams = array_merge($bindParams, $usedIds); } $stmtExtra->execute($bindParams); foreach ($stmtExtra->fetchAll() as $r) { $extraVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP); } } // ③ 슬롯 순서대로 채우기: 키워드 영상 → DB 보충 → FALLBACK (최후 수단) $firstPageVideos = []; $extraIdx = 0; $fbIdx = 0; foreach ($slots as $slot) { if ($slot !== null) { $firstPageVideos[] = $slot; } elseif (isset($extraVideos[$extraIdx])) { $firstPageVideos[] = $extraVideos[$extraIdx++]; } // DB에 영상이 없으면 해당 슬롯 생략 (하드코딩 fallback 없음) } // ── 6. 총 학습시간(게이지용) ───────────────────────────────── $stmtTotal = $pdo->prepare(" SELECT COALESCE(SUM(all_tm), 0) FROM edu_learning_histories WHERE member_id = :mid AND sys_comp_code = :comp "); $stmtTotal->execute([':mid' => $memberId, ':comp' => $sysCompCode]); $totalMin = (int)$stmtTotal->fetchColumn(); $dbSuccess = true; } catch (Exception $e) { // DB 오류 발생 시 fallback 기본값 사용 $errMsg = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(); error_log('[bbs/main_data.php] DB Error: ' . $errMsg); api_log('main_data', 'DB_ERROR', ['error' => $errMsg]); $userName = '사용자'; $userRank = ''; $myKeywords = [ ['keyword_code' => 'KW004', 'keyword_name' => '인물'], ['keyword_code' => 'KW005', 'keyword_name' => '소통'], ['keyword_code' => 'KW006', 'keyword_name' => '협업'], ]; $adminKeywords = [ ['keyword_code' => 'KW019', 'keyword_name' => '마인드셋'], ['keyword_code' => 'KW020', 'keyword_name' => '웰니스'], ]; $allKeywords = array_map( fn($k) => ['keyword_code' => $k, 'keyword_name' => $k], ['온보딩','성장','코칭','인물','소통','협업','AI','IT테크', '중간관리자','리더십','팔로우십','동기부여','인간관계','스킬업', '피드백','커리어','경영','경제','마인드셋','웰니스','자기개발'] ); $firstPageVideos = []; $totalMin = 0; } // ── DB 실패 시에만 기본값 보장 (DB 성공 시 빈 키워드는 그대로 유지) ── if (!$dbSuccess) { $myKeywords = [ ['keyword_code' => 'KW004', 'keyword_name' => '인물'], ['keyword_code' => 'KW005', 'keyword_name' => '소통'], ['keyword_code' => 'KW006', 'keyword_name' => '협업'], ]; $adminKeywords = [ ['keyword_code' => 'KW019', 'keyword_name' => '마인드셋'], ['keyword_code' => 'KW020', 'keyword_name' => '웰니스'], ]; } // ════════════════════════════════════════════════════════════════ // 뷰(skin)에서 사용할 JSON 직렬화 변수 // ════════════════════════════════════════════════════════════════ $videosJson = json_encode( $firstPageVideos, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP ); $myKwJson = json_encode( array_column($myKeywords, 'keyword_name'), JSON_UNESCAPED_UNICODE );