Files
edu/bbs/main_data_moon.php
T

420 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* bbs/main_data.php
* ─────────────────────────────────────────────────────────────────
* 메인 페이지(skin/index.php) 용 데이터 조회 백엔드
*
* 이 파일은 직접 호출하지 않고 skin/index.php 에서 require_once 로 사용한다.
* 실행 후 아래 변수가 설정된다:
*
* $userName string 사용자 이름
* $userRank string 직위
* $myKeywords array 내 키워드 [ ['keyword_code'=>..., '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
);