420 lines
19 KiB
PHP
420 lines
19 KiB
PHP
<?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
|
||
);
|