공통코드 수정중
This commit is contained in:
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||
import { ValidationMessage } from "@/components/common/ValidationMessage";
|
||||
@@ -83,6 +84,9 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
// 폼 스키마 선택 (생성/수정에 따라)
|
||||
const schema = isEditing ? updateCodeSchema : createCodeSchema;
|
||||
|
||||
// 부모 코드 선택 상태
|
||||
const [parentCodeValue, setParentCodeValue] = useState<string>("");
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
mode: "onChange", // 실시간 검증 활성화
|
||||
@@ -111,6 +115,9 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
|
||||
// codeValue는 별도로 설정 (표시용)
|
||||
form.setValue("codeValue" as any, editingCode.codeValue || editingCode.code_value);
|
||||
|
||||
// 부모 코드 설정
|
||||
setParentCodeValue(editingCode.parentCodeValue || editingCode.parent_code_value || "");
|
||||
} else {
|
||||
// 새 코드 모드: 자동 순서 계산
|
||||
const maxSortOrder = codes.length > 0 ? Math.max(...codes.map((c) => c.sortOrder || c.sort_order)) : 0;
|
||||
@@ -122,6 +129,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
description: "",
|
||||
sortOrder: maxSortOrder + 1,
|
||||
});
|
||||
setParentCodeValue("");
|
||||
}
|
||||
}
|
||||
}, [isOpen, isEditing, editingCode, codes]);
|
||||
@@ -129,22 +137,29 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
if (isEditing && editingCode) {
|
||||
// 수정
|
||||
// 수정 - 부모 코드 포함
|
||||
await updateCodeMutation.mutateAsync({
|
||||
categoryCode,
|
||||
codeValue: editingCode.codeValue || editingCode.code_value,
|
||||
data: data as UpdateCodeData,
|
||||
data: {
|
||||
...data,
|
||||
parentCodeValue: parentCodeValue || undefined,
|
||||
} as UpdateCodeData,
|
||||
});
|
||||
} else {
|
||||
// 생성
|
||||
// 생성 - 부모 코드 포함
|
||||
await createCodeMutation.mutateAsync({
|
||||
categoryCode,
|
||||
data: data as CreateCodeData,
|
||||
data: {
|
||||
...data,
|
||||
parentCodeValue: parentCodeValue || undefined,
|
||||
} as CreateCodeData,
|
||||
});
|
||||
}
|
||||
|
||||
onClose();
|
||||
form.reset();
|
||||
setParentCodeValue("");
|
||||
} catch (error) {
|
||||
console.error("코드 저장 실패:", error);
|
||||
}
|
||||
@@ -269,6 +284,43 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 부모 코드 (계층 구조) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="parentCodeValue" className="text-xs sm:text-sm">부모 코드 (선택)</Label>
|
||||
<Select
|
||||
value={parentCodeValue || "_none_"}
|
||||
onValueChange={(val) => setParentCodeValue(val === "_none_" ? "" : val)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue placeholder="부모 코드 선택 (최상위면 비워두세요)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_none_">없음 (최상위)</SelectItem>
|
||||
{codes
|
||||
.filter((c) => {
|
||||
// 자기 자신은 제외
|
||||
const currentCodeValue = editingCode?.codeValue || editingCode?.code_value;
|
||||
const codeValue = c.codeValue || c.code_value;
|
||||
return codeValue !== currentCodeValue;
|
||||
})
|
||||
.map((c) => {
|
||||
const codeValue = c.codeValue || c.code_value;
|
||||
if (!codeValue) return null;
|
||||
return (
|
||||
<SelectItem key={codeValue} value={codeValue}>
|
||||
{c.depth && c.depth > 1 ? "└".repeat(c.depth - 1) + " " : ""}
|
||||
{c.codeName || c.code_name} ({codeValue})
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||
계층 구조가 필요한 경우 부모 코드를 선택하세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 정렬 순서 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sortOrder" className="text-xs sm:text-sm">정렬 순서</Label>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Edit, Trash2 } from "lucide-react";
|
||||
import { Edit, Trash2, ChevronRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useUpdateCode } from "@/hooks/queries/useCodes";
|
||||
import type { CodeInfo } from "@/types/commonCode";
|
||||
@@ -61,20 +61,32 @@ export function SortableCodeItem({
|
||||
}
|
||||
};
|
||||
|
||||
// 계층 깊이 계산 (들여쓰기용)
|
||||
const depth = code.depth || 1;
|
||||
const indentPx = (depth - 1) * 20; // 깊이당 20px 들여쓰기
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
style={{
|
||||
...style,
|
||||
marginLeft: `${indentPx}px`,
|
||||
}}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={cn(
|
||||
"group cursor-grab rounded-lg border bg-card p-4 shadow-sm transition-all hover:shadow-md",
|
||||
isDragging && "cursor-grabbing opacity-50",
|
||||
depth > 1 && "border-l-2 border-l-primary/30",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 계층 표시 아이콘 */}
|
||||
{depth > 1 && (
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
<h4 className="text-sm font-semibold">{code.codeName || code.code_name}</h4>
|
||||
<Badge
|
||||
variant={code.isActive === "Y" || code.is_active === "Y" ? "default" : "secondary"}
|
||||
@@ -96,7 +108,14 @@ export function SortableCodeItem({
|
||||
{code.isActive === "Y" || code.is_active === "Y" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{code.codeValue || code.code_value}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{code.codeValue || code.code_value}
|
||||
{code.parentCodeValue || code.parent_code_value ? (
|
||||
<span className="ml-2 text-primary/70">
|
||||
(부모: {code.parentCodeValue || code.parent_code_value})
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
{code.description && <p className="mt-1 text-xs text-muted-foreground">{code.description}</p>}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* - swap: 스왑 선택 (좌우 이동)
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -26,6 +26,7 @@ import { cn } from "@/lib/utils";
|
||||
import { UnifiedSelectProps, SelectOption } from "@/types/unified-components";
|
||||
import { Check, ChevronsUpDown, X, ArrowLeftRight } from "lucide-react";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import UnifiedFormContext from "./UnifiedFormContext";
|
||||
|
||||
/**
|
||||
* 드롭다운 선택 컴포넌트
|
||||
@@ -463,20 +464,56 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>(
|
||||
const [optionsLoaded, setOptionsLoaded] = useState(false);
|
||||
|
||||
// 옵션 로딩에 필요한 값들만 추출 (객체 참조 대신 원시값 사용)
|
||||
const source = config.source;
|
||||
// category 소스는 code로 자동 변환 (카테고리 → 공통코드 통합)
|
||||
const rawSource = config.source;
|
||||
const categoryTable = (config as any).categoryTable;
|
||||
const categoryColumn = (config as any).categoryColumn;
|
||||
|
||||
// category 소스인 경우 code로 변환하고 codeGroup을 자동 생성
|
||||
const source = rawSource === "category" ? "code" : rawSource;
|
||||
const codeGroup = rawSource === "category" && categoryTable && categoryColumn
|
||||
? `${categoryTable.toUpperCase()}_${categoryColumn.toUpperCase()}`
|
||||
: config.codeGroup;
|
||||
|
||||
const entityTable = config.entityTable;
|
||||
const entityValueColumn = config.entityValueColumn || config.entityValueField;
|
||||
const entityLabelColumn = config.entityLabelColumn || config.entityLabelField;
|
||||
const codeGroup = config.codeGroup;
|
||||
const table = config.table;
|
||||
const valueColumn = config.valueColumn;
|
||||
const labelColumn = config.labelColumn;
|
||||
const apiEndpoint = config.apiEndpoint;
|
||||
const staticOptions = config.options;
|
||||
|
||||
// 계층 코드 연쇄 선택 관련
|
||||
const hierarchical = config.hierarchical;
|
||||
const parentField = config.parentField;
|
||||
|
||||
// FormContext에서 부모 필드 값 가져오기 (Context가 없으면 null)
|
||||
const formContext = useContext(UnifiedFormContext);
|
||||
|
||||
// 부모 필드의 값 계산
|
||||
const parentValue = useMemo(() => {
|
||||
if (!hierarchical || !parentField) return null;
|
||||
|
||||
// FormContext가 있으면 거기서 값 가져오기
|
||||
if (formContext) {
|
||||
const val = formContext.getValue(parentField);
|
||||
return val as string | null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [hierarchical, parentField, formContext]);
|
||||
|
||||
// 데이터 소스에 따른 옵션 로딩 (원시값 의존성만 사용)
|
||||
useEffect(() => {
|
||||
// 이미 로드된 경우 스킵 (static 제외)
|
||||
// 계층 구조인 경우 부모 값이 변경되면 다시 로드
|
||||
if (hierarchical && source === "code") {
|
||||
setOptionsLoaded(false);
|
||||
}
|
||||
}, [parentValue, hierarchical, source]);
|
||||
|
||||
useEffect(() => {
|
||||
// 이미 로드된 경우 스킵 (static 제외, 계층 구조 제외)
|
||||
if (optionsLoaded && source !== "static") {
|
||||
return;
|
||||
}
|
||||
@@ -493,14 +530,32 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>(
|
||||
let fetchedOptions: SelectOption[] = [];
|
||||
|
||||
if (source === "code" && codeGroup) {
|
||||
// 공통코드에서 로드
|
||||
const response = await apiClient.get(`/common-codes/${codeGroup}/items`);
|
||||
const data = response.data;
|
||||
if (data.success && data.data) {
|
||||
fetchedOptions = data.data.map((item: { code: string; codeName: string }) => ({
|
||||
value: item.code,
|
||||
label: item.codeName,
|
||||
}));
|
||||
// 계층 구조 사용 시 자식 코드만 로드
|
||||
if (hierarchical) {
|
||||
const params = new URLSearchParams();
|
||||
if (parentValue) {
|
||||
params.append("parentCodeValue", parentValue);
|
||||
}
|
||||
const queryString = params.toString();
|
||||
const url = `/common-codes/categories/${codeGroup}/children${queryString ? `?${queryString}` : ""}`;
|
||||
const response = await apiClient.get(url);
|
||||
const data = response.data;
|
||||
if (data.success && data.data) {
|
||||
fetchedOptions = data.data.map((item: { value: string; label: string; hasChildren: boolean }) => ({
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// 일반 공통코드에서 로드
|
||||
const response = await apiClient.get(`/common-codes/${codeGroup}/items`);
|
||||
const data = response.data;
|
||||
if (data.success && data.data) {
|
||||
fetchedOptions = data.data.map((item: { code: string; codeName: string }) => ({
|
||||
value: item.code,
|
||||
label: item.codeName,
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else if (source === "db" && table) {
|
||||
// DB 테이블에서 로드
|
||||
@@ -547,8 +602,8 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>(
|
||||
}
|
||||
};
|
||||
|
||||
loadOptions();
|
||||
}, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded]);
|
||||
loadOptions();
|
||||
}, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded, hierarchical, parentValue]);
|
||||
|
||||
// 모드별 컴포넌트 렌더링
|
||||
const renderSelect = () => {
|
||||
|
||||
Reference in New Issue
Block a user