refactor: GT 차트 폴더 구조 정리 및 2단계 산출물 추가

현물 GT 차트를 docs/02_ground_truth/gt로 통일하고, 선물 GT는 futures/gt로 이동하며 매매 기법 JSON을 추가한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-06-09 21:21:26 +09:00
parent 281f4cfa25
commit 8c05f6bdd5
30 changed files with 407600 additions and 1976 deletions

View File

@@ -58,6 +58,7 @@
<button id="btn-zoom-in" title="확대">+ 확대</button>
<button id="btn-zoom-out" title="축소"> 축소</button>
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
</div>
<div class="toolbar-group">
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
@@ -80,13 +81,16 @@
let showMarkers = true;
let detailVisible = false;
let lastDetailStart = 0;
let lastDetailEnd = 0;
let yAxisInverted = false;
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
const ARROW_HALF = 6;
const ARROW_HEIGHT = 8;
const LABEL_OFFSET_X = 8;
const LABEL_GAP = 12;
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
const ARROW_HALF = 12;
const ARROW_HEIGHT = 16;
const LABEL_OFFSET_X = 16;
const LABEL_GAP = 24;
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
const SIM_START_COLOR = "#7b1fa2";
let axisMeasureCtx = null;
@@ -161,7 +165,7 @@
ctx.font = MARKER_FONT;
const lx = x + LABEL_OFFSET_X;
ctx.textBaseline = "middle";
ctx.lineWidth = 2;
ctx.lineWidth = 4;
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(255,255,255,0.95)";
ctx.strokeText(label, lx, labelY);
@@ -170,6 +174,10 @@
ctx.textBaseline = "alphabetic";
}
function visualUp(up) {
return yAxisInverted ? !up : up;
}
function drawTriangleOnLine(ctx, x, lineY, up, color) {
ctx.fillStyle = color;
ctx.beginPath();
@@ -188,14 +196,17 @@
function drawOneMarker(u, m, color, up, label) {
const ctx = u.ctx;
const xOff = m.x_offset_px || 0;
const x = u.valToPos(m.time, "x", true) + xOff;
const x = u.valToPos(m.time, "x", true);
const lineY = u.valToPos(markerChartPrice(m), "y", true);
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
drawTriangleOnLine(ctx, x, lineY, up, color);
const labelY = up
? lineY + ARROW_HEIGHT + LABEL_GAP
: lineY - ARROW_HEIGHT - LABEL_GAP;
const arrowUp = visualUp(up);
const stackIdx = m.stack_index || 0;
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
const anchorY = lineY + stackShift;
drawTriangleOnLine(ctx, x, anchorY, arrowUp, color);
const labelY = arrowUp
? anchorY + ARROW_HEIGHT + LABEL_GAP
: anchorY - ARROW_HEIGHT - LABEL_GAP;
drawMarkerLabel(ctx, label, x, labelY, color);
}
@@ -219,13 +230,16 @@
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
const color = SIM_START_COLOR;
drawTriangleOnLine(ctx, x, lineY, false, color);
const arrowUp = visualUp(false);
drawTriangleOnLine(ctx, x, lineY, arrowUp, color);
const label = marker.label || "거래시작";
ctx.font = MARKER_FONT;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
const labelY = lineY - ARROW_HEIGHT - 6;
ctx.lineWidth = 2;
ctx.textBaseline = arrowUp ? "top" : "bottom";
const labelY = arrowUp
? lineY + ARROW_HEIGHT + 12
: lineY - ARROW_HEIGHT - 12;
ctx.lineWidth = 4;
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(255,255,255,0.95)";
ctx.strokeText(label, x, labelY);
@@ -275,8 +289,11 @@
const opts = {
width: document.getElementById("overview").clientWidth,
height: 480,
padding: [28, 10, 28, 10],
scales: { x: { time: true } },
padding: [40, 10, 40, 10],
scales: {
x: { time: true },
y: { dir: yAxisInverted ? -1 : 1 },
},
axes: [
{ gap: 6 },
{
@@ -308,6 +325,7 @@
function buildDetailCandles(startIdx, endIdx) {
lastDetailStart = startIdx;
const end = endIdx || DATA.times.length;
lastDetailEnd = end;
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
const wrap = document.getElementById("detail");
wrap.innerHTML = "";
@@ -320,6 +338,7 @@
borderVisible: true,
minimumWidth: priceAxisW,
scaleMargins: { top: 0.08, bottom: 0.08 },
invertScale: yAxisInverted,
},
timeScale: { timeVisible: true, secondsVisible: false },
width: wrap.clientWidth,
@@ -344,28 +363,41 @@
const t1 = DATA.times[end - 1];
const markers = [];
if (showMarkers) {
const add = (list, kind, up) => {
const colorMap = {
longOpen: COLORS.longOpen,
longClose: COLORS.longClose,
shortOpen: COLORS.shortOpen,
shortClose: COLORS.shortClose,
};
(list || []).forEach(m => {
if (m.time >= t0 && m.time <= t1) markers.push({
time: m.time,
position: up ? "belowBar" : "aboveBar",
color: colorMap[kind],
shape: up ? "arrowUp" : "arrowDown",
size: 5,
text: markerLabel(kind, m),
});
});
const colorMap = {
longOpen: COLORS.longOpen,
longClose: COLORS.longClose,
shortOpen: COLORS.shortOpen,
shortClose: COLORS.shortClose,
};
add(DATA.long_open_markers, "longOpen", true);
add(DATA.long_close_markers, "longClose", false);
add(DATA.short_open_markers, "shortOpen", false);
add(DATA.short_close_markers, "shortClose", true);
const kindUp = {
longOpen: true,
longClose: false,
shortOpen: false,
shortClose: true,
};
const pending = [];
[
[DATA.long_open_markers, "longOpen"],
[DATA.long_close_markers, "longClose"],
[DATA.short_open_markers, "shortOpen"],
[DATA.short_close_markers, "shortClose"],
].forEach(([list, kind]) => {
(list || []).forEach(m => {
if (m.time >= t0 && m.time <= t1) pending.push({ m, kind });
});
});
pending.sort((a, b) => a.m.time - b.m.time || (a.m.stack_index || 0) - (b.m.stack_index || 0));
pending.forEach(({ m, kind }) => {
const arrowUp = visualUp(kindUp[kind]);
markers.push({
time: m.time,
position: arrowUp ? "belowBar" : "aboveBar",
color: colorMap[kind],
shape: arrowUp ? "arrowUp" : "arrowDown",
size: 10,
text: markerLabel(kind, m),
});
});
}
markers.sort((a, b) => a.time - b.time);
detailSeries.setMarkers(markers);
@@ -468,6 +500,15 @@
}
}
function toggleYFlip() {
yAxisInverted = !yAxisInverted;
const btn = document.getElementById("btn-flip-y");
btn.classList.toggle("active", yAxisInverted);
btn.textContent = yAxisInverted ? "↕ 복원" : "↕ 뒤집기";
if (overviewPlot) buildOverview(true);
if (detailChart) buildDetailCandles(lastDetailStart, lastDetailEnd || undefined);
}
function renderLegend() {
@@ -528,6 +569,7 @@
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
document.getElementById("btn-fit").onclick = applyFit;
document.getElementById("btn-flip-y").onclick = toggleYFlip;
document.getElementById("btn-markers").onclick = () => {
showMarkers = !showMarkers;
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";

File diff suppressed because one or more lines are too long

View File

@@ -58,6 +58,7 @@
<button id="btn-zoom-in" title="확대">+ 확대</button>
<button id="btn-zoom-out" title="축소"> 축소</button>
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
</div>
<div class="toolbar-group">
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
@@ -80,13 +81,16 @@
let showMarkers = true;
let detailVisible = false;
let lastDetailStart = 0;
let lastDetailEnd = 0;
let yAxisInverted = false;
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
const ARROW_HALF = 6;
const ARROW_HEIGHT = 8;
const LABEL_OFFSET_X = 8;
const LABEL_GAP = 12;
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
const ARROW_HALF = 12;
const ARROW_HEIGHT = 16;
const LABEL_OFFSET_X = 16;
const LABEL_GAP = 24;
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
const SIM_START_COLOR = "#7b1fa2";
let axisMeasureCtx = null;
@@ -161,7 +165,7 @@
ctx.font = MARKER_FONT;
const lx = x + LABEL_OFFSET_X;
ctx.textBaseline = "middle";
ctx.lineWidth = 2;
ctx.lineWidth = 4;
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(255,255,255,0.95)";
ctx.strokeText(label, lx, labelY);
@@ -170,6 +174,10 @@
ctx.textBaseline = "alphabetic";
}
function visualUp(up) {
return yAxisInverted ? !up : up;
}
function drawTriangleOnLine(ctx, x, lineY, up, color) {
ctx.fillStyle = color;
ctx.beginPath();
@@ -188,14 +196,17 @@
function drawOneMarker(u, m, color, up, label) {
const ctx = u.ctx;
const xOff = m.x_offset_px || 0;
const x = u.valToPos(m.time, "x", true) + xOff;
const x = u.valToPos(m.time, "x", true);
const lineY = u.valToPos(markerChartPrice(m), "y", true);
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
drawTriangleOnLine(ctx, x, lineY, up, color);
const labelY = up
? lineY + ARROW_HEIGHT + LABEL_GAP
: lineY - ARROW_HEIGHT - LABEL_GAP;
const arrowUp = visualUp(up);
const stackIdx = m.stack_index || 0;
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
const anchorY = lineY + stackShift;
drawTriangleOnLine(ctx, x, anchorY, arrowUp, color);
const labelY = arrowUp
? anchorY + ARROW_HEIGHT + LABEL_GAP
: anchorY - ARROW_HEIGHT - LABEL_GAP;
drawMarkerLabel(ctx, label, x, labelY, color);
}
@@ -219,13 +230,16 @@
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
const color = SIM_START_COLOR;
drawTriangleOnLine(ctx, x, lineY, false, color);
const arrowUp = visualUp(false);
drawTriangleOnLine(ctx, x, lineY, arrowUp, color);
const label = marker.label || "거래시작";
ctx.font = MARKER_FONT;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
const labelY = lineY - ARROW_HEIGHT - 6;
ctx.lineWidth = 2;
ctx.textBaseline = arrowUp ? "top" : "bottom";
const labelY = arrowUp
? lineY + ARROW_HEIGHT + 12
: lineY - ARROW_HEIGHT - 12;
ctx.lineWidth = 4;
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(255,255,255,0.95)";
ctx.strokeText(label, x, labelY);
@@ -275,8 +289,11 @@
const opts = {
width: document.getElementById("overview").clientWidth,
height: 480,
padding: [28, 10, 28, 10],
scales: { x: { time: true } },
padding: [40, 10, 40, 10],
scales: {
x: { time: true },
y: { dir: yAxisInverted ? -1 : 1 },
},
axes: [
{ gap: 6 },
{
@@ -308,6 +325,7 @@
function buildDetailCandles(startIdx, endIdx) {
lastDetailStart = startIdx;
const end = endIdx || DATA.times.length;
lastDetailEnd = end;
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
const wrap = document.getElementById("detail");
wrap.innerHTML = "";
@@ -320,6 +338,7 @@
borderVisible: true,
minimumWidth: priceAxisW,
scaleMargins: { top: 0.08, bottom: 0.08 },
invertScale: yAxisInverted,
},
timeScale: { timeVisible: true, secondsVisible: false },
width: wrap.clientWidth,
@@ -344,28 +363,41 @@
const t1 = DATA.times[end - 1];
const markers = [];
if (showMarkers) {
const add = (list, kind, up) => {
const colorMap = {
longOpen: COLORS.longOpen,
longClose: COLORS.longClose,
shortOpen: COLORS.shortOpen,
shortClose: COLORS.shortClose,
};
(list || []).forEach(m => {
if (m.time >= t0 && m.time <= t1) markers.push({
time: m.time,
position: up ? "belowBar" : "aboveBar",
color: colorMap[kind],
shape: up ? "arrowUp" : "arrowDown",
size: 5,
text: markerLabel(kind, m),
});
});
const colorMap = {
longOpen: COLORS.longOpen,
longClose: COLORS.longClose,
shortOpen: COLORS.shortOpen,
shortClose: COLORS.shortClose,
};
add(DATA.long_open_markers, "longOpen", true);
add(DATA.long_close_markers, "longClose", false);
add(DATA.short_open_markers, "shortOpen", false);
add(DATA.short_close_markers, "shortClose", true);
const kindUp = {
longOpen: true,
longClose: false,
shortOpen: false,
shortClose: true,
};
const pending = [];
[
[DATA.long_open_markers, "longOpen"],
[DATA.long_close_markers, "longClose"],
[DATA.short_open_markers, "shortOpen"],
[DATA.short_close_markers, "shortClose"],
].forEach(([list, kind]) => {
(list || []).forEach(m => {
if (m.time >= t0 && m.time <= t1) pending.push({ m, kind });
});
});
pending.sort((a, b) => a.m.time - b.m.time || (a.m.stack_index || 0) - (b.m.stack_index || 0));
pending.forEach(({ m, kind }) => {
const arrowUp = visualUp(kindUp[kind]);
markers.push({
time: m.time,
position: arrowUp ? "belowBar" : "aboveBar",
color: colorMap[kind],
shape: arrowUp ? "arrowUp" : "arrowDown",
size: 10,
text: markerLabel(kind, m),
});
});
}
markers.sort((a, b) => a.time - b.time);
detailSeries.setMarkers(markers);
@@ -468,6 +500,15 @@
}
}
function toggleYFlip() {
yAxisInverted = !yAxisInverted;
const btn = document.getElementById("btn-flip-y");
btn.classList.toggle("active", yAxisInverted);
btn.textContent = yAxisInverted ? "↕ 복원" : "↕ 뒤집기";
if (overviewPlot) buildOverview(true);
if (detailChart) buildDetailCandles(lastDetailStart, lastDetailEnd || undefined);
}
function renderLegend() {
@@ -528,6 +569,7 @@
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
document.getElementById("btn-fit").onclick = applyFit;
document.getElementById("btn-flip-y").onclick = toggleYFlip;
document.getElementById("btn-markers").onclick = () => {
showMarkers = !showMarkers;
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";

File diff suppressed because one or more lines are too long

View File

@@ -58,6 +58,7 @@
<button id="btn-zoom-in" title="확대">+ 확대</button>
<button id="btn-zoom-out" title="축소"> 축소</button>
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
</div>
<div class="toolbar-group">
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
@@ -80,13 +81,16 @@
let showMarkers = true;
let detailVisible = false;
let lastDetailStart = 0;
let lastDetailEnd = 0;
let yAxisInverted = false;
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
const ARROW_HALF = 6;
const ARROW_HEIGHT = 8;
const LABEL_OFFSET_X = 8;
const LABEL_GAP = 12;
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
const ARROW_HALF = 12;
const ARROW_HEIGHT = 16;
const LABEL_OFFSET_X = 16;
const LABEL_GAP = 24;
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
const SIM_START_COLOR = "#7b1fa2";
let axisMeasureCtx = null;
@@ -161,7 +165,7 @@
ctx.font = MARKER_FONT;
const lx = x + LABEL_OFFSET_X;
ctx.textBaseline = "middle";
ctx.lineWidth = 2;
ctx.lineWidth = 4;
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(255,255,255,0.95)";
ctx.strokeText(label, lx, labelY);
@@ -170,6 +174,10 @@
ctx.textBaseline = "alphabetic";
}
function visualUp(up) {
return yAxisInverted ? !up : up;
}
function drawTriangleOnLine(ctx, x, lineY, up, color) {
ctx.fillStyle = color;
ctx.beginPath();
@@ -188,14 +196,17 @@
function drawOneMarker(u, m, color, up, label) {
const ctx = u.ctx;
const xOff = m.x_offset_px || 0;
const x = u.valToPos(m.time, "x", true) + xOff;
const x = u.valToPos(m.time, "x", true);
const lineY = u.valToPos(markerChartPrice(m), "y", true);
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
drawTriangleOnLine(ctx, x, lineY, up, color);
const labelY = up
? lineY + ARROW_HEIGHT + LABEL_GAP
: lineY - ARROW_HEIGHT - LABEL_GAP;
const arrowUp = visualUp(up);
const stackIdx = m.stack_index || 0;
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
const anchorY = lineY + stackShift;
drawTriangleOnLine(ctx, x, anchorY, arrowUp, color);
const labelY = arrowUp
? anchorY + ARROW_HEIGHT + LABEL_GAP
: anchorY - ARROW_HEIGHT - LABEL_GAP;
drawMarkerLabel(ctx, label, x, labelY, color);
}
@@ -219,13 +230,16 @@
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
const color = SIM_START_COLOR;
drawTriangleOnLine(ctx, x, lineY, false, color);
const arrowUp = visualUp(false);
drawTriangleOnLine(ctx, x, lineY, arrowUp, color);
const label = marker.label || "거래시작";
ctx.font = MARKER_FONT;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
const labelY = lineY - ARROW_HEIGHT - 6;
ctx.lineWidth = 2;
ctx.textBaseline = arrowUp ? "top" : "bottom";
const labelY = arrowUp
? lineY + ARROW_HEIGHT + 12
: lineY - ARROW_HEIGHT - 12;
ctx.lineWidth = 4;
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(255,255,255,0.95)";
ctx.strokeText(label, x, labelY);
@@ -275,8 +289,11 @@
const opts = {
width: document.getElementById("overview").clientWidth,
height: 480,
padding: [28, 10, 28, 10],
scales: { x: { time: true } },
padding: [40, 10, 40, 10],
scales: {
x: { time: true },
y: { dir: yAxisInverted ? -1 : 1 },
},
axes: [
{ gap: 6 },
{
@@ -308,6 +325,7 @@
function buildDetailCandles(startIdx, endIdx) {
lastDetailStart = startIdx;
const end = endIdx || DATA.times.length;
lastDetailEnd = end;
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
const wrap = document.getElementById("detail");
wrap.innerHTML = "";
@@ -320,6 +338,7 @@
borderVisible: true,
minimumWidth: priceAxisW,
scaleMargins: { top: 0.08, bottom: 0.08 },
invertScale: yAxisInverted,
},
timeScale: { timeVisible: true, secondsVisible: false },
width: wrap.clientWidth,
@@ -344,28 +363,41 @@
const t1 = DATA.times[end - 1];
const markers = [];
if (showMarkers) {
const add = (list, kind, up) => {
const colorMap = {
longOpen: COLORS.longOpen,
longClose: COLORS.longClose,
shortOpen: COLORS.shortOpen,
shortClose: COLORS.shortClose,
};
(list || []).forEach(m => {
if (m.time >= t0 && m.time <= t1) markers.push({
time: m.time,
position: up ? "belowBar" : "aboveBar",
color: colorMap[kind],
shape: up ? "arrowUp" : "arrowDown",
size: 5,
text: markerLabel(kind, m),
});
});
const colorMap = {
longOpen: COLORS.longOpen,
longClose: COLORS.longClose,
shortOpen: COLORS.shortOpen,
shortClose: COLORS.shortClose,
};
add(DATA.long_open_markers, "longOpen", true);
add(DATA.long_close_markers, "longClose", false);
add(DATA.short_open_markers, "shortOpen", false);
add(DATA.short_close_markers, "shortClose", true);
const kindUp = {
longOpen: true,
longClose: false,
shortOpen: false,
shortClose: true,
};
const pending = [];
[
[DATA.long_open_markers, "longOpen"],
[DATA.long_close_markers, "longClose"],
[DATA.short_open_markers, "shortOpen"],
[DATA.short_close_markers, "shortClose"],
].forEach(([list, kind]) => {
(list || []).forEach(m => {
if (m.time >= t0 && m.time <= t1) pending.push({ m, kind });
});
});
pending.sort((a, b) => a.m.time - b.m.time || (a.m.stack_index || 0) - (b.m.stack_index || 0));
pending.forEach(({ m, kind }) => {
const arrowUp = visualUp(kindUp[kind]);
markers.push({
time: m.time,
position: arrowUp ? "belowBar" : "aboveBar",
color: colorMap[kind],
shape: arrowUp ? "arrowUp" : "arrowDown",
size: 10,
text: markerLabel(kind, m),
});
});
}
markers.sort((a, b) => a.time - b.time);
detailSeries.setMarkers(markers);
@@ -468,6 +500,15 @@
}
}
function toggleYFlip() {
yAxisInverted = !yAxisInverted;
const btn = document.getElementById("btn-flip-y");
btn.classList.toggle("active", yAxisInverted);
btn.textContent = yAxisInverted ? "↕ 복원" : "↕ 뒤집기";
if (overviewPlot) buildOverview(true);
if (detailChart) buildDetailCandles(lastDetailStart, lastDetailEnd || undefined);
}
function renderLegend() {
@@ -528,6 +569,7 @@
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
document.getElementById("btn-fit").onclick = applyFit;
document.getElementById("btn-flip-y").onclick = toggleYFlip;
document.getElementById("btn-markers").onclick = () => {
showMarkers = !showMarkers;
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";

File diff suppressed because one or more lines are too long