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
+533
View File
@@ -0,0 +1,533 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../bbs/auth.php';
edu_require_login();
require_once __DIR__ . '/../bbs/db_conn.php';
$q = trim((string)($_GET['q'] ?? ''));
$results = [];
function h(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function parse_youtube_id(string $raw): string
{
if (preg_match('/(?:v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
return $m[1];
}
if (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
return $raw;
}
return '';
}
function make_thumb_url(string $thumb, string $contentUrl): string
{
$thumb = trim($thumb);
if ($thumb !== '') {
return $thumb;
}
$videoId = parse_youtube_id(trim($contentUrl));
if ($videoId !== '') {
return 'https://img.youtube.com/vi/' . $videoId . '/sddefault.jpg';
}
return '/img/video/img_thumb_01.png';
}
function highlight_keyword(string $text, string $keyword): string
{
$escaped = h($text);
$keyword = trim($keyword);
if ($keyword === '') {
return $escaped;
}
$pattern = '/' . preg_quote(h($keyword), '/') . '/iu';
return (string)preg_replace($pattern, '<span class="search-highlight">$0</span>', $escaped);
}
try {
$pdo = db_conn();
$memberId = trim((string)($_SESSION['member_id'] ?? ''));
$sysCompCode = trim((string)($_SESSION['sys_comp_code'] ?? ''));
$sql = "
SELECT
c.content_id,
c.title,
c.description,
c.description2,
c.content_url,
c.thumbnail_url,
c.category_code,
c.category_group,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(vs.view_cnt, 0) AS view_cnt,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked
FROM edu_contents c
LEFT JOIN edu_codes ec
ON ec.base_code = c.category_code
AND ec.group_code = 'CA100'
AND ec.is_active = 1
LEFT JOIN (
SELECT
content_id,
COUNT(DISTINCT member_id) AS view_cnt
FROM edu_learning_histories
GROUP BY content_id
) vs
ON vs.content_id = c.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.member_id = :mid
AND cw.sys_comp_code = :sc
AND cw.is_active = 1
AND cw.content_id = c.content_id
WHERE c.is_active = 1
AND (
:kw = ''
OR c.title LIKE :like_kw1
OR c.description LIKE :like_kw2
OR c.description2 LIKE :like_kw3
)
ORDER BY c.updated_at DESC, c.created_at DESC, c.content_id DESC
LIMIT 300
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':mid' => $memberId,
':sc' => $sysCompCode,
':kw' => $q,
':like_kw1' => '%' . $q . '%',
':like_kw2' => '%' . $q . '%',
':like_kw3' => '%' . $q . '%',
]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) {
error_log('[search_result] ' . $e->getMessage());
$results = [];
}
$totalCount = count($results);
// 카테고리별 그룹화
$groupedResults = [];
$categoryMeta = []; // category_code => ['name' => string, 'count' => int]
foreach ($results as $row) {
$catCode = (string)($row['category_code'] ?? 'etc');
$catName = (string)($row['category_name'] ?? $row['category_code'] ?? '기타');
if (!isset($groupedResults[$catCode])) {
$groupedResults[$catCode] = [];
$categoryMeta[$catCode] = ['name' => $catName, 'count' => 0];
}
$groupedResults[$catCode][] = $row;
$categoryMeta[$catCode]['count']++;
}
$videosForJs = array_map(static function (array $row): array {
$contentUrl = (string)($row['content_url'] ?? '');
$thumb = make_thumb_url((string)($row['thumbnail_url'] ?? ''), $contentUrl);
return [
'id' => (string)($row['content_id'] ?? ''),
'content_id' => (string)($row['content_id'] ?? ''),
'url' => $contentUrl,
'content_url' => $contentUrl,
'thumbnail' => $thumb,
'category' => (string)($row['category_name'] ?? $row['category_code'] ?? ''),
'category_name' => (string)($row['category_name'] ?? $row['category_code'] ?? ''),
'category_code' => (string)($row['category_code'] ?? ''),
'subcate' => (string)($row['category_group'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'description' => trim((string)($row['description'] ?? '')),
'description2' => trim((string)($row['description2'] ?? '')),
'bookmark' => ((string)($row['is_bookmarked'] ?? '0') === '1'),
'type' => 'main',
'keywords' => [],
'watch_tm' => 0,
'content_tm' => 0,
'all_tm' => 0,
'view_cnt' => (int)($row['view_cnt'] ?? 0),
];
}, $results);
$videosJson = json_encode($videosForJs, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP);
?>
<!doctype html>
<!-- dynamic-search-result-v2 -->
<html lang="ko">
<head>
<?php include(__DIR__ . '/_include/_head.php') ?>
<link rel="stylesheet" type="text/css" href="/css/main.css" />
<style>
.search-result .search-highlight {
color: #ff7a00;
font-weight: 700;
}
.search-result .item-desc-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 8px;
}
.search-result .item-view-count {
margin-left: auto;
font-size: 13px;
color: #6f6f6f;
line-height: 1.5;
white-space: nowrap;
}
</style>
</head>
<body>
<div class="wrap search-result">
<?php include(__DIR__ . '/_include/_header.php') ?>
<div class="container">
<div class="search-result-wrap">
<div class="page-header">
<div class="page-title">
<h3 class="blind">검색결과</h3>
<p class="search-summary">
'<?php echo h($q); ?>'에 대한 검색결과가 <em class="search-count"><?php echo (int)$totalCount; ?></em>건 있습니다.
</p>
</div>
</div>
<?php if ($q === ''): ?>
<p style="padding: 20px 0;">검색어를 입력해주세요.</p>
<?php elseif ($totalCount === 0): ?>
<p style="padding: 20px 0;">검색 결과가 없습니다.</p>
<?php else: ?>
<!-- 필터 탭 -->
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all">
전체 <span class="count"><?php echo (int)$totalCount; ?></span>
</button>
<?php foreach ($categoryMeta as $catCode => $meta): ?>
<button class="filter-tab" data-filter="<?php echo h($catCode); ?>">
<?php echo h($meta['name']); ?> <span class="count"><?php echo (int)$meta['count']; ?></span>
</button>
<?php endforeach; ?>
</div>
<!-- 검색 결과 섹션 -->
<div class="search-sections" id="videoCardsContainer">
<!-- [전체] 평면 목록: 카테고리 구분 없이 전체 결과를 순서대로 출력 -->
<section class="content-section" data-section="all">
<div class="section-header">
<h3 class="section-title">전체 <span class="section-count"><?php echo (int)$totalCount; ?></span></h3>
<!-- <div class="list-options">
<div class="select-wrap">
<select class="select-sort" title="정렬">
<option>조회수</option>
<option selected>업데이트</option>
<option>내가본컨텐츠</option>
<option>안본컨텐츠</option>
</select>
</div>
</div> -->
</div>
<ul class="video-grid">
<?php foreach ($results as $idx => $row):
$id = (string)($row['content_id'] ?? '');
$title = (string)($row['title'] ?? '');
$description = trim((string)($row['description'] ?? ''));
$description2 = trim((string)($row['description2'] ?? ''));
$thumb = make_thumb_url((string)($row['thumbnail_url'] ?? ''), (string)($row['content_url'] ?? ''));
$category = (string)($row['category_name'] ?? $row['category_code'] ?? '');
$bookmark = ((string)($row['is_bookmarked'] ?? '0') === '1');
$viewCnt = (int)($row['view_cnt'] ?? 0);
$bookmarkId = 'like_chk_all_' . ($idx + 1);
$titleHighlighted = highlight_keyword($title, $q);
$descHighlighted = highlight_keyword($description, $q);
$desc2Highlighted = highlight_keyword($description2, $q);
?>
<li class="video-item">
<a href="#" class="card-link card" data-video-id="<?php echo h($id); ?>">
<label class="bookmark" for="<?php echo h($bookmarkId); ?>" onclick="event.stopPropagation();"><input type="checkbox" id="<?php echo h($bookmarkId); ?>" <?php echo $bookmark ? 'checked' : ''; ?> title="좋아요"></label>
<div class="item-thumb">
<img src="<?php echo h($thumb); ?>" alt="" />
</div>
<div class="item-info">
<strong class="item-title"><?php echo $titleHighlighted; ?></strong>
<?php if ($description !== '' || $description2 !== ''): ?>
<div class="item-desc" style="color:#6f6f6f; font-size:13px; line-height:1.5;">
<?php if ($description !== ''): ?><p class="desc-1"><?php echo $descHighlighted; ?></p><?php endif; ?>
<?php if ($description2 !== ''): ?><p class="desc-2" style="margin-top:2px;"><?php echo $desc2Highlighted; ?></p><?php endif; ?>
</div>
<?php endif; ?>
<div class="tag-list item-desc-wrapper">
<span class="tag"><?php echo h($category); ?></span>
<span class="item-view-count">시청수 <?php echo (int)$viewCnt; ?> 회</span>
</div>
</div>
</a>
</li>
<?php endforeach; ?>
</ul>
</section>
<!-- [카테고리별] 섹션: 기본 hidden, 특정 탭 선택 시 표시 -->
<?php
$globalIdx = 0;
foreach ($groupedResults as $catCode => $catRows):
$catName = $categoryMeta[$catCode]['name'];
$catCount = $categoryMeta[$catCode]['count'];
?>
<section class="content-section" data-section="<?php echo h($catCode); ?>" style="display:none;">
<div class="section-header">
<h3 class="section-title"><?php echo h($catName); ?> <span class="section-count"><?php echo (int)$catCount; ?></span></h3>
<!-- <div class="list-options">
<div class="select-wrap">
<select class="select-sort" title="정렬">
<option>조회수</option>
<option selected>업데이트</option>
<option>내가본컨텐츠</option>
<option>안본컨텐츠</option>
</select>
</div>
</div> -->
</div>
<ul class="video-grid">
<?php foreach ($catRows as $row):
$globalIdx++;
$id = (string)($row['content_id'] ?? '');
$title = (string)($row['title'] ?? '');
$description = trim((string)($row['description'] ?? ''));
$description2 = trim((string)($row['description2'] ?? ''));
$thumb = make_thumb_url((string)($row['thumbnail_url'] ?? ''), (string)($row['content_url'] ?? ''));
$category = (string)($row['category_name'] ?? $row['category_code'] ?? '');
$bookmark = ((string)($row['is_bookmarked'] ?? '0') === '1');
$viewCnt = (int)($row['view_cnt'] ?? 0);
$bookmarkId = 'like_chk_search_' . $globalIdx;
$titleHighlighted = highlight_keyword($title, $q);
$descHighlighted = highlight_keyword($description, $q);
$desc2Highlighted = highlight_keyword($description2, $q);
?>
<li class="video-item">
<a href="#" class="card-link card" data-video-id="<?php echo h($id); ?>">
<label class="bookmark" for="<?php echo h($bookmarkId); ?>" onclick="event.stopPropagation();"><input type="checkbox" id="<?php echo h($bookmarkId); ?>" <?php echo $bookmark ? 'checked' : ''; ?> title="좋아요"></label>
<div class="item-thumb">
<img src="<?php echo h($thumb); ?>" alt="" />
</div>
<div class="item-info">
<strong class="item-title"><?php echo $titleHighlighted; ?></strong>
<?php if ($description !== '' || $description2 !== ''): ?>
<div class="item-desc" style="color:#6f6f6f; font-size:13px; line-height:1.5;">
<?php if ($description !== ''): ?><p class="desc-1"><?php echo $descHighlighted; ?></p><?php endif; ?>
<?php if ($description2 !== ''): ?><p class="desc-2" style="margin-top:2px;"><?php echo $desc2Highlighted; ?></p><?php endif; ?>
</div>
<?php endif; ?>
<div class="tag-list item-desc-wrapper">
<span class="tag"><?php echo h($category); ?></span>
<span class="item-view-count">시청수 <?php echo (int)$viewCnt; ?> 회</span>
</div>
</div>
</a>
</li>
<?php endforeach; ?>
</ul>
</section>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script src="/js/learning/config.js" defer></script>
<script src="/js/learning/markers.js" defer></script>
<script src="/js/learning/modal.js" defer></script>
<script src="/js/bridges/learning-modal-bridge.js" defer></script>
<script>
window.puzzleConfig = {
...(window.puzzleConfig || {}),
COMPLETION_MODE: 'COMPLETED',
CURRENT_MEMBER_ID: <?php echo json_encode($memberId, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP); ?>,
};
</script>
<script src="/js/puzzle-onboarding.js" defer></script>
<script src="/js/bridges/onboarding-modal-bridge.js" defer></script>
<script src="/js/main/Videomodalmanager.js" defer></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const currentVideos = <?= $videosJson ?: '[]' ?>;
const container = document.getElementById('videoCardsContainer');
// ── 모달 초기화 ──
let modalManager = null;
if (container && Array.isArray(currentVideos) && currentVideos.length > 0 && typeof VideoModalManager !== 'undefined') {
modalManager = new VideoModalManager({ videos: currentVideos });
}
// ── 필터 탭 ──
const filterTabs = document.querySelectorAll('.filter-tab');
filterTabs.forEach(function (tab) {
tab.addEventListener('click', function () {
filterTabs.forEach(function (t) { t.classList.remove('active'); });
this.classList.add('active');
const filter = this.dataset.filter;
document.querySelectorAll('.content-section').forEach(function (section) {
const sec = section.dataset.section;
if (filter === 'all') {
// 전체 탭: all 섹션만 표시, 카테고리 섹션 모두 숨김
section.style.display = (sec === 'all') ? 'block' : 'none';
} else {
// 카테고리 탭: all 섹션 숨기고 해당 카테고리만 표시
section.style.display = (sec === filter) ? 'block' : 'none';
}
});
});
});
// ── 북마크 ──
if (!container) return;
async function saveWishlist(videoId, isActive) {
const params = new URLSearchParams({
content_id: String(videoId || ''),
is_active: isActive ? '1' : '0',
});
const res = await fetch('/bbs/api/save_wishlist.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: params.toString(),
});
return res.json();
}
function setBookmarkState(videoId, isBookmarked) {
const id = String(videoId || '');
currentVideos.forEach(function (video) {
if (String(video?.id ?? '') === id) { video.bookmark = !!isBookmarked; }
});
container.querySelectorAll('.card[data-video-id]').forEach(function (cardEl) {
if (String(cardEl.getAttribute('data-video-id') || '') !== id) return;
const checkbox = cardEl.querySelector('.bookmark input[type="checkbox"]');
if (checkbox) checkbox.checked = !!isBookmarked;
});
if (modalManager && modalManager.config) { modalManager.config.videos = currentVideos; }
}
function getVideoDataById(videoId) {
const id = String(videoId || '');
return currentVideos.find(function (v) {
return String(v?.id ?? '') === id || String(v?.content_id ?? '') === id;
}) || null;
}
function toBridgeInfo(videoData) {
const contentId = String(videoData?.content_id ?? videoData?.id ?? '');
const description = [
String(videoData?.description || '').trim(),
String(videoData?.description2 || '').trim(),
].filter(Boolean).join('\n');
return {
contentId: contentId,
videoUrl: String(videoData?.content_url ?? videoData?.url ?? ''),
title: String(videoData?.title ?? ''),
description: description,
categoryName: String(videoData?.category_name ?? videoData?.category ?? ''),
subCategory: String(videoData?.subcate ?? ''),
watchTm: Number(videoData?.watch_tm || 0),
contentTm: Number(videoData?.content_tm || 0),
bookmark: !!videoData?.bookmark,
};
}
async function openSearchResultVideoByCategory(videoData) {
const categoryCode = String(videoData?.category_code || '').toUpperCase();
const bridgeInfo = toBridgeInfo(videoData);
// 브리지가 준비된 카테고리부터 우선 적용한다.
if (categoryCode === 'CA10006' && typeof window._biztrendOpenModal === 'function') {
window._biztrendOpenModal(bridgeInfo);
return;
}
if (categoryCode === 'CA10005' && typeof window._insightOpenModal === 'function') {
window._insightOpenModal(bridgeInfo);
return;
}
if (categoryCode === 'CA10004' && typeof window._leadershipOpenModal === 'function') {
window._leadershipOpenModal(bridgeInfo);
return;
}
if (categoryCode === 'CA10003' && typeof window._learningOpenModal === 'function') {
window._learningOpenModal(bridgeInfo);
return;
}
if (categoryCode === 'CA10002' && typeof window._onboardingOpenModal === 'function') {
window._onboardingOpenModal(bridgeInfo);
return;
}
// 기본값: 기존 공통 모달 흐름 유지
if (modalManager && typeof modalManager.openVideo === 'function') {
await modalManager.openVideo(String(videoData?.id ?? videoData?.content_id ?? ''));
}
}
container.addEventListener('click', function (e) {
if (e.target.closest('.bookmark')) { e.stopPropagation(); return; }
const card = e.target.closest('.card[data-video-id]');
if (!card) return;
e.preventDefault();
e.stopPropagation();
const videoId = String(card.getAttribute('data-video-id') || '');
if (!videoId) return;
const videoData = getVideoDataById(videoId);
if (!videoData) return;
openSearchResultVideoByCategory(videoData).catch(function (err) {
console.warn('[search_result] open by category failed:', err?.message || err);
});
});
container.addEventListener('change', async function (e) {
const checkbox = e.target.closest('input[type="checkbox"]');
if (!checkbox || !checkbox.closest('.bookmark')) return;
const card = checkbox.closest('.card[data-video-id]');
const videoId = card?.getAttribute('data-video-id');
if (!videoId) return;
const nextState = !!checkbox.checked;
checkbox.disabled = true;
try {
const result = await saveWishlist(videoId, nextState);
if (!result || !result.success) { checkbox.checked = !nextState; return; }
setBookmarkState(videoId, nextState);
} catch (err) {
checkbox.checked = !nextState;
console.warn('[search wishlist]', err?.message || err);
} finally {
checkbox.disabled = false;
}
});
});
</script>
</body>
</html>