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와 웹 기술을 결합하는 경험이 매우 뜻깊었고 앞으로도 계속 심화해서 공부하고 싶다.


