메인
home
소프트웨어
home

[나만의 웹사이트 제작하기][정신여고][이영은]

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
복사