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>
181 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|