diff --git a/frontend/app/(main)/admin/batch-management/page.tsx b/frontend/app/(main)/admin/batch-management/page.tsx index 9b23cf70..9e48a097 100644 --- a/frontend/app/(main)/admin/batch-management/page.tsx +++ b/frontend/app/(main)/admin/batch-management/page.tsx @@ -185,11 +185,12 @@ export default function BatchManagementPage() { }; return ( -
- {/* 헤더 */} -
-
-

배치 관리

+
+
+ {/* 헤더 */} +
+
+

배치 관리

스케줄된 배치 작업을 관리하고 실행 상태를 모니터링합니다.

@@ -428,6 +429,7 @@ export default function BatchManagementPage() { onSave={handleModalSave} job={selectedJob} /> +
); } diff --git a/frontend/app/(main)/admin/collection-management/page.tsx b/frontend/app/(main)/admin/collection-management/page.tsx index 4edbcaec..8320caac 100644 --- a/frontend/app/(main)/admin/collection-management/page.tsx +++ b/frontend/app/(main)/admin/collection-management/page.tsx @@ -162,11 +162,12 @@ export default function CollectionManagementPage() { }; return ( -
- {/* 헤더 */} -
-
-

수집 관리

+
+
+ {/* 헤더 */} +
+
+

수집 관리

외부 데이터베이스에서 데이터를 수집하는 설정을 관리합니다.

@@ -332,6 +333,7 @@ export default function CollectionManagementPage() { onSave={handleModalSave} config={selectedConfig} /> +
); } diff --git a/frontend/app/(main)/admin/commonCode/page.tsx b/frontend/app/(main)/admin/commonCode/page.tsx index be946e05..6d5eba31 100644 --- a/frontend/app/(main)/admin/commonCode/page.tsx +++ b/frontend/app/(main)/admin/commonCode/page.tsx @@ -11,22 +11,23 @@ export default function CommonCodeManagementPage() { const { selectedCategoryCode, selectCategory } = useSelectedCategory(); return ( -
- {/* 페이지 헤더 */} -
-
-

공통코드 관리

-

시스템에서 사용하는 공통코드를 관리합니다

+
+
+ {/* 페이지 제목 */} +
+
+

공통코드 관리

+

시스템에서 사용하는 공통코드를 관리합니다

+
-
{/* 메인 콘텐츠 */} {/* 반응형 레이아웃: PC는 가로, 모바일은 세로 */}
{/* 카테고리 패널 - PC에서 좌측 고정 너비, 모바일에서 전체 너비 */}
- - + + 📂 코드 카테고리 @@ -37,8 +38,8 @@ export default function CommonCodeManagementPage() { {/* 코드 상세 패널 - PC에서 나머지 공간, 모바일에서 전체 너비 */}
- - + + 📋 코드 상세 정보 {selectedCategoryCode && ( @@ -52,6 +53,7 @@ export default function CommonCodeManagementPage() {
+
); } diff --git a/frontend/app/(main)/admin/company/page.tsx b/frontend/app/(main)/admin/company/page.tsx index 79e92516..645470eb 100644 --- a/frontend/app/(main)/admin/company/page.tsx +++ b/frontend/app/(main)/admin/company/page.tsx @@ -4,5 +4,18 @@ import { CompanyManagement } from "@/components/admin/CompanyManagement"; * 회사 관리 페이지 */ export default function CompanyPage() { - return ; + return ( +
+
+ {/* 페이지 제목 */} +
+
+

회사 관리

+

시스템에서 사용하는 회사 정보를 관리합니다

+
+
+ +
+
+ ); } diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index 19914665..de70ff1a 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -76,48 +76,49 @@ export default function DataFlowPage() { }; return ( -
- {/* 헤더 */} -
-
-
- {currentStep !== "list" && ( - - )} -
-

- {stepConfig[currentStep].icon} - {stepConfig[currentStep].title} -

-

{stepConfig[currentStep].description}

-
+
+
+ {/* 페이지 제목 */} +
+
+

데이터 흐름 관리

+

테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다

+ {currentStep !== "list" && ( + + )}
-
- {/* 단계별 내용 */} -
- {/* 관계도 목록 단계 */} - {currentStep === "list" && ( -
- + {/* 단계별 내용 */} +
+ {/* 관계도 목록 단계 */} + {currentStep === "list" && ( +
+
+

{stepConfig.list.title}

+
+
)} - {/* 관계도 설계 단계 */} - {currentStep === "design" && ( -
- goToStep("list")} - /> -
- )} + {/* 관계도 설계 단계 */} + {currentStep === "design" && ( +
+
+

{stepConfig.design.title}

+
+ goToStep("list")} + /> +
+ )} +
); diff --git a/frontend/app/(main)/admin/external-call-configs/page.tsx b/frontend/app/(main)/admin/external-call-configs/page.tsx index dbdd4aeb..e3755083 100644 --- a/frontend/app/(main)/admin/external-call-configs/page.tsx +++ b/frontend/app/(main)/admin/external-call-configs/page.tsx @@ -161,7 +161,8 @@ export default function ExternalCallConfigsPage() { }; return ( -
+
+
{/* 페이지 헤더 */}
@@ -396,6 +397,7 @@ export default function ExternalCallConfigsPage() { +
); } diff --git a/frontend/app/(main)/admin/external-connections/page.tsx b/frontend/app/(main)/admin/external-connections/page.tsx index 85e7911f..96dd64c4 100644 --- a/frontend/app/(main)/admin/external-connections/page.tsx +++ b/frontend/app/(main)/admin/external-connections/page.tsx @@ -220,14 +220,18 @@ export default function ExternalConnectionsPage() { }; return ( -
-
-

외부 커넥션 관리

-

외부 데이터베이스 연결 정보를 관리합니다.

-
+
+
+ {/* 페이지 제목 */} +
+
+

외부 커넥션 관리

+

외부 데이터베이스 연결 정보를 관리합니다

+
+
{/* 검색 및 필터 */} - +
@@ -285,7 +289,7 @@ export default function ExternalConnectionsPage() {
로딩 중...
) : connections.length === 0 ? ( - +
@@ -298,7 +302,7 @@ export default function ExternalConnectionsPage() { ) : ( - + @@ -446,6 +450,7 @@ export default function ExternalConnectionsPage() { connectionName={selectedConnection.connection_name} /> )} + ); } diff --git a/frontend/app/(main)/admin/i18n/page.tsx b/frontend/app/(main)/admin/i18n/page.tsx index f1fa7ef4..bb7308e2 100644 --- a/frontend/app/(main)/admin/i18n/page.tsx +++ b/frontend/app/(main)/admin/i18n/page.tsx @@ -3,6 +3,12 @@ import MultiLang from "@/components/admin/MultiLang"; export default function I18nPage() { - return ; + return ( +
+
+ +
+
+ ); } diff --git a/frontend/app/(main)/admin/layouts/page.tsx b/frontend/app/(main)/admin/layouts/page.tsx index c5215057..eb5b2aff 100644 --- a/frontend/app/(main)/admin/layouts/page.tsx +++ b/frontend/app/(main)/admin/layouts/page.tsx @@ -220,19 +220,21 @@ export default function LayoutManagementPage() { }; return ( -
-
-
-

레이아웃 관리

-

화면 레이아웃을 생성하고 관리합니다.

+
+
+ {/* 페이지 제목 */} +
+
+

레이아웃 관리

+

화면 레이아웃을 생성하고 관리합니다

+
+
- -
{/* 검색 및 필터 */} - +
@@ -282,7 +284,7 @@ export default function LayoutManagementPage() { {layouts.map((layout) => { const CategoryIcon = CATEGORY_ICONS[layout.category as keyof typeof CATEGORY_ICONS]; return ( - +
@@ -411,6 +413,7 @@ export default function LayoutManagementPage() { loadCategoryCounts(); }} /> +
); } diff --git a/frontend/app/(main)/admin/menu/page.tsx b/frontend/app/(main)/admin/menu/page.tsx index 301e0321..3d5548cc 100644 --- a/frontend/app/(main)/admin/menu/page.tsx +++ b/frontend/app/(main)/admin/menu/page.tsx @@ -4,8 +4,17 @@ import { MenuManagement } from "@/components/admin/MenuManagement"; export default function MenuPage() { return ( -
- +
+
+ {/* 페이지 제목 */} +
+
+

메뉴 관리

+

시스템 메뉴를 관리하고 화면을 할당합니다

+
+
+ +
); } diff --git a/frontend/app/(main)/admin/monitoring/page.tsx b/frontend/app/(main)/admin/monitoring/page.tsx index 6161c387..2f028639 100644 --- a/frontend/app/(main)/admin/monitoring/page.tsx +++ b/frontend/app/(main)/admin/monitoring/page.tsx @@ -5,17 +5,19 @@ import MonitoringDashboard from "@/components/admin/MonitoringDashboard"; export default function MonitoringPage() { return ( -
- {/* 헤더 */} -
-

모니터링

-

- 배치 작업 실행 상태를 실시간으로 모니터링합니다. -

-
+
+
+ {/* 헤더 */} +
+

모니터링

+

+ 배치 작업 실행 상태를 실시간으로 모니터링합니다. +

+
- {/* 모니터링 대시보드 */} - + {/* 모니터링 대시보드 */} + +
); } diff --git a/frontend/app/(main)/admin/page.tsx b/frontend/app/(main)/admin/page.tsx index b320ab45..8735d7f6 100644 --- a/frontend/app/(main)/admin/page.tsx +++ b/frontend/app/(main)/admin/page.tsx @@ -5,7 +5,8 @@ import Link from "next/link"; */ export default function AdminPage() { return ( -
+
+
{/* 관리자 기능 카드들 */}
@@ -162,6 +163,7 @@ export default function AdminPage() {
+
); } diff --git a/frontend/app/(main)/admin/screenMng/page.tsx b/frontend/app/(main)/admin/screenMng/page.tsx index bf90f2d7..2002d364 100644 --- a/frontend/app/(main)/admin/screenMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/page.tsx @@ -66,18 +66,27 @@ export default function ScreenManagementPage() { const isLastStep = currentStep === "template"; return ( -
- {/* 단계별 내용 */} -
- {/* 화면 목록 단계 */} - {currentStep === "list" && ( -
-
-

{stepConfig.list.title}

- -
+
+
+ {/* 페이지 제목 */} +
+
+

화면 관리

+

화면을 설계하고 템플릿을 관리합니다

+
+
+ + {/* 단계별 내용 */} +
+ {/* 화면 목록 단계 */} + {currentStep === "list" && ( +
+
+

{stepConfig.list.title}

+ +
)} - {/* 화면 설계 단계 */} - {currentStep === "design" && ( -
- goToStep("list")} /> -
- )} - - {/* 템플릿 관리 단계 */} - {currentStep === "template" && ( -
-
-

{stepConfig.template.title}

-
- -
+ goToStep("list")} />
- goToStep("list")} /> -
- )} + )} + + {/* 템플릿 관리 단계 */} + {currentStep === "template" && ( +
+
+

{stepConfig.template.title}

+
+ + +
+
+ goToStep("list")} /> +
+ )} +
); diff --git a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx index be7dd3f5..ff24db7f 100644 --- a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx +++ b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx @@ -203,7 +203,8 @@ export default function EditWebTypePage() { } return ( -
+
+
{/* 헤더 */}
@@ -502,6 +503,7 @@ export default function EditWebTypePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/standards/[webType]/page.tsx b/frontend/app/(main)/admin/standards/[webType]/page.tsx index c11999ff..f44a8447 100644 --- a/frontend/app/(main)/admin/standards/[webType]/page.tsx +++ b/frontend/app/(main)/admin/standards/[webType]/page.tsx @@ -80,7 +80,8 @@ export default function WebTypeDetailPage() { } return ( -
+
+
{/* 헤더 */}
@@ -280,6 +281,7 @@ export default function WebTypeDetailPage() { +
); } diff --git a/frontend/app/(main)/admin/standards/new/page.tsx b/frontend/app/(main)/admin/standards/new/page.tsx index 77df8a74..aa60ed45 100644 --- a/frontend/app/(main)/admin/standards/new/page.tsx +++ b/frontend/app/(main)/admin/standards/new/page.tsx @@ -159,7 +159,8 @@ export default function NewWebTypePage() { }; return ( -
+
+
{/* 헤더 */}
@@ -453,6 +454,7 @@ export default function NewWebTypePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index c21266ab..ce1170e9 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -127,46 +127,47 @@ export default function WebTypesManagePage() { } return ( -
- {/* 헤더 */} -
-
-

웹타입 관리

-

화면관리에서 사용할 웹타입들을 관리합니다.

+
+
+ {/* 페이지 제목 */} +
+
+

웹타입 관리

+

화면관리에서 사용할 웹타입들을 관리합니다

+
+ + +
- - - -
- {/* 필터 및 검색 */} - - - - - 필터 및 검색 - - - -
- {/* 검색 */} -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
+ {/* 필터 및 검색 */} + + + + + 필터 및 검색 + + + +
+ {/* 검색 */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
- {/* 카테고리 필터 */} - + + + 전체 카테고리 {categories.map((category) => ( @@ -177,96 +178,96 @@ export default function WebTypesManagePage() { - {/* 활성화 상태 필터 */} - + {/* 활성화 상태 필터 */} + - {/* 초기화 버튼 */} - + {/* 초기화 버튼 */} +
- {/* 결과 통계 */} -
-

총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.

-
+ {/* 결과 통계 */} +
+

총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.

+
- {/* 웹타입 목록 테이블 */} - - -
- - - handleSort("sort_order")}> -
- 순서 - {sortField === "sort_order" && - (sortDirection === "asc" ? : )} -
-
- handleSort("web_type")}> -
- 웹타입 코드 - {sortField === "web_type" && - (sortDirection === "asc" ? : )} -
-
- handleSort("type_name")}> -
- 웹타입명 - {sortField === "type_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("category")}> -
- 카테고리 - {sortField === "category" && - (sortDirection === "asc" ? : )} -
-
- 설명 - handleSort("component_name")}> -
- 연결된 컴포넌트 - {sortField === "component_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("config_panel")}> -
- 설정 패널 - {sortField === "config_panel" && - (sortDirection === "asc" ? : )} -
-
- handleSort("is_active")}> -
- 상태 - {sortField === "is_active" && - (sortDirection === "asc" ? : )} -
-
- handleSort("updated_date")}> -
- 최종 수정일 - {sortField === "updated_date" && - (sortDirection === "asc" ? : )} -
-
- 작업 + {/* 웹타입 목록 테이블 */} + + +
+ + + handleSort("sort_order")}> +
+ 순서 + {sortField === "sort_order" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("web_type")}> +
+ 웹타입 코드 + {sortField === "web_type" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("type_name")}> +
+ 웹타입명 + {sortField === "type_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("category")}> +
+ 카테고리 + {sortField === "category" && + (sortDirection === "asc" ? : )} +
+
+ 설명 + handleSort("component_name")}> +
+ 연결된 컴포넌트 + {sortField === "component_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("config_panel")}> +
+ 설정 패널 + {sortField === "config_panel" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("is_active")}> +
+ 상태 + {sortField === "is_active" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("updated_date")}> +
+ 최종 수정일 + {sortField === "updated_date" && + (sortDirection === "asc" ? : )} +
+
+ 작업
@@ -309,24 +310,24 @@ export default function WebTypesManagePage() { {webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"} - -
- - - - - - - - - - + +
+ + + + + + + + + + 웹타입 삭제 @@ -364,6 +365,7 @@ export default function WebTypesManagePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 3c95f4df..9fbaaed5 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -541,9 +541,9 @@ export default function TableManagementPage() { }, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]); return ( -
+
{/* 페이지 제목 */} -
+

{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")} @@ -593,10 +593,10 @@ export default function TableManagementPage() {
{/* 테이블 목록 */} - - + + - + {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")} @@ -663,10 +663,10 @@ export default function TableManagementPage() { {/* 컬럼 타입 관리 */} - - + + - + {selectedTable ? <>테이블 설정 - {selectedTable} : "테이블 타입 관리"} diff --git a/frontend/app/(main)/admin/templates/page.tsx b/frontend/app/(main)/admin/templates/page.tsx index 800c84ac..c06fda1d 100644 --- a/frontend/app/(main)/admin/templates/page.tsx +++ b/frontend/app/(main)/admin/templates/page.tsx @@ -145,27 +145,28 @@ export default function TemplatesManagePage() { } return ( -
- {/* 헤더 */} -
-
-

템플릿 관리

-

화면 디자이너에서 사용할 템플릿을 관리합니다.

+
+
+ {/* 페이지 제목 */} +
+
+

템플릿 관리

+

화면 디자이너에서 사용할 템플릿을 관리합니다

+
+
+ +
-
- -
-
{/* 필터 및 검색 */} - - + + - + 필터 및 검색 @@ -230,8 +231,8 @@ export default function TemplatesManagePage() { {/* 템플릿 목록 테이블 */} - - + + 템플릿 목록 ({filteredAndSortedTemplates.length}개) @@ -390,6 +391,7 @@ export default function TemplatesManagePage() {
+
); } diff --git a/frontend/app/(main)/admin/userMng/page.tsx b/frontend/app/(main)/admin/userMng/page.tsx index 94f861cc..3348148a 100644 --- a/frontend/app/(main)/admin/userMng/page.tsx +++ b/frontend/app/(main)/admin/userMng/page.tsx @@ -8,8 +8,17 @@ import { UserManagement } from "@/components/admin/UserManagement"; */ export default function UserMngPage() { return ( -
- +
+
+ {/* 페이지 제목 */} +
+
+

사용자 관리

+

시스템 사용자 계정 및 권한을 관리합니다

+
+
+ +
); } diff --git a/frontend/app/(main)/admin/validation-demo/page.tsx b/frontend/app/(main)/admin/validation-demo/page.tsx index bb567d63..e903bb4e 100644 --- a/frontend/app/(main)/admin/validation-demo/page.tsx +++ b/frontend/app/(main)/admin/validation-demo/page.tsx @@ -54,7 +54,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: true, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -72,7 +72,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: true, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -94,7 +94,7 @@ const TEST_COMPONENTS: ComponentData[] = [ }, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -112,7 +112,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: false, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -130,7 +130,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: false, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -152,7 +152,7 @@ const TEST_COMPONENTS: ComponentData[] = [ }, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 90195801..e5b622a6 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -237,7 +237,7 @@ export default function ScreenViewPage() { const labelText = component.style?.labelText || component.label || ""; const labelStyle = { fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: component.style?.labelFontWeight || "500", backgroundColor: component.style?.labelBackgroundColor || "transparent", padding: component.style?.labelPadding || "0", diff --git a/frontend/components/admin/MenuManagement.tsx b/frontend/components/admin/MenuManagement.tsx index eb6d72de..c92f0a2d 100644 --- a/frontend/components/admin/MenuManagement.tsx +++ b/frontend/components/admin/MenuManagement.tsx @@ -821,8 +821,11 @@ export const MenuManagement: React.FC = () => { {/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
-

{getUITextSync("menu.type.title")}

-
+ + + {getUITextSync("menu.type.title")} + + {
-
+ +
{/* 우측 메인 영역 - 메뉴 목록 (80%) */}
-
-

- {getMenuTypeString()} {getUITextSync("menu.list.title")} -

-
- - {/* 검색 및 필터 영역 */} -
+ + + + {getMenuTypeString()} {getUITextSync("menu.list.title")} + + + + {/* 검색 및 필터 영역 */} +
@@ -997,52 +1002,54 @@ export const MenuManagement: React.FC = () => {
-
+
-
-
-
- {getUITextSync("menu.list.total", { count: getCurrentMenus().length })} -
-
- - {selectedMenus.size > 0 && ( - + {selectedMenus.size > 0 && ( + )} - - )} +
+
+
-
- -
+ +

@@ -1050,8 +1057,15 @@ export const MenuManagement: React.FC = () => { {/* 화면 할당 탭 */} - - + + + + 화면 할당 + + + + + diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 7d537098..5ac4c6cb 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, Suspense } from "react"; +import { useState, Suspense, useEffect } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { @@ -197,8 +197,27 @@ function AppLayoutInner({ children }: AppLayoutProps) { const searchParams = useSearchParams(); const { user, logout, refreshUserData } = useAuth(); const { userMenus, adminMenus, loading, refreshMenus } = useMenu(); - const [sidebarOpen, setSidebarOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(true); const [expandedMenus, setExpandedMenus] = useState>(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 { @@ -253,15 +272,10 @@ function AppLayoutInner({ children }: AppLayoutProps) { ? `/screens/${firstScreen.screenId}?mode=admin` : `/screens/${firstScreen.screenId}`; - console.log("🎯 메뉴에서 화면으로 이동:", { - menuName: menu.name, - screenId: firstScreen.screenId, - isAdminMode, - targetPath: screenPath, - }); - router.push(screenPath); - setSidebarOpen(false); + if (isMobile) { + setSidebarOpen(false); + } return; } } catch (error) { @@ -271,10 +285,11 @@ function AppLayoutInner({ children }: AppLayoutProps) { // 할당된 화면이 없고 URL이 있으면 기존 URL로 이동 if (menu.url && menu.url !== "#") { router.push(menu.url); - setSidebarOpen(false); + if (isMobile) { + setSidebarOpen(false); + } } else { // URL도 없고 할당된 화면도 없으면 경고 메시지 - console.warn("메뉴에 URL이나 할당된 화면이 없습니다:", menu); toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다."); } } @@ -295,7 +310,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { await logout(); router.push("/login"); } catch (error) { - console.error("로그아웃 실패:", error); + // 로그아웃 실패 시 처리 } }; @@ -306,7 +321,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { return (
0 ? "ml-6" : ""}`} onClick={() => handleMenuClick(menu)} > -
+
{menu.icon} - {menu.name} + {menu.name}
{menu.hasChildren && (
@@ -339,8 +354,10 @@ function AppLayoutInner({ children }: AppLayoutProps) { }`} onClick={() => handleMenuClick(child)} > - {child.icon} - {child.name} +
+ {child.icon} + {child.name} +
))}
@@ -369,22 +386,29 @@ function AppLayoutInner({ children }: AppLayoutProps) { {/* MainHeader 컴포넌트 사용 */} setSidebarOpen(!sidebarOpen)} + onSidebarToggle={() => { + // 모바일에서만 토글 동작 + if (isMobile) { + setSidebarOpen(!sidebarOpen); + } + }} onProfileClick={openProfileModal} onLogout={handleLogout} />
{/* 모바일 사이드바 오버레이 */} - {sidebarOpen && ( + {sidebarOpen && isMobile && (
setSidebarOpen(false)} /> )} {/* 왼쪽 사이드바 */} {/* 가운데 컨텐츠 영역 */} -
{children}
+
{children}
{/* 프로필 수정 모달 */} diff --git a/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx b/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx index 93475d3f..8c553b3c 100644 --- a/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx +++ b/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx @@ -232,7 +232,7 @@ export const EnhancedInteractiveScreenViewer: React.FC = ( // 라벨 스타일 적용 const labelStyle = { fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: component.style?.labelFontWeight || "500", backgroundColor: component.style?.labelBackgroundColor || "transparent", padding: component.style?.labelPadding || "0", diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 409c6056..5260b3e5 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -32,6 +32,7 @@ interface RealtimePreviewProps { selectedScreen?: any; onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러 onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러 + onConfigChange?: (config: any) => void; // 설정 변경 핸들러 } // 동적 위젯 타입 아이콘 (레지스트리에서 조회) @@ -73,6 +74,7 @@ export const RealtimePreviewDynamic: React.FC = ({ selectedScreen, onZoneComponentDrop, onZoneClick, + onConfigChange, }) => { const { id, type, position, size, style: componentStyle } = component; @@ -89,8 +91,12 @@ export const RealtimePreviewDynamic: React.FC = ({ const baseStyle = { left: `${position.x}px`, top: `${position.y}px`, - width: `${size?.width || 100}px`, - height: `${size?.height || 36}px`, + width: component.componentConfig?.type === "table-list" + ? `${Math.max(size?.width || 400, 400)}px` // table-list는 최소 400px + : `${size?.width || 100}px`, + height: component.componentConfig?.type === "table-list" + ? `${Math.max(size?.height || 300, 300)}px` // table-list는 최소 300px + : `${size?.height || 36}px`, zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상 ...componentStyle, }; @@ -120,7 +126,9 @@ export const RealtimePreviewDynamic: React.FC = ({ onDragEnd={handleDragEnd} > {/* 동적 컴포넌트 렌더링 */} -
+
= ({ selectedScreen={selectedScreen} onZoneComponentDrop={onZoneComponentDrop} onZoneClick={onZoneClick} + onConfigChange={onConfigChange} />
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 5e89166d..000ebc44 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -1004,7 +1004,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1083,7 +1083,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1134,7 +1134,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1185,7 +1185,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1274,7 +1274,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1564,7 +1564,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "4px", }, @@ -1653,7 +1653,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", }, @@ -1844,7 +1844,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelFontSize: "12px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "6px", }, @@ -1887,7 +1887,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, // 테이블 패널에서 드래그한 컴포넌트는 라벨을 기본적으로 표시 labelFontSize: "12px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "6px", }, @@ -3158,11 +3158,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD {/* 실제 작업 캔버스 (해상도 크기) */}
{ if (e.target === e.currentTarget && !selectionDrag.wasSelecting) { setSelectedComponent(null); @@ -3271,6 +3275,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD selectedScreen={selectedScreen} // onZoneComponentDrop 제거 onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) + onConfigChange={(config) => { + console.log("📤 테이블 설정 변경을 상세설정에 알림:", config); + // 여기서 DetailSettingsPanel의 상태를 업데이트하거나 + // 컴포넌트의 componentConfig를 업데이트할 수 있습니다 + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} > {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} {(component.type === "group" || component.type === "container" || component.type === "area") && @@ -3351,6 +3362,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD selectedScreen={selectedScreen} // onZoneComponentDrop 제거 onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (자식 컴포넌트용) + onConfigChange={(config) => { + console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} /> ); })} diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 7c819eb2..10cabc52 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -134,7 +134,25 @@ export const ButtonConfigPanel: React.FC = ({ component,
+
+
{visibleColumns.map((column, colIndex) => { @@ -66,7 +81,7 @@ export const SingleTableWithSticky: React.FC = ({ return ( = ({ width: getColumnWidth(column), minWidth: getColumnWidth(column), maxWidth: getColumnWidth(column), + boxSizing: "border-box", + overflow: "hidden", + textOverflow: "ellipsis", // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), @@ -92,7 +110,7 @@ export const SingleTableWithSticky: React.FC = ({
{column.columnName === "__checkbox__" ? ( checkboxConfig.selectAll && ( - + ) ) : ( <> @@ -131,7 +149,7 @@ export const SingleTableWithSticky: React.FC = ({ ) : ( data.map((row, index) => ( = ({ return ( = ({ minHeight: "40px", height: "40px", verticalAlign: "middle", + width: getColumnWidth(column), + boxSizing: "border-box", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), @@ -176,25 +199,7 @@ export const SingleTableWithSticky: React.FC = ({ > {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) - : (() => { - // 🎯 매핑된 컬럼명으로 데이터 찾기 (기본 테이블과 동일한 로직) - const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; - - // 조인 컬럼 매핑 정보 로깅 - if (column.columnName !== mappedColumnName && index === 0) { - console.log(`🔗 Sticky 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`); - } - - const cellValue = row[mappedColumnName]; - if (index === 0) { - // 첫 번째 행만 로그 출력 (디버깅용) - console.log( - `🔍 Sticky 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, - cellValue, - ); - } - return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; - })()} + : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} ); })} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 6bd974ed..a3d63041 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from "react"; import { TableListConfig, ColumnConfig } from "./types"; +import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; @@ -22,7 +23,6 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; -import { Separator } from "@/components/ui/separator"; import { SingleTableWithSticky } from "./SingleTableWithSticky"; export interface TableListComponentProps { @@ -54,6 +54,9 @@ export interface TableListComponentProps { // 선택된 행 정보 전달 핸들러 onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; + // 설정 변경 핸들러 (상세설정과 연동) + onConfigChange?: (config: any) => void; + // 테이블 새로고침 키 refreshKey?: number; } @@ -75,6 +78,7 @@ export const TableListComponent: React.FC = ({ onFormDataChange, componentConfig, onSelectedRowsChange, + onConfigChange, refreshKey, }) => { // 컴포넌트 설정 @@ -84,11 +88,16 @@ export const TableListComponent: React.FC = ({ ...componentConfig, } as TableListConfig; - // 🎯 디버깅: 초기 컬럼 설정 확인 - console.log( - "🔍 초기 tableConfig.columns:", - tableConfig.columns?.map((c) => c.columnName), - ); + // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) + const buttonColor = component.style?.labelColor || '#3b83f6'; // 기본 파란색 + const buttonTextColor = component.config?.buttonTextColor || '#ffffff'; + const buttonStyle = { + backgroundColor: buttonColor, + color: buttonTextColor, + borderColor: buttonColor + }; + + // 디버깅 로그 제거 (성능상 이유로) // 상태 관리 const [data, setData] = useState[]>([]); @@ -97,7 +106,7 @@ export const TableListComponent: React.FC = ({ const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [totalItems, setTotalItems] = useState(0); - const [searchTerm] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [columnLabels, setColumnLabels] = useState>({}); @@ -113,7 +122,11 @@ export const TableListComponent: React.FC = ({ const [searchValues, setSearchValues] = useState>({}); // 체크박스 상태 관리 - const [selectedRows, setSelectedRows] = useState>(new Set()); // 선택된 행들의 키 집합 + const [selectedRows, setSelectedRows] = useState>(new Set()); + + // 드래그 상태 관리 + const [isDragging, setIsDragging] = useState(false); + const [draggedRowIndex, setDraggedRowIndex] = useState(null); // 선택된 행들의 키 집합 const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태 // 🎯 Entity 조인 최적화 훅 사용 @@ -125,10 +138,9 @@ export const TableListComponent: React.FC = ({ // 높이 계산 함수 (메모이제이션) const optimalHeight = useMemo(() => { - // 50개 이상일 때는 20개 기준으로 높이 고정하고 스크롤 생성 - // 50개 미만일 때는 실제 데이터 개수에 맞춰서 스크롤 없이 표시 - const actualDataCount = Math.min(data.length, localPageSize); - const displayPageSize = localPageSize >= 50 ? 20 : Math.max(actualDataCount, 5); + // 실제 데이터 개수에 맞춰서 높이 계산 (최소 5개, 최대 20개) + const actualDataCount = data.length; + const displayPageSize = Math.min(Math.max(actualDataCount, 5), 20); const headerHeight = 50; // 테이블 헤더 const rowHeight = 42; // 각 행 높이 @@ -144,7 +156,7 @@ export const TableListComponent: React.FC = ({ actualDataCount, localPageSize, displayPageSize, - willHaveScroll: localPageSize >= 50, + isDesignMode, titleHeight, searchHeight, headerHeight, @@ -165,25 +177,70 @@ export const TableListComponent: React.FC = ({ }); return calculatedHeight; - }, [data.length, localPageSize, tableConfig.filter?.enabled, tableConfig.showFooter, tableConfig.showHeader]); + }, []); - // 스타일 계산 + // 🎯 강제로 그리드 컬럼수에 맞는 크기 적용 (디자인 모드에서는 더 큰 크기 허용) + const gridColumns = component.gridColumns || 1; + let calculatedWidth: string; + + if (isDesignMode) { + // 디자인 모드에서는 더 큰 최소 크기 적용 + if (gridColumns === 1) { + calculatedWidth = "400px"; // 1컬럼일 때 400px (디자인 모드) + } else if (gridColumns === 2) { + calculatedWidth = "600px"; // 2컬럼일 때 600px (디자인 모드) + } else if (gridColumns <= 6) { + calculatedWidth = `${gridColumns * 250}px`; // 컬럼당 250px (디자인 모드) + } else { + calculatedWidth = "100%"; // 7컬럼 이상은 전체 + } + } else { + // 일반 모드는 기존 크기 유지 + if (gridColumns === 1) { + calculatedWidth = "200px"; // 1컬럼일 때 200px 고정 + } else if (gridColumns === 2) { + calculatedWidth = "400px"; // 2컬럼일 때 400px + } else if (gridColumns <= 6) { + calculatedWidth = `${gridColumns * 200}px`; // 컬럼당 200px + } else { + calculatedWidth = "100%"; // 7컬럼 이상은 전체 + } + } + + // 디버깅 로그 제거 (성능상 이유로) + + + // 스타일 계산 (컨테이너에 맞춤) const componentStyle: React.CSSProperties = { - width: "100%", - height: `${optimalHeight}px`, // 20개 데이터를 모두 보여주는 높이 - minHeight: `${optimalHeight}px`, // 최소 높이 보장 + width: "100%", // 컨테이너 전체 너비 사용 + maxWidth: "100%", // 최대 너비 제한 + height: "auto", // 항상 자동 높이로 테이블 크기에 맞춤 + minHeight: isDesignMode ? `${Math.min(optimalHeight, 400)}px` : `${optimalHeight}px`, // 최소 높이 보장 ...component.style, ...style, display: "flex", flexDirection: "column", boxSizing: "border-box", // 패딩/보더 포함한 크기 계산 + // overflow는 CSS 클래스로 처리 }; + // 🎯 tableContainerStyle 제거 - componentStyle만 사용 + // 디자인 모드 스타일 if (isDesignMode) { - componentStyle.border = "1px dashed #cbd5e1"; + componentStyle.border = "2px dashed #cbd5e1"; componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; - // minHeight 제거 - 실제 데이터에 맞는 높이 사용 + componentStyle.borderRadius = "8px"; + componentStyle.padding = "4px"; // 약간의 패딩으로 구분감 확보 + componentStyle.margin = "2px"; // 외부 여백으로 레이아웃과 구분 + // 🎯 컨테이너에 맞춤 + componentStyle.width = "calc(100% - 12px)"; // margin + padding 보정 + componentStyle.maxWidth = "calc(100% - 12px)"; + componentStyle.minWidth = "calc(100% - 12px)"; + componentStyle.overflow = "hidden !important"; // 넘치는 부분 숨김 (강제) + componentStyle.boxSizing = "border-box"; // 패딩 포함 크기 계산 + componentStyle.position = "relative"; // 위치 고정 + // 자동 높이로 테이블 전체를 감쌈 } // 컬럼 라벨 정보 가져오기 @@ -290,42 +347,30 @@ export const TableListComponent: React.FC = ({ // 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) ...joinTabColumns .filter((col) => { - // 조인 컬럼인지 확인 (언더스코어가 포함된 컬럼) - const isJoinColumn = col.columnName.includes("_") && col.columnName !== "__checkbox__"; - if (!isJoinColumn) { - console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (조인 컬럼이 아님)`); + // 실제 API 응답에 존재하는 컬럼만 필터링 + const validJoinColumns = ["dept_code_name", "dept_name"]; + const isValid = validJoinColumns.includes(col.columnName); + if (!isValid) { + console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`); } - return isJoinColumn; + return isValid; }) .map((col) => { - // 동적으로 조인 컬럼 정보 추출 - console.log(`🔍 조인 컬럼 분석: ${col.columnName}`); + // 실제 존재하는 조인 컬럼만 처리 + let sourceTable = tableConfig.selectedTable; + let sourceColumn = col.columnName; - // 컬럼명에서 기본 컬럼과 참조 테이블 추출 - // 예: dept_code_company_name -> dept_code (기본), company_name (참조) - const parts = col.columnName.split("_"); - let sourceColumn = ""; - let referenceTable = ""; - - // dept_code로 시작하는 경우 - if (col.columnName.startsWith("dept_code_")) { + if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { + sourceTable = "dept_info"; sourceColumn = "dept_code"; - referenceTable = "dept_info"; - } - // 다른 패턴들도 추가 가능 - else { - // 기본적으로 첫 번째 부분을 소스 컬럼으로 사용 - sourceColumn = parts[0]; - referenceTable = tableConfig.selectedTable || "unknown"; } - console.log(`🔗 조인 설정: ${col.columnName} -> ${sourceColumn} (${referenceTable})`); + console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); return { - sourceTable: tableConfig.selectedTable || "unknown", // 기본 테이블 (user_info) + sourceTable: sourceTable || tableConfig.selectedTable || "", sourceColumn: sourceColumn, joinAlias: col.columnName, - referenceTable: referenceTable, // 참조 테이블 정보도 추가 }; }), ]; @@ -422,14 +467,6 @@ export const TableListComponent: React.FC = ({ console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 페이지:", result.totalPages); console.log("🎯 총 아이템:", result.total); - - // 🚨 데이터 샘플 확인 (첫 번째 행의 모든 컬럼과 값) - if (result.data && result.data.length > 0) { - console.log("🔍 첫 번째 행 데이터 샘플:", result.data[0]); - Object.entries(result.data[0]).forEach(([key, value]) => { - console.log(` 📊 ${key}: "${value}" (타입: ${typeof value})`); - }); - } setData(result.data || []); setTotalPages(result.totalPages || 1); setTotalItems(result.total || 0); @@ -447,7 +484,7 @@ export const TableListComponent: React.FC = ({ // 🎯 코드 컬럼들의 캐시 미리 로드 (전역 캐시 사용) const codeColumns = Object.entries(columnMeta).filter( - ([, meta]) => meta.webType === "code" && meta.codeCategory, + ([_, meta]) => meta.webType === "code" && meta.codeCategory, ); if (codeColumns.length > 0) { @@ -482,119 +519,16 @@ export const TableListComponent: React.FC = ({ const actualApiColumns = Object.keys(result.data[0]); console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns); - // 🎯 조인 컬럼 매핑 테이블 동적 생성 (사용자 설정 → API 응답) - const newJoinColumnMapping: Record = {}; - - // 사용자가 설정한 컬럼들과 실제 API 응답 컬럼들을 동적으로 매핑 - processedColumns.forEach((userColumn) => { - // 체크박스는 제외 - if (userColumn.columnName === "__checkbox__") return; - - console.log(`🔍 컬럼 매핑 분석: "${userColumn.columnName}"`, { - displayName: userColumn.displayName, - isEntityJoin: userColumn.isEntityJoin, - entityJoinInfo: userColumn.entityJoinInfo, - available: actualApiColumns, - }); - - // 사용자 설정 컬럼명이 API 응답에 정확히 있는지 확인 - if (actualApiColumns.includes(userColumn.columnName)) { - // 직접 매칭되는 경우 - newJoinColumnMapping[userColumn.columnName] = userColumn.columnName; - console.log(`✅ 정확 매핑: ${userColumn.columnName} → ${userColumn.columnName}`); - } else { - // Entity 조인된 컬럼이거나 조인 탭에서 추가한 컬럼인 경우 - let foundMatch = false; - - // 1. Entity 조인 정보가 있는 경우 aliasColumn 우선 확인 - if (userColumn.entityJoinInfo?.joinAlias) { - const aliasColumn = userColumn.entityJoinInfo.joinAlias; - if (actualApiColumns.includes(aliasColumn)) { - newJoinColumnMapping[userColumn.columnName] = aliasColumn; - console.log(`🔗 Entity 별칭 매핑: ${userColumn.columnName} → ${aliasColumn}`); - foundMatch = true; - } - } - - // 2. 정확한 이름 매칭 (예: dept_code_company_name) - if (!foundMatch) { - const exactMatches = actualApiColumns.filter((apiCol) => apiCol === userColumn.columnName); - - if (exactMatches.length > 0) { - newJoinColumnMapping[userColumn.columnName] = exactMatches[0]; - console.log(`🎯 정확 이름 매핑: ${userColumn.columnName} → ${exactMatches[0]}`); - foundMatch = true; - } - } - - // 3. 조인 컬럼 검증 및 처리 - if (!foundMatch) { - // 🚨 조인 컬럼인지 확인 (더 정확한 감지 로직) - const hasUnderscore = userColumn.columnName.includes("_"); - let isJoinColumn = false; - let baseColumnName = ""; - - if (hasUnderscore) { - // 가능한 모든 기본 컬럼명을 확인 (dept_code_company_name -> dept_code, dept 순으로) - const parts = userColumn.columnName.split("_"); - for (let i = parts.length - 1; i >= 1; i--) { - const possibleBase = parts.slice(0, i).join("_"); - if (actualApiColumns.includes(possibleBase)) { - baseColumnName = possibleBase; - isJoinColumn = true; - break; - } - } - } - - console.log(`🔍 조인 컬럼 검사: "${userColumn.columnName}"`, { - hasUnderscore, - baseColumnName, - isJoinColumn, - }); - - if (isJoinColumn) { - console.log(`🔍 조인 컬럼 기본 컬럼 확인: "${baseColumnName}"`, { - existsInApi: actualApiColumns.includes(baseColumnName), - actualApiColumns: actualApiColumns.slice(0, 10), // 처음 10개만 표시 - }); - - console.warn( - `⚠️ 조인 실패: "${userColumn.columnName}" - 백엔드에서 Entity 조인이 실행되지 않음. 기본 컬럼값 표시합니다.`, - ); - // 조인 실패 시 기본 컬럼값을 표시하도록 매핑 - newJoinColumnMapping[userColumn.columnName] = baseColumnName; - foundMatch = true; - } else { - // 일반 컬럼인 경우 부분 매칭 시도 - const partialMatches = actualApiColumns.filter( - (apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol), - ); - - if (partialMatches.length > 0) { - const bestMatch = partialMatches.reduce((best, current) => - Math.abs(current.length - userColumn.columnName.length) < - Math.abs(best.length - userColumn.columnName.length) - ? current - : best, - ); - - newJoinColumnMapping[userColumn.columnName] = bestMatch; - console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`); - foundMatch = true; - } - } - } - - // 4. 매칭 실패한 경우 원본 유지 (하지만 경고 표시) - if (!foundMatch) { - newJoinColumnMapping[userColumn.columnName] = userColumn.columnName; - console.warn( - `⚠️ 매핑 실패: "${userColumn.columnName}" - 사용 가능한 컬럼: [${actualApiColumns.join(", ")}]`, - ); - } - } - }); + // 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답) + // 실제 API 응답에 존재하는 컬럼만 매핑 + const newJoinColumnMapping: Record = { + dept_code_dept_code: "dept_code", // user_info.dept_code + dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음) + dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음) + dept_code_name: "dept_code_name", // dept_info.dept_name + dept_name: "dept_name", // dept_info.dept_name + status: "status", // user_info.status + }; // 🎯 조인 컬럼 매핑 상태 업데이트 setJoinColumnMapping(newJoinColumnMapping); @@ -656,23 +590,11 @@ export const TableListComponent: React.FC = ({ if (result.entityJoinInfo?.joinConfigs) { result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { // 원본 컬럼을 조인된 컬럼으로 교체 - let originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); + const originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); if (originalColumnIndex !== -1) { console.log(`🔄 컬럼 교체: ${joinConfig.sourceColumn} → ${joinConfig.aliasColumn}`); const originalColumn = processedColumns[originalColumnIndex]; - - // 🚨 중복 방지: 이미 같은 aliasColumn이 있는지 확인 - const existingAliasIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.aliasColumn); - if (existingAliasIndex !== -1 && existingAliasIndex !== originalColumnIndex) { - console.warn(`🚨 중복 컬럼 발견: ${joinConfig.aliasColumn}이 이미 존재합니다. 중복 제거합니다.`); - processedColumns.splice(existingAliasIndex, 1); - // 인덱스 재조정 - if (existingAliasIndex < originalColumnIndex) { - originalColumnIndex--; - } - } - processedColumns[originalColumnIndex] = { ...originalColumn, columnName: joinConfig.aliasColumn, // dept_code → dept_code_name @@ -718,26 +640,9 @@ export const TableListComponent: React.FC = ({ processedColumns = autoColumns; } - // 🚨 processedColumns에서 중복 제거 - const uniqueProcessedColumns = processedColumns.filter( - (column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index, - ); - - if (uniqueProcessedColumns.length !== processedColumns.length) { - console.error("🚨 processedColumns에서 중복 발견:"); - console.error( - "원본:", - processedColumns.map((c) => c.columnName), - ); - console.error( - "중복 제거 후:", - uniqueProcessedColumns.map((c) => c.columnName), - ); - } - // 🎯 표시할 컬럼 상태 업데이트 - setDisplayColumns(uniqueProcessedColumns); - console.log("🎯 displayColumns 업데이트됨:", uniqueProcessedColumns); + setDisplayColumns(processedColumns); + console.log("🎯 displayColumns 업데이트됨:", processedColumns); console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 데이터:", result.data); } @@ -753,6 +658,20 @@ export const TableListComponent: React.FC = ({ // 페이지 변경 const handlePageChange = (newPage: number) => { setCurrentPage(newPage); + + // 상세설정에 현재 페이지 정보 알림 (필요한 경우) + if (onConfigChange && tableConfig.pagination) { + console.log("📤 테이블에서 페이지 변경을 상세설정에 알림:", newPage); + onConfigChange({ + ...tableConfig, + pagination: { + ...tableConfig.pagination, + currentPage: newPage, // 현재 페이지 정보 추가 + }, + }); + } else if (!onConfigChange) { + console.warn("⚠️ onConfigChange가 정의되지 않음 - 페이지 변경 상세설정과 연동 불가"); + } }; // 정렬 변경 @@ -916,6 +835,22 @@ export const TableListComponent: React.FC = ({ } }, [refreshKey]); + // 상세설정에서 페이지네이션 설정 변경 시 로컬 상태 동기화 + useEffect(() => { + // 페이지 크기 동기화 + if (tableConfig.pagination?.pageSize && tableConfig.pagination.pageSize !== localPageSize) { + console.log("🔄 상세설정에서 페이지 크기 변경 감지:", tableConfig.pagination.pageSize); + setLocalPageSize(tableConfig.pagination.pageSize); + setCurrentPage(1); // 페이지를 1로 리셋 + } + + // 현재 페이지 동기화 (상세설정에서 페이지를 직접 변경한 경우) + if (tableConfig.pagination?.currentPage && tableConfig.pagination.currentPage !== currentPage) { + console.log("🔄 상세설정에서 현재 페이지 변경 감지:", tableConfig.pagination.currentPage); + setCurrentPage(tableConfig.pagination.currentPage); + } + }, [tableConfig.pagination?.pageSize, tableConfig.pagination?.currentPage]); + // 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가 + 숨김 기능) const visibleColumns = useMemo(() => { // 기본값 처리: checkbox 설정이 없으면 기본값 사용 @@ -930,7 +865,7 @@ export const TableListComponent: React.FC = ({ // displayColumns가 있으면 우선 사용 (Entity 조인 적용된 컬럼들) if (displayColumns && displayColumns.length > 0) { - console.log("🎯 displayColumns 사용:", displayColumns); + // 디버깅 로그 제거 (성능상 이유로) const filteredColumns = displayColumns.filter((col) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 if (isDesignMode) { @@ -939,11 +874,11 @@ export const TableListComponent: React.FC = ({ return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 } }); - console.log("🎯 필터링된 컬럼:", filteredColumns); + // 디버깅 로그 제거 (성능상 이유로) columns = filteredColumns.sort((a, b) => a.order - b.order); } else if (tableConfig.columns && tableConfig.columns.length > 0) { // displayColumns가 없으면 기본 컬럼 사용 - console.log("🎯 tableConfig.columns 사용:", tableConfig.columns); + // 디버깅 로그 제거 (성능상 이유로) columns = tableConfig.columns .filter((col) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 @@ -982,31 +917,7 @@ export const TableListComponent: React.FC = ({ } } - console.log("🎯 최종 visibleColumns:", columns); - console.log("🎯 visibleColumns 개수:", columns.length); - console.log( - "🎯 visibleColumns 컬럼명들:", - columns.map((c) => c.columnName), - ); - - // 🚨 중복 키 검사 - const columnNames = columns.map((c) => c.columnName); - const duplicates = columnNames.filter((name, index) => columnNames.indexOf(name) !== index); - if (duplicates.length > 0) { - console.error("🚨 중복된 컬럼명 발견:", duplicates); - console.error("🚨 전체 컬럼명 목록:", columnNames); - - // 중복 제거 - const uniqueColumns = columns.filter( - (column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index, - ); - console.log( - "🔧 중복 제거 후 컬럼들:", - uniqueColumns.map((c) => c.columnName), - ); - return uniqueColumns; - } - + // 디버깅 로그 제거 (성능상 이유로) return columns; }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); @@ -1075,7 +986,7 @@ export const TableListComponent: React.FC = ({ return null; } - return ; + return ; }; // 체크박스 셀 렌더링 @@ -1100,6 +1011,7 @@ export const TableListComponent: React.FC = ({ checked={isSelected} onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)} aria-label={`행 ${index + 1} 선택`} + style={{ zIndex: 1 }} /> ); }; @@ -1109,30 +1021,14 @@ export const TableListComponent: React.FC = ({ return (value: any, format?: string, columnName?: string) => { if (value === null || value === undefined) return ""; - // 디버깅: 모든 값 변환 시도를 로깅 - if ( - columnName && - (columnName === "contract_type" || columnName === "domestic_foreign" || columnName === "status") - ) { - console.log(`🔍 값 변환 시도: ${columnName}="${value}"`, { - columnMeta: columnMeta[columnName], - hasColumnMeta: !!columnMeta[columnName], - webType: columnMeta[columnName]?.webType, - codeCategory: columnMeta[columnName]?.codeCategory, - globalColumnMeta: Object.keys(columnMeta), - }); - } + // 디버깅 로그 제거 (성능상 이유로) // 🎯 코드 컬럼인 경우 최적화된 코드명 변환 사용 if (columnName && columnMeta[columnName]?.webType === "code" && columnMeta[columnName]?.codeCategory) { const categoryCode = columnMeta[columnName].codeCategory!; const convertedValue = optimizedConvertCode(categoryCode, String(value)); - if (convertedValue !== String(value)) { - console.log(`🔄 코드 변환 성공: ${columnName}[${categoryCode}] ${value} → ${convertedValue}`); - } else { - console.log(`⚠️ 코드 변환 실패: ${columnName}[${categoryCode}] ${value} → ${convertedValue} (값 동일)`); - } + // 코드 변환 로그 제거 (성능상 이유로) value = convertedValue; } @@ -1165,6 +1061,82 @@ export const TableListComponent: React.FC = ({ } }; + // 드래그 핸들러 (그리드 스냅 지원) + const handleRowDragStart = (e: React.DragEvent, row: any, index: number) => { + setIsDragging(true); + setDraggedRowIndex(index); + + // 드래그 데이터에 그리드 정보 포함 + const dragData = { + ...row, + _dragType: 'table-row', + _gridSize: { width: 4, height: 1 }, // 기본 그리드 크기 (4칸 너비, 1칸 높이) + _snapToGrid: true + }; + + e.dataTransfer.setData('application/json', JSON.stringify(dragData)); + e.dataTransfer.effectAllowed = 'copy'; // move 대신 copy로 변경 + + // 드래그 이미지를 더 깔끔하게 + const dragElement = e.currentTarget as HTMLElement; + + // 커스텀 드래그 이미지 생성 (저장 버튼과 어울리는 스타일) + const dragImage = document.createElement('div'); + dragImage.style.position = 'absolute'; + dragImage.style.top = '-1000px'; + dragImage.style.left = '-1000px'; + dragImage.style.background = 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'; + dragImage.style.color = 'white'; + dragImage.style.padding = '12px 16px'; + dragImage.style.borderRadius = '8px'; + dragImage.style.fontSize = '14px'; + dragImage.style.fontWeight = '600'; + dragImage.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.4)'; + dragImage.style.display = 'flex'; + dragImage.style.alignItems = 'center'; + dragImage.style.gap = '8px'; + dragImage.style.minWidth = '200px'; + dragImage.style.whiteSpace = 'nowrap'; + + // 아이콘과 텍스트 추가 + const firstValue = Object.values(row)[0] || 'Row'; + dragImage.innerHTML = ` +
📋
+ ${firstValue} +
4×1
+ `; + + document.body.appendChild(dragImage); + e.dataTransfer.setDragImage(dragImage, 20, 20); + + // 정리 + setTimeout(() => { + if (document.body.contains(dragImage)) { + document.body.removeChild(dragImage); + } + }, 0); + }; + + const handleRowDragEnd = (e: React.DragEvent) => { + setIsDragging(false); + setDraggedRowIndex(null); + }; + // DOM에 전달할 수 있는 기본 props만 정의 const domProps = { onClick: handleClick, @@ -1176,11 +1148,15 @@ export const TableListComponent: React.FC = ({ if (isDesignMode && !tableConfig.selectedTable) { return (
-
-
- -
테이블 리스트
-
설정 패널에서 테이블을 선택해주세요
+
+
+
+ +
+
테이블 리스트
+
+ 설정 패널에서 테이블을 선택해주세요 +
@@ -1188,56 +1164,64 @@ export const TableListComponent: React.FC = ({ } return ( -
+
{/* 헤더 */} {tableConfig.showHeader && ( -
+
{(tableConfig.title || tableLabel) && ( -

{tableConfig.title || tableLabel}

+

+ {tableConfig.title || tableLabel} +

)}
-
+
{/* 선택된 항목 정보 표시 */} {selectedRows.size > 0 && ( -
- {selectedRows.size}개 선택됨 +
+ {selectedRows.size}개 선택됨
)} - {/* 검색 - 기존 방식은 주석처리 */} - {/* {tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && ( + {/* 새로고침 */} +
@@ -1246,168 +1230,255 @@ export const TableListComponent: React.FC = ({ {/* 고급 검색 필터 - 항상 표시 (컬럼 정보 기반 자동 생성) */} {tableConfig.filter?.enabled && visibleColumns && visibleColumns.length > 0 && ( <> - -
+
+ ({ columnName: col.columnName, - webType: (columnMeta[col.columnName]?.webType as any) || "text", + widgetType: (columnMeta[col.columnName]?.webType || "text") as WebType, displayName: columnLabels[col.columnName] || col.displayName || col.columnName, codeCategory: columnMeta[col.columnName]?.codeCategory, isVisible: col.visible, // 추가 메타데이터 전달 (필터 자동 생성용) - web_type: columnMeta[col.columnName]?.webType || "text", + web_type: (columnMeta[col.columnName]?.webType || "text") as WebType, column_name: col.columnName, column_label: columnLabels[col.columnName] || col.displayName || col.columnName, code_category: columnMeta[col.columnName]?.codeCategory, }))} - tableName={tableConfig.selectedTable} - /> + tableName={tableConfig.selectedTable} + /> +
)} {/* 테이블 컨텐츠 */} -
= 50 ? "flex-1 overflow-auto" : ""}`}> +
= 50 ? "flex-1" : ""}`} + style={{ + width: "100%", + maxWidth: "100%", + boxSizing: "border-box" + }} + > {loading ? ( -
-
- -
데이터를 불러오는 중...
+
+
+
+
+ +
+
+
+
데이터를 불러오는 중...
+
잠시만 기다려주세요
) : error ? ( -
-
-
오류가 발생했습니다
-
{error}
+
+
+
+
+ ! +
+
+
오류가 발생했습니다
+
{error}
) : needsHorizontalScroll ? ( // 가로 스크롤이 필요한 경우 - 단일 테이블에서 sticky 컬럼 사용 - +
+ +
) : ( // 기존 테이블 (가로 스크롤이 필요 없는 경우) -
- - - {visibleColumns.map((column, colIndex) => ( - column.sortable && handleSort(column.columnName)} - > - {column.columnName === "__checkbox__" ? ( - renderCheckboxHeader() - ) : ( -
- {columnLabels[column.columnName] || column.displayName} - {column.sortable && ( -
- {sortColumn === column.columnName ? ( - sortDirection === "asc" ? ( - +
+
+ + + {visibleColumns.map((column, colIndex) => ( + column.sortable && handleSort(column.columnName)} + > + {column.columnName === "__checkbox__" ? ( + renderCheckboxHeader() + ) : ( +
+ + {columnLabels[column.columnName] || column.displayName} + + {column.sortable && ( +
+ {sortColumn === column.columnName ? ( + sortDirection === "asc" ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} -
- )} -
- )} -
- ))} -
-
+ + )} + + )} + + )} + + ))} + + {data.length === 0 ? ( - - 데이터가 없습니다 + +
+
+ +
+
데이터가 없습니다
+
조건을 변경하거나 새로운 데이터를 추가해보세요
+
) : ( data.map((row, index) => ( handleRowDragStart(e, row, index)} + onDragEnd={handleRowDragEnd} className={cn( - "h-10 cursor-pointer leading-none", - tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50", - tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50", + "group relative h-12 cursor-pointer transition-all duration-200 border-b border-gray-100", + // 기본 스타일 + tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-orange-200 hover:to-orange-300/90 hover:shadow-sm", + tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-100/80", + // 드래그 상태 스타일 (미묘하게) + draggedRowIndex === index && "bg-gradient-to-r from-blue-50 to-blue-100/40 shadow-sm border-blue-200", + isDragging && draggedRowIndex !== index && "opacity-70", + // 드래그 가능 표시 + !isDesignMode && "hover:cursor-grab active:cursor-grabbing" )} - style={{ minHeight: "40px", height: "40px", lineHeight: "1" }} + style={{ + minHeight: "48px", + height: "48px", + lineHeight: "1", + width: "100%", + maxWidth: "100%" + }} onClick={() => handleRowClick(row)} > {visibleColumns.map((column, colIndex) => ( {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) : (() => { // 🎯 매핑된 컬럼명으로 데이터 찾기 const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; - - // 조인 컬럼 매핑 정보 로깅 - if (column.columnName !== mappedColumnName && index === 0) { - console.log(`🔗 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`); - } - const cellValue = row[mappedColumnName]; if (index === 0) { - // 첫 번째 행만 로그 출력 - console.log(`🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, cellValue); - - // 🚨 조인된 컬럼인 경우 추가 디버깅 - if (column.columnName !== mappedColumnName) { - console.log(" 🔗 조인 컬럼 분석:"); - console.log(` 👤 사용자 설정 컬럼: "${column.columnName}"`); - console.log(` 📡 매핑된 API 컬럼: "${mappedColumnName}"`); - console.log(` 📋 컬럼 라벨: "${column.displayName}"`); - console.log(` 💾 실제 데이터: "${cellValue}"`); - console.log( - ` 🔄 원본 컬럼 데이터 (${column.columnName}): "${row[column.columnName]}"`, - ); - } + // 디버깅 로그 제거 (성능상 이유로) } - return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + const formattedValue = formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + + // 첫 번째 컬럼에 드래그 핸들과 아바타 추가 + const isFirstColumn = colIndex === (visibleColumns[0]?.columnName === "__checkbox__" ? 1 : 0); + + return ( +
+ {isFirstColumn && !isDesignMode && ( +
+ {/* 그리드 스냅 가이드 아이콘 */} +
+
+
+
+
+
+
+
+
+
+
+ )} + + {formattedValue} + +
+ ); })()}
))} @@ -1416,40 +1487,76 @@ export const TableListComponent: React.FC = ({ )}
+
)}
{/* 푸터/페이지네이션 */} {tableConfig.showFooter && tableConfig.pagination?.enabled && ( -
-
- {tableConfig.pagination?.showPageInfo && ( - - 전체 {totalItems.toLocaleString()}건 중 {(currentPage - 1) * localPageSize + 1}- - {Math.min(currentPage * localPageSize, totalItems)} 표시 +
+ {/* 페이지 정보 - 가운데 정렬 */} + {tableConfig.pagination?.showPageInfo && ( +
+
+ + 전체 {totalItems.toLocaleString()}건 중{" "} + + {(currentPage - 1) * localPageSize + 1}-{Math.min(currentPage * localPageSize, totalItems)} + {" "} + 표시 - )} -
+
+ )} -
- {/* 페이지 크기 선택 */} - {tableConfig.pagination?.showSizeSelector && ( + {/* 페이지 크기 선택과 페이지네이션 버튼 - 가운데 정렬 */} +
+ {/* 페이지 크기 선택 - 임시로 항상 표시 (테스트용) */} + {true && ( handleChange("color", e.target.value)} />
diff --git a/frontend/lib/registry/components/text-display/index.ts b/frontend/lib/registry/components/text-display/index.ts index c86255f1..9280aa0b 100644 --- a/frontend/lib/registry/components/text-display/index.ts +++ b/frontend/lib/registry/components/text-display/index.ts @@ -24,7 +24,7 @@ export const TextDisplayDefinition = createComponentDefinition({ text: "텍스트를 입력하세요", fontSize: "14px", fontWeight: "normal", - color: "#374151", + color: "#3b83f6", textAlign: "left", }, defaultSize: { width: 150, height: 24 }, diff --git a/frontend/lib/registry/components/text-input/TextInputComponent.tsx b/frontend/lib/registry/components/text-input/TextInputComponent.tsx index 4a5aabf6..f4fe7a9e 100644 --- a/frontend/lib/registry/components/text-input/TextInputComponent.tsx +++ b/frontend/lib/registry/components/text-input/TextInputComponent.tsx @@ -190,7 +190,7 @@ export const TextInputComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx index 04128d74..482280b0 100644 --- a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx +++ b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx @@ -84,7 +84,7 @@ export const TextareaBasicComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), diff --git a/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx b/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx index 8183e1c0..f71a4127 100644 --- a/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx +++ b/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx @@ -84,7 +84,7 @@ export const ToggleSwitchComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), @@ -173,7 +173,7 @@ export const ToggleSwitchComponent: React.FC = ({
{ const element = e.currentTarget; - element.style.borderColor = "#3b82f6"; - element.style.backgroundColor = "rgba(59, 130, 246, 0.02)"; - element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)"; + // 🎯 컴포넌트가 있는 존은 호버 효과 최소화 + if (zoneChildren.length > 0) { + element.style.backgroundColor = "rgba(59, 130, 246, 0.01)"; + } else { + element.style.borderColor = "#3b82f6"; + element.style.backgroundColor = "rgba(59, 130, 246, 0.02)"; + element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)"; + } }} onMouseLeave={(e) => { const element = e.currentTarget; - element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; - element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; + if (zoneChildren.length > 0) { + // 컴포넌트가 있는 존 복원 + element.style.borderColor = "transparent"; + element.style.backgroundColor = isDesignMode ? "rgba(248, 250, 252, 0.3)" : "rgba(248, 250, 252, 0.5)"; + } else { + // 빈 존 복원 + element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; + element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; + } element.style.boxShadow = "none"; }} onDrop={this.handleDrop(zone.id)} diff --git a/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx b/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx index 43488c51..e108b7c7 100644 --- a/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx +++ b/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx @@ -148,7 +148,7 @@ const AccordionSection: React.FC<{ const headerStyle: React.CSSProperties = { padding: "12px 16px", backgroundColor: isDesignMode ? "#3b82f6" : "#f8fafc", - color: isDesignMode ? "white" : "#374151", + color: isDesignMode ? "white" : "#3b83f6", border: "1px solid #e2e8f0", borderBottom: isExpanded ? "none" : "1px solid #e2e8f0", cursor: "pointer", diff --git a/frontend/lib/registry/utils/hotReload.ts b/frontend/lib/registry/utils/hotReload.ts index c4688100..a2f61a29 100644 --- a/frontend/lib/registry/utils/hotReload.ts +++ b/frontend/lib/registry/utils/hotReload.ts @@ -14,6 +14,10 @@ let hotReloadListeners: Array<() => void> = []; * Hot Reload 시스템 초기화 */ export function initializeHotReload(): void { + // 핫 리로드 시스템 임시 비활성화 (디버깅 목적) + console.log("🔥 컴포넌트 Hot Reload 시스템 비활성화됨 (디버깅 모드)"); + return; + if (process.env.NODE_ENV !== "development" || typeof window === "undefined") { return; } @@ -55,11 +59,15 @@ function setupDevServerEventListener(): void { const originalLog = console.log; let reloadPending = false; - // console.log 메시지를 감지하여 Hot Reload 트리거 + // console.log 메시지를 감지하여 Hot Reload 트리거 (특정 메시지만) console.log = (...args: any[]) => { const message = args.join(" "); - if (message.includes("compiled") || message.includes("Fast Refresh") || message.includes("component")) { + // 핫 리로드를 트리거할 특정 메시지만 감지 (디버깅 로그는 제외) + if ((message.includes("compiled") || message.includes("Fast Refresh")) && + !message.includes("🔍") && !message.includes("🎯") && !message.includes("📤") && + !message.includes("📥") && !message.includes("⚠️") && !message.includes("🔄") && + !message.includes("✅") && !message.includes("🔧") && !message.includes("📋")) { if (!reloadPending) { reloadPending = true; setTimeout(() => { diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index 8ff6fd55..fa464377 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -110,6 +110,8 @@ export const DynamicComponentConfigPanel: React.FC = screenTableName, tableColumns, }) => { + console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`); + const [ConfigPanelComponent, setConfigPanelComponent] = React.useState | null>(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); @@ -180,10 +182,21 @@ export const DynamicComponentConfigPanel: React.FC = ); } + 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 ( diff --git a/frontend/scripts/create-component.js b/frontend/scripts/create-component.js index 674f48d7..83d5c852 100755 --- a/frontend/scripts/create-component.js +++ b/frontend/scripts/create-component.js @@ -661,7 +661,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > @@ -709,7 +709,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > @@ -785,7 +785,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/types/component.ts b/frontend/types/component.ts index 34a8cd92..ea29cc5f 100644 --- a/frontend/types/component.ts +++ b/frontend/types/component.ts @@ -68,6 +68,9 @@ export interface ComponentRendererProps { // 새로운 기능들 autoGeneration?: AutoGenerationConfig; // 자동생성 설정 hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) + + // 설정 변경 핸들러 + onConfigChange?: (config: any) => void; [key: string]: any; } @@ -317,7 +320,7 @@ export const COMPONENT_CATEGORIES_INFO = { [ComponentCategory.CHART]: { name: "차트", description: "데이터 시각화 컴포넌트", - color: "#06b6d4", + color: "#3b83f6", }, [ComponentCategory.FORM]: { name: "폼", @@ -347,7 +350,7 @@ export const COMPONENT_CATEGORIES_INFO = { [ComponentCategory.CONTAINER]: { name: "컨테이너", description: "다른 컴포넌트를 담는 컨테이너", - color: "#374151", + color: "#3b83f6", }, [ComponentCategory.SYSTEM]: { name: "시스템",