Files
edu/skin/biztrend.php

1288 lines
50 KiB
PHP

<?php
require_once __DIR__ . '/../bbs/auth.php';
edu_require_login();
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../bbs/db_conn.php';
$pdoBiz = db_conn();
$pdoBiz->exec("SET NAMES 'utf8mb4'");
$bizMemberId = (string)($_SESSION['member_id'] ?? '');
$bizSysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
$bizLoggedIn = $bizMemberId !== '';
// 선택된 월 (기본값: 현재 월)
$selectedMonth = $_GET['month'] ?? date('Y-m');
if (!preg_match('/^\d{4}-\d{2}$/', $selectedMonth)) {
$selectedMonth = date('Y-m');
}
$selYear = substr($selectedMonth, 0, 4);
$selMonth = ltrim(substr($selectedMonth, 5, 2), '0');
// ── 비즈트렌드 컨텐츠 조회 ──
$bizData = [];
try {
$sql = "
SELECT
c.content_id,
c.title,
c.description,
c.description1,
c.description2,
c.content_url,
c.start_date,
c.category_group,
c.image_name,
c.issue_type_code,
CASE WHEN c.issue_type_code IS NOT NULL AND c.issue_type_code != ''
THEN fn_get_code_name(c.issue_type_code)
ELSE NULL END AS badge_name
";
if ($bizLoggedIn) {
$sql .= ",
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL AND cw.is_active = '1' THEN 1 ELSE 0 END AS is_liked
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_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = ?
AND cw.sys_comp_code = ?
AND cw.is_active = '1'
";
} else {
$sql .= ",
NULL AS watch_tm,
NULL AS content_tm,
0 AS is_liked
FROM edu_contents c
";
}
$sql .= "
WHERE c.category_code = 'CA10006'
AND c.is_active = 1
AND c.start_date IS NOT NULL
AND c.start_date <= CURDATE()
ORDER BY c.start_date ASC, c.category_group ASC
";
$stmt = $pdoBiz->prepare($sql);
if ($bizLoggedIn) {
$stmt->execute([$bizMemberId, $bizSysCompCode, $bizMemberId, $bizSysCompCode]);
} else {
$stmt->execute([]);
}
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$dateKey = $row['start_date'];
// YouTube videoId 추출
$videoId = '';
$url = $row['content_url'] ?? '';
if (preg_match('/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]+)/', $url, $m)) {
$videoId = $m[1];
} else {
$videoId = $url;
}
// description, description2 → list 배열
$list = [];
if (!empty($row['description1'])) {
$lines = array_filter(array_map('trim', preg_split('/[\r\n]+/', $row['description1'])));
$list = array_merge($list, array_values($lines));
}
if (!empty($row['description2'])) {
$lines = array_filter(array_map('trim', preg_split('/[\r\n]+/', $row['description2'])));
$list = array_merge($list, array_values($lines));
}
// gauge (시청 진행률)
$gauge = null;
$watchTm = (int)($row['watch_tm'] ?? 0);
$contentTm = (int)($row['content_tm'] ?? 0);
if ($contentTm > 0 && $watchTm > 0) {
$gauge = min(100, round($watchTm / $contentTm * 100));
}
// 이미지 경로 (edu/uploads/biztrend/ 폴더 기준)
$imageData = null;
if (!empty($row['image_name'])) {
$imageData = '/uploads/biztrend/' . $row['image_name'];
}
$entry = [
'contentId' => $row['content_id'] ?? '',
'title' => $row['title'] ?? '',
'list' => $list,
'videoId' => $videoId,
'contentUrl' => $row['content_url'] ?? '',
'description'=> $row['description'] ?? '',
'gauge' => $gauge,
'badge' => $row['badge_name'] ?: null,
'liked' => (bool)($row['is_liked'] ?? false),
'image' => $imageData,
'watchTm' => (int)($row['watch_tm'] ?? 0),
'contentTm' => (int)($row['content_tm'] ?? 0)
];
if (!isset($bizData[$dateKey])) {
$bizData[$dateKey] = [];
}
$bizData[$dateKey][] = $entry;
}
} catch (Exception $e) {
$bizData = [];
$bizError = $e->getMessage();
}
// ── 추천강의: 최근 업로드된 비즈트렌드 영상 8개 ──
$recentBizData = [];
try {
$recentSql = "
SELECT
c.content_id,
c.title,
c.description,
c.content_url,
c.start_date,
c.image_name
";
if ($bizLoggedIn) {
$recentSql .= ",
lh.watch_tm,
lh.content_tm
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 = ?
";
} else {
$recentSql .= ",
NULL AS watch_tm,
NULL AS content_tm
FROM edu_contents c
";
}
$recentSql .= "
WHERE c.category_code = 'CA10006'
AND c.is_active = 1
AND c.start_date <= CURDATE()
ORDER BY c.start_date DESC
LIMIT 8
";
$recentStmt = $pdoBiz->prepare($recentSql);
if ($bizLoggedIn) {
$recentStmt->execute([$bizMemberId, $bizSysCompCode]);
} else {
$recentStmt->execute([]);
}
$recentRows = $recentStmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($recentRows as $row) {
$videoId = '';
$url = $row['content_url'] ?? '';
if (preg_match('/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]+)/', $url, $m)) {
$videoId = $m[1];
} else {
$videoId = $url;
}
$imageData = null;
if (!empty($row['image_name'])) {
$imageData = '/uploads/biztrend/' . $row['image_name'];
}
$recentBizData[] = [
'contentId' => $row['content_id'] ?? '',
'title' => $row['title'] ?? '',
'videoId' => $videoId,
'contentUrl' => $row['content_url'] ?? '',
'description'=> $row['description'] ?? '',
'image' => $imageData,
'watchTm' => (int)($row['watch_tm'] ?? 0),
'contentTm' => (int)($row['content_tm'] ?? 0),
];
}
} catch (Exception $e) {
$recentBizData = [];
}
?>
<!doctype html>
<html lang="ko">
<head>
<?php include(__DIR__ . "/_include/_head.php") ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
</head>
<body>
<div class="wrap biztrend">
<?php include(__DIR__ . "/_include/_header.php") ?>
<!-- ===================== -->
<!-- 컨테이너 -->
<!-- ===================== -->
<div class="container">
<div class="biztrend-inner">
<!-- 페이지 헤더: 브레드크럼 + 제목 -->
<div class="page-header">
<ul class="breadcrumb" aria-label="breadcrumb">
<li><a href="./index.php">홈</a></li>
<li><a href="./biztrend.php">비즈트렌드</a></li>
<li><span class="current">성공예감&별책부록</span></li>
</ul>
<div class="page-title">
<h3>성공예감<span>&</span>별책부록</h3>
</div>
</div>
</div>
<!-- ===================== -->
<!-- 비즈트렌드 메인 섹션 -->
<!-- ===================== -->
<section class="biztrend-wrap">
<div class="biztrend-inner">
<!-- 헤더: 소개 + 연·월 선택 -->
<div class="biztrend-header">
<div class="biztrend-sub-title">
<div class="biztrend-photo">
<img src="/img/biztrend/img_profile.png" alt="">
</div>
<p class="biztrend-intro-desc">
KBS 제1라디오 '성공예감&별책부록'을 통해 <br>
<em>경제 정보를 쉽게 접하고, 생활 속 경제 흐름</em>을 알아보세요.
</p>
</div>
<!-- 연·월 선택기 -->
<div class="article-grid-date">
<button type="button" class="date-select-trigger datepicker-trigger" id="datepicker-trigger" title="연도·월 선택" aria-label="연도·월 선택" aria-haspopup="listbox" aria-expanded="false">
<span class="date-select-label">
<span class="date-year"><span class="date-select-year"><?= $selYear ?></span>년</span>
<span class="date-month"><em class="date-select-month"><?= $selMonth ?></em>월</span>
</span>
<span class="date-select-icon" aria-hidden="true"></span>
</button>
<input type="hidden" class="date-select-value" value="<?= htmlspecialchars($selectedMonth, ENT_QUOTES, 'UTF-8') ?>">
<!-- 날짜피커 드롭다운 (버튼 바로 아래 노출) -->
<div class="datepicker-dropdown" id="datepicker-dropdown" role="listbox" aria-hidden="true">
<div class="datepicker-dropdown-inner">
<div class="datepicker-wheels">
<div class="datepicker-wheel-wrap">
<span class="datepicker-wheel-label">연도</span>
<div class="datepicker-wheel" aria-label="연도 선택">
<div class="datepicker-wheel-track" data-wheel="year"></div>
</div>
</div>
<div class="datepicker-wheel-wrap">
<span class="datepicker-wheel-label">월</span>
<div class="datepicker-wheel" aria-label="월 선택">
<div class="datepicker-wheel-track" data-wheel="month"></div>
</div>
</div>
</div>
<div class="datepicker-actions">
<button type="button" class="btn-datepicker-cancel">취소</button>
<button type="button" class="btn-datepicker-confirm">확인</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="biztrend-auto">
<!-- ===================== -->
<!-- 기사 달력 그리드 -->
<!-- 월~토 6열, 각 날짜당 1개 카드 -->
<!-- ===================== -->
<div class="article-grid">
<div class="article-grid-inner">
<div class="article-grid-head">
<ul class="article-grid-days" aria-label="요일">
<li>월</li>
<li>화</li>
<li>수</li>
<li>목</li>
<li>금</li>
<li>토</li>
</ul>
</div>
<div class="article-grid-body">
<!-- JS(biztrend.js)에서 현재 달 기준으로 동적 생성 -->
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- ============================================ -->
<!-- 영상 모달 댓글 스타일 -->
<!-- ============================================ -->
<style>
/* #biztrendVideoModal .comment-list { list-style: none; margin: 0; padding: 0; }
#biztrendVideoModal .comment-list li { padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,.08); }
#biztrendVideoModal .comment-list li.empty { text-align: center; color: #888; padding: 20px 0; border-bottom: none; }
#biztrendVideoModal .comment-list .comment-header { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
#biztrendVideoModal .comment-list .comment-header .user-icon { width: 20px; height: 20px; border-radius: 50%; background: #555; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
#biztrendVideoModal .comment-list .comment-header .user-icon svg { width: 12px; height: 12px; fill: #ccc; }
#biztrendVideoModal .comment-list .comment-header .comment-author { font-size: 13px; color: #ccc; }
#biztrendVideoModal .comment-list .comment-content { font-size: 14px; color: #fff; line-height: 1.5; margin-bottom: 6px; word-break: break-word; text-align: left; }
#biztrendVideoModal .comment-list .comment-body { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
#biztrendVideoModal .comment-list .comment-body .comment-content { flex: 1; min-width: 0; margin-bottom: 0; }
#biztrendVideoModal .comment-list .comment-body .comment-actions { flex-shrink: 0; align-self: center; }
#biztrendVideoModal .comment-list .comment-footer { display: flex; align-items: center; }
#biztrendVideoModal .comment-list .comment-date { font-size: 12px; color: #888; }
#biztrendVideoModal .comment-list .comment-actions { display: flex; gap: 4px; }
#biztrendVideoModal .comment-list .comment-actions button { padding: 3px 8px; font-size: 12px; border: 1px solid #555; border-radius: 3px; background: #333; color: #ddd; cursor: pointer; white-space: nowrap; }
#biztrendVideoModal .comment-list .comment-actions button:hover { background: #555; color: #fff; }
#biztrendVideoModal .video-list { flex: 1; overflow-y: auto; min-height: 0; }
#biztrendVideoModal .video-list ul { list-style: none; margin: 0; padding: 0; } */
/* 관련영상 목록 스크롤 */
#biztrendVideoModal .video-side .video-list {
overflow: hidden;
}
#biztrendVideoModal #bizRecommendedList {
max-height: none;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
padding-right: 4px;
}
#biztrendVideoModal #bizRecommendedList::-webkit-scrollbar {
width: 6px;
}
#biztrendVideoModal #bizRecommendedList::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.35);
border-radius: 999px;
}
#biztrendVideoModal #bizRecommendedList::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.08);
}
/* 1024px 이하: 댓글 패널(슬라이드업) 토글 */
@media (max-width: 1024px) {
#biztrendVideoModal .comment-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
width: 100%;
max-height: min(70vh, 520px);
z-index: 1002;
transform: translateY(100%);
transition: transform 220ms ease;
}
#biztrendVideoModal.is-comment-open .comment-wrap {
transform: translateY(0);
}
}
</style>
<!-- ============================================ -->
<!-- 영상 모달 -->
<!-- ============================================ -->
<div class="modal video" id="biztrendVideoModal" style="display:none">
<div class="modal-content">
<div class="modal-body">
<div class="video-contents">
<div class="video-area">
<div class="video-box">
<div id="bizVideoPlayer"></div>
</div>
</div>
<div class="video-info">
<div class="tit-box">
<div class="meta">
<span id="bizModalCategoryName"></span>
<em id="bizModalSubCategory"></em>
</div>
<h3 id="bizModalTitle"></h3>
<div class="tit-right">
<button class="btn-comment"><i class="ico-comment"></i>0</button>
<label class="bookmark" for="biztrend-bookmark-chk">
<input id="biztrend-bookmark-chk" type="checkbox" value="" />
<span>북마크</span>
</label>
</div>
</div>
<div class="desc" id="bizModalDesc"></div>
</div>
</div>
<div class="video-side">
<div class="video-header">
<h5 class="tit">관련영상</h5>
<!-- <span class="badge" id="bizModalBadge">비즈트렌드</span> -->
<span class="close">&times;</span>
</div>
<div class="video-list">
<ul id="bizRecommendedList"></ul>
</div>
<div class="comment-wrap">
<!-- 🔥 드래그 리사이저 추가 -->
<div class="comment-resizer">
<div class="resizer-handle"></div>
</div>
<div class="comment-list-wrap">
<ul class="comment-list">
<li class="empty">등록된 댓글이 없습니다.</li>
</ul>
</div>
<div class="comment-box">
<textarea placeholder="댓글을 작성해주세요"></textarea>
<div class="btn-area">
<button class="btn-cancel" disabled>취소</button>
<button class="btn-save" disabled>등록</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/biztrend.js?ver=1.1" defer></script>
<script id="biztrend-data" type="application/json"><?= json_encode($bizData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?></script>
<script id="biztrend-recent" type="application/json"><?= json_encode($recentBizData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?></script>
<?php if (!empty($bizError)): ?>
<script>console.error('[biztrend] DB Error:', <?= json_encode($bizError) ?>);</script>
<?php endif; ?>
<?php if (empty($bizData)): ?>
<script>console.warn('[biztrend] No data loaded for month:', <?= json_encode($selectedMonth) ?>);</script>
<?php endif; ?>
<!-- ============================================ -->
<!-- 영상 모달 JS -->
<!-- ============================================ -->
<script>
(function () {
var API_BASE = '/bbs/api';
var $modal = $('#biztrendVideoModal');
var currentContentId = null;
var editingCommentId = null;
var commentToggleCleanup = null;
// ── YouTube IFrame API 시청 추적 변수 ──
var ytPlayer = null;
var trackingInterval = null;
var lastSaveTime = 0;
var lastCurrentTime = 0;
// YouTube IFrame API 스크립트 로드
if (!document.getElementById('youtube-iframe-api')) {
var ytScript = document.createElement('script');
ytScript.id = 'youtube-iframe-api';
ytScript.src = 'https://www.youtube.com/iframe_api';
ytScript.async = true;
document.head.appendChild(ytScript);
}
// ── YouTube 비디오 ID 추출 ──
function extractVideoId(url) {
if (!url) return '';
var videoId = url;
if (url.indexOf('youtube.com/watch') > -1) {
try { videoId = new URL(url).searchParams.get('v') || url; } catch (e) {}
} else if (url.indexOf('youtu.be/') > -1) {
videoId = url.split('youtu.be/')[1].split(/[?&#]/)[0];
} else if (url.indexOf('youtube.com/embed/') > -1) {
videoId = url.split('youtube.com/embed/')[1].split(/[?&#]/)[0];
}
if (videoId && videoId.indexOf('/') === -1 && videoId.indexOf('.') === -1) {
return videoId;
}
return '';
}
// ── YT.Player 생성 ──
function initYTPlayer(videoId, startSeconds) {
destroyYTPlayer();
startSeconds = Math.floor(startSeconds || 0);
var createPlayer = function () {
ytPlayer = new YT.Player('bizVideoPlayer', {
height: '100%',
width: '100%',
videoId: videoId,
playerVars: {
autoplay: 1, controls: 1, rel: 0,
modestbranding: 1, enablejsapi: 1, start: startSeconds
},
events: {
'onReady': function (event) {
if (event.target && typeof event.target.playVideo === 'function') {
event.target.playVideo();
}
},
'onStateChange': function (event) { handlePlayerStateChange(event); },
'onError': function (event) { console.error('[Biztrend] Player Error:', event.data); }
}
});
};
if (typeof YT !== 'undefined' && YT.Player && typeof YT.Player === 'function') {
createPlayer();
} else {
var checkYt = setInterval(function () {
if (typeof YT !== 'undefined' && YT.Player && typeof YT.Player === 'function') {
clearInterval(checkYt);
createPlayer();
}
}, 500);
setTimeout(function () { clearInterval(checkYt); }, 10000);
}
}
// ── 플레이어 파괴 ──
function destroyYTPlayer() {
stopTracking();
if (ytPlayer && typeof ytPlayer.destroy === 'function') {
try { ytPlayer.destroy(); } catch (e) {}
}
ytPlayer = null;
var $box = $modal.find('.video-box');
if (!$box.find('#bizVideoPlayer').length) {
$box.html('<div id="bizVideoPlayer"></div>');
}
}
// ── 플레이어 상태 변경 핸들러 ──
function handlePlayerStateChange(event) {
if (event.data === YT.PlayerState.PLAYING) {
startTracking();
} else {
stopTracking();
if (event.data === YT.PlayerState.PAUSED || event.data === YT.PlayerState.ENDED) {
saveLearningProgress(Math.floor(lastSaveTime), 'N');
lastSaveTime = 0;
}
}
}
// ── 시청 추적 시작 (1초 주기) ──
function startTracking() {
if (trackingInterval) return;
lastSaveTime = 0;
trackingInterval = setInterval(function () {
if (!ytPlayer || typeof ytPlayer.getCurrentTime !== 'function') return;
var playerState = (typeof ytPlayer.getPlayerState === 'function')
? ytPlayer.getPlayerState() : null;
if (playerState !== YT.PlayerState.PLAYING) return;
var currentTime = ytPlayer.getCurrentTime();
var diff = currentTime - lastCurrentTime;
if (Math.abs(diff) <= 5) { lastSaveTime += 1; }
lastCurrentTime = currentTime;
if (lastSaveTime >= 30) {
saveLearningProgress(lastSaveTime);
lastSaveTime = 0;
}
}, 1000);
}
// ── 시청 추적 중지 ──
function stopTracking() {
if (trackingInterval) { clearInterval(trackingInterval); trackingInterval = null; }
}
// ── 서버에 학습 진행상황 저장 ──
function saveLearningProgress(heartbeatSeconds, isWatching) {
if (!ytPlayer || !currentContentId) return;
if (typeof ytPlayer.getCurrentTime !== 'function') return;
var currentTime = Math.floor(ytPlayer.getCurrentTime());
var duration = Math.floor(ytPlayer.getDuration());
var incrementalWatch = Math.max(0, Math.floor(heartbeatSeconds || 0));
var params = new URLSearchParams({
content_id: String(currentContentId),
watch_tm: String(Math.max(0, currentTime)),
content_tm: String(Math.max(0, duration)),
all_tm_increment: String(incrementalWatch),
is_watching: isWatching === 'N' ? 'N' : 'Y'
});
fetch(API_BASE + '/save_learning.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: params.toString(),
keepalive: true
}).then(function (r) { return r.json(); }).then(function (result) {
if (!result.success) console.error('[Biztrend] Save Learning Failed:', result.message);
}).catch(function (e) {
console.error('[Biztrend] Save Learning Error:', e);
});
}
// ── 추천강의용 최근 비즈트렌드 영상 목록 ──
var allBizEntries = [];
(function () {
try {
var el = document.getElementById('biztrend-recent');
if (el) {
allBizEntries = JSON.parse(el.textContent || '[]');
}
} catch (e) { console.error('[Biztrend] Parse recent entries error:', e); }
})();
function syncAllBizEntriesBookmark(contentId, isBookmarked) {
var cid = String(contentId || '');
if (!cid) return;
allBizEntries.forEach(function (entry) {
if (String(entry.contentId || '') !== cid) return;
entry.liked = !!isBookmarked;
entry.bookmark = !!isBookmarked;
entry.is_liked = !!isBookmarked;
entry.is_bookmarked = !!isBookmarked;
});
}
function syncCardBookmarkByContentId(contentId, isBookmarked) {
var cid = String(contentId || '');
if (!cid) return;
// data-content-id가 어느 레벨에 붙어 있어도 북마크 UI를 최대한 폭넓게 동기화한다.
var checked = !!isBookmarked;
var $targets = $([
'[data-content-id="' + cid + '"]',
'[data-content_id="' + cid + '"]',
'[data-id="' + cid + '"]',
'[data-video-id="' + cid + '"]',
'[data-video_id="' + cid + '"]',
'[data-cid="' + cid + '"]'
].join(','));
if (!$targets.length) return;
$targets.each(function () {
var $target = $(this);
// 하루 카드 전체가 아니라 해당 영상 블록(article-card-content)만 동기화한다.
var $scope = $target.closest('.article-card-content');
if (!$scope.length) {
$scope = $target;
}
$scope.find('.article-card-like').toggleClass('is-active', checked).attr('aria-label', checked ? '좋아요 취소' : '좋아요');
$scope.find('.bookmark input[type="checkbox"], .bookmark input[type="radio"]').prop('checked', checked);
$scope.find('.bookmark').toggleClass('is-active', checked).attr('aria-pressed', checked ? 'true' : 'false');
$scope.attr('data-bookmarked', checked ? '1' : '0');
$scope.attr('data-liked', checked ? '1' : '0');
$scope.attr('data-is-liked', checked ? '1' : '0');
$scope.attr('data-is-bookmarked', checked ? '1' : '0');
$target.find('.article-card-like').toggleClass('is-active', checked).attr('aria-label', checked ? '좋아요 취소' : '좋아요');
$target.find('.bookmark input[type="checkbox"], .bookmark input[type="radio"]').prop('checked', checked);
$target.find('.bookmark').toggleClass('is-active', checked).attr('aria-pressed', checked ? 'true' : 'false');
$target.attr('data-bookmarked', checked ? '1' : '0');
$target.attr('data-liked', checked ? '1' : '0');
$target.attr('data-is-liked', checked ? '1' : '0');
$target.attr('data-is-bookmarked', checked ? '1' : '0');
});
}
function syncWatchTimeByContentId(contentId, watchTm) {
var cid = String(contentId || '');
if (!cid) return;
$('[data-content-id="' + cid + '"]').attr('data-watch-tm', watchTm);
}
function applyBookmarkState(contentId, isBookmarked) {
var cid = String(contentId || '');
if (!cid) return;
syncCardBookmarkByContentId(cid, isBookmarked);
syncAllBizEntriesBookmark(cid, isBookmarked);
// 렌더러가 script JSON 또는 전역 배열을 재사용하는 경우를 대비해 상태를 같이 갱신한다.
try {
var dataEl = document.getElementById('biztrend-data');
if (dataEl) {
var parsed = JSON.parse(dataEl.textContent || '{}');
Object.keys(parsed).forEach(function (k) {
var arr = parsed[k];
if (!Array.isArray(arr)) return;
arr.forEach(function (entry) {
if (String(entry && entry.contentId || '') !== cid) return;
entry.liked = !!isBookmarked;
entry.bookmark = !!isBookmarked;
entry.is_liked = !!isBookmarked;
entry.is_bookmarked = !!isBookmarked;
});
});
dataEl.textContent = JSON.stringify(parsed);
}
} catch (e) {}
['articleData', 'biztrendData', 'bizData'].forEach(function (name) {
var ref = window[name];
if (!ref) return;
if (Array.isArray(ref)) {
ref.forEach(function (entry) {
if (String(entry && entry.contentId || entry && entry.content_id || '') !== cid) return;
entry.liked = !!isBookmarked;
entry.bookmark = !!isBookmarked;
entry.is_liked = !!isBookmarked;
entry.is_bookmarked = !!isBookmarked;
});
return;
}
if (typeof ref === 'object') {
Object.keys(ref).forEach(function (k) {
var arr = ref[k];
if (!Array.isArray(arr)) return;
arr.forEach(function (entry) {
if (String(entry && entry.contentId || entry && entry.content_id || '') !== cid) return;
entry.liked = !!isBookmarked;
entry.bookmark = !!isBookmarked;
entry.is_liked = !!isBookmarked;
entry.is_bookmarked = !!isBookmarked;
});
});
}
});
if (String(currentContentId || '') === cid) {
$modal.find('#biztrend-bookmark-chk').prop('checked', !!isBookmarked);
}
}
function adjustRecommendedListHeight() {
if (!$modal.is(':visible')) return;
var $videoListWrap = $modal.find('.video-list');
var $list = $modal.find('#bizRecommendedList');
var $commentWrap = $modal.find('.comment-wrap');
if (!$videoListWrap.length || !$list.length || !$commentWrap.length) return;
var listTop = $videoListWrap.offset().top;
var commentTop = $commentWrap.offset().top;
if (!Number.isFinite(listTop) || !Number.isFinite(commentTop)) return;
var availableHeight = Math.floor(commentTop - listTop - 8);
if (availableHeight < 120) availableHeight = 120;
$list.css('max-height', availableHeight + 'px');
}
// ── 1024px 이하: btn-comment → comment-wrap 토글 (insight.php 패턴) ──
function setupCommentToggle() {
if (typeof commentToggleCleanup === 'function') return;
var $btnComment = $modal.find('.btn-comment').first();
var $videoSide = $modal.find('.video-side').first();
var $commentWrap = $modal.find('.comment-wrap').first();
if (!$btnComment.length || !$videoSide.length || !$commentWrap.length) return;
var mediaQuery = window.matchMedia('(max-width: 1024px)');
function isMobileView() {
return mediaQuery.matches;
}
function openComment() {
$modal.addClass('is-comment-open');
}
function closeComment() {
$modal.removeClass('is-comment-open');
}
function clearMobileStyles() {
var el = $commentWrap.get(0);
if (el && el.style) el.style.cssText = '';
var listWrap = $commentWrap.find('.comment-list-wrap').get(0);
if (listWrap && listWrap.style) listWrap.style.cssText = '';
}
function restoreToVideoSide() {
closeComment();
if ($commentWrap.parent().is($modal)) {
$videoSide.append($commentWrap);
}
clearMobileStyles();
}
function moveToModal() {
if (!$commentWrap.parent().is($modal)) {
$modal.append($commentWrap);
}
}
function handleResize() {
if (isMobileView()) {
moveToModal();
} else {
restoreToVideoSide();
}
}
if (isMobileView()) {
moveToModal();
} else {
restoreToVideoSide();
}
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', handleResize);
} else if (typeof mediaQuery.addListener === 'function') {
mediaQuery.addListener(handleResize);
}
var onBtnClick = function (e) {
e.stopPropagation();
if (!isMobileView()) return;
if ($modal.hasClass('is-comment-open')) closeComment();
else openComment();
};
$btnComment.on('click', onBtnClick);
var onResizerClick = function () {
if (isMobileView()) closeComment();
};
$commentWrap.on('click', '.comment-resizer', onResizerClick);
commentToggleCleanup = function () {
try {
$btnComment.off('click', onBtnClick);
$commentWrap.off('click', '.comment-resizer', onResizerClick);
if (typeof mediaQuery.removeEventListener === 'function') {
mediaQuery.removeEventListener('change', handleResize);
} else if (typeof mediaQuery.removeListener === 'function') {
mediaQuery.removeListener(handleResize);
}
} catch (e) {}
restoreToVideoSide();
commentToggleCleanup = null;
};
}
// ── 모달 열기 ──
function openModal(info) {
currentContentId = info.contentId;
editingCommentId = null;
lastSaveTime = 0;
lastCurrentTime = Math.floor(info.watchTm || 0);
var videoId = extractVideoId(info.videoUrl);
if (videoId) { initYTPlayer(videoId, info.watchTm || 0); }
// 서버에서 최신 watch_tm 조회 후 재생 위치 보정
$.ajax({
url: '/bbs/myclass/api/get_video_time.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ content_id: info.contentId }),
dataType: 'json',
success: function(res) {
if (res.success && res.watch_tm > 0) {
var serverWatchTm = parseInt(res.watch_tm, 10);
if (ytPlayer && typeof ytPlayer.seekTo === 'function' && serverWatchTm > (info.watchTm || 0)) {
ytPlayer.seekTo(serverWatchTm, true);
lastCurrentTime = serverWatchTm;
}
syncWatchTimeByContentId(info.contentId, serverWatchTm);
}
}
});
$modal.find('#bizModalTitle').text(info.title);
var descHtml = $('<div>').text(info.description || '').html().replace(/\n/g, '<br>');
$modal.find('#bizModalDesc').html(descHtml);
$modal.find('#bizModalCategoryName').text(info.categoryName || '비즈트렌드');
$modal.find('#bizModalSubCategory').text(info.subCategory || '');
$modal.find('#bizModalBadge').text('비즈트렌드');
buildRecommendedList(info.contentId);
loadComments(info.contentId);
resetCommentForm();
// 북마크 상태 동기화
syncBookmark(info.contentId, info.bookmark);
$modal.show();
$('body').css('overflow', 'hidden');
// 모바일 댓글 토글 세팅(1회 바인딩)
setupCommentToggle();
setTimeout(adjustRecommendedListHeight, 0);
}
// ── 모달 닫기 ──
function closeModal() {
stopTracking();
if (ytPlayer && currentContentId && typeof ytPlayer.getCurrentTime === 'function') {
var currentTime = Math.floor(ytPlayer.getCurrentTime());
syncWatchTimeByContentId(currentContentId, currentTime);
saveLearningProgress(Math.floor(lastSaveTime), 'N');
}
lastSaveTime = 0;
destroyYTPlayer();
$modal.hide();
$modal.removeClass('is-comment-open');
$('body').css('overflow', '');
currentContentId = null;
editingCommentId = null;
if (typeof commentToggleCleanup === 'function') {
commentToggleCleanup();
}
}
$modal.on('click', '.close', closeModal);
$modal.on('click', function (e) { if ($(e.target).hasClass('modal')) closeModal(); });
$(document).on('keydown', function (e) {
if (e.key === 'Escape' && $modal.is(':visible')) closeModal();
});
// ── 북마크 동기화 ──
function syncBookmark(contentId, isBookmarked) {
var $chk = $modal.find('.bookmark input[type="checkbox"]');
if (!$chk.length) return;
if (isBookmarked !== undefined) {
$chk.prop('checked', !!isBookmarked);
} else {
$.ajax({
url: '/bbs/myclass/api/get_video_time.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ content_id: contentId }),
dataType: 'json',
success: function(res) {
if (res.success) {
$chk.prop('checked', !!res.is_bookmarked);
}
}
});
}
}
// ── 북마크 토글 이벤트 ──
$modal.on('change', '.bookmark input[type="checkbox"]', function () {
var $chk = $(this);
var isActive = $chk.is(':checked') ? '1' : '0';
if (!currentContentId) return;
$chk.prop('disabled', true);
$.ajax({
url: '/bbs/api/save_wishlist.php',
type: 'POST',
data: { content_id: String(currentContentId), is_active: isActive },
dataType: 'json',
success: function (res) {
if (!res || res.success !== true) {
$chk.prop('checked', !$chk.is(':checked'));
alert(res?.message || '북마크 저장에 실패했습니다.');
} else {
var isBookmarked = (isActive === '1');
applyBookmarkState(currentContentId, isBookmarked);
// biztrend.js의 articleData도 즉시 동기화해 모달 재오픈 시 상태가 유지되도록 한다.
if (typeof window.dispatchEvent === 'function' && typeof window.CustomEvent === 'function') {
var detail = {
contentId: String(currentContentId),
content_id: String(currentContentId),
isBookmarked: isBookmarked,
is_bookmarked: isBookmarked,
isActive: isBookmarked ? '1' : '0'
};
window.dispatchEvent(new CustomEvent('biztrend:bookmark-changed', { detail: detail }));
window.dispatchEvent(new CustomEvent('bookmark-changed', { detail: detail }));
window.dispatchEvent(new CustomEvent('wishlist:changed', { detail: detail }));
$(document).trigger('biztrend:bookmark-changed', [detail]);
$(document).trigger('bookmark-changed', [detail]);
$(document).trigger('wishlist:changed', [detail]);
}
}
},
error: function () {
$chk.prop('checked', !$chk.is(':checked'));
alert('북마크 저장 중 오류가 발생했습니다.');
},
complete: function () {
$chk.prop('disabled', false);
}
});
});
$(window).on('biztrend:bookmark-changed', function (e) {
var detail = e.originalEvent && e.originalEvent.detail ? e.originalEvent.detail : null;
if (!detail) return;
var cid = String(detail.contentId || '');
if (!cid) return;
applyBookmarkState(cid, !!detail.isBookmarked);
});
$(document).on('biztrend:bookmark-changed', function (e, detail) {
if (!detail || typeof detail !== 'object') return;
var cid = String(detail.contentId || '');
if (!cid) return;
applyBookmarkState(cid, !!detail.isBookmarked);
});
// ── 추천영상 리스트 구성 (비즈트렌드 영상) ──
function buildRecommendedList(excludeContentId) {
var $list = $modal.find('#bizRecommendedList');
$list.empty();
var count = 0;
allBizEntries.forEach(function (entry) {
if (count >= 8) return;
var cid = entry.contentId || '';
if (!cid || String(cid) === String(excludeContentId)) return;
var title = entry.title || '';
var videoUrl = entry.contentUrl || '';
var desc = entry.description || '';
var thumbSrc = entry.image || (entry.videoId ? 'https://img.youtube.com/vi/' + entry.videoId + '/sddefault.jpg' : '/img/video/img_thumb_01.png');
var wTm = entry.watchTm || 0;
var cTm = entry.contentTm || 0;
var li =
'<li>' +
'<a href="#" class="list" ' +
'data-content-id="' + cid + '" ' +
'data-content-url="' + (videoUrl).replace(/"/g, '&quot;') + '" ' +
'data-title="' + title.replace(/"/g, '&quot;') + '" ' +
'data-description="' + desc.replace(/"/g, '&quot;') + '" ' +
'data-watch-tm="' + wTm + '" ' +
'data-content-tm="' + cTm + '">' +
'<div class="thumb"><img src="' + thumbSrc + '" alt="" /></div>' +
'<div class="txt-box">' +
'<div class="category leader">비즈트렌드</div>' +
'<div class="title">' + title + '</div>' +
'</div>' +
'</a>' +
'</li>';
$list.append(li);
count++;
});
if (count === 0) {
$list.html('<li class="empty">추천 영상이 없습니다.</li>');
}
setTimeout(adjustRecommendedListHeight, 0);
}
$(window).on('resize', function () {
if ($modal.is(':visible')) {
adjustRecommendedListHeight();
}
});
// ── 추천영상 클릭 → 영상 전환 ──
$modal.on('click', '#bizRecommendedList .list', function (e) {
e.preventDefault();
var $a = $(this);
stopTracking();
if (ytPlayer && currentContentId && typeof ytPlayer.getCurrentTime === 'function') {
saveLearningProgress(Math.floor(lastSaveTime), 'N');
}
lastSaveTime = 0;
openModal({
contentId: $a.data('content-id'),
videoUrl: $a.data('content-url') || '',
title: $a.data('title') || $a.find('.title').text().trim(),
description: $a.data('description') || '',
categoryName: '비즈트렌드',
subCategory: '',
watchTm: parseInt($a.data('watch-tm') || '0', 10),
contentTm: parseInt($a.data('content-tm') || '0', 10)
});
});
// ══════════════════════════════════════════════
// 댓글 CRUD
// ══════════════════════════════════════════════
function loadComments(contentId) {
var $commentList = $modal.find('.comment-list');
if (!contentId) return;
$.getJSON(API_BASE + '/get_comments.php', { content_id: contentId })
.done(function (data) {
if (data.success && Array.isArray(data.data)) {
renderComments(data.data);
} else {
$commentList.html('<li class="empty">등록된 댓글이 없습니다.</li>');
}
})
.fail(function () {
$commentList.html('<li class="empty">댓글을 불러올 수 없습니다.</li>');
});
}
function renderComments(comments) {
var $commentList = $modal.find('.comment-list');
if (!comments.length) {
$commentList.html('<li class="empty">등록된 댓글이 없습니다.</li>');
return;
}
var userIconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/></svg>';
// var html = comments.map(function (c) {
// var safeComment = (c.comment || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
// var safeName = (c.member_name || '익명').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// var buttons = c.is_author
// ? '<div class="comment-actions">' +
// '<button type="button" class="btn-edit" data-id="' + c.id + '">수정</button>' +
// '<button type="button" class="btn-delete" data-id="' + c.id + '">삭제</button>' +
// '</div>'
// : '';
// return '<li data-comment-id="' + c.id + '">' +
// '<div class="comment-header">' +
// '<span class="user-icon">' + userIconSvg + '</span>' +
// '<span class="comment-author">' + safeName + '</span>' +
// '</div>' +
// '<div class="comment-body">' +
// '<div class="comment-content">' + safeComment + '</div>' +
// buttons +
// '</div>' +
// '</li>';
// }).join('');
var html = comments.map(function (c) {
var safeComment = (c.comment || '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
var safeName = (c.member_name || '익명')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
var actions = c.is_author
? '<div class="actions">' +
'<button type="button" class="btn-edit-comment" data-id="' + c.id + '">수정</button>' +
'<button type="button" class="btn-del-comment" data-id="' + c.id + '">삭제</button>' +
'</div>'
: '';
return '<li data-id="' + c.id + '">' +
'<div class="comment-info">' +
'<div class="photo"><img src="' + (c.profile_image || '/img/ico/ico_user.svg') + '" onerror="this.src=\'/img/ico/ico_user.svg\'" /></div>' +
'<div class="user-comment">' +
'<span class="user-name">' + safeName + '</span>' +
'<textarea class="user-text" disabled>' + safeComment + '</textarea>' +
'</div>' +
actions +
'</div>' +
'</li>';
}).join('');
$commentList.html(html);
adjustUserTextHeights();
}
// ── 댓글 textarea 높이 자동 조정 ──
function adjustUserTextHeights() {
$modal.find('.user-text').each(function () {
this.style.setProperty('overflow-y', 'hidden', 'important');
this.style.setProperty('overflow-x', 'hidden', 'important');
this.style.setProperty('max-height', 'none', 'important');
this.style.setProperty('height', '0px', 'important');
var h = this.scrollHeight;
this.style.setProperty('height', (h > 0 ? h : 40) + 'px', 'important');
});
}
$modal.on('click', '.btn-edit-comment', function () {
var $li = $(this).closest('li');
var content = ($li.find('.user-text').val() || '').trim();
editingCommentId = $(this).data('id');
var $textarea = $modal.find('.comment-box textarea');
$textarea.val(content).focus();
$modal.find('.btn-cancel').prop('disabled', false);
$modal.find('.btn-save').prop('disabled', false).text('수정');
});
$modal.on('click', '.btn-del-comment', function () {
if (!confirm('댓글을 삭제하시겠습니까?')) return;
var commentId = $(this).data('id');
$.ajax({
url: API_BASE + '/delete_comment.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ id: commentId }),
success: function (data) {
if (data.success) { loadComments(currentContentId); }
else { alert(data.message || '삭제에 실패했습니다.'); }
},
error: function () { alert('삭제 중 오류가 발생했습니다.'); }
});
});
$modal.on('click', '.comment-box .btn-save', function () {
var $textarea = $modal.find('.comment-box textarea');
var comment = $textarea.val().trim();
if (!comment || !currentContentId) return;
if (comment.length > COMMENT_MAX_LENGTH) {
$textarea.val($textarea.val().slice(0, COMMENT_MAX_LENGTH));
alert('댓글은 200자까지 입력할 수 있습니다.');
return;
}
var payload = { content_id: currentContentId, comment: comment };
if (editingCommentId) payload.id = editingCommentId;
$.ajax({
url: API_BASE + '/save_comment.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload),
success: function (data) {
if (data.success) { resetCommentForm(); loadComments(currentContentId); }
else { alert(data.message || '저장에 실패했습니다.'); }
},
error: function () { alert('저장 중 오류가 발생했습니다.'); }
});
});
$modal.on('click', '.comment-box .btn-cancel', function () { resetCommentForm(); });
var COMMENT_MAX_LENGTH = 200;
var _lastLengthAlertAt = 0;
$modal.on('input', '.comment-box textarea', function () {
var $ta = $(this);
if ($ta.val().length > COMMENT_MAX_LENGTH) {
$ta.val($ta.val().slice(0, COMMENT_MAX_LENGTH));
var now = Date.now();
if (now - _lastLengthAlertAt > 800) {
_lastLengthAlertAt = now;
alert('댓글은 200자까지 입력할 수 있습니다.');
}
}
var hasText = $ta.val().trim().length > 0;
$modal.find('.btn-cancel').prop('disabled', !hasText);
$modal.find('.btn-save').prop('disabled', !hasText);
});
function resetCommentForm() {
var $ta = $modal.find('.comment-box textarea');
$ta.val('');
$ta.attr('maxlength', COMMENT_MAX_LENGTH);
$modal.find('.btn-cancel').prop('disabled', true);
$modal.find('.btn-save').prop('disabled', true).text('등록');
editingCommentId = null;
}
// biztrend.js에서 호출할 수 있도록 전역 노출
window._biztrendOpenModal = openModal;
})();
</script>
</body>
</html>