2348800e68
Build & Deploy to K8s / build-and-deploy (push) Successful in 9m22s
카테고리/캐스케이딩 시스템 (B/C/D) 전부 폐기:
- BE: mapper/Service/Controller 9세트 삭제 (cascading*, categoryTree, tableCategoryValue, categoryValueCascading, codeMerge)
- FE: 페이지 3 + API 8 + hooks 2 + 폐기 컴포넌트 6 삭제, 14곳 의존성 정리
- DB: 12 테이블 DROP, TABLE_TYPE_COLUMNS.CODE_CATEGORY → CODE_INFO rename
신설 commonCode 마스터-디테일:
- code_info: 1레벨 그룹 마스터
- code_detail: 2~∞ depth 재귀 트리 (parent_detail_id self-FK, depth 자동 계산)
- API: /api/common-codes/{info,detail}
- CodeCategoryFormModal/Panel → CodeInfoFormModal/Panel rename
- code_category 컬럼명 전부 code_info 로 치환 (mapper/Java/FE)
- 옛 commonCode API URL (/categories/...) → getCodeOptions 어댑터 + /detail?code_info=... 전환
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
199 lines
6.6 KiB
TypeScript
199 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
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, CornerDownRight, Plus, ChevronRight, ChevronDown } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { useUpdateCodeDetail } from "@/hooks/queries/useCodeDetail";
|
|
import type { CodeDetail } from "@/types/commonCode";
|
|
|
|
interface SortableCodeItemProps {
|
|
row: CodeDetail;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onAddChild: () => void;
|
|
isDragOverlay?: boolean;
|
|
hasChildren?: boolean;
|
|
childCount?: number;
|
|
isExpanded?: boolean;
|
|
onToggleExpand?: () => void;
|
|
}
|
|
|
|
export function SortableCodeItem({
|
|
row,
|
|
onEdit,
|
|
onDelete,
|
|
onAddChild,
|
|
isDragOverlay = false,
|
|
hasChildren = false,
|
|
childCount = 0,
|
|
isExpanded = true,
|
|
onToggleExpand,
|
|
}: SortableCodeItemProps) {
|
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
id: String(row.code_detail_id),
|
|
disabled: isDragOverlay,
|
|
});
|
|
const updateMutation = useUpdateCodeDetail();
|
|
|
|
const style = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
};
|
|
|
|
const handleToggleActive = async (next: "Y" | "N") => {
|
|
try {
|
|
await updateMutation.mutateAsync({
|
|
codeDetailId: row.code_detail_id,
|
|
data: {
|
|
code_info: row.code_info,
|
|
parent_detail_id: row.parent_detail_id ?? null,
|
|
code_value: row.code_value,
|
|
code_name: row.code_name,
|
|
code_name_eng: row.code_name_eng || "",
|
|
description: row.description || "",
|
|
sort_order: row.sort_order ?? 10,
|
|
is_active: next,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("디테일 활성 상태 변경 실패:", error);
|
|
}
|
|
};
|
|
|
|
// depth 는 2 부터. (그룹 직속 = 2)
|
|
const depth: number = row.depth ?? 2;
|
|
const indentLevel = Math.max(0, depth - 2) * 28;
|
|
const hasParent = row.parent_detail_id != null;
|
|
|
|
return (
|
|
<div className="flex items-stretch">
|
|
{indentLevel > 0 && (
|
|
<div
|
|
className="flex items-center justify-end pr-2"
|
|
style={{ width: `${indentLevel}px`, minWidth: `${indentLevel}px` }}
|
|
>
|
|
<CornerDownRight className="h-4 w-4 text-muted-foreground/50" />
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
ref={setNodeRef}
|
|
style={style}
|
|
{...attributes}
|
|
{...listeners}
|
|
className={cn(
|
|
"group flex-1 cursor-grab rounded-lg border bg-card p-4 shadow-sm transition-all hover:shadow-md",
|
|
isDragging && "cursor-grabbing opacity-50",
|
|
depth === 2 && "border-l-4 border-l-primary",
|
|
depth === 3 && "border-l-4 border-l-blue-400",
|
|
depth === 4 && "border-l-4 border-l-green-400",
|
|
depth >= 5 && "border-l-4 border-l-muted-foreground/40",
|
|
)}
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{hasChildren && onToggleExpand && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onToggleExpand();
|
|
}}
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
className="-ml-1 flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
title={isExpanded ? "접기" : "펼치기"}
|
|
>
|
|
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
</button>
|
|
)}
|
|
<h4 className="text-sm font-semibold">{row.code_name}</h4>
|
|
{hasChildren && !isExpanded && (
|
|
<span className="text-[10px] text-muted-foreground">({childCount})</span>
|
|
)}
|
|
<Badge variant="outline" className="bg-muted/40 px-1.5 py-0 text-[10px]">
|
|
depth {depth}
|
|
</Badge>
|
|
<Badge
|
|
variant={row.is_active === "Y" ? "default" : "secondary"}
|
|
className={cn(
|
|
"cursor-pointer text-xs transition-colors",
|
|
updateMutation.isPending && "cursor-not-allowed opacity-50",
|
|
)}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!updateMutation.isPending) {
|
|
handleToggleActive(row.is_active === "Y" ? "N" : "Y");
|
|
}
|
|
}}
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
>
|
|
{row.is_active === "Y" ? "활성" : "비활성"}
|
|
</Badge>
|
|
</div>
|
|
<p className="mt-1 text-xs text-muted-foreground">{row.code_value}</p>
|
|
{hasParent && (
|
|
<p className="mt-0.5 text-[10px] text-muted-foreground">
|
|
상위: detail#{row.parent_detail_id}
|
|
</p>
|
|
)}
|
|
{row.description && (
|
|
<p className="mt-1 text-xs text-muted-foreground">{row.description}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className="flex items-center gap-1"
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onAddChild();
|
|
}}
|
|
title="하위 코드 추가"
|
|
className="text-primary hover:bg-primary/10 hover:text-primary"
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onEdit();
|
|
}}
|
|
>
|
|
<Edit className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onDelete();
|
|
}}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|