block-blast-solver / index.html
chanmin0723's picture
Upload index.html
c05b442 verified
Raw
History Blame Contribute Delete
43.5 kB
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Block Blast Solver β€” λ‹€μŒ 수 계산기</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#12141b;
--panel:#1b1e28;
--panel-2:#20232f;
--grid-line:#2c3040;
--grid-line-soft:#242733;
--ink:#eef0f6;
--muted:#8b90a4;
--muted-2:#5d6175;
--piece-a:#ff6f61;
--piece-a-dim:#5a2c28;
--piece-b:#3fd1c4;
--piece-b-dim:#1f4744;
--piece-c:#ffcb4d;
--piece-c-dim:#4d411c;
--filled:#3b3f52;
--clear:#7effa3;
--radius:10px;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;}
body{
background:
linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0) 300px),
var(--bg);
color:var(--ink);
font-family:'Inter',sans-serif;
min-height:100vh;
padding:18px 16px 32px;
}
@media (prefers-reduced-motion: reduce){
*{animation-duration:0.001ms !important; transition-duration:0.001ms !important;}
}
.wrap{max-width:1220px;margin:0 auto;}
header.top{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:16px;
margin-bottom:16px;
padding-bottom:12px;
border-bottom:1px solid var(--grid-line-soft);
}
.brand{
font-family:'Space Grotesk',sans-serif;
font-weight:700;
font-size:22px;
letter-spacing:-0.01em;
display:flex;
align-items:baseline;
gap:10px;
}
.brand .mono-tag{
font-family:'JetBrains Mono',monospace;
font-size:11px;
font-weight:500;
color:var(--bg);
background:var(--clear);
padding:2px 7px;
border-radius:5px;
letter-spacing:0.02em;
}
.brand-sub{
color:var(--muted);
font-size:13px;
margin-top:4px;
}
.reset-btn{
background:none;
border:1px solid var(--grid-line);
color:var(--muted);
font-family:'Inter',sans-serif;
font-size:13px;
padding:8px 14px;
border-radius:8px;
cursor:pointer;
transition:border-color .15s, color .15s;
}
.reset-btn:hover{border-color:var(--muted-2); color:var(--ink);}
.reset-btn:focus-visible{outline:2px solid var(--clear); outline-offset:2px;}
main.layout{
display:grid;
grid-template-columns:minmax(300px, 400px) minmax(380px, 1fr);
gap:16px;
align-items:start;
}
@media (max-width:760px){
main.layout{grid-template-columns:1fr;}
}
.panel{
background:var(--panel);
border:1px solid var(--grid-line-soft);
border-radius:14px;
padding:16px;
}
.panel-title{
font-family:'Space Grotesk',sans-serif;
font-weight:600;
font-size:14px;
letter-spacing:0.02em;
text-transform:uppercase;
color:var(--muted);
margin:0 0 14px;
display:flex;
align-items:center;
justify-content:space-between;
}
.panel-title .hint{
font-family:'Inter',sans-serif;
text-transform:none;
font-weight:400;
font-size:12px;
color:var(--muted-2);
letter-spacing:0;
}
/* ---- 8x8 board ---- */
.board8{
display:grid;
grid-template-columns:repeat(8, 1fr);
gap:3px;
aspect-ratio:1/1;
max-width:460px;
margin:0 auto;
background:var(--grid-line-soft);
padding:3px;
border-radius:8px;
touch-action:none;
user-select:none;
-webkit-user-select:none;
}
.cell{
aspect-ratio:1/1;
border-radius:4px;
background:var(--panel-2);
cursor:pointer;
border:1px solid transparent;
position:relative;
transition:background .1s;
touch-action:none;
}
.cell:hover{background:#282c3a;}
.cell:focus-visible{outline:2px solid var(--clear); outline-offset:-2px;}
.cell.filled{background:var(--filled);}
.cell.piece-a{background:var(--piece-a);}
.cell.piece-b{background:var(--piece-b);}
.cell.piece-c{background:var(--piece-c);}
.cell.will-clear::after{
content:"";
position:absolute; inset:0;
border:2px solid var(--clear);
border-radius:4px;
box-shadow:0 0 8px rgba(126,255,163,0.55);
}
.cell .num{
position:absolute; top:1px; right:2px;
font-family:'JetBrains Mono',monospace;
font-size:9px;
color:rgba(0,0,0,0.55);
}
.board-legend{
display:flex;
gap:16px;
justify-content:center;
margin-top:10px;
font-size:11px;
color:var(--muted);
flex-wrap:wrap;
}
.board-legend span{display:inline-flex; align-items:center; gap:5px;}
.swatch{width:10px;height:10px;border-radius:3px;display:inline-block;}
/* ---- piece editors ---- */
.pieces{display:flex; flex-direction:column; gap:14px;}
.piece-card{
background:var(--panel);
border:1px solid var(--grid-line-soft);
border-radius:14px;
padding:14px;
}
.piece-head{
display:flex; align-items:flex-start; justify-content:space-between;
gap:10px;
margin-bottom:12px;
}
.piece-label{
font-family:'Space Grotesk',sans-serif;
font-weight:600;
font-size:13px;
display:flex; flex-direction:column; gap:3px;
}
.hint-inline{
font-family:'Inter',sans-serif;
font-weight:400;
font-size:11px;
color:var(--muted-2);
}
.piece-clear{
background:none; border:none; color:var(--muted-2);
font-size:11px; cursor:pointer; font-family:'Inter',sans-serif;
padding:2px 4px;
white-space:nowrap;
}
.piece-clear:hover{color:var(--ink);}
/* ---- slot row: pick which of A/B/C you're filling ---- */
.slot-row{
display:grid;
grid-template-columns:repeat(3, 1fr);
gap:8px;
margin-bottom:14px;
}
.slot-chip{
background:var(--panel-2);
border:1.5px solid var(--grid-line);
border-radius:10px;
padding:8px 6px;
cursor:pointer;
display:flex;
flex-direction:column;
align-items:center;
gap:6px;
transition:border-color .12s, box-shadow .12s, opacity .12s;
opacity:0.72;
}
.slot-chip:hover{border-color:var(--muted-2);}
.slot-chip:focus-visible{outline:2px solid var(--clear); outline-offset:2px;}
.slot-chip.active{
opacity:1;
border-color:var(--piece-color);
box-shadow:0 0 0 1px var(--piece-color) inset, 0 0 14px -4px var(--piece-color);
}
.slot-thumb-wrap{
width:100%;
height:34px;
display:flex;
align-items:center;
justify-content:center;
}
.slot-empty{
font-family:'JetBrains Mono',monospace;
font-size:11px;
color:var(--muted-2);
border:1.5px dashed var(--grid-line);
border-radius:6px;
width:26px; height:26px;
display:flex; align-items:center; justify-content:center;
}
.slot-chip.active .slot-empty{border-color:var(--piece-color); color:var(--piece-color);}
.slot-label{
font-family:'Space Grotesk',sans-serif;
font-weight:700;
font-size:12px;
color:var(--piece-color);
}
/* ---- mode tabs (preset vs draw) ---- */
.mode-tabs{
display:flex;
gap:4px;
background:var(--panel-2);
border-radius:8px;
padding:3px;
margin-bottom:12px;
}
.mode-tab{
flex:1;
background:none;
border:none;
color:var(--muted);
font-family:'Inter',sans-serif;
font-size:12px;
font-weight:500;
padding:7px 4px;
border-radius:6px;
cursor:pointer;
transition:background .12s, color .12s;
}
.mode-tab:hover{color:var(--ink);}
.mode-tab.active{
background:var(--panel);
color:var(--ink);
font-weight:600;
}
.mode-tab:focus-visible{outline:2px solid var(--clear); outline-offset:1px;}
/* ---- preset picker ---- */
/* Cell size is FIXED (--cell-px) across every shape, so a thumbnail's overall
footprint scales with the piece's real size instead of every thumbnail
being squeezed into an identical box. */
.preset-panel{
--cell-px:10px;
max-height:64vh;
overflow-y:auto;
padding-right:4px;
}
.preset-group-label{
font-family:'JetBrains Mono',monospace;
font-size:10px;
letter-spacing:0.03em;
text-transform:uppercase;
color:var(--muted-2);
margin:10px 0 6px;
}
.preset-group-label:first-child{margin-top:0;}
.preset-group-grid{
display:flex;
flex-wrap:wrap;
align-items:center;
gap:8px;
}
.preset-thumb-btn{
min-width:38px;
min-height:38px;
background:var(--panel-2);
border:1.5px solid var(--grid-line);
border-radius:8px;
cursor:pointer;
display:inline-flex;
align-items:center;
justify-content:center;
padding:6px;
transition:border-color .12s, background .12s;
}
.preset-thumb-btn:hover{border-color:var(--muted-2);}
.preset-thumb-btn:focus-visible{outline:2px solid var(--clear); outline-offset:1px;}
.preset-thumb-btn.selected{
border-color:var(--piece-color, var(--clear));
background:rgba(255,255,255,0.04);
box-shadow:0 0 0 1px var(--piece-color, var(--clear)) inset;
}
.thumb-grid{
display:grid;
gap:2px;
}
.thumb-cell{
width:var(--cell-px);
height:var(--cell-px);
background:transparent;
border-radius:1.5px;
}
.thumb-cell.filled{
background:var(--piece-color, var(--piece-a));
}
.piece-grid{
display:grid;
grid-template-columns:repeat(5, 1fr);
gap:2px;
width:120px;
margin:0 auto;
background:var(--grid-line-soft);
padding:2px;
border-radius:6px;
touch-action:none;
user-select:none;
-webkit-user-select:none;
}
.pcell{
aspect-ratio:1/1;
background:var(--panel-2);
border-radius:3px;
cursor:pointer;
border:none;
touch-action:none;
}
.pcell:hover{background:#282c3a;}
.pcell:focus-visible{outline:2px solid var(--clear); outline-offset:-1px;}
.pcell.on{background:var(--piece-color, var(--piece-a));}
.solve-row{
margin-top:6px;
}
.solve-btn{
width:100%;
background:var(--clear);
color:#0b1710;
border:none;
font-family:'Space Grotesk',sans-serif;
font-weight:700;
font-size:15px;
padding:13px;
border-radius:10px;
cursor:pointer;
transition:transform .08s, box-shadow .15s;
box-shadow:0 0 0 rgba(126,255,163,0);
}
.solve-btn:hover{box-shadow:0 4px 18px rgba(126,255,163,0.25);}
.solve-btn:active{transform:scale(0.98);}
.solve-btn:focus-visible{outline:2px solid var(--ink); outline-offset:2px;}
.solve-btn:disabled{background:var(--grid-line); color:var(--muted-2); cursor:default; box-shadow:none;}
.status-line{
font-family:'JetBrains Mono',monospace;
font-size:12px;
color:var(--muted);
margin-top:10px;
min-height:16px;
text-align:center;
}
/* ---- results ---- */
.results{margin-top:16px;}
.steps-row{
display:flex; gap:8px; flex-wrap:wrap;
margin-bottom:16px;
}
.step-chip{
font-family:'JetBrains Mono',monospace;
font-size:13px;
background:var(--panel-2);
border:1px solid var(--grid-line);
color:var(--muted);
padding:8px 14px;
border-radius:8px;
cursor:pointer;
transition:all .12s;
}
.step-chip:hover{color:var(--ink); border-color:var(--muted-2);}
.step-chip.active{
background:var(--clear);
color:#0b1710;
border-color:var(--clear);
font-weight:700;
}
.step-chip:focus-visible{outline:2px solid var(--clear); outline-offset:2px;}
.step-desc{
font-size:14px;
line-height:1.6;
color:var(--ink);
background:var(--panel-2);
border-left:3px solid var(--clear);
padding:12px 16px;
border-radius:0 8px 8px 0;
margin-bottom:16px;
}
.step-desc .coord{
font-family:'JetBrains Mono',monospace;
color:var(--clear);
}
.score-strip{
display:flex;
gap:22px;
flex-wrap:wrap;
font-family:'JetBrains Mono',monospace;
font-size:12px;
color:var(--muted);
padding-top:4px;
border-top:1px solid var(--grid-line-soft);
}
.score-strip b{color:var(--ink); font-weight:700;}
.continue-btn{
display:none;
width:100%;
margin-top:16px;
background:transparent;
color:var(--clear);
border:1.5px solid var(--clear);
font-family:'Space Grotesk',sans-serif;
font-weight:600;
font-size:14px;
padding:11px;
border-radius:10px;
cursor:pointer;
transition:background .15s, transform .08s;
}
.continue-btn:hover{background:rgba(126,255,163,0.08);}
.continue-btn:active{transform:scale(0.98);}
.continue-btn:focus-visible{outline:2px solid var(--clear); outline-offset:2px;}
.continue-btn.show{display:block;}
.empty-note{
color:var(--muted);
font-size:13px;
line-height:1.7;
text-align:center;
padding:20px 10px;
}
footer.foot{
max-width:1220px;
margin:22px auto 0;
color:var(--muted-2);
font-size:12px;
text-align:center;
line-height:1.6;
}
</style>
</head>
<body>
<div class="wrap">
<header class="top">
<div>
<div class="brand">Block Blast Solver <span class="mono-tag">SEARCH</span></div>
<div class="brand-sub">ν˜„μž¬ λ³΄λ“œμ™€ 3개 쑰각을 그리면, μ™„μ „νƒμƒ‰μœΌλ‘œ κ°€μž₯ 쀄을 많이 μ§€μš°λŠ” 배치 μˆœμ„œλ₯Ό μ°Ύμ•„λ“œλ €μš”.</div>
</div>
<button class="reset-btn" id="resetAllBtn">전체 μ΄ˆκΈ°ν™”</button>
</header>
<main class="layout">
<section class="panel">
<div class="panel-title">λ³΄λ“œ μƒνƒœ <span class="hint">칸을 클릭해 μ±„μ›Œμ§„ 블둝을 ν‘œμ‹œν•˜μ„Έμš”</span></div>
<div class="board8" id="board"></div>
<div class="board-legend">
<span><span class="swatch" style="background:var(--filled)"></span>μ±„μ›Œμ§„ μΉΈ</span>
<span><span class="swatch" style="background:var(--piece-a)"></span>쑰각 A</span>
<span><span class="swatch" style="background:var(--piece-b)"></span>쑰각 B</span>
<span><span class="swatch" style="background:var(--piece-c)"></span>쑰각 C</span>
</div>
<div class="results" id="resultsBlock" style="display:none;">
<div class="steps-row" id="stepsRow"></div>
<div class="step-desc" id="stepDesc"></div>
<div class="score-strip" id="scoreStrip"></div>
<button class="continue-btn" id="continueBtn" type="button">
이 κ²°κ³Ό λ³΄λ“œλ‘œ μ΄μ–΄μ„œ λ‹€μŒ 쑰각 놓기 β†’
</button>
</div>
<div class="empty-note" id="emptyNote">
μ•„λž˜μ—μ„œ μ„Έ 쑰각을 κ³ λ₯΄κ³  <b style="color:var(--ink)">ν•΄λ‹΅ μ°ΎκΈ°</b>λ₯Ό λˆ„λ₯΄λ©΄ μ—¬κΈ° κ²°κ³Όκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.
</div>
</section>
<aside class="pieces">
<div class="piece-card">
<div class="piece-head">
<div class="piece-label">쑰각 선택 <span class="hint-inline">같은 νŒμ—μ„œ 3번 클릭 β†’ A, B, C μˆœμ„œλ‘œ μ±„μ›Œμ Έμš”</span></div>
<button class="piece-clear" id="clearActiveBtn">μ§€κΈˆ 쑰각 μ§€μš°κΈ°</button>
</div>
<div class="slot-row" id="slotRow">
<button class="slot-chip active" data-slot="a" style="--piece-color:var(--piece-a)" type="button">
<span class="slot-thumb-wrap" id="slotThumb-a"><span class="slot-empty">A</span></span>
<span class="slot-label">A</span>
</button>
<button class="slot-chip" data-slot="b" style="--piece-color:var(--piece-b)" type="button">
<span class="slot-thumb-wrap" id="slotThumb-b"><span class="slot-empty">B</span></span>
<span class="slot-label">B</span>
</button>
<button class="slot-chip" data-slot="c" style="--piece-color:var(--piece-c)" type="button">
<span class="slot-thumb-wrap" id="slotThumb-c"><span class="slot-empty">C</span></span>
<span class="slot-label">C</span>
</button>
</div>
<div class="mode-tabs" id="modeTabs">
<button class="mode-tab active" data-mode="preset" type="button">ν”„λ¦¬μ…‹μ—μ„œ 선택</button>
<button class="mode-tab" data-mode="draw" type="button">직접 그리기</button>
</div>
<div class="preset-panel" id="presetPanel"></div>
<div class="piece-grid" id="pieceGridEditor" style="display:none;"></div>
</div>
<div class="solve-row">
<button class="solve-btn" id="solveBtn">ν•΄λ‹΅ μ°ΎκΈ°</button>
<div class="status-line" id="statusLine"></div>
</div>
</aside>
</main>
<footer class="foot">
프리셋 = μ‹€μ œ κ²Œμž„μ— λ‚˜μ˜€λŠ” μ •ν•΄μ§„ 쑰각 λͺ¨μ–‘ 쀑 선택 Β· λ³΄λ“œ/직접 κ·Έλ¦¬κΈ°λŠ” 클릭 λ˜λŠ” λ“œλž˜κ·Έλ‘œ μΉ ν•˜κΈ° Β· 탐색은 λΈŒλΌμš°μ €μ—μ„œλ§Œ μ‹€ν–‰λ˜λ©° μ„œλ²„λ‘œ μ „μ†‘λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
</footer>
</div>
<script>
(function(){
const N = 8;
const PIECE_GRID = 5;
const pieceKeys = ['a','b','c'];
const pieceColorVar = {a:'var(--piece-a)', b:'var(--piece-b)', c:'var(--piece-c)'};
const pieceNameKo = {a:'A', b:'B', c:'C'};
// ---- standard Block Blast piece shapes (fixed pool, no rotation in-game) ----
// Each shape's cells are offsets from its own top-left bounding box, matching
// how they actually appear in the real game (already-rotated variants listed separately).
const PRESET_SHAPES = [
// 라인 / 단일
{id:'1x1', group:'ν•œ μΉΈ / 직선', cells:[[0,0]]},
{id:'1x2h', group:'ν•œ μΉΈ / 직선', cells:[[0,0],[0,1]]},
{id:'2x1v', group:'ν•œ μΉΈ / 직선', cells:[[0,0],[1,0]]},
{id:'1x3h', group:'ν•œ μΉΈ / 직선', cells:[[0,0],[0,1],[0,2]]},
{id:'3x1v', group:'ν•œ μΉΈ / 직선', cells:[[0,0],[1,0],[2,0]]},
{id:'1x4h', group:'ν•œ μΉΈ / 직선', cells:[[0,0],[0,1],[0,2],[0,3]]},
{id:'4x1v', group:'ν•œ μΉΈ / 직선', cells:[[0,0],[1,0],[2,0],[3,0]]},
{id:'1x5h', group:'ν•œ μΉΈ / 직선', cells:[[0,0],[0,1],[0,2],[0,3],[0,4]]},
{id:'5x1v', group:'ν•œ μΉΈ / 직선', cells:[[0,0],[1,0],[2,0],[3,0],[4,0]]},
// μ‚¬κ°ν˜•
{id:'2x2', group:'μ‚¬κ°ν˜•', cells:[[0,0],[0,1],[1,0],[1,1]]},
{id:'3x3', group:'μ‚¬κ°ν˜•', cells:[[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]]},
{id:'2x3', group:'μ‚¬κ°ν˜•', cells:[[0,0],[0,1],[0,2],[1,0],[1,1],[1,2]]},
{id:'3x2', group:'μ‚¬κ°ν˜•', cells:[[0,0],[0,1],[1,0],[1,1],[2,0],[2,1]]},
// 꺾인 λͺ¨μ–‘ (3μΉΈ, μ„Έλͺ¨ μ½”λ„ˆ)
{id:'tri-1', group:'꺾인 λͺ¨μ–‘ (3μΉΈ)', cells:[[0,0],[1,0],[1,1]]},
{id:'tri-2', group:'꺾인 λͺ¨μ–‘ (3μΉΈ)', cells:[[0,0],[0,1],[1,0]]},
{id:'tri-3', group:'꺾인 λͺ¨μ–‘ (3μΉΈ)', cells:[[0,0],[0,1],[1,1]]},
{id:'tri-4', group:'꺾인 λͺ¨μ–‘ (3μΉΈ)', cells:[[0,1],[1,0],[1,1]]},
// L / J λͺ¨μ–‘ (4μΉΈ)
{id:'L1', group:'L자 λͺ¨μ–‘ (4μΉΈ)', cells:[[0,0],[1,0],[2,0],[2,1]]},
{id:'L2', group:'L자 λͺ¨μ–‘ (4μΉΈ)', cells:[[0,0],[0,1],[0,2],[1,0]]},
{id:'L3', group:'L자 λͺ¨μ–‘ (4μΉΈ)', cells:[[0,0],[0,1],[1,1],[2,1]]},
{id:'L4', group:'L자 λͺ¨μ–‘ (4μΉΈ)', cells:[[1,0],[1,1],[1,2],[0,2]]},
{id:'J1', group:'L자 λͺ¨μ–‘ (4μΉΈ)', cells:[[0,1],[1,1],[2,0],[2,1]]},
{id:'J2', group:'L자 λͺ¨μ–‘ (4μΉΈ)', cells:[[0,0],[1,0],[1,1],[1,2]]},
{id:'J3', group:'L자 λͺ¨μ–‘ (4μΉΈ)', cells:[[0,0],[0,1],[1,0],[2,0]]},
{id:'J4', group:'L자 λͺ¨μ–‘ (4μΉΈ)', cells:[[0,0],[0,1],[0,2],[1,2]]},
// T λͺ¨μ–‘
{id:'T-up', group:'T자 λͺ¨μ–‘', cells:[[0,0],[0,1],[0,2],[1,1]]},
{id:'T-down', group:'T자 λͺ¨μ–‘', cells:[[0,1],[1,0],[1,1],[1,2]]},
{id:'T-left', group:'T자 λͺ¨μ–‘', cells:[[0,1],[1,0],[1,1],[2,1]]},
{id:'T-right', group:'T자 λͺ¨μ–‘', cells:[[0,0],[1,0],[1,1],[2,0]]},
// S / Z λͺ¨μ–‘
{id:'S-h', group:'S / Z λͺ¨μ–‘', cells:[[0,1],[0,2],[1,0],[1,1]]},
{id:'S-v', group:'S / Z λͺ¨μ–‘', cells:[[0,0],[1,0],[1,1],[2,1]]},
{id:'Z-h', group:'S / Z λͺ¨μ–‘', cells:[[0,0],[0,1],[1,1],[1,2]]},
{id:'Z-v', group:'S / Z λͺ¨μ–‘', cells:[[0,1],[1,0],[1,1],[2,0]]},
// μ‹­μž / 큰 μ½”λ„ˆ
{id:'plus', group:'μ‹­μž / 큰 μ½”λ„ˆ', cells:[[0,1],[1,0],[1,1],[1,2],[2,1]]},
{id:'bigcorner-1', group:'μ‹­μž / 큰 μ½”λ„ˆ', cells:[[0,0],[1,0],[2,0],[2,1],[2,2]]},
{id:'bigcorner-2', group:'μ‹­μž / 큰 μ½”λ„ˆ', cells:[[0,0],[0,1],[0,2],[1,0],[2,0]]},
{id:'bigcorner-3', group:'μ‹­μž / 큰 μ½”λ„ˆ', cells:[[0,0],[0,1],[0,2],[1,2],[2,2]]},
{id:'bigcorner-4', group:'μ‹­μž / 큰 μ½”λ„ˆ', cells:[[0,2],[1,2],[2,0],[2,1],[2,2]]},
];
// ---- state ----
let board = Array.from({length:N}, ()=>Array(N).fill(0));
let pieceGrids = {
a: Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)),
b: Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)),
c: Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)),
};
let pieceMode = {a:'preset', b:'preset', c:'preset'}; // 'preset' | 'draw'
let selectedPreset = {a:null, b:null, c:null};
let activeSlot = 'a'; // which piece (a/b/c) the shared picker is currently filling
let lastSolution = null; // {steps:[{piece,r,c,offsets,boardBeforeClear,clearRows,clearCols,boardAfterClear}], totalLines, finalEmpty}
// ---- DOM refs ----
const boardEl = document.getElementById('board');
const statusLine = document.getElementById('statusLine');
const resultsBlock = document.getElementById('resultsBlock');
const emptyNote = document.getElementById('emptyNote');
const stepsRow = document.getElementById('stepsRow');
const stepDesc = document.getElementById('stepDesc');
const scoreStrip = document.getElementById('scoreStrip');
const continueBtn = document.getElementById('continueBtn');
// ---- build board grid ----
const boardCells = [];
for(let r=0;r<N;r++){
const row=[];
for(let c=0;c<N;c++){
const cell = document.createElement('button');
cell.className='cell';
cell.type='button';
cell.setAttribute('aria-label', `${r+1}ν–‰ ${c+1}μ—΄`);
cell.dataset.role='board';
cell.dataset.key='board';
cell.dataset.r=r;
cell.dataset.c=c;
boardEl.appendChild(cell);
row.push(cell);
}
boardCells.push(row);
}
// ---- build the single shared 5x5 piece draw grid (used by whichever slot is active) ----
const pieceCellEls = [];
{
const el = document.getElementById('pieceGridEditor');
for(let r=0;r<PIECE_GRID;r++){
const rowEls=[];
for(let c=0;c<PIECE_GRID;c++){
const b = document.createElement('button');
b.type='button';
b.className='pcell';
b.dataset.role='piece';
b.dataset.key='a'; // updated live to whichever slot is active
b.dataset.r=r;
b.dataset.c=c;
el.appendChild(b);
rowEls.push(b);
}
pieceCellEls.push(rowEls);
}
}
// ---- shared cell get/set (used by both click and drag-paint) ----
function getCellValue(role, key, r, c){
return role==='board' ? board[r][c] : pieceGrids[key][r][c];
}
function setCellValue(role, key, r, c, val){
if(getCellValue(role,key,r,c)===val) return false; // no-op, skip redundant render
if(role==='board'){ board[r][c]=val; } else { pieceGrids[key][r][c]=val; }
clearSolution();
if(role==='board'){
renderBoard();
} else {
selectedPreset[key] = null; // manual edit no longer matches any preset
renderPiece(key);
renderPresetSelection();
}
return true;
}
function toggleCellValue(role, key, r, c){
const nv = getCellValue(role,key,r,c) ? 0 : 1;
setCellValue(role,key,r,c,nv);
return nv;
}
// ---- drag painting: click-and-drag (mouse) or touch-drag to paint many cells at once ----
let dragActive = false;
let dragRole = null, dragKey = null, dragValue = 0;
function targetFromPoint(x, y){
const el = document.elementFromPoint(x, y);
return el ? el.closest('[data-role]') : null;
}
function startDrag(target){
if(!target) return;
dragActive = true;
dragRole = target.dataset.role;
dragKey = target.dataset.key;
const r = +target.dataset.r, c = +target.dataset.c;
dragValue = toggleCellValue(dragRole, dragKey, r, c);
}
function continueDrag(target){
if(!dragActive || !target) return;
if(target.dataset.role !== dragRole || target.dataset.key !== dragKey) return;
const r = +target.dataset.r, c = +target.dataset.c;
setCellValue(dragRole, dragKey, r, c, dragValue);
}
function endDrag(){ dragActive = false; dragRole = null; dragKey = null; }
// mouse: paint by holding button and moving over cells
document.addEventListener('mousedown', (e)=>{
const target = e.target.closest('[data-role]');
if(!target) return;
e.preventDefault(); // avoid text-selection while dragging
startDrag(target);
});
document.addEventListener('mouseover', (e)=>{
continueDrag(e.target.closest('[data-role]'));
});
document.addEventListener('mouseup', endDrag);
// touch: paint by dragging a finger across cells
document.addEventListener('touchstart', (e)=>{
const t = e.touches[0];
const target = targetFromPoint(t.clientX, t.clientY);
if(!target) return;
e.preventDefault(); // avoid page scroll while painting
startDrag(target);
}, {passive:false});
document.addEventListener('touchmove', (e)=>{
if(!dragActive) return;
e.preventDefault();
const t = e.touches[0];
continueDrag(targetFromPoint(t.clientX, t.clientY));
}, {passive:false});
document.addEventListener('touchend', endDrag);
document.addEventListener('touchcancel', endDrag);
// keyboard accessibility: Enter/Space on a focused cell still toggles it.
// (real mouse clicks have event.detail >= 1 and are already handled by mousedown above;
// keyboard-triggered clicks have event.detail === 0, so this only fires for keyboard use.)
document.addEventListener('click', (e)=>{
if(e.detail !== 0) return;
const target = e.target.closest('[data-role]');
if(!target) return;
toggleCellValue(target.dataset.role, target.dataset.key, +target.dataset.r, +target.dataset.c);
});
// ---- preset piece picker ----
function shapeBBox(cells){
let maxR=0, maxC=0;
cells.forEach(([r,c])=>{ if(r>maxR)maxR=r; if(c>maxC)maxC=c; });
return {h:maxR+1, w:maxC+1};
}
// Cell size is fixed (CSS var --cell-px on .preset-panel), so a shape's thumbnail
// grows/shrinks with its real footprint instead of every shape sharing one box size.
function buildThumb(cells){
const {h,w} = shapeBBox(cells);
const filled = new Set(cells.map(([r,c])=>r+','+c));
const grid = document.createElement('div');
grid.className = 'thumb-grid';
grid.style.gridTemplateColumns = `repeat(${w}, var(--cell-px))`;
grid.style.gridTemplateRows = `repeat(${h}, var(--cell-px))`;
for(let r=0;r<h;r++){
for(let c=0;c<w;c++){
const cell = document.createElement('div');
cell.className = 'thumb-cell' + (filled.has(r+','+c) ? ' filled' : '');
grid.appendChild(cell);
}
}
return grid;
}
const SLOT_ORDER = ['a','b','c'];
// apply a chosen preset shape to whichever slot is currently active, then
// auto-advance to the next slot (a -> b -> c) so 3 clicks in this one panel
// fill all three pieces in order.
function applyPresetToActiveSlot(shape){
const key = activeSlot;
pieceGrids[key] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0));
shape.cells.forEach(([r,c])=>{ pieceGrids[key][r][c] = 1; });
selectedPreset[key] = shape.id;
clearSolution();
renderSlotChip(key);
renderPresetSelection();
const idx = SLOT_ORDER.indexOf(key);
if(idx < SLOT_ORDER.length - 1){
setActiveSlot(SLOT_ORDER[idx+1]);
}
}
function renderPresetSelection(){
document.querySelectorAll('#presetPanel .preset-thumb-btn').forEach(btn=>{
btn.classList.toggle('selected', btn.dataset.presetId === selectedPreset[activeSlot]);
});
}
function buildPresetPanel(){
const container = document.getElementById('presetPanel');
container.innerHTML = '';
const groups = {};
const groupOrder = [];
PRESET_SHAPES.forEach(shape=>{
if(!groups[shape.group]){ groups[shape.group]=[]; groupOrder.push(shape.group); }
groups[shape.group].push(shape);
});
groupOrder.forEach(groupName=>{
const label = document.createElement('div');
label.className = 'preset-group-label';
label.textContent = groupName;
container.appendChild(label);
const row = document.createElement('div');
row.className = 'preset-group-grid';
groups[groupName].forEach(shape=>{
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'preset-thumb-btn';
btn.dataset.presetId = shape.id;
btn.setAttribute('aria-label', `${groupName} λͺ¨μ–‘ 선택`);
btn.appendChild(buildThumb(shape.cells));
btn.addEventListener('click', ()=> applyPresetToActiveSlot(shape));
row.appendChild(btn);
});
container.appendChild(row);
});
}
buildPresetPanel();
// ---- slot chip preview (small thumbnail of whatever's currently in that slot) ----
function renderSlotChip(key){
const wrap = document.getElementById('slotThumb-'+key);
wrap.innerHTML = '';
const trimmed = trimShape(pieceGrids[key]);
if(trimmed){
wrap.appendChild(buildThumb(trimmed.offsets));
} else {
const span = document.createElement('span');
span.className = 'slot-empty';
span.textContent = pieceNameKo[key];
wrap.appendChild(span);
}
}
// ---- switch which slot (A/B/C) the shared picker + draw grid apply to ----
function setActiveSlot(key){
activeSlot = key;
document.querySelectorAll('.slot-chip').forEach(chip=>{
chip.classList.toggle('active', chip.dataset.slot===key);
});
document.querySelectorAll('#modeTabs .mode-tab').forEach(tabBtn=>{
tabBtn.classList.toggle('active', tabBtn.dataset.mode===pieceMode[key]);
});
document.getElementById('presetPanel').style.display = pieceMode[key]==='preset' ? 'block' : 'none';
const editor = document.getElementById('pieceGridEditor');
editor.style.display = pieceMode[key]==='draw' ? 'grid' : 'none';
editor.style.setProperty('--piece-color', pieceColorVar[key]);
document.getElementById('presetPanel').style.setProperty('--piece-color', pieceColorVar[key]);
renderPresetSelection();
renderDrawGrid();
}
document.querySelectorAll('.slot-chip').forEach(chip=>{
chip.addEventListener('click', ()=> setActiveSlot(chip.dataset.slot));
});
// ---- mode tabs: ν”„λ¦¬μ…‹μ—μ„œ 선택 (default) vs 직접 그리기, applies to the active slot ----
document.querySelectorAll('#modeTabs .mode-tab').forEach(tabBtn=>{
tabBtn.addEventListener('click', ()=>{
const mode = tabBtn.dataset.mode;
if(pieceMode[activeSlot] === mode) return;
pieceMode[activeSlot] = mode;
document.querySelectorAll('#modeTabs .mode-tab').forEach(b=> b.classList.toggle('active', b===tabBtn));
document.getElementById('presetPanel').style.display = mode==='preset' ? 'block' : 'none';
const editor = document.getElementById('pieceGridEditor');
editor.style.display = mode==='draw' ? 'grid' : 'none';
if(mode==='draw') renderDrawGrid();
});
});
document.getElementById('clearActiveBtn').addEventListener('click', ()=>{
const key = activeSlot;
pieceGrids[key] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0));
selectedPreset[key] = null;
clearSolution();
renderSlotChip(key);
renderPresetSelection();
renderDrawGrid();
});
document.getElementById('resetAllBtn').addEventListener('click', ()=>{
board = Array.from({length:N}, ()=>Array(N).fill(0));
pieceKeys.forEach(k=>{
pieceGrids[k] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0));
selectedPreset[k] = null;
pieceMode[k] = 'preset';
});
clearSolution();
renderBoard();
pieceKeys.forEach(renderSlotChip);
setActiveSlot('a');
statusLine.textContent='';
});
document.getElementById('solveBtn').addEventListener('click', runSolve);
// ---- rendering ----
function renderBoard(overlay){
// overlay: optional {board: 8x8 with 0/'a'/'b'/'c', willClear:Set of "r,c"}
for(let r=0;r<N;r++){
for(let c=0;c<N;c++){
const cell = boardCells[r][c];
cell.className='cell';
if(overlay){
const v = overlay.board[r][c];
if(v==='a'||v==='b'||v==='c') cell.classList.add('piece-'+v);
else if(v===1) cell.classList.add('filled');
if(overlay.willClear && overlay.willClear.has(r+','+c)) cell.classList.add('will-clear');
} else {
if(board[r][c]) cell.classList.add('filled');
}
}
}
}
// refresh the single shared draw grid to show the active slot's shape
function renderDrawGrid(){
for(let r=0;r<PIECE_GRID;r++){
for(let c=0;c<PIECE_GRID;c++){
const el = pieceCellEls[r][c];
el.dataset.key = activeSlot;
el.setAttribute('aria-label', `${pieceNameKo[activeSlot]} 쑰각 ${r+1},${c+1}`);
el.classList.toggle('on', !!pieceGrids[activeSlot][r][c]);
}
}
}
// called after a manual (draw-mode) edit changes pieceGrids[key]
function renderPiece(key){
renderDrawGrid();
renderSlotChip(key);
}
function clearSolution(){
lastSolution = null;
resultsBlock.style.display='none';
emptyNote.style.display='block';
continueBtn.classList.remove('show');
}
renderBoard();
pieceKeys.forEach(renderSlotChip);
setActiveSlot('a');
// ---- shape trimming ----
function trimShape(grid){
let minR=99,maxR=-1,minC=99,maxC=-1;
for(let r=0;r<grid.length;r++){
for(let c=0;c<grid[0].length;c++){
if(grid[r][c]){
if(r<minR)minR=r; if(r>maxR)maxR=r;
if(c<minC)minC=c; if(c>maxC)maxC=c;
}
}
}
if(maxR===-1) return null; // empty piece
const offsets=[];
for(let r=minR;r<=maxR;r++){
for(let c=minC;c<=maxC;c++){
if(grid[r][c]) offsets.push([r-minR, c-minC]);
}
}
return {offsets, h:maxR-minR+1, w:maxC-minC+1};
}
// ---- board ops ----
function canPlace(bd, offsets, r, c){
for(const [dr,dc] of offsets){
const rr=r+dr, cc=c+dc;
if(rr<0||rr>=N||cc<0||cc>=N) return false;
if(bd[rr][cc]) return false;
}
return true;
}
function cloneBoard(bd){
return bd.map(row=>row.slice());
}
function place(bd, offsets, r, c){
for(const [dr,dc] of offsets){
bd[r+dr][c+dc]=1;
}
}
function findClears(bd){
const rows=[], cols=[];
for(let r=0;r<N;r++){
let full=true;
for(let c=0;c<N;c++) if(!bd[r][c]){full=false;break;}
if(full) rows.push(r);
}
for(let c=0;c<N;c++){
let full=true;
for(let r=0;r<N;r++) if(!bd[r][c]){full=false;break;}
if(full) cols.push(c);
}
return {rows, cols};
}
function applyClear(bd, rows, cols){
for(const r of rows) for(let c=0;c<N;c++) bd[r][c]=0;
for(const c of cols) for(let r=0;r<N;r++) bd[r][c]=0;
}
function countEmpty(bd){
let n=0;
for(let r=0;r<N;r++) for(let c=0;c<N;c++) if(!bd[r][c]) n++;
return n;
}
// ---- solver (backtracking over all orderings x all positions) ----
function solve(initialBoard, pieces){
// pieces: [{key, offsets, h, w}]
let best = null; // {steps, totalLines, finalEmpty, score}
let bestPartial = null; // fallback if no full solution: {steps, placedCount, totalLines, score}
function evaluate(steps, totalLines, finalBoard){
const finalEmpty = countEmpty(finalBoard);
const score = totalLines*1000 + finalEmpty;
return {steps: steps.slice(), totalLines, finalEmpty, score};
}
function recurse(bd, remaining, steps, totalLines){
if(remaining.length===0){
const result = evaluate(steps, totalLines, bd);
if(!best || result.score > best.score) best = result;
return;
}
// track partial best in case nothing completes
{
const partialScore = totalLines*1000 + countEmpty(bd) + steps.length*2000; // prioritize placing more pieces
if(!bestPartial || partialScore > bestPartial.score){
bestPartial = {steps: steps.slice(), placedCount: steps.length, totalLines, score: partialScore};
}
}
for(let i=0;i<remaining.length;i++){
const piece = remaining[i];
const restPieces = remaining.slice(0,i).concat(remaining.slice(i+1));
const maxR = N - piece.h, maxC = N - piece.w;
for(let r=0;r<=maxR;r++){
for(let c=0;c<=maxC;c++){
if(canPlace(bd, piece.offsets, r, c)){
const nb = cloneBoard(bd);
place(nb, piece.offsets, r, c);
const beforeClear = cloneBoard(nb);
const {rows, cols} = findClears(nb);
applyClear(nb, rows, cols);
const linesThisStep = rows.length + cols.length;
steps.push({
pieceKey: piece.key,
r, c,
offsets: piece.offsets,
boardBeforeClear: beforeClear,
clearRows: rows,
clearCols: cols,
boardAfterClear: cloneBoard(nb),
linesThisStep
});
recurse(nb, restPieces, steps, totalLines + linesThisStep);
steps.pop();
}
}
}
}
}
recurse(cloneBoard(initialBoard), pieces, [], 0);
return {best, bestPartial};
}
function runSolve(){
// prepare pieces
const pieces = [];
let anyEmpty = false;
for(const k of pieceKeys){
const trimmed = trimShape(pieceGrids[k]);
if(!trimmed){ anyEmpty = true; continue; }
pieces.push({key:k, offsets:trimmed.offsets, h:trimmed.h, w:trimmed.w});
}
if(pieces.length===0){
statusLine.textContent = '쑰각을 μ΅œμ†Œ 1개 이상 κ·Έλ €μ£Όμ„Έμš”.';
return;
}
if(anyEmpty){
statusLine.textContent = 'λΉ„μ–΄ μžˆλŠ” 쑰각은 이번 νƒμƒ‰μ—μ„œ μ œμ™ΈλΌμš”. (ν•„μš”ν•˜λ©΄ μ„Έ 쑰각을 λͺ¨λ‘ μ±„μ›Œμ£Όμ„Έμš”)';
} else {
statusLine.textContent = '';
}
statusLine.textContent += (statusLine.textContent?' Β· ':'') + '탐색 쀑...';
document.getElementById('solveBtn').disabled = true;
setTimeout(()=>{
const t0 = performance.now();
const {best, bestPartial} = solve(board, pieces);
const t1 = performance.now();
document.getElementById('solveBtn').disabled = false;
if(best){
lastSolution = best;
statusLine.textContent = `탐색 μ™„λ£Œ (${Math.round(t1-t0)}ms) Β· 전체 배치 κ°€λŠ₯`;
showSolution(best, pieces.length);
} else if(bestPartial && bestPartial.steps.length>0){
lastSolution = bestPartial;
statusLine.textContent = `탐색 μ™„λ£Œ (${Math.round(t1-t0)}ms) Β· 쑰각 ${pieces.length}개λ₯Ό λͺ¨λ‘ 놓을 방법은 μ—†μ–΄μš”`;
showSolution(bestPartial, pieces.length, true);
} else {
lastSolution = null;
statusLine.textContent = `탐색 μ™„λ£Œ (${Math.round(t1-t0)}ms) Β· 놓을 수 μžˆλŠ” 쑰각이 μ—†μ–΄μš”`;
resultsBlock.style.display='none';
emptyNote.style.display='block';
emptyNote.textContent = 'μ§€κΈˆ λ³΄λ“œ μƒνƒœλ‘œλŠ” μ„ νƒν•œ 쑰각을 ν•˜λ‚˜λ„ 놓을 수 μ—†μ–΄μš”. λ³΄λ“œλ‚˜ 쑰각 λͺ¨μ–‘을 λ‹€μ‹œ ν™•μΈν•΄μ£Όμ„Έμš”.';
}
}, 20);
}
// ---- display solution ----
let activeStepIdx = 0;
function showSolution(sol, totalPiecesRequested, isPartial){
resultsBlock.style.display='block';
emptyNote.style.display='none';
stepsRow.innerHTML='';
sol.steps.forEach((step, idx)=>{
const chip = document.createElement('button');
chip.type='button';
chip.className='step-chip';
chip.textContent = `${idx+1}단계 Β· ${pieceNameKo[step.pieceKey]}`;
chip.addEventListener('click', ()=> setActiveStep(idx));
stepsRow.appendChild(chip);
});
const finalChip = document.createElement('button');
finalChip.type='button';
finalChip.className='step-chip';
finalChip.textContent = 'μ΅œμ’… κ²°κ³Ό';
finalChip.addEventListener('click', ()=> setActiveStep(sol.steps.length));
stepsRow.appendChild(finalChip);
function setActiveStep(idx){
activeStepIdx = idx;
Array.from(stepsRow.children).forEach((el,i)=> el.classList.toggle('active', i===idx));
if(idx < sol.steps.length){
const step = sol.steps[idx];
const overlayBoard = step.boardBeforeClear.map(row=>row.slice());
// mark piece cells with its color letter for rendering
for(const [dr,dc] of step.offsets){
overlayBoard[step.r+dr][step.c+dc] = step.pieceKey;
}
const willClear = new Set();
for(const r of step.clearRows) for(let c=0;c<N;c++) willClear.add(r+','+c);
for(const c of step.clearCols) for(let r=0;r<N;r++) willClear.add(r+','+c);
renderBoard({board:overlayBoard, willClear});
const lineTxt = step.linesThisStep>0
? `이 배치둜 <b class="coord">${step.linesThisStep}쀄</b>이 μ§€μ›Œμ Έμš”.`
: `이 λ°°μΉ˜λ‘œλŠ” 아직 쀄이 μ§€μ›Œμ§€μ§€ μ•Šμ•„μš”.`;
stepDesc.innerHTML = `<b>${idx+1}단계</b> β€” 쑰각 <b class="coord">${pieceNameKo[step.pieceKey]}</b>λ₯Ό `
+ `<span class="coord">${step.r+1}ν–‰ ${step.c+1}μ—΄</span>(μ™Όμͺ½ μœ„ κΈ°μ€€)에 λ†“μœΌμ„Έμš”. ${lineTxt}`;
} else {
// final state
const last = sol.steps[sol.steps.length-1];
renderBoard({board: last.boardAfterClear});
stepDesc.innerHTML = `<b>μ΅œμ’… λ³΄λ“œ</b> β€” λͺ¨λ“  λ°°μΉ˜μ™€ 쀄 μ§€μš°κΈ°κ°€ λλ‚œ λ’€μ˜ μƒνƒœμ˜ˆμš”.`;
}
}
const totalLines = sol.totalLines;
const placedCount = sol.steps.length;
scoreStrip.innerHTML = `
<span>배치된 쑰각: <b>${placedCount} / ${totalPiecesRequested}</b></span>
<span>총 μ§€μ›Œμ§„ 쀄: <b>${totalLines}</b></span>
<span>탐색 방식: <b>완전탐색 (λ°±νŠΈλž˜ν‚Ή)</b></span>
`;
if(isPartial){
const warn = document.createElement('div');
warn.style.color = 'var(--piece-a)';
warn.style.marginTop = '6px';
warn.textContent = '⚠ 3개λ₯Ό λͺ¨λ‘ λ†“μœΌλ©΄ λ³΄λ“œκ°€ λ§‰ν˜€μš”. ν‘œμ‹œλœ 것은 μ΅œλŒ€ν•œ 많이 놓을 수 μžˆλŠ” λ°°μΉ˜μ˜ˆμš”.';
scoreStrip.appendChild(warn);
}
if(sol.steps.length > 0){
continueBtn.classList.add('show');
continueBtn.onclick = ()=> continueWithResult(sol);
} else {
continueBtn.classList.remove('show');
}
setActiveStep(0);
}
// ---- take the solved final board as the new starting board, and clear piece editors
// so the next 3 pieces can be drawn and solved right away ----
function continueWithResult(sol){
const last = sol.steps[sol.steps.length - 1];
board = last.boardAfterClear.map(row => row.slice());
pieceKeys.forEach(k=>{
pieceGrids[k] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0));
selectedPreset[k] = null;
pieceMode[k] = 'preset';
});
clearSolution();
renderBoard();
pieceKeys.forEach(renderSlotChip);
setActiveSlot('a');
statusLine.textContent = 'μ΄μ–΄μ„œ λ‹€μŒ 쑰각 3개λ₯Ό μ„ νƒν•΄μ£Όμ„Έμš”.';
emptyNote.textContent = 'μ•„λž˜μ—μ„œ λ‹€μŒ μ„Έ 쑰각을 κ³ λ₯΄κ³  ν•΄λ‹΅ μ°ΎκΈ°λ₯Ό λˆ„λ₯΄λ©΄ μ—¬κΈ° κ²°κ³Όκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.';
}
})();
</script>
</body>
</html>