데브포일 홈
DEV MODE - 실서버 영향 없음 📦 배포 관리
반복문으로 자동화
+60 XP
LEVEL 6 QUEST

반복문으로 자동화

같은 작업 100번? 한 줄이면 끝!

🔄
반복문으로 자동화
같은 작업 100번? 1000번? 코드 한 줄이면 끝! 반복의 마법을 배워봅시다
🤔 반복문이 필요한 이유

만약 "안녕하세요"를 100번 출력해야 한다면? console.log("안녕하세요")를 100번 복붙할 건가요? 그건 너무 비효율적이죠!

반복문을 사용하면 같은 작업을 원하는 만큼 자동으로 실행할 수 있어요. 프로그래밍에서 가장 강력한 도구 중 하나입니다!

❌ 반복문 없이

console.log("1번 학생 출석!"); console.log("2번 학생 출석!"); console.log("3번 학생 출석!"); console.log("4번 학생 출석!"); console.log("5번 학생 출석!"); // ... 30번까지 계속?! 😱

✅ 반복문 사용

for (let i = 1; i

실생활 속 반복문 비유

📋 출석부 부르기: 1번부터 30번까지 이름 호명하기
🏃 운동장 달리기: 운동장을 10바퀴 돌기
🎵 노래 반복: 후렴구를 3번 반복하기
📦 택배 분류: 상자 100개를 하나씩 확인하기
🍳 계란 프라이: 계란 5개를 하나씩 깨서 프라이하기
🔢 for 반복문 - 가장 많이 쓰는 반복

for문은 "몇 번 반복할지 정해져 있을 때" 사용해요. 시작, 조건, 증가를 한 줄에 작성합니다!

// for문 구조 // for (시작값; 조건; 증가/감소) { 반복할 코드 } for (let i = 0; i = 1; i--) { console.log(`${i}...`); } console.log("발사! 🚀");
1
초기화 (let i = 0)
반복에 사용할 변수를 만들고 시작값을 설정해요. 보통 i, j, k를 사용합니다. 딱 한 번만 실행돼요.
2
조건 (i
이 조건이 true인 동안 반복합니다. false가 되면 반복 종료! 매 반복마다 체크해요.
3
증감 (i++)
한 바퀴 돌 때마다 실행. i++는 1씩 증가, i--는 1씩 감소, i+=2는 2씩 증가를 의미합니다.
🔁 while 반복문 - 조건이 참인 동안

while문은 "몇 번 반복할지 모를 때" 주로 사용해요. 조건이 참인 동안 계속 반복합니다.

// while문 기본 구조 let count = 0; while (count 0.5; // 50% 확률 console.log(`${attempts}번째 시도: ${isHeads ? "앞면!" : "뒷면..."}`); } console.log(`${attempts}번 만에 앞면이 나왔습니다!`); // do...while - 최소 1번은 실행! let input; do { input = prompt("비밀번호를 입력하세요:"); } while (input !== "1234"); console.log("로그인 성공!");

for문 - 언제 쓸까?

반복 횟수가 정해져 있을 때
배열을 순회할 때
카운터가 필요할 때
90% 이상 for를 사용

while문 - 언제 쓸까?

반복 횟수를 모를 때
특정 조건이 만족될 때까지
사용자 입력 대기 시
조건 기반 반복에 적합

📋 forEach - 배열 전용 반복

배열의 각 요소를 하나씩 꺼내서 처리할 때는 forEach가 가장 깔끔해요! 배열 전용 반복 메서드입니다.

// 기본 forEach const fruits = ["사과", "바나나", "포도", "딸기", "수박"]; fruits.forEach(function(fruit) { console.log(`맛있는 ${fruit}!`); }); // 화살표 함수로 더 짧게! fruits.forEach(fruit => console.log(`맛있는 ${fruit}!`)); // 인덱스(순서)도 사용 가능 fruits.forEach((fruit, index) => { console.log(`${index + 1}번: ${fruit}`); }); // 1번: 사과 // 2번: 바나나 // 3번: 포도 ... // 실용 예제: 점수 합계 구하기 const scores = [90, 85, 100, 77, 92]; let total = 0; scores.forEach(score => { total += score; }); const average = total / scores.length; console.log(`평균: ${average}점`); // 평균: 88.8점 // for...of - forEach와 비슷하지만 break 가능! for (const fruit of fruits) { if (fruit === "포도") break; // 포도를 만나면 중단! console.log(fruit); // 사과, 바나나만 출력 } // for...in - 객체의 키 순회 const student = { name: "철수", age: 25, grade: "A" }; for (const key in student) { console.log(`${key}: ${student[key]}`); // name: 철수, age: 25, grade: A }
방법용도break 가능추천도
for범용 반복기본 중의 기본
while조건부 반복횟수 모를 때
forEach배열 순회배열 처리에 최고
for...of배열/이터러블모던한 방법
for...in객체 키 순회객체 전용
🛠️ 실습: 구구단 프로그램 만들기

반복문의 꽃! 구구단을 만들어 봅시다. 중첩 반복문(반복문 안의 반복문)도 배워요!

// 특정 단의 구구단 function gugudan(dan) { console.log(`=== ${dan}단 ===`); for (let i = 1; i

배열과 반복문 실전 예제

// 학생 성적 관리 시스템 const students = [ { name: "김철수", score: 95 }, { name: "이영희", score: 88 }, { name: "박민수", score: 72 }, { name: "정수진", score: 100 }, { name: "최동욱", score: 65 } ]; // 전체 평균 구하기 let total = 0; students.forEach(student => { total += student.score; }); const avg = total / students.length; console.log(`전체 평균: ${avg}점`); // 80점 이상인 학생만 필터링 const passed = []; for (const student of students) { if (student.score >= 80) { passed.push(student.name); } } console.log(`합격자: ${passed.join(", ")}`); // 합격자: 김철수, 이영희, 정수진 // 최고 점수 찾기 let maxScore = 0; let topStudent = ""; students.forEach(s => { if (s.score > maxScore) { maxScore = s.score; topStudent = s.name; } }); console.log(`1등: ${topStudent} (${maxScore}점)`);
⏹️ break & continue - 반복 제어

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의 차이를 이해했다
무한루프의 위험성과 예방법을 알았다
중첩 반복문(반복문 안의 반복문)을 이해했다
⚡ 필수 이 레슨의 난이도는 어땠나요?
👆 레슨 완료를 위해 난이도를 선택해주세요!
🤖 AI 학습 도우미 API 키 필요

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 += ` `; }); 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 = '
API 키가 감지되지 않습니다. 브라우저가 바뀌면 API 키를 다시 등록해야 합니다. 설정 → AI API 키 재등록
'; 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 ? `` : ''; // 유저 레벨 배지 const levelBadge = 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 = ` `; } let adminReplyHtml = ''; if (fb.admin_reply) { adminReplyHtml = ` `; } return ` `; }).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'; });
📣 응원하기
'; html += '
' + quiz.question + '
'; html += '
'; options.forEach(function(opt, idx) { var num = String.fromCharCode(9312 + idx); // ①②③④⑤ html += ''; }); html += '
'; html += ''; html += '
'; document.body.insertAdjacentHTML('beforeend', html); } function answerDbQuiz(popupId, isCorrect, btn, type, explanationEncoded) { var popup = document.getElementById(popupId); var result = document.getElementById(popupId + '_result'); var explanation = decodeURIComponent(explanationEncoded); // 모든 버튼 비활성화 popup.querySelectorAll('button').forEach(function(b) { b.disabled = true; b.style.opacity = '0.5'; }); btn.style.opacity = '1'; if (isCorrect) { btn.style.background = 'rgba(74,222,128,.15)'; btn.style.borderColor = '#4ade80'; btn.style.color = '#4ade80'; result.style.display = 'block'; result.style.background = 'rgba(74,222,128,.08)'; result.style.color = '#4ade80'; result.innerHTML = '✅ 정답! ' + (explanation ? '
' + explanation + '' : ''); if (type === 'checkpoint') checkpointsCompleted++; if (type === 'quiz') quizScore = 100; } else { btn.style.background = 'rgba(239,68,68,.15)'; btn.style.borderColor = '#ef4444'; btn.style.color = '#ef4444'; result.style.display = 'block'; result.style.background = 'rgba(239,68,68,.08)'; result.style.color = '#f87171'; result.innerHTML = '❌ 오답! ' + (explanation ? '
' + explanation + '' : ''); if (type === 'checkpoint') checkpointsCompleted++; if (type === 'quiz') quizScore = 50; } setTimeout(function() { popup.remove(); var el = Math.floor((Date.now() - lessonStartTime) / 1000); updateCompleteButton(el); }, 2500); }
🔒
여기까지는 미리보기입니다
반복문으로 자동화
무료 가입하면 이어서 볼 수 있고, 강의를 완료할 때마다 XP와 레벨이 쌓입니다.
Google로 3초 만에 시작 →🧵 Threads로 시작무료 공개 강의 둘러보기 (Lv.1~3)