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>
This commit is contained in:
2026-08-14 12:34:52 +09:00
parent 7a2e933c16
commit 119a128ee0
8 changed files with 828 additions and 351 deletions
+34 -5
View File
@@ -25,14 +25,14 @@ app/integrations/cafe24/ ← 공통 (상품관리 + 향후 주문관리
app/modules/cafe24/ ← 상품관리 모듈
├─ router.py 루트 라우터(prefix=/cafe24) + 서브 라우터 결합
├─ routes_products.py 상품 목록/검색 · 상세설명 조회 (읽기 전용)
├─ routes_products.py 2분할 화면 · 편집기 조각 · 적용(쓰기)
├─ routes_system.py 연결(OAuth)·상태·API 로그·작업 로그
├─ common.py 가드/컨텍스트 헬퍼 (순환 import 방지로 분리)
├─ db.py Cafe24Store (cafe24_db, psycopg3 raw SQL)
├─ store.py 순수 로직 — 상수·상태전이·재시도 규칙·검증
├─ tests/ DB/네트워크 없는 유닛테스트
└─ templates/cafe24/ _nav.html · products.html · product.html ·
schedules.html · system.html
└─ templates/cafe24/ _nav.html · products.html(2분할) ·
_editor.html(오른쪽 조각) · schedules.html · system.html
```
**규칙: 라우터에서 `httpx`/`requests` 를 직접 부르지 않는다.** 반드시
@@ -48,8 +48,9 @@ app/modules/cafe24/ ← 상품관리 모듈
| 경로 | 화면 | 권한 |
| --- | --- | --- |
| `GET /cafe24/` | 상품 목록·검색 (`q`, `page`) | `cafe24` |
| `GET /cafe24/products/{product_no}` | 상품 1건 + 상세설명 HTML 편집기 + 버전 이력 | `cafe24` |
| `GET /cafe24/` | 2분할 화면 (`q`, `display`, `selling`, `selected`) | `cafe24` |
| `GET /cafe24/products/{product_no}/pane` | 오른쪽 편집기 조각 (JS 가 가져감) | `cafe24` |
| `GET /cafe24/products/{product_no}` | → `/cafe24/?selected=` 로 리다이렉트(옛 주소) | `cafe24` |
| `POST /cafe24/products/{product_no}/apply` | 편집한 HTML 을 카페24에 즉시 적용 | `cafe24` |
| `GET /cafe24/schedules` | 예약관리 (Phase 5 안내) | `cafe24` |
| `GET /cafe24/system` | 연결 상태 · API 로그 · 작업 로그 | `cafe24` |
@@ -87,6 +88,34 @@ cafe24_oauth_tokens 저장
---
### 2-1. 상품관리 화면 구성 (2분할)
```
┌─ 360px ─────────┬────────────── 남은 폭 전부 ──────────────┐
│ 검색 / 진열·판매 │ 선택한 상품 이름·상태 │
│ 필터(중복 선택) │ PC 상세설명 HTML 편집(칸이 남은 높이 차지) │
│ ── 목록 ── │ [메모] [복사] [카페24에 적용] │
│ 번호 상품명 진열 │ ▸ 모바일 HTML(읽기 전용, 분리 상품만) │
│ 판매 수정 │ ▸ 버전 이력 │
└─────────────────┴───────────────────────────────────────────┘
```
- **왼쪽은 전체 목록**(페이지 없음). `list_all_products` 로 페이지를 넘겨가며 전부
받는다(1회 100개, 상한 1000개). 필터를 한 페이지에만 적용하면 다음 페이지의
해당 상품이 빠지기 때문이다.
- **필터**는 `진열중`/`판매중` 체크박스이며 **중복 선택 시 AND** 다. 문서에 없는 API
파라미터에 기대지 않고 받아온 뒤 파이썬에서 걸러낸다.
- **정렬**은 제목행 클릭(오름↔내림 토글). 브라우저에서 처리하므로 전체를 받아둔
덕분에 목록 전체가 대상이 된다.
- **상품 클릭 시 오른쪽만 교체**한다(`/pane` 조각을 fetch → 삽입). 목록을 다시 받지
않으므로 카페24 호출이 1회로 끝난다. JS 실패 시 각 행의 링크로 정상 동작한다.
- 편집 중 다른 상품을 클릭하거나 페이지를 벗어나면 **저장 안 됨 경고**가 뜬다.
- `.erp-page``max-width` 를 이 화면에서만 풀어 편집 영역을 넓게 쓴다. 이때
`box-sizing: border-box` 를 함께 줘야 한다(안 주면 padding 이 폭에 더해져 문서에
가로 스크롤이 생긴다).
---
## 3-1. 상세설명 API 사실 (실물 확인 결과 — 추측 금지)
운영 쇼핑몰(`miraskitchen`)에서 직접 확인한 내용이다. 문서에 없는 경로를