Files
DDD1542 2348800e68
Build & Deploy to K8s / build-and-deploy (push) Successful in 9m22s
refactor(common-code): 마스터-디테일 재설계 — code_info(그룹) + code_detail(재귀 트리)
카테고리/캐스케이딩 시스템 (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>
2026-05-15 16:50:50 +09:00

181 lines
5.7 KiB
TypeScript

"use client";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { CodeInfoFormModal } from "./CodeInfoFormModal";
import { CodeInfoItem } from "./CodeInfoItem";
import { AlertModal } from "@/components/common/AlertModal";
import { Search, Plus } from "lucide-react";
import { useDeleteCodeInfo } from "@/hooks/queries/useCodeInfo";
import { useCodeInfoInfinite } from "@/hooks/queries/useCodeInfoInfinite";
interface CodeInfoPanelProps {
selectedCodeInfo: string;
onSelectCodeInfo: (codeInfo: string) => void;
}
export function CodeInfoPanel({ selectedCodeInfo, onSelectCodeInfo }: CodeInfoPanelProps) {
const [searchTerm, setSearchTerm] = useState("");
const [showActiveOnly, setShowActiveOnly] = useState(false);
const {
data: rows = [],
isLoading,
error,
handleScroll,
isFetchingNextPage,
hasNextPage,
} = useCodeInfoInfinite({
search: searchTerm || undefined,
active: showActiveOnly || undefined,
});
const deleteMutation = useDeleteCodeInfo();
const [showFormModal, setShowFormModal] = useState(false);
const [editingCode, setEditingCode] = useState<string>("");
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deletingCode, setDeletingCode] = useState<string>("");
const handleNew = () => {
setEditingCode("");
setShowFormModal(true);
};
const handleEdit = (code: string) => {
setEditingCode(code);
setShowFormModal(true);
};
const handleDelete = (code: string) => {
setDeletingCode(code);
setShowDeleteModal(true);
};
const handleConfirmDelete = async () => {
if (!deletingCode) return;
try {
await deleteMutation.mutateAsync(deletingCode);
if (selectedCodeInfo === deletingCode) {
onSelectCodeInfo("");
}
setShowDeleteModal(false);
setDeletingCode("");
} catch (error) {
console.error("그룹 삭제 실패:", error);
}
};
if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<p className="text-destructive"> .</p>
<Button variant="outline" onClick={() => window.location.reload()} className="mt-2">
</Button>
</div>
</div>
);
}
return (
<div className="flex h-full flex-col space-y-4">
{/* 검색 + 등록 */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="그룹 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="h-10 pl-10 text-sm"
/>
</div>
<Button onClick={handleNew} className="h-10 gap-2 text-sm font-medium">
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="activeOnlyInfo"
checked={showActiveOnly}
onChange={(e) => setShowActiveOnly(e.target.checked)}
className="h-4 w-4 rounded border-input"
/>
<label htmlFor="activeOnlyInfo" className="text-sm text-muted-foreground">
</label>
</div>
</div>
{/* 목록 */}
<div className="space-y-3" onScroll={handleScroll}>
{isLoading ? (
<div className="flex h-32 items-center justify-center">
<LoadingSpinner />
</div>
) : rows.length === 0 ? (
<div className="flex h-32 items-center justify-center">
<p className="text-sm text-muted-foreground">
{searchTerm ? "검색 결과가 없습니다." : "그룹이 없습니다."}
</p>
</div>
) : (
<>
{rows.map((row, index) => (
<CodeInfoItem
key={`${row.code_info}-${index}`}
row={row}
isSelected={selectedCodeInfo === row.code_info}
onSelect={() => onSelectCodeInfo(row.code_info)}
onEdit={() => handleEdit(row.code_info)}
onDelete={() => handleDelete(row.code_info)}
/>
))}
{isFetchingNextPage && (
<div className="flex items-center justify-center py-4">
<LoadingSpinner size="sm" />
<span className="ml-2 text-sm text-muted-foreground"> ...</span>
</div>
)}
{!hasNextPage && rows.length > 0 && (
<div className="py-4 text-center text-sm text-muted-foreground">
.
</div>
)}
</>
)}
</div>
{showFormModal && (
<CodeInfoFormModal
isOpen={showFormModal}
onClose={() => setShowFormModal(false)}
editingCodeInfo={editingCode}
existingRows={rows}
/>
)}
{showDeleteModal && (
<AlertModal
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
type="error"
title="그룹 삭제"
message="정말로 이 그룹을 삭제하시겠습니까? 그룹의 모든 디테일 코드도 함께 삭제됩니다 (CASCADE)."
confirmText="삭제"
onConfirm={handleConfirmDelete}
/>
)}
</div>
);
}