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
@@ -0,0 +1,222 @@
<?php
//=========================
//개인정보 + 최신연도 학습레벨 + 배지
//=========================
require_once __DIR__ . '/db_conn.php';
if (!function_exists('mypage_h')) {
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
if (!function_exists('mypage_level_meta')) {
function mypage_level_meta(?string $level): array
{
$level = trim((string)$level);
$map = [
'MASTER' => [
'code' => 'Master',
'class' => 'master',
'label' => 'Master',
'icon' => '/edu/img/ico/ico_level_master.svg',
],
'ELITE' => [
'code' => 'Elite',
'class' => 'elite',
'label' => 'Elite',
'icon' => '/edu/img/ico/ico_level_elite.svg',
],
'LEARNER' => [
'code' => 'Learner',
'class' => 'learner',
'label' => 'Learner',
'icon' => '/edu/img/ico/ico_level_learner.svg',
],
'ROOKIE' => [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
],
];
$key = strtoupper($level);
return $map[$key] ?? [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
];
}
}
if (!function_exists('mypage_badge_map')) {
function mypage_badge_map(string $badgeCode): array
{
$badgeCode = strtoupper(trim($badgeCode));
$BADGE_ICON_MAP = [
// 실제 운영 시 코드값 확정되면 여기만 교체
'BG001' => [
'slot' => 'hat',
'img' => '/edu/img/mypage/ico_school.png',
'name' => '학습 배지',
],
'BG002' => [
'slot' => 'pencil',
'img' => '/edu/img/mypage/ico_pencil.png',
'name' => '작성 배지',
],
'BG003' => [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => 'Pick 배지',
],
];
return $BADGE_ICON_MAP[$badgeCode] ?? [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => '기본 배지',
];
}
}
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
$profileData = [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'name' => '사용자',
'rank_name' => '',
'working_comp' => '',
'join_date' => '',
'latest_stats_year' => '',
'learning_level' => 'Rookie',
'level_class' => 'rookie',
'level_icon' => '/edu/img/ico/ico_level_rookie.svg',
'total_minutes' => 0,
'profile_image' => '/edu/img/insight/profile.png', // 현재는 기본 이미지 고정
];
$profileBadges = [
'hat' => null,
'pencil' => null,
'pick' => null,
];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 최신년도 학습통계 우선
$sql = "
SELECT
u.member_id,
u.sys_comp_code,
u.name,
u.rank_name,
u.working_comp,
u.join_date,
ys.stats_year,
ys.learning_level,
ys.total_minutes
FROM edu_users u
LEFT JOIN edu_yearly_learning_stats ys
ON ys.member_id = u.member_id
AND ys.sys_comp_code = u.sys_comp_code
AND ys.stats_year = (
SELECT MAX(stats_year)
FROM edu_yearly_learning_stats
WHERE member_id = u.member_id
AND sys_comp_code = u.sys_comp_code
)
WHERE u.member_id = :member_id
AND u.sys_comp_code = :sys_comp_code
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$levelMeta = mypage_level_meta($row['learning_level'] ?? '');
$profileData = [
'member_id' => $row['member_id'],
'sys_comp_code' => $row['sys_comp_code'],
'name' => $row['name'] ?? '사용자',
'rank_name' => $row['rank_name'] ?? '',
'working_comp' => $row['working_comp'] ?? '',
'join_date' => $row['join_date'] ?? '',
'latest_stats_year' => $row['stats_year'] ?? '',
'learning_level' => $levelMeta['label'],
'level_class' => $levelMeta['class'],
'level_icon' => $levelMeta['icon'],
'total_minutes' => (int)($row['total_minutes'] ?? 0),
'profile_image' => '/edu/img/insight/profile.png',
];
}
// 최신통계가 없으면 누적 이력 합계 fallback
if ((int)$profileData['total_minutes'] === 0) {
$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
");
$stmtFallback->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$fallbackMin = (int)$stmtFallback->fetchColumn();
if ($fallbackMin > 0) {
$profileData['total_minutes'] = $fallbackMin;
}
}
// 배지 조회
$stmtBadge = $pdo->prepare("
SELECT badge_code, issued_at
FROM edu_user_badges
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
ORDER BY issued_at DESC, seq DESC
");
$stmtBadge->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmtBadge->fetchAll(PDO::FETCH_ASSOC) as $badgeRow) {
$badgeInfo = mypage_badge_map($badgeRow['badge_code'] ?? '');
$slot = $badgeInfo['slot'];
if (!isset($profileBadges[$slot]) || $profileBadges[$slot] !== null) {
continue;
}
$profileBadges[$slot] = [
'badge_code' => $badgeRow['badge_code'],
'slot' => $slot,
'img' => $badgeInfo['img'],
'name' => $badgeInfo['name'],
'issued_at' => $badgeRow['issued_at'],
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
@@ -0,0 +1,222 @@
<?php
//=========================
//개인정보 + 최신연도 학습레벨 + 배지
//=========================
require_once __DIR__ . '/db_conn.php';
if (!function_exists('mypage_h')) {
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
if (!function_exists('mypage_level_meta')) {
function mypage_level_meta(?string $level): array
{
$level = trim((string)$level);
$map = [
'MASTER' => [
'code' => 'Master',
'class' => 'master',
'label' => 'Master',
'icon' => '/edu/img/ico/ico_level_master.svg',
],
'ELITE' => [
'code' => 'Elite',
'class' => 'elite',
'label' => 'Elite',
'icon' => '/edu/img/ico/ico_level_elite.svg',
],
'LEARNER' => [
'code' => 'Learner',
'class' => 'learner',
'label' => 'Learner',
'icon' => '/edu/img/ico/ico_level_learner.svg',
],
'ROOKIE' => [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
],
];
$key = strtoupper($level);
return $map[$key] ?? [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
];
}
}
if (!function_exists('mypage_badge_map')) {
function mypage_badge_map(string $badgeCode): array
{
$badgeCode = strtoupper(trim($badgeCode));
$BADGE_ICON_MAP = [
// 실제 운영 시 코드값 확정되면 여기만 교체
'BG001' => [
'slot' => 'hat',
'img' => '/edu/img/mypage/ico_school.png',
'name' => '학습 배지',
],
'BG002' => [
'slot' => 'pencil',
'img' => '/edu/img/mypage/ico_pencil.png',
'name' => '작성 배지',
],
'BG003' => [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => 'Pick 배지',
],
];
return $BADGE_ICON_MAP[$badgeCode] ?? [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => '기본 배지',
];
}
}
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
$profileData = [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'name' => '사용자',
'rank_name' => '',
'working_comp' => '',
'join_date' => '',
'latest_stats_year' => '',
'learning_level' => 'Rookie',
'level_class' => 'rookie',
'level_icon' => '/edu/img/ico/ico_level_rookie.svg',
'total_minutes' => 0,
'profile_image' => '/edu/img/insight/profile.png', // 현재는 기본 이미지 고정
];
$profileBadges = [
'hat' => null,
'pencil' => null,
'pick' => null,
];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 최신년도 학습통계 우선
$sql = "
SELECT
u.member_id,
u.sys_comp_code,
u.name,
u.rank_name,
u.working_comp,
u.join_date,
ys.stats_year,
ys.learning_level,
ys.total_minutes
FROM edu_users u
LEFT JOIN edu_yearly_learning_stats ys
ON ys.member_id = u.member_id
AND ys.sys_comp_code = u.sys_comp_code
AND ys.stats_year = (
SELECT MAX(stats_year)
FROM edu_yearly_learning_stats
WHERE member_id = u.member_id
AND sys_comp_code = u.sys_comp_code
)
WHERE u.member_id = :member_id
AND u.sys_comp_code = :sys_comp_code
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$levelMeta = mypage_level_meta($row['learning_level'] ?? '');
$profileData = [
'member_id' => $row['member_id'],
'sys_comp_code' => $row['sys_comp_code'],
'name' => $row['name'] ?? '사용자',
'rank_name' => $row['rank_name'] ?? '',
'working_comp' => $row['working_comp'] ?? '',
'join_date' => $row['join_date'] ?? '',
'latest_stats_year' => $row['stats_year'] ?? '',
'learning_level' => $levelMeta['label'],
'level_class' => $levelMeta['class'],
'level_icon' => $levelMeta['icon'],
'total_minutes' => (int)($row['total_minutes'] ?? 0),
'profile_image' => '/edu/img/insight/profile.png',
];
}
// 최신통계가 없으면 누적 이력 합계 fallback
if ((int)$profileData['total_minutes'] === 0) {
$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
");
$stmtFallback->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$fallbackMin = (int)$stmtFallback->fetchColumn();
if ($fallbackMin > 0) {
$profileData['total_minutes'] = $fallbackMin;
}
}
// 배지 조회
$stmtBadge = $pdo->prepare("
SELECT badge_code, issued_at
FROM edu_user_badges
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
ORDER BY issued_at DESC, seq DESC
");
$stmtBadge->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmtBadge->fetchAll(PDO::FETCH_ASSOC) as $badgeRow) {
$badgeInfo = mypage_badge_map($badgeRow['badge_code'] ?? '');
$slot = $badgeInfo['slot'];
if (!isset($profileBadges[$slot]) || $profileBadges[$slot] !== null) {
continue;
}
$profileBadges[$slot] = [
'badge_code' => $badgeRow['badge_code'],
'slot' => $slot,
'img' => $badgeInfo['img'],
'name' => $badgeInfo['name'],
'issued_at' => $badgeRow['issued_at'],
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
@@ -0,0 +1,285 @@
<?php
//=========================
// 개인정보 + 배지
//=========================
require_once __DIR__ . '/db_conn.php';
if (!function_exists('mypage_h')) {
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
if (!function_exists('mypage_level_meta')) {
function mypage_level_meta(?string $level): array
{
$level = trim((string)$level);
$map = [
'MASTER' => [
'code' => 'Master',
'class' => 'master',
'label' => 'Master',
'icon' => '/edu/img/ico/ico_level_master.svg',
],
'ELITE' => [
'code' => 'Elite',
'class' => 'elite',
'label' => 'Elite',
'icon' => '/edu/img/ico/ico_level_elite.svg',
],
'LEARNER' => [
'code' => 'Learner',
'class' => 'learner',
'label' => 'Learner',
'icon' => '/edu/img/ico/ico_level_learner.svg',
],
'ROOKIE' => [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
],
];
$key = strtoupper($level);
return $map[$key] ?? [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
];
}
}
if (!function_exists('mypage_badge_map')) {
function mypage_badge_map(string $badgeCode): array
{
$badgeCode = strtoupper(trim($badgeCode));
$BADGE_ICON_MAP = [
// 실제 운영 시 코드값 확정되면 여기만 교체
'BG001' => [
'slot' => 'hat',
'img' => '/edu/img/mypage/ico_school.png',
'name' => '학습 배지',
],
'BG002' => [
'slot' => 'pencil',
'img' => '/edu/img/mypage/ico_pencil.png',
'name' => '작성 배지',
],
'BG003' => [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => 'Pick 배지',
],
];
return $BADGE_ICON_MAP[$badgeCode] ?? [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => '기본 배지',
];
}
}
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
//echo "memberId===".$memberId."<br>";
//echo "sysCompCode===".$sysCompCode."<br>";exit;
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
// ★ 선택년도 수집
$selectedYear = trim((string)($_GET['selected_year'] ?? date('Y')));
if ($selectedYear === '' || !preg_match('/^\d{4}$/', $selectedYear)) {
$selectedYear = date('Y');
}
$profileFileName = $memberId . '_' . $sysCompCode . '.png';
$profileFilePath = __DIR__ . '/../img/profile/' . $profileFileName;
$profileImageUrl = '/edu/img/profile/' . $profileFileName;
if (file_exists($profileFilePath)) {
$profileImage = $profileImageUrl;
/*
if($memberId=="M21420"){
echo 11111111;
exit;
}
*/
} else {
/*
if($memberId=="M21420"){
echo 222222222222;
exit;
}*/
$profileImage = '/edu/img/ico/ico_profile.svg';
}
/*
if (file_exists($profileFilePath)) {
$profileImage = $profileImageUrl;
} else {
$profileImage = '/edu/img/ico/ico_profile.svg';
}
*/
$profileData = [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'name' => '사용자',
'rank_name' => '',
'working_comp' => '',
'join_date' => '',
'latest_stats_year' => $selectedYear,
'learning_level' => 'Rookie',
'level_class' => 'rookie',
'level_icon' => '/edu/img/ico/ico_level_rookie.svg',
'total_minutes' => 0,
'profile_image' => $profileImage, // 현재는 기본 이미지 고정
];
$profileBadges = [
'hat' => null,
'pencil' => null,
'pick' => null,
];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ★ 선택년도 학습통계 기준
$sql = "
SELECT
u.member_id,
u.sys_comp_code,
u.name,
u.rank_name,
u.working_comp,
u.join_date,
ec.code,
ec.code_name,
ys.stats_year,
ys.learning_level,
ys.total_minutes
FROM edu_users u
LEFT JOIN edu_yearly_learning_stats ys
ON ys.member_id = u.member_id
AND ys.sys_comp_code = u.sys_comp_code
AND ys.stats_year = :selected_year
LEFT JOIN
(
SELECT code,code_name FROM edu_codes WHERE group_code = 'CO100'
) ec
ON ec.code = u.belong_comp
WHERE u.member_id = :member_id
AND u.sys_comp_code = :sys_comp_code
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$levelMeta = mypage_level_meta($row['learning_level'] ?? '');
$profileData = [
'member_id' => $row['member_id'],
'sys_comp_code' => $row['sys_comp_code'],
'name' => $row['name'] ?? '사용자',
'rank_name' => $row['rank_name'] ?? '',
'belong_comp_code' => $row['code'] ?? '',
'belong_comp_name' => $row['code_name'] ?? '',
'working_comp' => $row['working_comp'] ?? '',
'join_date' => $row['join_date'] ?? '',
'latest_stats_year' => $row['stats_year'] ?? $selectedYear,
'learning_level' => $levelMeta['label'],
'level_class' => $levelMeta['class'],
'level_icon' => $levelMeta['icon'],
'total_minutes' => (int)($row['total_minutes'] ?? 0),
'profile_image' => $profileImage, // 현재는 기본 이미지 고정
];
}
// ★ 선택년도 통계가 없으면 선택년도 학습이력 합계 fallback
if ((int)$profileData['total_minutes'] === 0) {
$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,
]);
$fallbackMin = (int)$stmtFallback->fetchColumn();
if ($fallbackMin > 0) {
$profileData['total_minutes'] = $fallbackMin;
}
}
// 배지 조회
$stmtBadge = $pdo->prepare("
SELECT badge_code, issued_at
FROM edu_user_badges
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
ORDER BY issued_at DESC, seq DESC
");
$stmtBadge->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmtBadge->fetchAll(PDO::FETCH_ASSOC) as $badgeRow) {
$badgeInfo = mypage_badge_map($badgeRow['badge_code'] ?? '');
$slot = $badgeInfo['slot'];
if (!isset($profileBadges[$slot]) || $profileBadges[$slot] !== null) {
continue;
}
$profileBadges[$slot] = [
'badge_code' => $badgeRow['badge_code'],
'slot' => $slot,
'img' => $badgeInfo['img'],
'name' => $badgeInfo['name'],
'issued_at' => $badgeRow['issued_at'],
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
@@ -0,0 +1,99 @@
<?php
//=========================
// 컨텐츠 제안하기 기본값 + 제안현황
//=========================
require_once __DIR__ . '/db_conn.php';
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
$offerDefaultTypeCode = 'OF10001'; // 제안
$offerDefaultStatusCode = 'OF10002'; // 검토중
$offerStatusMap = [
'OF10001' => '제안',
'OF10002' => '검토중',
'OF10003' => '게시완료',
'OF10004' => '게시불가',
];
$offerHistory = [];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 상태 코드맵 동적 조회 (기본값 fallback 유지)
$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 $codeRow) {
$offerStatusMap[$codeRow['base_code']] = $codeRow['code_name'];
}
// 제안 이력 조회
// 정렬 우선순위:
// 1) 검토중(OF10002) 최상단
// 2) 그 외 상태는 코드 우선순위대로
// 3) 같은 상태 안에서는 최신순
$stmt = $pdo->prepare("
SELECT
co.offer_id,
co.type_code,
co.title,
co.reference_url,
co.reason,
co.status_code,
co.created_at,
co.updated_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,
co.offer_id DESC
LIMIT 20
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$statusCode = trim((string)($row['status_code'] ?? ''));
$offerHistory[] = [
'offer_id' => $row['offer_id'] ?? '',
'type_code' => $row['type_code'] ?? '',
'title' => $row['title'] ?? '',
'reference_url' => $row['reference_url'] ?? '',
'reason' => $row['reason'] ?? '',
'status_code' => $statusCode,
'status_name' => $offerStatusMap[$statusCode] ?? $statusCode,
'created_at' => $row['created_at'] ?? '',
'updated_at' => $row['updated_at'] ?? '',
'created_at_dot' => !empty($row['created_at']) ? date('y.m.d', strtotime($row['created_at'])) : '',
'is_consider' => ($statusCode === 'OF10002'),
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
// error_log($e->getMessage());
}
@@ -0,0 +1,59 @@
<?php
//=========================
//한줄소감
//=========================
require_once __DIR__ . '/db_conn.php';
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
$myCommentList = [];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$sql = "
SELECT
lh.content_id,
lh.comment,
lh.last_viewed_at,
c.title,
c.category_code,
c.category_group,
u.name
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
INNER JOIN edu_users u
ON u.member_id = lh.member_id
AND u.sys_comp_code = lh.sys_comp_code
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :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
LIMIT 20
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$myCommentList[] = [
'content_id' => $row['content_id'],
'title' => $row['title'],
'comment' => $row['comment'],
'member_name' => $row['name'],
'last_viewed_at' => $row['last_viewed_at'],
'date_dot' => !empty($row['last_viewed_at']) ? date('y.m.d', strtotime($row['last_viewed_at'])) : '',
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../db_conn.php';
$pdo = db_conn();
echo "--- SHOW CREATE TABLE ---\n";
$row = $pdo->query('SHOW CREATE TABLE edu_learning_histories')->fetch(PDO::FETCH_ASSOC);
echo ($row['Create Table'] ?? 'N/A') . "\n\n";
echo "--- INDEXES ---\n";
$stmt = $pdo->query('SHOW INDEX FROM edu_learning_histories');
foreach ($stmt as $r) {
echo ($r['Key_name'] ?? ''), "\t",
($r['Seq_in_index'] ?? ''), "\t",
($r['Column_name'] ?? ''), "\t",
((int)($r['Non_unique'] ?? 1) === 0 ? 'UNIQUE' : 'NON_UNIQUE'),
"\n";
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../db_conn.php';
$pdo = db_conn();
$pdo->exec('ALTER TABLE edu_learning_histories DROP PRIMARY KEY, ADD PRIMARY KEY (member_id, sys_comp_code, content_id)');
echo "ALTER_OK\n";
+39
View File
@@ -0,0 +1,39 @@
<?php
header('Content-Type: text/plain; charset=utf-8');
try {
require_once __DIR__ . '/../db_conn.php';
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 1. edu_codes에서 keyword 관련 group_code 확인
echo "=== edu_codes: keyword 관련 group_code 목록 ===\n";
$stmt = $pdo->query("SELECT group_code, COUNT(*) as cnt FROM edu_codes WHERE group_code LIKE '%KW%' OR base_code LIKE 'KW%' GROUP BY group_code");
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
echo "group_code={$r['group_code']} => {$r['cnt']}\n";
}
echo "\n=== edu_codes: KW로 시작하는 base_code 전체 ===\n";
$stmt = $pdo->query("SELECT group_code, base_code, code_name FROM edu_codes WHERE base_code LIKE 'KW%' ORDER BY group_code, base_code LIMIT 30");
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
echo "[{$r['group_code']}] {$r['base_code']} => {$r['code_name']}\n";
}
echo "\n=== edu_content_keywords: keyword_code 분포 (상위 10개) ===\n";
$stmt = $pdo->query("SELECT keyword_code, COUNT(*) as cnt FROM edu_content_keywords GROUP BY keyword_code ORDER BY cnt DESC LIMIT 10");
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
echo "{$r['keyword_code']} => {$r['cnt']}\n";
}
echo "\n=== edu_contents: 전체 / is_offer=1 / is_offer!=1 건수 ===\n";
$total = $pdo->query("SELECT COUNT(*) FROM edu_contents WHERE is_active=1 OR is_active IS NULL")->fetchColumn();
$offer = $pdo->query("SELECT COUNT(*) FROM edu_contents WHERE is_offer=1 AND (is_active=1 OR is_active IS NULL)")->fetchColumn();
$normal = $pdo->query("SELECT COUNT(*) FROM edu_contents WHERE (is_offer IS NULL OR is_offer!=1) AND (is_active=1 OR is_active IS NULL)")->fetchColumn();
echo "전체={$total}, offer(Pick)={$offer}, 일반={$normal}\n";
echo "\n=== 일반 영상 중 keyword 매핑 있는 건수 ===\n";
$cnt = $pdo->query("SELECT COUNT(DISTINCT c.content_id) FROM edu_contents c JOIN edu_content_keywords ck ON ck.content_id=c.content_id WHERE (c.is_offer IS NULL OR c.is_offer!=1) AND (c.is_active=1 OR c.is_active IS NULL)")->fetchColumn();
echo "키워드 매핑 있는 일반 영상={$cnt}\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
+59
View File
@@ -0,0 +1,59 @@
<?php
header('Content-Type: text/plain; charset=utf-8');
try {
require_once __DIR__ . '/../db_conn.php';
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
echo "=== 1. edu_codes KW100 키워드 수 ===\n";
$cnt = $pdo->query("SELECT COUNT(*) FROM edu_codes WHERE group_code = 'KW100'")->fetchColumn();
echo "count={$cnt}\n";
echo "\n=== 1b. edu_codes 컬럼 목록 ===\n";
try {
$cols = $pdo->query("SHOW COLUMNS FROM edu_codes")->fetchAll(PDO::FETCH_ASSOC);
foreach ($cols as $c) echo $c['Field'] . " (" . $c['Type'] . ")\n";
} catch (Exception $e) { echo "에러: " . $e->getMessage() . "\n"; }
echo "\n=== 1c. main_data.php 실제 쿼리 테스트 ===\n";
try {
$rows2 = $pdo->query("SELECT base_code AS keyword_code, code_name AS keyword_name FROM edu_codes WHERE group_code = 'KW100' ORDER BY base_code")->fetchAll(PDO::FETCH_ASSOC);
echo count($rows2) . "건 반환\n";
if ($rows2) echo json_encode($rows2[0], JSON_UNESCAPED_UNICODE) . " ...\n";
} catch (Exception $e) { echo "에러: " . $e->getMessage() . "\n"; }
echo "\n=== 1d. sort_order 포함 쿼리 (이전 방식) ===\n";
try {
$rows3 = $pdo->query("SELECT base_code AS keyword_code, code_name AS keyword_name FROM edu_codes WHERE group_code = 'KW100' ORDER BY sort_order, base_code")->fetchAll(PDO::FETCH_ASSOC);
echo count($rows3) . "건 반환 (sort_order 존재)\n";
} catch (Exception $e) { echo "sort_order 없음: " . $e->getMessage() . "\n"; }
echo "\n=== 2. edu_recommend_keywords 테이블 존재 ===\n";
try {
$rows = $pdo->query("SELECT * FROM edu_recommend_keywords LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
echo count($rows) . "\n";
foreach ($rows as $r) echo json_encode($r, JSON_UNESCAPED_UNICODE) . "\n";
} catch (Exception $e) { echo "없음: " . $e->getMessage() . "\n"; }
echo "\n=== 3. edu_user_keywords 테이블 존재 ===\n";
try {
$cnt2 = $pdo->query("SELECT COUNT(*) FROM edu_user_keywords")->fetchColumn();
echo "count={$cnt2}\n";
} catch (Exception $e) { echo "없음: " . $e->getMessage() . "\n"; }
echo "\n=== 4. CREATE TABLE 권한 테스트 ===\n";
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS _edu_test_create (id INT) ENGINE=InnoDB");
$pdo->exec("DROP TABLE IF EXISTS _edu_test_create");
echo "CREATE 권한 있음\n";
} catch (Exception $e) { echo "CREATE 권한 없음: " . $e->getMessage() . "\n"; }
echo "\n=== 5. 세션 memberId/sysCompCode ===\n";
if (session_status() === PHP_SESSION_NONE) session_start();
$mid = $_SESSION['ss_mb_id'] ?? $_SESSION['member_id'] ?? '(없음)';
$sc = $_SESSION['sys_comp_code'] ?? '(없음)';
echo "member_id={$mid}, sys_comp_code={$sc}\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
+73
View File
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
$pdo = new PDO(
'mysql:host=baroncs.co.kr;port=3306;dbname=baronhomep;charset=utf8mb4',
'baronhomep',
'baron3840!!',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_TIMEOUT => 5,
]
);
$dbName = 'baronhomep';
$table = 'edu_learning_histories';
$out = [];
$out[] = '== TRIGGERS ON edu_learning_histories ==';
$stmt = $pdo->prepare(
'SELECT TRIGGER_NAME, EVENT_MANIPULATION, ACTION_TIMING, EVENT_OBJECT_TABLE, ACTION_STATEMENT
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = ?
AND EVENT_OBJECT_TABLE = ?
ORDER BY TRIGGER_NAME'
);
$stmt->execute([$dbName, $table]);
$triggers = $stmt->fetchAll();
if (!$triggers) {
$out[] = '(none)';
} else {
foreach ($triggers as $t) {
$out[] = sprintf('- %s | %s %s ON %s', $t['TRIGGER_NAME'], $t['ACTION_TIMING'], $t['EVENT_MANIPULATION'], $t['EVENT_OBJECT_TABLE']);
$out[] = ' ACTION: ' . preg_replace('/\s+/', ' ', (string)$t['ACTION_STATEMENT']);
}
}
$out[] = '';
$out[] = '== ROUTINES REFERENCING edu_learning_histories ==';
$stmt2 = $pdo->prepare(
'SELECT ROUTINE_TYPE, ROUTINE_NAME
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = ?
AND ROUTINE_DEFINITION LIKE ?
ORDER BY ROUTINE_TYPE, ROUTINE_NAME'
);
$stmt2->execute([$dbName, '%edu_learning_histories%']);
$routines = $stmt2->fetchAll();
if (!$routines) {
$out[] = '(none)';
} else {
foreach ($routines as $r) {
$out[] = sprintf('- %s %s', $r['ROUTINE_TYPE'], $r['ROUTINE_NAME']);
}
}
$out[] = '';
$out[] = '== COLUMNS ==';
$stmt3 = $pdo->prepare(
'SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION'
);
$stmt3->execute([$dbName, $table]);
$cols = $stmt3->fetchAll();
foreach ($cols as $c) {
$out[] = sprintf('- %s | %s | NULL=%s | KEY=%s | EXTRA=%s', $c['COLUMN_NAME'], $c['COLUMN_TYPE'], $c['IS_NULLABLE'], $c['COLUMN_KEY'], $c['EXTRA']);
}
echo implode(PHP_EOL, $out) . PHP_EOL;
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* 키워드 맵핑 확인 API
* KW10004, KW10005가 무엇으로 맵핑되는지 확인
*/
header('Content-Type: application/json; charset=utf-8');
require_once dirname(__DIR__) . '/db_conn.php';
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$result = [
'kw_codes' => ['KW10004', 'KW10005'],
'mapping' => [],
'all_kw100_sample' => [],
'recommend_kw_raw' => [],
];
// 1. KW10004, KW10005의 한글명 확인
$stmt = $pdo->prepare("SELECT base_code, code_name FROM edu_codes WHERE base_code IN ('KW10004', 'KW10005')");
$stmt->execute();
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$result['mapping'][$row['base_code']] = $row['code_name'];
}
// 2. KW100 그룹의 모든 키워드 샘플
$stmt2 = $pdo->query("SELECT base_code, code_name FROM edu_codes WHERE group_code = 'KW100' LIMIT 10");
$result['all_kw100_sample'] = $stmt2->fetchAll(PDO::FETCH_ASSOC);
// 3. edu_recommend_keywords 원본 데이터
$stmt3 = $pdo->query("SELECT * FROM edu_recommend_keywords");
$result['recommend_kw_raw'] = $stmt3->fetchAll(PDO::FETCH_ASSOC);
// 4. 최종 조인 결과 (main_data.php와 동일한 쿼리)
$stmt4 = $pdo->prepare("
SELECT rk.keyword_code, ec.code_name AS keyword_name, rk.is_active, rk.sys_comp_code
FROM edu_recommend_keywords rk
JOIN edu_codes ec ON ec.base_code = rk.keyword_code
WHERE rk.is_active = 1
");
$stmt4->execute();
$result['admin_keywords_final'] = $stmt4->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
} catch (Exception $e) {
echo json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
+67
View File
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once dirname(__DIR__) . '/db_conn.php';
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
$memberId = (string)($_SESSION['member_id'] ?? $_SESSION['user_id'] ?? '');
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? $_SESSION['company'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
$payload = [];
$rawBody = file_get_contents('php://input');
if (is_string($rawBody) && $rawBody !== '') {
$decoded = json_decode($rawBody, true);
if (is_array($decoded)) {
$payload = $decoded;
}
}
$contentId = trim((string)($payload['content_id'] ?? ''));
if ($contentId === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'content_id_required'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
if ($sysCompCode === '') {
$stmtUser = $pdo->prepare('SELECT sys_comp_code FROM edu_users WHERE member_id = ? ORDER BY sys_comp_code LIMIT 1');
$stmtUser->execute([$memberId]);
$sysCompCode = (string)($stmtUser->fetchColumn() ?: '');
}
$sql = 'UPDATE edu_learning_histories SET comment = NULL WHERE content_id = ? AND member_id = ?';
$params = [$contentId, $memberId];
if ($sysCompCode !== '') {
$sql .= ' AND sys_comp_code = ?';
$params[] = $sysCompCode;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
if ($stmt->rowCount() < 1) {
echo json_encode(['success' => false, 'message' => '삭제할 소감이 없습니다.'], JSON_UNESCAPED_UNICODE);
exit;
}
echo json_encode(['success' => true], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[clear_comment] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db_conn.php';
$memberId = (string)($_SESSION['member_id'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
$code = strtoupper(trim((string)($_GET['code'] ?? '')));
$code = preg_replace('/[^A-Z0-9]/', '', $code);
if ($code === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'code_required'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$stmt = $pdo->prepare('SELECT code_name FROM edu_codes WHERE REPLACE(REPLACE(UPPER(base_code), "-", ""), " ", "") = ? LIMIT 1');
$stmt->execute([$code]);
$codeName = (string)($stmt->fetchColumn() ?: '');
echo json_encode([
'success' => true,
'code' => $code,
'code_name' => $codeName,
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP);
} catch (Throwable $e) {
error_log('[code_name] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+146
View File
@@ -0,0 +1,146 @@
<?php
/**
* bbs 디렉토리 API 공통 함수 모듈
* 모든 API 파일에서 공통으로 사용하는 기능 제공
*/
declare(strict_types=1);
/**
* API 응답 헤더 설정
*/
function api_header_json() {
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
}
/**
* 세션 시작 및 로그인 확인
*
* @return array|null 로그인된 경우 ['member_id' => '...', 'sys_comp_code' => '...'] 반환, 아니면 null
*/
function api_get_session_user() {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = (string)($_SESSION['member_id'] ?? '');
if ($memberId === '') {
return null;
}
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
if ($sysCompCode === '') {
return null;
}
return [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
];
}
/**
* 로그인 체크 후 사용자 정보 반환
* 미로그인시 401 에러 응답 후 종료
*
* @return array ['member_id' => '...', 'sys_comp_code' => '...']
*/
function api_require_login() {
$user = api_get_session_user();
if ($user === null) {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Unauthorized',
], JSON_UNESCAPED_UNICODE);
exit;
}
return $user;
}
/**
* sys_comp_code 조회 (없으면 DB에서 조회)
*
* @param PDO $pdo 데이터베이스 연결
* @param string $memberId 회원 ID
* @param string $preferredCode 우선할 코드 (있으면 사용)
* @return string sys_comp_code 또는 빈 문자열
*/
function api_get_sys_comp_code(PDO $pdo, string $memberId, string $preferredCode = ''): string {
if ($preferredCode !== '') {
return $preferredCode;
}
$stmt = $pdo->prepare('SELECT sys_comp_code FROM edu_users WHERE member_id = ? ORDER BY sys_comp_code LIMIT 1');
$stmt->execute([$memberId]);
return (string)($stmt->fetchColumn() ?: '');
}
/**
* POST/JSON 입력 데이터 파싱
*
* @return array $_POST 또는 JSON decoded 배열
*/
function api_get_input(): array {
$input = json_decode((string)file_get_contents('php://input'), true);
if (is_array($input)) {
return $input;
}
return $_GET + $_POST;
}
/**
* content_id 유효성 확인
*
* @param PDO $pdo 데이터베이스 연결
* @param string $contentId 확인할 content_id
* @return string|null 존재하면 content_id 반환, 없으면 null
*/
function api_verify_content_id(PDO $pdo, string $contentId): ?string {
if ($contentId === '') {
return null;
}
$stmt = $pdo->prepare('SELECT content_id FROM edu_contents WHERE content_id = ? LIMIT 1');
$stmt->execute([$contentId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? (string)($row['content_id']) : null;
}
/**
* API 에러 응답
*
* @param int $httpCode HTTP 응답 코드
* @param string $message 에러 메시지
* @param array $extra 추가 정보
*/
function api_error(int $httpCode, string $message, array $extra = []) {
http_response_code($httpCode);
echo json_encode([
'success' => false,
'message' => $message,
...$extra
], JSON_UNESCAPED_UNICODE);
exit;
}
/**
* API 성공 응답
*
* @param mixed $data 응답 데이터
* @param string|null $message 성공 메시지
*/
function api_success($data = null, ?string $message = null) {
$response = ['success' => true];
if ($message !== null) {
$response['message'] = $message;
}
if ($data !== null) {
$response['data'] = $data;
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit;
}
+73
View File
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
try {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db_conn.php';
$pdo = db_conn();
$payload = [];
$rawBody = file_get_contents('php://input');
if (is_string($rawBody) && $rawBody !== '') {
$decoded = json_decode($rawBody, true);
if (is_array($decoded)) {
$payload = $decoded;
}
}
// id 또는 comment_id 파라미터 지원
$commentIdRaw = $payload['id'] ?? $payload['comment_id'] ?? $_POST['id'] ?? $_POST['comment_id'] ?? null;
$commentId = is_numeric($commentIdRaw) ? (int)$commentIdRaw : 0;
if ($commentId <= 0) {
echo json_encode([
'success' => false,
'message' => '삭제할 댓글 ID가 올바르지 않습니다.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$memberId = (string)($_SESSION['member_id'] ?? '');
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
if ($sysCompCode === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'sys_comp_code_missing'], JSON_UNESCAPED_UNICODE);
exit;
}
// edu_comments 테이블에서 DELETE (본인 댓글만)
$stmt = $pdo->prepare(
'DELETE FROM edu_comments WHERE id = ? AND member_id = ? AND sys_comp_code = ?'
);
$stmt->execute([$commentId, $memberId, $sysCompCode]);
if ($stmt->rowCount() === 0) {
echo json_encode([
'success' => false,
'message' => '삭제할 댓글이 없거나 권한이 없습니다.',
], JSON_UNESCAPED_UNICODE);
exit;
}
echo json_encode([
'success' => true,
'data' => ['id' => $commentId],
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'server_error',
], JSON_UNESCAPED_UNICODE);
}
+104
View File
@@ -0,0 +1,104 @@
<?php
/**
* bbs/api/diagnosis.php — 디버그용 진단 API
* 현재 사용자의 세션, 키워드 저장 상태, DB 연결 등을 확인
*/
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once dirname(__DIR__) . '/db_conn.php';
require_once dirname(__DIR__) . '/auth.php';
edu_start_session();
$memberId = edu_current_member_id();
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
$diag = [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'session_keys' => array_keys($_SESSION),
];
if ($memberId === '') {
echo json_encode(['success' => false, 'error' => 'not_logged_in'] + $diag, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 1. 사용자 정보
$userSt = $pdo->prepare('SELECT * FROM edu_users WHERE member_id = ? ORDER BY sys_comp_code LIMIT 1');
$userSt->execute([$memberId]);
$userRow = $userSt->fetch(PDO::FETCH_ASSOC);
$diag['user_info'] = $userRow ? [
'name' => $userRow['name'] ?? '',
'rank_name' => $userRow['rank_name'] ?? '',
'sys_comp_code' => $userRow['sys_comp_code'] ?? '',
] : null;
// 2. 저장된 키워드 (sys_comp_code 제약 없이)
$kwSt = $pdo->prepare("
SELECT uk.keyword_code, ec.code_name AS keyword_name, uk.member_id, uk.sys_comp_code
FROM edu_user_keywords uk
JOIN edu_codes ec ON ec.base_code = uk.keyword_code
WHERE uk.member_id = ?
ORDER BY uk.keyword_code
");
$kwSt->execute([$memberId]);
$savedKws = $kwSt->fetchAll(PDO::FETCH_ASSOC);
$diag['saved_keywords'] = $savedKws;
$diag['saved_keywords_count'] = count($savedKws);
// 3. 테이블 존재 확인
$tblCheck = [];
foreach (['edu_user_keywords', 'edu_codes', 'edu_recommend_keywords', 'edu_contents'] as $tbl) {
try {
$cnt = $pdo->query("SELECT COUNT(*) FROM {$tbl}")->fetchColumn();
$tblCheck[$tbl] = $cnt;
} catch (Exception $e) {
$tblCheck[$tbl] = 'ERROR: ' . $e->getMessage();
}
}
$diag['table_check'] = $tblCheck;
// 3b. edu_recommend_keywords 컬럼 확인
try {
$cols = $pdo->query("SHOW COLUMNS FROM edu_recommend_keywords")->fetchAll(PDO::FETCH_ASSOC);
$diag['edu_recommend_keywords_columns'] = array_column($cols, 'Field');
} catch (Exception $e) {
$diag['edu_recommend_keywords_columns'] = 'ERROR: ' . $e->getMessage();
}
// 4. 회사 추천 키워드 샘플 (main_data.php와 동일 규칙)
try {
$admSt = $pdo->prepare("
SELECT rk.keyword_code, ec.code_name AS keyword_name, rk.is_active, rk.sys_comp_code
FROM edu_recommend_keywords rk
JOIN edu_codes ec ON ec.base_code = rk.keyword_code
WHERE rk.is_active = 1
AND rk.keyword_code IS NOT NULL
AND rk.keyword_code <> ''
AND ec.group_code = 'KW100'
ORDER BY rk.keyword_code
LIMIT 2
");
$admSt->execute();
$admKws = $admSt->fetchAll(PDO::FETCH_ASSOC);
$diag['admin_keywords_sample'] = $admKws;
$diag['admin_keywords_count'] = count($admKws);
} catch (Exception $e) {
$diag['admin_keywords_sample'] = 'ERROR: ' . $e->getMessage();
$diag['admin_keywords_count'] = 0;
}
echo json_encode(['success' => true] + $diag, JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
] + $diag, JSON_UNESCAPED_UNICODE);
}
+124
View File
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once dirname(__DIR__) . '/db_conn.php';
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
$memberId = (string)($_SESSION['member_id'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
$contentId = trim((string)($_GET['content_id'] ?? ''));
if ($contentId === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'content_id_required'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$stmtCategory = $pdo->prepare('SELECT category_code FROM edu_contents WHERE content_id = ? LIMIT 1');
$stmtCategory->execute([$contentId]);
$categoryCode = strtoupper((string)($stmtCategory->fetchColumn() ?: ''));
// ── 마이클래스(CA10001): edu_learning_histories.comment 에서 본인 소감 조회 ──
if ($categoryCode === 'CA10001') {
$sqlLh = 'SELECT lh.member_id, COALESCE(u.name, lh.member_id) AS member_name,
lh.comment, lh.last_viewed_at
FROM edu_learning_histories lh
LEFT JOIN edu_users u ON u.member_id = lh.member_id AND u.sys_comp_code = lh.sys_comp_code
WHERE lh.content_id = ? AND lh.member_id = ?';
$paramsLh = [$contentId, $memberId];
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
if ($sysCompCode !== '') {
$sqlLh .= ' AND lh.sys_comp_code = ?';
$paramsLh[] = $sysCompCode;
}
$sqlLh .= ' ORDER BY lh.last_viewed_at DESC LIMIT 1';
$stmtLh = $pdo->prepare($sqlLh);
$stmtLh->execute($paramsLh);
$row = $stmtLh->fetch(PDO::FETCH_ASSOC);
$data = [];
if ($row && $row['comment'] !== null && trim((string)$row['comment']) !== '') {
$authorId = (string)($row['member_id'] ?? '');
$profileFilePath = __DIR__ . '/../../img/profile/' . $authorId . '_' . $sysCompCode . '.png';
$profileImageUrl = file_exists($profileFilePath)
? '/img/profile/' . $authorId . '_' . $sysCompCode . '.png'
: '/img/ico/ico_user.svg';
$data[] = [
'id' => $contentId,
'member_id' => $authorId,
'member_name' => (string)($row['member_name'] ?? $authorId),
'comment' => (string)$row['comment'],
'created_at' => (string)($row['last_viewed_at'] ?? ''),
'updated_at' => (string)($row['last_viewed_at'] ?? ''),
'is_author' => true,
'profile_image' => $profileImageUrl,
];
}
echo json_encode([
'success' => true,
'category' => $categoryCode,
'data' => $data,
], JSON_UNESCAPED_UNICODE);
exit;
}
// ── 기타 카테고리: 기존 edu_comments 테이블 조회 ──
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
$params = [$contentId];
$sql = 'SELECT c.id, c.member_id, c.sys_comp_code, COALESCE(u.name, c.member_id) AS member_name, c.comment, c.created_at, c.updated_at
FROM edu_comments c
LEFT JOIN edu_users u ON u.member_id = c.member_id AND u.sys_comp_code = c.sys_comp_code
WHERE c.content_id = ?';
$sql .= ' ORDER BY c.id DESC';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
$data = array_map(static function (array $row) use ($memberId, $sysCompCode): array {
$authorId = (string)($row['member_id'] ?? '');
$rowSysCompCode = (string)($row['sys_comp_code'] ?? '');
$profileFilePath = __DIR__ . '/../../img/profile/' . $authorId . '_' . $rowSysCompCode . '.png';
$profileImageUrl = file_exists($profileFilePath)
? '/img/profile/' . $authorId . '_' . $rowSysCompCode . '.png'
: '/img/ico/ico_user.svg';
return [
'id' => (int)($row['id'] ?? 0),
'member_id' => $authorId,
'member_name' => (string)($row['member_name'] ?? $authorId),
'comment' => (string)($row['comment'] ?? ''),
'created_at' => (string)($row['created_at'] ?? ''),
'updated_at' => (string)($row['updated_at'] ?? ''),
'is_author' => ($authorId === $memberId && $rowSysCompCode === $sysCompCode),
'profile_image' => $profileImageUrl,
];
}, $rows);
echo json_encode([
'success' => true,
'category' => $categoryCode,
'data' => $data,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_comments] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+159
View File
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db_conn.php';
require_once __DIR__ . '/common.php';
$sessionUser = api_get_session_user();
$memberId = (string)($sessionUser['member_id'] ?? $_SESSION['member_id'] ?? '');
$sysCompCode = (string)($sessionUser['sys_comp_code'] ?? $_SESSION['sys_comp_code'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$fixedLegalNameMap = [
'C01' => '개인정보보호',
'C02' => '직장내 괴롭힘 예방',
'C03' => '장애인 인식 개선',
'C04' => '성희롱 예방 교육',
'C05' => '퇴직금 교육',
];
$normalizeLegalGroupCode = static function ($value) {
$code = strtoupper(trim((string)$value));
if (preg_match('/^CA200(C\d{2})$/', $code, $m)) {
return $m[1];
}
if (preg_match('/^C\d{2}$/', $code)) {
return $code;
}
return '';
};
$legalCodes = array_keys($fixedLegalNameMap);
$legalRawCodes = [];
foreach ($legalCodes as $code) {
$legalRawCodes[] = $code;
$legalRawCodes[] = 'CA200' . $code;
}
$legalRawCodes = array_values(array_unique($legalRawCodes));
$phCodes = implode(',', array_fill(0, count($legalRawCodes), '?'));
// 법정교육(CA10003) 컨텐츠를 category_group 기준으로 조회
$sql = "
SELECT
c.content_id,
c.title,
c.content_url,
c.thumbnail_url,
c.category_code,
c.category_group,
c.description,
c.sort_order,
lh.watch_tm,
lh.content_tm,
lh.all_tm,
lh.completed_at
FROM edu_contents c
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
WHERE c.category_code = 'CA10003'
AND c.is_active = 1
AND c.category_group IN ($phCodes)
ORDER BY c.category_group ASC, c.sort_order ASC, c.content_id ASC
";
$stmt = $pdo->prepare($sql);
$stmt->execute(array_merge([$memberId, $sysCompCode], $legalRawCodes));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
$chapterMap = [];
foreach ($legalCodes as $index => $code) {
$chapterMap[$code] = [
'code' => $code,
'name' => $fixedLegalNameMap[$code] ?? $code,
'lessons' => [],
];
}
foreach ($rows as $row) {
$groupCode = $normalizeLegalGroupCode($row['category_group'] ?? '');
if ($groupCode === '' || !isset($chapterMap[$groupCode])) {
continue;
}
// YouTube videoId 추출
$raw = trim($row['content_url'] ?? '');
$videoId = '';
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;
}
$title = $row['title'] ?? '';
$watchTm = (int)($row['watch_tm'] ?? 0);
$contentTm = (int)($row['content_tm'] ?? 0);
$completed = !empty($row['completed_at']);
$chapterMap[$groupCode]['lessons'][] = [
'content_id' => $row['content_id'] ?? '',
'title' => $title,
'url' => $videoId,
'description'=> (string)($row['description'] ?? ''),
'sort_order' => (int)($row['sort_order'] ?? 0),
'watch_tm' => $watchTm,
'content_tm' => $contentTm,
'all_tm' => (int)($row['all_tm'] ?? 0),
'completed' => $completed,
];
}
$chapters = [];
foreach ($legalCodes as $code) {
$chapter = $chapterMap[$code];
usort($chapter['lessons'], static function ($left, $right) {
$sortDiff = (int)($left['sort_order'] ?? 0) <=> (int)($right['sort_order'] ?? 0);
if ($sortDiff !== 0) {
return $sortDiff;
}
return strcmp((string)($left['content_id'] ?? ''), (string)($right['content_id'] ?? ''));
});
$chapters[] = [
'code' => $chapter['code'],
'name' => $chapter['name'],
'lessons' => array_map(static function ($lesson) {
unset($lesson['sort_order']);
return $lesson;
}, $chapter['lessons']),
];
}
echo json_encode([
'success' => true,
'chapters' => $chapters,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'server_error',
], JSON_UNESCAPED_UNICODE);
}
+190
View File
@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db_conn.php';
require_once __DIR__ . '/common.php';
$sessionUser = api_get_session_user();
$memberId = (string)($sessionUser['member_id'] ?? $_SESSION['member_id'] ?? '');
$sysCompCode = (string)($sessionUser['sys_comp_code'] ?? $_SESSION['sys_comp_code'] ?? '');
if ($memberId === '' || $sysCompCode === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$onboardingGroups = [
'CA200O01', 'CA200O02', 'CA200O03', 'CA200O04', 'CA200O05',
'CA200O06', 'CA200O07', 'CA200O08', 'CA200O09', 'CA200O10',
];
$placeholders = implode(',', array_fill(0, count($onboardingGroups), '?'));
$categoryMap = [
'CA200O01' => ['pieceId' => 1, 'group' => 3, 'area' => 'value'],
'CA200O02' => ['pieceId' => 2, 'group' => 2, 'area' => 'hanmac'],
'CA200O03' => ['pieceId' => 3, 'group' => 2, 'area' => 'hanmac'],
'CA200O04' => ['pieceId' => 4, 'group' => 2, 'area' => 'hanmac'],
'CA200O05' => ['pieceId' => 5, 'group' => 2, 'area' => 'hanmac'],
'CA200O06' => ['pieceId' => 6, 'group' => 3, 'area' => 'value'],
'CA200O07' => ['pieceId' => 7, 'group' => 4, 'area' => 'company'],
'CA200O08' => ['pieceId' => 8, 'group' => 4, 'area' => 'company'],
'CA200O09' => ['pieceId' => 9, 'group' => 1, 'area' => 'family'],
'CA200O10' => ['pieceId' => 10, 'group' => 1, 'area' => 'family'],
];
$chapterNameMap = [];
try {
$stmtCodes = $pdo->prepare("\n SELECT base_code, code_name\n FROM edu_codes\n WHERE group_code = 'CA200'\n AND base_code IN ('CA200O01','CA200O02','CA200O03','CA200O04','CA200O05','CA200O06','CA200O07','CA200O08','CA200O09','CA200O10')\n ORDER BY base_code\n ");
$stmtCodes->execute();
foreach (($stmtCodes->fetchAll(PDO::FETCH_ASSOC) ?: []) as $codeRow) {
$baseCode = (string)($codeRow['base_code'] ?? '');
$codeName = trim((string)($codeRow['code_name'] ?? ''));
if ($baseCode !== '' && $codeName !== '') {
$chapterNameMap[$baseCode] = $codeName;
}
}
} catch (Throwable $e) {
// 코드명 조회 실패 시 콘텐츠 제목으로 폴백한다.
}
$useContentHistories = true;
try {
$checkTable = $pdo->query("SHOW TABLES LIKE 'edu_content_histories'");
if (!$checkTable || $checkTable->rowCount() === 0) {
$useContentHistories = false;
}
} catch (Throwable $e) {
$useContentHistories = false;
}
if ($useContentHistories) {
try {
$stmt = $pdo->prepare("\n SELECT\n c.*,\n COALESCE(lh.watch_tm, 0) AS watch_tm,\n COALESCE(lh.content_tm, 0) AS content_tm,\n lh.completed_at AS lh_completed_at,\n CASE\n WHEN ch.content_id IS NULL THEN 'none'\n WHEN ch.completed_at IS NOT NULL THEN 'completed'\n ELSE 'in_progress'\n END AS learning_status\n FROM edu_contents c\n LEFT JOIN edu_learning_histories lh\n ON lh.content_id = c.content_id\n AND lh.member_id = ?\n AND lh.sys_comp_code = ?\n LEFT JOIN edu_content_histories ch\n ON ch.content_id = c.content_id\n AND ch.member_id = ?\n AND ch.sys_comp_code = ?\n WHERE c.category_group IN ($placeholders)\n ORDER BY FIELD(c.category_group, 'CA200O01','CA200O02','CA200O03','CA200O04','CA200O05','CA200O06','CA200O07','CA200O08','CA200O09','CA200O10'), COALESCE(NULLIF(c.sort_order, 0), 9999), c.content_id\n ");
$stmt->execute(array_merge([$memberId, $sysCompCode, $memberId, $sysCompCode], $onboardingGroups));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) {
$useContentHistories = false;
}
}
if (!$useContentHistories) {
$stmt = $pdo->prepare("\n SELECT\n c.*,\n COALESCE(lh.watch_tm, 0) AS watch_tm,\n COALESCE(lh.content_tm, 0) AS content_tm,\n lh.completed_at AS lh_completed_at,\n CASE\n WHEN lh.content_id IS NULL THEN 'none'\n WHEN lh.completed_at IS NOT NULL THEN 'completed'\n ELSE 'in_progress'\n END AS learning_status\n FROM edu_contents c\n LEFT JOIN edu_learning_histories lh\n ON lh.content_id = c.content_id\n AND lh.member_id = ?\n AND lh.sys_comp_code = ?\n WHERE c.category_group IN ($placeholders)\n ORDER BY FIELD(c.category_group, 'CA200O01','CA200O02','CA200O03','CA200O04','CA200O05','CA200O06','CA200O07','CA200O08','CA200O09','CA200O10'), COALESCE(NULLIF(c.sort_order, 0), 9999), c.content_id\n ");
$stmt->execute(array_merge([$memberId, $sysCompCode], $onboardingGroups));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
}
$chapterMap = [];
foreach ($rows as $index => $row) {
$categoryGroup = (string)($row['category_group'] ?? '');
if ($categoryGroup === '') {
continue;
}
$mapped = $categoryMap[$categoryGroup] ?? null;
$description = trim((string)($row['description'] ?? $row['description1'] ?? $row['description_1'] ?? $row['content_desc'] ?? $row['content_desc1'] ?? $row['content_description'] ?? $row['content_description1'] ?? ''));
$description2 = trim((string)($row['description2'] ?? $row['description_2'] ?? $row['content_desc2'] ?? $row['content_description2'] ?? ''));
if (!isset($chapterMap[$categoryGroup])) {
$chapterMap[$categoryGroup] = [
'id' => 0,
'name' => $chapterNameMap[$categoryGroup] ?? str_replace('[온보딩] ', '', (string)($row['title'] ?? '')),
'pieceId' => (int)($mapped['pieceId'] ?? ($index + 1)),
'type' => 'youtube',
'group' => (int)($mapped['group'] ?? 1),
'category_group' => $categoryGroup,
'area' => (string)($mapped['area'] ?? 'family'),
'lessons' => [],
];
}
$learningStatus = (string)($row['learning_status'] ?? 'none');
$chapterMap[$categoryGroup]['lessons'][] = [
'content_id' => (string)($row['content_id'] ?? ''),
'bookmark_content_id' => (string)($row['content_id'] ?? ''),
'comment_content_id' => (string)($row['content_id'] ?? ''),
'title' => (string)($row['title'] ?? ''),
'label' => str_replace('[온보딩] ', '', (string)($row['title'] ?? '')),
'description' => $description,
'description2' => $description2,
'url' => (string)($row['content_url'] ?? ''),
'completed' => ($learningStatus === 'completed'),
'learning_status' => $learningStatus,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'sort_order' => (int)($row['sort_order'] ?? 0),
'is_bookmarked' => false,
];
}
$bookmarkIds = [];
foreach ($chapterMap as $chapterItem) {
foreach (($chapterItem['lessons'] ?? []) as $lessonItem) {
$bookmarkId = trim((string)($lessonItem['bookmark_content_id'] ?? ''));
if ($bookmarkId !== '') {
$bookmarkIds[] = $bookmarkId;
}
}
}
$bookmarkIds = array_values(array_unique($bookmarkIds));
$wishlistMap = [];
if (!empty($bookmarkIds)) {
$wishlistPlaceholders = implode(',', array_fill(0, count($bookmarkIds), '?'));
$stmtWishlist = $pdo->prepare(
"SELECT content_id, is_active\n FROM edu_content_wishlist\n WHERE member_id = ?\n AND sys_comp_code = ?\n AND content_id IN ($wishlistPlaceholders)"
);
$stmtWishlist->execute(array_merge([$memberId, $sysCompCode], $bookmarkIds));
foreach (($stmtWishlist->fetchAll(PDO::FETCH_ASSOC) ?: []) as $wishlistRow) {
$wishlistMap[(string)$wishlistRow['content_id']] = ((string)($wishlistRow['is_active'] ?? '0') === '1');
}
}
$chapters = [];
foreach ($onboardingGroups as $groupCode) {
if (!isset($chapterMap[$groupCode])) {
continue;
}
$chapter = $chapterMap[$groupCode];
usort($chapter['lessons'], static function ($left, $right) {
$sortDiff = (int)($left['sort_order'] ?? 0) <=> (int)($right['sort_order'] ?? 0);
if ($sortDiff !== 0) {
return $sortDiff;
}
return strcmp((string)($left['content_id'] ?? ''), (string)($right['content_id'] ?? ''));
});
$chapter['id'] = count($chapters) + 1;
$chapter['lessons'] = array_map(static function ($lesson) use ($wishlistMap) {
$bookmarkId = trim((string)($lesson['bookmark_content_id'] ?? ''));
$lesson['is_bookmarked'] = ($bookmarkId !== '' && !empty($wishlistMap[$bookmarkId]));
unset($lesson['sort_order']);
return $lesson;
}, $chapter['lessons']);
$chapters[] = $chapter;
}
echo json_encode([
'success' => true,
'chapters' => $chapters,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'server_error',
], JSON_UNESCAPED_UNICODE);
}
+261
View File
@@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db_conn.php';
$memberId = (string)($_SESSION['member_id'] ?? '');
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
$contentId = trim((string)($_GET['content_id'] ?? ''));
if ($contentId === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'content_id_required'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$normCode = static function (string $code): string {
return preg_replace('/[^A-Z0-9]/', '', strtoupper(trim($code)));
};
$looksLikeCode = static function (string $value): bool {
return preg_match('/^[A-Z]{2}[A-Z0-9]{3,}$/', strtoupper(trim($value))) === 1;
};
$codeMap = [];
try {
$stmtCodeMap = $pdo->query("SELECT base_code, code_name FROM edu_codes");
foreach ($stmtCodeMap->fetchAll(PDO::FETCH_ASSOC) as $cr) {
$key = $normCode((string)($cr['base_code'] ?? ''));
if ($key === '') continue;
$codeMap[$key] = (string)($cr['code_name'] ?? '');
}
} catch (Throwable $ignore) {
$codeMap = [];
}
// 1) 현재 영상의 키워드 코드 조회
$keywordCodes = [];
try {
$stmtKw = $pdo->prepare('SELECT keyword_code FROM edu_content_keywords WHERE content_id = ?');
$stmtKw->execute([$contentId]);
$keywordCodes = $stmtKw->fetchAll(PDO::FETCH_COLUMN) ?: [];
} catch (Throwable $kwErr) {
// edu_content_keywords 테이블이 없는 경우 무시 → fallback
$keywordCodes = [];
}
// 키워드 없으면 같은 카테고리 영상으로 fallback
if (empty($keywordCodes)) {
// 현재 영상의 카테고리 조회
$stmtCat = $pdo->prepare('SELECT category_code FROM edu_contents WHERE content_id = ? LIMIT 1');
$stmtCat->execute([$contentId]);
$curCatCode = $stmtCat->fetchColumn();
if (!$curCatCode) {
echo json_encode([
'success' => true,
'keywords' => [],
'videos' => [],
], JSON_UNESCAPED_UNICODE);
exit;
}
$stmtFallback = $pdo->prepare(
"SELECT c.content_id, c.title, c.content_url, c.thumbnail_url,
c.category_code, c.category_group, c.description,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(ec_grp.code_name, c.category_group) AS category_group_name
FROM edu_contents c
LEFT JOIN edu_codes ec ON TRIM(UPPER(ec.base_code)) = TRIM(UPPER(c.category_code))
LEFT JOIN edu_codes ec_grp ON TRIM(UPPER(ec_grp.base_code)) = TRIM(UPPER(c.category_group))
WHERE c.category_code = ?
AND c.content_id != ?
AND c.is_active = 1
ORDER BY c.start_date DESC
LIMIT 8"
);
$stmtFallback->execute([$curCatCode, $contentId]);
$rows = $stmtFallback->fetchAll(PDO::FETCH_ASSOC) ?: [];
$videos = [];
foreach ($rows as $row) {
$raw = trim($row['content_url'] ?? '');
$videoId = '';
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;
}
$thumbFromDb = trim($row['thumbnail_url'] ?? '');
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
$catCode = $row['category_code'] ?? '';
$subcate = trim((string)($row['category_group_name'] ?? $row['category_group'] ?? ''));
$catName = trim((string)($row['category_name'] ?? $catCode));
if (($catName === '' || $looksLikeCode($catName)) && $catCode !== '') {
$mapped = $codeMap[$normCode($catCode)] ?? '';
if ($mapped !== '') $catName = $mapped;
}
$groupRaw = trim((string)($row['category_group'] ?? ''));
if (($subcate === '' || $looksLikeCode($subcate)) && $groupRaw !== '') {
$mapped = $codeMap[$normCode($groupRaw)] ?? '';
if ($mapped !== '') $subcate = $mapped;
}
$videos[] = [
'content_id' => $row['content_id'],
'title' => $row['title'] ?? '',
'content_url' => $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw,
'thumbnail' => $thumbnail,
'category_code' => $catCode,
'category' => $catName,
'category_name' => $catName,
'subcate' => $subcate,
'description' => $row['description'] ?? '',
];
}
echo json_encode([
'success' => true,
'keywords' => [],
'videos' => $videos,
], JSON_UNESCAPED_UNICODE);
exit;
}
// 2) 키워드명 조회 (표시용)
$kwPlaceholders = implode(',', array_fill(0, count($keywordCodes), '?'));
$keywordNames = [];
try {
$stmtKwNames = $pdo->prepare(
"SELECT keyword_code, keyword_name FROM edu_keywords WHERE keyword_code IN ({$kwPlaceholders})"
);
$stmtKwNames->execute($keywordCodes);
while ($row = $stmtKwNames->fetch(PDO::FETCH_ASSOC)) {
$keywordNames[] = $row['keyword_name'];
}
} catch (Throwable $kwNameErr) {
$keywordNames = [];
}
// 3) 동일 키워드를 가진 다른 영상 조회 (현재 영상 제외, 랜덤 8개)
$rows = [];
try {
$stmtVideos = $pdo->prepare(
"SELECT DISTINCT
c.content_id,
c.title,
c.content_url,
c.thumbnail_url,
c.category_code,
c.category_group,
c.description,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(ec_grp.code_name, c.category_group) AS category_group_name
FROM edu_contents c
LEFT JOIN edu_codes ec ON TRIM(UPPER(ec.base_code)) = TRIM(UPPER(c.category_code))
LEFT JOIN edu_codes ec_grp ON TRIM(UPPER(ec_grp.base_code)) = TRIM(UPPER(c.category_group))
INNER JOIN edu_content_keywords ck ON ck.content_id = c.content_id
WHERE ck.keyword_code IN ({$kwPlaceholders})
AND c.content_id != ?
AND c.is_active = 1
ORDER BY RAND()
LIMIT 8"
);
$params = array_merge($keywordCodes, [$contentId]);
$stmtVideos->execute($params);
$rows = $stmtVideos->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $kwVideoErr) {
$rows = [];
}
// 키워드 매칭 결과 없으면 같은 카테고리 fallback
if (empty($rows)) {
$stmtCat = $pdo->prepare('SELECT category_code FROM edu_contents WHERE content_id = ? LIMIT 1');
$stmtCat->execute([$contentId]);
$curCatCode = $stmtCat->fetchColumn();
if ($curCatCode) {
$stmtFb = $pdo->prepare(
"SELECT c.content_id, c.title, c.content_url, c.thumbnail_url,
c.category_code, c.category_group, c.description,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(ec_grp.code_name, c.category_group) AS category_group_name
FROM edu_contents c
LEFT JOIN edu_codes ec ON TRIM(UPPER(ec.base_code)) = TRIM(UPPER(c.category_code))
LEFT JOIN edu_codes ec_grp ON TRIM(UPPER(ec_grp.base_code)) = TRIM(UPPER(c.category_group))
WHERE c.category_code = ?
AND c.content_id != ?
AND c.is_active = 1
ORDER BY c.start_date DESC
LIMIT 8"
);
$stmtFb->execute([$curCatCode, $contentId]);
$rows = $stmtFb->fetchAll(PDO::FETCH_ASSOC) ?: [];
}
}
$videos = [];
foreach ($rows as $row) {
$raw = trim($row['content_url'] ?? '');
$videoId = '';
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;
}
$thumbFromDb = trim($row['thumbnail_url'] ?? '');
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
$catCode = $row['category_code'] ?? '';
$subcate = trim((string)($row['category_group_name'] ?? $row['category_group'] ?? ''));
$catName = trim((string)($row['category_name'] ?? $catCode));
if (($catName === '' || $looksLikeCode($catName)) && $catCode !== '') {
$mapped = $codeMap[$normCode($catCode)] ?? '';
if ($mapped !== '') $catName = $mapped;
}
$groupRaw = trim((string)($row['category_group'] ?? ''));
if (($subcate === '' || $looksLikeCode($subcate)) && $groupRaw !== '') {
$mapped = $codeMap[$normCode($groupRaw)] ?? '';
if ($mapped !== '') $subcate = $mapped;
}
$videos[] = [
'content_id' => $row['content_id'],
'title' => $row['title'] ?? '',
'content_url' => $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw,
'thumbnail' => $thumbnail,
'category_code' => $catCode,
'category' => $catName,
'category_name' => $catName,
'subcate' => $subcate,
'description' => $row['description'] ?? '',
];
}
echo json_encode([
'success' => true,
'keywords' => $keywordNames,
'videos' => $videos,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_recommend_videos] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+139
View File
@@ -0,0 +1,139 @@
<?php
//마이클래스 영상모달 관련영상 리스트 API - 목표코드(goal_code) 기반
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db_conn.php';
$memberId = (string)($_SESSION['member_id'] ?? '');
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
$contentId = trim((string)($_GET['content_id'] ?? ''));
if ($contentId === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'content_id_required'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$stmtCurrent = $pdo->prepare(
"SELECT content_id, goal_code
FROM edu_contents
WHERE content_id = ?
AND is_active = 1
LIMIT 1"
);
$stmtCurrent->execute([$contentId]);
$current = $stmtCurrent->fetch(PDO::FETCH_ASSOC) ?: null;
if (!$current) {
echo json_encode([
'success' => true,
'mode' => 'goal_code',
'goal_code' => null,
'keywords' => [],
'videos' => [],
], JSON_UNESCAPED_UNICODE);
exit;
}
$goalCode = trim((string)($current['goal_code'] ?? ''));
if ($goalCode === '') {
echo json_encode([
'success' => true,
'mode' => 'goal_code',
'goal_code' => '',
'keywords' => [],
'videos' => [],
], JSON_UNESCAPED_UNICODE);
exit;
}
$stmtVideos = $pdo->prepare(
"SELECT
c.content_id,
c.title,
c.content_url,
c.thumbnail_url,
c.category_code,
c.category_group,
c.description,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(ec_grp.code_name, c.category_group) AS category_group_name,
c.goal_code
FROM edu_contents c
LEFT JOIN edu_codes ec
ON TRIM(UPPER(ec.base_code)) = TRIM(UPPER(c.category_code))
LEFT JOIN edu_codes ec_grp
ON TRIM(UPPER(ec_grp.base_code)) = TRIM(UPPER(c.category_group))
WHERE c.is_active = 1
AND c.goal_code = ?
AND c.content_id <> ?
ORDER BY
COALESCE(NULLIF(c.sort_order, 0), 9999) ASC,
c.updated_at DESC,
c.content_id DESC
LIMIT 6"
);
$stmtVideos->execute([$goalCode, $contentId]);
$rows = $stmtVideos->fetchAll(PDO::FETCH_ASSOC) ?: [];
$videos = [];
foreach ($rows as $row) {
$raw = trim((string)($row['content_url'] ?? ''));
$videoId = '';
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;
}
$thumbFromDb = trim((string)($row['thumbnail_url'] ?? ''));
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
$catCode = (string)($row['category_code'] ?? '');
$subcate = trim((string)($row['category_group_name'] ?? $row['category_group'] ?? ''));
$catName = trim((string)($row['category_name'] ?? $catCode));
$videos[] = [
'content_id' => $row['content_id'] ?? '',
'title' => (string)($row['title'] ?? ''),
'content_url' => $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : $raw,
'thumbnail' => $thumbnail,
'category_code' => $catCode,
'category' => $catName,
'category_name' => $catName,
'subcate' => $subcate,
'description' => (string)($row['description'] ?? ''),
'goal_code' => (string)($row['goal_code'] ?? ''),
];
}
echo json_encode([
'success' => true,
'mode' => 'goal_code',
'goal_code' => $goalCode,
'keywords' => [],
'videos' => $videos,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_recommend_videos] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* 영상 시청 시간 조회 API
* POST /bbs/api/get_video_time.php
*
* Parameters:
* - content_id (필수): 영상 content ID (string)
*
* Returns:
* - watch_tm : edu_learning_histories.watch_tm (이어보기 기준 시간, 초)
* - content_tm : edu_learning_histories.content_tm (영상 전체 길이, 초)
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
$member_id = trim((string)($_SESSION['member_id'] ?? ''));
$sys_comp_code = trim((string)($_SESSION['sys_comp_code'] ?? ''));
if ($member_id === '') {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($sys_comp_code === '') {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'sys_comp_code_missing']);
exit;
}
// content_id: JSON body 또는 POST 폼 데이터 모두 허용
$rawBody = file_get_contents('php://input');
$jsonBody = (is_string($rawBody) && $rawBody !== '') ? json_decode($rawBody, true) : null;
$content_id = trim((string)(
(is_array($jsonBody) ? ($jsonBody['content_id'] ?? '') : '') ?:
($_POST['content_id'] ?? '') ?:
($_GET['content_id'] ?? '')
));
if ($content_id === '') {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid content_id']);
exit;
}
try {
require_once __DIR__ . '/../db_conn.php';
$pdo = db_conn();
// edu_learning_histories 에서 watch_tm / content_tm 조회
$stmt = $pdo->prepare(
"SELECT watch_tm, content_tm
FROM edu_learning_histories
WHERE member_id = ?
AND sys_comp_code = ?
AND content_id = ?
LIMIT 1"
);
$stmt->execute([$member_id, $sys_comp_code, $content_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$watch_tm = (int)($row['watch_tm'] ?? 0);
$content_tm = (int)($row['content_tm'] ?? 0);
echo json_encode([
'success' => true,
'content_id' => $content_id,
'watch_tm' => $watch_tm,
'content_tm' => $content_tm,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_video_time.php] Error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'server_error']);
}
?>
+133
View File
@@ -0,0 +1,133 @@
<?php
/**
* edu_goal_contents 자동 매핑 스크립트
* category_group 기준으로 goal_code와 content_id 연결
*/
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/db_conn.php';
$pdo = db_conn();
try {
// 1. 모든 goal_code를 category_group별로 그룹화
$goals = $pdo->query("
SELECT goal_code, quarter
FROM edu_learning_goals
WHERE is_active = '1'
ORDER BY goal_code
")->fetchAll(PDO::FETCH_ASSOC);
// quarter를 category_group으로 맵핑하는 쿼리
// CA200Q01 → CA200Q01, CA200Q02 → CA200Q02 등
// 2. 각 quarter의 영상들을 조회
$inserted = 0;
foreach ($goals as $goal) {
$goalCode = $goal['goal_code'];
$quarter = $goal['quarter']; // 예: CA200Q01
// 같은 quarter의 영상 6개 조회
$contents = $pdo->query("
SELECT content_id
FROM edu_contents
WHERE category_group = ?
AND content_id NOT IN (
SELECT content_id FROM edu_goal_contents
WHERE category_group = ?
)
ORDER BY content_id ASC
LIMIT 6
")->fetchAll(PDO::FETCH_COLUMN);
// Wait - category_group이 edu_goal_contents에 없네
// 대신 이미 다른 goal에 할당된 content를 피해야 함
$contents = $pdo->query("
SELECT content_id
FROM edu_contents
WHERE category_group = ?
AND content_id NOT IN (
SELECT content_id FROM edu_goal_contents
)
ORDER BY content_id ASC
LIMIT 6
")->fetchAll(PDO::FETCH_COLUMN);
// INSERT
$stmt = $pdo->prepare("
INSERT INTO edu_goal_contents
(goal_code, content_id, is_active, sort_order)
VALUES (?, ?, '1', ?)
");
foreach ($contents as $idx => $contentId) {
$stmt->execute([$goalCode, $contentId, $idx + 1]);
$inserted++;
}
echo "- $goalCode: " . count($contents) . "개 영상 매핑\n";
}
// 1. quarter별로 goal들을 그룹화
$quarterGoals = $pdo->query("
SELECT quarter, GROUP_CONCAT(goal_code ORDER BY goal_code) as goals
FROM edu_learning_goals
WHERE is_active = '1'
GROUP BY quarter
ORDER BY quarter
")->fetchAll(PDO::FETCH_ASSOC);
$inserted = 0;
// 2. 각 quarter마다 처리
foreach ($quarterGoals as $qg) {
$quarter = $qg['quarter'];
$goalCodes = explode(',', $qg['goals']);
// 해당 quarter의 모든 영상 조회
$stmt = $pdo->prepare("
SELECT content_id
FROM edu_contents
WHERE category_group = ?
ORDER BY content_id ASC
");
$stmt->execute([$quarter]);
$allContents = $stmt->fetchAll(PDO::FETCH_COLUMN);
// 3. 영상들을 goal별로 6개씩 분배
$contentIdx = 0;
foreach ($goalCodes as $goalCode) {
$goalCode = trim($goalCode);
// 이 goal을 위해 6개 영상 선택
$goalContents = array_slice($allContents, $contentIdx, 6);
$contentIdx += 6;
// INSERT
$stmtInsert = $pdo->prepare("
INSERT INTO edu_goal_contents
(goal_code, content_id, is_active, sort_order)
VALUES (?, ?, '1', ?)
");
foreach ($goalContents as $idx => $contentId) {
$stmtInsert->execute([$goalCode, $contentId, $idx + 1]);
$inserted++;
}
echo "- {$goalCode}: " . count($goalContents) . "개 영상 (quarter: {$quarter})\n";
}
}
echo json_encode([
'success' => true,
'message' => "{$inserted}개 행 INSERT 완료",
'result' => "goal_code와 content_id 매핑 완료"
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
], JSON_UNESCAPED_UNICODE);
}
?>
+268
View File
@@ -0,0 +1,268 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db_conn.php';
function json_exit(array $payload, int $status = 200): void {
http_response_code($status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
exit;
}
function resolve_member_and_company(PDO $pdo): array {
$sessionMemberId = trim((string)($_SESSION['ss_mb_id'] ?? $_SESSION['member_id'] ?? ''));
$memberId = $sessionMemberId !== '' ? $sessionMemberId : 'U001';
$stmtUser = $pdo->prepare('SELECT sys_comp_code FROM edu_users WHERE member_id = ? ORDER BY sys_comp_code LIMIT 1');
$stmtUser->execute([$memberId]);
$sysCompCode = (string)($stmtUser->fetchColumn() ?: '');
if ($sysCompCode === '') {
json_exit([
'success' => false,
'message' => 'edu_users에 회원 정보가 없어 마이클래스 상태를 조회할 수 없습니다.',
'data' => ['member_id' => $memberId],
], 400);
}
return [$memberId, $sysCompCode];
}
function ensure_goal_contents(PDO $pdo, string $goalCode, string $quarter, string $actor): int {
$stmtCnt = $pdo->prepare("SELECT COUNT(*) FROM edu_goal_contents WHERE goal_code = ? AND (is_active = '1' OR is_active IS NULL)");
$stmtCnt->execute([$goalCode]);
$existing = (int)$stmtCnt->fetchColumn();
if ($existing >= 6) {
return $existing;
}
$stmtCandidates = $pdo->prepare(
"SELECT c.content_id
FROM edu_contents c
WHERE (c.is_active = '1' OR c.is_active IS NULL)
AND (c.goal_code = :goal_code OR c.category_group = :quarter)
ORDER BY
CASE WHEN c.sort_order IS NULL THEN 1 ELSE 0 END,
c.sort_order ASC,
c.content_id ASC
LIMIT 6"
);
$stmtCandidates->execute([
':goal_code' => $goalCode,
':quarter' => $quarter,
]);
$candidateIds = $stmtCandidates->fetchAll(PDO::FETCH_COLUMN);
if (count($candidateIds) < 6) {
$stmtFallback = $pdo->prepare(
"SELECT c.content_id
FROM edu_contents c
WHERE (c.is_active = '1' OR c.is_active IS NULL)
ORDER BY
CASE WHEN c.sort_order IS NULL THEN 1 ELSE 0 END,
c.sort_order ASC,
c.content_id ASC
LIMIT 6"
);
$stmtFallback->execute();
$candidateIds = $stmtFallback->fetchAll(PDO::FETCH_COLUMN);
}
$candidateIds = array_values(array_unique(array_filter(array_map('strval', $candidateIds))));
$stmtUpsert = $pdo->prepare(
"INSERT INTO edu_goal_contents
(goal_code, content_id, index_id, is_active, sort_order, created_by, created_at, updated_by, updated_at)
VALUES
(?, ?, ?, '1', ?, ?, NOW(), ?, NOW())
ON DUPLICATE KEY UPDATE
is_active = VALUES(is_active),
sort_order = VALUES(sort_order),
updated_by = VALUES(updated_by),
updated_at = NOW()"
);
foreach ($candidateIds as $idx => $contentId) {
$order = $idx + 1;
$stmtUpsert->execute([$goalCode, $contentId, $order, $order, $actor, $actor]);
}
$stmtCnt->execute([$goalCode]);
return (int)$stmtCnt->fetchColumn();
}
try {
$pdo = db_conn();
[$memberId, $sysCompCode] = resolve_member_and_company($pdo);
$stmtGoals = $pdo->query(
"SELECT goal_code, title, quarter, goal_no, sort_order
FROM edu_learning_goals
WHERE (is_active = '1' OR is_active IS NULL)
ORDER BY base_year DESC, quarter ASC, goal_no ASC, goal_code ASC"
);
$goals = $stmtGoals->fetchAll(PDO::FETCH_ASSOC);
if (!$goals) {
json_exit(['success' => true, 'data' => ['goals' => [], 'videos' => []]]);
}
$stmtSelected = $pdo->prepare(
"SELECT ug.goal_code, ug.quarter, ug.completed_date, ug.updated_at, lg.title
FROM edu_user_learning_goals ug
LEFT JOIN edu_learning_goals lg ON lg.goal_code = ug.goal_code
WHERE ug.member_id = ?
AND ug.sys_comp_code = ?
AND (ug.is_active = '1' OR ug.is_active IS NULL)
ORDER BY ug.updated_at DESC, ug.goal_code DESC"
);
$stmtSelected->execute([$memberId, $sysCompCode]);
$selectedGoals = $stmtSelected->fetchAll(PDO::FETCH_ASSOC);
$requestedGoalCode = trim((string)($_GET['goal_code'] ?? ''));
$activeGoalCode = $requestedGoalCode;
if ($activeGoalCode === '' && !empty($selectedGoals)) {
$activeGoalCode = (string)$selectedGoals[0]['goal_code'];
}
if ($activeGoalCode === '') {
$activeGoalCode = (string)$goals[0]['goal_code'];
}
$goalIndex = [];
foreach ($goals as $g) {
$goalIndex[(string)$g['goal_code']] = $g;
}
if (!isset($goalIndex[$activeGoalCode])) {
$activeGoalCode = (string)$goals[0]['goal_code'];
}
$activeQuarter = (string)($goalIndex[$activeGoalCode]['quarter'] ?? '');
ensure_goal_contents($pdo, $activeGoalCode, $activeQuarter, $memberId);
$stmtVideos = $pdo->prepare(
"SELECT
gc.goal_code,
gc.content_id,
gc.sort_order,
gc.index_id,
c.title,
c.description,
c.description2,
c.content_url,
c.thumbnail_url,
c.category_code,
c.category_group,
lh.watch_tm,
lh.content_tm,
lh.completed_at
FROM edu_goal_contents gc
JOIN edu_contents c ON c.content_id = gc.content_id
LEFT JOIN edu_learning_histories lh
ON lh.member_id = ?
AND lh.sys_comp_code = ?
AND lh.content_id = gc.content_id
WHERE gc.goal_code = ?
AND (gc.is_active = '1' OR gc.is_active IS NULL)
ORDER BY
CASE WHEN gc.sort_order IS NULL THEN 1 ELSE 0 END,
gc.sort_order ASC,
CASE WHEN gc.index_id IS NULL THEN 1 ELSE 0 END,
gc.index_id ASC,
gc.content_id ASC
LIMIT 6"
);
$stmtVideos->execute([$memberId, $sysCompCode, $activeGoalCode]);
$videos = $stmtVideos->fetchAll(PDO::FETCH_ASSOC);
$contentIds = array_values(array_unique(array_filter(array_map(static function ($r) {
return (string)($r['content_id'] ?? '');
}, $videos))));
$memoMap = [];
if (!empty($contentIds)) {
$in = implode(',', array_fill(0, count($contentIds), '?'));
$stmtMemo = $pdo->prepare(
"SELECT content_id, seq, title
FROM edu_content_memos
WHERE content_id IN ($in)
AND (is_active = '1' OR is_active IS NULL)
ORDER BY content_id ASC, seq ASC"
);
$stmtMemo->execute($contentIds);
$memoRows = $stmtMemo->fetchAll(PDO::FETCH_ASSOC);
foreach ($memoRows as $m) {
$cid = (string)$m['content_id'];
if (!isset($memoMap[$cid])) {
$memoMap[$cid] = [];
}
$memoTitle = trim((string)($m['title'] ?? ''));
if ($memoTitle !== '') {
$memoMap[$cid][] = $memoTitle;
}
}
}
foreach ($videos as $idx => $video) {
$cid = (string)$video['content_id'];
$videos[$idx]['memos'] = $memoMap[$cid] ?? [];
$videos[$idx]['is_completed'] = !empty($video['completed_at']);
$videos[$idx]['slot_no'] = $idx + 1;
}
$stmtProgress = $pdo->prepare(
"SELECT
ug.goal_code,
COUNT(gc.content_id) AS total_count,
SUM(CASE WHEN lh.completed_at IS NOT NULL THEN 1 ELSE 0 END) AS completed_count
FROM edu_user_learning_goals ug
LEFT JOIN edu_goal_contents gc
ON gc.goal_code = ug.goal_code
AND (gc.is_active = '1' OR gc.is_active IS NULL)
LEFT JOIN edu_learning_histories lh
ON lh.member_id = ug.member_id
AND lh.sys_comp_code = ug.sys_comp_code
AND lh.content_id = gc.content_id
WHERE ug.member_id = ?
AND ug.sys_comp_code = ?
AND (ug.is_active = '1' OR ug.is_active IS NULL)
GROUP BY ug.goal_code"
);
$stmtProgress->execute([$memberId, $sysCompCode]);
$progressRows = $stmtProgress->fetchAll(PDO::FETCH_ASSOC);
$goalProgress = [];
foreach ($progressRows as $row) {
$total = (int)($row['total_count'] ?? 0);
$completed = (int)($row['completed_count'] ?? 0);
$goalProgress[(string)$row['goal_code']] = [
'total_count' => $total,
'completed_count' => $completed,
'is_completed' => $total > 0 && $completed >= $total,
];
}
json_exit([
'success' => true,
'data' => [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'active_goal_code' => $activeGoalCode,
'goals' => $goals,
'selected_goals' => $selectedGoals,
'goal_progress' => $goalProgress,
'videos' => $videos,
],
]);
} catch (Throwable $e) {
json_exit([
'success' => false,
'message' => $e->getMessage(),
], 500);
}
+150
View File
@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
$debugLog = dirname(__DIR__) . '/_save_comment_debug.log';
require_once dirname(__DIR__) . '/db_conn.php';
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
$memberId = (string)($_SESSION['member_id'] ?? '');
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
file_put_contents($debugLog, "[" . date('Y-m-d H:i:s') . "] REQUEST START\n", FILE_APPEND);
file_put_contents($debugLog, "SESSION: member_id={$memberId}, sys_comp_code={$sysCompCode}\n", FILE_APPEND);
if ($memberId === '') {
file_put_contents($debugLog, "ERROR: member_id is empty\n", FILE_APPEND);
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
$input = json_decode((string)file_get_contents('php://input'), true);
if (!is_array($input)) {
$input = $_POST;
}
file_put_contents($debugLog, "INPUT: " . json_encode($input, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);
$contentId = trim((string)($input['content_id'] ?? ''));
$comment = trim((string)($input['comment'] ?? ''));
$commentId = (int)($input['id'] ?? 0);
if ($contentId === '') {
file_put_contents($debugLog, "ERROR: content_id is empty\n", FILE_APPEND);
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'content_id_required'], JSON_UNESCAPED_UNICODE);
exit;
}
if ($comment === '') {
file_put_contents($debugLog, "ERROR: comment is empty\n", FILE_APPEND);
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'comment_required'], JSON_UNESCAPED_UNICODE);
exit;
}
if (function_exists('mb_substr')) {
$comment = mb_substr($comment, 0, 255, 'UTF-8');
} else {
$comment = substr($comment, 0, 255);
}
try {
$pdo = db_conn();
if ($sysCompCode === '') {
file_put_contents($debugLog, "ERROR: sys_comp_code is empty\n", FILE_APPEND);
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'sys_comp_code_missing'], JSON_UNESCAPED_UNICODE);
exit;
}
$stmtCategory = $pdo->prepare('SELECT category_code FROM edu_contents WHERE content_id = ? LIMIT 1');
$stmtCategory->execute([$contentId]);
$categoryCode = strtoupper((string)($stmtCategory->fetchColumn() ?: ''));
if ($categoryCode === '') {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'content_not_found'], JSON_UNESCAPED_UNICODE);
exit;
}
$pdo->beginTransaction();
$savedId = 0;
$mode = 'insert';
// ── 마이클래스(CA10001): edu_learning_histories.comment 컬럼에 UPDATE ──
if ($categoryCode === 'CA10001') {
$sqlLh = 'UPDATE edu_learning_histories SET comment = ? WHERE content_id = ? AND member_id = ? AND sys_comp_code = ?';
$paramsLh = [$comment, $contentId, $memberId, $sysCompCode];
$stmtLh = $pdo->prepare($sqlLh);
$stmtLh->execute($paramsLh);
if ($stmtLh->rowCount() < 1) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => '학습 이력이 없어 소감을 저장할 수 없습니다.'], JSON_UNESCAPED_UNICODE);
exit;
}
$pdo->commit();
echo json_encode([
'success' => true,
'mode' => 'update',
'category' => $categoryCode,
], JSON_UNESCAPED_UNICODE);
exit;
}
// ── 기타 카테고리: 기존 edu_comments 테이블 사용 ──
if ($commentId > 0) {
$stmtUpdate = $pdo->prepare(
'UPDATE edu_comments
SET comment = ?, updated_by = ?, updated_at = NOW()
WHERE id = ? AND member_id = ? AND sys_comp_code = ?'
);
$stmtUpdate->execute([$comment, $memberId, $commentId, $memberId, $sysCompCode]);
if ($stmtUpdate->rowCount() < 1) {
$pdo->rollBack();
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'forbidden_or_not_found'], JSON_UNESCAPED_UNICODE);
exit;
}
$savedId = $commentId;
$mode = 'update';
} else {
$stmtInsert = $pdo->prepare(
'INSERT INTO edu_comments
(parent_id, sys_comp_code, member_id, comment, content_id, created_by, created_at, updated_by, updated_at)
VALUES (NULL, ?, ?, ?, ?, ?, NOW(), ?, NOW())'
);
$stmtInsert->execute([$sysCompCode, $memberId, $comment, $contentId, $memberId, $memberId]);
$savedId = (int)$pdo->lastInsertId();
$mode = 'insert';
}
$pdo->commit();
echo json_encode([
'success' => true,
'id' => $savedId,
'mode' => $mode,
'category' => $categoryCode,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('[save_comment] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+296
View File
@@ -0,0 +1,296 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
// Debug logging
$debugLog = __DIR__ . '/../../_save_learning_debug.log';
$rawBody = file_get_contents('php://input');
$logEntry = "[" . date('Y-m-d H:i:s') . "] REQUEST START\n";
$logEntry .= "POST Data (\$_POST): " . json_encode($_POST, JSON_UNESCAPED_UNICODE) . "\n";
$logEntry .= "Raw Body: " . $rawBody . "\n";
file_put_contents($debugLog, $logEntry, FILE_APPEND);
try {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db_conn.php';
$pdo = db_conn();
$payload = [];
if (is_string($rawBody) && $rawBody !== '') {
$decoded = json_decode($rawBody, true);
if (is_array($decoded)) {
$payload = $decoded;
}
}
if (empty($payload)) {
$payload = $_POST;
}
$logEntry = "Payload: " . json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n";
file_put_contents($debugLog, $logEntry, FILE_APPEND);
$sessionMemberId = trim((string)($_SESSION['member_id'] ?? ''));
$memberId = $sessionMemberId;
if ($memberId === '') {
file_put_contents($debugLog, "ERROR: member_id is empty\n", FILE_APPEND);
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'not_logged_in',
], JSON_UNESCAPED_UNICODE);
exit;
}
$sessionSysCompCode = trim((string)($_SESSION['sys_comp_code'] ?? ''));
if ($sessionSysCompCode === '') {
file_put_contents($debugLog, "ERROR: sys_comp_code is empty in session for member_id={$memberId}\n", FILE_APPEND);
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'sys_comp_code_missing',
], JSON_UNESCAPED_UNICODE);
exit;
}
$contentIdRaw = trim((string)($payload['content_id'] ?? ''));
if ($contentIdRaw === '') {
file_put_contents($debugLog, "ERROR: content_id empty\n", FILE_APPEND);
echo json_encode([
'success' => false,
'message' => 'content_id가 올바르지 않습니다.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$toInt = static function ($value): int {
if ($value === null || $value === '') {
return 0;
}
return max(0, (int)$value);
};
$watchTm = $toInt($payload['watch_tm'] ?? 0);
$contentTmInput = $toInt($payload['content_tm'] ?? 0);
$allTmInput = $toInt($payload['all_tm'] ?? 0);
$allTmIncrement = $toInt($payload['all_tm_increment'] ?? 0);
$rawCompleted = (string)($payload['completed'] ?? '0');
$isCompleted = in_array($rawCompleted, ['1', 'true', 'TRUE', 'y', 'Y', 'on', 'ON'], true);
$rawWatching = (string)($payload['is_watching'] ?? 'N');
$isWatching = in_array($rawWatching, ['1', 'true', 'TRUE', 'y', 'Y', 'on', 'ON', 'Y'], true) ? 'Y' : 'N';
$stmtContent = $pdo->prepare(
"SELECT content_id
FROM edu_contents
WHERE content_id = ?
LIMIT 1"
);
$stmtContent->execute([$contentIdRaw]);
$contentRow = $stmtContent->fetch(PDO::FETCH_ASSOC);
$resolvedContentId = $contentRow['content_id'] ?? null;
$defaultContentTm = 0;
if (!$resolvedContentId && preg_match('/^[0-9]+$/', $contentIdRaw)) {
$stmtLegacy = $pdo->prepare(
"SELECT content_id
FROM edu_contents
WHERE content_id LIKE ?
ORDER BY content_id DESC"
);
$stmtLegacy->execute([$contentIdRaw . '-%']);
$legacyRows = $stmtLegacy->fetchAll(PDO::FETCH_ASSOC);
if (count($legacyRows) === 1) {
$resolvedContentId = $legacyRows[0]['content_id'];
$defaultContentTm = 0;
}
}
if (!$resolvedContentId) {
file_put_contents($debugLog, "ERROR: content_id not found in edu_contents - requested={$contentIdRaw}\n", FILE_APPEND);
echo json_encode([
'success' => false,
'message' => 'edu_contents에 존재하지 않는 content_id입니다.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$payloadSysCompCode = trim((string)($payload['sys_comp_code'] ?? ''));
if ($payloadSysCompCode !== '' && $payloadSysCompCode !== $sessionSysCompCode) {
file_put_contents($debugLog, "ERROR: sys_comp_code mismatch (session={$sessionSysCompCode}, payload={$payloadSysCompCode}) for member_id={$memberId}\n", FILE_APPEND);
http_response_code(403);
echo json_encode([
'success' => false,
'message' => 'sys_comp_code_mismatch',
], JSON_UNESCAPED_UNICODE);
exit;
}
$sysCompCode = $sessionSysCompCode;
$stmtUser = $pdo->prepare('SELECT 1 FROM edu_users WHERE member_id = ? AND sys_comp_code = ? LIMIT 1');
$stmtUser->execute([$memberId, $sysCompCode]);
$userExists = (bool)$stmtUser->fetchColumn();
if (!$userExists) {
file_put_contents($debugLog, "ERROR: edu_users row not found for member_id={$memberId}, sys_comp_code={$sysCompCode}\n", FILE_APPEND);
echo json_encode([
'success' => false,
'message' => 'edu_users에 회원 정보가 없어 학습이력을 저장할 수 없습니다.',
'data' => [
'member_id' => $memberId,
'session_member_id' => $sessionMemberId,
'sys_comp_code' => $sysCompCode,
],
], JSON_UNESCAPED_UNICODE);
exit;
}
$contentTm = max($contentTmInput, $defaultContentTm);
$watchTm = min($watchTm, $contentTm > 0 ? $contentTm : $watchTm);
$stmtCurrent = $pdo->prepare(
"SELECT watch_tm, content_tm, all_tm, completed_at
FROM edu_learning_histories
WHERE member_id = ?
AND sys_comp_code = ?
AND content_id = ?
LIMIT 1"
);
$stmtCurrent->execute([$memberId, $sysCompCode, $resolvedContentId]);
$currentRow = $stmtCurrent->fetch(PDO::FETCH_ASSOC) ?: null;
$prevWatchTm = (int)($currentRow['watch_tm'] ?? 0);
$prevContentTm = (int)($currentRow['content_tm'] ?? 0);
$prevAllTm = (int)($currentRow['all_tm'] ?? 0);
$prevCompletedAt = $currentRow['completed_at'] ?? null;
$effectiveContentTm = max($contentTm, $prevContentTm);
$effectiveWatchTm = max($watchTm, $prevWatchTm);
if ($effectiveContentTm > 0) {
$effectiveWatchTm = min($effectiveWatchTm, $effectiveContentTm);
}
$deltaWatchTm = max(0, $effectiveWatchTm - $prevWatchTm);
$effectiveAllTm = max($prevAllTm + $allTmIncrement, $allTmInput, $prevAllTm + $deltaWatchTm);
// 90% 이상 시청 시 완료 처리
$completionByTime = ($effectiveContentTm > 0) && ($effectiveWatchTm >= 0.9 * $effectiveContentTm);
$completedAt = ($isCompleted || $completionByTime) ? ($prevCompletedAt ?: date('Y-m-d H:i:s')) : null;
if ($currentRow) {
$stmtSave = $pdo->prepare(
"UPDATE edu_learning_histories
SET watch_tm = ?,
content_tm = ?,
all_tm = ?,
completed_at = ?,
is_watching = ?,
last_viewed_at = NOW()
WHERE member_id = ?
AND sys_comp_code = ?
AND content_id = ?"
);
$stmtSave->execute([
$effectiveWatchTm,
$effectiveContentTm,
$effectiveAllTm,
$completedAt,
$isWatching,
$memberId,
$sysCompCode,
$resolvedContentId,
]);
} else {
$logEntry = "No existing row found. Attempting INSERT with data: member_id=$memberId, sys_comp_code=$sysCompCode, content_id=$resolvedContentId, watch_tm=$effectiveWatchTm, all_tm=$effectiveAllTm\n";
file_put_contents($debugLog, $logEntry, FILE_APPEND);
try {
$stmtSave = $pdo->prepare(
"INSERT INTO edu_learning_histories
(member_id, sys_comp_code, content_id, first_viewed_at, last_viewed_at, watch_tm, content_tm, all_tm, completed_at, is_watching)
VALUES
(?, ?, ?, NOW(), NOW(), ?, ?, ?, ?, ?)"
);
$stmtSave->execute([
$memberId,
$sysCompCode,
$resolvedContentId,
$effectiveWatchTm,
$effectiveContentTm,
$effectiveAllTm,
$completedAt,
$isWatching,
]);
$logEntry = "INSERT successful\n";
file_put_contents($debugLog, $logEntry, FILE_APPEND);
} catch (PDOException $e) {
$logEntry = "INSERT failed with exception: " . $e->getCode() . " - " . $e->getMessage() . "\n";
file_put_contents($debugLog, $logEntry, FILE_APPEND);
if ((string)$e->getCode() !== '23000') {
throw $e;
}
// 동시 저장 경합으로 동일 PK insert가 충돌하면 content_id 기준 update로 재시도
$logEntry = "Conflict detected. Retrying with UPDATE...\n";
file_put_contents($debugLog, $logEntry, FILE_APPEND);
$stmtRetry = $pdo->prepare(
"UPDATE edu_learning_histories
SET watch_tm = ?,
content_tm = ?,
all_tm = ?,
completed_at = ?,
is_watching = ?,
last_viewed_at = NOW()
WHERE member_id = ?
AND sys_comp_code = ?
AND content_id = ?"
);
$stmtRetry->execute([
$effectiveWatchTm,
$effectiveContentTm,
$effectiveAllTm,
$completedAt,
$isWatching,
$memberId,
$sysCompCode,
$resolvedContentId,
]);
$logEntry = "UPDATE (retry) completed\n";
file_put_contents($debugLog, $logEntry, FILE_APPEND);
}
}
echo json_encode([
'success' => true,
'data' => [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'content_id' => $resolvedContentId,
'requested_content_id' => $contentIdRaw,
'watch_tm' => $effectiveWatchTm,
'content_tm' => $effectiveContentTm,
'all_tm' => $effectiveAllTm,
'completed' => $completedAt !== null,
'completed_at' => $completedAt,
'is_watching' => $isWatching,
],
], JSON_UNESCAPED_UNICODE);
file_put_contents($debugLog, "SUCCESS: saved member_id={$memberId}, sys_comp_code={$sysCompCode}, content_id={$resolvedContentId}, watch_tm={$effectiveWatchTm}, all_tm={$effectiveAllTm}, is_watching={$isWatching}\n", FILE_APPEND);
} catch (Throwable $e) {
file_put_contents($debugLog, "FATAL: " . $e->getCode() . " - " . $e->getMessage() . "\n", FILE_APPEND);
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage(),
], JSON_UNESCAPED_UNICODE);
}
+125
View File
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once dirname(__DIR__) . '/db_conn.php';
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
$memberId = (string)($_SESSION['member_id'] ?? $_SESSION['user_id'] ?? '');
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? $_SESSION['company'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
if ($sysCompCode === '') {
$stmtComp = $pdo->prepare('SELECT sys_comp_code FROM edu_users WHERE member_id = ? ORDER BY sys_comp_code LIMIT 1');
$stmtComp->execute([$memberId]);
$sysCompCode = (string)($stmtComp->fetchColumn() ?: '');
}
if ($sysCompCode === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'sys_comp_code_missing'], JSON_UNESCAPED_UNICODE);
exit;
}
$input = json_decode((string)file_get_contents('php://input'), true) ?: [];
$goalCode = trim((string)($input['goal_code'] ?? ''));
$quarter = trim((string)($input['quarter'] ?? ''));
if ($goalCode === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'goal_code_required'], JSON_UNESCAPED_UNICODE);
exit;
}
if ($quarter !== '') {
$stmtGoal = $pdo->prepare('SELECT goal_code, quarter FROM edu_learning_goals WHERE is_active = \'1\' AND goal_code = ? AND quarter = ? LIMIT 1');
$stmtGoal->execute([$goalCode, $quarter]);
} else {
$stmtGoal = $pdo->prepare('SELECT goal_code, quarter FROM edu_learning_goals WHERE is_active = \'1\' AND goal_code = ? LIMIT 1');
$stmtGoal->execute([$goalCode]);
}
$goalRow = $stmtGoal->fetch(PDO::FETCH_ASSOC);
if (!$goalRow) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'goal_not_found'], JSON_UNESCAPED_UNICODE);
exit;
}
$goalCode = (string)$goalRow['goal_code'];
$quarter = trim((string)($goalRow['quarter'] ?? $quarter));
$pdo->beginTransaction();
if ($quarter !== '') {
$stmtDeactivate = $pdo->prepare(
'UPDATE edu_user_learning_goals
SET is_active = \'0\', updated_by = ?, updated_at = NOW()
WHERE member_id = ?
AND sys_comp_code = ?
AND quarter = ?
AND goal_code <> ?
AND is_active = \'1\'
AND (completed_date IS NULL OR completed_date = \'0000-00-00\')'
);
$stmtDeactivate->execute([$memberId, $memberId, $sysCompCode, $quarter, $goalCode]);
}
$stmtExists = $pdo->prepare(
'SELECT 1
FROM edu_user_learning_goals
WHERE member_id = ?
AND sys_comp_code = ?
AND goal_code = ?
LIMIT 1'
);
$stmtExists->execute([$memberId, $sysCompCode, $goalCode]);
$exists = (bool)$stmtExists->fetchColumn();
if ($exists) {
$stmtUp = $pdo->prepare(
'UPDATE edu_user_learning_goals
SET quarter = ?, is_active = \'1\', updated_by = ?, updated_at = NOW()
WHERE member_id = ?
AND sys_comp_code = ?
AND goal_code = ?'
);
$stmtUp->execute([$quarter, $memberId, $memberId, $sysCompCode, $goalCode]);
} else {
$stmtIn = $pdo->prepare(
'INSERT INTO edu_user_learning_goals
(member_id, sys_comp_code, goal_code, quarter, is_active, created_by, created_at, updated_by, updated_at)
VALUES (?, ?, ?, ?, \'1\', ?, NOW(), ?, NOW())'
);
$stmtIn->execute([$memberId, $sysCompCode, $goalCode, $quarter, $memberId, $memberId]);
}
$pdo->commit();
echo json_encode([
'success' => true,
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'goal_code' => $goalCode,
'quarter' => $quarter,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('[save_user_learning_goal] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/**
* 영상 시청 시간 저장 API
* POST /bbs/api/save_video_time.php
*
* Parameters:
* - content_id (필수): 영상 content ID
* - current_seconds (필수): 현재 시청 시간 (초 단위)
*/
session_start();
header('Content-Type: application/json; charset=utf-8');
// 세션 체크
if (!isset($_SESSION['member_id'])) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
try {
require $_SERVER['DOCUMENT_ROOT'] . '/www/baroncs/dbconfig.php';
require $_SERVER['DOCUMENT_ROOT'] . '/www/baroncs/head.php';
$member_id = intval($_SESSION['member_id']);
$content_id = intval($_POST['content_id'] ?? 0);
$current_seconds = intval($_POST['current_seconds'] ?? 0);
if ($content_id <= 0) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid content_id']);
exit;
}
// 마이클래스 영상인지 확인 (CA10001 카테고리)
$sql = "SELECT id, category_code FROM edu_contents WHERE id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $content_id);
$stmt->execute();
$result = $stmt->get_result();
$content = $result->fetch_assoc();
$stmt->close();
if (!$content) {
http_response_code(404);
echo json_encode(['success' => false, 'error' => 'Content not found']);
exit;
}
$is_myclass = ($content['category_code'] === 'CA10001');
// 시청 시간 저장 (마이클래스만)
if ($is_myclass) {
// edu_video_playback 테이블에 저장
$check_sql = "SELECT id FROM edu_video_playback WHERE member_id = ? AND content_id = ?";
$check_stmt = $conn->prepare($check_sql);
$check_stmt->bind_param("ii", $member_id, $content_id);
$check_stmt->execute();
$check_result = $check_stmt->get_result();
$exists = $check_result->fetch_assoc();
$check_stmt->close();
if ($exists) {
// 기존 레코드 업데이트
$update_sql = "UPDATE edu_video_playback SET current_seconds = ?, updated_at = NOW() WHERE member_id = ? AND content_id = ?";
$update_stmt = $conn->prepare($update_sql);
$update_stmt->bind_param("iii", $current_seconds, $member_id, $content_id);
$update_stmt->execute();
$update_stmt->close();
} else {
// 새 레코드 생성
$insert_sql = "INSERT INTO edu_video_playback (member_id, content_id, current_seconds) VALUES (?, ?, ?)";
$insert_stmt = $conn->prepare($insert_sql);
$insert_stmt->bind_param("iii", $member_id, $content_id, $current_seconds);
$insert_stmt->execute();
$insert_stmt->close();
}
}
echo json_encode([
'success' => true,
'content_id' => $content_id,
'current_seconds' => $current_seconds,
'is_myclass' => $is_myclass
]);
} catch (Exception $e) {
error_log('[save_video_time.php] Error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
?>
+92
View File
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once dirname(__DIR__) . '/db_conn.php';
$memberId = trim((string)($_SESSION['member_id'] ?? ''));
$sysCompCode = trim((string)($_SESSION['sys_comp_code'] ?? ''));
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => '로그인이 필요합니다.'], JSON_UNESCAPED_UNICODE);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'POST only'], JSON_UNESCAPED_UNICODE);
exit;
}
$contentId = trim((string)($_POST['content_id'] ?? ''));
$isActive = trim((string)($_POST['is_active'] ?? ''));
if ($contentId === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'content_id 필수'], JSON_UNESCAPED_UNICODE);
exit;
}
// is_active: '1' → 활성, 그 외 → '0' 비활성
$activeValue = ($isActive === '1') ? '1' : '0';
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// sys_comp_code가 세션에 없으면 DB에서 조회
if ($sysCompCode === '') {
$stmtUser = $pdo->prepare('SELECT sys_comp_code FROM edu_users WHERE member_id = ? ORDER BY sys_comp_code LIMIT 1');
$stmtUser->execute([$memberId]);
$row = $stmtUser->fetch();
$sysCompCode = (string)($row['sys_comp_code'] ?? '');
}
if ($sysCompCode === '') {
echo json_encode(['success' => false, 'message' => '회사코드 확인 불가'], JSON_UNESCAPED_UNICODE);
exit;
}
// 기존 레코드 확인
$stmtCheck = $pdo->prepare(
'SELECT content_id, is_active
FROM edu_content_wishlist
WHERE content_id = ?
AND member_id = ?
AND sys_comp_code = ?'
);
$stmtCheck->execute([$contentId, $memberId, $sysCompCode]);
$existing = $stmtCheck->fetch();
if ($existing) {
// UPDATE
$stmtUpdate = $pdo->prepare(
'UPDATE edu_content_wishlist
SET is_active = ?, updated_at = NOW()
WHERE content_id = ?
AND member_id = ?
AND sys_comp_code = ?'
);
$stmtUpdate->execute([$activeValue, $contentId, $memberId, $sysCompCode]);
} else {
// INSERT
$stmtInsert = $pdo->prepare(
'INSERT INTO edu_content_wishlist
(content_id, member_id, sys_comp_code, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, NOW(), NOW())'
);
$stmtInsert->execute([$contentId, $memberId, $sysCompCode, $activeValue]);
}
echo json_encode(['success' => true, 'is_active' => $activeValue], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => '서버 오류'], JSON_UNESCAPED_UNICODE);
}
+180
View File
@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/common.php';
require_once dirname(__DIR__) . '/db_conn.php';
api_header_json();
$user = api_require_login();
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
/**
* Normalize keyword for storage (trim + max 50 chars).
*/
function normalize_keyword(string $value): string {
$keyword = trim($value);
if ($keyword === '') {
return '';
}
if (function_exists('mb_substr')) {
return mb_substr($keyword, 0, 50, 'UTF-8');
}
return substr($keyword, 0, 50);
}
/**
* Insert one search log row using per-user seq and short retry on PK conflicts.
*/
function insert_search_log(PDO $pdo, string $memberId, string $sysCompCode, string $keyword): array {
$maxAttempts = 3;
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
try {
$pdo->beginTransaction();
$stmtSeq = $pdo->prepare(
'SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq
FROM edu_search_logs
WHERE member_id = ?
AND sys_comp_code = ?'
);
$stmtSeq->execute([$memberId, $sysCompCode]);
$nextSeq = (int)($stmtSeq->fetchColumn() ?: 1);
$stmtInsert = $pdo->prepare(
'INSERT INTO edu_search_logs
(member_id, sys_comp_code, seq, keyword, searched_at)
VALUES
(?, ?, ?, ?, NOW())'
);
$stmtInsert->execute([$memberId, $sysCompCode, $nextSeq, $keyword]);
$pdo->commit();
return [
'seq' => $nextSeq,
'keyword' => $keyword,
'searched_at' => date('Y-m-d H:i:s'),
'retry_count' => $attempt - 1,
];
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$sqlState = '';
if ($e instanceof PDOException && isset($e->errorInfo[0])) {
$sqlState = (string)$e->errorInfo[0];
}
$isDuplicateKey = ($sqlState === '23000');
if (!$isDuplicateKey || $attempt >= $maxAttempts) {
throw $e;
}
}
}
throw new RuntimeException('search_log_insert_retry_exceeded');
}
if ($method === 'GET') {
$windowDays = 7;
$limit = (int)($_GET['limit'] ?? 20);
if ($limit < 1) {
$limit = 1;
} elseif ($limit > 50) {
$limit = 50;
}
try {
$pdo = db_conn();
$stmt = $pdo->prepare(
'SELECT seq, keyword, searched_at
FROM edu_search_logs
WHERE member_id = ?
AND sys_comp_code = ?
AND searched_at >= (NOW() - INTERVAL 7 DAY)
ORDER BY searched_at DESC, seq DESC
LIMIT ' . $limit
);
$stmt->execute([(string)$user['member_id'], (string)$user['sys_comp_code']]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
$logs = array_map(static function (array $row): array {
return [
'seq' => (int)($row['seq'] ?? 0),
'keyword' => (string)($row['keyword'] ?? ''),
'searched_at' => (string)($row['searched_at'] ?? ''),
];
}, $rows);
echo json_encode([
'success' => true,
'data' => [
'logs' => $logs,
'window_days' => $windowDays,
'limit' => $limit,
'member_id' => (string)$user['member_id'],
'sys_comp_code' => (string)$user['sys_comp_code'],
],
'meta' => [
'api' => 'search_logs',
'version' => 1,
'status' => 'ok',
],
], JSON_UNESCAPED_UNICODE);
exit;
} catch (Throwable $e) {
error_log('[search_logs][GET] ' . $e->getMessage());
api_error(500, 'search_logs_fetch_failed');
}
}
if ($method === 'POST') {
$input = api_get_input();
$keyword = normalize_keyword((string)($input['keyword'] ?? ''));
if ($keyword === '') {
api_error(400, 'keyword_required');
}
try {
$pdo = db_conn();
$inserted = insert_search_log(
$pdo,
(string)$user['member_id'],
(string)$user['sys_comp_code'],
$keyword
);
echo json_encode([
'success' => true,
'data' => [
'accepted' => true,
'keyword' => $inserted['keyword'],
'seq' => (int)$inserted['seq'],
'searched_at' => (string)$inserted['searched_at'],
'member_id' => (string)$user['member_id'],
'sys_comp_code' => (string)$user['sys_comp_code'],
],
'meta' => [
'api' => 'search_logs',
'version' => 1,
'status' => 'ok',
'retry_count' => (int)$inserted['retry_count'],
],
], JSON_UNESCAPED_UNICODE);
exit;
} catch (Throwable $e) {
error_log('[search_logs][POST] ' . $e->getMessage());
api_error(500, 'search_log_insert_failed');
}
}
api_error(405, 'method_not_allowed', [
'allowed_methods' => ['GET', 'POST'],
]);
+109
View File
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once dirname(__DIR__) . '/db_conn.php';
require_once dirname(__DIR__) . '/auth.php';
edu_start_session();
$memberId = edu_current_member_id();
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
if ($memberId === '') {
echo json_encode(['success' => false, 'error' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// edu_user_keywords 테이블 없으면 생성 (권한 없으면 무시)
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS edu_user_keywords (
member_id VARCHAR(20) NOT NULL,
sys_comp_code VARCHAR(20) NOT NULL DEFAULT '',
keyword_code VARCHAR(20) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
PRIMARY KEY (member_id, sys_comp_code, keyword_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
} catch (Throwable $__ce) { error_log('[user_keywords] CREATE: ' . $__ce->getMessage()); }
if ($sysCompCode === '') {
$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() ?: '');
}
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// ── POST: 사용자 키워드 저장 ──────────────────────────
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$rawKws = array_slice(
array_values(array_filter(array_map('trim', $input['keywords'] ?? []))),
0, 3
);
// 한글 키워드명 → KW 코드 변환
$nameToKw = [];
$codeRows = $pdo->query("SELECT base_code, code_name FROM edu_codes WHERE group_code = 'KW100'")->fetchAll(PDO::FETCH_ASSOC);
foreach ($codeRows as $cr) {
$nameToKw[$cr['code_name']] = $cr['base_code'];
}
// 기존 삭제 후 재삽입
try {
$delStmt = $pdo->prepare('DELETE FROM edu_user_keywords WHERE member_id = ? AND sys_comp_code = ?');
$delStmt->execute([$memberId, $sysCompCode]);
error_log('[user_keywords] DELETE completed');
} catch (Throwable $__de) {
error_log('[user_keywords] DELETE failed: ' . $__de->getMessage());
}
$insertCnt = 0;
try {
$stmtIns = $pdo->prepare(
'INSERT INTO edu_user_keywords (member_id, sys_comp_code, keyword_code) VALUES (?, ?, ?)'
);
foreach ($rawKws as $i => $kw) {
$code = $nameToKw[$kw] ?? null;
if ($code) {
$stmtIns->execute([$memberId, $sysCompCode, $code]);
$insertCnt++;
error_log('[user_keywords] INSERT: keyword=' . $kw . ' -> code=' . $code);
}
}
error_log('[user_keywords] total INSERT: ' . $insertCnt);
} catch (Throwable $__ie) {
error_log('[user_keywords] INSERT failed: ' . $__ie->getMessage());
echo json_encode(['success' => false, 'error' => 'insert_failed: ' . $__ie->getMessage()], JSON_UNESCAPED_UNICODE);
exit;
}
echo json_encode(['success' => true, 'inserted' => $insertCnt], JSON_UNESCAPED_UNICODE);
exit;
}
// ── GET: 사용자 키워드 조회 ── (sys_comp_code 조건 제거, member_id만으로 조회)
$stmt = $pdo->prepare("
SELECT uk.keyword_code, ec.code_name AS keyword_name
FROM edu_user_keywords uk
JOIN edu_codes ec ON ec.base_code = uk.keyword_code
WHERE uk.member_id = ?
ORDER BY uk.keyword_code
LIMIT 3
");
$stmt->execute([$memberId]);
$keywords = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'keywords' => $keywords], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[user_keywords] ' . $e->getMessage());
echo json_encode(['success' => false, 'error' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+438
View File
@@ -0,0 +1,438 @@
<?php
/**
* bbs/api/videos_by_keywords.php
* ─────────────────────────────────────────────────────────────────────
* 활성 키워드 기반 영상 목록을 반환하는 AJAX 엔드포인트
*
* Request POST application/json
* { "my_keywords": ["AI","리더십"], "admin_keywords": ["경제"] }
*
* Response application/json
* { "success": true, "videos": [ ...video objects... ] }
*
* 슬롯 구성:
* [0] Pick 영상 (is_offer=1, 항상 고정)
* [1-5] 활성 키워드(my + admin 합집합)에 매칭되는 영상, 랜덤 최대 5개
* → 부족하면 랜덤 보충
* ─────────────────────────────────────────────────────────────────────
*/
header('Content-Type: application/json; charset=utf-8');
require_once dirname(__DIR__) . '/db_conn.php';
require_once dirname(__DIR__) . '/auth.php';
edu_start_session();
$memberId = edu_current_member_id();
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? '');
if ($memberId === '' || $sysCompCode === '') {
echo json_encode(['success' => false, 'error' => 'not_logged_in_or_invalid_session'], JSON_UNESCAPED_UNICODE);
exit;
}
// ── 요청 파싱 ──────────────────────────────────────────────────────
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$myKwRaw = array_values(array_filter(array_map('trim', $input['my_keywords'] ?? [])));
$adminKwRaw = array_values(array_filter(array_map('trim', $input['admin_keywords'] ?? [])));
$allInputKw = array_values(array_unique(array_merge($myKwRaw, $adminKwRaw)));
function edu_norm_code(string $code): string
{
return preg_replace('/[^A-Z0-9]/', '', strtoupper(trim($code)));
}
function edu_looks_like_code(string $value): bool
{
return preg_match('/^[A-Z]{2}[A-Z0-9]{3,}$/', strtoupper(trim($value))) === 1;
}
// ── mapRow (main_data.php mapContentRow 와 동일 로직) ─────────────────
function mapRow(array $row, string $picker = ''): array
{
global $eduCodeNameMap;
// keyword_code 는 한글 텍스트로 저장돼 있음
// GROUP_CONCAT 에서 이미 한글로 변환된 값이 옴
$kwStr = $row['keywords'] ?? '';
$keywords = $kwStr !== '' ? array_values(array_unique(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=|\.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}" : '';
$thumbnail = $thumbFromDb !== ''
? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
$categoryCode = trim((string)($row['category_code'] ?? ''));
// SQL COALESCE(ec.code_name, c.category_code) AS category_name 으로 가져온 한글명 직접 사용
$categoryName = trim((string)($row['category_name'] ?? $categoryCode));
if (($categoryName === '' || edu_looks_like_code($categoryName)) && $categoryCode !== '') {
$mappedCategory = $eduCodeNameMap[edu_norm_code($categoryCode)] ?? '';
if ($mappedCategory !== '') {
$categoryName = $mappedCategory;
}
}
$groupRaw = trim((string)($row['category_group'] ?? ''));
$subcate = trim((string)($row['category_group_name'] ?? $groupRaw));
if (($subcate === '' || edu_looks_like_code($subcate)) && $groupRaw !== '') {
$mappedGroup = $eduCodeNameMap[edu_norm_code($groupRaw)] ?? '';
if ($mappedGroup !== '') {
$subcate = $mappedGroup;
}
}
return [
'id' => $row['content_id'],
'content_id' => $row['content_id'],
'url' => $url,
'thumbnail' => $thumbnail,
'category' => $categoryName,
'category_code' => $categoryCode,
'subcate' => $subcate,
'bookmark' => (bool)($row['is_bookmarked'] ?? false),
'title' => $row['title'] ?? '',
'picker' => $picker,
'type' => 'main',
'keywords' => $keywords,
'gauge' => $gauge,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'all_tm' => (int)($row['all_tm'] ?? 0),
'watch_min' => (int)floor(((int)($row['watch_tm'] ?? 0)) / 60),
'content_min' => (int)floor(((int)($row['content_tm'] ?? 0)) / 60),
'all_min' => (int)floor(((int)($row['all_tm'] ?? 0)) / 60),
];
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$eduCodeNameMap = [];
try {
$stmtCodeMap = $pdo->query("SELECT base_code, code_name FROM edu_codes");
foreach ($stmtCodeMap->fetchAll(PDO::FETCH_ASSOC) as $cr) {
$k = edu_norm_code((string)($cr['base_code'] ?? ''));
if ($k === '') continue;
$eduCodeNameMap[$k] = (string)($cr['code_name'] ?? '');
}
} catch (Throwable $ignore) {
$eduCodeNameMap = [];
}
// ── edu_codes 매핑 로드 (KW↔한글 양방향) ───────────────────────
$codeStmt = $pdo->query("SELECT base_code, code_name FROM edu_codes WHERE group_code = 'KW100'");
$kwToName = []; // KW10001 => 온보딩
$nameToKw = []; // 온보딩 => KW10001
foreach ($codeStmt->fetchAll(PDO::FETCH_ASSOC) as $cr) {
$kwToName[$cr['base_code']] = $cr['code_name'];
$nameToKw[$cr['code_name']] = $cr['base_code'];
}
// 입력 키워드(한글)를 KW코드로 정규화
$allActiveKw = [];
foreach ($allInputKw as $kw) {
if (isset($kwToName[$kw])) {
$allActiveKw[] = $kw; // 이미 KW코드
} elseif (isset($nameToKw[$kw])) {
$allActiveKw[] = $nameToKw[$kw]; // 한글 → KW코드
}
}
$allActiveKw = array_values(array_unique($allActiveKw));
// 키워드가 비어있으면 전체 KW 코드 사용 (항상 영상 표시)
if (empty($allActiveKw)) {
$allActiveKw = array_keys($kwToName);
}
$stmtPick = $pdo->prepare("
SELECT c.*,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(ec_grp.code_name, c.category_group) AS category_group_name,
COALESCE(lh.watch_tm, 0) AS watch_tm,
COALESCE(lh.content_tm, 0) AS content_tm,
COALESCE(lh.all_tm, 0) AS all_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(COALESCE(kwec.code_name, ck.keyword_code) ORDER BY ck.keyword_code SEPARATOR ',') AS keywords,
u.name AS picker_name
FROM edu_contents c
LEFT JOIN edu_codes ec ON ec.base_code = c.category_code
LEFT JOIN edu_codes ec_grp ON TRIM(UPPER(ec_grp.base_code)) = TRIM(UPPER(c.category_group))
LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id
AND ck.is_active = 1
LEFT JOIN edu_codes kwec ON kwec.base_code = ck.keyword_code
LEFT JOIN edu_content_offer co ON co.offer_id = c.offer_id
LEFT JOIN edu_users u ON u.member_id = co.member_id
AND u.sys_comp_code = co.sys_comp_code
LEFT JOIN edu_content_wishlist cw ON cw.member_id = :mid_w
AND cw.sys_comp_code = :sc_w
AND cw.is_active = 1
AND cw.content_id = c.content_id
LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id
AND lh.member_id = :mid_lh
AND lh.sys_comp_code = :sc_lh
WHERE c.is_offer = 1
AND (c.is_active = 1 OR c.is_active IS NULL)
AND c.issue_type_code = 'IS10003'
GROUP BY c.content_id
ORDER BY c.sort_order, RAND()
LIMIT 1
");
$stmtPick->execute([
':mid_w' => $memberId,
':sc_w' => $sysCompCode,
':mid_lh' => $memberId,
':sc_lh' => $sysCompCode,
]);
$pickRow = $stmtPick->fetch();
$pickVideo = $pickRow ? mapRow($pickRow, $pickRow['picker_name'] ?? '동료') : null;
$usedIds = $pickVideo ? [$pickVideo['id']] : [];
// ── 활성 키워드 매칭 영상 (슬롯 1~5) ─────────────────────────
$kwVideos = [];
if (!empty($allActiveKw)) {
$phKw = implode(',', array_fill(0, count($allActiveKw), '?'));
$excClause = !empty($usedIds)
? 'AND c.content_id NOT IN (' . implode(',', array_fill(0, count($usedIds), '?')) . ')'
: '';
$stmt = $pdo->prepare("
SELECT c.*,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(ec_grp.code_name, c.category_group) AS category_group_name,
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(COALESCE(kwec.code_name, ck.keyword_code) ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_contents c
LEFT JOIN edu_codes ec ON ec.base_code = c.category_code
LEFT JOIN edu_codes ec_grp ON TRIM(UPPER(ec_grp.base_code)) = TRIM(UPPER(c.category_group))
LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id
AND ck.is_active = 1
LEFT JOIN edu_codes kwec ON kwec.base_code = ck.keyword_code
LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_wishlist cw ON cw.member_id = ?
AND cw.sys_comp_code = ?
AND cw.is_active = 1
AND cw.content_id = c.content_id
WHERE (c.is_active = 1 OR c.is_active IS NULL)
AND (c.is_offer IS NULL OR c.is_offer != 1)
AND c.category_code IN ('CA10004', 'CA10005')
AND c.content_id IN (
SELECT DISTINCT content_id
FROM edu_content_keywords
WHERE keyword_code IN ({$phKw})
AND is_active = 1
)
{$excClause}
GROUP BY c.content_id
ORDER BY RAND()
LIMIT 5
");
$params = [$memberId, $sysCompCode, $memberId, $sysCompCode];
$params = array_merge($params, $allActiveKw);
if (!empty($usedIds)) {
$params = array_merge($params, $usedIds);
}
$stmt->execute($params);
foreach ($stmt->fetchAll() as $r) {
$kwVideos[] = mapRow($r);
}
}
// ── 최종 조합 ─────────────────────────────────────────────────
// 키워드 매칭 영상이 5개 미만이면 랜덤 영상으로 보충 (최대 5개까지)
$usedIdsAll = $usedIds;
foreach ($kwVideos as $v) { $usedIdsAll[] = $v['id']; }
$need = 5 - count($kwVideos);
if ($need > 0) {
$excAll = !empty($usedIdsAll)
? 'AND c.content_id NOT IN (' . implode(',', array_fill(0, count($usedIdsAll), '?')) . ')'
: '';
$stmtFill = $pdo->prepare("
SELECT c.*,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(ec_grp.code_name, c.category_group) AS category_group_name,
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(COALESCE(kwec.code_name, ck.keyword_code) ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_contents c
LEFT JOIN edu_codes ec ON ec.base_code = c.category_code
LEFT JOIN edu_codes ec_grp ON TRIM(UPPER(ec_grp.base_code)) = TRIM(UPPER(c.category_group))
LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id
AND ck.is_active = 1
LEFT JOIN edu_codes kwec ON kwec.base_code = ck.keyword_code
LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_wishlist cw ON cw.member_id = ?
AND cw.sys_comp_code = ?
AND cw.is_active = 1
AND cw.content_id = c.content_id
WHERE (c.is_active = 1 OR c.is_active IS NULL)
AND (c.is_offer IS NULL OR c.is_offer != 1)
AND c.category_code IN ('CA10004', 'CA10005')
AND c.content_id IN (SELECT DISTINCT content_id FROM edu_content_keywords WHERE is_active = 1)
{$excAll}
GROUP BY c.content_id
ORDER BY RAND()
LIMIT {$need}
");
$fillParams = [$memberId, $sysCompCode, $memberId, $sysCompCode];
if (!empty($usedIdsAll)) {
$fillParams = array_merge($fillParams, $usedIdsAll);
}
$stmtFill->execute($fillParams);
foreach ($stmtFill->fetchAll() as $r) {
$kwVideos[] = mapRow($r);
}
}
// 항상 6개: [0]=pick영상, [1~5]=키워드 관련영상(중복X), 부족하면 랜덤, 그래도 부족하면 빈카드
$videos = [];
if ($pickVideo) {
$videos[] = $pickVideo;
}
// pick영상 ID를 제외한 키워드 영상 5개
$cnt = 0;
foreach ($kwVideos as $v) {
if ($pickVideo && $v['id'] == $pickVideo['id']) continue;
$videos[] = $v;
$cnt++;
if ($cnt >= 5) break;
}
// 키워드 영상이 5개 미만이면 랜덤 영상으로 보충 (pick/키워드 중복 제외)
$need = 6 - count($videos);
if ($need > 0) {
$usedIdsAll = array_map(function($v){return is_array($v)?$v['id']:$v;}, $videos);
$excAll = !empty($usedIdsAll)
? 'AND c.content_id NOT IN (' . implode(',', array_fill(0, count($usedIdsAll), '?')) . ')'
: '';
$stmtFill = $pdo->prepare("
SELECT c.*,
COALESCE(ec.code_name, c.category_code) AS category_name,
COALESCE(ec_grp.code_name, c.category_group) AS category_group_name,
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(COALESCE(kwec.code_name, ck.keyword_code) ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_contents c
LEFT JOIN edu_codes ec ON ec.base_code = c.category_code
LEFT JOIN edu_codes ec_grp ON TRIM(UPPER(ec_grp.base_code)) = TRIM(UPPER(c.category_group))
LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id
AND ck.is_active = 1
LEFT JOIN edu_codes kwec ON kwec.base_code = ck.keyword_code
LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_wishlist cw ON cw.member_id = ?
AND cw.sys_comp_code = ?
AND cw.is_active = 1
AND cw.content_id = c.content_id
WHERE (c.is_active = 1 OR c.is_active IS NULL)
AND (c.is_offer IS NULL OR c.is_offer != 1)
AND c.category_code IN ('CA10004', 'CA10005')
AND c.content_id IN (SELECT DISTINCT content_id FROM edu_content_keywords WHERE is_active = 1)
{$excAll}
GROUP BY c.content_id
ORDER BY RAND()
LIMIT {$need}
");
$fillParams = [$memberId, $sysCompCode, $memberId, $sysCompCode];
if (!empty($usedIdsAll)) {
$fillParams = array_merge($fillParams, $usedIdsAll);
}
$stmtFill->execute($fillParams);
foreach ($stmtFill->fetchAll() as $r) {
$videos[] = mapRow($r);
}
}
// 그래도 부족하면 빈 카드로 패딩
while (count($videos) < 6) {
$videos[] = [
'id' => 'empty_' . count($videos),
'url' => '',
'thumbnail' => '',
'category' => '',
'category_code' => '',
'subcate' => '',
'bookmark' => false,
'title' => '',
'picker' => '',
'type' => 'main',
'keywords' => [],
'gauge' => 0,
'watch_tm' => 0,
'content_tm' => 0,
'all_tm' => 0,
'watch_min' => 0,
'content_min' => 0,
'all_min' => 0,
];
}
// 메인 게이지용: 회원 누적 시청시간(watch_tm, 초) 합계
$stmtTotal = $pdo->prepare("\n SELECT COALESCE(SUM(watch_tm), 0) AS total_watch_tm\n FROM edu_learning_histories\n WHERE member_id = ? AND sys_comp_code = ?\n ");
$stmtTotal->execute([$memberId, $sysCompCode]);
$totalWatchTm = (int)$stmtTotal->fetchColumn();
$totalMin = (int)floor($totalWatchTm / 60);
// 전체 평균 학습시간 계산: (전사 모든 사용자의 총 watch_tm 합계) / (학습경험이 있는 사용자 수)
$avgWatchMin = 50; // 기본값
try {
$stmtAvg = $pdo->prepare("
SELECT
COALESCE(SUM(h.watch_tm), 0) as total_watch_tm,
COUNT(DISTINCT h.member_id) as unique_members
FROM edu_learning_histories h
WHERE h.sys_comp_code = ?
");
$stmtAvg->execute([$sysCompCode]);
$avgRow = $stmtAvg->fetch(PDO::FETCH_ASSOC);
if ($avgRow) {
$totalWatchTmAll = (int)($avgRow['total_watch_tm'] ?? 0);
$uniqueMembers = (int)($avgRow['unique_members'] ?? 0);
if ($uniqueMembers > 0) {
$avgWatchMin = (int)floor($totalWatchTmAll / $uniqueMembers / 60);
$avgWatchMin = max(50, $avgWatchMin);
}
}
} catch (Exception $e) {
error_log('[videos_by_keywords] avgWatchMin calculation failed: ' . $e->getMessage());
}
echo json_encode(
[
'success' => true,
'videos' => $videos,
'total_watch_tm' => $totalWatchTm,
'total_all_tm' => $totalWatchTm,
'total_min' => $totalMin,
'avg_watch_min' => $avgWatchMin,
],
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP
);
} catch (Exception $e) {
error_log('[videos_by_keywords] ' . $e->getMessage());
echo json_encode(['success' => false, 'videos' => [], 'error' => $e->getMessage()]);
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* bbs/api_log.php
* ─────────────────────────────────────────────────────────────────
* 공통 로그 기록 헬퍼
*
* 사용법:
* require_once __DIR__ . '/api_log.php';
* api_log('save_learning', 'unregistered content', ['content_id' => 'F1']);
* ─────────────────────────────────────────────────────────────────
*/
define('API_LOG_FILE', __DIR__ . '/logs/api.log');
define('API_LOG_MAX_BYTES', 512 * 1024); // 512KB 초과 시 rotate
/**
* 로그 한 줄 기록
* @param string $tag 태그 (파일명/기능명)
* @param string $message 메시지
* @param array $context 추가 데이터 (선택)
*/
function api_log(string $tag, string $message, array $context = []): void
{
$logDir = dirname(API_LOG_FILE);
if (!is_dir($logDir)) {
@mkdir($logDir, 0755, true);
// 디렉토리 직접 접근 차단
@file_put_contents($logDir . '/.htaccess', "Deny from all\n");
}
// 512KB 초과 시 rotate (이전 로그 보존)
if (file_exists(API_LOG_FILE) && filesize(API_LOG_FILE) > API_LOG_MAX_BYTES) {
@rename(API_LOG_FILE, API_LOG_FILE . '.' . date('YmdHis') . '.bak');
}
$ts = date('Y-m-d H:i:s');
$ctx = empty($context) ? '' : ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
$line = "[{$ts}] [{$tag}] {$message}{$ctx}" . PHP_EOL;
@file_put_contents(API_LOG_FILE, $line, FILE_APPEND | LOCK_EX);
}
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
if (!function_exists('edu_start_session')) {
function edu_start_session(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
}
if (!function_exists('edu_current_member_id')) {
function edu_current_member_id(): string
{
edu_start_session();
return trim((string)($_SESSION['ss_mb_id'] ?? $_SESSION['member_id'] ?? ''));
}
}
if (!function_exists('edu_is_logged_in')) {
function edu_is_logged_in(): bool
{
return edu_current_member_id() !== '';
}
}
if (!function_exists('edu_current_user')) {
function edu_current_user(): array
{
edu_start_session();
$user = $_SESSION['edu_user'] ?? [];
return is_array($user) ? $user : [];
}
}
if (!function_exists('edu_user_field')) {
function edu_user_field(string $field, $default = null)
{
$user = edu_current_user();
if (array_key_exists($field, $user)) {
return $user[$field];
}
edu_start_session();
$sessionKey = 'edu_user_' . $field;
if (array_key_exists($sessionKey, $_SESSION)) {
return $_SESSION[$sessionKey];
}
return $default;
}
}
if (!function_exists('edu_require_login')) {
function edu_require_login(): void
{
if (edu_is_logged_in()) {
return;
}
header('Location: http://erp.baroncs.co.kr/mobile/sys/controller/Study_controller.php');
exit;
}
}
+328
View File
@@ -0,0 +1,328 @@
<?php
require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/api_log.php';
// 외부(호출하는 파일)에서 $SET_PREFIX를 정의하지 않았을 경우를 대비한 기본값 설정
$prefixCondition = isset($SET_PREFIX) ? $SET_PREFIX : 'L'; //L=리더십(기본값), I=인사이트
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
if (!function_exists('edu_extract_youtube_id')) {
function edu_extract_youtube_id(string $rawUrl): string
{
$value = trim($rawUrl);
if ($value === '') {
return '';
}
if (preg_match('/^[A-Za-z0-9_-]{11}$/', $value) === 1) {
return $value;
}
if (strpos($value, 'youtube.com/watch') !== false) {
$query = parse_url($value, PHP_URL_QUERY);
if (is_string($query)) {
parse_str($query, $params);
return trim((string) ($params['v'] ?? ''));
}
}
if (strpos($value, 'youtu.be/') !== false) {
return trim((string) preg_replace('/[?&#].*$/', '', substr($value, strpos($value, 'youtu.be/') + 9)));
}
if (strpos($value, 'youtube.com/embed/') !== false) {
return trim((string) preg_replace('/[?&#].*$/', '', substr($value, strpos($value, 'youtube.com/embed/') + 18)));
}
return '';
}
}
if (!function_exists('edu_build_youtube_thumbnail')) {
function edu_build_youtube_thumbnail(string $rawUrl, string $thumbnailName = 'hqdefault.jpg'): string
{
$videoId = edu_extract_youtube_id($rawUrl);
if ($videoId === '') {
return '/img/video/img_thumb_01.png';
}
$thumbnailName = trim($thumbnailName);
if ($thumbnailName === '') {
$thumbnailName = 'hqdefault.jpg';
}
return 'https://img.youtube.com/vi/' . rawurlencode($videoId) . '/' . rawurlencode($thumbnailName);
}
}
if (!function_exists('edu_normalize_youtube_url')) {
function edu_normalize_youtube_url(string $rawUrl): string
{
$value = trim($rawUrl);
if ($value === '') {
return '';
}
if (preg_match('/^https?:\/\//i', $value) === 1) {
return $value;
}
$videoId = edu_extract_youtube_id($value);
if ($videoId === '') {
return $value;
}
return 'https://www.youtube.com/watch?v=' . rawurlencode($videoId);
}
}
if (!function_exists('edu_build_profile_image_path')) {
function edu_build_profile_image_path(string $memberId, string $sysCompCode = '', string $belongCompId = '', string $workingCompId = ''): string
{
$memberId = trim($memberId);
if ($memberId === '') {
return '/img/ico/ico_user.svg';
}
$teamCodeSource = '';
foreach ([$workingCompId, $belongCompId, $sysCompCode] as $candidate) {
$candidate = preg_replace('/[^A-Za-z0-9]/', '', strtoupper(trim((string) $candidate)));
if ($candidate !== '') {
$teamCodeSource = $candidate;
break;
}
}
$teamCode = $teamCodeSource === '' ? '' : substr($teamCodeSource, -2);
if ($teamCode === '') {
return '/img/ico/ico_user.svg';
}
return '/img/profile/' . rawurlencode($memberId . '_' . $teamCode) . '.png';
}
}
//Svg
if($prefixCondition=='L'){
$prefixSvg = "leadership";
}else if($prefixCondition=='I'){
$prefixSvg = "insight";
}
//배너설정
if($prefixCondition=='L'){//L=리더십(기본값), I=인사이트
$array_banner_img = [
['normal' => '/img/leadership/img_banner_01.png', 'mobile' => '/img/leadership/img_banner_01_m.png', 'title' => '실천으로 완성하는 리더십'],
['normal' => '/img/leadership/img_banner_02.png', 'mobile' => '/img/leadership/img_banner_02_m.png', 'title' => '성장하는 리더십 여정'],
['normal' => '/img/leadership/img_banner_03.png', 'mobile' => '/img/leadership/img_banner_03_m.png', 'title' => '리더로 성장하는 과정'],
];
}else if($prefixCondition=='I'){//L=리더십(기본값), I=인사이트
$array_banner_img = [
['normal' => '/img/insight/img_banner_01.png', 'mobile' => '/img/insight/img_banner_01_m.png', 'title' => '잘되는 일 하세요'],
['normal' => '/img/insight/img_banner_02.png', 'mobile' => '/img/insight/img_banner_02_m.png', 'title' => '다시 일하고 싶은 관계'],
['normal' => '/img/insight/img_banner_03.png', 'mobile' => '/img/insight/img_banner_03_m.png', 'title' => '3일의 실행'],
];
}
$dbSuccess = false;
$array_tab_info = [];
$array_leadership_banner = [];
$array_insight_banner = [];
try {
$pdo = db_conn();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES 'utf8mb4'");
// LIKE 연산자를 안전하게 처리하기 위해 준비된 선언(Prepared Statement) 사용
$stmtCode = $pdo->prepare("
SELECT group_code, base_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND group_code = 'CA200'
AND code LIKE :prefix
ORDER BY base_code ASC
");
// 'L%' 형태로 바인딩하여 검색
$stmtCode->execute([':prefix' => $prefixCondition . '%']);
$rows = $stmtCode->fetchAll(PDO::FETCH_ASSOC);
if ($rows) {
$i = 1;
foreach ($rows as $cr) {
$index = sprintf('%02d', $i);
$index_src = '/img/ico/ico_'.$prefixSvg.'_' . $index . '.svg';
$array_tab_info[] = [
'base_code' => $cr['base_code'],
'code_name' => $cr['code_name'],
'src' => $index_src
];
$i++;
}
}
if ($prefixCondition === 'L') {
$stmtBanner = $pdo->prepare(
"
SELECT
c.content_id,
c.title,
c.description1,
c.description2,
c.description3,
c.content_url,
c.sort_order
FROM edu_contents c
WHERE c.is_active = 1
AND c.issue_type_code = 'LD10010'
ORDER BY
CASE WHEN COALESCE(c.sort_order, 0) = 0 THEN 1 ELSE 0 END,
c.sort_order ASC,
c.content_id DESC
"
);
$stmtBanner->execute();
foreach ($stmtBanner->fetchAll(PDO::FETCH_ASSOC) as $bannerRow) {
$contentUrl = trim((string) ($bannerRow['content_url'] ?? ''));
$array_leadership_banner[] = [
'content_id' => (string) ($bannerRow['content_id'] ?? ''),
'title' => (string) ($bannerRow['title'] ?? ''),
'description1' => trim((string) ($bannerRow['description1'] ?? '')),
'description2' => trim((string) ($bannerRow['description2'] ?? '')),
'description3' => trim((string) ($bannerRow['description3'] ?? '')),
'content_url' => $contentUrl,
'thumbnail' => edu_build_youtube_thumbnail($contentUrl, 'sddefault.jpg'),
'banner_title_pc' => '/img/leadership/banner_leadership.svg',
'banner_title_mo' => '/img/leadership/banner_leadership_m.svg',
];
}
} elseif ($prefixCondition === 'I') {
$stmtBanner = $pdo->prepare(
"
SELECT
c.content_id,
c.title,
c.description1,
c.description2,
c.description3,
c.content_url,
c.category_code,
c.category_group,
c.issue_type_code,
c.is_offer,
c.offer_id,
c.sort_order,
cg.code_name AS sub_category_name,
o.member_id AS offer_member_id,
o.sys_comp_code AS offer_sys_comp_code,
u.name AS offer_member_name,
u.dept_name AS offer_dept_name,
u.rank_name AS offer_rank_name,
u.sys_comp_code AS user_sys_comp_code,
u.belong_comp_id,
u.working_comp_id
FROM edu_contents c
LEFT JOIN edu_codes cg
ON cg.base_code = c.category_group
AND cg.group_code = 'CA200'
AND cg.is_active = 1
LEFT JOIN edu_content_offer o
ON o.offer_id = c.offer_id
LEFT JOIN edu_users u
ON u.member_id = o.member_id
AND (
o.sys_comp_code IS NULL
OR o.sys_comp_code = ''
OR u.sys_comp_code = o.sys_comp_code
)
WHERE c.is_active = 1
AND c.issue_type_code IN ('IS10001', 'IS10002', 'IS10003')
ORDER BY
CASE c.issue_type_code
WHEN 'IS10001' THEN 1
WHEN 'IS10002' THEN 2
WHEN 'IS10003' THEN 3
ELSE 99
END ASC,
CASE WHEN COALESCE(c.sort_order, 0) = 0 THEN 1 ELSE 0 END,
c.sort_order ASC,
c.content_id DESC
"
);
$stmtBanner->execute();
$insightBannerTitleMap = [
'IS10001' => [
'pc' => '/img/insight/banner_issue.svg',
'mo' => '/img/insight/banner_issue_m.svg',
'alt' => 'Hot Issue',
],
'IS10002' => [
'pc' => '/img/insight/banner_pick.svg',
'mo' => '/img/insight/banner_pick_m.svg',
'alt' => "Editor's Pick",
],
'IS10003' => [
'pc' => '/img/insight/banner_choice.svg',
'mo' => '/img/insight/banner_choice_m.svg',
'alt' => "Top Learner's Choice",
],
];
foreach ($stmtBanner->fetchAll(PDO::FETCH_ASSOC) as $bannerRow) {
$issueTypeCode = (string) ($bannerRow['issue_type_code'] ?? '');
$titleMeta = $insightBannerTitleMap[$issueTypeCode] ?? $insightBannerTitleMap['IS10001'];
$contentUrl = edu_normalize_youtube_url((string) ($bannerRow['content_url'] ?? ''));
$offerMemberId = trim((string) ($bannerRow['offer_member_id'] ?? ''));
$offerMemberName = trim((string) ($bannerRow['offer_member_name'] ?? ''));
$offerDeptName = trim((string) ($bannerRow['offer_dept_name'] ?? ''));
$offerRankName = trim((string) ($bannerRow['offer_rank_name'] ?? ''));
$isTopLearner = $issueTypeCode === 'IS10003' && (string) ($bannerRow['is_offer'] ?? '') === '1';
$array_insight_banner[] = [
'content_id' => (string) ($bannerRow['content_id'] ?? ''),
'title' => (string) ($bannerRow['title'] ?? ''),
'description1' => trim((string) ($bannerRow['description1'] ?? '')),
'description2' => trim((string) ($bannerRow['description2'] ?? '')),
'description3' => trim((string) ($bannerRow['description3'] ?? '')),
'content_url' => $contentUrl,
'thumbnail' => edu_build_youtube_thumbnail($contentUrl, 'sddefault.jpg'),
'category_name' => '인사이트',
'sub_category_name' => trim((string) ($bannerRow['sub_category_name'] ?? '')),
'issue_type_code' => $issueTypeCode,
'banner_title_pc' => $titleMeta['pc'],
'banner_title_mo' => $titleMeta['mo'],
'banner_title_alt' => $titleMeta['alt'],
'is_top_learner' => $isTopLearner,
'offer_member_id' => $offerMemberId,
'offer_member_name' => $offerMemberName,
'offer_dept_name' => $offerDeptName,
'offer_rank_name' => $offerRankName,
'offer_profile_image' => $isTopLearner
? edu_build_profile_image_path(
$offerMemberId,
(string) ($bannerRow['user_sys_comp_code'] ?? $bannerRow['offer_sys_comp_code'] ?? ''),
(string) ($bannerRow['belong_comp_id'] ?? ''),
(string) ($bannerRow['working_comp_id'] ?? '')
)
: '/img/ico/ico_user.svg',
];
}
}
$dbSuccess = true;
} catch (Exception $e) {
$errMsg = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
error_log('[leadership_init_data.php] DB Error: ' . $errMsg);
// api_log 함수가 있다면 기록
if (function_exists('api_log')) {
api_log('leadership_init', 'DB_ERROR', ['error' => $errMsg]);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/db_conn.php';
try {
$pdo = db_conn();
echo "DB Connected.\n";
// Check procedure
$stmt = $pdo->query("SHOW PROCEDURE STATUS WHERE Db = 'baronhomep' AND Name = 'sp_insert_edu_access_log'");
$proc = $stmt->fetchAll();
if (empty($proc)) {
echo "Procedure sp_insert_edu_access_log DOES NOT EXIST.\n";
} else {
echo "Procedure sp_insert_edu_access_log exists.\n";
}
// check if u.belong_comp exists
$stmt = $pdo->query("SHOW COLUMNS FROM edu_users LIKE 'belong_comp'");
$col = $stmt->fetchAll();
if (empty($col)) {
echo "Column belong_comp DOES NOT EXIST in edu_users.\n";
} else {
echo "Column belong_comp exists.\n";
}
} catch (Exception $e) {
echo "Basic Error: " . $e->getMessage() . "\n";
}
View File
+62
View File
@@ -0,0 +1,62 @@
<?php
/**
* db_conn.php - MariaDB PDO
*/
// 세션 쿠키 도메인을 서브도메인 공유가 가능하도록 설정
session_set_cookie_params(0, '/', '.baroncs.co.kr');
// 세션이 시작되지 않았다면 시작
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function db_conn(): PDO
{
static $pdo = null;
if ($pdo !== null) { return $pdo; }
$host = 'localhost';
$port = 3306;
$dbname = 'baronhomep';
$user = 'baronhomep';
$pass = 'baron3840!!';
$charset = 'utf8mb4';
$dsn = "mysql:host={$host};dbname={$dbname};charset={$charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_TIMEOUT => 5,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
$pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
} catch (PDOException $e) {
error_log('[DB_CONN ERROR] ' . $e->getMessage());
throw new RuntimeException('DB error: ' . $e->getMessage());
}
return $pdo;
}
// 직접 실행 시 연결 테스트
if (basename(__FILE__) === basename($_SERVER['SCRIPT_FILENAME'] ?? '')) {
header('Content-Type: text/html; charset=utf-8');
echo '<h2>DB 연결 테스트</h2>';
try {
$pdo = db_conn();
$ver = $pdo->query('SELECT VERSION()')->fetchColumn();
echo "<p style='color:green'>연결 성공 - MariaDB {$ver}</p>";
$tables = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN);
echo '<p>테이블 (' . count($tables) . '개):</p><ul>';
foreach ($tables as $t) {
echo '<li>' . htmlspecialchars($t) . '</li>';
}
echo '</ul>';
} catch (Exception $e) {
echo "<p style='color:red'>실패: " . htmlspecialchars($e->getMessage()) . "</p>";
}
}
+156
View File
@@ -0,0 +1,156 @@
<?php
/**
* bbs/debug_log.php
* ─────────────────────────────────────────────────────────────────
* API 로그 뷰어 (개발/운영 디버깅용)
*
* 접근: /bbs/debug_log.php?key=baron_debug_2026
* key 파라미터가 틀리면 403 반환
* ─────────────────────────────────────────────────────────────────
*/
define('DEBUG_KEY', 'baron_debug_2026'); // ← 필요 시 변경
define('LOG_FILE', __DIR__ . '/logs/api.log');
define('LINES_MAX', 300); // 최대 표시 줄 수
// ── 인증 ──────────────────────────────────────────────────────────
if (($_GET['key'] ?? '') !== DEBUG_KEY) {
http_response_code(403);
exit('403 Forbidden');
}
// ── 로그 파일 읽기 ────────────────────────────────────────────────
$action = $_GET['action'] ?? 'view';
// 로그 삭제
if ($action === 'clear') {
@file_put_contents(LOG_FILE, '');
header('Location: debug_log.php?key=' . DEBUG_KEY . '&cleared=1');
exit;
}
// 로그 다운로드
if ($action === 'download') {
if (file_exists(LOG_FILE)) {
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="api_' . date('Ymd_His') . '.log"');
readfile(LOG_FILE);
}
exit;
}
// 로그 내용 조회
$logLines = [];
$fileSize = 0;
$fileExist = file_exists(LOG_FILE);
if ($fileExist) {
$fileSize = filesize(LOG_FILE);
$raw = file(LOG_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
// 최신 줄이 위로 오도록 역순 + 최대 LINES_MAX 줄
$logLines = array_slice(array_reverse($raw), 0, LINES_MAX);
}
// ── 필터 ─────────────────────────────────────────────────────────
$filter = trim($_GET['q'] ?? '');
if ($filter !== '') {
$logLines = array_values(array_filter(
$logLines,
fn($l) => stripos($l, $filter) !== false
));
}
// ── HTML 출력 ─────────────────────────────────────────────────────
$cleared = isset($_GET['cleared']);
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API Log Viewer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Consolas', monospace; background: #0d1117; color: #c9d1d9; padding: 16px; font-size: 13px; }
h1 { color: #58a6ff; margin-bottom: 12px; font-size: 18px; }
.toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.toolbar input { flex: 1; min-width: 200px; padding: 6px 10px; background: #161b22; border: 1px solid #30363d; color: #c9d1d9; border-radius: 4px; }
.toolbar a, .toolbar button { padding: 6px 14px; border-radius: 4px; text-decoration: none; font-size: 12px; cursor: pointer; border: none; }
.btn-refresh { background: #1f6feb; color: #fff; }
.btn-clear { background: #da3633; color: #fff; }
.btn-dl { background: #238636; color: #fff; }
.meta { color: #8b949e; font-size: 11px; margin-bottom: 8px; }
.notice { background: #1c2128; border: 1px solid #30363d; padding: 24px; border-radius: 6px; color: #8b949e; text-align: center; }
.log-table { width: 100%; border-collapse: collapse; }
.log-table tr:nth-child(even) { background: #161b22; }
.log-table td { padding: 4px 8px; vertical-align: top; border-bottom: 1px solid #21262d; word-break: break-all; }
.td-no { color: #8b949e; width: 40px; text-align: right; user-select: none; }
.td-line { white-space: pre-wrap; }
/* 태그별 색상 */
.tag-save_learning { color: #79c0ff; }
.tag-user_keywords { color: #56d364; }
.tag-error { color: #f85149; }
.tag-warn { color: #d29922; }
.cleared { background: #1b2a1b; border: 1px solid #238636; color: #56d364; padding: 8px 12px; border-radius: 4px; margin-bottom: 12px; }
</style>
</head>
<body>
<h1>🔍 API Log Viewer</h1>
<?php if ($cleared): ?>
<div class="cleared">✅ 로그가 초기화됐습니다.</div>
<?php endif; ?>
<div class="toolbar">
<form method="get" style="display:contents">
<input type="hidden" name="key" value="<?= htmlspecialchars(DEBUG_KEY) ?>">
<input type="text" name="q" placeholder="필터 (예: save_learning, unregistered, U001 ...)"
value="<?= htmlspecialchars($filter) ?>">
<button type="submit" class="btn-refresh">🔍 검색</button>
</form>
<a href="?key=<?= DEBUG_KEY ?>" class="btn-refresh">↺ 새로고침</a>
<a href="?key=<?= DEBUG_KEY ?>&action=download" class="btn-dl">⬇ 다운로드</a>
<a href="?key=<?= DEBUG_KEY ?>&action=clear"
onclick="return confirm('로그를 모두 삭제하시겠습니까?')" class="btn-clear">🗑 초기화</a>
</div>
<?php if (!$fileExist || $fileSize === 0): ?>
<div class="notice">
<?= $fileExist ? '로그 파일이 비어 있습니다.' : '아직 로그 파일이 없습니다.<br>API를 한 번 호출하면 자동 생성됩니다.' ?>
</div>
<?php else: ?>
<div class="meta">
파일: <?= htmlspecialchars(LOG_FILE) ?> |
크기: <?= number_format($fileSize) ?> bytes |
표시: <?= count($logLines) ?>줄
<?= $filter !== '' ? ' (필터 적용: <strong>' . htmlspecialchars($filter) . '</strong>)' : '' ?>
</div>
<table class="log-table">
<?php foreach ($logLines as $i => $line):
// 태그 추출 for 색상 ([2026-03-10 12:34:56] [태그] ...)
preg_match('/\[([^\]]+)\]\s*\[([^\]]+)\]/', $line, $m);
$tagClass = '';
if (!empty($m[2])) {
$t = strtolower($m[2]);
$tagClass = 'tag-' . preg_replace('/[^a-z0-9_]/', '_', $t);
}
?>
<tr>
<td class="td-no"><?= count($logLines) - $i ?></td>
<td class="td-line <?= htmlspecialchars($tagClass) ?>"><?= htmlspecialchars($line) ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<script>
// 30초마다 자동 갱신 (필터 없을 때만)
<?php if ($filter === ''): ?>
setTimeout(() => location.reload(), 30000);
<?php endif; ?>
</script>
</body>
</html>
+248
View File
@@ -0,0 +1,248 @@
<?php
/**
* 외부 인트라넷 → 배움터 진입 분기 페이지 (entry_new.php)
*
* 기존 세션 데이터를 초기화한 후 새로운 사용자 세션을 할당합니다.
*/
/**
* 외부 인트라넷 → 배움터 진입 API
*
* 인트라넷에서 POST 방식으로 member_id, sys_comp_code를 전달받아
* edu_users 테이블에서 사용자를 확인하고, intro_flag 값에 따라
* 적절한 페이지로 리다이렉트합니다.
*
* POST 파라미터:
* - member_id (필수) 사용자 ID
* - sys_comp_code (필수) 회사 코드
*
* 사용 예 (인트라넷 HTML form):
* <form method="POST" action="https://baroncs.co.kr/bbs/entry.php">
* <input type="hidden" name="member_id" value="U001" />
* <input type="hidden" name="sys_comp_code" value="COMP01" />
* <button type="submit">배움터</button>
* </form>
*/
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 1. 요청 방식 및 파라미터 검증
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('허용되지 않는 요청 방식입니다.');
}
$memberId = trim((string)($_POST['member_id'] ?? ''));
$sysCompCode = trim((string)($_POST['sys_comp_code'] ?? ''));
// 필수 값이 없으면 로그인 페이지로 이동
if ($memberId === '' || $sysCompCode === '') {
header("Location: /skin/login.php");
exit;
}
require_once __DIR__ . '/db_conn.php';
try {
$pdo = db_conn();
// --- [일일 통합 동기화 체크/실행] ---
// 동기화 실패가 사용자 로그인 자체를 막지 않도록 분리 처리한다.
$lockAcquired = false;
try {
$stmtLock = $pdo->query("SELECT GET_LOCK('edu_users_daily_sync', 5)");
$lockAcquired = ((int)$stmtLock->fetchColumn() === 1);
if ($lockAcquired) {
$stmtCheck = $pdo->query(
"SELECT EXISTS(
SELECT 1
FROM edu_users_daily_logs
WHERE DATE(injection_at) = CURDATE()
)"
);
$isDone = ((int)$stmtCheck->fetchColumn() === 1);
if (!$isDone) {
set_time_limit(0);
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$syncUrl = $scheme . '://' . $host . '/ajax/get_users_info.php';
$chSync = curl_init();
curl_setopt($chSync, CURLOPT_URL, $syncUrl);
curl_setopt($chSync, CURLOPT_RETURNTRANSFER, true);
curl_setopt($chSync, CURLOPT_TIMEOUT, 1800);
curl_setopt($chSync, CURLOPT_HTTPHEADER, ["Accept: text/html"]);
$syncResponse = curl_exec($chSync);
$syncHttpCode = curl_getinfo($chSync, CURLINFO_HTTP_CODE);
$syncError = curl_error($chSync);
curl_close($chSync);
if ($syncResponse === false || $syncHttpCode !== 200) {
throw new RuntimeException('daily sync API failed: http=' . $syncHttpCode . ', err=' . $syncError);
}
$stmtDaily = $pdo->prepare("CALL proc_edu_users_daily(?)");
$stmtDaily->execute([$memberId]);
}
} else {
error_log('[entry.php] daily sync lock not acquired, skip sync this request.');
}
} catch (Throwable $syncEx) {
error_log('[entry.php] daily sync failed: ' . $syncEx->getMessage());
} finally {
if ($lockAcquired) {
try {
$pdo->query("SELECT RELEASE_LOCK('edu_users_daily_sync')");
} catch (Throwable $unlockEx) {
error_log('[entry.php] release lock failed: ' . $unlockEx->getMessage());
}
}
}
// --- [일일 통합 동기화 체크/실행 끝] ---
// 2. 사용자 조회 (member_id, sys_comp_code 기반)
$stmt = $pdo->prepare(
'SELECT member_id, sys_comp_code, name, rank_name, intro_flag
FROM edu_users
WHERE member_id = ? AND sys_comp_code = ?
LIMIT 1'
);
$stmt = $pdo->prepare(
"
SELECT
u.member_id,
u.sys_comp_code,
u.name,
u.rank_name,
u.auth_level,
u.intro_flag,
ec.code,
ec.code_name as comp_name
FROM edu_users u
LEFT JOIN
(
SELECT code,code_name FROM edu_codes WHERE group_code = 'CO100'
) ec
ON ec.code = u.belong_comp
WHERE u.member_id = ? AND u.sys_comp_code = ?
LIMIT 1
"
);
$stmt->execute([$memberId, $sysCompCode]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 사용자가 없으면 로그인 페이지로 이동
if (!$user) {
header("Location: http://erp.baroncs.co.kr/mobile/sys/controller/Study_controller.php");
exit;
}
// --- [세션 초기화 단계] ---
// 새로운 사용자의 정보를 담기 전에 기존 세션 변수를 모두 제거합니다.
session_unset();
// 보안을 위해 세션 ID를 새로 발급합니다. (기존 세션 데이터 삭제 포함)
session_regenerate_id(true);
// -------------------------
// 3. 변수 정리 및 세션 저장 (요청하신 규칙 엄격 적용)
$U_member_id = $user['member_id'];
$U_sys_comp_code = $user['sys_comp_code'];
$U_name = $user['name'];
$U_rank_name = $user['rank_name'];
$U_auth_level = (string)($user['auth_level'] ?? '');
$U_comp_name = $user['comp_name'];
$U_intro_flag = (string)$user['intro_flag']; // '0' 또는 '1'
$_SESSION['member_id'] = $U_member_id;
$_SESSION['comp_name'] = $U_comp_name;
$_SESSION['sys_comp_code'] = $U_sys_comp_code;
$_SESSION['member_name'] = $U_name;
$_SESSION['name'] = $U_name;
$_SESSION['rank_name'] = $U_rank_name;
$_SESSION['auth_level'] = $U_auth_level;
$_SESSION['intro_flag'] = $U_intro_flag;
// 2026-04-02 권오재 추가.--- [추가: 접속 이력 프로시저 호출] ---
// 1. 정보 수집
$ip_address = $_SERVER['REMOTE_ADDR'];
$user_agent = $_SERVER['HTTP_USER_AGENT'];
// OS 및 디바이스 판별 (간단한 예시)
$os_name = 'Unknown OS';
if (preg_match('/windows|win32/i', $user_agent))
$os_name = 'Windows';
else if (preg_match('/macintosh|mac os x/i', $user_agent))
$os_name = 'Mac OS';
else if (preg_match('/android/i', $user_agent))
$os_name = 'Android';
else if (preg_match('/iphone/i', $user_agent))
$os_name = 'iOS';
$device_type = (preg_match('/mobile|android|iphone|ipad/i', $user_agent)) ? 'Mobile' : 'PC';
// 브라우저명 추출 (Whale을 Chrome보다 우선 판별)
$browser_name = 'Unknown Browser';
if (stripos($user_agent, 'Whale') !== false)
$browser_name = 'Whale';
else if (stripos($user_agent, 'Edg') !== false || stripos($user_agent, 'Edge') !== false)
$browser_name = 'Edge';
else if (stripos($user_agent, 'MSIE') !== false || stripos($user_agent, 'Trident') !== false)
$browser_name = 'IE';
else if (stripos($user_agent, 'Chrome') !== false)
$browser_name = 'Chrome';
else if (stripos($user_agent, 'Firefox') !== false)
$browser_name = 'Firefox';
else if (stripos($user_agent, 'Safari') !== false)
$browser_name = 'Safari';
// 2. 프로시저 실행
$stmtLog = $pdo->prepare("CALL proc_insert_edu_access_log(?, ?, ?, ?, ?, ?)");
$stmtLog->execute([
$_SESSION['member_id'],
$_SESSION['sys_comp_code'],
$ip_address,
$os_name,
$device_type,
$browser_name
]);
// 4. 리다이렉트 분기 및 DB 업데이트
if ($U_intro_flag === '1') {
// 읽음 완료 상태: 메인으로 이동
header("Location: /skin/index.php");
exit;
} else {
// 읽지 않음(0) 상태: DB 업데이트 후 인트로로 이동
$stmtUpdate = $pdo->prepare(
'UPDATE edu_users
SET intro_flag = "1"
WHERE member_id = ?
AND sys_comp_code = ?
AND intro_flag = "0"'
);
$stmtUpdate->execute([$U_member_id, $U_sys_comp_code]);
// 업데이트 성공 후 세션 동기화 (인트로 이동 직전)
$_SESSION['intro_flag'] = '1';
header("Location: /skin/intro.php");
exit;
}
} catch (Exception $e) {
error_log("Entry Error: " . $e->getMessage());
exit('시스템 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
}
+139
View File
@@ -0,0 +1,139 @@
<?php
/**
* 외부 인트라넷 → 배움터 진입 API
*
* 인트라넷에서 POST 방식으로 member_id, sys_comp_code를 전달받아
* edu_users 테이블에서 사용자를 확인하고, intro_flag 값에 따라
* 적절한 페이지로 리다이렉트합니다.
*
* POST 파라미터:
* - member_id (필수) 사용자 ID
* - sys_comp_code (필수) 회사 코드
*
* 사용 예 (인트라넷 HTML form):
* <form method="POST" action="https://baroncs.co.kr/edu/bbs/entry.php">
* <input type="hidden" name="member_id" value="U001" />
* <input type="hidden" name="sys_comp_code" value="COMP01" />
* <button type="submit">배움터</button>
* </form>
*/
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('허용되지 않는 요청 방식입니다.');
}
$memberId = trim((string)($_POST['member_id'] ?? ''));
$sysCompCode = trim((string)($_POST['sys_comp_code'] ?? ''));
// 테스트 토글: 아래 true 라인을 주석 해제하면 intro_flag와 무관하게 index.php로 진입
// $forceIndexRedirectForTest = false;
$forceIndexRedirectForTest = true;
// 테스트 토글: intro_flag 자동 업데이트(0->1) 실행 여부
$enableIntroFlagAutoUpdate = true;
// $enableIntroFlagAutoUpdate = false;
if ($memberId === '' || $sysCompCode === '') {
http_response_code(400);
exit('필수 파라미터가 누락되었습니다.');
}
require_once __DIR__ . '/db_conn.php';
try {
$pdo = db_conn();
// [1] 사용자 조회
$stmt = $pdo->prepare(
'SELECT name, rank_name, intro_flag
FROM edu_users
WHERE member_id = ? AND sys_comp_code = ?
LIMIT 1'
);
$stmt->execute([$memberId, $sysCompCode]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// [2] 신규 사용자 자동 등록
// if (!$user) {
// // 인트라넷에서 이름을 안 보내줄 경우를 대비한 기본값
// $newName = trim((string)($_POST['name'] ?? '신규사용자'));
// $newRank = trim((string)($_POST['rank_name'] ?? '사원'));
// $ins = $pdo->prepare(
// 'INSERT INTO edu_users (member_id, sys_comp_code, name, rank_name, intro_flag, created_at, is_active)
// VALUES (?, ?, ?, ?, "0", NOW(), "Y")'
// );
// $ins->execute([$memberId, $sysCompCode, $newName, $newRank]);
// $user = ['name' => $newName, 'rank_name' => $newRank, 'intro_flag' => '0'];
// }
// [3] 세션 저장 (통합 및 정리)
// 여러 시스템 호환을 위해 필요한 키값을 모두 채워주되, 이름은 'member_name'으로 통일하는 것이 좋습니다.
$_SESSION['member_id'] = $memberId;
$_SESSION['user_id'] = $memberId;
$_SESSION['ss_mb_id'] = $memberId;
$_SESSION['sys_comp_code'] = $sysCompCode;
$_SESSION['company'] = $sysCompCode;
$_SESSION['member_name'] = $user['name']; // 인트로에서 쓸 변수
$_SESSION['name'] = $user['name']; // 일반 페이지용
$_SESSION['rank_name'] = $user['rank_name']; // 직책
// 세션 보안 강화
session_regenerate_id(true);
// [4] 리다이렉트
$introFlag = (string)($user['intro_flag'] ?? '0');
$redirectUrl = ($introFlag === '1') ? '/edu/skin/index.php' : '/edu/skin/intro.php';
if ($forceIndexRedirectForTest) {
$redirectUrl = '/edu/skin/index.php';
}
// index.php 진입 시 intro_flag가 0이면 자동으로 1로 승격 (토글 가능)
if ($enableIntroFlagAutoUpdate && $redirectUrl === '/edu/skin/index.php' && $introFlag === '0') {
$stmtUpdateIntroFlag = $pdo->prepare(
'UPDATE edu_users
SET intro_flag = ?
WHERE member_id = ? AND sys_comp_code = ? AND intro_flag = 0'
);
$stmtUpdateIntroFlag->execute(['1', $memberId, $sysCompCode]);
$introFlag = '1';
}
header("Location: $redirectUrl");
exit;
} catch (Exception $e) {
exit('오류: ' . $e->getMessage());
}
// function goToEdu(memberId, sysCompCode) {
// var form = document.createElement('form');
// form.method = 'POST';
// form.action = 'https://baroncs.co.kr/edu/bbs/entry.php';
// var inputId = document.createElement('input');
// inputId.type = 'hidden';
// inputId.name = 'member_id';
// inputId.value = memberId;
// var inputComp = document.createElement('input');
// inputComp.type = 'hidden';
// inputComp.name = 'sys_comp_code';
// inputComp.value = sysCompCode;
// form.appendChild(inputId);
// form.appendChild(inputComp);
// document.body.appendChild(form);
// form.submit();
// }
// <a href="#" onclick="goToEdu('U001', 'COMP01'); return false;">배움터</a>
+100
View File
@@ -0,0 +1,100 @@
<?php
/**
* /bbs/entry_moon.php
* 그누보드 기반 시스템 분기 및 세션 처리 페이지
*/
// 1. 그누보드 환경 설정 로드 (경로에 맞춰 수정 필요)
// 보통 그누보드 루트의 common.php를 불러오면 세션 시작 및 DB 연결이 포함됩니다.
include_once('./_common.php');
/**
* 만약 common.php를 쓰지 않고 별도 DB 연결을 사용한다면
* 아래와 같이 세션과 DB 연결을 직접 처리합니다.
*/
/*
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/db_conn.php';
$pdo = db_conn();
*/
// 2. POST 파라미터 체크
$member_id = isset($_POST['member_id']) ? trim((string)$_POST['member_id']) : '';
$sys_comp_code = isset($_POST['sys_comp_code']) ? trim((string)$_POST['sys_comp_code']) : '';
// 값이 없거나 널이면 로그인 페이지로 이동
if (!$member_id || !$sys_comp_code) {
header("Location: /skin/login.php");
exit;
}
try {
// 3. 사용자 조회 (Prepared Statement)
$sql = "SELECT member_id, sys_comp_code, name, rank_name, intro_flag
FROM edu_users
WHERE member_id = ? AND sys_comp_code = ?
LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute([$member_id, $sys_comp_code]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 결과값이 없으면 로그인 페이지로 이동
if (!$user) {
header("Location: /skin/login.php");
exit;
}
// 4. 변수 정리 및 세션 할당
$U_member_id = $user['member_id'];
$U_sys_comp_code = $user['sys_comp_code'];
$U_name = $user['name'];
$U_rank_name = $user['rank_name'];
$U_intro_flag = (int)$user['intro_flag'];
// 요청하신 세션 할당 규칙 적용
$_SESSION['user_id'] = $U_member_id;
$_SESSION['ss_mb_id'] = $U_member_id; // 그누보드 표준 세션 키
$_SESSION['sys_comp_code'] = $U_sys_comp_code; // 기획안에 따른 flag 할당
$_SESSION['company'] = $U_sys_comp_code; // 기획안에 따른 flag 할당
$_SESSION['member_name'] = $U_name;
$_SESSION['name'] = $U_name;
$_SESSION['rank_name'] = $U_rank_name;
$_SESSION['intro_flag'] = $U_intro_flag;
// 세션 보안을 위한 ID 재생성
session_regenerate_id(true);
// 5. 페이지 이동 분기
if ($U_intro_flag === 1) {
// 이미 읽음 상태면 메인으로
header("Location: /skin/index.php");
exit;
} else {
// 읽지 않음(0) 상태면 DB 업데이트 후 인트로로
$update_sql = "UPDATE edu_users
SET intro_flag = 1
WHERE member_id = ?
AND sys_comp_code = ?
AND intro_flag = 0";
$up_stmt = $pdo->prepare($update_sql);
$up_stmt->execute([$U_member_id, $U_sys_comp_code]);
// 업데이트 성공 여부와 상관없이(혹은 성공 직후) 세션 동기화 후 이동
$_SESSION['intro_flag'] = 1;
$_SESSION['sys_comp_code'] = 1;
$_SESSION['company'] = 1;
header("Location: /skin/intro.php");
exit;
}
} catch (PDOException $e) {
// 운영 환경에서는 에러 로그만 남기고 로그인 페이지로 보내는 것이 안전합니다.
error_log($e->getMessage());
header("Location: /skin/login.php");
exit;
}
+125
View File
@@ -0,0 +1,125 @@
<?php
/**
* 외부 인트라넷 → 배움터 진입 분기 페이지 (entry_new.php)
*
* 기존 세션 데이터를 초기화한 후 새로운 사용자 세션을 할당합니다.
*/
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 1. 요청 방식 및 파라미터 검증
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('허용되지 않는 요청 방식입니다.');
}
$memberId = trim((string)($_POST['member_id'] ?? ''));
$sysCompCode = trim((string)($_POST['sys_comp_code'] ?? ''));
// 필수 값이 없으면 로그인 페이지로 이동
if ($memberId === '' || $sysCompCode === '') {
header("Location: /skin/login.php");
exit;
}
require_once __DIR__ . '/db_conn.php';
try {
$pdo = db_conn();
// 2. 사용자 조회 (member_id, sys_comp_code 기반)
$stmt = $pdo->prepare(
'SELECT member_id, sys_comp_code, name, rank_name, intro_flag
FROM edu_users
WHERE member_id = ? AND sys_comp_code = ?
LIMIT 1'
);
$stmt = $pdo->prepare(
"
SELECT
u.member_id,
u.sys_comp_code,
u.name,
u.rank_name,
u.intro_flag,
ec.code,
ec.code_name as comp_name
FROM edu_users u
LEFT JOIN
(
SELECT code,code_name FROM edu_codes WHERE group_code = 'CO100'
) ec
ON ec.code = u.belong_comp
WHERE u.member_id = ? AND u.sys_comp_code = ?
LIMIT 1
"
);
$stmt->execute([$memberId, $sysCompCode]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 사용자가 없으면 로그인 페이지로 이동
if (!$user) {
header("Location: /skin/login.php");
exit;
}
// --- [세션 초기화 단계] ---
// 새로운 사용자의 정보를 담기 전에 기존 세션 변수를 모두 제거합니다.
session_unset();
// 보안을 위해 세션 ID를 새로 발급합니다. (기존 세션 데이터 삭제 포함)
session_regenerate_id(true);
// -------------------------
// 3. 변수 정리 및 세션 저장 (요청하신 규칙 엄격 적용)
$U_member_id = $user['member_id'];
$U_sys_comp_code = $user['sys_comp_code'];
$U_name = $user['name'];
$U_rank_name = $user['rank_name'];
$U_comp_name = $user['comp_name'];
$U_intro_flag = (string)$user['intro_flag']; // '0' 또는 '1'
$_SESSION['user_id'] = $U_member_id;
$_SESSION['ss_mb_id'] = $U_member_id;
$_SESSION['comp_name'] = $U_comp_name;
$_SESSION['sys_comp_code'] = $U_sys_comp_code;
$_SESSION['company'] = $U_sys_comp_code;
$_SESSION['member_name'] = $U_name;
$_SESSION['name'] = $U_name;
$_SESSION['rank_name'] = $U_rank_name;
$_SESSION['intro_flag'] = $U_intro_flag;
// 4. 리다이렉트 분기 및 DB 업데이트
if ($U_intro_flag === '1') {
// 읽음 완료 상태: 메인으로 이동
header("Location: /skin/index.php");
exit;
} else {
// 읽지 않음(0) 상태: DB 업데이트 후 인트로로 이동
$stmtUpdate = $pdo->prepare(
'UPDATE edu_users
SET intro_flag = "1"
WHERE member_id = ?
AND sys_comp_code = ?
AND intro_flag = "0"'
);
$stmtUpdate->execute([$U_member_id, $U_sys_comp_code]);
// 업데이트 성공 후 세션 동기화 (인트로 이동 직전)
$_SESSION['intro_flag'] = '1';
header("Location: /skin/intro.php");
exit;
}
} catch (Exception $e) {
error_log("Entry Error: " . $e->getMessage());
exit('시스템 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
}
+9
View File
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
/*
* Index page backend entrypoint.
* Keep view/backend filename aligned: skin/index.php <-> bbs/index.php
*/
require_once __DIR__ . '/main_data.php';
+167
View File
@@ -0,0 +1,167 @@
<?php
//=========================
// 헤더 진도율 초기데이터
// - 마이클래스 / 온보딩 / 법정교육
// - 현재년도 기준
// - 마이페이지 "나의 학습 활동"과 동일한 시간기준 진도율 사용
//=========================
require_once __DIR__ . '/../bbs/db_conn.php';
// 세션 시작
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
// $memberId = 'U001';
// $sysCompCode = 'COMP01';
/*
// ---------------------------------------------------------
// 헤더에서 바로 사용할 기본 변수
// ---------------------------------------------------------
$header_myclass_percent = 0;
$header_onboarding_percent = 0;
$header_legal_percent = 0;
*/
// ---------------------------------------------------------
// 세션값이 없으면 기본값 0으로 종료
// ---------------------------------------------------------
if ($memberId === '' || $sysCompCode === '') {
return;
}
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 현재년도
$currentYear = (int)date('Y');
// 현재 분기코드 (CA200Q01 ~ CA200Q04)
$currentQuarterNum = (int)ceil((int)date('m') / 3);
$currentQuarterCode = sprintf('CA200Q%02d', $currentQuarterNum);
// ---------------------------------------------------------
// 1. 마이클래스(CA10001) 진도율
// - 마이페이지와 동일 기준:
// percent = SUM(watch_tm) / SUM(content_tm) * 100
// - 현재년도 기준: YEAR(last_viewed_at) = 현재년도
// ---------------------------------------------------------
$stmtMyclass = $pdo->prepare("
SELECT fn_get_progress_rate(:sys_comp_code,:current_year,:member_id,'CA10001',:quarter_code) AS percent ;
");
// $stmtMyclass = $pdo->prepare("
// SELECT
// CASE
// WHEN COALESCE(SUM(lh.content_tm), 0) > 0
// THEN ROUND(COALESCE(SUM(lh.watch_tm), 0) / SUM(lh.content_tm) * 100)
// ELSE 0
// END AS percent
// 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 c.category_code = 'CA10001'
// AND YEAR(lh.last_viewed_at) = :current_year
// ");
$stmtMyclass->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':current_year' => $currentYear,
':quarter_code' => $currentQuarterCode,
]);
$header_myclass_percent = (int)$stmtMyclass->fetchColumn();
// ---------------------------------------------------------
// 2. 온보딩(CA10002) 진도율
// - 마이페이지와 동일 기준:
// percent = SUM(watch_tm) / SUM(content_tm) * 100
// - 현재년도 기준: YEAR(last_viewed_at) = 현재년도
// ---------------------------------------------------------
$stmtOnboarding = $pdo->prepare("
SELECT fn_get_progress_rate(:sys_comp_code,:current_year,:member_id,'CA10002','') AS percent ;
");
// $stmtOnboarding = $pdo->prepare("
// SELECT
// CASE
// WHEN COALESCE(SUM(lh.content_tm), 0) > 0
// THEN ROUND(COALESCE(SUM(lh.watch_tm), 0) / SUM(lh.content_tm) * 100)
// ELSE 0
// END AS percent
// 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 c.category_code = 'CA10002'
// AND YEAR(lh.last_viewed_at) = :current_year
// ");
$stmtOnboarding->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':current_year' => $currentYear,
]);
$header_onboarding_percent = (int)$stmtOnboarding->fetchColumn();
// ---------------------------------------------------------
// 3. 법정교육(CA10003) 진도율
// - 마이페이지와 동일 기준:
// percent = SUM(watch_tm) / SUM(content_tm) * 100
// - 현재년도 기준: YEAR(last_viewed_at) = 현재년도
// ---------------------------------------------------------
$stmtLegal = $pdo->prepare("
SELECT fn_get_progress_rate(:sys_comp_code,:current_year,:member_id,'CA10003','') AS percent ;
");
// $stmtLegal = $pdo->prepare("
// SELECT
// CASE
// WHEN COALESCE(SUM(lh.content_tm), 0) > 0
// THEN ROUND(COALESCE(SUM(lh.watch_tm), 0) / SUM(lh.content_tm) * 100)
// ELSE 0
// END AS percent
// 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 c.category_code = 'CA10003'
// AND YEAR(lh.last_viewed_at) = :current_year
// ");
$stmtLegal->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':current_year' => $currentYear,
]);
$header_legal_percent = (int)$stmtLegal->fetchColumn();
// ---------------------------------------------------------
// 예외 방어: 0~100 범위 보정
// ---------------------------------------------------------
$header_myclass_percent = max(0, min(100, $header_myclass_percent));
$header_onboarding_percent = max(0, min(100, $header_onboarding_percent));
$header_legal_percent = max(0, min(100, $header_legal_percent));
if($memberId=="M21420" ){
//echo "test=".$memberId.":".$header_myclass_percent;
//echo "test=".$memberId.":".$header_onboarding_percent;
//echo "test=".$memberId.":".$header_legal_percent;
//exit;
}
} catch (Throwable $e) {
// 헤더는 페이지 공통영역이므로, 오류가 나더라도 전체 화면이 죽지 않게
// 기본값 0 유지
$header_myclass_percent = 0;
$header_onboarding_percent = 0;
$header_legal_percent = 0;
//echo "test 11=".$memberId.":".$header_myclass_percent;
// 필요시 개발단계에서만 로그 확인
// error_log('[init_data_for_header.php] ' . $e->getMessage());
}
+78
View File
@@ -0,0 +1,78 @@
<?php
require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/api_log.php';
// 외부(호출하는 파일)에서 $SET_PREFIX를 정의하지 않았을 경우를 대비한 기본값 설정
$prefixCondition = isset($SET_PREFIX) ? $SET_PREFIX : 'L'; //L=리더십(기본값), I=인사이트
// $memberId = 'U001';
// $sysCompCode = 'COMP01';
//Svg
if($prefixCondition=='L'){
$prefixSvg = "leadership";
}else if($prefixCondition=='I'){
$prefixSvg = "insight";
}
//배너설정
if($prefixCondition=='L'){//L=리더십(기본값), I=인사이트
$array_banner_img = [
['normal' => '/img/leadership/img_banner_01.png', 'mobile' => '/img/leadership/img_banner_01_m.png', 'title' => '실천으로 완성하는 리더십'],
['normal' => '/img/leadership/img_banner_02.png', 'mobile' => '/img/leadership/img_banner_02_m.png', 'title' => '성장하는 리더십 여정'],
['normal' => '/img/leadership/img_banner_03.png', 'mobile' => '/img/leadership/img_banner_03_m.png', 'title' => '리더로 성장하는 과정'],
];
}else if($prefixCondition=='I'){//L=리더십(기본값), I=인사이트
$array_banner_img = [
['normal' => '/img/insight/img_banner_01.png', 'mobile' => '/img/insight/img_banner_01_m.png', 'title' => '배너 1'],
['normal' => '/img/insight/img_banner_02.png', 'mobile' => '/img/insight/img_banner_02_m.png', 'title' => '배너 2'],
];
}
$dbSuccess = false;
$array_tab_info = [];
try {
$pdo = db_conn();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES 'utf8mb4'");
// LIKE 연산자를 안전하게 처리하기 위해 준비된 선언(Prepared Statement) 사용
$stmtCode = $pdo->prepare("
SELECT group_code, base_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND group_code = 'CA200'
AND code LIKE :prefix
ORDER BY base_code ASC
");
// 'L%' 형태로 바인딩하여 검색
$stmtCode->execute([':prefix' => $prefixCondition . '%']);
$rows = $stmtCode->fetchAll(PDO::FETCH_ASSOC);
if ($rows) {
$i = 1;
foreach ($rows as $cr) {
$index = sprintf('%02d', $i);
$index_src = '/img/ico/ico_'.$prefixSvg.'_' . $index . '.svg';
$array_tab_info[] = [
'base_code' => $cr['base_code'],
'code_name' => $cr['code_name'],
'src' => $index_src
];
$i++;
}
}
$dbSuccess = true;
} catch (Exception $e) {
$errMsg = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
error_log('[leadership_init_data.php] DB Error: ' . $errMsg);
// api_log 함수가 있다면 기록
if (function_exists('api_log')) {
api_log('leadership_init', 'DB_ERROR', ['error' => $errMsg]);
}
}
+242
View File
@@ -0,0 +1,242 @@
<?php
/**
* bbs/legal_learning_data.php — 법정교육 페이지 데이터
*
* 제공 변수:
* $userName - 사용자 이름
* $userRank - 직위/직급
* $dDay - D-day 숫자
* $deadlineStr - 마감일 문자열 (n월 j일)
* $chapters - 챕터 배열 (id, code, name, lessons[])
* $chaptersJson - $chapters JSON 문자열 (window.learningChapterData)
* $configJson - window.learningConfigData 형식 JSON
* $progressRate - 전체 진도율 (0~100 정수)
*/
require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/api/common.php';
$pdo = db_conn();
// ── 1. 로그인 유저 ID (로그인 세션 변수 사용) ────────────────────────────
$sessionUser = api_get_session_user();
if (!$sessionUser || empty($sessionUser['member_id'])) {
// 로그인 세션이 없으면 접근 불가 또는 리다이렉트 처리 (여기선 예시로 403)
http_response_code(403);
exit('로그인 세션이 필요합니다.');
}
$memberId = $sessionUser['member_id'];
$sysCompCode = $sessionUser['sys_comp_code'];
// ── 2. 유저 정보 ─────────────────────────────────────────────────────────
$stmtUser = $pdo->prepare("
SELECT name, rank_name, sys_comp_code
FROM edu_users
WHERE member_id = ?
AND sys_comp_code = ?
LIMIT 1
");
$stmtUser->execute([$memberId, $sysCompCode]);
$userRow = $stmtUser->fetch();
$userName = $userRow['name'] ?? '사용자';
$userRank = $userRow['rank_name'] ?? '';
// ── 3. 법정교육 카테고리/챕터 고정 매핑 (관리자 코드관리 기준) ───────────────
$fixedLegalNameMap = [
'C01' => '개인정보보호',
'C02' => '직장내 괴롭힘 예방',
'C03' => '장애인 인식 개선',
'C04' => '성희롱 예방 교육',
'C05' => '퇴직금 교육',
// 'C06' => '산업안전보건',
];
$normalizeLegalGroupCode = static function ($value) {
$code = strtoupper(trim((string)$value));
if (preg_match('/^CA200(C\d{2})$/', $code, $m)) {
return $m[1];
}
if (preg_match('/^C\d{2}$/', $code)) {
return $code;
}
return '';
};
$legalCodes = array_keys($fixedLegalNameMap);
$legalRawCodes = [];
foreach ($legalCodes as $code) {
$legalRawCodes[] = $code;
$legalRawCodes[] = 'CA200' . $code;
}
$legalRawCodes = array_values(array_unique($legalRawCodes));
// 카테고리명 조회 (edu_codes 테이블)
$phCodes = implode(',', array_fill(0, count($legalRawCodes), '?'));
$stmtCodes = $pdo->prepare("
SELECT base_code, code_name
FROM edu_codes
WHERE base_code IN ($phCodes)
ORDER BY base_code
");
$stmtCodes->execute($legalRawCodes);
$codeNameMap = [];
foreach ($stmtCodes->fetchAll() as $row) {
$codeNameMap[$row['base_code']] = $row['code_name'];
}
// ── 4. 콘텐츠 + 시청 이력 조회 ───────────────────────────────────────────
$stmtContents = $pdo->prepare("
SELECT
c.content_id,
c.title,
c.content_url,
c.description,
c.category_group,
c.end_date,
c.sort_order,
COALESCE(lh.content_tm, 0) AS content_tm,
COALESCE(lh.watch_tm, 0) AS watch_tm,
COALESCE(lh.all_tm, 0) AS all_tm,
lh.completed_at
FROM edu_contents c
LEFT JOIN edu_learning_histories lh
ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
WHERE c.is_active = 1
AND c.category_code = 'CA10003'
AND c.category_group IN ($phCodes)
ORDER BY c.category_group, c.sort_order ASC, c.content_id ASC
");
$stmtContents->execute(array_merge([$memberId, $sysCompCode], $legalRawCodes));
$rows = $stmtContents->fetchAll();
// ── 5. 디버그 정보 준비 ──────────────────────────────────────────────
$debugData = [
'WHERE_category_code' => 'CA10003',
'WHERE_category_group_IN' => $legalRawCodes,
'rows_count' => count($rows),
'chapter_configs' => [],
];
foreach ($legalCodes as $code) {
$debugData['chapter_configs'][$code] = [
'normalize_from' => [$code, 'CA200' . $code],
];
}
// ── 6. 챕터별 그룹화 ─────────────────────────────────────────────────────
$chapterMap = [];
foreach ($legalCodes as $idx => $code) {
$chapterMap[$code] = [
'id' => $idx + 1,
'code' => $code,
'name' => $fixedLegalNameMap[$code] ?? ($codeNameMap[$code] ?? $code),
'lessons' => [],
'completed' => true, // 아래에서 미완료 레슨 있으면 false
'min_end_date' => null,
];
}
foreach ($rows as $row) {
$code = $normalizeLegalGroupCode($row['category_group'] ?? '');
if ($code === '' || !isset($chapterMap[$code])) { continue; }
$contentTm = (int)$row['content_tm'];
$watchTm = (int)$row['watch_tm'];
$allTm = (int)$row['all_tm'];
$completedAt = $row['completed_at'];
$hasHistory = ($contentTm > 0 || $watchTm > 0 || !empty($completedAt));
$completed = (!empty($completedAt));
$status = !$hasHistory ? 'not-started' : ($completed ? 'completed' : 'in-progress');
$lessonEndDate = !empty($row['end_date']) ? substr($row['end_date'], 0, 10) : null;
$chapterMap[$code]['lessons'][] = [
'content_id' => $row['content_id'],
'title' => $row['title'],
'url' => $row['content_url'],
'description' => $row['description'] ?? '',
'content_tm' => $contentTm,
'watch_tm' => $watchTm,
'all_tm' => $allTm,
'completed' => $completed,
'status' => $status,
'end_date' => $lessonEndDate,
'sort_order' => (int)($row['sort_order'] ?? 0),
];
if ($lessonEndDate !== null) {
if ($chapterMap[$code]['min_end_date'] === null || $lessonEndDate < $chapterMap[$code]['min_end_date']) {
$chapterMap[$code]['min_end_date'] = $lessonEndDate;
}
}
if (!$completed) {
$chapterMap[$code]['completed'] = false;
}
}
// 영상이 없는 챕터는 미완료 처리
foreach ($chapterMap as &$ch) {
if (empty($ch['lessons'])) {
$ch['completed'] = false;
}
}
unset($ch);
$chapters = array_values($chapterMap);
// lessons를 sort_order 기준으로 재정렬 (동률 시 content_id ASC)
foreach ($chapters as &$ch) {
usort($ch['lessons'], static function ($a, $b) {
$diff = ($a['sort_order'] ?? 0) <=> ($b['sort_order'] ?? 0);
return $diff !== 0 ? $diff : ($a['content_id'] <=> $b['content_id']);
});
}
unset($ch);
// ── 6. 전체 진도율 계산 ──────────────────────────────────────────────────
$totalLessons = 0;
$completedLessons = 0;
foreach ($chapters as $ch) {
foreach ($ch['lessons'] as $lesson) {
$totalLessons++;
if ($lesson['completed']) { $completedLessons++; }
}
}
$progressRate = $totalLessons > 0
? (int)round($completedLessons / $totalLessons * 100)
: 0;
// ── 7. D-day 계산 (edu_contents.end_date 최솟값 기준, 없으면 당해 12월 31일) ──
$minEndDate = null;
foreach ($chapters as $ch) {
if (!empty($ch['min_end_date'])) {
if ($minEndDate === null || $ch['min_end_date'] < $minEndDate) {
$minEndDate = $ch['min_end_date'];
}
}
}
$deadline = $minEndDate
? new DateTime($minEndDate)
: new DateTime(date('Y') . '-12-31');
$today = new DateTime(date('Y-m-d'));
$diff = $today->diff($deadline);
$dDay = ($today <= $deadline) ? (int)$diff->days : 0;
$deadlineStr = $deadline->format('n월 j일');
// ── 8. JS 주입용 JSON 생성 ────────────────────────────────────────────────
$chaptersJson = json_encode($chapters, JSON_UNESCAPED_UNICODE);
// window.learningConfigData 형식: { chapterId: { completed, lessons:[{completed}] } }
$configData = [];
foreach ($chapters as $ch) {
$chapterConfig = [
'completed' => $ch['completed'],
'lessons' => array_map(
fn($l) => ['completed' => $l['completed']],
$ch['lessons']
),
];
// 프론트 코드 호환을 위해 code/id 키 모두 제공
$configData[$ch['code']] = $chapterConfig;
$configData[(string)$ch['id']] = $chapterConfig;
}
$configJson = json_encode($configData, JSON_UNESCAPED_UNICODE);
+134
View File
@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/auth.php';
edu_start_session();
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
header('Location: /skin/login.php');
exit;
}
$redirect = trim((string) ($_POST['redirect'] ?? '/skin/index.php'));
if ($redirect === '' || strpos($redirect, '/') !== 0) {
$redirect = '/skin/index.php';
}
$memberIdInput = trim((string) ($_POST['member_id'] ?? ''));
$passwordInput = trim((string) ($_POST['intra_pw'] ?? ''));
$sysCompCode = trim((string) ($_POST['sys_comp_code'] ?? ''));
//개발편의 : 임의셋팅 : 오픈후 삭제
// if (strtoupper($memberIdInput) === '12') {
// $memberIdInput = "U001";
// $passwordInput = "baron3840";
// }
if ($memberIdInput === '' || $passwordInput === '' || $sysCompCode === '') {
$back = '/skin/login.php?error=' . rawurlencode('회사코드, ID, 비밀번호를 모두 입력해 주세요.')
. '&redirect=' . rawurlencode($redirect)
. '&member_id=' . rawurlencode($memberIdInput)
. '&sys_comp_code=' . rawurlencode($sysCompCode);
header('Location: ' . $back);
exit;
}
try {
$pdo = db_conn();
$stmt = $pdo->prepare(
"SELECT *
FROM edu_users
WHERE member_id = ?
AND sys_comp_code = ?
LIMIT 1"
);
$stmt->execute([$memberIdInput, $sysCompCode]);
$user = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
$storedPw = trim((string) ($user['intra_pw'] ?? ''));
$verified = $user && ($storedPw !== '') && hash_equals($storedPw, $passwordInput);
if (!$verified) {
$back = '/skin/login.php?error=' . rawurlencode('ID 또는 비밀번호가 올바르지 않습니다.')
. '&redirect=' . rawurlencode($redirect)
. '&member_id=' . rawurlencode($memberIdInput)
. '&sys_comp_code=' . rawurlencode($sysCompCode);
header('Location: ' . $back);
exit;
}
// 세션 초기화 후 세션 지정
$_SESSION = []; // 기존 세션 데이터 전체 초기화
session_regenerate_id(true); // 새 세션 ID 발급 (세션 고정 공격 방지)
$_SESSION['member_id'] = (string) $user['member_id'];
$_SESSION['sys_comp_code'] = (string) ($user['sys_comp_code'] ?? '');
$_SESSION['belong_comp'] = (string) ($user['belong_comp'] ?? '');
$_SESSION['working_comp'] = (string) ($user['working_comp'] ?? '');
$_SESSION['member_name'] = (string) ($user['name'] ?? '');
$_SESSION['auth_level'] = (string) ($user['auth_level'] ?? 0);
// Keep the full edu_users row in session for cross-page use (password excluded).
unset($user['intra_pw']);
$_SESSION['edu_user'] = $user;
$_SESSION['edu_user_all'] = $user;
foreach ($user as $field => $value) {
$_SESSION['edu_user_' . $field] = $value;
}
$_SESSION['edu_login'] = '1';
$_SESSION['edu_login_at'] = date('Y-m-d H:i:s');
// 2026-04-02 권오재 추가.--- [추가: 접속 이력 프로시저 호출] ---
// 1. 정보 수집
$ip_address = $_SERVER['REMOTE_ADDR'];
$user_agent = $_SERVER['HTTP_USER_AGENT'];
// OS 및 디바이스 판별 (간단한 예시)
$os_name = 'Unknown OS';
if (preg_match('/windows|win32/i', $user_agent))
$os_name = 'Windows';
else if (preg_match('/macintosh|mac os x/i', $user_agent))
$os_name = 'Mac OS';
else if (preg_match('/android/i', $user_agent))
$os_name = 'Android';
else if (preg_match('/iphone/i', $user_agent))
$os_name = 'iOS';
$device_type = (preg_match('/mobile|android|iphone|ipad/i', $user_agent)) ? 'Mobile' : 'PC';
// 브라우저명 추출
$browser_name = 'Unknown Browser';
if (strpos($user_agent, 'MSIE') !== FALSE || strpos($user_agent, 'Trident') !== FALSE)
$browser_name = 'IE';
else if (strpos($user_agent, 'Edge') !== FALSE)
$browser_name = 'Edge';
else if (strpos($user_agent, 'Chrome') !== FALSE)
$browser_name = 'Chrome';
else if (strpos($user_agent, 'Firefox') !== FALSE)
$browser_name = 'Firefox';
else if (strpos($user_agent, 'Safari') !== FALSE)
$browser_name = 'Safari';
// 2. 프로시저 실행
$stmtLog = $pdo->prepare("CALL proc_insert_edu_access_log(?, ?, ?, ?, ?, ?)");
$stmtLog->execute([
$_SESSION['member_id'],
$_SESSION['sys_comp_code'],
$ip_address,
$os_name,
$device_type,
$browser_name
]);
header('Location: ' . $redirect);
exit;
} catch (Throwable $e) {
$back = '/skin/login.php?error=' . rawurlencode('로그인 처리 중 오류가 발생했습니다.')
. '&redirect=' . rawurlencode($redirect)
. '&member_id=' . rawurlencode($memberIdInput);
header('Location: ' . $back);
exit;
}
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* 로그아웃 API
* 모든 세션 데이터를 제거하고 쿠키를 삭제합니다.
*/
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
try {
// 세션 시작
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 로그 기록 (디버깅용)
$logMsg = '[logout.php] Logout requested | Session ID: ' . session_id() . ' | Time: ' . date('Y-m-d H:i:s');
error_log($logMsg);
// 현재 세션 데이터 로그 (디버깅용)
$sessionKeys = implode(', ', array_keys($_SESSION));
error_log('[logout.php] Session keys before clear: ' . ($sessionKeys ?: 'empty'));
// 모든 세션 변수 제거
$_SESSION = [];
// 세션 쿠키 제거
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
$cookiePath = $params['path'] ?: '/';
$cookieDomain = $params['domain'] ?: '';
$cookieSecure = $params['secure'] ?? false;
$cookieHttpOnly = $params['httponly'] ?? true;
// PHP 7.3+ 배열 문법
if (PHP_VERSION_ID >= 70300) {
setcookie(
session_name(),
'',
[
'expires' => 0,
'path' => $cookiePath,
'domain' => $cookieDomain,
'secure' => $cookieSecure,
'httponly' => $cookieHttpOnly,
'samesite' => 'Lax'
]
);
} else {
// PHP 7.2 이하 호환
setcookie(
session_name(),
'',
0,
$cookiePath,
$cookieDomain,
$cookieSecure,
$cookieHttpOnly
);
}
}
// 세션 파괴
@session_destroy();
error_log('[logout.php] Session destroyed successfully');
// 성공 응답
http_response_code(200);
echo json_encode([
'success' => true,
'message' => '로그아웃 되었습니다.',
'timestamp' => date('Y-m-d H:i:s')
], JSON_UNESCAPED_UNICODE);
exit;
} catch (Exception $e) {
error_log('[logout.php] Exception: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'message' => '로그아웃 중 오류가 발생했습니다.',
'error' => $e->getMessage()
], JSON_UNESCAPED_UNICODE);
exit;
}
+1
View File
@@ -0,0 +1 @@
Deny from all
+799
View File
@@ -0,0 +1,799 @@
[2026-03-10 18:55:33] [save_learning] request {"content_id":"F3","watch_tm":140,"content_tm":141,"heartbeat":0,"member_id":"U001"}
[2026-03-10 18:55:33] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:55:52] [save_learning] request {"content_id":"F3","watch_tm":5,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:55:52] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:55:57] [save_learning] request {"content_id":"F3","watch_tm":10,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:55:57] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:02] [save_learning] request {"content_id":"F3","watch_tm":15,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:02] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:07] [save_learning] request {"content_id":"F3","watch_tm":20,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:07] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:12] [save_learning] request {"content_id":"F3","watch_tm":25,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:12] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:17] [save_learning] request {"content_id":"F3","watch_tm":30,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:17] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:22] [save_learning] request {"content_id":"F3","watch_tm":35,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:22] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:27] [save_learning] request {"content_id":"F3","watch_tm":40,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:27] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:32] [save_learning] request {"content_id":"F3","watch_tm":45,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:32] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:37] [save_learning] request {"content_id":"F3","watch_tm":50,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:37] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:42] [save_learning] request {"content_id":"F3","watch_tm":55,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:42] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:47] [save_learning] request {"content_id":"F3","watch_tm":60,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:47] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:52] [save_learning] request {"content_id":"F3","watch_tm":65,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:52] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:56:57] [save_learning] request {"content_id":"F3","watch_tm":70,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:56:57] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:02] [save_learning] request {"content_id":"F3","watch_tm":75,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:02] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:07] [save_learning] request {"content_id":"F3","watch_tm":80,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:07] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:12] [save_learning] request {"content_id":"F3","watch_tm":85,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:12] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:17] [save_learning] request {"content_id":"F3","watch_tm":90,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:17] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:22] [save_learning] request {"content_id":"F3","watch_tm":95,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:22] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:27] [save_learning] request {"content_id":"F3","watch_tm":100,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:27] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:32] [save_learning] request {"content_id":"F3","watch_tm":105,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:32] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:37] [save_learning] request {"content_id":"F3","watch_tm":110,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:37] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:42] [save_learning] request {"content_id":"F3","watch_tm":115,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:42] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:47] [save_learning] request {"content_id":"F3","watch_tm":120,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:47] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:52] [save_learning] request {"content_id":"F3","watch_tm":125,"content_tm":140,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:57:52] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:57:53] [save_learning] request {"content_id":"F3","watch_tm":125,"content_tm":140,"heartbeat":0,"member_id":"U001"}
[2026-03-10 18:57:53] [save_learning] unregistered content_id {"content_id":"F3","member_id":"U001"}
[2026-03-10 18:58:06] [save_learning] request {"content_id":"F5","watch_tm":4,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:06] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:11] [save_learning] request {"content_id":"F5","watch_tm":9,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:11] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:16] [save_learning] request {"content_id":"F5","watch_tm":14,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:16] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:21] [save_learning] request {"content_id":"F5","watch_tm":19,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:21] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:26] [save_learning] request {"content_id":"F5","watch_tm":24,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:26] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:31] [save_learning] request {"content_id":"F5","watch_tm":29,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:31] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:36] [save_learning] request {"content_id":"F5","watch_tm":34,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:36] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:41] [save_learning] request {"content_id":"F5","watch_tm":39,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:41] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:46] [save_learning] request {"content_id":"F5","watch_tm":44,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:46] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:51] [save_learning] request {"content_id":"F5","watch_tm":49,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:51] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:58:56] [save_learning] request {"content_id":"F5","watch_tm":54,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:58:56] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:01] [save_learning] request {"content_id":"F5","watch_tm":59,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:01] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:06] [save_learning] request {"content_id":"F5","watch_tm":64,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:06] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:11] [save_learning] request {"content_id":"F5","watch_tm":69,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:11] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:16] [save_learning] request {"content_id":"F5","watch_tm":74,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:16] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:21] [save_learning] request {"content_id":"F5","watch_tm":79,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:21] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:26] [save_learning] request {"content_id":"F5","watch_tm":84,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:26] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:31] [save_learning] request {"content_id":"F5","watch_tm":89,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:31] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:36] [save_learning] request {"content_id":"F5","watch_tm":94,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:36] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:41] [save_learning] request {"content_id":"F5","watch_tm":99,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:41] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:46] [save_learning] request {"content_id":"F5","watch_tm":104,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:46] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:51] [save_learning] request {"content_id":"F5","watch_tm":109,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:51] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 18:59:56] [save_learning] request {"content_id":"F5","watch_tm":114,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 18:59:56] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:01] [save_learning] request {"content_id":"F5","watch_tm":119,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:01] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:06] [save_learning] request {"content_id":"F5","watch_tm":124,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:06] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:11] [save_learning] request {"content_id":"F5","watch_tm":129,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:11] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:16] [save_learning] request {"content_id":"F5","watch_tm":134,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:16] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:21] [save_learning] request {"content_id":"F5","watch_tm":139,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:21] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:26] [save_learning] request {"content_id":"F5","watch_tm":144,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:26] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:31] [save_learning] request {"content_id":"F5","watch_tm":150,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:31] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:36] [save_learning] request {"content_id":"F5","watch_tm":155,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:36] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:00:41] [save_learning] request {"content_id":"F5","watch_tm":159,"content_tm":1364,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:00:41] [save_learning] unregistered content_id {"content_id":"F5","member_id":"U001"}
[2026-03-10 19:12:10] [save_learning] request {"content_id":"F4","watch_tm":5,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:10] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:15] [save_learning] request {"content_id":"F4","watch_tm":10,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:15] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:20] [save_learning] request {"content_id":"F4","watch_tm":15,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:20] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:25] [save_learning] request {"content_id":"F4","watch_tm":20,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:25] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:30] [save_learning] request {"content_id":"F4","watch_tm":25,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:30] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:35] [save_learning] request {"content_id":"F4","watch_tm":30,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:35] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:40] [save_learning] request {"content_id":"F4","watch_tm":35,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:40] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:45] [save_learning] request {"content_id":"F4","watch_tm":40,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:45] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:50] [save_learning] request {"content_id":"F4","watch_tm":45,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:50] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:12:55] [save_learning] request {"content_id":"F4","watch_tm":50,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:12:55] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:00] [save_learning] request {"content_id":"F4","watch_tm":55,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:00] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:05] [save_learning] request {"content_id":"F4","watch_tm":60,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:05] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:10] [save_learning] request {"content_id":"F4","watch_tm":65,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:10] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:15] [save_learning] request {"content_id":"F4","watch_tm":70,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:15] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:20] [save_learning] request {"content_id":"F4","watch_tm":75,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:20] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:25] [save_learning] request {"content_id":"F4","watch_tm":80,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:25] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:30] [save_learning] request {"content_id":"F4","watch_tm":85,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:30] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:35] [save_learning] request {"content_id":"F4","watch_tm":90,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:35] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:40] [save_learning] request {"content_id":"F4","watch_tm":95,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:40] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:45] [save_learning] request {"content_id":"F4","watch_tm":100,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:45] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:50] [save_learning] request {"content_id":"F4","watch_tm":105,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:50] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:13:55] [save_learning] request {"content_id":"F4","watch_tm":110,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:13:55] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:00] [save_learning] request {"content_id":"F4","watch_tm":115,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:00] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:05] [save_learning] request {"content_id":"F4","watch_tm":120,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:05] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:10] [save_learning] request {"content_id":"F4","watch_tm":125,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:10] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:15] [save_learning] request {"content_id":"F4","watch_tm":130,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:15] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:20] [save_learning] request {"content_id":"F4","watch_tm":135,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:20] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:25] [save_learning] request {"content_id":"F4","watch_tm":140,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:25] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:30] [save_learning] request {"content_id":"F4","watch_tm":145,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:30] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:35] [save_learning] request {"content_id":"F4","watch_tm":150,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:35] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:40] [save_learning] request {"content_id":"F4","watch_tm":155,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:40] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:45] [save_learning] request {"content_id":"F4","watch_tm":160,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:45] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:50] [save_learning] request {"content_id":"F4","watch_tm":165,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:50] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:14:55] [save_learning] request {"content_id":"F4","watch_tm":170,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:14:55] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:00] [save_learning] request {"content_id":"F4","watch_tm":175,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:00] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:05] [save_learning] request {"content_id":"F4","watch_tm":180,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:05] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:10] [save_learning] request {"content_id":"F4","watch_tm":185,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:10] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:15] [save_learning] request {"content_id":"F4","watch_tm":190,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:15] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:20] [save_learning] request {"content_id":"F4","watch_tm":195,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:20] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:25] [save_learning] request {"content_id":"F4","watch_tm":200,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:25] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:30] [save_learning] request {"content_id":"F4","watch_tm":205,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:30] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:35] [save_learning] request {"content_id":"F4","watch_tm":210,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:35] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:40] [save_learning] request {"content_id":"F4","watch_tm":215,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:40] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:45] [save_learning] request {"content_id":"F4","watch_tm":220,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:45] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:50] [save_learning] request {"content_id":"F4","watch_tm":225,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:50] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:55] [save_learning] request {"content_id":"F4","watch_tm":230,"content_tm":234,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:15:55] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:15:59] [save_learning] request {"content_id":"F4","watch_tm":234,"content_tm":235,"heartbeat":4,"member_id":"U001"}
[2026-03-10 19:15:59] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:17:17] [save_learning] request {"content_id":"F4","watch_tm":234,"content_tm":235,"heartbeat":0,"member_id":"U001"}
[2026-03-10 19:17:17] [save_learning] unregistered content_id {"content_id":"F4","member_id":"U001"}
[2026-03-10 19:32:02] [save_learning] request {"content_id":"CNT001","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-10 19:32:02] [save_learning] UPDATE done {"content_id":"CNT001","watch_tm":0,"heartbeat":0}
[2026-03-10 19:34:33] [save_learning] request {"content_id":"F6","watch_tm":5,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:34:33] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:34:38] [save_learning] request {"content_id":"F6","watch_tm":10,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:34:38] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:34:43] [save_learning] request {"content_id":"F6","watch_tm":15,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:34:43] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:34:48] [save_learning] request {"content_id":"F6","watch_tm":20,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:34:48] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:34:53] [save_learning] request {"content_id":"F6","watch_tm":25,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:34:53] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:34:58] [save_learning] request {"content_id":"F6","watch_tm":30,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:34:58] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:03] [save_learning] request {"content_id":"F6","watch_tm":35,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:03] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:08] [save_learning] request {"content_id":"F6","watch_tm":40,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:08] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:13] [save_learning] request {"content_id":"F6","watch_tm":45,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:13] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:18] [save_learning] request {"content_id":"F6","watch_tm":50,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:18] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:23] [save_learning] request {"content_id":"F6","watch_tm":55,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:23] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:28] [save_learning] request {"content_id":"F6","watch_tm":60,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:28] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:33] [save_learning] request {"content_id":"F6","watch_tm":65,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:33] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:38] [save_learning] request {"content_id":"F6","watch_tm":70,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:38] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:43] [save_learning] request {"content_id":"F6","watch_tm":75,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:43] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:48] [save_learning] request {"content_id":"F6","watch_tm":80,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:48] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:53] [save_learning] request {"content_id":"F6","watch_tm":85,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:53] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:35:58] [save_learning] request {"content_id":"F6","watch_tm":90,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:35:58] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:03] [save_learning] request {"content_id":"F6","watch_tm":95,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:03] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:08] [save_learning] request {"content_id":"F6","watch_tm":100,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:08] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:13] [save_learning] request {"content_id":"F6","watch_tm":105,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:13] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:18] [save_learning] request {"content_id":"F6","watch_tm":110,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:18] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:23] [save_learning] request {"content_id":"F6","watch_tm":115,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:23] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:28] [save_learning] request {"content_id":"F6","watch_tm":120,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:28] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:33] [save_learning] request {"content_id":"F6","watch_tm":125,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:33] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:38] [save_learning] request {"content_id":"F6","watch_tm":130,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:38] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:43] [save_learning] request {"content_id":"F6","watch_tm":135,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:43] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:48] [save_learning] request {"content_id":"F6","watch_tm":140,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:48] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:53] [save_learning] request {"content_id":"F6","watch_tm":145,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:53] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:36:58] [save_learning] request {"content_id":"F6","watch_tm":150,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:36:58] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:37:03] [save_learning] request {"content_id":"F6","watch_tm":155,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:37:03] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:37:08] [save_learning] request {"content_id":"F6","watch_tm":160,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:37:08] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:37:13] [save_learning] request {"content_id":"F6","watch_tm":165,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:37:13] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:37:18] [save_learning] request {"content_id":"F6","watch_tm":170,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:37:18] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:37:23] [save_learning] request {"content_id":"F6","watch_tm":175,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:37:23] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:37:28] [save_learning] request {"content_id":"F6","watch_tm":180,"content_tm":3115,"heartbeat":5,"member_id":"U001"}
[2026-03-10 19:37:28] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:37:32] [save_learning] request {"content_id":"F6","watch_tm":184,"content_tm":3115,"heartbeat":4,"member_id":"U001"}
[2026-03-10 19:37:32] [save_learning] unregistered content_id {"content_id":"F6","member_id":"U001"}
[2026-03-10 19:45:39] [save_learning] request {"content_id":"CNT009","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-10 19:45:39] [save_learning] INSERT done {"content_id":"CNT009","watch_tm":0,"heartbeat":0}
[2026-03-10 19:53:51] [save_learning] request {"content_id":"20260305-005","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-10 19:53:51] [save_learning] INSERT done {"content_id":"20260305-005","watch_tm":0,"heartbeat":0}
[2026-03-10 19:53:56] [save_learning] request {"content_id":"20260310-001","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-10 19:53:56] [save_learning] INSERT done {"content_id":"20260310-001","watch_tm":0,"heartbeat":0}
[2026-03-10 20:00:04] [save_learning] request {"content_id":"20260310-001","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-10 20:00:05] [save_learning] UPDATE done {"content_id":"20260310-001","watch_tm":0,"heartbeat":0}
[2026-03-10 20:09:56] [main_data] DB_ERROR {"error":"SQLSTATE[42S02]: Base table or view not found: 1146 Table 'baronhomep.edu_edu_recommend_keywords' doesn't exist in \/baronhomep\/www\/edu\/bbs\/main_data.php:131"}
[2026-03-10 20:10:04] [main_data] DB_ERROR {"error":"SQLSTATE[42S02]: Base table or view not found: 1146 Table 'baronhomep.edu_edu_recommend_keywords' doesn't exist in \/baronhomep\/www\/edu\/bbs\/main_data.php:131"}
[2026-03-10 20:11:15] [main_data] DB_ERROR {"error":"SQLSTATE[42S02]: Base table or view not found: 1146 Table 'baronhomep.edu_edu_recommend_keywords' doesn't exist in \/baronhomep\/www\/edu\/bbs\/main_data.php:131"}
[2026-03-10 20:11:22] [main_data] DB_ERROR {"error":"SQLSTATE[42S02]: Base table or view not found: 1146 Table 'baronhomep.edu_edu_recommend_keywords' doesn't exist in \/baronhomep\/www\/edu\/bbs\/main_data.php:131"}
[2026-03-10 20:13:35] [save_learning] request {"content_id":"20260305-004","watch_tm":5,"content_tm":11111,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:13:35] [save_learning] INSERT done {"content_id":"20260305-004","watch_tm":5,"heartbeat":5}
[2026-03-10 20:13:40] [save_learning] request {"content_id":"20260305-004","watch_tm":10,"content_tm":11111,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:13:40] [save_learning] UPDATE done {"content_id":"20260305-004","watch_tm":10,"heartbeat":5}
[2026-03-10 20:13:45] [save_learning] request {"content_id":"20260305-004","watch_tm":15,"content_tm":11111,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:13:45] [save_learning] UPDATE done {"content_id":"20260305-004","watch_tm":15,"heartbeat":5}
[2026-03-10 20:13:50] [save_learning] request {"content_id":"20260305-004","watch_tm":20,"content_tm":11111,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:13:50] [save_learning] UPDATE done {"content_id":"20260305-004","watch_tm":20,"heartbeat":5}
[2026-03-10 20:13:55] [save_learning] request {"content_id":"20260305-004","watch_tm":25,"content_tm":11111,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:13:55] [save_learning] UPDATE done {"content_id":"20260305-004","watch_tm":25,"heartbeat":5}
[2026-03-10 20:14:00] [save_learning] request {"content_id":"20260305-004","watch_tm":30,"content_tm":11111,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:14:00] [save_learning] UPDATE done {"content_id":"20260305-004","watch_tm":30,"heartbeat":5}
[2026-03-10 20:14:05] [save_learning] request {"content_id":"20260305-004","watch_tm":35,"content_tm":11111,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:14:05] [save_learning] UPDATE done {"content_id":"20260305-004","watch_tm":35,"heartbeat":5}
[2026-03-10 20:14:10] [save_learning] request {"content_id":"20260305-004","watch_tm":40,"content_tm":11111,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:14:10] [save_learning] UPDATE done {"content_id":"20260305-004","watch_tm":40,"heartbeat":5}
[2026-03-10 20:14:11] [save_learning] request {"content_id":"20260305-004","watch_tm":41,"content_tm":11111,"heartbeat":1,"member_id":"U001"}
[2026-03-10 20:14:11] [save_learning] UPDATE done {"content_id":"20260305-004","watch_tm":41,"heartbeat":1}
[2026-03-10 20:18:15] [save_learning] request {"content_id":"CNT001","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-10 20:18:15] [save_learning] UPDATE done {"content_id":"CNT001","watch_tm":0,"heartbeat":0}
[2026-03-10 20:40:38] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-10 20:40:38] [save_learning] INSERT done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-10 20:40:40] [save_learning] request {"content_id":"20260305-001","watch_tm":6,"content_tm":155,"heartbeat":1,"member_id":"U001"}
[2026-03-10 20:40:40] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":6,"heartbeat":1}
[2026-03-10 20:41:04] [save_learning] request {"content_id":"20260306-004","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-10 20:41:04] [save_learning] INSERT done {"content_id":"20260306-004","watch_tm":0,"heartbeat":0}
[2026-03-10 20:41:08] [save_learning] request {"content_id":"20260305-006","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-10 20:41:08] [save_learning] INSERT done {"content_id":"20260305-006","watch_tm":0,"heartbeat":0}
[2026-03-10 20:41:12] [save_learning] request {"content_id":"20260305-001","watch_tm":1,"content_tm":155,"heartbeat":1,"member_id":"U001"}
[2026-03-10 20:41:12] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":1,"heartbeat":1}
[2026-03-11 13:13:49] [save_learning] request {"content_id":"20260305-001","watch_tm":4,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:13:49] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":4,"heartbeat":5}
[2026-03-11 13:13:54] [save_learning] request {"content_id":"20260305-001","watch_tm":9,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:13:54] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":9,"heartbeat":5}
[2026-03-11 13:13:59] [save_learning] request {"content_id":"20260305-001","watch_tm":14,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:13:59] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":14,"heartbeat":5}
[2026-03-11 13:14:04] [save_learning] request {"content_id":"20260305-001","watch_tm":19,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:14:04] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":19,"heartbeat":5}
[2026-03-11 13:14:09] [save_learning] request {"content_id":"20260305-001","watch_tm":24,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:14:09] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":24,"heartbeat":5}
[2026-03-11 13:14:12] [save_learning] request {"content_id":"20260305-001","watch_tm":28,"content_tm":155,"heartbeat":3,"member_id":"U001"}
[2026-03-11 13:14:12] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":28,"heartbeat":3}
[2026-03-11 13:14:59] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:14:59] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-11 13:15:04] [save_learning] request {"content_id":"20260305-001","watch_tm":10,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:04] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":10,"heartbeat":5}
[2026-03-11 13:15:10] [save_learning] request {"content_id":"20260305-001","watch_tm":16,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:10] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":16,"heartbeat":5}
[2026-03-11 13:15:15] [save_learning] request {"content_id":"20260305-001","watch_tm":21,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:15] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":21,"heartbeat":5}
[2026-03-11 13:15:20] [save_learning] request {"content_id":"20260305-001","watch_tm":26,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:20] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":26,"heartbeat":5}
[2026-03-11 13:15:25] [save_learning] request {"content_id":"20260305-001","watch_tm":31,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:25] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":31,"heartbeat":5}
[2026-03-11 13:15:30] [save_learning] request {"content_id":"20260305-001","watch_tm":36,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:30] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":36,"heartbeat":5}
[2026-03-11 13:15:35] [save_learning] request {"content_id":"20260305-001","watch_tm":41,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:35] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":41,"heartbeat":5}
[2026-03-11 13:15:40] [save_learning] request {"content_id":"20260305-001","watch_tm":46,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:40] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":46,"heartbeat":5}
[2026-03-11 13:15:45] [save_learning] request {"content_id":"20260305-001","watch_tm":51,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:45] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":51,"heartbeat":5}
[2026-03-11 13:15:50] [save_learning] request {"content_id":"20260305-001","watch_tm":56,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:50] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":56,"heartbeat":5}
[2026-03-11 13:15:55] [save_learning] request {"content_id":"20260305-001","watch_tm":61,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:15:55] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":61,"heartbeat":5}
[2026-03-11 13:16:00] [save_learning] request {"content_id":"20260305-001","watch_tm":66,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:00] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":66,"heartbeat":5}
[2026-03-11 13:16:05] [save_learning] request {"content_id":"20260305-001","watch_tm":71,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:05] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":71,"heartbeat":5}
[2026-03-11 13:16:10] [save_learning] request {"content_id":"20260305-001","watch_tm":76,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:10] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":76,"heartbeat":5}
[2026-03-11 13:16:15] [save_learning] request {"content_id":"20260305-001","watch_tm":81,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:15] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":81,"heartbeat":5}
[2026-03-11 13:16:20] [save_learning] request {"content_id":"20260305-001","watch_tm":86,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:20] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":86,"heartbeat":5}
[2026-03-11 13:16:25] [save_learning] request {"content_id":"20260305-001","watch_tm":91,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:25] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":91,"heartbeat":5}
[2026-03-11 13:16:30] [save_learning] request {"content_id":"20260305-001","watch_tm":96,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:30] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":96,"heartbeat":5}
[2026-03-11 13:16:35] [save_learning] request {"content_id":"20260305-001","watch_tm":101,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:35] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":101,"heartbeat":5}
[2026-03-11 13:16:40] [save_learning] request {"content_id":"20260305-001","watch_tm":106,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:40] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":106,"heartbeat":5}
[2026-03-11 13:16:45] [save_learning] request {"content_id":"20260305-001","watch_tm":111,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:45] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":111,"heartbeat":5}
[2026-03-11 13:16:50] [save_learning] request {"content_id":"20260305-001","watch_tm":116,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:50] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":116,"heartbeat":5}
[2026-03-11 13:16:55] [save_learning] request {"content_id":"20260305-001","watch_tm":121,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:16:55] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":121,"heartbeat":5}
[2026-03-11 13:17:00] [save_learning] request {"content_id":"20260305-001","watch_tm":126,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:17:00] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":126,"heartbeat":5}
[2026-03-11 13:17:05] [save_learning] request {"content_id":"20260305-001","watch_tm":131,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:17:05] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":131,"heartbeat":5}
[2026-03-11 13:17:10] [save_learning] request {"content_id":"20260305-001","watch_tm":136,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:17:10] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":136,"heartbeat":5}
[2026-03-11 13:17:15] [save_learning] request {"content_id":"20260305-001","watch_tm":141,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:17:15] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":141,"heartbeat":5}
[2026-03-11 13:17:20] [save_learning] request {"content_id":"20260305-001","watch_tm":146,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:17:20] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":146,"heartbeat":5}
[2026-03-11 13:17:25] [save_learning] request {"content_id":"20260305-001","watch_tm":151,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-11 13:17:25] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":151,"heartbeat":5}
[2026-03-11 13:17:29] [save_learning] request {"content_id":"20260305-001","watch_tm":155,"content_tm":156,"heartbeat":4,"member_id":"U001"}
[2026-03-11 13:17:29] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":155,"heartbeat":4}
[2026-03-11 14:06:02] [save_learning] request {"content_id":"20260305-001","watch_tm":155,"content_tm":156,"heartbeat":0,"member_id":"U001"}
[2026-03-11 14:06:02] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":155,"heartbeat":0}
[2026-03-11 14:32:53] [save_learning] request {"content_id":"20260306-002","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-11 14:32:53] [save_learning] INSERT done {"content_id":"20260306-002","watch_tm":0,"heartbeat":0}
[2026-03-12 15:32:31] [save_learning] request {"content_id":"20260305-001","watch_tm":4,"content_tm":155,"heartbeat":4,"member_id":"U001"}
[2026-03-12 15:32:31] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":4,"heartbeat":4}
[2026-03-12 15:32:35] [save_learning] request {"content_id":"20260305-001","watch_tm":0,"content_tm":155,"heartbeat":0,"member_id":"U001"}
[2026-03-12 15:32:35] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":0,"heartbeat":0}
[2026-03-12 15:32:38] [save_learning] request {"content_id":"CNT003","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-12 15:32:38] [save_learning] INSERT done {"content_id":"CNT003","watch_tm":0,"heartbeat":0}
[2026-03-12 15:32:40] [save_learning] request {"content_id":"C018","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-12 15:32:40] [save_learning] INSERT done {"content_id":"C018","watch_tm":0,"heartbeat":0}
[2026-03-12 15:37:47] [save_learning] request {"content_id":"20260305-001","watch_tm":4,"content_tm":155,"heartbeat":4,"member_id":"U001"}
[2026-03-12 15:37:47] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":4,"heartbeat":4}
[2026-03-12 15:37:48] [save_learning] request {"content_id":"20260305-001","watch_tm":4,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-12 15:37:48] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":4,"heartbeat":5}
[2026-03-12 15:37:49] [save_learning] request {"content_id":"20260305-001","watch_tm":6,"content_tm":155,"heartbeat":1,"member_id":"U001"}
[2026-03-12 15:37:49] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":6,"heartbeat":1}
[2026-03-12 16:11:42] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-12 16:11:42] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-12 16:11:43] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-12 16:11:43] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-12 16:11:47] [save_learning] request {"content_id":"20260305-001","watch_tm":10,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-12 16:11:47] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":10,"heartbeat":5}
[2026-03-12 16:11:48] [save_learning] request {"content_id":"20260305-001","watch_tm":10,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-12 16:11:48] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":10,"heartbeat":5}
[2026-03-12 16:11:52] [save_learning] request {"content_id":"20260305-001","watch_tm":15,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-12 16:11:52] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":15,"heartbeat":5}
[2026-03-12 16:11:53] [save_learning] request {"content_id":"20260305-001","watch_tm":15,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-12 16:11:53] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":15,"heartbeat":5}
[2026-03-12 16:11:56] [save_learning] request {"content_id":"20260305-001","watch_tm":18,"content_tm":155,"heartbeat":3,"member_id":"U001"}
[2026-03-12 16:11:56] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":18,"heartbeat":3}
[2026-03-12 16:11:57] [save_learning] request {"content_id":"20260305-001","watch_tm":19,"content_tm":155,"heartbeat":4,"member_id":"U001"}
[2026-03-12 16:11:57] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":19,"heartbeat":4}
[2026-03-16 13:40:45] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-16 13:40:45] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-16 13:40:45] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":156,"heartbeat":5,"member_id":"U001"}
[2026-03-16 13:40:45] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-16 13:40:45] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:40:45] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":0}
[2026-03-16 13:40:46] [save_learning] request {"content_id":"20260305-001","watch_tm":6,"content_tm":156,"heartbeat":1,"member_id":"U001"}
[2026-03-16 13:40:46] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":6,"heartbeat":1}
[2026-03-16 13:40:52] [save_learning] request {"content_id":"20260305-001","watch_tm":1,"content_tm":155,"heartbeat":1,"member_id":"U001"}
[2026-03-16 13:40:52] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":1,"heartbeat":1}
[2026-03-16 13:40:55] [save_learning] request {"content_id":"20260305-001","watch_tm":4,"content_tm":155,"heartbeat":4,"member_id":"U001"}
[2026-03-16 13:40:55] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":4,"heartbeat":4}
[2026-03-16 13:41:03] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-16 13:41:03] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-16 13:41:03] [save_learning] request {"content_id":"20260305-001","watch_tm":4,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-16 13:41:03] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":4,"heartbeat":5}
[2026-03-16 13:41:07] [save_learning] request {"content_id":"20260305-001","watch_tm":9,"content_tm":155,"heartbeat":4,"member_id":"U001"}
[2026-03-16 13:41:07] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":9,"heartbeat":4}
[2026-03-16 13:41:08] [save_learning] request {"content_id":"20260305-001","watch_tm":10,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-16 13:41:08] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":10,"heartbeat":5}
[2026-03-16 13:41:11] [save_learning] request {"content_id":"20260305-001","watch_tm":12,"content_tm":155,"heartbeat":2,"member_id":"U001"}
[2026-03-16 13:41:11] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":12,"heartbeat":2}
[2026-03-16 13:41:14] [save_learning] request {"content_id":"20260305-001","watch_tm":0,"content_tm":155,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:41:14] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":0,"heartbeat":0}
[2026-03-16 13:41:16] [save_learning] request {"content_id":"20260305-001","watch_tm":2,"content_tm":155,"heartbeat":2,"member_id":"U001"}
[2026-03-16 13:41:16] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":2,"heartbeat":2}
[2026-03-16 13:41:24] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-16 13:41:24] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-16 13:41:24] [save_learning] request {"content_id":"20260305-001","watch_tm":4,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-16 13:41:24] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":4,"heartbeat":5}
[2026-03-16 13:41:24] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:41:24] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":0}
[2026-03-16 13:41:29] [save_learning] request {"content_id":"20260305-001","watch_tm":10,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-16 13:41:29] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":10,"heartbeat":5}
[2026-03-16 13:41:29] [save_learning] request {"content_id":"20260305-001","watch_tm":10,"content_tm":155,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:41:29] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":10,"heartbeat":0}
[2026-03-16 13:41:35] [save_learning] request {"content_id":"CNT003","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:41:35] [save_learning] UPDATE done {"content_id":"CNT003","watch_tm":0,"heartbeat":0}
[2026-03-16 13:41:40] [save_learning] request {"content_id":"CNT013","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:41:40] [save_learning] INSERT done {"content_id":"CNT013","watch_tm":0,"heartbeat":0}
[2026-03-16 13:41:41] [save_learning] request {"content_id":"CNT013","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:41:41] [save_learning] UPDATE done {"content_id":"CNT013","watch_tm":0,"heartbeat":0}
[2026-03-16 13:55:06] [save_learning] request {"content_id":1,"watch_tm":0,"content_tm":1195,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:55:06] [save_learning] unregistered content_id {"content_id":1,"member_id":"U001"}
[2026-03-16 13:55:10] [save_learning] request {"content_id":3,"watch_tm":0,"content_tm":140,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:55:10] [save_learning] unregistered content_id {"content_id":3,"member_id":"U001"}
[2026-03-16 13:55:16] [save_learning] request {"content_id":4,"watch_tm":0,"content_tm":234,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:55:16] [save_learning] unregistered content_id {"content_id":4,"member_id":"U001"}
[2026-03-16 13:55:35] [save_learning] request {"content_id":5,"watch_tm":1,"content_tm":1364,"heartbeat":1,"member_id":"U001"}
[2026-03-16 13:55:35] [save_learning] unregistered content_id {"content_id":5,"member_id":"U001"}
[2026-03-16 13:55:37] [save_learning] request {"content_id":3,"watch_tm":0,"content_tm":140,"heartbeat":0,"member_id":"U001"}
[2026-03-16 13:55:37] [save_learning] unregistered content_id {"content_id":3,"member_id":"U001"}
[2026-03-16 14:21:11] [save_learning] request {"content_id":"20260305-001","watch_tm":7,"content_tm":156,"heartbeat":5,"member_id":"U001"}
[2026-03-16 14:21:11] [save_learning] request {"content_id":"20260305-001","watch_tm":7,"content_tm":156,"heartbeat":5,"member_id":"U001"}
[2026-03-16 14:21:11] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":7,"heartbeat":5}
[2026-03-16 14:21:11] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":7,"heartbeat":5}
[2026-03-16 14:21:16] [save_learning] request {"content_id":"20260305-001","watch_tm":8,"content_tm":156,"heartbeat":5,"member_id":"U001"}
[2026-03-16 14:21:16] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":8,"heartbeat":5}
[2026-03-16 14:21:16] [save_learning] request {"content_id":"20260305-001","watch_tm":9,"content_tm":156,"heartbeat":5,"member_id":"U001"}
[2026-03-16 14:21:16] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":9,"heartbeat":5}
[2026-03-16 14:21:21] [save_learning] request {"content_id":"20260305-001","watch_tm":13,"content_tm":156,"heartbeat":5,"member_id":"U001"}
[2026-03-16 14:21:21] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":13,"heartbeat":5}
[2026-03-16 14:21:21] [save_learning] request {"content_id":"20260305-001","watch_tm":14,"content_tm":156,"heartbeat":5,"member_id":"U001"}
[2026-03-16 14:21:21] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":14,"heartbeat":5}
[2026-03-16 14:21:22] [save_learning] request {"content_id":"20260305-001","watch_tm":15,"content_tm":156,"heartbeat":1,"member_id":"U001"}
[2026-03-16 14:21:22] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":15,"heartbeat":1}
[2026-03-16 14:21:24] [save_learning] request {"content_id":"20260305-001","watch_tm":17,"content_tm":156,"heartbeat":3,"member_id":"U001"}
[2026-03-16 14:21:24] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":17,"heartbeat":3}
[2026-03-16 14:33:58] [save_learning] request {"content_id":"20260305-001","watch_tm":0,"content_tm":155,"heartbeat":0,"member_id":"U001"}
[2026-03-16 14:33:58] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":0,"heartbeat":0}
[2026-03-16 14:35:39] [save_learning] request {"content_id":"CNT013","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 14:35:39] [save_learning] UPDATE done {"content_id":"CNT013","watch_tm":0,"heartbeat":0}
[2026-03-16 14:35:41] [save_learning] request {"content_id":"CNT003","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 14:35:41] [save_learning] UPDATE done {"content_id":"CNT003","watch_tm":0,"heartbeat":0}
[2026-03-16 14:35:47] [save_learning] request {"content_id":"20260305-001","watch_tm":5,"content_tm":155,"heartbeat":5,"member_id":"U001"}
[2026-03-16 14:35:47] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":5,"heartbeat":5}
[2026-03-16 14:35:48] [save_learning] request {"content_id":"20260305-001","watch_tm":6,"content_tm":155,"heartbeat":1,"member_id":"U001"}
[2026-03-16 14:35:48] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":6,"heartbeat":1}
[2026-03-16 14:44:10] [save_learning] request {"content_id":"CNT004","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 14:44:10] [save_learning] INSERT done {"content_id":"CNT004","watch_tm":0,"heartbeat":0}
[2026-03-16 16:28:41] [save_learning] request {"content_id":"20260305-001","watch_tm":0,"content_tm":155,"heartbeat":0,"member_id":"U001"}
[2026-03-16 16:28:41] [save_learning] UPDATE done {"content_id":"20260305-001","watch_tm":0,"heartbeat":0}
[2026-03-16 17:54:08] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 17:54:08] [save_learning] INSERT done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-16 17:54:10] [save_learning] request {"content_id":"20260316-094","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 17:54:10] [save_learning] INSERT done {"content_id":"20260316-094","watch_tm":0,"heartbeat":0}
[2026-03-16 17:54:12] [save_learning] request {"content_id":"20260316-119","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 17:54:12] [save_learning] INSERT done {"content_id":"20260316-119","watch_tm":0,"heartbeat":0}
[2026-03-16 17:54:16] [save_learning] request {"content_id":"20260316-120","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 17:54:16] [save_learning] INSERT done {"content_id":"20260316-120","watch_tm":0,"heartbeat":0}
[2026-03-16 17:54:25] [save_learning] request {"content_id":"20260316-107","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 17:54:25] [save_learning] INSERT done {"content_id":"20260316-107","watch_tm":0,"heartbeat":0}
[2026-03-16 17:55:07] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 17:55:07] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-16 17:58:43] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 17:58:43] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-16 19:49:44] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 19:49:44] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-16 19:49:45] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 19:49:45] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-16 21:22:43] [save_learning] request {"content_id":"20260316-096","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 21:22:43] [save_learning] INSERT done {"content_id":"20260316-096","watch_tm":0,"heartbeat":0}
[2026-03-16 21:22:46] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 21:22:46] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-16 21:22:47] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 21:22:47] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-16 21:22:49] [save_learning] request {"content_id":"20260316-103","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 21:22:49] [save_learning] INSERT done {"content_id":"20260316-103","watch_tm":0,"heartbeat":0}
[2026-03-16 21:22:50] [save_learning] request {"content_id":"20260316-103","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-16 21:22:50] [save_learning] UPDATE done {"content_id":"20260316-103","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:12] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:12] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:14] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:14] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:15] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:15] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:15] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:15] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:16] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:16] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:16] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:16] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:17] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:17] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:17] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:17] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:18] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:18] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:19] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:19] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:19] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:19] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:20] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:20] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:31] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:31] [save_learning] INSERT done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:32] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:32] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:32] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:32] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:33] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:33] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:34] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:34] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:34] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:34] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:34] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:34] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:34] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:34] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 09:32:35] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 09:32:35] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 12:49:02] [save_learning] request {"content_id":"20260316-086","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:49:02] [save_learning] INSERT done {"content_id":"20260316-086","watch_tm":0,"heartbeat":0}
[2026-03-17 12:49:08] [save_learning] request {"content_id":"20260316-099","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:49:08] [save_learning] INSERT done {"content_id":"20260316-099","watch_tm":0,"heartbeat":0}
[2026-03-17 12:49:12] [save_learning] request {"content_id":"20260316-096","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:49:12] [save_learning] UPDATE done {"content_id":"20260316-096","watch_tm":0,"heartbeat":0}
[2026-03-17 12:49:14] [save_learning] request {"content_id":"20260316-096","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:49:14] [save_learning] UPDATE done {"content_id":"20260316-096","watch_tm":0,"heartbeat":0}
[2026-03-17 12:49:19] [save_learning] request {"content_id":"20260316-087","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:49:19] [save_learning] INSERT done {"content_id":"20260316-087","watch_tm":0,"heartbeat":0}
[2026-03-17 12:49:43] [save_learning] request {"content_id":"20260316-103","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:49:43] [save_learning] UPDATE done {"content_id":"20260316-103","watch_tm":0,"heartbeat":0}
[2026-03-17 12:50:10] [save_learning] request {"content_id":"20260316-108","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:50:10] [save_learning] UPDATE done {"content_id":"20260316-108","watch_tm":0,"heartbeat":0}
[2026-03-17 12:53:05] [save_learning] request {"content_id":"20260316-112","watch_tm":4,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 12:53:05] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":4,"heartbeat":5}
[2026-03-17 12:53:12] [save_learning] request {"content_id":"20260316-112","watch_tm":11,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 12:53:12] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":11,"heartbeat":5}
[2026-03-17 12:53:17] [save_learning] request {"content_id":"20260316-112","watch_tm":16,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 12:53:17] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":16,"heartbeat":5}
[2026-03-17 12:53:17] [save_learning] request {"content_id":"20260316-112","watch_tm":48,"content_tm":1091,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:53:17] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":48,"heartbeat":0}
[2026-03-17 12:53:18] [save_learning] request {"content_id":"20260316-112","watch_tm":119,"content_tm":1091,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:53:18] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":119,"heartbeat":0}
[2026-03-17 12:53:19] [save_learning] request {"content_id":"20260316-112","watch_tm":193,"content_tm":1091,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:53:19] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":193,"heartbeat":0}
[2026-03-17 12:53:20] [save_learning] request {"content_id":"20260316-112","watch_tm":278,"content_tm":1091,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:53:20] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":278,"heartbeat":0}
[2026-03-17 12:53:20] [save_learning] request {"content_id":"20260316-112","watch_tm":343,"content_tm":1091,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:53:20] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":343,"heartbeat":0}
[2026-03-17 12:53:22] [save_learning] request {"content_id":"20260316-112","watch_tm":461,"content_tm":1091,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:53:22] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":461,"heartbeat":0}
[2026-03-17 12:53:28] [save_learning] request {"content_id":"20260316-112","watch_tm":467,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 12:53:28] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":467,"heartbeat":5}
[2026-03-17 12:53:31] [save_learning] request {"content_id":"20260316-112","watch_tm":470,"content_tm":1091,"heartbeat":2,"member_id":"U001"}
[2026-03-17 12:53:31] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":470,"heartbeat":2}
[2026-03-17 12:53:32] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:53:32] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 12:56:55] [save_learning] request {"content_id":"20260316-112","watch_tm":1,"content_tm":1090,"heartbeat":1,"member_id":"U001"}
[2026-03-17 12:56:55] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":1,"heartbeat":1}
[2026-03-17 12:56:58] [save_learning] request {"content_id":"20260316-112","watch_tm":0,"content_tm":0,"heartbeat":0,"member_id":"U001"}
[2026-03-17 12:56:58] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":0,"heartbeat":0}
[2026-03-17 13:47:22] [save_learning] request {"content_id":"20260316-112","watch_tm":15,"content_tm":300,"heartbeat":0,"member_id":"U001"}
[2026-03-17 13:47:22] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":15,"heartbeat":0}
[2026-03-17 13:49:32] [save_learning] request {"content_id":"20260316-112","watch_tm":22,"content_tm":300,"heartbeat":9,"member_id":"U001"}
[2026-03-17 13:49:32] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":22,"heartbeat":9}
[2026-03-17 13:50:16] [save_learning] request {"content_id":"20260316-125","watch_tm":5,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:50:16] [save_learning] INSERT done {"content_id":"20260316-125","watch_tm":5,"heartbeat":5}
[2026-03-17 13:50:21] [save_learning] request {"content_id":"20260316-125","watch_tm":10,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:50:21] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":10,"heartbeat":5}
[2026-03-17 13:50:26] [save_learning] request {"content_id":"20260316-125","watch_tm":54,"content_tm":1091,"heartbeat":4,"member_id":"U001"}
[2026-03-17 13:50:26] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":54,"heartbeat":4}
[2026-03-17 13:50:27] [save_learning] request {"content_id":"20260316-125","watch_tm":221,"content_tm":1091,"heartbeat":0,"member_id":"U001"}
[2026-03-17 13:50:27] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":221,"heartbeat":0}
[2026-03-17 13:50:34] [save_learning] request {"content_id":"20260316-125","watch_tm":227,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:50:34] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":227,"heartbeat":5}
[2026-03-17 13:50:39] [save_learning] request {"content_id":"20260316-125","watch_tm":232,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:50:39] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":232,"heartbeat":5}
[2026-03-17 13:50:44] [save_learning] request {"content_id":"20260316-125","watch_tm":237,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:50:44] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":237,"heartbeat":5}
[2026-03-17 13:50:49] [save_learning] request {"content_id":"20260316-125","watch_tm":242,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:50:49] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":242,"heartbeat":5}
[2026-03-17 13:50:54] [save_learning] request {"content_id":"20260316-125","watch_tm":247,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:50:54] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":247,"heartbeat":5}
[2026-03-17 13:50:59] [save_learning] request {"content_id":"20260316-125","watch_tm":252,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:50:59] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":252,"heartbeat":5}
[2026-03-17 13:51:04] [save_learning] request {"content_id":"20260316-125","watch_tm":257,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:04] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":257,"heartbeat":5}
[2026-03-17 13:51:09] [save_learning] request {"content_id":"20260316-125","watch_tm":262,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:09] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":262,"heartbeat":5}
[2026-03-17 13:51:14] [save_learning] request {"content_id":"20260316-125","watch_tm":267,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:14] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":267,"heartbeat":5}
[2026-03-17 13:51:19] [save_learning] request {"content_id":"20260316-125","watch_tm":272,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:19] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":272,"heartbeat":5}
[2026-03-17 13:51:24] [save_learning] request {"content_id":"20260316-125","watch_tm":277,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:24] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":277,"heartbeat":5}
[2026-03-17 13:51:29] [save_learning] request {"content_id":"20260316-125","watch_tm":282,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:29] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":282,"heartbeat":5}
[2026-03-17 13:51:34] [save_learning] request {"content_id":"20260316-125","watch_tm":287,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:34] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":287,"heartbeat":5}
[2026-03-17 13:51:39] [save_learning] request {"content_id":"20260316-125","watch_tm":292,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:39] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":292,"heartbeat":5}
[2026-03-17 13:51:44] [save_learning] request {"content_id":"20260316-125","watch_tm":297,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:44] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":297,"heartbeat":5}
[2026-03-17 13:51:49] [save_learning] request {"content_id":"20260316-125","watch_tm":302,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:49] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":302,"heartbeat":5}
[2026-03-17 13:51:54] [save_learning] request {"content_id":"20260316-125","watch_tm":307,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:54] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":307,"heartbeat":5}
[2026-03-17 13:51:59] [save_learning] request {"content_id":"20260316-125","watch_tm":312,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:51:59] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":312,"heartbeat":5}
[2026-03-17 13:52:01] [save_learning] request {"content_id":"20260316-125","watch_tm":314,"content_tm":1091,"heartbeat":2,"member_id":"U001"}
[2026-03-17 13:52:01] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":314,"heartbeat":2}
[2026-03-17 13:52:14] [save_learning] request {"content_id":"20260316-125","watch_tm":5,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 13:52:14] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":5,"heartbeat":5}
[2026-03-17 13:52:16] [save_learning] request {"content_id":"20260316-125","watch_tm":7,"content_tm":1090,"heartbeat":2,"member_id":"U001"}
[2026-03-17 13:52:16] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":7,"heartbeat":2}
[2026-03-17 14:22:26] [save_learning] request {"content_id":"20260316-086","watch_tm":3,"content_tm":1090,"heartbeat":3,"member_id":"U001"}
[2026-03-17 14:22:26] [save_learning] UPDATE done {"content_id":"20260316-086","watch_tm":3,"heartbeat":3}
[2026-03-17 14:25:58] [save_learning] request {"content_id":"20260316-112","watch_tm":2,"content_tm":1090,"heartbeat":2,"member_id":"U001"}
[2026-03-17 14:25:58] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":2,"heartbeat":2}
[2026-03-17 14:26:00] [save_learning] request {"content_id":"20260316-112","watch_tm":5,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:00] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":5,"heartbeat":5}
[2026-03-17 14:26:05] [save_learning] request {"content_id":"20260316-112","watch_tm":10,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:05] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":10,"heartbeat":5}
[2026-03-17 14:26:10] [save_learning] request {"content_id":"20260316-112","watch_tm":15,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:10] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":15,"heartbeat":5}
[2026-03-17 14:26:15] [save_learning] request {"content_id":"20260316-112","watch_tm":20,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:15] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":20,"heartbeat":5}
[2026-03-17 14:26:20] [save_learning] request {"content_id":"20260316-112","watch_tm":25,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:20] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":25,"heartbeat":5}
[2026-03-17 14:26:25] [save_learning] request {"content_id":"20260316-112","watch_tm":30,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:25] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":30,"heartbeat":5}
[2026-03-17 14:26:30] [save_learning] request {"content_id":"20260316-112","watch_tm":35,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:30] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":35,"heartbeat":5}
[2026-03-17 14:26:35] [save_learning] request {"content_id":"20260316-112","watch_tm":40,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:35] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":40,"heartbeat":5}
[2026-03-17 14:26:40] [save_learning] request {"content_id":"20260316-112","watch_tm":45,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:40] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":45,"heartbeat":5}
[2026-03-17 14:26:45] [save_learning] request {"content_id":"20260316-112","watch_tm":50,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:45] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":50,"heartbeat":5}
[2026-03-17 14:26:50] [save_learning] request {"content_id":"20260316-112","watch_tm":55,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:50] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":55,"heartbeat":5}
[2026-03-17 14:26:55] [save_learning] request {"content_id":"20260316-112","watch_tm":60,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:26:55] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":60,"heartbeat":5}
[2026-03-17 14:26:57] [save_learning] request {"content_id":"20260316-112","watch_tm":62,"content_tm":1090,"heartbeat":2,"member_id":"U001"}
[2026-03-17 14:26:57] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":62,"heartbeat":2}
[2026-03-17 14:28:58] [save_learning] request {"content_id":"20260316-092","watch_tm":0,"content_tm":1090,"heartbeat":0,"member_id":"U001"}
[2026-03-17 14:28:58] [save_learning] INSERT done {"content_id":"20260316-092","watch_tm":0,"heartbeat":0}
[2026-03-17 14:39:40] [main_data] DB_ERROR {"error":"SQLSTATE[42000]: Syntax error or access violation: 1305 FUNCTION baronhomep.group_code does not exist in \/baronhomep\/www\/edu\/bbs\/leadership_init_data.php:67"}
[2026-03-17 14:58:45] [save_learning] request {"content_id":"20260316-112","watch_tm":5,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:58:45] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":5,"heartbeat":5}
[2026-03-17 14:58:45] [save_learning] request {"content_id":"20260316-112","watch_tm":5,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:58:45] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":5,"heartbeat":5}
[2026-03-17 14:58:50] [save_learning] request {"content_id":"20260316-112","watch_tm":10,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:58:50] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":10,"heartbeat":5}
[2026-03-17 14:58:50] [save_learning] request {"content_id":"20260316-112","watch_tm":10,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:58:50] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":10,"heartbeat":5}
[2026-03-17 14:58:55] [save_learning] request {"content_id":"20260316-112","watch_tm":15,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:58:55] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":15,"heartbeat":5}
[2026-03-17 14:58:55] [save_learning] request {"content_id":"20260316-112","watch_tm":15,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:58:55] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":15,"heartbeat":5}
[2026-03-17 14:59:00] [save_learning] request {"content_id":"20260316-112","watch_tm":20,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:59:00] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":20,"heartbeat":5}
[2026-03-17 14:59:00] [save_learning] request {"content_id":"20260316-112","watch_tm":20,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:59:00] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":20,"heartbeat":5}
[2026-03-17 14:59:05] [save_learning] request {"content_id":"20260316-112","watch_tm":25,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:59:05] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":25,"heartbeat":5}
[2026-03-17 14:59:05] [save_learning] request {"content_id":"20260316-112","watch_tm":25,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 14:59:05] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":25,"heartbeat":5}
[2026-03-17 14:59:07] [save_learning] request {"content_id":"20260316-112","watch_tm":26,"content_tm":1090,"heartbeat":1,"member_id":"U001"}
[2026-03-17 14:59:07] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":26,"heartbeat":1}
[2026-03-17 14:59:08] [save_learning] request {"content_id":"20260316-112","watch_tm":28,"content_tm":1090,"heartbeat":3,"member_id":"U001"}
[2026-03-17 14:59:08] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":28,"heartbeat":3}
[2026-03-17 15:03:28] [save_learning] request {"content_id":"20260316-112","watch_tm":5,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 15:03:28] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":5,"heartbeat":5}
[2026-03-17 15:03:28] [save_learning] request {"content_id":"20260316-112","watch_tm":5,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 15:03:28] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":5,"heartbeat":5}
[2026-03-17 15:03:31] [save_learning] request {"content_id":"20260316-112","watch_tm":7,"content_tm":1090,"heartbeat":2,"member_id":"U001"}
[2026-03-17 15:03:31] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":7,"heartbeat":2}
[2026-03-17 15:03:33] [save_learning] request {"content_id":"20260316-112","watch_tm":10,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 15:03:33] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":10,"heartbeat":5}
[2026-03-17 15:03:33] [save_learning] request {"content_id":"20260316-112","watch_tm":10,"content_tm":1090,"heartbeat":0,"member_id":"U001"}
[2026-03-17 15:03:33] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":10,"heartbeat":0}
[2026-03-17 15:07:00] [save_learning] request {"content_id":"20260316-125","watch_tm":8,"content_tm":1090,"heartbeat":1,"member_id":"U001"}
[2026-03-17 15:07:00] [save_learning] UPDATE done {"content_id":"20260316-125","watch_tm":8,"heartbeat":1}
[2026-03-17 15:15:16] [save_learning] request {"content_id":"20260316-112","watch_tm":5,"content_tm":1090,"heartbeat":5,"member_id":"U001"}
[2026-03-17 15:15:16] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":5,"heartbeat":5}
[2026-03-17 15:15:16] [save_learning] request {"content_id":"20260316-112","watch_tm":5,"content_tm":1090,"heartbeat":0,"member_id":"U001"}
[2026-03-17 15:15:16] [save_learning] UPDATE done {"content_id":"20260316-112","watch_tm":5,"heartbeat":0}
[2026-03-17 15:54:05] [save_learning] request {"content_id":"20260316-105","watch_tm":4,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 15:54:05] [save_learning] INSERT done {"content_id":"20260316-105","watch_tm":4,"heartbeat":5}
[2026-03-17 15:54:10] [save_learning] request {"content_id":"20260316-105","watch_tm":9,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 15:54:10] [save_learning] UPDATE done {"content_id":"20260316-105","watch_tm":9,"heartbeat":5}
[2026-03-17 15:54:15] [save_learning] request {"content_id":"20260316-105","watch_tm":14,"content_tm":1091,"heartbeat":5,"member_id":"U001"}
[2026-03-17 15:54:15] [save_learning] UPDATE done {"content_id":"20260316-105","watch_tm":14,"heartbeat":5}
[2026-03-17 15:54:19] [save_learning] request {"content_id":"20260316-105","watch_tm":18,"content_tm":1091,"heartbeat":3,"member_id":"U001"}
[2026-03-17 15:54:19] [save_learning] UPDATE done {"content_id":"20260316-105","watch_tm":18,"heartbeat":3}
[2026-03-18 16:19:55] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-18 17:36:19] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-18 17:46:08] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-18 17:46:27] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-18 17:53:10] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-18 20:38:25] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-18 20:38:33] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 09:02:16] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 09:33:11] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 09:33:27] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 09:33:31] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 09:33:40] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 09:47:46] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 09:54:02] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 10:04:44] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 10:06:25] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 10:07:47] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 10:13:33] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 10:17:37] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 10:27:28] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-19 10:31:30] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-21 17:51:19] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-21 17:51:57] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-21 17:51:59] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-21 17:56:06] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-21 17:56:12] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-21 17:56:14] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:157"}
[2026-03-21 18:01:11] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:149"}
[2026-03-21 18:01:47] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:149"}
[2026-03-21 18:02:16] [main_data] DB_ERROR {"error":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'rk.sys_comp_code' in 'where clause' in \/baronhomep\/www\/edu\/bbs\/main_data.php:149"}
+251
View File
@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
// auth.php and session are already initialized by skin/index.php before this file is included.
$userName = edu_user_field('name', (string) ($_SESSION['member_name'] ?? ''));
$userRank = (string) ($_SESSION['rank_name'] ?? edu_user_field('rank_name', ''));
$memberId = edu_current_member_id();
$sysCompCode = (string) ($_SESSION['sys_comp_code'] ?? '');
require_once __DIR__ . '/db_conn.php';
// 세션에 누락된 정보 DB에서 보완
if (($userRank === '' || $sysCompCode === '') && $memberId !== '') {
try {
if ($sysCompCode !== '') {
$__st = db_conn()->prepare(
'SELECT name, rank_name, sys_comp_code FROM edu_users WHERE member_id = ? AND sys_comp_code = ? LIMIT 1'
);
$__st->execute([$memberId, $sysCompCode]);
} else {
$__st = db_conn()->prepare(
'SELECT name, rank_name, sys_comp_code FROM edu_users WHERE member_id = ? ORDER BY sys_comp_code LIMIT 1'
);
$__st->execute([$memberId]);
}
$__row = $__st->fetch(PDO::FETCH_ASSOC);
if ($__row) {
if ($userName === '')
$userName = (string) ($__row['name'] ?? '');
if ($userRank === '')
$userRank = (string) ($__row['rank_name'] ?? '');
if ($sysCompCode === '')
$sysCompCode = (string) ($__row['sys_comp_code'] ?? '');
}
} catch (Throwable $__e) {
error_log('[main_data rank fallback] ' . $__e->getMessage());
}
}
$userRank = trim($userRank);
// ── 키워드 & 영상 설정 ─────────────────────────────────────────
$allKeywords = []; // 전체 키워드 목록 (모달 선택용, 21개)
$adminKeywords = []; // 회사 추천 키워드 (kw-deny)
$myKeywords = []; // 사용자 선택 키워드 0~3개 (kw-allow)
$videosJson = '[]';
$myKwJson = '[]'; // JS fallback용
$totalMin = 0;
$avgWatchMin = 0; // 전체 평균 학습시간 (분)
$mainDataError = ''; // DEBUG용 오류 메시지
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// edu_user_keywords 테이블 자동 생성 (권한 없으면 무시)
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS edu_user_keywords (
member_id VARCHAR(20) NOT NULL,
sys_comp_code VARCHAR(20) NOT NULL DEFAULT '',
keyword_code VARCHAR(20) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
PRIMARY KEY (member_id, sys_comp_code, keyword_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
} catch (Throwable $__ce) {
error_log('[main_data] CREATE edu_user_keywords: ' . $__ce->getMessage());
}
// 전체 키워드 (모달 선택용) — sort_order 없는 테이블도 안전하게 base_code만으로 정렬
$stmtKw = $pdo->query("SELECT base_code AS keyword_code, code_name AS keyword_name FROM edu_codes WHERE group_code = 'KW100' ORDER BY base_code");
$allKeywords = $stmtKw->fetchAll(PDO::FETCH_ASSOC);
// 회사 추천 키워드: 반드시 edu_recommend_keywords의 코드값을 edu_codes와 한글 매핑해서 사용
try {
$stmtAdm = $pdo->prepare("
SELECT rk.keyword_code, ec.code_name AS keyword_name
FROM edu_recommend_keywords rk
JOIN edu_codes ec ON ec.base_code = rk.keyword_code
WHERE rk.is_active = 1
AND rk.keyword_code IS NOT NULL
AND rk.keyword_code <> ''
AND ec.group_code = 'KW100'
ORDER BY rk.keyword_code
LIMIT 2
");
$stmtAdm->execute();
$adminKeywords = $stmtAdm->fetchAll(PDO::FETCH_ASSOC);
error_log('[main_data] edu_recommend_keywords result: ' . count($adminKeywords) . ' rows');
} catch (Throwable $__ae) {
error_log('[main_data] edu_recommend_keywords 쿼리 실패: ' . $__ae->getMessage());
$adminKeywords = [];
}
// 사용자 선택 키워드 (0~3개)
// ⚠️ sys_comp_code 없어도 저장되도록 조건 완화 (저장 후 바로 조회 위함)
// sys_comp_code 조건 추가 : 권오재
if ($memberId !== '') {
$stmtMy = $pdo->prepare("
SELECT uk.keyword_code, ec.code_name AS keyword_name
FROM edu_user_keywords uk
JOIN edu_codes ec ON ec.base_code = uk.keyword_code
WHERE uk.member_id = ?
AND uk.sys_comp_code = ?
AND uk.sys_comp_code <> ''
ORDER BY uk.keyword_code
LIMIT 3
");
$stmtMy->execute([$memberId, $sysCompCode]);
$myKeywords = $stmtMy->fetchAll(PDO::FETCH_ASSOC);
error_log('[main_data] myKeywords query: memberId=' . $memberId . ', result_count=' . count($myKeywords));
}
// JS fallback용: 사용자 키워드명 배열 (JSON)
$myKwJson = json_encode(
array_column($myKeywords, 'keyword_name'),
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG
);
// 누적 시청 시간 (게이지): watch_tm 기준
if ($memberId !== '' && $sysCompCode !== '') {
$stmtTm = $pdo->prepare(
'SELECT COALESCE(SUM(watch_tm), 0) FROM edu_learning_histories WHERE member_id = ? AND sys_comp_code = ?'
);
$stmtTm->execute([$memberId, $sysCompCode]);
$totalMin = (int) floor((int) $stmtTm->fetchColumn() / 60);
}
// 전체 평균 학습시간 계산: (모든 member_id의 총 watch_tm 합계) / (학습경험이 있는 사용자 수)
try {
$stmtAvg = $pdo->prepare("
SELECT
COALESCE(SUM(h.watch_tm), 0) as total_watch_tm,
COUNT(DISTINCT h.member_id) as unique_members
FROM edu_learning_histories h
WHERE h.sys_comp_code = ?
");
$stmtAvg->execute([$sysCompCode]);
$avgRow = $stmtAvg->fetch(PDO::FETCH_ASSOC);
if ($avgRow) {
$totalWatchTm = (int) ($avgRow['total_watch_tm'] ?? 0);
$uniqueMembers = (int) ($avgRow['unique_members'] ?? 0);
if ($uniqueMembers > 0) {
$avgWatchMin = (int) floor($totalWatchTm / $uniqueMembers / 60);
// 최소 50분으로 설정해서 게이지의 maxValue 보장
$avgWatchMin = max(50, $avgWatchMin);
error_log('[main_data] avgWatchMin calc: totalWatchTm=' . $totalWatchTm . ', uniqueMembers=' . $uniqueMembers . ', avgWatchMin=' . $avgWatchMin);
}
}
} catch (Throwable $__ae) {
error_log('[main_data] avgWatchMin 계산 실패: ' . $__ae->getMessage());
}
// ── Pick 영상(추천) + 키워드 영상 5개 조합: 항상 6개 보장 ──
$videos = [];
// 1. Pick(추천) 영상: is_offer=1, 본인 제외, 랜덤 1개
$stmtPick = $pdo->prepare("SELECT * FROM edu_contents WHERE is_offer=1 AND is_active=1 AND issue_type_code = 'IS10003' AND member_id != ? ORDER BY RAND() LIMIT 1");
$stmtPick->execute([$memberId]);
$pick = $stmtPick->fetch(PDO::FETCH_ASSOC);
if ($pick) {
$pick['is_pick'] = true;
$videos[] = $pick;
}
// 2. 나머지 5개: 실제 사용자 키워드 기반 영상 쿼리로 채움
$keywordVideos = [];
$userKeywordCodes = array_column($myKeywords, 'keyword_code');
if (!empty($userKeywordCodes)) {
$ph = implode(',', array_fill(0, count($userKeywordCodes), '?'));
$sql = "SELECT * FROM edu_contents c
JOIN edu_content_keywords k ON c.content_id = k.content_id AND k.is_active = 1
WHERE c.is_active=1 AND k.keyword_code IN ($ph)";
$params = $userKeywordCodes;
// pick과 중복 제거
if ($pick && isset($pick['content_id'])) {
$sql .= " AND c.content_id != ?";
$params[] = $pick['content_id'];
}
$sql .= " GROUP BY c.content_id ORDER BY c.content_id DESC LIMIT 5";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$row['is_pick'] = false;
$keywordVideos[] = $row;
}
}
// 부족하면 전체 영상에서 추가로 채움
if (count($keywordVideos) < 5) {
$already = array_column($keywordVideos, 'content_id');
if ($pick && isset($pick['content_id']))
$already[] = $pick['content_id'];
$ph = implode(',', array_fill(0, count($already), '?'));
$sql = "SELECT * FROM edu_contents WHERE is_active=1";
if (!empty($already))
$sql .= " AND content_id NOT IN ($ph)";
$sql .= " ORDER BY content_id DESC LIMIT " . (5 - count($keywordVideos));
$stmt = $pdo->prepare($sql);
$stmt->execute($already);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$row['is_pick'] = false;
$keywordVideos[] = $row;
}
}
// 최대 5개만
$keywordVideos = array_slice($keywordVideos, 0, 5);
foreach ($keywordVideos as $v) {
$videos[] = $v;
}
// 3. 부족하면 빈 카드로 채움
while (count($videos) < 6) {
$videos[] = [
'content_id' => 'empty_' . count($videos),
'title' => '',
'is_pick' => false,
'img' => '',
'category' => '',
'author' => '',
'keywords' => [],
];
}
// 카테고리 코드→한글명 매핑 (edu_codes)
$codeMap = [];
$normCode = static function (string $code): string {
return preg_replace('/[^A-Z0-9]/', '', strtoupper(trim($code)));
};
try {
$stmtCm = $pdo->query("SELECT base_code, code_name FROM edu_codes");
foreach ($stmtCm->fetchAll(PDO::FETCH_ASSOC) as $cr) {
$baseCode = $normCode((string) ($cr['base_code'] ?? ''));
if ($baseCode === '')
continue;
$codeMap[$baseCode] = $cr['code_name'];
}
} catch (Throwable $e) { /* ignore */
}
foreach ($videos as &$v) {
if (!is_array($v) || !isset($v['content_id']) || strpos((string) ($v['content_id'] ?? ''), 'empty_') === 0)
continue;
$categoryCode = $normCode((string) ($v['category_code'] ?? ''));
$groupCode = $normCode((string) ($v['category_group'] ?? ''));
$v['category'] = $codeMap[$categoryCode] ?? ($v['category_code'] ?? '');
$v['subcate'] = $codeMap[$groupCode] ?? ($v['category_group'] ?? '');
}
unset($v);
// 4. JSON 직렬화
$videosJson = json_encode($videos, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG);
} catch (Throwable $e) {
$mainDataError = $e->getMessage();
error_log('[main_data] ' . $e->getMessage());
}
+419
View File
@@ -0,0 +1,419 @@
<?php
/**
* bbs/main_data.php
* ─────────────────────────────────────────────────────────────────
* 메인 페이지(skin/index.php) 용 데이터 조회 백엔드
*
* 이 파일은 직접 호출하지 않고 skin/index.php 에서 require_once 로 사용한다.
* 실행 후 아래 변수가 설정된다:
*
* $userName string 사용자 이름
* $userRank string 직위
* $myKeywords array 내 키워드 [ ['keyword_code'=>..., 'keyword_name'=>...], ... ] (max 3)
* $adminKeywords array 관리자 추천 키워드 (max 2)
* $allKeywords array 모달용 전체 키워드 목록
* $firstPageVideos array 메인 6개 영상 배열 (JS video 객체 형태)
* $totalMin int 총 학습시간(분)
* $videosJson string JSON 직렬화 영상 배열
* $myKwJson string JSON 직렬화 내 키워드명 배열
* ─────────────────────────────────────────────────────────────────
*/
require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/api_log.php';
// ════════════════════════════════════════════════════════════════
// 현재 사용자 식별
// TODO: 실제 세션 로그인 구현 후 아래 두 줄을 세션으로 교체
// $memberId = $_SESSION['member_id'] ?? null;
// $sysCompCode = $_SESSION['sys_comp_code'] ?? null;
// ════════════════════════════════════════════════════════════════
$memberId = 'U001';
$sysCompCode = 'COMP01';
// ────────────────────────────────────────────────────────────────
// 카테고리/서브카테고리 코드 → 한글 변환 맵 (DB 조회 전 fallback 기본값)
// DB에서 edu_codes 조회 후 아래 값은 덮어쓰여짐
// ────────────────────────────────────────────────────────────────
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$SUBCATE_MAP = [];
/**
* contents 테이블 한 행을 JS video 객체 형태 배열로 변환
*
* @param array $row PDO fetchAll 행 데이터
* @param string $type 카드 타입 (main / comment / onboarding 등)
* @param string $picker Pick 배지에 표시할 이름 (없으면 '')
* @return array
*/
function mapContentRow(array $row, string $type = 'main', string $picker = '', array $catMap = [], array $subcateMap = []): array
{
// GROUP_CONCAT 결과 키워드 문자열 → 배열
$kwStr = $row['keywords'] ?? '';
$keywords = $kwStr !== '' ? explode(',', $kwStr) : [];
// 학습 진행률 (watch_tm / content_tm × 100, 0~100 클램프)
$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));
// content_url: 풀URL(https://www.youtube.com/watch?v=ID) 또는 영상ID만 저장된 경우 모두 지원
$raw = trim($row['content_url'] ?? '');
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1]; // 풀 URL에서 ID 추출
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw; // 정확히 11자리 유효한 YouTube ID
} else {
$videoId = ''; // 유효하지 않은 값 (example02, 빈값 등)
}
// thumbnail_url 컬럼이 있으면 우선 사용, 없으면 YouTube API로 생성
$thumbFromDb = trim($row['thumbnail_url'] ?? '');
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : '';
$thumbnail = $thumbFromDb !== '' ? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
// 카테고리 코드 → 한글
$categoryCode = $row['category_code'] ?? '';
$category = $catMap[$categoryCode] ?? $categoryCode;
// 서브카테고리 코드 → 한글
$groupCode = $row['category_group'] ?? '';
$subcate = $subcateMap[$groupCode] ?? $groupCode;
return [
'id' => $row['content_id'],
'url' => $url,
'thumbnail' => $thumbnail,
'category' => $category,
'category_code' => $categoryCode,
'subcate' => $subcate,
'bookmark' => (bool)($row['is_bookmarked'] ?? false),
'title' => $row['title'] ?? '',
'picker' => $picker,
'type' => $type,
'keywords' => $keywords,
'gauge' => $gauge,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
];
}
// ════════════════════════════════════════════════════════════════
// DB 데이터 조회
// ════════════════════════════════════════════════════════════════
$dbSuccess = false;
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ── 0. edu_codes에서 카테고리/서브카테고리 맵 동적 빌드 ──────
$codesRows = $pdo->query("
SELECT group_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND group_code IN ('CA100', 'CA200')
")->fetchAll();
foreach ($codesRows as $cr) {
$key = $cr['group_code'] . $cr['code']; // e.g. CA10001, CA200L01
if ($cr['group_code'] === 'CA100') {
$CATEGORY_MAP[$key] = $cr['code_name'];
} else {
$SUBCATE_MAP[$key] = $cr['code_name'];
}
}
// ── 1. 사용자 기본 정보 ─────────────────────────────────────
$stmtUser = $pdo->prepare("
SELECT name, rank_name
FROM edu_users
WHERE member_id = :mid AND sys_comp_code = :comp
LIMIT 1
");
$stmtUser->execute([':mid' => $memberId, ':comp' => $sysCompCode]);
$userRow = $stmtUser->fetch();
$userName = $userRow['name'] ?? '사용자';
$userRank = $userRow['rank_name'] ?? '';
// ── 2. 내 키워드 (최대 3개) ──────────────────────────────────
$stmtMyKw = $pdo->prepare("
SELECT keyword_code, keyword_name
FROM edu_user_keywords
WHERE member_id = :mid AND sys_comp_code = :comp
ORDER BY created_at DESC
LIMIT 3
");
$stmtMyKw->execute([':mid' => $memberId, ':comp' => $sysCompCode]);
$myKeywords = $stmtMyKw->fetchAll();
// ── 3. 관리자(법인) 추천 키워드 (최대 2개) ──────────────────
$stmtAdminKw = $pdo->prepare("
SELECT rk.keyword_code,
COALESCE(c.code_name, rk.keyword_code) AS keyword_name
FROM edu_recommend_keywords rk
LEFT JOIN edu_codes c
ON c.group_code = 'KW100'
AND c.code = rk.keyword_code
WHERE rk.sys_comp_code = :comp
AND rk.is_active = 1
ORDER BY rk.created_at DESC
LIMIT 2
");
$stmtAdminKw->execute([':comp' => $sysCompCode]);
$adminKeywords = $stmtAdminKw->fetchAll();
// ── 4. 모달용 전체 키워드 목록 ──────────────────────────────
$stmtAllKw = $pdo->query("
SELECT code AS keyword_code, code_name AS keyword_name
FROM edu_codes
WHERE group_code = 'KW100' AND is_active = 1
ORDER BY code_name
");
$allKeywords = $stmtAllKw ? $stmtAllKw->fetchAll() : [];
// DB에 코드 데이터 없으면 하드코딩 fallback
if (empty($allKeywords)) {
$fallbackKwNames = [
'온보딩','성장','코칭','인물','소통','협업','AI','IT테크',
'중간관리자','리더십','팔로우십','동기부여','인간관계','스킬업',
'피드백','커리어','경영','경제','마인드셋','웰니스','자기개발',
];
$allKeywords = array_map(
fn($k) => ['keyword_code' => $k, 'keyword_name' => $k],
$fallbackKwNames
);
}
// ── 5. 영상 슬롯 조합 ────────────────────────────────────────
// [0] Pick (좌상단 고정 - 다른 사람이 추천한 영상)
// [1] 관리자 키워드 영상 (좌중단)
// [2] 관리자 키워드 영상 (좌하단)
// [3] 내 키워드 영상 (우상단)
// [4] 내 키워드 영상 (우중단)
// [5] 내 키워드 영상 (우하단)
$myKwCodes = array_column($myKeywords, 'keyword_code');
$adminKwCodes = array_column($adminKeywords, 'keyword_code');
// 5-1. Pick 영상 (다른 사람이 제안, is_offer=1, 본인 제외)
$stmtPick = $pdo->prepare("
SELECT c.*,
NULL AS watch_tm,
NULL AS content_tm,
0 AS is_bookmarked,
GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords,
u.name AS picker_name
FROM edu_contents c
LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id
LEFT JOIN edu_content_offer co ON co.offer_id = c.offer_id
LEFT JOIN edu_users u ON u.member_id = co.member_id
AND u.sys_comp_code = co.sys_comp_code
WHERE c.is_offer = 1
AND c.is_active = 1
AND (co.member_id IS NULL OR co.member_id != :mid)
GROUP BY c.content_id
ORDER BY c.sort_order, RAND()
LIMIT 1
");
$stmtPick->execute([':mid' => $memberId]);
$pickRow = $stmtPick->fetch();
$pickVideo = $pickRow
? mapContentRow($pickRow, 'main', $pickRow['picker_name'] ?? '동료', $CATEGORY_MAP, $SUBCATE_MAP)
: null;
// 5-2. 관리자 키워드 관련 영상 (2개)
$adminVideos = [];
if (!empty($adminKwCodes)) {
$ph = implode(',', array_fill(0, count($adminKwCodes), '?'));
$stmtAdminV = $pdo->prepare("
SELECT c.*,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_contents c
JOIN edu_content_keywords ck ON ck.content_id = c.content_id
AND ck.keyword_code IN ({$ph})
LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_wishlist cw ON cw.content_id = c.content_id
AND cw.member_id = ?
AND cw.sys_comp_code = ?
AND cw.is_active = 1
WHERE c.is_active = 1
GROUP BY c.content_id
ORDER BY RAND()
LIMIT 2
");
$stmtAdminV->execute(array_merge(
$adminKwCodes,
[$memberId, $sysCompCode, $memberId, $sysCompCode]
));
foreach ($stmtAdminV->fetchAll() as $r) {
$adminVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP);
}
}
// 5-3. 내 키워드 관련 영상 (3개)
$myVideos = [];
if (!empty($myKwCodes)) {
$ph2 = implode(',', array_fill(0, count($myKwCodes), '?'));
$stmtMyV = $pdo->prepare("
SELECT c.*,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_contents c
JOIN edu_content_keywords ck ON ck.content_id = c.content_id
AND ck.keyword_code IN ({$ph2})
LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_wishlist cw ON cw.content_id = c.content_id
AND cw.member_id = ?
AND cw.sys_comp_code = ?
AND cw.is_active = 1
WHERE c.is_active = 1
GROUP BY c.content_id
ORDER BY RAND()
LIMIT 3
");
$stmtMyV->execute(array_merge(
$myKwCodes,
[$memberId, $sysCompCode, $memberId, $sysCompCode]
));
foreach ($stmtMyV->fetchAll() as $r) {
$myVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP);
}
}
// 5-4. 6 슬롯 최종 조합
// ① 키워드 매칭으로 채워진 영상 목록
$slots = [
$pickVideo,
$adminVideos[0] ?? null,
$adminVideos[1] ?? null,
$myVideos[0] ?? null,
$myVideos[1] ?? null,
$myVideos[2] ?? null,
];
$usedIds = array_values(array_filter(array_map(fn($s) => $s ? $s['id'] : null, $slots)));
$slotsLeft = 6 - count(array_filter($slots, fn($s) => $s !== null));
// ② 부족한 슬롯 → edu_contents 에서 직접 조회 (키워드·is_offer 무관, 랜덤)
// 키워드를 선택하지 않아도 항상 DB 영상이 표시되도록 함
$extraVideos = [];
if ($slotsLeft > 0) {
$excClause = !empty($usedIds)
? 'AND c.content_id NOT IN (' . implode(',', array_fill(0, count($usedIds), '?')) . ')'
: '';
$stmtExtra = $pdo->prepare("
SELECT c.*,
lh.watch_tm,
lh.content_tm,
0 AS is_bookmarked,
GROUP_CONCAT(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 = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id
WHERE c.is_active = 1
{$excClause}
GROUP BY c.content_id
ORDER BY c.sort_order, RAND()
LIMIT {$slotsLeft}
");
$bindParams = [$memberId, $sysCompCode];
if (!empty($usedIds)) {
$bindParams = array_merge($bindParams, $usedIds);
}
$stmtExtra->execute($bindParams);
foreach ($stmtExtra->fetchAll() as $r) {
$extraVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP);
}
}
// ③ 슬롯 순서대로 채우기: 키워드 영상 → DB 보충 → FALLBACK (최후 수단)
$firstPageVideos = [];
$extraIdx = 0;
$fbIdx = 0;
foreach ($slots as $slot) {
if ($slot !== null) {
$firstPageVideos[] = $slot;
} elseif (isset($extraVideos[$extraIdx])) {
$firstPageVideos[] = $extraVideos[$extraIdx++];
}
// DB에 영상이 없으면 해당 슬롯 생략 (하드코딩 fallback 없음)
}
// ── 6. 총 학습시간(게이지용) ─────────────────────────────────
$stmtTotal = $pdo->prepare("
SELECT COALESCE(SUM(all_tm), 0)
FROM edu_learning_histories
WHERE member_id = :mid AND sys_comp_code = :comp
");
$stmtTotal->execute([':mid' => $memberId, ':comp' => $sysCompCode]);
$totalMin = (int)$stmtTotal->fetchColumn();
$dbSuccess = true;
} catch (Exception $e) {
// DB 오류 발생 시 fallback 기본값 사용
$errMsg = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
error_log('[bbs/main_data.php] DB Error: ' . $errMsg);
api_log('main_data', 'DB_ERROR', ['error' => $errMsg]);
$userName = '사용자';
$userRank = '';
$myKeywords = [
['keyword_code' => 'KW004', 'keyword_name' => '인물'],
['keyword_code' => 'KW005', 'keyword_name' => '소통'],
['keyword_code' => 'KW006', 'keyword_name' => '협업'],
];
$adminKeywords = [
['keyword_code' => 'KW019', 'keyword_name' => '마인드셋'],
['keyword_code' => 'KW020', 'keyword_name' => '웰니스'],
];
$allKeywords = array_map(
fn($k) => ['keyword_code' => $k, 'keyword_name' => $k],
['온보딩','성장','코칭','인물','소통','협업','AI','IT테크',
'중간관리자','리더십','팔로우십','동기부여','인간관계','스킬업',
'피드백','커리어','경영','경제','마인드셋','웰니스','자기개발']
);
$firstPageVideos = [];
$totalMin = 0;
}
// ── DB 실패 시에만 기본값 보장 (DB 성공 시 빈 키워드는 그대로 유지) ──
if (!$dbSuccess) {
$myKeywords = [
['keyword_code' => 'KW004', 'keyword_name' => '인물'],
['keyword_code' => 'KW005', 'keyword_name' => '소통'],
['keyword_code' => 'KW006', 'keyword_name' => '협업'],
];
$adminKeywords = [
['keyword_code' => 'KW019', 'keyword_name' => '마인드셋'],
['keyword_code' => 'KW020', 'keyword_name' => '웰니스'],
];
}
// ════════════════════════════════════════════════════════════════
// 뷰(skin)에서 사용할 JSON 직렬화 변수
// ════════════════════════════════════════════════════════════════
$videosJson = json_encode(
$firstPageVideos,
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP
);
$myKwJson = json_encode(
array_column($myKeywords, 'keyword_name'),
JSON_UNESCAPED_UNICODE
);
+419
View File
@@ -0,0 +1,419 @@
<?php
/**
* bbs/main_data.php
* ─────────────────────────────────────────────────────────────────
* 메인 페이지(skin/index.php) 용 데이터 조회 백엔드
*
* 이 파일은 직접 호출하지 않고 skin/index.php 에서 require_once 로 사용한다.
* 실행 후 아래 변수가 설정된다:
*
* $userName string 사용자 이름
* $userRank string 직위
* $myKeywords array 내 키워드 [ ['keyword_code'=>..., 'keyword_name'=>...], ... ] (max 3)
* $adminKeywords array 관리자 추천 키워드 (max 2)
* $allKeywords array 모달용 전체 키워드 목록
* $firstPageVideos array 메인 6개 영상 배열 (JS video 객체 형태)
* $totalMin int 총 학습시간(분)
* $videosJson string JSON 직렬화 영상 배열
* $myKwJson string JSON 직렬화 내 키워드명 배열
* ─────────────────────────────────────────────────────────────────
*/
require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/api_log.php';
// ════════════════════════════════════════════════════════════════
// 현재 사용자 식별
// TODO: 실제 세션 로그인 구현 후 아래 두 줄을 세션으로 교체
// $memberId = $_SESSION['member_id'] ?? null;
// $sysCompCode = $_SESSION['sys_comp_code'] ?? null;
// ════════════════════════════════════════════════════════════════
$memberId = 'U001';
$sysCompCode = 'COMP01';
// ────────────────────────────────────────────────────────────────
// 카테고리/서브카테고리 코드 → 한글 변환 맵 (DB 조회 전 fallback 기본값)
// DB에서 edu_codes 조회 후 아래 값은 덮어쓰여짐
// ────────────────────────────────────────────────────────────────
$CATEGORY_MAP = [
'CA10001' => '마이클래스',
'CA10002' => '온보딩',
'CA10003' => '법정교육',
'CA10004' => '리더십',
'CA10005' => '인사이트',
'CA10006' => '비즈트렌드',
];
$SUBCATE_MAP = [];
/**
* contents 테이블 한 행을 JS video 객체 형태 배열로 변환
*
* @param array $row PDO fetchAll 행 데이터
* @param string $type 카드 타입 (main / comment / onboarding 등)
* @param string $picker Pick 배지에 표시할 이름 (없으면 '')
* @return array
*/
function mapContentRow(array $row, string $type = 'main', string $picker = '', array $catMap = [], array $subcateMap = []): array
{
// GROUP_CONCAT 결과 키워드 문자열 → 배열
$kwStr = $row['keywords'] ?? '';
$keywords = $kwStr !== '' ? explode(',', $kwStr) : [];
// 학습 진행률 (watch_tm / content_tm × 100, 0~100 클램프)
$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));
// content_url: 풀URL(https://www.youtube.com/watch?v=ID) 또는 영상ID만 저장된 경우 모두 지원
$raw = trim($row['content_url'] ?? '');
if (preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $raw, $m)) {
$videoId = $m[1]; // 풀 URL에서 ID 추출
} elseif (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
$videoId = $raw; // 정확히 11자리 유효한 YouTube ID
} else {
$videoId = ''; // 유효하지 않은 값 (example02, 빈값 등)
}
// thumbnail_url 컬럼이 있으면 우선 사용, 없으면 YouTube API로 생성
$thumbFromDb = trim($row['thumbnail_url'] ?? '');
$url = $videoId !== '' ? "https://www.youtube.com/watch?v={$videoId}" : '';
$thumbnail = $thumbFromDb !== '' ? $thumbFromDb
: ($videoId !== '' ? "https://img.youtube.com/vi/{$videoId}/sddefault.jpg" : '');
// 카테고리 코드 → 한글
$categoryCode = $row['category_code'] ?? '';
$category = $catMap[$categoryCode] ?? $categoryCode;
// 서브카테고리 코드 → 한글
$groupCode = $row['category_group'] ?? '';
$subcate = $subcateMap[$groupCode] ?? $groupCode;
return [
'id' => $row['content_id'],
'url' => $url,
'thumbnail' => $thumbnail,
'category' => $category,
'category_code' => $categoryCode,
'subcate' => $subcate,
'bookmark' => (bool)($row['is_bookmarked'] ?? false),
'title' => $row['title'] ?? '',
'picker' => $picker,
'type' => $type,
'keywords' => $keywords,
'gauge' => $gauge,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
];
}
// ════════════════════════════════════════════════════════════════
// DB 데이터 조회
// ════════════════════════════════════════════════════════════════
$dbSuccess = false;
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ── 0. edu_codes에서 카테고리/서브카테고리 맵 동적 빌드 ──────
$codesRows = $pdo->query("
SELECT group_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND group_code IN ('CA100', 'CA200')
")->fetchAll();
foreach ($codesRows as $cr) {
$key = $cr['group_code'] . $cr['code']; // e.g. CA10001, CA200L01
if ($cr['group_code'] === 'CA100') {
$CATEGORY_MAP[$key] = $cr['code_name'];
} else {
$SUBCATE_MAP[$key] = $cr['code_name'];
}
}
// ── 1. 사용자 기본 정보 ─────────────────────────────────────
$stmtUser = $pdo->prepare("
SELECT name, rank_name
FROM edu_users
WHERE member_id = :mid AND sys_comp_code = :comp
LIMIT 1
");
$stmtUser->execute([':mid' => $memberId, ':comp' => $sysCompCode]);
$userRow = $stmtUser->fetch();
$userName = $userRow['name'] ?? '사용자';
$userRank = $userRow['rank_name'] ?? '';
// ── 2. 내 키워드 (최대 3개) ──────────────────────────────────
$stmtMyKw = $pdo->prepare("
SELECT keyword_code, keyword_name
FROM edu_user_keywords
WHERE member_id = :mid AND sys_comp_code = :comp
ORDER BY created_at DESC
LIMIT 3
");
$stmtMyKw->execute([':mid' => $memberId, ':comp' => $sysCompCode]);
$myKeywords = $stmtMyKw->fetchAll();
// ── 3. 관리자(법인) 추천 키워드 (최대 2개) ──────────────────
$stmtAdminKw = $pdo->prepare("
SELECT rk.keyword_code,
COALESCE(c.code_name, rk.keyword_code) AS keyword_name
FROM edu_recommend_keywords rk
LEFT JOIN edu_codes c
ON c.group_code = 'KW100'
AND c.code = rk.keyword_code
WHERE rk.sys_comp_code = :comp
AND rk.is_active = 1
ORDER BY rk.created_at DESC
LIMIT 2
");
$stmtAdminKw->execute([':comp' => $sysCompCode]);
$adminKeywords = $stmtAdminKw->fetchAll();
// ── 4. 모달용 전체 키워드 목록 ──────────────────────────────
$stmtAllKw = $pdo->query("
SELECT code AS keyword_code, code_name AS keyword_name
FROM edu_codes
WHERE group_code = 'KW100' AND is_active = 1
ORDER BY code_name
");
$allKeywords = $stmtAllKw ? $stmtAllKw->fetchAll() : [];
// DB에 코드 데이터 없으면 하드코딩 fallback
if (empty($allKeywords)) {
$fallbackKwNames = [
'온보딩','성장','코칭','인물','소통','협업','AI','IT테크',
'중간관리자','리더십','팔로우십','동기부여','인간관계','스킬업',
'피드백','커리어','경영','경제','마인드셋','웰니스','자기개발',
];
$allKeywords = array_map(
fn($k) => ['keyword_code' => $k, 'keyword_name' => $k],
$fallbackKwNames
);
}
// ── 5. 영상 슬롯 조합 ────────────────────────────────────────
// [0] Pick (좌상단 고정 - 다른 사람이 추천한 영상)
// [1] 관리자 키워드 영상 (좌중단)
// [2] 관리자 키워드 영상 (좌하단)
// [3] 내 키워드 영상 (우상단)
// [4] 내 키워드 영상 (우중단)
// [5] 내 키워드 영상 (우하단)
$myKwCodes = array_column($myKeywords, 'keyword_code');
$adminKwCodes = array_column($adminKeywords, 'keyword_code');
// 5-1. Pick 영상 (다른 사람이 제안, is_offer=1, 본인 제외)
$stmtPick = $pdo->prepare("
SELECT c.*,
NULL AS watch_tm,
NULL AS content_tm,
0 AS is_bookmarked,
GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords,
u.name AS picker_name
FROM edu_contents c
LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id
LEFT JOIN edu_content_offer co ON co.offer_id = c.offer_id
LEFT JOIN edu_users u ON u.member_id = co.member_id
AND u.sys_comp_code = co.sys_comp_code
WHERE c.is_offer = 1
AND c.is_active = 1
AND (co.member_id IS NULL OR co.member_id != :mid)
GROUP BY c.content_id
ORDER BY c.sort_order, RAND()
LIMIT 1
");
$stmtPick->execute([':mid' => $memberId]);
$pickRow = $stmtPick->fetch();
$pickVideo = $pickRow
? mapContentRow($pickRow, 'main', $pickRow['picker_name'] ?? '동료', $CATEGORY_MAP, $SUBCATE_MAP)
: null;
// 5-2. 관리자 키워드 관련 영상 (2개)
$adminVideos = [];
if (!empty($adminKwCodes)) {
$ph = implode(',', array_fill(0, count($adminKwCodes), '?'));
$stmtAdminV = $pdo->prepare("
SELECT c.*,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_contents c
JOIN edu_content_keywords ck ON ck.content_id = c.content_id
AND ck.keyword_code IN ({$ph})
LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_wishlist cw ON cw.content_id = c.content_id
AND cw.member_id = ?
AND cw.sys_comp_code = ?
AND cw.is_active = 1
WHERE c.is_active = 1
GROUP BY c.content_id
ORDER BY RAND()
LIMIT 2
");
$stmtAdminV->execute(array_merge(
$adminKwCodes,
[$memberId, $sysCompCode, $memberId, $sysCompCode]
));
foreach ($stmtAdminV->fetchAll() as $r) {
$adminVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP);
}
}
// 5-3. 내 키워드 관련 영상 (3개)
$myVideos = [];
if (!empty($myKwCodes)) {
$ph2 = implode(',', array_fill(0, count($myKwCodes), '?'));
$stmtMyV = $pdo->prepare("
SELECT c.*,
lh.watch_tm,
lh.content_tm,
CASE WHEN cw.content_id IS NOT NULL THEN 1 ELSE 0 END AS is_bookmarked,
GROUP_CONCAT(ck.keyword_code ORDER BY ck.keyword_code SEPARATOR ',') AS keywords
FROM edu_contents c
JOIN edu_content_keywords ck ON ck.content_id = c.content_id
AND ck.keyword_code IN ({$ph2})
LEFT JOIN edu_learning_histories lh ON lh.content_id = c.content_id
AND lh.member_id = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_wishlist cw ON cw.content_id = c.content_id
AND cw.member_id = ?
AND cw.sys_comp_code = ?
AND cw.is_active = 1
WHERE c.is_active = 1
GROUP BY c.content_id
ORDER BY RAND()
LIMIT 3
");
$stmtMyV->execute(array_merge(
$myKwCodes,
[$memberId, $sysCompCode, $memberId, $sysCompCode]
));
foreach ($stmtMyV->fetchAll() as $r) {
$myVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP);
}
}
// 5-4. 6 슬롯 최종 조합
// ① 키워드 매칭으로 채워진 영상 목록
$slots = [
$pickVideo,
$adminVideos[0] ?? null,
$adminVideos[1] ?? null,
$myVideos[0] ?? null,
$myVideos[1] ?? null,
$myVideos[2] ?? null,
];
$usedIds = array_values(array_filter(array_map(fn($s) => $s ? $s['id'] : null, $slots)));
$slotsLeft = 6 - count(array_filter($slots, fn($s) => $s !== null));
// ② 부족한 슬롯 → edu_contents 에서 직접 조회 (키워드·is_offer 무관, 랜덤)
// 키워드를 선택하지 않아도 항상 DB 영상이 표시되도록 함
$extraVideos = [];
if ($slotsLeft > 0) {
$excClause = !empty($usedIds)
? 'AND c.content_id NOT IN (' . implode(',', array_fill(0, count($usedIds), '?')) . ')'
: '';
$stmtExtra = $pdo->prepare("
SELECT c.*,
lh.watch_tm,
lh.content_tm,
0 AS is_bookmarked,
GROUP_CONCAT(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 = ?
AND lh.sys_comp_code = ?
LEFT JOIN edu_content_keywords ck ON ck.content_id = c.content_id
WHERE c.is_active = 1
{$excClause}
GROUP BY c.content_id
ORDER BY c.sort_order, RAND()
LIMIT {$slotsLeft}
");
$bindParams = [$memberId, $sysCompCode];
if (!empty($usedIds)) {
$bindParams = array_merge($bindParams, $usedIds);
}
$stmtExtra->execute($bindParams);
foreach ($stmtExtra->fetchAll() as $r) {
$extraVideos[] = mapContentRow($r, 'main', '', $CATEGORY_MAP, $SUBCATE_MAP);
}
}
// ③ 슬롯 순서대로 채우기: 키워드 영상 → DB 보충 → FALLBACK (최후 수단)
$firstPageVideos = [];
$extraIdx = 0;
$fbIdx = 0;
foreach ($slots as $slot) {
if ($slot !== null) {
$firstPageVideos[] = $slot;
} elseif (isset($extraVideos[$extraIdx])) {
$firstPageVideos[] = $extraVideos[$extraIdx++];
}
// DB에 영상이 없으면 해당 슬롯 생략 (하드코딩 fallback 없음)
}
// ── 6. 총 학습시간(게이지용) ─────────────────────────────────
$stmtTotal = $pdo->prepare("
SELECT COALESCE(SUM(all_tm), 0)
FROM edu_learning_histories
WHERE member_id = :mid AND sys_comp_code = :comp
");
$stmtTotal->execute([':mid' => $memberId, ':comp' => $sysCompCode]);
$totalMin = (int)$stmtTotal->fetchColumn();
$dbSuccess = true;
} catch (Exception $e) {
// DB 오류 발생 시 fallback 기본값 사용
$errMsg = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
error_log('[bbs/main_data.php] DB Error: ' . $errMsg);
api_log('main_data', 'DB_ERROR', ['error' => $errMsg]);
$userName = '사용자';
$userRank = '';
$myKeywords = [
['keyword_code' => 'KW004', 'keyword_name' => '인물'],
['keyword_code' => 'KW005', 'keyword_name' => '소통'],
['keyword_code' => 'KW006', 'keyword_name' => '협업'],
];
$adminKeywords = [
['keyword_code' => 'KW019', 'keyword_name' => '마인드셋'],
['keyword_code' => 'KW020', 'keyword_name' => '웰니스'],
];
$allKeywords = array_map(
fn($k) => ['keyword_code' => $k, 'keyword_name' => $k],
['온보딩','성장','코칭','인물','소통','협업','AI','IT테크',
'중간관리자','리더십','팔로우십','동기부여','인간관계','스킬업',
'피드백','커리어','경영','경제','마인드셋','웰니스','자기개발']
);
$firstPageVideos = [];
$totalMin = 0;
}
// ── DB 실패 시에만 기본값 보장 (DB 성공 시 빈 키워드는 그대로 유지) ──
if (!$dbSuccess) {
$myKeywords = [
['keyword_code' => 'KW004', 'keyword_name' => '인물'],
['keyword_code' => 'KW005', 'keyword_name' => '소통'],
['keyword_code' => 'KW006', 'keyword_name' => '협업'],
];
$adminKeywords = [
['keyword_code' => 'KW019', 'keyword_name' => '마인드셋'],
['keyword_code' => 'KW020', 'keyword_name' => '웰니스'],
];
}
// ════════════════════════════════════════════════════════════════
// 뷰(skin)에서 사용할 JSON 직렬화 변수
// ════════════════════════════════════════════════════════════════
$videosJson = json_encode(
$firstPageVideos,
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP
);
$myKwJson = json_encode(
array_column($myKeywords, 'keyword_name'),
JSON_UNESCAPED_UNICODE
);
+10
View File
@@ -0,0 +1,10 @@
<?php
$pdo = new PDO('mysql:host=baroncs.co.kr;port=3306;dbname=baronhomep;charset=utf8mb4','baronhomep','baron3840!!');
echo "=== edu_contents category_code 현황 ===\n";
$rows = $pdo->query("SELECT content_id, category_code, category_group, title FROM edu_contents ORDER BY content_id")->fetchAll(PDO::FETCH_ASSOC);
foreach($rows as $r) {
echo $r['content_id'].' | cat='.$r['category_code'].' | grp='.$r['category_group'].' | '.$r['title']."\n";
}
echo "\n=== edu_content_keywords 현황 (첫 20개) ===\n";
$rows2 = $pdo->query("SELECT content_id, keyword_code FROM edu_content_keywords ORDER BY content_id LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
foreach($rows2 as $r) echo $r['content_id'].' | '.$r['keyword_code']."\n";
+9
View File
@@ -0,0 +1,9 @@
<?php
$pdo = new PDO('mysql:host=baroncs.co.kr;port=3306;dbname=baronhomep;charset=utf8mb4','baronhomep','baron3840!!');
echo "=== content_url & thumbnail_url 현황 ===\n";
$rows = $pdo->query("SELECT content_id, content_url, thumbnail_url, is_active, is_offer FROM edu_contents ORDER BY content_id")->fetchAll(PDO::FETCH_ASSOC);
foreach($rows as $r) {
echo sprintf("%-20s | is_offer=%-2s | is_active=%-2s | url=%-40s | thumb=%s\n",
$r['content_id'], $r['is_offer'], $r['is_active'], $r['content_url'], $r['thumbnail_url']
);
}
+28
View File
@@ -0,0 +1,28 @@
<?php
try {
$pdo = new PDO('mysql:host=baroncs.co.kr;port=3306;dbname=baronhomep;charset=utf8mb4', 'baronhomep', 'baron3840!!', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
echo "=== edu_contents ===" . PHP_EOL;
$rows = $pdo->query('SELECT content_id, category_code, title, is_active, is_offer FROM edu_contents ORDER BY content_id')->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) echo implode(' | ', $r) . PHP_EOL;
echo PHP_EOL . "=== edu_codes (KW100) ===" . PHP_EOL;
$kws = $pdo->query("SELECT code, code_name FROM edu_codes WHERE group_code='KW100' ORDER BY code")->fetchAll(PDO::FETCH_ASSOC);
foreach ($kws as $r) echo implode(' | ', $r) . PHP_EOL;
echo PHP_EOL . "=== edu_content_keywords (현재) ===" . PHP_EOL;
$ck = $pdo->query('SELECT content_id, keyword_code FROM edu_content_keywords ORDER BY content_id, keyword_code')->fetchAll(PDO::FETCH_ASSOC);
echo count($ck) . ' rows' . PHP_EOL;
foreach ($ck as $r) echo implode(' | ', $r) . PHP_EOL;
echo PHP_EOL . "=== edu_user_keywords ===" . PHP_EOL;
$uk = $pdo->query('SELECT member_id, keyword_code, keyword_name FROM edu_user_keywords ORDER BY member_id')->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($uk, JSON_UNESCAPED_UNICODE) . PHP_EOL;
echo PHP_EOL . "=== edu_recommend_keywords ===" . PHP_EOL;
$rk = $pdo->query('SELECT sys_comp_code, keyword_code, is_active FROM edu_recommend_keywords')->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($rk, JSON_UNESCAPED_UNICODE) . PHP_EOL;
} catch (Exception $e) {
echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
}
@@ -0,0 +1,90 @@
<?php
/**
* 마이클래스 데이터 연결 진단
*/
header('Content-Type: text/html; charset=utf-8');
require_once __DIR__ . '/db_conn.php';
$pdo = db_conn();
echo '<h1>마이클래스 데이터 연결 진단</h1>';
echo '<style>
body { font-family: Arial; margin: 20px; }
table { border-collapse: collapse; margin: 20px 0; width: 100%; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
th { background: #f0f0f0; }
.ok { color: green; }
.err { color: red; }
.warn { color: orange; }
</style>';
// 1. edu_learning_goals 현황
echo '<h2>1. 학습목표 현황 (edu_learning_goals)</h2>';
$goals = $pdo->query("SELECT * FROM edu_learning_goals ORDER BY goal_code")->fetchAll();
echo '<table><tr><th>goal_code</th><th>title</th><th>quarter</th><th>is_active</th></tr>';
foreach ($goals as $g) {
echo "<tr><td>{$g['goal_code']}</td><td>{$g['title']}</td><td>{$g['quarter']}</td><td>{$g['is_active']}</td></tr>";
}
echo '</table>';
echo "<p>총 {$goals} 개 목표</p>";
// 2. edu_contents 샘플 (영상 리스트)
echo '<h2>2. 콘텐츠 현황 (edu_contents)</h2>';
$contents = $pdo->query("SELECT * FROM edu_contents LIMIT 20")->fetchAll();
echo '<table><tr><th>content_id</th><th>title</th><th>category_group</th><th>content_url</th></tr>';
foreach ($contents as $c) {
echo "<tr><td>{$c['content_id']}</td><td>{$c['title']}</td><td>{$c['category_group']}</td><td>" . substr($c['content_url'], 0, 50) . "...</td></tr>";
}
echo '</table>';
echo "<p>총 " . $pdo->query("SELECT COUNT(*) FROM edu_contents")->fetchColumn() . " 개 영상</p>";
// 3. edu_goal_contents 현황
echo '<h2>3. 목표-콘텐츠 매핑 현황 (edu_goal_contents)</h2>';
$goalContents = $pdo->query("SELECT COUNT(*) FROM edu_goal_contents")->fetchColumn();
if ($goalContents > 0) {
$rows = $pdo->query("SELECT * FROM edu_goal_contents LIMIT 20")->fetchAll();
echo '<table><tr><th>goal_code</th><th>content_id</th><th>sort_order</th><th>is_active</th></tr>';
foreach ($rows as $r) {
echo "<tr><td>{$r['goal_code']}</td><td>{$r['content_id']}</td><td>{$r['sort_order']}</td><td>{$r['is_active']}</td></tr>";
}
echo '</table>';
} else {
echo '<p class="warn">⚠ 데이터 없음 - 맵핑이 필요합니다</p>';
}
// 4. edu_user_learning_goals 현황
echo '<h2>4. 사용자 목표 선택 (edu_user_learning_goals)</h2>';
$userGoals = $pdo->query("SELECT COUNT(*) FROM edu_user_learning_goals")->fetchColumn();
if ($userGoals > 0) {
$rows = $pdo->query("SELECT * FROM edu_user_learning_goals LIMIT 20")->fetchAll();
echo '<table><tr><th>member_id</th><th>goal_code</th><th>quarter</th><th>selected_at</th></tr>';
foreach ($rows as $r) {
echo "<tr><td>{$r['member_id']}</td><td>{$r['goal_code']}</td><td>{$r['quarter']}</td><td>{$r['selected_at']}</td></tr>";
}
echo '</table>';
} else {
echo '<p class="warn">⚠ 데이터 없음 - 사용자가 아직 목표를 선택하지 않았습니다</p>';
}
// 5. 액션 아이템
echo '<h2>5. 필요한 작업</h2>';
echo '<ol>';
echo '<li>✗ edu_goal_contents에 goal_code ↔ content_id 맵핑 데이터 INSERT</li>';
echo '<li>✓ myclass_list.php: 사용자 선택 목표 조회 로직</li>';
echo '<li>✓ myclass_list.php: edu_goal_contents에서 6개 영상 조회 로직</li>';
echo '<li>✓ goal-layer.php: 목표 선택 모달 이벤트 처리</li>';
echo '<li>✓ 새 API 또는 기존 API: 목표 선택 저장 (edu_user_learning_goals)</li>';
echo '</ol>';
// 6. 권장 맵핑 규칙
echo '<h2>6. 맵핑 규칙 (제안)</h2>';
echo '<pre>';
echo "각 goal_code마다 6개의 content_id 할당:
- 2026-001: content_id 1~6
- 2026-002: content_id 7~12
- 2026-003: content_id 13~18
- ...등등 (content_id가 있는 범위 내에서 균등 분배)
또는 현재 content_id 수에 따라 동적으로 분배
";
echo '</pre>';
?>
+14
View File
@@ -0,0 +1,14 @@
<?php
try {
$pdo = new PDO('mysql:host=baroncs.co.kr;port=3306;dbname=baronhomep;charset=utf8mb4', 'baronhomep', 'baron3840!!', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$rows = $pdo->query('SELECT group_code, code, code_name, is_active FROM edu_codes ORDER BY group_code, code')->fetchAll(PDO::FETCH_ASSOC);
echo count($rows) . ' rows' . PHP_EOL;
foreach ($rows as $r) {
echo implode(' | ', $r) . PHP_EOL;
}
echo PHP_EOL . '--- SHOW TABLES ---' . PHP_EOL;
$tables = $pdo->query('SHOW TABLES LIKE "edu_%"')->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $t) echo $t . PHP_EOL;
} catch (Exception $e) {
echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
}
@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/db_conn.php';
header('Content-Type: text/plain; charset=utf-8');
$pdo = db_conn();
$sql = "SELECT rg.goal_code,
rg.seq,
rg.title AS rg_title,
rg.description AS rg_desc,
c.sort_order,
c.title AS c_title,
c.description AS c_desc
FROM edu_recommended_goals rg
LEFT JOIN edu_contents c
ON c.goal_code = rg.goal_code
AND c.sort_order = rg.seq
AND c.is_active = '1'
WHERE rg.is_active = '1'
ORDER BY rg.goal_code ASC, rg.seq ASC";
$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
echo ($r['goal_code'] ?? '') . '#' . ($r['seq'] ?? '')
. ' | rg_title=' . ($r['rg_title'] ?? '')
. ' | rg_desc=' . ($r['rg_desc'] ?? '')
. ' | c_title=' . ($r['c_title'] ?? '')
. ' | c_desc=' . ($r['c_desc'] ?? '')
. PHP_EOL;
}
@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/db_conn.php';
header('Content-Type: text/plain; charset=utf-8');
$year = date('Y');
$pdo = db_conn();
$sql = "SELECT content_id, goal_code, title, description, description1, description2, sort_order, base_year, category_code, category_group, updated_at
FROM edu_contents
WHERE is_active = '1'
AND category_code = 'CA10001'
AND category_group = 'CA200Q01'
AND sort_order = 1
AND base_year = ?
ORDER BY goal_code ASC";
$st = $pdo->prepare($sql);
$st->execute([$year]);
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
echo "[CURRENT QUERY ROWS] count=" . count($rows) . "\n";
foreach ($rows as $r) {
echo ($r['goal_code'] ?? '') . " | title=" . ($r['title'] ?? '')
. " | description=" . ($r['description'] ?? '')
. " | description1=" . ($r['description1'] ?? '')
. " | description2=" . ($r['description2'] ?? '')
. " | content_id=" . ($r['content_id'] ?? '')
. "\n";
}
echo "\n[ALL GOAL ROWS SAMPLE]\n";
$rows2 = $pdo->query("SELECT goal_code, sort_order, title, description, description1, description2, content_id
FROM edu_contents
WHERE is_active='1' AND category_code='CA10001' AND category_group='CA200Q01'
ORDER BY goal_code ASC, sort_order ASC, content_id ASC
LIMIT 40")->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows2 as $r) {
echo ($r['goal_code'] ?? '') . "#" . ($r['sort_order'] ?? '')
. " | t=" . ($r['title'] ?? '')
. " | d=" . ($r['description'] ?? '')
. " | d1=" . ($r['description1'] ?? '')
. " | d2=" . ($r['description2'] ?? '')
. " | id=" . ($r['content_id'] ?? '')
. "\n";
}
+124
View File
@@ -0,0 +1,124 @@
<?php
/**
* _tmp_index_diag.php — index.php 영상 로딩 진단
* 확인 후 삭제할 임시 파일
*/
header('Content-Type: text/html; charset=utf-8');
require_once __DIR__ . '/db_conn.php';
$pdo = db_conn();
echo '<h2>EDU 인덱스 영상 로딩 진단</h2>';
echo '<style>table{border-collapse:collapse;margin:10px 0} td,th{border:1px solid #ccc;padding:4px 8px} .ok{color:green} .err{color:red} .warn{color:orange}</style>';
// 1. 테이블 목록 확인
echo '<h3>1. 관련 테이블 존재 여부</h3>';
$tables = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN);
$checkTables = ['edu_contents','edu_content_keywords','edu_keywords','edu_users','edu_user_keywords','edu_codes'];
echo '<table><tr><th>테이블명</th><th>존재</th><th>레코드 수</th></tr>';
foreach ($checkTables as $tbl) {
$exists = in_array($tbl, $tables);
$count = 0;
if ($exists) {
$count = $pdo->query("SELECT COUNT(*) FROM `$tbl`")->fetchColumn();
}
$cls = $exists ? 'ok' : 'err';
echo "<tr><td>$tbl</td><td class='$cls'>" . ($exists ? '✔' : '✘ 없음') . "</td><td>$count</td></tr>";
}
echo '</table>';
// 2. edu_contents 컬럼 구조
echo '<h3>2. edu_contents 컬럼 구조</h3>';
if (in_array('edu_contents', $tables)) {
$cols = $pdo->query("SHOW COLUMNS FROM edu_contents")->fetchAll();
echo '<table><tr><th>Field</th><th>Type</th><th>Null</th><th>Key</th><th>Default</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td><td>{$c['Null']}</td><td>{$c['Key']}</td><td>{$c['Default']}</td></tr>";
}
echo '</table>';
// 샘플 데이터 3건
echo '<h3>2-1. edu_contents 샘플 (최대 5건)</h3>';
$rows = $pdo->query("SELECT * FROM edu_contents LIMIT 5")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="err">⚠ edu_contents 에 데이터가 없습니다 (DB 초기화됨)</p>';
}
} else {
echo '<p class="err">edu_contents 테이블이 존재하지 않습니다.</p>';
}
// 3. edu_content_keywords 구조 & 샘플
echo '<h3>3. edu_content_keywords (키워드-콘텐츠 매핑)</h3>';
if (in_array('edu_content_keywords', $tables)) {
$cols = $pdo->query("SHOW COLUMNS FROM edu_content_keywords")->fetchAll();
echo '<table><tr><th>Field</th><th>Type</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td></tr>";
}
echo '</table>';
$rows = $pdo->query("SELECT * FROM edu_content_keywords LIMIT 5")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="warn">edu_content_keywords 에 데이터가 없습니다.</p>';
}
} else {
echo '<p class="warn">edu_content_keywords 테이블이 없습니다.</p>';
}
// 4. edu_keywords 구조 & 샘플
echo '<h3>4. edu_keywords</h3>';
if (in_array('edu_keywords', $tables)) {
$rows = $pdo->query("SELECT * FROM edu_keywords LIMIT 10")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="warn">edu_keywords 에 데이터가 없습니다.</p>';
}
} else {
echo '<p class="warn">edu_keywords 테이블이 없습니다.</p>';
}
// 5. edu_user_keywords (유저-키워드 연결)
echo '<h3>5. edu_user_keywords</h3>';
if (in_array('edu_user_keywords', $tables)) {
$cols = $pdo->query("SHOW COLUMNS FROM edu_user_keywords")->fetchAll();
echo '<table><tr><th>Field</th><th>Type</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td></tr>";
}
echo '</table>';
$rows = $pdo->query("SELECT * FROM edu_user_keywords LIMIT 10")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="warn">edu_user_keywords 에 데이터가 없습니다.</p>';
}
} else {
echo '<p class="warn">edu_user_keywords 테이블이 없습니다.</p>';
}
// 6. 전체 테이블 목록
echo '<h3>6. DB 전체 테이블 목록 (' . count($tables) . '개)</h3>';
echo '<ul>' . implode('', array_map(fn($t) => "<li>$t</li>", $tables)) . '</ul>';
+160
View File
@@ -0,0 +1,160 @@
<?php
/**
* _tmp_index_diag2.php — 키워드 상세 진단 (edu_keywords 복원용)
*/
header('Content-Type: text/html; charset=utf-8');
require_once __DIR__ . '/db_conn.php';
$pdo = db_conn();
echo '<h2>EDU 키워드 상세 진단</h2>';
echo '<style>table{border-collapse:collapse;margin:10px 0} td,th{border:1px solid #ccc;padding:4px 8px} .ok{color:green} .err{color:red} .warn{color:orange} pre{background:#f5f5f5;padding:8px;font-size:12px}</style>';
// 1. edu_content_keywords 전체 unique keyword_code 목록
echo '<h3>1. edu_content_keywords - 전체 unique keyword_code</h3>';
$rows = $pdo->query("
SELECT keyword_code, COUNT(*) as cnt
FROM edu_content_keywords
WHERE is_active = '1' OR is_active IS NULL
GROUP BY keyword_code
ORDER BY keyword_code
")->fetchAll();
echo '<table><tr><th>keyword_code</th><th>연결 콘텐츠 수</th></tr>';
foreach ($rows as $r) {
echo "<tr><td>{$r['keyword_code']}</td><td>{$r['cnt']}</td></tr>";
}
echo '</table>';
// 2. edu_recommend_keywords 구조 및 데이터
echo '<h3>2. edu_recommend_keywords 구조 및 데이터</h3>';
$tables = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN);
if (in_array('edu_recommend_keywords', $tables)) {
$cols = $pdo->query("SHOW COLUMNS FROM edu_recommend_keywords")->fetchAll();
echo '<b>컬럼:</b><br><table><tr><th>Field</th><th>Type</th><th>Key</th><th>Default</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td><td>{$c['Key']}</td><td>{$c['Default']}</td></tr>";
}
echo '</table>';
$rows = $pdo->query("SELECT * FROM edu_recommend_keywords LIMIT 20")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="warn">데이터 없음</p>';
}
} else {
echo '<p class="err">테이블 없음</p>';
}
// 3. edu_user_keywords 전체 데이터
echo '<h3>3. edu_user_keywords 전체</h3>';
$rows = $pdo->query("SELECT * FROM edu_user_keywords ORDER BY member_id, keyword_code")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="warn">데이터 없음</p>';
}
// 4. edu_codes 에서 KW 계열 코드 확인
echo '<h3>4. edu_codes - KW 계열 코드 (keyword 관련)</h3>';
$rows = $pdo->query("
SELECT * FROM edu_codes
WHERE base_code LIKE 'KW%'
ORDER BY base_code
")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="warn">KW 계열 코드 없음</p>';
// edu_codes 전체 구조 확인
$cols = $pdo->query("SHOW COLUMNS FROM edu_codes")->fetchAll();
echo '<b>edu_codes 컬럼:</b><table><tr><th>Field</th><th>Type</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td></tr>";
}
echo '</table>';
// 샘플 10건
$sample = $pdo->query("SELECT * FROM edu_codes LIMIT 20")->fetchAll();
if ($sample) {
$keys = array_keys($sample[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($sample as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
}
}
// 5. edu_code_group 구조 확인
echo '<h3>5. edu_code_group</h3>';
if (in_array('edu_code_group', $tables)) {
$rows = $pdo->query("SELECT * FROM edu_code_group LIMIT 20")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
}
}
// 6. edu_contents 에서 keyword 관련 컬럼 값 샘플
echo '<h3>6. edu_contents - category 관련 고유값</h3>';
$rows = $pdo->query("
SELECT category_code, category_group, COUNT(*) as cnt
FROM edu_contents
GROUP BY category_code, category_group
ORDER BY category_group, category_code
")->fetchAll();
echo '<table><tr><th>category_code</th><th>category_group</th><th>콘텐츠 수</th></tr>';
foreach ($rows as $r) {
echo "<tr><td>{$r['category_code']}</td><td>{$r['category_group']}</td><td>{$r['cnt']}</td></tr>";
}
echo '</table>';
// 7. edu_contents 중 keyword 연결된 영상 샘플 (edu_content_keywords join)
echo '<h3>7. edu_content_keywords ↔ edu_contents 조인 샘플</h3>';
$rows = $pdo->query("
SELECT ck.keyword_code, c.content_id, c.title, c.category_group, c.content_url
FROM edu_content_keywords ck
JOIN edu_contents c ON c.content_id = ck.content_id
WHERE ck.is_active = '1'
ORDER BY ck.keyword_code, c.content_id
LIMIT 20
")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="warn">결과 없음</p>';
}
// 8. index.php 에서 사용하는 main_data 관련 api 로그 (있으면)
echo '<h3>8. API 관련 파일 목록 확인</h3>';
$apiDir = __DIR__ . '/api/';
if (is_dir($apiDir)) {
foreach (scandir($apiDir) as $f) {
if ($f === '.' || $f === '..') continue;
$fpath = $apiDir . $f;
$size = filesize($fpath);
echo "<li><b>$f</b> ({$size}bytes)</li>";
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* _tmp_index_diag3.php — API 파일 내용 + main_data 구조 진단
*/
header('Content-Type: text/html; charset=utf-8');
echo '<h2>API 파일 내용 진단</h2>';
echo '<style>pre{background:#f5f5f5;padding:10px;font-size:11px;overflow:auto;white-space:pre-wrap;word-break:break-all;border:1px solid #ddd;max-height:600px} h3{margin-top:20px;color:#333}</style>';
$base = __DIR__;
$files = [
'api/videos_by_keywords.php',
'api/user_keywords.php',
'main_data.php',
'main_data_260316_1420.php',
'main_data_moon.php',
'../skin/index.php',
'../skin/index_over.php',
'../skin/index_moon.php',
'../skin/index_guide.php',
'../skin/_backup/index.php',
'../skin/_backup/keyword.php',
];
foreach ($files as $f) {
$full = realpath($base . '/' . $f);
echo "<h3>$f</h3>";
if (!$full || !file_exists($full)) {
echo '<p style="color:red">파일 없음 또는 접근 불가</p>';
continue;
}
$size = filesize($full);
if ($size === 0) {
echo '<p style="color:orange">파일 크기 0 (empty)</p>';
continue;
}
echo "<p>크기: {$size} bytes</p>";
echo '<pre>' . htmlspecialchars(file_get_contents($full)) . '</pre>';
}
+22
View File
@@ -0,0 +1,22 @@
<?php
try {
$pdo = new PDO('mysql:host=baroncs.co.kr;port=3306;dbname=baronhomep;charset=utf8mb4', 'baronhomep', 'baron3840!!', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$pdo->exec("SET NAMES utf8mb4");
$sql = file_get_contents(__DIR__ . '/../sample_content_keywords.sql');
$pdo->exec($sql);
$cnt = $pdo->query('SELECT COUNT(*) FROM edu_content_keywords')->fetchColumn();
echo json_encode(['status' => 'ok', 'inserted_total' => (int)$cnt], JSON_UNESCAPED_UNICODE) . PHP_EOL;
// 키워드별 매핑 건수 확인
$rows = $pdo->query('SELECT keyword_code, COUNT(*) as cnt FROM edu_content_keywords GROUP BY keyword_code ORDER BY cnt DESC')->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($rows, JSON_UNESCAPED_UNICODE) . PHP_EOL;
// recommend_keywords 확인
$rk = $pdo->query('SELECT * FROM edu_recommend_keywords')->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($rk, JSON_UNESCAPED_UNICODE) . PHP_EOL;
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE) . PHP_EOL;
}
+26
View File
@@ -0,0 +1,26 @@
<?php
require __DIR__ . '/db_conn.php';
$pdo = db_conn();
$existsStmt = $pdo->prepare("SELECT base_code, code, code_name FROM edu_codes WHERE group_code='KW100' AND code_name = ? LIMIT 1");
$existsStmt->execute(['자기개발']);
$exists = $existsStmt->fetch(PDO::FETCH_ASSOC);
if ($exists) {
echo "EXISTS|{$exists['base_code']}|{$exists['code']}|{$exists['code_name']}\n";
} else {
$nextCode = (int)$pdo->query("SELECT COALESCE(MAX(CAST(code AS UNSIGNED)), 0) FROM edu_codes WHERE group_code='KW100'")->fetchColumn() + 1;
$code = str_pad((string)$nextCode, 2, '0', STR_PAD_LEFT);
$baseCode = 'KW100' . $code;
$ins = $pdo->prepare("INSERT INTO edu_codes (group_code, code, base_code, code_name, is_active, created_by, created_at, updated_by, updated_at) VALUES ('KW100', ?, ?, ?, '1', 'admin', NOW(), 'admin', NOW())");
$ins->execute([$code, $baseCode, '자기개발']);
echo "INSERTED|{$baseCode}|{$code}|자기개발\n";
}
$rows = $pdo->query("SELECT code, base_code, code_name FROM edu_codes WHERE group_code='KW100' ORDER BY CAST(code AS UNSIGNED)")->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
echo "ROW|{$r['code']}|{$r['base_code']}|{$r['code_name']}\n";
}
@@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/db_conn.php';
header('Content-Type: text/plain; charset=utf-8');
$pdo = db_conn();
echo "[COLUMNS]\n";
$cols = $pdo->query('SHOW COLUMNS FROM edu_learning_goals')->fetchAll(PDO::FETCH_ASSOC);
foreach ($cols as $c) {
echo $c['Field'] . '|' . $c['Type'] . PHP_EOL;
}
echo "\n[SAMPLES]\n";
$rows = $pdo->query('SELECT * FROM edu_learning_goals ORDER BY 1 ASC LIMIT 20')->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
$pairs = [];
foreach ($r as $k => $v) {
$pairs[] = $k . '=' . str_replace(["\r", "\n"], ' ', (string)$v);
}
echo implode(' | ', $pairs) . PHP_EOL;
}
@@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/db_conn.php';
header('Content-Type: text/plain; charset=utf-8');
$pdo = db_conn();
echo "[COLUMNS]\n";
$cols = $pdo->query('SHOW COLUMNS FROM edu_contents')->fetchAll(PDO::FETCH_ASSOC);
foreach ($cols as $c) {
echo $c['Field'] . '|' . $c['Type'] . PHP_EOL;
}
echo "\n[SAMPLES_WITH_DESC]\n";
$rows = $pdo->query("SELECT content_id, title, description, goal_code, category_group, category_code, sort_order, is_active
FROM edu_contents
WHERE is_active = '1' AND CHAR_LENGTH(TRIM(COALESCE(description, ''))) >= 5
ORDER BY updated_at DESC, created_at DESC
LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
$id = isset($r['content_id']) ? (string)$r['content_id'] : '';
$title = isset($r['title']) ? (string)$r['title'] : '';
$desc = isset($r['description']) ? (string)$r['description'] : '';
$goal = isset($r['goal_code']) ? (string)$r['goal_code'] : '';
$group = isset($r['category_group']) ? (string)$r['category_group'] : '';
$cate = isset($r['category_code']) ? (string)$r['category_code'] : '';
$sort = isset($r['sort_order']) ? (string)$r['sort_order'] : '';
$active = isset($r['is_active']) ? (string)$r['is_active'] : '';
echo $id . '|' . $title . '|' . preg_replace('/\s+/', ' ', $desc) . '|goal=' . $goal . '|group=' . $group . '|cate=' . $cate . '|sort=' . $sort . '|active=' . $active . PHP_EOL;
}
+161
View File
@@ -0,0 +1,161 @@
<?php
/**
* 마이클래스 구조 진단 파일
* edu_learning_goals와 edu_goal_contents 구조 파악
*/
header('Content-Type: text/html; charset=utf-8');
require_once __DIR__ . '/db_conn.php';
$pdo = db_conn();
echo '<h1>마이클래스 데이터베이스 구조</h1>';
echo '<style>
body { font-family: Arial; margin: 20px; }
table { border-collapse: collapse; margin: 20px 0; width: 100%; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
th { background: #f0f0f0; }
.ok { color: green; }
.err { color: red; }
h2 { margin-top: 30px; }
</style>';
// 1. 모든 테이블 목록
echo '<h2>1. 전체 테이블 목록</h2>';
$tables = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN);
echo '<table><tr><th>테이블명</th></tr>';
foreach ($tables as $t) {
echo "<tr><td>$t</td></tr>";
}
echo '</table>';
// 2. edu_learning_goals 확인
echo '<h2>2. edu_learning_goals 테이블</h2>';
if (in_array('edu_learning_goals', $tables)) {
echo '<b>컬럼 구조:</b>';
$cols = $pdo->query("SHOW COLUMNS FROM edu_learning_goals")->fetchAll();
echo '<table><tr><th>Field</th><th>Type</th><th>Null</th><th>Key</th><th>Default</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td><td>{$c['Null']}</td><td>{$c['Key']}</td><td>{$c['Default']}</td></tr>";
}
echo '</table>';
echo '<b>샘플 데이터:</b>';
$rows = $pdo->query("SELECT * FROM edu_learning_goals LIMIT 10")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
} else {
echo '<p class="err">데이터 없음</p>';
}
} else {
echo '<p class="err">테이블 없음</p>';
}
// 3. edu_goal_contents 확인
echo '<h2>3. edu_goal_contents 테이블</h2>';
if (in_array('edu_goal_contents', $tables)) {
echo '<b>컬럼 구조:</b>';
$cols = $pdo->query("SHOW COLUMNS FROM edu_goal_contents")->fetchAll();
echo '<table><tr><th>Field</th><th>Type</th><th>Null</th><th>Key</th><th>Default</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td><td>{$c['Null']}</td><td>{$c['Key']}</td><td>{$c['Default']}</td></tr>";
}
echo '</table>';
echo '<b>샘플 데이터:</b>';
$rows = $pdo->query("SELECT * FROM edu_goal_contents LIMIT 20")->fetchAll();
if ($rows) {
$keys = array_keys($rows[0]);
echo '<table><tr>' . implode('', array_map(fn($k) => "<th>$k</th>", $keys)) . '</tr>';
foreach ($rows as $r) {
echo '<tr>' . implode('', array_map(fn($v) => '<td>' . htmlspecialchars((string)$v) . '</td>', $r)) . '</tr>';
}
echo '</table>';
// 통계
echo '<b>통계:</b>';
$stats = $pdo->query("
SELECT
COUNT(DISTINCT goal_id) as goal_cnt,
COUNT(DISTINCT content_id) as content_cnt,
COUNT(*) as total_rows
FROM edu_goal_contents
")->fetch();
echo '<div><p>총 goal_id: ' . $stats['goal_cnt'] . '</p>';
echo '<p>총 content_id: ' . $stats['content_cnt'] . '</p>';
echo '<p>전체 행: ' . $stats['total_rows'] . '</p></div>';
} else {
echo '<p class="err">데이터 없음</p>';
}
} else {
echo '<p class="err">테이블 없음</p>';
}
// 4. 관계도 확인 (learning_goals ↔ goal_contents)
echo '<h2>4. edu_learning_goals ↔ edu_goal_contents 연결</h2>';
if (in_array('edu_learning_goals', $tables) && in_array('edu_goal_contents', $tables)) {
echo '<b>학습목표별 영상 수:</b>';
$rows = $pdo->query("
SELECT
g.goal_id,
g.goal_name,
COUNT(c.content_id) as video_count,
GROUP_CONCAT(c.content_id ORDER BY c.sort_order) as content_ids
FROM edu_learning_goals g
LEFT JOIN edu_goal_contents c ON g.goal_id = c.goal_id
GROUP BY g.goal_id, g.goal_name
ORDER BY g.goal_id
")->fetchAll();
if ($rows) {
echo '<table><tr><th>goal_id</th><th>goal_name</th><th>영상 수</th><th>content_ids</th></tr>';
foreach ($rows as $r) {
echo "<tr>";
echo "<td>{$r['goal_id']}</td>";
echo "<td>{$r['goal_name']}</td>";
echo "<td>{$r['video_count']}</td>";
echo "<td>" . htmlspecialchars($r['content_ids'] ?? '') . "</td>";
echo "</tr>";
}
echo '</table>';
}
}
// 5. 사용자 선택 목표 (if exists)
echo '<h2>5. 사용자별 선택된 학습목표</h2>';
if (in_array('edu_user_goals', $tables)) {
$cols = $pdo->query("SHOW COLUMNS FROM edu_user_goals")->fetchAll();
echo '<table><tr><th>Field</th><th>Type</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td></tr>";
}
echo '</table>';
} else if (in_array('edu_goal_user_select', $tables)) {
$cols = $pdo->query("SHOW COLUMNS FROM edu_goal_user_select")->fetchAll();
echo '<table><tr><th>Field</th><th>Type</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td></tr>";
}
echo '</table>';
} else {
echo '<p>사용자 선택 테이블을 찾을 수 없습니다.</p>';
}
// 6. 영상 시청 기록 (learning status)
echo '<h2>6. 사용자 학습 진행상황 관련 테이블</h2>';
$learningTables = ['edu_learning_progress', 'edu_user_learning', 'edu_learning_status'];
foreach ($learningTables as $tbl) {
if (in_array($tbl, $tables)) {
echo "<h3>$tbl</h3>";
$cols = $pdo->query("SHOW COLUMNS FROM $tbl")->fetchAll();
echo '<table><tr><th>Field</th><th>Type</th></tr>';
foreach ($cols as $c) {
echo "<tr><td>{$c['Field']}</td><td>{$c['Type']}</td></tr>";
}
echo '</table>';
}
}
?>
@@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/db_conn.php';
header('Content-Type: text/plain; charset=utf-8');
$pdo = db_conn();
echo "[COLUMNS]\n";
$cols = $pdo->query('SHOW COLUMNS FROM edu_recommended_goals')->fetchAll(PDO::FETCH_ASSOC);
foreach ($cols as $c) {
echo $c['Field'] . '|' . $c['Type'] . PHP_EOL;
}
echo "\n[SAMPLES]\n";
$rows = $pdo->query('SELECT * FROM edu_recommended_goals ORDER BY 1 ASC LIMIT 30')->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
$pairs = [];
foreach ($r as $k => $v) {
$pairs[] = $k . '=' . str_replace(["\r", "\n"], ' ', (string)$v);
}
echo implode(' | ', $pairs) . PHP_EOL;
}
+57
View File
@@ -0,0 +1,57 @@
<?php
require __DIR__ . '/db_conn.php';
$pdo = db_conn();
$pdo->beginTransaction();
// Map KW100xx codes in edu_content_keywords to Korean keyword names in edu_codes.code_name.
$sql = "
INSERT INTO edu_content_keywords (
content_id,
keyword_code,
is_active,
created_by,
created_at,
updated_by,
updated_at
)
SELECT
ck.content_id,
ec.code_name AS keyword_code,
'1' AS is_active,
'admin' AS created_by,
NOW() AS created_at,
'admin' AS updated_by,
NOW() AS updated_at
FROM edu_content_keywords ck
JOIN edu_codes ec
ON ec.base_code = ck.keyword_code
AND ec.group_code = 'KW100'
LEFT JOIN edu_content_keywords ck2
ON ck2.content_id = ck.content_id
AND ck2.keyword_code = ec.code_name
WHERE ck.keyword_code LIKE 'KW%'
AND ck.is_active = '1'
AND ec.code_name IS NOT NULL
AND ec.code_name <> ''
AND ck2.content_id IS NULL
";
$inserted = $pdo->exec($sql);
$pdo->commit();
echo "INSERTED_ROWS|" . (int)$inserted . "\n";
$checkKeywords = ['AI', '리더십', '자기개발', '온보딩', '중간관리자'];
$stmt = $pdo->prepare("SELECT keyword_code, COUNT(*) AS cnt FROM edu_content_keywords WHERE keyword_code = ? GROUP BY keyword_code");
foreach ($checkKeywords as $kw) {
$stmt->execute([$kw]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$cnt = $row ? (int)$row['cnt'] : 0;
echo "KW_COUNT|{$kw}|{$cnt}\n";
}
$sample = $pdo->query("SELECT content_id, keyword_code FROM edu_content_keywords WHERE keyword_code IN ('AI','리더십','자기개발') ORDER BY keyword_code, content_id LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
foreach ($sample as $r) {
echo "SAMPLE|{$r['content_id']}|{$r['keyword_code']}\n";
}
+23
View File
@@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/db_conn.php';
$pdo = db_conn();
$tables = [
'edu_user_learning_goals',
'edu_learning_histories',
'edu_content_memos',
'edu_goal_contents',
'edu_learning_goals',
'edu_contents'
];
foreach ($tables as $t) {
echo '=== ' . $t . ' ===' . PHP_EOL;
$rows = $pdo->query('SHOW COLUMNS FROM ' . $t)->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
$def = $r['Default'];
if ($def === null) {
$def = 'NULL';
}
echo $r['Field'] . '|' . $r['Type'] . '|' . $r['Null'] . '|' . $r['Key'] . '|' . $def . PHP_EOL;
}
echo PHP_EOL;
}
+14
View File
@@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/db_conn.php';
$pdo = db_conn();
$cols = $pdo->query("SHOW COLUMNS FROM edu_users")->fetchAll(PDO::FETCH_ASSOC);
foreach ($cols as $c) {
echo $c['Field'] . '|' . $c['Type'] . '|' . $c['Null'] . '|' . $c['Key'] . '|' . ($c['Default'] ?? 'NULL') . PHP_EOL;
}
echo "---sample---" . PHP_EOL;
$rows = $pdo->query("SELECT member_id, intra_pw FROM edu_users ORDER BY member_id LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
$pw = (string)($r['intra_pw'] ?? '');
$prefix = substr($pw, 0, 4);
echo $r['member_id'] . '|len=' . strlen($pw) . '|prefix=' . $prefix . PHP_EOL;
}
+89
View File
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once dirname(__DIR__, 2) . '/db_conn.php';
$memberId = (string)($_SESSION['member_id'] ?? $_SESSION['user_id'] ?? '');
$sysCompCode = (string)($_SESSION['sys_comp_code'] ?? $_SESSION['company'] ?? '');
if ($memberId === '') {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'not_logged_in'], JSON_UNESCAPED_UNICODE);
exit;
}
$rawBody = file_get_contents('php://input');
$input = is_string($rawBody) && $rawBody !== '' ? json_decode($rawBody, true) : [];
if (!is_array($input)) {
$input = $_POST;
}
$contentId = trim((string)($input['content_id'] ?? ''));
if ($contentId === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'content_id_required'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = db_conn();
// 학습 이력 조회
$sql = 'SELECT watch_tm, content_tm, completed_at, last_viewed_at
FROM edu_learning_histories
WHERE member_id = ? AND content_id = ?';
$params = [$memberId, $contentId];
if ($sysCompCode !== '') {
$sql .= ' AND sys_comp_code = ?';
$params[] = $sysCompCode;
}
$sql .= ' ORDER BY last_viewed_at DESC LIMIT 1';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// 콘텐츠 상세정보 조회 (description)
$stmtContent = $pdo->prepare('SELECT description FROM edu_contents WHERE content_id = ? LIMIT 1');
$stmtContent->execute([$contentId]);
$contentRow = $stmtContent->fetch(PDO::FETCH_ASSOC);
$description = ($contentRow['description'] ?? '');
// 북마크 상태 조회
$bookmarkSql = 'SELECT is_active FROM edu_content_wishlist WHERE content_id = ? AND member_id = ?';
$bookmarkParams = [$contentId, $memberId];
if ($sysCompCode !== '') {
$bookmarkSql .= ' AND sys_comp_code = ?';
$bookmarkParams[] = $sysCompCode;
}
$bookmarkSql .= ' LIMIT 1';
$stmtBookmark = $pdo->prepare($bookmarkSql);
$stmtBookmark->execute($bookmarkParams);
$bookmarkRow = $stmtBookmark->fetch(PDO::FETCH_ASSOC);
$isBookmarked = (($bookmarkRow['is_active'] ?? '0') === '1');
$result = [
'success' => true,
'watch_tm' => (int)($row['watch_tm'] ?? 0),
'content_tm' => (int)($row['content_tm'] ?? 0),
'completed_at' => $row['completed_at'] ?? null,
'last_viewed_at' => $row['last_viewed_at'] ?? null,
'description' => $description,
'is_bookmarked' => $isBookmarked,
];
echo json_encode($result, JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
error_log('[get_video_time] ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'server_error'], JSON_UNESCAPED_UNICODE);
}
+1
View File
@@ -0,0 +1 @@
<?php
+291
View File
@@ -0,0 +1,291 @@
<?php
//=========================
// 개인정보 + 배지
//=========================
require_once __DIR__ . '/db_conn.php';
if (!function_exists('mypage_h')) {
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
if (!function_exists('mypage_level_meta')) {
function mypage_level_meta(?string $level): array
{
$level = trim((string)$level);
$map = [
'MASTER' => [
'code' => 'Master',
'class' => 'master',
'label' => 'Master',
'icon' => '/img/ico/ico_level_master.svg',
],
'ELITE' => [
'code' => 'Elite',
'class' => 'elite',
'label' => 'Elite',
'icon' => '/img/ico/ico_level_elite.svg',
],
'LEARNER' => [
'code' => 'Learner',
'class' => 'learner',
'label' => 'Learner',
'icon' => '/img/ico/ico_level_learner.svg',
],
'ROOKIE' => [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/img/ico/ico_level_rookie.svg',
],
];
$key = strtoupper($level);
return $map[$key] ?? [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/img/ico/ico_level_rookie.svg',
];
}
}
if (!function_exists('mypage_badge_map')) {
function mypage_badge_map(string $badgeCode): array
{
$badgeCode = strtoupper(trim($badgeCode));
$BADGE_ICON_MAP = [
// 실제 운영 시 코드값 확정되면 여기만 교체
'BG001' => [
'slot' => 'hat',
'img' => '/img/mypage/ico_school.png',
'name' => '학습 배지',
],
'BG002' => [
'slot' => 'pencil',
'img' => '/img/mypage/ico_pencil.png',
'name' => '작성 배지',
],
'BG003' => [
'slot' => 'pick',
'img' => '/img/mypage/ico_pick_2.png',
'name' => 'Pick 배지',
],
];
return $BADGE_ICON_MAP[$badgeCode] ?? [
'slot' => 'pick',
'img' => '/img/mypage/ico_pick_2.png',
'name' => '기본 배지',
];
}
}
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
$comp_name = $_SESSION['comp_name'] ?? '';
$member_name = $_SESSION['member_name'] ?? '';
$rank_name = $_SESSION['rank_name'];
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
//echo "memberId===".$memberId."<br>";
//echo "sysCompCode===".$sysCompCode."<br>";exit;
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
// ★ 선택년도 수집
$selectedYear = trim((string)($_GET['selected_year'] ?? date('Y')));
if ($selectedYear === '' || !preg_match('/^\d{4}$/', $selectedYear)) {
$selectedYear = date('Y');
}
$profileFileName = $memberId . '_' . $sysCompCode . '.png';
$profileFilePath = __DIR__ . '/../img/profile/' . $profileFileName;
$profileImageUrl = '/img/profile/' . $profileFileName;
if (file_exists($profileFilePath)) {
$profileImage = $profileImageUrl;
/*
if($memberId=="M21420"){
echo 11111111;
exit;
}
*/
} else {
/*
if($memberId=="M21420"){
echo 222222222222;
exit;
}*/
$profileImage = '/img/ico/ico_profile.svg';
}
/*
if (file_exists($profileFilePath)) {
$profileImage = $profileImageUrl;
} else {
$profileImage = '/img/ico/ico_profile.svg';
}
*/
$profileData = [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'name' => $member_name,
'rank_name' => $rank_name,
'working_comp' => '',
'belong_comp_name' => $comp_name,
'join_date' => '',
'latest_stats_year' => $selectedYear,
'learning_level' => 'Rookie',
'level_class' => 'rookie',
'level_icon' => '/img/ico/ico_level_rookie.svg',
'total_minutes' => 0,
'profile_image' => $profileImage, // 현재는 기본 이미지 고정
];
$profileBadges = [
'hat' => null,
'pencil' => null,
'pick' => null,
];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// ★ 선택년도 학습통계 기준
$sql = "
SELECT
u.member_id,
u.sys_comp_code,
u.name,
u.rank_name,
u.working_comp,
u.join_date,
ec.code,
ec.code_name,
ys.stats_year,
ys.learning_level,
ys.total_minutes
FROM edu_users u
LEFT JOIN edu_yearly_learning_stats ys
ON ys.member_id = u.member_id
AND ys.sys_comp_code = u.sys_comp_code
AND ys.stats_year = :selected_year
LEFT JOIN
(
SELECT code,code_name FROM edu_codes WHERE group_code = 'CO100'
) ec
ON ec.code = u.belong_comp
WHERE u.member_id = :member_id
AND u.sys_comp_code = :sys_comp_code
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
':selected_year' => (int)$selectedYear,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$levelMeta = mypage_level_meta($row['learning_level'] ?? '');
$profileData = [
'member_id' => $row['member_id'],
'sys_comp_code' => $row['sys_comp_code'],
'name' => $row['name'] ?? '사용자',
'rank_name' => $row['rank_name'] ?? '',
'belong_comp_code' => $row['code'] ?? '',
'belong_comp_name' => $row['code_name'] ?? $comp_name,
'working_comp' => $row['working_comp'] ?? '',
'join_date' => $row['join_date'] ?? '',
'latest_stats_year' => $row['stats_year'] ?? $selectedYear,
'learning_level' => $levelMeta['label'],
'level_class' => $levelMeta['class'],
'level_icon' => $levelMeta['icon'],
'total_minutes' => (int)($row['total_minutes'] ?? 0),
'profile_image' => $profileImage, // 현재는 기본 이미지 고정
];
}
// ★ 선택년도 통계가 없으면 선택년도 학습이력 합계 fallback
if ((int)$profileData['total_minutes'] === 0) {
$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,
]);
$fallbackMin = (int)$stmtFallback->fetchColumn();
if ($fallbackMin > 0) {
$profileData['total_minutes'] = $fallbackMin;
}
}
// 배지 조회
$stmtBadge = $pdo->prepare("
SELECT badge_code, issued_at
FROM edu_user_badges
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
ORDER BY issued_at DESC, seq DESC
");
$stmtBadge->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmtBadge->fetchAll(PDO::FETCH_ASSOC) as $badgeRow) {
$badgeInfo = mypage_badge_map($badgeRow['badge_code'] ?? '');
$slot = $badgeInfo['slot'];
if (!isset($profileBadges[$slot]) || $profileBadges[$slot] !== null) {
continue;
}
$profileBadges[$slot] = [
'badge_code' => $badgeRow['badge_code'],
'slot' => $slot,
'img' => $badgeInfo['img'],
'name' => $badgeInfo['name'],
'issued_at' => $badgeRow['issued_at'],
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
+222
View File
@@ -0,0 +1,222 @@
<?php
//=========================
//개인정보 + 최신연도 학습레벨 + 배지
//=========================
require_once __DIR__ . '/db_conn.php';
if (!function_exists('mypage_h')) {
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
if (!function_exists('mypage_level_meta')) {
function mypage_level_meta(?string $level): array
{
$level = trim((string)$level);
$map = [
'MASTER' => [
'code' => 'Master',
'class' => 'master',
'label' => 'Master',
'icon' => '/edu/img/ico/ico_level_master.svg',
],
'ELITE' => [
'code' => 'Elite',
'class' => 'elite',
'label' => 'Elite',
'icon' => '/edu/img/ico/ico_level_elite.svg',
],
'LEARNER' => [
'code' => 'Learner',
'class' => 'learner',
'label' => 'Learner',
'icon' => '/edu/img/ico/ico_level_learner.svg',
],
'ROOKIE' => [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
],
];
$key = strtoupper($level);
return $map[$key] ?? [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
];
}
}
if (!function_exists('mypage_badge_map')) {
function mypage_badge_map(string $badgeCode): array
{
$badgeCode = strtoupper(trim($badgeCode));
$BADGE_ICON_MAP = [
// 실제 운영 시 코드값 확정되면 여기만 교체
'BG001' => [
'slot' => 'hat',
'img' => '/edu/img/mypage/ico_school.png',
'name' => '학습 배지',
],
'BG002' => [
'slot' => 'pencil',
'img' => '/edu/img/mypage/ico_pencil.png',
'name' => '작성 배지',
],
'BG003' => [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => 'Pick 배지',
],
];
return $BADGE_ICON_MAP[$badgeCode] ?? [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => '기본 배지',
];
}
}
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
$profileData = [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'name' => '사용자',
'rank_name' => '',
'working_comp' => '',
'join_date' => '',
'latest_stats_year' => '',
'learning_level' => 'Rookie',
'level_class' => 'rookie',
'level_icon' => '/edu/img/ico/ico_level_rookie.svg',
'total_minutes' => 0,
'profile_image' => '/edu/img/insight/profile.png', // 현재는 기본 이미지 고정
];
$profileBadges = [
'hat' => null,
'pencil' => null,
'pick' => null,
];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 최신년도 학습통계 우선
$sql = "
SELECT
u.member_id,
u.sys_comp_code,
u.name,
u.rank_name,
u.working_comp,
u.join_date,
ys.stats_year,
ys.learning_level,
ys.total_minutes
FROM edu_users u
LEFT JOIN edu_yearly_learning_stats ys
ON ys.member_id = u.member_id
AND ys.sys_comp_code = u.sys_comp_code
AND ys.stats_year = (
SELECT MAX(stats_year)
FROM edu_yearly_learning_stats
WHERE member_id = u.member_id
AND sys_comp_code = u.sys_comp_code
)
WHERE u.member_id = :member_id
AND u.sys_comp_code = :sys_comp_code
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$levelMeta = mypage_level_meta($row['learning_level'] ?? '');
$profileData = [
'member_id' => $row['member_id'],
'sys_comp_code' => $row['sys_comp_code'],
'name' => $row['name'] ?? '사용자',
'rank_name' => $row['rank_name'] ?? '',
'working_comp' => $row['working_comp'] ?? '',
'join_date' => $row['join_date'] ?? '',
'latest_stats_year' => $row['stats_year'] ?? '',
'learning_level' => $levelMeta['label'],
'level_class' => $levelMeta['class'],
'level_icon' => $levelMeta['icon'],
'total_minutes' => (int)($row['total_minutes'] ?? 0),
'profile_image' => '/edu/img/insight/profile.png',
];
}
// 최신통계가 없으면 누적 이력 합계 fallback
if ((int)$profileData['total_minutes'] === 0) {
$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
");
$stmtFallback->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$fallbackMin = (int)$stmtFallback->fetchColumn();
if ($fallbackMin > 0) {
$profileData['total_minutes'] = $fallbackMin;
}
}
// 배지 조회
$stmtBadge = $pdo->prepare("
SELECT badge_code, issued_at
FROM edu_user_badges
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
ORDER BY issued_at DESC, seq DESC
");
$stmtBadge->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmtBadge->fetchAll(PDO::FETCH_ASSOC) as $badgeRow) {
$badgeInfo = mypage_badge_map($badgeRow['badge_code'] ?? '');
$slot = $badgeInfo['slot'];
if (!isset($profileBadges[$slot]) || $profileBadges[$slot] !== null) {
continue;
}
$profileBadges[$slot] = [
'badge_code' => $badgeRow['badge_code'],
'slot' => $slot,
'img' => $badgeInfo['img'],
'name' => $badgeInfo['name'],
'issued_at' => $badgeRow['issued_at'],
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
+222
View File
@@ -0,0 +1,222 @@
<?php
//=========================
//개인정보 + 최신연도 학습레벨 + 배지
//=========================
require_once __DIR__ . '/db_conn.php';
if (!function_exists('mypage_h')) {
function mypage_h(?string $str): string
{
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
if (!function_exists('mypage_level_meta')) {
function mypage_level_meta(?string $level): array
{
$level = trim((string)$level);
$map = [
'MASTER' => [
'code' => 'Master',
'class' => 'master',
'label' => 'Master',
'icon' => '/edu/img/ico/ico_level_master.svg',
],
'ELITE' => [
'code' => 'Elite',
'class' => 'elite',
'label' => 'Elite',
'icon' => '/edu/img/ico/ico_level_elite.svg',
],
'LEARNER' => [
'code' => 'Learner',
'class' => 'learner',
'label' => 'Learner',
'icon' => '/edu/img/ico/ico_level_learner.svg',
],
'ROOKIE' => [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
],
];
$key = strtoupper($level);
return $map[$key] ?? [
'code' => 'Rookie',
'class' => 'rookie',
'label' => 'Rookie',
'icon' => '/edu/img/ico/ico_level_rookie.svg',
];
}
}
if (!function_exists('mypage_badge_map')) {
function mypage_badge_map(string $badgeCode): array
{
$badgeCode = strtoupper(trim($badgeCode));
$BADGE_ICON_MAP = [
// 실제 운영 시 코드값 확정되면 여기만 교체
'BG001' => [
'slot' => 'hat',
'img' => '/edu/img/mypage/ico_school.png',
'name' => '학습 배지',
],
'BG002' => [
'slot' => 'pencil',
'img' => '/edu/img/mypage/ico_pencil.png',
'name' => '작성 배지',
],
'BG003' => [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => 'Pick 배지',
],
];
return $BADGE_ICON_MAP[$badgeCode] ?? [
'slot' => 'pick',
'img' => '/edu/img/mypage/ico_pick_2.png',
'name' => '기본 배지',
];
}
}
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
$profileData = [
'member_id' => $memberId,
'sys_comp_code' => $sysCompCode,
'name' => '사용자',
'rank_name' => '',
'working_comp' => '',
'join_date' => '',
'latest_stats_year' => '',
'learning_level' => 'Rookie',
'level_class' => 'rookie',
'level_icon' => '/edu/img/ico/ico_level_rookie.svg',
'total_minutes' => 0,
'profile_image' => '/edu/img/insight/profile.png', // 현재는 기본 이미지 고정
];
$profileBadges = [
'hat' => null,
'pencil' => null,
'pick' => null,
];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 최신년도 학습통계 우선
$sql = "
SELECT
u.member_id,
u.sys_comp_code,
u.name,
u.rank_name,
u.working_comp,
u.join_date,
ys.stats_year,
ys.learning_level,
ys.total_minutes
FROM edu_users u
LEFT JOIN edu_yearly_learning_stats ys
ON ys.member_id = u.member_id
AND ys.sys_comp_code = u.sys_comp_code
AND ys.stats_year = (
SELECT MAX(stats_year)
FROM edu_yearly_learning_stats
WHERE member_id = u.member_id
AND sys_comp_code = u.sys_comp_code
)
WHERE u.member_id = :member_id
AND u.sys_comp_code = :sys_comp_code
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$levelMeta = mypage_level_meta($row['learning_level'] ?? '');
$profileData = [
'member_id' => $row['member_id'],
'sys_comp_code' => $row['sys_comp_code'],
'name' => $row['name'] ?? '사용자',
'rank_name' => $row['rank_name'] ?? '',
'working_comp' => $row['working_comp'] ?? '',
'join_date' => $row['join_date'] ?? '',
'latest_stats_year' => $row['stats_year'] ?? '',
'learning_level' => $levelMeta['label'],
'level_class' => $levelMeta['class'],
'level_icon' => $levelMeta['icon'],
'total_minutes' => (int)($row['total_minutes'] ?? 0),
'profile_image' => '/edu/img/insight/profile.png',
];
}
// 최신통계가 없으면 누적 이력 합계 fallback
if ((int)$profileData['total_minutes'] === 0) {
$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
");
$stmtFallback->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$fallbackMin = (int)$stmtFallback->fetchColumn();
if ($fallbackMin > 0) {
$profileData['total_minutes'] = $fallbackMin;
}
}
// 배지 조회
$stmtBadge = $pdo->prepare("
SELECT badge_code, issued_at
FROM edu_user_badges
WHERE member_id = :member_id
AND sys_comp_code = :sys_comp_code
ORDER BY issued_at DESC, seq DESC
");
$stmtBadge->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmtBadge->fetchAll(PDO::FETCH_ASSOC) as $badgeRow) {
$badgeInfo = mypage_badge_map($badgeRow['badge_code'] ?? '');
$slot = $badgeInfo['slot'];
if (!isset($profileBadges[$slot]) || $profileBadges[$slot] !== null) {
continue;
}
$profileBadges[$slot] = [
'badge_code' => $badgeRow['badge_code'],
'slot' => $slot,
'img' => $badgeInfo['img'],
'name' => $badgeInfo['name'],
'issued_at' => $badgeRow['issued_at'],
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
+109
View File
@@ -0,0 +1,109 @@
<?php
//=========================
// 컨텐츠 제안하기 기본값 + 제안현황
//=========================
require_once __DIR__ . '/db_conn.php';
// TODO: 실제 로그인 세션 연동 후 교체
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
$offerDefaultTypeCode = 'OF10001'; // 제안
$offerDefaultStatusCode = 'OF10001'; // 검토중
$offerStatusMap = [
'OF10001' => '제안',
'OF10002' => '검토중',
'OF10003' => '게시완료',
'OF10004' => '게시불가',
];
$offerHistory = [];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 상태 코드맵 동적 조회 (기본값 fallback 유지)
$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 $codeRow) {
$offerStatusMap[$codeRow['base_code']] = $codeRow['code_name'];
}
// 제안 이력 조회
// 정렬 우선순위:
// 1) 검토중(OF10002) 최상단
// 2) 그 외 상태는 코드 우선순위대로
// 3) 같은 상태 안에서는 최신순
$stmt = $pdo->prepare("
SELECT
co.offer_id,
co.type_code,
co.title,
co.reference_url,
co.reason,
co.status_code,
co.reason_return,
co.created_at,
co.updated_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,
co.offer_id DESC
LIMIT 20
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$statusCode = trim((string) ($row['status_code'] ?? ''));
$offerHistory[] = [
'offer_id' => $row['offer_id'] ?? '',
'type_code' => $row['type_code'] ?? '',
'title' => $row['title'] ?? '',
'reference_url' => $row['reference_url'] ?? '',
'reason' => $row['reason'] ?? '',
'status_code' => $statusCode,
'status_name' => $offerStatusMap[$statusCode] ?? $statusCode,
'reason_return' => $row['reason_return'] ?? '',
'created_at' => $row['created_at'] ?? '',
'updated_at' => $row['updated_at'] ?? '',
'created_at_dot' => !empty($row['created_at']) ? date('y.m.d', strtotime($row['created_at'])) : '',
'is_consider' => ($statusCode === 'OF10001'),
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
// error_log($e->getMessage());
}
+99
View File
@@ -0,0 +1,99 @@
<?php
//=========================
// 컨텐츠 제안하기 기본값 + 제안현황
//=========================
require_once __DIR__ . '/db_conn.php';
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
$offerDefaultTypeCode = 'OF10001'; // 제안
$offerDefaultStatusCode = 'OF10002'; // 검토중
$offerStatusMap = [
'OF10001' => '제안',
'OF10002' => '검토중',
'OF10003' => '게시완료',
'OF10004' => '게시불가',
];
$offerHistory = [];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
// 상태 코드맵 동적 조회 (기본값 fallback 유지)
$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 $codeRow) {
$offerStatusMap[$codeRow['base_code']] = $codeRow['code_name'];
}
// 제안 이력 조회
// 정렬 우선순위:
// 1) 검토중(OF10002) 최상단
// 2) 그 외 상태는 코드 우선순위대로
// 3) 같은 상태 안에서는 최신순
$stmt = $pdo->prepare("
SELECT
co.offer_id,
co.type_code,
co.title,
co.reference_url,
co.reason,
co.status_code,
co.created_at,
co.updated_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,
co.offer_id DESC
LIMIT 20
");
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$statusCode = trim((string)($row['status_code'] ?? ''));
$offerHistory[] = [
'offer_id' => $row['offer_id'] ?? '',
'type_code' => $row['type_code'] ?? '',
'title' => $row['title'] ?? '',
'reference_url' => $row['reference_url'] ?? '',
'reason' => $row['reason'] ?? '',
'status_code' => $statusCode,
'status_name' => $offerStatusMap[$statusCode] ?? $statusCode,
'created_at' => $row['created_at'] ?? '',
'updated_at' => $row['updated_at'] ?? '',
'created_at_dot' => !empty($row['created_at']) ? date('y.m.d', strtotime($row['created_at'])) : '',
'is_consider' => ($statusCode === 'OF10002'),
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
// error_log($e->getMessage());
}
+1
View File
@@ -0,0 +1 @@
<?php
+1
View File
@@ -0,0 +1 @@
<?php
+65
View File
@@ -0,0 +1,65 @@
<?php
//=========================
// 한줄소감
// 동적변환처리로 인해 해당파일 사용안함 230324
//=========================
require_once __DIR__ . '/db_conn.php';
// TODO: 실제 로그인 세션 연동 후 교체
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$memberId = $_SESSION['member_id'] ?? '';
$sysCompCode = $_SESSION['sys_comp_code'] ?? '';
//$memberId = 'U001';
//$sysCompCode = 'COMP01';
$myCommentList = [];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$sql = "
SELECT
lh.content_id,
lh.comment,
lh.last_viewed_at,
c.title,
c.category_code,
c.category_group,
u.name
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
INNER JOIN edu_users u
ON u.member_id = lh.member_id
AND u.sys_comp_code = lh.sys_comp_code
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :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
LIMIT 20
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$myCommentList[] = [
'content_id' => $row['content_id'],
'title' => $row['title'],
'comment' => $row['comment'],
'member_name' => $row['name'],
'last_viewed_at' => $row['last_viewed_at'],
'date_dot' => !empty($row['last_viewed_at']) ? date('y.m.d', strtotime($row['last_viewed_at'])) : '',
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
+59
View File
@@ -0,0 +1,59 @@
<?php
//=========================
//한줄소감
//=========================
require_once __DIR__ . '/db_conn.php';
// TODO: 실제 로그인 세션 연동 후 교체
$memberId = 'U001';
$sysCompCode = 'COMP01';
$myCommentList = [];
try {
$pdo = db_conn();
$pdo->exec("SET NAMES 'utf8mb4'");
$sql = "
SELECT
lh.content_id,
lh.comment,
lh.last_viewed_at,
c.title,
c.category_code,
c.category_group,
u.name
FROM edu_learning_histories lh
INNER JOIN edu_contents c
ON c.content_id = lh.content_id
INNER JOIN edu_users u
ON u.member_id = lh.member_id
AND u.sys_comp_code = lh.sys_comp_code
WHERE lh.member_id = :member_id
AND lh.sys_comp_code = :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
LIMIT 20
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':member_id' => $memberId,
':sys_comp_code' => $sysCompCode,
]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$myCommentList[] = [
'content_id' => $row['content_id'],
'title' => $row['title'],
'comment' => $row['comment'],
'member_name' => $row['name'],
'last_viewed_at' => $row['last_viewed_at'],
'date_dot' => !empty($row['last_viewed_at']) ? date('y.m.d', strtotime($row['last_viewed_at'])) : '',
];
}
} catch (Throwable $e) {
// 운영 시 로깅 권장
}
+78
View File
@@ -0,0 +1,78 @@
<?php
require_once __DIR__ . '/db_conn.php';
require_once __DIR__ . '/api_log.php';
// 외부(호출하는 파일)에서 $SET_PREFIX를 정의하지 않았을 경우를 대비한 기본값 설정
$prefixCondition = isset($SET_PREFIX) ? $SET_PREFIX : 'L'; //L=리더십(기본값), I=인사이트
$memberId = 'U001';
$sysCompCode = 'COMP01';
//Svg
if($prefixCondition=='L'){
$prefixSvg = "leadership";
}else if($prefixCondition=='I'){
$prefixSvg = "insight";
}
//배너설정
if($prefixCondition=='L'){//L=리더십(기본값), I=인사이트
$array_banner_img = [
['normal' => '/edu/img/leadership/img_banner_01.png', 'mobile' => '/edu/img/leadership/img_banner_01_m.png', 'title' => '실천으로 완성하는 리더십'],
['normal' => '/edu/img/leadership/img_banner_02.png', 'mobile' => '/edu/img/leadership/img_banner_02_m.png', 'title' => '성장하는 리더십 여정'],
['normal' => '/edu/img/leadership/img_banner_03.png', 'mobile' => '/edu/img/leadership/img_banner_03_m.png', 'title' => '리더로 성장하는 과정'],
];
}else if($prefixCondition=='I'){//L=리더십(기본값), I=인사이트
$array_banner_img = [
['normal' => '/edu/img/insight/img_banner_01.png', 'mobile' => '/edu/img/insight/img_banner_01_m.png', 'title' => '배너 1'],
['normal' => '/edu/img/insight/img_banner_02.png', 'mobile' => '/edu/img/insight/img_banner_02_m.png', 'title' => '배너 2'],
];
}
$dbSuccess = false;
$array_tab_info = [];
try {
$pdo = db_conn();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES 'utf8mb4'");
// LIKE 연산자를 안전하게 처리하기 위해 준비된 선언(Prepared Statement) 사용
$stmtCode = $pdo->prepare("
SELECT group_code, base_code, code, code_name
FROM edu_codes
WHERE is_active = 1
AND group_code = 'CA200'
AND code LIKE :prefix
ORDER BY base_code ASC
");
// 'L%' 형태로 바인딩하여 검색
$stmtCode->execute([':prefix' => $prefixCondition . '%']);
$rows = $stmtCode->fetchAll(PDO::FETCH_ASSOC);
if ($rows) {
$i = 1;
foreach ($rows as $cr) {
$index = sprintf('%02d', $i);
$index_src = '/edu/img/ico/ico_'.$prefixSvg.'_' . $index . '.svg';
$array_tab_info[] = [
'base_code' => $cr['base_code'],
'code_name' => $cr['code_name'],
'src' => $index_src
];
$i++;
}
}
$dbSuccess = true;
} catch (Exception $e) {
$errMsg = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
error_log('[leadership_init_data.php] DB Error: ' . $errMsg);
// api_log 함수가 있다면 기록
if (function_exists('api_log')) {
api_log('leadership_init', 'DB_ERROR', ['error' => $errMsg]);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
/**
* sync_users.php - 낮 12시, 밤 12시 자동 실행용
*/
declare(strict_types=1);
require_once __DIR__ . '/db_conn.php';
try {
$pdo = db_conn();
// 1. 현재 우리 DB에 등록된 전체 사용자 ID 가져오기
$stmt = $pdo->query("SELECT member_id FROM edu_users WHERE is_active = 'Y'");
$members = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo "[" . date('Y-m-d H:i:s') . "] 동기화 시작 (대상: " . count($members) . "명)\n";
foreach ($members as $mid) {
// 2. 인트라넷 API 호출
$apiUrl = "http://erp.hanmaceng.co.kr/intranet/sys/model/EduJsonAPI.php?member_id=" . urlencode($mid);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
curl_close($ch);
if ($response) {
$apiData = json_decode($response, true);
if (($apiData['rstCd'] ?? '') == 200 && !empty($apiData['data'])) {
$userData = $apiData['data'][0];
$name = $userData['NM'] ?? $userData['name']; // 실제 API 키값 확인 필요
$rank = $userData['RANK_NM'] ?? $userData['rank_name'];
// 3. 정보 업데이트 (이름이나 직책이 변경되었을 수 있으니 갱신)
$upd = $pdo->prepare("UPDATE edu_users SET name = ?, rank_name = ?, updated_at = NOW() WHERE member_id = ?");
$upd->execute([$name, $rank, $mid]);
echo " - ID {$mid}: 업데이트 완료 ({$name} {$rank})\n";
}
}
// 서버 부하 방지를 위해 0.1초씩 쉬어줌
usleep(100000);
}
echo "전체 동기화 완료!\n";
} catch (Exception $e) {
error_log("[Sync Error] " . $e->getMessage());
echo "오류 발생: " . $e->getMessage();
}
+27
View File
@@ -0,0 +1,27 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
// 트래픽 낭비를 막기 위해 단 1분 30초만 짧게 테스트합니다.
export const options = {
stages: [
{ duration: '30s', target: 30 }, // 30초 동안 동시 접속자를 30명까지 올림
{ duration: '30s', target: 30 }, // 30명 동시 접속 상태를 30초 유지 (이때 에러가 나는지 집중 확인!)
{ duration: '30s', target: 0 }, // 30초 동안 접속 서서히 종료
],
};
export default function () {
const baseUrl = 'https://baroncs.co.kr/edu/skin/index.php'; // 실제 서비스 도메인으로 변경
// [Point] 불필요한 하위 페이지는 빼고, 쿼리가 가장 많이 도는 메인 페이지만 타격합니다.
let res = http.get(`${baseUrl}/`);
check(res, {
'정상 접속(200)': (r) => r.status === 200,
'빠른 응답(<1초)': (r) => r.timings.duration < 1000,
});
// [Point] 방화벽 디도스(DDoS) 영구 차단을 피하기 위해 요청 간격을 여유 있게 줍니다.
sleep(Math.random() * 4 + 2); // 2~6초 사이 랜덤 대기
}
+26
View File
@@ -0,0 +1,26 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
// 트래픽 낭비를 막기 위해 단 1분 30초만 짧게 테스트합니다.
export const options = {
stages: [
{ duration: '30s', target: 30 }, // 30초 동안 동시 접속자를 30명까지 올림
{ duration: '30s', target: 30 }, // 30명 동시 접속 상태를 30초 유지 (이때 에러가 나는지 집중 확인!)
{ duration: '30s', target: 0 }, // 30초 동안 접속 서서히 종료
],
};
export default function () {
const baseUrl = 'https://domain.cafe24.com'; // 실제 서비스 도메인으로 변경
// [Point] 불필요한 하위 페이지는 빼고, 쿼리가 가장 많이 도는 메인 페이지만 타격합니다.
let res = http.get(`${baseUrl}/`);
check(res, {
'정상 접속(200)': (r) => r.status === 200,
'빠른 응답(<1초)': (r) => r.timings.duration < 1000,
});
// [Point] 방화벽 디도스(DDoS) 영구 차단을 피하기 위해 요청 간격을 여유 있게 줍니다.
sleep(Math.random() * 4 + 2); // 2~6초 사이 랜덤 대기
}