diff --git a/app/modules/cafe24/templates/cafe24/_editor.html b/app/modules/cafe24/templates/cafe24/_editor.html index a19b20d..5e71d05 100644 --- a/app/modules/cafe24/templates/cafe24/_editor.html +++ b/app/modules/cafe24/templates/cafe24/_editor.html @@ -69,13 +69,11 @@ 줄바꿈을 하지 않고(wrap=off) 가로로 스크롤하므로 줄 번호가 항상 맞는다. 두 요소의 폰트·여백이 다르면 글자가 어긋난다 — CSS 에서 함께 관리한다. #}
-
- -
- - -
+ +
+ +
diff --git a/app/modules/cafe24/templates/cafe24/products.html b/app/modules/cafe24/templates/cafe24/products.html index 3b5c4a4..7eaccbd 100644 --- a/app/modules/cafe24/templates/cafe24/products.html +++ b/app/modules/cafe24/templates/cafe24/products.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} {% block content %} @@ -161,24 +161,30 @@ gutter.dataset.lines = String(count); } + // 스크롤 주체는 textarea 다. 색칠 층과 줄 번호를 같은 양만큼 이동시켜 + // 글자 위치를 맞춘다(크기를 계산해 맞추는 방식은 긴 줄에서 어긋났다). + function sync() { + var x = ta.scrollLeft, y = ta.scrollTop; + hl.style.transform = "translate(" + -x + "px," + -y + "px)"; + if (gutter) gutter.style.transform = "translateY(" + -y + "px)"; + } + function repaint() { // 마지막 줄이 잘리지 않게 개행을 하나 덧붙인다(
 특성).
       if (ta.value.length > HL_LIMIT) hl.textContent = ta.value + "\n";
       else hl.innerHTML = paintHtml(ta.value) + "\n";
       renderGutter(ta.value.split("\n").length);
-      // 크기는 
 가 정하고 textarea 가 그 위를 덮는다. 다시 칠하는 사이
-      // 내부 스크롤이 생겼으면 되돌려 두 층의 정렬을 유지한다.
-      ta.scrollTop = 0;
-      ta.scrollLeft = 0;
+      sync();
     }
 
     function refresh() {
-      // 짧은 소스는 바로 칠해야 줄 번호·상자 크기가 즉시 따라온다.
+      // 짧은 소스는 바로 칠해야 줄 번호가 즉시 따라온다.
       if (ta.value.length < 50000) { repaint(); return; }
       clearTimeout(timer);
       timer = setTimeout(repaint, 80);
     }
 
+    ta.addEventListener("scroll", sync);
     ta.addEventListener("input", function () { dirty = true; refresh(); });
     // Tab 은 포커스 이동이 아니라 들여쓰기로 쓴다.
     ta.addEventListener("keydown", function (e) {
diff --git a/app/modules/cafe24/templates/cafe24/schedules.html b/app/modules/cafe24/templates/cafe24/schedules.html
index dfb77c4..e62501a 100644
--- a/app/modules/cafe24/templates/cafe24/schedules.html
+++ b/app/modules/cafe24/templates/cafe24/schedules.html
@@ -1,7 +1,7 @@
 {% extends "erp_base.html" %}
 
 {% block head_extra %}
-
+
 {% endblock %}
 
 {% block content %}
diff --git a/app/modules/cafe24/templates/cafe24/system.html b/app/modules/cafe24/templates/cafe24/system.html
index ce59fce..33a7c4d 100644
--- a/app/modules/cafe24/templates/cafe24/system.html
+++ b/app/modules/cafe24/templates/cafe24/system.html
@@ -1,7 +1,7 @@
 {% extends "erp_base.html" %}
 
 {% block head_extra %}
-
+
 {% endblock %}
 
 {% block content %}
diff --git a/app/static/cafe24.css b/app/static/cafe24.css
index 8a3d8b0..fdf3649 100644
--- a/app/static/cafe24.css
+++ b/app/static/cafe24.css
@@ -260,30 +260,30 @@
    않는다. 아래 두 선택자에 붙은 속성을 바꿀 때는 반드시 함께 바꿀 것.
    외부 라이브러리를 쓰지 않는다(자체 호스팅 원칙 + CDN 의존 제거).
    ════════════════════════════════════════════════════════════ */
+/* 스크롤은 **textarea 가** 담당하고, 색칠 층과 줄 번호가 transform 으로 따라간다.
+   폭·높이를 계산해 맞추는 방식은 flex 안에서 `width: max-content` 가 기대대로
+   동작하지 않아 실패했다 — 긴 줄이 있으면 textarea 만 내부 스크롤되고 색칠 층은
+   제자리에 남아, 커서 위치와 보이는 글자가 어긋났다(실측 706 vs 686).
+   스크롤 동기화 방식은 크기 계산이 아예 필요 없어 어긋날 여지가 없다. */
 .cf24-code {
   position: relative;
   flex: 1 1 auto;
   min-height: 340px;
-  overflow: auto; /* 가로·세로 스크롤을 여기서 담당 */
+  overflow: hidden;
   border: 1px solid var(--color-subtle-ash, #e5e5e5);
   border-radius: var(--r-lg, 10px);
   background: var(--color-canvas-white, #fff);
 }
 
-.cf24-code-rows {
-  display: flex;
-  align-items: stretch;
-  min-width: max-content; /* 가장 긴 줄에 맞춰 넓어진다(줄바꿈 없음) */
-  min-height: 100%;
-}
-
-/* 줄 번호 — 가로로 스크롤해도 왼쪽에 붙어 있게 sticky */
+/* 줄 번호 — 코드 영역 왼쪽에 고정된 별도 칸(가로 스크롤과 무관) */
 .cf24-gutter {
-  position: sticky;
+  position: absolute;
   left: 0;
-  z-index: 2;
-  flex: 0 0 auto;
-  padding: var(--sp-12, 12px) var(--sp-8, 8px);
+  top: 0;
+  bottom: 0;
+  width: 42px;
+  overflow: hidden;
+  padding: var(--sp-12, 12px) var(--sp-8, 8px) 0 0;
   text-align: right;
   background: var(--color-ghost-gray, #f6f8fa);
   border-right: 1px solid var(--color-subtle-ash, #e5e5e5);
@@ -292,9 +292,17 @@
   white-space: pre;
 }
 
+.cf24-gutter-inner {
+  will-change: transform; /* 세로 스크롤을 따라 움직인다 */
+}
+
 .cf24-code-body {
-  position: relative; /* textarea 의 기준 박스 — 크기는 
 가 정한다 */
-  flex: 1 0 auto;
+  position: absolute;
+  left: 43px; /* 줄 번호 칸 + 경계선 */
+  right: 0;
+  top: 0;
+  bottom: 0;
+  overflow: hidden;
 }
 
 /* 두 층의 글자가 정확히 겹치려면 아래 속성이 전부 같아야 한다.
@@ -318,15 +326,19 @@
   overflow-wrap: normal;
 }
 
-/* 
 가 흐름에 남아 본문 칸의 크기를 결정한다. */
+/* 색칠 층 — textarea 의 스크롤량만큼 transform 으로 이동한다. */
 .cf24-code-hl {
+  position: absolute;
+  top: 0;
+  left: 0;
   width: max-content;
   min-width: 100%;
   pointer-events: none;
   color: var(--color-rich-black, #0a0a0a);
+  will-change: transform;
 }
 
-/* textarea 는 그 위를 정확히 덮는다(크기 계산을 JS 로 하지 않아도 어긋나지 않음). */
+/* textarea 가 실제 스크롤 주체다. */
 .cf24-code-input {
   position: absolute;
   top: 0;
@@ -338,7 +350,7 @@
   color: transparent;
   caret-color: var(--color-rich-black, #0a0a0a);
   resize: none;
-  overflow: hidden;
+  overflow: auto;
 }
 
 .cf24-code-input:focus {