refactor(admin): 테이블 타입 관리 Entity 조인 UI 레이아웃 개선

- Flexbox에서 Grid 레이아웃으로 변경 (160px 200px 250px 1fr)
- "상세 설정" 컬럼 제거하고 4개 컬럼 구조로 단순화
- Entity 조인 설정(참조/조인/표시 컬럼)을 입력 타입 컬럼 내 세로 배치
- Select 박스 너비를 192px (w-48)로 통일
- UI 겹침 현상 해결 및 순차적 설정 흐름 개선
This commit is contained in:
SeongHyun Kim 2025-11-27 12:22:39 +09:00
parent a1117092aa
commit 244c597ac9
2 changed files with 119 additions and 129 deletions

View File

@ -1108,14 +1108,11 @@ export default function TableManagementPage() {
) : (
<div className="space-y-4">
{/* 컬럼 헤더 */}
<div className="text-foreground flex h-12 items-center border-b px-6 py-3 text-sm font-semibold">
<div className="w-40 pr-4"></div>
<div className="w-48 px-4"></div>
<div className="w-48 pr-6"> </div>
<div className="flex-1 pl-6" style={{ maxWidth: "calc(100% - 808px)" }}>
</div>
<div className="w-80 pl-4"></div>
<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>
{/* 컬럼 리스트 */}
@ -1132,12 +1129,13 @@ export default function TableManagementPage() {
{columns.map((column, index) => (
<div
key={column.columnName}
className="bg-background hover:bg-muted/50 flex min-h-16 items-center border-b px-6 py-3 transition-colors"
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="w-40 pr-4">
<div className="pr-4 pt-1">
<div className="font-mono text-sm">{column.columnName}</div>
</div>
<div className="w-48 px-4">
<div className="px-4">
<Input
value={column.displayName || ""}
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
@ -1145,107 +1143,106 @@ export default function TableManagementPage() {
className="h-8 text-xs"
/>
</div>
<div className="w-48 pr-6">
<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>
</div>
<div className="flex-1 pl-6" style={{ maxWidth: "calc(100% - 808px)" }}>
{/* 입력 타입이 'code'인 경우 공통코드 선택 */}
{column.inputType === "code" && (
<div className="pr-6">
<div className="space-y-3">
{/* 입력 타입 선택 */}
<Select
value={column.codeCategory || "none"}
onValueChange={(value) =>
handleDetailSettingsChange(column.columnName, "code", value)
}
value={column.inputType || "text"}
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="공통코드 선택" />
<SelectValue placeholder="입력 타입 선택" />
</SelectTrigger>
<SelectContent>
{commonCodeOptions.map((option, index) => (
<SelectItem key={`code-${option.value}-${index}`} value={option.value}>
{memoizedInputTypeOptions.map((option) => (
<SelectItem key={option.value} 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);
{/* 입력 타입이 '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>
);
})
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}
</p>
)}
</div>
{column.categoryMenus && column.categoryMenus.length > 0 && (
<p className="text-primary text-xs">
{column.categoryMenus.length}
</p>
)}
</div>
)}
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
{column.inputType === "entity" && (
<div className="space-y-2">
<div className="grid grid-cols-3 gap-2">
)}
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
{column.inputType === "entity" && (
<>
{/* 참조 테이블 */}
<div>
<div className="w-48">
<label className="text-muted-foreground mb-1 block text-xs">
</label>
@ -1255,7 +1252,7 @@ export default function TableManagementPage() {
handleDetailSettingsChange(column.columnName, "entity", value)
}
>
<SelectTrigger className="bg-background h-8 text-xs">
<SelectTrigger className="bg-background h-8 w-full text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
@ -1278,7 +1275,7 @@ export default function TableManagementPage() {
{/* 조인 컬럼 */}
{column.referenceTable && column.referenceTable !== "none" && (
<div>
<div className="w-48">
<label className="text-muted-foreground mb-1 block text-xs">
</label>
@ -1292,7 +1289,7 @@ export default function TableManagementPage() {
)
}
>
<SelectTrigger className="bg-background h-8 text-xs">
<SelectTrigger className="bg-background h-8 w-full text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
@ -1324,7 +1321,7 @@ export default function TableManagementPage() {
column.referenceTable !== "none" &&
column.referenceColumn &&
column.referenceColumn !== "none" && (
<div>
<div className="w-48">
<label className="text-muted-foreground mb-1 block text-xs">
</label>
@ -1338,7 +1335,7 @@ export default function TableManagementPage() {
)
}
>
<SelectTrigger className="bg-background h-8 text-xs">
<SelectTrigger className="bg-background h-8 w-full text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
@ -1364,37 +1361,29 @@ export default function TableManagementPage() {
</Select>
</div>
)}
</div>
{/* 설정 완료 표시 */}
{column.referenceTable &&
column.referenceTable !== "none" &&
column.referenceColumn &&
column.referenceColumn !== "none" &&
column.displayColumn &&
column.displayColumn !== "none" && (
<div className="bg-primary/10 text-primary mt-2 flex items-center gap-1 rounded px-2 py-1 text-xs">
<span></span>
<span className="truncate">
{column.columnName} {column.referenceTable}.{column.displayColumn}
</span>
</div>
)}
</div>
)}
{/* 다른 입력 타입인 경우 빈 공간 */}
{column.inputType !== "code" && column.inputType !== "category" && column.inputType !== "entity" && (
<div className="text-muted-foreground flex h-8 items-center justify-center text-xs">
-
</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="w-80 pl-4">
<div className="pl-4">
<Input
value={column.description || ""}
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
placeholder="설명"
className="h-8 text-xs"
className="h-8 w-full text-xs"
/>
</div>
</div>
@ -1585,3 +1574,4 @@ export default function TableManagementPage() {
</div>
);
}

View File

@ -734,7 +734,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
? "전화번호 형식: 010-1234-5678"
: isManualMode
? `${component.label} (수동 입력 모드 - 채번 규칙 미적용)`
: component.label
: component.label
? `${component.label}${component.columnName ? ` (${component.columnName})` : ""}`
: component.columnName || undefined
}