소개
Ai상담후 현재 기분,날씨,시간에 어울리고 이용자에개 필요한 음악 추천하는 웹 사이트
진행 방법
기획안을 몇일간 작성후 Claude 에게 기획안을 주고 만들어 달라고 했습니다
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Noto+Serif+KR:wght@300;400;600&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet"/>
<style>
*{box-sizing:border-box;margin:0;padding:0;}
:root{
--bg:#090909;--bg2:#0f0f0f;--bg3:#141414;
--gold:#C4A96A;--gold-dim:#8a7249;--gold-bright:#e8c87e;
--white:#f0ece4;--white-dim:rgba(240,236,228,0.55);--white-faint:rgba(240,236,228,0.10);
--border:rgba(196,169,106,0.22);--border-faint:rgba(196,169,106,0.09);
--fd:'Cormorant Garamond',Georgia,serif;
--fb:'Noto Serif KR',serif;
--fm:'DM Mono',monospace;
}
html,body{height:100%;overflow:hidden;}
body{background:var(--bg);color:var(--white);font-family:var(--fb);font-size:14px;}
canvas#pc{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;}
.glow{position:fixed;width:500px;height:500px;background:radial-gradient(circle,rgba(196,169,106,0.07) 0%,transparent 70%);right:-100px;bottom:-80px;pointer-events:none;z-index:0;animation:gp 24s ease-in-out infinite;}
@keyframes gp{0%,100%{opacity:.3;}50%{opacity:1;}}
nav{position:fixed;top:0;left:0;right:0;z-index:100;display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid var(--border-faint);background:rgba(9,9,9,0.92);backdrop-filter:blur(20px);}
.logo{font-family:var(--fd);font-size:1.1rem;letter-spacing:.1em;cursor:pointer;}
.logo span{color:var(--gold);}
.nav-links{display:flex;gap:18px;}
.nav-links a{font-family:var(--fm);font-size:.6rem;letter-spacing:.1em;text-transform:uppercase;color:var(--white-dim);cursor:pointer;transition:color .2s;padding:4px 0;border-bottom:1px solid transparent;}
.nav-links a:hover,.nav-links a.on{color:var(--gold);border-bottom-color:var(--gold);}
.page{display:none;height:100vh;padding-top:52px;position:relative;z-index:1;overflow-y:auto;}
.page.active{display:flex;flex-direction:column;}
/* ── HOME ── */
#ph{align-items:center;justify-content:center;text-align:center;}
.hw{max-width:560px;padding:0 20px;animation:fu .9s ease both;}
@keyframes fu{from{opacity:0;transform:translateY(24px);}to{opacity:1;transform:translateY(0);}}
.ey{display:flex;align-items:center;gap:10px;justify-content:center;font-family:var(--fm);font-size:.58rem;letter-spacing:.16em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:28px;}
.el{display:block;width:28px;height:1px;background:var(--border);}
h1.ht{font-family:var(--fd);font-size:clamp(2.4rem,7vw,4.8rem);font-weight:300;line-height:1.05;margin-bottom:20px;}
.ht .tg{color:var(--gold);display:block;}
.ht .ti{font-style:italic;display:block;}
.ht span{display:block;}
.hs{font-size:.85rem;color:var(--white-dim);line-height:1.9;margin-bottom:36px;font-weight:300;}
.bp{display:inline-flex;align-items:center;gap:10px;padding:12px 28px;background:transparent;border:1px solid var(--gold);color:var(--gold);font-family:var(--fm);font-size:.65rem;letter-spacing:.14em;text-transform:uppercase;cursor:pointer;transition:all .3s;position:relative;overflow:hidden;}
.bp::before{content:'';position:absolute;inset:0;background:var(--gold);transform:scaleX(0);transform-origin:left;transition:transform .3s;z-index:-1;}
.bp:hover{color:var(--bg);}
.bp:hover::before{transform:scaleX(1);}
.stats{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:44px;padding-top:28px;border-top:1px solid var(--border-faint);}
.sn{display:block;font-family:var(--fd);font-size:2.2rem;font-weight:300;color:var(--gold);line-height:1;}
.sl{display:block;font-family:var(--fm);font-size:.56rem;letter-spacing:.08em;color:var(--white-dim);margin-top:4px;}
.sdiv{font-family:var(--fd);font-size:1.1rem;color:var(--border);}
/* ── CHAT ── */
#pc2{padding-top:52px;}
.cw{width:100%;max-width:640px;height:calc(100vh - 52px);display:flex;flex-direction:column;padding:0 16px;margin:0 auto;}
.ch{display:flex;align-items:center;justify-content:space-between;padding:14px 0 12px;border-bottom:1px solid var(--border-faint);flex-shrink:0;}
.av{width:34px;height:34px;background:var(--bg3);border:1px solid var(--border);border-radius:50%;display:flex;align-items:center;justify-content:center;position:relative;font-family:var(--fd);font-size:.95rem;color:var(--gold);}
.ap{position:absolute;inset:-3px;border-radius:50%;border:1px solid var(--gold);animation:ap 3s ease-in-out infinite;opacity:.3;}
@keyframes ap{0%,100%{transform:scale(1);opacity:.3;}50%{transform:scale(1.1);opacity:.6;}}
.ai-i{display:flex;align-items:center;gap:10px;}
.ain{font-family:var(--fd);font-size:.9rem;color:var(--white);}
.ais{font-family:var(--fm);font-size:.58rem;color:var(--white-dim);display:flex;align-items:center;gap:4px;}
.sd{width:5px;height:5px;border-radius:50%;background:#5cd88a;animation:sb 2s ease-in-out infinite;}
@keyframes sb{0%,100%{opacity:1;}50%{opacity:.3;}}
.ti2{font-family:var(--fm);font-size:.6rem;color:var(--white-dim);}
#tc{color:var(--gold);}
.msgs{flex:1;overflow-y:auto;padding:16px 0;display:flex;flex-direction:column;gap:14px;scrollbar-width:thin;scrollbar-color:var(--border-faint) transparent;}
.msgs::-webkit-scrollbar{width:3px;}
.msgs::-webkit-scrollbar-thumb{background:var(--border);}
.ddate{text-align:center;font-family:var(--fm);font-size:.56rem;letter-spacing:.1em;color:var(--white-dim);display:flex;align-items:center;gap:8px;}
.ddate::before,.ddate::after{content:'';flex:1;height:1px;background:var(--border-faint);}
.msg{display:flex;flex-direction:column;gap:3px;max-width:84%;animation:mf .3s ease both;}
@keyframes mf{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:translateY(0);}}
.am{align-self:flex-start;}
.um{align-self:flex-end;}
.bb{padding:11px 14px;font-size:.82rem;line-height:1.75;}
.am .bb{background:var(--bg3);border:1px solid var(--border-faint);border-left:2px solid var(--gold-dim);color:var(--white);}
.um .bb{background:rgba(196,169,106,.1);border:1px solid var(--border);color:var(--white);}
.mt{font-family:var(--fm);font-size:.54rem;color:var(--white-dim);padding:0 2px;}
.am .mt{align-self:flex-start;}
.um .mt{align-self:flex-end;}
.typ .bb{display:flex;gap:4px;align-items:center;padding:13px 16px;}
.dot{width:5px;height:5px;background:var(--gold-dim);border-radius:50%;animation:td 1.4s ease-in-out infinite;}
.dot:nth-child(2){animation-delay:.2s;}
.dot:nth-child(3){animation-delay:.4s;}
@keyframes td{0%,60%,100%{transform:translateY(0);opacity:.4;}30%{transform:translateY(-5px);opacity:1;}}
.ia{padding:12px 0 16px;border-top:1px solid var(--border-faint);flex-shrink:0;}
.akbar{background:var(--bg3);border:1px solid var(--border);padding:10px 14px;margin-bottom:10px;display:flex;gap:8px;align-items:center;}
.akbar input{flex:1;background:transparent;border:none;color:var(--white);font-family:var(--fm);font-size:.68rem;outline:none;}
.akbar input::placeholder{color:var(--white-dim);}
.akbar button{background:var(--gold);color:var(--bg);border:none;font-family:var(--fm);font-size:.6rem;letter-spacing:.08em;padding:6px 14px;cursor:pointer;}
.akbar button:hover{background:var(--gold-bright);}
.iw{display:flex;gap:8px;align-items:flex-end;}
#ci{flex:1;background:var(--bg3);border:1px solid var(--border);color:var(--white);font-family:var(--fb);font-size:.82rem;padding:10px 13px;resize:none;outline:none;transition:border-color .2s;max-height:100px;line-height:1.55;}
#ci:focus{border-color:var(--gold-dim);}
#ci::placeholder{color:var(--white-dim);}
.sb{width:38px;height:38px;background:var(--gold);color:var(--bg);display:flex;align-items:center;justify-content:center;flex-shrink:0;cursor:pointer;border:none;transition:background .2s;}
.sb:hover{background:var(--gold-bright);}
.disc{font-family:var(--fm);font-size:.55rem;color:var(--white-dim);text-align:center;margin-top:7px;}
/* ── LOADING ── */
#pload{align-items:center;justify-content:center;text-align:center;}
.lv{width:140px;height:140px;position:relative;animation:vs 3s linear infinite;margin:0 auto 40px;}
@keyframes vs{from{transform:rotate(0);}to{transform:rotate(360deg);}}
.vo{position:absolute;inset:0;border-radius:50%;background:conic-gradient(#1a1a1a 0,#0d0d0d 30deg,#1a1a1a 60deg,#0d0d0d 90deg,#1a1a1a 120deg,#0d0d0d 150deg,#1a1a1a 180deg,#0d0d0d 210deg,#1a1a1a 240deg,#0d0d0d 270deg,#1a1a1a 300deg,#0d0d0d 330deg,#1a1a1a 360deg);border:1px solid var(--border);}
.vi{position:absolute;inset:24px;border-radius:50%;background:var(--bg2);border:1px solid var(--border-faint);}
.vc{position:absolute;width:18px;height:18px;background:var(--gold);border-radius:50%;top:50%;left:50%;transform:translate(-50%,-50%);}
.lm{font-family:var(--fd);font-size:1.2rem;font-weight:300;color:var(--white);margin-bottom:10px;}
.ld{display:flex;gap:6px;justify-content:center;margin-bottom:14px;}
.ld span{width:4px;height:4px;background:var(--gold);border-radius:50%;animation:td 1.4s ease-in-out infinite;}
.ld span:nth-child(2){animation-delay:.2s;}
.ld span:nth-child(3){animation-delay:.4s;}
.ls{font-size:.78rem;color:var(--white-dim);font-weight:300;}
/* ── RESULT ── */
#pres{padding:52px 16px 32px;}
.rw{max-width:660px;margin:0 auto;animation:fu .7s ease both;}
.rh{text-align:center;padding-bottom:32px;border-bottom:1px solid var(--border-faint);margin-bottom:32px;}
.rey{font-family:var(--fm);font-size:.58rem;letter-spacing:.16em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:10px;}
.rtit{font-family:var(--fd);font-size:clamp(1.6rem,4vw,2.4rem);font-weight:300;color:var(--gold);margin-bottom:12px;}
.rmsg{font-size:.82rem;color:var(--white-dim);line-height:1.9;max-width:480px;margin:0 auto 16px;font-weight:300;font-style:italic;}
.kw{display:flex;gap:5px;flex-wrap:wrap;justify-content:center;}
.kt{padding:3px 10px;border:1px solid var(--border);font-family:var(--fm);font-size:.58rem;letter-spacing:.08em;color:var(--gold-dim);}
.trk{display:flex;flex-direction:column;gap:12px;}
.tc{display:grid;grid-template-columns:38px 1fr auto;gap:14px;align-items:center;padding:16px 18px;background:var(--bg2);border:1px solid var(--border-faint);border-left:2px solid var(--border);transition:border-left-color .2s;animation:fu .5s ease both;}
.tc:hover{border-left-color:var(--gold);}
.tnum{font-family:var(--fd);font-size:1.6rem;font-weight:300;color:var(--border);text-align:center;line-height:1;}
.ttl{font-family:var(--fd);font-size:1rem;font-weight:400;color:var(--white);margin-bottom:2px;}
.tar{font-family:var(--fm);font-size:.6rem;letter-spacing:.07em;color:var(--gold-dim);margin-bottom:5px;}
.tre{font-size:.74rem;color:var(--white-dim);line-height:1.6;font-weight:300;}
.tl{display:flex;flex-direction:column;gap:5px;flex-shrink:0;}
.tli{display:flex;align-items:center;gap:4px;padding:4px 9px;border:1px solid var(--border-faint);font-family:var(--fm);font-size:.56rem;letter-spacing:.07em;color:var(--white-dim);text-decoration:none;transition:all .2s;text-transform:uppercase;}
.tli.yt:hover{border-color:#ff4444;color:#ff4444;}
.tli.sp:hover{border-color:#1db954;color:#1db954;}
.ra{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:24px;padding-bottom:32px;}
.bs{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;background:var(--white-faint);border:1px solid var(--border);color:var(--white-dim);font-family:var(--fm);font-size:.6rem;letter-spacing:.08em;cursor:pointer;transition:all .2s;}
.bs:hover{background:rgba(196,169,106,.1);color:var(--white);}
.bo{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;background:transparent;border:1px solid var(--border-faint);color:var(--white-dim);font-family:var(--fm);font-size:.6rem;letter-spacing:.08em;cursor:pointer;transition:all .2s;}
.bo:hover{border-color:var(--border);color:var(--white);}
/* ── HISTORY ── */
#phi{padding:52px 16px 32px;}
.hiw{max-width:660px;margin:0 auto;}
.hlist{display:flex;flex-direction:column;gap:12px;}
.hcard{padding:18px 20px;background:var(--bg2);border:1px solid var(--border-faint);cursor:pointer;transition:border-color .2s;}
.hcard:hover{border-color:var(--border);}
.hch{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:9px;}
.hem{font-family:var(--fd);font-size:1rem;font-weight:400;color:var(--gold);}
.hdt{font-family:var(--fm);font-size:.58rem;color:var(--white-dim);}
.hps{display:flex;flex-wrap:wrap;gap:5px;}
.hp{padding:3px 9px;background:var(--bg3);border:1px solid var(--border-faint);font-size:.68rem;color:var(--white-dim);}
.hempty{text-align:center;padding:60px 16px;display:flex;flex-direction:column;align-items:center;gap:14px;}
.hei{font-size:2rem;opacity:.25;}
.hempty p{color:var(--white-dim);font-size:.84rem;}
/* ── ALGO ── */
#pal{padding:52px 16px 32px;}
.iw2{max-width:660px;margin:0 auto;}
.steps{display:flex;flex-direction:column;}
.step{display:grid;grid-template-columns:56px 1fr;gap:16px;padding:24px 0;}
.sc2{width:1px;height:20px;background:linear-gradient(to bottom,var(--border),transparent);margin-left:28px;}
.sn2{font-family:var(--fd);font-size:2rem;font-weight:300;color:var(--border);line-height:1;}
.sc h3{font-family:var(--fd);font-size:1.05rem;font-weight:400;color:var(--gold);margin-bottom:7px;}
.sc p{font-size:.8rem;color:var(--white-dim);line-height:1.8;font-weight:300;}
.arw{text-align:center;padding:36px 0;border-top:1px solid var(--border-faint);}
.arc{position:relative;width:100px;height:100px;margin:0 auto 10px;display:flex;align-items:center;justify-content:center;}
.ar{position:absolute;border-radius:50%;border:1px solid var(--border);}
.ar1{inset:0;animation:rp 3s ease-in-out infinite;}
.ar2{inset:14px;animation:rp 3s .5s ease-in-out infinite;border-color:var(--gold-dim);}
.ar3{inset:28px;animation:rp 3s 1s ease-in-out infinite;border-color:var(--gold);}
@keyframes rp{0%,100%{opacity:.3;}50%{opacity:1;}}
.adot{font-size:1.1rem;color:var(--gold-bright);}
.albl{font-family:var(--fm);font-size:.58rem;letter-spacing:.16em;text-transform:uppercase;color:var(--gold-dim);}
/* ── BRAND ── */
#pbr{padding:52px 16px 32px;}
.bw{max-width:660px;margin:0 auto;}
.bsp{display:grid;grid-template-columns:1fr 1px 1fr;gap:24px;margin-bottom:40px;align-items:start;}
.bdv{background:var(--border-faint);align-self:stretch;}
.btr{font-family:var(--fd);font-size:2.2rem;font-weight:300;color:var(--white);margin-bottom:12px;letter-spacing:.06em;}
.bdf p{font-size:.8rem;color:var(--white-dim);line-height:1.85;font-weight:300;margin-bottom:7px;}
.bdf strong{color:var(--white);}
.bman{padding:32px 28px;border:1px solid var(--border);text-align:center;margin-bottom:32px;background:var(--bg2);position:relative;}
.bman::before{content:'"';position:absolute;top:-14px;left:50%;transform:translateX(-50%);background:var(--bg);padding:0 10px;font-family:var(--fd);font-size:1.6rem;color:var(--gold-dim);}
.mt2{font-family:var(--fd);font-size:1rem;font-weight:300;line-height:2;color:var(--white);margin-bottom:10px;}
.mt2 em{color:var(--gold);font-style:italic;}
.ms{font-family:var(--fm);font-size:.58rem;letter-spacing:.08em;color:var(--white-dim);}
.btl{display:flex;flex-direction:column;}
.bti{display:flex;gap:24px;align-items:baseline;padding:14px 0;border-bottom:1px solid var(--border-faint);}
.bti:last-child{border-bottom:none;}
.bty{font-family:var(--fm);font-size:.58rem;letter-spacing:.11em;text-transform:uppercase;color:var(--gold-dim);min-width:44px;}
.btd{font-size:.8rem;color:var(--white-dim);font-weight:300;}
/* ── PAGE HEADER ── */
.ph{text-align:center;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border-faint);}
.phe{font-family:var(--fm);font-size:.58rem;letter-spacing:.16em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:10px;}
.ph h2{font-family:var(--fd);font-size:clamp(1.8rem,4vw,2.8rem);font-weight:300;color:var(--white);margin-bottom:7px;}
.ph p{font-size:.84rem;color:var(--white-dim);font-weight:300;}
/* TOAST */
#toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(50px);background:var(--bg3);border:1px solid var(--border);color:var(--white);font-family:var(--fm);font-size:.62rem;letter-spacing:.06em;padding:10px 18px;z-index:2000;opacity:0;transition:all .3s;pointer-events:none;white-space:nowrap;}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
</style>
</head>
<body>
<canvas id="pc"></canvas>
<div class="glow"></div>
<nav>
<div class="logo" onclick="go('h')">EVRS<span>0024</span></div>
<div class="nav-links">
<a onclick="go('h')" id="nl-h" class="on">Home</a>
<a onclick="go('c')" id="nl-c">상담</a>
<a onclick="go('i')" id="nl-i">저장함</a>
<a onclick="go('a')" id="nl-a">알고리즘</a>
<a onclick="go('b')" id="nl-b">브랜드</a>
</div>
</nav>
<!-- HOME -->
<div id="ph2" class="page active">
<div class="hw">
<div class="ey"><span class="el"></span>AI 감성 음악 상담 서비스<span class="el"></span></div>
<h1 class="ht">
<span>Every</span><span class="tg">Sound,</span><span class="ti">영원히</span><span>사랑해</span>
</h1>
<p class="hs">당신의 감정을 말해주세요.<br>세상 어딘가의 완벽한 5곡이 당신을 기다립니다.</p>
<button class="bp" onclick="go('c')">AI 심리상담 시작 →</button>
<div class="stats">
<div><span class="sn">∞</span><span class="sl">개의 음악 중</span></div>
<span class="sdiv">×</span>
<div><span class="sn">5</span><span class="sl">곡만을 위해</span></div>
<span class="sdiv">×</span>
<div><span class="sn">1</span><span class="sl">개의 당신</span></div>
</div>
</div>
</div>
<!-- CHAT -->
<div id="pc2" class="page">
<div class="cw">
<div class="akbar" id="akb">
<input type="password" id="aki" placeholder="Anthropic API 키 (sk-ant-...)"/>
<button onclick="saveKey()">저장</button>
</div>
<div class="ch">
<div class="ai-i">
<div class="av"><div class="ap"></div>E</div>
<div>
<div class="ain">Evrs AI</div>
<div class="ais"><span class="sd"></span>당신의 이야기를 듣고 있어요</div>
</div>
</div>
<div class="ti2"><span id="tc">0</span>/8</div>
</div>
<div class="msgs" id="msgs">
<div class="ddate"><span>오늘</span></div>
<div class="msg am">
<div class="bb">안녕하세요. 저는 Evrs AI예요 🎵<br><br>지금 어떤 감정이신가요? 편하게 이야기해 주세요. 장르나 분위기는 고를 필요 없어요.</div>
<div class="mt" id="it"></div>
</div>
</div>
<div class="ia">
<div class="iw">
<textarea id="ci" placeholder="지금 어떤 감정인지 자유롭게 적어보세요..." rows="1"></textarea>
<button class="sb" onclick="send()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
</div>
<div class="disc">최대 8번의 대화 후 자동으로 음악이 추천됩니다</div>
</div>
</div>
</div>
<!-- LOADING -->
<div id="pload" class="page">
<div style="text-align:center;">
<div class="lv"><div class="vo"></div><div class="vi"></div><div class="vc"></div></div>
<p class="lm" id="ltxt">감정의 교차점을 분석하는 중</p>
<div class="ld"><span></span><span></span><span></span></div>
<p class="ls">전 세계의 음악 중에서 당신만을 위한 5곡을 찾고 있어요</p>
</div>
</div>
<!-- RESULT -->
<div id="pres" class="page">
<div class="rw">
<div class="rh">
<div class="rey">당신을 위한 플레이리스트</div>
<h2 class="rtit" id="rtit">오늘의 감정</h2>
<p class="rmsg" id="rmsg"></p>
<div class="kw" id="rkw"></div>
</div>
<div class="trk" id="rtrk"></div>
</div>
<div class="ra">
<button class="bs" onclick="saveH()">저장함에 보관</button>
<button class="bo" onclick="reset();go('c')">다시 상담받기</button>
</div>
</div>
<!-- HISTORY -->
<div id="phi" class="page">
<div class="hiw">
<div class="ph"><div class="phe">Archive</div><h2>저장함</h2><p>지난 감정들과 그날의 음악들</p></div>
<div class="hlist" id="hlist"></div>
<div class="hempty" id="he" style="display:none">
<div class="hei">🎵</div><p>아직 저장된 상담이 없어요</p>
<button class="bp" onclick="go('c')">첫 상담 시작하기 →</button>
</div>
</div>
</div>
<!-- ALGO -->
<div id="pal" class="page">
<div class="iw2">
<div class="ph"><div class="phe">How it works</div><h2>교차점 분석 알고리즘</h2><p>EVRS0024가 음악을 찾는 방식</p></div>
<div class="steps">
<div class="step"><div class="sn2">01</div><div class="sc"><h3>감정 벡터 추출</h3><p>단순한 태그 분류가 아닙니다. AI는 시간적 맥락, 관계적 맥락, 감정의 방향성을 동시에 읽어냅니다.</p></div></div>
<div class="sc2"></div>
<div class="step"><div class="sn2">02</div><div class="sc"><h3>청취 패턴 교차 분석</h3><p>전 세계 청취자들이 특정 감정 상태에서 반복 재생한 음악들의 패턴을 분석합니다. 국가와 시대를 초월해 반복 선택된 곡들이 교차점을 형성합니다.</p></div></div>
<div class="sc2"></div>
<div class="step"><div class="sn2">03</div><div class="sc"><h3>마이너리티 룰 적용</h3><p>메가히트 곡을 의도적으로 억제합니다. 당신이 아직 모르지만 지금 이 순간에 완벽한 인디/마이너 트랙을 발굴합니다.</p></div></div>
<div class="sc2"></div>
<div class="step"><div class="sn2">04</div><div class="sc"><h3>5곡 최종 선별</h3><p>교차점 밀도가 가장 높은 5곡만 선별합니다. 5곡은 하나의 완전한 감정 여정을 만드는 황금 숫자입니다.</p></div></div>
</div>
<div class="arw">
<div class="arc"><div class="ar ar1"></div><div class="ar ar2"></div><div class="ar ar3"></div><span class="adot">✦</span></div>
<div class="albl">교차점</div>
</div>
</div>
</div>
<!-- BRAND -->
<div id="pbr" class="page">
<div class="bw">
<div class="ph"><div class="phe">Brand Story</div><h2>EVRS<span style="color:var(--gold)">0024</span></h2><p>이 이름이 태어난 이유</p></div>
<div class="bsp">
<div><div class="btr">EVRS</div><div class="bdf"><p><strong>Every Sound</strong>의 두 번째 자음들.</p><p>세상의 모든 소리, 모든 음악, 모든 감정의 진동을 담겠다는 의지입니다.</p></div></div>
<div class="bdv"></div>
<div><div class="btr" style="color:var(--gold)">0024</div><div class="bdf"><p><strong>0시부터 24시</strong>, 하루의 모든 순간.</p><p>아침의 설렘부터 새벽 4시의 적막함까지, 24시간 내내 당신 곁에 있겠다는 약속입니다.</p></div></div>
</div>
<div class="bman">
<p class="mt2">"음악 취향이 비슷한 사람들이 있습니다.<br>하지만 감정이 완벽히 같은 사람은 없습니다.<br>우리는 취향이 아닌, <em>감정</em>으로 음악을 찾습니다."</p>
<span class="ms">— EVRS0024 창립 선언문</span>
</div>
<div class="btl">
<div class="bti"><span class="bty">개념</span><span class="btd">장르 태그가 아닌 '감정 맥락'으로 음악을 분류한다</span></div>
<div class="bti"><span class="bty">철학</span><span class="btd">인디 뮤지션의 숨겨진 명곡을 세상과 연결 한다</span></div>
<div class="bti"><span class="bty">비전</span><span class="btd">Every Sound, 영원히 사랑해</span></div>
</div>
</div>
</div>
<div id="toast"></div>
<script>
const S={key:localStorage.getItem('evrs_k')||'',hist:[],turns:0,res:null};
function now(){const d=new Date();return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0');}
document.getElementById('it').textContent=now();
const pageMap={h:'ph2',c:'pc2',load:'pload',res:'pres',i:'phi',a:'pal',b:'pbr'};
const navMap={h:'nl-h',c:'nl-c',i:'nl-i',a:'nl-a',b:'nl-b'};
function go(id){
Object.values(pageMap).forEach(p=>{const el=document.getElementById(p);if(el)el.classList.remove('active');});
const t=document.getElementById(pageMap[id]);
if(t){t.classList.add('active');t.scrollTop=0;}
Object.values(navMap).forEach(n=>{const el=document.getElementById(n);if(el)el.classList.remove('on');});
const nl=document.getElementById(navMap[id]);
if(nl)nl.classList.add('on');
if(id==='i')renderH();
if(id==='c')document.getElementById('akb').style.display=S.key?'none':'flex';
}
function saveKey(){
const v=document.getElementById('aki').value.trim();
if(!v.startsWith('sk-ant-')){toast('올바른 API 키를 입력해 주세요');return;}
S.key=v;localStorage.setItem('evrs_k',v);
document.getElementById('akb').style.display='none';
toast('API 키 저장 완료 ✦');
}
function addMsg(r,t){
const el=document.createElement('div');
el.className='msg '+(r==='ai'?'am':'um');
el.innerHTML=`<div class="bb">${t.replace(/\n/g,'<br>')}</div><div class="mt">${now()}</div>`;
const m=document.getElementById('msgs');m.appendChild(el);m.scrollTop=m.scrollHeight;
return el;
}
function addTyp(){
const el=document.createElement('div');
el.className='msg am typ';
el.innerHTML='<div class="bb"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>';
const m=document.getElementById('msgs');m.appendChild(el);m.scrollTop=m.scrollHeight;
return el;
}
const SYS=`당신은 "Evrs AI"입니다. 따뜻하고 공감 능력이 뛰어난 음악 심리 상담사입니다.
사용자의 감정, 상황, 맥락을 자연스러운 대화로 파악합니다. 한국어로 대화합니다.
다음이 충분히 파악되면 답변 맨 끝에 [READY]를 붙입니다: 현재 감정, 시간적/관계적 맥락, 음악적 분위기. 최대 8턴 안에 붙여야 합니다.
음악 추천 요청 시 아래 JSON으로만 응답합니다:
{"emotionTitle":"감정 제목","aiMessage":"따뜻한 마지막 멘트 2-3문장","keywords":["k1","k2","k3"],"tracks":[{"title":"곡명","artist":"아티스트","reason":"추천 이유"}]}
규칙: 5곡 필수, 인디/마이너 최소 3곡, 뻔한 히트곡 최소화, 국가/시대 다양하게.`;
async function callAI(msgs){
const r=await fetch('https://api.anthropic.com/v1/messages',{
method:'POST',
headers:{'Content-Type':'application/json','x-api-key':S.key,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'},
body:JSON.stringify({model:'claude-sonnet-4-20250514',max_tokens:1200,system:SYS,messages:msgs})
});
if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.error?.message||'HTTP '+r.status);}
const d=await r.json();return d.content?.[0]?.text||'';
}
async function send(){
const ci=document.getElementById('ci');
const text=ci.value.trim();
if(!text||!S.key){if(!S.key){toast('먼저 API 키를 입력해 주세요');document.getElementById('akb').style.display='flex';}return;}
addMsg('user',text);ci.value='';ci.style.height='auto';
S.hist.push({role:'user',content:text});S.turns++;
document.getElementById('tc').textContent=S.turns;
const typ=addTyp();
try{
const resp=await callAI(S.hist);typ.remove();
const rdy=resp.includes('[READY]');
addMsg('ai',resp.replace('[READY]','').trim());
S.hist.push({role:'assistant',content:resp});
if(rdy||S.turns>=8)setTimeout(startRec,1200);
}catch(e){typ.remove();addMsg('ai','오류가 발생했어요. ('+e.message+')');}
}
const ltx=['감정의 교차점을 분석하는 중','전 세계 청취 패턴과 대조하는 중','마이너 트랙을 발굴하는 중','당신만의 5곡을 선별하는 중'];
async function startRec(){
go('load');let ti=0;
const iv=setInterval(()=>{ti=(ti+1)%ltx.length;const el=document.getElementById('ltxt');if(el)el.textContent=ltx[ti];},1800);
try{
const raw=await callAI([...S.hist,{role:'user',content:'위 대화를 바탕으로 음악을 추천해 주세요. JSON 형식으로만 응답해 주세요.'}]);
clearInterval(iv);
const m=raw.match(/\{[\s\S]*\}/);if(!m)throw new Error('JSON 파싱 실패');
S.res=JSON.parse(m[0]);renderRes(S.res);go('res');
}catch(e){clearInterval(iv);toast('추천 오류: '+e.message);go('c');}
}
function renderRes(d){
document.getElementById('rtit').textContent=d.emotionTitle||'오늘의 감정';
document.getElementById('rmsg').textContent=d.aiMessage||'';
const kw=document.getElementById('rkw');kw.innerHTML='';
(d.keywords||[]).forEach(k=>{const s=document.createElement('span');s.className='kt';s.textContent='#'+k;kw.appendChild(s);});
const tr=document.getElementById('rtrk');tr.innerHTML='';
(d.tracks||[]).forEach((t,i)=>{
const q=encodeURIComponent(t.title+' '+t.artist);
const c=document.createElement('div');c.className='tc';c.style.animationDelay=(i*.1)+'s';
c.innerHTML=`<div class="tnum">${String(i+1).padStart(2,'0')}</div>
<div><div class="ttl">${t.title}</div><div class="tar">${t.artist}</div><div class="tre">${t.reason}</div></div>
<div class="tl">
<a href="https://www.youtube.com/results?search_query=${q}" target="_blank" class="tli yt">▶ YT</a>
<a href="https://open.spotify.com/search/${q}" target="_blank" class="tli sp">● SP</a>
</div>`;
tr.appendChild(c);
});
}
function saveH(){
if(!S.res)return;
const h=JSON.parse(localStorage.getItem('evrs_h')||'[]');
h.unshift({id:Date.now(),date:new Date().toLocaleDateString('ko-KR'),emotion:S.res.emotionTitle,keywords:S.res.keywords||[],tracks:(S.res.tracks||[]).map(t=>({title:t.title,artist:t.artist}))});
if(h.length>20)h.pop();
localStorage.setItem('evrs_h',JSON.stringify(h));
toast('저장함에 보관되었습니다 ✦');
}
function renderH(){
const h=JSON.parse(localStorage.getItem('evrs_h')||'[]');
const hl=document.getElementById('hlist'),he=document.getElementById('he');
hl.innerHTML='';
if(!h.length){he.style.display='flex';hl.style.display='none';return;}
he.style.display='none';hl.style.display='flex';
h.forEach(e=>{
const c=document.createElement('div');c.className='hcard';
c.innerHTML=`<div class="hch"><div class="hem">${e.emotion||'감정 기록'}</div><div class="hdt">${e.date}</div></div>
<div class="hps">${(e.tracks||[]).map(t=>`<span class="hp">${t.title} — ${t.artist}</span>`).join('')}</div>`;
hl.appendChild(c);
});
}
function reset(){
S.hist=[];S.turns=0;S.res=null;
document.getElementById('tc').textContent='0';
document.getElementById('msgs').innerHTML=`<div class="ddate"><span>오늘</span></div>
<div class="msg am"><div class="bb">안녕하세요. 저는 Evrs AI예요 🎵<br><br>지금 어떤 감정이신가요? 편하게 이야기해 주세요.</div><div class="mt">${now()}</div></div>`;
}
document.getElementById('ci').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey&&!e.isComposing){e.preventDefault();send();}});
document.getElementById('ci').addEventListener('input',function(){this.style.height='auto';this.style.height=Math.min(this.scrollHeight,100)+'px';});
if(S.key)document.getElementById('akb').style.display='none';
(function(){
const cv=document.getElementById('pc'),ctx=cv.getContext('2d'),ps=[];
function sz(){cv.width=innerWidth;cv.height=innerHeight;}
window.addEventListener('resize',sz);sz();
class P{constructor(){this.r(true);}r(i=false){this.x=Math.random()*cv.width;this.y=i?Math.random()*cv.height:cv.height+8;this.rs=Math.random()*1.5+.3;this.sy=Math.random()*.28+.07;this.sx=(Math.random()-.5)*.15;this.op=0;this.mo=Math.random()*.38+.08;this.l=0;this.ml=Math.random()*350+160;}update(){this.y-=this.sy;this.x+=this.sx;this.l++;if(this.l<50)this.op=(this.l/50)*this.mo;else if(this.l>this.ml-50)this.op=((this.ml-this.l)/50)*this.mo;else this.op=this.mo;if(this.l>=this.ml||this.y<-6)this.r();}draw(){ctx.save();ctx.globalAlpha=this.op;ctx.fillStyle='#C4A96A';ctx.beginPath();ctx.arc(this.x,this.y,this.rs,0,Math.PI*2);ctx.fill();ctx.restore();}}
for(let i=0;i<45;i++)ps.push(new P());
function loop(){ctx.clearRect(0,0,cv.width,cv.height);ps.forEach(p=>{p.update();p.draw();});requestAnimationFrame(loop);}
loop();
})();
let tt=null;
function toast(msg){const el=document.getElementById('toast');el.textContent=msg;el.classList.add('show');if(tt)clearTimeout(tt);tt=setTimeout(()=>el.classList.remove('show'),3000);}
</script>
</body>
</html>
결과와 배운 점
기획안을 여러번 적고 수정 또 수정하면서 세세하고 디테일이 있을수록 결과는 좋다는것을 배움과동시 꿀팁이라고 생각합니다
과정 중에 오류도 있었고 빠트린 부분이 많았어서 시도후 기획안을 계속 보충및 수정을 했습니다
배포 및 프론트 부분이 아닌 백엔드 DB 등 개발 관련해서 도움이 필요합니다
이번 강의에서 조금이나마 도움을 받아 올해안에 제가 원하는 웹+어플을 배포하여 경력을 만들고 싶습니다