메인
home
소프트웨어
home

[나만의 웹사이트 제작하기][영동고][배현준]

1. 자유로운 웹사이트 코드펜 작업

자유로운 웹사이트 코드펜 작업링크 : CodePen CodePen - Create a New Pen
웹사이트 구상 : 학습 플래너 쓸때 목표에다가 명언 하나씩 쓰는데 찾기 귀찮아서 스페이스바를 누르면 명언이 나오게 하는 사이트를 만들었다
만들기
이런 프롬포트를 적었더니
<!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:#60a5fa; --muted:#94a3b8; --glass: rgba(255,255,255,0.03); } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; font-family: Inter, "Noto Sans KR", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; background: linear-gradient(180deg, #071026 0%, #081124 60%, #04101a 100%); color: #e6eef8; display:flex; align-items:center; justify-content:center; padding:24px; } .card{ width:100%; max-width:720px; background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); border-radius:16px; padding:28px; box-shadow: 0 10px 30px rgba(2,6,23,0.6); border:1px solid rgba(255,255,255,0.04); backdrop-filter: blur(6px); } header{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:18px; } .title{ display:flex; gap:12px; align-items:center; } .logo{ width:48px; height:48px; display:grid; place-items:center; border-radius:10px; background:linear-gradient(135deg, rgba(96,165,250,0.15), rgba(96,165,250,0.06)); border:1px solid rgba(96,165,250,0.12); font-weight:700; color:var(--accent); font-size:18px; } h1{ font-size:18px; margin:0; letter-spacing: -0.2px; } p.lead{ margin:0; color:var(--muted); font-size:13px; } .quote-wrap{ padding:28px; border-radius:12px; background: linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.02)); border:1px solid rgba(255,255,255,0.03); min-height:160px; display:flex; flex-direction:column; justify-content:center; gap:14px; transition: transform .18s ease, opacity .18s ease; } blockquote{ margin:0; font-size:20px; line-height:1.45; font-weight:500; quotes: "“" "”" "‘" "’"; } blockquote::before{content: open-quote; margin-right:6px; color:var(--accent); font-size:28px} .author{ text-align:right; color:var(--muted); font-size:14px; margin-top:6px; } .controls{ display:flex; gap:12px; margin-top:18px; align-items:center; justify-content:space-between; } .left-controls{ display:flex; gap:8px; align-items:center; } button{ background:var(--glass); color:inherit; border:1px solid rgba(255,255,255,0.04); padding:10px 14px; border-radius:10px; cursor:pointer; font-size:14px; display:inline-flex; gap:8px; align-items:center; transition: transform .12s ease, background .12s ease; } button:hover{ transform: translateY(-3px) } .primary{ background: linear-gradient(90deg, rgba(96,165,250,0.12), rgba(96,165,250,0.06)); border:1px solid rgba(96,165,250,0.18); box-shadow: 0 6px 18px rgba(2,6,23,0.45); color:var(--accent); font-weight:600; } .icon{ width:18px;height:18px;display:inline-grid;place-items:center; } .muted-small{ color:var(--muted); font-size:13px } /* fade animation classes */ .fade-out{ opacity:0; transform: translateY(6px); } .fade-in{ opacity:1; transform: translateY(0); transition: opacity .28s ease, transform .28s ease; } @media (max-width:520px){ blockquote{ font-size:18px } .card{ padding:18px } .controls{flex-direction:column;align-items:stretch} .left-controls{width:100%;justify-content:space-between} } </style> </head> <body> <main class="card" aria-live="polite"> <header> <div class="title"> <div class="logo" aria-hidden="true">QT</div> <div> <h1>랜덤 명언 생성기</h1> <p class="lead">버튼을 누르면 새로운 명언이 나타납니다. (API: type.fit)</p> </div> </div> <div class="muted-small" id="status">로딩 전</div> </header> <section class="quote-wrap fade-in" id="quoteWrap"> <blockquote id="quote">로딩 중...</blockquote> <div class="author" id="author"></div> </section> <div class="controls"> <div class="left-controls"> <button id="newQuoteBtn" class="primary" title="새 명언"> <span class="icon">🔁</span>새 명언 </button> <button id="copyBtn" title="명언 복사"> <span class="icon">📋</span>복사 </button> <button id="tweetBtn" title="트윗으로 공유"> <span class="icon">🐦</span>트윗 </button> </div> <div class="muted-small" style="text-align:right"> <div>Tip: 스페이스바로도 새 명언을 볼 수 있어요</div> </div> </div> </main> <script> (function(){ const API = "https://type.fit/api/quotes"; const quoteEl = document.getElementById("quote"); const authorEl = document.getElementById("author"); const newBtn = document.getElementById("newQuoteBtn"); const copyBtn = document.getElementById("copyBtn"); const tweetBtn = document.getElementById("tweetBtn"); const statusEl = document.getElementById("status"); const wrap = document.getElementById("quoteWrap"); let quotes = []; let lastIndex = -1; const fallback = [ { text: "성공은 최종 목표가 아니라 과정이다. 계속 움직여라.", author: "익명" }, { text: "작은 습관들이 큰 변화를 만든다.", author: "익명" }, { text: "배움은 멈추지 않는 자의 특권이다.", author: "익명" } ]; function setStatus(text){ statusEl.textContent = text; } function fadeSwitch(newText, newAuthor){ wrap.classList.remove("fade-in"); wrap.classList.add("fade-out"); // 짧게 기다렸다가 텍스트 바꾸고 fade-in setTimeout(() => { quoteEl.textContent = newText; authorEl.textContent = newAuthor ? `— ${newAuthor}` : "— 익명"; wrap.classList.remove("fade-out"); wrap.classList.add("fade-in"); }, 180); } function pickRandom(){ if (!quotes || quotes.length === 0) return fallback[Math.floor(Math.random()*fallback.length)]; if (quotes.length === 1) return quotes[0]; let idx; do { idx = Math.floor(Math.random()*quotes.length); } while (idx === lastIndex); lastIndex = idx; return quotes[idx]; } async function fetchQuotes(){ setStatus("API에서 명언 불러오는 중..."); try { const res = await fetch(API, {cache: "force-cache"}); if (!res.ok) throw new Error("네트워크 에러"); const data = await res.json(); // 정상적 구조인지 확인해서 필터 quotes = Array.isArray(data) ? data.filter(q => q && q.text).map(q => ({text: q.text.trim(), author: (q.author || "").trim()})) : []; if (quotes.length === 0) throw new Error("빈 데이터"); setStatus(`명언 ${quotes.length}개 로드됨`); } catch (e) { console.warn("Quotes fetch failed:", e); quotes = fallback; setStatus("API 불가 — 기본 문구 사용"); } } function showRandom(){ const q = pickRandom(); fadeSwitch(q.text, q.author); } // 이벤트 바인딩 newBtn.addEventListener("click", showRandom); copyBtn.addEventListener("click", async () => { const text = `${quoteEl.textContent}\n${authorEl.textContent}`; try { await navigator.clipboard.writeText(text); setStatus("명언을 클립보드에 복사했어요"); setTimeout(()=> setStatus(quotes && quotes.length ? `명언 ${quotes.length}개 로드됨` : "기본 문구 사용"), 1400); } catch { setStatus("복사 실패 — 수동으로 선택해 복사하세요"); } }); tweetBtn.addEventListener("click", () => { const tweetText = `${quoteEl.textContent} ${authorEl.textContent}`; const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`; window.open(url, "_blank", "noopener"); }); // 스페이스바로도 새 명언 window.addEventListener("keydown", (e) => { if (e.code === "Space" && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA") { e.preventDefault(); showRandom(); } }); // 초기 로드: fetch 후 첫 명언 표시 (async function init(){ setStatus("로딩 중..."); await fetchQuotes(); showRandom(); })(); // accessibility: ARIA live already on main; focus management optional })(); </script> </body> </html>
Python
복사
작동을 시켜보니 익명으로 뜨는 것이 아쉬워서 고쳐보기로 했다
익명인 자료들을 다 지워서 해결하였다
수정본 작동 영상
소감:이 랜덤 명언 생성기 만들면서, 간단하지만 사용자 경험을 신경 쓴 게 마음에 들었다. API에서 실시간으로 명언을 받아오고, 익명 대신 작가 이름 있는 명언만 보여주니까 더 신뢰감도 생기고 멋져 보이고. 복사나 공유 기능도 있어서 실제로 쓸 때 편리하고, 디자인도 세련돼서 사용하기 좋았다. 이렇게 작은 과제로도 재미있게 웹 개발 감각을 키울 수 있다는 걸 다시 느꼈다.
간단한 설명: 이 사이트는 랜덤 명언 생성기다. 버튼이나 스페이스바를 누르면 실제 인물들이 한 명언과 작가 이름이 새로 뜨는데, 작가 이름 없는 명언은 자동으로 걸러져서 ‘익명’으로 안 나온다. 명언은 공개 API에서 받아오고, 복사 버튼으로 클립보드에 저장할 수도 있고, 트위터로 바로 공유도 가능하다. 명언이 부드럽게 바뀌는 애니메이션도 있고, 디자인도 깔끔한 유리판 느낌이라 보기 좋다. 쉽게 명언을 보고 공유할 수 있는 간단한 웹사이트이다.

2. ai를 활용한 인식관련된 웹사이트 제작

ai를 활용한 인식관련된 웹사이트 코드펜 링크: https://codepen.io/rxgfdzbv-the-flexboxer/pen/yyemeMz
구상: 얼굴표정을 인식해서 표정에 따라 아래 적어주는 사이트
코드
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>얼굴 표정 인식 감정 읽기</title> <style> body { margin: 0; padding: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; background-color: #222; color: #eee; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; transition: background-color 0.5s ease; } video, canvas { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 640px; height: 480px; border-radius: 12px; box-shadow: 0 0 20px #0008; } #emotionText { position: relative; margin-top: 520px; font-size: 2rem; font-weight: bold; text-align: center; user-select: none; } </style> </head> <body> <video class="input_video" autoplay muted playsinline></video> <canvas class="output_canvas" width="640" height="480"></canvas> <div id="emotionText">감정 인식 중...</div> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script> <script> const videoElement = document.querySelector('.input_video'); const canvasElement = document.querySelector('.output_canvas'); const canvasCtx = canvasElement.getContext('2d'); const emotionText = document.getElementById('emotionText'); const faceMesh = new FaceMesh({ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}` }); faceMesh.setOptions({ maxNumFaces: 1, refineLandmarks: true, minDetectionConfidence: 0.7, minTrackingConfidence: 0.7 }); faceMesh.onResults(onResults); const camera = new Camera(videoElement, { onFrame: async () => { await faceMesh.send({image: videoElement}); }, width: 640, height: 480 }); camera.start(); // 입술 좌표 인덱스 (미디어파이프 기준) const upperLip = 13; // 윗입술 중심 const lowerLip = 14; // 아랫입술 중심 function distance(a, b) { return Math.sqrt( Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2) ); } function onResults(results) { canvasCtx.save(); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); // 비디오 원본 출력 canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) { const landmarks = results.multiFaceLandmarks[0]; // 랜드마크 점 그리기 for (const landmark of landmarks) { canvasCtx.beginPath(); canvasCtx.arc(landmark.x * canvasElement.width, landmark.y * canvasElement.height, 1.5, 0, 2 * Math.PI); canvasCtx.fillStyle = '#32e0c4'; canvasCtx.fill(); } // 입술 거리 계산 (웃음 판단 단순 기준) const lipDist = distance(landmarks[upperLip], landmarks[lowerLip]); // 대략 입 벌린 정도에 따라 웃음 판단 // 실험적으로 0.04 이상이면 웃음으로 간주 (조절 가능) if (lipDist > 0.04) { emotionText.textContent = '😊 웃고 있어요!'; document.body.style.backgroundColor = '#4CAF50'; } else { emotionText.textContent = '😐 무표정 또는 입 닫음'; document.body.style.backgroundColor = '#222'; } } else { emotionText.textContent = '얼굴을 인식할 수 없어요'; document.body.style.backgroundColor = '#222'; } canvasCtx.restore(); } </script> </body> </html>
Python
복사
작동영상
모든 표정이 다 무표정으로 떠서 기준값을 수정했다
수정본 작동영상
이번 과제를 하면서 미디어파이프 Face Mesh의 강력한 기능을 직접 체험할 수 있어 매우 흥미로웠다. 웹캠을 통해 실시간으로 얼굴 랜드마크를 추적하고, 이를 기반으로 웃음과 무표정 상태를 구분하는 간단한 알고리즘을 구현하며 컴퓨터 비전의 기본 원리를 이해하는 데 큰 도움이 되었다. 특히 얼굴 크기에 따른 입 벌림 비율을 계산해 정확도를 높인 점이 인상적이었고, 직접 눈으로 결과를 확인하며 실시간 피드백이 주는 재미도 컸다. 앞으로 더 다양한 표정과 감정을 인식할 수 있도록 확장해보고 싶고, 이 기술이 인터랙티브 웹앱, 게임, 소셜 필터 등 여러 분야에 활용될 가능성에 대해 기대가 커졌다. 프로젝트를 통해 AI와 웹 기술을 결합하는 경험이 매우 뜻깊었고 앞으로도 계속 심화해서 공부하고 싶다.