Merge pull request 'feature/screen-management' (#172) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/172
This commit is contained in:
commit
5604771d23
|
|
@ -17,6 +17,7 @@ import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
|
||||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||||
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
||||||
import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보
|
import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보
|
||||||
|
import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감지
|
||||||
|
|
||||||
export default function ScreenViewPage() {
|
export default function ScreenViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -25,6 +26,9 @@ export default function ScreenViewPage() {
|
||||||
|
|
||||||
// 🆕 현재 로그인한 사용자 정보
|
// 🆕 현재 로그인한 사용자 정보
|
||||||
const { user, userName, companyCode } = useAuth();
|
const { user, userName, companyCode } = useAuth();
|
||||||
|
|
||||||
|
// 🆕 모바일 환경 감지
|
||||||
|
const { isMobile } = useResponsive();
|
||||||
|
|
||||||
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
|
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
|
||||||
const [layout, setLayout] = useState<LayoutData | null>(null);
|
const [layout, setLayout] = useState<LayoutData | null>(null);
|
||||||
|
|
@ -59,6 +63,7 @@ export default function ScreenViewPage() {
|
||||||
|
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [scale, setScale] = useState(1);
|
const [scale, setScale] = useState(1);
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initComponents = async () => {
|
const initComponents = async () => {
|
||||||
|
|
@ -142,22 +147,33 @@ export default function ScreenViewPage() {
|
||||||
}
|
}
|
||||||
}, [screenId]);
|
}, [screenId]);
|
||||||
|
|
||||||
// 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일)
|
// 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일) - 모바일에서는 비활성화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 모바일 환경에서는 스케일 조정 비활성화 (반응형만 작동)
|
||||||
|
if (isMobile) {
|
||||||
|
setScale(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updateScale = () => {
|
const updateScale = () => {
|
||||||
if (containerRef.current && layout) {
|
if (containerRef.current && layout) {
|
||||||
const designWidth = layout?.screenResolution?.width || 1200;
|
const designWidth = layout?.screenResolution?.width || 1200;
|
||||||
const designHeight = layout?.screenResolution?.height || 800;
|
const designHeight = layout?.screenResolution?.height || 800;
|
||||||
|
|
||||||
|
// containerRef는 이미 패딩이 적용된 영역 내부이므로 offsetWidth는 패딩을 제외한 크기입니다
|
||||||
const containerWidth = containerRef.current.offsetWidth;
|
const containerWidth = containerRef.current.offsetWidth;
|
||||||
const containerHeight = containerRef.current.offsetHeight;
|
const containerHeight = containerRef.current.offsetHeight;
|
||||||
|
|
||||||
// 가로/세로 비율 중 작은 것을 선택 (화면에 맞게)
|
// 가로/세로 비율 중 작은 것을 선택하여 화면에 맞게 스케일 조정
|
||||||
|
// 하지만 화면이 컨테이너 전체 너비를 차지하도록 하기 위해 가로를 우선시
|
||||||
const scaleX = containerWidth / designWidth;
|
const scaleX = containerWidth / designWidth;
|
||||||
const scaleY = containerHeight / designHeight;
|
const scaleY = containerHeight / designHeight;
|
||||||
|
// 가로를 우선으로 하되, 세로가 넘치지 않도록 제한
|
||||||
const newScale = Math.min(scaleX, scaleY);
|
const newScale = Math.min(scaleX, scaleY);
|
||||||
|
|
||||||
setScale(newScale);
|
setScale(newScale);
|
||||||
|
// 컨테이너 너비 업데이트
|
||||||
|
setContainerWidth(containerWidth);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -169,7 +185,7 @@ export default function ScreenViewPage() {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
window.removeEventListener("resize", updateScale);
|
window.removeEventListener("resize", updateScale);
|
||||||
};
|
};
|
||||||
}, [layout]);
|
}, [layout, isMobile]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -205,18 +221,16 @@ export default function ScreenViewPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenPreviewProvider isPreviewMode={false}>
|
<ScreenPreviewProvider isPreviewMode={false}>
|
||||||
<div ref={containerRef} className="bg-background flex h-full w-full items-start justify-start overflow-hidden">
|
<div ref={containerRef} className="bg-background h-full w-full overflow-hidden">
|
||||||
{/* 절대 위치 기반 렌더링 */}
|
{/* 절대 위치 기반 렌더링 */}
|
||||||
{layout && layout.components.length > 0 ? (
|
{layout && layout.components.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
className="bg-background relative origin-top-left"
|
className="bg-background relative origin-top-left h-full flex justify-start items-start"
|
||||||
style={{
|
style={{
|
||||||
width: layout?.screenResolution?.width || 1200,
|
|
||||||
height: layout?.screenResolution?.height || 800,
|
|
||||||
transform: `scale(${scale})`,
|
transform: `scale(${scale})`,
|
||||||
transformOrigin: "top left",
|
transformOrigin: "top left",
|
||||||
display: "flex",
|
width: containerWidth > 0 ? `${containerWidth / scale}px` : "100%",
|
||||||
flexDirection: "column",
|
minWidth: containerWidth > 0 ? `${containerWidth / scale}px` : "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 최상위 컴포넌트들 렌더링 */}
|
{/* 최상위 컴포넌트들 렌더링 */}
|
||||||
|
|
|
||||||
|
|
@ -342,4 +342,18 @@ select {
|
||||||
/* 필요시 특정 컴포넌트에 대한 스타일 오버라이드를 여기에 추가 */
|
/* 필요시 특정 컴포넌트에 대한 스타일 오버라이드를 여기에 추가 */
|
||||||
/* 예: Calendar, Table 등의 미세 조정 */
|
/* 예: Calendar, Table 등의 미세 조정 */
|
||||||
|
|
||||||
|
/* 모바일에서 테이블 레이아웃 고정 (화면 밖으로 넘어가지 않도록) */
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.table-mobile-fixed {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 데스크톱에서 테이블 레이아웃 자동 (기본값이지만 명시적으로 설정) */
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.table-mobile-fixed {
|
||||||
|
table-layout: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== End of Global Styles ===== */
|
/* ===== End of Global Styles ===== */
|
||||||
|
|
|
||||||
|
|
@ -542,7 +542,7 @@ export function DashboardViewer({
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
{/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
|
{/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
|
||||||
<div className="hidden min-h-screen bg-muted py-8 lg:block" style={{ backgroundColor }}>
|
<div className="hidden min-h-screen bg-muted py-8 lg:block" style={{ backgroundColor }}>
|
||||||
<div className="mx-auto px-4" style={{ maxWidth: `${canvasConfig.width}px` }}>
|
<div className="mx-auto px-4" style={{ width: '100%', maxWidth: 'none' }}>
|
||||||
{/* 다운로드 버튼 */}
|
{/* 다운로드 버튼 */}
|
||||||
<div className="mb-4 flex justify-end">
|
<div className="mb-4 flex justify-end">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
@ -700,12 +700,13 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
||||||
// 데스크톱: 디자이너에서 설정한 위치 그대로 absolute positioning
|
// 데스크톱: 디자이너에서 설정한 위치 그대로 absolute positioning
|
||||||
// 단, 너비는 화면 크기에 따라 비율로 조정
|
// 단, 너비는 화면 크기에 따라 비율로 조정
|
||||||
const widthPercentage = (element.size.width / canvasWidth) * 100;
|
const widthPercentage = (element.size.width / canvasWidth) * 100;
|
||||||
|
const leftPercentage = (element.position.x / canvasWidth) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute overflow-hidden rounded-lg border border-border bg-background shadow-sm"
|
className="absolute overflow-hidden rounded-lg border border-border bg-background shadow-sm"
|
||||||
style={{
|
style={{
|
||||||
left: `${(element.position.x / canvasWidth) * 100}%`,
|
left: `${leftPercentage}%`,
|
||||||
top: element.position.y,
|
top: element.position.y,
|
||||||
width: `${widthPercentage}%`,
|
width: `${widthPercentage}%`,
|
||||||
height: element.size.height,
|
height: element.size.height,
|
||||||
|
|
|
||||||
|
|
@ -459,7 +459,9 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
|
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
|
||||||
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white">{children}</main>
|
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-hidden bg-white">
|
||||||
|
<div className="h-full w-full p-4">{children}</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 프로필 수정 모달 */}
|
{/* 프로필 수정 모달 */}
|
||||||
|
|
|
||||||
|
|
@ -389,14 +389,27 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
finalHeight = actualHeight;
|
finalHeight = actualHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔍 디버깅: position.x 값 확인
|
||||||
|
const positionX = position?.x || 0;
|
||||||
|
console.log("🔍 RealtimePreview componentStyle 설정:", {
|
||||||
|
componentId: id,
|
||||||
|
positionX,
|
||||||
|
sizeWidth: size?.width,
|
||||||
|
styleWidth: style?.width,
|
||||||
|
willUse100Percent: positionX === 0,
|
||||||
|
});
|
||||||
|
|
||||||
const componentStyle = {
|
const componentStyle = {
|
||||||
position: "absolute" as const,
|
position: "absolute" as const,
|
||||||
left: position?.x || 0,
|
...style, // 먼저 적용하고
|
||||||
|
left: positionX,
|
||||||
top: position?.y || 0,
|
top: position?.y || 0,
|
||||||
width: size?.width || 200,
|
// 🆕 left가 0이면 부모 너비를 100% 채우도록 수정 (우측 여백 제거)
|
||||||
|
width: positionX === 0 ? "100%" : (size?.width || 200),
|
||||||
height: finalHeight,
|
height: finalHeight,
|
||||||
zIndex: position?.z || 1,
|
zIndex: position?.z || 1,
|
||||||
...style,
|
// right 속성 강제 제거
|
||||||
|
right: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 선택된 컴포넌트 스타일
|
// 선택된 컴포넌트 스타일
|
||||||
|
|
|
||||||
|
|
@ -238,11 +238,15 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
left: `${position.x}px`,
|
left: `${position.x}px`,
|
||||||
top: `${position.y}px`,
|
top: `${position.y}px`,
|
||||||
width: getWidth(),
|
// 🆕 left가 0이면 부모 너비를 100% 채우도록 수정 (우측 여백 제거)
|
||||||
|
width: position.x === 0 ? "100%" : getWidth(),
|
||||||
height: getHeight(), // 모든 컴포넌트 고정 높이로 변경
|
height: getHeight(), // 모든 컴포넌트 고정 높이로 변경
|
||||||
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
|
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
|
||||||
...componentStyle,
|
...componentStyle,
|
||||||
// style.width와 style.height는 이미 getWidth/getHeight에서 처리했으므로 중복 적용됨
|
// style.width가 있어도 position.x === 0이면 100%로 강제
|
||||||
|
...(position.x === 0 && { width: "100%" }),
|
||||||
|
// right 속성 강제 제거
|
||||||
|
right: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,8 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className={cn(
|
className={cn(
|
||||||
column.columnName === "__checkbox__"
|
column.columnName === "__checkbox__"
|
||||||
? "h-12 border-0 px-6 py-3 text-center align-middle"
|
? "h-10 border-0 px-3 py-2 text-center align-middle sm:h-12 sm:px-6 sm:py-3"
|
||||||
: "h-12 cursor-pointer border-0 px-6 py-3 text-left align-middle font-semibold whitespace-nowrap text-foreground transition-all duration-200 select-none hover:text-foreground",
|
: "h-10 cursor-pointer border-0 px-3 py-2 text-left align-middle font-semibold whitespace-nowrap text-xs text-foreground transition-all duration-200 select-none hover:text-foreground sm:h-12 sm:px-6 sm:py-3 sm:text-sm",
|
||||||
`text-${column.align}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-primary/10",
|
column.sortable && "hover:bg-primary/10",
|
||||||
// 고정 컬럼 스타일
|
// 고정 컬럼 스타일
|
||||||
|
|
@ -133,11 +133,11 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
{columnLabels[column.columnName] || column.displayName || column.columnName}
|
{columnLabels[column.columnName] || column.displayName || column.columnName}
|
||||||
</span>
|
</span>
|
||||||
{column.sortable && sortColumn === column.columnName && (
|
{column.sortable && sortColumn === column.columnName && (
|
||||||
<span className="ml-2 flex h-5 w-5 items-center justify-center rounded-md bg-background/50 shadow-sm">
|
<span className="ml-1 flex h-4 w-4 items-center justify-center rounded-md bg-background/50 shadow-sm sm:ml-2 sm:h-5 sm:w-5">
|
||||||
{sortDirection === "asc" ? (
|
{sortDirection === "asc" ? (
|
||||||
<ArrowUp className="h-3.5 w-3.5 text-primary" />
|
<ArrowUp className="h-2.5 w-2.5 text-primary sm:h-3.5 sm:w-3.5" />
|
||||||
) : (
|
) : (
|
||||||
<ArrowDown className="h-3.5 w-3.5 text-primary" />
|
<ArrowDown className="h-2.5 w-2.5 text-primary sm:h-3.5 sm:w-3.5" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -177,7 +177,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
<TableRow
|
<TableRow
|
||||||
key={`row-${index}`}
|
key={`row-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-16 cursor-pointer border-b transition-colors bg-background",
|
"h-14 cursor-pointer border-b transition-colors bg-background sm:h-16",
|
||||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-muted/50",
|
tableConfig.tableStyle?.hoverEffect && "hover:bg-muted/50",
|
||||||
)}
|
)}
|
||||||
onClick={() => handleRowClick(row)}
|
onClick={() => handleRowClick(row)}
|
||||||
|
|
@ -201,7 +201,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
<TableCell
|
<TableCell
|
||||||
key={`cell-${column.columnName}`}
|
key={`cell-${column.columnName}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-16 px-6 py-3 align-middle text-sm whitespace-nowrap text-foreground transition-colors",
|
"h-14 px-3 py-2 align-middle text-xs whitespace-nowrap text-foreground transition-colors sm:h-16 sm:px-6 sm:py-3 sm:text-sm",
|
||||||
`text-${column.align}`,
|
`text-${column.align}`,
|
||||||
// 고정 컬럼 스타일
|
// 고정 컬럼 스타일
|
||||||
column.fixed === "left" &&
|
column.fixed === "left" &&
|
||||||
|
|
|
||||||
|
|
@ -772,45 +772,32 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
className="w-full h-14 flex items-center justify-center relative border-t-2 border-border bg-background px-4 flex-shrink-0 sm:h-[60px] sm:px-6"
|
||||||
width: "100%",
|
|
||||||
height: "60px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
position: "relative",
|
|
||||||
borderTop: "2px solid #e5e7eb",
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
padding: "0 24px",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* 중앙 페이지네이션 컨트롤 */}
|
{/* 중앙 페이지네이션 컨트롤 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
className="flex items-center gap-2 sm:gap-4"
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "16px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePageChange(1)}
|
onClick={() => handlePageChange(1)}
|
||||||
disabled={currentPage === 1 || loading}
|
disabled={currentPage === 1 || loading}
|
||||||
|
className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
|
||||||
>
|
>
|
||||||
<ChevronsLeft className="h-4 w-4" />
|
<ChevronsLeft className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
disabled={currentPage === 1 || loading}
|
disabled={currentPage === 1 || loading}
|
||||||
|
className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<span className="text-sm font-medium text-foreground min-w-[80px] text-center">
|
<span className="text-xs font-medium text-foreground min-w-[60px] text-center sm:text-sm sm:min-w-[80px]">
|
||||||
{currentPage} / {totalPages || 1}
|
{currentPage} / {totalPages || 1}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
@ -819,19 +806,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
disabled={currentPage >= totalPages || loading}
|
disabled={currentPage >= totalPages || loading}
|
||||||
|
className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePageChange(totalPages)}
|
onClick={() => handlePageChange(totalPages)}
|
||||||
disabled={currentPage >= totalPages || loading}
|
disabled={currentPage >= totalPages || loading}
|
||||||
|
className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
|
||||||
>
|
>
|
||||||
<ChevronsRight className="h-4 w-4" />
|
<ChevronsRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<span className="text-xs text-muted-foreground ml-4">
|
<span className="text-[10px] text-muted-foreground ml-2 sm:text-xs sm:ml-4">
|
||||||
전체 {totalItems.toLocaleString()}개
|
전체 {totalItems.toLocaleString()}개
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -842,9 +831,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
style={{ position: "absolute", right: "24px" }}
|
className="absolute right-2 h-8 w-8 p-0 sm:right-6 sm:h-9 sm:w-auto sm:px-3"
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
|
<RefreshCw className={cn("h-3 w-3", loading && "animate-spin")} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -884,14 +873,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return (
|
return (
|
||||||
<div {...domProps}>
|
<div {...domProps}>
|
||||||
{tableConfig.showHeader && (
|
{tableConfig.showHeader && (
|
||||||
<div className="px-6 py-4 border-b border-border">
|
<div className="px-4 py-3 border-b border-border sm:px-6 sm:py-4">
|
||||||
<h2 className="text-lg font-semibold text-foreground">{tableConfig.title || tableLabel}</h2>
|
<h2 className="text-base font-semibold text-foreground sm:text-lg">{tableConfig.title || tableLabel}</h2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tableConfig.filter?.enabled && (
|
{tableConfig.filter?.enabled && (
|
||||||
<div className="px-6 py-4 border-b border-border">
|
<div className="px-4 py-3 border-b border-border sm:px-6 sm:py-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<AdvancedSearchFilters
|
<AdvancedSearchFilters
|
||||||
filters={activeFilters}
|
filters={activeFilters}
|
||||||
|
|
@ -905,7 +894,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsFilterSettingOpen(true)}
|
onClick={() => setIsFilterSettingOpen(true)}
|
||||||
className="flex-shrink-0 mt-1"
|
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
||||||
>
|
>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
필터 설정
|
필터 설정
|
||||||
|
|
@ -946,16 +935,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<div {...domProps}>
|
<div {...domProps}>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
{tableConfig.showHeader && (
|
{tableConfig.showHeader && (
|
||||||
<div style={{ padding: "16px 24px", borderBottom: "1px solid #e5e7eb", flexShrink: 0 }}>
|
<div className="px-4 py-3 border-b border-border flex-shrink-0 sm:px-6 sm:py-4">
|
||||||
<h2 style={{ fontSize: "18px", fontWeight: 600, color: "#111827" }}>{tableConfig.title || tableLabel}</h2>
|
<h2 className="text-base font-semibold text-foreground sm:text-lg">{tableConfig.title || tableLabel}</h2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 필터 */}
|
{/* 필터 */}
|
||||||
{tableConfig.filter?.enabled && (
|
{tableConfig.filter?.enabled && (
|
||||||
<div style={{ padding: "16px 24px", borderBottom: "1px solid #e5e7eb", flexShrink: 0 }}>
|
<div className="px-4 py-3 border-b border-border flex-shrink-0 sm:px-6 sm:py-4">
|
||||||
<div style={{ display: "flex", alignItems: "flex-start", gap: "16px" }}>
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<AdvancedSearchFilters
|
<AdvancedSearchFilters
|
||||||
filters={activeFilters}
|
filters={activeFilters}
|
||||||
searchValues={searchValues}
|
searchValues={searchValues}
|
||||||
|
|
@ -968,7 +957,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsFilterSettingOpen(true)}
|
onClick={() => setIsFilterSettingOpen(true)}
|
||||||
style={{ flexShrink: 0, marginTop: "4px" }}
|
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
||||||
>
|
>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
필터 설정
|
필터 설정
|
||||||
|
|
@ -978,33 +967,34 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 테이블 컨테이너 */}
|
{/* 테이블 컨테이너 */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden w-full max-w-full">
|
||||||
{/* 스크롤 영역 */}
|
{/* 스크롤 영역 */}
|
||||||
<div
|
<div
|
||||||
className="w-full h-[500px] overflow-y-scroll overflow-x-auto bg-background"
|
className="w-full max-w-full h-[400px] overflow-y-scroll overflow-x-auto bg-background sm:h-[500px]"
|
||||||
>
|
>
|
||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<table
|
<table
|
||||||
|
className="w-full max-w-full table-mobile-fixed"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
|
||||||
borderCollapse: "collapse",
|
borderCollapse: "collapse",
|
||||||
tableLayout: "auto",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 헤더 (sticky) */}
|
{/* 헤더 (sticky) */}
|
||||||
<thead
|
<thead
|
||||||
className="sticky top-0 z-10 bg-background"
|
className="sticky top-0 z-10 bg-background"
|
||||||
>
|
>
|
||||||
<tr className="h-12 border-b border-border">
|
<tr className="h-10 border-b border-border sm:h-12">
|
||||||
{visibleColumns.map((column) => (
|
{visibleColumns.map((column) => (
|
||||||
<th
|
<th
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-12 px-6 py-3 text-sm font-semibold text-foreground whitespace-nowrap bg-background",
|
"h-10 px-2 py-2 text-xs font-semibold text-foreground overflow-hidden text-ellipsis bg-background sm:h-12 sm:px-6 sm:py-3 sm:text-sm sm:whitespace-nowrap",
|
||||||
column.sortable && "cursor-pointer"
|
column.sortable && "cursor-pointer"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
textAlign: column.align || "left",
|
textAlign: column.align || "left",
|
||||||
|
width: `${100 / visibleColumns.length}%`, // 컬럼 수에 따라 균등 분배
|
||||||
}}
|
}}
|
||||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||||
>
|
>
|
||||||
|
|
@ -1063,7 +1053,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
onDragStart={(e) => handleRowDragStart(e, row, index)}
|
onDragStart={(e) => handleRowDragStart(e, row, index)}
|
||||||
onDragEnd={handleRowDragEnd}
|
onDragEnd={handleRowDragEnd}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-16 border-b transition-colors bg-background hover:bg-muted/50 cursor-pointer"
|
"h-14 border-b transition-colors bg-background hover:bg-muted/50 cursor-pointer sm:h-16"
|
||||||
)}
|
)}
|
||||||
onClick={() => handleRowClick(row)}
|
onClick={() => handleRowClick(row)}
|
||||||
>
|
>
|
||||||
|
|
@ -1075,10 +1065,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<td
|
<td
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-16 px-6 py-3 text-sm text-foreground whitespace-nowrap overflow-hidden text-ellipsis"
|
"h-14 px-2 py-2 text-xs text-foreground overflow-hidden text-ellipsis sm:h-16 sm:px-6 sm:py-3 sm:text-sm sm:whitespace-nowrap"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
textAlign: column.align || "left",
|
textAlign: column.align || "left",
|
||||||
|
width: `${100 / visibleColumns.length}%`, // 컬럼 수에 따라 균등 분배
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{column.columnName === "__checkbox__"
|
{column.columnName === "__checkbox__"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue