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
+25
View File
@@ -0,0 +1,25 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = (string)($_SESSION['member_id'] ?? '');
$authLevel = strtoupper(trim((string)($_SESSION['auth_level'] ?? '')));
if ($memberId === '') {
header('Location: /bbs/login.php');
exit;
}
if (!in_array($authLevel, ['LE10001', 'LE10002'], true)) {
http_response_code(403);
exit('접근 권한이 없습니다.');
}
$currentPage = basename((string)($_SERVER['PHP_SELF'] ?? ''));
$legalOnlyPage = 'legal_edu.php';
if ($authLevel === 'LE10002' && $currentPage !== $legalOnlyPage) {
header('Location: /admin/skin/' . $legalOnlyPage);
exit;
}
+767
View File
@@ -0,0 +1,767 @@
<?php
require_once __DIR__ . '/../../bbs/auth.php';
edu_require_login();
// 공통 상단 영역(헤더)과 데이터베이스 연결 파일을 포함합니다.
include_once 'header.php';
require_once __DIR__ . '/../../bbs/db_conn.php';
// PDO 연결 생성
$pdo = db_conn();
// 대분류 카테고리(CA100) 목록을 프로시저를 통해 DB에서 조회합니다.
$catsStmt = $pdo->prepare("CALL proc_get_code_list('CA100')");
$catsStmt->execute();
$categories = $catsStmt->fetchAll();
$catsStmt->closeCursor();
// 분기(quarter) 목록을 edu_codes 에서 desc01 기준으로 조회합니다.
// desc01 = 'CA10001' 인 코드들을 분기 코드로 사용합니다.
$qtStmt = $pdo->prepare("SELECT base_code AS code, code_name AS name FROM edu_codes WHERE desc01 = 'CA10001'");
$qtStmt->execute();
$quarters = $qtStmt->fetchAll(PDO::FETCH_ASSOC);
$qtStmt->closeCursor();
// 키워드(KW100) 목록을 프로시저를 통해 DB에서 조회합니다.
$keywordsStmt = $pdo->prepare("CALL proc_get_code_list('KW100')");
$keywordsStmt->execute();
$keywords = $keywordsStmt->fetchAll(PDO::FETCH_ASSOC);
$keywordsStmt->closeCursor();
$goalNosStmt = $pdo->prepare("CALL proc_get_code_list('GN100')");
$goalNosStmt->execute();
$goalNos = $goalNosStmt->fetchAll(PDO::FETCH_ASSOC);
$goalNosStmt->closeCursor();
// 인사이트 이슈구분(IS100) 목록을 사전 준비합니다.
$is100Stmt = $pdo->prepare("CALL proc_get_code_list('IS100')");
$is100Stmt->execute();
$issueTypesIS = $is100Stmt->fetchAll(PDO::FETCH_ASSOC);
$is100Stmt->closeCursor();
// 리더십 이슈구분(LD100) 목록을 사전 준비합니다.
$ld100Stmt = $pdo->prepare("CALL proc_get_code_list('LD100')");
$ld100Stmt->execute();
$issueTypesLD = $ld100Stmt->fetchAll(PDO::FETCH_ASSOC);
$ld100Stmt->closeCursor();
// 기본 선택될 카테고리 코드를 결정합니다. '마이클래스'를 찾으면 우선 적용합니다.
$defaultCatCode = null;
foreach ($categories as $c) {
if ($c['name'] === '마이클래스') {
$defaultCatCode = $c['code'];
break;
}
}
// '마이클래스'가 없으면 첫 번째 카테고리를 기본값으로 사용합니다.
if ($defaultCatCode === null && count($categories) > 0) {
$defaultCatCode = $categories[0]['code'];
}
// GET 파라미터로 넘겨받은 검색 조건(카테고리, 검색어)을 변수에 저장합니다.
$category = isset($_GET['category']) ? $_GET['category'] : 'CA10006';
$q = isset($_GET['q']) ? trim($_GET['q']) : '';
// 쿼리 바인딩을 위한 파라미터 배열과 WHERE 조건 배열을 초기화합니다.
$params = [];
$where = [];
// 카테고리 검색 조건이 '전체'가 아닐 경우 조건절에 추가합니다.
if ($category !== '' && $category !== '전체') {
$where[] = 'category_code = :category';
$params[':category'] = $category;
}
// 텍스트 검색어가 있을 경우 제목(title) 부분 일치 조건(LIKE)을 추가합니다.
if ($q !== '') {
$where[] = 'title LIKE :q';
$params[':q'] = '%' . $q . '%';
}
// 메인 콘텐츠 리스트를 조회하기 위한 기본 SQL 쿼리입니다.
// 서브쿼리를 사용해 카테고리명, 중분류명, 연결된 학습목표명을 함께 가져옵니다.
$sql = 'SELECT c.content_id, c.category_code, c.category_group, c.title, c.is_offer, c.is_active, c.sort_order, c.description,c.description1, c.description2, c.description3, c.content_url, c.goal_code, c.issue_type_code, c.offer_id, c.start_date,
(SELECT COUNT(*) FROM edu_content_keywords ck WHERE ck.content_id=c.content_id AND ck.is_active=\'1\') as keyword_count,
(SELECT code_name FROM edu_codes WHERE group_code=\'CA100\' AND base_code=c.category_code LIMIT 1) as cat_name,
(SELECT code_name FROM edu_codes WHERE group_code=\'CA200\' AND base_code=c.category_group LIMIT 1) as group_name,
(SELECT title FROM edu_learning_goals WHERE goal_code=c.goal_code LIMIT 1) as goal_title,
(SELECT code_name FROM edu_codes WHERE group_code=\'IS100\' AND base_code=c.issue_type_code LIMIT 1) as issue_type_name
FROM edu_contents c';
// WHERE 조건이 하나라도 있다면 조합하여 SQL문에 덧붙입니다.
if ($where) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
// 정렬 순서를 지정합니다.
$sql .= ' ORDER BY c.category_code , c.category_group ,c.goal_code, c.content_id DESC';
// 완성된 쿼리를 실행하여 결과($rows)를 가져옵니다.
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
?>
<main class="max-w-[1600px] mx-auto p-6">
<header class="flex justify-between items-center mb-8">
<h2 class="text-3xl font-bold text-gray-800 tracking-tight">콘텐츠 입력</h2>
<div class="flex items-center gap-2">
<button id="btn-goal" onclick="openGoalModal()"
class="px-4 py-2 bg-blue-700 text-white rounded-lg font-bold flex items-center shadow hover:bg-blue-800 transition">
<i class="fa-solid fa-bullseye mr-2"></i>학습목표 등록
</button>
<button onclick="openNewModal()"
class="px-5 py-2.5 bg-[#114b3d] text-white rounded-lg font-bold flex items-center shadow-lg hover:bg-[#0d3a2f] transition">
<i class="fa-solid fa-plus mr-2"></i>새 콘텐츠 추가
</button>
</div>
</header>
<section class="bg-white p-6 rounded-2xl border border-gray-200 shadow-sm mb-8">
<form class="grid grid-cols-1 md:grid-cols-3 gap-6" method="get">
<div>
<label class="block text-sm font-bold text-gray-700 mb-2">카테고리</label>
<select name="category" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-xl p-3 bg-gray-50 focus:ring-2 focus:ring-teal-500">
<option <?= $category === '전체' ? 'selected' : ''; ?>>전체</option>
<?php foreach ($categories as $c): ?>
<option value="<?= htmlspecialchars($c['code'], ENT_QUOTES, 'UTF-8'); ?>" <?= $category === $c['code'] ? 'selected' : ''; ?>><?= htmlspecialchars($c['name'], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-2">콘텐츠명 검색</label>
<div class="relative">
<input name="q" value="<?= htmlspecialchars($q, ENT_QUOTES, 'UTF-8'); ?>" type="text"
placeholder="콘텐츠명을 검색하세요"
class="w-full border-gray-200 rounded-xl p-3 pl-11 bg-gray-50 focus:ring-2 focus:ring-teal-500">
<i class="fa-solid fa-magnifying-glass absolute left-4 top-4 text-gray-400"></i>
</div>
</div>
<div class="flex items-end">
<button class="w-full md:w-auto px-5 py-3 bg-gray-800 text-white rounded-xl font-bold">검색</button>
</div>
</form>
</section>
<section class="bg-white rounded-2xl border border-gray-200 shadow-sm overflow-hidden mb-12">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50/50 border-b border-gray-100">
<th class="p-4 text-sm font-bold text-gray-500 w-16">No</th>
<th class="p-4 text-sm font-bold text-gray-500 w-32">콘텐츠ID</th>
<th class="p-4 text-sm font-bold text-gray-500 w-44">카테고리</th>
<th class="p-4 text-sm font-bold text-gray-500 w-44 whitespace-nowrap">카테고리구분</th>
<th class="p-4 text-sm font-bold text-gray-500">콘텐츠명</th>
<th class="p-4 text-sm font-bold text-gray-500 ">콘텐츠설명</th>
<th class="p-4 text-sm font-bold text-gray-500 hidden">콘텐츠설명1</th>
<th class="p-4 text-sm font-bold text-gray-500 hidden">콘텐츠설명2</th>
<th class="p-4 text-sm font-bold text-gray-500 hidden">콘텐츠설명3</th>
<th class="p-4 text-sm font-bold text-gray-500">URL</th>
<th class="p-4 text-sm font-bold text-gray-500 w-28 text-center">키워드</th>
<th class="p-4 text-sm font-bold text-gray-500">사용여부</th>
<th class="p-4 text-sm font-bold text-gray-500 text-center w-24">관리</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50 text-sm">
<?php if (!$rows): ?>
<tr>
<td colspan="8" class="p-6 text-center text-gray-400">등록된 콘텐츠가 없습니다</td>
</tr>
<?php else: ?>
<?php $idx = 0;
foreach ($rows as $row):
$idx++; ?>
<tr class="hover:bg-teal-50/30 transition">
<td class="p-4 text-center text-gray-400"><?= $idx; ?></td>
<td class="p-4 font-mono text-xs text-gray-500">
<?= htmlspecialchars($row['content_id'], ENT_QUOTES, 'UTF-8'); ?>
</td>
<td class="p-4">
<span
class="text-gray-600 bg-transparent border-none rounded-none text-sm p-0"><?= htmlspecialchars($row['cat_name'] ?: $row['category_code'], ENT_QUOTES, 'UTF-8'); ?></span>
</td>
<td class="p-4 text-gray-600 text-sm whitespace-nowrap">
<?= htmlspecialchars($row['group_name'] ?: '-', ENT_QUOTES, 'UTF-8'); ?>
</td>
<?php
$displayTitle = $row['title'];
$catDisplay = $row['cat_name'] ?: $row['category_code'];
if ($catDisplay === '마이클래스' && $row['goal_title']) {
$displayTitle = '[' . $row['goal_title'] . '] ' . $displayTitle;
}
?>
<td class="p-4 font-bold text-gray-800 truncate max-w-xs"
title="<?= htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8'); ?>
</td>
<td class="p-4 text-gray-500 truncate max-w-xs"
title="<?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
</td>
<td class="p-4 text-gray-500 truncate max-w-xs hidden"
title="<?= htmlspecialchars($row['description1'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($row['description1'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
</td>
<td class="p-4 text-gray-500 truncate max-w-xs hidden"
title="<?= htmlspecialchars($row['description2'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($row['description2'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
</td>
<td class="p-4 text-gray-500 truncate max-w-xs hidden"
title="<?= htmlspecialchars($row['description3'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($row['description3'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
</td>
<td class="p-4">
<?php if ($row['content_url']): ?>
<a href="https://www.youtube.com/watch?v=<?= htmlspecialchars($row['content_url'], ENT_QUOTES, 'UTF-8'); ?>"
target="_blank" class="text-blue-600 hover:underline"><i class="fa-solid fa-link"></i></a>
<?php endif; ?>
</td>
<td class="p-4 text-center text-gray-600 font-bold">
<?= (int) ($row['keyword_count'] ?? 0); ?>
</td>
<td class="p-4 text-center">
<?php if ($row['is_active'] === '1'): ?>
<i class="fa-solid fa-check text-teal-600"></i>
<?php else: ?>
<span class="text-gray-300">-</span>
<?php endif; ?>
</td>
<td class="p-4 text-center">
<button class="px-3 py-1 bg-teal-800 text-white rounded text-xs hover:bg-teal-900 transition btn-edit"
data-row="<?= htmlspecialchars(json_encode($row), ENT_QUOTES, 'UTF-8'); ?>">수정</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</section>
</main>
<div id="upload-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[100] hidden p-4">
<div class="bg-white w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden">
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<h3 class="text-xl font-bold text-gray-800">새 콘텐츠 등록</h3>
<button onclick="document.getElementById('upload-modal').classList.add('hidden')"
class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-xmark text-xl"></i></button>
</div>
<form action="../bbs/content_save.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="content_id" id="content_id" value="">
<div class="p-8 space-y-5">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">카테고리 <span
class="text-red-500">*</span></label>
<div class="flex items-center gap-3">
<!-- Hidden input to ensure value is sent when disabled via JS on edit -->
<input type="hidden" name="category_code_hidden" id="category_code_hidden">
<select name="category_code" id="category_code"
class="flex-1 border-gray-200 rounded-lg p-2.5 bg-gray-50 font-bold focus:border-teal-500 focus:ring-teal-500"
required onchange="document.getElementById('category_code_hidden').value=this.value;">
<?php foreach ($categories as $c): ?>
<option value="<?= htmlspecialchars($c['code'], ENT_QUOTES, 'UTF-8'); ?>"
data-name="<?= htmlspecialchars($c['name'], ENT_QUOTES, 'UTF-8'); ?>" <?= $c['code'] === $defaultCatCode ? 'selected' : ''; ?>>
<?= htmlspecialchars($c['name'], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">카테고리구분</label>
<select id="category_group" name="category_group"
class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50">
<option value="">선택</option>
</select>
</div>
</div>
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">콘텐츠명 (강좌명)</label>
<input name="title" type="text" class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50"
placeholder="콘텐츠 제목을 입력하세요" required>
</div>
<!-- 영상 길이 저장용 hidden (edu_contents.content_tm) -->
<input type="hidden" name="content_tm" id="content_tm_input" value="">
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">유튜브 URL (영상 ID)</label>
<div class="flex gap-2 items-center">
<input name="content_url" id="content_url_input" type="text"
class="flex-1 border-gray-200 rounded-lg p-2.5 bg-gray-50"
placeholder="예: ELd23Hll3is 또는 전체 URL">
<button type="button" id="btn-yt-fetch"
onclick="fetchYouTubeInfo()"
class="hidden px-3 py-2.5 bg-red-600 text-white rounded-lg text-xs font-bold whitespace-nowrap hover:bg-red-700 transition flex items-center gap-1.5">
<i class="fa-brands fa-youtube"></i>영상 정보 가져오기
</button>
</div>
<!-- 유튜브 API 상태 메시지 -->
<div id="yt-fetch-status" class="mt-1.5 text-xs hidden">
<span id="yt-fetch-msg"></span>
</div>
</div>
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase flex justify-between">
<span>콘텐츠설명</span>
<span id="yt-duration-display" class="text-gray-300 font-normal hidden">⏱ <span id="yt-duration-text"></span></span>
</label>
<textarea name="description" id="description_input" rows="2"
class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50 resize-none"
placeholder="유튜브 URL 입력 후 [영상 정보 가져오기]를 클릭하면 자동 입력됩니다."></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">콘텐츠설명1</label>
<input name="description1" type="text" class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50"
placeholder="">
</div>
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">콘텐츠설명2</label>
<input name="description2" type="text" class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50"
placeholder="">
</div>
</div>
<!-- 마이클래스 전용 -->
<div class="space-y-4 cat-block" data-cat="마이클래스">
<div class="grid grid-cols-2 gap-4">
<div class="col-span-1">
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">기준년도</label>
<input name="base_year" type="text" maxlength="4"
class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50" placeholder="YYYY" value="<?= date('Y'); ?>">
</div>
</div>
<div class="grid grid-cols-1">
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">학습목표코드</label>
<div class="flex gap-2">
<select name="goal_code" id="goal_code" class="flex-1 border-gray-200 rounded-lg p-2.5 bg-gray-50">
<option value="">선택</option>
</select>
<button type="button" onclick="openGoalModal()"
class="px-3 py-2 bg-blue-600 text-white rounded-lg text-xs whitespace-nowrap">등록</button>
</div>
</div>
</div>
</div>
<!-- 온보딩 -->
<div class="space-y-4 cat-block hidden" data-cat="온보딩"></div>
<!-- 법정교육 -->
<div class="space-y-4 cat-block hidden" data-cat="법정교육">
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">기준년도</label>
<input name="base_year_law" type="text" class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50"
placeholder="예: 2026">
</div>
</div>
<!-- 리더십 / 인사이트 -->
<div class="space-y-4 cat-block hidden" data-cat="인사이트,리더십">
<div class="grid grid-cols-2 gap-4">
<div id="issue_type_is_container">
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">이슈구분 (인사이트)</label>
<select id="issue_type_code_is" name="issue_type_code"
class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50">
<option value="">선택</option>
<?php foreach ($issueTypesIS as $it): ?>
<option value="<?= htmlspecialchars($it['code'], ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($it['name'], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div id="issue_type_ld_container" class="hidden">
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">이슈구분 (리더십)</label>
<select id="issue_type_code_ld" name="issue_type_code"
class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50" disabled>
<option value="">선택</option>
<?php foreach ($issueTypesLD as $it): ?>
<option value="<?= htmlspecialchars($it['code'], ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($it['name'], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="flex items-center gap-6">
<label class="inline-flex items-center space-x-2 cursor-pointer">
<input name="is_offer" type="checkbox" class="w-4 h-4 text-teal-600 rounded"> <span
class="text-sm font-bold">추천콘텐츠 적용</span>
</label>
<div class="flex items-center gap-2" id="offer_id_container">
<span class="text-xs font-bold text-gray-400 uppercase">제안ID</span>
<input name="offer_id" type="text" class="border-gray-200 rounded-lg p-2.5 bg-gray-50">
</div>
</div>
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">콘텐츠설명3 (메모)</label>
<textarea name="description3" rows="3"
class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50 resize-none"
placeholder="콘텐츠설명3 내용을 입력하세요"></textarea>
</div>
</div>
<!-- 비즈트렌드 -->
<div class="space-y-4 cat-block hidden" data-cat="비즈트렌드">
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">기준일자</label>
<input name="start_date_bzt" type="date" class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50">
</div>
<!-- 사내도서여부: category_group=CA200B01(1부) 일때만 표시 (JS 제어) -->
<div id="book_yn_container" class="hidden">
<label class="inline-flex items-center space-x-2 cursor-pointer">
<input name="book_yn" id="book_yn" type="checkbox" class="w-4 h-4 text-teal-600 rounded" value="1">
<span class="text-sm font-bold">사내도서여부</span>
</label>
</div>
</div>
<!-- Keyword UI logic moved to modal -->
<div class="flex items-end gap-4">
<div class="flex-1" id="sort_order_container" style="display:none;">
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">정렬 순번</label>
<input name="sort_order" type="number" class="w-full border-gray-200 rounded-lg p-2.5 bg-gray-50" value="">
</div>
<div class="flex items-center space-x-4">
<label class="inline-flex items-center space-x-2 cursor-pointer">
<input name="is_active" type="checkbox" class="w-4 h-4 text-teal-600 rounded" checked> <span
class="text-sm font-bold">사용여부</span>
</label>
<button type="button" onclick="openKeywordModal()"
class="text-sm font-bold text-orange-600 hover:text-orange-700 underline underline-offset-2">키워드등록(<span
id="keyword-count-display">0</span>개)</button>
<div id="image_upload_container" class="hidden relative inline-block">
<label for="image_name_input" id="image_name_label"
class="cursor-pointer text-sm font-bold text-purple-700 hover:text-purple-800 underline underline-offset-2 whitespace-nowrap">대표이미지등록(N)</label>
<input type="file" id="image_name_input" name="image_name" class="hidden" accept="image/*"
onchange="previewImageUpload(this)">
</div>
<button type="button" id="btn-memo-open" onclick="openMemoModal()"
class="text-sm font-bold text-teal-700 hover:text-teal-800 underline underline-offset-2 hidden">포스트잇
등록</button>
</div>
</div>
</div>
<div class="p-6 bg-gray-50 border-t border-gray-100 flex justify-between items-center">
<div>
<button type="button" id="btn-delete"
class="px-6 py-2 text-white bg-red-600 rounded-lg font-bold shadow-lg hover:bg-red-700 hidden">삭제</button>
</div>
<div class="flex space-x-3">
<button type="button" onclick="document.getElementById('upload-modal').classList.add('hidden')"
class="px-6 py-2 text-gray-500 font-bold hover:text-gray-700">취소</button>
<button type="submit" class="px-8 py-2 bg-teal-800 text-white rounded-lg font-bold shadow-lg">저장하기</button>
</div>
</div>
</form>
</div>
</div>
<!-- 포스트잇 등록 모달 (마이클래스 전용) -->
<div id="memo-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[115] hidden p-4">
<div class="bg-white w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50 shrink-0">
<h3 class="text-xl font-bold text-gray-800">포스트잇 등록</h3>
<button type="button" onclick="closeMemoModal()" class="text-gray-400 hover:text-gray-600"><i
class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="flex-1 overflow-auto p-6 bg-white space-y-6">
<div class="flex items-center justify-between">
<div class="text-sm text-gray-500">
콘텐츠ID: <span id="memo-content-id" class="font-mono text-xs text-gray-700"></span>
</div>
<button type="button" onclick="newMemo()"
class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700">신규 작성</button>
</div>
<div class="bg-gray-50/50 border border-gray-200 rounded-xl overflow-hidden">
<table class="w-full text-left border-collapse bg-white">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="p-3 text-sm font-bold text-gray-500 w-16 text-center">순번</th>
<th class="p-3 text-sm font-bold text-gray-500">내용</th>
<th class="p-3 text-sm font-bold text-gray-500 w-24 text-center">사용여부</th>
<th class="p-3 text-sm font-bold text-gray-500 w-24 text-center">수정</th>
</tr>
</thead>
<tbody id="memo-grid-body" class="divide-y divide-gray-100 text-sm">
<!-- JS injection -->
</tbody>
</table>
</div>
<form id="memo-form" class="space-y-4">
<input type="hidden" name="content_id" id="memo_form_content_id" value="">
<input type="hidden" name="seq" id="memo_form_seq" value="">
<div class="grid grid-cols-1 gap-4">
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">내용</label>
<textarea name="title" id="memo_form_title" rows="4"
class="w-full border-gray-200 rounded-lg p-3 bg-gray-50 resize-none"
placeholder="포스트잇 내용을 입력하세요"></textarea>
</div>
<label class="inline-flex items-center space-x-2 cursor-pointer">
<input name="is_active" id="memo_form_active" type="checkbox" class="w-4 h-4 text-teal-600 rounded" checked>
<span class="text-sm font-bold">사용여부</span>
</label>
</div>
</form>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-100 flex justify-between items-center">
<button type="button" id="btn-memo-delete" onclick="deleteMemo()"
class="px-5 py-2 text-white bg-red-600 rounded-lg font-bold shadow-lg hover:bg-red-700 hidden">삭제</button>
<div class="flex gap-2">
<button type="button" onclick="closeMemoModal()"
class="px-6 py-2 text-gray-500 font-bold hover:text-gray-700">닫기</button>
<button type="button" onclick="saveMemo()"
class="px-8 py-2 bg-teal-800 text-white rounded-lg font-bold shadow-lg">저장</button>
</div>
</div>
</div>
</div>
<!-- 추천이유 등록 모달 -->
<div id="recommend-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[116] hidden p-4">
<div class="bg-white w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50 shrink-0">
<h3 class="text-xl font-bold text-gray-800">추천이유 등록</h3>
<button type="button" onclick="closeRecommendModal()" class="text-gray-400 hover:text-gray-600"><i
class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="flex-1 overflow-auto p-6 bg-white space-y-6">
<div class="flex items-center justify-between">
<div class="text-sm text-gray-500">
학습목표코드: <span id="recommend-goal-code" class="font-mono text-xs text-gray-700"></span>
</div>
<button type="button" onclick="newRecommend()"
class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700">신규 작성</button>
</div>
<div class="bg-gray-50/50 border border-gray-200 rounded-xl overflow-hidden">
<table class="w-full text-left border-collapse bg-white">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="p-3 text-sm font-bold text-gray-500 w-16 text-center">순번</th>
<th class="p-3 text-sm font-bold text-gray-500">내용</th>
<th class="p-3 text-sm font-bold text-gray-500">내용2</th>
<th class="p-3 text-sm font-bold text-gray-500 w-24 text-center">사용여부</th>
<th class="p-3 text-sm font-bold text-gray-500 w-24 text-center">수정</th>
</tr>
</thead>
<tbody id="recommend-grid-body" class="divide-y divide-gray-100 text-sm">
<!-- JS injection -->
</tbody>
</table>
</div>
<form id="recommend-form" class="space-y-4">
<input type="hidden" name="goal_code" id="recommend_form_goal_code" value="">
<input type="hidden" name="seq" id="recommend_form_seq" value="">
<div class="grid grid-cols-1 gap-4">
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">내용</label>
<textarea name="title" id="recommend_form_title" rows="4"
class="w-full border-gray-200 rounded-lg p-3 bg-gray-50 resize-none"
placeholder="추천이유 내용을 입력하세요"></textarea>
</div>
<div>
<label class="block text-xs font-bold text-gray-400 mb-2 uppercase">내용2</label>
<textarea name="title2" id="recommend_form_title2" rows="4"
class="w-full border-gray-200 rounded-lg p-3 bg-gray-50 resize-none"
placeholder="추천이유 내용2를 입력하세요"></textarea>
</div>
<label class="inline-flex items-center space-x-2 cursor-pointer">
<input name="is_active" id="recommend_form_active" type="checkbox" class="w-4 h-4 text-teal-600 rounded"
checked>
<span class="text-sm font-bold">사용여부</span>
</label>
</div>
</form>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-100 flex justify-between items-center">
<button type="button" id="btn-recommend-delete" onclick="deleteRecommend()"
class="px-5 py-2 text-white bg-red-600 rounded-lg font-bold shadow-lg hover:bg-red-700 hidden">삭제</button>
<div class="flex gap-2">
<button type="button" onclick="closeRecommendModal()"
class="px-6 py-2 text-gray-500 font-bold hover:text-gray-700">닫기</button>
<button type="button" onclick="saveRecommend()"
class="px-8 py-2 bg-teal-800 text-white rounded-lg font-bold shadow-lg">저장</button>
</div>
</div>
</div>
</div>
<!-- 학습목표 등록 모달 -->
<div id="goal-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[110] hidden p-4">
<div class="bg-white w-full max-w-4xl rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50 shrink-0">
<h3 class="text-xl font-bold text-gray-800">학습목표 등록</h3>
<button type="button" onclick="closeGoalModal()" class="text-gray-400 hover:text-gray-600"><i
class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="p-4 bg-white border-b border-gray-200 shrink-0">
<div class="flex items-center gap-4">
<label class="text-sm font-bold text-gray-700">기준년도</label>
<div class="relative w-32">
<input type="text" id="goal_search_year" class="w-full border-gray-200 rounded-lg p-2 bg-gray-50 text-center"
value="<?= date('Y') ?>" maxlength="4">
</div>
<label class="text-sm font-bold text-gray-700">분기</label>
<div class="relative w-32">
<select id="goal_search_quarter" class="w-full border-gray-200 rounded-lg p-2 bg-gray-50 text-center">
<option value="">전체</option>
<?php foreach ($quarters as $q): ?>
<option value="<?= htmlspecialchars($q['code'], ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($q['name'], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="button" onclick="loadGoalGrid()"
class="px-4 py-2 bg-gray-800 text-white rounded-lg font-bold text-sm">검색</button>
</div>
</div>
<div class="flex-1 overflow-auto p-4 bg-gray-50/50 min-h-[200px]">
<table
class="w-full text-left border-collapse bg-white border border-gray-200 shadow-sm rounded-lg overflow-hidden">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="p-3 text-sm font-bold text-gray-500 w-16 text-center">No</th>
<th
class="p-3 bg-gray-50 text-gray-500 font-bold whitespace-nowrap border-b border-gray-100 w-24 text-center text-sm">
분기</th>
<th
class="p-3 bg-gray-50 text-gray-500 font-bold whitespace-nowrap border-b border-gray-100 w-24 text-center text-sm">
책장번호</th>
<th class="p-3 bg-gray-50 text-gray-500 font-bold whitespace-nowrap border-b border-gray-100 w-32 text-sm">
학습목표코드
</th>
<th class="p-3 text-sm font-bold text-gray-500 min-w-[16rem]">학습목표제목</th>
<th class="p-3 text-sm font-bold text-gray-500 w-24 text-center">사용여부</th>
<th class="p-3 text-sm font-bold text-gray-500 w-16">비고</th>
<th class="p-3 text-sm font-bold text-gray-500 w-24 text-center">수정</th>
</tr>
</thead>
<tbody id="goal-grid-body" class="divide-y divide-gray-100 text-sm">
<!-- JS injection -->
</tbody>
</table>
</div>
<form id="goal-form" action="../bbs/goal_save.php" method="post" class="shrink-0 bg-white border-t border-gray-200">
<input type="hidden" name="goal_code" id="goal_form_code" value="">
<input type="hidden" name="base_year" id="goal_form_year" value="<?= date('Y') ?>">
<div class="p-4 space-y-4">
<div class="grid grid-cols-12 gap-4 items-start">
<div class="col-span-5 flex gap-2">
<div class="flex-1">
<label class="block text-xs font-bold text-gray-400 mb-1 uppercase">분기</label>
<select id="goal_form_quarter" name="quarter"
class="w-full border-gray-200 rounded-lg p-2 bg-gray-50 text-sm">
<option value="">선택</option>
<?php foreach ($quarters as $q): ?>
<option value="<?= htmlspecialchars($q['code'], ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($q['name'], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex-1">
<label class="block text-xs font-bold text-gray-400 mb-1 uppercase">책장번호</label>
<select id="goal_form_goal_no" name="goal_no"
class="w-full border-gray-200 rounded-lg p-2 bg-gray-50 text-sm">
<option value="">선택</option>
<?php foreach ($goalNos as $g): ?>
<option value="<?= htmlspecialchars($g['code'], ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($g['name'], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="col-span-5">
<label class="block text-xs font-bold text-gray-400 mb-1 uppercase flex justify-between">
<span>학습목표제목</span>
<span class="text-gray-300 font-normal"><span id="goal_title_len">0</span>/30자</span>
</label>
<input name="title" id="goal_form_title" type="text" maxlength="30"
class="w-full border-gray-200 rounded-lg p-2 bg-gray-50 text-sm" required
oninput="document.getElementById('goal_title_len').textContent=this.value.length">
</div>
<div class="col-span-2">
<label class="block text-xs font-bold text-gray-400 mb-1 uppercase">비고</label>
<input name="remarks" id="goal_form_remarks" type="text"
class="w-full border-gray-200 rounded-lg p-2 bg-gray-50 text-sm">
</div>
</div>
<div class="flex items-center justify-between">
<label class="inline-flex items-center space-x-2 cursor-pointer">
<input name="is_active" id="goal_form_active" type="checkbox" class="w-4 h-4 text-teal-600 rounded" checked>
<span class="text-sm font-bold">사용여부</span>
</label>
<div class="flex items-center gap-4">
<button type="button" onclick="openRecommendModal()"
class="text-sm font-bold text-teal-700 hover:text-teal-800 underline underline-offset-2">추천이유 등록</button>
<div class="w-32 hidden">
<label class="block text-xs font-bold text-gray-400 mb-1 uppercase text-right">정렬순번</label>
<input name="sort_order" id="goal_form_sort" type="number"
class="w-full border-gray-200 rounded-lg p-2 bg-gray-50 text-right">
</div>
</div>
</div>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-100 flex justify-between items-center">
<div class="flex gap-2">
<button type="button" onclick="closeGoalModal()"
class="px-6 py-2 text-gray-500 font-bold hover:text-gray-700">취소</button>
<button type="button" id="btn-goal-delete" onclick="deleteGoal()"
class="px-5 py-2 text-white bg-red-600 rounded-lg font-bold shadow-lg hover:bg-red-700 hidden">삭제</button>
<button type="button" id="btn-goal-new" onclick="resetGoalForm()"
class="px-5 py-2 text-white bg-green-600 rounded-lg font-bold shadow-lg hover:bg-green-700 hidden">신규
작성</button>
</div>
<div class="flex space-x-3">
<button type="submit" class="px-8 py-2 bg-blue-700 text-white rounded-lg font-bold shadow-lg">저장</button>
</div>
</div>
</form>
</div>
</div>
<!-- 키워드 등록 모달 -->
<div id="keyword-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[120] hidden p-4">
<div class="bg-white w-[500px] rounded-2xl shadow-2xl overflow-hidden relative">
<div class="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<h3 class="text-xl font-bold text-gray-800">나의 키워드 수정</h3>
<button type="button" onclick="closeKeywordModal()" class="text-gray-400 hover:text-gray-600"><i
class="fa-solid fa-xmark text-xl"></i></button>
</div>
<form id="keyword-form" onsubmit="saveKeywords(event)">
<input type="hidden" name="content_id" id="kw_content_id">
<input type="hidden" name="keywords" id="kw_selected">
<div class="p-8">
<div class="flex flex-wrap justify-center gap-3 relative z-10" id="keyword-container-modal">
<?php foreach ($keywords as $kw): ?>
<button type="button"
class="kw-btn flex items-center justify-between gap-2 px-5 py-2.5 rounded-full border border-gray-200 bg-white text-gray-400 text-sm font-bold hover:border-orange-300 hover:text-orange-500 transition shadow-sm data-[active=true]:bg-orange-600 data-[active=true]:border-orange-700 data-[active=true]:text-white data-[active=true]:shadow-md"
data-code="<?= htmlspecialchars($kw['code'], ENT_QUOTES, 'UTF-8'); ?>">
#<?= htmlspecialchars($kw['name'], ENT_QUOTES, 'UTF-8'); ?>
<span class="kw-icon"><i class="fa-solid fa-plus text-xs"></i></span>
</button>
<?php endforeach; ?>
</div>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-100 flex justify-end gap-2">
<button type="button" onclick="closeKeywordModal()"
class="px-5 py-2 text-gray-500 font-bold hover:text-gray-700">닫기</button>
<button type="submit" class="px-6 py-2 bg-orange-600 text-white rounded-lg font-bold shadow-lg">저장</button>
</div>
</form>
</div>
</div>
</body>
<script src="../js/content_upload.js?v=<?= time() ?>"></script>
+198
View File
@@ -0,0 +1,198 @@
<?php
// 세션 시작 (아직 시작되지 않은 경우에만)
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 현재 파일명을 가져와서 메뉴 활성화에 사용
$current_page = basename($_SERVER['PHP_SELF']);
// 로그인 정보 시작 세션정보
$auth_level = $_SESSION['auth_level'] ?? ''; // 권한코드
$sys_comp_code = $_SESSION['sys_comp_code'] ?? ''; // 시스템 로그인 법인
$member_id = $_SESSION['member_id'] ?? ''; // 로그인 아이디
// 현재 년도의 법정의무교육 기간 조회
$legal_edu_period = '';
$user_qty = 0;
$user_name = '';
$comp_name = '';
try {
require_once __DIR__ . '/../../bbs/db_conn.php';
$current_year = date('Y');
$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([$current_year]);
$period = $stmt->fetch(PDO::FETCH_ASSOC);
if ($period && !empty($period['start_date']) && !empty($period['end_date'])) {
$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)
FROM edu_users
WHERE (end_date IS NULL OR end_date > CURDATE())
AND sys_comp_code = working_comp
");
$stmt_qty->execute();
$user_qty = (int) $stmt_qty->fetchColumn();
// 관리자명 (이름 + 아이디)
$stmt_user_name = $pdo->prepare("
SELECT CONCAT(name, ' (', member_id, ')')
FROM edu_users
WHERE sys_comp_code = ?
AND member_id = ?
LIMIT 1
");
$stmt_user_name->execute([$sys_comp_code, $member_id]);
$user_name = $stmt_user_name->fetchColumn() ?: $member_id;
// 법인명
$stmt_comp_name = $pdo->prepare("
SELECT code_name
FROM edu_codes
WHERE group_code = 'CO100'
AND code = ?
LIMIT 1
");
$stmt_comp_name->execute([$sys_comp_code]);
$comp_name = $stmt_comp_name->fetchColumn() ?: $sys_comp_code;
} catch (Exception $e) {
// 오류 무시
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
body {
font-family: 'Noto Sans KR', sans-serif;
}
.nav-active {
background-color: rgba(255, 255, 255, 0.1);
font-weight: bold;
border-bottom: 4px solid rgba(255, 255, 255, 0.4);
}
</style>
<script>
// [DEBUG] 세션 auth_level 확인용 콘솔 출력
console.log('[auth_level]', '<?php echo addslashes($auth_level); ?>');
</script>
</head>
<body class="bg-[#f8fafc]">
<nav class="w-full shadow-md font-['Noto_Sans_KR'] sticky top-0 z-50">
<div class="bg-[#114b3d] text-white">
<div class="max-w-[1600px] mx-auto px-6 h-16 flex items-center justify-between">
<div class="flex items-center">
<h1 class="text-xl font-bold flex items-center tracking-tight cursor-pointer"
onclick="location.href='<?php echo ($auth_level === 'LE10002') ? 'legal_edu.php' : 'index.php'; ?>'">
<span class="bg-white text-[#114b3d] p-1 rounded mr-2"><i class="fa-solid fa-graduation-cap"></i></span>
배움터 <span class="ml-2 text-sm font-light opacity-70">관리자</span>
</h1>
</div>
<div class="hidden md:flex items-center h-full text-sm">
<?php
// 권한에 따른 탭 표시 제어
// LE10001: 전체권한 - 모든 탭 표시
// LE10002: 법인권한 - 전체학습현황, 학습자관리, 법정의무교육만 표시
// 그 외: 모든 탭 숨김
$is_full = ($auth_level === 'LE10001'); // 전체권한
$is_corp = ($auth_level === 'LE10002'); // 법인권한
$show_all = $is_full; // 콘텐츠입력, 설정 탭 표시 여부
$show_base = ($is_full || $is_corp); // 기본 3개 탭 표시 여부
?>
<?php if ($is_corp): ?>
<a href="legal_edu.php"
class="px-5 h-16 flex items-center hover:bg-white/10 transition space-x-2 <?php echo ($current_page == 'legal_edu.php') ? 'nav-active' : ''; ?>">
<i class="fa-solid fa-book opacity-80"></i>
<span>법정의무교육</span>
</a>
<?php
endif; ?>
<?php if ($show_all): ?>
<a href="index.php"
class="px-5 h-16 flex items-center hover:bg-white/10 transition space-x-2 <?php echo ($current_page == 'index.php') ? 'nav-active' : ''; ?>">
<i class="fa-solid fa-chart-line opacity-80"></i>
<span>전체학습현황</span>
</a>
<a href="member_list.php"
class="px-5 h-16 flex items-center hover:bg-white/10 transition space-x-2 <?php echo ($current_page == 'member_list.php') ? 'nav-active' : ''; ?>">
<i class="fa-solid fa-users opacity-80"></i>
<span>학습자관리</span>
</a>
<a href="legal_edu.php"
class="px-5 h-16 flex items-center hover:bg-white/10 transition space-x-2 <?php echo ($current_page == 'legal_edu.php') ? 'nav-active' : ''; ?>">
<i class="fa-solid fa-book opacity-80"></i>
<span>법정의무교육</span>
</a>
<a href="content_upload.php"
class="px-5 h-16 flex items-center hover:bg-white/10 transition space-x-2 <?php echo ($current_page == 'content_upload.php') ? 'nav-active' : ''; ?>">
<i class="fa-solid fa-pen-to-square opacity-80"></i>
<span>콘텐츠 입력</span>
</a>
<a href="settings.php"
class="ml-2 px-4 h-10 self-center flex items-center bg-white/10 hover:bg-white/20 rounded-lg transition <?php echo ($current_page == 'settings.php') ? 'ring-2 ring-white/50' : ''; ?>">
<i class="fa-solid fa-gear mr-2"></i>설정
</a>
<?php
endif; ?>
</div>
<div class="flex items-center space-x-4">
</div>
</div>
</div>
<div class="bg-[#0d3a2f] text-white/90 border-t border-white/5">
<div class="max-w-[1600px] mx-auto px-6 h-10 flex items-center text-[13px] space-x-8">
<div class="flex items-center border-l border-white/10 pl-8"><span class="opacity-60 mr-2">법인명:</span><span
class="font-bold"><?php echo $comp_name; ?></span></div>
<div class="flex items-center border-l border-white/10 pl-8"><span class="opacity-60 mr-2">관리자 ID:</span><span
class="font-bold"><?php echo $user_name; ?></span></div>
<div class="flex items-center border-l border-white/10 pl-8"><span class="opacity-60 mr-2">전체 학습자 수:</span><span
class="font-bold text-teal-300"> <?php echo $user_qty; ?></span></div>
<div class="flex items-center border-l border-white/10 pl-8"><span class="opacity-60 mr-2">법정의무교육
기간:</span><span
class="font-bold"><?php echo !empty($legal_edu_period) ? htmlspecialchars($legal_edu_period, ENT_QUOTES, 'UTF-8') : '미설정'; ?></span>
</div>
</div>
</div>
</nav>
+373
View File
@@ -0,0 +1,373 @@
<?php
require_once __DIR__ . '/../../bbs/auth.php';
edu_require_login();
include_once '../../bbs/db_conn.php';
include_once 'header.php';
$pdo = db_conn();
// 분기 선택 데이터
$stmt_quarters = $pdo->query("SELECT base_code AS code, code_name FROM edu_codes WHERE group_code = 'CA200' AND DESC01 = 'CA10001' ORDER BY base_code ASC");
$quarters = $stmt_quarters->fetchAll(PDO::FETCH_ASSOC);
// 현재 연도 및 디폴트 분기 설정
$current_year = date('Y');
$current_quarter = ceil(date('n') / 3);
// 법정의무교육 기간 (하드코딩된 예시 텍스트 대체용, 필요시 DB 조회)
$legal_edu_period = '2026.04.13 ~ 2026.12.31';
?>
<main class="max-w-[1600px] mx-auto p-6 bg-gray-50/50 min-h-screen font-sans">
<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">
전체 학습현황
</h2>
<p class="text-sm text-gray-400 mt-1">법정의무교육 기간: <?php echo htmlspecialchars($legal_edu_period); ?></p>
</div>
<div class="flex items-center space-x-3">
<div class="text-sm font-bold text-gray-600 mr-2">이번 분기 핵심 지표</div>
<div class="flex bg-white border border-gray-200 p-1 rounded-md shadow-sm">
<button id="btn_year_prev" class="px-3 py-1.5 text-sm rounded transition text-gray-500 hover:bg-gray-50"
onclick="dashboard.changeYear(<?php echo $current_year - 1; ?>)"><?php echo $current_year - 1; ?>년</button>
<button id="btn_year_curr" class="px-3 py-1.5 text-sm rounded transition bg-[#114b3d] text-white shadow-sm font-bold"
onclick="dashboard.changeYear(<?php echo $current_year; ?>)"><?php echo $current_year; ?>년</button>
</div>
<select id="select_quarter" class="bg-white border border-gray-200 text-gray-700 text-sm rounded-md px-4 py-2 font-medium shadow-sm outline-none focus:ring-2 focus:ring-[#114b3d]/50" onchange="dashboard.changeQuarter(this.value)">
<?php foreach ($quarters as $q): ?>
<option value="<?php echo htmlspecialchars($q['code']); ?>" <?php echo substr($q['code'], -1) == $current_quarter ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($q['code_name']); ?>
</option>
<?php endforeach; ?>
<?php if(empty($quarters)): ?>
<option value="1">1분기 (1~3월)</option>
<option value="2" selected>2분기 (4~6월)</option>
<option value="3">3분기 (7~9월)</option>
<option value="4">4분기 (10~12월)</option>
<?php endif; ?>
</select>
</div>
</header>
<!-- KPI 섹션 -->
<section class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<!-- 전체 접속률 -->
<div class="bg-[#114b3d] text-white p-5 rounded-xl shadow-md relative overflow-hidden flex flex-col justify-between">
<h3 class="text-sm font-bold opacity-90 mb-4">전체 접속률</h3>
<div class="flex justify-between mb-6">
<!-- 이번 분기 -->
<div class="flex-1 pr-4">
<div class="text-[11px] opacity-70 mb-1">이번 분기</div>
<div class="text-3xl font-extrabold flex items-baseline gap-1 mb-1" id="kpi_access_rate_current">
0<span class="text-lg font-bold">%</span>
</div>
<div class="text-[10px] opacity-70" id="kpi_access_desc_current">-명 중 -명 접속</div>
</div>
<!-- 전분기 -->
<div class="flex-1 pl-4 border-l border-white/20">
<div class="text-[11px] opacity-70 mb-1">전분기</div>
<div class="text-2xl font-bold flex items-baseline gap-1 mb-1 opacity-80" id="kpi_access_rate_prev">
0<span class="text-sm font-bold">%</span>
</div>
<div class="text-[10px] opacity-70" id="kpi_access_desc_prev">-명 중 -명 접속</div>
</div>
</div>
<!-- 하단 게이지 -->
<div class="space-y-3">
<div class="flex items-center gap-2">
<span class="text-[10px] opacity-70 w-8">전분기</span>
<div class="flex-1 bg-white/10 rounded-full h-1.5">
<div class="bg-gray-400 h-1.5 rounded-full transition-all duration-1000" id="kpi_access_bar_prev" style="width: 0%"></div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-[10px] opacity-70 w-8">현재</span>
<div class="flex-1 bg-white/10 rounded-full h-1.5">
<div class="bg-[#4ade80] h-1.5 rounded-full transition-all duration-1000" id="kpi_access_bar_current" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<!-- 법정의무교육 이수율 -->
<div class="bg-white border border-gray-200 p-5 rounded-xl shadow-sm flex flex-col justify-between">
<h3 class="text-sm font-bold text-gray-700 mb-2">법정의무교육 이수율</h3>
<div class="flex justify-between items-center mb-1">
<div class="text-xs font-bold text-[#eab308]" id="kpi_legal_uncompleted">미이수 -명 잔여</div>
</div>
<div class="text-3xl font-extrabold text-[#16a34a] flex items-baseline gap-1 mb-2" id="kpi_legal_rate">
0<span class="text-lg">%</span>
</div>
<div class="text-xs text-gray-500" id="kpi_legal_desc">이수 -명 / 전체 -명</div>
<div class="w-full bg-gray-100 rounded-full h-1.5 mt-2">
<div class="bg-[#16a34a] h-1.5 rounded-full transition-all duration-1000" id="kpi_legal_bar" style="width: 0%"></div>
</div>
</div>
<!-- 마이클래스 -->
<div class="bg-white border border-gray-200 p-5 rounded-xl shadow-sm flex flex-col justify-between">
<h3 class="text-sm font-bold text-gray-700 mb-4">마이클래스</h3>
<div class="flex justify-between items-end mb-4">
<div>
<div class="text-[10px] text-gray-400 mb-1">목표 설정률</div>
<div class="text-2xl font-bold text-blue-600 flex items-baseline gap-1" id="kpi_myclass_target_rate">
0<span class="text-sm">%</span>
</div>
<div class="text-[11px] text-gray-500 mt-1" id="kpi_myclass_target_desc">설정 -명</div>
</div>
<div>
<div class="text-[10px] text-gray-400 mb-1">목표 달성률</div>
<div class="text-xl font-bold text-[#eab308] flex items-baseline gap-1" id="kpi_myclass_achieve_rate">
0<span class="text-sm">%</span>
</div>
<div class="text-[11px] text-gray-500 mt-1" id="kpi_myclass_achieve_desc">달성 -명</div>
</div>
</div>
<button class="w-full py-2 border border-gray-200 rounded text-xs font-bold text-gray-600 hover:bg-gray-50 transition flex justify-center items-center gap-1 mb-3" onclick="myclassModal.open()">
상세보기 <i class="fa-solid fa-arrow-right text-[10px]"></i>
</button>
<div class="w-full bg-gray-100 rounded-full h-1.5">
<div class="bg-[#eab308] h-1.5 rounded-full transition-all duration-1000" id="kpi_myclass_bar" style="width: 0%"></div>
</div>
</div>
<!-- 접속자 1인당 완주 콘텐츠 -->
<div class="bg-white border border-gray-200 p-5 rounded-xl shadow-sm flex flex-col justify-between relative overflow-hidden">
<h3 class="text-sm font-bold text-gray-700 mb-2">접속자 1인당 완주 콘텐츠</h3>
<div class="text-xs font-bold text-[#16a34a] mb-1 flex items-center gap-1" id="kpi_content_diff">
<i class="fa-solid fa-caret-up"></i> 전분기 대비 +-편
</div>
<div class="text-3xl font-extrabold text-gray-800 flex items-baseline gap-1 mb-2" id="kpi_content_per_user">
0<span class="text-lg">편</span>
</div>
<div class="text-xs text-gray-500" id="kpi_content_desc">접속자 -명 기준<br>총 완수 -회</div>
<div class="w-full bg-gray-100 rounded-full h-1.5 mt-2 relative z-10">
<div class="bg-[#f97316] h-1.5 rounded-full transition-all duration-1000" id="kpi_content_bar" style="width: 0%"></div>
</div>
</div>
</section>
<!-- 접속 현황 상세 섹션 -->
<div class="text-xs text-gray-400 mb-2 font-medium">접속 현황 상세</div>
<section class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<!-- 법인별 접속률 -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden flex flex-col h-[320px]">
<div class="px-5 py-4 flex justify-between items-center border-b border-gray-100">
<h3 class="font-bold text-gray-800 text-sm">법인별 접속률</h3>
<div class="text-[11px] text-gray-500 flex items-center gap-2 bg-gray-50 px-2 py-1 rounded">
기간 <i class="fa-regular fa-calendar"></i> <span id="txt_period_start">-</span> ~ <i class="fa-regular fa-calendar"></i> <span id="txt_period_end">-</span>
</div>
</div>
<div class="overflow-y-auto flex-1 p-0">
<table class="w-full text-xs text-left">
<thead class="bg-gray-50 text-gray-500 sticky top-0 z-10">
<tr>
<th class="py-3 px-5 font-medium">법인</th>
<th class="py-3 px-2 font-medium text-center">전분기 대비</th>
<th class="py-3 px-2 font-medium text-center">전체</th>
<th class="py-3 px-2 font-medium text-center">접속인원</th>
<th class="py-3 px-2 font-medium text-center">접속률</th>
<th class="py-3 px-5 font-medium text-right">미접속</th>
</tr>
</thead>
<tbody id="tbody_corp_access" class="divide-y divide-gray-50">
<!-- JS 렌더링 -->
</tbody>
</table>
</div>
</div>
<!-- 스택 바 차트 그룹 -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6 flex flex-col justify-between h-[320px]">
<!-- 학습자 접속 빈도 -->
<div>
<h3 class="font-bold text-gray-800 text-sm mb-3">학습자 접속 빈도</h3>
<div class="flex w-full h-8 rounded-md overflow-hidden mb-2" id="bar_access_freq">
<div class="bg-[#114b3d] h-full flex items-center justify-center text-white font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
<div class="bg-[#22c55e] h-full flex items-center justify-center text-white font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
<div class="bg-[#f97316] h-full flex items-center justify-center text-white font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
<div class="bg-gray-200 h-full flex items-center justify-center text-gray-500 font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
</div>
<div class="flex text-[10px] text-gray-500 gap-4">
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-[#114b3d]"></span> 적극적 12회↑</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-[#22c55e]"></span> 보통 5~11회</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-[#f97316]"></span> 저 1~4회</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-gray-200"></span> 미사용</div>
</div>
</div>
<!-- 접속 시간대 -->
<div>
<h3 class="font-bold text-gray-800 text-sm mb-3">접속 시간대</h3>
<div class="flex w-full h-8 rounded-md overflow-hidden mb-2" id="bar_access_time">
<div class="bg-[#114b3d] h-full flex items-center justify-center text-white font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
<div class="bg-[#22c55e] h-full flex items-center justify-center text-white font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
<div class="bg-gray-200 h-full flex items-center justify-center text-gray-500 font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
</div>
<div class="flex text-[10px] text-gray-500 gap-4">
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-[#114b3d]"></span> 업무시간 09:00~17:00</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-[#22c55e]"></span> 점심시간 11:30~13:30</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-gray-200"></span> 업무외시간 9시 이전·17시 이후</div>
</div>
</div>
<!-- 접속 방법 -->
<div>
<h3 class="font-bold text-gray-800 text-sm mb-3">접속 방법</h3>
<div class="flex w-full h-8 rounded-md overflow-hidden mb-2" id="bar_access_device">
<div class="bg-[#114b3d] border-r border-white/20 h-full flex items-center justify-center text-white font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
<div class="bg-[#22c55e] h-full flex items-center justify-center text-white font-bold text-xs transition-all duration-1000" style="width: 0%"></div>
</div>
<div class="flex text-[10px] text-gray-500 gap-4">
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-[#114b3d]"></span> PC</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-[#22c55e]"></span> 모바일</div>
</div>
</div>
</div>
</section>
<!-- 콘텐츠 이용 현황 섹션 -->
<div class="text-xs text-gray-400 mb-2 font-medium">콘텐츠 이용 현황</div>
<section class="grid grid-cols-1 lg:grid-cols-3 gap-4 pb-10">
<!-- 카테고리별 이용 현황 -->
<div class="lg:col-span-1 bg-white border border-gray-200 rounded-xl shadow-sm p-5">
<h3 class="font-bold text-gray-800 text-sm mb-5">카테고리별 이용 현황</h3>
<div class="space-y-6" id="list_content_usage">
<!-- JS 렌더링 -->
</div>
</div>
<!-- 가장 많이 본 콘텐츠 -->
<div class="lg:col-span-2 bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden flex flex-col">
<div class="px-5 py-4 flex justify-between items-center border-b border-gray-100">
<h3 class="font-bold text-gray-800 text-sm">가장 많이 본 콘텐츠</h3>
<div class="flex gap-2" id="tab_popular_categories">
<button class="px-3 py-1 text-[11px] font-bold rounded-full border border-gray-200 bg-[#114b3d] text-white" data-category="ALL">전체</button>
<button class="px-3 py-1 text-[11px] font-bold rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50" data-category="CA10001">마이클래스</button>
<button class="px-3 py-1 text-[11px] font-bold rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50" data-category="CA10005">인사이트</button>
<button class="px-3 py-1 text-[11px] font-bold rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50" data-category="CA10004">리더십</button>
<button class="px-3 py-1 text-[11px] font-bold rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50" data-category="CA10006">비즈트렌드</button>
</div>
</div>
<div class="overflow-y-auto flex-1 p-0 max-h-[295px]">
<table class="w-full text-[11px] text-left">
<thead class="bg-gray-50 text-gray-500 sticky top-0 z-10">
<tr>
<th class="py-3 px-5 font-medium text-center w-20">순위</th>
<th class="py-3 px-2 font-medium">영상명</th>
<th class="py-3 px-2 font-medium text-center w-24">카테고리</th>
<th class="py-3 px-2 font-medium text-center w-24">시청수</th>
<th class="py-3 px-2 font-medium text-center w-24">완주율</th>
<th class="py-3 px-5 font-medium text-center w-24">댓글수</th>
</tr>
</thead>
<tbody id="tbody_popular_contents" class="divide-y divide-gray-50">
<!-- JS 렌더링 -->
</tbody>
</table>
</div>
<div class="px-5 py-2 bg-gray-50 border-t border-gray-100 text-[10px] text-gray-400">
정렬기준 : 시청수
</div>
</div>
</section>
<!-- 마이클래스 모달 -->
<div id="modal_myclass" class="fixed inset-0 z-50 hidden">
<!-- 배경 -->
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" onclick="myclassModal.close()"></div>
<!-- 모달 컨텐츠 -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[90%] max-w-4xl bg-white rounded-xl shadow-2xl flex flex-col max-h-[90vh]">
<!-- 헤더 -->
<div class="flex justify-between items-start p-6 border-b border-gray-100">
<div>
<h2 class="text-xl font-extrabold text-gray-800">마이클래스 현황</h2>
<p class="text-sm text-gray-500 mt-1">이번 분기 마이클래스 목표 선택 및 달성 현황을 확인할 수 있습니다.</p>
</div>
<div class="flex items-center gap-2">
<!-- 분기 선택 -->
<select id="modal_myclass_quarter" class="bg-white border border-gray-200 text-gray-700 text-sm rounded px-3 py-1.5 focus:ring-1 focus:ring-blue-500" onchange="myclassModal.fetchData(this.value)">
<?php foreach ($quarters as $q): ?>
<option value="<?php echo htmlspecialchars($q['code']); ?>" <?php echo substr($q['code'], -1) == $current_quarter ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($q['code_name']); ?>
</option>
<?php endforeach; ?>
</select>
<button class="p-1.5 rounded border border-gray-200 text-gray-500 hover:bg-gray-50" onclick="myclassModal.fetchData()">
<i class="fa-solid fa-rotate-right"></i>
</button>
<button class="p-1.5 rounded border border-gray-200 text-gray-500 hover:bg-gray-50" onclick="myclassModal.close()">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
<!-- KPI 영역 -->
<div class="grid grid-cols-3 divide-x divide-gray-100 border-b border-gray-100">
<div class="p-6">
<div class="text-xs font-bold text-gray-500 mb-2 flex items-center gap-1.5">
<span class="bg-blue-100 text-blue-600 rounded-full w-5 h-5 flex items-center justify-center text-[10px]"><i class="fa-solid fa-bullseye"></i></span> 목표 선택률
</div>
<div class="text-3xl font-extrabold text-blue-600 mb-1" id="modal_mc_target_rate">0<span class="text-lg">%</span></div>
<div class="text-[11px] text-gray-400" id="modal_mc_target_desc">선택자 0명 / 전체 0명</div>
</div>
<div class="p-6">
<div class="text-xs font-bold text-gray-500 mb-2 flex items-center gap-1.5">
<span class="bg-green-100 text-green-600 rounded-full w-5 h-5 flex items-center justify-center text-[10px]"><i class="fa-solid fa-trophy"></i></span> 목표 달성률
</div>
<div class="text-3xl font-extrabold text-[#4ade80] mb-1" id="modal_mc_achieve_rate">0<span class="text-lg">%</span></div>
<div class="text-[11px] text-gray-400" id="modal_mc_achieve_desc">달성자 0명 / 선택자 0명</div>
</div>
<div class="p-6">
<div class="text-xs font-bold text-gray-500 mb-2 flex items-center gap-1.5">
<span class="bg-gray-100 text-gray-800 rounded-full w-5 h-5 flex items-center justify-center text-[10px]"><i class="fa-solid fa-user-minus"></i></span> 미선택자 수
</div>
<div class="text-3xl font-extrabold text-[#f97316] mb-1" id="modal_mc_unselect_qty">0<span class="text-lg">명</span></div>
<div class="text-[11px] text-gray-400" id="modal_mc_unselect_desc">전체 학습자의 0%</div>
</div>
</div>
<!-- 탭 영역 -->
<div class="px-6 flex gap-6 border-b border-gray-100">
<button class="py-3 text-sm font-bold border-b-2 border-blue-600 text-blue-600 transition" id="tab_btn_goals" onclick="myclassModal.switchTab('goals')">목표별 선택률</button>
<button class="py-3 text-sm font-bold border-b-2 border-transparent text-gray-400 hover:text-gray-600" id="tab_btn_comps" onclick="myclassModal.switchTab('comps')">법인별 비교</button>
</div>
<!-- 리스트 컨텐츠 -->
<div class="overflow-y-auto flex-1 p-6 relative min-h-[300px]">
<div class="absolute right-6 top-2 text-[10px] text-gray-400">(단위: %)</div>
<table class="w-full text-sm mt-4">
<thead class="bg-gray-50 text-gray-500 text-xs hidden" id="modal_table_thead">
<tr>
<th class="py-2 px-4 text-left w-1/3" id="modal_table_th1">목표</th>
<th class="py-2 px-4 text-right text-[10px] font-normal w-2/3" id="modal_table_th2">목표 선택률</th>
</tr>
</thead>
<tbody id="modal_list_tbody" class="divide-y divide-gray-50">
<tr><td class="text-center py-10 text-gray-400">데이터를 불러오는 중입니다...</td></tr>
</tbody>
</table>
</div>
<!-- 하단 닫기 -->
<div class="p-4 border-t border-gray-100 flex justify-end">
<button class="px-4 py-2 bg-white border border-gray-200 text-sm font-bold text-gray-600 rounded hover:bg-gray-50 transition" onclick="myclassModal.close()">닫기</button>
</div>
</div>
</div>
</main>
<script src="../js/dashboard.js?v=<?php echo time(); ?>"></script>
+922
View File
@@ -0,0 +1,922 @@
<?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>
+486
View File
@@ -0,0 +1,486 @@
<?php
/**
* legal_cert_print.php - 수료증 출력 화면 스킨
*/
require_once __DIR__ . '/../../bbs/auth.php';
edu_require_login();
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>수료증 출력 - 배움터</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&family=Gowun+Batang:wght@400;700&display=swap');
body {
margin: 0;
padding: 0;
background-color: #f1f5f9;
font-family: 'Noto Sans KR', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
/* 화면용 컨트롤 패널 */
.control-panel {
width: 100%;
max-width: 800px;
margin: 20px auto 10px;
padding: 15px 20px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
font-weight: bold;
font-size: 14px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #114b3d;
color: #ffffff;
}
.btn-primary:hover {
background-color: #0d3a2f;
box-shadow: 0 4px 12px rgba(17, 75, 61, 0.2);
}
.btn-secondary {
background-color: #e2e8f0;
color: #334155;
}
.btn-secondary:hover {
background-color: #cbd5e1;
}
/* 수료증 컨테이너 (화면용 A4 비율 가이드) */
.cert-page {
background: #ffffff;
width: 210mm;
height: 297mm;
padding: 20mm;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
margin: 10px auto 40px;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
/* 인장 외곽 얇은 테두리 */
.cert-outer-border {
border: 1px solid #c0c0c0;
width: 100%;
height: 100%;
padding: 6px;
box-sizing: border-box;
position: relative;
}
/* 인장 내부 두껍고 고급스러운 테두리 */
.cert-inner-border {
border: 2px solid #4a5568;
width: 100%;
height: 100%;
padding: 55px 45px;
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
z-index: 1;
}
/* 모서리 고전 장식 문양 */
.corner-decor {
position: absolute;
width: 24px;
height: 24px;
z-index: 10;
}
.decor-tl {
top: -6px;
left: -6px;
}
.decor-tr {
top: -6px;
right: -6px;
transform: scaleX(-1);
}
.decor-bl {
bottom: -6px;
left: -6px;
transform: scaleY(-1);
}
.decor-br {
bottom: -6px;
right: -6px;
transform: scale(-1);
}
/* 발급 번호 영역 */
.cert-no-area {
font-size: 13px;
color: #4a5568;
font-weight: 500;
text-align: left;
height: 20px;
}
/* 수료증 메인 타이틀 */
.cert-title {
font-family: 'Noto Sans KR', sans-serif;
font-size: 52px;
font-weight: 700;
text-align: center;
letter-spacing: 28px;
text-indent: 28px;
margin: 35px 0 45px;
color: #1a202c;
}
/* 가로 구분선 */
.divider {
border-top: 1.2px solid #a0aec0;
width: 100%;
margin: 0 auto;
}
/* 본문 데이터 리스트 */
.info-table {
width: 90%;
margin: 45px auto;
display: flex;
flex-direction: column;
gap: 24px;
}
.info-row {
display: flex;
align-items: center;
font-size: 18px;
line-height: 1.6;
}
.info-label {
width: 150px;
font-weight: 700;
color: #2d3748;
display: flex;
align-items: center;
}
.info-label .bullet {
color: #a0aec0;
font-size: 12px;
margin-right: 12px;
}
.info-label .text {
flex: 1;
display: flex;
justify-content: space-between;
padding-right: 15px;
}
.info-value {
flex: 1;
font-weight: 500;
color: #1a202c;
}
/* 은은하게 흐르는 백그라운드 워터마크 */
.watermark-container {
position: absolute;
left: 50%;
top: 48%;
transform: translate(-50%, -50%);
width: 320px;
height: 320px;
opacity: 0.08;
z-index: 0;
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
}
.watermark-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* 수여 선언 문구 */
.cert-statement {
font-family: 'Noto Sans KR', sans-serif;
font-size: 21px;
font-weight: 700;
text-align: center;
line-height: 2;
color: #2d3748;
margin: 40px 0;
word-break: keep-all;
}
/* 수료일자 */
.cert-date {
font-family: 'Noto Sans KR', sans-serif;
font-size: 18px;
font-weight: 700;
text-align: center;
letter-spacing: 4px;
color: #2d3748;
margin: 30px 0;
}
/* 발송 기관 및 직인 */
.cert-footer {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30px;
position: relative;
}
.company-name {
font-family: 'Noto Sans KR', sans-serif;
font-size: 26px;
font-weight: 700;
color: #1a202c;
letter-spacing: 2px;
margin-bottom: 8px;
}
.ceo-name-area {
font-family: 'Noto Sans KR', sans-serif;
font-size: 20px;
font-weight: 700;
color: #2d3748;
display: inline-flex;
align-items: center;
position: relative;
}
/* 대표이사 텍스트 라인 전체 정의 */
.ceo-text {
position: relative; /* 💡 도장(absolute)의 절대 기준점이 됨 */
display: inline-block;
font-size: 20px; /* 프로젝트 환경에 맞게 조절 */
font-weight: bold;
line-height: 1;
}
/* 도장 컨테이너 위치 정밀 제어 */
.stamp-container {
position: absolute;
top: -15px; /* 💡 위아래 정렬 (이름 글자 중간쯤 오도록 마이너스 조절) */
left: 100%; /* 💡 이름 텍스트가 끝나는 바로 우측 끝 지점에 강제 배치 */
margin-left: 5px; /* 이름과 도장 사이의 기본 여백 */
display: inline-block;
}
/* 도장 이미지 사이즈 및 효과 */
.stamp-img {
width: 50px; /* 💡 수료증 직인 표준 사이즈 (50px ~ 60px 추천) */
height: 50px;
object-fit: contain;
opacity: 0.9; /* 글자가 살짝 비치도록 리얼리티 부여 */
}
/* 프린팅 관련 설정 */
@media print {
body {
background-color: #ffffff;
padding: 0;
margin: 0;
}
.no-print {
display: none !important;
}
.cert-page {
margin: 0;
box-shadow: none;
width: 210mm;
height: 297mm;
page-break-after: always;
page-break-before: avoid;
}
.cert-page:last-child {
page-break-after: avoid;
}
/* 웹 브라우저 인쇄 강제 여백 제거 */
@page {
size: A4 portrait;
margin: 0;
}
}
</style>
</head>
<body>
<!-- 화면용 상단 컨트롤 패널 -->
<div class="control-panel no-print">
<div style="font-weight: bold; color: #334155; font-size: 16px;">
<i class="fa-solid fa-graduation-cap text-[#114b3d] mr-1"></i> 수료증 인쇄 미리보기
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-primary" onclick="window.print()">
<i class="fa-solid fa-print"></i> 인쇄하기 (PDF 저장)
</button>
<button class="btn btn-secondary" onclick="window.close()">
<i class="fa-solid fa-xmark"></i> 창 닫기
</button>
</div>
</div>
<!-- 수료증 메인 A4 용지 -->
<div class="cert-page">
<div class="cert-outer-border">
<!-- 모서리 코너 장식 문양 (SVG) -->
<!-- 탑 레프트 -->
<svg class="corner-decor decor-tl" viewBox="0 0 30 30" width="30" height="30">
<rect x="0" y="0" width="8" height="8" fill="#4a5568" />
<line x1="4" y1="4" x2="30" y2="4" stroke="#4a5568" stroke-width="2" />
<line x1="4" y1="4" x2="4" y2="30" stroke="#4a5568" stroke-width="2" />
</svg>
<!-- 탑 라이트 -->
<svg class="corner-decor decor-tr" viewBox="0 0 30 30" width="30" height="30">
<rect x="0" y="0" width="8" height="8" fill="#4a5568" />
<line x1="4" y1="4" x2="30" y2="4" stroke="#4a5568" stroke-width="2" />
<line x1="4" y1="4" x2="4" y2="30" stroke="#4a5568" stroke-width="2" />
</svg>
<!-- 바텀 레프트 -->
<svg class="corner-decor decor-bl" viewBox="0 0 30 30" width="30" height="30">
<rect x="0" y="0" width="8" height="8" fill="#4a5568" />
<line x1="4" y1="4" x2="30" y2="4" stroke="#4a5568" stroke-width="2" />
<line x1="4" y1="4" x2="4" y2="30" stroke="#4a5568" stroke-width="2" />
</svg>
<!-- 바텀 라이트 -->
<svg class="corner-decor decor-br" viewBox="0 0 30 30" width="30" height="30">
<rect x="0" y="0" width="8" height="8" fill="#4a5568" />
<line x1="4" y1="4" x2="30" y2="4" stroke="#4a5568" stroke-width="2" />
<line x1="4" y1="4" x2="4" y2="30" stroke="#4a5568" stroke-width="2" />
</svg>
<!-- 수료증 컨텐츠 내부 테두리 안쪽 -->
<div class="cert-inner-border">
<!-- 백그라운드 흐릿한 로고 워터마크 -->
<div class="watermark-container">
<img id="val-watermark" src="" class="watermark-img" alt="" style="display: none;">
</div>
<!-- 발급 번호 -->
<div class="cert-no-area">
발급번호 : <span id="val-cert-no">제 - 호</span>
</div>
<!-- 메인 타이틀 -->
<div class="cert-title">수료증</div>
<!-- 상단 가로 구분선 -->
<div class="divider"></div>
<!-- 본문 리스트 -->
<div class="info-table">
<!-- 성명 -->
<div class="info-row">
<div class="info-label">
<span class="bullet">●</span>
<span class="text"><span>성</span><span>명 : </span></span>
</div>
<div class="info-value" id="val-name">-</div>
</div>
<!-- 교육과정 -->
<div class="info-row">
<div class="info-label">
<span class="bullet">●</span>
<span class="text"><span>교</span><span>육</span><span>과</span><span>정 : </span></span>
</div>
<div class="info-value" id="val-category">-</div>
</div>
<!-- 교육기간 -->
<div class="info-row">
<div class="info-label">
<span class="bullet">●</span>
<span class="text"><span>교</span><span>육</span><span>기</span><span>간 : </span></span>
</div>
<div class="info-value" id="val-period">-</div>
</div>
<!-- 교육시간 -->
<div class="info-row">
<div class="info-label">
<span class="bullet">●</span>
<span class="text"><span>교</span><span>육</span><span>시</span><span>간</span></span>
</div>
<div class="info-value" id="val-hours">-</div>
</div>
</div>
<!-- 하단 가로 구분선 -->
<div class="divider"></div>
<!-- 수여 성명 및 선언문 -->
<div class="cert-statement">
상기인은 위의 교육과정을 수료하였으므로<br>이 증서를 수여합니다.
</div>
<!-- 수료 년월일 -->
<div class="cert-date" id="val-prt-date">
- 년 - 월 - 일
</div>
<!-- 발송 회사명 및 대표이사 서명/도장 -->
<div class="cert-footer">
<div class="company-name" id="val-company">-</div>
<div class="ceo-name-area">
<span class="ceo-text">
대표이사 &nbsp;<span id="val-ceo">-</span>
<span class="stamp-container">
<img id="val-stamp" src="" class="stamp-img" alt="직인" style="display: none;">
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 자바스크립트 로직 로드 -->
<script src="../js/legal_cert_print.js?v=<?= time() ?>"></script>
</body>
</html>
+697
View File
@@ -0,0 +1,697 @@
<?php
require_once __DIR__ . '/../../bbs/auth.php';
edu_require_login();
include_once 'header.php';
require_once __DIR__ . '/../../bbs/db_conn.php';
$search_comp = $_GET['comp'] ?? '';
// 만약 사용자가 처음 페이지에 들어왔거나(GET값이 없음), '전체'를 누른 게 아니라면 초기값 설정
// 소속회사(comp)도 동일한 메커니즘 적용
$search_comp = $_GET['comp'] ?? '';
if (empty($search_comp) && !isset($_GET['comp'])) {
$search_comp = $sys_comp_code;
}
$search_year = $_GET['year'] ?? date('Y');
$search_dept = $_GET['dept'] ?? '';
$search_name = $_GET['name'] ?? '';
$search_comp_status = $_GET['comp_status'] ?? '';
$message_name = '';
$all_user_qty = 0;
$completed_qty = 0;
$incomplete_qty = 0;
$corp_list = [];
$rows = [];
try {
$pdo = db_conn();
// 1. 프로시저 호입 후 nextRowset으로 부분적 result set을 완전 소진
try {
$stmt_corp = $pdo->query("CALL proc_get_code2_list('CO100')");
$corp_list = $stmt_corp->fetchAll(PDO::FETCH_ASSOC);
while ($stmt_corp->nextRowset()) {
}
unset($stmt_corp);
} catch (Exception $eProc) {
$corp_list = [];
}
// 1-2. 교육과정별 수료증 출력을 위한 과정 목록 조회
$course_list = [];
try {
$stmt_courses = $pdo->query("SELECT base_code AS code, code_name FROM edu_codes WHERE group_code='CA200' AND desc01 = 'CA10003' ORDER BY base_code;");
$course_list = $stmt_courses->fetchAll(PDO::FETCH_ASSOC);
while ($stmt_courses->nextRowset()) {
}
unset($stmt_courses);
} catch (Exception $eCourses) {
$course_list = [];
}
// 권한에 따른 법인 목록 제어
// LE10001: 전체권한 → 전체 법인 표시 (corp_list 그대로)
// LE10002: 법인권한 → 본인 법인($sys_comp_code)만 표시, 검색값도 고정
// 그 외: 법인 목록 없음
// 법인 초기값은 로그인한 사용자의 법인으로 설정
$message_name = $pdo->prepare("SELECT DESC01 FROM edu_codes WHERE base_code = 'AL100100'");
$message_name->execute();
$message_name = $message_name->fetchColumn();
if ($auth_level === 'LE10002') {
// 본인 법인만 필터링
$corp_list = array_filter($corp_list, fn($c) => $c['code'] === $sys_comp_code);
$corp_list = array_values($corp_list);
// 검색 법인도 강제 고정
$search_comp = $sys_comp_code;
} elseif ($auth_level !== 'LE10001') {
// 그 외 권한: 법인 목록 비움
$corp_list = [];
}
// 2. 전체 대상자 수 로그인한 법인과 소속회사가 같은 기준으로 계산, 퇴사자 제외
$stmt_all = $pdo->prepare("SELECT COUNT(DISTINCT member_id)
FROM edu_users
WHERE (end_date IS NULL OR (end_date > '1000-01-01' AND YEAR(end_date) >= ?))
AND (? = '' OR belong_comp = ?)
and sys_comp_code = belong_comp");
$stmt_all->execute([$search_year, $search_comp, $search_comp]);
$all_user_qty = (int) $stmt_all->fetchColumn();
// 3. 미수료 인원
$stmt_incomp = $pdo->prepare("SELECT COUNT(DISTINCT u.member_id)
FROM edu_users u
WHERE (u.end_date IS NULL OR (u.end_date > '1000-01-01' AND YEAR(u.end_date) >= ?))
AND (? = '' OR u.belong_comp = ?)
AND sys_comp_code = belong_comp
AND fn_get_progress_rate(u.sys_comp_code,?,u.member_id,'CA10003','') != 100");
$stmt_incomp->execute([$search_year, $search_comp, $search_comp, $search_year]);
$incomplete_qty = (int) $stmt_incomp->fetchColumn();
$completed_qty = max(0, $all_user_qty - $incomplete_qty);
// 4. G1 메인 쿼리 (? 위치 파라미터 사용으로 재사용 문제 없음)
$sql_inner = "SELECT a.sys_comp_code, a.belong_comp
, (SELECT code_name FROM edu_codes c WHERE c.group_code = 'CO100' AND c.code = a.belong_comp LIMIT 1) AS comp_name
, a.name, a.member_id
, a.dept_name
, IFNULL(b.formatted_tm, '00시간 00분') AS all_tm
, CASE WHEN a.member_id IN (
SELECT u2.member_id FROM edu_users u2 WHERE NOT EXISTS (
SELECT 1 FROM edu_contents c2 WHERE c2.category_code = 'CA10003' AND c2.base_year = ? AND c2.is_active = '1'
AND NOT EXISTS (SELECT 1 FROM edu_learning_histories h2 WHERE h2.content_id = c2.content_id AND h2.member_id = u2.member_id AND h2.sys_comp_code = u2.sys_comp_code AND h2.completed_at IS NOT NULL AND h2.completed_at != '')
)
) THEN '수료' ELSE '미수료' END AS completion_status
, fn_get_progress_rate(a.sys_comp_code, ?, a.member_id, 'CA10003', '') AS progress_rate -- 진행율
, fn_get_completion_date(a.sys_comp_code, ?, a.member_id, 'CA10003', '') AS completion_date -- 학습완료일
FROM edu_users a
LEFT JOIN (
SELECT
t.sys_comp_code,
t.member_id,
CONCAT(
LPAD(FLOOR(SUM(t.calc_tm)/3600), 2, '0'), '시간 ',
LPAD(FLOOR((SUM(t.calc_tm)%3600)/60), 2, '0'), '분'
) AS formatted_tm
FROM (
SELECT
z.sys_comp_code,
x.member_id,
CASE
WHEN x.completed_at IS NOT NULL AND x.completed_at <> '' THEN x.content_tm
ELSE x.watch_tm
END AS calc_tm
FROM edu_learning_histories x
JOIN edu_contents y ON x.content_id = y.content_id
JOIN edu_users z ON x.sys_comp_code = z.working_comp AND x.member_id = z.member_id
WHERE y.category_code = 'CA10003'
AND YEAR(x.first_viewed_at) = ?
) t
GROUP BY t.sys_comp_code, t.member_id
) b ON a.member_id = b.member_id AND a.sys_comp_code = b.sys_comp_code
WHERE (a.end_date IS NULL OR (a.end_date > '1000-01-01' AND YEAR(a.end_date) >= ?))
and a.sys_comp_code = a.belong_comp
AND (? = '' OR a.belong_comp = ?)
AND a.dept_name LIKE CONCAT('%', ?, '%')
AND a.name LIKE CONCAT('%', ?, '%')";
if ($search_comp_status === 'Y') {
$sql = "SELECT * FROM ($sql_inner) t WHERE completion_status = '수료'";
} elseif ($search_comp_status === 'N') {
$sql = "SELECT * FROM ($sql_inner) t WHERE completion_status = '미수료'";
} else {
$sql = "SELECT * FROM ($sql_inner) t";
}
$stmt_g1 = $pdo->prepare($sql);
// ? 순서: 1=completion_status(base_year), 2=progress_rate(year), 3=completion_date(year), 4=LEFT JOIN(first_viewed_at), 5=WHERE(end_date), 6=belong_comp 체크, 7=belong_comp 필터, 8=dept_name, 9=name
$stmt_g1->execute([$search_year, $search_year, $search_year, $search_year, $search_year, $search_comp, $search_comp, $search_dept, $search_name]);
$rows = $stmt_g1->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$db_error = $e->getMessage();
}
?>
<main class="max-w-[1600px] mx-auto p-6">
<header class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<h2 class="text-2xl font-bold text-gray-800 italic">법정의무교육</h2>
<div class="flex flex-wrap gap-2">
<button onclick="downloadReportExcel()"
class="px-4 py-2 bg-white border border-gray-200 rounded-md text-sm font-medium hover:bg-gray-50 flex items-center shadow-sm">
<i class="fa-solid fa-file-excel mr-2"></i>교육결과보고서
</button>
<button onclick="openCourseSelectModal()"
class="px-4 py-2 bg-[#114b3d] text-white rounded-md text-sm font-bold flex items-center hover:bg-[#0d3a2f] shadow-sm transition">
<i class="fa-solid fa-print mr-2"></i>수료증출력
</button>
<button onclick="sendIncompleteNotification()"
class="px-4 py-2 bg-red-50 text-red-600 border border-red-100 rounded-md text-sm font-bold flex items-center hover:bg-red-100 shadow-sm transition">
<i class="fa-solid fa-bell mr-2"></i>미수료자 알림 (<?php echo $incomplete_qty; ?>명)
</button>
<button onclick="downloadExcel()"
class="px-4 py-2 bg-gray-100 text-gray-600 rounded-md text-sm font-medium hover:bg-gray-200 flex items-center transition">
<i class="fa-solid fa-download mr-2"></i>다운로드
</button>
</div>
</header>
<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">
<p class="text-xs font-bold text-gray-400 mb-1">전체 대상자</p>
<p class="text-3xl font-bold text-gray-800"><?php echo $all_user_qty; ?></p>
</div>
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<p class="text-xs font-bold text-gray-400 mb-1">수료 완료</p>
<p class="text-3xl font-bold text-teal-600"><?php echo $completed_qty; ?></p>
</div>
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<p class="text-xs font-bold text-gray-400 mb-1">미수료</p>
<p class="text-3xl font-bold text-red-500"><?php echo $incomplete_qty; ?>명</p>
</div>
</section>
<!-- 검색 조건 폼 -->
<form id="searchForm" method="GET" action="legal_edu.php"
class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm mb-6 flex flex-wrap md:flex-row gap-4 items-end">
<div class="flex-1 min-w-[120px]">
<label for="comp" class="block text-xs font-bold text-gray-500 mb-2">법인 선택</label>
<?php if ($auth_level === 'LE10002'): ?>
<?php
// LE10002: 본인 법인명 표시 (변경 불가)
$fixed_corp_name = !empty($corp_list) ? htmlspecialchars($corp_list[0]['name']) : htmlspecialchars($sys_comp_code);
?>
<!-- 실제 전송값은 hidden으로, UI는 고정 텍스트로 표시 -->
<input type="hidden" name="comp" value="<?= htmlspecialchars($sys_comp_code) ?>">
<input type="text" value="<?= $fixed_corp_name ?>" readonly
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-100 text-gray-600 cursor-not-allowed">
<?php else: ?>
<select id="comp" name="comp" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<?php foreach ($corp_list as $corp): ?>
<option value="<?= htmlspecialchars($corp['code']) ?>" <?= $search_comp === $corp['code'] ? 'selected' : '' ?>>
<?= htmlspecialchars($corp['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</div>
<div class="flex-1 min-w-[100px]">
<label for="year" class="block text-xs font-bold text-gray-500 mb-2">기준년도</label>
<input type="text" id="year" name="year" value="<?= htmlspecialchars($search_year) ?>" placeholder="YYYY"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[120px]">
<label for="dept" class="block text-xs font-bold text-gray-500 mb-2">부서</label>
<input type="text" id="dept" name="dept" value="<?= htmlspecialchars($search_dept) ?>"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[100px]">
<label for="name" class="block text-xs font-bold text-gray-500 mb-2">성명</label>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($search_name) ?>"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[120px]">
<label for="comp_status" class="block text-xs font-bold text-gray-500 mb-2">이수여부</label>
<select id="comp_status" name="comp_status" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<option value="Y" <?= $search_comp_status === 'Y' ? 'selected' : '' ?>>이수</option>
<option value="N" <?= $search_comp_status === 'N' ? 'selected' : '' ?>>미이수</option>
</select>
</div>
</form>
<section class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden mb-12">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="bg-gray-50 border-b border-gray-200 text-gray-500 font-bold italic">
<tr>
<th class="p-4 w-16 text-center">NO</th>
<th class="p-4">소속법인</th>
<th class="p-4">성명</th>
<th class="p-4">사번</th>
<th class="p-4">부서</th>
<th class="p-4">학습시간</th>
<th class="p-4 w-48">진도율</th>
<th class="p-4">교육이수일</th>
<th class="p-4 text-center">수료구분</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if (count($rows) === 0): ?>
<tr>
<td colspan="9" class="p-8 text-center text-gray-400">데이터가 없습니다.</td>
</tr>
<?php else: ?>
<?php $idx = 1;
foreach ($rows as $row): ?>
<tr class="hover:bg-gray-50 transition cursor-pointer"
onclick="openDetailModal('<?= htmlspecialchars($row['sys_comp_code']) ?>', '<?= htmlspecialchars($row['member_id']) ?>', '<?= htmlspecialchars($row['name']) ?>', '<?= htmlspecialchars($row['dept_name']) ?>')">
<td class="p-4 text-center text-gray-500"><?= $idx++ ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['comp_name'] ?? '') ?></td>
<td class="p-4 font-bold text-gray-800"><?= htmlspecialchars($row['name'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['member_id'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['dept_name'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['all_tm'] ?? '') ?></td>
<td class="p-4">
<div class="flex items-center space-x-2">
<?php
$progress = $row['progress_rate'] ?? '0%';
$progress_value = is_numeric($progress) ? (int) $progress : (int) preg_replace('/[^0-9]/', '', $progress);
$progress_value = min(100, max(0, $progress_value));
?>
<div class="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div class="bg-teal-500 h-full" style="width: <?= $progress_value ?>%;"></div>
</div>
<span class="text-[10px] font-bold text-teal-600"><?= $progress_value ?>%</span>
</div>
</td>
<td class="p-4 text-gray-600"><?= htmlspecialchars($row['completion_date'] ?? '-') ?></td>
<td class="p-4 text-center">
<?php if ($row['completion_status'] === '수료'): ?>
<span
class="px-3 py-1 bg-green-50 text-green-600 border border-green-100 rounded-full text-[11px] font-bold"><i
class="fa-solid fa-check-circle mr-1"></i>수료</span>
<?php else: ?>
<span class="px-3 py-1 bg-red-50 text-red-600 border border-red-100 rounded-full text-[11px] font-bold"><i
class="fa-solid fa-circle-xmark mr-1"></i>미수료</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
<!-- 상세 데이터 (G2) 모달 팝업 -->
<div id="detail-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[100] hidden p-4">
<div class="bg-white w-full max-w-4xl rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50 shrink-0">
<h3 class="text-xl font-bold text-gray-800">법정의무교육 이수 상세</h3>
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600"><i
class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="p-4 bg-white border-b border-gray-100 flex gap-6 text-sm font-bold text-gray-600 shrink-0">
<div>성명: <span id="modal-name" class="text-gray-900"></span></div>
<div>사번: <span id="modal-member-id" class="text-gray-900"></span></div>
<div>부서명: <span id="modal-dept" class="text-gray-900"></span></div>
</div>
<div class="flex-1 overflow-auto p-4 bg-gray-50/50">
<table class="w-full text-left border-collapse bg-white border border-gray-200">
<thead class="bg-gray-50 border-b border-gray-200 text-gray-500 font-bold">
<tr>
<th class="p-3 text-center w-12"><input type="checkbox" id="chk-all-certs" onchange="toggleAllDetailCerts(this)" checked class="w-4 h-4 text-teal-600 border-gray-300 rounded focus:ring-teal-500"></th>
<th class="p-3 text-center w-12">NO</th>
<th class="p-3">교육과정명</th>
<th class="p-3 w-32">학습시간</th>
<th class="p-3 w-32">진도율</th>
<th class="p-3 w-32">학습완료일</th>
<th class="p-3 w-28 text-center">수료구분</th>
</tr>
</thead>
<tbody id="detail-grid-body" class="divide-y divide-gray-100 text-sm">
<!-- AJAX JS INJECTION -->
</tbody>
</table>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-100 flex justify-between items-center shrink-0">
<button onclick="printSelectedCertificates()"
class="px-5 py-2 bg-[#114b3d] text-white rounded-lg font-bold shadow-md hover:bg-[#0d3a2f] transition flex items-center gap-2">
<i class="fa-solid fa-print"></i> 선택 수료증 일괄출력 (멀티출력)
</button>
<button onclick="closeDetailModal()"
class="px-6 py-2 bg-gray-500 text-white rounded-lg font-bold shadow-md">닫기</button>
</div>
</div>
</div>
<!-- 교육과정선택 (수료증 일괄출력) 모달 팝업 -->
<div id="course-select-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[100] hidden p-4">
<div class="bg-white w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden flex flex-col">
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50 shrink-0">
<h3 class="text-xl font-bold text-gray-800"><i class="fa-solid fa-print text-[#114b3d] mr-2"></i>교육과정별 수료증 일괄출력</h3>
<button onclick="closeCourseSelectModal()" class="text-gray-400 hover:text-gray-600"><i
class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="p-6 bg-white flex flex-col gap-5 text-sm shrink-0">
<!-- 고정 메타데이터 정보 -->
<div class="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-xl border border-gray-100 font-medium text-gray-600">
<div>출력 기준년도: <span class="text-gray-900 font-bold"><?= htmlspecialchars($search_year) ?>년</span></div>
<div>출력 대상법인: <span class="text-gray-900 font-bold">
<?php
if ($search_comp === '') {
echo '전체 법인';
} else {
$comp_name_found = $search_comp;
foreach ($corp_list as $corp) {
if ($corp['code'] === $search_comp) {
$comp_name_found = $corp['name'];
break;
}
}
echo htmlspecialchars($comp_name_found);
}
?>
</span></div>
</div>
<!-- 과정 선택 영역 -->
<div class="flex flex-col gap-2">
<label class="block text-xs font-bold text-gray-500 uppercase">출력할 교육과정 선택</label>
<div class="flex flex-col gap-2.5 max-h-[300px] overflow-y-auto pr-1">
<?php if (empty($course_list)): ?>
<p class="text-gray-400 text-center py-4">조회 가능한 교육과정이 없습니다.</p>
<?php else: ?>
<?php foreach ($course_list as $idx => $course): ?>
<label class="flex items-center gap-3 p-3 border border-gray-100 rounded-xl hover:bg-gray-50 cursor-pointer transition">
<input type="radio" name="selected_course_group" value="<?= htmlspecialchars($course['code']) ?>" <?= $idx === 0 ? 'checked' : '' ?>
class="w-4 h-4 text-[#114b3d] border-gray-300 focus:ring-[#114b3d]">
<div class="flex flex-col">
<span class="font-bold text-gray-800"><?= htmlspecialchars($course['code_name']) ?></span>
<span class="text-xs text-gray-400">코드: <?= htmlspecialchars($course['code']) ?></span>
</div>
</label>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-100 flex justify-end gap-2 shrink-0">
<button onclick="closeCourseSelectModal()"
class="px-5 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg font-bold transition">닫기</button>
<button onclick="printCourseCertificates()"
class="px-5 py-2 bg-[#114b3d] text-white rounded-lg font-bold shadow-md hover:bg-[#0d3a2f] transition flex items-center gap-2">
<i class="fa-solid fa-print"></i> 수료증 일괄출력
</button>
</div>
</div>
</div>
<script>
function downloadExcel() {
const form = document.getElementById('searchForm');
const urlParams = new URLSearchParams(new FormData(form)).toString();
window.location.href = '../bbs/legal_edu_excel.php?' + urlParams;
}
function toggleAllDetailCerts(master) {
const checkboxes = document.querySelectorAll('.cert-item-chk:not(:disabled)');
checkboxes.forEach(chk => {
chk.checked = master.checked;
});
}
function openDetailModal(sys_comp_code, member_id, name, dept) {
document.getElementById('modal-name').textContent = name;
document.getElementById('modal-member-id').textContent = member_id;
document.getElementById('modal-dept').textContent = dept;
// 선택 일괄출력을 위한 데이터 바인딩
const modal = document.getElementById('detail-modal');
modal.dataset.sysCompCode = sys_comp_code;
modal.dataset.memberId = member_id;
modal.classList.remove('hidden');
const gridBody = document.getElementById('detail-grid-body');
gridBody.innerHTML = '<tr><td colspan="7" class="p-6 text-center text-gray-500">로딩 중...</td></tr>';
const year = document.getElementById('year').value;
modal.dataset.year = year;
const requestUrl = `../bbs/get_legal_edu_detail.php?sys_comp_code=${encodeURIComponent(sys_comp_code)}&member_id=${encodeURIComponent(member_id)}&year=${encodeURIComponent(year)}`;
console.log('[G2] requestUrl', requestUrl, { sys_comp_code, member_id, year });
fetch(requestUrl)
.then(res => res.text())
.then(text => {
console.log('[G2] raw response', text);
let data;
try {
data = JSON.parse(text);
} catch (e) {
console.error('[G2] JSON parse error', e, text);
gridBody.innerHTML = '<tr><td colspan="7" class="p-6 text-center text-red-500">JSON 파싱 오류 발생했습니다.</td></tr>';
return;
}
gridBody.innerHTML = '';
if (!data.success || !data.items || data.items.length === 0) {
gridBody.innerHTML = '<tr><td colspan="7" class="p-6 text-center text-gray-400">학습 내역이 없습니다.</td></tr>';
return;
}
// 전체 체크박스 선택기 초기화
const chkAll = document.getElementById('chk-all-certs');
if (chkAll) chkAll.checked = true;
data.items.forEach((it, idx) => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50 transition';
const checkboxHtml = it.comp_status === '수료'
? `<input type="checkbox" class="cert-item-chk w-4 h-4 text-teal-600 border-gray-300 rounded focus:ring-teal-500" data-title="${escapeHtml(it.title)}" checked>`
: `<input type="checkbox" disabled class="w-4 h-4 border-gray-200 rounded cursor-not-allowed bg-gray-50 opacity-50">`;
const stHtml = it.comp_status === '수료'
? `<div class="flex flex-col items-center gap-1.5">
<span class="px-3 py-1 bg-green-50 text-green-600 border border-green-100 rounded-full text-[11px] font-bold"><i class="fa-solid fa-check-circle mr-1"></i>수료</span>
<button onclick="event.stopPropagation(); printCertificate('${year}', '${sys_comp_code}', '${member_id}', '${it.title}')" class="px-2 py-0.5 bg-[#114b3d] text-white rounded text-[10px] font-bold hover:bg-[#0d3a2f] transition flex items-center gap-1 shadow-sm"><i class="fa-solid fa-print"></i>수료증발급</button>
</div>`
: `<span class="px-3 py-1 bg-red-50 text-red-600 border border-red-100 rounded-full text-[11px] font-bold"><i class="fa-solid fa-circle-xmark mr-1"></i>미수료</span>`;
// progress_rate에서 숫자만 추출
let progressValue = 0;
if (it.progress_rate) {
const match = it.progress_rate.toString().match(/\d+/);
progressValue = match ? parseInt(match[0]) : 0;
}
progressValue = Math.min(100, Math.max(0, progressValue));
tr.innerHTML = `
<td class="p-3 text-center">${checkboxHtml}</td>
<td class="p-3 text-center text-gray-400">${idx + 1}</td>
<td class="p-3 font-bold text-gray-800">${escapeHtml(it.title)}</td>
<td class="p-3 text-gray-600">${escapeHtml(it.learn_time)}</td>
<td class="p-3">
<div class="flex items-center space-x-2">
<div class="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div class="bg-teal-500 h-full" style="width: ${progressValue}%;"></div>
</div>
<span class="text-[10px] font-bold text-teal-600">${progressValue}%</span>
</div>
</td>
<td class="p-3 text-gray-600">${escapeHtml(it.completion_date) || '-'}</td>
<td class="p-3 text-center">${stHtml}</td>
`;
gridBody.appendChild(tr);
});
})
.catch(err => {
console.error('데이터 로드 오류:', err);
gridBody.innerHTML = '<tr><td colspan="7" class="p-6 text-center text-red-500">데이터를 불러오는 중 오류가 발생했습니다.</td></tr>';
});
}
function printSelectedCertificates() {
const modal = document.getElementById('detail-modal');
const sysCompCode = modal.dataset.sysCompCode;
const memberId = modal.dataset.memberId;
const year = modal.dataset.year;
// 선택된 체크박스 가져오기
const checkedBoxes = document.querySelectorAll('.cert-item-chk:checked');
if (checkedBoxes.length === 0) {
alert('출력할 수료증 과정을 1개 이상 선택해 주세요.');
return;
}
const titles = Array.from(checkedBoxes).map(chk => chk.dataset.title);
const categoryMap = {
'개인정보보호': 'CA200C01',
'직장내 괴롭힘 예방': 'CA200C02',
'장애인 인식 개선': 'CA200C03',
'성희롱 예방 교육': 'CA200C04',
'퇴직금 교육': 'CA200C05',
'산업안전보건': 'CA200C06'
};
const categories = [];
titles.forEach(title => {
const code = categoryMap[title];
if (code) {
categories.push(code);
}
});
if (categories.length === 0) {
alert('선택한 과정 중 수료증 출력이 가능한 과정이 없습니다.');
return;
}
// 쉼표로 연결하여 멀티 파라미터 전달
const categoryGroup = categories.join(',');
const url = `legal_cert_print.php?year=${encodeURIComponent(year)}&comp=${encodeURIComponent(sysCompCode)}&member_id=${encodeURIComponent(memberId)}&category_group=${encodeURIComponent(categoryGroup)}`;
window.open(url, '_blank', 'width=950,height=1000,scrollbars=yes');
}
function printCertificate(year, comp, memberId, title) {
const categoryMap = {
'개인정보보호': 'CA200C01',
'직장내 괴롭힘 예방': 'CA200C02',
'장애인 인식 개선': 'CA200C03',
'성희롱 예방 교육': 'CA200C04',
'퇴직금 교육': 'CA200C05',
'산업안전보건': 'CA200C06'
};
const categoryGroup = categoryMap[title] || '';
if (!categoryGroup) {
alert('이 교육과정은 수료증 출력을 지원하지 않습니다.');
return;
}
const url = `legal_cert_print.php?year=${encodeURIComponent(year)}&comp=${encodeURIComponent(comp)}&member_id=${encodeURIComponent(memberId)}&category_group=${encodeURIComponent(categoryGroup)}`;
window.open(url, '_blank', 'width=950,height=1000,scrollbars=yes');
}
function closeDetailModal() {
document.getElementById('detail-modal').classList.add('hidden');
}
function openCourseSelectModal() {
document.getElementById('course-select-modal').classList.remove('hidden');
}
function closeCourseSelectModal() {
document.getElementById('course-select-modal').classList.add('hidden');
}
function printCourseCertificates() {
const checkedRadio = document.querySelector('input[name="selected_course_group"]:checked');
if (!checkedRadio) {
alert('출력할 교육과정을 선택해 주세요.');
return;
}
const categoryGroup = checkedRadio.value;
const year = document.getElementById('year').value || '<?= htmlspecialchars($search_year) ?>';
const comp = '<?= htmlspecialchars($search_comp) ?>';
const url = `legal_cert_print.php?year=${encodeURIComponent(year)}&comp=${encodeURIComponent(comp)}&member_id=ALL&category_group=${encodeURIComponent(categoryGroup)}`;
window.open(url, '_blank', 'width=950,height=1000,scrollbars=yes');
closeCourseSelectModal();
}
function sendIncompleteNotification() {
const incompleteCount = <?php echo $incomplete_qty; ?>;
if (incompleteCount === 0) {
alert('미수료자가 없습니다.');
return;
}
if (!confirm(`<?php echo $message_name; ?> \n"위의 메시지로발송됩니다."\n미수료자 ${incompleteCount}명에게 알림을 발송하시겠습니까?`)) {
return;
}
// 현재 날짜 + 14일 계산
const today = new Date();
const endDate = new Date(today);
endDate.setDate(today.getDate() + 14);
const endDateStr = endDate.toISOString().split('T')[0];
// 알림 발송 요청
fetch('../bbs/notification_send.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
code: '100',
end_date: endDateStr,
action: 'send'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`미수료자 알림이 성공적으로 발송되었습니다.\n발송 대상: ${data.sent_count || incompleteCount}명`);
} else {
alert(`알림 발송에 실패했습니다: ${data.message || '알 수 없는 오류'}`);
}
})
.catch(err => {
console.error('알림 발송 오류:', err);
alert('알림 발송 중 오류가 발생했습니다.');
});
}
function downloadExcel() {
const params = new URLSearchParams({
comp: document.getElementById('comp').value || '',
year: document.getElementById('year').value || '',
dept: document.getElementById('dept').value || '',
name: document.getElementById('name').value || '',
comp_status: document.getElementById('comp_status').value || ''
});
window.location.href = `../bbs/legal_edu_excel.php?${params.toString()}`;
}
function downloadReportExcel() {
const params = new URLSearchParams({
comp: document.getElementById('comp').value || '',
year: document.getElementById('year').value || ''
});
window.location.href = `../bbs/legal_edu_report_excel.php?${params.toString()}`;
}
function escapeHtml(unsafe) {
return (unsafe || '').toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
</script>
</body>
</html>
+492
View File
@@ -0,0 +1,492 @@
<?php
require_once __DIR__ . '/../../bbs/auth.php';
edu_require_login();
include_once 'header.php';
require_once __DIR__ . '/../../bbs/db_conn.php';
$search_comp = $_GET['comp'] ?? '';
// 만약 사용자가 처음 페이지에 들어왔거나(GET값이 없음), '전체'를 누른 게 아니라면 초기값 설정
// 소속회사(comp)도 동일한 메커니즘 적용
$search_comp = $_GET['comp'] ?? '';
if (empty($search_comp) && !isset($_GET['comp'])) {
$search_comp = $sys_comp_code;
}
$search_year = $_GET['year'] ?? date('Y');
$search_dept = $_GET['dept'] ?? '';
$search_name = $_GET['name'] ?? '';
$search_comp_status = $_GET['comp_status'] ?? '';
$message_name = '';
$all_user_qty = 0;
$completed_qty = 0;
$incomplete_qty = 0;
$corp_list = [];
$rows = [];
try {
$pdo = db_conn();
// 1. 프로시저 호입 후 nextRowset으로 부분적 result set을 완전 소진
try {
$stmt_corp = $pdo->query("CALL proc_get_code2_list('CO100')");
$corp_list = $stmt_corp->fetchAll(PDO::FETCH_ASSOC);
while ($stmt_corp->nextRowset()) {
}
unset($stmt_corp);
} catch (Exception $eProc) {
$corp_list = [];
}
// 권한에 따른 법인 목록 제어
// LE10001: 전체권한 → 전체 법인 표시 (corp_list 그대로)
// LE10002: 법인권한 → 본인 법인($sys_comp_code)만 표시, 검색값도 고정
// 그 외: 법인 목록 없음
// 법인 초기값은 로그인한 사용자의 법인으로 설정
$message_name = $pdo->prepare("SELECT DESC01 FROM edu_codes WHERE base_code = 'AL100100'");
$message_name->execute();
$message_name = $message_name->fetchColumn();
if ($auth_level === 'LE10002') {
// 본인 법인만 필터링
$corp_list = array_filter($corp_list, fn($c) => $c['code'] === $sys_comp_code);
$corp_list = array_values($corp_list);
// 검색 법인도 강제 고정
$search_comp = $sys_comp_code;
} elseif ($auth_level !== 'LE10001') {
// 그 외 권한: 법인 목록 비움
$corp_list = [];
}
// 2. 전체 대상자 수 로그인한 법인과 소속회사가 같은 기준으로 계산, 퇴사자 제외
$stmt_all = $pdo->prepare("SELECT COUNT(DISTINCT member_id)
FROM edu_users
WHERE (end_date IS NULL OR (end_date > '1000-01-01' AND YEAR(end_date) >= ?))
AND (? = '' OR belong_comp = ?)
and sys_comp_code = belong_comp");
$stmt_all->execute([$search_year, $search_comp, $search_comp]);
$all_user_qty = (int) $stmt_all->fetchColumn();
// 3. 미수료 인원
$stmt_incomp = $pdo->prepare("SELECT COUNT(DISTINCT u.member_id)
FROM edu_users u
WHERE (u.end_date IS NULL OR (u.end_date > '1000-01-01' AND YEAR(u.end_date) >= ?))
AND (? = '' OR u.belong_comp = ?)
AND sys_comp_code = belong_comp
AND fn_get_progress_rate(u.sys_comp_code,?,u.member_id,'CA10003','') != 100");
$stmt_incomp->execute([$search_year, $search_comp, $search_comp, $search_year]);
$incomplete_qty = (int) $stmt_incomp->fetchColumn();
$completed_qty = max(0, $all_user_qty - $incomplete_qty);
// 4. G1 메인 쿼리 (? 위치 파라미터 사용으로 재사용 문제 없음)
$sql_inner = "SELECT a.sys_comp_code, a.belong_comp
, (SELECT code_name FROM edu_codes c WHERE c.group_code = 'CO100' AND c.code = a.belong_comp LIMIT 1) AS comp_name
, a.name, a.member_id
, a.dept_name
, IFNULL(b.formatted_tm, '00시간 00분') AS all_tm
, CASE WHEN a.member_id IN (
SELECT u2.member_id FROM edu_users u2 WHERE NOT EXISTS (
SELECT 1 FROM edu_contents c2 WHERE c2.category_code = 'CA10003' AND c2.base_year = ? AND c2.is_active = '1'
AND NOT EXISTS (SELECT 1 FROM edu_learning_histories h2 WHERE h2.content_id = c2.content_id AND h2.member_id = u2.member_id AND h2.sys_comp_code = u2.sys_comp_code AND h2.completed_at IS NOT NULL AND h2.completed_at != '')
)
) THEN '수료' ELSE '미수료' END AS completion_status
, fn_get_progress_rate(a.sys_comp_code, ?, a.member_id, 'CA10003', '') AS progress_rate -- 진행율
, fn_get_completion_date(a.sys_comp_code, ?, a.member_id, 'CA10003', '') AS completion_date -- 학습완료일
FROM edu_users a
LEFT JOIN (
SELECT
t.sys_comp_code,
t.member_id,
CONCAT(
LPAD(FLOOR(SUM(t.calc_tm)/3600), 2, '0'), '시간 ',
LPAD(FLOOR((SUM(t.calc_tm)%3600)/60), 2, '0'), '분'
) AS formatted_tm
FROM (
SELECT
z.sys_comp_code,
x.member_id,
CASE
WHEN x.completed_at IS NOT NULL AND x.completed_at <> '' THEN x.content_tm
ELSE x.watch_tm
END AS calc_tm
FROM edu_learning_histories x
JOIN edu_contents y ON x.content_id = y.content_id
JOIN edu_users z ON x.sys_comp_code = z.working_comp AND x.member_id = z.member_id
WHERE y.category_code = 'CA10003'
AND YEAR(x.first_viewed_at) = ?
) t
GROUP BY t.sys_comp_code, t.member_id
) b ON a.member_id = b.member_id AND a.sys_comp_code = b.sys_comp_code
WHERE (a.end_date IS NULL OR a.end_date = '' OR (a.end_date > '1000-01-01' AND YEAR(a.end_date) >= ?))
and a.sys_comp_code = a.belong_comp
AND (? = '' OR a.belong_comp = ?)
AND a.dept_name LIKE CONCAT('%', ?, '%')
AND a.name LIKE CONCAT('%', ?, '%')";
if ($search_comp_status === 'Y') {
$sql = "SELECT * FROM ($sql_inner) t WHERE completion_status = '수료'";
} elseif ($search_comp_status === 'N') {
$sql = "SELECT * FROM ($sql_inner) t WHERE completion_status = '미수료'";
} else {
$sql = "SELECT * FROM ($sql_inner) t";
}
$stmt_g1 = $pdo->prepare($sql);
// ? 순서: 1=completion_status(base_year), 2=progress_rate(year), 3=completion_date(year), 4=LEFT JOIN(first_viewed_at), 5=WHERE(end_date), 6=belong_comp 체크, 7=belong_comp 필터, 8=dept_name, 9=name
$stmt_g1->execute([$search_year, $search_year, $search_year, $search_year, $search_year, $search_comp, $search_comp, $search_dept, $search_name]);
$rows = $stmt_g1->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$db_error = $e->getMessage();
}
?>
<main class="max-w-[1600px] mx-auto p-6">
<header class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<h2 class="text-2xl font-bold text-gray-800 italic">법정의무교육</h2>
<div class="flex flex-wrap gap-2">
<button onclick="alert('준비중입니다.')"
class="px-4 py-2 bg-white border border-gray-200 rounded-md text-sm font-medium hover:bg-gray-50 flex items-center shadow-sm">
<i class="fa-solid fa-file-excel mr-2"></i>교육결과보고서
</button>
<button onclick="sendIncompleteNotification()"
class="px-4 py-2 bg-red-50 text-red-600 border border-red-100 rounded-md text-sm font-bold flex items-center hover:bg-red-100 shadow-sm transition">
<i class="fa-solid fa-bell mr-2"></i>미수료자 알림 (<?php echo $incomplete_qty; ?>명)
</button>
<button onclick="downloadExcel()"
class="px-4 py-2 bg-gray-100 text-gray-600 rounded-md text-sm font-medium hover:bg-gray-200 flex items-center transition">
<i class="fa-solid fa-download mr-2"></i>다운로드
</button>
</div>
</header>
<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">
<p class="text-xs font-bold text-gray-400 mb-1">전체 대상자</p>
<p class="text-3xl font-bold text-gray-800"><?php echo $all_user_qty; ?></p>
</div>
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<p class="text-xs font-bold text-gray-400 mb-1">수료 완료</p>
<p class="text-3xl font-bold text-teal-600"><?php echo $completed_qty; ?></p>
</div>
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<p class="text-xs font-bold text-gray-400 mb-1">미수료</p>
<p class="text-3xl font-bold text-red-500"><?php echo $incomplete_qty; ?>명</p>
</div>
</section>
<!-- 검색 조건 폼 -->
<form id="searchForm" method="GET" action="legal_edu.php"
class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm mb-6 flex flex-wrap md:flex-row gap-4 items-end">
<div class="flex-1 min-w-[120px]">
<label for="comp" class="block text-xs font-bold text-gray-500 mb-2">법인 선택</label>
<?php if ($auth_level === 'LE10002'): ?>
<?php
// LE10002: 본인 법인명 표시 (변경 불가)
$fixed_corp_name = !empty($corp_list) ? htmlspecialchars($corp_list[0]['name']) : htmlspecialchars($sys_comp_code);
?>
<!-- 실제 전송값은 hidden으로, UI는 고정 텍스트로 표시 -->
<input type="hidden" name="comp" value="<?= htmlspecialchars($sys_comp_code) ?>">
<input type="text" value="<?= $fixed_corp_name ?>" readonly
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-100 text-gray-600 cursor-not-allowed">
<?php else: ?>
<select id="comp" name="comp" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<?php foreach ($corp_list as $corp): ?>
<option value="<?= htmlspecialchars($corp['code']) ?>" <?= $search_comp === $corp['code'] ? 'selected' : '' ?>>
<?= htmlspecialchars($corp['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</div>
<div class="flex-1 min-w-[100px]">
<label for="year" class="block text-xs font-bold text-gray-500 mb-2">기준년도</label>
<input type="text" id="year" name="year" value="<?= htmlspecialchars($search_year) ?>" placeholder="YYYY"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[120px]">
<label for="dept" class="block text-xs font-bold text-gray-500 mb-2">부서</label>
<input type="text" id="dept" name="dept" value="<?= htmlspecialchars($search_dept) ?>"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[100px]">
<label for="name" class="block text-xs font-bold text-gray-500 mb-2">성명</label>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($search_name) ?>"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[120px]">
<label for="comp_status" class="block text-xs font-bold text-gray-500 mb-2">이수여부</label>
<select id="comp_status" name="comp_status" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<option value="Y" <?= $search_comp_status === 'Y' ? 'selected' : '' ?>>이수</option>
<option value="N" <?= $search_comp_status === 'N' ? 'selected' : '' ?>>미이수</option>
</select>
</div>
</form>
<section class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden mb-12">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="bg-gray-50 border-b border-gray-200 text-gray-500 font-bold italic">
<tr>
<th class="p-4 w-16 text-center">NO</th>
<th class="p-4">소속법인</th>
<th class="p-4">성명</th>
<th class="p-4">사번</th>
<th class="p-4">부서</th>
<th class="p-4">학습시간</th>
<th class="p-4 w-48">진도율</th>
<th class="p-4">교육이수일</th>
<th class="p-4 text-center">수료구분</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if (count($rows) === 0): ?>
<tr>
<td colspan="9" class="p-8 text-center text-gray-400">데이터가 없습니다.</td>
</tr>
<?php else: ?>
<?php $idx = 1;
foreach ($rows as $row): ?>
<tr class="hover:bg-gray-50 transition cursor-pointer"
onclick="openDetailModal('<?= htmlspecialchars($row['sys_comp_code']) ?>', '<?= htmlspecialchars($row['member_id']) ?>', '<?= htmlspecialchars($row['name']) ?>', '<?= htmlspecialchars($row['dept_name']) ?>')">
<td class="p-4 text-center text-gray-500"><?= $idx++ ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['comp_name'] ?? '') ?></td>
<td class="p-4 font-bold text-gray-800"><?= htmlspecialchars($row['name'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['member_id'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['dept_name'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['all_tm'] ?? '') ?></td>
<td class="p-4">
<div class="flex items-center space-x-2">
<?php
$progress = $row['progress_rate'] ?? '0%';
$progress_value = is_numeric($progress) ? (int) $progress : (int) preg_replace('/[^0-9]/', '', $progress);
$progress_value = min(100, max(0, $progress_value));
?>
<div class="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div class="bg-teal-500 h-full" style="width: <?= $progress_value ?>%;"></div>
</div>
<span class="text-[10px] font-bold text-teal-600"><?= $progress_value ?>%</span>
</div>
</td>
<td class="p-4 text-gray-600"><?= htmlspecialchars($row['completion_date'] ?? '-') ?></td>
<td class="p-4 text-center">
<?php if ($row['completion_status'] === '수료'): ?>
<span
class="px-3 py-1 bg-green-50 text-green-600 border border-green-100 rounded-full text-[11px] font-bold"><i
class="fa-solid fa-check-circle mr-1"></i>수료</span>
<?php else: ?>
<span class="px-3 py-1 bg-red-50 text-red-600 border border-red-100 rounded-full text-[11px] font-bold"><i
class="fa-solid fa-circle-xmark mr-1"></i>미수료</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
<!-- 상세 데이터 (G2) 모달 팝업 -->
<div id="detail-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[100] hidden p-4">
<div class="bg-white w-full max-w-4xl rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50 shrink-0">
<h3 class="text-xl font-bold text-gray-800">법정의무교육 이수 상세</h3>
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600"><i
class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="p-4 bg-white border-b border-gray-100 flex gap-6 text-sm font-bold text-gray-600 shrink-0">
<div>성명: <span id="modal-name" class="text-gray-900"></span></div>
<div>사번: <span id="modal-member-id" class="text-gray-900"></span></div>
<div>부서명: <span id="modal-dept" class="text-gray-900"></span></div>
</div>
<div class="flex-1 overflow-auto p-4 bg-gray-50/50">
<table class="w-full text-left border-collapse bg-white border border-gray-200">
<thead class="bg-gray-50 border-b border-gray-200 text-gray-500 font-bold">
<tr>
<th class="p-3 text-center w-12">NO</th>
<th class="p-3">교육과정명</th>
<th class="p-3 w-32">학습시간</th>
<th class="p-3 w-32">진도율</th>
<th class="p-3 w-32">학습완료일</th>
<th class="p-3 w-28 text-center">수료구분</th>
</tr>
</thead>
<tbody id="detail-grid-body" class="divide-y divide-gray-100 text-sm">
<!-- AJAX JS INJECTION -->
</tbody>
</table>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-100 flex justify-end shrink-0">
<button onclick="closeDetailModal()"
class="px-6 py-2 bg-gray-500 text-white rounded-lg font-bold shadow-lg">닫기</button>
</div>
</div>
</div>
<script>
function downloadExcel() {
const form = document.getElementById('searchForm');
const urlParams = new URLSearchParams(new FormData(form)).toString();
window.location.href = '../bbs/legal_edu_excel.php?' + urlParams;
}
function openDetailModal(sys_comp_code, member_id, name, dept) {
document.getElementById('modal-name').textContent = name;
document.getElementById('modal-member-id').textContent = member_id;
document.getElementById('modal-dept').textContent = dept;
document.getElementById('detail-modal').classList.remove('hidden');
const gridBody = document.getElementById('detail-grid-body');
gridBody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-gray-500">로딩 중...</td></tr>';
const year = document.getElementById('year').value;
const requestUrl = `../bbs/get_legal_edu_detail.php?sys_comp_code=${encodeURIComponent(sys_comp_code)}&member_id=${encodeURIComponent(member_id)}&year=${encodeURIComponent(year)}`;
console.log('[G2] requestUrl', requestUrl, { sys_comp_code, member_id, year });
fetch(requestUrl)
.then(res => res.text())
.then(text => {
console.log('[G2] raw response', text);
let data;
try {
data = JSON.parse(text);
} catch (e) {
console.error('[G2] JSON parse error', e, text);
gridBody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-red-500">JSON 파싱 오류 발생했습니다.</td></tr>';
return;
}
gridBody.innerHTML = '';
if (!data.success || !data.items || data.items.length === 0) {
gridBody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-gray-400">학습 내역이 없습니다.</td></tr>';
return;
}
data.items.forEach((it, idx) => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50 transition';
const stHtml = it.comp_status === '수료'
? `<span class="px-3 py-1 bg-green-50 text-green-600 border border-green-100 rounded-full text-[11px] font-bold"><i class="fa-solid fa-check-circle mr-1"></i>수료</span>`
: `<span class="px-3 py-1 bg-red-50 text-red-600 border border-red-100 rounded-full text-[11px] font-bold"><i class="fa-solid fa-circle-xmark mr-1"></i>미수료</span>`;
// progress_rate에서 숫자만 추출
let progressValue = 0;
if (it.progress_rate) {
const match = it.progress_rate.toString().match(/\d+/);
progressValue = match ? parseInt(match[0]) : 0;
}
progressValue = Math.min(100, Math.max(0, progressValue));
tr.innerHTML = `
<td class="p-3 text-center text-gray-400">${idx + 1}</td>
<td class="p-3 font-bold text-gray-800">${escapeHtml(it.title)}</td>
<td class="p-3 text-gray-600">${escapeHtml(it.learn_time)}</td>
<td class="p-3">
<div class="flex items-center space-x-2">
<div class="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div class="bg-teal-500 h-full" style="width: ${progressValue}%;"></div>
</div>
<span class="text-[10px] font-bold text-teal-600">${progressValue}%</span>
</div>
</td>
<td class="p-3 text-gray-600">${escapeHtml(it.completion_date) || '-'}</td>
<td class="p-3 text-center">${stHtml}</td>
`;
gridBody.appendChild(tr);
});
})
.catch(err => {
console.error('데이터 로드 오류:', err);
gridBody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-red-500">데이터를 불러오는 중 오류가 발생했습니다.</td></tr>';
});
}
function closeDetailModal() {
document.getElementById('detail-modal').classList.add('hidden');
}
function sendIncompleteNotification() {
const incompleteCount = <?php echo $incomplete_qty; ?>;
if (incompleteCount === 0) {
alert('미수료자가 없습니다.');
return;
}
if (!confirm(`<?php echo $message_name; ?> \n"위의 메시지로발송됩니다."\n미수료자 ${incompleteCount}명에게 알림을 발송하시겠습니까?`)) {
return;
}
// 현재 날짜 + 14일 계산
const today = new Date();
const endDate = new Date(today);
endDate.setDate(today.getDate() + 14);
const endDateStr = endDate.toISOString().split('T')[0];
// 알림 발송 요청
fetch('../bbs/notification_send.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
code: '100',
end_date: endDateStr,
action: 'send'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`미수료자 알림이 성공적으로 발송되었습니다.\n발송 대상: ${data.sent_count || incompleteCount}명`);
} else {
alert(`알림 발송에 실패했습니다: ${data.message || '알 수 없는 오류'}`);
}
})
.catch(err => {
console.error('알림 발송 오류:', err);
alert('알림 발송 중 오류가 발생했습니다.');
});
}
function downloadExcel() {
const params = new URLSearchParams({
comp: document.getElementById('comp').value || '',
year: document.getElementById('year').value || '',
dept: document.getElementById('dept').value || '',
name: document.getElementById('name').value || '',
comp_status: document.getElementById('comp_status').value || ''
});
window.location.href = `../bbs/legal_edu_excel.php?${params.toString()}`;
}
function escapeHtml(unsafe) {
return (unsafe || '').toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
</script>
</body>
</html>
+249
View File
@@ -0,0 +1,249 @@
<?php
require_once __DIR__ . '/../../bbs/auth.php';
edu_require_login();
include_once 'header.php';
require_once __DIR__ . '/../../bbs/db_conn.php';
$search_year = $_GET['year'] ?? date('Y');
$search_sys_comp = $_GET['sys_comp'] ?? '';
// 만약 사용자가 처음 페이지에 들어왔거나(GET값이 없음), '전체'를 누른 게 아니라면 초기값 설정
if (empty($search_sys_comp) && !isset($_GET['sys_comp'])) {
$search_sys_comp = $sys_comp_code;
}
$search_comp = $_GET['comp'] ?? '';
$search_dept = $_GET['dept'] ?? '';
$search_text = $_GET['search_text'] ?? '';
$search_quarter = $_GET['quarter'] ?? '';
$corp_list = [];
$rows = [];
try {
$pdo = db_conn();
// 법인 리스트 가져오기
try {
$stmt_corp = $pdo->query("CALL proc_get_code2_list('CO100')");
$corp_list = $stmt_corp->fetchAll(PDO::FETCH_ASSOC);
while ($stmt_corp->nextRowset()) {}
unset($stmt_corp);
} catch (Exception $eProc) {
$corp_list = [];
}
// 권한에 따른 법인 목록 제어
// LE10001: 전체권한 → 전체 법인 표시
// LE10002: 법인권한 → 본인 법인($sys_comp_code)만 표시, 검색값도 강제 고정
// 그 외: 법인 목록 비움
// 기준법인 초기값은 로그인한 사용자의 법인으로 설정
if ($auth_level === 'LE10002') {
$corp_list = array_values(array_filter($corp_list, fn($c) => $c['code'] === $sys_comp_code));
$search_sys_comp = $sys_comp_code;
$search_comp = $sys_comp_code;
} elseif ($auth_level !== 'LE10001') {
$corp_list = [];
}
// 프로시저 호출로 데이터 가져오기
$stmt = $pdo->prepare("CALL proc_get_learner_status(?, ?, ?, ?, ?, ?)");
$stmt->execute([$search_year, $search_sys_comp, $search_comp, $search_dept, $search_text, $search_quarter]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($stmt->nextRowset()) {}
unset($stmt);
} catch (Exception $e) {
$db_error = $e->getMessage();
}
?>
<main class="max-w-[1600px] mx-auto p-6">
<header class="mb-6">
<h2 class="text-2xl font-bold text-gray-800 italic">학습자관리</h2>
</header>
<section class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm mb-6">
<form id="searchForm" method="GET" action="member_list.php"
class="grid grid-cols-1 md:grid-cols-6 gap-4">
<div>
<label class="block text-xs font-bold text-gray-500 mb-2">기준년도</label>
<input type="text" id="year" name="year" value="<?= htmlspecialchars($search_year) ?>" placeholder="YYYY"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div>
<label class="block text-xs font-bold text-gray-500 mb-2">기준법인</label>
<?php if ($auth_level === 'LE10002'): ?>
<?php $fixed_corp_name = !empty($corp_list) ? htmlspecialchars($corp_list[0]['name']) : htmlspecialchars($sys_comp_code); ?>
<input type="hidden" name="sys_comp" value="<?= htmlspecialchars($sys_comp_code) ?>">
<input type="text" value="<?= $fixed_corp_name ?>" readonly
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-100 text-gray-600 cursor-not-allowed">
<?php else: ?>
<select id="sys_comp" name="sys_comp" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<?php foreach ($corp_list as $corp): ?>
<option value="<?= htmlspecialchars($corp['code']) ?>" <?= $search_sys_comp === $corp['code'] ? 'selected' : '' ?>>
<?= htmlspecialchars($corp['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</div>
<div>
<label class="block text-xs font-bold text-gray-500 mb-2">소속회사</label>
<?php if ($auth_level === 'LE10002'): ?>
<input type="hidden" name="comp" value="<?= htmlspecialchars($sys_comp_code) ?>">
<input type="text" value="<?= $fixed_corp_name ?>" readonly
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-100 text-gray-600 cursor-not-allowed">
<?php else: ?>
<select id="comp" name="comp" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<?php foreach ($corp_list as $corp): ?>
<option value="<?= htmlspecialchars($corp['code']) ?>" <?= $search_comp === $corp['code'] ? 'selected' : '' ?>>
<?= htmlspecialchars($corp['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</div>
<div>
<label class="block text-xs font-bold text-gray-500 mb-2">부서명</label>
<input type="text" id="dept" name="dept" value="<?= htmlspecialchars($search_dept) ?>"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div>
<label class="block text-xs font-bold text-gray-500 mb-2">분기</label>
<select id="quarter" name="quarter" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<option value="CA200Q01" <?= $search_quarter === 'CA200Q01' ? 'selected' : '' ?>>1분기</option>
<option value="CA200Q02" <?= $search_quarter === 'CA200Q02' ? 'selected' : '' ?>>2분기</option>
<option value="CA200Q03" <?= $search_quarter === 'CA200Q03' ? 'selected' : '' ?>>3분기</option>
<option value="CA200Q04" <?= $search_quarter === 'CA200Q04' ? 'selected' : '' ?>>4분기</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-500 mb-2">성명/사번</label>
<div class="relative">
<input type="text" id="search_text" name="search_text" value="<?= htmlspecialchars($search_text) ?>" placeholder="성명 또는 사번"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 pl-8 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
<i class="fa-solid fa-magnifying-glass absolute left-3 top-3 text-gray-400 text-xs"></i>
</div>
</div>
</form>
</section>
<section class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden mb-12">
<div class="overflow-x-auto">
<?php if (isset($db_error)): ?>
<div class="p-4 bg-red-50 border border-red-200 rounded-md">
<p class="text-red-600 text-sm">데이터베이스 오류: <?= htmlspecialchars($db_error) ?></p>
</div>
<?php endif; ?>
<table class="w-full text-sm text-left">
<thead class="bg-gray-50 border-b border-gray-200 text-gray-500 font-bold">
<tr>
<th class="p-4">NO</th>
<th class="p-4">사번</th>
<th class="p-4">성명</th>
<th class="p-4">소속법인</th>
<th class="p-4">근무법인</th>
<th class="p-4">부서</th>
<th class="p-4 w-40">마이클래스</th>
<th class="p-4 w-40">법정의무교육</th>
<th class="p-4">학습시간</th>
<th class="p-4">최근 접속일</th>
<th class="p-4 text-center">학습 레벨</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if (count($rows) === 0): ?>
<tr>
<td colspan="11" class="p-8 text-center text-gray-400">데이터가 없습니다.</td>
</tr>
<?php else: ?>
<?php foreach ($rows as $row): ?>
<tr class="hover:bg-blue-50/30 transition">
<td class="p-4 text-gray-500 font-medium"><?= htmlspecialchars($row['no'] ?? '') ?></td>
<td class="p-4 text-gray-500 font-medium"><?= htmlspecialchars($row['member_id'] ?? '') ?></td>
<td class="p-4 font-bold text-gray-800"><?= htmlspecialchars($row['name'] ?? '') ?></td>
<td class="p-4"><?= htmlspecialchars($row['belong_name'] ?? '') ?></td>
<td class="p-4"><?= htmlspecialchars($row['working_name'] ?? '') ?></td>
<td class="p-4"><?= htmlspecialchars($row['dept_name'] ?? '') ?></td>
<td class="p-4">
<div class="flex items-center space-x-2">
<div class="progress-bar flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div class="progress-fill bg-blue-500 h-full" style="width: <?= (int)($row['progress_rate1'] ?? 0) ?>%;"></div>
</div>
<span class="text-[10px] font-bold text-blue-600"><?= (int)($row['progress_rate1'] ?? 0) ?>%</span>
</div>
</td>
<td class="p-4">
<div class="flex items-center space-x-2">
<div class="progress-bar flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div class="progress-fill bg-teal-500 h-full" style="width: <?= (int)($row['progress_rate2'] ?? 0) ?>%;"></div>
</div>
<span class="text-[10px] font-bold text-teal-600"><?= (int)($row['progress_rate2'] ?? 0) ?>%</span>
</div>
</td>
<td class="p-4 font-bold"><?= htmlspecialchars($row['all_tm'] ?? '') ?></td>
<td class="p-4 text-gray-400"><?= htmlspecialchars($row['last_login_time'] ?? '-') ?></td>
<td class="p-4 text-center">
<?php
$all_tm_text = $row['all_tm'] ?? '';
preg_match('/(\d+)시간/', $all_tm_text, $matches);
$total_hours = isset($matches[1]) ? (int)$matches[1] : 0;
$level = $total_hours >= 40 ? 'Master' : ($total_hours >= 20 ? 'Elite' : ($total_hours >= 8 ? 'Learner' : 'Rookie'));
$level_color = $level == 'Master' ? 'purple' : ($level == 'Elite' ? 'blue' : ($level == 'Learner' ? 'green' : 'gray'));
$levelClass = "bg-{$level_color}-100 text-{$level_color}-600";
$levelIcon = '';
switch ($level) {
case 'Master': $levelIcon = 'fa-crown'; break;
case 'Elite': $levelIcon = 'fa-star'; break;
case 'Learner': $levelIcon = 'fa-graduation-cap'; break;
case 'Rookie':
default: $levelIcon = 'fa-seedling'; break;
}
?>
<span class="px-2 py-1 <?= $levelClass ?> rounded-md text-[10px] font-bold uppercase">
<i class="fa-solid <?= $levelIcon ?> mr-1"></i><?= $level ?>
</span>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<section class="mt-6 flex flex-wrap gap-4 justify-center py-4 bg-white rounded-xl border border-dashed border-gray-300">
<div class="flex items-center text-xs font-medium text-gray-500">
<span class="w-3 h-3 rounded-full bg-gray-200 mr-2"></span> Rookie: 0-8시간
</div>
<div class="flex items-center text-xs font-medium text-gray-500">
<span class="w-3 h-3 rounded-full bg-green-200 mr-2"></span> Learner: 8-20시간
</div>
<div class="flex items-center text-xs font-medium text-gray-500">
<span class="w-3 h-3 rounded-full bg-blue-200 mr-2"></span> Elite: 20-40시간
</div>
<div class="flex items-center text-xs font-medium text-gray-500">
<span class="w-3 h-3 rounded-full bg-purple-200 mr-2"></span> Master: 40시간 이상
</div>
</section>
</main>
</body>
</html>
File diff suppressed because it is too large Load Diff