1. 자유로운 웹사이트
•
끝말잇기 웹사이트인데, 컴퓨터가 가지고 있는 단어 데이터셋이 작다는 한계가 있다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>끝말잇기 게임</title>
<style>
body { font-family: 'Arial', sans-serif; background: #f5f5f5; text-align: center; padding: 20px; }
h1 { color: #333; }
#gameContainer { background: white; padding: 20px; border-radius: 12px; display: inline-block; width: 90%; max-width: 500px; }
input { padding: 8px; width: 60%; font-size: 16px; }
button { padding: 8px 12px; font-size: 16px; margin-left: 6px; cursor: pointer; }
#log { margin-top: 20px; max-height: 300px; overflow-y: auto; text-align: left; }
.logItem { margin: 4px 0; }
.player { color: #1d4ed8; }
.computer { color: #dc2626; }
</style>
</head>
Python
복사
<body>
<h1>끝말잇기 게임</h1>
<div id="gameContainer">
<div>
<input type="text" id="playerInput" placeholder="단어 입력" />
<button id="submitBtn">제출</button>
</div>
<div id="log"></div>
<button id="restartBtn" style="margin-top:10px;">게임 다시 시작</button>
</div>
<script>
const wordBank = [
"가게","가구","가방","가수","가위","가을","가장","가족","간장","갈비","감자","감사","강아지","개미","개발","거울","건물","결혼","경찰","계란",
"고구마","고등어","고양이","공기","공원","과일","관심","교실","구두","국수","귤","귀걸이","그림","금요일","기계","기차","김치","깃발","나무",
"나비","나이","낚시","날씨","남자","냉장고","노트","노래","농구","눈","다리","다섯","다이어트","단어","단추","달력","담배","대문","대학",
"도넛","도서관","독서","동물","돼지","드럼","라디오","라면","로봇","마늘","마당","마스크","마음","마차","만두","망치","맛집","매미","머리",
"메뉴","모자","목걸이","무궁화","무대","문제","물고기","물병","미술","미소","민주","바다","바람","바지","박물관","반지","발목","밥","방문",
"배낭","백화점","버스","버섯","벌","병원","보리","보물","볼펜","부엌","부채","북극","분수","불고기","사과","사랑","사진","사회","산책",
"상자","새우","생일","서랍","선물","설탕","성냥","세수","소금","소방서","소파","손수건","수박","수영","스키","스마트폰","시간","시계",
"신문","신발","심장","쌀","아기","아이스크림","아이디어","악기","안경","앵무새","야구","약국","양말","어깨","어린이","얼굴","연필","영화",
"오렌지","오리","오토바이","옥수수","온도","와인","우유","우산","운동","원숭이","위치","유리","유리병","은행","의자","이름","이불","이유",
"인형","일기","일본","자동차","자전거","장갑","재미","저녁","점심","정원","젓가락","조개","조명","주스","주전자","지갑","지우개","진달래",
"짜장면","차량","창문","책상","천장","청소","체육관","초콜릿","추석","치약","카메라","캠핑","커피","컴퓨터","코끼리","콩","컵","타조","타이어",
"탁자","탐험","태양","터널","토마토","통계","파도","파란색","파리","팥","팬","펜","포도","포장","푸른색","프린터","피아노","필통","하늘",
"학교","학원","한글","한라산","항공","해변","햄버거","행복","향수","허리","헬리콥터","호수","호텔","홍수","화장실","화장품","환자","회색",
"회전문","효과","후추","휴대폰","흰색","가르침","가사","가족사진","각도","간호","감기","갑옷","강물","강아지풀","개발자","거미","건강",
"결혼식","경기","계단","계절","고양이털","골목","공원벤치","과학","관람","교육","구름","구급차","국기","국립공원","군인","굴","권투","귀신",
"그네","그림자","금고","금연","기념일","기념품","기차역","김밥","깃발꽂이","나비꽃","나침반","남산","남편","냄비","냉면","노래방","노트북",
"농장","눈송이","단추구멍","달팽이","담요","대나무","대문간","대학원","도시락","도장","독서대","동굴","동상","돼지감자","드라이기","라벨",
"라이터","라임","마라톤","마술","마이크","마켓","마지막","만화","망원경","맛집투어","머리띠","메달","모래","모기","목재","몰래","무늬",
"무지개","문구점","문학","물고기잡이","미니","미술관","바닷가","바닷물","바둑","바람개비","바지춤","박람회","박물관","반지갑","발레","발전소",
"밥솥","방수","배경","백사장","버튼","번개","벌집","병뚜껑","보리밭","보물상자","볼링","부엌용품","부채질","북극곰","분필","불꽃놀이",
"사다리","사막","사업","사진첩","사회학","산책로","상자","새벽","생쥐","서랍장","선글라스","선물상자","설거지","설탕물","성냥갑","세탁기",
"소나무","소방차","소풍","소화기","손목시계","수건","수박화채","수영복","스마트워치","스케이트","스프링","시간표","시소","시장","시계탑",
"신발장","심장박동","쌀밥","아이스","아이스박스","아이디어","안전모","앵두","야채","야외","약국앞","양말목","어깨끈","어린왕자","얼음","연극",
"연필꽂이","영화관","오렌지주스","오리발","오징어","옥수수밭","온돌","와인잔","우산꽃","우체국","운동화","원숭이바나나","위성","유리병뚜껑",
"은행나무","의사","이름표","이불커버","이유식","인형극","일기장","자동판매기","자전거도로","장갑끈","재킷","저녁밥","점심시간","정원수","젓가락통",
"조개껍질","조명등","주스팩","주전자","지갑속","지우개통","진달래꽃","짜장면사발","차량등록","창문틀","책상서랍","천장등","청소기","체육관운동",
"초콜릿케이크","추석선물","치약튜브","카메라렌즈","캠핑장","커피잔","컴퓨터마우스","코끼리상아","콩나물","컵라면","타조알","타이어펑크","탁자위",
"탐험가","태양빛","터널입구","토마토케첩","통계표","파도소리","파란색연필","파리바게뜨","팥빙수","팬티","펜슬","포도주","포장지","푸른색펜","프린터용지",
"피아노건반","필통","하늘색","학교정문","학원버스","한글책","한라산국립공원","항공권","해변모래","햄버거세트","행복주머니","향수병","허리띠","헬리콥터모형",
"호수공원","호텔방","홍수경보","화장실","화장품상자","환자복","회색양말","회전문","효과음","후추","휴대폰","흰색셔츠"
];
let lastChar = "";
let usedWords = [];
const input = document.getElementById("playerInput");
const submitBtn = document.getElementById("submitBtn");
const log = document.getElementById("log");
const restartBtn = document.getElementById("restartBtn");
function appendLog(text, who) {
const div = document.createElement("div");
div.className = "logItem " + who;
div.textContent = text;
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
function computerTurn() {
const possible = wordBank.filter(w => !usedWords.includes(w) && w[0] === lastChar);
if(possible.length === 0) {
appendLog("컴퓨터가 낼 단어가 없습니다. 플레이어 승리!", "computer");
submitBtn.disabled = true;
input.disabled = true;
return;
}
const word = possible[Math.floor(Math.random() * possible.length)];
usedWords.push(word);
lastChar = word[word.length-1];
appendLog("컴퓨터: " + word, "computer");
}
submitBtn.addEventListener("click", ()=>{
const word = input.value.trim();
if(word === "") return;
if(usedWords.includes(word)) {
alert("이미 사용한 단어입니다!");
return;
}
if(lastChar && word[0] !== lastChar) {
alert(`단어가 이어지지 않습니다! 단어의 첫 글자는 '${lastChar}' 이어야 합니다.`);
return;
}
usedWords.push(word);
lastChar = word[word.length-1];
appendLog("플레이어: " + word, "player");
input.value = "";
setTimeout(computerTurn, 800);
});
restartBtn.addEventListener("click", ()=>{
usedWords = [];
lastChar = "";
log.innerHTML = "";
input.disabled = false;
submitBtn.disabled = false;
});
</script>
</body>
</html>
Python
복사
2. 미디어파이프 AI를 활용한 손 인식 서비스
•
수도 맞추기 퀴즈 웹사이트인데, 웹캠을 이용하여 손가락으로 표시하면 손가락 개수를 인식해서 정답 여부를 알려준다. 첫 번째 시도에 맞추면 +3점, 두 번째 시도에 맞추면 +2점, 2번 틀리면 다음 문제로 넘어간다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>세계 수도 객관식 퀴즈 (손 인식)</title>
<style>
:root{--bg:#0f1724;--card:#0b1220;--accent:#3b82f6;--muted:#94a3b8;color-scheme:dark}
html,body{height:100%;margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,'Noto Sans KR',sans-serif;background:linear-gradient(180deg,#071026 0%, #071527 40%);color:#e6eef8}
.wrap{max-width:980px;margin:28px auto;padding:20px}
header{display:flex;align-items:center;gap:12px}h1{font-size:20px;margin:0}
.card{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));border-radius:12px;padding:18px;box-shadow:0 6px 18px rgba(2,6,23,0.6);}
.layout{display:grid;grid-template-columns:1fr 360px;gap:18px;margin-top:14px}
.map-area{min-height:420px;display:flex;flex-direction:column;gap:12px}
.map{flex:1;border-radius:8px;overflow:hidden;background:#07142a;display:flex;align-items:center;justify-content:center;position:relative}
.map img{max-width:100%;height:auto;display:block}
.question{display:flex;align-items:center;justify-content:space-between;gap:12px}
.country-name{font-size:18px;font-weight:600}
.controls{display:flex;gap:8px}
button{background:transparent;border:1px solid rgba(255,255,255,0.06);color:inherit;padding:8px 12px;border-radius:8px;cursor:pointer}
.choices{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}
.choice{padding:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;cursor:pointer;text-align:left}
.choice.correct{outline:3px solid rgba(59,130,246,0.18);border-color:rgba(59,130,246,0.28)}
.choice.wrong{opacity:0.6;filter:grayscale(40%)}
.sidebar{width:100%;}.stat{display:flex;flex-direction:column;gap:10px}.stat .row{display:flex;justify-content:space-between;align-items:center}
.small{font-size:13px;color:var(--muted)}
.score{font-size:28px;font-weight:700;color:var(--accent)}
.footer{margin-top:14px;display:flex;justify-content:space-between;align-items:center}
.seed{font-size:12px;color:var(--muted)}
.hint{font-size:13px;color:var(--muted);margin-top:8px}
.top-controls{display:flex;gap:8px}
#cameraContainer{position:absolute;right:12px;top:12px;width:220px;height:160px;border-radius:8px;overflow:hidden;background:rgba(0,0,0,0.35);display:flex;align-items:center;justify-content:center;flex-direction:column;padding:6px}
#videoElement{width:100%;height:100%;object-fit:cover;display:block}
#canvasOverlay{position:absolute;right:12px;top:12px;width:220px;height:160px;border-radius:8px;pointer-events:none}
#fingerCountBadge{position:absolute;right:12px;bottom:12px;background:rgba(0,0,0,0.6);padding:6px 10px;border-radius:999px;font-weight:700}
@media(max-width:880px){.layout{grid-template-columns:1fr}.sidebar{order:2}.map-area{order:1}}
</style>
</head>
Python
복사
<body>
<div class="wrap">
<header>
<h1>세계 수도 객관식 퀴즈 (손 인식)</h1>
</header>
<div class="layout">
<section class="card map-area">
<div class="map">
<img id="worldMap" src="https://upload.wikimedia.org/wikipedia/commons/8/80/World_map_-_low_resolution.svg" alt="World map">
<div id="cameraContainer" style="display:none">
<video id="videoElement" autoplay muted playsinline></video>
<canvas id="cameraCanvas" width="220" height="160"></canvas>
</div>
<canvas id="canvasOverlay" width="220" height="160" style="display:none"></canvas>
<div id="fingerCountBadge" style="display:none">0</div>
</div>
<div class="question">
<div>
<div class="country-name" id="countryName">로딩 중...</div>
<div class="hint" id="hintText">손가락 개수로 선택</div>
</div>
<div class="controls">
<button id="nextBtn">다음</button>
</div>
</div>
<div class="choices" id="choices"></div>
<div id="answerMessage" class="hint" style="margin-top:6px;font-weight:bold;"></div>
</section>
<aside class="card sidebar">
<label><input type="checkbox" id="useHand"> 손 인식 사용</label>
<div class="stat">
<div>점수: <span id="score">0</span></div>
<div>문제 수: <span id="qCount">0</span></div>
<div>정답 수: <span id="correctNum">0</span></div>
<div>오답 수: <span id="wrongNum">0</span></div>
</div>
</aside>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
<script>
const allData=[
{country:'대한민국',capital:'서울'},{country:'일본',capital:'도쿄'},{country:'중국',capital:'베이징'},{country:'미국',capital:'워싱턴 D.C.'},
{country:'영국',capital:'런던'},{country:'프랑스',capital:'파리'},{country:'독일',capital:'베를린'},{country:'이탈리아',capital:'로마'},
{country:'스페인',capital:'마드리드'},{country:'러시아',capital:'모스크바'},{country:'캐나다',capital:'오타와'},{country:'호주',capital:'캔버라'},
{country:'인도',capital:'뉴델리'},{country:'브라질',capital:'브라질리아'},{country:'아르헨티나',capital:'부에노스아이레스'},{country:'멕시코',capital:'멕시코시티'},
{country:'네덜란드',capital:'암스테르담'},{country:'벨기에',capital:'브뤼셀'},{country:'스웨덴',capital:'스톡홀름'},{country:'노르웨이',capital:'오슬로'},
{country:'폴란드',capital:'바르샤바'},{country:'터키',capital:'앙카라'},{country:'사우디아라비아',capital:'리야드'},{country:'이집트',capital:'카이로'},
{country:'남아프리카공화국',capital:'프리토리아'},{country:'나이지리아',capital:'아부자'},{country:'케냐',capital:'나이로비'},{country:'태국',capital:'방콕'},
{country:'베트남',capital:'하노이'},{country:'인도네시아',capital:'자카르타'},{country:'말레이시아',capital:'쿠알라룸푸르'},{country:'필리핀',capital:'마닐라'},
{country:'이스라엘',capital:'예루살렘'},{country:'그리스',capital:'아테네'},{country:'포르투갈',capital:'리스본'},{country:'스위스',capital:'베른'}
];
let pool=[],current=null,score=0,totalAsked=0,correctCount=0,wrongCount=0,tries=0;
const countryNameEl=document.getElementById('countryName');
const choicesEl=document.getElementById('choices');
const scoreEl=document.getElementById('score');
const qCountEl=document.getElementById('qCount');
const correctNumEl=document.getElementById('correctNum');
const wrongNumEl=document.getElementById('wrongNum');
const answerMessageEl=document.getElementById('answerMessage');
const useHandEl=document.getElementById('useHand');
function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}
function buildPool(){pool=shuffle(allData.slice()); totalAsked=0; correctCount=0; wrongCount=0; updateStats();}
function pickQuestion(){if(pool.length===0) buildPool(); current=pool.pop(); totalAsked++; tries=0; countryNameEl.textContent=current.country; renderChoices(current); answerMessageEl.textContent=''; updateStats();}
function renderChoices(q){const opts=new Set([q.capital]); while(opts.size<4){ opts.add(allData[Math.floor(Math.random()*allData.length)].capital); } const arr=shuffle(Array.from(opts)); choicesEl.innerHTML=''; arr.forEach((text,idx)=>{ const btn=document.createElement('button'); btn.className='choice card'; btn.textContent=text; btn.dataset.index=idx+1; btn.onclick=()=>handleAnswer(btn,text); choicesEl.appendChild(btn); });}
function handleAnswer(btn,text){if(btn.disabled) return; tries++; if(text===current.capital){ let pts=(tries===1)?3:(tries===2)?2:0; score+=pts; correctCount++; btn.classList.add('correct'); answerMessageEl.textContent=`✅ 정답입니다! (+${pts}점)`; Array.from(choicesEl.children).forEach(c=>c.disabled=true); updateStats(); setTimeout(pickQuestion,3000); } else { btn.classList.add('wrong'); answerMessageEl.textContent='❌ 오답입니다! 다시 시도하세요'; updateStats(); if(tries>=2){ wrongCount++; Array.from(choicesEl.children).forEach(c=>{ if(c.textContent===current.capital) c.classList.add('correct'); c.disabled=true; }); setTimeout(pickQuestion,3000); }}}
function updateStats(){scoreEl.textContent=score; qCountEl.textContent=`${totalAsked} / ${totalAsked + pool.length}`; correctNumEl.textContent=correctCount; wrongNumEl.textContent=wrongCount;}
document.getElementById('nextBtn').addEventListener('click',pickQuestion);
buildPool(); pickQuestion();
// MediaPipe Hands
let hands=null,camera=null,lastSelected=null,lastCountTime=0;
function startHands(){if(hands) return; hands=new Hands({locateFile:(file)=>`https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`}); hands.setOptions({maxNumHands:1,modelComplexity:1,minDetectionConfidence:0.7,minTrackingConfidence:0.6}); hands.onResults(onResults); camera=new Camera(document.getElementById('videoElement'),{onFrame: async ()=>{await hands.send({image:document.getElementById('videoElement')});},width:640,height:480}); camera.start(); document.getElementById('cameraContainer').style.display='block'; document.getElementById('canvasOverlay').style.display='block'; document.getElementById('fingerCountBadge').style.display='block';}
function stopHands(){if(camera){camera.stop(); camera=null;} if(hands){hands.close(); hands=null;} document.getElementById('cameraContainer').style.display='none'; document.getElementById('canvasOverlay').style.display='none'; document.getElementById('fingerCountBadge').style.display='none';}
useHandEl.addEventListener('change',()=>{if(useHandEl.checked){startHands();} else{stopHands();}});
function countFingers(landmarks,handLabel){if(!landmarks) return 0; const tips=[8,12,16,20]; const pips=[6,10,14,18]; let count=0; for(let i=0;i<4;i++){if(landmarks[tips[i]].y<landmarks[pips[i]].y) count++;} const thumbTip=landmarks[4],thumbIp=landmarks[3]; if(handLabel==='Left'){if(thumbTip.x>thumbIp.x) count++;}else{if(thumbTip.x<thumbIp.x) count++;} return count;}
function onResults(results){const camCtx=document.getElementById('cameraCanvas').getContext('2d'); camCtx.save(); camCtx.clearRect(0,0,220,160); camCtx.drawImage(results.image,0,0,220,160); camCtx.restore(); const canvasCtx=document.getElementById('canvasOverlay').getContext('2d'); canvasCtx.save(); canvasCtx.clearRect(0,0,220,160); if(results.multiHandLandmarks && results.multiHandLandmarks.length>0){ for(let i=0;i<results.multiHandLandmarks.length;i++){const landmarks=results.multiHandLandmarks[i]; drawConnectors(canvasCtx,landmarks,HAND_CONNECTIONS,{lineWidth:2}); drawLandmarks(canvasCtx,landmarks,{lineWidth:1}); const handLabel=results.multiHandedness[i].label||'Right'; const cnt=countFingers(landmarks,handLabel); document.getElementById('fingerCountBadge').textContent=cnt; const now=Date.now(); if(cnt>=1 && cnt<=4){ if(lastSelected!==cnt || now-lastCountTime>900){ const btn=Array.from(choicesEl.children).find(c=>c.dataset.index==cnt); if(btn && !btn.disabled){ btn.click(); lastSelected=cnt; lastCountTime=now; }}}}} else{document.getElementById('fingerCountBadge').textContent='—';} canvasCtx.restore();}
window.addEventListener('keydown',e=>{if(e.key>='1' && e.key<='4'){ const el=choicesEl.children[parseInt(e.key)-1]; if(el && !el.disabled) el.click();}});
</script>
</body>
</html>
Python
복사
