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
+860
View File
@@ -0,0 +1,860 @@
<?php
require_once __DIR__ . '/../bbs/auth.php';
edu_require_login();
$mode = isset($_GET['mode']) ? trim((string) $_GET['mode']) : 'initial';
$completedGoalId = isset($_GET['completed_goal']) ? (int) $_GET['completed_goal'] : 1;
$debugRecsRaw = isset($_GET['debug_recs']) ? strtolower(trim((string)$_GET['debug_recs'])) : '';
$debugRecs = in_array($debugRecsRaw, ['1', 'true', 'yes', 'on', 'y', 'debug'], true)
|| (isset($_SERVER['QUERY_STRING']) && strpos((string)$_SERVER['QUERY_STRING'], 'debug_recs') !== false);
$debugRecsPayload = [];
if ($debugRecs && !headers_sent()) {
header('X-Debug-Recs: on');
}
// ========== DB 연동 초기화 ==========
require_once __DIR__ . '/../bbs/db_conn.php';
// 세션 정보로부터 사용자 ID 및 정보 추출
$memberId = $_SESSION['member_id'] ?? null;
$userName = $_SESSION['member_name'] ?? $_SESSION['user_name'] ?? '사용자';
$userPosition = $_SESSION['member_rank'] ?? $_SESSION['user_position'] ?? '';
$userCompany = $_SESSION['sys_comp_code'] ?? '';
// ========== 년도/분기/남은일수 계산 ==========
$today = new DateTime('now', new DateTimeZone('Asia/Seoul'));
$currentYear = (int) $today->format('Y');
$currentMonth = (int) $today->format('m');
$currentQuarterNum = (int) ceil($currentMonth / 3);
$currentQuarterCode = sprintf('CA200Q%02d', $currentQuarterNum);
$modalQuarterLabel = substr((string)$currentYear, 2) . '년 ' . $currentQuarterNum . '분기)';
// DB에서 추가 정보 보충 (member_id + sys_comp_code 복합키 기준)
if (isset($memberId) && $memberId !== null && $memberId !== '' && $userCompany !== '' && ($userPosition === '' || $userName === '' || $userName === '사용자')) {
try {
$stmt = db_conn()->prepare(
'SELECT name, rank_name, sys_comp_code FROM edu_users WHERE member_id = ? AND sys_comp_code = ? LIMIT 1'
);
$stmt->execute([(string)$memberId, (string)$userCompany]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
if ($userName === '' || $userName === '사용자') {
$userName = $row['name'] ?? '사용자';
}
if ($userPosition === '') {
$userPosition = $row['rank_name'] ?? '';
}
}
} catch (Throwable $e) {
error_log('[myclass.php] DB fetch error: ' . $e->getMessage());
}
}
// ========== 목표 설정 여부 확인 → 이미 설정한 사용자는 myclass_list.php로 바로 이동 ==========
// mode=initial(기본 진입)일 때만 체크. completed/additional 모드는 목표 완료 플로우이므로 통과.
if ($mode === 'initial' && isset($memberId) && !$debugRecs) {
try {
$sysCompCodeCheck = (string)($userCompany ?? '');
$hasGoal = false;
if ($memberId !== null && $memberId !== '' && $sysCompCodeCheck !== '') {
$stmtGoalCheck = db_conn()->prepare(
"SELECT COUNT(*) FROM edu_user_learning_goals
WHERE member_id = ?
AND sys_comp_code = ?
AND quarter = ?
AND is_active = '1'
LIMIT 1"
);
$stmtGoalCheck->execute([(string)$memberId, $sysCompCodeCheck, $currentQuarterCode]);
$hasGoal = (int)$stmtGoalCheck->fetchColumn() > 0;
}
if ($hasGoal) {
header('Location: /skin/myclass_list.php');
exit;
}
} catch (Throwable $e) {
error_log('[myclass.php] goal check error: ' . $e->getMessage());
// 오류 시 그냥 myclass.php 렌더링 계속
}
}
// 분기 마지막 날짜
$quarterEndDates = [
1 => '03-31', 2 => '06-30', 3 => '09-30', 4 => '12-31'
];
$todayDateOnly = (clone $today)->setTime(0, 0, 0);
$targetDateStr = $currentYear . '-' . $quarterEndDates[$currentQuarterNum];
$targetDate = new DateTime($targetDateStr, new DateTimeZone('Asia/Seoul'));
$targetDate->setTime(0, 0, 0);
$daysInterval = $todayDateOnly->diff($targetDate);
$daysRemaining = max(0, (int) $daysInterval->format('%a'));
$daysDisplay = 'D-' . $daysRemaining;
// 분기 마지막 날짜 (한글)
$endDateParts = explode('-', $quarterEndDates[$currentQuarterNum]);
$endMonth = (int) $endDateParts[0];
$endDay = (int) $endDateParts[1];
$targetDateKr = $endMonth . '월 ' . $endDay . '일';
$completedGoalId = max(1, min(6, $completedGoalId));
$defaultGoals = [
['id' => 1, 'title' => '자기이해와 강점 찾기', 'desc' => '나의 성향, 가치관, 강점 등을 탐색하고 이해하며 자기인식을 높이는 과정', 'icon' => '01', 'gif' => 'ico_study_01.png', 'json' => 'ico_study_01.json'],
['id' => 2, 'title' => '효과적인\n의사소통 배우기', 'desc' => '상황에 맞는 말하기와 경청을 통해 타인과 원활하게 소통하는 방법을 익히는 과정', 'icon' => '02', 'gif' => 'ico_study_02.png', 'json' => 'ico_study_02.json'],
['id' => 3, 'title' => '감정 조절과 자기관리', 'desc' => '다양한 감정을 인식하고 조절하여 안정적인 삶을 유지하는 자기관리 훈련 과정', 'icon' => '03', 'gif' => 'ico_study_03.png', 'json' => 'ico_study_03.json'],
['id' => 4, 'title' => '비판적 사고와 문제 해결', 'desc' => '다양한 관점에서 생각하고 문제 상황을 논리적으로 해결하는 능력을 기르는 과정', 'icon' => '04', 'gif' => 'ico_study_04.png', 'json' => 'ico_study_04.json'],
['id' => 5, 'title' => '협업과 팀워크 기르기', 'desc' => '타인과 협력하며 공동의 목표를 위해 함께 노력하는 태도와 기술을 기르는 과정', 'icon' => '05', 'gif' => 'ico_study_05.png', 'json' => 'ico_study_05.json'],
['id' => 6, 'title' => '커리어 탐색과 역량 개발', 'desc' => '직무와 산업의 흐름을 이해하고, 자신의 커리어 방향성과 필요한 역량을 점검·계획하는 과정', 'icon' => '06', 'gif' => 'ico_study_06.png', 'json' => 'ico_study_06.json'],
];
$defaultModalBooks = [
['id' => 1, 'youtube' => 'KE_MeQZgnPM', 'title' => '조직도가 리셋된다!', 'sub' => '위계 중심 조직은\n더 이상 통하지 않습니다.', 'main' => '더 유연하게\n일할 방법', 'tag' => 'IT 테크', 'img' => 'img_book_01'],
['id' => 2, 'youtube' => 'a2l1uZfsRi0', 'title' => '혈당 스파이크', 'sub' => '식후 졸림은 의지 문제가\n아니라 혈당의 문제!', 'main' => '혈당을 안정시켜줄\n실생활 관리법', 'tag' => '웰니스', 'img' => 'img_book_02'],
['id' => 3, 'youtube' => 'IeF8r0ycgVg', 'title' => '제대로 쉬는 방법', 'sub' => '아무리 자도\n피곤한가요?', 'main' => '진짜 회복되는 쉼이\n무엇인지 알게됩니다.', 'tag' => '마인드셋', 'img' => 'img_book_03'],
['id' => 4, 'youtube' => 'KMZXMI0QPoA', 'title' => "'AI 도파민'의 바다에 빠진 이유", 'sub' => 'AI는 선택이 아닌\n생존의 도구', 'main' => 'AI는 도구가 아닌\n업무 파트너', 'tag' => 'IT 테크', 'img' => 'img_book_04'],
['id' => 5, 'youtube' => 'CRKwszz6l2M', 'title' => '살찌고 망가진몸 되살리는 방법', 'sub' => '몸을 망쳤다면?\n다이어트 아닌 리셋이 답!', 'main' => '건강한 몸 되찾기', 'tag' => '웰니스', 'img' => 'img_book_05'],
['id' => 6, 'youtube' => 'Gf5WoZ3BmgI', 'title' => '자기 관점의 힘', 'sub' => '당신의 삶을 바꾸는\n가장 강력한 무기?', 'main' => '자신만의 관점으로\n경쟁력을 키우는 방법', 'tag' => '리더십', 'img' => 'img_book_06'],
];
$goals = $defaultGoals;
$goalCodeById = [];
$defaultRecommendedItems = [
['title' => '업무에 익숙해졌지만 성장 정체를 느끼는 구성원', 'description' => "창의적 사고를 '아이디어가 아니라 업무를 바라보는 방식으로 재정렬 할 수 있어요!"],
['title' => '문제 해결을 요구받기 시작한 주니어, 중급 구성원', 'description' => "창의적 사고를 '아이디어가 아니라 업무를 바라보는 방식으로 재정렬 할 수 있어요!"],
['title' => '팀 또는 프로젝트 단위로 사고의 확장이 필요한 구성원', 'description' => "창의적 사고를 '아이디어가 아니라 업무를 바라보는 방식으로 재정렬 할 수 있어요!"],
];
$goalRecommendations = [];
for ($i = 1; $i <= 6; $i++) {
$goalRecommendations[$i] = $defaultRecommendedItems;
}
$goalBooksById = [];
for ($i = 1; $i <= 6; $i++) {
$goalBooksById[$i] = [];
}
function myclass_extract_video_id($value) {
$value = trim((string)$value);
if ($value === '') {
return '';
}
if (preg_match('~(?:v=|\.be/)([A-Za-z0-9_-]{11})~', $value, $m)) {
return $m[1];
}
if (preg_match('/^[A-Za-z0-9_-]{11}$/', $value)) {
return $value;
}
return '';
}
// 목표 카드 6개: edu_learning_goals.title / edu_learning_goals.remarks 연동
try {
$stmtGoals = db_conn()->prepare(
"SELECT goal_code,
title AS goal_title,
remarks AS goal_remarks
FROM edu_learning_goals
WHERE is_active = '1'
AND base_year = ?
AND quarter = ?
ORDER BY goal_code ASC"
);
$stmtGoals->execute([(string)$currentYear, $currentQuarterCode]);
$goalRows = $stmtGoals->fetchAll(PDO::FETCH_ASSOC);
// 현재 연도/분기 데이터가 부족하면 동일 분기의 최신 연도로 fallback
if (count($goalRows) < 6) {
$stmtGoals = db_conn()->query(
"SELECT goal_code,
title AS goal_title,
remarks AS goal_remarks
FROM edu_learning_goals
WHERE is_active = '1'
AND quarter = " . db_conn()->quote($currentQuarterCode) . "
ORDER BY base_year DESC, goal_code ASC
LIMIT 6"
);
$goalRows = $stmtGoals->fetchAll(PDO::FETCH_ASSOC);
}
$goalSlot = 1;
foreach ($goalRows as $goalRow) {
if ($goalSlot > 6) {
break;
}
$goalCode = (string)($goalRow['goal_code'] ?? '');
$title = trim((string)($goalRow['goal_title'] ?? ''));
$desc = trim((string)($goalRow['goal_remarks'] ?? ''));
if ($goalCode === '') {
continue;
}
$goalId = $goalSlot;
$goalCodeById[$goalId] = $goalCode;
if ($title !== '') {
$goals[$goalId - 1]['title'] = $title;
}
if ($desc !== '') {
$goals[$goalId - 1]['desc'] = $desc;
}
$goalSlot++;
}
} catch (Throwable $e) {
error_log('[myclass.php] edu_learning_goals fetch error: ' . $e->getMessage());
}
// 목표별 추천 3개: goal_code + seq 기준으로 연동 (rec-text는 title2 우선 사용)
try {
$goalCodes = array_values(array_unique(array_filter($goalCodeById)));
if (count($goalCodes) > 0) {
$normalizeGoalCode = static function ($value) {
return strtoupper(trim((string)$value));
};
$goalIdByCode = [];
foreach ($goalCodeById as $goalId => $goalCodeRaw) {
$normalizedCode = $normalizeGoalCode($goalCodeRaw);
if ($normalizedCode === '') {
continue;
}
$goalIdByCode[$normalizedCode] = (int)$goalId;
}
$normalizedGoalCodes = [];
foreach ($goalCodes as $goalCodeRaw) {
$normalizedCode = $normalizeGoalCode($goalCodeRaw);
if ($normalizedCode !== '') {
$normalizedGoalCodes[] = $normalizedCode;
}
}
$normalizedGoalCodes = array_values(array_unique($normalizedGoalCodes));
if (count($normalizedGoalCodes) === 0) {
throw new RuntimeException('goal codes are empty after normalization');
}
$goalSuffixes = [];
foreach (array_keys($goalIdByCode) as $normalizedCode) {
if (preg_match('/-(\d{3})$/', $normalizedCode, $m)) {
$goalSuffixes[] = '-' . $m[1];
}
}
$goalSuffixes = array_values(array_unique($goalSuffixes));
$placeholders = implode(',', array_fill(0, count($normalizedGoalCodes), '?'));
$suffixPlaceholders = count($goalSuffixes) > 0
? implode(',', array_fill(0, count($goalSuffixes), '?'))
: '';
$pdo = db_conn();
$hasColumn = static function (PDO $conn, $tableName, $columnName) {
$stmt = $conn->prepare(
"SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?"
);
$stmt->execute([(string)$tableName, (string)$columnName]);
return ((int)$stmt->fetchColumn()) > 0;
};
$hasTable = static function (PDO $conn, $tableName) {
$stmt = $conn->prepare(
"SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = ?"
);
$stmt->execute([(string)$tableName]);
return ((int)$stmt->fetchColumn()) > 0;
};
// 운영 환경별 오탈자 대응: edu_recommended_goals / edu_recommended_gaols
// 둘 다 존재할 수 있으므로, 실제 현재 목표코드와 매칭되는 row 수가 많은 테이블을 선택
$availableRecTables = [];
foreach (['edu_recommended_goals', 'edu_recommended_gaols'] as $candTable) {
if ($hasTable($pdo, $candTable)) {
$availableRecTables[] = $candTable;
}
}
if (count($availableRecTables) === 0) {
throw new RuntimeException('recommended goals table not found');
}
$tableScores = [];
$recTable = $availableRecTables[0];
$bestScore = -1;
foreach ($availableRecTables as $candTable) {
$candHasQuarter = $hasColumn($pdo, $candTable, 'quarter');
$candHasBaseYear = $hasColumn($pdo, $candTable, 'base_year');
$candHasIsActive = $hasColumn($pdo, $candTable, 'is_active');
$candCodeCondition = "UPPER(TRIM(goal_code)) IN ($placeholders)";
if ($suffixPlaceholders !== '') {
$candCodeCondition .= " OR RIGHT(UPPER(TRIM(goal_code)), 4) IN ($suffixPlaceholders)";
}
$candWhere = [];
if ($candHasIsActive) {
$candWhere[] = "is_active = '1'";
}
$candWhere[] = "({$candCodeCondition})";
if ($candHasQuarter) {
$candWhere[] = 'quarter = ?';
}
if ($candHasBaseYear) {
$candWhere[] = 'base_year = ?';
}
$candSql = "SELECT COUNT(*) FROM {$candTable} WHERE " . implode(' AND ', $candWhere);
$candParams = $normalizedGoalCodes;
if (count($goalSuffixes) > 0) {
$candParams = array_merge($candParams, $goalSuffixes);
}
if ($candHasQuarter) {
$candParams[] = $currentQuarterCode;
}
if ($candHasBaseYear) {
$candParams[] = (string)$currentYear;
}
$stmtScore = $pdo->prepare($candSql);
$stmtScore->execute($candParams);
$score = (int)$stmtScore->fetchColumn();
$tableScores[$candTable] = $score;
if ($score > $bestScore) {
$bestScore = $score;
$recTable = $candTable;
}
}
$hasTitle2Col = $hasColumn($pdo, $recTable, 'title2');
$hasQuarterCol = $hasColumn($pdo, $recTable, 'quarter');
$hasBaseYearCol = $hasColumn($pdo, $recTable, 'base_year');
$hasIsActiveCol = $hasColumn($pdo, $recTable, 'is_active');
$hasSeqCol = $hasColumn($pdo, $recTable, 'seq');
$hasSortOrderCol = $hasColumn($pdo, $recTable, 'sort_order');
$hasDescCol = $hasColumn($pdo, $recTable, 'description');
$title2Expr = $hasTitle2Col ? 'rg.title2' : "''";
$recDescExpr = $hasDescCol ? 'rg.description' : "''";
$seqExpr = $hasSeqCol ? 'rg.seq' : ($hasSortOrderCol ? 'rg.sort_order' : '0');
$joinSeqCondition = '';
if ($hasSeqCol) {
$joinSeqCondition = ' AND c.sort_order = rg.seq';
} elseif ($hasSortOrderCol) {
$joinSeqCondition = ' AND c.sort_order = rg.sort_order';
}
$codeCondition = "UPPER(TRIM(rg.goal_code)) IN ($placeholders)";
if ($suffixPlaceholders !== '') {
$codeCondition .= " OR RIGHT(UPPER(TRIM(rg.goal_code)), 4) IN ($suffixPlaceholders)";
}
$whereConditions = [];
if ($hasIsActiveCol) {
$whereConditions[] = "rg.is_active = '1'";
}
$whereConditions[] = "({$codeCondition})";
if ($hasQuarterCol) {
$whereConditions[] = 'rg.quarter = ?';
}
if ($hasBaseYearCol) {
$whereConditions[] = 'rg.base_year = ?';
}
$whereSql = implode("\n AND ", $whereConditions);
$stmtRec = $pdo->prepare(
"SELECT rg.goal_code,
{$seqExpr} AS seq,
rg.title AS rec_title,
{$recDescExpr} AS rec_description,
{$title2Expr} AS rec_title2,
c.title AS content_title,
c.description AS content_description
FROM {$recTable} rg
LEFT JOIN edu_contents c
ON c.goal_code = rg.goal_code
{$joinSeqCondition}
AND c.is_active = '1'
WHERE {$whereSql}
ORDER BY rg.goal_code ASC, seq ASC"
);
$stmtRecParams = $normalizedGoalCodes;
if (count($goalSuffixes) > 0) {
$stmtRecParams = array_merge($stmtRecParams, $goalSuffixes);
}
if ($hasQuarterCol) {
$stmtRecParams[] = $currentQuarterCode;
}
if ($hasBaseYearCol) {
$stmtRecParams[] = (string)$currentYear;
}
$stmtRec->execute($stmtRecParams);
$recRows = $stmtRec->fetchAll(PDO::FETCH_ASSOC);
if ($debugRecs) {
$debugRecsPayload['table'] = $recTable;
$debugRecsPayload['tableScores'] = $tableScores;
$debugRecsPayload['schema'] = [
'hasTitle2Col' => $hasTitle2Col,
'hasQuarterCol' => $hasQuarterCol,
'hasBaseYearCol' => $hasBaseYearCol,
'hasIsActiveCol' => $hasIsActiveCol,
'hasSeqCol' => $hasSeqCol,
'hasSortOrderCol' => $hasSortOrderCol,
'hasDescCol' => $hasDescCol,
];
$debugRecsPayload['normalizedGoalCodes'] = $normalizedGoalCodes;
$debugRecsPayload['stmtRecParams'] = $stmtRecParams;
$debugRecsPayload['recRows'] = $recRows;
error_log('[myclass.php][debug_recs] table=' . $recTable . ' normalizedGoalCodes=' . json_encode($normalizedGoalCodes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
error_log('[myclass.php][debug_recs] recRows=' . json_encode($recRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
$fallbackSeqByGoalId = [];
foreach ($recRows as $recRow) {
$goalCode = $normalizeGoalCode($recRow['goal_code'] ?? '');
$seq = (int)($recRow['seq'] ?? 0);
$recTitle = trim((string)($recRow['rec_title'] ?? ''));
$recTitle2 = trim((string)($recRow['rec_title2'] ?? ''));
$recDesc = trim((string)($recRow['rec_description'] ?? ''));
$contentTitle = trim((string)($recRow['content_title'] ?? ''));
$goalId = 0;
if ($goalCode !== '' && isset($goalIdByCode[$goalCode])) {
$goalId = (int)$goalIdByCode[$goalCode];
} elseif (preg_match('/-(\d{3})$/', $goalCode, $m)) {
$suffixGoalId = (int)$m[1];
if ($suffixGoalId >= 1 && $suffixGoalId <= 6) {
$goalId = $suffixGoalId;
}
}
if ($goalId < 1 || $goalId > 6) {
continue;
}
if ($seq < 1 || $seq > 3) {
$fallbackSeqByGoalId[$goalId] = (int)($fallbackSeqByGoalId[$goalId] ?? 0) + 1;
$seq = $fallbackSeqByGoalId[$goalId];
if ($seq < 1 || $seq > 3) {
continue;
}
}
if ($recTitle === '') {
$recTitle = $contentTitle;
}
if ($recTitle2 !== '') {
$recDesc = $recTitle2;
}
if ($recTitle !== '') {
$goalRecommendations[$goalId][$seq - 1]['title'] = $recTitle;
}
if ($recDesc !== '') {
$goalRecommendations[$goalId][$seq - 1]['description'] = $recDesc;
}
if ($debugRecs) {
error_log('[myclass.php][debug_recs] mapped goalCode=' . $goalCode . ' seq=' . $seq . ' title=' . $recTitle . ' title2=' . $recTitle2 . ' desc=' . $recDesc);
}
}
}
} catch (Throwable $e) {
if ($debugRecs) {
$debugRecsPayload['recsError'] = $e->getMessage();
}
error_log('[myclass.php] edu_recommended_goals fetch error: ' . $e->getMessage());
}
// 목표별 영상 리스트: edu_contents.title/description1/description2/content_url/thumbnail 연동
try {
$goalCodes = array_values(array_unique(array_filter($goalCodeById)));
if (count($goalCodes) > 0) {
$goalIdByCode = array_flip($goalCodeById);
$placeholders = implode(',', array_fill(0, count($goalCodes), '?'));
$stmtContents = db_conn()->prepare(
"SELECT content_id,
goal_code,
sort_order,
title,
description,
description1,
description2,
content_url,
thumbnail_url
FROM edu_contents
WHERE is_active = '1'
AND category_code = 'CA10001'
AND category_group = ?
AND goal_code IN ($placeholders)
ORDER BY goal_code ASC, sort_order ASC, content_id ASC"
);
$stmtContents->execute(array_merge([$currentQuarterCode], $goalCodes));
$contentRows = $stmtContents->fetchAll(PDO::FETCH_ASSOC);
foreach ($contentRows as $contentRow) {
$goalCode = (string)($contentRow['goal_code'] ?? '');
if (!isset($goalIdByCode[$goalCode])) {
continue;
}
$goalId = (int)$goalIdByCode[$goalCode];
if (!isset($goalBooksById[$goalId])) {
$goalBooksById[$goalId] = [];
}
$contentUrl = trim((string)($contentRow['content_url'] ?? ''));
$thumbnailUrl = trim((string)($contentRow['thumbnail_url'] ?? ''));
$sortOrder = (int)($contentRow['sort_order'] ?? 0);
if ($sortOrder < 1 || $sortOrder > 6) {
continue;
}
$templateBook = $defaultModalBooks[$sortOrder - 1] ?? null;
if ($templateBook === null) {
continue;
}
if (count($goalBooksById[$goalId]) === 0) {
$goalBooksById[$goalId] = $defaultModalBooks;
}
$description1 = trim((string)($contentRow['description1'] ?? ''));
$description2 = trim((string)($contentRow['description2'] ?? ''));
$description = trim((string)($contentRow['description'] ?? ''));
if ($description1 === '') {
$description1 = $description;
}
$goalBooksById[$goalId][$sortOrder - 1] = [
'id' => (int)($templateBook['id'] ?? $sortOrder),
'order' => $sortOrder,
'youtube' => ($tmpVideoId = myclass_extract_video_id($contentUrl)) !== '' ? $tmpVideoId : (string)($templateBook['youtube'] ?? ''),
'content_url' => $contentUrl,
'title' => trim((string)($contentRow['title'] ?? (string)($templateBook['title'] ?? ''))),
'sub' => $description1 !== '' ? $description1 : (string)($templateBook['sub'] ?? ''),
'main' => $description2 !== '' ? $description2 : (string)($templateBook['main'] ?? ''),
'tag' => (string)($templateBook['tag'] ?? ''),
'img' => (string)($templateBook['img'] ?? 'img_book_01'),
'thumbnail' => $thumbnailUrl,
];
}
foreach ($goalBooksById as $goalId => $goalBooks) {
if (count($goalBooks) > 0) {
ksort($goalBooks);
$goalBooksById[$goalId] = array_values($goalBooks);
} else {
$goalBooksById[$goalId] = $defaultModalBooks;
}
}
}
} catch (Throwable $e) {
error_log('[myclass.php] edu_contents fetch error: ' . $e->getMessage());
}
foreach ($goalBooksById as $goalId => $goalBooks) {
if (count($goalBooks) === 0) {
$goalBooksById[$goalId] = $defaultModalBooks;
}
}
$modalBooks = $goalBooksById[1] ?? $defaultModalBooks;
$modalRecItems = $goalRecommendations[1] ?? $defaultRecommendedItems;
if ($debugRecs) {
$debugRecsPayload['goalCodeById'] = $goalCodeById;
$debugRecsPayload['goalRecommendations1'] = $goalRecommendations[1] ?? [];
$debugRecsPayload['modalRecItems'] = $modalRecItems;
error_log('[myclass.php][debug_recs] goalCodeById=' . json_encode($goalCodeById, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
error_log('[myclass.php][debug_recs] goalRecommendations[1]=' . json_encode($goalRecommendations[1] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
error_log('[myclass.php][debug_recs] modalRecItems=' . json_encode($modalRecItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
// 실제 완료된 목표 목록 조회 (DB 기준)
$completedGoalIds = [];
if (isset($memberId) && $userCompany !== '') {
try {
$stmtCompleted = db_conn()->prepare(
"SELECT g.goal_code, g.title, g.remarks, u.completed_date, u.goal_code AS user_goal_code
FROM edu_user_learning_goals u
JOIN edu_learning_goals g ON u.goal_code = g.goal_code AND g.is_active = '1'
WHERE u.member_id = ?
AND u.sys_comp_code = ?
AND u.is_active = '1'
AND g.quarter = ?
AND u.completed_date IS NOT NULL"
);
$stmtCompleted->execute([(string)$memberId, (string)$userCompany, $currentQuarterCode]);
$rowsCompleted = $stmtCompleted->fetchAll(PDO::FETCH_ASSOC);
foreach ($rowsCompleted as $row) {
// goalCodeById: [1=>goal_code1, 2=>goal_code2, ...]
$goalCode = (string)($row['user_goal_code'] ?? $row['goal_code'] ?? '');
$goalId = array_search($goalCode, $goalCodeById, true);
if ($goalId !== false) {
$completedGoalIds[] = (int)$goalId;
}
}
} catch (Throwable $e) {
error_log('[myclass.php] fetch completed goals error: ' . $e->getMessage());
}
}
// completed/additional 화면은 DB completed_date(트리거 결과)가 확인될 때만 허용
$isTriggeredCompletion = in_array($completedGoalId, $completedGoalIds, true);
if (in_array($mode, ['completed', 'additional'], true) && !$isTriggeredCompletion) {
$mode = 'initial';
}
$completedGoal = $goals[$completedGoalId - 1];
$isCompletionFlow = in_array($mode, ['completed', 'additional'], true);
?>
<!doctype html>
<html lang="ko">
<head>
<?php include(__DIR__ . '/_include/_head.php'); ?>
<link rel="stylesheet" type="text/css" href="/css/style.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
</head>
<body>
<div class="wrap myclass">
<?php include(__DIR__ . '/_include/_header.php'); ?>
<div class="container">
<div class="myclass-wrap">
<div class="myclass-top-bar">
<div class="top-bar-left">
<!-- <div class="user-rank-area">
<div class="user-rank-area-inner">
<div class="user-profile">
<img src="/img/insight/profile.png" alt="프로필 이미지" />
</div>
<span class="rank-text">홍길동님 현재 <em>128</em>등 <span class="rank-change">(지난 주 대비 4↑)</span></span>
<span class="rank-text">홍길동님 현재 <em>1250</em>등</span>
</div>
<div class="rank-toggle-wrap">
<button class="btn-rank-toggle" type="button" aria-label="순위 상세 보기" aria-expanded="false" aria-controls="rankListPopup">
<span class="ico-trophy" aria-hidden="true">
<img class="lottie-fallback" src="/img/myclass/ico_trophy.gif" alt="" aria-hidden="true" />
<span class="lottie-icon lottie-trophy" data-lottie-url="/img/myclass/ico_trophy.json"></span>
</span>
<span class="ico-chevron-down" aria-hidden="true"></span>
</button>
<div class="rank-list-popup" id="rankListPopup" role="region" aria-label="순위 목록" hidden>
<ul class="rank-list">
<li class="rank-item rank-1"><span class="rank-medal rank-gold" aria-hidden="true"><img src="/img/myclass/ico_medal_gold.png" alt="금메달 아이콘" /></span><span class="rank-name">박길동 <small>(바론컨설턴트)</small></span></li>
<li class="rank-item rank-2"><span class="rank-medal rank-silver" aria-hidden="true"><img src="/img/myclass/ico_medal_silver.png" alt="은메달 아이콘" /></span><span class="rank-name">홍길동 <small>(한맥기술)</small></span></li>
<li class="rank-item rank-3"><span class="rank-medal rank-bronze" aria-hidden="true"><img src="/img/myclass/ico_medal_bronze.png" alt="동메달 아이콘" /></span><span class="rank-name">김철수 <small>(삼안)</small></span></li>
<li class="rank-item rank-4"><span class="rank-num">4.</span><span class="rank-name">박철수 <small>(바른컨설턴트)</small></span></li>
<li class="rank-item rank-5"><span class="rank-num">5.</span><span class="rank-name">김영희 <small>(한맥기술)</small></span></li>
</ul>
</div>
</div>
</div> -->
</div>
<div class="page-intro" id="pageIntro">
<?php
// DB에서 현재 유저의 활성 목표 title 가져오기 (최신 분기 기준)
$activeGoalTitle = null;
if (isset($memberId) && $userCompany !== '') {
try {
$stmtActiveGoal = db_conn()->prepare(
"SELECT g.title
FROM edu_user_learning_goals u
JOIN edu_learning_goals g ON u.goal_code = g.goal_code AND g.is_active = '1'
WHERE u.member_id = ?
AND u.sys_comp_code = ?
AND u.is_active = '1'
AND g.quarter = ?
ORDER BY u.completed_date IS NULL DESC, u.completed_date ASC, u.goal_code ASC
LIMIT 1"
);
$stmtActiveGoal->execute([(string)$memberId, (string)$userCompany, $currentQuarterCode]);
$activeGoalTitle = $stmtActiveGoal->fetchColumn();
} catch (Throwable $e) {
error_log('[myclass.php] active goal title fetch error: ' . $e->getMessage());
}
}
?>
<?php if ($mode === 'completed') : ?>
<div class="page-title">
<h3><?php echo $currentYear; ?>년도 <?php echo $currentQuarterNum; ?>분기의 첫번째 목표를 완성했어요!</h3>
<p><em>다음 목표를 선택</em>해, 나만의 책장을 더 풍성하게 채워보세요.</p>
</div>
<?php elseif ($mode === 'additional') : ?>
<div class="page-title">
<h3><em>축하합니다.</em> <strong>[<?php echo htmlspecialchars($completedGoal['title'], ENT_QUOTES, 'UTF-8'); ?>]</strong>, 빛나는 책장을 완성했어요.</h3>
<p><em>다음 책장</em>을 열어볼까요?</p>
</div>
<?php else : ?>
<div class="page-title page-title--goal" data-state="initial">
<h3>
<em><?php echo htmlspecialchars($userName, ENT_QUOTES, 'UTF-8') . ' ' . htmlspecialchars($userPosition, ENT_QUOTES, 'UTF-8'); ?></em>님,
<strong><?php echo $currentYear; ?>년도 <?php echo $currentQuarterNum; ?>분기 학습 목표</strong>를 골라볼까요?
</h3>
<p class="page-title-deadline">
<span class="deadline-highlight"><?php echo $daysDisplay; ?><small>(<?php echo $targetDateKr; ?>)</small></span>
<span class="deadline-body">
<?php if ($activeGoalTitle) : ?>
현재 <strong>선택한 목표</strong>: <strong style="color:#f5b800;"><?php echo htmlspecialchars($activeGoalTitle, ENT_QUOTES, 'UTF-8'); ?></strong>
<?php else : ?>
<strong>선택한 목표</strong>와 함께 배움을 채워가세요.
<?php endif; ?>
</span>
</p>
</div>
<?php endif; ?>
</div>
<div class="page-intro-mobile">
<div class="page-title page-title--goal">
<?php if ($isCompletionFlow) : ?>
<h3><strong>다음 목표</strong>를 선택해 책장을 채워보세요.</h3>
<?php else : ?>
<h3><strong>선택한 목표</strong>와 함께 배움을 채워가세요.</h3>
<?php endif; ?>
<p class="page-title-deadline">D-64</p>
</div>
</div>
</div>
<div class="myclass-inner">
<div class="sub-title">
<h4><?php echo substr($currentYear, 2); ?>년 <strong><?php echo $currentQuarterNum; ?></strong>분기)</h4>
</div>
<section class="bookshelf goal" id="bookshelf" aria-label="학습 목표 선택">
<div class="shelf-legs">
<span class="shelf-leg shelf-leg-left"></span>
<span class="shelf-leg shelf-leg-center"></span>
<span class="shelf-leg shelf-leg-center"></span>
<span class="shelf-leg shelf-leg-right"></span>
</div>
<?php for ($row = 0; $row < 2; $row++) : ?>
<div class="shelf-row">
<ul class="goal-list">
<?php for ($col = 0; $col < 3; $col++) : ?>
<?php
$idx = $row * 3 + $col;
$goal = $goals[$idx];
$goalId = $goal['id'];
$isCompletedDB = in_array($goalId, $completedGoalIds, true);
?>
<li class="goal-item<?php echo ($isCompletedDB ? ' goal-item-completed-db' : ''); ?>" data-goal-id="<?php echo $goalId; ?>">
<button class="goal-card" type="button" aria-label="<?php echo str_replace("\n", ' ', $goal['title']); ?> 목표 선택"
<?php if ($isCompletedDB) : ?>
disabled tabindex="-1" aria-disabled="true"
<?php endif; ?>
>
<?php if ($isCompletedDB) : ?>
<div class="goal-completed-preview" aria-hidden="true">
<div class="goal-preview-header">
<span class="goal-preview-badge">
<img src="/img/myclass/ico_medal_completed.svg" alt="완득" />
<span class="badge-text">완득</span>
</span>
<strong class="goal-preview-title"><?php echo str_replace("\n", '', $goal['title']); ?></strong>
</div>
</div>
<?php else : ?>
<div class="card-default-area">
<div class="card-title-wrap">
<div class="card-icon-wrap">
<span class="card-icon" aria-hidden="true">
<img class="lottie-fallback" src="/img/myclass/<?php echo $goal['gif']; ?>" alt="" aria-hidden="true" />
<span class="lottie-icon lottie-<?php echo $goal['icon']; ?>" data-lottie-url="/img/myclass/<?php echo $goal['json']; ?>"></span>
</span>
</div>
<strong class="card-title"><?php echo nl2br(htmlspecialchars($goal['title'], ENT_QUOTES, 'UTF-8')); ?></strong>
</div>
<div class="card-body">
<p class="card-desc"><?php echo htmlspecialchars($goal['desc'], ENT_QUOTES, 'UTF-8'); ?></p>
</div>
</div>
<?php endif; ?>
</button>
</li>
<?php endfor; ?>
</ul>
<div class="shelf-board"></div>
<?php if ($row === 1) : ?><div class="shelf-board shelf-board-mobile"></div><?php endif; ?>
</div>
<?php endfor; ?>
</section>
<?php if ($isCompletionFlow) : ?>
<!--
<div class="myclass-extra-actions" style="text-align:center; margin-bottom:28px;">
<button type="button" class="btn-next-growth" id="btnSelectGoal">추가 목표 고르기</button>
<a href="/skin/myclass_list.php" class="btn-next-growth" style="margin-left:8px;">학습하러 가기</a>
</div>
-->
<?php endif; ?>
</div>
</div>
</div>
<?php include(__DIR__ . '/_modal/goal-layer.php'); ?>
</div>
<script>
window.MYCLASS_GOALS = <?php
echo json_encode(array_map(function ($g) {
return [
'id' => (int)$g['id'],
'title' => str_replace("\n", ' ', (string)$g['title']),
'desc' => (string)$g['desc'],
];
}, $goals), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>;
window.MYCLASS_GOAL_RECS = <?php
echo json_encode($goalRecommendations, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>;
window.MYCLASS_GOAL_BOOKS = <?php
echo json_encode($goalBooksById, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>;
window.MYCLASS_GOAL_CODE_MAP = <?php
echo json_encode($goalCodeById, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>;
window.MYCLASS_QUARTER_CODE = <?php
echo json_encode($currentQuarterCode, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>;
<?php if ($debugRecs) : ?>
window.DEBUG_RECS_PAYLOAD = <?php echo json_encode($debugRecsPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
console.error('[debug_recs] ACTIVE');
console.error('[debug_recs] MYCLASS_GOAL_CODE_MAP', window.MYCLASS_GOAL_CODE_MAP);
console.error('[debug_recs] MYCLASS_GOAL_RECS', window.MYCLASS_GOAL_RECS);
console.error('[debug_recs] MODAL_REC_ITEMS_FROM_PHP', <?php echo json_encode($modalRecItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>);
console.error('[debug_recs] PAYLOAD', window.DEBUG_RECS_PAYLOAD);
<?php endif; ?>
</script>
<script src="/js/myclass.js"></script>
</body>
</html>