feat: 관리자 페이지 레이아웃 통일 및 JSX 구문 수정
- admin/screenMng, dataflow 페이지에 tableMng 레퍼런스 레이아웃 적용 - admin/standards 페이지 JSX 괄호 문제 수정 - 전체 관리자 페이지 UI 일관성 향상 - bg-gray-50 배경, container 구조, 통일된 제목 스타일 적용
This commit is contained in:
parent
3c839a56bf
commit
1a60177fe4
|
|
@ -185,11 +185,12 @@ export default function BatchManagementPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">배치 관리</h1>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">배치 관리</h1>
|
||||
<p className="text-muted-foreground">
|
||||
스케줄된 배치 작업을 관리하고 실행 상태를 모니터링합니다.
|
||||
</p>
|
||||
|
|
@ -428,6 +429,7 @@ export default function BatchManagementPage() {
|
|||
onSave={handleModalSave}
|
||||
job={selectedJob}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,11 +162,12 @@ export default function CollectionManagementPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">수집 관리</h1>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">수집 관리</h1>
|
||||
<p className="text-muted-foreground">
|
||||
외부 데이터베이스에서 데이터를 수집하는 설정을 관리합니다.
|
||||
</p>
|
||||
|
|
@ -332,6 +333,7 @@ export default function CollectionManagementPage() {
|
|||
onSave={handleModalSave}
|
||||
config={selectedConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ export default function CommonCodeManagementPage() {
|
|||
const { selectedCategoryCode, selectCategory } = useSelectedCategory();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6 p-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">공통코드 관리</h1>
|
||||
<p className="text-muted-foreground">시스템에서 사용하는 공통코드를 관리합니다</p>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">공통코드 관리</h1>
|
||||
<p className="mt-2 text-gray-600">시스템에서 사용하는 공통코드를 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메인 콘텐츠 */}
|
||||
{/* 반응형 레이아웃: PC는 가로, 모바일은 세로 */}
|
||||
|
|
@ -52,6 +53,7 @@ export default function CommonCodeManagementPage() {
|
|||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,5 +4,18 @@ import { CompanyManagement } from "@/components/admin/CompanyManagement";
|
|||
* 회사 관리 페이지
|
||||
*/
|
||||
export default function CompanyPage() {
|
||||
return <CompanyManagement />;
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">회사 관리</h1>
|
||||
<p className="mt-2 text-gray-600">시스템에서 사용하는 회사 정보를 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<CompanyManagement />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,48 +76,49 @@ export default function DataFlowPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 bg-white px-6 py-4">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{currentStep !== "list" && (
|
||||
<Button variant="outline" size="sm" onClick={goToPreviousStep} className="flex items-center">
|
||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="flex items-center text-2xl font-bold text-gray-900">
|
||||
<span className="mr-2">{stepConfig[currentStep].icon}</span>
|
||||
{stepConfig[currentStep].title}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-gray-600">{stepConfig[currentStep].description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">데이터 흐름 관리</h1>
|
||||
<p className="mt-2 text-gray-600">테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다</p>
|
||||
</div>
|
||||
{currentStep !== "list" && (
|
||||
<Button variant="outline" onClick={goToPreviousStep} className="flex items-center">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 관계도 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="h-full p-6">
|
||||
<DataFlowList onDesignDiagram={handleDesignDiagram} />
|
||||
{/* 단계별 내용 */}
|
||||
<div className="space-y-6">
|
||||
{/* 관계도 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
|
||||
</div>
|
||||
<DataFlowList onDesignDiagram={handleDesignDiagram} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 관계도 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="h-full">
|
||||
<DataFlowDesigner
|
||||
companyCode={user?.company_code || "COMP001"}
|
||||
onSave={handleSave}
|
||||
selectedDiagram={null}
|
||||
onBackToList={() => goToStep("list")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 관계도 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
|
||||
</div>
|
||||
<DataFlowDesigner
|
||||
companyCode={user?.company_code || "COMP001"}
|
||||
onSave={handleSave}
|
||||
selectedDiagram={null}
|
||||
onBackToList={() => goToStep("list")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -161,7 +161,8 @@ export default function ExternalCallConfigsPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6 p-6">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -396,6 +397,7 @@ export default function ExternalCallConfigsPage() {
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,11 +220,15 @@ export default function ExternalConnectionsPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="mb-2 text-2xl font-bold text-gray-900">외부 커넥션 관리</h1>
|
||||
<p className="text-gray-600">외부 데이터베이스 연결 정보를 관리합니다.</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">외부 커넥션 관리</h1>
|
||||
<p className="mt-2 text-gray-600">외부 데이터베이스 연결 정보를 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검색 및 필터 */}
|
||||
<Card className="mb-6">
|
||||
|
|
@ -446,6 +450,7 @@ export default function ExternalConnectionsPage() {
|
|||
connectionName={selectedConnection.connection_name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
import MultiLang from "@/components/admin/MultiLang";
|
||||
|
||||
export default function I18nPage() {
|
||||
return <MultiLang />;
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6">
|
||||
<MultiLang />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -220,12 +220,14 @@ export default function LayoutManagementPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">레이아웃 관리</h1>
|
||||
<p className="text-gray-600">화면 레이아웃을 생성하고 관리합니다.</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">레이아웃 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면 레이아웃을 생성하고 관리합니다</p>
|
||||
</div>
|
||||
<Button className="flex items-center gap-2" onClick={() => setCreateModalOpen(true)}>
|
||||
<Plus className="h-4 w-4" />새 레이아웃
|
||||
</Button>
|
||||
|
|
@ -411,6 +413,7 @@ export default function LayoutManagementPage() {
|
|||
loadCategoryCounts();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,17 @@ import { MenuManagement } from "@/components/admin/MenuManagement";
|
|||
|
||||
export default function MenuPage() {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<MenuManagement />
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">메뉴 관리</h1>
|
||||
<p className="mt-2 text-gray-600">시스템 메뉴를 관리하고 화면을 할당합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<MenuManagement />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,19 @@ import MonitoringDashboard from "@/components/admin/MonitoringDashboard";
|
|||
|
||||
export default function MonitoringPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">모니터링</h1>
|
||||
<p className="text-muted-foreground">
|
||||
배치 작업 실행 상태를 실시간으로 모니터링합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">모니터링</h1>
|
||||
<p className="text-muted-foreground">
|
||||
배치 작업 실행 상태를 실시간으로 모니터링합니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 모니터링 대시보드 */}
|
||||
<MonitoringDashboard />
|
||||
{/* 모니터링 대시보드 */}
|
||||
<MonitoringDashboard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import Link from "next/link";
|
|||
*/
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50/30 p-8 space-y-8">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 관리자 기능 카드들 */}
|
||||
<div className="mx-auto max-w-7xl grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Link href="/admin/userMng" className="block">
|
||||
|
|
@ -162,6 +163,7 @@ export default function AdminPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,14 +66,23 @@ export default function ScreenManagementPage() {
|
|||
const isLastStep = currentStep === "template";
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 화면 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="h-full p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">{stepConfig.list.title}</h2>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">화면 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면을 설계하고 템플릿을 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 화면 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToNextStep("design")}>
|
||||
화면 설계하기 <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
|
|
@ -89,18 +98,24 @@ export default function ScreenManagementPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 화면 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="h-full">
|
||||
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
)}
|
||||
{/* 화면 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
|
||||
<Button variant="outline" onClick={() => goToStep("list")}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />목록으로 돌아가기
|
||||
</Button>
|
||||
</div>
|
||||
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 템플릿 관리 단계 */}
|
||||
{currentStep === "template" && (
|
||||
<div className="h-full p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">{stepConfig.template.title}</h2>
|
||||
{/* 템플릿 관리 단계 */}
|
||||
{currentStep === "template" && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.template.title}</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goToPreviousStep}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
|
|
@ -111,9 +126,10 @@ export default function ScreenManagementPage() {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TemplateManager selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
)}
|
||||
<TemplateManager selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@ export default function EditWebTypePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<Link href={`/admin/standards/${webType}`}>
|
||||
|
|
@ -502,6 +503,7 @@ export default function EditWebTypePage() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ export default function WebTypeDetailPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
@ -280,6 +281,7 @@ export default function WebTypeDetailPage() {
|
|||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,8 @@ export default function NewWebTypePage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<Link href="/admin/standards">
|
||||
|
|
@ -453,6 +454,7 @@ export default function NewWebTypePage() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,19 +127,20 @@ export default function WebTypesManagePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">웹타입 관리</h1>
|
||||
<p className="text-muted-foreground">화면관리에서 사용할 웹타입들을 관리합니다.</p>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">웹타입 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면관리에서 사용할 웹타입들을 관리합니다</p>
|
||||
</div>
|
||||
<Link href="/admin/standards/new">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />새 웹타입 추가
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Link href="/admin/standards/new">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />새 웹타입 추가
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 필터 및 검색 */}
|
||||
<Card className="mb-6">
|
||||
|
|
@ -364,6 +365,7 @@ export default function WebTypesManagePage() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@ export default function TableManagementPage() {
|
|||
}, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-none space-y-6 p-6">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -145,13 +145,14 @@ export default function TemplatesManagePage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6 p-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">템플릿 관리</h1>
|
||||
<p className="text-muted-foreground">화면 디자이너에서 사용할 템플릿을 관리합니다.</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">템플릿 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면 디자이너에서 사용할 템플릿을 관리합니다</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button asChild>
|
||||
<Link href="/admin/templates/new">
|
||||
|
|
@ -390,6 +391,7 @@ export default function TemplatesManagePage() {
|
|||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,17 @@ import { UserManagement } from "@/components/admin/UserManagement";
|
|||
*/
|
||||
export default function UserMngPage() {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<UserManagement />
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">사용자 관리</h1>
|
||||
<p className="mt-2 text-gray-600">시스템 사용자 계정 및 권한을 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<UserManagement />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
required: true,
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -72,7 +72,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
required: true,
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -94,7 +94,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
},
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -112,7 +112,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
required: false,
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -130,7 +130,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
required: false,
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
@ -152,7 +152,7 @@ const TEST_COMPONENTS: ComponentData[] = [
|
|||
},
|
||||
style: {
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
},
|
||||
} as WidgetComponent,
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ export default function ScreenViewPage() {
|
|||
const labelText = component.style?.labelText || component.label || "";
|
||||
const labelStyle = {
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: component.style?.labelFontWeight || "500",
|
||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||||
padding: component.style?.labelPadding || "0",
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
|||
className={`mb-1 block text-sm font-medium ${hasError ? "text-destructive" : ""}`}
|
||||
style={{
|
||||
fontSize: labelStyle.labelFontSize || "14px",
|
||||
color: hasError ? "#ef4444" : labelStyle.labelColor || "#374151",
|
||||
color: hasError ? "#ef4444" : labelStyle.labelColor || "#3b83f6",
|
||||
fontWeight: labelStyle.labelFontWeight || "500",
|
||||
fontFamily: labelStyle.labelFontFamily,
|
||||
textAlign: labelStyle.labelTextAlign || "left",
|
||||
|
|
|
|||
|
|
@ -1708,7 +1708,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
|||
// 라벨 스타일 적용
|
||||
const labelStyle = {
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: component.style?.labelFontWeight || "500",
|
||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||||
padding: component.style?.labelPadding || "0",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ interface RealtimePreviewProps {
|
|||
selectedScreen?: any;
|
||||
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러
|
||||
onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러
|
||||
onConfigChange?: (config: any) => void; // 설정 변경 핸들러
|
||||
}
|
||||
|
||||
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
|
||||
|
|
@ -73,6 +74,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
selectedScreen,
|
||||
onZoneComponentDrop,
|
||||
onZoneClick,
|
||||
onConfigChange,
|
||||
}) => {
|
||||
const { id, type, position, size, style: componentStyle } = component;
|
||||
|
||||
|
|
@ -89,8 +91,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
const baseStyle = {
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: `${size?.width || 100}px`,
|
||||
height: `${size?.height || 36}px`,
|
||||
width: component.componentConfig?.type === "table-list"
|
||||
? `${Math.max(size?.width || 400, 400)}px` // table-list는 최소 400px
|
||||
: `${size?.width || 100}px`,
|
||||
height: component.componentConfig?.type === "table-list"
|
||||
? `${Math.max(size?.height || 300, 300)}px` // table-list는 최소 300px
|
||||
: `${size?.height || 36}px`,
|
||||
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
|
||||
...componentStyle,
|
||||
};
|
||||
|
|
@ -120,7 +126,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{/* 동적 컴포넌트 렌더링 */}
|
||||
<div className="h-full w-full">
|
||||
<div className={`h-full w-full ${
|
||||
component.componentConfig?.type === "table-list" ? "overflow-visible" : ""
|
||||
}`}>
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
isSelected={isSelected}
|
||||
|
|
@ -133,6 +141,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
selectedScreen={selectedScreen}
|
||||
onZoneComponentDrop={onZoneComponentDrop}
|
||||
onZoneClick={onZoneClick}
|
||||
onConfigChange={onConfigChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1004,7 +1004,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1083,7 +1083,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1134,7 +1134,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1185,7 +1185,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1274,7 +1274,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
...templateComp.style,
|
||||
|
|
@ -1564,7 +1564,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
labelMarginBottom: "4px",
|
||||
},
|
||||
|
|
@ -1653,7 +1653,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "600",
|
||||
labelMarginBottom: "8px",
|
||||
},
|
||||
|
|
@ -1844,7 +1844,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정
|
||||
labelFontSize: "12px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
labelMarginBottom: "6px",
|
||||
},
|
||||
|
|
@ -1887,7 +1887,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
style: {
|
||||
labelDisplay: true, // 테이블 패널에서 드래그한 컴포넌트는 라벨을 기본적으로 표시
|
||||
labelFontSize: "12px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
labelMarginBottom: "6px",
|
||||
},
|
||||
|
|
@ -3158,11 +3158,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
{/* 실제 작업 캔버스 (해상도 크기) */}
|
||||
<div
|
||||
className="mx-auto bg-white shadow-lg"
|
||||
style={{ width: screenResolution.width, height: screenResolution.height }}
|
||||
style={{
|
||||
width: screenResolution.width,
|
||||
height: Math.max(screenResolution.height, 800), // 최소 높이 보장
|
||||
minHeight: screenResolution.height
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className="relative h-full w-full overflow-hidden bg-white"
|
||||
className="relative h-full w-full overflow-visible bg-white" // overflow-visible로 변경
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget && !selectionDrag.wasSelecting) {
|
||||
setSelectedComponent(null);
|
||||
|
|
@ -3271,6 +3275,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
selectedScreen={selectedScreen}
|
||||
// onZoneComponentDrop 제거
|
||||
onZoneClick={handleZoneClick}
|
||||
// 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영)
|
||||
onConfigChange={(config) => {
|
||||
console.log("📤 테이블 설정 변경을 상세설정에 알림:", config);
|
||||
// 여기서 DetailSettingsPanel의 상태를 업데이트하거나
|
||||
// 컴포넌트의 componentConfig를 업데이트할 수 있습니다
|
||||
// TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
|
||||
}}
|
||||
>
|
||||
{/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */}
|
||||
{(component.type === "group" || component.type === "container" || component.type === "area") &&
|
||||
|
|
@ -3351,6 +3362,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
selectedScreen={selectedScreen}
|
||||
// onZoneComponentDrop 제거
|
||||
onZoneClick={handleZoneClick}
|
||||
// 설정 변경 핸들러 (자식 컴포넌트용)
|
||||
onConfigChange={(config) => {
|
||||
console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config);
|
||||
// TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,25 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<Select
|
||||
value={config.action?.type || "save"}
|
||||
defaultValue="save"
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action", { type: value })}
|
||||
onValueChange={(value) => {
|
||||
// 액션 설정 업데이트
|
||||
onUpdateProperty("componentConfig.action", { type: value });
|
||||
|
||||
// 액션에 따른 라벨 색상 자동 설정
|
||||
if (value === 'delete') {
|
||||
// 삭제 액션일 때 빨간색으로 설정
|
||||
onUpdateProperty("style", {
|
||||
...component.style,
|
||||
labelColor: '#ef4444'
|
||||
});
|
||||
} else {
|
||||
// 다른 액션일 때 기본 파란색으로 리셋
|
||||
onUpdateProperty("style", {
|
||||
...component.style,
|
||||
labelColor: '#3b83f6'
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="버튼 액션 선택" />
|
||||
|
|
|
|||
|
|
@ -240,12 +240,6 @@ export const AdvancedSearchFilters: React.FC<AdvancedSearchFiltersProps> = ({
|
|||
);
|
||||
|
||||
case "code":
|
||||
console.log("🔍 코드 필터 렌더링:", {
|
||||
columnName: filter.columnName,
|
||||
codeCategory: filter.codeCategory,
|
||||
options: codeOptions[filter.codeCategory || ""],
|
||||
loading: loadingStates[filter.codeCategory || ""],
|
||||
});
|
||||
return (
|
||||
<CodeFilter
|
||||
key={filter.columnName}
|
||||
|
|
|
|||
|
|
@ -1006,7 +1006,12 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<DynamicComponentConfigPanel
|
||||
componentId={componentId}
|
||||
config={selectedComponent.componentConfig || {}}
|
||||
config={(() => {
|
||||
const config = selectedComponent.componentConfig || {};
|
||||
console.log("🔍 DetailSettingsPanel에서 전달하는 config:", config);
|
||||
console.log("🔍 selectedComponent 전체:", selectedComponent);
|
||||
return config;
|
||||
})()}
|
||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
||||
tableColumns={(() => {
|
||||
console.log("🔍 DetailSettingsPanel tableColumns 전달:", {
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
: "1"),
|
||||
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
|
||||
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
|
||||
labelColor: selectedComponent?.style?.labelColor || "#374151",
|
||||
labelColor: selectedComponent?.style?.labelColor || "#3b83f6",
|
||||
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
|
||||
required: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).required : false) || false,
|
||||
readonly: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).readonly : false) || false,
|
||||
|
|
@ -261,7 +261,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
: "1"),
|
||||
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
|
||||
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
|
||||
labelColor: selectedComponent?.style?.labelColor || "#374151",
|
||||
labelColor: selectedComponent?.style?.labelColor || "#3b83f6",
|
||||
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
|
||||
required: widget?.required || false,
|
||||
readonly: widget?.readonly || false,
|
||||
|
|
@ -285,6 +285,84 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
dragState?.justFinishedDrag, // 드래그 완료 직후 감지
|
||||
]);
|
||||
|
||||
// 🔴 삭제 액션일 때 라벨 색상 자동 설정
|
||||
useEffect(() => {
|
||||
if (selectedComponent && selectedComponent.type === "component") {
|
||||
// 삭제 액션 감지 로직 (실제 필드명 사용)
|
||||
const isDeleteAction = () => {
|
||||
const deleteKeywords = ['삭제', 'delete', 'remove', '제거', 'del'];
|
||||
return (
|
||||
selectedComponent.componentConfig?.action?.type === 'delete' ||
|
||||
selectedComponent.config?.action?.type === 'delete' ||
|
||||
selectedComponent.webTypeConfig?.actionType === 'delete' ||
|
||||
selectedComponent.text?.toLowerCase().includes('삭제') ||
|
||||
selectedComponent.text?.toLowerCase().includes('delete') ||
|
||||
selectedComponent.label?.toLowerCase().includes('삭제') ||
|
||||
selectedComponent.label?.toLowerCase().includes('delete') ||
|
||||
deleteKeywords.some(keyword =>
|
||||
selectedComponent.config?.buttonText?.toLowerCase().includes(keyword) ||
|
||||
selectedComponent.config?.text?.toLowerCase().includes(keyword)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// 🔍 디버깅: 컴포넌트 구조 확인
|
||||
console.log("🔍 PropertiesPanel 삭제 액션 디버깅:", {
|
||||
componentType: selectedComponent.type,
|
||||
componentId: selectedComponent.id,
|
||||
componentConfig: selectedComponent.componentConfig,
|
||||
config: selectedComponent.config,
|
||||
webTypeConfig: selectedComponent.webTypeConfig,
|
||||
actionType1: selectedComponent.componentConfig?.action?.type,
|
||||
actionType2: selectedComponent.config?.action?.type,
|
||||
actionType3: selectedComponent.webTypeConfig?.actionType,
|
||||
isDeleteAction: isDeleteAction(),
|
||||
currentLabelColor: selectedComponent.style?.labelColor,
|
||||
});
|
||||
|
||||
// 액션에 따른 라벨 색상 자동 설정
|
||||
if (isDeleteAction()) {
|
||||
// 삭제 액션일 때 빨간색으로 설정 (이미 빨간색이 아닌 경우에만)
|
||||
if (selectedComponent.style?.labelColor !== '#ef4444') {
|
||||
console.log("🔴 삭제 액션 감지: 라벨 색상을 빨간색으로 자동 설정");
|
||||
onUpdateProperty("style", {
|
||||
...selectedComponent.style,
|
||||
labelColor: '#ef4444'
|
||||
});
|
||||
|
||||
// 로컬 입력 상태도 업데이트
|
||||
setLocalInputs(prev => ({
|
||||
...prev,
|
||||
labelColor: '#ef4444'
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// 다른 액션일 때 기본 파란색으로 리셋 (현재 빨간색인 경우에만)
|
||||
if (selectedComponent.style?.labelColor === '#ef4444') {
|
||||
console.log("🔵 일반 액션 감지: 라벨 색상을 기본 파란색으로 리셋");
|
||||
onUpdateProperty("style", {
|
||||
...selectedComponent.style,
|
||||
labelColor: '#3b83f6'
|
||||
});
|
||||
|
||||
// 로컬 입력 상태도 업데이트
|
||||
setLocalInputs(prev => ({
|
||||
...prev,
|
||||
labelColor: '#3b83f6'
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
selectedComponent?.componentConfig?.action?.type,
|
||||
selectedComponent?.config?.action?.type,
|
||||
selectedComponent?.webTypeConfig?.actionType,
|
||||
selectedComponent?.id,
|
||||
selectedComponent?.style?.labelColor, // 라벨 색상 변경도 감지
|
||||
JSON.stringify(selectedComponent?.componentConfig), // 전체 componentConfig 변경 감지
|
||||
onUpdateProperty
|
||||
]);
|
||||
|
||||
// 렌더링 시마다 실행되는 직접적인 드래그 상태 체크
|
||||
if (dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id) {
|
||||
console.log("🎯 렌더링 중 드래그 상태 감지:", {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
|
|||
const totalRequests = useRef(0);
|
||||
const cacheHits = useRef(0);
|
||||
const batchLoadCount = useRef(0);
|
||||
|
||||
// 변환된 값 캐시 (중복 변환 방지)
|
||||
const convertedCache = useRef(new Map<string, string>());
|
||||
|
||||
// 공통 코드 카테고리 추출 (메모이제이션)
|
||||
const codeCategories = useMemo(() => {
|
||||
|
|
@ -175,29 +178,41 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
|
|||
const startTime = Date.now();
|
||||
totalRequests.current += 1;
|
||||
|
||||
// 🎯 디버깅: 캐시 상태 로깅
|
||||
console.log(`🔍 optimizedConvertCode 호출: categoryCode="${categoryCode}", codeValue="${codeValue}"`);
|
||||
// 🎯 중복 호출 방지: 이미 변환된 값인지 확인
|
||||
const cacheKey = `${categoryCode}:${codeValue}`;
|
||||
if (convertedCache.current.has(cacheKey)) {
|
||||
return convertedCache.current.get(cacheKey)!;
|
||||
}
|
||||
|
||||
// 🎯 디버깅: 캐시 상태 로깅 (빈도 줄이기)
|
||||
if (totalRequests.current % 10 === 1) { // 10번마다 한 번만 로깅
|
||||
console.log(`🔍 optimizedConvertCode 호출: categoryCode="${categoryCode}", codeValue="${codeValue}"`);
|
||||
}
|
||||
|
||||
// 캐시에서 동기적으로 조회 시도
|
||||
const syncResult = codeCache.getCodeSync(categoryCode);
|
||||
console.log(`🔍 getCodeSync("${categoryCode}") 결과:`, syncResult);
|
||||
if (totalRequests.current % 10 === 1) {
|
||||
console.log(`🔍 getCodeSync("${categoryCode}") 결과:`, syncResult);
|
||||
}
|
||||
|
||||
// 🎯 캐시 내용 상세 로깅 (키값들 확인)
|
||||
if (syncResult) {
|
||||
// 🎯 캐시 내용 상세 로깅 (키값들 확인) - 빈도 줄이기
|
||||
if (syncResult && totalRequests.current % 10 === 1) {
|
||||
console.log(`🔍 캐시 키값들:`, Object.keys(syncResult));
|
||||
console.log(`🔍 캐시 전체 데이터:`, JSON.stringify(syncResult, null, 2));
|
||||
}
|
||||
|
||||
if (syncResult && Array.isArray(syncResult)) {
|
||||
cacheHits.current += 1;
|
||||
console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`);
|
||||
console.log(
|
||||
`🔍 캐시 배열 내용:`,
|
||||
syncResult.map((item) => ({
|
||||
code_value: item.code_value,
|
||||
code_name: item.code_name,
|
||||
})),
|
||||
);
|
||||
if (totalRequests.current % 10 === 1) {
|
||||
console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`);
|
||||
console.log(
|
||||
`🔍 캐시 배열 내용:`,
|
||||
syncResult.map((item) => ({
|
||||
code_value: item.code_value,
|
||||
code_name: item.code_name,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// 배열에서 해당 code_value를 가진 항목 찾기
|
||||
const foundCode = syncResult.find(
|
||||
|
|
@ -205,7 +220,13 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
|
|||
);
|
||||
|
||||
const result = foundCode ? foundCode.code_name : codeValue;
|
||||
console.log(`🔍 최종 결과: "${codeValue}" → "${result}"`, { foundCode });
|
||||
|
||||
// 변환 결과를 캐시에 저장
|
||||
convertedCache.current.set(cacheKey, result);
|
||||
|
||||
if (totalRequests.current % 10 === 1) {
|
||||
console.log(`🔍 최종 결과: "${codeValue}" → "${result}"`, { foundCode });
|
||||
}
|
||||
|
||||
// 응답 시간 추적 (캐시 히트)
|
||||
requestTimes.current.push(Date.now() - startTime);
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ export class AutoRegisteringComponentRenderer {
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
marginBottom: component.style?.labelMarginBottom || "4px",
|
||||
fontWeight: "500",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export interface ComponentRenderer {
|
|||
refreshKey?: number;
|
||||
// 편집 모드
|
||||
mode?: "view" | "edit";
|
||||
// 설정 변경 핸들러 (상세설정과 연동)
|
||||
onConfigChange?: (config: any) => void;
|
||||
[key: string]: any;
|
||||
}): React.ReactElement;
|
||||
}
|
||||
|
|
@ -170,6 +172,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
selectedRowsData,
|
||||
onSelectedRowsChange,
|
||||
refreshKey,
|
||||
onConfigChange,
|
||||
...safeProps
|
||||
} = props;
|
||||
|
||||
|
|
@ -224,6 +227,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
selectedRows={selectedRows}
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
// 설정 변경 핸들러 전달
|
||||
onConfigChange={onConfigChange}
|
||||
refreshKey={refreshKey}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -661,7 +661,7 @@ export const AccordionBasicComponent: React.FC<AccordionBasicComponentProps> = (
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -86,12 +86,69 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
// 삭제 액션 감지 로직 (실제 필드명 사용)
|
||||
const isDeleteAction = () => {
|
||||
const deleteKeywords = ['삭제', 'delete', 'remove', '제거', 'del'];
|
||||
return (
|
||||
component.componentConfig?.action?.type === 'delete' ||
|
||||
component.config?.action?.type === 'delete' ||
|
||||
component.webTypeConfig?.actionType === 'delete' ||
|
||||
component.text?.toLowerCase().includes('삭제') ||
|
||||
component.text?.toLowerCase().includes('delete') ||
|
||||
component.label?.toLowerCase().includes('삭제') ||
|
||||
component.label?.toLowerCase().includes('delete') ||
|
||||
deleteKeywords.some(keyword =>
|
||||
component.config?.buttonText?.toLowerCase().includes(keyword) ||
|
||||
component.config?.text?.toLowerCase().includes(keyword)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// 삭제 액션일 때 라벨 색상 자동 설정
|
||||
useEffect(() => {
|
||||
if (isDeleteAction() && !component.style?.labelColor) {
|
||||
// 삭제 액션이고 라벨 색상이 설정되지 않은 경우 빨간색으로 자동 설정
|
||||
if (component.style) {
|
||||
component.style.labelColor = '#ef4444';
|
||||
} else {
|
||||
component.style = { labelColor: '#ef4444' };
|
||||
}
|
||||
}
|
||||
}, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]);
|
||||
|
||||
// 컴포넌트 설정
|
||||
const componentConfig = {
|
||||
...config,
|
||||
...component.config,
|
||||
} as ButtonPrimaryConfig;
|
||||
|
||||
// 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동)
|
||||
const getLabelColor = () => {
|
||||
if (isDeleteAction()) {
|
||||
return component.style?.labelColor || '#ef4444'; // 빨간색 기본값 (Tailwind red-500)
|
||||
}
|
||||
return component.style?.labelColor || '#3b83f6'; // 기본 파란색 (Tailwind blue-500)
|
||||
};
|
||||
|
||||
const buttonColor = getLabelColor();
|
||||
|
||||
// 그라데이션용 어두운 색상 계산
|
||||
const getDarkColor = (baseColor: string) => {
|
||||
const hex = baseColor.replace('#', '');
|
||||
const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40);
|
||||
const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40);
|
||||
const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40);
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const buttonDarkColor = getDarkColor(buttonColor);
|
||||
|
||||
console.log("🎨 동적 색상 연동:", {
|
||||
labelColor: component.style?.labelColor,
|
||||
buttonColor,
|
||||
buttonDarkColor,
|
||||
});
|
||||
|
||||
// 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환
|
||||
const processedConfig = { ...componentConfig };
|
||||
if (componentConfig.action && typeof componentConfig.action === "string") {
|
||||
|
|
@ -368,26 +425,29 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: "100%", // 최소 높이 강제 적용
|
||||
maxHeight: "100%", // 최대 높이 제한
|
||||
border: "1px solid #3b82f6",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "#3b82f6",
|
||||
color: "white",
|
||||
minHeight: "100%",
|
||||
maxHeight: "100%",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
background: componentConfig.disabled
|
||||
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
|
||||
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
|
||||
color: componentConfig.disabled ? "#9ca3af" : "white",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
fontWeight: "600",
|
||||
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
||||
outline: "none",
|
||||
boxSizing: "border-box", // 패딩/보더 포함 크기 계산
|
||||
display: "flex", // flex로 변경
|
||||
alignItems: "center", // 세로 중앙 정렬
|
||||
justifyContent: "center", // 가로 중앙 정렬
|
||||
padding: "0", // 패딩 제거
|
||||
margin: "0", // 마진 제거
|
||||
lineHeight: "1", // 라인 높이 고정
|
||||
// 강제 높이 적용
|
||||
boxSizing: "border-box",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0 16px",
|
||||
margin: "0",
|
||||
lineHeight: "1",
|
||||
minHeight: "36px",
|
||||
height: "36px",
|
||||
boxShadow: componentConfig.disabled
|
||||
? "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
|
||||
: `0 2px 4px 0 ${buttonColor}33`, // 33은 20% 투명도
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -141,7 +141,7 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
|||
/>
|
||||
<span
|
||||
style={{
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export const DividerLineComponent: React.FC<DividerLineComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export const ImageDisplayComponent: React.FC<ImageDisplayComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -155,7 +155,7 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
|||
/>
|
||||
<span
|
||||
style={{
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -601,7 +601,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -149,7 +149,7 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
|||
width: "30%",
|
||||
textAlign: "center",
|
||||
fontSize: "14px",
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ interface SingleTableWithStickyProps {
|
|||
renderCheckboxCell: (row: any, index: number) => React.ReactNode;
|
||||
formatCellValue: (value: any, format?: string, columnName?: string) => string;
|
||||
getColumnWidth: (column: ColumnConfig) => number;
|
||||
joinColumnMapping: Record<string, string>; // 조인 컬럼 매핑 추가
|
||||
containerWidth?: string; // 컨테이너 너비 설정
|
||||
}
|
||||
|
||||
export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
|
|
@ -40,13 +40,28 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
renderCheckboxCell,
|
||||
formatCellValue,
|
||||
getColumnWidth,
|
||||
joinColumnMapping,
|
||||
containerWidth,
|
||||
}) => {
|
||||
const checkboxConfig = tableConfig.checkbox || {};
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<Table className="w-full">
|
||||
<div
|
||||
className="relative h-full overflow-auto"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
className="w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
tableLayout: "fixed",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
|
||||
<TableRow>
|
||||
{visibleColumns.map((column, colIndex) => {
|
||||
|
|
@ -66,7 +81,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
|
||||
return (
|
||||
<TableHead
|
||||
key={`sticky-header-${colIndex}-${column.columnName}`}
|
||||
key={column.columnName}
|
||||
className={cn(
|
||||
column.columnName === "__checkbox__"
|
||||
? "h-10 border-b px-4 py-2 text-center align-middle"
|
||||
|
|
@ -83,6 +98,9 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
width: getColumnWidth(column),
|
||||
minWidth: getColumnWidth(column),
|
||||
maxWidth: getColumnWidth(column),
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
|
|
@ -92,7 +110,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
<div className="flex items-center gap-2">
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
checkboxConfig.selectAll && (
|
||||
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" />
|
||||
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" style={{ zIndex: 1 }} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -131,7 +149,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
) : (
|
||||
data.map((row, index) => (
|
||||
<TableRow
|
||||
key={`sticky-row-${index}`}
|
||||
key={`row-${index}`}
|
||||
className={cn(
|
||||
"h-10 cursor-pointer border-b leading-none",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
|
|
@ -157,7 +175,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
|
||||
return (
|
||||
<TableCell
|
||||
key={`sticky-cell-${index}-${colIndex}-${column.columnName}`}
|
||||
key={`cell-${column.columnName}`}
|
||||
className={cn(
|
||||
"h-10 px-4 py-2 align-middle text-sm whitespace-nowrap",
|
||||
`text-${column.align}`,
|
||||
|
|
@ -169,6 +187,11 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
minHeight: "40px",
|
||||
height: "40px",
|
||||
verticalAlign: "middle",
|
||||
width: getColumnWidth(column),
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
|
|
@ -176,25 +199,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
>
|
||||
{column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: (() => {
|
||||
// 🎯 매핑된 컬럼명으로 데이터 찾기 (기본 테이블과 동일한 로직)
|
||||
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
||||
|
||||
// 조인 컬럼 매핑 정보 로깅
|
||||
if (column.columnName !== mappedColumnName && index === 0) {
|
||||
console.log(`🔗 Sticky 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`);
|
||||
}
|
||||
|
||||
const cellValue = row[mappedColumnName];
|
||||
if (index === 0) {
|
||||
// 첫 번째 행만 로그 출력 (디버깅용)
|
||||
console.log(
|
||||
`🔍 Sticky 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`,
|
||||
cellValue,
|
||||
);
|
||||
}
|
||||
return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0";
|
||||
})()}
|
||||
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -33,7 +33,13 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
tableColumns,
|
||||
}) => {
|
||||
console.log("🔍 TableListConfigPanel props:", {
|
||||
config: config?.selectedTable,
|
||||
config,
|
||||
configType: typeof config,
|
||||
configSelectedTable: config?.selectedTable,
|
||||
configPagination: config?.pagination,
|
||||
paginationEnabled: config?.pagination?.enabled,
|
||||
paginationPageSize: config?.pagination?.pageSize,
|
||||
configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object',
|
||||
screenTableName,
|
||||
tableColumns: tableColumns?.length,
|
||||
tableColumnsSample: tableColumns?.[0],
|
||||
|
|
@ -210,13 +216,25 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
};
|
||||
|
||||
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {
|
||||
console.log("🔧 TableListConfigPanel handleNestedChange:", {
|
||||
parentKey,
|
||||
childKey,
|
||||
value,
|
||||
parentValue: config[parentKey],
|
||||
hasOnChange: !!onChange,
|
||||
onChangeType: typeof onChange,
|
||||
});
|
||||
|
||||
const parentValue = config[parentKey] as any;
|
||||
onChange({
|
||||
const newConfig = {
|
||||
[parentKey]: {
|
||||
...parentValue,
|
||||
[childKey]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
|
||||
onChange(newConfig);
|
||||
};
|
||||
|
||||
// 컬럼 추가
|
||||
|
|
|
|||
|
|
@ -13,9 +13,27 @@ export class TableListRenderer extends AutoRegisteringComponentRenderer {
|
|||
static componentDefinition = TableListDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
return <TableListComponent {...this.props} renderer={this} />;
|
||||
return <TableListComponent
|
||||
{...this.props}
|
||||
renderer={this}
|
||||
onConfigChange={this.handleConfigChange}
|
||||
/>;
|
||||
}
|
||||
|
||||
// 설정 변경 핸들러
|
||||
protected handleConfigChange = (config: any) => {
|
||||
console.log("📥 TableListRenderer에서 설정 변경 받음:", config);
|
||||
|
||||
// 상위 컴포넌트의 onConfigChange 호출 (화면 설계자에게 알림)
|
||||
if (this.props.onConfigChange) {
|
||||
this.props.onConfigChange(config);
|
||||
} else {
|
||||
console.log("⚠️ 상위 컴포넌트에서 onConfigChange가 전달되지 않음");
|
||||
}
|
||||
|
||||
this.updateComponent({ config });
|
||||
};
|
||||
|
||||
/**
|
||||
* 컴포넌트별 특화 메서드들
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export const TestInputComponent: React.FC<TestInputComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
|
|||
const textStyle: React.CSSProperties = {
|
||||
fontSize: componentConfig.fontSize || "14px",
|
||||
fontWeight: componentConfig.fontWeight || "normal",
|
||||
color: componentConfig.color || "#374151",
|
||||
color: componentConfig.color || "#3b83f6",
|
||||
textAlign: componentConfig.textAlign || "left",
|
||||
backgroundColor: componentConfig.backgroundColor || "transparent",
|
||||
padding: componentConfig.padding || "0",
|
||||
|
|
@ -102,7 +102,7 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const TextDisplayConfigPanel: React.FC<TextDisplayConfigPanelProps> = ({
|
|||
<Input
|
||||
id="color"
|
||||
type="color"
|
||||
value={config.color || "#374151"}
|
||||
value={config.color || "#3b83f6"}
|
||||
onChange={(e) => handleChange("color", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const TextDisplayDefinition = createComponentDefinition({
|
|||
text: "텍스트를 입력하세요",
|
||||
fontSize: "14px",
|
||||
fontWeight: "normal",
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
textAlign: "left",
|
||||
},
|
||||
defaultSize: { width: 150, height: 24 },
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const TextareaBasicComponent: React.FC<TextareaBasicComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -173,7 +173,7 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
|||
</div>
|
||||
<span
|
||||
style={{
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ export class AutoRegisteringLayoutRenderer {
|
|||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
labelColor: "#374151",
|
||||
labelColor: "#3b83f6",
|
||||
labelFontWeight: "500",
|
||||
labelMarginBottom: "4px",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -60,8 +60,15 @@ export abstract class BaseLayoutRenderer extends React.Component<LayoutRendererP
|
|||
|
||||
// 디자인 모드일 때 더 강조된 스타일
|
||||
if (isDesignMode) {
|
||||
zoneStyle.border = "2px dashed #cbd5e1";
|
||||
zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.8)";
|
||||
// 🎯 컴포넌트가 있는 존은 테두리 제거 (컴포넌트 자체 테두리와 충돌 방지)
|
||||
if (zoneChildren.length === 0) {
|
||||
zoneStyle.border = "2px dashed #cbd5e1";
|
||||
zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.8)";
|
||||
} else {
|
||||
// 컴포넌트가 있는 존은 미묘한 배경만
|
||||
zoneStyle.border = "1px solid transparent";
|
||||
zoneStyle.backgroundColor = "rgba(248, 250, 252, 0.3)";
|
||||
}
|
||||
}
|
||||
|
||||
// 호버 효과를 위한 추가 스타일
|
||||
|
|
@ -91,14 +98,26 @@ export abstract class BaseLayoutRenderer extends React.Component<LayoutRendererP
|
|||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const element = e.currentTarget;
|
||||
element.style.borderColor = "#3b82f6";
|
||||
element.style.backgroundColor = "rgba(59, 130, 246, 0.02)";
|
||||
element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)";
|
||||
// 🎯 컴포넌트가 있는 존은 호버 효과 최소화
|
||||
if (zoneChildren.length > 0) {
|
||||
element.style.backgroundColor = "rgba(59, 130, 246, 0.01)";
|
||||
} else {
|
||||
element.style.borderColor = "#3b82f6";
|
||||
element.style.backgroundColor = "rgba(59, 130, 246, 0.02)";
|
||||
element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const element = e.currentTarget;
|
||||
element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0";
|
||||
element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)";
|
||||
if (zoneChildren.length > 0) {
|
||||
// 컴포넌트가 있는 존 복원
|
||||
element.style.borderColor = "transparent";
|
||||
element.style.backgroundColor = isDesignMode ? "rgba(248, 250, 252, 0.3)" : "rgba(248, 250, 252, 0.5)";
|
||||
} else {
|
||||
// 빈 존 복원
|
||||
element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0";
|
||||
element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)";
|
||||
}
|
||||
element.style.boxShadow = "none";
|
||||
}}
|
||||
onDrop={this.handleDrop(zone.id)}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ const AccordionSection: React.FC<{
|
|||
const headerStyle: React.CSSProperties = {
|
||||
padding: "12px 16px",
|
||||
backgroundColor: isDesignMode ? "#3b82f6" : "#f8fafc",
|
||||
color: isDesignMode ? "white" : "#374151",
|
||||
color: isDesignMode ? "white" : "#3b83f6",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderBottom: isExpanded ? "none" : "1px solid #e2e8f0",
|
||||
cursor: "pointer",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ let hotReloadListeners: Array<() => void> = [];
|
|||
* Hot Reload 시스템 초기화
|
||||
*/
|
||||
export function initializeHotReload(): void {
|
||||
// 핫 리로드 시스템 임시 비활성화 (디버깅 목적)
|
||||
console.log("🔥 컴포넌트 Hot Reload 시스템 비활성화됨 (디버깅 모드)");
|
||||
return;
|
||||
|
||||
if (process.env.NODE_ENV !== "development" || typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
|
@ -55,11 +59,15 @@ function setupDevServerEventListener(): void {
|
|||
const originalLog = console.log;
|
||||
let reloadPending = false;
|
||||
|
||||
// console.log 메시지를 감지하여 Hot Reload 트리거
|
||||
// console.log 메시지를 감지하여 Hot Reload 트리거 (특정 메시지만)
|
||||
console.log = (...args: any[]) => {
|
||||
const message = args.join(" ");
|
||||
|
||||
if (message.includes("compiled") || message.includes("Fast Refresh") || message.includes("component")) {
|
||||
// 핫 리로드를 트리거할 특정 메시지만 감지 (디버깅 로그는 제외)
|
||||
if ((message.includes("compiled") || message.includes("Fast Refresh")) &&
|
||||
!message.includes("🔍") && !message.includes("🎯") && !message.includes("📤") &&
|
||||
!message.includes("📥") && !message.includes("⚠️") && !message.includes("🔄") &&
|
||||
!message.includes("✅") && !message.includes("🔧") && !message.includes("📋")) {
|
||||
if (!reloadPending) {
|
||||
reloadPending = true;
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
screenTableName,
|
||||
tableColumns,
|
||||
}) => {
|
||||
console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`);
|
||||
|
||||
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
|
@ -180,10 +182,21 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
);
|
||||
}
|
||||
|
||||
console.log(`🔧 DynamicComponentConfigPanel 렌더링:`, {
|
||||
componentId,
|
||||
ConfigPanelComponent: ConfigPanelComponent?.name,
|
||||
config,
|
||||
configType: typeof config,
|
||||
configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object',
|
||||
screenTableName,
|
||||
tableColumns: Array.isArray(tableColumns) ? tableColumns.length : tableColumns
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigPanelComponent
|
||||
config={config}
|
||||
onChange={onChange}
|
||||
onConfigChange={onChange} // TableListConfigPanel을 위한 추가 prop
|
||||
screenTableName={screenTableName}
|
||||
tableColumns={tableColumns}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -661,7 +661,7 @@ function getComponentJSXByWebType(webType) {
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
@ -709,7 +709,7 @@ function getComponentJSXByWebType(webType) {
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
@ -785,7 +785,7 @@ function getComponentJSXByWebType(webType) {
|
|||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#374151",
|
||||
color: component.style?.labelColor || "#3b83f6",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ export interface ComponentRendererProps {
|
|||
// 새로운 기능들
|
||||
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
|
||||
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
|
||||
|
||||
// 설정 변경 핸들러
|
||||
onConfigChange?: (config: any) => void;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
|
@ -317,7 +320,7 @@ export const COMPONENT_CATEGORIES_INFO = {
|
|||
[ComponentCategory.CHART]: {
|
||||
name: "차트",
|
||||
description: "데이터 시각화 컴포넌트",
|
||||
color: "#06b6d4",
|
||||
color: "#3b83f6",
|
||||
},
|
||||
[ComponentCategory.FORM]: {
|
||||
name: "폼",
|
||||
|
|
@ -347,7 +350,7 @@ export const COMPONENT_CATEGORIES_INFO = {
|
|||
[ComponentCategory.CONTAINER]: {
|
||||
name: "컨테이너",
|
||||
description: "다른 컴포넌트를 담는 컨테이너",
|
||||
color: "#374151",
|
||||
color: "#3b83f6",
|
||||
},
|
||||
[ComponentCategory.SYSTEM]: {
|
||||
name: "시스템",
|
||||
|
|
|
|||
Loading…
Reference in New Issue