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
+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()
]);
}