Commit Graph

353 Commits

Author SHA1 Message Date
king c9a16d8cc4 feat(cupang): 센터에 담긴 혼합 박스 내용물 트리 표시
③ 센터 분배에서 혼합 박스 줄 아래에 무엇이 몇 개 들어가는지 트리로
펼쳐 보여준다. 2박스 이상이면 총 개수와 함께 "(4개 × 2박스)" 를 덧붙인다.
수량을 바꾸면 트리 수치도 즉시 갱신된다. 단일 제품 박스는 트리가 없다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:42:39 +09:00
king 87b49abb71 feat(cupang): 센터 즉시 추가·대화상자 센터 버튼 선택
- ③ 센터 분배: 목록에서 센터를 고르는 즉시 추가된다. "+ 센터 추가" 버튼
  제거. 고른 뒤 선택값을 비워 같은 센터를 다시 고를 수 있게 한다.
- 담기 대화상자: 센터 select 를 추가된 센터 버튼 목록으로 바꿔 한 번에
  대상을 고른다(현재 대상은 검은 버튼 + aria-pressed).

tests/js: 센터 추가를 change 이벤트로, 대화상자 센터 버튼 표시·전환·
선택한 센터로 담기는지까지 검증. 전체 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:37:48 +09:00
king 8f71ef4d71 chore(cupang): 되돌린 뒤 CSS 캐시버전 갱신
47e09b9 되돌리기로 cupang.css 내용이 20260829e 시점으로 돌아갔지만,
브라우저에 20260829f(핸들 스타일 포함)가 캐시돼 있을 수 있어 새 버전으로
올린다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:33:24 +09:00
king 45807e545d Revert "feat(cupang): 드래그 조작감 개선 — 핸들·플레이스홀더·오토스크롤"
This reverts commit 47e09b94d7.
2026-08-29 03:33:03 +09:00
king 47e09b94d7 feat(cupang): 드래그 조작감 개선 — 핸들·플레이스홀더·오토스크롤
Pointer Events 기반으로 바꾸고(터치·펜 지원, setPointerCapture 로 창 밖
이동에도 이벤트 유실 없음) 업무툴 수준의 조작 피드백을 붙였다.

- 드래그 핸들(⠿)에서만 드래그 시작 → 카드 클릭/텍스트 선택과 충돌 없음
- 커서를 따라오는 고스트에 "잔여 N박스" 배지, 담을 수 없는 위치에서는
  빨간 테두리로 금지 표시
- 대상 센터에 "○○ — 여기에 담기" 플레이스홀더를 끼워 놓일 자리를 보여줌
- 센터 목록 위/아래 44px 안에 커서가 오면 자동 스크롤(센터가 많을 때 필수)
- pointermove 처리는 requestAnimationFrame 으로 1프레임 1회만 실행
- pointercancel / 창 blur / Esc 에서 드래그를 안전하게 취소

SortableJS 도입은 보류했다. 웨일에서 네이티브 DnD 가 시작조차 되지 않아
어차피 fallback 모드로만 동작하고, 브라우저 실측 검증 수단이 없는 상태에서
의존성만 늘어난다.

tests/js: 핸들 밖 드래그 차단·고스트 배지·플레이스홀더·금지 표시·Esc 취소
시나리오 추가(rAF 지연을 고려해 프레임 대기). 전체 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:28:12 +09:00
king e7db2a90fc fix(cupang): 센터 분배를 마우스 기반 드래그로 교체 (웨일 대응)
웨일에서 카드를 눌러 움직여도 드래그 미리보기조차 생기지 않았다 =
HTML5 네이티브 DnD 가 시작 자체를 못 한 것. 브라우저 구현에 기대는
draggable/dragstart 를 걷어내고 마우스 이벤트로 직접 구현했다.

- mousedown 후 4px 이상 이동하면 드래그 시작, 카드 복제본(고스트)이
  커서를 따라온다. 커서 아래 센터 패널은 elementFromPoint 로 찾아 강조.
- mouseup 위치의 센터에 놓으면 수량 대화상자가 열린다. ③ 카드 여백에
  놓아도 센터가 하나면 그 센터로, ③ 밖이면 안내만 띄우고 담지 않는다.
- Esc 로 드래그 취소. 카드 위에서 손을 뗀 경우에만 뒤따르는 click 을
  1회 무시해, 드래그 후에도 클릭 경로가 막히지 않는다.

tests/js: 드래그 시작 임계값·고스트 생성/제거·대상 강조·여백 드롭·
③ 밖 드롭·클릭 대체 경로까지 마우스 이벤트로 검증. 전체 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:17:08 +09:00
king ae7daabae5 fix(cupang): 센터 분배 드래그 실패 지점 제거·센터 목록 가나다 정렬
드래그가 브라우저에서 먹지 않던 문제를 추정 대신 실패 가능 지점별로
전부 막았다.

- dragstart 리스너를 카드 요소에 직접 바인딩(위임으로는 잡히지 않는
  환경 대비). 렌더할 때마다 다시 바인딩한다.
- dragenter/dragover 를 모두 preventDefault — 하나만 막으면 드롭을
  거부하는 브라우저가 있다.
- 드롭 대상 범위를 ③ 카드 전체로 확대하고, 대상 패널은
  closest → elementFromPoint → (센터가 하나면) 그 센터 순으로 찾는다.
  패널 사이 여백에 놓아도 담긴다.
- dataTransfer.setData/getData 가 막힌 환경 대비로 dragKey 를 따로 보관해
  드롭 시 사용한다. 그래도 못 읽으면 클릭으로 담으라는 안내를 띄운다.
- 드래그 중에는 ③ 영역에 점선 강조를 준다.
- 드래그 직후 발생하는 click 은 무시해 대화상자가 두 번 뜨지 않게 한다.

센터 선택 목록은 이름 가나다순 정렬(한글 음절은 코드포인트 순).

tests/js: 여백 드롭·dragenter·setData 차단 환경 시나리오 추가. 전체 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:08:09 +09:00
king ead1361790 fix(cupang): 센터 분배 드롭 동작·수량 대화상자·센터 선택
드래그가 먹지 않던 원인: 드롭 대상이 점선 상자(.cpg-dist-drop) 안쪽으로
한정돼 센터 카드 여백에 놓으면 dragover 에서 preventDefault 가 걸리지
않았다 → 드롭 대상을 센터 패널 전체로 넓혔다. 카드를 클릭해도 같은
대화상자가 열리는 대체 경로도 추가.

- ③ 은 처음에 비어 있고, 셀렉트에서 고른 센터만 패널로 추가된다(✕ 로 제거,
  제거하면 담긴 박스는 잔여로 복귀)
- 드롭/클릭 시 대화상자에서 박스 수량을 입력한다. "잔여 전부 담기" 버튼으로
  남은 수량을 한 번에 담을 수 있고, Enter 로 확정 / Esc 로 취소
- 센터에 담긴 줄과 센터 합계에 박스 수와 상품 수(박스×입수량)를 함께 표시
- 요약 카드에도 잔여 개수(잔여 박스 × 입수량)를 표시

tests/js: jsdom 으로 계산→드래그→대화상자→수량 수정→제거까지 36개 항목을
검증하는 UI 테스트 추가(README 에 실행법). 전체 통과 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:01:01 +09:00
king 2de019a203 feat(cupang): 박스 계산 3열 작업공간 + 센터 분배 드래그
① 박스 계산: 제품명·수량만 입력(결과 열 제거). 수량 칸에서 Tab 은 다음
   행 수량으로 이동하고 마지막 행이면 새 행을 추가한다. Enter 는 계산.
② 박스 요약: 제품별 박스/자투리 혼합 박스 카드가 드래그 소스. 표시되는
   수치는 센터에 배분하고 남은 "잔여" 기준으로 실시간 갱신되며, KPI 는
   총 박스 / 센터 배분 / 미배분 잔여 3개로 바꿨다.
③ 센터 분배: 활성 입고센터(cupang_centers)만 표시. 카드를 끌어다 놓으면
   해당 센터에 1박스가 담기고, 박스 수량을 직접 입력할 수 있다(잔여를
   넘으면 자동으로 잘림). ✕ 로 빼면 잔여로 되돌아간다.

혼합 박스는 내용이 서로 달라 1장 = 1박스로 취급한다. 분배 내역은 화면
안에서만 유지(DB 저장 없음)하며, 재계산하면 근거가 바뀌므로 초기화한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 02:47:48 +09:00
king fe344f9fda fix(cupang): 박스 계산 표 표기 정리·상자 라벨 고정
- 제품 드롭다운은 제품명만 표시(박스·입수량 병기 제거)
- 표 머리글 가운데 정렬 + 굵게
- 셀 옆에 나타나던 "…"" 제거: input/button 이 든 셀까지 text-overflow:
  ellipsis 가 걸려 빈 말줄임표가 그려졌다. 말줄임은 글자 셀에만 적용
- 혼합 박스 그림의 호수 추출 로직 삭제 → 항상 "쿠팡박스" 표기(글자 수에
  맞춰 라벨 크기 11px)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 02:27:01 +09:00
king 07f71d1df0 feat(cupang): 혼합 박스 그림에 박스 호수 표시
상자 SVG 안에 "2호"처럼 호수만 넣는다("쿠팡 " 접두어 제거).
채움 색 위에서도 읽히도록 흰색 외곽선(paint-order: stroke)을 준다.
aria-label 도 "2호 박스"로 바꿔 스크린리더에서 구분 가능하게 함.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 13:41:33 +09:00
king 5c54c99f7e feat(cupang): 제품명 설정에 수기 추가 폼
itemcode_db 목록에 없는 상품도 제품명·제품코드를 직접 입력해 등록할 수
있게 왼쪽 카드 상단에 폼 추가. 기존 POST /cupang/products (upsert)를
그대로 사용하며 같은 제품코드는 덮어쓴다. itemcode 검색이 비활성인
환경에서도 동작하도록 search_enabled 분기 밖에 둔다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:50:22 +09:00
king 414c47e24d fix(cupang): 계산표 제품명 열만 좁히고 나머지 열 헤더 잘림 복구
제품명 열이 남는 폭을 전부 가져가면서 다른 열이 눌려 헤더가 "박…"
처럼 잘렸다. 제품명 열에 고정 폭 180px 을 주고 수량 78 / 박스·입수량
116 / 숫자 64 / 삭제 40 으로 되돌려, 남는 폭은 지정 폭 비율대로
나눠 갖게 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:40:14 +09:00
king a23cbd7b67 fix(cupang): 계산표 가로 스크롤 제거·좌우 카드 상단선 정렬
요약 카드에 남아 있던 margin-top:16px 이 2열 그리드에서 오른쪽 카드를
아래로 밀고 있었다 → 0 으로.

가로 스크롤: fixed 레이아웃에서도 셀 내용이 밖으로 삐져나오면 래퍼가
스크롤되므로 th/td 에 overflow:hidden + text-overflow:ellipsis 를 주고
래퍼는 overflow-x:hidden 으로 고정. 열 폭도 축소(수량 68 / 박스 96 /
숫자 46 / 삭제 34). 드롭다운 라벨은 "미라네 1호 세트 (2호·8)" 처럼
"쿠팡 " 접두어를 떼어 짧게 표기.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:38:41 +09:00
king aab5157386 fix(cupang): 박스 계산표 가로 스크롤 제거·좌우 카드 높이 정렬
.erp-input 의 min-width:240px 때문에 수량 칸이 넓어져 표가 가로로
넘쳤다. 계산표를 table-layout:fixed + colgroup 고정폭으로 바꾸고
수량/선택 입력의 min-width 를 해제, 셀 여백을 8px 6px 로 축소.

박스명·입수량 열을 "박스 / 입수량" 한 열로 합치고(쿠팡 2호 · 8개),
헤더를 박스/낱개/필요 로 줄이고 삭제 버튼을 ✕ 아이콘으로 바꿔 열 수를
8 → 7 로 줄였다.

2열 그리드를 align-items:stretch 로 두고 카드를 flex column 으로 만들어
좌우 카드 높이를 맞춘다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:35:48 +09:00
king 127f396366 fix(cupang): 박스 계산 화면 2열 배치·표 잘림 방지
입력표와 요약을 좌우 2열 그리드로 배치(1280px 이하는 세로 쌓기).
입력표는 자체 스크롤(max-height + sticky thead)로 행이 늘어도 카드가
잘리지 않게 하고, 요약 그리드 최소 폭을 줄여 좁은 열에서도 카드가
한 줄에 맞게 조정.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:31:33 +09:00
king c6ee37ee48 feat(cupang): 박스 계산 요약 — 자투리 혼합 박스 시각화
하단 "박스명별 합계" 표를 요약 카드로 교체.
- KPI 3개: 총 박스 / 제품별 박스 / 혼합 박스(자투리 N개)
- 제품별 카드: 몇 박스 + 자투리 몇 개(딱 맞으면 "딱 맞음")
- 자투리 혼합 박스: 박스 1개당 카드로 어떤 상품이 몇 개 들어가는지 표시.
  상자 SVG(뚜껑 열림·내용물 차오름), 채움률 바, 카드 등장 애니메이션.
  prefers-reduced-motion 에서는 애니메이션 비활성.

혼합 계산은 서버 _pack_leftovers: 박스 용량을 1 로 두고 제품 1개 =
1/units_per_box 부피로 환산, 같은 박스명끼리만 채운다. 부동소수 오차를
피하려고 Fraction 사용. 한 제품 자투리가 두 박스로 나뉘는 것은 허용해
박스 수가 최소가 되게 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:28:34 +09:00
king c72c0710ad feat(cupang): 박스 계산 화면 추가
달력 상단 "박스 계산" 버튼 → /cupang/box-calc. 제품명+수량을 여러 행
입력하면 제품별 박스 수·남은 낱개·총 필요 박스와 박스명별 합계를 계산.
수량/제품을 고치고 "계산 / 재계산" 으로 다시 계산할 수 있다(수량 칸에서
Enter 도 동일). 저장하지 않는 계산 전용 화면.

계산은 POST /cupang/api/box-calc 에서 store.compute_boxes 로 수행 —
클라이언트 계산을 신뢰하지 않는다. 입수량 규칙이 없는 제품은 "미설정"
으로 표시하고 합계에서 제외.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:18:52 +09:00
king 7eb63ff664 feat(cupang): 등록 제품명 표 정렬 + 박스 입수량 시드 SQL
제품명/제품코드 헤더 클릭으로 오름·내림차순 토글. Intl.Collator(ko,
numeric) 사용해 "미라네 2호"가 "미라네 10호"보다 앞에 오도록 정렬.
검색 비활성 상태에서도 동작하게 스크립트를 search_enabled 밖에 배치.

박스 입수량 14건(미라네 1~7호, 황토 김치통 1~7호) 시드 SQL 추가.
피킹비 전용 컬럼이 없어 memo 에 기록.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:13:22 +09:00
king 55d1c1d6f3 fix(cafe24): 단축키 안내에 줄 복사 추가·편집창 툴팁 제거
- 안내 문구를 "주석토글[Ctrl+/] · 줄 복사[Alt+Shift+↑↓] · 줄 이동[Alt+↑↓]
  · 줄 삭제[Shift+Del]" 로 맞춘다(줄 복사가 빠져 있었다).
- textarea 의 title 툴팁을 뺀다. 편집 중 마우스 옆에 떠서 소스를 가렸다.
  같은 내용이 위 안내줄에 항상 보이므로 잃는 정보가 없다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:01:44 +09:00
king 739bb80b04 fix(cafe24): 편집기 상단 단축키 안내·가격 표기·선택행 hover
- 편집기 바 왼쪽 문구를 단축키 안내만 남긴다. 글자 수·PC/모바일 공통은
  화면에서 쓸 일이 없어 뺐다. 옅은 파랑(#79a6dd)으로 본문과 구분한다.
- 가격을 '6900.00' 대신 '6,900원' 으로 보여준다. 원 단위 쇼핑몰이라 소수점
  아래는 언제나 .00 이므로 버린다. 숫자로 못 읽으면 받은 값을 그대로 둔다.
- 선택된 행에 마우스를 올려도 배경이 변하지 않게 한다. erp-shell.css 의
  `.erp-table tbody tr:hover` 가 특이도가 더 높아 검은 배경을 덮어쓰고
  있었다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:50:57 +09:00
king 8cfe963526 feat(erp): 공용 확인/경고 창으로 브라우저 기본 confirm 대체
브라우저 기본 confirm()/alert() 은 제목에 도메인이 찍히고("dbx.no1king.
freeddns.org 내용:") 위치·정렬·아이콘을 바꿀 수 없다. 같은 역할의 창을 직접
그려 화면 정중앙에 띄운다.

- app/static/erp-dialog.{js,css} 신규. 제목은 "No.1 King ERP 프로그램",
  내용은 가운데 정렬, 왼쪽에 경고 삼각형 아이콘(인라인 SVG - 외부 아이콘
  라이브러리를 쓰지 않는다).
- erpConfirm()/erpAlert() 은 Promise 를 돌려준다. 기본 confirm() 과 달리
  동기 반환이 불가능하므로 호출부를 then() 으로 바꿨다. 폼 제출은 일단
  막고 확인 후 form.submit() 으로 다시 보낸다(required 검사는 그 전에 이미
  통과한다).
- HTML 만으로 쓰려면 form 에 data-erp-confirm 을 달면 된다. 카페24 시스템·
  예약 화면의 인라인 onsubmit=confirm(...) 을 이 방식으로 옮겼다.
- 메시지는 textContent 로 넣어 서버·사용자 문자열이 HTML 로 해석되지 않는다.
- Esc 취소 / Enter 확인 / Tab 은 창 안에서만 돌고, 닫을 때 원래 포커스로
  돌아간다. 바깥을 누르면 취소(안전한 쪽).
- erp_base.html 에서 전역 로드. defer 를 쓰지 않아 각 화면의 인라인
  스크립트가 곧바로 쓸 수 있다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:33:42 +09:00
king a58fa7b83b feat(cafe24): HTML 편집기 Shift+Delete 줄 삭제
커서가 있는 줄(선택이 있으면 그 줄들)을 통째로 지운다. 윈도우 기본 동작인
"잘라내기"를 대신한다.

- 줄바꿈까지 함께 지워 빈 줄이 남지 않게 한다. 마지막 줄이면 대신 앞의
  줄바꿈을 지운다(문서 끝에 빈 줄이 생기지 않게).
- 커서는 지운 자리로 올라온 줄의 같은 칸에 둔다(칸이 모자라면 줄 끝).
- 빈 문자열은 insertText 가 브라우저에 따라 무시되므로 applyEdit 이
  execCommand("delete") 로 갈라진다. Ctrl+Z 는 그대로 동작한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:22:17 +09:00
king 024653dc8e feat(cafe24): HTML 편집기 줄 이동·복사 단축키
Alt+↑/↓ 로 커서가 있는 줄(선택이 있으면 그 줄들)을 위/아래로 옮기고,
Alt+Shift+↑/↓ 로 같은 줄을 하나 더 만든다. 상세페이지는 <img> 한 줄이 곧
한 구획이라 순서 바꾸기·반복이 잦다.

- 이동은 윗줄/아랫줄과 통째로 자리를 바꾸는 방식이라 줄 수가 변하지 않는다.
  첫 줄 위·마지막 줄 아래에서는 아무 것도 하지 않는다.
- 복사는 위로 하면 커서가 원래 자리(위쪽 사본)에, 아래로 하면 새로 생긴
  아래쪽 사본으로 간다.
- 주석 토글과 같은 줄 범위 규칙을 쓰도록 lineRange()/applyEdit() 로 묶었다.
  선택이 개행에서 끝나면 다음 줄은 대상이 아니다.
- execCommand("insertText") 라 Ctrl+Z 로 되돌아간다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:17:18 +09:00
king 233c8db631 feat(cafe24): HTML 편집기 Ctrl+/ 주석 토글
- 선택이 있으면 선택이 걸친 줄 전체가 대상. 반쯤 걸친 줄이 잘려 태그가
  깨지지 않게 하기 위함. 선택이 없으면 커서가 있는 줄 하나.
- 대상 안에 주석 기호가 하나라도 있으면 제거, 없으면 블록 전체를 <!-- -->
  로 감싼다. HTML 주석은 중첩이 안 되므로 이미 주석인 부분을 또 감싸지
  않는다.
- 선택이 개행에서 끝나면 다음 줄은 제외(줄 끝까지 드래그했을 때 아래 줄이
  딸려오지 않게).
- execCommand("insertText") 로 넣어 Ctrl+Z 이력에 남긴다. 미지원 브라우저는
  value 를 직접 바꾼다.
- 되돌린 뒤 커서/선택 위치를 복원한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:10:50 +09:00
king 3537e058b0 feat(cafe24): 편집기에서 상품명 수정
제목 옆 연필 버튼 → 입력칸 → 저장. 이름만 바꾸려고 카페24 관리자에 들어갈
필요가 없어진다.

- POST /cafe24/products/{no}/name (JSON) 추가. 쓰기 전에 카페24 현재값을 읽어
  이전 이름을 감사로그(rename_product)에 남긴다 - 되돌릴 revision 이 없으므로
  로그가 유일한 복구 단서다. 값이 같으면 호출하지 않는다.
- build_update_payload/update_product 에 product_name 추가(부분 수정이라
  상세설명·진열·판매는 그대로).
- 빈 값과 250자 초과는 서버에서 400. 화면도 maxlength 로 막는다.
- 평소에는 읽기 전용 제목이고 연필을 눌러야 입력칸이 된다 - 클릭 한 번으로
  실수로 고쳐지지 않게. Enter 저장, Esc 취소.
- 화면은 요청값이 아니라 카페24가 확인해 준 이름으로 다시 그리고, 왼쪽 목록의
  이름·정렬키(data-name)도 함께 갱신한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:35:37 +09:00
king 5127600a3d feat(cafe24): 진열/판매 배지 클릭으로 상태 토글
편집기 오른쪽 위 배지를 눌러 진열·판매를 바로 바꾼다. 상태만 바꾸려고
카페24 관리자에 들어갈 필요가 없어진다.

- POST /cafe24/products/{no}/status (JSON) 추가. 상세설명은 건드리지 않아
  BACKUP revision 을 만들지 않는다 - 되돌릴 HTML 이 없고 다시 눌러 복구된다.
- 요청값을 낙관적으로 반영하지 않고 쓰기 후 카페24가 돌려준 실제 상태로
  화면을 다시 그린다. 실패해도 화면과 카페24가 어긋나지 않는다.
- 왼쪽 목록의 점과 정렬용 data-display/data-selling 도 함께 갱신(목록을
  다시 받지 않으므로).
- 카페24 조회 실패 시에는 현재 상태를 믿을 수 없어 배지를 버튼으로 만들지
  않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:38:33 +09:00
king 67c28ae1c7 fix(cafe24): 예약 날짜/시간 — 칸 전체 클릭 반응 + 요일 표시 + 날짜 폭 확대
1) 칸 전체를 클릭해야 팝업이 뜨지 않던 문제

네이티브 date/time input 은 자신의 달력 아이콘(우측 끝 작은 영역)을 클릭해야만
팝업이 뜬다. 투명한 네이티브 input 을 칸 전체 크기로 늘려놔도 그 작은 아이콘
영역만 반응하는 건 그대로였다 — 실제 "오른쪽 부분을 선택해야 나온다"로 보고된
문제 그대로다.

클릭 핸들러에서 showPicker() 를 직접 호출해 칸 어디를 클릭해도 팝업이 뜨게
했다. 미지원 브라우저에서는 조용히 무시되고 기존처럼 포커스만 이동한다.

실제 클릭(진짜 사용자 제스처)으로 검증했다 — showPicker() 는 클릭당 한 번만
유효한 제스처를 소비하므로, 같은 클릭에서 두 번째 호출을 시도하면
"NotAllowedError: requires a user gesture" 가 나는 것으로 첫 호출이 성공했음을
간접 확인했다. 이 미리보기 도구는 네이티브 OS 팝업을 스크린샷에 담지 못해
시각적으로는 확인할 수 없었다(도구 제약이지 코드 문제 아님).

2) 요일 표시

"2026년 08월 20일" 뒤에 "(목)" 을 붙인다. new Date("YYYY-MM-DD") 로 문자열을
그대로 파싱하면 UTC 로 해석돼 하루 밀릴 수 있어, 연/월/일을 분해해
new Date(y, mo-1, d) 로 로컬 시간대 기준으로 만든 뒤 getDay() 를 쓴다.

3) 날짜 칸 폭 확대

요일이 붙어 텍스트가 길어진 만큼 날짜 칸을 시간 칸보다 넓게 잡았다
(flex: 1.7 대 1). 라벨 전체 폭도 240px→300px 로 늘렸다.

검증: 유닛테스트 61개 통과(백엔드 무변경). 브라우저 실측 — "2026년 08월 20일 (목)"
표시 확인, 날짜/시간 칸 모두 잘림 없음(날짜 185px·시간 109px), showPicker 지원
확인, 가로 스크롤 없음.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:47:03 +09:00
king 86914f0bc6 feat(cafe24): 예약 시각을 한글 형식("2026년 08월 20일" / "오후 07시 30분")으로 표시
datetime-local 을 date+time 으로 나눈 뒤에도 여전히 브라우저가 강제하는 표시
형식("2026. 08. 20.", "오후 07:30")이 남아 있었다. 네이티브 date/time input 은
표시 형식을 CSS 로 바꿀 수 없다(브라우저·로캘가 강제) — 지원되는 방법이 없다.

코드 편집기의 오버레이 방식(.cf24-code-input 투명 textarea 를 .cf24-code-hl 색칠된
글자 위에 겹치는 것)을 그대로 적용했다. 네이티브 date/time input 을 opacity:0 으로
완전히 투명하게 두고, 우리가 형식화한 텍스트(.cf24-dt-display) 바로 위에 포개 놓는다.
클릭·키보드·달력 팝업은 네이티브가 그대로 처리하고(elementFromPoint 로 클릭이
네이티브에 도달하는지 확인함), 화면에 보이는 글자만 JS(paintDate/paintTime)가
input/change 마다 다시 그린다. input.value 자체는 형식과 무관하게 항상
YYYY-MM-DD / HH:MM 이라 제출 시 합치는 로직(store.parse_schedule_at 이 받는 형식)은
전혀 바뀌지 않았다.

정오/자정 12시간제 변환(0시→오전 12시, 12시→오후 12시)을 포함해 처리한다.

폭도 줄여달라는 요청에 맞춰, 실측으로 잘리지 않는 최소값(글자크기 12px·필드폭
117px)을 찾아 적용했다 — 이전 174px 대비 크게 줄었다.

검증(브라우저 실측): 표시 텍스트가 정확한 한글 형식으로 나옴, 정오/자정 처리 정확,
잘림 없음, 클릭이 실제 네이티브 input 에 도달, 제출 시 hidden 필드가
"2026-08-20T19:30" 으로 정확히 조합됨, 가로 스크롤 없음. 백엔드 무변경이라
유닛테스트 61개 그대로 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:37:52 +09:00
king 0862c5572b fix(cafe24): 예약 시각을 datetime-local 대신 날짜+시간 별도 입력으로
datetime-local 단일 입력은 한국어 로캘에서 "2026. 08. 14. 오후 07:00" 형태로
렌더링되는데 브라우저·로캘마다 표시 폭이 달라 잘리는 사고가 있었다(실제 발생,
직전 커밋에서 칸 폭을 넓혀 임시로 막았지만 근본 원인은 남아 있었다).

type=date + type=time 두 개의 별도 input 으로 바꿨다. 각각 폭이 고정이라 잘릴
여지가 없다. 제출 직전 JS 가 두 값을 "YYYY-MM-DD" + "T" + "HH:MM" 로 합쳐 hidden
scheduled_at 에 넣는다 — 이 형식은 예전 datetime-local 이 만들던 값과 동일해서
서버(store.parse_schedule_at)는 손대지 않았다. date/time 두 입력에 required 를
둬서, 브라우저의 기본 폼 검증이 빈 값 제출을 막는다(hidden 필드는 타입 특성상
required 검증 대상이 아니라 시각 입력 쪽에 걸었다).

검증: 유닛테스트 61개 통과(백엔드 무변경이라 영향 없음). 브라우저 실측 — 두 입력
각 170px 로 내부 잘림 없음(scrollWidth==clientWidth), 가로 스크롤 없음, 실제 제출
이벤트를 발생시켜 2026-08-20 19:30 입력이 hidden 필드에 "2026-08-20T19:30" 으로
정확히 합쳐지는 것을 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:24:42 +09:00
king 8bdf8695f9 fix(cafe24): 예약 시각 입력란이 잘려서 시간이 안 보이던 문제
datetime-local 입력란이 한국어 로캘로 "2026. 08. 14. 오후 07:00" 형태로 렌더링되는데,
예약 폼 grid 가 auto-fit minmax(160px, 1fr) 라 칸이 170px 밖에 안 돼 시간 부분이
잘렸다.

예약 시각 칸에만 전용 클래스(cf24-schedule-datetime)를 붙여 grid-column: span 2 로
두 칸을 차지하게 했다. 진열·판매 select 는 원래 폭으로도 충분해 그대로 뒀다.

검증(브라우저 실측): 예약 시각 입력란 폭 348px, scrollWidth==clientWidth(내부 잘림
없음), 진열/판매가 나란히 이어지고 메모가 다음 줄 전체폭을 차지, 가로 스크롤 없음.
유닛테스트 61개 통과(레이아웃 변경만이라 로직 테스트는 영향 없음).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:15:16 +09:00
king 8d93d09dac fix(cafe24): 예약·버전이력을 펼쳐도 편집기 크기 유지 — 오른쪽 칸만 스크롤
「예약 적용」이나 「버전 이력」을 펼치면 코드 칸이 눌려 작아졌다. .cf24-code 와
.cf24-editor-form 이 flex: 1 1 auto 라 아래 내용이 늘어나면 그만큼 편집기가 줄어드는
구조였다 — 펼칠 때마다 보고 있던 위치를 잃는다.

둘을 flex: 0 0 auto 로 바꾸고 코드 칸에 height: 56vh 를 줬다. 무엇을 펼쳐도 높이가
그대로이고, 늘어난 내용은 아래로 밀리며 오른쪽 칸(.cf24-pane-editor)이 스크롤된다.
vh 를 쓴 것은 창 크기에 적응하기 위함이다(고정 px 는 작은 화면에서 넘친다).

56vh 는 실측으로 고른 값이다. 62vh 는 접힌 상태에서도 51px 넘쳐 요약줄이 잘렸고,
56vh 는 넘침 0 이라 두 요약줄이 스크롤 없이 보인다. calc(100% - 260px) 은 342px 까지
줄어 편집 영역이 너무 좁았다.

검증(브라우저 실측): 접힘/펼침 모두 코드 칸 506px 동일, 접힘 시 넘침 0, 펼치면 오른쪽
칸이 498px 스크롤, 오른쪽 칸을 200px 스크롤해도 색칠 층·줄 번호·textarea 정렬 유지,
페이지 가로 스크롤 없음, 편집 영역 25줄. 유닛테스트 61개 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:10:17 +09:00
king 469de15998 feat(cafe24): 편집기에 상품 다이렉트 주소 + 클립보드 복사
상세 화면 상단에 고객이 보는 상세페이지 주소를 보여주고 「주소 복사」·「쇼핑몰에서
열기」를 붙였다.

    https://miras.co.kr/product/detail.html?product_no=119

도메인은 하드코딩하지 않고 CAFE24_SHOP_URL 로 받는다. 커스텀 도메인은 mall_id 로
알 수 없기 때문이다. 미설정 시 카페24 기본 도메인(https://<mall_id>.cafe24.com)으로
대체해 환경변수가 없어도 항상 유효한 주소가 나온다. 스킴 누락·끝 슬래시도 정규화한다.

주소 칸은 readonly <input> 이라 기존 복사 버튼(data-copy)이 값을 그대로 읽어간다 —
클립보드 로직을 새로 만들지 않았다. 클립보드 API 가 막힌 환경에서는 입력칸 선택으로
대체되고, 칸을 클릭하면 전체 선택된다.

CAFE24_SHOP_URL 은 .env.example 과 문서에 설명을 함께 넣었다(신규 환경변수 규칙).

검증: 유닛테스트 61개 통과(신규 2개 — 스킴/슬래시 유무 3가지 입력에서 같은 주소,
미설정 시 카페24 도메인 대체). 렌더 확인 — 주소 표시·복사 버튼·새 창 열기(noopener),
주소를 만들 수 없으면 줄 자체를 숨김.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:46:04 +09:00
king 8ec38db6d1 revert(cafe24): 일괄수정 기능 제거
사용자 결정에 따라 화면·라우트·로직·문서를 모두 제거했다.

제거 대상:
  routes_bulk.py            (파일 삭제)
  templates/cafe24/bulk.html (파일 삭제)
  _nav.html                 「일괄수정」 탭
  router.py                 import·include·설명
  store.py                  find_style_blocks / replace_first_style_block / 정규식
  tests                     <style> 블록 교체 테스트 6건
  docs                      2-3 절, 경로 표 3줄, 구조도, Phase 7 표기

DB 는 손대지 않았다. 일괄수정은 전용 스키마를 만들지 않았고, 기존 테이블에 남은
기록은 **실제로 상품에 적용된 변경의 이력**이다. 특히 그때 만들어진 BACKUP revision
은 일괄 적용 이전 내용을 되찾을 유일한 수단이라 지우면 복구가 불가능해진다.
감사로그(action='bulk_style')도 누가 언제 무엇을 바꿨는지 남기는 기록이라 보존한다.
정말 지워야 한다면 별도로 요청받아 진행한다.

부수 수정: 예약 시각 검증 테스트가 실행 시각의 초에 따라 실패할 수 있었다.
`datetime-local` 은 초를 버리므로 지금 이 분(分)을 고르면 최대 59초 과거가 되는데,
테스트가 now 의 초를 고정하지 않아 경계에서 흔들렸다. now 를 고정해 결정적으로 만들었다
(3회 반복 실행으로 확인).

검증: 유닛테스트 59개 통과, 라우트 13개, 템플릿 컴파일 확인, 모듈에 bulk 참조 0건.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:14:40 +09:00
king 6dae0e45c9 feat(cafe24): 예약관리 — 지정 시각에 상세페이지·진열/판매 자동 적용 (Phase 5)
되돌리기(자동 복원)는 요청대로 만들지 않았다. 예약은 "그 시각에 이 내용을 적용"
하나뿐이며, 한 예약에서 상세페이지 HTML·진열·판매를 각각 고를 수 있다. 셋 다
"변경 없음"인 예약은 DB CHECK 로 막는다.

등록은 편집기 아래 「예약 적용」에서 한다. HTML 을 적용하는 예약이면 그 시점의 편집기
내용을 DRAFT revision 으로 저장해 고정한다 — 이후 편집기를 더 고쳐도 예약된 내용이
바뀌지 않아야 한다. 단건 적용과 같은 다듬기(URL 인코딩 → 소스 정리)를 거치므로 화면에서
본 값이 그대로 저장된다. 예약 폼은 적용 폼과 형제로 두고(폼 중첩 불가) 편집기 내용을
JS 가 hidden 에 복사한다.

실행은 web 이 아니라 worker 다(app/modules/cafe24/worker.py, compose 서비스
dbx-cafe24-worker, --loop 60). 웹 요청 안에서 기다리면 프록시 타임아웃·재기동에
무너지고, 브라우저를 닫으면 실행되지 않는다.

worker 는 claim_due_schedule 로 한 건씩 FOR UPDATE SKIP LOCKED 로 잠그고 PROCESSING
으로 바꾼 뒤 잠금을 푼다. worker 가 둘 떠도 같은 예약을 두 번 적용하지 않고, 긴 API
호출 동안 DB 잠금을 쥐지 않는다. 적용 순서는 화면 편집과 같다(현재값 재조회 → BACKUP
→ PUT → 감사로그). HTML 없이 진열/판매만 바꾸는 예약은 상세설명을 읽지도 백업하지도
않는다. 실패는 1분→5분→15분 재시도 후 FAILED 확정이며, 한 건의 오류로 worker 가
죽지 않는다.

진열/판매를 한 번의 PUT 으로 함께 보내려고 products.update_product 를 추가했다
(update_descriptions 는 이 함수로 위임). None 인 필드는 payload 에서 빼므로 "건드리지
않음"이 그대로 표현된다.

DB: scripts/sql/cafe24_db_002_schedule_flags.sql (멱등) — set_display/set_selling
BOOLEAN NULL 추가 + 아무것도 하지 않는 예약 금지 제약. 되돌리기용 end_* 컬럼은 쓰지
않지만 삭제하지 않는다(파괴적).

시각은 KST 로 해석한다(datetime-local 은 타임존이 없다). 과거는 거부하되 폼을 채우는
동안 시간이 흐른 경우를 위해 1분 여유를 뒀다.

검증: 유닛테스트 66개 통과(신규 15개 — 3-상태 파싱, KST 해석·과거 거부·1분 여유,
요약 문구, payload 의 T/F 와 None 생략, 바꿀 것 없으면 미호출, worker 의 성공 경로
(백업+PC/모바일 동시+진열만 전송)·상태만 변경 시 백업 생략·재시도 후 최종 실패·
버전 누락 시 크래시 대신 실패·처리할 것 없을 때 종료). 예약 목록/편집기 예약 폼 렌더 확인.
라우트 16개. 실제 예약 실행은 서버 배포 후 확인 필요.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:06:48 +09:00
king b2a70ad0ab fix(cafe24): 블록 선택 시 글자가 안 보이던 문제 — 선택 배경 반투명
편집기는 투명한 textarea 를 색칠된 <pre> 위에 겹쳐 놓은 구조다. textarea 의 글자는
투명이고 실제 글자는 아래 <pre> 가 그린다. 선택 배경은 위층인 textarea 가 칠하므로
불투명(#b3d4fc)이면 아래 글자를 완전히 덮는다 — 드래그로 블록을 잡으면 파란 칸만
남고 글자가 사라졌다.

::selection 배경을 rgba(51,122,226,0.28) 로 바꿔 색칠된 글자가 비쳐 보이게 했다.
Firefox 용 ::-moz-selection 도 함께 넣었다.

검증: 브라우저에서 규칙 적용 확인(rgba 0.28), 선택 상태에서도 색칠 층이 글자를 그대로
그리고 있음 확인. 유닛테스트 51개 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:53:01 +09:00
king 8e3e2d1f76 style(cafe24): 편집기 상단 설명 문단 제거
편집기 위 안내 문단(PC/모바일 공통·소스 정리·한글 경로 설명)을 삭제했다. 매번 읽을
내용이 아니고 편집 영역을 밀어내고 있었다. 동작 자체를 설명하는 내용은
docs/CAFE24_MODULE.md 2-2 절에 그대로 남아 있다.

.cf24-note 스타일은 일괄수정 화면에서 계속 쓰므로 유지한다 — 거기서는 지워질 CSS 를
경고하는 내용이라 읽어야 한다.

검증: 유닛테스트 51개 통과. 렌더 확인 — 설명 문단만 사라지고 라벨·메모·복사·
다시 읽기·적용·버전 이력·코드 편집기는 그대로.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:49:54 +09:00
king 39dc28d664 feat(cafe24): 편집기에서 PC/모바일 구분 제거 — 항상 같은 내용으로 반영
요청대로 단순화했다. 모바일 상세설명 보기 영역과 「모바일도 함께」 체크박스를 없애고,
적용 시 description 과 mobile_description 에 **항상 같은 HTML** 을 쓴다.
separated_mobile_description 값과 무관하므로 한쪽만 바뀌어 어긋나는 사고가 없어진다.

다만 분리 사용 상품의 모바일 내용이 PC 와 달랐다면 그 내용을 덮어쓰게 된다. 그래서
쓰기 전에 **모바일 내용도 BACKUP revision 으로 따로 남긴다** — 백업이 없으면 되찾을
방법이 없다. 기존 BACKUP 은 PC 값만 담고 있었다.

부수 정리: 편집기 컨텍스트에서 html_mobile 제거, mobile_html 의 None 분기 제거,
감사로그 표기를 "PC·모바일 동시 반영"으로 고정, 라벨을 "상세설명 HTML · PC/모바일
공통"으로 변경.

검증: 유닛테스트 51개 통과. 분리+내용상이 상품으로 편집기를 렌더해 모바일 보기 영역·
체크박스가 사라지고 공통 문구·버전 이력·다시 읽기가 남은 것을 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:46:32 +09:00
king 34476ba611 feat(cafe24): 진열중·판매중 기본 체크 + 분리 상품 모바일 동시 반영 선택
1) 필터 기본값

진열중·판매중을 기본 체크로 바꿨다. 체크박스는 해제 상태면 아무 값도 보내지 않아
기본값이 체크면 "사용자가 일부러 해제함"을 구분할 수 없다. 그래서 폼에 표식(f=1)을
넣어, 표식이 없으면 첫 방문(기본값), 있으면 실제 체크 상태를 따르게 했다. 표식은
목록 링크·적용 후 리다이렉트에도 이어 붙어 해제 상태가 유지된다.

2) 분리 상품의 모바일 반영

"소스를 수정하면 PC와 모바일이 같이 수정되는 것 아닌가" 라는 지적대로, PC/모바일
분리 사용 상품은 지금까지 PC 만 바뀌고 있었다(미분리 상품은 원래 함께 반영).

편집기에 「모바일도 함께」 체크박스를 추가했다. 현재 두 내용이 같으면 기본 체크라
그대로 적용하면 함께 반영되고, 내용이 다르면 기본 해제하고 경고를 띄운다 — 일부러
다르게 만든 모바일 페이지를 조용히 덮어쓰는 것이 더 큰 사고이기 때문이다. 미분리
상품은 종전처럼 항상 함께 반영하며 체크박스를 보여주지 않는다.

검증: 유닛테스트 51개 통과. 필터 판정을 5가지 경우로 확인(첫 방문·둘 다 체크·하나만·
둘 다 해제·검색 링크) — 둘 다 해제가 f=1 표식으로 유지됨. 편집기 렌더를 3가지
상태로 확인(분리+동일=기본체크, 분리+상이=기본해제+경고, 미분리=체크박스 없음).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:41:07 +09:00
king 9b9bafdf95 fix(cafe24): 카페24에서 바뀐 소스가 화면에 반영되지 않던 문제 — 캐시 금지 + 다시 읽기
카페24 관리자에서 소스를 고쳤는데 우리 화면은 예전 것을 보여주는 문제.

편집기 조각(GET /products/{no}/pane)과 목록 화면 응답에 캐시 헤더가 없었다.
브라우저가 이전 응답을 재사용하면 카페24의 현재값이 아닌 예전 소스가 그려진다.
그 상태에서 편집·적용하면 카페24 관리자에서 한 수정을 덮어쓰게 되므로, 단순한
표시 문제가 아니라 데이터 손실로 이어질 수 있다.

응답에 Cache-Control: no-store 를 붙이고 조각을 가져가는 fetch 에도
cache: "no-store" 를 걸었다. 일괄수정 검사 조회도 같다.

편집기에 [다시 읽기] 버튼을 추가했다. 카페24 관리자에서 방금 고친 경우 목록을
다시 그리지 않고 그 상품의 현재 소스만 강제로 받아온다. 편집 중이면 저장 안 됨
경고를 먼저 띄운다.

카페24 API 가 쓰기 직후 잠시 예전 값을 돌려줄 가능성도 있다(읽기 지연). 그 경우도
[다시 읽기] 로 확인할 수 있게 했다.

검증: 유닛테스트 51개 통과. 렌더된 편집기 JS 를 브라우저에서 구문 검사(new Function)
통과, no-store·다시 읽기 반영 확인. 라우트 13개.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:46:55 +09:00
king 3522fc2119 feat(cafe24): 일괄수정 — 상세페이지 <style> 블록 통일
87개 상품의 상세설명 맨 위 <style> 을 정해진 내용으로 바꾸는 화면을 추가했다.

    <style>
    	div {
    		text-align: center;
    	}
    </style>

그냥 덮어쓰지 않고 검사 → 선택 → 적용 2단계로 만들었다. 상품 131번의 style 안에는
"비디오 태그 모바일 반응형 스타일" 같은 CSS 가 들어 있어서, 무엇이 지워지는지 보지
않고 87건을 일괄 실행하면 필요한 규칙이 조용히 사라진다. 검사 결과 표에 지금 들어
있는 CSS 를 그대로 보여주고, 변경이 필요한 상품만 자동 선택한다(이미 같은 내용이면
「이미 동일」로 제외).

맨 앞 <style> 블록 하나만 바꾼다. 아래쪽에 <style> 이 더 있으면 건드리지 않고
「블록 2개 · 주의」로 표시해 사람이 판단하게 한다 — 일괄 작업이 남의 CSS 를 조용히
지우는 것이 가장 위험하다. 블록이 없는 상품은 맨 앞에 넣는다.

상품 1건당 1요청으로 쪼갰다. 87건을 한 요청으로 묶으면 1분 가까이 걸려 프록시
타임아웃에 걸리고, 동시에 던지면 카페24 호출 제한(429)에 걸린다. 브라우저가 순차
호출하며 진행률을 보여주고, 한 건 실패가 나머지를 막지 않으며 어디까지 됐는지
화면에 남는다.

적용 순서는 단건 편집과 같은 원칙을 지킨다: 카페24 현재값 재조회 → BACKUP 버전 →
교체 → PUT → MANUAL 버전 + 감사로그(action=bulk_style). 검사 때 읽은 값을 재사용하지
않고 쓰기 직전에 다시 읽는다. PC/모바일 분리 상품은 모바일도 함께 바꾼다.

검증: 유닛테스트 51개 통과(신규 6개 — 앞 블록만 교체하고 뒤 블록 보존, 없을 때 삽입,
멱등, 포맷 후 탭 유지, 여러 줄 원문 정확히 절단). 실제 데이터로 미리보기 로직 확인:
비디오 CSS 가 "지워질 내용"에 잡히고, 이미 동일한 상품은 will_change=False,
style 없는 상품은 삽입 대상으로 판정. 라우트 13개 등록, 템플릿 렌더 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:12:24 +09:00
king 25ed369583 revert(cafe24): 상단 공통 홍보 숨기기 기능 제거
사용자 결정에 따라 없던 기능으로 되돌린다. 카페24 API 로 스킨 detail.html 의
상품번호 목록을 고칠 수 없다는 것이 확인된 뒤, 대안(상세설명 CSS 주입 / 스크립트
태그)을 모두 채택하지 않기로 정리했다.

제거 대상:
  store.py        HIDE_PROMO_* 상수, has_hidden_promo, set_promo_hidden
  routes_products 편집기 컨텍스트의 promo 상태, hide_promo 폼 필드,
                  적용 시 블록 주입, 분리 상품 모바일 블록 동기화, 감사로그 표기
  _editor.html    「상단 공통 홍보 숨기기」 체크박스
  cafe24.css      .cf24-check-inline
  tests           promo 관련 4건
  docs            2-3 절

적용 시 "변경 없음" 판정에 모바일 비교를 포함시킨 것은 남겼다. 공통 홍보와 무관하게
맞는 동작이다(PC 는 그대로여도 모바일이 PC 와 다르면 맞춰줘야 한다).

문서 정리도 함께 했다. 앞선 편집에서 3-1·2-2 절이 중복 삽입되고 절 순서가
뒤섞여 있던 것을 2 → 2-1 → 2-2 → 3 → 3-1 → 3-2 로 바로잡았다. 편집기 절에는
아직 이전 방식(크기 맞추기·sticky 줄번호)이 적혀 있었는데 실제 구현인 스크롤
동기화로 갱신하고, 크기 계산이 두 번 실패한 이유를 근거 수치와 함께 남겼다.

⚠️ 이미 이 기능으로 적용한 상품이 있다면 그 상품 상세설명에
   <style id="cf24-hide-common-promo"> 블록이 남아 있다. 편집기에서 그 줄을 지우고
   적용하면 된다.

검증: 유닛테스트 45개 통과, 라우트 10개 등록, 템플릿 컴파일 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:03:10 +09:00
king a1e222acb9 test(cafe24): scope 검증을 DEFAULT_SCOPES 기준으로
디자인 scope 를 추가하면서 요청 scope 문자열을 하드코딩한 테스트가 깨졌다.
DEFAULT_SCOPES 를 기준으로 비교하도록 바꿔, 앞으로 scope 가 늘어도 authorize 와
클라이언트가 같은 목록을 쓰는지만 확인하게 했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:54:55 +09:00
king 08ebe7b53d feat(cafe24): 상단 공통 홍보 숨기기 체크박스 (PC·모바일 동시)
요청은 스킨 detail.html 의 `const numbers = [...]` 를 고치는 것이었지만, 카페24
Admin API 로는 스킨 HTML 파일을 읽거나 쓸 수 없다. 확인 결과 테마는 조회만
가능하고(GET /admin/themes) 스킨 파일 엔드포인트가 없다. 쓸 수 있는 것은 테마
페이지와 스크립트 태그뿐이다.

그래서 같은 결과를 상품 상세설명 안의 CSS 로 낸다. 상세설명은 이미 우리가 쓸 수
있는 영역이고, 상품별로 켜고 끌 수 있으며, 상태가 그 상품 소스에 그대로 보인다.
새 권한이나 재인증도 필요하지 않다.

    <style id="cf24-hide-common-promo">.edb-img-tag-w{display:none !important}</style>

id 로 우리 블록만 찾으므로 사람이 쓴 <style> 은 건드리지 않는다. 넣기/빼기는
멱등이고 소스 정리(format_html)를 거쳐도 상태가 유지된다.

PC·모바일 모두 반영한다. 미분리 상품은 같은 HTML 이 양쪽에 들어가고, 분리 상품은
모바일 본문을 건드리지 않되 이 블록만 모바일에도 맞춘다 — 양쪽에 걸지 않으면 한쪽에
홍보가 그대로 남는다. PC/모바일 상태가 다르면 화면에 불일치를 알린다.

"변경 없음" 판정에 모바일 변경도 포함시켰다. PC 는 그대로인데 모바일 숨김만
바뀌는 경우가 있어서, 예전 조건이면 아무 일도 하지 않고 끝났다.

스킨의 numbers 목록과는 독립이며 충돌하지 않는다(스킨은 요소 제거, 이쪽은 CSS 숨김).
이미 목록에 있는 상품은 그대로 두면 된다.

검증: 유닛테스트 49개 통과(신규 4개 — 추가/제거 왕복, 멱등, 포맷 통과 후 인식,
사람이 쓴 style 보존).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:44:43 +09:00
king b60690d3e1 fix(cafe24): 편집기 커서 위치가 어긋나던 문제 — 스크롤 동기화로 전환
증상: 줄 끝에 커서를 두고 엔터를 치면 커서보다 앞쪽에서 줄이 나뉘었다.

원인은 두 층의 크기를 맞추는 방식이었다. 색칠된 <pre> 가 상자 크기를 정하고
textarea 가 그 위를 덮게 했는데, flex 자식에서 `width: max-content` 가 기대대로
적용되지 않아 화면보다 긴 줄이 있으면 textarea 만 내부 스크롤이 가능한 상태가 됐다
(실측 scrollWidth 706 vs clientWidth 686). 캐럿을 따라 textarea 가 내부적으로
스크롤되면 색칠 층은 제자리에 남아, 보이는 글자와 실제 문자 오프셋이 어긋난다.
그래서 커서를 둔 곳과 다른 위치에 개행이 들어갔다.

크기를 맞추려는 시도를 버리고 스크롤 주체를 textarea 로 두고 색칠 층과 줄 번호를
transform 으로 같은 양만큼 이동시킨다. 크기 계산이 아예 없으므로 어긋날 여지가
없다. 줄 번호 칸도 코드 영역 왼쪽의 독립 박스로 바꿔(sticky 제거) 가로 스크롤과
무관해졌다.

검증(브라우저 실측): 가로 스크롤 33px·세로 200px 에서 색칠 층과 줄 번호가 정확히
같은 양만큼 이동, 스크롤 0 에서 두 층의 텍스트 원점 일치, 줄 번호 개수가 줄 수와
일치(10/10, 70/70). 유닛테스트 45개 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:41:09 +09:00
king 5284fb1c6e fix(cafe24): 소스 들여쓰기 정상화 + 줄 번호·가로 스크롤 편집기
1) 들여쓰기가 안 되던 원인

인라인 요소가 여러 줄에 걸쳐 있으면 그 구간을 하나의 텍스트 덩어리로 다뤄
첫 줄에만 들여쓰기를 붙이고 있었다. 그래서 <img> 가 한 줄에 하나씩 적힌
상세페이지에서 두 번째 <img> 부터 1열에 붙어 나왔다.

원문 줄바꿈을 살리면서 각 줄을 현재 깊이로 들여쓰도록 고쳤다. 줄 앞 공백은
렌더링에 영향이 없으므로 안전하다. 상세페이지는 <img> 를 한 줄에 하나씩 적어두는
경우가 많고 그 모양이 저자의 의도라, 한 줄로 합치는 것보다 살리는 쪽이 읽기 좋다.

주석도 줄을 강제로 나누지 않게 바꿨다. `<!-- 대파_타임랩스 --><img ...>` 처럼 바로
뒤 요소를 설명하는 주석이 많아서, 나누면 라벨과 대상이 떨어져 오히려 읽기 나빠진다.

빈 줄은 구획 표시로 한 줄까지 유지한다. 이 과정에서 멱등성이 다시 깨지는 것을
테스트가 잡았다 — 텍스트 끝에 남은 "\n  " 조각이 매번 빈 줄로 바뀌고 있었고,
양 끝 공백을 함께 제거하도록 고쳤다.

2) 줄 번호와 가로 스크롤

전문 편집기처럼 줄 번호 칸을 넣었다. 줄바꿈을 허용하면 한 논리 줄이 여러 행이 되어
번호가 어긋나므로, 줄바꿈을 끄고(white-space:pre + wrap=off) 가로로 스크롤한다.
번호 칸은 position:sticky 라 가로로 스크롤해도 왼쪽에 남는다.

크기 계산도 단순해졌다. <pre> 가 흐름에 남아 상자 크기를 정하고 textarea 가
inset:0 으로 그 위를 덮는다 — JS 로 높이를 맞추지 않으니 어긋날 여지가 없다.
색상 계열은 그대로 뒀다(태그 초록·속성 갈색·값 남색·주석 회색).

검증: 유닛테스트 45개 통과(신규 4개 — 모든 줄 들여쓰기, 주석과 요소 붙임,
빈 줄 1개 유지, 실제 상세페이지 모양 멱등). 브라우저 실측: 줄 번호 개수가 줄 수와
일치(18/18, 19/19), 번호와 코드의 세로 위치 일치, 긴 줄에서 편집기 안에서만 가로
스크롤(1920 > 716)되고 페이지는 가로 스크롤 없음, 400px 스크롤 후에도 번호 칸 고정,
textarea 내부 스크롤 0(두 층 정렬 유지).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:03:54 +09:00
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
king 119a128ee0 feat(cafe24): 상품관리를 좌우 2분할로 — 목록(좁게) | 상세페이지 편집(넓게)
왼쪽에서 상품을 클릭하면 오른쪽에 편집기가 바로 열린다. 목록을 다시 받지 않고
오른쪽 조각만 교체한다(GET /products/{no}/pane → JS 삽입). 목록까지 다시 그리면
클릭마다 카페24 호출이 2회 더 늘어나기 때문이다. JS 가 실패하거나 없으면 각 행의
링크(/cafe24/?selected=)로 그대로 동작한다.

목록은 페이지를 없애고 전체를 한 번에 받는다(list_all_products, 1회 100개·상한
1000개). 필터·정렬을 한 페이지에만 적용하면 다음 페이지에 있는 상품이 빠져
"진열중만 보기" 가 거짓이 된다. 현재 87개라 1회 호출로 끝난다.

컬럼은 요청대로 번호·상품명·진열·판매·수정 5개다. 좁은 칸에 맞춰 진열/판매는
배지 대신 점, 수정일은 월-일만 표시하고 전체 값은 title 로 둔다. 긴 상품명은
2줄로 제한해 행 높이를 고르게 유지한다(전체 이름은 title·편집기 제목에서 확인).

진열중/판매중 체크박스는 중복 선택이 되며 둘 다 켜면 AND 다. 문서에 없는 API
필터 파라미터에 기대지 않고 받아온 뒤 파이썬에서 걸러낸다. 제목행 클릭은
오름↔내림 토글이며 한글 정렬은 localeCompare(ko) 를 쓴다.

편집 영역을 넓게 쓰려고 이 화면에서만 .erp-page 의 max-width 를 풀었다. 이때
box-sizing:border-box 를 함께 줘야 한다 — width:100% + padding:24px 이라
max-width 만 풀면 문서 전체에 가로 스크롤이 생긴다(측정으로 확인 후 수정).

편집 중 다른 상품을 클릭하거나 페이지를 벗어나면 저장 안 됨 경고를 띄운다.

옛 단독 화면(product.html)은 제거하고 /products/{no} 는 2분할 화면으로
리다이렉트한다. 편집기 조각을 두 곳에서 함께 쓰도록 _editor.html 로 분리했다.

검증: 유닛테스트 33개 통과(신규 3개 — 전체 조회의 페이지 순회·상한 처리·1회
종료). 상한 처리는 테스트가 잡아서 고쳤다(요청한 만큼 받았는지로 판정). 가짜
데이터로 렌더해 브라우저에서 실측: 왼쪽 360px·오른쪽 940px, 각 칸 독립 스크롤,
분할 영역이 화면 높이에 맞고, 가로 스크롤 없음, 정렬 오름/내림 동작 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:34:52 +09:00
king 7a2e933c16 feat(cafe24): 상세페이지 HTML 편집·적용 + 이미지 경로 한글 표시
1) 한글 파일명이 %EC%9A%A9… 으로 보이던 문제

카페24는 상세페이지 안 이미지 경로를 퍼센트 인코딩해서 저장한다. 화면에서는
읽을 수 없으므로 store.decode_html_urls 로 풀어 보여주고, 저장할 때
encode_html_urls 로 되돌린다. 두 함수는 서로의 역이며 왕복이 보존된다 —
편집하지 않고 적용해도 카페24 저장값이 한 바이트도 달라지지 않는다.

깨뜨리지 않기 위한 두 가지 제약을 뒀다. 디코딩은 non-ASCII(%80~%FF)만 한다.
%20·%3C 를 풀면 URL·HTML 구조가 깨진다. 인코딩은 src/href/poster/data-src 와
CSS url() 안의 값만 한다. 본문 한글 텍스트를 인코딩하면 페이지가 망가진다.
UTF-8 로 해석되지 않는 이스케이프(EUC-KR 등)는 건드리지 않고 그대로 둔다.

2) 편집 후 적용

POST /cafe24/products/{no}/apply 는 이 순서를 지킨다.
  카페24 현재값 재조회 → BACKUP revision → 지문 대조 → PUT → MANUAL revision
현재값을 다시 읽는 것은 로컬 DB 의 마지막 버전이 지금 카페24에 올라간 값이라고
믿을 수 없기 때문이다(관리자 페이지에서 직접 고쳤을 수 있다). 지문(sha256 앞
32자)은 편집 중 남이 바꾼 내용을 조용히 덮어쓰는 것을 막는 낙관적 잠금이다.

미분리 상품(separated_mobile_description='F')은 모바일 필드도 같은 HTML 로
함께 쓴다. PC 만 바꾸면 모바일 상세가 어긋난다. 분리 상품은 모바일을 건드리지
않고 화면에 별도 반영 안내를 띄운다.

빈 내용은 거부한다(상세페이지를 통째로 날리는 실수 방지). 변경이 없으면 API 를
호출하지 않는다. 실패 시에도 BACKUP 은 남아 있으므로 오류 메시지에 버전 번호를
알려준다. 편집 중 페이지 이탈 경고도 넣었다.

버전 이력 표를 상세 화면에 붙였다(목록 조회는 html_content 를 제외하고 길이만
계산한다 — 수 MB 가 될 수 있다). 버전 선택 복원은 Phase 6.

검증: 유닛테스트 30개 통과(신규 7개 — 실제 파일명으로 왕복 동일성, ASCII
이스케이프 미변환, 본문 한글 보존, CSS url(), 잘못된 UTF-8 무시, 지문).
실제 쓰기(PUT)는 서버 배포 후 테스트 상품 1건으로 확인 필요.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:14:42 +09:00
king 07626bcaf8 feat(cafe24): 상품 목록·검색 + 현재 상세페이지 HTML 조회 (Phase 2)
상세설명 API 경로가 틀려 있던 것을 실물 확인으로 바로잡았다.
`/admin/products/{no}/description` 은 존재하지 않는다(운영몰 호출 결과
`No API found.`). 상세설명은 상품 리소스의 필드이므로 GET/PUT 을
`/admin/products/{no}` 로 옮겼고, PUT body 는 {"request": {...}} 다.

PC/모바일 상세설명이 별도 필드라는 것도 확인됐다. `separated_mobile_description`
('T'/'F') 이 분리 사용 여부이며, 미분리 상품을 수정할 때 description 만 바꾸면
모바일이 어긋난다. Descriptions 데이터클래스에 이 플래그와 불일치 여부를 담아
화면에서 경고로 노출한다.

목록 응답에는 description 이 없어(확인됨) 상세설명은 상품 1건씩 조회한다.
그래서 목록 화면에 미리보기를 뿌리지 않는다 — 상품 87개면 87호출이라 호출
제한에 걸린다.

화면은 읽기 전용이다(편집·적용은 Phase 3~4). 목록은 카페24를 매번 조회해
현재값을 보여주고, 결과를 cafe24_products 에 UPSERT 해둔다(예약·로그 화면에서
API 없이 상품명을 쓰기 위함).

상단 탭의 예약관리가 404 였으므로 Phase 5 안내 화면을 붙였다.

토큰 만료 시각이 화면에 +00:00 로 보이던 것도 고쳤다. 컬럼이 timestamptz 라
psycopg 가 UTC 로 돌려주는 값을 그대로 출력하고 있었다(시각 자체는 정확했다).

검증: 유닛테스트 23개 통과(신규 7개 — 상세설명 경로가 /description 으로
되돌아가지 않는지, PUT payload 모양, 미분리 플래그 파싱, 페이징 clamp).
라우트 8개 등록 확인. 실제 화면은 서버 배포 후 확인 필요.

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