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); }