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
+146
View File
@@ -0,0 +1,146 @@
<?php
// 에러 출력 설정
ini_set('display_errors', 1);
error_reporting(E_ALL);
set_time_limit(0); // 12,650명 처리를 위해 스크립트 실행 시간 제한 해제
extract($_REQUEST);
$url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 120); // 데이터 양이 많으므로 2분으로 연장
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json"]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
die("CURL 에러: " . curl_error($ch));
}
curl_close($ch);
if ($httpCode !== 200) {
die("API 호출 실패 (HTTP 상태 코드: $httpCode)");
}
$data = json_decode($response, true);
if (!isset($data['data']) || !is_array($data['data'])) {
die("유효한 데이터가 없습니다.");
}
// DB 연결
require_once __DIR__ . '/../bbs/db_conn.php';
$conn = db_conn();
$user_list = $data['data'];
$total_count = count($user_list);
$success_count = 0;
$error_count = 0;
echo "<h3>인사 정보 동기화 시작 (PDO 방식)</h3>";
echo "총 대상 인원: {$total_count}명<br><hr>";
// 1. PDO Prepared Statement 준비 (위치 보유자 '?' 사용)
$sql = "CALL proc_edu_users_in(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$SET_injection_at = date('Y-m-d H:i:s');
foreach ($user_list as $row) {
// 데이터 정리 (null 처리)
$params = [
$row['member_id'] ?? '',
$row['sys_comp_code'] ?? '',
$row['name'] ?? '',
$row['dept_name'] ?? '',
$row['rank_name'] ?? '',
(!empty($row['join_date'])) ? $row['join_date'] : null,
$row['belong_comp'] ?? '',
$row['working_comp'] ?? '',
$row['intra_pw'] ?? '',
$row['auth_level'] ?? '',
(!empty($row['end_date'])) ? $row['end_date'] : null,
$row['intro_flag'] ?? '0',
$row['belong_comp_id'] ?? '',
$row['working_comp_id'] ?? '',
$row['created_at'] ?? date('Y-m-d H:i:s'),
$row['updated_at'] ?? date('Y-m-d H:i:s'),
$SET_injection_at // injection_at
];
// 2. 실행 (PDO는 execute에 배열을 바로 넣으면 바인딩됩니다)
try {
if ($stmt->execute($params)) {
$success_count++;
// 대량 데이터 처리 시 진행 상황 출력 (100명 단위)
if ($success_count % 1000 == 0) {
echo "현재 {$success_count}명 처리 중...<br>";
flush(); // 화면에 즉시 반영
}
} else {
$error_count++;
}
} catch (PDOException $e) {
$error_count++;
// 프로시저 내부 에러 로그는 테이블에 쌓이겠지만 PHP 에러도 확인용 출력
echo "에러(ID: " . ($row['member_id'] ?? 'unknown') . "): " . $e->getMessage() . "<br>";
}
}
// ---------------------------------------------------------
// 로그 파일 생성 및 저장 로직 (한글 깨짐 방지 적용)
// ---------------------------------------------------------
$log_dir = $_SERVER['DOCUMENT_ROOT'] . "/edu/log/";
$log_file_name = "users_info_batch_" . date('Y') . ".txt";
$log_path = $log_dir . $log_file_name;
// 폴더 생성
if (!is_dir($log_dir)) {
mkdir($log_dir, 0777, true);
}
// 로그 내용 구성
$new_log_entry = "주입일자= {$SET_injection_at} , 성공카운트 = {$success_count} , 실패카운트= {$error_count}" . PHP_EOL;
// UTF-8 BOM (한글 깨짐 방지용 식별코드)
$bom = "\xEF\xBB\xBF";
if (file_exists($log_path)) {
// 기존 내용을 읽어옴
$existing_content = file_get_contents($log_path);
// 기존 내용에 BOM이 이미 있다면 제거 (내용 결합 시 중복 방지)
$existing_content = str_replace($bom, '', $existing_content);
// [BOM] + [새 로그] + [기존 로그] 순서로 저장
file_put_contents($log_path, $bom . $new_log_entry . $existing_content);
} else {
// 파일이 처음 생성될 때 BOM과 함께 저장
file_put_contents($log_path, $bom . $new_log_entry);
}
// ---------------------------------------------------------
// ---------------------------------------------------------
echo "<hr>";
echo "<strong>처리 완료!</strong><br>";
echo "성공: {$success_count}건 / 실패: {$error_count}";
/*
해당파일 처리완료후 로그파일 생성
생성위치 /edu/log/ 폴더아래
users_info_batch_2026.txt 파일을 생성하여 저장한다
파일명 마지막은 현재년도가 들어간 파일명이다.
해당폴더에 파일이 없을 경우 생성
파일이 존재시
파일에 있는 로그내용 위에 새로운 로그내용을 추가한다.
추가될 내용은 주입일자= $SET_injection_at , 성공카운트 = $success_count , 실패카운트= $error_count
*/
?>
+81
View File
@@ -0,0 +1,81 @@
<?php
extract($_REQUEST);
/* https://baroncs.co.kr/edu/ajax/get_users_info.php
* https://baroncs.co.kr/edu/ajax/get_users_info.php?sys_comp_code=30
*
* $sys_comp_code 필수입력값. EduJsonAPI.php데이터 로직상에는 파라미터가 없을 시,
* 가족사 전체 통합인사정보를 가져와서 일정부분 부하가 예상된다.
* 때문에
* 기본적으로 최소한의 조건 sys_comp_code를 전달받아서
* 해당 회사 인원들에 대한 데이터만 업데이트하는 것으로 한다.
* ※필요 시, 조건에 따라서 가족사전인원 업데이트도 가능하게 할 수 있음
* (예시: 특정 시간대(24:00~05:00)에 접근시에는 전인원 업데이트)
* $member_id
*/
$sys_comp_code = $sys_comp_code==""?"":$sys_comp_code;
$member_id = $member_id==""?"":$member_id;
if($sys_comp_code==""){
echo "Parameter Fail : sys_comp_code is null";
exit;
}else{
if( $sys_comp_code=="10" ||
$sys_comp_code=="20" ||
$sys_comp_code=="30" ||
$sys_comp_code=="40" ||
$sys_comp_code=="50"
){
echo "Success :";
echo "<br>";
echo "sys_comp_code=".$sys_comp_code;
echo "<br>";
echo "member_id=".$member_id;
//exit;
}else{
echo "Parameter Fail : This is not allowed sys_comp_code";
exit;
}
}
//$url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php";
$url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php?sys_comp_code=".$sys_comp_code;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json"]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
die("CURL 에러: " . curl_error($ch));
}
curl_close($ch);
if ($httpCode !== 200) {
die("API 호출 실패 (HTTP 상태 코드: $httpCode)");
}
$data = json_decode($response, true);
//print_r($data);
echo "<br><br>";
echo "총인원=".count($data['data'])."";
echo "<br><br>";
$TEMP_array_data = $data['data'];
echo "<br><br>";
print_r($TEMP_array_data[80]);
echo "<br><br>";
print_r($TEMP_array_data);
echo "<br><br>";
?>
+326
View File
@@ -0,0 +1,326 @@
<?php
//=========================
//[마이페이지][시청중인,시청완료,저장한 콘텐츠][북마크] DB update,데이터 동적변환
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/edu/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching'));
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// 탭 카운트
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND is_active = '1'
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND cw.is_active = '1'
GROUP BY c.content_id
ORDER BY COALESCE(cw.favorited_at, cw.updated_at) DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li
class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>"
data-content-id="<?= mypage_h($item['content_id']) ?>"
>
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+321
View File
@@ -0,0 +1,321 @@
<?php
//=========================
// 콘텐츠리스트 AJAX
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching'));
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// 탭 카운트
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND is_active = '1'
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND cw.is_active = '1'
GROUP BY c.content_id
ORDER BY COALESCE(cw.favorited_at, cw.updated_at) DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li
class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>"
data-content-id="<?= mypage_h($item['content_id']) ?>"
>
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+321
View File
@@ -0,0 +1,321 @@
<?php
//=========================
// 콘텐츠리스트 AJAX
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/edu/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching'));
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// 탭 카운트
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND is_active = '1'
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND cw.is_active = '1'
GROUP BY c.content_id
ORDER BY COALESCE(cw.favorited_at, cw.updated_at) DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li
class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>"
data-content-id="<?= mypage_h($item['content_id']) ?>"
>
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,210 @@
<?php
//=========================
//나의 학습활동 AJAX
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$categoryBase = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$categoryBase[$row['base_code']] = $row['code_name'];
}
// 연도 목록
$stmtYear = $pdo->prepare("
SELECT DISTINCT YEAR(last_viewed_at) AS view_year
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND last_viewed_at IS NOT NULL
ORDER BY view_year DESC
");
$stmtYear->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$yearList = array_map(
static fn($v) => (int)$v,
array_filter($stmtYear->fetchAll(PDO::FETCH_COLUMN))
);
$selectedYear = isset($_GET['year']) ? (int)$_GET['year'] : 0;
if ($selectedYear <= 0) {
$selectedYear = !empty($yearList) ? (int)$yearList[0] : (int)date('Y');
}
// 내 총학습시간
$stmtMyTotal = $pdo->prepare("
SELECT COALESCE(SUM(watch_tm), 0) AS total_watch_tm
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND YEAR(last_viewed_at) = :stats_year
");
$stmtMyTotal->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':stats_year' => $selectedYear,
]);
$myTotalMinutes = (int)$stmtMyTotal->fetchColumn();
// 전체평균
$stmtAvg = $pdo->prepare("
SELECT COALESCE(AVG(x.user_total_watch_tm), 0) AS avg_total_watch_tm
FROM (
SELECT
member_id,
sys_comp_code,
SUM(watch_tm) AS user_total_watch_tm
FROM edu_learning_histories
WHERE YEAR(last_viewed_at) = :stats_year
GROUP BY member_id, sys_comp_code
) x
");
$stmtAvg->execute([':stats_year' => $selectedYear]);
$avgTotalMinutes = (int)round((float)$stmtAvg->fetchColumn());
// 온보딩 기준일
$stmtUser = $pdo->prepare("
SELECT join_date
FROM edu_users
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
LIMIT 1
");
$stmtUser->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$joinDate = $stmtUser->fetchColumn();
$onboardingDueDate = '';
$isOnboardingPeriod = 'N';
if (!empty($joinDate)) {
$onboardingDueDate = date('Y-m-d', strtotime($joinDate . ' +14 days'));
$isOnboardingPeriod = (date('Y-m-d') <= $onboardingDueDate) ? 'Y' : 'N';
}
// 선택연도 카테고리별 집계
$stmtCategory = $pdo->prepare("
SELECT
c.category_code,
COUNT(DISTINCT lh.content_id) AS learned_content_cnt,
COUNT(DISTINCT CASE WHEN lh.watch_tm >= lh.content_tm THEN lh.content_id END) AS completed_content_cnt,
COALESCE(SUM(lh.watch_tm), 0) AS total_watch_tm,
COALESCE(SUM(lh.content_tm), 0) AS total_content_tm
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND YEAR(lh.last_viewed_at) = :stats_year
GROUP BY c.category_code
");
$stmtCategory->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':stats_year' => $selectedYear,
]);
$aggMap = [];
foreach ($stmtCategory->fetchAll(PDO::FETCH_ASSOC) as $row) {
$aggMap[$row['category_code']] = $row;
}
// 카테고리별 전체 콘텐츠 수
$stmtTotalCnt = $pdo->prepare("
SELECT
category_code,
COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
GROUP BY category_code
");
$stmtTotalCnt->execute();
$totalCntMap = [];
foreach ($stmtTotalCnt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$totalCntMap[$row['category_code']] = (int)$row['total_cnt'];
}
$activityItems = [];
foreach ($categoryBase as $categoryCode => $categoryName) {
$agg = $aggMap[$categoryCode] ?? null;
$learnedContentCnt = (int)($agg['learned_content_cnt'] ?? 0);
$completedContentCnt = (int)($agg['completed_content_cnt'] ?? 0);
$totalWatchTm = (int)($agg['total_watch_tm'] ?? 0);
$totalContentTm = (int)($agg['total_content_tm'] ?? 0);
$masterTotalCnt = (int)($totalCntMap[$categoryCode] ?? 0);
// 1차안: gauge는 학습이력 기준 content_tm 합계가 있을 때 그 기준으로 계산
// schema상 edu_contents에 표준 duration 컬럼이 없어 완전한 "전체 기준 분모"는 별도 운영정책 확정 필요
$percent = ($totalContentTm > 0) ? (int)round(($totalWatchTm / $totalContentTm) * 100) : 0;
$percent = max(0, min(100, $percent));
$activityItems[] = [
'category_code' => $categoryCode,
'category_name' => $categoryName,
'learned_content_cnt' => $learnedContentCnt,
'completed_content_cnt' => $completedContentCnt,
'master_total_cnt' => $masterTotalCnt,
'total_watch_tm' => $totalWatchTm,
'total_content_tm' => $totalContentTm,
'percent' => $percent,
'onboarding_due_date' => ($categoryCode === 'CA10002') ? $onboardingDueDate : '',
'is_onboarding_period' => ($categoryCode === 'CA10002') ? $isOnboardingPeriod : 'N',
];
}
response_json([
'success' => true,
'year_list' => $yearList,
'selected_year' => $selectedYear,
'my_total_minutes' => $myTotalMinutes,
'avg_total_minutes' => $avgTotalMinutes,
'onboarding_due_date'=> $onboardingDueDate,
'is_onboarding_period' => $isOnboardingPeriod,
'activity_items' => $activityItems,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '학습활동 데이터를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,327 @@
<?php
//=========================
//나의 학습활동 AJAX
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$categoryBase = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$categoryBase[$row['base_code']] = $row['code_name'];
}
// 연도 목록
$stmtYear = $pdo->prepare("
SELECT DISTINCT YEAR(last_viewed_at) AS view_year
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND last_viewed_at IS NOT NULL
ORDER BY view_year DESC
");
$stmtYear->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$yearList = array_map(
static fn($v) => (int)$v,
array_filter($stmtYear->fetchAll(PDO::FETCH_COLUMN))
);
$selectedYear = isset($_GET['year']) ? (int)$_GET['year'] : 0;
if ($selectedYear <= 0) {
$selectedYear = !empty($yearList) ? (int)$yearList[0] : (int)date('Y');
}
// 내 총학습시간
$stmtMyTotal = $pdo->prepare("
SELECT COALESCE(SUM(watch_tm), 0) AS total_watch_tm
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND YEAR(last_viewed_at) = :stats_year
");
$stmtMyTotal->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':stats_year' => $selectedYear,
]);
$myTotalMinutes = (int)$stmtMyTotal->fetchColumn();
// 전체평균
$stmtAvg = $pdo->prepare("
SELECT COALESCE(AVG(x.user_total_watch_tm), 0) AS avg_total_watch_tm
FROM (
SELECT
member_id,
sys_comp_code,
SUM(watch_tm) AS user_total_watch_tm
FROM edu_learning_histories
WHERE YEAR(last_viewed_at) = :stats_year
GROUP BY member_id, sys_comp_code
) x
");
$stmtAvg->execute([':stats_year' => $selectedYear]);
$avgTotalMinutes = (int)round((float)$stmtAvg->fetchColumn());
// 온보딩 기준일
$stmtUser = $pdo->prepare("
SELECT join_date
FROM edu_users
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
LIMIT 1
");
$stmtUser->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$joinDate = $stmtUser->fetchColumn();
$onboardingDueDate = '';
$isOnboardingPeriod = 'N';
if (!empty($joinDate)) {
$onboardingDueDate = date('Y-m-d', strtotime($joinDate . ' +14 days'));
$isOnboardingPeriod = (date('Y-m-d') <= $onboardingDueDate) ? 'Y' : 'N';
}
// 선택연도 카테고리별 집계
$stmtCategory = $pdo->prepare("
SELECT
c.category_code,
COUNT(DISTINCT lh.content_id) AS learned_content_cnt,
COUNT(DISTINCT CASE WHEN lh.watch_tm >= lh.content_tm THEN lh.content_id END) AS completed_content_cnt,
COALESCE(SUM(lh.watch_tm), 0) AS total_watch_tm,
COALESCE(SUM(lh.content_tm), 0) AS total_content_tm
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND YEAR(lh.last_viewed_at) = :stats_year
GROUP BY c.category_code
");
$stmtCategory->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':stats_year' => $selectedYear,
]);
$aggMap = [];
foreach ($stmtCategory->fetchAll(PDO::FETCH_ASSOC) as $row) {
$aggMap[$row['category_code']] = $row;
}
/*
// 카테고리별 전체 콘텐츠 수
$stmtTotalCnt = $pdo->prepare("
SELECT
category_code,
COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
GROUP BY category_code
");
$stmtTotalCnt->execute();
$totalCntMap = [];
foreach ($stmtTotalCnt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$totalCntMap[$row['category_code']] = (int)$row['total_cnt'];
}
*/
// ---------------------------------------------------------
// 카테고리별 전체 콘텐츠 수 산정 : Start
// - 카테고리마다 total_cnt 산정 조건이 다를 수 있으므로
// 공통 GROUP BY 방식 대신, 카테고리별 분기 구조로 처리한다.
// ---------------------------------------------------------
$totalCntMap = [
'CA10001' => 0, // 마이클래스
'CA10002' => 0, // 온보딩
'CA10003' => 0, // 법정교육
'CA10004' => 0, // 리더십
'CA10005' => 0, // 인사이트
'CA10006' => 0, // 비즈트렌드
];
/**
* 현재 월을 기준으로 분기 코드 계산
* - 1~3월 : CA200Q01
* - 4~6월 : CA200Q02
* - 7~9월 : CA200Q03
* - 10~12월 : CA200Q04
*/
/*
$currentMonth = (int)date('n');
if ($currentMonth >= 1 && $currentMonth <= 3) {
$currentQuarterBaseCode = 'CA200Q01';
} elseif ($currentMonth >= 4 && $currentMonth <= 6) {
$currentQuarterBaseCode = 'CA200Q02';
} elseif ($currentMonth >= 7 && $currentMonth <= 9) {
$currentQuarterBaseCode = 'CA200Q03';
} else {
$currentQuarterBaseCode = 'CA200Q04';
}
*/
// ---------------------------------------------------------
// 1. 마이클래스(CA10001)
// - 현재 분기 기준 + 선택년도 기준으로 콘텐츠 수 산정
// - 조건:
// category_code = CA10001
// category_group = 현재분기 base_code
// goal_code 앞 4자리 = 선택년도
// ---------------------------------------------------------
$stmtMyClassCnt = $pdo->prepare("
SELECT COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
AND category_code = 'CA10001'
AND base_year = :selected_year
");
$stmtMyClassCnt->execute([
':selected_year' => (string)$selectedYear,
]);
$totalCntMap['CA10001'] = (int)$stmtMyClassCnt->fetchColumn();
// ---------------------------------------------------------
// 2. 온보딩(CA10002)
// - 현재는 기본 조건만 적용
// - 추후 카테고리별 별도 조건을 쉽게 추가할 수 있도록
// 분리된 쿼리 구조로 작성
// ---------------------------------------------------------
$stmtOnboardingCnt = $pdo->prepare("
SELECT COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
AND category_code = 'CA10002'
");
$stmtOnboardingCnt->execute();
$totalCntMap['CA10002'] = (int)$stmtOnboardingCnt->fetchColumn();
// ---------------------------------------------------------
// 3. 법정교육(CA10003)
// - 현재는 기본 조건만 적용
// - 추후 카테고리별 별도 조건을 쉽게 추가할 수 있도록
// 분리된 쿼리 구조로 작성
// ---------------------------------------------------------
$stmtLegalCnt = $pdo->prepare("
SELECT COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
AND category_code = 'CA10003'
AND base_year = :selected_year
");
$stmtLegalCnt->execute([
':selected_year' => (string)$selectedYear,
]);
$totalCntMap['CA10003'] = (int)$stmtLegalCnt->fetchColumn();
// ---------------------------------------------------------
// 4. 나머지 카테고리 공통 처리
// - 현재는 공통 조건으로 콘텐츠 수 산정
// - 필요 시 이후 각 카테고리도 개별 쿼리로 분리 가능
// ---------------------------------------------------------
$stmtEtcCnt = $pdo->prepare("
SELECT
category_code,
COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
AND category_code IN ('CA10004', 'CA10005', 'CA10006')
GROUP BY category_code
");
$stmtEtcCnt->execute();
foreach ($stmtEtcCnt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$totalCntMap[$row['category_code']] = (int)$row['total_cnt'];
}
// ---------------------------------------------------------
// 카테고리별 전체 콘텐츠 수 산정 : End
// ---------------------------------------------------------
$activityItems = [];
foreach ($categoryBase as $categoryCode => $categoryName) {
$agg = $aggMap[$categoryCode] ?? null;
$learnedContentCnt = (int)($agg['learned_content_cnt'] ?? 0);
$completedContentCnt = (int)($agg['completed_content_cnt'] ?? 0);
$totalWatchTm = (int)($agg['total_watch_tm'] ?? 0);
$totalContentTm = (int)($agg['total_content_tm'] ?? 0);
$masterTotalCnt = (int)($totalCntMap[$categoryCode] ?? 0);
// 1차안: gauge는 학습이력 기준 content_tm 합계가 있을 때 그 기준으로 계산
// schema상 edu_contents에 표준 duration 컬럼이 없어 완전한 "전체 기준 분모"는 별도 운영정책 확정 필요
$percent = ($totalContentTm > 0) ? (int)round(($totalWatchTm / $totalContentTm) * 100) : 0;
$percent = max(0, min(100, $percent));
$activityItems[] = [
'category_code' => $categoryCode,
'category_name' => $categoryName,
'learned_content_cnt' => $learnedContentCnt,
'completed_content_cnt' => $completedContentCnt,
'master_total_cnt' => $masterTotalCnt,
'total_watch_tm' => $totalWatchTm,
'total_content_tm' => $totalContentTm,
'percent' => $percent,
'onboarding_due_date' => ($categoryCode === 'CA10002') ? $onboardingDueDate : '',
'is_onboarding_period' => ($categoryCode === 'CA10002') ? $isOnboardingPeriod : 'N',
];
}
response_json([
'success' => true,
'year_list' => $yearList,
'selected_year' => $selectedYear,
'my_total_minutes' => $myTotalMinutes,
'avg_total_minutes' => $avgTotalMinutes,
'onboarding_due_date'=> $onboardingDueDate,
'is_onboarding_period' => $isOnboardingPeriod,
'activity_items' => $activityItems,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '학습활동 데이터를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,158 @@
<?php
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
/**
* JSON 응답 출력 함수
*/
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
/**
* 학습레벨 메타 반환 함수
*/
function mypage_level_meta(?string $level): array
{
$level = trim((string)$level);
$map = [
'MASTER' => [
'label' => 'Master',
'class' => 'master',
'icon' => '/edu/img/ico/ico_level_master.svg',
],
'ELITE' => [
'label' => 'Elite',
'class' => 'elite',
'icon' => '/edu/img/ico/ico_level_elite.svg',
],
'LEARNER' => [
'label' => 'Learner',
'class' => 'learner',
'icon' => '/edu/img/ico/ico_level_learner.svg',
],
'ROOKIE' => [
'label' => 'Rookie',
'class' => 'rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
],
];
$key = strtoupper($level);
return $map[$key] ?? [
'label' => 'Rookie',
'class' => 'rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
];
}
/**
* 총 학습시간 기준으로 학습레벨 계산
* - 40시간 이상: Master
* - 20 ~ 40시간: Elite
* - 8 ~ 20시간: Learner
* - 0 ~ 8시간: Rookie
*/
function calc_level_by_minutes(int $totalMinutes): string
{
if ($totalMinutes >= 2400) {
return 'MASTER';
}
if ($totalMinutes >= 1200) {
return 'ELITE';
}
if ($totalMinutes >= 480) {
return 'LEARNER';
}
return 'ROOKIE';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ★ 선택년도 수집
$selectedYear = trim((string)($_GET['selected_year'] ?? date('Y')));
if ($selectedYear === '' || !preg_match('/^\d{4}$/', $selectedYear)) {
$selectedYear = date('Y');
}
$levelCode = 'ROOKIE';
$totalMinutes = 0;
$statsYear = $selectedYear;
// ★ 선택년도 통계 우선 조회
$stmt = $pdo->prepare("
SELECT
stats_year,
learning_level,
total_minutes
FROM edu_yearly_learning_stats
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND stats_year = :selected_year
LIMIT 1
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$levelCode = (string)($row['learning_level'] ?? 'ROOKIE');
$totalMinutes = (int)($row['total_minutes'] ?? 0);
$statsYear = (string)($row['stats_year'] ?? $selectedYear);
} else {
// ★ 선택년도 통계가 없으면 학습이력 합계로 fallback
$stmtFallback = $pdo->prepare("
SELECT COALESCE(SUM(watch_tm), 0) AS total_minutes
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND YEAR(last_viewed_at) = :selected_year
");
$stmtFallback->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$totalMinutes = (int)$stmtFallback->fetchColumn();
$levelCode = calc_level_by_minutes($totalMinutes);
}
$levelMeta = mypage_level_meta($levelCode);
response_json([
'success' => true,
'selected_year' => $selectedYear,
'stats_year' => $statsYear,
'total_minutes' => $totalMinutes,
'learning_level' => $levelMeta['label'],
'level_class' => $levelMeta['class'],
'level_icon' => $levelMeta['icon'],
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '학습레벨 정보를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+110
View File
@@ -0,0 +1,110 @@
<?php
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$offerStatusMap = [
'OF10001' => '제안',
'OF10002' => '검토중',
'OF10003' => '게시완료',
'OF10004' => '게시불가',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'OF100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$offerStatusMap[$row['base_code']] = $row['code_name'];
}
$stmt = $pdo->prepare("
SELECT
co.offer_id,
co.status_code,
co.created_at
FROM edu_content_offer co
WHERE co.member_id = :member_id
AND co.sys_comp_code = :sys_comp_code
ORDER BY
CASE co.status_code
WHEN 'OF10002' THEN 0
WHEN 'OF10001' THEN 1
WHEN 'OF10003' THEN 2
WHEN 'OF10004' THEN 3
ELSE 9
END,
co.created_at DESC
LIMIT 20
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
if (!empty($rows)) {
foreach ($rows as $row) {
$statusCode = (string)($row['status_code'] ?? '');
$statusName = $offerStatusMap[$statusCode] ?? $statusCode;
$isConsider = ($statusCode === 'OF10002');
$createdAtDot = !empty($row['created_at']) ? date('y.m.d', strtotime($row['created_at'])) : '';
?>
<li class="<?= $isConsider ? 'consider' : '' ?>" data-offer-id="<?= h($row['offer_id'] ?? '') ?>">
<span class="suggest-date"><?= h($createdAtDot) ?></span>
<?php if ($isConsider): ?>
<span class="suggest-dots"></span>
<?php endif; ?>
<span class="suggest-state"><?= h($statusName) ?></span>
</li>
<?php
}
} else {
?>
<li>
<span class="suggest-date">-</span>
<span class="suggest-state">등록된 제안이 없습니다.</span>
</li>
<?php
}
$html = ob_get_clean();
response_json([
'success' => true,
'html' => $html,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '제안현황을 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,155 @@
<?php
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
/**
* JSON 응답 출력 함수
*/
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
/**
* HTML escape 함수
*/
function h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/**
* 카테고리 태그 클래스 반환
*/
function review_category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ★ 선택년도 파라미터 수집
$selectedYear = trim((string)($_GET['selected_year'] ?? date('Y')));
if ($selectedYear === '' || !preg_match('/^\d{4}$/', $selectedYear)) {
$selectedYear = date('Y');
}
// ★ 한줄소감 조회
// - 선택년도 기준
// - comment 값이 있는 이력만
// - 마이클래스(CA10001)만
// - 같은 content_id는 최신 시청이력 1건만
$sql = "
SELECT
lh.content_id,
lh.member_id,
lh.sys_comp_code,
lh.watch_tm,
lh.content_tm,
lh.last_viewed_at,
lh.completed_at,
lh.comment,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url
FROM edu_learning_histories lh
INNER JOIN (
SELECT
content_id,
MAX(last_viewed_at) AS max_last_viewed_at
FROM edu_learning_histories
WHERE member_id = :sub_member_id
AND sys_comp_code = :sub_sys_comp_code
AND YEAR(last_viewed_at) = :sub_selected_year
AND comment IS NOT NULL
AND TRIM(comment) <> ''
GROUP BY content_id
) latest
ON latest.content_id = lh.content_id
AND latest.max_last_viewed_at = lh.last_viewed_at
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
WHERE lh.member_id = :main_member_id
AND lh.sys_comp_code = :main_sys_comp_code
AND c.category_code = 'CA10001'
AND lh.comment IS NOT NULL
AND TRIM(lh.comment) <> ''
ORDER BY lh.last_viewed_at DESC
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':sub_member_id' => $memberId,
':sub_sys_comp_code' => $sysCompCode,
':sub_selected_year' => (int)$selectedYear,
':main_member_id' => $memberId,
':main_sys_comp_code' => $sysCompCode,
]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$totalCount = count($rows);
ob_start();
if (!empty($rows)) {
foreach ($rows as $row) {
$categoryCode = (string)($row['category_code'] ?? '');
$tagClass = review_category_tag_class($categoryCode);
// ★ 실제 한줄소감은 edu_learning_histories.comment 사용
$reviewText = trim((string)($row['comment'] ?? ''));
?>
<li class="review-item" data-content-id="<?= h($row['content_id'] ?? '') ?>">
<div class="review-head">
<span class="category-tag <?= h($tagClass) ?>">마이클래스</span>
<p class="review-text"><?= h($row['title'] ?? '') ?></p>
</div>
<div class="review-comment">
<span class="review-comment-text">
<?= h($reviewText) ?>
</span>
</div>
</li>
<?php
}//foreach
} else {
?>
<li class="review-empty">한줄소감이 없습니다.</li>
<?php
}
$html = ob_get_clean();
response_json([
'success' => true,
'selected_year' => $selectedYear,
'total_count' => $totalCount,
'html' => $html,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '한줄소감 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+83
View File
@@ -0,0 +1,83 @@
<?php
extract($_REQUEST);
/* https://baroncs.co.kr/edu/ajax/get_users_info.php
* https://baroncs.co.kr/edu/ajax/get_users_info.php?sys_comp_code=30
*
* $sys_comp_code 필수입력값. EduJsonAPI.php데이터 로직상에는 파라미터가 없을 시,
* 가족사 전체 통합인사정보를 가져와서 일정부분 부하가 예상된다.
* 때문에
* 기본적으로 최소한의 조건 sys_comp_code를 전달받아서
* 해당 회사 인원들에 대한 데이터만 업데이트하는 것으로 한다.
* ※필요 시, 조건에 따라서 가족사전인원 업데이트도 가능하게 할 수 있음
* (예시: 특정 시간대(24:00~05:00)에 접근시에는 전인원 업데이트)
* $member_id
*/
//배치작업 진행후 작업결과 로그 남길것
$sys_comp_code = $sys_comp_code==""?"":$sys_comp_code;
$member_id = $member_id==""?"":$member_id;
/*
if($sys_comp_code==""){
//echo "Parameter Fail : sys_comp_code is null";
//exit;
}else{
if( $sys_comp_code=="10" ||
$sys_comp_code=="20" ||
$sys_comp_code=="30" ||
$sys_comp_code=="40" ||
$sys_comp_code=="50"
){
echo "Success :";
echo "<br>";
echo "sys_comp_code=".$sys_comp_code;
echo "<br>";
echo "member_id=".$member_id;
//exit;
}else{
echo "Parameter Fail : This is not allowed sys_comp_code";
exit;
}
}
*/
$url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php";
//$url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php?sys_comp_code=".$sys_comp_code;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json"]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
die("CURL 에러: " . curl_error($ch));
}
curl_close($ch);
if ($httpCode !== 200) {
die("API 호출 실패 (HTTP 상태 코드: $httpCode)");
}
$data = json_decode($response, true);
//print_r($data);
echo "<br><br>";
echo "총인원=".count($data['data'])."";
echo "<br><br>";
$TEMP_array_data = $data['data'];
echo "<br><br>";
print_r($TEMP_array_data[80]);
echo "<br><br>";
print_r($TEMP_array_data);
echo "<br><br>";
?>
+431
View File
@@ -0,0 +1,431 @@
<?php
//require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// ---------------------------------------------------------
// TODO: 실제 로그인 세션 연동 후 교체
// ---------------------------------------------------------
$memberId = 'U001';
$sysCompCode = 'COMP01';
// ---------------------------------------------------------
// 디버그 설정
// true : 바인딩 적용된 SQL 출력 후 종료
// false : 정상 실행
// ---------------------------------------------------------
$DEBUG_SQL = false;
// ---------------------------------------------------------
// 요청 파라미터
// ---------------------------------------------------------
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 2;
$offset = ($page - 1) * $limit;
$category = trim($_GET['category'] ?? ''); // 예: CA200L01
$sort = trim($_GET['sort'] ?? 'view'); // view / latest / seen / unseen
// ---------------------------------------------------------
// 기본 코드맵 (DB코드 조회 실패 대비)
// ---------------------------------------------------------
$CATEGORY_MAP = [
'CA10004' => '리더십',
];
$SUBCATE_MAP = [
'CA200L01' => '리더십 시작하기',
'CA200L02' => '셀프 리더십',
'CA200L03' => '팀 리더십',
'CA200L04' => '실전조직 리더십',
'CA200L05' => '리더케이스탐구',
];
// ---------------------------------------------------------
// 공통 함수
// ---------------------------------------------------------
function h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function mapContentRow(array $row, array $catMap = [], array $subcateMap = []): array
{
$kwStr = $row['keywords'] ?? '';
$keywords = $kwStr !== '' ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = min(100, max(0, $gauge));
$raw = trim($row['content_url'] ?? '');
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim($row['thumbnail_url'] ?? '');
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
$categoryCode = $row['category_code'] ?? '';
$groupCode = $row['category_group'] ?? '';
return [
'id' => $row['content_id'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'category' => $catMap[$categoryCode] ?? $categoryCode,
'category_code' => $categoryCode,
'subcate' => $subcateMap[$groupCode] ?? $groupCode,
'bookmark' => !empty($row['is_bookmarked']),
'title' => $row['title'] ?? '',
'keywords' => $keywords,
'gauge' => $gauge,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'view_cnt' => (int)($row['view_cnt'] ?? 0),
];
}
/**
* 디버그용: 바인딩값을 SQL에 치환한 문자열 생성
* 주의: 실제 실행 SQL을 PDO가 제공하는 것은 아니고, 보기 쉽게 만든 디버그용 문자열임
*/
function buildDebugSql(string $sql, array $params): string
{
// :member_id 와 :member_id2 같은 이름 충돌 방지
uksort($params, function ($a, $b) {
return strlen((string)$b) <=> strlen((string)$a);
});
foreach ($params as $key => $value) {
if ($value === null) {
$replace = 'NULL';
} elseif (is_int($value) || is_float($value)) {
$replace = (string)$value;
} else {
$replace = "'" . str_replace("'", "''", (string)$value) . "'";
}
$sql = str_replace($key, $replace, $sql);
}
return $sql;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ---------------------------------------------------------
// edu_codes 에서 실제 코드맵 조회
// ---------------------------------------------------------
$stmtCode = $pdo->query("
SELECT group_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND group_code IN ('CA100', 'CA200')
");
if ($stmtCode) {
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $cr) {
$key = $cr['group_code'] . $cr['code'];
if ($cr['group_code'] === 'CA100') {
$CATEGORY_MAP[$key] = $cr['code_name'];
} elseif ($cr['group_code'] === 'CA200') {
$SUBCATE_MAP[$key] = $cr['code_name'];
}
}
}
// ---------------------------------------------------------
// 이 페이지는 리더십 대분류 전용
// 탭은 category_group 으로 필터링
// category 파라미터는 CA200L01 형태를 권장
// ---------------------------------------------------------
$categoryGroup = '';
if ($category !== '' && strtolower($category) !== 'all') {
if (preg_match('/^L\d{2}$/', $category)) {
$categoryGroup = 'CA200' . $category; // L01 -> CA200L01
} else {
$categoryGroup = $category; // 이미 CA200L01이면 그대로
}
}
// ---------------------------------------------------------
// 정렬 조건
// ---------------------------------------------------------
switch ($sort) {
case 'latest':
$orderBy = "
COALESCE(c.updated_at, c.created_at) DESC,
c.sort_order ASC,
c.content_id DESC
";
break;
case 'seen':
$orderBy = "
CASE WHEN lh.content_id IS NOT NULL THEN 0 ELSE 1 END ASC,
lh.last_viewed_at DESC,
c.content_id DESC
";
break;
case 'unseen':
$orderBy = "
CASE WHEN lh.content_id IS NULL THEN 0 ELSE 1 END ASC,
COALESCE(c.updated_at, c.created_at) DESC,
c.content_id DESC
";
break;
case 'view':
default:
$orderBy = "
COALESCE(vs.view_cnt, 0) DESC,
COALESCE(vs.total_all_tm, 0) DESC,
COALESCE(c.updated_at, c.created_at) DESC,
c.content_id DESC
";
break;
}
// ---------------------------------------------------------
// 기본 조건
// ---------------------------------------------------------
$where = [];
$bind = [];
$where[] = "(c.is_active = '1' OR c.is_active = 'Y')";
$where[] = "(c.start_date IS NULL OR c.start_date <= CURDATE())";
$where[] = "(c.end_date IS NULL OR c.end_date >= CURDATE())";
// 리더십 대분류 고정
$where[] = "c.category_code = 'CA10004'";
// 탭 필터
if ($categoryGroup !== '') {
$where[] = "c.category_group = :category_group";
$bind[':category_group'] = $categoryGroup;
}
$whereSql = implode("\n AND ", $where);
// ---------------------------------------------------------
// TOTAL COUNT
// ---------------------------------------------------------
$countSql = "
SELECT COUNT(*)
FROM edu_contents c
WHERE {$whereSql}
";
$stmtCount = $pdo->prepare($countSql);
foreach ($bind as $key => $value) {
$stmtCount->bindValue($key, $value, PDO::PARAM_STR);
}
if ($DEBUG_SQL) {
$countDebugSql = buildDebugSql($countSql, $bind);
echo '<pre>';
echo "[COUNT SQL]\n\n";
// echo htmlspecialchars($countDebugSql, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo $countDebugSql;
echo "\n\n------------------------------\n\n";
}
$stmtCount->execute();
$totalCount = (int)$stmtCount->fetchColumn();
// ---------------------------------------------------------
// 목록 조회
// ---------------------------------------------------------
$sql = "
SELECT
c.content_id,
c.category_code,
c.category_group,
c.title,
c.content_url,
c.thumbnail_url,
c.sort_order,
c.created_at,
c.updated_at,
lh.content_id AS lh_content_id,
lh.watch_tm,
lh.content_tm,
lh.last_viewed_at,
CASE
WHEN cw.content_id IS NOT NULL THEN 1
ELSE 0
END AS is_bookmarked,
COALESCE(vs.view_cnt, 0) AS view_cnt,
COALESCE(vs.total_all_tm, 0) AS total_all_tm,
GROUP_CONCAT(
DISTINCT ck.keyword_code
ORDER BY ck.keyword_code
SEPARATOR ','
) AS keywords
FROM edu_contents c
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = :member_id2
AND cw.sys_comp_code = :sys_comp_code2
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN (
SELECT
content_id,
COUNT(*) AS view_cnt,
COALESCE(SUM(all_tm), 0) AS total_all_tm
FROM edu_learning_histories
GROUP BY content_id
) vs
ON vs.content_id = c.content_id
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE {$whereSql}
GROUP BY c.content_id
ORDER BY {$orderBy}
LIMIT :limit OFFSET :offset
";
/*LIMIT :limit OFFSET :offset*/
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':member_id2', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code2', $sysCompCode, PDO::PARAM_STR);
foreach ($bind as $key => $value) {
$stmt->bindValue($key, $value, PDO::PARAM_STR);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
if ($DEBUG_SQL) {
$debugParams = [
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':member_id2' => $memberId,
':sys_comp_code2' => $sysCompCode,
':limit' => $limit,
':offset' => $offset,
];
if (!empty($bind)) {
$debugParams = array_merge($debugParams, $bind);
}
$debugSql = buildDebugSql($sql, $debugParams);
echo "[LIST SQL]\n\n";
//echo htmlspecialchars($debugSql, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo $debugSql;
echo '</pre>';
}
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($DEBUG_SQL) {
print_r($rows);
exit;
}
// ---------------------------------------------------------
// HTML 조각 생성
// ---------------------------------------------------------
ob_start();
foreach ($rows as $row) {
$item = mapContentRow($row, $CATEGORY_MAP, $SUBCATE_MAP);
$contentId = h($item['id']);
$title = h($item['title']);
$thumb = h($item['thumbnail']);
$categoryNm= h($item['category']);
$subcateNm = h($item['subcate']);
$checked = $item['bookmark'] ? ' checked' : '';
$viewUrl = "/edu/video_view.php?content_id=" . rawurlencode($item['id']);
?>
<li class="video-item">
<a href="<?= h($viewUrl) ?>" class="card-link">
<label class="bookmark" for="like_chk_<?= $contentId ?>" onclick="event.preventDefault(); event.stopPropagation();">
<input type="checkbox" id="like_chk_<?= $contentId ?>" title="좋아요"<?= $checked ?> />
</label>
<div class="item-thumb">
<?php if ($thumb !== '') { ?>
<img src="<?= $thumb ?>" alt="<?= $title ?>" loading="lazy" />
<?php } else { ?>
<img src="/edu/img/video/no-image.png" alt="<?= $title ?>" loading="lazy" />
<?php } ?>
</div>
<div class="item-info">
<strong class="item-title"><?= $title ?></strong>
<span class="item-desc"><?= $subcateNm !== '' ? $subcateNm : $categoryNm ?></span>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
echo json_encode([
'success' => true,
'html' => $html,
'total_count' => $totalCount
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_video_list.php] ' . $e->getMessage());
echo json_encode([
'success' => false,
'html' => '',
'total_count' => 0
], JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,320 @@
<?php
//=========================
//콘텐츠리스트 AJAX
//기존 참고 파일처럼 html 조각을 반환하는 방식으로 잡았습니다.
//현재 참고 구현도 코드맵/진행률/북마크/키워드 배열 구조를 그렇게 처리하고 있습니다.
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/edu/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching')); // watching / completed / saved
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// 탭 카운트
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND (is_active = '1' OR is_active = 'Y')
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
GROUP BY c.content_id
ORDER BY cw.favorited_at DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>">
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,323 @@
<?php
//=========================
//콘텐츠리스트 AJAX
//기존 참고 파일처럼 html 조각을 반환하는 방식으로 잡았습니다.
//현재 참고 구현도 코드맵/진행률/북마크/키워드 배열 구조를 그렇게 처리하고 있습니다.
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/edu/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching')); // watching / completed / saved
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// 탭 카운트
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND (is_active = '1' OR is_active = 'Y')
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
GROUP BY c.content_id
ORDER BY cw.favorited_at DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li
class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>"
data-content-id="<?= mypage_h($item['content_id']) ?>"
>
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,351 @@
<?php
//=========================
// 콘텐츠리스트 AJAX
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
//$memberId = 'U004';
$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/edu/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching'));
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
// ★ 선택년도 수집
$selectedYear = trim((string)($_GET['selected_year'] ?? date('Y')));
if ($selectedYear === '' || !preg_match('/^\d{4}$/', $selectedYear)) {
$selectedYear = date('Y');
}
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// =========================
// 탭 카운트 (선택년도 기준)
// =========================
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
AND YEAR(last_viewed_at) = :selected_year
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
AND YEAR(last_viewed_at) = :selected_year
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
// ★ 저장한 콘텐츠도 선택년도에 따라 변동되도록
// 해당 연도 학습이력이 있는 저장 콘텐츠만 count
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(DISTINCT cw.content_id)
FROM edu_content_wishlist cw
INNER JOIN edu_learning_histories lh
ON lh.content_id = cw.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND cw.is_active = '1'
AND YEAR(lh.last_viewed_at) = :selected_year
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
// =========================
// 탭별 메인 쿼리
// =========================
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords,
MAX(lh.last_viewed_at) AS sort_last_viewed_at
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
INNER JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND cw.is_active = '1'
AND YEAR(lh.last_viewed_at) = :selected_year
GROUP BY c.content_id
ORDER BY sort_last_viewed_at DESC, COALESCE(cw.favorited_at, cw.updated_at) DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
AND YEAR(lh.last_viewed_at) = :selected_year
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
AND YEAR(lh.last_viewed_at) = :selected_year
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':selected_year', (int)$selectedYear, PDO::PARAM_INT);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li
class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>"
data-content-id="<?= mypage_h($item['content_id']) ?>"
>
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'selected_year' => $selectedYear,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+138
View File
@@ -0,0 +1,138 @@
<?php
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '1');
error_reporting(E_ALL);
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
/**
* JSON 응답 출력 함수
*/
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$typeCode = trim((string)($_POST['type_code'] ?? ''));
$statusCode = trim((string)($_POST['status_code'] ?? ''));
$referenceUrl = trim((string)($_POST['reference_url'] ?? ''));
$reason = trim((string)($_POST['reason'] ?? ''));
//제안자는 URL(reference_url)과 제안이유(reason)만 입력한다.
//나머지 입력이 필요한 컬럼은 관리자가 입력하게 된다.(type_code,title)
// ★ 기본값 보정
if ($typeCode === '') {
$typeCode = '';
}
if ($statusCode === '') {
$statusCode = 'OF10002';
}
// ★ 필수값 검증
if ($referenceUrl === '') {
response_json([
'success' => false,
'message' => 'URL을 입력해주세요.'
]);
}
if ($reason === '') {
response_json([
'success' => false,
'message' => '추천이유를 입력해주세요.'
]);
}
// ★ URL 형식 검증
if (!filter_var($referenceUrl, FILTER_VALIDATE_URL)) {
response_json([
'success' => false,
'message' => '올바른 URL 형식이 아닙니다.'
]);
}
// ★ 해당 년월(YYYYMM) 기준 offer_id 생성
// 예: 202603-001, 202603-002
$offerYm = date('Ym');
// ★ 같은 년월 prefix를 가진 마지막 offer_id 조회
$stmtSeq = $pdo->prepare("
SELECT offer_id
FROM edu_content_offer
WHERE offer_id LIKE :offer_prefix
ORDER BY offer_id DESC
LIMIT 1
");
$stmtSeq->execute([
':offer_prefix' => $offerYm . '-%',
]);
$lastOfferId = (string)$stmtSeq->fetchColumn();
$nextSeq = 1;
// ★ 마지막 번호가 있으면 뒤 3자리 숫자를 추출해서 +1
if ($lastOfferId !== '') {
$lastSeq = (int)substr($lastOfferId, -3);
$nextSeq = $lastSeq + 1;
}
$offerId = $offerYm . '-' . str_pad((string)$nextSeq, 3, '0', STR_PAD_LEFT);
// ★ 저장
$stmt = $pdo->prepare("
INSERT INTO edu_content_offer (
offer_id,
member_id,
sys_comp_code,
reference_url,
reason,
status_code,
created_at,
updated_at
) VALUES (
:offer_id,
:member_id,
:sys_comp_code,
:reference_url,
:reason,
:status_code,
NOW(),
NOW()
)
");
$stmt->execute([
':offer_id' => $offerId,
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':reference_url' => $referenceUrl,
':reason' => $reason,
':status_code' => $statusCode,
]);
response_json([
'success' => true,
'message' => '제안이 정상적으로 등록되었습니다.',
'offer_id' => $offerId,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '제안 등록 중 오류가 발생했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,321 @@
<?php
//=========================
// 콘텐츠리스트 AJAX
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/edu/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching'));
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// 탭 카운트
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND is_active = '1'
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND cw.is_active = '1'
GROUP BY c.content_id
ORDER BY COALESCE(cw.favorited_at, cw.updated_at) DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li
class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>"
data-content-id="<?= mypage_h($item['content_id']) ?>"
>
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+117
View File
@@ -0,0 +1,117 @@
<?php
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
function response_json($data) {
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// 세션 (실제 환경 기준)
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode= $_SESSION['sys_comp_code'] ?? '';
// 테스트용 (필요시 사용)
// $memberId = 'U001';
// $sysCompCode = 'COMP01';
try {
if (!$memberId || !$sysCompCode) {
response_json([
'success' => false,
'message' => '로그인 정보가 없습니다.'
]);
}
if (!isset($_FILES['profile_image'])) {
response_json([
'success' => false,
'message' => '파일이 없습니다.'
]);
}
$file = $_FILES['profile_image'];
if ($file['error'] !== UPLOAD_ERR_OK) {
response_json([
'success' => false,
'message' => '업로드 오류 발생'
]);
}
// MIME 체크
$mime = mime_content_type($file['tmp_name']);
if (!in_array($mime, ['image/jpeg', 'image/png'])) {
response_json([
'success' => false,
'message' => 'jpg, png 파일만 업로드 가능합니다.'
]);
}
// 이미지 생성
if ($mime === 'image/jpeg') {
$srcImage = imagecreatefromjpeg($file['tmp_name']);
} else {
$srcImage = imagecreatefrompng($file['tmp_name']);
}
if (!$srcImage) {
response_json([
'success' => false,
'message' => '이미지 처리 실패'
]);
}
// 원본 사이즈
$width = imagesx($srcImage);
$height = imagesy($srcImage);
// 새 이미지 생성 (png)
$newImage = imagecreatetruecolor($width, $height);
// 투명 배경 처리
imagealphablending($newImage, false);
imagesavealpha($newImage, true);
$transparent = imagecolorallocatealpha($newImage, 0, 0, 0, 127);
imagefilledrectangle($newImage, 0, 0, $width, $height, $transparent);
// 복사
imagecopy($newImage, $srcImage, 0, 0, 0, 0, $width, $height);
// 저장 경로
$fileName = $memberId . '_' . $sysCompCode . '.png';
$savePath = __DIR__ . '/../img/profile/' . $fileName;
$saveUrl = '/edu/img/profile/' . $fileName;
// 저장 (덮어쓰기)
if (!imagepng($newImage, $savePath)) {
response_json([
'success' => false,
'message' => '파일 저장 실패'
]);
}
// 메모리 해제
imagedestroy($srcImage);
imagedestroy($newImage);
response_json([
'success' => true,
'image_url' => $saveUrl
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '프로필 업로드 중 오류가 발생했습니다.',
'error' => $e->getMessage()
]);
}
+102
View File
@@ -0,0 +1,102 @@
<?php
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// JSON 입력 받기
$raw = file_get_contents('php://input');
$req = json_decode($raw, true);
$contentId = trim((string)($req['content_id'] ?? ''));
$tab = trim((string)($req['tab'] ?? ''));
if ($contentId === '') {
response_json([
'success' => false,
'message' => 'content_id가 없습니다.'
]);
}
// 탭별 삭제 처리
if ($tab === 'watching') {
// 시청중 목록 제거: 학습이력 삭제 대신
// "마이페이지에서 숨김" 정책이 아직 없으면 임시로 위시리스트 제거만 처리하거나
// 별도 정책 확정 필요
$stmt = $pdo->prepare("
DELETE FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND content_id = :content_id
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':content_id' => $contentId,
]);
} elseif ($tab === 'completed') {
// 시청완료 목록 제거
$stmt = $pdo->prepare("
DELETE FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND content_id = :content_id
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':content_id' => $contentId,
]);
} elseif ($tab === 'saved') {
// 저장한 콘텐츠 제거
$stmt = $pdo->prepare("
DELETE FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND content_id = :content_id
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':content_id' => $contentId,
]);
} else {
response_json([
'success' => false,
'message' => '유효하지 않은 탭입니다.'
]);
}
response_json([
'success' => true,
'content_id' => $contentId,
'tab' => $tab
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '삭제 처리 중 오류가 발생했습니다.',
'error' => $e->getMessage()
]);
}
+394
View File
@@ -0,0 +1,394 @@
<?php
//=========================
//나의 학습활동 AJAX
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$categoryBase = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$categoryBase[$row['base_code']] = $row['code_name'];
}
// 연도 목록
$stmtYear = $pdo->prepare("
SELECT DISTINCT YEAR(last_viewed_at) AS view_year
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND last_viewed_at IS NOT NULL
ORDER BY view_year DESC
");
$stmtYear->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$yearList = array_map(
static fn($v) => (int)$v,
array_filter($stmtYear->fetchAll(PDO::FETCH_COLUMN))
);
$currentYear = (int)date('Y');
if (empty($yearList)) {
// 리스트가 완전히 비어있다면 현재 연도 한 개만 추가
$yearList = [$currentYear];
}
$selectedYear = isset($_GET['year']) ? (int)$_GET['year'] : 0;
if ($selectedYear <= 0) {
$selectedYear = !empty($yearList) ? (int)$yearList[0] : (int)date('Y');
}
// 내 총학습시간
$stmtMyTotal = $pdo->prepare("
SELECT COALESCE(SUM(watch_tm), 0) AS total_watch_tm
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND YEAR(last_viewed_at) = :stats_year
");
$stmtMyTotal->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':stats_year' => $selectedYear,
]);
$myTotalMinutes = (int)$stmtMyTotal->fetchColumn();
// 전체평균
$stmtAvg = $pdo->prepare("
SELECT COALESCE(AVG(x.user_total_watch_tm), 0) AS avg_total_watch_tm
FROM (
SELECT
member_id,
sys_comp_code,
SUM(watch_tm) AS user_total_watch_tm
FROM edu_learning_histories
WHERE YEAR(last_viewed_at) = :stats_year
GROUP BY member_id, sys_comp_code
) x
");
$stmtAvg->execute([':stats_year' => $selectedYear]);
$avgTotalMinutes = (int)round((float)$stmtAvg->fetchColumn());
// 온보딩 기준일
$stmtUser = $pdo->prepare("
SELECT join_date
FROM edu_users
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
LIMIT 1
");
$stmtUser->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$joinDate = $stmtUser->fetchColumn();
$onboardingDueDate = '';
$isOnboardingPeriod = 'N';
if (!empty($joinDate)) {
$onboardingDueDate = date('Y-m-d', strtotime($joinDate . ' +14 days'));
$isOnboardingPeriod = (date('Y-m-d') <= $onboardingDueDate) ? 'Y' : 'N';
}
/**
* 현재 월을 기준으로 분기 코드 계산
* - 1~3월 : CA200Q01
* - 4~6월 : CA200Q02
* - 7~9월 : CA200Q03
* - 10~12월 : CA200Q04
*/
$currentMonth = (int)date('n');
if ($currentMonth >= 1 && $currentMonth <= 3) {
$currentQuarterBaseCode = 'CA200Q01';
} elseif ($currentMonth >= 4 && $currentMonth <= 6) {
$currentQuarterBaseCode = 'CA200Q02';
} elseif ($currentMonth >= 7 && $currentMonth <= 9) {
$currentQuarterBaseCode = 'CA200Q03';
} else {
$currentQuarterBaseCode = 'CA200Q04';
}
// --fn_get_progress_rate(:sys_comp_code, :stats_year1, :member_id, c.category_code, '') AS category_percent,
if($memberId=="test"){
}else{
// 선택연도 카테고리별 집계
$stmtCategory = $pdo->prepare("
SELECT
c.category_code,
CASE
WHEN c.category_code = 'CA10001' THEN
fn_get_progress_rate(:sys_comp_code_01, :stats_year_01, :member_id_01, c.category_code, :quarter_code_01)
ELSE
fn_get_progress_rate(:sys_comp_code_02, :stats_year_02, :member_id_02, c.category_code, '')
END AS category_percent,
COUNT(DISTINCT lh.content_id) AS learned_content_cnt,
COUNT(DISTINCT CASE WHEN lh.watch_tm >= lh.content_tm THEN lh.content_id END) AS completed_content_cnt,
COALESCE(SUM(lh.watch_tm), 0) AS total_watch_tm,
COALESCE(SUM(lh.content_tm), 0) AS total_content_tm
,FLOOR(COALESCE(SUM(lh.watch_tm), 0)/60) AS total_watch_tm_min
FROM edu_learning_histories lh
INNER JOIN edu_contents c ON c.content_id = lh.content_id
WHERE lh.member_id = :member_id_03
AND lh.sys_comp_code = :sys_comp_code_03
AND YEAR(lh.last_viewed_at) = :stats_year_03
GROUP BY c.category_code
");
$stmtCategory->execute([
':sys_comp_code_01' => $sysCompCode,
':stats_year_01' => $selectedYear,
':member_id_01' => $memberId,
':quarter_code_01' => $currentQuarterBaseCode,
':sys_comp_code_02' => $sysCompCode,
':stats_year_02' => $selectedYear,
':member_id_02' => $memberId,
':member_id_03' => $memberId,
':sys_comp_code_03' => $sysCompCode,
':stats_year_03' => $selectedYear,
]);
}
// $stmtCategory = $pdo->prepare("
// SELECT
// c.category_code,
// COUNT(DISTINCT lh.content_id) AS learned_content_cnt,
// COUNT(DISTINCT CASE WHEN lh.watch_tm >= lh.content_tm THEN lh.content_id END) AS completed_content_cnt,
// COALESCE(SUM(lh.watch_tm), 0) AS total_watch_tm,
// COALESCE(SUM(lh.content_tm), 0) AS total_content_tm
// FROM edu_learning_histories lh
// INNER JOIN edu_contents c
// ON c.content_id = lh.content_id
// WHERE lh.member_id = :member_id
// AND lh.sys_comp_code = :sys_comp_code
// AND YEAR(lh.last_viewed_at) = :stats_year
// GROUP BY c.category_code
// ");
// $stmtCategory->execute([
// ':member_id' => $memberId,
// ':sys_comp_code' => $sysCompCode,
// ':stats_year' => $selectedYear,
// ]);
$aggMap = [];
foreach ($stmtCategory->fetchAll(PDO::FETCH_ASSOC) as $row) {
$aggMap[$row['category_code']] = $row;
}
/*
// 카테고리별 전체 콘텐츠 수
$stmtTotalCnt = $pdo->prepare("
SELECT
category_code,
COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
GROUP BY category_code
");
$stmtTotalCnt->execute();
$totalCntMap = [];
foreach ($stmtTotalCnt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$totalCntMap[$row['category_code']] = (int)$row['total_cnt'];
}
*/
// ---------------------------------------------------------
// 카테고리별 전체 콘텐츠 수 산정 : Start
// - 카테고리마다 total_cnt 산정 조건이 다를 수 있으므로
// 공통 GROUP BY 방식 대신, 카테고리별 분기 구조로 처리한다.
// ---------------------------------------------------------
$totalCntMap = [
'CA10001' => 0, // 마이클래스
'CA10002' => 0, // 온보딩
'CA10003' => 0, // 법정교육
'CA10004' => 0, // 리더십
'CA10005' => 0, // 인사이트
'CA10006' => 0, // 비즈트렌드
];
// ---------------------------------------------------------
// 1. 마이클래스(CA10001)
// - 현재 분기 기준 + 선택년도 기준으로 콘텐츠 수 산정
// - 조건:
// category_code = CA10001
// category_group = 현재분기 base_code
// goal_code 앞 4자리 = 선택년도
// ---------------------------------------------------------
$stmtMyClassCnt = $pdo->prepare("
SELECT COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
AND category_code = 'CA10001'
AND base_year = :selected_year
");
$stmtMyClassCnt->execute([
':selected_year' => (string)$selectedYear,
]);
$totalCntMap['CA10001'] = (int)$stmtMyClassCnt->fetchColumn();
// ---------------------------------------------------------
// 2. 온보딩(CA10002)
// - 현재는 기본 조건만 적용
// - 추후 카테고리별 별도 조건을 쉽게 추가할 수 있도록
// 분리된 쿼리 구조로 작성
// ---------------------------------------------------------
$stmtOnboardingCnt = $pdo->prepare("
SELECT COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
AND category_code = 'CA10002'
");
$stmtOnboardingCnt->execute();
$totalCntMap['CA10002'] = (int)$stmtOnboardingCnt->fetchColumn();
// ---------------------------------------------------------
// 3. 법정교육(CA10003)
// - 현재는 기본 조건만 적용
// - 추후 카테고리별 별도 조건을 쉽게 추가할 수 있도록
// 분리된 쿼리 구조로 작성
// ---------------------------------------------------------
$stmtLegalCnt = $pdo->prepare("
SELECT COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
AND category_code = 'CA10003'
AND base_year = :selected_year
");
$stmtLegalCnt->execute([
':selected_year' => (string)$selectedYear,
]);
$totalCntMap['CA10003'] = (int)$stmtLegalCnt->fetchColumn();
// ---------------------------------------------------------
// 4. 나머지 카테고리 공통 처리
// - 현재는 공통 조건으로 콘텐츠 수 산정
// - 필요 시 이후 각 카테고리도 개별 쿼리로 분리 가능
// ---------------------------------------------------------
$stmtEtcCnt = $pdo->prepare("
SELECT
category_code,
COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
AND category_code IN ('CA10004', 'CA10005', 'CA10006')
GROUP BY category_code
");
$stmtEtcCnt->execute();
foreach ($stmtEtcCnt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$totalCntMap[$row['category_code']] = (int)$row['total_cnt'];
}
// ---------------------------------------------------------
// 카테고리별 전체 콘텐츠 수 산정 : End
// ---------------------------------------------------------
$activityItems = [];
foreach ($categoryBase as $categoryCode => $categoryName) {
$agg = $aggMap[$categoryCode] ?? null;
$learnedContentCnt = (int)($agg['learned_content_cnt'] ?? 0);
$completedContentCnt = (int)($agg['completed_content_cnt'] ?? 0);
$totalWatchTm = (int)($agg['total_watch_tm'] ?? 0);
$percent = (int)($agg['category_percent'] ?? 0);
$totalContentTm = (int)($agg['total_content_tm'] ?? 0);
$masterTotalCnt = (int)($totalCntMap[$categoryCode] ?? 0);
// 1차안: gauge는 학습이력 기준 content_tm 합계가 있을 때 그 기준으로 계산
// schema상 edu_contents에 표준 duration 컬럼이 없어 완전한 "전체 기준 분모"는 별도 운영정책 확정 필요
//$percent = ($totalContentTm > 0) ? (int)round(($totalWatchTm / $totalContentTm) * 100) : 0;
//$percent = max(0, min(100, $percent));
$activityItems[] = [
'category_code' => $categoryCode,
'category_name' => $categoryName,
'learned_content_cnt' => $learnedContentCnt,
'completed_content_cnt' => $completedContentCnt,
'master_total_cnt' => $masterTotalCnt,
'total_watch_tm' => $totalWatchTm,
'total_content_tm' => $totalContentTm,
'percent' => $percent,
'onboarding_due_date' => ($categoryCode === 'CA10002') ? $onboardingDueDate : '',
'is_onboarding_period' => ($categoryCode === 'CA10002') ? $isOnboardingPeriod : 'N',
];
}
response_json([
'success' => true,
'year_list' => $yearList,
'selected_year' => $selectedYear,
'my_total_minutes' => $myTotalMinutes,
'avg_total_minutes' => $avgTotalMinutes,
'onboarding_due_date'=> $onboardingDueDate,
'is_onboarding_period' => $isOnboardingPeriod,
'activity_items' => $activityItems,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '학습활동 데이터를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,210 @@
<?php
//=========================
//나의 학습활동 AJAX
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$categoryBase = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$categoryBase[$row['base_code']] = $row['code_name'];
}
// 연도 목록
$stmtYear = $pdo->prepare("
SELECT DISTINCT YEAR(last_viewed_at) AS view_year
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND last_viewed_at IS NOT NULL
ORDER BY view_year DESC
");
$stmtYear->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$yearList = array_map(
static fn($v) => (int)$v,
array_filter($stmtYear->fetchAll(PDO::FETCH_COLUMN))
);
$selectedYear = isset($_GET['year']) ? (int)$_GET['year'] : 0;
if ($selectedYear <= 0) {
$selectedYear = !empty($yearList) ? (int)$yearList[0] : (int)date('Y');
}
// 내 총학습시간
$stmtMyTotal = $pdo->prepare("
SELECT COALESCE(SUM(watch_tm), 0) AS total_watch_tm
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND YEAR(last_viewed_at) = :stats_year
");
$stmtMyTotal->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':stats_year' => $selectedYear,
]);
$myTotalMinutes = (int)$stmtMyTotal->fetchColumn();
// 전체평균
$stmtAvg = $pdo->prepare("
SELECT COALESCE(AVG(x.user_total_watch_tm), 0) AS avg_total_watch_tm
FROM (
SELECT
member_id,
sys_comp_code,
SUM(watch_tm) AS user_total_watch_tm
FROM edu_learning_histories
WHERE YEAR(last_viewed_at) = :stats_year
GROUP BY member_id, sys_comp_code
) x
");
$stmtAvg->execute([':stats_year' => $selectedYear]);
$avgTotalMinutes = (int)round((float)$stmtAvg->fetchColumn());
// 온보딩 기준일
$stmtUser = $pdo->prepare("
SELECT join_date
FROM edu_users
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
LIMIT 1
");
$stmtUser->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$joinDate = $stmtUser->fetchColumn();
$onboardingDueDate = '';
$isOnboardingPeriod = 'N';
if (!empty($joinDate)) {
$onboardingDueDate = date('Y-m-d', strtotime($joinDate . ' +14 days'));
$isOnboardingPeriod = (date('Y-m-d') <= $onboardingDueDate) ? 'Y' : 'N';
}
// 선택연도 카테고리별 집계
$stmtCategory = $pdo->prepare("
SELECT
c.category_code,
COUNT(DISTINCT lh.content_id) AS learned_content_cnt,
COUNT(DISTINCT CASE WHEN lh.watch_tm >= lh.content_tm THEN lh.content_id END) AS completed_content_cnt,
COALESCE(SUM(lh.watch_tm), 0) AS total_watch_tm,
COALESCE(SUM(lh.content_tm), 0) AS total_content_tm
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND YEAR(lh.last_viewed_at) = :stats_year
GROUP BY c.category_code
");
$stmtCategory->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':stats_year' => $selectedYear,
]);
$aggMap = [];
foreach ($stmtCategory->fetchAll(PDO::FETCH_ASSOC) as $row) {
$aggMap[$row['category_code']] = $row;
}
// 카테고리별 전체 콘텐츠 수
$stmtTotalCnt = $pdo->prepare("
SELECT
category_code,
COUNT(*) AS total_cnt
FROM edu_contents
WHERE (is_active = '1' OR is_active = 'Y')
GROUP BY category_code
");
$stmtTotalCnt->execute();
$totalCntMap = [];
foreach ($stmtTotalCnt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$totalCntMap[$row['category_code']] = (int)$row['total_cnt'];
}
$activityItems = [];
foreach ($categoryBase as $categoryCode => $categoryName) {
$agg = $aggMap[$categoryCode] ?? null;
$learnedContentCnt = (int)($agg['learned_content_cnt'] ?? 0);
$completedContentCnt = (int)($agg['completed_content_cnt'] ?? 0);
$totalWatchTm = (int)($agg['total_watch_tm'] ?? 0);
$totalContentTm = (int)($agg['total_content_tm'] ?? 0);
$masterTotalCnt = (int)($totalCntMap[$categoryCode] ?? 0);
// 1차안: gauge는 학습이력 기준 content_tm 합계가 있을 때 그 기준으로 계산
// schema상 edu_contents에 표준 duration 컬럼이 없어 완전한 "전체 기준 분모"는 별도 운영정책 확정 필요
$percent = ($totalContentTm > 0) ? (int)round(($totalWatchTm / $totalContentTm) * 100) : 0;
$percent = max(0, min(100, $percent));
$activityItems[] = [
'category_code' => $categoryCode,
'category_name' => $categoryName,
'learned_content_cnt' => $learnedContentCnt,
'completed_content_cnt' => $completedContentCnt,
'master_total_cnt' => $masterTotalCnt,
'total_watch_tm' => $totalWatchTm,
'total_content_tm' => $totalContentTm,
'percent' => $percent,
'onboarding_due_date' => ($categoryCode === 'CA10002') ? $onboardingDueDate : '',
'is_onboarding_period' => ($categoryCode === 'CA10002') ? $isOnboardingPeriod : 'N',
];
}
response_json([
'success' => true,
'year_list' => $yearList,
'selected_year' => $selectedYear,
'my_total_minutes' => $myTotalMinutes,
'avg_total_minutes' => $avgTotalMinutes,
'onboarding_due_date'=> $onboardingDueDate,
'is_onboarding_period' => $isOnboardingPeriod,
'activity_items' => $activityItems,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '학습활동 데이터를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+165
View File
@@ -0,0 +1,165 @@
<?php
//[마이페이지][학습레벨] 데이터 동적변환
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
/**
* JSON 응답 출력 함수
*/
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
/**
* 학습레벨 메타 반환 함수
*/
function mypage_level_meta(?string $level): array
{
$level = trim((string)$level);
$map = [
'MASTER' => [
'label' => 'Master',
'class' => 'master',
'icon' => '/img/ico/ico_level_master.svg',
],
'ELITE' => [
'label' => 'Elite',
'class' => 'elite',
'icon' => '/img/ico/ico_level_elite.svg',
],
'LEARNER' => [
'label' => 'Learner',
'class' => 'learner',
'icon' => '/img/ico/ico_level_learner.svg',
],
'ROOKIE' => [
'label' => 'Rookie',
'class' => 'rookie',
'icon' => '/img/ico/ico_level_rookie.svg',
],
];
$key = strtoupper($level);
return $map[$key] ?? [
'label' => 'Rookie',
'class' => 'rookie',
'icon' => '/img/ico/ico_level_rookie.svg',
];
}
/**
* 총 학습시간 기준으로 학습레벨 계산
* - 40시간 이상: Master
* - 20 ~ 40시간: Elite
* - 8 ~ 20시간: Learner
* - 0 ~ 8시간: Rookie
*/
function calc_level_by_minutes(int $totalMinutes): string
{
if ($totalMinutes >= 2400) {
return 'MASTER';
}
if ($totalMinutes >= 1200) {
return 'ELITE';
}
if ($totalMinutes >= 480) {
return 'LEARNER';
}
return 'ROOKIE';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ★ 선택년도 수집
$selectedYear = trim((string)($_GET['selected_year'] ?? date('Y')));
if ($selectedYear === '' || !preg_match('/^\d{4}$/', $selectedYear)) {
$selectedYear = date('Y');
}
$levelCode = 'ROOKIE';
$totalMinutes = 0;
$statsYear = $selectedYear;
// ★ 선택년도 통계 우선 조회
$stmt = $pdo->prepare("
SELECT
stats_year,
learning_level,
total_minutes
FROM edu_yearly_learning_stats
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND stats_year = :selected_year
LIMIT 1
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$levelCode = (string)($row['learning_level'] ?? 'ROOKIE');
$totalMinutes = (int)($row['total_minutes'] ?? 0);
$statsYear = (string)($row['stats_year'] ?? $selectedYear);
} else {
// ★ 선택년도 통계가 없으면 학습이력 합계로 fallback
$stmtFallback = $pdo->prepare("
SELECT COALESCE(SUM(watch_tm), 0) AS total_minutes
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND YEAR(last_viewed_at) = :selected_year
");
$stmtFallback->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$totalMinutes = (int)$stmtFallback->fetchColumn();
$totalMinutes = floor($totalMinutes / 60);//초를 분으로
$levelCode = calc_level_by_minutes($totalMinutes);
}
$levelMeta = mypage_level_meta($levelCode);
response_json([
'success' => true,
'selected_year' => $selectedYear,
'stats_year' => $statsYear,
'total_minutes' => $totalMinutes,
'learning_level' => $levelMeta['label'],
'level_class' => $levelMeta['class'],
'level_icon' => $levelMeta['icon'],
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '학습레벨 정보를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+120
View File
@@ -0,0 +1,120 @@
<?php
//[마이페이지][제안하기][제안이력리스트] 데이터 동적변환
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$offerStatusMap = [
'OF10001' => '제안',
'OF10002' => '검토중',
'OF10003' => '게시완료',
'OF10004' => '게시불가',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'OF100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$offerStatusMap[$row['base_code']] = $row['code_name'];
}
$stmt = $pdo->prepare("
SELECT
co.offer_id,
co.status_code,
co.reason,
co.reason_return,
co.created_at
FROM edu_content_offer co
WHERE co.member_id = :member_id
AND co.sys_comp_code = :sys_comp_code
ORDER BY
CASE co.status_code
WHEN 'OF10002' THEN 0
WHEN 'OF10001' THEN 1
WHEN 'OF10003' THEN 2
WHEN 'OF10004' THEN 3
ELSE 9
END,
co.created_at DESC
LIMIT 20
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
if (!empty($rows)) {
foreach ($rows as $row) {
$statusCode = (string)($row['status_code'] ?? '');
$reason = (string)($row['reason'] ?? '');
$reason_return = (string)($row['reason_return'] ?? '');
$statusName = $offerStatusMap[$statusCode] ?? $statusCode;
$isConsider = ($statusCode === 'OF10002');
$createdAtDot = !empty($row['created_at']) ? date('y.m.d', strtotime($row['created_at'])) : '';
?>
<li style="cursor: pointer;" title="클릭 시 상세결과를 볼 수 있습니다." data-offer-date="<?= h($createdAtDot) ?>" data-offer-reason="<?= h($row['reason'] ?? '') ?>" data-offer-return="<?= h($row['reason_return'] ?? '') ?>" class="<?= $isConsider ? 'consider' : '' ?>" data-offer-id="<?= h($row['offer_id'] ?? '') ?>">
<span class="suggest-date"><?= h($createdAtDot) ?></span>
<?php if ($isConsider): ?>
<span class="suggest-dots"></span>
<?php endif; ?>
<span class="suggest-state"><?= h($statusName) ?></span>
</li>
<?php
}
} else {
?>
<li>
<span class="suggest-date">-</span>
<span class="suggest-state">등록된 제안이 없습니다.</span>
</li>
<?php
}
$html = ob_get_clean();
response_json([
'success' => true,
'html' => $html,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '제안현황을 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+161
View File
@@ -0,0 +1,161 @@
<?php
//[마이페이지][한줄소감] 데이터 동적변환
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
/**
* JSON 응답 출력 함수
*/
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
/**
* HTML escape 함수
*/
function h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/**
* 카테고리 태그 클래스 반환
*/
function review_category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ★ 선택년도 파라미터 수집
$selectedYear = trim((string)($_GET['selected_year'] ?? date('Y')));
if ($selectedYear === '' || !preg_match('/^\d{4}$/', $selectedYear)) {
$selectedYear = date('Y');
}
// ★ 한줄소감 조회
// - 선택년도 기준
// - comment 값이 있는 이력만
// - 마이클래스(CA10001)만
// - 같은 content_id는 최신 시청이력 1건만
$sql = "
SELECT
lh.content_id,
lh.member_id,
lh.sys_comp_code,
lh.watch_tm,
lh.content_tm,
lh.last_viewed_at,
lh.completed_at,
lh.comment,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url
FROM edu_learning_histories lh
INNER JOIN (
SELECT
content_id,
MAX(last_viewed_at) AS max_last_viewed_at
FROM edu_learning_histories
WHERE member_id = :sub_member_id
AND sys_comp_code = :sub_sys_comp_code
AND YEAR(last_viewed_at) = :sub_selected_year
AND comment IS NOT NULL
AND TRIM(comment) <> ''
GROUP BY content_id
) latest
ON latest.content_id = lh.content_id
AND latest.max_last_viewed_at = lh.last_viewed_at
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
WHERE lh.member_id = :main_member_id
AND lh.sys_comp_code = :main_sys_comp_code
AND c.category_code = 'CA10001'
AND lh.comment IS NOT NULL
AND TRIM(lh.comment) <> ''
ORDER BY lh.last_viewed_at DESC
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':sub_member_id' => $memberId,
':sub_sys_comp_code' => $sysCompCode,
':sub_selected_year' => (int)$selectedYear,
':main_member_id' => $memberId,
':main_sys_comp_code' => $sysCompCode,
]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$totalCount = count($rows);
ob_start();
if (!empty($rows)) {
foreach ($rows as $row) {
$categoryCode = (string)($row['category_code'] ?? '');
$tagClass = review_category_tag_class($categoryCode);
// ★ 실제 한줄소감은 edu_learning_histories.comment 사용
$reviewText = trim((string)($row['comment'] ?? ''));
?>
<li class="review-item" data-content-id="<?= h($row['content_id'] ?? '') ?>">
<div class="review-head">
<span class="category-tag <?= h($tagClass) ?>">마이클래스</span>
<p class="review-text"><?= h($row['title'] ?? '') ?></p>
</div>
<div class="review-comment">
<span class="review-comment-text">
<?= h($reviewText) ?>
</span>
</div>
</li>
<?php
}//foreach
} else {
?>
<li class="review-empty">한줄소감이 없습니다.</li>
<?php
}
$html = ob_get_clean();
response_json([
'success' => true,
'selected_year' => $selectedYear,
'total_count' => $totalCount,
'html' => $html,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '한줄소감 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+157
View File
@@ -0,0 +1,157 @@
<?php
// 에러 출력 설정
ini_set('display_errors', 1);
error_reporting(E_ALL);
set_time_limit(0); // 12,650명 처리를 위해 스크립트 실행 시간 제한 해제
extract($_REQUEST);
$url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 120); // 데이터 양이 많으므로 2분으로 연장
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json"]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
die("CURL 에러: " . curl_error($ch));
}
curl_close($ch);
if ($httpCode !== 200) {
die("API 호출 실패 (HTTP 상태 코드: $httpCode)");
}
$data = json_decode($response, true);
if (!isset($data['data']) || !is_array($data['data'])) {
die("유효한 데이터가 없습니다.");
}
// DB 연결
require_once __DIR__ . '/../bbs/db_conn.php';
$conn = db_conn();
$user_list = $data['data'];
$total_count = count($user_list);
$success_count = 0;
$error_count = 0;
echo "<h3>인사 정보 동기화 시작 (PDO 방식)</h3>";
echo "총 대상 인원: {$total_count}명<br><hr>";
$SET_injection_at = date('Y-m-d H:i:s');
$backup_log = "";
// ---------------------------------------------------------
// [추가] 데이터 주입 전 기존 데이터 백업 (삭제 후 재입력 방식)
// ---------------------------------------------------------
try {
$bk_stmt = $conn->prepare("CALL proc_edu_users_backup(?)");
$bk_stmt->execute([$SET_injection_at]);
echo "기존 데이터 백업 완료 (이전 백업 삭제 후 재생성)...<br><br>";
$backup_log .= "기존 데이터 백업 완료 (이전 백업 삭제 후 재생성) : 백업일시".$SET_injection_at;
flush();
} catch (PDOException $e) {
// 백업 실패 시 안전을 위해 스크립트를 중단하거나 경고를 띄울 수 있습니다.
echo "<span style='color:red;'>백업 실패: " . $e->getMessage() . "</span><br>작업을 중단합니다.";
$backup_log .= "기존 데이터 백업 실패 : 백업일시".$SET_injection_at;
exit;
}
// ---------------------------------------------------------
// 1. PDO Prepared Statement 준비 (위치 보유자 '?' 사용)
$sql = "CALL proc_edu_users_in(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
foreach ($user_list as $row) {
// 데이터 정리 (null 처리)
$params = [
$row['member_id'] ?? '',
$row['sys_comp_code'] ?? '',
$row['name'] ?? '',
$row['dept_name'] ?? '',
$row['rank_name'] ?? '',
(!empty($row['join_date'])) ? $row['join_date'] : null,
$row['belong_comp'] ?? '',
$row['working_comp'] ?? '',
$row['intra_pw'] ?? '',
$row['auth_level'] ?? '',
(!empty($row['end_date'])) ? $row['end_date'] : null,
$row['intro_flag'] ?? '0',
$row['belong_comp_id'] ?? '',
$row['working_comp_id'] ?? '',
$row['created_at'] ?? date('Y-m-d H:i:s'),
$row['updated_at'] ?? date('Y-m-d H:i:s'),
$SET_injection_at // injection_at
];
// 2. 실행 (PDO는 execute에 배열을 바로 넣으면 바인딩됩니다)
try {
if ($stmt->execute($params)) {
$success_count++;
// 대량 데이터 처리 시 진행 상황 출력 (100명 단위)
if ($success_count % 1000 == 0) {
echo "현재 {$success_count}명 처리 중...<br>";
flush(); // 화면에 즉시 반영
}
} else {
$error_count++;
}
} catch (PDOException $e) {
$error_count++;
// 프로시저 내부 에러 로그는 테이블에 쌓이겠지만 PHP 에러도 확인용 출력
echo "에러(ID: " . ($row['member_id'] ?? 'unknown') . "): " . $e->getMessage() . "<br>";
}
}
// ---------------------------------------------------------
// 로그 파일 생성 및 저장 로직 (한글 깨짐 방지 적용)
// ---------------------------------------------------------
$log_dir = $_SERVER['DOCUMENT_ROOT'] . "/log/";
$log_file_name = "users_info_batch_" . date('Y') . ".txt";
$log_path = $log_dir . $log_file_name;
// 폴더 생성
if (!is_dir($log_dir)) {
mkdir($log_dir, 0777, true);
}
// 로그 내용 구성
if($backup_log==""){
$new_log_entry = "주입일자= {$SET_injection_at} , 성공카운트 = {$success_count} , 실패카운트= {$error_count}" . PHP_EOL;
}else{
$new_log_entry = $backup_log."\n";
$new_log_entry .= "주입일자= {$SET_injection_at} , 성공카운트 = {$success_count} , 실패카운트= {$error_count}" . PHP_EOL;
}
// UTF-8 BOM (한글 깨짐 방지용 식별코드)
$bom = "\xEF\xBB\xBF";
if (file_exists($log_path)) {
// 기존 내용을 읽어옴
$existing_content = file_get_contents($log_path);
// 기존 내용에 BOM이 이미 있다면 제거 (내용 결합 시 중복 방지)
$existing_content = str_replace($bom, '', $existing_content);
// [BOM] + [새 로그] + [기존 로그] 순서로 저장
file_put_contents($log_path, $bom . $new_log_entry . $existing_content);
} else {
// 파일이 처음 생성될 때 BOM과 함께 저장
file_put_contents($log_path, $bom . $new_log_entry);
}
// ---------------------------------------------------------
echo "<hr>";
echo "<strong>처리 완료!</strong><br>";
echo "성공: {$success_count}건 / 실패: {$error_count}";
?>
+175
View File
@@ -0,0 +1,175 @@
<?php
// // 1. DB 연결 파일 불러오기 (bbs 폴더 안에 있으므로 경로 주의)
// require_once __DIR__ . '/../bbs/db_conn.php';
// // 2. 프로시저 파일 불러오기
// require_once __DIR__ . '/pros_users_inup.php';
// extract($_REQUEST);
// /* https://baroncs.co.kr/ajax/get_users_info.php
// * https://baroncs.co.kr/ajax/get_users_info.php?sys_comp_code=30
// *
// * $sys_comp_code 필수입력값. EduJsonAPI.php데이터 로직상에는 파라미터가 없을 시,
// * 가족사 전체 통합인사정보를 가져와서 일정부분 부하가 예상된다.
// * 때문에
// * 기본적으로 최소한의 조건 sys_comp_code를 전달받아서
// * 해당 회사 인원들에 대한 데이터만 업데이트하는 것으로 한다.
// * ※필요 시, 조건에 따라서 가족사전인원 업데이트도 가능하게 할 수 있음
// * (예시: 특정 시간대(24:00~05:00)에 접근시에는 전인원 업데이트)
// * $member_id
// */
// //배치작업 진행후 작업결과 로그 남길것
// $sys_comp_code = $sys_comp_code==""?"":$sys_comp_code;
// $member_id = $member_id==""?"":$member_id;
// /*
// if($sys_comp_code==""){
// //echo "Parameter Fail : sys_comp_code is null";
// //exit;
// }else{
// if( $sys_comp_code=="10" ||
// $sys_comp_code=="20" ||
// $sys_comp_code=="30" ||
// $sys_comp_code=="40" ||
// $sys_comp_code=="50"
// ){
// echo "Success :";
// echo "<br>";
// echo "sys_comp_code=".$sys_comp_code;
// echo "<br>";
// echo "member_id=".$member_id;
// //exit;
// }else{
// echo "Parameter Fail : This is not allowed sys_comp_code";
// exit;
// }
// }
// */
// $url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php";
// //$url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php?sys_comp_code=".$sys_comp_code;
// $ch = curl_init();
// curl_setopt($ch, CURLOPT_URL, $url);
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// curl_setopt($ch, CURLOPT_TIMEOUT, 10);
// curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json"]);
// $response = curl_exec($ch);
// $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// if (curl_errno($ch)) {
// die("CURL 에러: " . curl_error($ch));
// }
// curl_close($ch);
// if ($httpCode !== 200) {
// die("API 호출 실패 (HTTP 상태 코드: $httpCode)");
// }
// $data = json_decode($response, true);
// //print_r($data);
// // echo "<br><br>";
// // echo "총인원=".count($data['data'])."명";
// // echo "<br><br>";
// // $TEMP_array_data = $data['data'];
// // echo "<br><br>";
// // print_r($TEMP_array_data[80]);
// // echo "<br><br>";
// // print_r($TEMP_array_data);
// // echo "<br><br>";
// if ($httpCode !== 200) {
// die("API 호출 실패 (HTTP 상태 코드: $httpCode)");
// }
// $data = json_decode($response, true);
// // 3. 실행 및 결과 출력
// if (function_exists('db_conn')) {
// $pdo = db_conn(); // bbs/db_conn.php에 정의된 함수 호출
// if (!empty($data['data'])) {
// // 송 선임님이 검토 요청하신 그 함수 호출!
// $result = processUserSync($pdo, $data['data']);
// echo "──────────────────────────<br>";
// echo "동기화 결과 요약 (".date('Y-m-d H:i:s').")<br>";
// echo "- 신규 등록: {$result['ins']}건<br>";
// echo "- 정보 수정: {$result['upd']}건<br>";
// if($result['err'] > 0) echo "- 오류 발생: {$result['err']}건<br>";
// echo "──────────────────────────";
// }
// } else {
// die("에러: db_conn() 함수를 찾을 수 없습니다. 경로를 확인해주세요.");
// }
?>
<?php
// get_users_info2.php
ini_set('display_errors', 1); // 에러 확인용
require_once __DIR__ . '/../bbs/db_conn.php';
require_once __DIR__ . '/pros_users_inup.php';
extract($_REQUEST);
// 1. 파라미터 체크 (테스트를 위해 30번 고정하거나 파라미터 우선)
$target_comp = isset($sys_comp_code) && $sys_comp_code != "" ? $sys_comp_code : "30";
// 2. API URL에 파라미터 붙이기
$url = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php?sys_comp_code=" . $target_comp;
echo "Target Company: " . $target_comp . " (데이터 가져오는 중...)<br>";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 시간을 넉넉히 60초로 늘림
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Accept: application/json"]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
die("CURL 에러: " . curl_error($ch));
}
curl_close($ch);
$data = json_decode($response, true);
if (empty($data['data'])) {
die("해당 회사({$target_comp})의 데이터가 없거나 API 응답이 비어있습니다.");
}
// 3. DB 연결 및 실행
if (function_exists('db_conn')) {
$pdo = db_conn();
echo "총 인원: " . count($data['data']) . "명 처리 시작...<br>";
// pros_users_inup.php의 함수 호출
$result = processUserSync($pdo, $data['data']);
echo "──────────────────────────<br>";
echo "동기화 결과 요약<br>";
echo "- 대상 회사 코드: {$target_comp}<br>";
echo "- 신규 등록: {$result['ins']}건<br>";
echo "- 정보 수정: {$result['upd']}건<br>";
if($result['err'] > 0) echo "- 오류 발생: {$result['err']}건<br>";
echo "──────────────────────────";
}
?>
+528
View File
@@ -0,0 +1,528 @@
<?php
//require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
extract($_REQUEST);
// ---------------------------------------------------------
// 로그인 세션에서 사용자 정보 가져오기
// ---------------------------------------------------------
$memberId = trim((string)($_SESSION['member_id'] ?? $_SESSION['user_id'] ?? $_SESSION['ss_mb_id'] ?? ''));
$sysCompCode = trim((string)($_SESSION['sys_comp_code'] ?? $_SESSION['company'] ?? ''));
// sysCompCode 세션 미설정 시 DB에서 보완
if ($memberId !== '' && $sysCompCode === '') {
try {
$__pdo = db_conn();
$__st = $__pdo->prepare('SELECT sys_comp_code FROM edu_users WHERE member_id = ? ORDER BY sys_comp_code LIMIT 1');
$__st->execute([$memberId]);
$sysCompCode = (string)($__st->fetchColumn() ?: '');
} catch (Throwable $__e) { /* ignore */ }
}
// ---------------------------------------------------------
// 디버그 설정
// true : 바인딩 적용된 SQL 출력 후 종료
// false : 정상 실행
// ---------------------------------------------------------
//https://baroncs.co.kr/ajax/get_video_list.php?SET_PREFIX=I
$DEBUG_SQL = false;//정상진행
//$DEBUG_SQL = true; //디버깅
$prefixCondition = isset($SET_PREFIX) ? $SET_PREFIX : 'L'; //L=리더십(기본값), I=인사이트
// ---------------------------------------------------------
// 요청 파라미터
// ---------------------------------------------------------
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limitParam = trim((string)($_GET['limit'] ?? ''));
$limit = 8;
$noLimit = false;
if ($limitParam !== '') {
if (strtolower($limitParam) === 'all') {
$noLimit = true;
} else {
$parsedLimit = (int)$limitParam;
if ($parsedLimit > 0) {
$limit = $parsedLimit;
}
}
}
$offset = ($page - 1) * $limit;
$category = trim($_GET['category'] ?? ''); // 예: CA200L01
$sort = trim($_GET['sort'] ?? 'view'); // view / latest / seen / unseen
// ---------------------------------------------------------
// 기본 코드맵 (DB코드 조회 실패 대비)
// ---------------------------------------------------------
if($prefixCondition=="L"){
$SET_category_code = "CA10004";//리더십
$CATEGORY_MAP = [
'CA10004' => '리더십',
];
$SUBCATE_MAP = [
'CA200L01' => '리더십 시작하기',
'CA200L02' => '셀프 리더십',
'CA200L03' => '팀 리더십',
'CA200L04' => '실전 리더십',
'CA200L05' => '인물탐구',
];
}else if($prefixCondition=="I"){
$SET_category_code = "CA10005";//인사이트
$CATEGORY_MAP = [
'CA10005' => '인사이트',
];
$SUBCATE_MAP = [
'CA200I01' => '경제와 사회',
'CA200I02' => '기술과 미래',
'CA200I03' => '트렌드 업',
'CA200I04' => '행복과 건강',
'CA200I05' => '피플스토리',
'CA200I06' => '라이프',
];
}
// ---------------------------------------------------------
// 공통 함수
// ---------------------------------------------------------
function FN_change_entity(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function mapContentRow(array $row, array $catMap = [], array $subcateMap = []): array
{
$kwStr = $row['keyword_names'] ?? ($row['keywords'] ?? '');
$keywords = $kwStr !== '' ? explode(',', $kwStr) : [];
$keywords = array_values(array_filter(array_map('trim', $keywords), static function ($v) {
return $v !== '';
}));
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = min(100, max(0, $gauge));
$raw = trim($row['content_url'] ?? '');
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim($row['thumbnail_url'] ?? '');
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
$categoryCode = $row['category_code'] ?? '';
$groupCode = $row['category_group'] ?? '';
return [
'id' => $row['content_id'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'category' => $catMap[$categoryCode] ?? $categoryCode,
'category_code' => $categoryCode,
'subcate' => $subcateMap[$groupCode] ?? $groupCode,
'bookmark' => !empty($row['is_bookmarked']),
'title' => $row['title'] ?? '',
'description' => $row['description'] ?? '',
'keywords' => $keywords,
'gauge' => $gauge,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'view_cnt' => (int)($row['view_cnt'] ?? 0),
];
}
/**
* 디버그용: 바인딩값을 SQL에 치환한 문자열 생성
* 주의: 실제 실행 SQL을 PDO가 제공하는 것은 아니고, 보기 쉽게 만든 디버그용 문자열임
*/
function buildDebugSql(string $sql, array $params): string
{
// :member_id 와 :member_id2 같은 이름 충돌 방지
uksort($params, function ($a, $b) {
return strlen((string)$b) <=> strlen((string)$a);
});
foreach ($params as $key => $value) {
if ($value === null) {
$replace = 'NULL';
} elseif (is_int($value) || is_float($value)) {
$replace = (string)$value;
} else {
$replace = "'" . str_replace("'", "''", (string)$value) . "'";
}
$sql = str_replace($key, $replace, $sql);
}
return $sql;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ---------------------------------------------------------
// edu_codes 에서 실제 코드맵 조회
// ---------------------------------------------------------
$stmtCode = $pdo->query("
SELECT group_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND
(
group_code = 'CA100'
OR
( group_code = 'CA200' AND code LIKE '".$prefixCondition."%' )
)
");
if ($stmtCode) {
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $cr) {
$key = $cr['group_code'] . $cr['code'];
if ($cr['group_code'] === 'CA100') {
$CATEGORY_MAP[$key] = $cr['code_name'];
} elseif ($cr['group_code'] === 'CA200') {
$SUBCATE_MAP[$key] = $cr['code_name'];
}
}
}
// ---------------------------------------------------------
// 이 페이지는 리더십 대분류 전용
// 탭은 category_group 으로 필터링
// category 파라미터는 CA200L01 형태를 권장
// ---------------------------------------------------------
$categoryGroup = '';
if ($category !== '' && strtolower($category) !== 'all') {
if (preg_match('/^L\d{2}$/', $category)) {
$categoryGroup = 'CA200' . $category; // L01 -> CA200L01
} else {
$categoryGroup = $category; // 이미 CA200L01이면 그대로
}
}
// ---------------------------------------------------------
// 정렬 조건
// ---------------------------------------------------------
switch ($sort) {
case 'latest':
$orderBy = "
c.created_at DESC,
c.content_id DESC
";
break;
case 'seen':
$orderBy = "
c.created_at DESC,
CASE WHEN lh.content_id IS NOT NULL THEN 0 ELSE 1 END ASC,
lh.last_viewed_at DESC,
c.content_id DESC
";
break;
case 'unseen':
$orderBy = "
c.created_at DESC,
CASE WHEN lh.content_id IS NULL THEN 0 ELSE 1 END ASC,
COALESCE(c.updated_at, c.created_at) DESC,
c.content_id DESC
";
break;
case 'view':
default:
$orderBy = "
COALESCE(vs.view_cnt, 0) DESC,
COALESCE(vs.total_all_tm, 0) DESC,
c.created_at DESC,
c.content_id DESC
";
break;
}
// ---------------------------------------------------------
// 기본 조건
// ---------------------------------------------------------
$where = [];
$bind = [];
//배열추가
$where[] = "c.is_active = '1'";
$where[] = "(c.start_date IS NULL OR c.start_date <= CURDATE())";
$where[] = "(c.end_date IS NULL OR c.end_date >= CURDATE())";
// 리더십 대분류 고정
$where[] = "c.category_code = '{$SET_category_code}'"; //edu_codes.base_code == edu_contents.category_code
// 탭 필터
if ($categoryGroup !== '') {
$where[] = "c.category_group = :category_group";
$bind[':category_group'] = $categoryGroup;
}
$whereSql = implode("\n AND ", $where);//배열 사이에 구분자를 넣어서 연결
// ---------------------------------------------------------
// TOTAL COUNT
// ---------------------------------------------------------
$countSql = "
SELECT COUNT(*)
FROM edu_contents c
WHERE {$whereSql}
";
$stmtCount = $pdo->prepare($countSql);
foreach ($bind as $key => $value) {
$stmtCount->bindValue($key, $value, PDO::PARAM_STR);
}
if ($DEBUG_SQL) {
$countDebugSql = buildDebugSql($countSql, $bind);
echo '<pre>';
echo "[COUNT SQL]\n\n";
// echo htmlspecialchars($countDebugSql, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo $countDebugSql;
echo "\n\n------------------------------\n\n";
}
$stmtCount->execute();
$totalCount = (int)$stmtCount->fetchColumn();
// ---------------------------------------------------------
// 목록 조회
// ---------------------------------------------------------
$limitSql = $noLimit ? '' : "\n LIMIT :limit OFFSET :offset\n";
$sql = "
SELECT
c.content_id,
c.category_code,
c.category_group,
c.title,
c.content_url,
c.thumbnail_url,
c.description,
c.sort_order,
c.created_at,
c.updated_at,
lh.content_id AS lh_content_id,
lh.watch_tm,
lh.content_tm,
lh.last_viewed_at,
CASE
WHEN cw.content_id IS NOT NULL THEN 1
ELSE 0
END AS is_bookmarked,
COALESCE(vs.view_cnt, 0) AS view_cnt,
COALESCE(vs.total_all_tm, 0) AS total_all_tm,
GROUP_CONCAT(
DISTINCT ck.keyword_code
ORDER BY ck.keyword_code
SEPARATOR ','
) AS keywords,
GROUP_CONCAT(
DISTINCT COALESCE(kc.code_name, ck.keyword_code)
ORDER BY ck.keyword_code
SEPARATOR ','
) AS keyword_names
FROM edu_contents c
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = :member_id2
AND cw.sys_comp_code = :sys_comp_code2
AND cw.is_active = '1'
LEFT JOIN (
SELECT
content_id,
COUNT(DISTINCT member_id) AS view_cnt,
COALESCE(SUM(all_tm), 0) AS total_all_tm
FROM edu_learning_histories
GROUP BY content_id
) vs
ON vs.content_id = c.content_id
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
AND ck.is_active = '1'
LEFT JOIN edu_codes kc
ON kc.base_code = ck.keyword_code
AND kc.is_active = 1
WHERE {$whereSql}
GROUP BY c.content_id
ORDER BY {$orderBy}
{$limitSql}
";
/*LIMIT :limit OFFSET :offset*/
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':member_id2', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code2', $sysCompCode, PDO::PARAM_STR);
foreach ($bind as $key => $value) {
$stmt->bindValue($key, $value, PDO::PARAM_STR);
}
if (!$noLimit) {
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
}
if ($DEBUG_SQL) {
$debugParams = [
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':member_id2' => $memberId,
':sys_comp_code2' => $sysCompCode,
];
if (!$noLimit) {
$debugParams[':limit'] = $limit;
$debugParams[':offset'] = $offset;
}
if (!empty($bind)) {
$debugParams = array_merge($debugParams, $bind);
}
$debugSql = buildDebugSql($sql, $debugParams);
echo "[LIST SQL]\n\n";
//echo htmlspecialchars($debugSql, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo $debugSql;
echo '</pre>';
}
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($DEBUG_SQL) {
print_r($rows);
exit;
}
// ---------------------------------------------------------
// HTML 조각 생성
// ---------------------------------------------------------
ob_start();
foreach ($rows as $row) {
$item = mapContentRow($row, $CATEGORY_MAP, $SUBCATE_MAP);
$contentId = FN_change_entity($item['id']);
$title = FN_change_entity($item['title']);
$thumb = FN_change_entity($item['thumbnail']);
$categoryNm= FN_change_entity($item['category']);
$subcateNm = FN_change_entity($item['subcate']);
$descRaw = $item['description'] ?? '';
$keywords = $item['keywords'] ?? [];
$keywordsCsv = implode(',', $keywords);
$keywordTags = '';
if (!empty($keywords)) {
$keywordTags = implode(' ', array_map(static function ($kw) {
return '#' . $kw;
}, $keywords));
}
$itemDesc = $keywordTags !== ''
? FN_change_entity($keywordTags)
: ($subcateNm !== '' ? $subcateNm : $categoryNm);
$checked = $item['bookmark'] ? ' checked' : '';
$viewUrl = "/video_view.php?content_id=" . rawurlencode($item['id']);
?>
<li class="video-item" data-content-id="<?= $contentId ?>" data-content-url="<?= FN_change_entity($item['url']) ?>" data-title="<?= $title ?>" data-category-name="<?= $categoryNm ?>" data-sub-category="<?= $subcateNm ?>" data-watch-tm="<?= (int)($item['watch_tm'] ?? 0) ?>" data-content-tm="<?= (int)($item['content_tm'] ?? 0) ?>" data-description="<?= FN_change_entity($descRaw) ?>" data-keywords="<?= FN_change_entity($keywordsCsv) ?>">
<a href="<?= FN_change_entity($viewUrl) ?>" class="card-link">
<label class="bookmark" for="like_chk_<?= $contentId ?>" onclick="event.preventDefault(); event.stopPropagation();">
<input type="checkbox" id="like_chk_<?= $contentId ?>" title="좋아요"<?= $checked ?> />
</label>
<div class="item-thumb">
<?php if ($thumb !== '') { ?>
<img src="<?= $thumb ?>" alt="<?= $title ?>" loading="lazy" />
<?php } else { ?>
<img src="/img/video/no-image.png" alt="<?= $title ?>" loading="lazy" />
<?php } ?>
</div>
<div class="item-info">
<strong class="item-title"><?= $title ?></strong>
<div class="item-desc-wrapper">
<span class="item-desc"><?= $itemDesc ?></span>
<span class="item-view-count">시청수 <?= (int)($item['view_cnt'] ?? 0) ?> 회</span>
</div>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
echo json_encode([
'success' => true,
'html' => $html,
'total_count' => $totalCount
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_video_list.php] ' . $e->getMessage());
echo json_encode([
'success' => false,
'html' => '',
'total_count' => 0
], JSON_UNESCAPED_UNICODE);
}
+431
View File
@@ -0,0 +1,431 @@
<?php
//require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// ---------------------------------------------------------
// TODO: 실제 로그인 세션 연동 후 교체
// ---------------------------------------------------------
$memberId = 'U001';
$sysCompCode = 'COMP01';
// ---------------------------------------------------------
// 디버그 설정
// true : 바인딩 적용된 SQL 출력 후 종료
// false : 정상 실행
// ---------------------------------------------------------
$DEBUG_SQL = false;
// ---------------------------------------------------------
// 요청 파라미터
// ---------------------------------------------------------
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = 2;
$offset = ($page - 1) * $limit;
$category = trim($_GET['category'] ?? ''); // 예: CA200L01
$sort = trim($_GET['sort'] ?? 'view'); // view / latest / seen / unseen
// ---------------------------------------------------------
// 기본 코드맵 (DB코드 조회 실패 대비)
// ---------------------------------------------------------
$CATEGORY_MAP = [
'CA10004' => '리더십',
];
$SUBCATE_MAP = [
'CA200L01' => '리더십 시작하기',
'CA200L02' => '셀프 리더십',
'CA200L03' => '팀 리더십',
'CA200L04' => '실전조직 리더십',
'CA200L05' => '리더케이스탐구',
];
// ---------------------------------------------------------
// 공통 함수
// ---------------------------------------------------------
function h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function mapContentRow(array $row, array $catMap = [], array $subcateMap = []): array
{
$kwStr = $row['keywords'] ?? '';
$keywords = $kwStr !== '' ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = min(100, max(0, $gauge));
$raw = trim($row['content_url'] ?? '');
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim($row['thumbnail_url'] ?? '');
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
$categoryCode = $row['category_code'] ?? '';
$groupCode = $row['category_group'] ?? '';
return [
'id' => $row['content_id'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'category' => $catMap[$categoryCode] ?? $categoryCode,
'category_code' => $categoryCode,
'subcate' => $subcateMap[$groupCode] ?? $groupCode,
'bookmark' => !empty($row['is_bookmarked']),
'title' => $row['title'] ?? '',
'keywords' => $keywords,
'gauge' => $gauge,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'view_cnt' => (int)($row['view_cnt'] ?? 0),
];
}
/**
* 디버그용: 바인딩값을 SQL에 치환한 문자열 생성
* 주의: 실제 실행 SQL을 PDO가 제공하는 것은 아니고, 보기 쉽게 만든 디버그용 문자열임
*/
function buildDebugSql(string $sql, array $params): string
{
// :member_id 와 :member_id2 같은 이름 충돌 방지
uksort($params, function ($a, $b) {
return strlen((string)$b) <=> strlen((string)$a);
});
foreach ($params as $key => $value) {
if ($value === null) {
$replace = 'NULL';
} elseif (is_int($value) || is_float($value)) {
$replace = (string)$value;
} else {
$replace = "'" . str_replace("'", "''", (string)$value) . "'";
}
$sql = str_replace($key, $replace, $sql);
}
return $sql;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ---------------------------------------------------------
// edu_codes 에서 실제 코드맵 조회
// ---------------------------------------------------------
$stmtCode = $pdo->query("
SELECT group_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND group_code IN ('CA100', 'CA200')
");
if ($stmtCode) {
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $cr) {
$key = $cr['group_code'] . $cr['code'];
if ($cr['group_code'] === 'CA100') {
$CATEGORY_MAP[$key] = $cr['code_name'];
} elseif ($cr['group_code'] === 'CA200') {
$SUBCATE_MAP[$key] = $cr['code_name'];
}
}
}
// ---------------------------------------------------------
// 이 페이지는 리더십 대분류 전용
// 탭은 category_group 으로 필터링
// category 파라미터는 CA200L01 형태를 권장
// ---------------------------------------------------------
$categoryGroup = '';
if ($category !== '' && strtolower($category) !== 'all') {
if (preg_match('/^L\d{2}$/', $category)) {
$categoryGroup = 'CA200' . $category; // L01 -> CA200L01
} else {
$categoryGroup = $category; // 이미 CA200L01이면 그대로
}
}
// ---------------------------------------------------------
// 정렬 조건
// ---------------------------------------------------------
switch ($sort) {
case 'latest':
$orderBy = "
COALESCE(c.updated_at, c.created_at) DESC,
c.sort_order ASC,
c.content_id DESC
";
break;
case 'seen':
$orderBy = "
CASE WHEN lh.content_id IS NOT NULL THEN 0 ELSE 1 END ASC,
lh.last_viewed_at DESC,
c.content_id DESC
";
break;
case 'unseen':
$orderBy = "
CASE WHEN lh.content_id IS NULL THEN 0 ELSE 1 END ASC,
COALESCE(c.updated_at, c.created_at) DESC,
c.content_id DESC
";
break;
case 'view':
default:
$orderBy = "
COALESCE(vs.view_cnt, 0) DESC,
COALESCE(vs.total_all_tm, 0) DESC,
COALESCE(c.updated_at, c.created_at) DESC,
c.content_id DESC
";
break;
}
// ---------------------------------------------------------
// 기본 조건
// ---------------------------------------------------------
$where = [];
$bind = [];
$where[] = "(c.is_active = '1' OR c.is_active = 'Y')";
$where[] = "(c.start_date IS NULL OR c.start_date <= CURDATE())";
$where[] = "(c.end_date IS NULL OR c.end_date >= CURDATE())";
// 리더십 대분류 고정
$where[] = "c.category_code = 'CA10004'";
// 탭 필터
if ($categoryGroup !== '') {
$where[] = "c.category_group = :category_group";
$bind[':category_group'] = $categoryGroup;
}
$whereSql = implode("\n AND ", $where);
// ---------------------------------------------------------
// TOTAL COUNT
// ---------------------------------------------------------
$countSql = "
SELECT COUNT(*)
FROM edu_contents c
WHERE {$whereSql}
";
$stmtCount = $pdo->prepare($countSql);
foreach ($bind as $key => $value) {
$stmtCount->bindValue($key, $value, PDO::PARAM_STR);
}
if ($DEBUG_SQL) {
$countDebugSql = buildDebugSql($countSql, $bind);
echo '<pre>';
echo "[COUNT SQL]\n\n";
// echo htmlspecialchars($countDebugSql, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo $countDebugSql;
echo "\n\n------------------------------\n\n";
}
$stmtCount->execute();
$totalCount = (int)$stmtCount->fetchColumn();
// ---------------------------------------------------------
// 목록 조회
// ---------------------------------------------------------
$sql = "
SELECT
c.content_id,
c.category_code,
c.category_group,
c.title,
c.content_url,
c.thumbnail_url,
c.sort_order,
c.created_at,
c.updated_at,
lh.content_id AS lh_content_id,
lh.watch_tm,
lh.content_tm,
lh.last_viewed_at,
CASE
WHEN cw.content_id IS NOT NULL THEN 1
ELSE 0
END AS is_bookmarked,
COALESCE(vs.view_cnt, 0) AS view_cnt,
COALESCE(vs.total_all_tm, 0) AS total_all_tm,
GROUP_CONCAT(
DISTINCT ck.keyword_code
ORDER BY ck.keyword_code
SEPARATOR ','
) AS keywords
FROM edu_contents c
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = :member_id2
AND cw.sys_comp_code = :sys_comp_code2
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN (
SELECT
content_id,
COUNT(*) AS view_cnt,
COALESCE(SUM(all_tm), 0) AS total_all_tm
FROM edu_learning_histories
GROUP BY content_id
) vs
ON vs.content_id = c.content_id
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE {$whereSql}
GROUP BY c.content_id
ORDER BY {$orderBy}
LIMIT :limit OFFSET :offset
";
/*LIMIT :limit OFFSET :offset*/
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':member_id2', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code2', $sysCompCode, PDO::PARAM_STR);
foreach ($bind as $key => $value) {
$stmt->bindValue($key, $value, PDO::PARAM_STR);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
if ($DEBUG_SQL) {
$debugParams = [
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':member_id2' => $memberId,
':sys_comp_code2' => $sysCompCode,
':limit' => $limit,
':offset' => $offset,
];
if (!empty($bind)) {
$debugParams = array_merge($debugParams, $bind);
}
$debugSql = buildDebugSql($sql, $debugParams);
echo "[LIST SQL]\n\n";
//echo htmlspecialchars($debugSql, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo $debugSql;
echo '</pre>';
}
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($DEBUG_SQL) {
print_r($rows);
exit;
}
// ---------------------------------------------------------
// HTML 조각 생성
// ---------------------------------------------------------
ob_start();
foreach ($rows as $row) {
$item = mapContentRow($row, $CATEGORY_MAP, $SUBCATE_MAP);
$contentId = h($item['id']);
$title = h($item['title']);
$thumb = h($item['thumbnail']);
$categoryNm= h($item['category']);
$subcateNm = h($item['subcate']);
$checked = $item['bookmark'] ? ' checked' : '';
$viewUrl = "/edu/video_view.php?content_id=" . rawurlencode($item['id']);
?>
<li class="video-item">
<a href="<?= h($viewUrl) ?>" class="card-link">
<label class="bookmark" for="like_chk_<?= $contentId ?>" onclick="event.preventDefault(); event.stopPropagation();">
<input type="checkbox" id="like_chk_<?= $contentId ?>" title="좋아요"<?= $checked ?> />
</label>
<div class="item-thumb">
<?php if ($thumb !== '') { ?>
<img src="<?= $thumb ?>" alt="<?= $title ?>" loading="lazy" />
<?php } else { ?>
<img src="/edu/img/video/no-image.png" alt="<?= $title ?>" loading="lazy" />
<?php } ?>
</div>
<div class="item-info">
<strong class="item-title"><?= $title ?></strong>
<span class="item-desc"><?= $subcateNm !== '' ? $subcateNm : $categoryNm ?></span>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
echo json_encode([
'success' => true,
'html' => $html,
'total_count' => $totalCount
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_video_list.php] ' . $e->getMessage());
echo json_encode([
'success' => false,
'html' => '',
'total_count' => 0
], JSON_UNESCAPED_UNICODE);
}
+539
View File
@@ -0,0 +1,539 @@
<?php
//=========================
// 콘텐츠리스트 AJAX
// [마이페이지][시청중인, 시청완료, 저장한 콘텐츠] 데이터 동적변환
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
//keywords_code_name
$kwStr = (string)($row['keywords_code_name'] ?? $row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$allTm = (float)($row['all_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/img/video/img_thumb_01.png');
return [
'id' => $row['content_id'] ?? '',
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'subcate' => (string)($row['category_group'] ?? ''),
'title' => $row['title'] ?? '',
'description' => trim((string)($row['description'] ?? '')),
'description2' => trim((string)($row['description2'] ?? '')),
'url' => $raw,
'content_url' => $raw,
'video_url' => $url,
'video_id' => $videoId,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'all_tm' => (int)$allTm,
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'bookmark' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching'));
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
// ★ 선택년도 수집
$selectedYear = trim((string)($_GET['selected_year'] ?? date('Y')));
if ($selectedYear === '' || !preg_match('/^\d{4}$/', $selectedYear)) {
$selectedYear = date('Y');
}
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// =========================
// 탭 카운트 (선택년도 기준)
// =========================
// 시청중인 콘텐츠 카운트
/*
* $stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
AND YEAR(last_viewed_at) = :selected_year
");
*/
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(DISTINCT lh.content_id)
FROM edu_learning_histories lh
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
AND YEAR(lh.last_viewed_at) = :selected_year
AND NOT EXISTS (
SELECT 1
FROM edu_learning_histories lh2
WHERE lh2.content_id = lh.content_id
AND lh2.member_id = lh.member_id
AND lh2.sys_comp_code = lh.sys_comp_code
AND lh2.completed_at IS NOT NULL
)
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
// 시청완료
// 기준:
// 1) completed_at 이 있는 이력만 완료로 인정
// 2) completed_at 기준 연도 필터
// 3) 같은 콘텐츠는 최신 완료 1건만 카운트
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM (
SELECT
content_id,
MAX(completed_at) AS latest_completed_at
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND completed_at IS NOT NULL
AND YEAR(completed_at) = :selected_year
GROUP BY content_id
) t
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
// 저장한 콘텐츠
// ★ 저장한 시점 기준 년도(favorited_at 우선, 없으면 updated_at, created_at fallback)
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(DISTINCT content_id)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND is_active = '1'
AND YEAR(COALESCE(favorited_at, updated_at, created_at)) = :selected_year
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
// =========================
// 탭별 메인 쿼리
// =========================
if ($tab === 'saved') {
// 저장한 콘텐츠 : wishlist 기준
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.category_group,
c.title,
c.description,
c.description2,
c.content_url,
c.thumbnail_url,
COALESCE(MAX(lh.watch_tm), 0) AS watch_tm,
COALESCE(MAX(lh.content_tm), 0) AS content_tm,
COALESCE(MAX(lh.all_tm), 0) AS all_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords,
MAX(lh.last_viewed_at) AS sort_last_viewed_at,
MAX(COALESCE(cw.favorited_at, cw.updated_at, cw.created_at)) AS sort_saved_at
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
AND YEAR(lh.last_viewed_at) = :selected_year_lh
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND cw.is_active = '1'
AND YEAR(COALESCE(cw.favorited_at, cw.updated_at, cw.created_at)) = :selected_year_saved
GROUP BY c.content_id
ORDER BY
CASE WHEN MAX(lh.last_viewed_at) IS NULL THEN 1 ELSE 0 END ASC,
MAX(lh.last_viewed_at) DESC,
sort_saved_at DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
// 시청완료 콘텐츠 completed_at IS NOT NULL (최신 1건)
// 기준:
// 1) completed_at 이 있는 이력만 완료로 인정
// 2) 선택년도는 completed_at 기준
// 3) 같은 콘텐츠가 여러 번 완료된 경우 최신 완료 1건만 반영
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.category_group,
c.title,
c.description,
c.description2,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
lh.all_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords,
lh.completed_at AS sort_completed_at
FROM (
SELECT
t1.content_id,
t1.member_id,
t1.sys_comp_code,
t1.watch_tm,
t1.content_tm,
t1.all_tm,
t1.completed_at
FROM edu_learning_histories t1
INNER JOIN (
SELECT
content_id,
MAX(completed_at) AS latest_completed_at
FROM edu_learning_histories
WHERE member_id = :completed_member_id_1
AND sys_comp_code = :completed_sys_comp_code_1
AND completed_at IS NOT NULL
AND YEAR(completed_at) = :completed_selected_year_1
GROUP BY content_id
) t2
ON t1.content_id = t2.content_id
AND t1.completed_at = t2.latest_completed_at
AND t1.member_id = :completed_member_id_2
AND t1.sys_comp_code = :completed_sys_comp_code_2
) lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
GROUP BY
c.content_id,
c.category_code,
c.category_group,
c.title,
c.description,
c.description2,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
lh.all_tm,
cw.content_id,
lh.completed_at
ORDER BY lh.completed_at DESC
LIMIT :limit OFFSET :offset
";
} else {
// 시청중인 콘텐츠 : watch_tm < content_tm
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.category_group,
c.title,
c.description,
c.description2,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
lh.all_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
-- 기존 코드 리스트
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords,
-- 추가된 코드명 리스트
GROUP_CONCAT(DISTINCT ec.code_name ORDER BY ck.keyword_code SEPARATOR ',') AS keywords_code_name
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND cw.is_active = '1'
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
-- [추가] 코드 테이블 조인
LEFT JOIN edu_codes ec
ON ec.base_code = ck.keyword_code
AND ec.group_code = 'KW100'
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
AND YEAR(lh.last_viewed_at) = :selected_year
AND NOT EXISTS (
SELECT 1
FROM edu_learning_histories lh2
WHERE lh2.content_id = lh.content_id
AND lh2.member_id = lh.member_id
AND lh2.sys_comp_code = lh.sys_comp_code
AND lh2.completed_at IS NOT NULL
)
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
/*
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
AND YEAR(lh.last_viewed_at) = :selected_year
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";*/
// $sql = "
// SELECT
// c.content_id,
// c.category_code,
// c.title,
// c.content_url,
// c.thumbnail_url,
// lh.watch_tm,
// lh.content_tm,
// CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
// GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
// FROM edu_learning_histories lh
// INNER JOIN edu_contents c
// ON c.content_id = lh.content_id
// LEFT JOIN edu_content_wishlist cw
// ON cw.content_id = c.content_id
// AND cw.member_id = lh.member_id
// AND cw.sys_comp_code = lh.sys_comp_code
// AND cw.is_active = '1'
// LEFT JOIN edu_content_keywords ck
// ON ck.content_id = c.content_id
// WHERE lh.member_id = :member_id
// AND lh.sys_comp_code = :sys_comp_code
// AND lh.watch_tm < lh.content_tm
// AND YEAR(lh.last_viewed_at) = :selected_year
// GROUP BY c.content_id
// ORDER BY lh.last_viewed_at DESC
// LIMIT :limit OFFSET :offset
// ";
}
$stmt = $pdo->prepare($sql);
if ($tab === 'saved') {
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':selected_year_lh', (int)$selectedYear, PDO::PARAM_INT);
$stmt->bindValue(':selected_year_saved', (int)$selectedYear, PDO::PARAM_INT);
} elseif ($tab === 'completed') {
$stmt->bindValue(':completed_member_id_1', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':completed_sys_comp_code_1', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':completed_selected_year_1', (int)$selectedYear, PDO::PARAM_INT);
$stmt->bindValue(':completed_member_id_2', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':completed_sys_comp_code_2', $sysCompCode, PDO::PARAM_STR);
} else {
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':selected_year', (int)$selectedYear, PDO::PARAM_INT);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
$videos = [];
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
$videos[] = $item;
$item['url']="#"; //마이페이지용 모달 개발전까지 링크삭제
?>
<li title="클릭 시, 모달 연결은 추후 개발예정"
class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>"
data-content-id="<?= mypage_h($item['content_id']) ?>"
>
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'selected_year' => $selectedYear,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'videos' => $videos,
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,320 @@
<?php
//=========================
//콘텐츠리스트 AJAX
//기존 참고 파일처럼 html 조각을 반환하는 방식으로 잡았습니다.
//현재 참고 구현도 코드맵/진행률/북마크/키워드 배열 구조를 그렇게 처리하고 있습니다.
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/edu/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching')); // watching / completed / saved
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// 탭 카운트
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND (is_active = '1' OR is_active = 'Y')
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
GROUP BY c.content_id
ORDER BY cw.favorited_at DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>">
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
@@ -0,0 +1,323 @@
<?php
//=========================
//콘텐츠리스트 AJAX
//기존 참고 파일처럼 html 조각을 반환하는 방식으로 잡았습니다.
//현재 참고 구현도 코드맵/진행률/북마크/키워드 배열 구조를 그렇게 처리하고 있습니다.
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
function mypage_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function map_content_row(array $row, array $catMap = []): array
{
$kwStr = (string)($row['keywords'] ?? '');
$keywords = ($kwStr !== '') ? explode(',', $kwStr) : [];
$watchTm = (float)($row['watch_tm'] ?? 0);
$contentTm = (float)($row['content_tm'] ?? 0);
$gauge = ($contentTm > 0) ? (int)round(($watchTm / $contentTm) * 100) : 0;
$gauge = max(0, min(100, $gauge));
$raw = trim((string)($row['content_url'] ?? ''));
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1];
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw;
} else {
$videoId = '';
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw;
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '/edu/img/video/img_thumb_01.png');
return [
'content_id' => $row['content_id'] ?? '',
'category_code' => $row['category_code'] ?? '',
'category_name' => $catMap[$row['category_code'] ?? ''] ?? ($row['category_code'] ?? ''),
'title' => $row['title'] ?? '',
'url' => $url,
'thumbnail' => $thumbnail,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'gauge' => $gauge,
'is_bookmarked' => !empty($row['is_bookmarked']),
'keywords' => $keywords,
];
}
function category_tag_class(string $categoryCode): string
{
$map = [
'CA10001' => 'myclass',
'CA10002' => 'onboarding',
'CA10003' => 'legal',
'CA10004' => 'leader',
'CA10005' => 'insight',
'CA10006' => 'biz',
];
return $map[$categoryCode] ?? 'etc';
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$tab = trim((string)($_GET['tab'] ?? 'watching')); // watching / completed / saved
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(1, (int)($_GET['limit'] ?? 12));
$offset = ($page - 1) * $limit;
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$stmtCode = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE group_code = 'CA100'
AND is_active = '1'
ORDER BY code
");
$stmtCode->execute();
foreach ($stmtCode->fetchAll(PDO::FETCH_ASSOC) as $row) {
$CATEGORY_MAP[$row['base_code']] = $row['code_name'];
}
// 탭 카운트
$stmtCountWatching = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm < content_tm
");
$stmtCountWatching->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countWatching = (int)$stmtCountWatching->fetchColumn();
$stmtCountCompleted = $pdo->prepare("
SELECT COUNT(*)
FROM edu_learning_histories
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND watch_tm >= content_tm
");
$stmtCountCompleted->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countCompleted = (int)$stmtCountCompleted->fetchColumn();
$stmtCountSaved = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND (is_active = '1' OR is_active = 'Y')
");
$stmtCountSaved->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$countSaved = (int)$stmtCountSaved->fetchColumn();
if ($tab === 'saved') {
$countTotal = $countSaved;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
1 AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_content_wishlist cw
INNER JOIN edu_contents c
ON c.content_id = cw.content_id
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = cw.member_id
AND lh.sys_comp_code = cw.sys_comp_code
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE cw.member_id = :member_id
AND cw.sys_comp_code = :sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
GROUP BY c.content_id
ORDER BY cw.favorited_at DESC
LIMIT :limit OFFSET :offset
";
} elseif ($tab === 'completed') {
$countTotal = $countCompleted;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm >= lh.content_tm
GROUP BY c.content_id
ORDER BY COALESCE(lh.completed_at, lh.last_viewed_at) DESC
LIMIT :limit OFFSET :offset
";
} else {
$tab = 'watching';
$countTotal = $countWatching;
$sql = "
SELECT
c.content_id,
c.category_code,
c.title,
c.content_url,
c.thumbnail_url,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(DISTINCT ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
LEFT JOIN edu_content_wishlist cw
ON cw.content_id = c.content_id
AND cw.member_id = lh.member_id
AND cw.sys_comp_code = lh.sys_comp_code
AND (cw.is_active = '1' OR cw.is_active = 'Y')
LEFT JOIN edu_content_keywords ck
ON ck.content_id = c.content_id
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :sys_comp_code
AND lh.watch_tm < lh.content_tm
GROUP BY c.content_id
ORDER BY lh.last_viewed_at DESC
LIMIT :limit OFFSET :offset
";
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':member_id', $memberId, PDO::PARAM_STR);
$stmt->bindValue(':sys_comp_code', $sysCompCode, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
ob_start();
foreach ($rows as $row) {
$item = map_content_row($row, $CATEGORY_MAP);
$tagClass = category_tag_class($item['category_code']);
?>
<li
class="content-card <?php if ($tab === 'completed') { ?>content-card--completed<?php } ?>"
data-content-id="<?= mypage_h($item['content_id']) ?>"
>
<a href="<?= mypage_h($item['url']) ?>" class="card-link">
<label class="bookmark" for="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>" onclick="event.stopPropagation();">
<input
type="checkbox"
id="wish_<?= mypage_h($tab . '_' . $item['content_id']) ?>"
title="저장"
<?php if ($item['is_bookmarked']) { ?>checked<?php } ?>
>
</label>
<div class="item-thumb">
<img src="<?= mypage_h($item['thumbnail']) ?>" alt="<?= mypage_h($item['title']) ?>">
<div class="gauge-bar rect">
<div class="gauge-fill" style="width: <?= (int)$item['gauge'] ?>%"></div>
</div>
</div>
<div class="item-info">
<span class="category-tag <?= mypage_h($tagClass) ?>">
<?= mypage_h($item['category_name']) ?>
</span>
<strong class="item-title"><?= mypage_h($item['title']) ?></strong>
<div class="tag-list">
<?php foreach ($item['keywords'] as $kw): ?>
<?php if (trim((string)$kw) === '') { continue; } ?>
<span class="tag"><?= mypage_h($kw) ?></span>
<?php endforeach; ?>
</div>
<button type="button" class="btn-remove-card" title="삭제"></button>
</div>
</a>
</li>
<?php
}
$html = ob_get_clean();
mypage_json([
'success' => true,
'tab' => $tab,
'page' => $page,
'limit' => $limit,
'total_count' => $countTotal,
'counts' => [
'watching' => $countWatching,
'completed' => $countCompleted,
'saved' => $countSaved,
],
'html' => $html,
'is_last' => (($offset + $limit) >= $countTotal),
]);
} catch (Throwable $e) {
mypage_json([
'success' => false,
'message' => '콘텐츠 리스트를 불러오지 못했습니다.',
'error' => $e->getMessage(),
]);
}
+144
View File
@@ -0,0 +1,144 @@
<?php
//[마이페이지][제안하기][제안등록] DB insert
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '1');
error_reporting(E_ALL);
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
/**
* JSON 응답 출력 함수
*/
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$typeCode = trim((string)($_POST['type_code'] ?? ''));
$statusCode = trim((string)($_POST['status_code'] ?? ''));
$referenceUrl = trim((string)($_POST['reference_url'] ?? ''));
$reason = trim((string)($_POST['reason'] ?? ''));
//제안자는 URL(reference_url)과 제안이유(reason)만 입력한다.
//나머지 입력이 필요한 컬럼은 관리자가 입력하게 된다.(type_code,title)
// ★ 기본값 보정
if ($typeCode === '') {
$typeCode = '';
}
if ($statusCode === '') {
$statusCode = 'OF10002';
}
// ★ 필수값 검증
if ($referenceUrl === '') {
response_json([
'success' => false,
'message' => 'URL을 입력해주세요.'
]);
}
if ($reason === '') {
response_json([
'success' => false,
'message' => '추천이유를 입력해주세요.'
]);
}
// ★ URL 형식 검증
if (!filter_var($referenceUrl, FILTER_VALIDATE_URL)) {
response_json([
'success' => false,
'message' => '올바른 URL 형식이 아닙니다.'
]);
}
// ★ 해당 년월(YYYYMM) 기준 offer_id 생성
// 예: 202603-001, 202603-002
$offerYm = date('Ym');
// ★ 같은 년월 prefix를 가진 마지막 offer_id 조회
$stmtSeq = $pdo->prepare("
SELECT offer_id
FROM edu_content_offer
WHERE offer_id LIKE :offer_prefix
ORDER BY offer_id DESC
LIMIT 1
");
$stmtSeq->execute([
':offer_prefix' => $offerYm . '-%',
]);
$lastOfferId = (string)$stmtSeq->fetchColumn();
$nextSeq = 1;
// ★ 마지막 번호가 있으면 뒤 3자리 숫자를 추출해서 +1
if ($lastOfferId !== '') {
$lastSeq = (int)substr($lastOfferId, -3);
$nextSeq = $lastSeq + 1;
}
$offerId = $offerYm . '-' . str_pad((string)$nextSeq, 3, '0', STR_PAD_LEFT);
// ★ 저장
$stmt = $pdo->prepare("
INSERT INTO edu_content_offer (
offer_id,
member_id,
sys_comp_code,
reference_url,
reason,
status_code,
created_at,
updated_at
) VALUES (
:offer_id,
:member_id,
:sys_comp_code,
:reference_url,
:reason,
:status_code,
NOW(),
NOW()
)
");
$stmt->execute([
':offer_id' => $offerId,
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':reference_url' => $referenceUrl,
':reason' => $reason,
':status_code' => $statusCode,
]);
response_json([
'success' => true,
'message' => '제안이 정상적으로 등록되었습니다.',
'offer_id' => $offerId,
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '제안 등록 중 오류가 발생했습니다.',
'error' => $e->getMessage(),
]);
}
+104
View File
@@ -0,0 +1,104 @@
<?php
// pros_users_inup.php
/**
* 사용자 정보 동기화 및 이력 저장 함수
* @param PDO $pdo DB연결 객체
* @param array $userData API로부터 받은 사용자 배열 데이터
* @return array 처리 결과 카운트
*/
function processUserSync($pdo, $userData) {
$insCount = 0;
$updCount = 0;
$errCount = 0;
foreach ($userData as $user) {
$mid = $user['member_id'];
$scc = $user['sys_comp_code'];
if (empty($mid) || empty($scc)) continue; // 필수 키 없으면 패스
try {
// 1. 존재 여부 확인 (복합키 기준)
$checkSql = "SELECT COUNT(*) FROM edu_users_temp WHERE member_id = ? AND sys_comp_code = ?";
$stmt = $pdo->prepare($checkSql);
$stmt->execute([$mid, $scc]);
$exists = $stmt->fetchColumn() > 0;
// 날짜 세팅 (비어있거나 잘못된 날짜 처리)
$join_date = (!empty($user['join_date']) && $user['join_date'] != "0000-00-00") ? $user['join_date'] : null;
$end_date = (!empty($user['end_date']) && $user['end_date'] != "0000-00-00") ? $user['end_date'] : null;
if ($exists) {
// 2-1. UPDATE (기존 정보 갱신)
$sql = "UPDATE edu_users_temp SET
name = :name, dept_name = :dept_name, rank_name = :rank_name,
join_date = :join_date, belong_comp = :belong_comp, working_comp = :working_comp,
end_date = :end_date, belong_comp_id = :belong_comp_id, working_comp_id = :working_comp_id,
updated_at = NOW()
WHERE member_id = :member_id AND sys_comp_code = :sys_comp_code";
$action = "UPDATE";
} else {
// 2-2. INSERT (신규 등록)
$sql = "INSERT INTO edu_users_temp (
member_id, sys_comp_code, name, dept_name, rank_name,
join_date, belong_comp, working_comp, end_date,
belong_comp_id, working_comp_id, created_at, updated_at
) VALUES (
:member_id, :sys_comp_code, :name, :dept_name, :rank_name,
:join_date, :belong_comp, :working_comp, :end_date,
:belong_comp_id, :working_comp_id, NOW(), NOW()
)";
$action = "INSERT";
}
$stmt = $pdo->prepare($sql);
$res = $stmt->execute([
':member_id' => $mid,
':sys_comp_code' => $scc,
':name' => $user['name'] ?? '',
':dept_name' => $user['dept_name'] ?? '',
':rank_name' => $user['rank_name'] ?? '',
':join_date' => $join_date,
':belong_comp' => $user['belong_comp'] ?? '',
':working_comp' => $user['working_comp'] ?? '',
':end_date' => $end_date,
':belong_comp_id' => $user['belong_comp_id'] ?? '',
':working_comp_id'=> $user['working_comp_id'] ?? ''
]);
if ($res) {
if ($action == "INSERT") {
$insCount++;
// [INSERT 전용 이력] 생성일시만 기록
$hisSql = "INSERT INTO edu_userinfo_history (
uh_member_id, uh_sys_comp_code, uh_created_at, uh_updated_at, uh_etc_01
) VALUES (
:mid, :scc, NOW(), NULL, :etc
)";
} else {
$updCount++;
// [UPDATE 전용 이력] 수정일시만 기록
$hisSql = "INSERT INTO edu_userinfo_history (
uh_member_id, uh_sys_comp_code, uh_created_at, uh_updated_at, uh_etc_01
) VALUES (
:mid, :scc, NULL, NOW(), :etc
)";
}
$hisStmt = $pdo->prepare($hisSql);
$hisStmt->execute([
':mid' => $mid,
':scc' => $scc,
':etc' => $action . " 수행 완료"
]);
}
} catch (PDOException $e) {
error_log("Sync Error ($mid): " . $e->getMessage());
$errCount++;
}
}
return ['ins' => $insCount, 'upd' => $updCount, 'err' => $errCount];
}
+239
View File
@@ -0,0 +1,239 @@
<?php
//=========================
// [마이페이지][북마크 토글]
// - 저장한 콘텐츠 체크 / 체크해제 처리
// - 체크해제 시 실제 delete 하지 않고 is_active 값을 0으로 update
// - 체크 시 is_active 값을 1로 update 또는 신규 insert
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
// ---------------------------------------------------------
// 세션 시작
// TODO: 실제 로그인 세션 연동 후 교체
// ---------------------------------------------------------
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
// 테스트용 임시값
// $memberId = 'U001';
// $sysCompCode = 'COMP01';
/**
* JSON 응답 출력 후 종료
*/
function response_json(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ---------------------------------------------------------
// 1. 요청 JSON 읽기
// - 프런트에서 fetch + JSON body로 전달
// ---------------------------------------------------------
$raw = file_get_contents('php://input');
$req = json_decode($raw, true);
$contentId = trim((string)($req['content_id'] ?? ''));
$isChecked = trim((string)($req['is_checked'] ?? ''));
// ---------------------------------------------------------
// 2. 기본 유효성 검사
// ---------------------------------------------------------
if ($memberId === '' || $sysCompCode === '') {
response_json([
'success' => false,
'message' => '로그인 정보가 없습니다.'
]);
}
if ($contentId === '') {
response_json([
'success' => false,
'message' => 'content_id가 없습니다.'
]);
}
// ---------------------------------------------------------
// 3. 체크값 정규화
// - 현재 운영 중 프런트는 Y/N을 보낼 수 있고,
// 향후 1/0으로 바뀔 가능성도 있으므로 둘 다 허용
// - 최종적으로 DB 저장용 is_active 값은 '1' / '0' 으로 맞춘다.
// ---------------------------------------------------------
if ($isChecked === 'Y' || $isChecked === '1') {
$activeValue = '1';
} elseif ($isChecked === 'N' || $isChecked === '0') {
$activeValue = '0';
} else {
response_json([
'success' => false,
'message' => '유효하지 않은 요청입니다.'
]);
}
// ---------------------------------------------------------
// 4. 기존 북마크 이력 존재 여부 확인
// - member_id + sys_comp_code + content_id 조합 기준
// ---------------------------------------------------------
$stmtCheck = $pdo->prepare("
SELECT COUNT(*) AS cnt
FROM edu_content_wishlist
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND content_id = :content_id
");
$stmtCheck->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':content_id' => $contentId,
]);
$exists = (int)$stmtCheck->fetchColumn();
// ---------------------------------------------------------
// 5. INSERT / UPDATE 처리
//
// [기존 row 있음]
// - is_active만 update
// - updated_at 갱신
// - 체크(활성화) 시 favorited_at도 NOW()로 갱신
//
// [기존 row 없음]
// - 신규 insert
// - created_at / updated_at 저장
// - 체크 상태가 1이면 favorited_at도 NOW() 저장
//
// ※ 실제 delete 하지 않고 soft delete 방식 유지
// ---------------------------------------------------------
if ($exists > 0) {
if ($activeValue === '1') {
// 체크 ON
$stmtUpdate = $pdo->prepare("
UPDATE edu_content_wishlist
SET is_active = :is_active,
favorited_at = NOW(),
updated_at = NOW()
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND content_id = :content_id
");
} else {
// 체크 OFF
$stmtUpdate = $pdo->prepare("
UPDATE edu_content_wishlist
SET is_active = :is_active,
updated_at = NOW()
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
AND content_id = :content_id
");
}
$stmtUpdate->execute([
':is_active' => $activeValue,
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':content_id' => $contentId,
]);
} else {
if ($activeValue === '1') {
// 신규 체크 ON insert
$stmtInsert = $pdo->prepare("
INSERT INTO edu_content_wishlist (
member_id,
sys_comp_code,
content_id,
is_active,
favorited_at,
created_at,
updated_at
) VALUES (
:member_id,
:sys_comp_code,
:content_id,
:is_active,
NOW(),
NOW(),
NOW()
)
");
} else {
// 신규 체크 OFF 요청이 들어온 경우도 예외 없이 기록은 남김
// (운영상 불필요하다면 이 분기는 막아도 됨)
$stmtInsert = $pdo->prepare("
INSERT INTO edu_content_wishlist (
member_id,
sys_comp_code,
content_id,
is_active,
created_at,
updated_at
) VALUES (
:member_id,
:sys_comp_code,
:content_id,
:is_active,
NOW(),
NOW()
)
");
}
$stmtInsert->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':content_id' => $contentId,
':is_active' => $activeValue,
]);
}
// ---------------------------------------------------------
// 6. saved 탭 카운트만 재계산
// - 북마크 토글은 saved 상태만 변경하므로
// watching / completed 카운트는 여기서 건드리지 않는다.
// ---------------------------------------------------------
$stmtSavedCount = $pdo->prepare("
SELECT COUNT(*)
FROM edu_content_wishlist cw
WHERE cw.member_id = :saved_member_id
AND cw.sys_comp_code = :saved_sys_comp_code
AND cw.is_active = '1'
");
$stmtSavedCount->execute([
':saved_member_id' => $memberId,
':saved_sys_comp_code' => $sysCompCode,
]);
$savedCount = (int)$stmtSavedCount->fetchColumn();
// ---------------------------------------------------------
// 7. 성공 응답 반환
// ---------------------------------------------------------
response_json([
'success' => true,
'content_id' => $contentId,
'is_checked' => $activeValue,
'counts' => [
'saved' => $savedCount,
]
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '북마크 처리 중 오류가 발생했습니다.',
'error' => $e->getMessage(),
]);
}
+166
View File
@@ -0,0 +1,166 @@
<?php
require_once __DIR__ . '/../bbs/db_conn.php';
header('Content-Type: application/json; charset=utf-8');
function response_json($data) {
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// 세션 (실제 환경 기준)
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode= $_SESSION['sys_comp_code'] ?? '';
// 테스트용 (필요시 사용)
// $memberId = 'U001';
// $sysCompCode = 'COMP01';
try {
if (!$memberId || !$sysCompCode) {
response_json([
'success' => false,
'message' => '로그인 정보가 없습니다.'
]);
}
if (!isset($_FILES['profile_image'])) {
response_json([
'success' => false,
'message' => '파일이 없습니다.'
]);
}
$file = $_FILES['profile_image'];
if ($file['error'] !== UPLOAD_ERR_OK) {
response_json([
'success' => false,
'message' => '업로드 오류 발생'
]);
}
// 2차 검사: 서버에서 3MB 제한
$maxSize = 2 * 1024 * 1024; // 2MB
if ($file['size'] > $maxSize) {
response_json([
'success' => false,
'message' => '2MB 이하의 이미지 파일만 업로드 가능합니다.'
]);
}
// MIME 체크
$mime = mime_content_type($file['tmp_name']);
if (!in_array($mime, ['image/jpeg', 'image/png'])) {
response_json([
'success' => false,
'message' => 'jpg, png 파일만 업로드 가능합니다.'
]);
}
// 이미지 생성
if ($mime === 'image/jpeg') {
$srcImage = imagecreatefromjpeg($file['tmp_name']);
} else {
$srcImage = imagecreatefrompng($file['tmp_name']);
}
if (!$srcImage) {
response_json([
'success' => false,
'message' => '이미지 처리 실패'
]);
}
/*
// 원본 사이즈
$width = imagesx($srcImage);
$height = imagesy($srcImage);
// 새 이미지 생성 (png)
$newImage = imagecreatetruecolor($width, $height);
// 투명 배경 처리
imagealphablending($newImage, false);
imagesavealpha($newImage, true);
$transparent = imagecolorallocatealpha($newImage, 0, 0, 0, 127);
imagefilledrectangle($newImage, 0, 0, $width, $height, $transparent);
// 복사
imagecopy($newImage, $srcImage, 0, 0, 0, 0, $width, $height);
*/
// 원본 사이즈
$srcWidth = imagesx($srcImage);
$srcHeight = imagesy($srcImage);
// 최대 사이즈 (긴 변 기준)
$maxSize = 250;
// 비율 계산
if ($srcWidth >= $srcHeight) {
$newWidth = $maxSize;
$newHeight = intval($srcHeight * ($maxSize / $srcWidth));
} else {
$newHeight = $maxSize;
$newWidth = intval($srcWidth * ($maxSize / $srcHeight));
}
// 새 이미지 생성
$newImage = imagecreatetruecolor($newWidth, $newHeight);
// 투명 배경
imagealphablending($newImage, false);
imagesavealpha($newImage, true);
$transparent = imagecolorallocatealpha($newImage, 0, 0, 0, 127);
imagefilledrectangle($newImage, 0, 0, $newWidth, $newHeight, $transparent);
// 리사이즈 복사
imagecopyresampled(
$newImage,
$srcImage,
0, 0, 0, 0,
$newWidth, $newHeight,
$srcWidth, $srcHeight
);
// 저장 경로
$fileName = $memberId . '_' . $sysCompCode . '.png';
$savePath = __DIR__ . '/../img/profile/' . $fileName;
$saveUrl = '/img/profile/' . $fileName;
// 저장 (덮어쓰기)
if (!imagepng($newImage, $savePath)) {
response_json([
'success' => false,
'message' => '파일 저장 실패'
]);
}
// 메모리 해제
imagedestroy($srcImage);
imagedestroy($newImage);
response_json([
'success' => true,
'image_url' => $saveUrl
]);
} catch (Throwable $e) {
response_json([
'success' => false,
'message' => '프로필 업로드 중 오류가 발생했습니다.',
'error' => $e->getMessage()
]);
}