break - 반복 완전 중단!
// 숫자 7을 찾으면 즉시 중단
for (let i = 1; i
continue - 현재만 건너뛰기
// 3의 배수는 건너뛰기
for (let i = 1; i
💡 break vs continue 쉽게 외우기
break = "나 그만할래!" → 반복문 자체를 완전히 빠져나감. 더 이상 반복 안 함.
continue = "이번 건 패스!" → 현재 반복만 건너뛰고 다음 반복으로 넘어감.
비유: 출석부를 부르다가...
break = "여기서 그만 부를게요!" (나머지 학생은 아예 안 부름)
continue = "3번은 결석이니 넘어갈게요!" (4번부터 계속 부름)
⚠️ 무한루프 - 끝나지 않는 반복의 공포
🚨 절대 주의! 무한루프란?
반복 조건이 절대 false가 되지 않으면 반복이 영원히 계속됩니다. 이걸 무한루프(Infinite Loop) 라고 해요. 브라우저가 멈추고, 컴퓨터가 느려지는 무서운 상황이 발생합니다!
❌ 무한루프 예시 (절대 하지 마세요!)
// 실수 1: 증감 빼먹기
let i = 0;
while (i = 0; i++) {
console.log(i); // i가 계속 커짐!
}
// 실수 3: 잘못된 조건
let count = 10;
while (count > 0) {
console.log(count);
count++; // 감소가 아니라 증가!
}
✅ 올바른 반복문
// 올바른 while문
let i = 0;
while (i
🔧 무한루프에 빠졌을 때 탈출법
브라우저: 탭 닫기(Ctrl+W) 또는 작업 관리자(Ctrl+Shift+Esc)에서 브라우저 종료
VS Code 터미널: Ctrl+C로 프로세스 강제 종료
예방법: while 사용 시 반드시 종료 조건과 증감이 있는지 확인! 안전장치로 최대 반복 횟수를 설정하세요.
🧠 퀴즈 타임!
Q1. for (let i = 0; i
5
4
3
6
정답은 4 입니다! i
Q2. 반복 도중 "이번 회차만 건너뛰고 다음으로" 넘어가려면?
break
continue
return
skip
정답은 continue 입니다! continue는 현재 반복만 건너뛰고 다음 반복으로 넘어가요. break는 반복문 자체를 완전히 종료하고, skip은 JavaScript에 존재하지 않는 키워드입니다.
Q3. 배열을 순회할 때 가장 깔끔한 방법은?
while문
do...while문
forEach
switch문
정답은 forEach 입니다! forEach는 배열 전용 메서드로, 인덱스 관리 없이 각 요소를 깔끔하게 순회할 수 있어요. switch문은 반복문이 아니라 조건 분기문입니다!
Q4. while문에서 무한루프를 방지하려면 반드시 필요한 것은?
조건이 언젠가 false가 되도록 변수를 변경하는 코드
break 문
continue 문
return 문
정답! while문의 조건이 언젠가 false가 되도록 루프 안에서 변수를 변경해야 합니다. 예를 들어 count++처럼 카운터를 증가시켜서 조건을 벗어나게 해야 해요!
✅ 반복문 마스터 체크리스트
클릭해서 배운 내용을 체크해 보세요!
for문의 초기화/조건/증감 구조를 이해했다
while문과 do...while문의 차이를 안다
forEach로 배열을 순회할 수 있다
for...of와 for...in의 차이를 안다
구구단 프로그램을 만들 수 있다
break와 continue의 차이를 이해했다
무한루프의 위험성과 예방법을 알았다
중첩 반복문(반복문 안의 반복문)을 이해했다
❤️
0
⭐
즐겨찾기
👍
0
Threads
📷
Instagram
💬
카카오톡
🔗
링크
⚡ 필수
이 레슨의 난이도는 어땠나요?
😊 쉬움
🙂 보통
😅 어려움
😵 매우어려움
👆 레슨 완료를 위해 난이도를 선택해주세요!
AI 도우미를 사용하려면 API 키를 설정하세요. Gemini는 무료 로 사용 가능합니다!
API 키 설정하기
🔒 학습 중... (최소 시간 필요)
잠깐만요!
${message}
이 앱의 목표는 점수 올리기가 아닌, 실제 학습 입니다.
이해했어요
`;
document.body.appendChild(popup);
// 경고 시 세션에 기록
if (sessionId) {
fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'session_warning',
session_id: sessionId,
warning_count: warningCount,
scroll_depth: scrollDepth
})
});
}
}
// 경고 확인
function acknowledgeWarning(btn) {
warningAcknowledged = true;
btn.closest('.skip-warning-popup').remove();
toast('경고가 기록되었습니다. 다시 완료 버튼을 눌러주세요.');
}
// 세션 시작
async function startSession() {
document.getElementById('learningProgress').style.display = 'block';
initScrollTracking(); // 스크롤 추적 시작
if (USER) { try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'session_start', lesson_id: LESSON_ID})
});
const data = await safeJson(res);
if (data.success) {
sessionId = data.session_id;
// 단순화: MIN_TIME만 사용, 초보자는 시간 제한 없음
minDuration = (IS_REVIEW || ALREADY_COMPLETED || IS_BEGINNER) ? 5 : (MIN_TIME || 30);
}
} catch(e) {} } // end USER check
// 타이머 시작
timerInterval = setInterval(updateProgress, 1000);
}
// 진행 상태 업데이트
function updateProgress() {
const elapsed = Math.floor((Date.now() - lessonStartTime) / 1000);
const progress = Math.min(100, (elapsed / minDuration) * 100);
// 시간 표시
const mins = Math.floor(elapsed / 60);
const secs = elapsed % 60;
document.getElementById('progressTime').textContent = mins + ':' + (secs {
if (!checkpointShown[i] && timeRatio >= cp.time && checkpointsCompleted === i) {
checkpointShown[i] = true;
showCheckpoint(cp.title, cp.question);
}
});
// 돌발 퀴즈 (60% 지점) - 레벨 6 이상만
if (!quizShown && timeRatio >= 0.6 && checkpointsCompleted >= 1) {
quizShown = true;
setTimeout(showQuiz, 2000);
}
} else {
// 초보자는 체크포인트 자동 완료 처리
checkpointsCompleted = checkpointsRequired;
}
// 완료 버튼 활성화 체크
updateCompleteButton(elapsed);
}
// 완료 버튼 상태 업데이트
function updateCompleteButton(elapsed) {
const btn = document.getElementById('completeBtn');
const btnText = document.getElementById('completeBtnText');
if (!btn || lessonCompleted) return;
// 초보자 모드 (레벨 5 이하): 시간/체크 제한 없음
const timeOK = (elapsed >= minDuration);
// 체크포인트: 팝업 비활성화 시 자동 통과, 초보자도 자동 통과
const checkOK = IS_BEGINNER || !POPUP_SETTINGS.checkpoint ? true : (checkpointsCompleted >= checkpointsRequired);
// 퀴즈: 팝업 비활성화 시 자동 통과
const quizOK = !POPUP_SETTINGS.quiz || quizScore === null || quizScore >= 50;
const diffOK = (IS_REVIEW || ALREADY_COMPLETED) ? true : difficultySelected; // 난이도 필수 선택
const interactOK = (IS_REVIEW || ALREADY_COMPLETED) ? true : interactionDone; // 좋아요/별/추천 중 하나 필수
const customCheck = window.checkCustomCompletion(); // 커스텀 조건 (O/X 등)
const customOK = customCheck.ok;
if (timeOK && checkOK && quizOK && diffOK && interactOK && customOK) {
canComplete = true;
btn.disabled = false;
btn.style.opacity = '1';
btnText.textContent = '학습 완료 (+' + LESSON_XP + ' XP)';
} else {
canComplete = false;
let status = [];
if (!timeOK && !IS_BEGINNER) {
const remaining = minDuration - elapsed;
const m = Math.floor(remaining / 60);
const s = remaining % 60;
status.push(m + '분 ' + s + '초 남음');
}
if (!diffOK) status.push('난이도 선택');
if (!interactOK) status.push('❤️/⭐/👍 중 1개');
if (!customOK && customCheck.message) status.push(customCheck.message);
btnText.textContent = '🔒 ' + status.join(' · ');
// 시간은 OK인데 난이도나 인터랙션이 안됐으면 강조 효과
if (timeOK && checkOK && quizOK && customOK) {
if (!diffOK) showDifficultyHighlight();
if (!interactOK) showInteractionHighlight();
}
}
// 초보자는 체크포인트 카운트 숨김
const checkpointEl = document.getElementById('checkpointCount');
if (IS_BEGINNER) {
checkpointEl.textContent = '자유 학습';
checkpointEl.style.color = 'var(--success)';
} else {
checkpointEl.textContent = '체크 ' + checkpointsCompleted + '/' + checkpointsRequired;
}
}
// 체크포인트 팝업
function showCheckpoint(title, question) {
// 팝업 비활성화 설정 확인
if (!POPUP_SETTINGS.checkpoint) {
// 자동으로 체크포인트 완료 처리
checkpointsCompleted++;
toast('✓ 체크포인트 자동 완료');
return;
}
document.getElementById('checkpointTitle').textContent = title;
document.getElementById('checkpointQuestion').textContent = question;
document.getElementById('checkpointOptions').innerHTML = `
네, 이해했어요!
다시 한번 볼게요
`;
document.getElementById('checkpointPopup').style.display = 'flex';
}
// 체크포인트 제출
async function submitCheckpoint() {
// "다시 보지 않기" 체크 확인 후 저장
if (document.getElementById('disableCheckpointPopup')?.checked) {
savePopupSetting('checkpoint_disabled', true);
}
document.getElementById('checkpointPopup').style.display = 'none';
checkpointsCompleted++;
if (sessionId) {
fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'session_checkpoint', session_id: sessionId})
});
}
toast('✓ 체크포인트 완료!');
}
// 돌발 퀴즈
function showQuiz() {
// 팝업 비활성화 설정 확인
if (!POPUP_SETTINGS.quiz) {
// 자동으로 퀴즈 완료 처리
quizScore = 100;
toast('✓ 퀴즈 자동 완료');
return;
}
const quiz = quizQuestions[Math.floor(Math.random() * quizQuestions.length)];
document.getElementById('quizQuestion').textContent = quiz.q;
document.getElementById('quizResult').style.display = 'none';
let optionsHtml = '';
quiz.options.forEach((opt, i) => {
optionsHtml += `
${opt}
`;
});
document.getElementById('quizOptions').innerHTML = optionsHtml;
document.getElementById('quizPopup').style.display = 'flex';
}
// 퀴즈 답변
function answerQuiz(selected, correct) {
const isCorrect = selected === correct;
quizScore = isCorrect ? 100 : 30;
const result = document.getElementById('quizResult');
result.style.display = 'block';
result.style.background = isCorrect ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)';
result.innerHTML = isCorrect
? '
🎉 정답입니다!
'
: '
아쉬워요! 다시 한번 읽어보세요.
';
// 서버에 점수 저장
if (sessionId) {
fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'session_quiz', session_id: sessionId, score: quizScore})
});
}
setTimeout(() => {
// "다시 보지 않기" 체크 확인 후 저장
if (document.getElementById('disableQuizPopup')?.checked) {
savePopupSetting('quiz_disabled', true);
}
document.getElementById('quizPopup').style.display = 'none';
if (isCorrect) toast('퀴즈 정답! +보너스');
}, 1500);
}
// 팝업 설정 저장 (DB에 저장)
async function savePopupSetting(key, value) {
if (!USER) return;
try {
await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'save_popup_setting',
setting_key: key,
setting_value: value
})
});
toast('✓ 설정이 저장되었습니다');
} catch(e) {
console.error('팝업 설정 저장 실패:', e);
}
}
// 페이지 로드 시 세션 시작
// 타이머는 항상 시작 (세션 API는 내부에서 로그인 체크)
startSession();
// 로컬 스토리지에서 API 키 정보 확인
const API_KEY_STORAGE = 'devfoil_api_keys_encrypted';
const API_KEY_SIMPLE_STORAGE = 'devfoil_api_keys_simple';
let hasApiKey = false;
let apiProvider = '';
let apiModel = '';
let simpleApiKeys = {}; // 간편 저장된 키
// 간단한 디코드 (html.php와 동일)
function simpleDecode(str) {
try {
return decodeURIComponent(atob(str).split('').reverse().join(''));
} catch(e) { return ''; }
}
function checkApiKeys() {
const priority = ['gemini', 'gpt', 'claude', 'grok'];
// 1. 간편 저장 확인 (devfoil_api_keys_simple)
const simpleStored = localStorage.getItem(API_KEY_SIMPLE_STORAGE);
if (simpleStored) {
try {
simpleApiKeys = JSON.parse(simpleDecode(simpleStored)) || {};
for (const p of priority) {
if (simpleApiKeys[p]) {
hasApiKey = true;
apiProvider = p;
apiModel = localStorage.getItem('devfoil_model_' + p) || getDefaultModel(p);
return true;
}
}
} catch(e) {}
}
// 2. 암호화 저장 확인 (devfoil_api_keys_encrypted)
const encryptedStored = localStorage.getItem(API_KEY_STORAGE);
if (encryptedStored) {
try {
const data = JSON.parse(encryptedStored);
if (data.providers && data.providers.length > 0) {
hasApiKey = true;
for (const p of priority) {
if (data.providers.includes(p)) {
apiProvider = p;
apiModel = localStorage.getItem('devfoil_model_' + p) || getDefaultModel(p);
return true;
}
}
}
} catch(e) {}
}
return false;
}
function getDefaultModel(provider) {
const defaults = {
gemini: 'gemini-2.0-flash',
gpt: 'gpt-4o-mini',
claude: 'claude-sonnet-4-20250514',
grok: 'grok-beta'
};
return defaults[provider] || '';
}
// 간편 저장된 키 직접 가져오기 (비밀번호 불필요)
function getSimpleApiKey(provider) {
return simpleApiKeys[provider] || null;
}
// 초기화
document.addEventListener('DOMContentLoaded', function() {
loadInteractions();
loadFeedbacks(); // 피드백 목록 로드
if (checkApiKeys()) {
document.getElementById('aiNoKey').style.display = 'none';
document.getElementById('aiChatContent').style.display = 'block';
document.getElementById('aiBadge').textContent = apiProvider.toUpperCase() + ' 연결됨';
document.getElementById('aiBadge').classList.remove('inactive');
loadChatHistory();
} else {
document.getElementById('aiBadge').classList.add('inactive');
}
});
// 인터랙션 로드
async function loadInteractions() {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'get_lesson_interactions', lesson_id: LESSON_ID})
});
const data = await safeJson(res);
if (data.success) {
// 통계 업데이트 (상단 + 하단)
document.getElementById('likeCount').textContent = data.stats?.likes || 0;
document.getElementById('recommendCount').textContent = data.stats?.recommends || 0;
if (document.getElementById('likeCount2')) document.getElementById('likeCount2').textContent = data.stats?.likes || 0;
if (document.getElementById('recommendCount2')) document.getElementById('recommendCount2').textContent = data.stats?.recommends || 0;
// 사용자 상태 표시 (상단 + 하단 모두)
if (data.user) {
document.querySelectorAll('.like-btn').forEach(btn => btn.classList.toggle('active', data.user.liked));
document.querySelectorAll('.star-btn').forEach(btn => btn.classList.toggle('active', data.user.starred));
document.querySelectorAll('.recommend-btn').forEach(btn => btn.classList.toggle('active', data.user.recommended));
// 좋아요/별/추천 중 하나라도 했으면 interactionDone = true
if (data.user.liked || data.user.starred || data.user.recommended) {
interactionDone = true;
}
if (data.user.user_difficulty) {
// 이미 난이도 선택한 경우 표시
difficultySelected = true;
document.querySelectorAll('.diff-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.diff == data.user.user_difficulty);
});
// 필수 배지 숨김
const badge = document.getElementById('diffRequiredBadge');
if (badge) badge.style.display = 'none';
// 완료 버튼 상태 업데이트
const elapsed = Math.floor((Date.now() - lessonStartTime) / 1000);
updateCompleteButton(elapsed);
}
}
}
}
// 인터랙션 - 안티그래비티 애니메이션 효과
async function interact(action) {
if (!USER) {
toast('로그인이 필요합니다');
return;
}
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'lesson_interact', lesson_id: LESSON_ID, interact_action: action})
});
const data = await safeJson(res);
if (data.success) {
// 상단 + 하단 카운트 모두 업데이트
document.getElementById('likeCount').textContent = data.stats?.likes || 0;
document.getElementById('recommendCount').textContent = data.stats?.recommends || 0;
if (document.getElementById('likeCount2')) document.getElementById('likeCount2').textContent = data.stats?.likes || 0;
if (document.getElementById('recommendCount2')) document.getElementById('recommendCount2').textContent = data.stats?.recommends || 0;
// 상단 + 하단 버튼 active 상태 동기화
if (data.user) {
const classMap = {like: 'like-btn', star: 'star-btn', recommend: 'recommend-btn'};
const stateKey = action === 'like' ? 'liked' : action === 'star' ? 'starred' : 'recommended';
const isActive = data.user[stateKey];
document.querySelectorAll('.' + classMap[action]).forEach(btn => {
btn.classList.toggle('active', isActive);
// 안티그래비티 효과 (활성화될 때만)
if (isActive) {
createAntigravityEffect(btn, action);
}
});
// 좋아요/별/추천 중 하나라도 했으면 interactionDone = true
if (data.user.liked || data.user.starred || data.user.recommended) {
interactionDone = true;
} else {
interactionDone = false;
}
// 완료 버튼 상태 업데이트
const elapsed = Math.floor((Date.now() - lessonStartTime) / 1000);
updateCompleteButton(elapsed);
}
}
}
// 안티그래비티 파티클 효과
function createAntigravityEffect(element, action) {
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const icons = {
like: ['❤️', '💕', '💖', '💗', '💓'],
star: ['⭐', '✨', '🌟', '💫', '⭐'],
recommend: ['👍', '👏', '🙌', '💪', '🔥']
};
const particles = icons[action] || ['✨'];
for (let i = 0; i {
const angle = (i / 8) * Math.PI * 2;
const distance = 40 + Math.random() * 60;
particle.style.transform = `translate(
calc(-50% + ${Math.cos(angle) * distance}px),
calc(-50% + ${-80 - Math.random() * 40}px)
) scale(${0.5 + Math.random() * 0.5}) rotate(${Math.random() * 360}deg)`;
particle.style.opacity = '0';
});
setTimeout(() => particle.remove(), 1000);
}
}
// 난이도 강조 효과 표시
let difficultyHighlightShown = false;
function showDifficultyHighlight() {
if (difficultyHighlightShown || difficultySelected || ALREADY_COMPLETED) return;
difficultyHighlightShown = true;
const diffWrap = document.getElementById('difficultyWrap');
if (!diffWrap) return;
// 강조 효과 추가 (스크롤은 하지 않음 - 공부 흐름 방해 방지)
diffWrap.classList.add('highlight-required');
}
// 인터랙션 강조 효과 표시
let interactionHighlightShown = false;
function showInteractionHighlight() {
if (interactionHighlightShown || interactionDone || ALREADY_COMPLETED) return;
interactionHighlightShown = true;
// 인터랙션 버튼들 강조
const interactBtns = document.querySelectorAll('.like-btn, .star-btn, .recommend-btn');
interactBtns.forEach(btn => {
btn.style.animation = 'pulse-highlight 0.5s ease 3';
});
toast('❤️ 좋아요/⭐즐겨찾기/👍추천 중 하나를 선택해주세요!');
}
// 난이도 설정
async function setDifficulty(level) {
if (!USER) {
toast('로그인이 필요합니다');
return;
}
document.querySelectorAll('.diff-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.diff == level);
});
// 난이도 선택 완료 표시
difficultySelected = true;
// 강조 효과 제거
const diffWrap = document.getElementById('difficultyWrap');
if (diffWrap) {
diffWrap.classList.remove('highlight-required');
}
// 필수 배지 숨김
const badge = document.getElementById('diffRequiredBadge');
if (badge) badge.style.display = 'none';
await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'lesson_interact', lesson_id: LESSON_ID, interact_action: 'difficulty', value: level})
});
toast('난이도가 저장되었습니다 ✓');
// 완료 버튼 상태 즉시 업데이트
const elapsed = Math.floor((Date.now() - lessonStartTime) / 1000);
updateCompleteButton(elapsed);
}
// AI 채팅 기록 로드
async function loadChatHistory() {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'get_lesson_chat_history', lesson_id: LESSON_ID})
});
const data = await safeJson(res);
if (data.success && data.history && data.history.length > 0) {
const container = document.getElementById('aiMessages');
data.history.forEach(msg => {
const div = document.createElement('div');
div.className = 'ai-msg ' + msg.role;
div.innerHTML = `
${escapeHtml(msg.message)}
`;
container.appendChild(div);
});
container.scrollTop = container.scrollHeight;
}
}
// AI 메시지 전송
async function sendAiMessage() {
const input = document.getElementById('aiInput');
const message = input.value.trim();
if (!message) return;
const sendBtn = document.getElementById('aiSendBtn');
sendBtn.disabled = true;
// 사용자 메시지 표시
const container = document.getElementById('aiMessages');
const userDiv = document.createElement('div');
userDiv.className = 'ai-msg user';
userDiv.innerHTML = `
${escapeHtml(message)}
`;
container.appendChild(userDiv);
input.value = '';
container.scrollTop = container.scrollHeight;
// 로딩 표시
const loadingDiv = document.createElement('div');
loadingDiv.className = 'ai-msg assistant';
loadingDiv.innerHTML = '
생각 중...
';
container.appendChild(loadingDiv);
try {
// 1. 간편 저장된 키 먼저 확인
let apiKey = getSimpleApiKey(apiProvider);
// 2. 없으면 암호화된 키 복호화 시도
if (!apiKey) {
apiKey = await getDecryptedApiKey(apiProvider);
}
if (!apiKey) {
loadingDiv.innerHTML = '
';
sendBtn.disabled = false;
return;
}
// ===== 브라우저에서 직접 API 호출 (서버 경유 X) =====
// API 키가 서버를 절대 거치지 않음 — 유저 브라우저에서만 존재
const lessonContext = document.querySelector('.content')?.innerText?.substring(0, 1500) || '';
const systemPrompt = `당신은 "${LESSON_TITLE}" 레슨의 AI 학습 도우미입니다. 학생의 질문에 친절하고 정확하게 답변하세요. 레슨 내용을 참고하여 답변하되, 한국어로 답변하세요.`;
let aiResponse = '';
if (apiProvider === 'gemini') {
// Gemini — 브라우저 직접 호출
const gRes = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${apiModel || 'gemini-2.0-flash'}:generateContent?key=${apiKey}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
contents: [{parts: [{text: systemPrompt + '\n\n' + message}]}],
generationConfig: {maxOutputTokens: 1000, temperature: 0.7}
})
});
const gData = await gRes.json();
aiResponse = gData?.candidates?.[0]?.content?.parts?.[0]?.text || '응답을 받을 수 없습니다.';
} else if (apiProvider === 'openai') {
// OpenAI — 브라우저 직접 호출
const oRes = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey},
body: JSON.stringify({
model: apiModel || 'gpt-4o-mini',
messages: [{role:'system', content:systemPrompt}, {role:'user', content:message}],
max_tokens: 1000, temperature: 0.7
})
});
const oData = await oRes.json();
aiResponse = oData?.choices?.[0]?.message?.content || oData?.error?.message || '응답 오류';
} else if (apiProvider === 'grok') {
// Grok — 브라우저 직접 호출
const xRes = await fetch('https://api.x.ai/v1/chat/completions', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey},
body: JSON.stringify({
model: apiModel || 'grok-3-mini',
messages: [{role:'system', content:systemPrompt}, {role:'user', content:message}],
max_tokens: 1000
})
});
const xData = await xRes.json();
aiResponse = xData?.choices?.[0]?.message?.content || '응답 오류';
} else if (apiProvider === 'claude') {
// Claude — CORS 차단이라 서버 프록시 필수 (키는 쿠키로 1회성 전달)
const ck = btoa(unescape(encodeURIComponent(apiKey))).split('').reverse().join('');
document.cookie = 'df_ak=' + encodeURIComponent(ck) + ';path=/;max-age=60;SameSite=Strict;Secure';
const cRes = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action:'ai_proxy', provider:'claude', api_key:'__COOKIE__', model:apiModel||'claude-sonnet-4-20250514', system:systemPrompt, message:message})
});
document.cookie = 'df_ak=;path=/;max-age=0';
const cData = await cRes.json();
aiResponse = cData?.reply || cData?.error || '응답 오류';
}
// 대화 기록만 서버에 저장 (API 키 없이!)
fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action:'save_chat_log', lesson_id:LESSON_ID, user_msg:message, ai_msg:aiResponse})
}).catch(()=>{});
// AI 응답 표시
if (aiResponse) {
loadingDiv.innerHTML = `
${formatAiResponse(aiResponse)}
`;
} else {
loadingDiv.innerHTML = `
응답을 받지 못했습니다
`;
}
} catch(e) {
loadingDiv.innerHTML = '
네트워크 오류
';
}
container.scrollTop = container.scrollHeight;
sendBtn.disabled = false;
}
// API 키 복호화
async function getDecryptedApiKey(provider) {
const password = prompt('API 키 사용을 위해 비밀번호를 입력하세요');
if (!password) return null;
try {
const saltRes = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'get_api_salt'})
});
const saltData = await safeJson(saltRes);
if (!saltData.success) return null;
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits', 'deriveKey']);
const key = await crypto.subtle.deriveKey(
{name: 'PBKDF2', salt: enc.encode(saltData.salt), iterations: 100000, hash: 'SHA-256'},
keyMaterial,
{name: 'AES-GCM', length: 256},
false,
['decrypt']
);
const stored = JSON.parse(localStorage.getItem(API_KEY_STORAGE));
const iv = new Uint8Array(stored.encrypted.iv);
const data = new Uint8Array(stored.encrypted.data);
const decrypted = await crypto.subtle.decrypt({name: 'AES-GCM', iv}, key, data);
const keys = JSON.parse(new TextDecoder().decode(decrypted));
return keys[provider] || null;
} catch(e) {
toast('비밀번호가 올바르지 않습니다');
return null;
}
}
// 학습 완료 조건 안내 팝업
function showCompletionGuidePopup() {
const elapsed = Math.floor((Date.now() - lessonStartTime) / 1000);
const timeOK = (elapsed >= minDuration);
const diffOK = (IS_REVIEW || ALREADY_COMPLETED) ? true : difficultySelected;
const interactOK = (IS_REVIEW || ALREADY_COMPLETED) ? true : interactionDone;
const customCheck = window.checkCustomCompletion ? window.checkCustomCompletion() : { ok: true, message: '' };
const customOK = customCheck.ok;
let missingItems = [];
if (!timeOK) {
const remaining = minDuration - elapsed;
const m = Math.floor(remaining / 60);
const s = remaining % 60;
missingItems.push(`
⏱️
학습 시간 ${m}분 ${s}초 더 학습해주세요
`);
}
if (!customOK && customCheck.message) {
missingItems.push(`
✅
학습 활동 ${customCheck.message}
`);
}
if (!diffOK) {
missingItems.push(`
📊
난이도 평가 이 레슨의 난이도를 선택해주세요
`);
}
if (!interactOK) {
missingItems.push(`
💜
피드백 남기기 ❤️ 좋아요, ⭐ 즐겨찾기, 👍 추천 중 하나를 선택해주세요
`);
}
// 팝업 생성
const popup = document.createElement('div');
popup.id = 'completionGuidePopup';
popup.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:1001;display:flex;align-items:center;justify-content:center;padding:20px;animation:fadeIn .3s';
popup.innerHTML = `
📋
학습 완료 조건
아래 항목을 완료하면 XP를 받을 수 있어요!
${missingItems.join('')}
확인했어요!
`;
document.body.appendChild(popup);
popup.onclick = (e) => { if (e.target === popup) popup.remove(); };
}
// 레슨 완료
async function completeLesson() {
if (!USER) { toast('로그인 후 학습 완료가 가능합니다'); return; }
if (!canComplete) {
// 무엇이 부족한지 안내 팝업 표시
showCompletionGuidePopup();
return;
}
const btn = document.getElementById('completeBtn');
const btnText = document.getElementById('completeBtnText');
btn.disabled = true;
btnText.textContent = '처리 중...';
const duration = Math.floor((Date.now() - lessonStartTime) / 1000);
const finalXP = LESSON_XP; // 단순화: 고정 XP
// XP 획득
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'complete_lesson',
lesson_id: LESSON_ID,
xp: finalXP,
duration: duration,
is_review: IS_REVIEW,
time_spent: duration,
min_time: MIN_TIME
})
});
const data = await safeJson(res);
if (data.success) {
lessonCompleted = true;
// 세션 종료 (공부시간 기록)
if (sessionId) {
var dur = Math.floor((Date.now() - lessonStartTime) / 1000);
fetch('/ajax_quest.php', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'session_end', session_id:sessionId, duration:dur})});
}
btn.className = 'complete-btn done';
const actualXp = data.xp_awarded ?? finalXP;
const xpMsg = IS_REVIEW
? (actualXp > 0 ? `복습 완료! +${actualXp} XP` : '복습 완료! (오늘 이미 XP 획득)')
: `+${actualXp} XP`;
btnText.textContent = '✓ 완료됨 (' + xpMsg + ')';
if (timerInterval) clearInterval(timerInterval);
document.getElementById('learningProgress').style.display = 'none';
toast('🎉 ' + (IS_REVIEW ? xpMsg : '학습 완료! +' + actualXp + ' XP'));
if (data.levelUp) {
setTimeout(() => toast('🎉 레벨 업! Lv.' + data.level), 1500);
}
} else {
btn.disabled = false;
btnText.textContent = '학습 완료 (+' + finalXP + ' XP)';
toast(data.error || '오류 발생');
}
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function formatAiResponse(text) {
// 간단한 마크다운 처리
return escapeHtml(text)
.replace(/\*\*(.*?)\*\*/g, '
$1 ')
.replace(/\n/g, '
');
}
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2500);
}
// 레퍼럴 링크 생성
function getShareLink() {
const baseUrl = window.location.origin + window.location.pathname;
// 유저 레퍼럴 코드 생성 (세션 기반 또는 유저 ID 기반)
let refCode = localStorage.getItem('devfoil_ref_code');
if (!refCode) {
// 간단한 레퍼럴 코드 생성 (세션 ID 해시 + 타임스탬프)
refCode = 'df' + Math.random().toString(36).substring(2, 8) + Date.now().toString(36).slice(-4);
localStorage.setItem('devfoil_ref_code', refCode);
}
return baseUrl + '?ref=' + refCode;
}
// Threads 공유
function shareToThreads() {
const shareUrl = getShareLink();
let shareText = '';
if (USER_DATA) {
const u = USER_DATA;
shareText = `✨ DEVFOIL에서 "${LESSON_TITLE}" 학습 완료!\n\n` +
`📊 Lv.${u.level} · ${u.xp.toLocaleString()} XP\n` +
`🔥 ${u.streak}일 연속 학습 중\n` +
`💎 ${u.devcoin.toLocaleString()} DP\n\n` +
`#DEVFOIL #바이브코딩 #AI학습 #코딩\n\n`;
} else {
shareText = `✨ [DEVFOIL] ${LESSON_TITLE}\n\nAI 활용 능력을 키워보세요!\n\n#DEVFOIL #바이브코딩\n\n`;
}
const threadsUrl = 'https://www.threads.net/intent/post?text=' + encodeURIComponent(shareText + shareUrl);
window.open(threadsUrl, '_blank', 'width=600,height=600');
recordShare('threads');
toast('Threads에 공유됩니다');
}
// Instagram 공유
function shareToInstagram() {
const shareUrl = getShareLink();
const shareText = `[DEVFOIL] ${LESSON_TITLE}\n\nAI 활용 능력을 키워보세요!\n\n🔗 ${shareUrl}`;
navigator.clipboard.writeText(shareText).then(() => {
toast('📋 복사 완료! Instagram에 붙여넣기 하세요');
setTimeout(() => {
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
window.location.href = 'instagram://';
} else {
window.open('https://www.instagram.com/', '_blank');
}
}, 500);
recordShare('instagram');
}).catch(() => {
fallbackCopy(shareText);
toast('📋 복사 완료! Instagram에 붙여넣기 하세요');
});
}
// 카카오톡 공유
function shareToKakao() {
const shareUrl = getShareLink();
const shareText = `[DEVFOIL] ${LESSON_TITLE}\n\nAI 활용 능력을 키워보세요!\n\n🔗 ${shareUrl}`;
navigator.clipboard.writeText(shareText).then(() => {
toast('📋 복사 완료! 카카오톡에 붙여넣기 하세요');
setTimeout(() => {
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
window.location.href = 'kakaotalk://';
}
}, 500);
recordShare('kakao');
}).catch(() => {
fallbackCopy(shareText);
toast('📋 복사 완료! 카카오톡에 붙여넣기 하세요');
});
}
// X/Twitter 공유
function shareToTwitter() {
const shareUrl = getShareLink();
const shareText = `[DEVFOIL] ${LESSON_TITLE} - AI 활용 능력 키우기`;
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(shareUrl)}`;
window.open(twitterUrl, '_blank', 'width=600,height=450');
recordShare('twitter');
toast('X에 공유됩니다');
}
// 공유 통계 기록
function recordShare(platform) {
fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'record_share',
lesson_id: LESSON_ID,
platform: platform,
ref_code: localStorage.getItem('devfoil_ref_code')
})
});
}
// 클립보드 복사 fallback
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
// 피드백 목록 로드
async function loadFeedbacks() {
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'get_feedbacks', lesson_id: LESSON_ID})
});
const data = await safeJson(res);
if (data.success && data.feedbacks) {
renderFeedbacks(data.feedbacks);
}
} catch(e) {}
}
// 피드백 렌더링
function renderFeedbacks(feedbacks) {
const container = document.getElementById('feedbackList');
if (!feedbacks.length) {
container.innerHTML = '
아직 피드백이 없습니다. 첫 번째로 남겨보세요!
';
return;
}
container.innerHTML = feedbacks.map(fb => {
const timeAgo = getTimeAgo(fb.created_at);
const editedLabel = fb.is_edited ? '
수정됨 ' : '';
// Threads 유저네임 배지
const threadsUsername = fb.threads_username ?
`
@${fb.threads_username} ` : '';
// 유저 레벨 배지
const levelBadge = fb.level ? `
Lv.${fb.level} ` : '';
let actionsHtml = '';
if (fb.is_mine) {
actionsHtml = `
✏️ 수정
🗑️ 삭제
`;
} else {
// 신고 버튼 (본인 글이 아닐 때만)
const reportCount = fb.report_count || 0;
const reportedByMe = fb.reported_by_me;
const reportBtnClass = reportedByMe ? 'feedback-action-btn report reported' : 'feedback-action-btn report';
const reportBtnText = reportedByMe ? '🚩 신고됨' : '🚨 신고';
const reportCountHtml = reportCount > 0 ? `
${reportCount} ` : '';
actionsHtml = `
${reportBtnText}${reportCountHtml}
`;
}
let adminReplyHtml = '';
if (fb.admin_reply) {
adminReplyHtml = `
👑 관리자 답변
${escapeHtml(fb.admin_reply)}
`;
}
return `
${escapeHtml(fb.message)}
${adminReplyHtml}
${actionsHtml}
`;
}).join('');
}
// 시간 표시 (몇 분 전, 몇 시간 전 등)
function getTimeAgo(dateStr) {
const date = new Date(dateStr.replace(' ', 'T') + '+09:00'); // KST
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff ');
// 수정 UI 표시
messageEl.style.display = 'none';
actionsEl.style.display = 'none';
const editWrap = document.createElement('div');
editWrap.className = 'feedback-edit-wrap';
editWrap.innerHTML = `
취소
저장
`;
actionsEl.parentNode.insertBefore(editWrap, actionsEl);
}
// 피드백 수정 취소
function cancelEditFeedback(id) {
const item = document.getElementById('feedback-' + id);
const messageEl = item.querySelector('.feedback-message');
const actionsEl = item.querySelector('.feedback-actions');
const editWrap = item.querySelector('.feedback-edit-wrap');
if (editWrap) editWrap.remove();
messageEl.style.display = '';
actionsEl.style.display = '';
}
// 피드백 저장
async function saveFeedback(id) {
const item = document.getElementById('feedback-' + id);
const textarea = item.querySelector('.feedback-edit-input');
const message = textarea.value.trim();
if (!message) {
toast('내용을 입력해주세요');
return;
}
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'update_feedback', feedback_id: id, message: message})
});
const data = await safeJson(res);
if (data.success) {
toast('수정되었습니다');
loadFeedbacks(); // 새로고침
} else {
toast(data.error || '수정 실패');
}
} catch(e) {
toast('네트워크 오류');
}
}
// 피드백 삭제
async function deleteFeedback(id) {
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'delete_feedback', feedback_id: id})
});
const data = await safeJson(res);
if (data.success) {
toast('삭제되었습니다');
loadFeedbacks(); // 새로고침
} else {
toast(data.error || '삭제 실패');
}
} catch(e) {
toast('네트워크 오류');
}
}
// 피드백 신고
async function reportFeedback(id, btn) {
if (!USER) {
toast('로그인이 필요합니다');
return;
}
if (!confirm('이 피드백을 신고하시겠습니까?\n허위 신고는 제재 대상이 될 수 있습니다.')) return;
btn.disabled = true;
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'report_feedback', feedback_id: id})
});
const data = await safeJson(res);
if (data.success) {
toast('신고가 접수되었습니다');
btn.classList.add('reported');
btn.innerHTML = '🚩 신고됨
' + data.report_count + ' ';
} else {
toast(data.error || '신고 실패');
btn.disabled = false;
}
} catch(e) {
toast('네트워크 오류');
btn.disabled = false;
}
}
// 피드백 제출
async function submitFeedback() {
const input = document.getElementById('feedbackInput');
const btn = document.getElementById('feedbackBtn');
const message = input.value.trim();
if (!message) {
toast('내용을 입력해주세요');
return;
}
if (!USER) {
toast('로그인이 필요합니다');
return;
}
btn.disabled = true;
btn.textContent = '전송 중...';
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'submit_feedback',
lesson_id: LESSON_ID,
message: message
})
});
const data = await safeJson(res);
if (data.success) {
input.value = '';
toast('피드백이 전송되었습니다. 감사합니다!');
loadFeedbacks(); // 목록 새로고침
} else {
toast(data.error || '전송 실패');
}
} catch(e) {
toast('네트워크 오류');
}
btn.disabled = false;
btn.textContent = '보내기';
}
// 링크 복사
async function copyShareLink() {
const shareUrl = getShareLink();
const btn = document.getElementById('copyLinkBtn');
const text = document.getElementById('copyLinkText');
try {
await navigator.clipboard.writeText(shareUrl);
btn.classList.add('copied');
text.textContent = '복사됨!';
toast('링크가 복사되었습니다');
setTimeout(() => {
btn.classList.remove('copied');
text.textContent = '링크 복사';
}, 2000);
// 복사 통계 기록
fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'record_share',
lesson_id: LESSON_ID,
platform: 'copy',
ref_code: localStorage.getItem('devfoil_ref_code')
})
});
} catch(e) {
// 폴백: 구형 브라우저
const input = document.createElement('input');
input.value = shareUrl;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
toast('링크가 복사되었습니다');
}
}
// 관리자 체크 및 슈퍼패널 초기화
async function initSuperPanel() {
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'check_admin'})
});
const data = await safeJson(res);
if (data.is_admin) {
const adminBtn = document.getElementById('superPanelAdmin');
if (adminBtn) adminBtn.style.display = 'flex';
}
} catch(e) {}
}
if (USER) initSuperPanel();
// 수강 정보 로드 (수강생 수, 타인 난이도, 함께 학습자)
async function loadSocialInfo() {
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'get_lesson_social', lesson_id: LESSON_ID})
});
const data = await safeJson(res);
if (data.success) {
// 수강생 수
document.getElementById('studentNum').textContent = data.student_count || 0;
// 예상 공부시간
if (data.avg_duration) {
const mins = Math.round(data.avg_duration / 60);
let timeText = '약 ';
if (mins 0) {
const learnersDiv = document.getElementById('socialLearners');
const avatarsDiv = document.getElementById('learnerAvatars');
avatarsDiv.innerHTML = data.recent_learners.slice(0, 5).map(l =>
`
${l.avatar || '👤'}
`
).join('');
learnersDiv.style.display = 'flex';
window.recentLearners = data.recent_learners;
}
}
} catch(e) {}
}
// 응원 모달 표시
async function showCheerModal() {
if (!USER) { toast('로그인이 필요합니다'); return; }
const modal = document.getElementById('cheerModal');
const targetsDiv = document.getElementById('cheerTargets');
if (!modal || !targetsDiv) { toast('응원 기능을 로딩 중입니다. 잠시 후 다시 시도해주세요.'); return; }
// 이미 데이터가 있으면 바로 표시, 없으면 fetch
let learners = window.recentLearners;
if (!learners || learners.length === 0) {
targetsDiv.innerHTML = '
학습자 로딩 중...
';
modal.classList.add('show');
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'get_lesson_social', lesson_id: LESSON_ID})
});
const data = await safeJson(res);
if (data.success && data.recent_learners && data.recent_learners.length > 0) {
learners = data.recent_learners;
window.recentLearners = learners;
}
} catch(e) {}
} else {
modal.classList.add('show');
}
if (learners && learners.length > 0) {
targetsDiv.innerHTML = learners.map(l => `
${l.avatar || '👤'}
${l.nickname}
Lv.${l.level}
`).join('');
} else {
targetsDiv.innerHTML = '
아직 함께 학습한 사람이 없어요.다른 학습자가 이 레슨을 수강하면 응원할 수 있어요!
';
}
}
function closeCheerModal() {
document.getElementById('cheerModal').classList.remove('show');
}
function selectCheerTarget(el) {
document.querySelectorAll('.cheer-target').forEach(t => t.classList.remove('selected'));
el.classList.add('selected');
}
function selectCheerMessage(el) {
document.querySelectorAll('.cheer-msg-btn').forEach(b => b.classList.remove('selected'));
el.classList.add('selected');
document.getElementById('cheerCustom').value = el.textContent;
}
async function sendCheer() {
const target = document.querySelector('.cheer-target.selected');
const message = document.getElementById('cheerCustom').value.trim();
if (!target) {
toast('응원할 사람을 선택해주세요');
return;
}
if (!message) {
toast('응원 메시지를 입력해주세요');
return;
}
try {
const res = await fetch('/ajax_quest.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'send_cheer',
target_user_id: target.dataset.userId,
lesson_id: LESSON_ID,
message: message
})
});
const data = await safeJson(res);
if (data.success) {
toast('🎉 응원을 보냈어요!');
closeCheerModal();
} else {
toast(data.error || '전송 실패');
}
} catch(e) {
toast('네트워크 오류');
}
}
// 페이지 로드 시 수강 정보 로드
if (USER) loadSocialInfo();
// ========================================
// AI 학습 도우미
// ========================================
let aiHelperOpen = false;
const aiMessages = [];
function toggleAiHelper() {
const fab = document.getElementById('aiHelperFab');
const panel = document.getElementById('aiHelperPanel');
if (!fab || !panel) return;
aiHelperOpen = !aiHelperOpen;
if (aiHelperOpen) {
fab.style.display = 'none';
panel.classList.add('active');
document.getElementById('aiHelperInput')?.focus();
} else {
fab.style.display = 'flex';
panel.classList.remove('active');
}
}
async function sendAiQuestion() {
const input = document.getElementById('aiHelperInput');
const messages = document.getElementById('aiHelperMessages');
const sendBtn = document.getElementById('aiHelperSend');
const question = input.value.trim();
if (!question) return;
// 사용자 메시지 추가
const userMsg = document.createElement('div');
userMsg.className = 'ai-msg user';
userMsg.innerHTML = escapeHtml(question);
messages.appendChild(userMsg);
input.value = '';
input.style.height = 'auto';
sendBtn.disabled = true;
messages.scrollTop = messages.scrollHeight;
// 타이핑 인디케이터
const typingMsg = document.createElement('div');
typingMsg.className = 'ai-msg assistant typing';
typingMsg.innerHTML = '생각 중...';
messages.appendChild(typingMsg);
messages.scrollTop = messages.scrollHeight;
try {
// API 키 확인 (checkApiKeys와 동일한 방식으로 디코딩)
if (!hasApiKey || !apiProvider) {
checkApiKeys();
}
let apiKey = '';
if (simpleApiKeys && simpleApiKeys[apiProvider]) {
apiKey = simpleApiKeys[apiProvider];
}
if (!apiKey) {
// 폴백: 암호화 저장소에서 시도
const encStored = localStorage.getItem(API_KEY_STORAGE);
if (encStored) {
try {
const encData = JSON.parse(encStored);
if (encData[apiProvider]) apiKey = encData[apiProvider];
} catch(e) {}
}
}
if (!apiKey) {
typingMsg.innerHTML = '⚠️ AI API 키가 등록되지 않았습니다.
설정 → AI API 키에서 OpenAI, Claude, 또는 Gemini API 키를 등록해주세요.';
typingMsg.classList.remove('typing');
sendBtn.disabled = false;
return;
}
// 컨텍스트 구성
const context = {
lessonTitle: LESSON_TITLE,
lessonId: LESSON_ID,
userLevel: USER_LEVEL,
userXP: USER_DATA?.xp || 0,
pageContent: document.querySelector('.content')?.innerText?.substring(0, 3000) || ''
};
const systemPrompt = `당신은 DEVFOIL 플랫폼의 AI 학습 도우미입니다.
현재 사용자 정보:
- 레벨: ${context.userLevel}/100
- XP: ${context.userXP}
현재 학습 중인 레슨: "${context.lessonTitle}"
레슨 내용 요약:
${context.pageContent.substring(0, 1500)}
사용자의 레벨에 맞춰 친절하고 쉽게 설명해주세요.
레벨 1-5는 완전 초보자, 6-10은 초급, 11-20은 중급으로 판단합니다.
답변은 간결하게 3-5문장으로 해주세요.`;
// 프로바이더별 API 호출
let response;
const currentModel = apiModel || getDefaultModel(apiProvider);
if (apiProvider === 'gemini') {
// Google Gemini API
response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${currentModel}:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: systemPrompt + '\n\n사용자 질문: ' + question }] }],
generationConfig: { maxOutputTokens: 500, temperature: 0.7 }
})
});
} else if (apiProvider === 'claude') {
// Anthropic Claude API (CORS 프록시 필요 - 서버사이드 호출)
// Claude는 CORS 때문에 서버 프록시 필수 — 쿠키로 키 전달
var encK = btoa(unescape(encodeURIComponent(apiKey))).split('').reverse().join('');
document.cookie = 'df_ak=' + encodeURIComponent(encK) + ';path=/;max-age=60;SameSite=Strict;Secure';
response = await fetch('/ajax_quest.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'ai_proxy',
provider: 'claude',
api_key: '__COOKIE__',
model: currentModel,
system: systemPrompt,
message: question
})
});
document.cookie = 'df_ak=;path=/;max-age=0';
} else {
// OpenAI / GPT API (기본)
response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: currentModel,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
max_tokens: 500,
temperature: 0.7
})
});
}
const data = await safeJson(response);
// 프로바이더별 응답 파싱
let aiReply = '';
if (apiProvider === 'gemini') {
aiReply = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
if (!aiReply && data.error) {
aiReply = '';
}
} else if (apiProvider === 'claude') {
aiReply = data.reply || data.content?.[0]?.text || '';
} else {
aiReply = data.choices?.[0]?.message?.content || '';
}
if (aiReply) {
typingMsg.innerHTML = aiReply.replace(/\n/g, '
');
typingMsg.classList.remove('typing');
// AI 질문 XP 보상 요청
try {
const xpRes = await fetch('/ajax_quest.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'ai_question_xp',
lesson_id: LESSON_ID
})
});
const xpData = await safeJson(xpRes);
if (xpData.success && xpData.xp_awarded > 0) {
// XP 보상 알림 표시
const xpNotice = document.createElement('div');
xpNotice.className = 'ai-msg assistant';
xpNotice.style.cssText = 'background:linear-gradient(135deg,rgba(34,197,94,.2),rgba(16,185,129,.1));font-size:12px;padding:10px 14px;';
xpNotice.innerHTML = `🎉
+${xpData.xp_awarded} XP 획득! (오늘 ${xpData.daily_count}/${xpData.daily_limit}회)`;
messages.appendChild(xpNotice);
// 헤더 XP 업데이트
if (xpData.new_xp && window.USER_DATA) {
window.USER_DATA.xp = xpData.new_xp;
}
} else if (xpData.success && xpData.xp_awarded === 0) {
// 일일 한도 도달
const limitNotice = document.createElement('div');
limitNotice.className = 'ai-msg assistant';
limitNotice.style.cssText = 'background:rgba(255,255,255,.05);font-size:11px;padding:8px 12px;color:var(--text-muted);';
limitNotice.innerHTML = `💡 ${xpData.message}`;
messages.appendChild(limitNotice);
}
} catch(xpErr) {
console.log('XP reward error:', xpErr);
}
} else {
const errMsg = data.error?.message || data.error?.status || data.error || 'API 오류가 발생했습니다.';
typingMsg.innerHTML = `⚠️ ${errMsg}`;
typingMsg.classList.remove('typing');
}
} catch(e) {
typingMsg.innerHTML = '⚠️ 네트워크 오류가 발생했습니다. 다시 시도해주세요.';
typingMsg.classList.remove('typing');
}
sendBtn.disabled = false;
messages.scrollTop = messages.scrollHeight;
}
// Textarea 자동 높이 조절
document.getElementById('aiHelperInput')?.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 100) + 'px';
});
×
📣 응원하기
화이팅! 💪
같이 열공해요! 📚
멋져요! ⭐
응원합니다! 🎉
응원 보내기
';
options.forEach(function(opt, idx) {
var num = String.fromCharCode(9312 + idx); // ①②③④⑤
html += '';
html += ''+num+' ';
html += ''+opt.text+' ';
});
html += '