Files
invyone/frontend/app/test-card-responsive/page.tsx
T
DDD1542 2f398ae0b3 chore: 제어모드 IDE 작업 + v2/legacy 레지스트리 컴포넌트 폐기
- 제어모드 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 후속 노트
2026-05-19 21:31:03 +09:00

225 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}