refactor(UniversalFormModal): 다중 컬럼 저장 기능을 필드 레벨로 이동
- 섹션 레벨 linkedFieldGroups 제거, 필드 레벨 linkedFieldGroup으로 변경 - FormFieldConfig에 linkedFieldGroup 속성 추가 (enabled, sourceTable, displayColumn, displayFormat, mappings) - select 필드 렌더링에서 linkedFieldGroup 활성화 시 다중 컬럼 저장 처리 - API 응답 파싱 개선 (responseData.data 구조 지원) - 저장 실패 시 상세 에러 메시지 표시 - ConfigPanel에 다중 컬럼 저장 설정 UI 및 HelpText 추가
This commit is contained in:
@@ -37,7 +37,6 @@ import {
|
||||
UniversalFormModalConfigPanelProps,
|
||||
FormSectionConfig,
|
||||
FormFieldConfig,
|
||||
LinkedFieldGroup,
|
||||
LinkedFieldMapping,
|
||||
FIELD_TYPE_OPTIONS,
|
||||
MODAL_SIZE_OPTIONS,
|
||||
@@ -49,11 +48,8 @@ import {
|
||||
defaultSectionConfig,
|
||||
defaultNumberingRuleConfig,
|
||||
defaultSelectOptionsConfig,
|
||||
defaultLinkedFieldGroupConfig,
|
||||
defaultLinkedFieldMappingConfig,
|
||||
generateSectionId,
|
||||
generateFieldId,
|
||||
generateLinkedFieldGroupId,
|
||||
} from "./config";
|
||||
|
||||
// 도움말 텍스트 컴포넌트
|
||||
@@ -93,13 +89,14 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config.saveConfig.tableName]);
|
||||
|
||||
// 연동 필드 그룹의 소스 테이블 컬럼 로드
|
||||
// 다중 컬럼 저장의 소스 테이블 컬럼 로드
|
||||
useEffect(() => {
|
||||
const allSourceTables = new Set<string>();
|
||||
config.sections.forEach((section) => {
|
||||
(section.linkedFieldGroups || []).forEach((group) => {
|
||||
if (group.sourceTable) {
|
||||
allSourceTables.add(group.sourceTable);
|
||||
// 필드 레벨의 linkedFieldGroup 확인
|
||||
section.fields.forEach((field) => {
|
||||
if (field.linkedFieldGroup?.sourceTable) {
|
||||
allSourceTables.add(field.linkedFieldGroup.sourceTable);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -578,47 +575,6 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
</Select>
|
||||
<HelpText>겸직 등 반복 데이터가 있는 섹션</HelpText>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<Label className="text-[10px]">구분 컬럼</Label>
|
||||
<Input
|
||||
value={config.saveConfig.multiRowSave?.typeColumn || "employment_type"}
|
||||
onChange={(e) =>
|
||||
updateSaveConfig({
|
||||
multiRowSave: { ...config.saveConfig.multiRowSave, typeColumn: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder="employment_type"
|
||||
className="h-6 text-[10px] mt-1"
|
||||
/>
|
||||
<HelpText>메인/서브를 구분하는 컬럼명</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">메인 값</Label>
|
||||
<Input
|
||||
value={config.saveConfig.multiRowSave?.mainTypeValue || "main"}
|
||||
onChange={(e) =>
|
||||
updateSaveConfig({
|
||||
multiRowSave: { ...config.saveConfig.multiRowSave, mainTypeValue: e.target.value },
|
||||
})
|
||||
}
|
||||
className="h-6 text-[10px] mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">서브 값</Label>
|
||||
<Input
|
||||
value={config.saveConfig.multiRowSave?.subTypeValue || "concurrent"}
|
||||
onChange={(e) =>
|
||||
updateSaveConfig({
|
||||
multiRowSave: { ...config.saveConfig.multiRowSave, subTypeValue: e.target.value },
|
||||
})
|
||||
}
|
||||
className="h-6 text-[10px] mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -683,7 +639,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
<Card
|
||||
key={section.id}
|
||||
className={cn(
|
||||
"cursor-pointer transition-colors",
|
||||
"cursor-pointer transition-colors !p-0",
|
||||
selectedSectionId === section.id && "ring-2 ring-primary",
|
||||
)}
|
||||
onClick={() => {
|
||||
@@ -866,305 +822,6 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 연동 필드 그룹 설정 */}
|
||||
<div className="border rounded-md p-2 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-medium">연동 필드 그룹</span>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newGroup: LinkedFieldGroup = {
|
||||
...defaultLinkedFieldGroupConfig,
|
||||
id: generateLinkedFieldGroupId(),
|
||||
};
|
||||
updateSection(selectedSection.id, {
|
||||
linkedFieldGroups: [...(selectedSection.linkedFieldGroups || []), newGroup],
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[9px] text-muted-foreground">
|
||||
부서코드/부서명 연동 저장
|
||||
</p>
|
||||
|
||||
{(selectedSection.linkedFieldGroups || []).length > 0 && (
|
||||
<div className="space-y-1.5 pt-1.5 border-t">
|
||||
{(selectedSection.linkedFieldGroups || []).map((group, groupIndex) => (
|
||||
<div key={group.id} className="border rounded p-1.5 space-y-1 bg-muted/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] font-medium text-muted-foreground">
|
||||
#{groupIndex + 1}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 text-destructive"
|
||||
onClick={() => {
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).filter(
|
||||
(g) => g.id !== group.id
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 라벨 */}
|
||||
<div>
|
||||
<Label className="text-[9px]">라벨</Label>
|
||||
<Input
|
||||
value={group.label}
|
||||
onChange={(e) => {
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, label: e.target.value } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
placeholder="예: 겸직부서"
|
||||
className="h-5 text-[9px] mt-0.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 소스 테이블 */}
|
||||
<div>
|
||||
<Label className="text-[9px]">소스 테이블</Label>
|
||||
<Select
|
||||
value={group.sourceTable}
|
||||
onValueChange={(value) => {
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, sourceTable: value } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
if (value && !tableColumns[value]) {
|
||||
loadTableColumns(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] mt-0.5">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tables.map((table) => (
|
||||
<SelectItem key={table.name} value={table.name} className="text-xs">
|
||||
{table.label || table.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 표시 형식 */}
|
||||
<div>
|
||||
<Label className="text-[9px]">표시 형식</Label>
|
||||
<Select
|
||||
value={group.displayFormat}
|
||||
onValueChange={(value: "name_only" | "code_name" | "name_code") => {
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, displayFormat: value } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] mt-0.5">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LINKED_FIELD_DISPLAY_FORMAT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 표시 컬럼 / 값 컬럼 */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<div className="min-w-0">
|
||||
<Label className="text-[9px]">표시</Label>
|
||||
<Select
|
||||
value={group.displayColumn}
|
||||
onValueChange={(value) => {
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, displayColumn: value } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] mt-0.5">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[group.sourceTable] || []).map((col) => (
|
||||
<SelectItem key={col.name} value={col.name} className="text-xs">
|
||||
{col.label || col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<Label className="text-[9px]">값</Label>
|
||||
<Select
|
||||
value={group.valueColumn}
|
||||
onValueChange={(value) => {
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, valueColumn: value } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] mt-0.5">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[group.sourceTable] || []).map((col) => (
|
||||
<SelectItem key={col.name} value={col.name} className="text-xs">
|
||||
{col.label || col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 필드 매핑 */}
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[9px]">필드 매핑</Label>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newMapping: LinkedFieldMapping = { ...defaultLinkedFieldMappingConfig };
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id
|
||||
? { ...g, mappings: [...(g.mappings || []), newMapping] }
|
||||
: g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-4 w-4"
|
||||
>
|
||||
<Plus className="h-2.5 w-2.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(group.mappings || []).map((mapping, mappingIndex) => (
|
||||
<div key={mappingIndex} className="flex items-center gap-0.5 bg-background p-0.5 rounded">
|
||||
<Select
|
||||
value={mapping.sourceColumn}
|
||||
onValueChange={(value) => {
|
||||
const updatedMappings = (group.mappings || []).map((m, i) =>
|
||||
i === mappingIndex ? { ...m, sourceColumn: value } : m
|
||||
);
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, mappings: updatedMappings } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[8px] flex-1 min-w-0">
|
||||
<SelectValue placeholder="소스" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[group.sourceTable] || []).map((col) => (
|
||||
<SelectItem key={col.name} value={col.name} className="text-xs">
|
||||
{col.label || col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-[8px] text-muted-foreground">-></span>
|
||||
<Select
|
||||
value={mapping.targetColumn}
|
||||
onValueChange={(value) => {
|
||||
const updatedMappings = (group.mappings || []).map((m, i) =>
|
||||
i === mappingIndex ? { ...m, targetColumn: value } : m
|
||||
);
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, mappings: updatedMappings } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[8px] flex-1 min-w-0">
|
||||
<SelectValue placeholder="대상" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[config.saveConfig.tableName] || []).map((col) => (
|
||||
<SelectItem key={col.name} value={col.name} className="text-xs">
|
||||
{col.label || col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 text-destructive shrink-0"
|
||||
onClick={() => {
|
||||
const updatedMappings = (group.mappings || []).filter(
|
||||
(_, i) => i !== mappingIndex
|
||||
);
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, mappings: updatedMappings } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-2.5 w-2.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 기타 옵션 */}
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Checkbox
|
||||
id={`required-${group.id}`}
|
||||
checked={group.required || false}
|
||||
onCheckedChange={(checked) => {
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, required: !!checked } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<Label htmlFor={`required-${group.id}`} className="text-[8px]">
|
||||
필수
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Label className="text-[8px]">스팬</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={12}
|
||||
value={group.gridSpan || 6}
|
||||
onChange={(e) => {
|
||||
const updatedGroups = (selectedSection.linkedFieldGroups || []).map((g) =>
|
||||
g.id === group.id ? { ...g, gridSpan: parseInt(e.target.value) || 6 } : g
|
||||
);
|
||||
updateSection(selectedSection.id, { linkedFieldGroups: updatedGroups });
|
||||
}}
|
||||
className="h-4 w-8 text-[8px] px-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 필드 목록 */}
|
||||
@@ -1467,7 +1124,8 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
{/* Select 옵션 설정 */}
|
||||
{selectedField.fieldType === "select" && (
|
||||
<div className="border rounded-md p-2 space-y-2">
|
||||
<Label className="text-[10px] font-medium">선택 옵션 설정</Label>
|
||||
<Label className="text-[10px] font-medium">드롭다운 옵션 설정</Label>
|
||||
<HelpText>드롭다운에 표시될 옵션 목록을 어디서 가져올지 설정합니다.</HelpText>
|
||||
<Select
|
||||
value={selectedField.selectOptions?.type || "static"}
|
||||
onValueChange={(value) =>
|
||||
@@ -1491,10 +1149,15 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedField.selectOptions?.type === "static" && (
|
||||
<HelpText>직접 입력: 옵션을 수동으로 입력합니다. (현재 미구현 - 테이블 참조 사용 권장)</HelpText>
|
||||
)}
|
||||
|
||||
{selectedField.selectOptions?.type === "table" && (
|
||||
<div className="space-y-2 pt-2 border-t">
|
||||
<HelpText>테이블 참조: DB 테이블에서 옵션 목록을 가져옵니다.</HelpText>
|
||||
<div>
|
||||
<Label className="text-[10px]">참조 테이블</Label>
|
||||
<Label className="text-[10px]">참조 테이블 (옵션을 가져올 테이블)</Label>
|
||||
<Select
|
||||
value={selectedField.selectOptions?.tableName || ""}
|
||||
onValueChange={(value) =>
|
||||
@@ -1517,9 +1180,10 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<HelpText>예: dept_info (부서 테이블)</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">값 컬럼</Label>
|
||||
<Label className="text-[10px]">값 컬럼 (저장될 값)</Label>
|
||||
<Input
|
||||
value={selectedField.selectOptions?.valueColumn || ""}
|
||||
onChange={(e) =>
|
||||
@@ -1530,12 +1194,13 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="code"
|
||||
placeholder="dept_code"
|
||||
className="h-6 text-[10px] mt-1"
|
||||
/>
|
||||
<HelpText>선택 시 실제 저장되는 값 (예: D001)</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">라벨 컬럼</Label>
|
||||
<Label className="text-[10px]">라벨 컬럼 (화면에 표시될 텍스트)</Label>
|
||||
<Input
|
||||
value={selectedField.selectOptions?.labelColumn || ""}
|
||||
onChange={(e) =>
|
||||
@@ -1546,15 +1211,17 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="name"
|
||||
placeholder="dept_name"
|
||||
className="h-6 text-[10px] mt-1"
|
||||
/>
|
||||
<HelpText>드롭다운에 보여질 텍스트 (예: 영업부)</HelpText>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedField.selectOptions?.type === "code" && (
|
||||
<div className="pt-2 border-t">
|
||||
<HelpText>공통코드: 공통코드 테이블에서 옵션을 가져옵니다.</HelpText>
|
||||
<Label className="text-[10px]">공통코드 카테고리</Label>
|
||||
<Input
|
||||
value={selectedField.selectOptions?.codeCategory || ""}
|
||||
@@ -1569,6 +1236,235 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
||||
placeholder="POSITION_CODE"
|
||||
className="h-6 text-[10px] mt-1"
|
||||
/>
|
||||
<HelpText>예: POSITION_CODE (직급), STATUS_CODE (상태) 등</HelpText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 다중 컬럼 저장 (select 타입만) */}
|
||||
{selectedField.fieldType === "select" && (
|
||||
<div className="border rounded-md p-2 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-medium">다중 컬럼 저장</span>
|
||||
<Switch
|
||||
checked={selectedField.linkedFieldGroup?.enabled || false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateField(selectedSection.id, selectedField.id, {
|
||||
linkedFieldGroup: {
|
||||
...selectedField.linkedFieldGroup,
|
||||
enabled: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<HelpText>
|
||||
드롭다운 선택 시 여러 컬럼에 동시 저장합니다.
|
||||
<br />예: 부서 선택 시 부서코드 + 부서명을 각각 다른 컬럼에 저장
|
||||
</HelpText>
|
||||
|
||||
{selectedField.linkedFieldGroup?.enabled && (
|
||||
<div className="space-y-2 pt-2 border-t">
|
||||
{/* 소스 테이블 */}
|
||||
<div>
|
||||
<Label className="text-[10px]">데이터 소스 테이블</Label>
|
||||
<Select
|
||||
value={selectedField.linkedFieldGroup?.sourceTable || ""}
|
||||
onValueChange={(value) => {
|
||||
updateField(selectedSection.id, selectedField.id, {
|
||||
linkedFieldGroup: {
|
||||
...selectedField.linkedFieldGroup,
|
||||
sourceTable: value,
|
||||
},
|
||||
});
|
||||
if (value && !tableColumns[value]) {
|
||||
loadTableColumns(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px] mt-1">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tables.map((table) => (
|
||||
<SelectItem key={table.name} value={table.name} className="text-xs">
|
||||
{table.label || table.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<HelpText>드롭다운 옵션을 가져올 테이블</HelpText>
|
||||
</div>
|
||||
|
||||
{/* 표시 형식 */}
|
||||
<div>
|
||||
<Label className="text-[10px]">드롭다운 표시 형식</Label>
|
||||
<Select
|
||||
value={selectedField.linkedFieldGroup?.displayFormat || "name_only"}
|
||||
onValueChange={(value: "name_only" | "code_name" | "name_code") =>
|
||||
updateField(selectedSection.id, selectedField.id, {
|
||||
linkedFieldGroup: {
|
||||
...selectedField.linkedFieldGroup,
|
||||
displayFormat: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px] mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LINKED_FIELD_DISPLAY_FORMAT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 표시 컬럼 / 값 컬럼 */}
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<Label className="text-[10px]">표시 컬럼 (사용자에게 보여줄 텍스트)</Label>
|
||||
<Select
|
||||
value={selectedField.linkedFieldGroup?.displayColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
updateField(selectedSection.id, selectedField.id, {
|
||||
linkedFieldGroup: {
|
||||
...selectedField.linkedFieldGroup,
|
||||
displayColumn: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px] mt-1">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[selectedField.linkedFieldGroup?.sourceTable || ""] || []).map((col) => (
|
||||
<SelectItem key={col.name} value={col.name} className="text-xs">
|
||||
{col.label || col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<HelpText>사용자가 드롭다운에서 보게 될 텍스트 (예: 영업부, 개발부)</HelpText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 저장할 컬럼 매핑 */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[10px]">저장할 컬럼 매핑</Label>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newMapping: LinkedFieldMapping = { sourceColumn: "", targetColumn: "" };
|
||||
updateField(selectedSection.id, selectedField.id, {
|
||||
linkedFieldGroup: {
|
||||
...selectedField.linkedFieldGroup,
|
||||
mappings: [...(selectedField.linkedFieldGroup?.mappings || []), newMapping],
|
||||
},
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<HelpText>드롭다운 선택 시 소스 테이블의 어떤 값을 어떤 컬럼에 저장할지 설정</HelpText>
|
||||
|
||||
{(selectedField.linkedFieldGroup?.mappings || []).map((mapping, mappingIndex) => (
|
||||
<div key={mappingIndex} className="bg-muted/30 p-1.5 rounded space-y-1 border">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] text-muted-foreground">매핑 #{mappingIndex + 1}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 text-destructive"
|
||||
onClick={() => {
|
||||
const updatedMappings = (selectedField.linkedFieldGroup?.mappings || []).filter(
|
||||
(_, i) => i !== mappingIndex
|
||||
);
|
||||
updateField(selectedSection.id, selectedField.id, {
|
||||
linkedFieldGroup: {
|
||||
...selectedField.linkedFieldGroup,
|
||||
mappings: updatedMappings,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[9px]">가져올 컬럼 (소스 테이블)</Label>
|
||||
<Select
|
||||
value={mapping.sourceColumn}
|
||||
onValueChange={(value) => {
|
||||
const updatedMappings = (selectedField.linkedFieldGroup?.mappings || []).map((m, i) =>
|
||||
i === mappingIndex ? { ...m, sourceColumn: value } : m
|
||||
);
|
||||
updateField(selectedSection.id, selectedField.id, {
|
||||
linkedFieldGroup: {
|
||||
...selectedField.linkedFieldGroup,
|
||||
mappings: updatedMappings,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] mt-0.5">
|
||||
<SelectValue placeholder="소스 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[selectedField.linkedFieldGroup?.sourceTable || ""] || []).map((col) => (
|
||||
<SelectItem key={col.name} value={col.name} className="text-xs">
|
||||
{col.label || col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[9px]">저장할 컬럼 (저장 테이블)</Label>
|
||||
<Select
|
||||
value={mapping.targetColumn}
|
||||
onValueChange={(value) => {
|
||||
const updatedMappings = (selectedField.linkedFieldGroup?.mappings || []).map((m, i) =>
|
||||
i === mappingIndex ? { ...m, targetColumn: value } : m
|
||||
);
|
||||
updateField(selectedSection.id, selectedField.id, {
|
||||
linkedFieldGroup: {
|
||||
...selectedField.linkedFieldGroup,
|
||||
mappings: updatedMappings,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] mt-0.5">
|
||||
<SelectValue placeholder="저장할 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[config.saveConfig.tableName] || []).map((col) => (
|
||||
<SelectItem key={col.name} value={col.name} className="text-xs">
|
||||
{col.label || col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(selectedField.linkedFieldGroup?.mappings || []).length === 0 && (
|
||||
<p className="text-[9px] text-muted-foreground text-center py-2">
|
||||
+ 버튼을 눌러 매핑을 추가하세요
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user