Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
<?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
|
||||
);
|
||||
Reference in New Issue
Block a user