<!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>
๋๊ธ ์์:
๋๊ธ ์ฐ๊ธฐ