테이블 타입관리 ui개선
This commit is contained in:
parent
eb5ea411c9
commit
4e29f92268
|
|
@ -1093,229 +1093,283 @@ export default function TableManagementPage() {
|
|||
|
||||
{/* 우측 메인 영역: 컬럼 타입 관리 (80%) */}
|
||||
<div className="flex h-full w-[80%] flex-col overflow-hidden pl-0">
|
||||
<div className="flex h-full flex-col space-y-4 overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{!selectedTable ? (
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{!selectedTable ? (
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 테이블 라벨 설정 */}
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={tableLabel}
|
||||
onChange={(e) => setTableLabel(e.target.value)}
|
||||
placeholder="테이블 표시명"
|
||||
className="h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={tableDescription}
|
||||
onChange={(e) => setTableDescription(e.target.value)}
|
||||
placeholder="테이블 설명"
|
||||
className="h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 테이블 라벨 설정 + 저장 버튼 (고정 영역) */}
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={tableLabel}
|
||||
onChange={(e) => setTableLabel(e.target.value)}
|
||||
placeholder="테이블 표시명"
|
||||
className="h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={tableDescription}
|
||||
onChange={(e) => setTableDescription(e.target.value)}
|
||||
placeholder="테이블 설명"
|
||||
className="h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
{/* 저장 버튼 (항상 보이도록 상단에 배치) */}
|
||||
<Button
|
||||
onClick={saveAllSettings}
|
||||
disabled={!selectedTable || columns.length === 0}
|
||||
className="h-10 gap-2 text-sm font-medium"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
전체 설정 저장
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{columnsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<LoadingSpinner />
|
||||
<span className="text-muted-foreground ml-2 text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
|
||||
</span>
|
||||
{columnsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<LoadingSpinner />
|
||||
<span className="text-muted-foreground ml-2 text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
|
||||
</span>
|
||||
</div>
|
||||
) : columns.length === 0 ? (
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* 컬럼 헤더 (고정) */}
|
||||
<div className="text-foreground grid h-12 flex-shrink-0 items-center border-b px-6 py-3 text-sm font-semibold" style={{ gridTemplateColumns: "160px 200px 250px 1fr" }}>
|
||||
<div className="pr-4">컬럼명</div>
|
||||
<div className="px-4">라벨</div>
|
||||
<div className="pr-6">입력 타입</div>
|
||||
<div className="pl-4">설명</div>
|
||||
</div>
|
||||
) : columns.length === 0 ? (
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* 컬럼 헤더 */}
|
||||
<div className="text-foreground grid h-12 items-center border-b px-6 py-3 text-sm font-semibold" style={{ gridTemplateColumns: "160px 200px 250px 1fr" }}>
|
||||
<div className="pr-4">컬럼명</div>
|
||||
<div className="px-4">라벨</div>
|
||||
<div className="pr-6">입력 타입</div>
|
||||
<div className="pl-4">설명</div>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 리스트 */}
|
||||
<div
|
||||
className="max-h-96 overflow-y-auto"
|
||||
onScroll={(e) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||
// 스크롤이 끝에 가까워지면 더 많은 데이터 로드
|
||||
if (scrollHeight - scrollTop <= clientHeight + 100) {
|
||||
loadMoreColumns();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map((column, index) => (
|
||||
<div
|
||||
key={column.columnName}
|
||||
className="bg-background hover:bg-muted/50 grid min-h-16 items-start border-b px-6 py-3 transition-colors"
|
||||
style={{ gridTemplateColumns: "160px 200px 250px 1fr" }}
|
||||
>
|
||||
<div className="pr-4 pt-1">
|
||||
<div className="font-mono text-sm">{column.columnName}</div>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<Input
|
||||
value={column.displayName || ""}
|
||||
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
|
||||
placeholder={column.columnName}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="pr-6">
|
||||
<div className="space-y-3">
|
||||
{/* 입력 타입 선택 */}
|
||||
{/* 컬럼 리스트 (스크롤 영역) */}
|
||||
<div
|
||||
className="flex-1 overflow-y-auto"
|
||||
onScroll={(e) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||
// 스크롤이 끝에 가까워지면 더 많은 데이터 로드
|
||||
if (scrollHeight - scrollTop <= clientHeight + 100) {
|
||||
loadMoreColumns();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map((column, index) => (
|
||||
<div
|
||||
key={column.columnName}
|
||||
className="bg-background hover:bg-muted/50 grid min-h-16 items-start border-b px-6 py-3 transition-colors"
|
||||
style={{ gridTemplateColumns: "160px 200px 250px 1fr" }}
|
||||
>
|
||||
<div className="pr-4 pt-1">
|
||||
<div className="font-mono text-sm">{column.columnName}</div>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<Input
|
||||
value={column.displayName || ""}
|
||||
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
|
||||
placeholder={column.columnName}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="pr-6">
|
||||
<div className="space-y-3">
|
||||
{/* 입력 타입 선택 */}
|
||||
<Select
|
||||
value={column.inputType || "text"}
|
||||
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="입력 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{memoizedInputTypeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* 입력 타입이 'code'인 경우 공통코드 선택 */}
|
||||
{column.inputType === "code" && (
|
||||
<Select
|
||||
value={column.inputType || "text"}
|
||||
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
|
||||
value={column.codeCategory || "none"}
|
||||
onValueChange={(value) =>
|
||||
handleDetailSettingsChange(column.columnName, "code", value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="입력 타입 선택" />
|
||||
<SelectValue placeholder="공통코드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{memoizedInputTypeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{commonCodeOptions.map((option, index) => (
|
||||
<SelectItem key={`code-${option.value}-${index}`} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* 입력 타입이 'code'인 경우 공통코드 선택 */}
|
||||
{column.inputType === "code" && (
|
||||
<Select
|
||||
value={column.codeCategory || "none"}
|
||||
onValueChange={(value) =>
|
||||
handleDetailSettingsChange(column.columnName, "code", value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="공통코드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{commonCodeOptions.map((option, index) => (
|
||||
<SelectItem key={`code-${option.value}-${index}`} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{/* 입력 타입이 'category'인 경우 2레벨 메뉴 다중 선택 */}
|
||||
{column.inputType === "category" && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-muted-foreground mb-1 block text-xs">
|
||||
적용할 메뉴 (2레벨)
|
||||
</label>
|
||||
<div className="border rounded-lg p-3 space-y-2 max-h-48 overflow-y-auto">
|
||||
{secondLevelMenus.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
2레벨 메뉴가 없습니다. 메뉴를 선택하지 않으면 모든 메뉴에서 사용 가능합니다.
|
||||
</p>
|
||||
) : (
|
||||
secondLevelMenus.map((menu) => {
|
||||
// menuObjid를 숫자로 변환하여 비교
|
||||
const menuObjidNum = Number(menu.menuObjid);
|
||||
const isChecked = (column.categoryMenus || []).includes(menuObjidNum);
|
||||
|
||||
return (
|
||||
<div key={menu.menuObjid} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`category-menu-${column.columnName}-${menu.menuObjid}`}
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
const currentMenus = column.categoryMenus || [];
|
||||
const newMenus = e.target.checked
|
||||
? [...currentMenus, menuObjidNum]
|
||||
: currentMenus.filter((id) => id !== menuObjidNum);
|
||||
|
||||
setColumns((prev) =>
|
||||
prev.map((col) =>
|
||||
col.columnName === column.columnName
|
||||
? { ...col, categoryMenus: newMenus }
|
||||
: col
|
||||
)
|
||||
);
|
||||
}}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`category-menu-${column.columnName}-${menu.menuObjid}`}
|
||||
className="text-xs cursor-pointer flex-1"
|
||||
>
|
||||
{menu.parentMenuName} → {menu.menuName}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
{column.categoryMenus && column.categoryMenus.length > 0 && (
|
||||
<p className="text-primary text-xs">
|
||||
{column.categoryMenus.length}개 메뉴 선택됨
|
||||
)}
|
||||
{/* 입력 타입이 'category'인 경우 2레벨 메뉴 다중 선택 */}
|
||||
{column.inputType === "category" && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-muted-foreground mb-1 block text-xs">
|
||||
적용할 메뉴 (2레벨)
|
||||
</label>
|
||||
<div className="border rounded-lg p-3 space-y-2 max-h-48 overflow-y-auto">
|
||||
{secondLevelMenus.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
2레벨 메뉴가 없습니다. 메뉴를 선택하지 않으면 모든 메뉴에서 사용 가능합니다.
|
||||
</p>
|
||||
) : (
|
||||
secondLevelMenus.map((menu) => {
|
||||
// menuObjid를 숫자로 변환하여 비교
|
||||
const menuObjidNum = Number(menu.menuObjid);
|
||||
const isChecked = (column.categoryMenus || []).includes(menuObjidNum);
|
||||
|
||||
return (
|
||||
<div key={menu.menuObjid} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`category-menu-${column.columnName}-${menu.menuObjid}`}
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
const currentMenus = column.categoryMenus || [];
|
||||
const newMenus = e.target.checked
|
||||
? [...currentMenus, menuObjidNum]
|
||||
: currentMenus.filter((id) => id !== menuObjidNum);
|
||||
|
||||
setColumns((prev) =>
|
||||
prev.map((col) =>
|
||||
col.columnName === column.columnName
|
||||
? { ...col, categoryMenus: newMenus }
|
||||
: col
|
||||
)
|
||||
);
|
||||
}}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`category-menu-${column.columnName}-${menu.menuObjid}`}
|
||||
className="text-xs cursor-pointer flex-1"
|
||||
>
|
||||
{menu.parentMenuName} → {menu.menuName}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
|
||||
{column.inputType === "entity" && (
|
||||
<>
|
||||
{/* 참조 테이블 */}
|
||||
{column.categoryMenus && column.categoryMenus.length > 0 && (
|
||||
<p className="text-primary text-xs">
|
||||
{column.categoryMenus.length}개 메뉴 선택됨
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
|
||||
{column.inputType === "entity" && (
|
||||
<>
|
||||
{/* 참조 테이블 */}
|
||||
<div className="w-48">
|
||||
<label className="text-muted-foreground mb-1 block text-xs">
|
||||
참조 테이블
|
||||
</label>
|
||||
<Select
|
||||
value={column.referenceTable || "none"}
|
||||
onValueChange={(value) =>
|
||||
handleDetailSettingsChange(column.columnName, "entity", value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-background h-8 w-full text-xs">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{referenceTableOptions.map((option, index) => (
|
||||
<SelectItem
|
||||
key={`entity-${option.value}-${index}`}
|
||||
value={option.value}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{option.label}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{option.value}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 조인 컬럼 */}
|
||||
{column.referenceTable && column.referenceTable !== "none" && (
|
||||
<div className="w-48">
|
||||
<label className="text-muted-foreground mb-1 block text-xs">
|
||||
참조 테이블
|
||||
조인 컬럼
|
||||
</label>
|
||||
<Select
|
||||
value={column.referenceTable || "none"}
|
||||
value={column.referenceColumn || "none"}
|
||||
onValueChange={(value) =>
|
||||
handleDetailSettingsChange(column.columnName, "entity", value)
|
||||
handleDetailSettingsChange(
|
||||
column.columnName,
|
||||
"entity_reference_column",
|
||||
value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-background h-8 w-full text-xs">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{referenceTableOptions.map((option, index) => (
|
||||
<SelectItem value="none">-- 선택 안함 --</SelectItem>
|
||||
{referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
|
||||
<SelectItem
|
||||
key={`entity-${option.value}-${index}`}
|
||||
value={option.value}
|
||||
key={`ref-col-${refCol.columnName}-${index}`}
|
||||
value={refCol.columnName}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{option.label}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{option.value}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium">{refCol.columnName}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
{(!referenceTableColumns[column.referenceTable] ||
|
||||
referenceTableColumns[column.referenceTable].length === 0) && (
|
||||
<SelectItem value="loading" disabled>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="border-primary h-3 w-3 animate-spin rounded-full border border-t-transparent"></div>
|
||||
로딩중
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 조인 컬럼 */}
|
||||
{column.referenceTable && column.referenceTable !== "none" && (
|
||||
{/* 표시 컬럼 */}
|
||||
{column.referenceTable &&
|
||||
column.referenceTable !== "none" &&
|
||||
column.referenceColumn &&
|
||||
column.referenceColumn !== "none" && (
|
||||
<div className="w-48">
|
||||
<label className="text-muted-foreground mb-1 block text-xs">
|
||||
조인 컬럼
|
||||
표시 컬럼
|
||||
</label>
|
||||
<Select
|
||||
value={column.referenceColumn || "none"}
|
||||
value={column.displayColumn || "none"}
|
||||
onValueChange={(value) =>
|
||||
handleDetailSettingsChange(
|
||||
column.columnName,
|
||||
"entity_reference_column",
|
||||
"entity_display_column",
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
|
@ -1347,79 +1401,32 @@ export default function TableManagementPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 표시 컬럼 */}
|
||||
{column.referenceTable &&
|
||||
column.referenceTable !== "none" &&
|
||||
column.referenceColumn &&
|
||||
column.referenceColumn !== "none" && (
|
||||
<div className="w-48">
|
||||
<label className="text-muted-foreground mb-1 block text-xs">
|
||||
표시 컬럼
|
||||
</label>
|
||||
<Select
|
||||
value={column.displayColumn || "none"}
|
||||
onValueChange={(value) =>
|
||||
handleDetailSettingsChange(
|
||||
column.columnName,
|
||||
"entity_display_column",
|
||||
value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-background h-8 w-full text-xs">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">-- 선택 안함 --</SelectItem>
|
||||
{referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
|
||||
<SelectItem
|
||||
key={`ref-col-${refCol.columnName}-${index}`}
|
||||
value={refCol.columnName}
|
||||
>
|
||||
<span className="font-medium">{refCol.columnName}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
{(!referenceTableColumns[column.referenceTable] ||
|
||||
referenceTableColumns[column.referenceTable].length === 0) && (
|
||||
<SelectItem value="loading" disabled>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="border-primary h-3 w-3 animate-spin rounded-full border border-t-transparent"></div>
|
||||
로딩중
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 설정 완료 표시 */}
|
||||
{column.referenceTable &&
|
||||
column.referenceTable !== "none" &&
|
||||
column.referenceColumn &&
|
||||
column.referenceColumn !== "none" &&
|
||||
column.displayColumn &&
|
||||
column.displayColumn !== "none" && (
|
||||
<div className="bg-primary/10 text-primary flex items-center gap-1 rounded px-2 py-1 text-xs w-48">
|
||||
<span>✓</span>
|
||||
<span className="truncate">설정 완료</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-4">
|
||||
<Input
|
||||
value={column.description || ""}
|
||||
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
|
||||
placeholder="설명"
|
||||
className="h-8 w-full text-xs"
|
||||
/>
|
||||
{/* 설정 완료 표시 */}
|
||||
{column.referenceTable &&
|
||||
column.referenceTable !== "none" &&
|
||||
column.referenceColumn &&
|
||||
column.referenceColumn !== "none" &&
|
||||
column.displayColumn &&
|
||||
column.displayColumn !== "none" && (
|
||||
<div className="bg-primary/10 text-primary flex items-center gap-1 rounded px-2 py-1 text-xs w-48">
|
||||
<span>✓</span>
|
||||
<span className="truncate">설정 완료</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pl-4">
|
||||
<Input
|
||||
value={column.description || ""}
|
||||
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
|
||||
placeholder="설명"
|
||||
className="h-8 w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 로딩 표시 */}
|
||||
{columnsLoading && (
|
||||
|
|
@ -1428,28 +1435,16 @@ export default function TableManagementPage() {
|
|||
<span className="text-muted-foreground ml-2 text-sm">더 많은 컬럼 로딩 중...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 페이지 정보 */}
|
||||
<div className="text-muted-foreground text-center text-sm">
|
||||
{columns.length} / {totalColumns} 컬럼 표시됨
|
||||
</div>
|
||||
|
||||
{/* 전체 저장 버튼 */}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button
|
||||
onClick={saveAllSettings}
|
||||
disabled={!selectedTable || columns.length === 0}
|
||||
className="h-10 gap-2 text-sm font-medium"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
전체 설정 저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지 정보 (고정 하단) */}
|
||||
<div className="text-muted-foreground flex-shrink-0 border-t py-2 text-center text-sm">
|
||||
{columns.length} / {totalColumns} 컬럼 표시됨
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ export const CategoryValueEditDialog: React.FC<
|
|||
|
||||
onUpdate(value.valueId!, {
|
||||
valueLabel: valueLabel.trim(),
|
||||
description: description.trim(),
|
||||
color: color,
|
||||
description: description.trim() || undefined, // 빈 문자열 대신 undefined
|
||||
color: color === "none" ? null : color, // "none"은 null로 (배지 없음)
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import React, { useEffect, useState, useMemo, useCallback } from "react";
|
|||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { CardDisplayConfig } from "./types";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { getFullImageUrl } from "@/lib/api/client";
|
||||
import { getFullImageUrl, apiClient } from "@/lib/api/client";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
||||
import { useModalDataStore } from "@/stores/modalDataStore";
|
||||
|
|
@ -52,6 +53,14 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 카테고리 매핑 상태 (카테고리 코드 -> 라벨/색상)
|
||||
const [columnMeta, setColumnMeta] = useState<
|
||||
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
|
||||
>({});
|
||||
const [categoryMappings, setCategoryMappings] = useState<
|
||||
Record<string, Record<string, { label: string; color?: string }>>
|
||||
>({});
|
||||
|
||||
// 선택된 카드 상태 (Set으로 변경하여 테이블 리스트와 동일하게)
|
||||
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
||||
|
||||
|
|
@ -120,44 +129,78 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
const tableNameToUse = tableName || component.componentConfig?.tableName || 'user_info'; // 기본 테이블명 설정
|
||||
|
||||
if (!tableNameToUse) {
|
||||
// console.log("📋 CardDisplay: 테이블명이 설정되지 않음", {
|
||||
// tableName,
|
||||
// componentTableName: component.componentConfig?.tableName,
|
||||
// });
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("📋 CardDisplay: 사용할 테이블명", {
|
||||
// tableName,
|
||||
// componentTableName: component.componentConfig?.tableName,
|
||||
// finalTableName: tableNameToUse,
|
||||
// });
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
// console.log(`📋 CardDisplay: ${tableNameToUse} 테이블 데이터 로딩 시작`);
|
||||
|
||||
// 테이블 데이터와 컬럼 정보를 병렬로 로드
|
||||
const [dataResponse, columnsResponse] = await Promise.all([
|
||||
// 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드
|
||||
const [dataResponse, columnsResponse, inputTypesResponse] = await Promise.all([
|
||||
tableTypeApi.getTableData(tableNameToUse, {
|
||||
page: 1,
|
||||
size: 50, // 카드 표시용으로 적당한 개수
|
||||
}),
|
||||
tableTypeApi.getColumns(tableNameToUse),
|
||||
tableTypeApi.getColumnInputTypes(tableNameToUse),
|
||||
]);
|
||||
|
||||
// console.log(`📋 CardDisplay: ${tableNameToUse} 데이터 로딩 완료`, {
|
||||
// total: dataResponse.total,
|
||||
// dataLength: dataResponse.data.length,
|
||||
// columnsLength: columnsResponse.length,
|
||||
// sampleData: dataResponse.data.slice(0, 2),
|
||||
// sampleColumns: columnsResponse.slice(0, 3),
|
||||
// });
|
||||
|
||||
setLoadedTableData(dataResponse.data);
|
||||
setLoadedTableColumns(columnsResponse);
|
||||
|
||||
// 컬럼 메타 정보 설정 (inputType 포함)
|
||||
const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string }> = {};
|
||||
inputTypesResponse.forEach((item: any) => {
|
||||
meta[item.columnName || item.column_name] = {
|
||||
webType: item.webType || item.web_type,
|
||||
inputType: item.inputType || item.input_type,
|
||||
codeCategory: item.codeCategory || item.code_category,
|
||||
};
|
||||
});
|
||||
console.log("📋 [CardDisplay] 컬럼 메타 정보:", meta);
|
||||
setColumnMeta(meta);
|
||||
|
||||
// 카테고리 타입 컬럼 찾기 및 매핑 로드
|
||||
const categoryColumns = Object.entries(meta)
|
||||
.filter(([_, m]) => m.inputType === "category")
|
||||
.map(([columnName]) => columnName);
|
||||
|
||||
console.log("📋 [CardDisplay] 카테고리 컬럼:", categoryColumns);
|
||||
|
||||
if (categoryColumns.length > 0) {
|
||||
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
|
||||
|
||||
for (const columnName of categoryColumns) {
|
||||
try {
|
||||
console.log(`📋 [CardDisplay] 카테고리 매핑 로드 시작: ${tableNameToUse}/${columnName}`);
|
||||
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values`);
|
||||
|
||||
console.log(`📋 [CardDisplay] 카테고리 API 응답 [${columnName}]:`, response.data);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const mapping: Record<string, { label: string; color?: string }> = {};
|
||||
response.data.data.forEach((item: any) => {
|
||||
// API 응답 형식: valueCode, valueLabel (camelCase)
|
||||
const code = item.valueCode || item.value_code || item.category_code || item.code || item.value;
|
||||
const label = item.valueLabel || item.value_label || item.category_name || item.name || item.label || code;
|
||||
// color가 null/undefined/"none"이면 undefined로 유지 (배지 없음)
|
||||
const rawColor = item.color ?? item.badge_color;
|
||||
const color = (rawColor && rawColor !== "none") ? rawColor : undefined;
|
||||
mapping[code] = { label, color };
|
||||
console.log(`📋 [CardDisplay] 매핑 추가: ${code} -> ${label} (color: ${color})`);
|
||||
});
|
||||
mappings[columnName] = mapping;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ CardDisplay: 카테고리 매핑 로드 실패 [${columnName}]`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("📋 [CardDisplay] 최종 카테고리 매핑:", mappings);
|
||||
setCategoryMappings(mappings);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ CardDisplay: ${tableNameToUse} 데이터 로딩 실패`, error);
|
||||
console.error(`❌ CardDisplay: 데이터 로딩 실패`, error);
|
||||
setLoadedTableData([]);
|
||||
setLoadedTableColumns([]);
|
||||
} finally {
|
||||
|
|
@ -392,10 +435,58 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
return text.substring(0, maxLength) + "...";
|
||||
};
|
||||
|
||||
// 컬럼 매핑에서 값 가져오기
|
||||
const getColumnValue = (data: any, columnName?: string) => {
|
||||
// 컬럼 값을 문자열로 가져오기 (카테고리 타입인 경우 매핑된 라벨 반환)
|
||||
const getColumnValueAsString = (data: any, columnName?: string): string => {
|
||||
if (!columnName) return "";
|
||||
return data[columnName] || "";
|
||||
const value = data[columnName];
|
||||
if (value === null || value === undefined || value === "") return "";
|
||||
|
||||
// 카테고리 타입인 경우 매핑된 라벨 반환
|
||||
const meta = columnMeta[columnName];
|
||||
if (meta?.inputType === "category") {
|
||||
const mapping = categoryMappings[columnName];
|
||||
const valueStr = String(value);
|
||||
const categoryData = mapping?.[valueStr];
|
||||
return categoryData?.label || valueStr;
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// 컬럼 매핑에서 값 가져오기 (카테고리 타입인 경우 배지로 표시)
|
||||
const getColumnValue = (data: any, columnName?: string): React.ReactNode => {
|
||||
if (!columnName) return "";
|
||||
const value = data[columnName];
|
||||
if (value === null || value === undefined || value === "") return "";
|
||||
|
||||
// 카테고리 타입인 경우 매핑된 라벨과 배지로 표시
|
||||
const meta = columnMeta[columnName];
|
||||
if (meta?.inputType === "category") {
|
||||
const mapping = categoryMappings[columnName];
|
||||
const valueStr = String(value);
|
||||
const categoryData = mapping?.[valueStr];
|
||||
const displayLabel = categoryData?.label || valueStr;
|
||||
const displayColor = categoryData?.color;
|
||||
|
||||
// 색상이 없거나(null/undefined), 빈 문자열이거나, "none"이면 일반 텍스트로 표시 (배지 없음)
|
||||
if (!displayColor || displayColor === "none") {
|
||||
return displayLabel;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: displayColor,
|
||||
borderColor: displayColor,
|
||||
}}
|
||||
className="text-white text-xs"
|
||||
>
|
||||
{displayLabel}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// 컬럼명을 라벨로 변환하는 헬퍼 함수
|
||||
|
|
@ -516,16 +607,16 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
</div>
|
||||
) : (
|
||||
displayData.map((data, index) => {
|
||||
// 타이틀, 서브타이틀, 설명 값 결정 (원래 카드 레이아웃과 동일한 로직)
|
||||
// 타이틀, 서브타이틀, 설명 값 결정 (문자열로 가져와서 표시)
|
||||
const titleValue =
|
||||
getColumnValue(data, componentConfig.columnMapping?.titleColumn) || getAutoFallbackValue(data, "title");
|
||||
getColumnValueAsString(data, componentConfig.columnMapping?.titleColumn) || getAutoFallbackValue(data, "title");
|
||||
|
||||
const subtitleValue =
|
||||
getColumnValue(data, componentConfig.columnMapping?.subtitleColumn) ||
|
||||
getColumnValueAsString(data, componentConfig.columnMapping?.subtitleColumn) ||
|
||||
getAutoFallbackValue(data, "subtitle");
|
||||
|
||||
const descriptionValue =
|
||||
getColumnValue(data, componentConfig.columnMapping?.descriptionColumn) ||
|
||||
getColumnValueAsString(data, componentConfig.columnMapping?.descriptionColumn) ||
|
||||
getAutoFallbackValue(data, "description");
|
||||
|
||||
// 이미지 컬럼 자동 감지 (image_path, photo 등) - 대소문자 무시
|
||||
|
|
@ -628,27 +719,33 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 카드 액션 */}
|
||||
<div className="mt-2 flex justify-end space-x-2">
|
||||
<button
|
||||
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCardView(data);
|
||||
}}
|
||||
>
|
||||
상세보기
|
||||
</button>
|
||||
<button
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCardEdit(data);
|
||||
}}
|
||||
>
|
||||
편집
|
||||
</button>
|
||||
</div>
|
||||
{/* 카드 액션 - 설정에 따라 표시 */}
|
||||
{(componentConfig.cardStyle?.showActions ?? true) && (
|
||||
<div className="mt-2 flex justify-end space-x-2">
|
||||
{(componentConfig.cardStyle?.showViewButton ?? true) && (
|
||||
<button
|
||||
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCardView(data);
|
||||
}}
|
||||
>
|
||||
상세보기
|
||||
</button>
|
||||
)}
|
||||
{(componentConfig.cardStyle?.showEditButton ?? true) && (
|
||||
<button
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCardEdit(data);
|
||||
}}
|
||||
>
|
||||
편집
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -672,16 +769,48 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.entries(selectedData)
|
||||
.filter(([key, value]) => value !== null && value !== undefined && value !== '')
|
||||
.map(([key, value]) => (
|
||||
<div key={key} className="bg-muted rounded-lg p-3">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">
|
||||
{key.replace(/_/g, ' ')}
|
||||
.map(([key, value]) => {
|
||||
// 카테고리 타입인 경우 배지로 표시
|
||||
const meta = columnMeta[key];
|
||||
let displayValue: React.ReactNode = String(value);
|
||||
|
||||
if (meta?.inputType === "category") {
|
||||
const mapping = categoryMappings[key];
|
||||
const valueStr = String(value);
|
||||
const categoryData = mapping?.[valueStr];
|
||||
const displayLabel = categoryData?.label || valueStr;
|
||||
const displayColor = categoryData?.color;
|
||||
|
||||
// 색상이 있고 "none"이 아닌 경우에만 배지로 표시
|
||||
if (displayColor && displayColor !== "none") {
|
||||
displayValue = (
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: displayColor,
|
||||
borderColor: displayColor,
|
||||
}}
|
||||
className="text-white"
|
||||
>
|
||||
{displayLabel}
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
// 배지 없음: 일반 텍스트로 표시
|
||||
displayValue = displayLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} className="bg-muted rounded-lg p-3">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">
|
||||
{getColumnLabel(key)}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground break-words">
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground break-words">
|
||||
{String(value)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -277,6 +277,37 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
|||
액션 버튼 표시
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 개별 버튼 설정 (액션 버튼이 활성화된 경우에만 표시) */}
|
||||
{(config.cardStyle?.showActions ?? true) && (
|
||||
<div className="ml-4 space-y-2 border-l-2 border-gray-200 pl-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="showViewButton"
|
||||
checked={config.cardStyle?.showViewButton ?? true}
|
||||
onChange={(e) => handleNestedChange("cardStyle.showViewButton", e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<label htmlFor="showViewButton" className="text-xs text-gray-600">
|
||||
상세보기 버튼
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="showEditButton"
|
||||
checked={config.cardStyle?.showEditButton ?? true}
|
||||
onChange={(e) => handleNestedChange("cardStyle.showEditButton", e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<label htmlFor="showEditButton" className="text-xs text-gray-600">
|
||||
편집 버튼
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export interface CardStyleConfig {
|
|||
maxDescriptionLength?: number;
|
||||
imagePosition?: "top" | "left" | "right";
|
||||
imageSize?: "small" | "medium" | "large";
|
||||
showActions?: boolean; // 액션 버튼 표시 여부
|
||||
showActions?: boolean; // 액션 버튼 표시 여부 (전체)
|
||||
showViewButton?: boolean; // 상세보기 버튼 표시 여부
|
||||
showEditButton?: boolean; // 편집 버튼 표시 여부
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue