Files
edu/admin/skin/index_old.php
T

922 lines
42 KiB
PHP

<?php
require_once __DIR__ . '/../../bbs/auth.php';
edu_require_login();
include_once '../../bbs/db_conn.php';
include_once 'header.php';
// 현재 년도
$current_year = date('Y');
$selected_year = $_GET['year'] ?? $current_year;
$selected_access_comp = $_GET['access_comp'] ?? '';
$selected_ranking_comp = $_GET['ranking_comp'] ?? '';
// 신규 필터 파라미터 (선택된 년도 기준)
$fr_date = $_GET['fr_date'] ?? "{$selected_year}-01-01";
$to_date = $_GET['to_date'] ?? "{$selected_year}-12-31";
$exclude_admin = ($_GET['exclude_admin'] ?? '0') === '1';
// 섹션별 별도 날짜가 필요한 경우를 위해 (추후 확장성 고려)
$rank_fr_date = $_GET['rank_fr_date'] ?? $fr_date;
$rank_to_date = $_GET['rank_to_date'] ?? $to_date;
$video_fr_date = $_GET['video_fr_date'] ?? $fr_date;
$video_to_date = $_GET['video_to_date'] ?? $to_date;
$stat_fr_date = $_GET['stat_fr_date'] ?? $fr_date;
$stat_to_date = $_GET['stat_to_date'] ?? $to_date;
$access_fr_date = $_GET['access_fr_date'] ?? $fr_date;
$access_to_date = $_GET['access_to_date'] ?? $to_date;
// 현재 년도의 법정의무교육 기간 조회
$legal_edu_period = '';
$user_qty = 0;
try {
require_once __DIR__ . '/../../bbs/db_conn.php';
$pdo = db_conn();
$stmt = $pdo->prepare("
SELECT start_date, end_date
FROM edu_contents
WHERE category_code = 'CA10003'
AND base_year = ?
GROUP BY start_date, end_date
LIMIT 1
");
$stmt->execute([$selected_year]);
$period = $stmt->fetch(PDO::FETCH_ASSOC);
if ($period && !empty($period['start_date']) && !empty($period['end_date'])) {
// 날짜 포맷: YYYY.MM.DD
$start = date('Y.m.d', strtotime($period['start_date']));
$end = date('Y.m.d', strtotime($period['end_date']));
$legal_edu_period = "{$start} ~ {$end}";
// 전체 학습자 수 표시 퇴사자 제외
$stmt_qty = $pdo->prepare("
SELECT COUNT(member_id) as qty
FROM edu_users
WHERE (end_date IS NULL OR end_date > CURDATE())
AND sys_comp_code = working_comp
");
$stmt_qty->execute();
$user_qty = $stmt_qty->fetchColumn();
}
} catch (Exception $e) {
// 법정의무교육 기간 조회 실패 시 기본값 유지
}
// 법인 리스트 가져오기 (프로시저 사용)
$pdo = db_conn();
$stmt_corp = $pdo->query("CALL proc_get_code2_list('CO100')");
$companies = $stmt_corp->fetchAll(PDO::FETCH_ASSOC);
while ($stmt_corp->nextRowset()) {
}
unset($stmt_corp);
// 법인별 학습인원 현황 (재직 중인 인원 수)
$learner_count_query = $pdo->prepare("
SELECT c.code_name, COUNT(DISTINCT b.member_id) as learner_count
FROM edu_codes c
LEFT JOIN edu_users b ON c.code = b.belong_comp AND b.end_date IS NULL
WHERE c.group_code = 'CO100' AND c.is_active = '1'
GROUP BY c.code, c.code_name
ORDER BY c.code ASC
");
$learner_count_query->execute();
$learner_counts = $learner_count_query->fetchAll();
// 법인별 통계 (총 학습시간)
$total_time_query = $pdo->prepare("
SELECT
c.code,
c.code_name,
CONCAT(
LPAD(FLOOR(IFNULL(SUM(CASE WHEN a.completed_at IS NULL THEN a.watch_tm ELSE a.content_tm END), 0) / 3600), 2, '0'), '시간 ',
LPAD(FLOOR((IFNULL(SUM(CASE WHEN a.completed_at IS NULL THEN a.watch_tm ELSE a.content_tm END), 0) % 3600) / 60), 2, '0'), '분'
) AS formatted_total_tm
FROM edu_codes c
LEFT JOIN edu_users b ON c.code = b.sys_comp_code
LEFT JOIN edu_learning_histories a ON b.member_id = a.member_id AND b.sys_comp_code = a.sys_comp_code AND a.last_viewed_at BETWEEN ? AND ?
WHERE c.group_code = 'CO100' AND c.is_active = '1'
GROUP BY c.code, c.code_name
ORDER BY c.code ASC
");
$total_time_query->execute(["$stat_fr_date 00:00:00", "$stat_to_date 23:59:59"]);
$total_times = $total_time_query->fetchAll();
// 법인별 통계 (평균 학습횟수)
$avg_count_query = $pdo->prepare("
SELECT
c.code,
c.code_name,
CONCAT(
IFNULL(
ROUND(
COUNT(a.content_id) ,
1
),
0
), '회'
) AS avg_view_count
FROM edu_codes c
LEFT JOIN edu_users b ON c.code = b.sys_comp_code
LEFT JOIN edu_learning_histories a ON b.member_id = a.member_id AND b.sys_comp_code = a.sys_comp_code AND a.last_viewed_at BETWEEN ? AND ?
WHERE c.group_code = 'CO100' AND c.is_active = '1'
GROUP BY c.code, c.code_name
ORDER BY c.code ASC
");
$avg_count_query->execute(["$stat_fr_date 00:00:00", "$stat_to_date 23:59:59"]);
$avg_counts = $avg_count_query->fetchAll();
// 법인별 접속 추이 (월별)
$access_trend_query = $pdo->prepare("
SELECT
MONTH(accessed_at) as month,
COUNT(al.member_id) as access_count
FROM edu_access_logs al
WHERE al.accessed_at BETWEEN ? AND ? AND (? = '' OR EXISTS (
SELECT 1 FROM edu_users u WHERE u.member_id = al.member_id AND u.sys_comp_code = ?
))
GROUP BY MONTH(al.accessed_at)
ORDER BY MONTH(al.accessed_at) ASC
");
$access_trend_query->execute(["$access_fr_date 00:00:00", "$access_to_date 23:59:59", $selected_access_comp, $selected_access_comp]);
$access_trends = $access_trend_query->fetchAll();
// 가장 많이 본 영상 (카테고리별 top5)
$popular_videos_query = $pdo->prepare("
SELECT
b.category_code,
b.title AS content_title,
COUNT(a.content_id) as view_count
FROM edu_learning_histories a
JOIN edu_contents b ON a.content_id = b.content_id
WHERE a.last_viewed_at BETWEEN ? AND ?
GROUP BY b.category_code, b.content_id, b.title
ORDER BY b.category_code, view_count DESC
");
$popular_videos_query->execute(["$video_fr_date 00:00:00", "$video_to_date 23:59:59"]);
$popular_videos = $popular_videos_query->fetchAll();
// 배움터 학습 랭킹
$ranking_sql = "
SELECT a.sys_comp_code,
a.name,
a.dept_name,
c.code_name as company_name,
SUM(CASE WHEN b.completed_at IS NULL THEN b.watch_tm ELSE b.content_tm END) / 3600 as total_hours
FROM edu_users a
JOIN edu_learning_histories b ON a.member_id = b.member_id AND a.sys_comp_code = b.sys_comp_code
JOIN edu_codes c ON a.belong_comp = c.code
WHERE b.last_viewed_at BETWEEN ? AND ? AND c.group_code = 'CO100' AND (? = '' OR a.belong_comp = ?)
";
if ($exclude_admin) {
$ranking_sql .= " AND a.auth_level NOT IN ('LE10001', 'LE10002') ";
}
$ranking_sql .= "
GROUP BY a.sys_comp_code, a.member_id, a.name, a.dept_name, c.code_name
ORDER BY total_hours DESC
LIMIT 20
";
$ranking_query = $pdo->prepare($ranking_sql);
$ranking_query->execute(["$rank_fr_date 00:00:00", "$rank_to_date 23:59:59", $selected_ranking_comp, $selected_ranking_comp]);
$rankings = $ranking_query->fetchAll();
?>
<main class="max-w-[1600px] mx-auto p-6">
<header class="flex flex-col md:flex-row justify-between items-end md:items-center mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-gray-800 flex items-center">
전체학습현황
<span class="ml-4 text-xs font-normal px-2 py-1 bg-gray-200 rounded text-gray-600">전체 학습자 수:
<?php echo $user_qty; ?>명</span>
</h2>
<p class="text-sm text-gray-400 mt-1 italic leading-relaxed">법정의무교육 기간: <?php echo $legal_edu_period; ?></p>
</div>
<div class="flex items-center space-x-2">
<div class="flex bg-gray-200 p-1 rounded-md">
<?php
$is_prev_selected = $selected_year == ($current_year - 1);
$is_curr_selected = $selected_year == $current_year;
?>
<button class="px-3 py-1 text-sm rounded transition <?php echo $is_prev_selected ? 'bg-[#114b3d] text-white shadow-sm font-bold' : 'text-gray-500 hover:text-gray-800'; ?>"
onclick="changeYear(<?php echo $current_year - 1; ?>)"><?php echo $current_year - 1; ?>년</button>
<button class="px-3 py-1 text-sm rounded transition <?php echo $is_curr_selected ? 'bg-[#114b3d] text-white shadow-sm font-bold' : 'text-gray-500 hover:text-gray-800'; ?>"
onclick="changeYear(<?php echo $current_year; ?>)"><?php echo $current_year; ?>년</button>
</div>
<button
class="px-4 py-2 bg-[#2563eb] text-white rounded-md text-sm font-bold flex items-center hover:bg-blue-700 transition shadow-lg">
<i class="fa-solid fa-file-invoice mr-2"></i>교육결과보고서
</button>
</div>
</header>
<!-- 통계 카드 3개 -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- 법인별 학습인원 현황 (막대 차트) -->
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<h3 class="font-bold text-gray-800 mb-4 flex items-center justify-between text-sm">
법인별 학습인원 현황
<i class="fa-solid fa-ellipsis-vertical text-gray-300"></i>
</h3>
<?php
$max_count = 0;
foreach ($learner_counts as $count) {
$val = (int) $count['learner_count'];
if ($val > $max_count)
$max_count = $val;
}
// 스케일 계산: 데이터가 있으면 500 단위로 올림, 없으면 기본 100
$y_max = $max_count > 0 ? ceil($max_count / 500) * 500 : 500;
if ($y_max < 1)
$y_max = 100;
$y_step_count = 4;
$y_unit = $y_max / $y_step_count;
$scale = 165 / $y_max;
$bar_width = 30;
$gap = 12;
$start_x = 45;
?>
<!-- SVG Bar Chart -->
<div class="relative w-full" style="height:240px;">
<svg viewBox="0 0 320 210" class="w-full h-full" xmlns="http://www.w3.org/2000/svg">
<!-- 격자선 -->
<?php for ($i = 0; $i <= $y_step_count; $i++):
$y = 175 - ($i * (165 / $y_step_count));
?>
<line x1="42" y1="<?php echo $y; ?>" x2="315" y2="<?php echo $y; ?>" stroke="#e5e7eb"
stroke-width="<?php echo $i === 0 ? '1' : '0.8'; ?>" <?php echo $i === 0 ? '' : 'stroke-dasharray="4,3"'; ?> />
<?php endfor; ?>
<!-- Y축 레이블 -->
<?php for ($i = 0; $i <= $y_step_count; $i++):
$y = 175 - ($i * (165 / $y_step_count));
$label = $i * $y_unit;
?>
<text x="38" y="<?php echo $y + 3; ?>" text-anchor="end" font-size="10"
fill="#9ca3af"><?php echo number_format($label); ?></text>
<?php endfor; ?>
<!-- Y축 라인 -->
<line x1="42" y1="8" x2="42" y2="175" stroke="#d1d5db" stroke-width="1" />
<?php
foreach ($learner_counts as $index => $data) {
if ($index >= 7)
break; // 차트 공간상 7개까지만 표시
$val = (int) $data['learner_count'];
$height = $val * $scale;
$y = 175 - $height;
$x = $start_x + $index * ($bar_width + $gap);
$color = $index < 4 ? '#114b3d' : '#1d6b56';
echo "<rect x='$x' y='$y' width='$bar_width' height='$height' fill='$color' rx='3'/>\n";
$text_x = $x + $bar_width / 2;
$short_name = mb_substr($data['code_name'], 0, 4); // 이름이 길면 자름
echo "<text x='$text_x' y='194' text-anchor='middle' font-size='9' fill='#6b7280'>{$short_name}</text>\n";
}
?>
</svg>
</div>
</div>
<!-- 법인별 통계 -->
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<div class="space-y-2 mb-5">
<div class="flex justify-between items-center">
<h3 class="font-bold text-gray-800 italic underline decoration-blue-200 decoration-4 text-sm">법인별 통계</h3>
<select id="statType" class="text-xs bg-gray-50 border border-gray-100 rounded p-1"
onchange="changeStatType()">
<option value="avg">학습횟수</option>
<option value="total">총 학습시간</option>
</select>
</div>
</div>
<div id="statContent" class="space-y-3">
<!-- 평균학습 내용 -->
<div id="avgStats" class="space-y-3">
<?php foreach ($avg_counts as $data): ?>
<div
class="flex justify-between items-center pb-2 border-b border-gray-50 cursor-pointer hover:bg-gray-50 transition"
onclick="showCorpDetail('<?php echo htmlspecialchars($data['code']); ?>', 'avg', '<?php echo htmlspecialchars($data['code_name']); ?>')">
<span
class="text-sm font-medium text-gray-600"><?php echo htmlspecialchars($data['code_name']); ?></span><span
class="text-sm font-bold text-blue-600"><?php echo htmlspecialchars($data['avg_view_count']); ?></span>
</div>
<?php endforeach; ?>
</div>
<!-- 총 학습시간 내용 -->
<div id="totalStats" class="space-y-3" style="display: none;">
<?php foreach ($total_times as $data): ?>
<div
class="flex justify-between items-center pb-2 border-b border-gray-50 cursor-pointer hover:bg-gray-50 transition"
onclick="showCorpDetail('<?php echo htmlspecialchars($data['code']); ?>', 'total', '<?php echo htmlspecialchars($data['code_name']); ?>')">
<span
class="text-sm font-medium text-gray-600"><?php echo htmlspecialchars($data['code_name']); ?></span><span
class="text-sm font-bold text-blue-600"><?php echo htmlspecialchars($data['formatted_total_tm']); ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 법인별 접속 추이 (라인 차트) -->
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-gray-800 text-sm">법인별 접속 추이(로그인 법인)</h3>
<div class="flex flex-col gap-1 items-end">
<select id="accessTrendComp" class="text-xs bg-gray-50 border border-gray-100 rounded p-1 w-24"
onchange="changeAccessTrendComp()">
<option value="">전체</option>
<?php foreach ($companies as $comp): ?>
<option value="<?php echo htmlspecialchars($comp['code']); ?>" <?php echo $selected_access_comp === $comp['code'] ? 'selected' : ''; ?>><?php echo htmlspecialchars($comp['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- SVG Line Chart -->
<div class="relative w-full" style="height:240px;">
<svg viewBox="0 0 320 210" class="w-full h-full" xmlns="http://www.w3.org/2000/svg">
<?php
$max_access = 0;
foreach ($access_trends as $trend) {
$max_access = max($max_access, $trend['access_count']);
}
$target_max_access = ceil($max_access * 1.2);
if ($target_max_access == 0)
$target_max_access = 20;
$y_trend_step_count = 4;
$y_trend_unit = ceil($target_max_access / $y_trend_step_count);
$y_trend_max = $y_trend_unit * $y_trend_step_count;
$scale_y_trend = $y_trend_max > 0 ? 165 / $y_trend_max : 0;
?>
<!-- 격자선 -->
<?php for ($i = 0; $i <= $y_trend_step_count; $i++):
$y = 175 - ($i * (165 / $y_trend_step_count));
?>
<line x1="42" y1="<?php echo $y; ?>" x2="315" y2="<?php echo $y; ?>" stroke="#e5e7eb"
stroke-width="<?php echo $i === 0 ? '1' : '0.8'; ?>" <?php echo $i === 0 ? '' : 'stroke-dasharray="4,3"'; ?> />
<?php endfor; ?>
<!-- Y축 레이블 -->
<?php for ($i = 0; $i <= $y_trend_step_count; $i++):
$y = 175 - ($i * (165 / $y_trend_step_count));
$label = $i * $y_trend_unit;
?>
<text x="38" y="<?php echo $y + 3; ?>" text-anchor="end" font-size="11"
fill="#9ca3af"><?php echo number_format($label); ?></text>
<?php endfor; ?>
<!-- Y축 라인 -->
<line x1="42" y1="8" x2="42" y2="175" stroke="#d1d5db" stroke-width="1" />
<!-- 라인 경로 -->
<?php
$points = [];
$x_step = 273 / 11; // 12개월
$x_start = 42;
foreach ($access_trends as $trend) {
$month = $trend['month'];
$count = $trend['access_count'];
$x = $x_start + ($month - 1) * $x_step;
$y = 175 - ($count * $scale_y_trend);
$points[] = "$x,$y";
}
$points_str = implode(' ', $points);
?>
<polyline points="<?php echo $points_str; ?>" fill="none" stroke="#0d9488" stroke-width="2.5"
stroke-linejoin="round" />
<!-- 데이터 포인트 -->
<g>
<?php foreach ($access_trends as $trend):
$month = $trend['month'];
$count = $trend['access_count'];
$x = $x_start + ($month - 1) * $x_step;
$y = 175 - ($count * $scale_y_trend);
?>
<circle cx="<?php echo $x; ?>" cy="<?php echo $y; ?>" r="10" fill="transparent" class="cursor-pointer"
onclick="showAccessLogs(<?php echo $month; ?>)" />
<circle cx="<?php echo $x; ?>" cy="<?php echo $y; ?>" r="4.5" fill="white" stroke="#0d9488"
stroke-width="2.5" class="pointer-events-none" />
<?php endforeach; ?>
</g>
<!-- X축 레이블 -->
<?php for ($m = 1; $m <= 12; $m++):
$x = $x_start + ($m - 1) * $x_step;
?>
<text x="<?php echo $x; ?>" y="194" text-anchor="middle" font-size="10"
fill="#9ca3af"><?php echo $m; ?>월</text>
<?php endfor; ?>
</svg>
</div>
</div>
</section>
<!-- 하단: 가장 많이 본 영상 + 배움터 학습 랭킹 -->
<section class="grid grid-cols-1 lg:grid-cols-2 gap-6 pb-12">
<!-- 가장 많이 본 영상 -->
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<div class="p-5 border-b border-gray-50 flex justify-between items-center bg-gray-50/50">
<h3 class="font-bold text-gray-800 flex items-center italic text-sm">
<i class="fa-solid fa-play-circle text-blue-500 mr-2"></i>가장 많이 본 영상
</h3>
<div class="flex flex-col gap-1 items-end">
<div class="flex gap-1 items-center">
<input type="date" id="video_fr_date" value="<?php echo $video_fr_date; ?>"
class="text-xs border border-gray-200 rounded p-1" onchange="updateVideoDate()">
<span class="text-gray-400">~</span>
<input type="date" id="video_to_date" value="<?php echo $video_to_date; ?>"
class="text-xs border border-gray-200 rounded p-1" onchange="updateVideoDate()">
</div>
<select id="videoCategory" class="text-xs bg-white border border-gray-200 rounded p-1 w-24"
onchange="changeVideoCategory()">
<option value="CA10001">마이클래스</option>
<option value="CA10002">온보딩</option>
<option value="CA10003">법정교육</option>
<option value="CA10004">리더십</option>
<option value="CA10005">인사이트</option>
<option value="CA10006">비즈트렌드</option>
</select>
</div>
</div>
<div id="videoContent" class="p-5 space-y-5 overflow-y-auto max-h-[350px]">
<?php
$categories = ['CA10001' => '마이클래스', 'CA10002' => '온보딩', 'CA10003' => '법정교육', 'CA10004' => '리더십', 'CA10005' => '인사이트', 'CA10006' => '비즈트렌드'];
foreach ($categories as $cat_code => $cat_name):
$videos = array_filter($popular_videos, function ($v) use ($cat_code) {
return $v['category_code'] == $cat_code;
});
usort($videos, function ($a, $b) {
return $b['view_count'] - $a['view_count'];
});
$top5 = array_slice($videos, 0, 20);
?>
<div id="videos-<?php echo $cat_code; ?>" class="space-y-5"
style="display: <?php echo $cat_code == 'CA10001' ? 'block' : 'none'; ?>;">
<?php foreach ($top5 as $index => $video): ?>
<div class="flex items-center space-x-3">
<div class="font-bold text-blue-600 text-lg w-10 flex-shrink-0 text-center"><?php echo $index + 1; ?></div>
<div
class="w-24 h-14 bg-slate-200 rounded flex-shrink-0 flex items-center justify-center text-slate-400 text-xs">
<i class="fa-solid fa-play text-lg"></i>
</div>
<div>
<h4 class="font-bold text-sm leading-tight"><?php echo htmlspecialchars($video['content_title']); ?></h4>
<p class="text-[11px] text-gray-400 mt-1">시청수: <span
class="text-gray-700 font-bold"><?php echo htmlspecialchars($video['view_count']); ?>회</span></p>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<!--
<button class="w-full py-3 bg-slate-50 text-xs text-gray-400 font-medium hover:bg-slate-100 border-t border-gray-100 italic transition">
<i class="fa-solid fa-comment-dots mr-2"></i>한줄 소감문 보기
</button>
-->
</div>
<!-- 배움터 학습 랭킹 -->
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<div class="p-5 border-b border-gray-50 flex justify-between items-center bg-gray-50/50">
<h3 class="font-bold text-gray-800 flex items-center italic text-sm">
<i class="fa-solid fa-award text-teal-600 mr-2"></i>배움터 학습 랭킹
</h3>
<div class="flex flex-col gap-1 items-end">
<div class="flex gap-1 items-center">
<input type="date" id="rank_fr_date" value="<?php echo $rank_fr_date; ?>"
class="text-xs border border-gray-200 rounded p-1" onchange="updateRankingDate()">
<span class="text-gray-400">~</span>
<input type="date" id="rank_to_date" value="<?php echo $rank_to_date; ?>"
class="text-xs border border-gray-200 rounded p-1" onchange="updateRankingDate()">
</div>
<div class="flex gap-2 items-center">
<label class="flex items-center text-xs text-gray-500 cursor-pointer">
<input type="checkbox" id="excludeAdmin" class="mr-1" <?php echo $exclude_admin ? 'checked' : ''; ?>
onchange="updateRankingFilter()">
관리자 제외
</label>
<select id="rankingComp" class="text-xs bg-white border border-gray-200 rounded p-1 w-24"
onchange="changeRankingComp()">
<option value="">전체</option>
<?php foreach ($companies as $comp): ?>
<option value="<?php echo htmlspecialchars($comp['code']); ?>" <?php echo $selected_ranking_comp === $comp['code'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($comp['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="p-4 overflow-y-auto max-h-[350px]">
<table class="w-full text-sm">
<tbody>
<?php foreach ($rankings as $index => $rank):
$level = $rank['total_hours'] >= 40 ? 'Master' : ($rank['total_hours'] >= 20 ? 'Elite' : ($rank['total_hours'] >= 8 ? 'Learner' : 'Rookie'));
$level_color = $level == 'Master' ? 'purple' : ($level == 'Elite' ? 'blue' : ($level == 'Learner' ? 'green' : 'gray'));
?>
<tr class="hover:bg-gray-50 transition <?php echo $index > 0 ? 'border-t border-gray-50' : ''; ?>">
<td class="p-3 font-bold text-blue-600 text-lg w-10"><?php echo $index + 1; ?></td>
<td class="p-3">
<p class="font-bold"><?php echo htmlspecialchars($rank['name']); ?></p>
<p class="text-[10px] text-gray-400"><?php echo htmlspecialchars($rank['company_name']); ?>
<?php echo htmlspecialchars($rank['dept_name']); ?>
</p>
</td>
<td class="p-3 text-right">
<span class="font-bold mr-2"><?php echo number_format($rank['total_hours'], 1); ?>시간</span>
<span
class="px-2 py-0.5 bg-<?php echo $level_color; ?>-100 text-<?php echo $level_color; ?>-600 text-[10px] rounded font-bold"><?php echo $level; ?></span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!--
<button class="w-full py-3 bg-slate-50 text-xs text-gray-400 font-medium hover:bg-slate-100 border-t border-gray-100 italic transition">
<i class="fa-solid fa-thumbs-up mr-2"></i>추천 영상 보기
</button>
-->
</div>
</section>
<!-- 접속자 리스트 모달 -->
<div id="accessListModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 hidden">
<div class="bg-white rounded-xl shadow-lg w-full max-w-4xl overflow-hidden flex flex-col max-h-[85vh]">
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<h3 class="font-bold text-gray-800" id="accessListTitle">법인별 접속자 리스트</h3>
<button onclick="closeAccessListModal()" class="text-gray-400 hover:text-gray-600 transition">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="px-6 py-4 bg-gray-50/50 flex flex-wrap gap-4 items-center">
<div class="flex items-center gap-2">
<span class="text-xs font-bold text-gray-600">조회기간</span>
<input type="date" id="modal_access_fr_date"
class="border border-gray-300 rounded px-2 py-1 text-sm bg-white shadow-sm focus:ring-2 focus:ring-blue-500 outline-none">
<span class="text-gray-400">~</span>
<input type="date" id="modal_access_to_date"
class="border border-gray-300 rounded px-2 py-1 text-sm bg-white shadow-sm focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div class="flex items-center gap-2">
<span class="text-xs font-bold text-gray-600">기준법인</span>
<select id="modal_access_comp"
class="border border-gray-300 rounded px-2 py-1 text-sm bg-white shadow-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">전체</option>
<?php foreach ($companies as $comp): ?>
<option value="<?php echo htmlspecialchars($comp['code']); ?>">
<?php echo htmlspecialchars($comp['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button onclick="searchAccessLogsInModal()"
class="px-4 py-1.5 bg-blue-600 text-white rounded font-bold text-sm hover:bg-blue-700 transition flex items-center transform active:scale-95 duration-100">
<i class="fa-solid fa-magnifying-glass mr-2 text-xs"></i>검색
</button>
</div>
<div class="px-6 pb-6 pt-2 overflow-y-auto max-h-[50vh]">
<table class="w-full text-sm text-left border-collapse">
<thead class="bg-gray-100 text-gray-600 sticky top-0 z-10 whitespace-nowrap shadow-[0_1px_0_0_#e5e7eb]">
<tr>
<th class="py-2 px-4 font-bold border-b border-gray-200">기준법인</th>
<th class="py-2 px-4 font-bold border-b border-gray-200">이름</th>
<th class="py-2 px-4 font-bold border-b border-gray-200">부서명</th>
<th class="py-2 px-4 font-bold border-b border-gray-200">직위</th>
<th id="thAccessedAt"
class="py-2 px-4 font-bold border-b border-gray-200 cursor-pointer select-none hover:bg-gray-200 transition whitespace-nowrap"
onclick="sortByAccessedAt()">
접속일시 <span id="sortIcon" class="ml-1 text-gray-400">↕</span>
</th>
</tr>
</thead>
<tbody id="accessListBody">
<!-- 데이터 삽입 영역 -->
</tbody>
</table>
<div id="accessListEmpty" class="text-center py-6 text-gray-500 hidden">
접속자 데이터가 없습니다.
</div>
</div>
</div>
</div>
<!-- 법인별 통계 상세 모달 -->
<div id="corpDetailModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 hidden">
<div class="bg-white rounded-xl shadow-lg w-full max-w-5xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<h3 class="font-bold text-gray-800" id="corpDetailTitle">법인별 통계 상세 정보</h3>
<button onclick="closeCorpDetailModal()" class="text-gray-400 hover:text-gray-600 transition">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="px-6 py-4 bg-gray-50/50 flex flex-wrap gap-4 items-center">
<div class="flex items-center gap-2">
<span class="text-xs font-bold text-gray-600">조회기간</span>
<input type="date" id="modal_stat_fr_date"
class="border border-gray-300 rounded px-2 py-1 text-sm bg-white shadow-sm focus:ring-2 focus:ring-teal-500 outline-none">
<span class="text-gray-400">~</span>
<input type="date" id="modal_stat_to_date"
class="border border-gray-300 rounded px-2 py-1 text-sm bg-white shadow-sm focus:ring-2 focus:ring-teal-500 outline-none">
</div>
<div class="flex items-center gap-2">
<span class="text-xs font-bold text-gray-600">기준법인</span>
<select id="modal_stat_comp_code"
class="border border-gray-300 rounded px-2 py-1 text-sm bg-white shadow-sm focus:ring-2 focus:ring-teal-500 outline-none">
<option value="">전체</option>
<?php foreach ($companies as $comp): ?>
<option value="<?php echo htmlspecialchars($comp['code']); ?>">
<?php echo htmlspecialchars($comp['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button onclick="searchCorpDetailInModal()"
class="px-4 py-1.5 bg-teal-600 text-white rounded font-bold text-sm hover:bg-teal-700 transition flex items-center shadow-md transform active:scale-95 duration-100">
<i class="fa-solid fa-magnifying-glass mr-2 text-xs"></i>검색
</button>
</div>
<div class="px-6 pb-6 pt-2 overflow-y-auto">
<table class="w-full text-sm text-left border-collapse">
<thead class="bg-gray-100 text-gray-600 sticky top-0 z-10 whitespace-nowrap shadow-[0_1px_0_0_#e5e7eb]">
<tr>
<th class="py-2 px-4 font-bold border-b border-gray-200 w-[5%] text-center">번호</th>
<th class="py-2 px-4 font-bold border-b border-gray-200 w-[10%] text-center">사번</th>
<th class="py-2 px-4 font-bold border-b border-gray-200 w-[12%]">성명</th>
<th class="py-2 px-4 font-bold border-b border-gray-200 w-[18%]">부서</th>
<th class="py-2 px-4 font-bold border-b border-gray-200 w-[30%]">과정명</th>
<th class="py-2 px-4 font-bold border-b border-gray-200 w-[25%] text-center">최종학습일</th>
</tr>
</thead>
<tbody id="corpDetailBody">
<!-- 데이터 삽입 영역 -->
</tbody>
</table>
<div id="corpDetailEmpty" class="text-center py-6 text-gray-500 hidden">
데이터가 없습니다.
</div>
</div>
</div>
</div>
</main>
<script>
function getCommonParams() {
return {
ranking_comp: document.getElementById('rankingComp').value,
rank_fr_date: document.getElementById('rank_fr_date').value,
rank_to_date: document.getElementById('rank_to_date').value,
exclude_admin: document.getElementById('excludeAdmin').checked ? '1' : '0',
access_comp: document.getElementById('accessTrendComp').value,
video_fr_date: document.getElementById('video_fr_date').value,
video_to_date: document.getElementById('video_to_date').value,
video_category: document.getElementById('videoCategory').value,
fr_date: '<?php echo $fr_date; ?>',
to_date: '<?php echo $to_date; ?>'
};
}
function reloadWithParams(params) {
const urlParams = new URLSearchParams(window.location.search);
for (const [key, value] of Object.entries(params)) {
urlParams.set(key, value);
}
window.location.href = '?' + urlParams.toString();
}
function changeYear(year) {
const urlParams = new URLSearchParams(window.location.search);
urlParams.set('year', year);
urlParams.set('fr_date', year + '-01-01');
urlParams.set('to_date', year + '-12-31');
// 섹션별 상세 필터 파라미터가 있다면 제거하여 새 년도 기본값으로 리셋
const paramsToRemove = [
'rank_fr_date', 'rank_to_date',
'video_fr_date', 'video_to_date',
'stat_fr_date', 'stat_to_date',
'access_fr_date', 'access_to_date'
];
paramsToRemove.forEach(p => urlParams.delete(p));
window.location.href = '?' + urlParams.toString();
}
function updateRankingDate() {
const p = getCommonParams();
reloadWithParams(p);
}
function updateRankingFilter() {
const p = getCommonParams();
reloadWithParams(p);
}
function changeRankingComp() {
const p = getCommonParams();
reloadWithParams(p);
}
function updateVideoDate() {
const p = getCommonParams();
reloadWithParams(p);
}
function changeAccessTrendComp() {
const p = getCommonParams();
reloadWithParams(p);
}
function changeStatType() {
const type = document.getElementById('statType').value;
document.getElementById('avgStats').style.display = type === 'avg' ? 'block' : 'none';
document.getElementById('totalStats').style.display = type === 'total' ? 'block' : 'none';
}
function changeVideoCategory() {
const category = document.getElementById('videoCategory').value;
const contents = document.querySelectorAll('#videoContent > div');
contents.forEach(div => {
div.style.display = div.id === 'videos-' + category ? 'block' : 'none';
});
}
// 접속자 리스트 정렬 상태
let _accessLogData = [];
let _accessSortDir = 'desc'; // 기본: 최신순
function renderAccessLogTable(data) {
const tbody = document.getElementById('accessListBody');
tbody.innerHTML = '';
if (data.length === 0) {
document.getElementById('accessListEmpty').classList.remove('hidden');
return;
}
document.getElementById('accessListEmpty').classList.add('hidden');
data.forEach(log => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-100 hover:bg-gray-50';
tr.innerHTML = `
<td class="py-2 px-4 text-gray-700">${log.comp_name || log.sys_comp_code || '-'}</td>
<td class="py-2 px-4 text-gray-800 font-medium">${log.name || '-'}</td>
<td class="py-2 px-4 text-gray-600">${log.dept_name || '-'}</td>
<td class="py-2 px-4 text-gray-600">${log.rank_name || '-'}</td>
<td class="py-2 px-4 text-gray-500">${log.accessed_at || '-'}</td>
`;
tbody.appendChild(tr);
});
}
function sortByAccessedAt() {
if (_accessLogData.length === 0) return;
_accessSortDir = _accessSortDir === 'desc' ? 'asc' : 'desc';
const icon = document.getElementById('sortIcon');
if (_accessSortDir === 'asc') {
icon.textContent = '↑';
icon.classList.remove('text-gray-400');
icon.classList.add('text-blue-500');
} else {
icon.textContent = '↓';
icon.classList.remove('text-gray-400');
icon.classList.add('text-blue-500');
}
const sorted = [..._accessLogData].sort((a, b) => {
const da = new Date(a.accessed_at || 0);
const db = new Date(b.accessed_at || 0);
return _accessSortDir === 'asc' ? da - db : db - da;
});
renderAccessLogTable(sorted);
}
function showAccessLogs(month) {
const fr_date = '<?php echo $access_fr_date; ?>';
const to_date = '<?php echo $access_to_date; ?>';
const accessComp = document.getElementById('accessTrendComp').value;
document.getElementById('modal_access_fr_date').value = fr_date;
document.getElementById('modal_access_to_date').value = to_date;
document.getElementById('modal_access_comp').value = accessComp;
// 정렬 상태 초기화
_accessLogData = [];
_accessSortDir = 'desc';
const icon = document.getElementById('sortIcon');
icon.textContent = '↕';
icon.className = 'ml-1 text-gray-400';
document.getElementById('accessListModal').classList.remove('hidden');
searchAccessLogsInModal(month);
}
function searchAccessLogsInModal(month = '') {
const fr_date = document.getElementById('modal_access_fr_date').value;
const to_date = document.getElementById('modal_access_to_date').value;
const accessComp = document.getElementById('modal_access_comp').value;
const year = fr_date ? fr_date.split('-')[0] : '<?php echo $selected_year; ?>';
document.getElementById('accessListTitle').innerText = accessComp ? `${accessComp} 접속자 리스트 (${fr_date} ~ ${to_date})` : `전체 접속자 리스트 (${fr_date} ~ ${to_date})`;
document.getElementById('accessListBody').innerHTML = '<tr><td colspan="5" class="text-center py-4 text-gray-500"><i class="fa-solid fa-spinner fa-spin mr-2"></i>로딩 중...</td></tr>';
document.getElementById('accessListEmpty').classList.add('hidden');
fetch(`../bbs/get_access_logs.php?year=${year}&month=${month}&access_comp=${accessComp}&fr_date=${fr_date}&to_date=${to_date}`)
.then(response => response.json())
.then(res => {
if (res.success) {
_accessLogData = res.data;
document.getElementById('accessListTitle').innerText += ` - ${_accessLogData.length}회`;
renderAccessLogTable(_accessLogData);
} else {
alert(res.message);
}
})
.catch(error => {
console.error('Error fetching logs:', error);
document.getElementById('accessListBody').innerHTML = '<tr><td colspan="5" class="text-center py-4 text-red-500">데이터를 불러오는 중 오류가 발생했습니다.</td></tr>';
});
}
function closeAccessListModal() {
document.getElementById('accessListModal').classList.add('hidden');
}
function showCorpDetail(corpCode, type, corpName) {
const fr_date = '<?php echo $stat_fr_date; ?>';
const to_date = '<?php echo $stat_to_date; ?>';
document.getElementById('modal_stat_fr_date').value = fr_date;
document.getElementById('modal_stat_to_date').value = to_date;
document.getElementById('modal_stat_comp_code').value = corpCode;
document.getElementById('_corp_detail_type') ? null : (window._corp_detail_type = type);
document.getElementById('corpDetailModal').classList.remove('hidden');
searchCorpDetailInModal(type, corpName);
}
function searchCorpDetailInModal(type = window._corp_detail_type, corpName) {
const corpCode = document.getElementById('modal_stat_comp_code').value;
const fr_date = document.getElementById('modal_stat_fr_date').value;
const to_date = document.getElementById('modal_stat_to_date').value;
// 모달 타이틀 업데이트 (선택된 법인명 가져오기)
const select = document.getElementById('modal_stat_comp_code');
const selectedName = select.options[select.selectedIndex].text;
document.getElementById('corpDetailTitle').innerText = corpCode ? `${selectedName} 통계 상세 정보` : `전체 법인 통계 상세 정보`;
document.getElementById('corpDetailBody').innerHTML = '<tr><td colspan="6" class="text-center py-4 text-gray-500"><i class="fa-solid fa-spinner fa-spin mr-2"></i>로딩 중...</td></tr>';
document.getElementById('corpDetailEmpty').classList.add('hidden');
fetch(`../bbs/get_corp_stats_detail.php?corp_code=${corpCode}&fr_date=${fr_date}&to_date=${to_date}&type=${type}`)
.then(response => response.json())
.then(res => {
if (res.success) {
const tbody = document.getElementById('corpDetailBody');
tbody.innerHTML = '';
if (res.data.length === 0) {
document.getElementById('corpDetailEmpty').classList.remove('hidden');
return;
}
res.data.forEach((item, index) => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-100 hover:bg-gray-50';
tr.innerHTML = `
<td class="py-2 px-4 text-gray-500 text-center text-xs">${index + 1}</td>
<td class="py-2 px-4 text-gray-700 text-center">${item.member_id || '-'}</td>
<td class="py-2 px-4 text-gray-800 font-medium">${item.name || '-'}</td>
<td class="py-2 px-4 text-gray-600">${item.dept_name || '-'}</td>
<td class="py-2 px-4 text-gray-600 truncate max-w-0" title="${item.content_title || ''}">${item.content_title || '-'}</td>
<td class="py-2 px-4 text-gray-500 text-center">${item.last_viewed_at || '-'}</td>
`;
tbody.appendChild(tr);
});
} else {
alert(res.message);
}
})
.catch(error => {
console.error('Error fetching detail:', error);
document.getElementById('corpDetailBody').innerHTML = '<tr><td colspan="6" class="text-center py-4 text-red-500">데이터를 불러오는 중 오류가 발생했습니다.</td></tr>';
});
}
function closeCorpDetailModal() {
document.getElementById('corpDetailModal').classList.add('hidden');
}
</script>
</body>
</html>