2f398ae0b3
- 제어모드 IDE: ControlCardPanel, control/ide/* (Canvas/LeftRail/RightRail/PanZoomStage/V3RuleNode 등), schemas, lib/api/control - 레지스트리 정리: aggregation-widget, status-count, section-card/paper, table-list(legacy/v2), tabs-widget 폐기 → table/_shared/ 로 통합 - InvLegacyButtonConfigPanel cp 마이그레이션 - canonical data view cleanup 후속 노트
225 lines
10 KiB
TypeScript
225 lines
10 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* Phase 1 PoC — 카드 폭 기반 반응형 메커니즘 시각 검증 (2026-04-10)
|
||
*
|
||
* 스펙: notes/gbpark/2026-04-10-card-engine-final-spec.md (§2, §10)
|
||
*
|
||
* 확인 항목:
|
||
* 1. v2-table-list 래퍼의 ResizeObserver 가 카드 폭에 따라 data-mode 를 전환
|
||
* 2. v2-table-search-widget 의 CSS @container 가 narrow 에서 세로 스택으로 전환
|
||
*
|
||
* 실제 v2-table-list/search-widget 의 전체 렌더링은 ComponentRegistry + 백엔드
|
||
* 데이터가 필요하므로, 이 페이지는 두 반응형 메커니즘의 **레이아웃 로직만**
|
||
* 동일하게 재현해 시각 검증한다. 실 컴포넌트 연동 검증은 Phase 2 대시보드가
|
||
* 작동한 뒤 수행.
|
||
*/
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
import "@/lib/registry/components/v2-table-search-widget/table-search-widget-responsive.css";
|
||
|
||
const NARROW_BREAKPOINT = 600;
|
||
|
||
export default function TestCardResponsivePage() {
|
||
const [width, setWidth] = useState(800);
|
||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||
const [detectedMode, setDetectedMode] = useState<"wide" | "narrow">("wide");
|
||
|
||
useEffect(() => {
|
||
const el = rootRef.current;
|
||
if (!el || typeof ResizeObserver === "undefined") return;
|
||
const apply = (w: number) => {
|
||
setDetectedMode((prev) => {
|
||
const next = w < NARROW_BREAKPOINT ? "narrow" : "wide";
|
||
return prev === next ? prev : next;
|
||
});
|
||
};
|
||
apply(el.getBoundingClientRect().width);
|
||
const ro = new ResizeObserver((entries) => {
|
||
for (const entry of entries) apply(entry.contentRect.width);
|
||
});
|
||
ro.observe(el);
|
||
return () => ro.disconnect();
|
||
}, []);
|
||
|
||
return (
|
||
<div className="p-6 text-slate-800">
|
||
<div className="mb-4">
|
||
<h1 className="text-lg font-bold">Phase 1 PoC — 카드 폭 반응형 검증</h1>
|
||
<p className="text-xs text-slate-500">
|
||
스펙: notes/gbpark/2026-04-10-card-engine-final-spec.md §2, §10
|
||
</p>
|
||
</div>
|
||
|
||
<div className="mb-4 flex items-center gap-4 rounded-md border border-slate-200 bg-white p-3 text-sm">
|
||
<label className="whitespace-nowrap font-medium">카드 폭</label>
|
||
<input
|
||
type="range"
|
||
min={240}
|
||
max={1400}
|
||
step={10}
|
||
value={width}
|
||
onChange={(e) => setWidth(Number(e.target.value))}
|
||
className="flex-1"
|
||
/>
|
||
<span className="w-20 text-right font-mono">{width}px</span>
|
||
<span className="whitespace-nowrap rounded bg-slate-100 px-2 py-1 text-xs">
|
||
감지된 모드: <b className={detectedMode === "narrow" ? "text-rose-600" : "text-indigo-600"}>{detectedMode}</b>
|
||
</span>
|
||
<div className="flex gap-1">
|
||
{[320, 520, 800, 1200].map((w) => (
|
||
<button
|
||
key={w}
|
||
type="button"
|
||
onClick={() => setWidth(w)}
|
||
className="rounded border border-slate-200 bg-white px-2 py-1 text-xs hover:bg-slate-100"
|
||
>
|
||
{w}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
ref={rootRef}
|
||
className="rounded-lg border-2 border-dashed border-indigo-400 bg-indigo-50/40 p-3"
|
||
style={{ width: `${width}px`, maxWidth: "100%", transition: "width 0.15s ease" }}
|
||
>
|
||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-wide text-indigo-600">
|
||
카드 시뮬레이션 (width: {width}px)
|
||
</div>
|
||
|
||
{/* ── 1. v2-text-display (경량, 항상 동일) ── */}
|
||
<div className="mb-2 text-base font-semibold text-slate-800">수주관리</div>
|
||
|
||
{/* ── 2. canonical stats (경량, container-type 만 부착) ── */}
|
||
<div
|
||
className="mb-3 grid grid-cols-4 gap-2 rounded border border-slate-200 bg-white p-2"
|
||
style={{ containerType: "inline-size", containerName: "stats" }}
|
||
>
|
||
{[
|
||
{ label: "전체", v: "128" },
|
||
{ label: "진행", v: "42" },
|
||
{ label: "완료", v: "74" },
|
||
{ label: "대기", v: "12" },
|
||
].map((k) => (
|
||
<div key={k.label} className="rounded bg-slate-50 p-2 text-center">
|
||
<div className="text-[10px] text-slate-500">{k.label}</div>
|
||
<div className="text-base font-bold text-indigo-600">{k.v}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* ── 3. v2-table-search-widget (CSS @container 완전 마이그레이션) ── */}
|
||
<div className="v2-tsw-responsive-root mb-3">
|
||
<div className="flex w-full flex-wrap items-center gap-2 rounded border border-slate-200 bg-white p-2">
|
||
<div className="flex flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
||
<input
|
||
placeholder="검색어"
|
||
className="rounded border border-slate-300 px-2 py-1 text-xs"
|
||
style={{ flex: "0 1 25%", minWidth: 120 }}
|
||
/>
|
||
<select className="rounded border border-slate-300 px-2 py-1 text-xs" style={{ flex: "0 1 25%", minWidth: 120 }}>
|
||
<option>고객사 전체</option>
|
||
</select>
|
||
<select className="rounded border border-slate-300 px-2 py-1 text-xs" style={{ flex: "0 1 25%", minWidth: 120 }}>
|
||
<option>상태 전체</option>
|
||
</select>
|
||
<button className="h-7 shrink-0 rounded border border-slate-300 bg-white px-2 text-xs">초기화</button>
|
||
</div>
|
||
<div className="flex w-full flex-shrink-0 items-center justify-between gap-2 sm:w-auto sm:justify-end">
|
||
<div className="rounded bg-slate-100 px-2 py-1 text-xs text-slate-600">128건</div>
|
||
<button className="h-7 rounded border border-slate-300 bg-white px-2 text-xs">테이블 설정</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 4. v2-button-primary (경량) ── */}
|
||
<div className="mb-3 flex gap-2">
|
||
<button className="rounded bg-indigo-600 px-3 py-1 text-xs text-white">+ 등록</button>
|
||
<button className="rounded border border-slate-300 bg-white px-3 py-1 text-xs">수정</button>
|
||
<button className="rounded border border-rose-300 bg-white px-3 py-1 text-xs text-rose-600">삭제</button>
|
||
</div>
|
||
|
||
{/* ── 5. v2-table-list (ResizeObserver 완전 마이그레이션) ── */}
|
||
<div
|
||
data-v2-table-list-mode={detectedMode}
|
||
className="rounded border border-slate-200 bg-white p-2"
|
||
style={{ containerType: "inline-size", containerName: "v2-table-list" }}
|
||
>
|
||
<div className="mb-2 flex items-center justify-between text-[10px] text-slate-400">
|
||
<span>v2-table-list</span>
|
||
<span>
|
||
data-v2-table-list-mode=<b className="text-slate-700">{detectedMode}</b>
|
||
</span>
|
||
</div>
|
||
{detectedMode === "wide" ? (
|
||
<table className="w-full border-collapse text-xs">
|
||
<thead>
|
||
<tr className="border-b border-slate-200 bg-slate-50">
|
||
<th className="p-2 text-left">#</th>
|
||
<th className="p-2 text-left">수주번호</th>
|
||
<th className="p-2 text-left">고객</th>
|
||
<th className="p-2 text-left">상태</th>
|
||
<th className="p-2 text-right">금액</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ no: "SO-2026-0001", cust: "ACME 코리아", status: "완료", amt: 12_300_000 },
|
||
{ no: "SO-2026-0002", cust: "델타 산업", status: "진행", amt: 8_450_000 },
|
||
{ no: "SO-2026-0003", cust: "글로벌 테크", status: "대기", amt: 5_200_000 },
|
||
{ no: "SO-2026-0004", cust: "한국전자", status: "완료", amt: 15_800_000 },
|
||
].map((row, i) => (
|
||
<tr key={row.no} className="border-b border-slate-100">
|
||
<td className="p-2">{i + 1}</td>
|
||
<td className="p-2 font-mono">{row.no}</td>
|
||
<td className="p-2">{row.cust}</td>
|
||
<td className="p-2">{row.status}</td>
|
||
<td className="p-2 text-right">{row.amt.toLocaleString()}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{[
|
||
{ no: "SO-2026-0001", cust: "ACME 코리아", status: "완료", amt: 12_300_000 },
|
||
{ no: "SO-2026-0002", cust: "델타 산업", status: "진행", amt: 8_450_000 },
|
||
{ no: "SO-2026-0003", cust: "글로벌 테크", status: "대기", amt: 5_200_000 },
|
||
{ no: "SO-2026-0004", cust: "한국전자", status: "완료", amt: 15_800_000 },
|
||
].map((row) => (
|
||
<div key={row.no} className="rounded border border-slate-200 bg-slate-50 p-2 text-xs">
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-mono text-indigo-600">{row.no}</span>
|
||
<span className="rounded bg-slate-200 px-1.5 py-0.5 text-[10px]">{row.status}</span>
|
||
</div>
|
||
<div className="mt-1 text-slate-600">{row.cust}</div>
|
||
<div className="mt-0.5 text-right font-medium">{row.amt.toLocaleString()} 원</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 rounded-md border border-slate-200 bg-white p-3 text-xs text-slate-600">
|
||
<div className="mb-1 font-semibold text-slate-800">✅ 검증 포인트</div>
|
||
<ul className="list-disc space-y-1 pl-5">
|
||
<li>
|
||
카드 폭을 <b>800 → 400</b> 으로 줄이면{" "}
|
||
<b className="text-indigo-600">v2-table-list</b> 가 테이블 → 카드 리스트로 전환 (ResizeObserver 기반).
|
||
</li>
|
||
<li>
|
||
같은 조건에서 <b className="text-indigo-600">v2-table-search-widget</b> 의 필터/버튼이 가로 → 세로 스택으로 재배열 (CSS @container 기반).
|
||
</li>
|
||
<li>
|
||
나머지 컴포넌트(text-display, stats, button-primary)는 <b>container-type: inline-size</b> 만 부착된 상태.
|
||
모드 분기는 Phase 2 에서 개별 재작성.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|