공통코드 계층구조 구현

This commit is contained in:
kjs
2025-12-23 09:31:18 +09:00
parent b85b888007
commit 5f406fbe88
32 changed files with 3673 additions and 478 deletions

View File

@@ -36,7 +36,7 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
const [cascadingEnabled, setCascadingEnabled] = useState(!!config.cascadingRelationCode);
const [relationList, setRelationList] = useState<CascadingRelation[]>([]);
const [loadingRelations, setLoadingRelations] = useState(false);
// 🆕 카테고리 값 연쇄관계 상태
const [categoryRelationEnabled, setCategoryRelationEnabled] = useState(!!(config as any).categoryRelationCode);
const [categoryRelationList, setCategoryRelationList] = useState<CategoryValueCascadingGroup[]>([]);
@@ -102,8 +102,8 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
setCascadingEnabled(enabled);
if (!enabled) {
// 비활성화 시 관계 설정 제거
const newConfig = {
...config,
const newConfig = {
...config,
cascadingRelationCode: undefined,
cascadingRole: undefined,
cascadingParentField: undefined,
@@ -124,8 +124,8 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
setCategoryRelationEnabled(enabled);
if (!enabled) {
// 비활성화 시 관계 설정 제거
const newConfig = {
...config,
const newConfig = {
...config,
categoryRelationCode: undefined,
cascadingRole: undefined,
cascadingParentField: undefined,
@@ -140,7 +140,7 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
}
}
};
// 🆕 같은 연쇄 관계의 부모 역할 컴포넌트 찾기
const findParentComponent = (relationCode: string) => {
console.log("🔍 findParentComponent 호출:", {
@@ -148,12 +148,12 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
allComponentsLength: allComponents?.length,
currentComponentId: currentComponent?.id,
});
if (!allComponents || allComponents.length === 0) {
console.log("❌ allComponents가 비어있음");
return null;
}
// 모든 컴포넌트의 cascading 설정 확인
allComponents.forEach((comp: any) => {
const compConfig = comp.componentConfig || {};
@@ -166,7 +166,7 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
});
}
});
const found = allComponents.find((comp: any) => {
const compConfig = comp.componentConfig || {};
return (
@@ -175,7 +175,7 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
compConfig.cascadingRole === "parent"
);
});
console.log("🔍 찾은 부모 컴포넌트:", found);
return found;
};
@@ -183,7 +183,7 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
// 역할 변경 시 부모 필드 자동 감지
const handleRoleChange = (role: "parent" | "child") => {
let parentField = config.cascadingParentField;
// 자식 역할 선택 시 부모 필드 자동 감지
if (role === "child" && config.cascadingRelationCode) {
const parentComp = findParentComponent(config.cascadingRelationCode);
@@ -192,9 +192,9 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
console.log("🔗 부모 필드 자동 감지:", parentField);
}
}
const newConfig = {
...config,
const newConfig = {
...config,
cascadingRole: role,
// 부모 역할일 때는 부모 필드 불필요, 자식일 때는 자동 감지된 값 또는 기존 값
cascadingParentField: role === "parent" ? undefined : parentField,
@@ -203,13 +203,11 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
};
// 선택된 관계 정보
const selectedRelation = relationList.find(r => r.relation_code === config.cascadingRelationCode);
const selectedRelation = relationList.find((r) => r.relation_code === config.cascadingRelationCode);
return (
<div className="space-y-4">
<div className="text-sm font-medium">
select-basic
</div>
<div className="text-sm font-medium">select-basic </div>
{/* select 관련 설정 */}
<div className="space-y-2">
@@ -259,23 +257,18 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
</div>
{/* 연쇄 드롭다운 설정 */}
<div className="border-t pt-4 mt-4 space-y-3">
<div className="mt-4 space-y-3 border-t pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Link2 className="h-4 w-4" />
<Label className="text-sm font-medium"> </Label>
</div>
<Switch
checked={cascadingEnabled}
onCheckedChange={handleCascadingToggle}
/>
<Switch checked={cascadingEnabled} onCheckedChange={handleCascadingToggle} />
</div>
<p className="text-muted-foreground text-xs">
.
</p>
<p className="text-muted-foreground text-xs"> .</p>
{cascadingEnabled && (
<div className="space-y-3 rounded-md border p-3 bg-muted/30">
<div className="bg-muted/30 space-y-3 rounded-md border p-3">
{/* 관계 선택 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
@@ -326,66 +319,66 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
</Button>
</div>
<p className="text-muted-foreground text-xs">
{config.cascadingRole === "parent"
{config.cascadingRole === "parent"
? "이 필드가 상위 선택 역할을 합니다. (예: 창고 선택)"
: config.cascadingRole === "child"
? "이 필드는 상위 필드 값에 따라 옵션이 변경됩니다. (예: 위치 선택)"
: "이 필드의 역할을 선택하세요."}
? "이 필드는 상위 필드 값에 따라 옵션이 변경됩니다. (예: 위치 선택)"
: "이 필드의 역할을 선택하세요."}
</p>
</div>
)}
{/* 부모 필드 설정 (자식 역할일 때만) */}
{config.cascadingRelationCode && config.cascadingRole === "child" && (() => {
// 선택된 관계에서 부모 값 컬럼 가져오기
const expectedParentColumn = selectedRelation?.parent_value_column;
// 부모 역할에 맞는 컴포넌트만 필터링
const parentFieldCandidates = allComponents.filter((comp) => {
// 현재 컴포넌트 제외
if (currentComponent && comp.id === currentComponent.id) return false;
// 관계에서 지정한 부모 컬럼명과 일치하는 컴포넌트
if (expectedParentColumn && comp.columnName !== expectedParentColumn) return false;
// columnName이 있어야 함
return !!comp.columnName;
});
return (
<div className="space-y-2">
<Label className="text-xs"> </Label>
{expectedParentColumn && (
<p className="text-muted-foreground text-xs">
: <strong>{expectedParentColumn}</strong>
</p>
)}
<Select
value={config.cascadingParentField || ""}
onValueChange={(value) => handleChange("cascadingParentField", value || undefined)}
>
<SelectTrigger className="text-xs">
<SelectValue placeholder="부모 필드 선택" />
</SelectTrigger>
<SelectContent>
{parentFieldCandidates.map((comp) => (
<SelectItem key={comp.id} value={comp.columnName}>
{comp.label || comp.columnName} ({comp.columnName})
</SelectItem>
))}
{parentFieldCandidates.length === 0 && (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
{expectedParentColumn
? `'${expectedParentColumn}' 컬럼을 가진 컴포넌트가 화면에 없습니다`
: "선택 가능한 부모 필드가 없습니다"}
</div>
)}
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs">
.
</p>
</div>
);
})()}
{config.cascadingRelationCode &&
config.cascadingRole === "child" &&
(() => {
// 선택된 관계에서 부모 값 컬럼 가져오기
const expectedParentColumn = selectedRelation?.parent_value_column;
// 부모 역할에 맞는 컴포넌트만 필터링
const parentFieldCandidates = allComponents.filter((comp) => {
// 현재 컴포넌트 제외
if (currentComponent && comp.id === currentComponent.id) return false;
// 관계에서 지정한 부모 컬럼명과 일치하는 컴포넌트만
if (expectedParentColumn && comp.columnName !== expectedParentColumn) return false;
// columnName이 있어야 함
return !!comp.columnName;
});
return (
<div className="space-y-2">
<Label className="text-xs"> </Label>
{expectedParentColumn && (
<p className="text-muted-foreground text-xs">
: <strong>{expectedParentColumn}</strong>
</p>
)}
<Select
value={config.cascadingParentField || ""}
onValueChange={(value) => handleChange("cascadingParentField", value || undefined)}
>
<SelectTrigger className="text-xs">
<SelectValue placeholder="부모 필드 선택" />
</SelectTrigger>
<SelectContent>
{parentFieldCandidates.map((comp) => (
<SelectItem key={comp.id} value={comp.columnName}>
{comp.label || comp.columnName} ({comp.columnName})
</SelectItem>
))}
{parentFieldCandidates.length === 0 && (
<div className="text-muted-foreground px-2 py-1.5 text-xs">
{expectedParentColumn
? `'${expectedParentColumn}' 컬럼을 가진 컴포넌트가 화면에 없습니다`
: "선택 가능한 부모 필드가 없습니다"}
</div>
)}
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs"> .</p>
</div>
);
})()}
{/* 선택된 관계 정보 표시 */}
{selectedRelation && config.cascadingRole && (
@@ -436,24 +429,22 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
</div>
{/* 🆕 카테고리 값 연쇄관계 설정 */}
<div className="border-t pt-4 mt-4 space-y-3">
<div className="mt-4 space-y-3 border-t pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Link2 className="h-4 w-4" />
<Label className="text-sm font-medium"> </Label>
</div>
<Switch
checked={categoryRelationEnabled}
onCheckedChange={handleCategoryRelationToggle}
/>
<Switch checked={categoryRelationEnabled} onCheckedChange={handleCategoryRelationToggle} />
</div>
<p className="text-muted-foreground text-xs">
.
<br />: 검사유형
<br />
: 검사유형
</p>
{categoryRelationEnabled && (
<div className="space-y-3 rounded-md border p-3 bg-muted/30">
<div className="bg-muted/30 space-y-3 rounded-md border p-3">
{/* 관계 선택 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
@@ -470,7 +461,8 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
<div className="flex flex-col">
<span>{relation.relation_name}</span>
<span className="text-muted-foreground text-xs">
{relation.parent_table_name}.{relation.parent_column_name} {relation.child_table_name}.{relation.child_column_name}
{relation.parent_table_name}.{relation.parent_column_name} {relation.child_table_name}.
{relation.child_column_name}
</span>
</div>
</SelectItem>
@@ -504,69 +496,69 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
</Button>
</div>
<p className="text-muted-foreground text-xs">
{config.cascadingRole === "parent"
{config.cascadingRole === "parent"
? "이 필드가 상위 카테고리 선택 역할을 합니다. (예: 검사유형)"
: config.cascadingRole === "child"
? "이 필드는 상위 카테고리 값에 따라 옵션이 변경됩니다. (예: 적용대상)"
: "이 필드의 역할을 선택하세요."}
? "이 필드는 상위 카테고리 값에 따라 옵션이 변경됩니다. (예: 적용대상)"
: "이 필드의 역할을 선택하세요."}
</p>
</div>
)}
{/* 부모 필드 설정 (자식 역할일 때만) */}
{(config as any).categoryRelationCode && config.cascadingRole === "child" && (() => {
// 선택된 관계 정보 가져오기
const selectedRelation = categoryRelationList.find(
(r) => r.relation_code === (config as any).categoryRelationCode
);
const expectedParentColumn = selectedRelation?.parent_column_name;
// 부모 역할에 맞는 컴포넌트만 필터링
const parentFieldCandidates = allComponents.filter((comp) => {
// 현재 컴포넌트 제외
if (currentComponent && comp.id === currentComponent.id) return false;
// 관계에서 지정한 부모 컬럼명과 일치하는 컴포넌트
if (expectedParentColumn && comp.columnName !== expectedParentColumn) return false;
// columnName이 있어야 함
return !!comp.columnName;
});
return (
<div className="space-y-2">
<Label className="text-xs"> </Label>
{expectedParentColumn && (
<p className="text-muted-foreground text-xs">
: <strong>{expectedParentColumn}</strong>
</p>
)}
<Select
value={config.cascadingParentField || ""}
onValueChange={(value) => handleChange("cascadingParentField", value || undefined)}
>
<SelectTrigger className="text-xs">
<SelectValue placeholder="부모 필드 선택" />
</SelectTrigger>
<SelectContent>
{parentFieldCandidates.map((comp) => (
<SelectItem key={comp.id} value={comp.columnName}>
{comp.label || comp.columnName} ({comp.columnName})
</SelectItem>
))}
{parentFieldCandidates.length === 0 && (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
{expectedParentColumn
? `'${expectedParentColumn}' 컬럼을 가진 컴포넌트가 화면에 없습니다`
: "선택 가능한 부모 필드가 없습니다"}
</div>
)}
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs">
.
</p>
</div>
);
})()}
{(config as any).categoryRelationCode &&
config.cascadingRole === "child" &&
(() => {
// 선택된 관계 정보 가져오기
const selectedRelation = categoryRelationList.find(
(r) => r.relation_code === (config as any).categoryRelationCode,
);
const expectedParentColumn = selectedRelation?.parent_column_name;
// 부모 역할에 맞는 컴포넌트만 필터링
const parentFieldCandidates = allComponents.filter((comp) => {
// 현재 컴포넌트 제외
if (currentComponent && comp.id === currentComponent.id) return false;
// 관계에서 지정한 부모 컬럼명과 일치하는 컴포넌트만
if (expectedParentColumn && comp.columnName !== expectedParentColumn) return false;
// columnName이 있어야 함
return !!comp.columnName;
});
return (
<div className="space-y-2">
<Label className="text-xs"> </Label>
{expectedParentColumn && (
<p className="text-muted-foreground text-xs">
: <strong>{expectedParentColumn}</strong>
</p>
)}
<Select
value={config.cascadingParentField || ""}
onValueChange={(value) => handleChange("cascadingParentField", value || undefined)}
>
<SelectTrigger className="text-xs">
<SelectValue placeholder="부모 필드 선택" />
</SelectTrigger>
<SelectContent>
{parentFieldCandidates.map((comp) => (
<SelectItem key={comp.id} value={comp.columnName}>
{comp.label || comp.columnName} ({comp.columnName})
</SelectItem>
))}
{parentFieldCandidates.length === 0 && (
<div className="text-muted-foreground px-2 py-1.5 text-xs">
{expectedParentColumn
? `'${expectedParentColumn}' 컬럼을 가진 컴포넌트가 화면에 없습니다`
: "선택 가능한 부모 필드가 없습니다"}
</div>
)}
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs"> .</p>
</div>
);
})()}
{/* 관계 관리 페이지 링크 */}
<div className="flex justify-end">
@@ -580,6 +572,118 @@ export const SelectBasicConfigPanel: React.FC<SelectBasicConfigPanelProps> = ({
</div>
)}
</div>
{/* 계층구조 코드 설정 */}
<div className="mt-4 space-y-3 border-t pt-4">
<div className="flex items-center gap-2">
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 2v4m0 12v4m-6-10H2m20 0h-4m-1.5-6.5L18 4m-12 0 1.5 1.5M6 18l-1.5 1.5M18 18l1.5 1.5" />
</svg>
<Label className="text-sm font-medium"> </Label>
</div>
<div className="rounded border border-blue-200 bg-blue-50 p-2 text-xs text-blue-800">
(depth 2 ) .
</div>
{/* 상세 설정 (항상 표시, 계층구조가 있을 때 적용됨) */}
<div className="bg-muted/30 space-y-3 rounded-md border p-3">
<p className="text-muted-foreground text-xs"> .</p>
{/* 코드 카테고리 선택 안내 */}
{!config.codeCategory && (
<div className="rounded border border-yellow-200 bg-yellow-50 p-2 text-xs text-yellow-800">
.
</div>
)}
{/* 최대 깊이 선택 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={String(config.hierarchicalMaxDepth || 3)}
onValueChange={(value) => handleChange("hierarchicalMaxDepth", Number(value) as 1 | 2 | 3)}
>
<SelectTrigger className="text-xs">
<SelectValue placeholder="깊이 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs"> .</p>
</div>
{/* 1단계 라벨 */}
<div className="space-y-2">
<Label className="text-xs">1 </Label>
<Input
value={config.hierarchicalLabels?.[0] || ""}
onChange={(e) => {
const newLabels: [string, string?, string?] = [
e.target.value,
config.hierarchicalLabels?.[1],
config.hierarchicalLabels?.[2],
];
handleChange("hierarchicalLabels", newLabels);
}}
placeholder="예: 대분류"
className="h-8 text-xs"
/>
</div>
{/* 2단계 라벨 */}
{(config.hierarchicalMaxDepth || 3) >= 2 && (
<div className="space-y-2">
<Label className="text-xs">2 </Label>
<Input
value={config.hierarchicalLabels?.[1] || ""}
onChange={(e) => {
const newLabels: [string, string?, string?] = [
config.hierarchicalLabels?.[0] || "대분류",
e.target.value,
config.hierarchicalLabels?.[2],
];
handleChange("hierarchicalLabels", newLabels);
}}
placeholder="예: 중분류"
className="h-8 text-xs"
/>
</div>
)}
{/* 3단계 라벨 */}
{(config.hierarchicalMaxDepth || 3) >= 3 && (
<div className="space-y-2">
<Label className="text-xs">3 </Label>
<Input
value={config.hierarchicalLabels?.[2] || ""}
onChange={(e) => {
const newLabels: [string, string?, string?] = [
config.hierarchicalLabels?.[0] || "대분류",
config.hierarchicalLabels?.[1] || "중분류",
e.target.value,
];
handleChange("hierarchicalLabels", newLabels);
}}
placeholder="예: 소분류"
className="h-8 text-xs"
/>
</div>
)}
{/* 인라인 표시 */}
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.hierarchicalInline || false}
onCheckedChange={(checked) => handleChange("hierarchicalInline", checked)}
/>
</div>
<p className="text-muted-foreground text-xs"> .</p>
</div>
</div>
</div>
);
};