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

11-1

 <!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;

      nodes = {};

      edges = {};

      nodeNames = [];

      pathNodes = [];

      blinkNode = null;

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

      updateSelectOptions();

      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}' ๋…ธ๋“œ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`)){

        // 1) nodes์—์„œ ์‚ญ์ œ

        delete nodes[clickedNode];


        // 2) nodeNames์—์„œ ์‚ญ์ œ

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


        // 3) edges์—์„œ ์—ฐ๊ฒฐ ์ œ๊ฑฐ

        delete edges[clickedNode];

        for(let key in edges){

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

        }


        // 4) ์„ ํƒ ๋ฐ•์Šค ๊ฐฑ์‹ 

        updateSelectOptions();


        // 5) ๊ฒฝ๋กœ ์ดˆ๊ธฐํ™”

        pathNodes = [];


        draw();

      }

      return;

    }

  }


  openNodeDialog(x,y);

});


function findNodeAt(x,y){

  for(let name of nodeNames){

    const node = nodes[name];

    const dx=node.x-x;

    const dy=node.y-y;

    if(Math.sqrt(dx*dx+dy*dy)<=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 distance(n1,n2){

  const dx=n1.x-n2.x;

  const dy=n1.y-n2.y;

  return Math.sqrt(dx*dx+dy*dy);

}


// Dijkstra with allowed nodes

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

  const visited=new Set();

  const distances={};

  const prev={};

  const targetNodes=allowedNodes||nodeNames;

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

  distances[start]=0;


  while(visited.size<targetNodes.length){

    let minNode=null;

    for(let node of targetNodes){

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

        minNode=node;

      }

    }

    if(minNode===null) break;

    if(minNode===end) break;

    visited.add(minNode);

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

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

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

      if(alt<distances[neighbor]){

        distances[neighbor]=alt;

        prev[neighbor]=minNode;

      }

    }

  }


  const path=[];

  let curr=end;

  while(curr){

    path.unshift(curr);

    curr=prev[curr];

  }

  return path;

}


function getLabel(fullName){

  return fullName.split(' ').slice(1).join(' ').trim();

}


/* ====================== [UPDATED] ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ธต๊ฐ„ ์ด๋™ ์ง€์› ======================= */

// ์ธต ๋ฌธ์ž์—ด์„ ํŒŒ์‹ฑํ•ด์„œ {level, zone} ๋ฐ˜ํ™˜

// level: ์ง€์ƒ ์–‘์ˆ˜, ์ง€ํ•˜ ์Œ์ˆ˜. zone: '' | 'A' | 'B'

function parseFloor(floorStr){

  const s = floorStr.trim();

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

  const isGround   = s.includes('์ง€์ƒ') || (!isBasement && s.includes('์ธต'));

  // ์ˆซ์ž

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

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

  const level = isBasement ? -num : num;


  // ๋ถ„๊ธฐ(zone): "์ง€ํ•˜2์ธต A", "์ง€ํ•˜3์ธต B" ๋“ฑ์—์„œ A/B ์ธ์‹

  let zone = '';

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

  if(zoneMatch){

    zone = zoneMatch[0].toUpperCase();

  }else{

    // "์ง€ํ•˜2์ธตA" ๊ฐ™์ด ๋ถ™์–ด์žˆ๋Š” ๊ฒฝ์šฐ

    const compactZone = s.replace(/\s+/g,'').match(/์ง€ํ•˜[23]์ธต([AB])/i);

    if(compactZone) zone = compactZone[1].toUpperCase();

  }

  return { level, zone };

}


// ๊ฐ™์€ "๊ตฌ์—ญ" ํŒ์ •: level ๋™์ผ && zone ๋™์ผ (๋‹จ, zone์ด ์—†์œผ๋ฉด ๋™์ผ ์ธต์œผ๋กœ ๊ฐ„์ฃผ)

function sameArea(f1, f2){

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

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

  // ๋ถ„๊ธฐ๊ฐ€ ์žˆ๋Š” ์ธต(-2, -3)์—์„œ๋Š” zone๊นŒ์ง€ ๊ฐ™์•„์•ผ ๊ฐ™์€ ์ธต์œผ๋กœ ๊ฐ„์ฃผ

  const isSplit = (a.level === -2 || a.level === -3);

  if(isSplit) return a.zone === b.zone && a.zone !== '';

  return true;

}


// ๊ทœ์น™ ํ…Œ์ด๋ธ”: (fromState) -> [ {toState, filters[]} ]

// filter: {type:'stairs'|'escalator'|'elevator', labelIncludes:'1๋ฒˆ' ๋“ฑ}

const RULES = {

  // ์ง€์ƒ1์ธต -> ์ง€ํ•˜1์ธต : 1๋ฒˆ/4๋ฒˆ/5๋ฒˆ ๊ณ„๋‹จ

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

  // ์ง€ํ•˜1์ธต -> ์ง€ํ•˜2์ธต A : 1๋ฒˆ ์—์Šค์ปฌ๋ ˆ์ดํ„ฐ

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

  // ์ง€ํ•˜2์ธต A -> ์ง€ํ•˜3์ธต A/B : 2๋ฒˆ/3๋ฒˆ ์—์Šค์ปฌ๋ ˆ์ดํ„ฐ

  '-2:A': [

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

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

  ],

  // ์ง€์ƒ2์ธต -> ์ง€ํ•˜2์ธต B : 2๋ฒˆ ๊ณ„๋‹จ

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

  // ์ง€์ƒ3์ธต -> ์ง€ํ•˜2์ธต B : 3๋ฒˆ ๊ณ„๋‹จ

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

  // ์ง€ํ•˜2์ธต B -> ์ง€ํ•˜3์ธต A/B : (A: 1๋ฒˆ ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ/4๋ฒˆ ์—์Šค์ปฌ๋ ˆ์ดํ„ฐ) (B: 2๋ฒˆ ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ/5๋ฒˆ ์—์Šค์ปฌ๋ ˆ์ดํ„ฐ)

  '-2:B': [

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

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

  ]

};


// ์ƒํƒœ ํ‚ค ์ƒ์„ฑ: "level:zone" (zone ์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด)

function stateKeyFromFloorStr(floorStr){

  const {level, zone} = parseFloor(floorStr);

  return `${level}:${zone||''}`;

}


// ์ƒํƒœ ํ‚ค์—์„œ ์‚ฌ๋žŒ์ด ์ฝ๋Š” ์„ค๋ช…(๋””๋ฒ„๊ทธ์šฉ)

function prettyState(key){

  const [levelStr, zone] = key.split(':');

  const level = parseInt(levelStr,10);

  const isB = level < 0;

  const abs = Math.abs(level);

  if(isB){

    return `์ง€ํ•˜${abs}์ธต${zone?` ${zone}`:''}`;

  }else{

    return `์ง€์ƒ${abs}์ธต`;

  }

}


// RULES ๊ธฐ๋ฐ˜ ์ธ์ ‘ ์ƒํƒœ ๋ชฉ๋ก

function neighbors(state){

  return (RULES[state]||[]).map(r=>r.to);

}


// BFS๋กœ ์ธต ์ƒํƒœ ๊ฒฝ๋กœ ๊ณ„์‚ฐ

function bfsStatePath(startState, endState){

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

  const q=[startState];

  const prev={};

  const 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){

        const 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} = parseStateKey(stateKey);

  const candidates = nodeNames.filter(n=>{

    const f = parseFloor(nodes[n].floor);

    if(f.level!==level) return false;

    // ๋ถ„๊ธฐ์ธต์—์„œ๋Š” zone ์ผ์น˜ ํ•„์š”

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

    // ํƒ€์ž…/๋ผ๋ฒจ ํ•„ํ„ฐ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋งŒ์กฑํ•˜๋ฉด ํ›„๋ณด

    const label = getLabel(n);

    return filters.some(fi=>{

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

    });

  });

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

  // ์ถœ๋ฐœ์ ๊ณผ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ปค๋„ฅํ„ฐ ์„ ํƒ

  const currNode = nodes[currNodeName];

  return candidates.reduce((best,now)=>

    distance(currNode, nodes[now]) < distance(currNode, nodes[best]) ? now : best

  );

}


// ๋ฐ˜๋Œ€ํŽธ ์ธต์—์„œ "๋ผ๋ฒจ ๋™์ผ + ํƒ€์ž… ๋™์ผ" ๋…ธ๋“œ ์ฐพ๊ธฐ

function matchConnectorOnNext(closestCurrName, nextStateKey){

  const {level, zone} = parseStateKey(nextStateKey);

  const t = nodes[closestCurrName].type;

  const label = getLabel(closestCurrName);

  const candidates = nodeNames.filter(n=>{

    const f = parseFloor(nodes[n].floor);

    if(f.level!==level) return false;

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

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

  });

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

  // ํ˜„์žฌ ์ปค๋„ฅํ„ฐ์™€ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋™์ผ ๋ผ๋ฒจ/ํƒ€์ž… ๋…ธ๋“œ ์„ ํƒ

  const curr = nodes[closestCurrName];

  return candidates.reduce((best,now)=>

    distance(curr, nodes[now]) < distance(curr, nodes[best]) ? now : best

  );

}


function parseStateKey(key){

  const [lv, zone] = key.split(':');

  return { level: parseInt(lv,10), zone: zone||'' };

}


// ---------- ์ตœ์ข… ๊ฒฝ๋กœ ๊ณ„์‚ฐ ----------

function multiFloorPath(startName,endName){

  const startFloorStr = nodes[startName].floor;

  const endFloorStr   = nodes[endName].floor;


  const startState = stateKeyFromFloorStr(startFloorStr);

  const endState   = stateKeyFromFloorStr(endFloorStr);


  // ๊ฐ™์€ ๊ตฌ์—ญ(๊ฐ™์€ ์ธต + ๊ฐ™์€ ๋ถ„๊ธฐ)์ผ ๋•Œ๋งŒ ๊ฐ™์€ ์ธต ์ตœ๋‹จ๊ฑฐ๋ฆฌ ํ—ˆ์šฉ

  if(sameArea(startFloorStr, endFloorStr)){

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

    return dijkstra(startName, endName, allowed);

  }


  // ์ƒํƒœ ๊ทธ๋ž˜ํ”„์—์„œ ์ธต๊ฐ„ ๊ฒฝ๋กœ ์ฐพ๊ธฐ

  const statePath = bfsStatePath(startState, endState);

  if(!statePath){

    alert(`์ธต๊ฐ„ ๊ฒฝ๋กœ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. (๊ทœ์น™์œผ๋กœ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Œ)\n${prettyState(startState)} → ${prettyState(endState)}`);

    return null;

  }


  let currNode = startName;

  const full = [];


  // ๊ฐ ์ƒํƒœ ์ „์ด๋งˆ๋‹ค: (currState -> nextState)

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

    const currState = statePath[i];

    const nextState = statePath[i+1];


    // ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ•„ํ„ฐ(์ปค๋„ฅํ„ฐ ์ข…๋ฅ˜/๋ฒˆํ˜ธ)

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

    if(!rule){

      alert(`๋‚ด๋ถ€ ๊ทœ์น™ ์˜ค๋ฅ˜: ${prettyState(currState)} → ${prettyState(nextState)} ์ „์ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`);

      return null;

    }


    // 1) ํ˜„์žฌ ์ƒํƒœ์—์„œ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด "ํ—ˆ์šฉ ์ปค๋„ฅํ„ฐ"

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

    if(!closestCurr){

      alert(`${prettyState(currState)} ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ปค๋„ฅํ„ฐ ๋…ธ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.\n(ํ•„์š”: ${rule.filters.map(f=>`${f.labelIncludes} ${f.type}`).join(' ๋˜๋Š” ')})`);

      return null;

    }


    // 2) ๋‹ค์Œ ์ƒํƒœ์—์„œ ๊ฐ™์€ ๋ผ๋ฒจ/ํƒ€์ž…์˜ ๋Œ€์‘ ๋…ธ๋“œ

    const closestNext = matchConnectorOnNext(closestCurr, nextState);

    if(!closestNext){

      alert(`${prettyState(nextState)} ์—์„œ '${getLabel(closestCurr)}' (${nodes[closestCurr].type})์— ๋Œ€์‘ํ•˜๋Š” ๋…ธ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`);

      return null;

    }


    // 3) ํ˜„์žฌ ์ƒํƒœ ๋‚ด๋ถ€ ์ตœ๋‹จ๊ฑฐ๋ฆฌ(ํƒ€์ž… ๋ฌด์‹œ)๋กœ ์ปค๋„ฅํ„ฐ๊นŒ์ง€ ์ด๋™

    const allowedCurr = nodeNames.filter(n=> {

      const f = nodes[n].floor;

      return sameArea(f, 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));


    // 4) ์ธต ์ด๋™(์ปค๋„ฅํ„ฐ ๋Œ€์‘ ๋…ธ๋“œ๋กœ ‘์ ํ”„’)

    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;

}

/* ====================== [/UPDATED] ======================= */


function findPath(){

  const startName=document.getElementById('start').value;

  const endName=document.getElementById('end').value;

  pathNodes=multiFloorPath(startName,endName)||[];

  draw();

}


// ๊ฒ€์ƒ‰ ์ž…๋ ฅ ์‹œ label ๊ธฐ์ค€์œผ๋กœ ๋…ธ๋“œ ์ฐพ๊ธฐ

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(()=>{ draw(visible); visible=!visible; },500);

}


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

function resetAll(){

  if(!confirm("๊ธธ์ฐพ๊ธฐ ๊ฒฝ๋กœ์™€ ๊ฒ€์ƒ‰ ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.")) return;

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

  blinkNode=null;

  pathNodes=[];

  draw();

}


// ๊ทธ๋ฆฌ๊ธฐ

function draw(blinkVisible=true){

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

  if(floorImg){

    const scale=Math.min(canvas.width/floorImg.width,canvas.height/floorImg.height);

    const imgW=floorImg.width*scale;

    const imgH=floorImg.height*scale;

    const x=(canvas.width-imgW)/2;

    const y=(canvas.height-imgH)/2;

    ctx.drawImage(floorImg,x,y,imgW,imgH);

  }

  nodeNames.forEach(n=>{

    ctx.beginPath();

    ctx.arc(nodes[n].x,nodes[n].y,6,0,Math.PI*2);

    ctx.fillStyle=(n===blinkNode && !blinkVisible)?'white':'gray';

    ctx.fill();

    ctx.strokeStyle='black';

    ctx.stroke();

  });


  if(pathNodes.length>=2){

    ctx.strokeStyle='red';

    ctx.lineWidth=4;

    ctx.beginPath();

    ctx.moveTo(nodes[pathNodes[0]].x,nodes[pathNodes[0]].y);

    for(let i=1;i<pathNodes.length;i++) ctx.lineTo(nodes[pathNodes[i]].x,nodes[pathNodes[i]].y);

    ctx.stroke();

  }


  if(pathNodes.length>=1){

    [pathNodes[0], pathNodes[pathNodes.length-1]].forEach((node,i)=>{

      ctx.beginPath();

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

      ctx.fillStyle=i===0?'green':'blue';

      ctx.fill();

      ctx.strokeStyle='black';

      ctx.stroke();

    });

  }

}


resizeCanvas();


/* ---------- ๋…ธ๋“œ ์ถ”๊ฐ€ ๋‹ค์ด์–ผ๋กœ๊ทธ ---------- */

let pendingX=0, pendingY=0;

let selectedType="normal";


document.querySelectorAll(".typeBtn").forEach(btn=>{

  btn.addEventListener("click",()=>{ 

    selectedType=btn.dataset.type;

    document.querySelectorAll(".typeBtn").forEach(b=>b.style.background="");

    btn.style.background="yellow";

  });

});


function openNodeDialog(x,y){

  pendingX=x; pendingY=y;

  selectedType="normal";

  document.querySelectorAll(".typeBtn").forEach(b=>b.style.background="");

  document.querySelector(".typeBtn[data-type='normal']").style.background="yellow";

  document.getElementById("nodeDialog").style.display="block";

}


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


function saveNode(){

  const floorType=document.querySelector("input[name=floorType]:checked").value;

  const floorNum=document.getElementById("floorNumber").value;

  if(!floorNum){ alert("์ธต ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"); return; }

  let floor=`${floorType}${floorNum}์ธต`;


  // ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ A/B ๊ตฌ์—ญ์„ ์ด๋ฆ„์— ๋„ฃ๊ณ  ์‹ถ๋‹ค๋ฉด, ๋…ธ๋“œ ์ด๋ฆ„์— 'A' ๋˜๋Š” 'B'๋ฅผ ํฌํ•จํ•˜์„ธ์š”.

  // ์˜ˆ: "์ง€ํ•˜2์ธต A"๋กœ ๋งŒ๋“ค๋ ค๋ฉด ์ธต ์ž…๋ ฅ ๋’ค, ๋…ธ๋“œ ์ด๋ฆ„์— A/B๋ฅผ ๋ถ™์—ฌ ์ €์žฅํ•˜๊ฑฐ๋‚˜

  // ์•„๋ž˜์ฒ˜๋Ÿผ floor ๋ณ€์ˆ˜ ํ›„์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜์ •ํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.


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

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

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

  if(nodes[fullName]){ alert("๊ฐ™์€ ์ด๋ฆ„์˜ ๋…ธ๋“œ๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."); return; }

  const note=document.getElementById("nodeNote").value.trim();

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

  nodeNames.push(fullName);

  if(nodeNames.length>1){

    const prev=nodeNames[nodeNames.length-2];

    if(!edges[prev]) edges[prev]=[];

    if(!edges[fullName]) edges[fullName]=[];

    edges[prev].push(fullName);

    edges[fullName].push(prev);

  }

  updateSelectOptions();

  closeNodeDialog();

  draw();

}

</script>

</body>

</html>

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

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