Merge pull request 'dev' (#58) from dev into main

Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/58
This commit is contained in:
hjlee 2025-09-25 09:31:48 +09:00
commit cb0058cd37
64 changed files with 1461 additions and 927 deletions

View File

@ -185,11 +185,12 @@ export default function BatchManagementPage() {
}; };
return ( return (
<div className="space-y-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> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"> </h1> <div>
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
. .
</p> </p>
@ -428,6 +429,7 @@ export default function BatchManagementPage() {
onSave={handleModalSave} onSave={handleModalSave}
job={selectedJob} job={selectedJob}
/> />
</div>
</div> </div>
); );
} }

View File

@ -162,11 +162,12 @@ export default function CollectionManagementPage() {
}; };
return ( return (
<div className="space-y-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> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"> </h1> <div>
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
. .
</p> </p>
@ -332,6 +333,7 @@ export default function CollectionManagementPage() {
onSave={handleModalSave} onSave={handleModalSave}
config={selectedConfig} config={selectedConfig}
/> />
</div>
</div> </div>
); );
} }

View File

@ -11,22 +11,23 @@ export default function CommonCodeManagementPage() {
const { selectedCategoryCode, selectCategory } = useSelectedCategory(); const { selectedCategoryCode, selectCategory } = useSelectedCategory();
return ( 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> <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<h1 className="text-3xl font-bold tracking-tight"> </h1> <div>
<p className="text-muted-foreground"> </p> <h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
</div>
</div> </div>
</div>
{/* 메인 콘텐츠 */} {/* 메인 콘텐츠 */}
{/* 반응형 레이아웃: PC는 가로, 모바일은 세로 */} {/* 반응형 레이아웃: PC는 가로, 모바일은 세로 */}
<div className="flex flex-col gap-6 lg:flex-row lg:gap-8"> <div className="flex flex-col gap-6 lg:flex-row lg:gap-8">
{/* 카테고리 패널 - PC에서 좌측 고정 너비, 모바일에서 전체 너비 */} {/* 카테고리 패널 - PC에서 좌측 고정 너비, 모바일에서 전체 너비 */}
<div className="w-full lg:w-80 lg:flex-shrink-0"> <div className="w-full lg:w-80 lg:flex-shrink-0">
<Card className="h-full"> <Card className="h-full shadow-sm">
<CardHeader> <CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2">📂 </CardTitle> <CardTitle className="flex items-center gap-2">📂 </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
@ -37,8 +38,8 @@ export default function CommonCodeManagementPage() {
{/* 코드 상세 패널 - PC에서 나머지 공간, 모바일에서 전체 너비 */} {/* 코드 상세 패널 - PC에서 나머지 공간, 모바일에서 전체 너비 */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<Card className="h-fit"> <Card className="h-fit shadow-sm">
<CardHeader> <CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
📋 📋
{selectedCategoryCode && ( {selectedCategoryCode && (
@ -52,6 +53,7 @@ export default function CommonCodeManagementPage() {
</Card> </Card>
</div> </div>
</div> </div>
</div>
</div> </div>
); );
} }

View File

@ -4,5 +4,18 @@ import { CompanyManagement } from "@/components/admin/CompanyManagement";
* *
*/ */
export default function CompanyPage() { 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 bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
</div>
</div>
<CompanyManagement />
</div>
</div>
);
} }

View File

@ -76,48 +76,49 @@ export default function DataFlowPage() {
}; };
return ( return (
<div className="flex h-full w-full flex-col"> <div className="min-h-screen bg-gray-50">
{/* 헤더 */} <div className="container mx-auto p-6 space-y-6">
<div className="border-b border-gray-200 bg-white px-6 py-4"> {/* 페이지 제목 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div className="flex items-center space-x-4"> <div>
{currentStep !== "list" && ( <h1 className="text-3xl font-bold text-gray-900"> </h1>
<Button variant="outline" size="sm" onClick={goToPreviousStep} className="flex items-center"> <p className="mt-2 text-gray-600"> </p>
<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> </div>
{currentStep !== "list" && (
<Button variant="outline" onClick={goToPreviousStep} className="flex items-center shadow-sm">
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
)}
</div> </div>
</div>
{/* 단계별 내용 */} {/* 단계별 내용 */}
<div className="flex-1 overflow-hidden"> <div className="space-y-6">
{/* 관계도 목록 단계 */} {/* 관계도 목록 단계 */}
{currentStep === "list" && ( {currentStep === "list" && (
<div className="h-full p-6"> <div className="space-y-6">
<DataFlowList onDesignDiagram={handleDesignDiagram} /> <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
</div>
<DataFlowList onDesignDiagram={handleDesignDiagram} />
</div> </div>
)} )}
{/* 관계도 설계 단계 */} {/* 관계도 설계 단계 */}
{currentStep === "design" && ( {currentStep === "design" && (
<div className="h-full"> <div className="space-y-6">
<DataFlowDesigner <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
companyCode={user?.company_code || "COMP001"} <h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
onSave={handleSave} </div>
selectedDiagram={null} <DataFlowDesigner
onBackToList={() => goToStep("list")} companyCode={user?.company_code || "COMP001"}
/> onSave={handleSave}
</div> selectedDiagram={null}
)} onBackToList={() => goToStep("list")}
/>
</div>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -161,7 +161,8 @@ export default function ExternalCallConfigsPage() {
}; };
return ( 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 className="flex items-center justify-between">
<div> <div>
@ -396,6 +397,7 @@ export default function ExternalCallConfigsPage() {
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div>
</div> </div>
); );
} }

View File

@ -220,14 +220,18 @@ export default function ExternalConnectionsPage() {
}; };
return ( return (
<div className="container mx-auto p-6"> <div className="min-h-screen bg-gray-50">
<div className="mb-6"> <div className="container mx-auto p-6 space-y-6">
<h1 className="mb-2 text-2xl font-bold text-gray-900"> </h1> {/* 페이지 제목 */}
<p className="text-gray-600"> .</p> <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
</div> <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"> <Card className="mb-6 shadow-sm">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-col gap-4 md:flex-row md:items-center"> <div className="flex flex-col gap-4 md:flex-row md:items-center">
@ -285,7 +289,7 @@ export default function ExternalConnectionsPage() {
<div className="text-gray-500"> ...</div> <div className="text-gray-500"> ...</div>
</div> </div>
) : connections.length === 0 ? ( ) : connections.length === 0 ? (
<Card> <Card className="shadow-sm">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="py-8 text-center text-gray-500"> <div className="py-8 text-center text-gray-500">
<Database className="mx-auto mb-4 h-12 w-12 text-gray-400" /> <Database className="mx-auto mb-4 h-12 w-12 text-gray-400" />
@ -298,7 +302,7 @@ export default function ExternalConnectionsPage() {
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<Card> <Card className="shadow-sm">
<CardContent className="p-0"> <CardContent className="p-0">
<Table> <Table>
<TableHeader> <TableHeader>
@ -446,6 +450,7 @@ export default function ExternalConnectionsPage() {
connectionName={selectedConnection.connection_name} connectionName={selectedConnection.connection_name}
/> />
)} )}
</div>
</div> </div>
); );
} }

View File

@ -3,6 +3,12 @@
import MultiLang from "@/components/admin/MultiLang"; import MultiLang from "@/components/admin/MultiLang";
export default function I18nPage() { 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>
);
} }

View File

@ -220,19 +220,21 @@ export default function LayoutManagementPage() {
}; };
return ( return (
<div className="container mx-auto p-6"> <div className="min-h-screen bg-gray-50">
<div className="mb-6 flex items-center justify-between"> <div className="container mx-auto p-6 space-y-6">
<div> {/* 페이지 제목 */}
<h1 className="text-3xl font-bold"> </h1> <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<p className="text-gray-600"> .</p> <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 shadow-sm" onClick={() => setCreateModalOpen(true)}>
<Plus className="h-4 w-4" />
</Button>
</div> </div>
<Button className="flex items-center gap-2" onClick={() => setCreateModalOpen(true)}>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 검색 및 필터 */} {/* 검색 및 필터 */}
<Card className="mb-6"> <Card className="mb-6 shadow-sm">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex-1"> <div className="flex-1">
@ -282,7 +284,7 @@ export default function LayoutManagementPage() {
{layouts.map((layout) => { {layouts.map((layout) => {
const CategoryIcon = CATEGORY_ICONS[layout.category as keyof typeof CATEGORY_ICONS]; const CategoryIcon = CATEGORY_ICONS[layout.category as keyof typeof CATEGORY_ICONS];
return ( return (
<Card key={layout.layoutCode} className="transition-shadow hover:shadow-lg"> <Card key={layout.layoutCode} className="shadow-sm transition-shadow hover:shadow-lg">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -411,6 +413,7 @@ export default function LayoutManagementPage() {
loadCategoryCounts(); loadCategoryCounts();
}} }}
/> />
</div>
</div> </div>
); );
} }

View File

@ -4,8 +4,17 @@ import { MenuManagement } from "@/components/admin/MenuManagement";
export default function MenuPage() { export default function MenuPage() {
return ( return (
<div className="h-full"> <div className="min-h-screen bg-gray-50">
<MenuManagement /> <div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
</div>
</div>
<MenuManagement />
</div>
</div> </div>
); );
} }

View File

@ -5,17 +5,19 @@ import MonitoringDashboard from "@/components/admin/MonitoringDashboard";
export default function MonitoringPage() { export default function MonitoringPage() {
return ( return (
<div className="space-y-6"> <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> <div>
<p className="text-muted-foreground"> <h1 className="text-2xl font-bold"></h1>
. <p className="text-muted-foreground">
</p> .
</div> </p>
</div>
{/* 모니터링 대시보드 */} {/* 모니터링 대시보드 */}
<MonitoringDashboard /> <MonitoringDashboard />
</div>
</div> </div>
); );
} }

View File

@ -5,7 +5,8 @@ import Link from "next/link";
*/ */
export default function AdminPage() { export default function AdminPage() {
return ( 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"> <div className="mx-auto max-w-7xl grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Link href="/admin/userMng" className="block"> <Link href="/admin/userMng" className="block">
@ -162,6 +163,7 @@ export default function AdminPage() {
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
); );
} }

View File

@ -66,18 +66,27 @@ export default function ScreenManagementPage() {
const isLastStep = currentStep === "template"; const isLastStep = currentStep === "template";
return ( return (
<div className="flex h-full w-full flex-col"> <div className="min-h-screen bg-gray-50">
{/* 단계별 내용 */} <div className="container mx-auto p-6 space-y-6">
<div className="flex-1 overflow-hidden"> {/* 페이지 제목 */}
{/* 화면 목록 단계 */} <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
{currentStep === "list" && ( <div>
<div className="h-full p-6"> <h1 className="text-3xl font-bold text-gray-900"> </h1>
<div className="mb-6 flex items-center justify-between"> <p className="mt-2 text-gray-600"> 릿 </p>
<h2 className="text-2xl font-bold">{stepConfig.list.title}</h2> </div>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToNextStep("design")}> </div>
<ArrowRight className="ml-2 h-4 w-4" />
</Button> {/* 단계별 내용 */}
</div> <div className="flex-1 overflow-hidden">
{/* 화면 목록 단계 */}
{currentStep === "list" && (
<div className="space-y-6">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
<Button className="bg-blue-600 hover:bg-blue-700 shadow-sm" onClick={() => goToNextStep("design")}>
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
<ScreenList <ScreenList
onScreenSelect={setSelectedScreen} onScreenSelect={setSelectedScreen}
selectedScreen={selectedScreen} selectedScreen={selectedScreen}
@ -89,31 +98,38 @@ export default function ScreenManagementPage() {
</div> </div>
)} )}
{/* 화면 설계 단계 */} {/* 화면 설계 단계 */}
{currentStep === "design" && ( {currentStep === "design" && (
<div className="h-full"> <div className="space-y-6">
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} /> <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
</div> <h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
)} <Button variant="outline" className="shadow-sm" onClick={() => goToStep("list")}>
<ArrowLeft className="mr-2 h-4 w-4" />
{/* 템플릿 관리 단계 */}
{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>
<div className="flex gap-2">
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToStep("list")}>
</Button> </Button>
</div> </div>
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
</div> </div>
<TemplateManager selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} /> )}
</div>
)} {/* 템플릿 관리 단계 */}
{currentStep === "template" && (
<div className="space-y-6">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.template.title}</h2>
<div className="flex gap-2">
<Button variant="outline" className="shadow-sm" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 shadow-sm" onClick={() => goToStep("list")}>
</Button>
</div>
</div>
<TemplateManager selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
</div>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -203,7 +203,8 @@ export default function EditWebTypePage() {
} }
return ( 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"> <div className="mb-6 flex items-center gap-4">
<Link href={`/admin/standards/${webType}`}> <Link href={`/admin/standards/${webType}`}>
@ -502,6 +503,7 @@ export default function EditWebTypePage() {
</p> </p>
</div> </div>
)} )}
</div>
</div> </div>
); );
} }

View File

@ -80,7 +80,8 @@ export default function WebTypeDetailPage() {
} }
return ( 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="mb-6 flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@ -280,6 +281,7 @@ export default function WebTypeDetailPage() {
</Card> </Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div>
</div> </div>
); );
} }

View File

@ -159,7 +159,8 @@ export default function NewWebTypePage() {
}; };
return ( 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"> <div className="mb-6 flex items-center gap-4">
<Link href="/admin/standards"> <Link href="/admin/standards">
@ -453,6 +454,7 @@ export default function NewWebTypePage() {
</p> </p>
</div> </div>
)} )}
</div>
</div> </div>
); );
} }

View File

@ -127,46 +127,47 @@ export default function WebTypesManagePage() {
} }
return ( 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> <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<h1 className="text-3xl font-bold tracking-tight"> </h1> <div>
<p className="text-muted-foreground"> .</p> <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 className="shadow-sm">
<Plus className="mr-2 h-4 w-4" />
</Button>
</Link>
</div> </div>
<Link href="/admin/standards/new">
<Button>
<Plus className="mr-2 h-4 w-4" />
</Button>
</Link>
</div>
{/* 필터 및 검색 */} {/* 필터 및 검색 */}
<Card className="mb-6"> <Card className="shadow-sm">
<CardHeader> <CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="flex items-center gap-2 text-lg">
<Filter className="h-5 w-5" /> <Filter className="h-5 w-5 text-gray-600" />
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* 검색 */} {/* 검색 */}
<div className="relative"> <div className="relative">
<Search className="text-muted-foreground absolute top-3 left-3 h-4 w-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input <Input
placeholder="웹타입명, 설명 검색..." placeholder="웹타입명, 설명 검색..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10" className="pl-10"
/> />
</div> </div>
{/* 카테고리 필터 */} {/* 카테고리 필터 */}
<Select value={categoryFilter} onValueChange={setCategoryFilter}> <Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="카테고리 선택" /> <SelectValue placeholder="카테고리 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all"> </SelectItem> <SelectItem value="all"> </SelectItem>
{categories.map((category) => ( {categories.map((category) => (
@ -177,96 +178,96 @@ export default function WebTypesManagePage() {
</SelectContent> </SelectContent>
</Select> </Select>
{/* 활성화 상태 필터 */} {/* 활성화 상태 필터 */}
<Select value={activeFilter} onValueChange={setActiveFilter}> <Select value={activeFilter} onValueChange={setActiveFilter}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="상태 선택" /> <SelectValue placeholder="상태 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all"></SelectItem> <SelectItem value="all"></SelectItem>
<SelectItem value="Y"></SelectItem> <SelectItem value="Y"></SelectItem>
<SelectItem value="N"></SelectItem> <SelectItem value="N"></SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
{/* 초기화 버튼 */} {/* 초기화 버튼 */}
<Button variant="outline" onClick={resetFilters}> <Button variant="outline" onClick={resetFilters}>
<RotateCcw className="mr-2 h-4 w-4" /> <RotateCcw className="mr-2 h-4 w-4" />
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* 결과 통계 */} {/* 결과 통계 */}
<div className="mb-4"> <div className="bg-white rounded-lg border px-4 py-3">
<p className="text-muted-foreground text-sm"> {filteredAndSortedWebTypes.length} .</p> <p className="text-gray-700 text-sm font-medium"> {filteredAndSortedWebTypes.length} .</p>
</div> </div>
{/* 웹타입 목록 테이블 */} {/* 웹타입 목록 테이블 */}
<Card> <Card className="shadow-sm">
<CardContent className="p-0"> <CardContent className="p-0">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("sort_order")}> <TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("sort_order")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sortField === "sort_order" && {sortField === "sort_order" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)} (sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div> </div>
</TableHead> </TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("web_type")}> <TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("web_type")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sortField === "web_type" && {sortField === "web_type" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)} (sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div> </div>
</TableHead> </TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("type_name")}> <TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("type_name")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sortField === "type_name" && {sortField === "type_name" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)} (sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div> </div>
</TableHead> </TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("category")}> <TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("category")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sortField === "category" && {sortField === "category" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)} (sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div> </div>
</TableHead> </TableHead>
<TableHead></TableHead> <TableHead></TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("component_name")}> <TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("component_name")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sortField === "component_name" && {sortField === "component_name" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)} (sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div> </div>
</TableHead> </TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}> <TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sortField === "config_panel" && {sortField === "config_panel" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)} (sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div> </div>
</TableHead> </TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}> <TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sortField === "is_active" && {sortField === "is_active" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)} (sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div> </div>
</TableHead> </TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("updated_date")}> <TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("updated_date")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sortField === "updated_date" && {sortField === "updated_date" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)} (sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div> </div>
</TableHead> </TableHead>
<TableHead className="text-center"></TableHead> <TableHead className="text-center"></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@ -309,24 +310,24 @@ export default function WebTypesManagePage() {
<TableCell className="text-muted-foreground text-sm"> <TableCell className="text-muted-foreground text-sm">
{webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"} {webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link href={`/admin/standards/${webType.web_type}`}> <Link href={`/admin/standards/${webType.web_type}`}>
<Button variant="ghost" size="sm"> <Button variant="ghost" size="sm">
<Eye className="h-4 w-4" /> <Eye className="h-4 w-4" />
</Button> </Button>
</Link> </Link>
<Link href={`/admin/standards/${webType.web_type}/edit`}> <Link href={`/admin/standards/${webType.web_type}/edit`}>
<Button variant="ghost" size="sm"> <Button variant="ghost" size="sm">
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
</Link> </Link>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="ghost" size="sm"> <Button variant="ghost" size="sm">
<Trash2 className="h-4 w-4 text-red-500" /> <Trash2 className="h-4 w-4 text-red-500" />
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle> <AlertDialogTitle> </AlertDialogTitle>
@ -364,6 +365,7 @@ export default function WebTypesManagePage() {
</p> </p>
</div> </div>
)} )}
</div>
</div> </div>
); );
} }

View File

@ -541,9 +541,9 @@ export default function TableManagementPage() {
}, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]); }, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]);
return ( 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 className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900"> <h1 className="text-3xl font-bold text-gray-900">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")} {getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
@ -593,10 +593,10 @@ export default function TableManagementPage() {
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-5">
{/* 테이블 목록 */} {/* 테이블 목록 */}
<Card className="lg:col-span-1"> <Card className="lg:col-span-1 shadow-sm">
<CardHeader> <CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" /> <Database className="h-5 w-5 text-gray-600" />
{getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")} {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -663,10 +663,10 @@ export default function TableManagementPage() {
</Card> </Card>
{/* 컬럼 타입 관리 */} {/* 컬럼 타입 관리 */}
<Card className="lg:col-span-4"> <Card className="lg:col-span-4 shadow-sm">
<CardHeader> <CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" /> <Settings className="h-5 w-5 text-gray-600" />
{selectedTable ? <> - {selectedTable}</> : "테이블 타입 관리"} {selectedTable ? <> - {selectedTable}</> : "테이블 타입 관리"}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>

View File

@ -145,27 +145,28 @@ export default function TemplatesManagePage() {
} }
return ( 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> <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<h1 className="text-3xl font-bold">릿 </h1> <div>
<p className="text-muted-foreground"> 릿 .</p> <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 className="shadow-sm">
<Link href="/admin/templates/new">
<Plus className="mr-2 h-4 w-4" /> 릿
</Link>
</Button>
</div>
</div> </div>
<div className="flex space-x-2">
<Button asChild>
<Link href="/admin/templates/new">
<Plus className="mr-2 h-4 w-4" /> 릿
</Link>
</Button>
</div>
</div>
{/* 필터 및 검색 */} {/* 필터 및 검색 */}
<Card> <Card className="shadow-sm">
<CardHeader> <CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center"> <CardTitle className="flex items-center">
<Filter className="mr-2 h-5 w-5" /> <Filter className="mr-2 h-5 w-5 text-gray-600" />
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -230,8 +231,8 @@ export default function TemplatesManagePage() {
</Card> </Card>
{/* 템플릿 목록 테이블 */} {/* 템플릿 목록 테이블 */}
<Card> <Card className="shadow-sm">
<CardHeader> <CardHeader className="bg-gray-50/50">
<CardTitle>릿 ({filteredAndSortedTemplates.length})</CardTitle> <CardTitle>릿 ({filteredAndSortedTemplates.length})</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -390,6 +391,7 @@ export default function TemplatesManagePage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div>
</div> </div>
); );
} }

View File

@ -8,8 +8,17 @@ import { UserManagement } from "@/components/admin/UserManagement";
*/ */
export default function UserMngPage() { export default function UserMngPage() {
return ( return (
<div className="h-full"> <div className="min-h-screen bg-gray-50">
<UserManagement /> <div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
</div>
</div>
<UserManagement />
</div>
</div> </div>
); );
} }

View File

@ -54,7 +54,7 @@ const TEST_COMPONENTS: ComponentData[] = [
required: true, required: true,
style: { style: {
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
}, },
} as WidgetComponent, } as WidgetComponent,
@ -72,7 +72,7 @@ const TEST_COMPONENTS: ComponentData[] = [
required: true, required: true,
style: { style: {
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
}, },
} as WidgetComponent, } as WidgetComponent,
@ -94,7 +94,7 @@ const TEST_COMPONENTS: ComponentData[] = [
}, },
style: { style: {
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
}, },
} as WidgetComponent, } as WidgetComponent,
@ -112,7 +112,7 @@ const TEST_COMPONENTS: ComponentData[] = [
required: false, required: false,
style: { style: {
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
}, },
} as WidgetComponent, } as WidgetComponent,
@ -130,7 +130,7 @@ const TEST_COMPONENTS: ComponentData[] = [
required: false, required: false,
style: { style: {
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
}, },
} as WidgetComponent, } as WidgetComponent,
@ -152,7 +152,7 @@ const TEST_COMPONENTS: ComponentData[] = [
}, },
style: { style: {
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
}, },
} as WidgetComponent, } as WidgetComponent,

View File

@ -237,7 +237,7 @@ export default function ScreenViewPage() {
const labelText = component.style?.labelText || component.label || ""; const labelText = component.style?.labelText || component.label || "";
const labelStyle = { const labelStyle = {
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: component.style?.labelFontWeight || "500", fontWeight: component.style?.labelFontWeight || "500",
backgroundColor: component.style?.labelBackgroundColor || "transparent", backgroundColor: component.style?.labelBackgroundColor || "transparent",
padding: component.style?.labelPadding || "0", padding: component.style?.labelPadding || "0",

View File

@ -821,8 +821,11 @@ export const MenuManagement: React.FC = () => {
{/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */} {/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
<div className="w-[20%] border-r bg-gray-50"> <div className="w-[20%] border-r bg-gray-50">
<div className="p-6"> <div className="p-6">
<h2 className="mb-4 text-lg font-semibold">{getUITextSync("menu.type.title")}</h2> <Card className="shadow-sm">
<div className="space-y-3"> <CardHeader className="bg-gray-50/50 pb-3">
<CardTitle className="text-lg">{getUITextSync("menu.type.title")}</CardTitle>
</CardHeader>
<CardContent className="space-y-3 pt-4">
<Card <Card
className={`cursor-pointer transition-all ${ className={`cursor-pointer transition-all ${
selectedMenuType === "admin" ? "border-blue-500 bg-blue-50" : "hover:border-gray-300" selectedMenuType === "admin" ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
@ -864,21 +867,23 @@ export const MenuManagement: React.FC = () => {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </CardContent>
</Card>
</div> </div>
</div> </div>
{/* 우측 메인 영역 - 메뉴 목록 (80%) */} {/* 우측 메인 영역 - 메뉴 목록 (80%) */}
<div className="w-[80%] overflow-hidden"> <div className="w-[80%] overflow-hidden">
<div className="flex h-full flex-col p-6"> <div className="flex h-full flex-col p-6">
<div className="mb-6 flex-shrink-0"> <Card className="flex-1 shadow-sm">
<h2 className="mb-2 text-xl font-semibold"> <CardHeader className="bg-gray-50/50">
{getMenuTypeString()} {getUITextSync("menu.list.title")} <CardTitle className="text-xl">
</h2> {getMenuTypeString()} {getUITextSync("menu.list.title")}
</div> </CardTitle>
</CardHeader>
{/* 검색 및 필터 영역 */} <CardContent className="flex-1 overflow-hidden">
<div className="mb-4 flex-shrink-0"> {/* 검색 및 필터 영역 */}
<div className="mb-4 flex-shrink-0">
<div className="grid grid-cols-1 gap-4 md:grid-cols-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<div> <div>
<Label htmlFor="company">{getUITextSync("filter.company")}</Label> <Label htmlFor="company">{getUITextSync("filter.company")}</Label>
@ -997,52 +1002,54 @@ export const MenuManagement: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })} {getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]"> <Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
{getUITextSync("button.add.top.level")} {getUITextSync("button.add.top.level")}
</Button> </Button>
{selectedMenus.size > 0 && ( {selectedMenus.size > 0 && (
<Button <Button
variant="destructive" variant="destructive"
onClick={handleDeleteSelectedMenus} onClick={handleDeleteSelectedMenus}
disabled={deleting} disabled={deleting}
className="min-w-[120px]" className="min-w-[120px]"
> >
{deleting ? ( {deleting ? (
<> <>
<LoadingSpinner size="sm" className="mr-2" /> <LoadingSpinner size="sm" className="mr-2" />
{getUITextSync("button.delete.processing")} {getUITextSync("button.delete.processing")}
</> </>
) : ( ) : (
getUITextSync("button.delete.selected.count", { getUITextSync("button.delete.selected.count", {
count: selectedMenus.size, count: selectedMenus.size,
}) })
)}
</Button>
)} )}
</Button> </div>
)} </div>
<MenuTable
menus={getCurrentMenus()}
title=""
onAddMenu={handleAddMenu}
onEditMenu={handleEditMenu}
onToggleStatus={handleToggleStatus}
selectedMenus={selectedMenus}
onMenuSelectionChange={handleMenuSelectionChange}
onSelectAllMenus={handleSelectAllMenus}
expandedMenus={expandedMenus}
onToggleExpand={handleToggleExpand}
uiTexts={uiTexts}
/>
</div> </div>
</div> </CardContent>
<MenuTable </Card>
menus={getCurrentMenus()}
title=""
onAddMenu={handleAddMenu}
onEditMenu={handleEditMenu}
onToggleStatus={handleToggleStatus}
selectedMenus={selectedMenus}
onMenuSelectionChange={handleMenuSelectionChange}
onSelectAllMenus={handleSelectAllMenus}
expandedMenus={expandedMenus}
onToggleExpand={handleToggleExpand}
uiTexts={uiTexts}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -1050,8 +1057,15 @@ export const MenuManagement: React.FC = () => {
</TabsContent> </TabsContent>
{/* 화면 할당 탭 */} {/* 화면 할당 탭 */}
<TabsContent value="screen-assignment" className="flex-1 overflow-hidden"> <TabsContent value="screen-assignment" className="flex-1 overflow-hidden p-6">
<ScreenAssignmentTab menus={[...adminMenus, ...userMenus]} /> <Card className="h-full shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="h-full overflow-hidden">
<ScreenAssignmentTab menus={[...adminMenus, ...userMenus]} />
</CardContent>
</Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, Suspense } from "react"; import { useState, Suspense, useEffect } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -197,8 +197,27 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { user, logout, refreshUserData } = useAuth(); const { user, logout, refreshUserData } = useAuth();
const { userMenus, adminMenus, loading, refreshMenus } = useMenu(); const { userMenus, adminMenus, loading, refreshMenus } = useMenu();
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(true);
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set()); const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
const [isMobile, setIsMobile] = useState(false);
// 화면 크기 감지 및 사이드바 초기 상태 설정
useEffect(() => {
const checkIsMobile = () => {
const mobile = window.innerWidth < 1024; // lg 브레이크포인트
setIsMobile(mobile);
// 모바일에서만 사이드바를 닫음
if (mobile) {
setSidebarOpen(false);
} else {
setSidebarOpen(true);
}
};
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
return () => window.removeEventListener('resize', checkIsMobile);
}, []);
// 프로필 관련 로직 // 프로필 관련 로직
const { const {
@ -253,15 +272,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
? `/screens/${firstScreen.screenId}?mode=admin` ? `/screens/${firstScreen.screenId}?mode=admin`
: `/screens/${firstScreen.screenId}`; : `/screens/${firstScreen.screenId}`;
console.log("🎯 메뉴에서 화면으로 이동:", {
menuName: menu.name,
screenId: firstScreen.screenId,
isAdminMode,
targetPath: screenPath,
});
router.push(screenPath); router.push(screenPath);
setSidebarOpen(false); if (isMobile) {
setSidebarOpen(false);
}
return; return;
} }
} catch (error) { } catch (error) {
@ -271,10 +285,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
// 할당된 화면이 없고 URL이 있으면 기존 URL로 이동 // 할당된 화면이 없고 URL이 있으면 기존 URL로 이동
if (menu.url && menu.url !== "#") { if (menu.url && menu.url !== "#") {
router.push(menu.url); router.push(menu.url);
setSidebarOpen(false); if (isMobile) {
setSidebarOpen(false);
}
} else { } else {
// URL도 없고 할당된 화면도 없으면 경고 메시지 // URL도 없고 할당된 화면도 없으면 경고 메시지
console.warn("메뉴에 URL이나 할당된 화면이 없습니다:", menu);
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다."); toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
} }
} }
@ -295,7 +310,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
await logout(); await logout();
router.push("/login"); router.push("/login");
} catch (error) { } catch (error) {
console.error("로그아웃 실패:", error); // 로그아웃 실패 시 처리
} }
}; };
@ -306,7 +321,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
return ( return (
<div key={menu.id}> <div key={menu.id}>
<div <div
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:cursor-pointer ${ className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out h-10 ${
pathname === menu.url pathname === menu.url
? "bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900 border-l-4 border-blue-500" ? "bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900 border-l-4 border-blue-500"
: isExpanded : isExpanded
@ -315,9 +330,9 @@ function AppLayoutInner({ children }: AppLayoutProps) {
} ${level > 0 ? "ml-6" : ""}`} } ${level > 0 ? "ml-6" : ""}`}
onClick={() => handleMenuClick(menu)} onClick={() => handleMenuClick(menu)}
> >
<div className="flex items-center"> <div className="flex items-center min-w-0 flex-1">
{menu.icon} {menu.icon}
<span className="ml-3">{menu.name}</span> <span className="ml-3 truncate" title={menu.name}>{menu.name}</span>
</div> </div>
{menu.hasChildren && ( {menu.hasChildren && (
<div className="ml-auto"> <div className="ml-auto">
@ -339,8 +354,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
}`} }`}
onClick={() => handleMenuClick(child)} onClick={() => handleMenuClick(child)}
> >
{child.icon} <div className="flex items-center min-w-0 flex-1">
<span className="ml-3">{child.name}</span> {child.icon}
<span className="ml-3 truncate" title={child.name}>{child.name}</span>
</div>
</div> </div>
))} ))}
</div> </div>
@ -369,22 +386,29 @@ function AppLayoutInner({ children }: AppLayoutProps) {
{/* MainHeader 컴포넌트 사용 */} {/* MainHeader 컴포넌트 사용 */}
<MainHeader <MainHeader
user={user} user={user}
onSidebarToggle={() => setSidebarOpen(!sidebarOpen)} onSidebarToggle={() => {
// 모바일에서만 토글 동작
if (isMobile) {
setSidebarOpen(!sidebarOpen);
}
}}
onProfileClick={openProfileModal} onProfileClick={openProfileModal}
onLogout={handleLogout} onLogout={handleLogout}
/> />
<div className="flex flex-1"> <div className="flex flex-1">
{/* 모바일 사이드바 오버레이 */} {/* 모바일 사이드바 오버레이 */}
{sidebarOpen && ( {sidebarOpen && isMobile && (
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} /> <div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} />
)} )}
{/* 왼쪽 사이드바 */} {/* 왼쪽 사이드바 */}
<aside <aside
className={`${ className={`${
sidebarOpen ? "translate-x-0" : "-translate-x-full" isMobile
} fixed top-14 left-0 z-40 flex h-full w-64 flex-col border-r border-slate-200 bg-white transition-transform duration-300 lg:relative lg:top-0 lg:z-auto lg:h-full lg:translate-x-0 lg:transform-none`} ? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-14 left-0 z-40"
: "translate-x-0 relative top-0 z-auto"
} flex h-full w-72 min-w-72 max-w-72 flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
> >
{/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */} {/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */}
{(user as ExtendedUserInfo)?.userType === "admin" && ( {(user as ExtendedUserInfo)?.userType === "admin" && (
@ -428,7 +452,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
</aside> </aside>
{/* 가운데 컨텐츠 영역 */} {/* 가운데 컨텐츠 영역 */}
<main className="flex-1 bg-white">{children}</main> <main className="flex-1 min-w-0 bg-white overflow-hidden">{children}</main>
</div> </div>
{/* 프로필 수정 모달 */} {/* 프로필 수정 모달 */}

View File

@ -232,7 +232,7 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
className={`mb-1 block text-sm font-medium ${hasError ? "text-destructive" : ""}`} className={`mb-1 block text-sm font-medium ${hasError ? "text-destructive" : ""}`}
style={{ style={{
fontSize: labelStyle.labelFontSize || "14px", fontSize: labelStyle.labelFontSize || "14px",
color: hasError ? "#ef4444" : labelStyle.labelColor || "#374151", color: hasError ? "#ef4444" : labelStyle.labelColor || "#3b83f6",
fontWeight: labelStyle.labelFontWeight || "500", fontWeight: labelStyle.labelFontWeight || "500",
fontFamily: labelStyle.labelFontFamily, fontFamily: labelStyle.labelFontFamily,
textAlign: labelStyle.labelTextAlign || "left", textAlign: labelStyle.labelTextAlign || "left",

View File

@ -1708,7 +1708,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
// 라벨 스타일 적용 // 라벨 스타일 적용
const labelStyle = { const labelStyle = {
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: component.style?.labelFontWeight || "500", fontWeight: component.style?.labelFontWeight || "500",
backgroundColor: component.style?.labelBackgroundColor || "transparent", backgroundColor: component.style?.labelBackgroundColor || "transparent",
padding: component.style?.labelPadding || "0", padding: component.style?.labelPadding || "0",

View File

@ -32,6 +32,7 @@ interface RealtimePreviewProps {
selectedScreen?: any; selectedScreen?: any;
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러 onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러
onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러 onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러
onConfigChange?: (config: any) => void; // 설정 변경 핸들러
} }
// 동적 위젯 타입 아이콘 (레지스트리에서 조회) // 동적 위젯 타입 아이콘 (레지스트리에서 조회)
@ -73,6 +74,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
selectedScreen, selectedScreen,
onZoneComponentDrop, onZoneComponentDrop,
onZoneClick, onZoneClick,
onConfigChange,
}) => { }) => {
const { id, type, position, size, style: componentStyle } = component; const { id, type, position, size, style: componentStyle } = component;
@ -89,8 +91,12 @@ 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: `${size?.width || 100}px`, width: component.componentConfig?.type === "table-list"
height: `${size?.height || 36}px`, ? `${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 이상 zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
...componentStyle, ...componentStyle,
}; };
@ -120,7 +126,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
{/* 동적 컴포넌트 렌더링 */} {/* 동적 컴포넌트 렌더링 */}
<div className="h-full w-full"> <div className={`h-full w-full ${
component.componentConfig?.type === "table-list" ? "overflow-visible" : ""
}`}>
<DynamicComponentRenderer <DynamicComponentRenderer
component={component} component={component}
isSelected={isSelected} isSelected={isSelected}
@ -133,6 +141,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
selectedScreen={selectedScreen} selectedScreen={selectedScreen}
onZoneComponentDrop={onZoneComponentDrop} onZoneComponentDrop={onZoneComponentDrop}
onZoneClick={onZoneClick} onZoneClick={onZoneClick}
onConfigChange={onConfigChange}
/> />
</div> </div>

View File

@ -1004,7 +1004,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: true, labelDisplay: true,
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "600", labelFontWeight: "600",
labelMarginBottom: "8px", labelMarginBottom: "8px",
...templateComp.style, ...templateComp.style,
@ -1083,7 +1083,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: true, labelDisplay: true,
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "600", labelFontWeight: "600",
labelMarginBottom: "8px", labelMarginBottom: "8px",
...templateComp.style, ...templateComp.style,
@ -1134,7 +1134,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: true, labelDisplay: true,
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "600", labelFontWeight: "600",
labelMarginBottom: "8px", labelMarginBottom: "8px",
...templateComp.style, ...templateComp.style,
@ -1185,7 +1185,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: true, labelDisplay: true,
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "600", labelFontWeight: "600",
labelMarginBottom: "8px", labelMarginBottom: "8px",
...templateComp.style, ...templateComp.style,
@ -1274,7 +1274,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: true, labelDisplay: true,
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "600", labelFontWeight: "600",
labelMarginBottom: "8px", labelMarginBottom: "8px",
...templateComp.style, ...templateComp.style,
@ -1564,7 +1564,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
labelMarginBottom: "4px", labelMarginBottom: "4px",
}, },
@ -1653,7 +1653,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: true, labelDisplay: true,
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "600", labelFontWeight: "600",
labelMarginBottom: "8px", labelMarginBottom: "8px",
}, },
@ -1844,7 +1844,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정
labelFontSize: "12px", labelFontSize: "12px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
labelMarginBottom: "6px", labelMarginBottom: "6px",
}, },
@ -1887,7 +1887,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
style: { style: {
labelDisplay: true, // 테이블 패널에서 드래그한 컴포넌트는 라벨을 기본적으로 표시 labelDisplay: true, // 테이블 패널에서 드래그한 컴포넌트는 라벨을 기본적으로 표시
labelFontSize: "12px", labelFontSize: "12px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
labelMarginBottom: "6px", labelMarginBottom: "6px",
}, },
@ -3158,11 +3158,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
{/* 실제 작업 캔버스 (해상도 크기) */} {/* 실제 작업 캔버스 (해상도 크기) */}
<div <div
className="mx-auto bg-white shadow-lg" 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 <div
ref={canvasRef} 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) => { onClick={(e) => {
if (e.target === e.currentTarget && !selectionDrag.wasSelecting) { if (e.target === e.currentTarget && !selectionDrag.wasSelecting) {
setSelectedComponent(null); setSelectedComponent(null);
@ -3271,6 +3275,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
selectedScreen={selectedScreen} selectedScreen={selectedScreen}
// onZoneComponentDrop 제거 // onZoneComponentDrop 제거
onZoneClick={handleZoneClick} onZoneClick={handleZoneClick}
// 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영)
onConfigChange={(config) => {
console.log("📤 테이블 설정 변경을 상세설정에 알림:", config);
// 여기서 DetailSettingsPanel의 상태를 업데이트하거나
// 컴포넌트의 componentConfig를 업데이트할 수 있습니다
// TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
}}
> >
{/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */}
{(component.type === "group" || component.type === "container" || component.type === "area") && {(component.type === "group" || component.type === "container" || component.type === "area") &&
@ -3351,6 +3362,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
selectedScreen={selectedScreen} selectedScreen={selectedScreen}
// onZoneComponentDrop 제거 // onZoneComponentDrop 제거
onZoneClick={handleZoneClick} onZoneClick={handleZoneClick}
// 설정 변경 핸들러 (자식 컴포넌트용)
onConfigChange={(config) => {
console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config);
// TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
}}
/> />
); );
})} })}

View File

@ -134,7 +134,25 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
<Select <Select
value={config.action?.type || "save"} value={config.action?.type || "save"}
defaultValue="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> <SelectTrigger>
<SelectValue placeholder="버튼 액션 선택" /> <SelectValue placeholder="버튼 액션 선택" />

View File

@ -240,12 +240,6 @@ export const AdvancedSearchFilters: React.FC<AdvancedSearchFiltersProps> = ({
); );
case "code": case "code":
console.log("🔍 코드 필터 렌더링:", {
columnName: filter.columnName,
codeCategory: filter.codeCategory,
options: codeOptions[filter.codeCategory || ""],
loading: loadingStates[filter.codeCategory || ""],
});
return ( return (
<CodeFilter <CodeFilter
key={filter.columnName} key={filter.columnName}

View File

@ -1006,7 +1006,12 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
<DynamicComponentConfigPanel <DynamicComponentConfigPanel
componentId={componentId} 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} screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
tableColumns={(() => { tableColumns={(() => {
console.log("🔍 DetailSettingsPanel tableColumns 전달:", { console.log("🔍 DetailSettingsPanel tableColumns 전달:", {

View File

@ -203,7 +203,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
: "1"), : "1"),
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "", labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
labelFontSize: selectedComponent?.style?.labelFontSize || "12px", labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
labelColor: selectedComponent?.style?.labelColor || "#374151", labelColor: selectedComponent?.style?.labelColor || "#3b83f6",
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px", labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
required: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).required : false) || false, required: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).required : false) || false,
readonly: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).readonly : false) || false, readonly: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).readonly : false) || false,
@ -261,7 +261,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
: "1"), : "1"),
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "", labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
labelFontSize: selectedComponent?.style?.labelFontSize || "12px", labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
labelColor: selectedComponent?.style?.labelColor || "#374151", labelColor: selectedComponent?.style?.labelColor || "#3b83f6",
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px", labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
required: widget?.required || false, required: widget?.required || false,
readonly: widget?.readonly || false, readonly: widget?.readonly || false,
@ -285,6 +285,84 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
dragState?.justFinishedDrag, // 드래그 완료 직후 감지 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) { if (dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id) {
console.log("🎯 렌더링 중 드래그 상태 감지:", { console.log("🎯 렌더링 중 드래그 상태 감지:", {

View File

@ -56,6 +56,9 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
const totalRequests = useRef(0); const totalRequests = useRef(0);
const cacheHits = useRef(0); const cacheHits = useRef(0);
const batchLoadCount = useRef(0); const batchLoadCount = useRef(0);
// 변환된 값 캐시 (중복 변환 방지)
const convertedCache = useRef(new Map<string, string>());
// 공통 코드 카테고리 추출 (메모이제이션) // 공통 코드 카테고리 추출 (메모이제이션)
const codeCategories = useMemo(() => { const codeCategories = useMemo(() => {
@ -175,29 +178,41 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
const startTime = Date.now(); const startTime = Date.now();
totalRequests.current += 1; 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); 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(`🔍 캐시 키값들:`, Object.keys(syncResult));
console.log(`🔍 캐시 전체 데이터:`, JSON.stringify(syncResult, null, 2)); console.log(`🔍 캐시 전체 데이터:`, JSON.stringify(syncResult, null, 2));
} }
if (syncResult && Array.isArray(syncResult)) { if (syncResult && Array.isArray(syncResult)) {
cacheHits.current += 1; cacheHits.current += 1;
console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`); if (totalRequests.current % 10 === 1) {
console.log( console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`);
`🔍 캐시 배열 내용:`, console.log(
syncResult.map((item) => ({ `🔍 캐시 배열 내용:`,
code_value: item.code_value, syncResult.map((item) => ({
code_name: item.code_name, code_value: item.code_value,
})), code_name: item.code_name,
); })),
);
}
// 배열에서 해당 code_value를 가진 항목 찾기 // 배열에서 해당 code_value를 가진 항목 찾기
const foundCode = syncResult.find( const foundCode = syncResult.find(
@ -205,7 +220,13 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
); );
const result = foundCode ? foundCode.code_name : codeValue; 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); requestTimes.current.push(Date.now() - startTime);

View File

@ -217,7 +217,7 @@ export class AutoRegisteringComponentRenderer {
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
marginBottom: component.style?.labelMarginBottom || "4px", marginBottom: component.style?.labelMarginBottom || "4px",
fontWeight: "500", fontWeight: "500",
}; };

View File

@ -34,6 +34,8 @@ export interface ComponentRenderer {
refreshKey?: number; refreshKey?: number;
// 편집 모드 // 편집 모드
mode?: "view" | "edit"; mode?: "view" | "edit";
// 설정 변경 핸들러 (상세설정과 연동)
onConfigChange?: (config: any) => void;
[key: string]: any; [key: string]: any;
}): React.ReactElement; }): React.ReactElement;
} }
@ -170,6 +172,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
selectedRowsData, selectedRowsData,
onSelectedRowsChange, onSelectedRowsChange,
refreshKey, refreshKey,
onConfigChange,
...safeProps ...safeProps
} = props; } = props;
@ -224,6 +227,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
selectedRows={selectedRows} selectedRows={selectedRows}
selectedRowsData={selectedRowsData} selectedRowsData={selectedRowsData}
onSelectedRowsChange={onSelectedRowsChange} onSelectedRowsChange={onSelectedRowsChange}
// 설정 변경 핸들러 전달
onConfigChange={onConfigChange}
refreshKey={refreshKey} refreshKey={refreshKey}
/> />
); );

View File

@ -661,7 +661,7 @@ export const AccordionBasicComponent: React.FC<AccordionBasicComponentProps> = (
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -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 = { const componentConfig = {
...config, ...config,
...component.config, ...component.config,
} as ButtonPrimaryConfig; } 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에서 문자열로 저장된 액션을 객체로 변환 // 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환
const processedConfig = { ...componentConfig }; const processedConfig = { ...componentConfig };
if (componentConfig.action && typeof componentConfig.action === "string") { if (componentConfig.action && typeof componentConfig.action === "string") {
@ -368,26 +425,29 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
style={{ style={{
width: "100%", width: "100%",
height: "100%", height: "100%",
minHeight: "100%", // 최소 높이 강제 적용 minHeight: "100%",
maxHeight: "100%", // 최대 높이 제한 maxHeight: "100%",
border: "1px solid #3b82f6", border: "none",
borderRadius: "4px", borderRadius: "8px",
backgroundColor: "#3b82f6", background: componentConfig.disabled
color: "white", ? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
color: componentConfig.disabled ? "#9ca3af" : "white",
fontSize: "14px", fontSize: "14px",
fontWeight: "500", fontWeight: "600",
cursor: componentConfig.disabled ? "not-allowed" : "pointer", cursor: componentConfig.disabled ? "not-allowed" : "pointer",
outline: "none", outline: "none",
boxSizing: "border-box", // 패딩/보더 포함 크기 계산 boxSizing: "border-box",
display: "flex", // flex로 변경 display: "flex",
alignItems: "center", // 세로 중앙 정렬 alignItems: "center",
justifyContent: "center", // 가로 중앙 정렬 justifyContent: "center",
padding: "0", // 패딩 제거 padding: "0 16px",
margin: "0", // 마진 제거 margin: "0",
lineHeight: "1", // 라인 높이 고정 lineHeight: "1",
// 강제 높이 적용
minHeight: "36px", 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 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
}} }}

View File

@ -84,7 +84,7 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
@ -141,7 +141,7 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
/> />
<span <span
style={{ style={{
color: "#374151", color: "#3b83f6",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
}} }}

View File

@ -298,7 +298,7 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -81,7 +81,7 @@ export const DividerLineComponent: React.FC<DividerLineComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -84,7 +84,7 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),

View File

@ -81,7 +81,7 @@ export const ImageDisplayComponent: React.FC<ImageDisplayComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -91,7 +91,7 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -84,7 +84,7 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
@ -155,7 +155,7 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
/> />
<span <span
style={{ style={{
color: "#374151", color: "#3b83f6",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
}} }}

View File

@ -601,7 +601,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),

View File

@ -84,7 +84,7 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
@ -149,7 +149,7 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
width: "30%", width: "30%",
textAlign: "center", textAlign: "center",
fontSize: "14px", fontSize: "14px",
color: "#374151", color: "#3b83f6",
fontWeight: "500", fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),

View File

@ -22,7 +22,7 @@ interface SingleTableWithStickyProps {
renderCheckboxCell: (row: any, index: number) => React.ReactNode; renderCheckboxCell: (row: any, index: number) => React.ReactNode;
formatCellValue: (value: any, format?: string, columnName?: string) => string; formatCellValue: (value: any, format?: string, columnName?: string) => string;
getColumnWidth: (column: ColumnConfig) => number; getColumnWidth: (column: ColumnConfig) => number;
joinColumnMapping: Record<string, string>; // 조인 컬럼 매핑 추가 containerWidth?: string; // 컨테이너 너비 설정
} }
export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
@ -40,13 +40,28 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
renderCheckboxCell, renderCheckboxCell,
formatCellValue, formatCellValue,
getColumnWidth, getColumnWidth,
joinColumnMapping, containerWidth,
}) => { }) => {
const checkboxConfig = tableConfig.checkbox || {}; const checkboxConfig = tableConfig.checkbox || {};
return ( return (
<div className="relative h-full w-full overflow-auto"> <div
<Table className="w-full"> 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" : ""}> <TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
<TableRow> <TableRow>
{visibleColumns.map((column, colIndex) => { {visibleColumns.map((column, colIndex) => {
@ -66,7 +81,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
return ( return (
<TableHead <TableHead
key={`sticky-header-${colIndex}-${column.columnName}`} key={column.columnName}
className={cn( className={cn(
column.columnName === "__checkbox__" column.columnName === "__checkbox__"
? "h-10 border-b px-4 py-2 text-center align-middle" ? "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), width: getColumnWidth(column),
minWidth: getColumnWidth(column), minWidth: getColumnWidth(column),
maxWidth: getColumnWidth(column), maxWidth: getColumnWidth(column),
boxSizing: "border-box",
overflow: "hidden",
textOverflow: "ellipsis",
// sticky 위치 설정 // sticky 위치 설정
...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "left" && { left: leftFixedWidth }),
...(column.fixed === "right" && { right: rightFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }),
@ -92,7 +110,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{column.columnName === "__checkbox__" ? ( {column.columnName === "__checkbox__" ? (
checkboxConfig.selectAll && ( 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) => ( data.map((row, index) => (
<TableRow <TableRow
key={`sticky-row-${index}`} key={`row-${index}`}
className={cn( className={cn(
"h-10 cursor-pointer border-b leading-none", "h-10 cursor-pointer border-b leading-none",
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50", tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
@ -157,7 +175,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
return ( return (
<TableCell <TableCell
key={`sticky-cell-${index}-${colIndex}-${column.columnName}`} key={`cell-${column.columnName}`}
className={cn( className={cn(
"h-10 px-4 py-2 align-middle text-sm whitespace-nowrap", "h-10 px-4 py-2 align-middle text-sm whitespace-nowrap",
`text-${column.align}`, `text-${column.align}`,
@ -169,6 +187,11 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
minHeight: "40px", minHeight: "40px",
height: "40px", height: "40px",
verticalAlign: "middle", verticalAlign: "middle",
width: getColumnWidth(column),
boxSizing: "border-box",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
// sticky 위치 설정 // sticky 위치 설정
...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "left" && { left: leftFixedWidth }),
...(column.fixed === "right" && { right: rightFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }),
@ -176,25 +199,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
> >
{column.columnName === "__checkbox__" {column.columnName === "__checkbox__"
? renderCheckboxCell(row, index) ? renderCheckboxCell(row, index)
: (() => { : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
// 🎯 매핑된 컬럼명으로 데이터 찾기 (기본 테이블과 동일한 로직)
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";
})()}
</TableCell> </TableCell>
); );
})} })}

View File

@ -33,7 +33,13 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
tableColumns, tableColumns,
}) => { }) => {
console.log("🔍 TableListConfigPanel props:", { 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, screenTableName,
tableColumns: tableColumns?.length, tableColumns: tableColumns?.length,
tableColumnsSample: tableColumns?.[0], tableColumnsSample: tableColumns?.[0],
@ -210,13 +216,25 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
}; };
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => { 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; const parentValue = config[parentKey] as any;
onChange({ const newConfig = {
[parentKey]: { [parentKey]: {
...parentValue, ...parentValue,
[childKey]: value, [childKey]: value,
}, },
}); };
console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
onChange(newConfig);
}; };
// 컬럼 추가 // 컬럼 추가

View File

@ -13,9 +13,27 @@ export class TableListRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = TableListDefinition; static componentDefinition = TableListDefinition;
render(): React.ReactElement { 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 });
};
/** /**
* *
*/ */

View File

@ -81,7 +81,7 @@ export const TestInputComponent: React.FC<TestInputComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -72,7 +72,7 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
const textStyle: React.CSSProperties = { const textStyle: React.CSSProperties = {
fontSize: componentConfig.fontSize || "14px", fontSize: componentConfig.fontSize || "14px",
fontWeight: componentConfig.fontWeight || "normal", fontWeight: componentConfig.fontWeight || "normal",
color: componentConfig.color || "#374151", color: componentConfig.color || "#3b83f6",
textAlign: componentConfig.textAlign || "left", textAlign: componentConfig.textAlign || "left",
backgroundColor: componentConfig.backgroundColor || "transparent", backgroundColor: componentConfig.backgroundColor || "transparent",
padding: componentConfig.padding || "0", padding: componentConfig.padding || "0",
@ -102,7 +102,7 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -85,7 +85,7 @@ export const TextDisplayConfigPanel: React.FC<TextDisplayConfigPanelProps> = ({
<Input <Input
id="color" id="color"
type="color" type="color"
value={config.color || "#374151"} value={config.color || "#3b83f6"}
onChange={(e) => handleChange("color", e.target.value)} onChange={(e) => handleChange("color", e.target.value)}
/> />
</div> </div>

View File

@ -24,7 +24,7 @@ export const TextDisplayDefinition = createComponentDefinition({
text: "텍스트를 입력하세요", text: "텍스트를 입력하세요",
fontSize: "14px", fontSize: "14px",
fontWeight: "normal", fontWeight: "normal",
color: "#374151", color: "#3b83f6",
textAlign: "left", textAlign: "left",
}, },
defaultSize: { width: 150, height: 24 }, defaultSize: { width: 150, height: 24 },

View File

@ -190,7 +190,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -84,7 +84,7 @@ export const TextareaBasicComponent: React.FC<TextareaBasicComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),

View File

@ -84,7 +84,7 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
@ -173,7 +173,7 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
</div> </div>
<span <span
style={{ style={{
color: "#374151", color: "#3b83f6",
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
}} }}

View File

@ -308,7 +308,7 @@ export class AutoRegisteringLayoutRenderer {
style: { style: {
labelDisplay: true, labelDisplay: true,
labelFontSize: "14px", labelFontSize: "14px",
labelColor: "#374151", labelColor: "#3b83f6",
labelFontWeight: "500", labelFontWeight: "500",
labelMarginBottom: "4px", labelMarginBottom: "4px",
}, },

View File

@ -60,8 +60,15 @@ export abstract class BaseLayoutRenderer extends React.Component<LayoutRendererP
// 디자인 모드일 때 더 강조된 스타일 // 디자인 모드일 때 더 강조된 스타일
if (isDesignMode) { 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) => { onMouseEnter={(e) => {
const element = e.currentTarget; const element = e.currentTarget;
element.style.borderColor = "#3b82f6"; // 🎯 컴포넌트가 있는 존은 호버 효과 최소화
element.style.backgroundColor = "rgba(59, 130, 246, 0.02)"; if (zoneChildren.length > 0) {
element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)"; 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) => { onMouseLeave={(e) => {
const element = e.currentTarget; const element = e.currentTarget;
element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; if (zoneChildren.length > 0) {
element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; // 컴포넌트가 있는 존 복원
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"; element.style.boxShadow = "none";
}} }}
onDrop={this.handleDrop(zone.id)} onDrop={this.handleDrop(zone.id)}

View File

@ -148,7 +148,7 @@ const AccordionSection: React.FC<{
const headerStyle: React.CSSProperties = { const headerStyle: React.CSSProperties = {
padding: "12px 16px", padding: "12px 16px",
backgroundColor: isDesignMode ? "#3b82f6" : "#f8fafc", backgroundColor: isDesignMode ? "#3b82f6" : "#f8fafc",
color: isDesignMode ? "white" : "#374151", color: isDesignMode ? "white" : "#3b83f6",
border: "1px solid #e2e8f0", border: "1px solid #e2e8f0",
borderBottom: isExpanded ? "none" : "1px solid #e2e8f0", borderBottom: isExpanded ? "none" : "1px solid #e2e8f0",
cursor: "pointer", cursor: "pointer",

View File

@ -14,6 +14,10 @@ let hotReloadListeners: Array<() => void> = [];
* Hot Reload * Hot Reload
*/ */
export function initializeHotReload(): void { export function initializeHotReload(): void {
// 핫 리로드 시스템 임시 비활성화 (디버깅 목적)
console.log("🔥 컴포넌트 Hot Reload 시스템 비활성화됨 (디버깅 모드)");
return;
if (process.env.NODE_ENV !== "development" || typeof window === "undefined") { if (process.env.NODE_ENV !== "development" || typeof window === "undefined") {
return; return;
} }
@ -55,11 +59,15 @@ function setupDevServerEventListener(): void {
const originalLog = console.log; const originalLog = console.log;
let reloadPending = false; let reloadPending = false;
// console.log 메시지를 감지하여 Hot Reload 트리거 // console.log 메시지를 감지하여 Hot Reload 트리거 (특정 메시지만)
console.log = (...args: any[]) => { console.log = (...args: any[]) => {
const message = args.join(" "); 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) { if (!reloadPending) {
reloadPending = true; reloadPending = true;
setTimeout(() => { setTimeout(() => {

View File

@ -110,6 +110,8 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
screenTableName, screenTableName,
tableColumns, tableColumns,
}) => { }) => {
console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`);
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null); const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null); 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 ( return (
<ConfigPanelComponent <ConfigPanelComponent
config={config} config={config}
onChange={onChange} onChange={onChange}
onConfigChange={onChange} // TableListConfigPanel을 위한 추가 prop
screenTableName={screenTableName} screenTableName={screenTableName}
tableColumns={tableColumns} tableColumns={tableColumns}
/> />

View File

@ -661,7 +661,7 @@ function getComponentJSXByWebType(webType) {
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >
@ -709,7 +709,7 @@ function getComponentJSXByWebType(webType) {
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >
@ -785,7 +785,7 @@ function getComponentJSXByWebType(webType) {
top: "-25px", top: "-25px",
left: "0px", left: "0px",
fontSize: component.style?.labelFontSize || "14px", fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151", color: component.style?.labelColor || "#3b83f6",
fontWeight: "500", fontWeight: "500",
}} }}
> >

View File

@ -68,6 +68,9 @@ export interface ComponentRendererProps {
// 새로운 기능들 // 새로운 기능들
autoGeneration?: AutoGenerationConfig; // 자동생성 설정 autoGeneration?: AutoGenerationConfig; // 자동생성 설정
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
// 설정 변경 핸들러
onConfigChange?: (config: any) => void;
[key: string]: any; [key: string]: any;
} }
@ -317,7 +320,7 @@ export const COMPONENT_CATEGORIES_INFO = {
[ComponentCategory.CHART]: { [ComponentCategory.CHART]: {
name: "차트", name: "차트",
description: "데이터 시각화 컴포넌트", description: "데이터 시각화 컴포넌트",
color: "#06b6d4", color: "#3b83f6",
}, },
[ComponentCategory.FORM]: { [ComponentCategory.FORM]: {
name: "폼", name: "폼",
@ -347,7 +350,7 @@ export const COMPONENT_CATEGORIES_INFO = {
[ComponentCategory.CONTAINER]: { [ComponentCategory.CONTAINER]: {
name: "컨테이너", name: "컨테이너",
description: "다른 컴포넌트를 담는 컨테이너", description: "다른 컴포넌트를 담는 컨테이너",
color: "#374151", color: "#3b83f6",
}, },
[ComponentCategory.SYSTEM]: { [ComponentCategory.SYSTEM]: {
name: "시스템", name: "시스템",