From 4924fbe71de6c57516c434a73fedaaf799b1a81c Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 30 Oct 2025 17:02:30 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20UI=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=82=AD=EC=A0=9C=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테이블 삭제 방식을 체크박스 선택 기반 일괄 삭제로 변경 - 좌측 테이블 리스트 영역에 스크롤 적용 - 선택된 테이블에 검정 테두리 표시 (border-2 border-black) - 우측 상단 타이틀 제거 - 각 테이블 카드에 라운딩 적용 (rounded-lg) - 컬럼 간 간격 개선 (입력 타입-상세 설정 간격 증가) - Entity 설정 박스 스타일 제거 (평면적 레이아웃으로 변경) - 좌측 영역 우측 여백 조정 (pr-4) --- .../(main)/admin/batch-management/page.tsx | 16 +- .../admin/collection-management/page.tsx | 34 +- .../admin/external-connections/page.tsx | 40 +- frontend/app/(main)/admin/standards/page.tsx | 22 +- frontend/app/(main)/admin/tableMng/page.tsx | 437 +++++++++++++----- frontend/app/(main)/admin/templates/page.tsx | 46 +- frontend/components/admin/CompanyTable.tsx | 24 +- frontend/components/admin/UserTable.tsx | 34 +- frontend/components/screen/ScreenList.tsx | 87 ++-- 9 files changed, 458 insertions(+), 282 deletions(-) diff --git a/frontend/app/(main)/admin/batch-management/page.tsx b/frontend/app/(main)/admin/batch-management/page.tsx index ed762521..5bb82a84 100644 --- a/frontend/app/(main)/admin/batch-management/page.tsx +++ b/frontend/app/(main)/admin/batch-management/page.tsx @@ -353,14 +353,14 @@ export default function BatchManagementPage() { - 작업명 - 타입 - 스케줄 - 상태 - 실행 통계 - 성공률 - 마지막 실행 - 작업 + 작업명 + 타입 + 스케줄 + 상태 + 실행 통계 + 성공률 + 마지막 실행 + 작업 diff --git a/frontend/app/(main)/admin/collection-management/page.tsx b/frontend/app/(main)/admin/collection-management/page.tsx index 6523b1d0..75f00cdb 100644 --- a/frontend/app/(main)/admin/collection-management/page.tsx +++ b/frontend/app/(main)/admin/collection-management/page.tsx @@ -249,20 +249,20 @@ export default function CollectionManagementPage() {
- 설정명 - 수집 타입 - 소스 테이블 - 대상 테이블 - 스케줄 - 상태 - 마지막 수집 - 작업 + 설정명 + 수집 타입 + 소스 테이블 + 대상 테이블 + 스케줄 + 상태 + 마지막 수집 + 작업 {filteredConfigs.map((config) => ( - - + +
{config.config_name}
{config.description && ( @@ -272,27 +272,27 @@ export default function CollectionManagementPage() { )}
- + {getTypeBadge(config.collection_type)} - + {config.source_table} - + {config.target_table || "-"} - + {config.schedule_cron || "-"} - + {getStatusBadge(config.is_active)} - + {config.last_collected_at ? new Date(config.last_collected_at).toLocaleString() : "-"} - +
- - 연결명 - DB 타입 - 호스트:포트 - 데이터베이스 - 사용자 - 상태 - 생성일 - 연결 테스트 - 작업 + + 연결명 + DB 타입 + 호스트:포트 + 데이터베이스 + 사용자 + 상태 + 생성일 + 연결 테스트 + 작업 {connections.map((connection) => ( - - + +
{connection.connection_name}
- + {DB_TYPE_LABELS[connection.db_type] || connection.db_type} - + {connection.host}:{connection.port} - {connection.database_name} - {connection.username} - + {connection.database_name} + {connection.username} + {connection.is_active === "Y" ? "활성" : "비활성"} - + {connection.created_date ? new Date(connection.created_date).toLocaleDateString() : "N/A"} - +
+ )} +
+ )} + {loading ? (
@@ -707,40 +828,42 @@ export default function TableManagementPage() { .map((table) => (
-
handleTableSelect(table.tableName)}> -

{table.displayName || table.tableName}

-

- {table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")} -

-
- 컬럼 - - {table.columnCount} - +
+ {/* 체크박스 (최고 관리자만) */} + {isSuperAdmin && ( + handleTableCheck(table.tableName, checked as boolean)} + aria-label={`${table.displayName || table.tableName} 선택`} + className="mt-0.5" + onClick={(e) => e.stopPropagation()} + /> + )} +
handleTableSelect(table.tableName)} + > +

{table.displayName || table.tableName}

+

+ {table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")} +

+
+ 컬럼 + + {table.columnCount} + +
- - {/* 삭제 버튼 (최고 관리자만) */} - {isSuperAdmin && ( -
- -
- )}
)) )} @@ -749,14 +872,9 @@ export default function TableManagementPage() {
{/* 우측 메인 영역: 컬럼 타입 관리 (80%) */} -
-
-

- - {selectedTable ? <>테이블 설정 - {selectedTable} : "테이블 타입 관리"} -

- -
+
+
+
{!selectedTable ? (
@@ -801,19 +919,19 @@ export default function TableManagementPage() { ) : (
{/* 컬럼 헤더 */} -
-
컬럼명
+
+
컬럼명
라벨
-
입력 타입
-
+
입력 타입
+
상세 설정
-
설명
+
설명
{/* 컬럼 리스트 */}
{ const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; // 스크롤이 끝에 가까워지면 더 많은 데이터 로드 @@ -825,9 +943,9 @@ export default function TableManagementPage() { {columns.map((column, index) => (
-
+
{column.columnName}
@@ -838,7 +956,7 @@ export default function TableManagementPage() { className="h-8 text-xs" />
-
+
-
+
{/* 웹 타입이 'code'인 경우 공통코드 선택 */} {column.inputType === "code" && ( + handleDetailSettingsChange(column.columnName, "entity", value) + } + > + + + + + {referenceTableOptions.map((option, index) => ( + +
+ {option.label} + + {option.value} + +
+
+ ))} +
+
-
- {/* 참조 테이블 */} + {/* 조인 컬럼 */} + {column.referenceTable && column.referenceTable !== "none" && (
+ )} - {/* 조인 컬럼 */} - {column.referenceTable && column.referenceTable !== "none" && ( + {/* 표시 컬럼 */} + {column.referenceTable && + column.referenceTable !== "none" && + column.referenceColumn && + column.referenceColumn !== "none" && (
)} -
- - {/* 설정 완료 표시 - 간소화 */} - {column.referenceTable && - column.referenceTable !== "none" && - column.referenceColumn && - column.referenceColumn !== "none" && - column.displayColumn && - column.displayColumn !== "none" && ( -
- - - {column.columnName} → {column.referenceTable}.{column.displayColumn} - -
- )}
+ + {/* 설정 완료 표시 */} + {column.referenceTable && + column.referenceTable !== "none" && + column.referenceColumn && + column.referenceColumn !== "none" && + column.displayColumn && + column.displayColumn !== "none" && ( +
+ + + {column.columnName} → {column.referenceTable}.{column.displayColumn} + +
+ )}
)} {/* 다른 웹 타입인 경우 빈 공간 */} {column.inputType !== "code" && column.inputType !== "entity" && ( -
-
+
+ - +
)}
-
+
handleColumnChange(index, "description", e.target.value)} @@ -1075,26 +1234,62 @@ export default function TableManagementPage() { - 테이블 삭제 확인 + + {selectedTableIds.size > 0 ? "테이블 일괄 삭제 확인" : "테이블 삭제 확인"} + - 정말로 테이블을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. + {selectedTableIds.size > 0 ? ( + <> + 선택된 {selectedTableIds.size}개의 테이블을 삭제하시겠습니까? +
+ 이 작업은 되돌릴 수 없습니다. + + ) : ( + <> + 정말로 테이블을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. + + )}
-
-
-

경고

-

- 테이블 {tableToDelete}과 모든 데이터가 영구적으로 - 삭제됩니다. -

+ {selectedTableIds.size === 0 && tableToDelete && ( +
+
+

경고

+

+ 테이블 {tableToDelete}과 모든 데이터가 영구적으로 + 삭제됩니다. +

+
-
+ )} + + {selectedTableIds.size > 0 && ( +
+
+

경고

+

+ 다음 테이블들과 모든 데이터가 영구적으로 삭제됩니다: +

+
    + {Array.from(selectedTableIds).map((tableName) => ( +
  • + {tableName} +
  • + ))} +
+
+
+ )}
- + - + - + - 카테고리 - 설명 - 아이콘 - 기본 크기 - 공개 여부 - 활성화 - 수정일 - 작업 + 카테고리 + 설명 + 아이콘 + 기본 크기 + 공개 여부 + 활성화 + 수정일 + 작업 @@ -299,39 +299,39 @@ export default function TemplatesManagePage() { ) : ( filteredAndSortedTemplates.map((template) => ( - - {template.sort_order || 0} - {template.template_code} - + + {template.sort_order || 0} + {template.template_code} + {template.template_name} {template.template_name_eng && (
{template.template_name_eng}
)}
- + {template.category} - {template.description || "-"} - + {template.description || "-"} +
{renderIcon(template.icon_name)}
- + {template.default_size ? `${template.default_size.width}×${template.default_size.height}` : "-"} - + {template.is_public === "Y" ? "공개" : "비공개"} - + {template.is_active === "Y" ? "활성화" : "비활성화"} - + {template.updated_date ? new Date(template.updated_date).toLocaleDateString("ko-KR") : "-"} - +
- - 화면명 - 화면 코드 - 테이블명 - 상태 - 생성일 - 작업 + + 화면명 + 화면 코드 + 테이블명 + 상태 + 생성일 + 작업 {screens.map((screen) => ( onDesignScreen(screen)} > - +
{screen.screenName}
{screen.description && ( @@ -461,26 +458,26 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr )}
- + {screen.screenCode} - + {screen.tableLabel || screen.tableName} - + {screen.isActive === "Y" ? "활성" : "비활성"} - +
{screen.createdDate.toLocaleDateString()}
{screen.createdBy}
- + - )} - +
- - + + 0 && selectedScreenIds.length === deletedScreens.length} onCheckedChange={handleSelectAll} aria-label="전체 선택" /> - 화면명 - 화면 코드 - 테이블명 - 삭제일 - 삭제자 - 삭제 사유 - 작업 + 화면명 + 화면 코드 + 테이블명 + 삭제일 + 삭제자 + 삭제 사유 + 작업 {deletedScreens.map((screen) => ( - - + + handleScreenCheck(screen.screenId, checked as boolean)} aria-label={`${screen.screenName} 선택`} /> - +
{screen.screenName}
{screen.description && ( @@ -724,28 +706,28 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr )}
- + {screen.screenCode} - + {screen.tableLabel || screen.tableName} - +
{screen.deletedDate?.toLocaleDateString()}
- +
{screen.deletedBy}
- +
{screen.deleteReason || "-"}
- +
{selectedScreenIds.length > 0 && (
- + 연결명 DB 타입 호스트:포트 diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index 673d3970..8c5e7e82 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -117,7 +117,7 @@ export default function WebTypesManagePage() { return (
-
웹타입 목록을 불러오는데 실패했습니다.
+
웹타입 목록을 불러오는데 실패했습니다.
@@ -127,13 +127,13 @@ export default function WebTypesManagePage() { } return ( -
+
{/* 페이지 제목 */} -
+
-

웹타입 관리

-

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

+

웹타입 관리

+

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

- - - 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" ? : )} +
+
+ 작업
@@ -325,7 +324,7 @@ export default function WebTypesManagePage() { @@ -341,7 +340,7 @@ export default function WebTypesManagePage() { handleDelete(webType.web_type, webType.type_name)} disabled={isDeleting} - className="bg-red-600 hover:bg-red-700" + className="bg-destructive hover:bg-destructive/90" > {isDeleting ? "삭제 중..." : "삭제"} @@ -355,12 +354,11 @@ export default function WebTypesManagePage() { )}
- - + {deleteError && ( -
-

+

+

삭제 중 오류가 발생했습니다: {deleteError instanceof Error ? deleteError.message : "알 수 없는 오류"}

diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 0a5f205b..353e487c 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -829,7 +829,9 @@ export default function TableManagementPage() {
{!selectedTable ? ( -
+

{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")} diff --git a/frontend/components/admin/CompanyTable.tsx b/frontend/components/admin/CompanyTable.tsx index 6ba78cc0..86ccdee3 100644 --- a/frontend/components/admin/CompanyTable.tsx +++ b/frontend/components/admin/CompanyTable.tsx @@ -129,7 +129,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company return ( <> {/* 데스크톱 테이블 뷰 (lg 이상) */} -

+
diff --git a/frontend/components/admin/MenuManagement.tsx b/frontend/components/admin/MenuManagement.tsx index 4e948651..0dc30248 100644 --- a/frontend/components/admin/MenuManagement.tsx +++ b/frontend/components/admin/MenuManagement.tsx @@ -37,6 +37,10 @@ export const MenuManagement: React.FC = () => { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [selectedMenuId, setSelectedMenuId] = useState(""); const [selectedMenus, setSelectedMenus] = useState>(new Set()); + + // 메뉴 관리 화면용 로컬 상태 (모든 상태의 메뉴 표시) + const [localAdminMenus, setLocalAdminMenus] = useState([]); + const [localUserMenus, setLocalUserMenus] = useState([]); // 다국어 텍스트 훅 사용 // getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용 @@ -176,6 +180,7 @@ export const MenuManagement: React.FC = () => { // 초기 로딩 useEffect(() => { loadCompanies(); + loadMenus(false); // 메뉴 목록 로드 (메뉴 관리 화면용 - 모든 상태 표시) // 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정 if (!userLang) { initializeDefaultTexts(); @@ -373,12 +378,54 @@ export const MenuManagement: React.FC = () => { }; }, [isCompanyDropdownOpen]); + // 특정 메뉴 타입만 로드하는 함수 + const loadMenusForType = async (type: MenuType, showLoading = true) => { + try { + if (showLoading) { + setLoading(true); + } + + if (type === "admin") { + const adminResponse = await menuApi.getAdminMenusForManagement(); + if (adminResponse.success && adminResponse.data) { + setLocalAdminMenus(adminResponse.data); + } + } else { + const userResponse = await menuApi.getUserMenusForManagement(); + if (userResponse.success && userResponse.data) { + setLocalUserMenus(userResponse.data); + } + } + } catch (error) { + toast.error(getUITextSync("message.error.load.menu.list")); + } finally { + if (showLoading) { + setLoading(false); + } + } + }; + const loadMenus = async (showLoading = true) => { // console.log(`📋 메뉴 목록 조회 시작 (showLoading: ${showLoading})`); try { if (showLoading) { setLoading(true); } + + // 선택된 메뉴 타입에 해당하는 메뉴만 로드 + if (selectedMenuType === "admin") { + const adminResponse = await menuApi.getAdminMenusForManagement(); + if (adminResponse.success && adminResponse.data) { + setLocalAdminMenus(adminResponse.data); + } + } else { + const userResponse = await menuApi.getUserMenusForManagement(); + if (userResponse.success && userResponse.data) { + setLocalUserMenus(userResponse.data); + } + } + + // 전역 메뉴 상태도 업데이트 (좌측 사이드바용) await refreshMenus(); // console.log("📋 메뉴 목록 조회 성공"); } catch (error) { @@ -558,7 +605,7 @@ export const MenuManagement: React.FC = () => { const handleAddMenu = (parentId: string, menuType: string, level: number) => { // 상위 메뉴의 회사 정보 찾기 - const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus; + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; const parentMenu = currentMenus.find((menu) => menu.objid === parentId); setFormData({ @@ -575,7 +622,7 @@ export const MenuManagement: React.FC = () => { // console.log("🔧 메뉴 수정 시작 - menuId:", menuId); // 현재 메뉴 정보 찾기 - const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus; + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId); if (menuToEdit) { @@ -614,7 +661,7 @@ export const MenuManagement: React.FC = () => { }; const handleSelectAllMenus = (checked: boolean) => { - const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus; + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; if (checked) { // 모든 메뉴 선택 (최상위 메뉴 포함) setSelectedMenus(new Set(currentMenus.map((menu) => menu.objid || menu.OBJID || ""))); @@ -726,7 +773,8 @@ export const MenuManagement: React.FC = () => { }; const getCurrentMenus = () => { - const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus; + // 메뉴 관리 화면용: 모든 상태의 메뉴 표시 (localAdminMenus/localUserMenus 사용) + const currentMenus = selectedMenuType === "admin" ? localAdminMenus : localUserMenus; // 검색어 필터링 let filteredMenus = currentMenus; @@ -755,6 +803,13 @@ export const MenuManagement: React.FC = () => { setSelectedMenuType(type); setSelectedMenus(new Set()); // 선택된 메뉴 초기화 setExpandedMenus(new Set()); // 메뉴 타입 변경 시 확장 상태 초기화 + + // 선택한 메뉴 타입에 해당하는 메뉴만 로드 + if (type === "admin" && localAdminMenus.length === 0) { + loadMenusForType("admin", false); + } else if (type === "user" && localUserMenus.length === 0) { + loadMenusForType("user", false); + } }; const handleToggleExpand = (menuId: string) => { @@ -777,8 +832,8 @@ export const MenuManagement: React.FC = () => { // uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산 const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]); - const adminMenusCount = useMemo(() => adminMenus?.length || 0, [adminMenus]); - const userMenusCount = useMemo(() => userMenus?.length || 0, [userMenus]); + const adminMenusCount = useMemo(() => localAdminMenus?.length || 0, [localAdminMenus]); + const userMenusCount = useMemo(() => localUserMenus?.length || 0, [localUserMenus]); // 디버깅을 위한 간단한 상태 표시 // console.log("🔍 MenuManagement 렌더링 상태:", { @@ -823,7 +878,7 @@ export const MenuManagement: React.FC = () => {

- {adminMenus.length} + {localAdminMenus.length} @@ -842,7 +897,7 @@ export const MenuManagement: React.FC = () => {

- {userMenus.length} + {localUserMenus.length} diff --git a/frontend/components/admin/MenuTable.tsx b/frontend/components/admin/MenuTable.tsx index 1eb31b8c..eb790e93 100644 --- a/frontend/components/admin/MenuTable.tsx +++ b/frontend/components/admin/MenuTable.tsx @@ -145,10 +145,10 @@ export const MenuTable: React.FC = ({ return (
{title &&

{title}

} -
+
- + -
로딩 중...
+
+
로딩 중...
) : connections.length === 0 ? ( -
+
-

등록된 REST API 연결이 없습니다

+

등록된 REST API 연결이 없습니다

) : ( -
+
- - 연결명 - 기본 URL - 인증 타입 - 헤더 수 - 상태 - 마지막 테스트 - 연결 테스트 - 작업 + + 연결명 + 기본 URL + 인증 타입 + 헤더 수 + 상태 + 마지막 테스트 + 연결 테스트 + 작업 {connections.map((connection) => ( - - + +
{connection.connection_name} @@ -291,23 +291,23 @@ export function RestApiConnectionList() { )}
- +
{connection.base_url}
- + {AUTH_TYPE_LABELS[connection.auth_type] || connection.auth_type} - + {Object.keys(connection.default_headers || {}).length} - + {connection.is_active === "Y" ? "활성" : "비활성"} - + {connection.last_test_date ? (
{new Date(connection.last_test_date).toLocaleDateString()}
@@ -322,7 +322,7 @@ export function RestApiConnectionList() { - )} - +
@@ -123,7 +123,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on // 빈 상태 if (users.length === 0) { return ( -
+

등록된 사용자가 없습니다.

); @@ -133,7 +133,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on return (
{/* 데스크톱 테이블 */} -
+
@@ -182,7 +182,7 @@ export function UserAuthTable({ users, isLoading, paginationInfo, onEditAuth, on return (
{/* 헤더 */}
diff --git a/frontend/components/admin/UserTable.tsx b/frontend/components/admin/UserTable.tsx index 5c1e16f5..1d6d6d46 100644 --- a/frontend/components/admin/UserTable.tsx +++ b/frontend/components/admin/UserTable.tsx @@ -186,7 +186,7 @@ export function UserTable({ return ( <> {/* 데스크톱 테이블 뷰 (lg 이상) */} -
+
diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx index a9dfcdc4..fa96bb99 100644 --- a/frontend/components/dataflow/DataFlowList.tsx +++ b/frontend/components/dataflow/DataFlowList.tsx @@ -146,12 +146,6 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { return (
- {/* 섹션 제목 */} -
-

플로우 목록

-

저장된 노드 플로우를 불러오거나 새로운 플로우를 생성합니다

-
- {/* 검색 및 액션 영역 */}
{/* 검색 영역 */} @@ -184,33 +178,33 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { {loading ? ( <> {/* 데스크톱 테이블 스켈레톤 */} -
+
- - 플로우명 - 설명 - 생성일 - 최근 수정 - 작업 + + 플로우명 + 설명 + 생성일 + 최근 수정 + 작업 {Array.from({ length: 5 }).map((_, index) => ( - - + +
- +
- +
- +
- +
@@ -264,46 +258,46 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { ) : ( <> {/* 데스크톱 테이블 뷰 (lg 이상) */} -
+
- - 플로우명 - 설명 - 생성일 - 최근 수정 - 작업 + + 플로우명 + 설명 + 생성일 + 최근 수정 + 작업 {filteredFlows.map((flow) => ( onLoadFlow(flow.flowId)} > - +
{flow.flowName}
- +
{flow.flowDescription || "설명 없음"}
- +
{new Date(flow.createdAt).toLocaleDateString()}
- +
{new Date(flow.updatedAt).toLocaleDateString()}
- e.stopPropagation()}> + e.stopPropagation()}>
diff --git a/frontend/components/ui/tabs.tsx b/frontend/components/ui/tabs.tsx index 497ba5ea..95fdc082 100644 --- a/frontend/components/ui/tabs.tsx +++ b/frontend/components/ui/tabs.tsx @@ -26,7 +26,7 @@ function TabsList({ { } export const menuApi = { - // 관리자 메뉴 목록 조회 + // 관리자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시) getAdminMenus: async (): Promise> => { const response = await apiClient.get("/admin/menus", { params: { menuType: "0" } }); if (response.data.success && response.data.data && response.data.data.length > 0) { @@ -84,12 +84,24 @@ export const menuApi = { return response.data; }, - // 사용자 메뉴 목록 조회 + // 사용자 메뉴 목록 조회 (좌측 사이드바용 - active만 표시) getUserMenus: async (): Promise> => { const response = await apiClient.get("/admin/menus", { params: { menuType: "1" } }); return response.data; }, + // 관리자 메뉴 목록 조회 (메뉴 관리 화면용 - 모든 상태 표시) + getAdminMenusForManagement: async (): Promise> => { + const response = await apiClient.get("/admin/menus", { params: { menuType: "0", includeInactive: "true" } }); + return response.data; + }, + + // 사용자 메뉴 목록 조회 (메뉴 관리 화면용 - 모든 상태 표시) + getUserMenusForManagement: async (): Promise> => { + const response = await apiClient.get("/admin/menus", { params: { menuType: "1", includeInactive: "true" } }); + return response.data; + }, + // 메뉴 정보 조회 getMenuInfo: async (menuId: string): Promise> => { const response = await apiClient.get(`/admin/menus/${menuId}`); From a819ea6bfa30ec48f7eb2d67580cd9ada1fa14b7 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 30 Oct 2025 18:30:39 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EA=B2=80=EC=83=89=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 플로우 위젯 단계 박스 미니멀 디자인 적용 - 테두리와 배경 제거, 하단 선만 표시 - STEP 배지 제거, 단계명과 건수 상하 배치 - 선택 인디케이터(ChevronUp) 제거 - 건수 폰트 굵기 조정 (font-medium) - 검색 필터 기능 개선 - 그리드 컬럼 수 확장 (최대 6개까지) - 상단 타이틀과 검색 필터 사이 여백 조정 - 검색 필터 설정 시 표시되는 컬럼만 선택 가능하도록 변경 - 필터 설정을 사용자별로 저장하도록 변경 - 이전 사용자의 필터 설정 자동 정리 로직 추가 - 기본 버튼 컴포넌트 스타일 변경 - 배경 흰색, 검정 테두리로 변경 --- .../components/screen/widgets/FlowWidget.tsx | 165 ++++++++++++------ frontend/components/ui/button.tsx | 2 +- 2 files changed, 110 insertions(+), 57 deletions(-) diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index 9508cb0d..bffae228 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -38,6 +38,7 @@ import { import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; +import { useAuth } from "@/hooks/useAuth"; interface FlowWidgetProps { component: FlowComponent; @@ -55,6 +56,7 @@ export function FlowWidget({ onFlowRefresh, }: FlowWidgetProps) { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 + const { user } = useAuth(); // 사용자 정보 가져오기 // 🆕 전역 상태 관리 const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep); @@ -117,30 +119,64 @@ export function FlowWidget({ // 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용) const flowComponentId = component.id; - // 🆕 localStorage 키 생성 + // 🆕 localStorage 키 생성 (사용자별로 저장) const filterSettingKey = useMemo(() => { - if (!flowId || selectedStepId === null) return null; - return `flowWidget_searchFilters_${flowId}_${selectedStepId}`; - }, [flowId, selectedStepId]); + if (!flowId || selectedStepId === null || !user?.userId) return null; + return `flowWidget_searchFilters_${user.userId}_${flowId}_${selectedStepId}`; + }, [flowId, selectedStepId, user?.userId]); // 🆕 저장된 필터 설정 불러오기 useEffect(() => { - if (!filterSettingKey || allAvailableColumns.length === 0) return; + if (!filterSettingKey || stepDataColumns.length === 0 || !user?.userId) return; try { + // 현재 사용자의 필터 설정만 불러오기 const saved = localStorage.getItem(filterSettingKey); if (saved) { const savedFilters = JSON.parse(saved); - setSearchFilterColumns(new Set(savedFilters)); + // 현재 단계에 표시되는 컬럼만 필터링 + const validFilters = savedFilters.filter((col: string) => stepDataColumns.includes(col)); + setSearchFilterColumns(new Set(validFilters)); } else { // 초기값: 빈 필터 (사용자가 선택해야 함) setSearchFilterColumns(new Set()); } + + // 이전 사용자의 필터 설정 정리 (사용자 ID가 다른 키들 제거) + if (typeof window !== "undefined") { + const currentUserId = user.userId; + const keysToRemove: string[] = []; + + // localStorage의 모든 키를 확인 + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith("flowWidget_searchFilters_")) { + // 키 형식: flowWidget_searchFilters_${userId}_${flowId}_${stepId} + // split("_")를 하면 ["flowWidget", "searchFilters", "사용자ID", "플로우ID", "스텝ID"] + // 따라서 userId는 parts[2]입니다 + const parts = key.split("_"); + if (parts.length >= 3) { + const userIdFromKey = parts[2]; // flowWidget_searchFilters_ 다음이 userId + // 현재 사용자 ID와 다른 사용자의 설정은 제거 + if (userIdFromKey !== currentUserId) { + keysToRemove.push(key); + } + } + } + } + + // 이전 사용자의 설정 제거 + if (keysToRemove.length > 0) { + keysToRemove.forEach(key => { + localStorage.removeItem(key); + }); + } + } } catch (error) { console.error("필터 설정 불러오기 실패:", error); setSearchFilterColumns(new Set()); } - }, [filterSettingKey, allAvailableColumns]); + }, [filterSettingKey, stepDataColumns, user?.userId]); // 🆕 필터 설정 저장 const saveFilterSettings = useCallback(() => { @@ -174,14 +210,14 @@ export function FlowWidget({ // 🆕 전체 선택/해제 const toggleAllFilters = useCallback(() => { - if (searchFilterColumns.size === allAvailableColumns.length) { + if (searchFilterColumns.size === stepDataColumns.length) { // 전체 해제 setSearchFilterColumns(new Set()); } else { // 전체 선택 - setSearchFilterColumns(new Set(allAvailableColumns)); + setSearchFilterColumns(new Set(stepDataColumns)); } - }, [searchFilterColumns, allAvailableColumns]); + }, [searchFilterColumns, stepDataColumns]); // 🆕 검색 초기화 const handleClearSearch = useCallback(() => { @@ -638,59 +674,76 @@ export function FlowWidget({ {/* 스텝 카드 */}
handleStepClick(step.id, step.stepName)} > - {/* 단계 번호 배지 */} -
- Step {step.stepOrder} -
+ {/* 콘텐츠 */} +
+ {/* 스텝 이름 */} +

+ {step.stepName} +

- {/* 스텝 이름 */} -

- {step.stepName} -

- - {/* 데이터 건수 */} - {showStepCount && ( -
-
- + {/* 데이터 건수 */} + {showStepCount && ( +
+ {stepCounts[step.id] || 0} - +
-
- )} + )} +
- {/* 선택 인디케이터 */} - {selectedStepId === step.id && ( -
- -
- )} + {/* 하단 선 */} +
{/* 화살표 (마지막 스텝 제외) */} {index < steps.length - 1 && ( -
+
{displayMode === "horizontal" ? ( - - - +
+
+ + + +
+
) : ( - - - +
+
+ + + +
+
)}
)} @@ -720,7 +773,7 @@ export function FlowWidget({
{/* 🆕 필터 설정 버튼 */} - {allAvailableColumns.length > 0 && ( + {stepDataColumns.length > 0 && (
-
+
{Array.from(searchFilterColumns).map((col) => (
+ + {showIndex && ( -
- {itemIndex + 1} -
+ # )} - - {/* 드래그 핸들 */} - {allowReorder && !readonly && !disabled && ( -
- -
+ {allowReorder && ( + )} - - {/* 필드들 */} {fields.map((field) => ( -
- {renderField(field, itemIndex, item[field.name])} -
+ + {field.label} + {field.required && *} + ))} - - {/* 삭제 버튼 */} -
- {!readonly && !disabled && items.length > minItems && ( - + 작업 + + + + {items.map((item, itemIndex) => ( + -
- ))} - + draggable={allowReorder && !readonly && !disabled} + onDragStart={() => handleDragStart(itemIndex)} + onDragOver={(e) => handleDragOver(e, itemIndex)} + onDrop={(e) => handleDrop(e, itemIndex)} + onDragEnd={handleDragEnd} + > + {/* 인덱스 번호 */} + {showIndex && ( + + {itemIndex + 1} + + )} + + {/* 드래그 핸들 */} + {allowReorder && !readonly && !disabled && ( + + + + )} + + {/* 필드들 */} + {fields.map((field) => ( + + {renderField(field, itemIndex, item[field.name])} + + ))} + + {/* 삭제 버튼 */} + + {!readonly && !disabled && items.length > minItems && ( + + )} + +
+ ))} + +
); }; @@ -423,12 +420,12 @@ export const RepeaterInput: React.FC = ({
{/* 드래그 핸들 */} {allowReorder && !readonly && !disabled && ( - + )} {/* 인덱스 번호 */} {showIndex && ( - 항목 {itemIndex + 1} + 항목 {itemIndex + 1} )}
@@ -453,7 +450,7 @@ export const RepeaterInput: React.FC = ({ variant="ghost" size="icon" onClick={() => handleRemoveItem(itemIndex)} - className="h-8 w-8 text-red-500 hover:bg-red-50 hover:text-red-700" + className="h-8 w-8 text-destructive hover:bg-destructive/10 hover:text-destructive" title="항목 제거" > @@ -467,9 +464,9 @@ export const RepeaterInput: React.FC = ({
{fields.map((field) => (
-
@@ -500,7 +497,7 @@ export const RepeaterInput: React.FC = ({ )} {/* 제한 안내 */} -
+
현재: {items.length}개 항목 (최소: {minItems}, 최대: {maxItems})