Initial commit: 교육 프로젝트 배포

This commit is contained in:
송대일
2026-07-01 18:32:42 +09:00
commit be6dccd120
1483 changed files with 5082202 additions and 0 deletions
+431
View File
@@ -0,0 +1,431 @@
<?php
//require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// ---------------------------------------------------------
// TODO: 실제 로그인 세션 연동 후 교체
// ---------------------------------------------------------
$memberId = 'U001';
$sysCompCode = 'COMP01';
// ---------------------------------------------------------
// 디버그 설정
// true : 바인딩 적용된 SQL 출력 후 종료
// false : 정상 실행
// ---------------------------------------------------------
$DEBUG_SQL = false;
// ---------------------------------------------------------
// 요청 파라미터
// ---------------------------------------------------------
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 2;
$offset = ($page - 1) * $limit;
$category = trim($_GET['category'] ?? ''); // 예: CA200L01
$sort = trim($_GET['sort'] ?? 'view'); // view / latest / seen / unseen
// ---------------------------------------------------------
// 기본 코드맵 (DB코드 조회 실패 대비)
// ---------------------------------------------------------
$CATEGORY_MAP = [
'CA10004' => '리더십',
];
$SUBCATE_MAP = [
'CA200L01' => '리더십 시작하기',
'CA200L02' => '셀프 리더십',
'CA200L03' => '팀 리더십',
'CA200L04' => '실전조직 리더십',
'CA200L05' => '리더케이스탐구',
];
// ---------------------------------------------------------
// 공통 함수
// ---------------------------------------------------------
function h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function mapContentRow(array $row, array $catMap = [], array $subcateMap = []): array
{
$kwStr = $row['keywords'] ?? '';
$keywords = $kwStr !== '' ? explode(',', $kwStr) : [];
$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));
$raw = trim($row['content_url'] ?? '');
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim($row['thumbnail_url'] ?? '');
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
$categoryCode = $row['category_code'] ?? '';
$groupCode = $row['category_group'] ?? '';
return [
'id' => $row['content_id'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'category' => $catMap[$categoryCode] ?? $categoryCode,
'category_code' => $categoryCode,
'subcate' => $subcateMap[$groupCode] ?? $groupCode,
'bookmark' => !empty($row['is_bookmarked']),
'title' => $row['title'] ?? '',
'keywords' => $keywords,
'gauge' => $gauge,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'view_cnt' => (int)($row['view_cnt'] ?? 0),
];
}
/**
* 디버그용: 바인딩값을 SQL에 치환한 문자열 생성
* 주의: 실제 실행 SQL을 PDO가 제공하는 것은 아니고, 보기 쉽게 만든 디버그용 문자열임
*/
function buildDebugSql(string $sql, array $params): string
{
// :member_id 와 :member_id2 같은 이름 충돌 방지
uksort($params, function ($a, $b) {
return strlen((string)$b) <=> strlen((string)$a);
});
foreach ($params as $key => $value) {
if ($value === null) {
$replace = 'NULL';
} elseif (is_int($value) || is_float($value)) {
$replace = (string)$value;
} else {
$replace = "'" . str_replace("'", "''", (string)$value) . "'";
}
$sql = str_replace($key, $replace, $sql);
}
return $sql;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ---------------------------------------------------------
// edu_codes 에서 실제 코드맵 조회
// ---------------------------------------------------------
$stmtCode = $pdo->query("
SELECT group_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND group_code IN ('CA100', 'CA200')
");
if ($stmtCode) {
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $cr) {
$key = $cr['group_code'] . $cr['code'];
if ($cr['group_code'] === 'CA100') {
$CATEGORY_MAP[$key] = $cr['code_name'];
} elseif ($cr['group_code'] === 'CA200') {
$SUBCATE_MAP[$key] = $cr['code_name'];
}
}
}
// ---------------------------------------------------------
// 이 페이지는 리더십 대분류 전용
// 탭은 category_group 으로 필터링
// category 파라미터는 CA200L01 형태를 권장
// ---------------------------------------------------------
$categoryGroup = '';
if ($category !== '' && strtolower($category) !== 'all') {
if (preg_match('/^L\d{2}$/', $category)) {
$categoryGroup = 'CA200' . $category; // L01 -> CA200L01
} else {
$categoryGroup = $category; // 이미 CA200L01이면 그대로
}
}
// ---------------------------------------------------------
// 정렬 조건
// ---------------------------------------------------------
switch ($sort) {
case 'latest':
$orderBy = "
COALESCE(c.updated_at, c.created_at) DESC,
c.sort_order ASC,
c.content_id DESC
";
break;
case 'seen':
$orderBy = "
CASE WHEN lh.content_id IS NOT NULL THEN 0 ELSE 1 END ASC,
lh.last_viewed_at DESC,
c.content_id DESC
";
break;
case 'unseen':
$orderBy = "
CASE WHEN lh.content_id IS NULL THEN 0 ELSE 1 END ASC,
COALESCE(c.updated_at, c.created_at) DESC,
c.content_id DESC
";
break;
case 'view':
default:
$orderBy = "
COALESCE(vs.view_cnt, 0) DESC,
COALESCE(vs.total_all_tm, 0) DESC,
COALESCE(c.updated_at, c.created_at) DESC,
c.content_id DESC
";
break;
}
// ---------------------------------------------------------
// 기본 조건
// ---------------------------------------------------------
$where = [];
$bind = [];
$where[] = "(c.is_active = '1' OR c.is_active = 'Y')";
$where[] = "(c.start_date IS NULL OR c.start_date <= CURDATE())";
$where[] = "(c.end_date IS NULL OR c.end_date >= CURDATE())";
// 리더십 대분류 고정
$where[] = "c.category_code = 'CA10004'";
// 탭 필터
if ($categoryGroup !== '') {
$where[] = "c.category_group = :category_group";
$bind[':category_group'] = $categoryGroup;
}
$whereSql = implode("\n AND ", $where);
// ---------------------------------------------------------
// TOTAL COUNT
// ---------------------------------------------------------
$countSql = "
SELECT COUNT(*)
FROM edu_contents c
WHERE {$whereSql}
";
$stmtCount = $pdo->prepare($countSql);
foreach ($bind as $key => $value) {
$stmtCount->bindValue($key, $value, PDO::PARAM_STR);
}
if ($DEBUG_SQL) {
$countDebugSql = buildDebugSql($countSql, $bind);
echo '<pre>';
echo "[COUNT SQL]\n\n";
// echo htmlspecialchars($countDebugSql, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo $countDebugSql;
echo "\n\n------------------------------\n\n";
}
$stmtCount->execute();
$totalCount = (int)$stmtCount->fetchColumn();
// ---------------------------------------------------------
// 목록 조회
// ---------------------------------------------------------
$sql = "
SELECT
c.content_id,
c.category_code,
c.category_group,
c.title,
c.content_url,
c.thumbnail_url,
c.sort_order,
c.created_at,
c.updated_at,
lh.content_id AS lh_content_id,
lh.watch_tm,
lh.content_tm,
lh.last_viewed_at,
CASE
WHEN cw.content_id IS NOT NULL THEN 1
ELSE 0
END AS is_bookmarked,
COALESCE(vs.view_cnt, 0) AS view_cnt,
COALESCE(vs.total_all_tm, 0) AS total_all_tm,
GROUP_CONCAT(
DISTINCT 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 = :member_id
AND lh.sys_comp_code = :sys_comp_code
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = :member_id2
AND cw.sys_comp_code = :sys_comp_code2
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN (
SELECT
content_id,
COUNT(*) AS view_cnt,
COALESCE(SUM(all_tm), 0) AS total_all_tm
FROM edu_learning_histories
GROUP BY content_id
) vs
ON vs.content_id = c.content_id
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE {$whereSql}
GROUP BY c.content_id
ORDER BY {$orderBy}
LIMIT :limit OFFSET :offset
";
/*LIMIT :limit OFFSET :offset*/
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':member_id2', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code2', $sysCompCode, PDO::PARAM_STR);
foreach ($bind as $key => $value) {
$stmt->bindValue($key, $value, PDO::PARAM_STR);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
if ($DEBUG_SQL) {
$debugParams = [
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':member_id2' => $memberId,
':sys_comp_code2' => $sysCompCode,
':limit' => $limit,
':offset' => $offset,
];
if (!empty($bind)) {
$debugParams = array_merge($debugParams, $bind);
}
$debugSql = buildDebugSql($sql, $debugParams);
echo "[LIST SQL]\n\n";
//echo htmlspecialchars($debugSql, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo $debugSql;
echo '</pre>';
}
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($DEBUG_SQL) {
print_r($rows);
exit;
}
// ---------------------------------------------------------
// HTML 조각 생성
// ---------------------------------------------------------
ob_start();
foreach ($rows as $row) {
$item = mapContentRow($row, $CATEGORY_MAP, $SUBCATE_MAP);
$contentId = h($item['id']);
$title = h($item['title']);
$thumb = h($item['thumbnail']);
$categoryNm= h($item['category']);
$subcateNm = h($item['subcate']);
$checked = $item['bookmark'] ? ' checked' : '';
$viewUrl = "/edu/video_view.php?content_id=" . rawurlencode($item['id']);
?>
<li class="video-item">
<a href="<?= h($viewUrl) ?>" class="card-link">
<label class="bookmark" for="like_chk_<?= $contentId ?>" onclick="event.preventDefault(); event.stopPropagation();">
<input type="checkbox" id="like_chk_<?= $contentId ?>" title="좋아요"<?= $checked ?> />
</label>
<div class="item-thumb">
<?php if ($thumb !== '') { ?>
<img src="<?= $thumb ?>" alt="<?= $title ?>" loading="lazy" />
<?php } else { ?>
<img src="/edu/img/video/no-image.png" alt="<?= $title ?>" loading="lazy" />
<?php } ?>
</div>
<div class="item-info">
<strong class="item-title"><?= $title ?></strong>
<span class="item-desc"><?= $subcateNm !== '' ? $subcateNm : $categoryNm ?></span>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
echo json_encode([
'success' => true,
'html' => $html,
'total_count' => $totalCount
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_video_list.php] ' . $e->getMessage());
echo json_encode([
'success' => false,
'html' => '',
'total_count' => 0
], JSON_UNESCAPED_UNICODE);
}