297 lines
11 KiB
PHP
297 lines
11 KiB
PHP
<?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);
|
|
}
|