2025년 10월 9일 목요일

임의동행 고지

 I, the above-named person, hereby confirm that I have received a request of voluntary accompaniment made by a police officer at the aforementioned date, time, and place, that I have been informed that I may refuse to accompany the police officer and leave freely at any time, and that I myself have consented to the request of voluntary accompaniment.


저는 상기 기재된 본인이며, 상기 일시 및 장소에서 경찰관으로부터 임의 동행 요청을 받았고, 그 요청에 대해 동행을 거부하고 언제든지 자유롭게 떠날 수 있다는 설명을 들었으며, 제가 자발적으로 그 임의 동행 요청에 동의하였음을 확인합니다.

私は上記の本人であり、上記の日時および場所において警察官からの任意同行の要請を受けたこと、その要請に対して同行を拒否し、いつでも自由に立ち去ることができる旨の説明を受けたこと、そして自らその任意同行の要請に同意したことをここに確認します。


私は(わたしは)上記(じょうき)の本人(ほんにん)であり、上記(じょうき)の日時(にちじ)および場所(ばしょ)において 警察官(けいさつかん)からの任意同行(にんいどうこう)の要請(ようせい)を受(う)けたこと、その要請(ようせい)に対(たい)して同行(どうこう)を拒否(きょひ)し、いつでも自由(じゆう)に立(た)ち去(さ)ることができる旨(むね)の説明(せつめい)を受(う)けたこと、そして自(みずか)らその任意同行(にんいどうこう)の要請(ようせい)に同意(どうい)したことをここに確認(かくにん)します。


    および [び] 및, 또

     において (동작 작용이 행해지는 곳이나 때를 나타냄)

                  ...에서

     たちさる (立ち去る) 떠나다/ 물러나다

    旨(むね) 취지, 뜻

    自(みずか)ら 자기/저/몸소


私は、上記の日時と場所で警察官から任意同行(強制ではなく、本人の同意によって警察官に同行すること)のお願いを受け、その際、「同行は自由であり、いつでも断ったり、その場から立ち去ったりすることができる」といった説明を受けました。そして、自分の意思でその要請に同意したことをここに確認します。


私は(わたしは)、上記(じょうき)のとおり警察官(けいさつかん)から任意同行(にんいどうこう) のお願い(おねがい)を受(う)け、そのとき、「同行(どうこう)は自由(じゆう)であり、いつでも断(ことわ)ったり、その場所(ばしょ)から立(た)ち去(さ)ったりすることができる」と説明(せつめい)を受(う)けました。そして、自分(じぶん)の意思(いし)でその要請(ようせい)に同意(どうい)したことをここに確認(かくにん)します。

任意同行同意書  にんいどうこう どういしょ

同行を求めた日時・場所  どうこうをもとめた にちじ・ばしょ

同行先 どうこうさき

同行の理由  どうこうのりゆう

同行対象者  どうこうたいしょうしゃ

担当警察官 たんとう けいさつかん

所属  しょぞく

階級  かいきゅう

氏名 しめい

私は、上記のとおり警察官から任意同行のお願いを受け、そのとき、「同行は自由であり、いつでも断ったり、その場所から立ち去ったりすることができる」と説明を受けました。そして、自分の意思でその要請に同意したことをここに確認します。


I hereby confirm that I received a request for voluntary accompaniment from a police officer as stated above, that at that time I was informed that accompaniment is voluntary and that I may refuse or leave the place at any time, and that I have consented to the request of voluntary accompaniment of my own free will.


任意同行同意書   

同行を求めた日時・場所   

同行先  

同行の理由   

同行対象者   

担当警察官  

所属   

階級   

氏名 

     

    

가정폭력 피해자 권리 및 지원 안내서

 家庭暴力被害者権利および支援案内書

1. 緊急臨時措置

いつ: 家庭内暴力犯罪再発するおそれがあり緊急する場合

支援内容:

 ① 住居などからの加害者隔離

 ② 電話・メールによる接近禁止

 ③ 被害者家族構成員住居職場から100メートル以内接近禁止

申請方法: 警察官申請

2. 臨時措置

いつ: 家庭内暴力犯罪再発するおそれがある場合

支援内容:

 ① 住居などからの加害者隔離

 ② 電話・メールによる接近禁止

 ③ 被害者家族構成員住居職場から100メートル以内接近禁止

 ④ 既存臨時措置違反した場合留置場または拘置所への収容

 ⑤ 裁判官職権により相談所への相談委託

 ⑥ 裁判官職権により医療機関への治療委託

申請方法:

 被害者警察官申請 警察官検察官請求 裁判所決定

3. 被害者保護命令

いつ: 家庭内暴力被害者する保護措置必要場合

支援内容:

 ① 住居などからの加害者隔離

 ② 電話・メールによる接近禁止

 ③ 被害者家族構成員住居職場から100メートル以内接近禁止

 ④ 親権行使制限

 ⑤ 面会交流権制限

申請方法:

 警察などの捜査機関経由せず被害者直接家庭裁判所申請

4. 身辺安全措置

いつ: 家庭内暴力被害者する保護措置必要場合

支援内容:

 ① 保護治療施設への

 ② 被害者居住地周辺巡回

 ③ 裁判所出廷帰宅または面会交流権行使時同行

申請方法: 被害者または法定代理人家庭裁判所要請

5. 家庭保護事件

制度案内:

 加害者に対して懲役・罰金などの刑事処罰に代えて、暴力性の矯正・治療を目的として保護処分を受ける制度で、保護処分の種類は以下のとおりです

保護処分:

 ① 加害者接近制限

 ② 電話・メールによる接近禁止

 ③ 保護観察

 ④ 感化委託

 ⑤ 社会奉仕講習命令

 ⑥ 親権行使制限

 ⑦ 治療委託

 ⑧ 相談委託

2025년 9월 26일 금요일

개인형이동장치

구분기준

원동기장치자전거

125cc 이하 및 11kw 이하 이륜자동차

개인형이동장치

(원동기장치자전거 중)

1. 시속 25킬로미터 이상으로 운행할 경우 전동기가 작동하지 아니하고,

2. 자체중량 30kg 미만,

3. 행정안전부령으로 정하는 것

- 전동킥보드, 전동이륜평행차, 전동기의 동력만으로 움직일 수 있는 자전거

자전거(전기자전거 포함)

-자전거:

구동장치(사람의 힘), 조향장치, 제동장치, 바퀴 2개 이상

-전기자전거:

페달과 전동기 동시 동력(전동기만으로 움직이지 않을 것)

시속 25km 이상으로 움직일 경우 전동기가 작동하지 않을 것

자체중략 30kg 미만

 

2. 위반사항

무면허위반(원동기장치자전거, 개인형이동장치)

음주운전위반(원동기장치자전거, 개인형이동장치, 자전거)

운전자준수사항위반(안전모 및 승차정원)(원동기장치자전거, 개인형이동장치)

통행방법위반(원동기장치자전거, 개인형이동장치, 자전거)

 

3

2025년 9월 17일 수요일

좌표

 <!-- direct-kakao.html -->

<!doctype html>

<html>

<head><meta charset="utf-8"><title>Kakao 직접 호출 (노출주의)</title></head>

<body>

  <input id="addr" placeholder="주소 입력" style="width:60%">

  <button id="btn">조회(직접)</button>

  <div id="out"></div>


  <script>

    const KAKAO_API_KEY = "bb1b2ac193ac0040534e8560671067e7"; // 노출 위험!

    document.getElementById("btn").addEventListener("click", async ()=>{

      const address = document.getElementById("addr").value.trim();

      if (!address) return alert("주소 입력");

      try {

        const url = new URL("https://dapi.kakao.com/v2/local/search/address.json");

        url.searchParams.set("query", address);

        const r = await fetch(url.toString(), {

          headers: { "Authorization": `KakaoAK ${KAKAO_API_KEY}` }

        });

        const j = await r.json();

        if (j.documents && j.documents.length) {

          const d = j.documents[0];

          document.getElementById("out").innerText = `${d.y},${d.x}`;

        } else document.getElementById("out").innerText = "좌표 없음";

      } catch (e) {

        document.getElementById("out").innerText = "오류: " + e.message;

      }

    });

  </script>

</body>

</html>

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>