2025๋…„ 8์›” 29์ผ ๊ธˆ์š”์ผ

11-2

 <!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<title>๐Ÿฌ ๋‹ค์ธต ๋ฐฑํ™”์  ๊ธธ์•ˆ๋‚ด (์ธต๊ฐ„ ์ด๋™ ์ง€์›)</title>

<style>

body { margin:0; overflow:hidden; font-family:sans-serif; }

canvas { border:1px solid #ccc; display:block; cursor:crosshair; }

#controls {

  position: fixed; top:10px; left:10px;

  background: rgba(255,255,255,0.95); padding:10px; border-radius:6px; z-index:10;

}

select, button, input { margin:2px; }

#infoBox {

  position: fixed; bottom:10px; left:10px;

  background: rgba(0,0,0,0.7); color:#fff; padding:5px 10px; border-radius:6px; font-weight:bold;

  display:none;

}

#nodeDialog {

  display:none; position:fixed; top:50%; left:50%;

  transform:translate(-50%,-50%);

  background:#fff; border:1px solid #ccc; padding:15px;

  border-radius:8px; z-index:1000; box-shadow:0 4px 12px rgba(0,0,0,0.2);

}

#nodeDialog h3 { margin:0 0 8px 0; font-size:16px; }

#nodeDialog .typeBtn { margin:2px; padding:3px 6px; cursor:pointer; }

</style>

</head>

<body>


<div id="controls">

  <div>

    Floor plan ์—…๋กœ๋“œ: <input type="file" id="fileInput" accept="image/*">

  </div>

  <div>

    ์ถœ๋ฐœ์ง€: <select id="start"></select>

    ๋ชฉ์ ์ง€: <select id="end"></select>

    <button onclick="findPath()">๊ธธ์ฐพ๊ธฐ</button>

    <button onclick="resetAll()">์ดˆ๊ธฐํ™”</button>

  </div>

  <div>

    ์œ„์น˜ ๊ฒ€์ƒ‰: <input type="text" id="searchInput">

    <button onclick="highlightNode()">๊ฒ€์ƒ‰</button>

  </div>

</div>


<div id="infoBox"></div>

<canvas id="map"></canvas>


<div id="nodeDialog">

  <h3>์ƒˆ ๋…ธ๋“œ ์ถ”๊ฐ€</h3>

  <div>

    <label><input type="radio" name="floorType" value="์ง€์ƒ" checked> ์ง€์ƒ</label>

    <label><input type="radio" name="floorType" value="์ง€ํ•˜"> ์ง€ํ•˜</label>

    <input type="number" id="floorNumber" min="1" value="1" style="width:60px"> ์ธต

  </div>

  <div style="margin-top:8px;">

    <label>๋…ธ๋“œ ์ด๋ฆ„: <input type="text" id="nodeName" placeholder="์˜ˆ: 1๋ฒˆ ๊ณ„๋‹จ / 4๋ฒˆ ์—์Šค์ปฌ๋ ˆ์ดํ„ฐ"></label>

  </div>

  <div style="margin-top:8px;">

    <span>๋…ธ๋“œ ํƒ€์ž…:</span><br>

    <button type="button" class="typeBtn" data-type="normal">normal</button>

    <button type="button" class="typeBtn" data-type="elevator">elevator</button>

    <button type="button" class="typeBtn" data-type="stairs">stairs</button>

    <button type="button" class="typeBtn" data-type="escalator">escalator</button>

  </div>

  <div style="margin-top:8px;">

    <label>์ฐธ๊ณ ์‚ฌํ•ญ: <input type="text" id="nodeNote"></label>

  </div>

  <div style="margin-top:10px; text-align:right;">

    <button onclick="closeNodeDialog()">์ทจ์†Œ</button>

    <button onclick="saveNode()">์ €์žฅ</button>

  </div>

</div>


<script>

const canvas = document.getElementById('map');

const ctx = canvas.getContext('2d');

const fileInput = document.getElementById('fileInput');

const searchInput = document.getElementById('searchInput');

const infoBox = document.getElementById('infoBox');


let floorImg = null;

let nodes = {};

let edges = {};

let nodeNames = [];

let blinkNode = null;

let blinkInterval = null;

let pathNodes = [];


function resizeCanvas(){

  canvas.width = window.innerWidth;

  canvas.height = window.innerHeight;

  draw();

}

window.addEventListener('resize', resizeCanvas);


fileInput.addEventListener('change',(e)=>{

  const file = e.target.files[0];

  if(!file) return;

  const reader = new FileReader();

  reader.onload = function(evt){

    const img = new Image();

    img.onload = ()=>{ 

      floorImg = img; 

      resetAll(true); // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™”

      resizeCanvas();

    };

    img.src = evt.target.result;

  };

  reader.readAsDataURL(file);

});


// ----------------- ๋…ธ๋“œ ํด๋ฆญ / ์‚ญ์ œ / ์ด๋ฆ„ ๋ณ€๊ฒฝ -----------------

canvas.addEventListener('click',(e)=>{

  if(!floorImg) return;

  const rect = canvas.getBoundingClientRect();

  const x = e.clientX - rect.left;

  const y = e.clientY - rect.top;

  const clickedNode = findNodeAt(x,y);


  if(clickedNode){

    if(e.shiftKey){

      const newName = prompt("์ƒˆ ๋…ธ๋“œ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”:", clickedNode);

      if(!newName || nodes[newName]) { alert("์ด๋ฆ„์ด ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."); return; }

      const note = prompt("๋…ธ๋“œ ์ฐธ๊ณ ์‚ฌํ•ญ์„ ์ž…๋ ฅํ•˜์„ธ์š”:", nodes[clickedNode].note||"");

      nodes[newName] = {...nodes[clickedNode], note};

      const idx = nodeNames.indexOf(clickedNode);

      if(idx!==-1) nodeNames[idx]=newName;

      edges[newName]=edges[clickedNode]||[];

      delete edges[clickedNode];

      for(let key in edges) edges[key]=edges[key].map(n=>n===clickedNode?newName:n);

      delete nodes[clickedNode];

      updateSelectOptions();

      draw();

      return;

    } else {

      const n = nodes[clickedNode];

      infoBox.style.display='block';

      infoBox.textContent = `${clickedNode} (${n.type}, ${n.floor}) ${n.note||''}`;

      setTimeout(()=>{ infoBox.style.display='none'; }, 2000);


      if(confirm(`'${clickedNode}' ๋…ธ๋“œ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`)){

        delete nodes[clickedNode];

        nodeNames = nodeNames.filter(n=>n!==clickedNode);

        delete edges[clickedNode];

        for(let key in edges) edges[key]=edges[key].filter(n=>n!==clickedNode);

        updateSelectOptions();

        pathNodes=[];

        draw();

      }

      return;

    }

  }


  openNodeDialog(x,y);

});


function findNodeAt(x,y){

  for(let name of nodeNames){

    const node = nodes[name];

    if(Math.hypot(node.x-x,node.y-y)<=8) return name;

  }

  return null;

}


// ----------------- ์„ ํƒ ๋ฐ•์Šค ์—…๋ฐ์ดํŠธ -----------------

function updateSelectOptions(){

  const startSel=document.getElementById('start');

  const endSel=document.getElementById('end');

  startSel.innerHTML=''; endSel.innerHTML='';

  nodeNames.forEach(n=>{

    const opt1=document.createElement('option'); opt1.value=n; opt1.textContent=n; startSel.appendChild(opt1);

    const opt2=document.createElement('option'); opt2.value=n; opt2.textContent=n; endSel.appendChild(opt2);

  });

}


function getLabel(fullName){ return fullName.split(' ').slice(1).join(' ').trim(); }


// ----------------- ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ -----------------

function distance(n1,n2){ return Math.hypot(n1.x-n2.x,n1.y-n2.y); }


// ----------------- Dijkstra (๋…ธ๋“œ ๋ฒ”์œ„ ์ œํ•œ ๊ฐ€๋Šฅ) -----------------

function dijkstra(start,end,allowedNodes=null){

  const visited=new Set(), distances={}, prev={};

  const targetNodes = allowedNodes||nodeNames;

  targetNodes.forEach(n=>distances[n]=Infinity);

  distances[start]=0;


  while(visited.size<targetNodes.length){

    let minNode=null;

    for(let n of targetNodes){

      if(!visited.has(n)&&(minNode===null||distances[n]<distances[minNode])) minNode=n;

    }

    if(minNode===null) break;

    if(minNode===end) break;

    visited.add(minNode);

    for(let nb of edges[minNode]||[]){

      if(!targetNodes.includes(nb)) continue;

      let alt = distances[minNode]+distance(nodes[minNode],nodes[nb]);

      if(alt<distances[nb]) { distances[nb]=alt; prev[nb]=minNode; }

    }

  }


  const path=[]; let curr=end;

  while(curr){ path.unshift(curr); curr=prev[curr]; }

  return path;

}


// ----------------- ์ธต ํŒŒ์‹ฑ ๋ฐ RULES -----------------

function parseFloor(f){

  const s=f.trim();

  const isBasement = s.includes('์ง€ํ•˜');

  const numMatch = s.match(/(\d+)/);

  const num = numMatch?parseInt(numMatch[1],10):0;

  let zone='';

  const zMatch = s.match(/[AB]\b/i);

  if(zMatch) zone=zMatch[0].toUpperCase();

  return {level:isBasement?-num:num, zone};

}

function sameArea(f1,f2){

  const a=parseFloor(f1), b=parseFloor(f2);

  if(a.level!==b.level) return false;

  if(a.level===-2 || a.level===-3) return a.zone===b.zone && a.zone!=='';

  return true;

}

const RULES={

  '1:':[{to:'-1:', filters:[{type:'stairs',labelIncludes:'1๋ฒˆ'},{type:'stairs',labelIncludes:'4๋ฒˆ'},{type:'stairs',labelIncludes:'5๋ฒˆ'}]}],

  '-1:':[ {to:'-2:', filters:[{type:'escalator',labelIncludes:'1๋ฒˆ'}]} ],

  '-2:':[ {to:'-3:', filters:[{type:'escalator',labelIncludes:'1๋ฒˆ'}]} ]

};

function stateKeyFromFloorStr(floorStr){ const {level,zone}=parseFloor(floorStr); return `${level}:${zone||''}`; }

function prettyState(key){ const [lv,zone]=key.split(':'); const level=parseInt(lv,10); return level<0?`์ง€ํ•˜${-level}์ธต${zone?` ${zone}`:''}`:`์ง€์ƒ${level}์ธต`; }

function neighbors(state){ return (RULES[state]||[]).map(r=>r.to); }

function bfsStatePath(startState,endState){

  if(startState===endState) return [startState];

  const q=[startState], prev={}, seen=new Set([startState]);

  while(q.length){

    const cur=q.shift();

    for(const nb of neighbors(cur)){

      if(seen.has(nb)) continue;

      seen.add(nb); prev[nb]=cur;

      if(nb===endState){ let path=[nb]; let p=cur; while(p){ path.unshift(p); p=prev[p]; } return path; }

      q.push(nb);

    }

  }

  return null;

}


// ----------------- ์ปค๋„ฅํ„ฐ ์„ ํƒ -----------------

function pickConnectorOnState(currNodeName,stateKey,filters){

  const {level,zone}=parseFloor(stateKey.replace(':','์ธต'));

  const candidates=nodeNames.filter(n=>{

    const f=parseFloor(nodes[n].floor); if(f.level!==level) return false;

    if(level===-2||level===-3) if((zone||'')!==(f.zone||'')) return false;

    const label=getLabel(n);

    return filters.some(fi=>nodes[n].type===fi.type && label.includes(fi.labelIncludes));

  });

  if(candidates.length===0) return null;

  const curr=nodes[currNodeName];

  return candidates.reduce((best,now)=>distance(curr,nodes[now])<distance(curr,nodes[best])?now:best);

}

function matchConnectorOnNext(currName,nextStateKey){

  const {level,zone}=parseFloor(nextStateKey.replace(':','์ธต'));

  const t=nodes[currName].type;

  const label=getLabel(currName);

  const candidates=nodeNames.filter(n=>{

    const f=parseFloor(nodes[n].floor); if(f.level!==level) return false;

    if(level===-2||level===-3) if((zone||'')!==(f.zone||'')) return false;

    return nodes[n].type===t && getLabel(n)===label;

  });

  if(candidates.length===0) return null;

  const curr=nodes[currName];

  return candidates.reduce((best,now)=>distance(curr,nodes[now])<distance(curr,nodes[best])?now:best);

}


// ----------------- ์ตœ์ข… ๊ฒฝ๋กœ -----------------

function multiFloorPath(startName,endName){

  const startFloorStr=nodes[startName].floor;

  const endFloorStr=nodes[endName].floor;

  if(sameArea(startFloorStr,endFloorStr)){

    const allowed=nodeNames.filter(n=>sameArea(nodes[n].floor,startFloorStr));

    return dijkstra(startName,endName,allowed);

  }


  const startState=stateKeyFromFloorStr(startFloorStr);

  const endState=stateKeyFromFloorStr(endFloorStr);

  const statePath=bfsStatePath(startState,endState);

  if(!statePath){ alert(`์ธต๊ฐ„ ๊ฒฝ๋กœ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.\n${prettyState(startState)} → ${prettyState(endState)}`); return null; }


  let currNode=startName, full=[];

  for(let i=0;i<statePath.length-1;i++){

    const currState=statePath[i], nextState=statePath[i+1];

    const rule=(RULES[currState]||[]).find(r=>r.to===nextState);

    if(!rule){ alert(`๊ทœ์น™ ์˜ค๋ฅ˜: ${prettyState(currState)} → ${prettyState(nextState)}`); return null; }

    const closestCurr=pickConnectorOnState(currNode,currState,rule.filters);

    if(!closestCurr){ alert(`${prettyState(currState)}์— ์‚ฌ์šฉํ•  ์ปค๋„ฅํ„ฐ ์—†์Œ`); return null; }

    const closestNext=matchConnectorOnNext(closestCurr,nextState);

    if(!closestNext){ alert(`${prettyState(nextState)}์— ๋Œ€์‘ ์ปค๋„ฅํ„ฐ ์—†์Œ`); return null; }

    const allowedCurr=nodeNames.filter(n=>sameArea(nodes[n].floor,nodes[closestCurr].floor));

    const seg=dijkstra(currNode,closestCurr,allowedCurr);

    if(seg.length===0){ alert(`${currNode} → ${closestCurr} ๊ฒฝ๋กœ ์—†์Œ`); return null; }

    full.push(...(full.length?seg.slice(1):seg));

    if(full[full.length-1]!==closestNext) full.push(closestNext);

    currNode=closestNext;

  }

  const allowedEnd=nodeNames.filter(n=>sameArea(nodes[n].floor,endFloorStr));

  const endSeg=dijkstra(currNode,endName,allowedEnd);

  if(endSeg.length===0){ alert(`${currNode} → ${endName} ๊ฒฝ๋กœ ์—†์Œ`); return null; }

  full.push(...endSeg.slice(1));

  return full;

}


// ----------------- ๊ธธ์ฐพ๊ธฐ, ๊ฒ€์ƒ‰, ์ดˆ๊ธฐํ™” -----------------

function findPath(){ pathNodes=multiFloorPath(document.getElementById('start').value,document.getElementById('end').value)||[]; draw(); }


function highlightNode(){

  const input=searchInput.value.trim();

  if(!input){ alert("๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ"); return; }

  const found=nodeNames.find(n=>getLabel(n)===input);

  if(!found){ alert("๋…ธ๋“œ ์—†์Œ"); return; }

  if(blinkInterval) clearInterval(blinkInterval);

  blinkNode=found;

  let visible=true;

  blinkInterval=setInterval(()=>{ visible=!visible; draw(); },400);

  setTimeout(()=>{ clearInterval(blinkInterval); blinkInterval=null; blinkNode=null; draw(); },3000);

}


function resetAll(skipConfirm=false){

  if(!skipConfirm && !confirm("๋ชจ๋“  ๋…ธ๋“œ์™€ ๊ฒฝ๋กœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?")) return;

  nodes={}; edges={}; nodeNames=[]; pathNodes=[]; updateSelectOptions();

  if(blinkInterval){ clearInterval(blinkInterval); blinkInterval=null; }

  blinkNode=null; draw();

}


// ----------------- ๋…ธ๋“œ ๋‹ค์ด์–ผ๋กœ๊ทธ -----------------

let tempX=0,tempY=0,selectedType='normal';

function openNodeDialog(x,y){ tempX=x; tempY=y; selectedType='normal'; document.getElementById('nodeDialog').style.display='block'; }

function closeNodeDialog(){ document.getElementById('nodeDialog').style.display='none'; }

document.querySelectorAll('#nodeDialog .typeBtn').forEach(btn=>btn.addEventListener('click',()=>{ selectedType=btn.dataset.type; }));


function saveNode(){

  const name=document.getElementById('nodeName').value.trim();

  const floor=document.querySelector('input[name="floorType"]:checked').value+document.getElementById('floorNumber').value+'์ธต';

  const note=document.getElementById('nodeNote').value;

  if(!name){ alert("๋…ธ๋“œ ์ด๋ฆ„ ์ž…๋ ฅ"); return; }

  const fullName=`${floor} ${name}`;

  if(nodes[fullName]){ alert("์ด๋ฏธ ์กด์žฌ"); return; }

  nodes[fullName]={x:tempX,y:tempY,floor,type:selectedType,note};

  nodeNames.push(fullName);


  // ๊ฐ™์€ ์ธต ์ผ๋ฐ˜ ์—ฐ๊ฒฐ

  nodeNames.forEach(n=>{

    if(n!==fullName && nodes[n].floor===floor){

      edges[n]=edges[n]||[]; edges[n].push(fullName);

      edges[fullName]=edges[fullName]||[]; edges[fullName].push(n);

    }

  });


  // ์ธต๊ฐ„ ์ด๋™ ์ปค๋„ฅํ„ฐ ์—ฐ๊ฒฐ

  if(selectedType==='escalator'||selectedType==='stairs'||selectedType==='elevator'){

    for(let n of nodeNames){

      if(n===fullName) continue;

      if(nodes[n].type===selectedType && getLabel(n)===getLabel(fullName)){

        edges[n]=edges[n]||[]; edges[n].push(fullName);

        edges[fullName]=edges[fullName]||[]; edges[fullName].push(n);

      }

    }

  }


  updateSelectOptions(); closeNodeDialog(); draw();

}


// ----------------- ๊ทธ๋ฆผ -----------------

function draw(){

  ctx.clearRect(0,0,canvas.width,canvas.height);

  if(floorImg) ctx.drawImage(floorImg,0,0,canvas.width,canvas.height);


  // ๊ฒฝ๋กœ ๊ทธ๋ฆฌ๊ธฐ

  if(pathNodes.length>1){

    ctx.strokeStyle='red'; ctx.lineWidth=4;

    ctx.beginPath();

    pathNodes.forEach((n,i)=>{

      const node=nodes[n];

      if(i===0) ctx.moveTo(node.x,node.y);

      else ctx.lineTo(node.x,node.y);

    });

    ctx.stroke();

  }


  // ๋…ธ๋“œ ๊ทธ๋ฆฌ๊ธฐ

  nodeNames.forEach(n=>{

    const node=nodes[n];

    ctx.beginPath();

    ctx.arc(node.x,node.y,8,0,Math.PI*2);

    if(n===blinkNode) ctx.fillStyle='yellow';

    else if(pathNodes.includes(n)) ctx.fillStyle='red';

    else ctx.fillStyle=node.type==='normal'?'blue':(node.type==='stairs'?'orange':node.type==='elevator'?'green':'purple');

    ctx.fill();

    ctx.strokeStyle='#000'; ctx.stroke();

  });

}


resizeCanvas();

</script>

</body>

</html>

๋Œ“๊ธ€ ์—†์Œ:

๋Œ“๊ธ€ ์“ฐ๊ธฐ