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