<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>QR 끝말잇기 대결</title>
<script src="https://unpkg.com/peerjs@1.5.5/dist/peerjs.min.js"><\/script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"><\/script>
<style>
*{box-sizing:border-box}
body{margin:0;padding:18px;font-family:system-ui,sans-serif;background:#eef3ff;color:#17213a}
main{max-width:480px;margin:auto}
.card{background:white;padding:18px;border-radius:18px;margin-bottom:14px;box-shadow:0 5px 18px #17213a12}
h1{text-align:center;color:#315eea}
button,input{width:100%;padding:13px;margin-top:8px;border-radius:11px;border:1px solid #d5ddef;font:inherit}
button{background:#315eea;color:white;border:0;font-weight:bold}
#status{background:#e9efff;padding:12px;border-radius:10px;text-align:center;overflow-wrap:anywhere}
#qr{display:flex;justify-content:center;margin:15px}
#words{line-height:1.9;overflow-wrap:anywhere}
.small{font-size:13px;color:#68748a;overflow-wrap:anywhere}
</style>
</head>
<body>
<main>
<section class="card">
<h1>🔗 끝말잇기 대결</h1>
<p id="status">방을 만들거나 친구의 방 코드로 참가하세요.</p>
</section>
<section class="card" id="lobby">
<button id="create">🎮 방 만들기</button>
<div id="invite" hidden>
<p>친구가 QR을 찍거나 방 코드를 입력하세요.</p>
<div id="qr"></div>
<input id="code" readonly placeholder="방 코드">
<button id="copy">초대 링크 복사</button>
</div>
<input id="room" placeholder="친구의 방 코드">
<button id="join">친구 방 참가</button>
<p class="small">공개 배포된 페이지에서 사용해야 친구도 접속할 수 있어요.</p>
</section>
<section class="card">
<h3 id="turn">게임 대기 중</h3>
<p id="last">첫 단어를 기다리고 있어요.</p>
<input id="word" placeholder="한국어 단어 입력" maxlength="20">
<button id="send">단어 제출</button>
<button id="restart">다시 시작</button>
<h3>📜 단어 기록</h3>
<div id="words"></div>
</section>
</main>
<script>
const $=id=>document.getElementById(id);
let peer=null,conn=null,isHost=false,myTurn=-1,turn=0,last="",words=[],over=true;
function say(s){$("status").textContent=s}
function state(){return {turn,last,words,over}}
function showState(s){
turn=s.turn;last=s.last;words=s.words||[];over=s.over;
render();
}
function render(){
$("last").textContent=last?"마지막 단어: "+last:"첫 단어를 입력하세요.";
$("turn").textContent=over?"게임 대기/종료":(turn===0?"🔵 플레이어 1 차례":"🔴 플레이어 2 차례");
$("words").replaceChildren();
words.forEach((w,i)=>{
const div=document.createElement("div");
div.textContent=(i+1)+". "+w;
$("words").appendChild(div);
});
}
function sendState(){
if(conn&&conn.open)conn.send({type:"state",state:state()});
}
function startGame(){
turn=0;last="";words=[];over=false;render();sendState();
}
function connectEvents(){
conn.on("data",m=>{
if(m.type==="hello"&&isHost){
startGame();conn.send({type:"state",state:state()});say("친구와 연결됐어요!");
}else if(m.type==="state"&&!isHost){
showState(m.state);say("게임 상태를 받았어요.");
}else if(m.type==="move"&&isHost){
const w=m.word;
if(!over&&turn===1&&valid(w)){
words.push(w);last=w;turn=0;render();sendState();
}else{
over=true;conn.send({type:"end",winner:0,message:"플레이어 2가 규칙을 어겼어요. 플레이어 1 승리!"});sendState();
}
}else if(m.type==="move"&&!isHost){
say("상대의 입력을 확인 중이에요.");
}else if(m.type==="end"){
over=true;render();say(m.message);
}else if(m.type==="restart"){
showState(m.state);say("새 게임을 시작했어요.");
}
});
conn.on("close",()=>say("친구와 연결이 끊어졌어요."));
}
function valid(w){
return /^[가-힣]{2,}$/.test(w)&&!words.includes(w)&&(!last||w[0]===last[last.length-1]);
}
$("create").onclick=()=>{
if(typeof Peer==="undefined"){say("연결 라이브러리를 불러오지 못했어요.");return}
if(peer)peer.destroy();
isHost=true;myTurn=0;peer=new Peer();
say("방을 만들고 있어요...");
peer.on("open",id=>{
$("invite").hidden=false;$("code").value=id;$("room").value=id;
$("qr").replaceChildren();
const link=new URL(location.href);
link.searchParams.set("room",id);
new QRCode($("qr"),{text:link.href,width:180,height:180});
say("방이 만들어졌어요. 친구가 참가하면 시작합니다.");
});
peer.on("connection",c=>{
if(conn){c.close();return}
conn=c;connectEvents();conn.on("open",()=>{});
say("친구가 연결됐어요. 게임 시작!");
});
peer.on("error",e=>say("연결 오류: "+e.type));
};
$("join").onclick=()=>{
const id=$("room").value.trim()||new URLSearchParams(location.search).get("room");
if(!id){say("친구의 방 코드를 입력하세요.");return}
if(peer)peer.destroy();
isHost=false;myTurn=1;peer=new Peer();
say("친구 방에 연결 중...");
peer.on("open",()=>{
conn=peer.connect(id,{reliable:true});
conn.on("open",()=>{connectEvents();conn.send({type:"hello"});say("방에 연결됐어요.");});
conn.on("error",()=>say("연결에 실패했어요. 방 코드와 공개 주소를 확인하세요."));
});
peer.on("error",e=>say("연결 오류: "+e.type));
};
$("send").onclick=()=>{
const w=$("word").value.trim().replace(/\s/g,"");
if(over||!conn||!conn.open){say("먼저 친구와 연결하세요.");return}
if(turn!==myTurn){say("상대방 차례예요.");return}
if(!valid(w)){
over=true;render();
const msg="규칙 위반! 플레이어 "+(myTurn+1)+" 패배!";
say(msg);conn.send({type:"end",winner:1-myTurn,message:msg});sendState();return;
}
if(isHost){
words.push(w);last=w;turn=1;render();sendState();
}else{
conn.send({type:"move",word:w});say("단어를 보냈어요. 상대의 차례입니다.");
}
$("word").value="";
};
$("word").addEventListener("keydown",e=>{if(e.key==="Enter")$("send").click()});
$("copy").onclick=async()=>{
try{await navigator.clipboard.writeText(location.href+"?room="+encodeURIComponent($("code").value));say("초대 링크를 복사했어요.");}
catch(e){say("복사가 안 되면 방 코드를 친구에게 알려주세요.");}
};
$("restart").onclick=()=>{
if(!isHost||!conn||!conn.open){say("방을 만든 플레이어가 연결된 상태에서 다시 시작할 수 있어요.");return}
startGame();conn.send({type:"restart",state:state()});
};
const roomParam=new URLSearchParams(location.search).get("room");
if(roomParam)$("room").value=roomParam;
render();
<\/script>
</body>
</html>