Compare commits

...

2 Commits

4 changed files with 217 additions and 186 deletions

View File

@ -52,9 +52,8 @@ export default function ScreenViewPage() {
modalDescription?: string; modalDescription?: string;
}>({}); }>({});
// 자동 스케일 조정 (사용자 화면 크기에 맞춤)
const [scale, setScale] = useState(1);
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
useEffect(() => { useEffect(() => {
const initComponents = async () => { const initComponents = async () => {
@ -140,32 +139,37 @@ export default function ScreenViewPage() {
} }
}, [screenId]); }, [screenId]);
// 자동 스케일 조정 useEffect (항상 화면에 꽉 차게) // 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일)
useEffect(() => { useEffect(() => {
const updateScale = () => { const updateScale = () => {
if (containerRef.current && layout) { if (containerRef.current && layout) {
const screenWidth = layout?.screenResolution?.width || 1200; const designWidth = layout?.screenResolution?.width || 1200;
const designHeight = layout?.screenResolution?.height || 800;
const containerWidth = containerRef.current.offsetWidth; const containerWidth = containerRef.current.offsetWidth;
const availableWidth = containerWidth - 32; // 좌우 패딩 16px * 2 const containerHeight = containerRef.current.offsetHeight;
// 항상 화면에 맞춰서 스케일 조정 (늘리거나 줄임) // 가로/세로 비율 중 작은 것을 선택 (화면에 맞게)
const newScale = availableWidth / screenWidth; const scaleX = containerWidth / designWidth;
const scaleY = containerHeight / designHeight;
const newScale = Math.min(scaleX, scaleY);
console.log("📏 스케일 계산 (화면 꽉 차게):", { console.log("📏 캔버스 스케일 계산:", {
screenWidth, designWidth,
designHeight,
containerWidth, containerWidth,
availableWidth, containerHeight,
scale: newScale, scaleX,
scaleY,
finalScale: newScale,
}); });
setScale(newScale); setScale(newScale);
} }
}; };
// 초기 측정 (DOM이 완전히 렌더링된 후) // 초기 측정
const timer = setTimeout(() => { const timer = setTimeout(updateScale, 100);
updateScale();
}, 100);
window.addEventListener("resize", updateScale); window.addEventListener("resize", updateScale);
return () => { return () => {
@ -207,17 +211,16 @@ export default function ScreenViewPage() {
const screenHeight = layout?.screenResolution?.height || 800; const screenHeight = layout?.screenResolution?.height || 800;
return ( return (
<div ref={containerRef} className="bg-background flex h-full w-full flex-col overflow-hidden"> <div ref={containerRef} className="bg-background flex h-full w-full items-start justify-start overflow-hidden">
{/* 절대 위치 기반 렌더링 */} {/* 절대 위치 기반 렌더링 */}
{layout && layout.components.length > 0 ? ( {layout && layout.components.length > 0 ? (
<div <div
className="bg-background relative flex-1" className="bg-background relative origin-top-left"
style={{ style={{
width: screenWidth, width: layout?.screenResolution?.width || 1200,
height: "100%", height: layout?.screenResolution?.height || 800,
transform: `scale(${scale})`, transform: `scale(${scale})`,
transformOrigin: "top left", transformOrigin: "top left",
overflow: "hidden",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}} }}

View File

@ -334,7 +334,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
console.log("🔍 InteractiveScreenViewer 최종 flowComponent:", flowComponent); console.log("🔍 InteractiveScreenViewer 최종 flowComponent:", flowComponent);
return ( return (
<div className="h-full w-full"> <div className="w-full">
<FlowWidget component={flowComponent as any} /> <FlowWidget component={flowComponent as any} />
</div> </div>
); );

View File

@ -142,11 +142,11 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
newHeight: finalHeight, newHeight: finalHeight,
}); });
// size는 별도 속성이므로 직접 업데이트 // size는 별도 속성이므로 직접 업데이트
const event = new CustomEvent('updateComponentSize', { const event = new CustomEvent("updateComponentSize", {
detail: { detail: {
componentId: component.id, componentId: component.id,
height: finalHeight height: finalHeight,
} },
}); });
window.dispatchEvent(event); window.dispatchEvent(event);
} }
@ -276,10 +276,10 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
> >
{/* 동적 컴포넌트 렌더링 */} {/* 동적 컴포넌트 렌더링 */}
<div <div
ref={component.type === "component" && (component as any).componentType === "flow-widget" ? contentRef : undefined} ref={
className={`${component.type === "component" && (component as any).componentType === "flow-widget" ? "h-auto" : "h-full"} w-full max-w-full ${ component.type === "component" && (component as any).componentType === "flow-widget" ? contentRef : undefined
component.componentConfig?.type === "table-list" ? "overflow-hidden" : "overflow-visible" }
}`} className={`${component.type === "component" && (component as any).componentType === "flow-widget" ? "h-auto" : "h-full"} w-full max-w-full overflow-visible`}
> >
<DynamicComponentRenderer <DynamicComponentRenderer
component={component} component={component}

View File

@ -9,6 +9,7 @@ import { getFlowById, getAllStepCounts, getStepDataList, getFlowAuditLogs } from
import type { FlowDefinition, FlowStep, FlowAuditLog } from "@/types/flow"; import type { FlowDefinition, FlowStep, FlowAuditLog } from "@/types/flow";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
Dialog, Dialog,
@ -63,7 +64,7 @@ export function FlowWidget({
// 🆕 스텝 데이터 페이지네이션 상태 // 🆕 스텝 데이터 페이지네이션 상태
const [stepDataPage, setStepDataPage] = useState(1); const [stepDataPage, setStepDataPage] = useState(1);
const [stepDataPageSize] = useState(20); const [stepDataPageSize, setStepDataPageSize] = useState(10);
// 오딧 로그 상태 // 오딧 로그 상태
const [auditLogs, setAuditLogs] = useState<FlowAuditLog[]>([]); const [auditLogs, setAuditLogs] = useState<FlowAuditLog[]>([]);
@ -385,7 +386,7 @@ export function FlowWidget({
: "flex flex-col items-center gap-4"; : "flex flex-col items-center gap-4";
return ( return (
<div className="@container flex h-full w-full flex-col p-2 sm:p-4 lg:p-6"> <div className="@container flex w-full flex-col p-2 sm:p-4 lg:p-6">
{/* 플로우 제목 */} {/* 플로우 제목 */}
<div className="mb-3 flex-shrink-0 sm:mb-4"> <div className="mb-3 flex-shrink-0 sm:mb-4">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
@ -647,8 +648,8 @@ export function FlowWidget({
{/* 선택된 스텝의 데이터 리스트 */} {/* 선택된 스텝의 데이터 리스트 */}
{selectedStepId !== null && ( {selectedStepId !== null && (
<div className="bg-muted/30 mt-4 flex min-h-0 w-full flex-1 flex-col rounded-lg border sm:mt-6 lg:mt-8"> <div className="bg-muted/30 mt-4 flex w-full flex-col rounded-lg border sm:mt-6 lg:mt-8">
{/* 헤더 */} {/* 헤더 - 자동 높이 */}
<div className="bg-background flex-shrink-0 border-b px-4 py-3 sm:px-6 sm:py-4"> <div className="bg-background flex-shrink-0 border-b px-4 py-3 sm:px-6 sm:py-4">
<h4 className="text-foreground text-base font-semibold sm:text-lg"> <h4 className="text-foreground text-base font-semibold sm:text-lg">
{steps.find((s) => s.id === selectedStepId)?.stepName} {steps.find((s) => s.id === selectedStepId)?.stepName}
@ -661,15 +662,14 @@ export function FlowWidget({
</p> </p>
</div> </div>
{/* 데이터 영역 - 스크롤 가능 */} {/* 데이터 영역 - 고정 높이 + 스크롤 */}
<div className="min-h-0 flex-1 overflow-auto">
{stepDataLoading ? ( {stepDataLoading ? (
<div className="flex h-full items-center justify-center py-12"> <div className="flex h-64 items-center justify-center">
<Loader2 className="text-primary h-6 w-6 animate-spin sm:h-8 sm:w-8" /> <Loader2 className="text-primary h-6 w-6 animate-spin sm:h-8 sm:w-8" />
<span className="text-muted-foreground ml-2 text-sm"> ...</span> <span className="text-muted-foreground ml-2 text-sm"> ...</span>
</div> </div>
) : stepData.length === 0 ? ( ) : stepData.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center py-12"> <div className="flex h-64 flex-col items-center justify-center">
<svg <svg
className="text-muted-foreground/50 mb-3 h-12 w-12" className="text-muted-foreground/50 mb-3 h-12 w-12"
fill="none" fill="none"
@ -687,8 +687,9 @@ export function FlowWidget({
</div> </div>
) : ( ) : (
<> <>
{/* 모바일: 카드 뷰 */} {/* 모바일: 카드 뷰 - 고정 높이 + 스크롤 */}
<div className="space-y-2 p-3 @sm:hidden"> <div className="overflow-y-auto @sm:hidden" style={{ height: "450px" }}>
<div className="space-y-2 p-3">
{paginatedStepData.map((row, pageIndex) => { {paginatedStepData.map((row, pageIndex) => {
const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex;
return ( return (
@ -725,9 +726,10 @@ export function FlowWidget({
); );
})} })}
</div> </div>
</div>
{/* 데스크톱: 테이블 뷰 */} {/* 데스크톱: 테이블 뷰 - 고정 높이 + 스크롤 */}
<div className="hidden @sm:block"> <div className="hidden overflow-auto @sm:block" style={{ height: "450px" }}>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50"> <TableRow className="bg-muted/50 hover:bg-muted/50">
@ -782,15 +784,40 @@ export function FlowWidget({
</div> </div>
</> </>
)} )}
</div>
{/* 페이지네이션 푸터 */} {/* 페이지네이션 - 항상 하단에 고정 */}
{!stepDataLoading && stepData.length > 0 && totalStepDataPages > 1 && ( {!stepDataLoading && stepData.length > 0 && (
<div className="bg-background flex-shrink-0 border-t px-4 py-3 sm:px-6"> <div className="bg-background flex-shrink-0 border-t px-4 py-3 sm:px-6">
<div className="flex flex-col items-center justify-between gap-3 sm:flex-row"> <div className="flex flex-col items-center justify-between gap-3 sm:flex-row">
{/* 왼쪽: 페이지 정보 + 페이지 크기 선택 */}
<div className="flex flex-col items-center gap-2 sm:flex-row sm:gap-4">
<div className="text-muted-foreground text-xs sm:text-sm"> <div className="text-muted-foreground text-xs sm:text-sm">
{stepDataPage} / {totalStepDataPages} ( {stepData.length}) {stepDataPage} / {totalStepDataPages} ( {stepData.length})
</div> </div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs"> :</span>
<Select
value={stepDataPageSize.toString()}
onValueChange={(value) => {
setStepDataPageSize(Number(value));
setStepDataPage(1); // 페이지 크기 변경 시 첫 페이지로
}}
>
<SelectTrigger className="h-8 w-20 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 오른쪽: 페이지네이션 */}
{totalStepDataPages > 1 && (
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
@ -851,6 +878,7 @@ export function FlowWidget({
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination> </Pagination>
)}
</div> </div>
</div> </div>
)} )}