Files
dbx-main/app/static/cafe24.css
T
king 1d3dac3bec feat(cafe24): 문법 강조 편집기 + 소스 자동 정리, 목록 550px
1) 검색 입력란이 거대했던 버그

.cf24-filters 가 세로 flex 인데 .cf24-search 에 flex:0 1 320px 을 줬다. 세로
방향에서는 flex-basis 가 '높이'로 적용돼 입력란이 320px 짜리 상자가 됐다.
height:32px 로 한 줄에 고정했고, 그만큼 목록이 더 보인다.

2) 목록 550px

요청대로 왼쪽을 550px 로 넓혔다. 남은 폭(약 290px)이 상품명 몫이라 대부분 한 줄에
들어가고, 수정일도 월-일 시:분까지 보여준다. 컬럼은 번호·상품명·진열·판매·수정 5개
그대로다.

3) 문법 강조 편집기

색칠된 <pre> 위에 투명한 <textarea> 를 겹치는 방식으로 직접 구현했다. 외부
라이브러리를 쓰지 않는 이유는 자체 호스팅 원칙이다(CDN 의존 금지). 태그·속성이름·
속성값·주석·기호를 색으로 구분하고 Tab 은 들여쓰기로 쓴다.

두 층의 글자가 어긋나지 않으려면 폰트·줄높이·padding·줄바꿈 규칙이 완전히 같아야
한다. 특히 높이는 <pre> 의 scrollHeight 를 기준으로 textarea 에 지정한다 —
textarea 의 scrollHeight 를 쓰면 두 줄쯤 더 잡혀 어긋난다(실측 830 vs 792,
브라우저에서 확인 후 수정). 20만 자를 넘으면 강조를 끈다.

4) 소스 정리(포맷)와 저장 반영

store.format_html 을 추가했다. 화면 표시와 저장에 같은 함수를 쓰므로 화면에서 본
정리된 소스가 그대로 카페24에 저장된다.

렌더링을 바꾸지 않는 것을 최우선으로 했다. HTML 에서 공백은 의미가 있어서 인라인
요소 사이에 줄바꿈을 넣으면 화면에 공백이 생긴다 — 이미지 사이가 벌어지는 고전적인
사고다. 그래서 블록 요소 경계에서만 줄을 나누고 img·br·span·a 는 블록 목록에서
일부러 뺐다. <style>·<script>·<pre>·<textarea> 안쪽은 한 글자도 건드리지 않는다.
내용이 한 줄뿐인 짧은 블록은 다시 한 줄로 합친다.

멱등성을 테스트로 고정했다. 처음 구현은 <style> 안 빈 줄이 실행마다 한 줄씩 늘어나
멱등이 깨졌고(테스트가 잡음), 앞뒤 빈 줄을 버리도록 고쳤다. 편집하지 않고 다시
적용해도 저장값이 계속 달라지면 버전 이력이 의미를 잃는다.

닫는 태그가 빠진 HTML 이 흔하므로 들여쓰기 상한(12)을 뒀고, 어떤 이유로든 실패하면
원본을 그대로 돌려준다.

검증: 유닛테스트 41개 통과(신규 8개 — 블록 분리·인라인 보존(이미지 붙음)·style
원문 보존·멱등·짧은 블록 합치기·깨진 HTML 내성·속성값 미변경·정리+인코딩 왕복).
브라우저 실측: 검색란 32px, 목록 550px/편집기 750px, 오버레이 두 층 높이 일치
(편집 전 792=792, 20줄 추가 후 1175=1175), 토큰 색상 적용, 가로 스크롤 없음.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:48:23 +09:00

528 lines
13 KiB
CSS

/* 카페24 상품관리 모듈 전용 스타일.
전역(erp.css) 은 건드리지 않는다. 클래스 접두사: cf24-
색상은 erp.css 의 :root 토큰을 재사용한다. */
/* .erp-page 는 flex column + overflow-y:auto 다. 자식의 flex-shrink 기본값이
1 이라 내용이 화면보다 길면 카드가 눌려 잘리고(스크롤도 안 생긴다),
시스템 화면의 [카페24 연결] 버튼처럼 카드 아래쪽이 사라진다.
축소를 막아 넘친 만큼 .erp-page 가 스크롤하게 한다.
이 파일은 카페24 템플릿에서만 로드되므로 다른 모듈에는 영향이 없다. */
.erp-page > * {
flex-shrink: 0;
}
/* 2분할 화면은 편집 영역을 최대한 넓게 쓴다 — 이 페이지에서만 폭 제한을 푼다.
box-sizing 을 함께 바꿔야 한다: .erp-page 는 width:100% + padding:24px 라서
max-width 를 풀면 padding 이 폭에 더해져 문서 전체에 가로 스크롤이 생긴다. */
.erp-page {
max-width: none;
box-sizing: border-box;
}
.cf24-card {
margin-bottom: var(--sp-16, 16px);
}
/* ════════════════════════════════════════════════════════════
좌우 2분할: 왼쪽 목록(좁게) | 오른쪽 상세페이지 편집(넓게)
각 칸이 따로 스크롤되고, 전체 높이는 화면에 맞춘다.
════════════════════════════════════════════════════════════ */
.cf24-split {
display: grid;
grid-template-columns: 550px minmax(0, 1fr);
gap: var(--sp-12, 12px);
align-items: stretch;
}
/* .erp-page 는 flex column 이다. 위 flex-shrink:0 규칙의 예외로, 분할 영역만은
남은 높이를 모두 차지하고 안에서 스크롤되게 한다(휴가/쿠팡 모듈과 같은 방식). */
.erp-page > .cf24-split {
flex: 1 1 auto;
min-height: 0;
}
.cf24-pane {
min-height: 0;
overflow: auto;
margin-bottom: 0;
padding: var(--sp-12, 12px);
}
.cf24-pane-list {
display: flex;
flex-direction: column;
gap: var(--sp-8, 8px);
overflow: hidden; /* 표만 스크롤 — 검색/필터는 고정 */
}
.cf24-pane-editor {
display: flex;
flex-direction: column;
gap: var(--sp-8, 8px);
padding: var(--sp-16, 16px);
}
@media (max-width: 1100px) {
.cf24-split {
grid-template-columns: minmax(0, 1fr);
}
.cf24-pane {
overflow: visible;
}
.cf24-pane-list {
max-height: 45vh;
}
}
/* ── 왼쪽: 검색·필터 ── */
.cf24-filters {
display: flex;
flex-direction: column;
gap: var(--sp-8, 8px);
flex: 0 0 auto;
}
.cf24-checks {
display: flex;
gap: var(--sp-12, 12px);
font-size: var(--text-caption, 12px);
}
.cf24-checks label {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.cf24-list-count {
font-size: var(--text-caption, 12px);
color: var(--color-midtone-gray, #737373);
}
/* ── 왼쪽: 목록 표 (좁게) ── */
.cf24-list-scroll {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
.cf24-list-table {
font-size: 12px;
table-layout: fixed;
width: 100%;
}
.cf24-list-table thead th {
position: sticky;
top: 0;
z-index: 1;
padding: 6px 4px;
white-space: nowrap;
}
.cf24-list-table tbody td {
padding: 6px 4px;
vertical-align: top;
}
/* 목록 폭 550px 기준. 고정폭을 뺀 나머지(약 290px)가 상품명 몫이다.
진열/판매 칸은 제목(2글자) + 정렬 화살표가 들어갈 만큼만. */
.cf24-col-no { width: 40px; text-align: right; color: var(--color-midtone-gray, #737373); }
.cf24-col-flag { width: 40px; text-align: center; }
.cf24-col-date { width: 78px; white-space: nowrap; color: var(--color-midtone-gray, #737373); }
.cf24-col-name { word-break: break-word; }
/* 긴 상품명은 2줄까지만 — 행 높이를 고르게 유지해 목록을 훑기 쉽게 한다.
전체 이름은 title 툴팁과 오른쪽 편집기 제목에서 확인한다. */
.cf24-col-name a {
color: inherit;
text-decoration: none;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
.cf24-row {
cursor: pointer;
}
.cf24-row:hover {
background: var(--color-ghost-gray, #f2f2f2);
}
.cf24-row.is-active {
background: var(--color-rich-black, #0a0a0a);
}
.cf24-row.is-active td,
.cf24-row.is-active .cf24-col-name a {
color: #fff;
}
/* 진열/판매 표시는 좁은 칸이라 배지 대신 점으로 */
.cf24-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-subtle-ash, #e5e5e5);
}
.cf24-dot-on {
background: var(--color-success-green, #10c22b);
}
/* 제목행 클릭 정렬 */
.cf24-sortable {
cursor: pointer;
user-select: none;
}
.cf24-sortable::after {
content: "↕";
opacity: 0.35;
margin-left: 2px;
}
.cf24-sortable.is-asc::after { content: "▲"; opacity: 1; }
.cf24-sortable.is-desc::after { content: "▼"; opacity: 1; }
/* ── 오른쪽: 편집기 ── */
.cf24-editor-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--sp-12, 12px);
flex: 0 0 auto;
}
.cf24-editor-title {
margin: 0;
font-size: var(--text-heading, 18px);
letter-spacing: -0.45px;
}
.cf24-editor-sub {
margin: 4px 0 0;
font-size: var(--text-caption, 12px);
color: var(--color-midtone-gray, #737373);
}
.cf24-editor-badges {
display: flex;
gap: 4px;
flex: 0 0 auto;
}
.cf24-editor-form {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
gap: var(--sp-8, 8px);
}
.cf24-editor-bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--sp-8, 8px);
flex-wrap: wrap;
flex: 0 0 auto;
}
.cf24-editor-bar-right {
display: flex;
gap: var(--sp-8, 8px);
align-items: center;
}
.cf24-memo {
width: 260px;
padding: 6px var(--sp-10, 10px);
border: 1px solid var(--color-subtle-ash, #e5e5e5);
border-radius: var(--r-lg, 10px);
font-size: 12px;
}
.cf24-html-main {
flex: 1 1 auto;
min-height: 340px;
}
/* ════════════════════════════════════════════════════════════
문법 강조 편집기
투명한 <textarea> 를 색칠된 <pre> 위에 정확히 겹쳐 놓는 방식이다.
두 요소의 폰트·줄높이·여백·줄바꿈 규칙이 **완전히 같아야** 글자가 어긋나지
않는다. 아래 두 선택자에 붙은 속성을 바꿀 때는 반드시 함께 바꿀 것.
외부 라이브러리를 쓰지 않는다(자체 호스팅 원칙 + CDN 의존 제거).
════════════════════════════════════════════════════════════ */
.cf24-code {
position: relative;
flex: 1 1 auto;
min-height: 340px;
overflow: auto;
border: 1px solid var(--color-subtle-ash, #e5e5e5);
border-radius: var(--r-lg, 10px);
background: var(--color-canvas-white, #fff);
}
.cf24-code-hl,
.cf24-code-input {
margin: 0;
padding: var(--sp-12, 12px);
border: 0;
width: 100%;
box-sizing: border-box;
font-family: var(--font-geist-mono, ui-monospace, "Consolas", monospace);
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
overflow-wrap: break-word;
tab-size: 2;
}
.cf24-code-hl {
position: absolute;
top: 0;
left: 0;
min-height: 100%;
pointer-events: none;
color: var(--color-rich-black, #0a0a0a);
}
.cf24-code-input {
position: relative;
display: block;
background: transparent;
color: transparent;
caret-color: var(--color-rich-black, #0a0a0a);
resize: none;
overflow: hidden; /* 높이를 내용에 맞추고 스크롤은 .cf24-code 가 담당 */
}
.cf24-code-input:focus {
outline: none;
}
.cf24-code:focus-within {
border-color: var(--color-rich-black, #0a0a0a);
}
/* 선택 영역이 보이게 (글자는 투명이므로 배경만 남는다) */
.cf24-code-input::selection {
background: #b3d4fc;
}
/* 토큰 색 — GitHub 라이트 계열 */
.cf24-t-tag { color: #116329; } /* 태그 이름 */
.cf24-t-attr { color: #953800; } /* 속성 이름 */
.cf24-t-val { color: #0a3069; } /* 속성 값 */
.cf24-t-pun { color: #57606a; } /* < > / = */
.cf24-t-com { color: #6e7781; font-style: italic; } /* 주석 */
.cf24-t-doc { color: #6639ba; } /* DOCTYPE 등 선언 */
.cf24-code-hint {
font-size: var(--text-caption, 12px);
color: var(--color-midtone-gray, #737373);
}
.cf24-details {
flex: 0 0 auto;
border-top: 1px solid var(--color-subtle-ash, #e5e5e5);
padding-top: var(--sp-8, 8px);
}
.cf24-details > summary {
cursor: pointer;
font-size: var(--text-caption, 12px);
color: var(--color-midtone-gray, #737373);
}
.cf24-compact {
font-size: 12px;
}
.cf24-compact tbody td,
.cf24-compact thead th {
padding: 6px 8px;
}
.cf24-empty-pane {
margin: auto;
text-align: center;
padding: var(--sp-24, 24px);
}
.cf24-empty-pane h3 {
margin: 0 0 var(--sp-8, 8px);
}
.cf24-card-head {
display: flex;
align-items: center;
gap: var(--sp-8, 8px);
margin-bottom: var(--sp-12, 12px);
}
.cf24-card-head h3 {
margin: 0;
font-size: var(--text-heading, 18px);
letter-spacing: -0.45px;
}
.cf24-muted {
color: var(--color-midtone-gray, #737373);
font-size: var(--text-caption, 12px);
}
.cf24-err {
color: var(--color-callout-red, #c22b10);
}
.cf24-nowrap {
white-space: nowrap;
}
/* 상태 배지 */
.cf24-badge-ok {
background: var(--color-success-green, #10c22b);
color: #fff;
}
.cf24-badge-off {
background: var(--color-ghost-gray, #f2f2f2);
color: var(--color-rich-black, #0a0a0a);
}
/* 안내/오류 배너 */
.cf24-flash {
padding: var(--sp-10, 10px) var(--sp-12, 12px);
border-radius: var(--r-lg, 10px);
margin-bottom: var(--sp-12, 12px);
font-size: var(--text-body, 14px);
}
.cf24-flash-ok {
background: #eefaf0;
border: 1px solid var(--color-success-green, #10c22b);
}
.cf24-flash-err {
background: #fdefec;
border: 1px solid var(--color-callout-red, #c22b10);
}
/* 키-값 표 */
.cf24-kv th {
width: 180px;
text-align: left;
color: var(--color-midtone-gray, #737373);
font-weight: 500;
white-space: nowrap;
}
.cf24-actions {
display: flex;
gap: var(--sp-8, 8px);
align-items: center;
margin-top: var(--sp-12, 12px);
}
.cf24-warn {
color: var(--color-callout-red, #c22b10);
font-size: var(--text-caption, 12px);
font-weight: 500;
}
/* 검색 도구모음 / 페이지 이동 */
.cf24-toolbar {
display: flex;
gap: var(--sp-8, 8px);
align-items: center;
flex-wrap: wrap;
margin-bottom: var(--sp-12, 12px);
}
/* 한 줄 높이로 고정한다. 세로 flex 안에서 flex-basis 를 주면 그 값이 '높이'로
적용돼 입력란이 거대해진다(실제로 그랬다 — flex 방향을 항상 확인할 것). */
.cf24-search {
flex: 0 0 auto;
width: 100%;
box-sizing: border-box;
height: 32px;
padding: 0 var(--sp-10, 10px);
border: 1px solid var(--color-subtle-ash, #e5e5e5);
border-radius: var(--r-lg, 10px);
font-size: var(--text-body, 14px);
}
.cf24-pager {
display: flex;
gap: var(--sp-12, 12px);
align-items: center;
justify-content: center;
margin-top: var(--sp-16, 16px);
}
/* 상세설명 HTML 원문 */
.cf24-label {
display: block;
margin: var(--sp-16, 16px) 0 var(--sp-8, 8px);
font-size: var(--text-caption, 12px);
font-weight: 500;
color: var(--color-midtone-gray, #737373);
}
.cf24-html {
width: 100%;
box-sizing: border-box;
padding: var(--sp-12, 12px);
border: 1px solid var(--color-subtle-ash, #e5e5e5);
border-radius: var(--r-lg, 10px);
background: var(--color-canvas-white, #fff);
font-family: var(--font-geist-mono, ui-monospace, monospace);
font-size: 12px;
line-height: 1.6;
white-space: pre;
overflow: auto;
resize: vertical;
}
/* 읽기 전용은 배경으로 구분 — 편집 가능한 칸과 헷갈리지 않게 */
.cf24-html[readonly] {
background: var(--color-ghost-gray, #f2f2f2);
}
.cf24-note {
margin: var(--sp-12, 12px) 0 0;
padding: var(--sp-10, 10px) var(--sp-12, 12px);
border-left: 3px solid var(--color-subtle-ash, #e5e5e5);
background: var(--color-ghost-gray, #f2f2f2);
border-radius: var(--r-sm, 6px);
color: var(--color-midtone-gray, #737373);
font-size: var(--text-caption, 12px);
line-height: 1.7;
}
/* 넓은 로그 표는 카드 안에서만 가로 스크롤 */
.cf24-scroll {
overflow-x: auto;
}
.cf24-scroll code {
font-family: var(--font-geist-mono, ui-monospace, monospace);
font-size: 12px;
}
.cf24-empty {
padding: var(--sp-24, 24px);
}
.cf24-empty h3 {
margin: 0 0 var(--sp-8, 8px);
}