코드 무한 스크롤 구현
This commit is contained in:
@@ -10,7 +10,8 @@ import { SortableCodeItem } from "./SortableCodeItem";
|
||||
import { AlertModal } from "@/components/common/AlertModal";
|
||||
import { Search, Plus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCodes, useDeleteCode, useReorderCodes } from "@/hooks/queries/useCodes";
|
||||
import { useDeleteCode, useReorderCodes } from "@/hooks/queries/useCodes";
|
||||
import { useCodesInfinite } from "@/hooks/queries/useCodesInfinite";
|
||||
import type { CodeInfo } from "@/types/commonCode";
|
||||
|
||||
// Drag and Drop
|
||||
@@ -24,19 +25,27 @@ interface CodeDetailPanelProps {
|
||||
}
|
||||
|
||||
export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
|
||||
// React Query로 코드 데이터 관리
|
||||
const { data: codes = [], isLoading, error } = useCodes(categoryCode);
|
||||
// 검색 및 필터 상태 (먼저 선언)
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showActiveOnly, setShowActiveOnly] = useState(false);
|
||||
|
||||
// React Query로 코드 데이터 관리 (무한 스크롤)
|
||||
const {
|
||||
data: codes = [],
|
||||
isLoading,
|
||||
error,
|
||||
handleScroll,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
} = useCodesInfinite(categoryCode, {
|
||||
search: searchTerm || undefined,
|
||||
active: showActiveOnly || undefined,
|
||||
});
|
||||
const deleteCodeMutation = useDeleteCode();
|
||||
const reorderCodesMutation = useReorderCodes();
|
||||
|
||||
// 검색 및 필터링 훅 사용
|
||||
const {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
showActiveOnly,
|
||||
setShowActiveOnly,
|
||||
filteredItems: filteredCodes,
|
||||
} = useSearchAndFilter(codes, {
|
||||
// 드래그앤드롭을 위해 필터링된 코드 목록 사용
|
||||
const { filteredItems: filteredCodes } = useSearchAndFilter(codes, {
|
||||
searchFields: ["code_name", "code_value"],
|
||||
});
|
||||
|
||||
@@ -47,7 +56,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
|
||||
const [deletingCode, setDeletingCode] = useState<CodeInfo | null>(null);
|
||||
|
||||
// 드래그 앤 드롭 훅 사용
|
||||
const dragAndDrop = useDragAndDrop({
|
||||
const dragAndDrop = useDragAndDrop<CodeInfo>({
|
||||
items: filteredCodes,
|
||||
onReorder: async (reorderedItems) => {
|
||||
await reorderCodesMutation.mutateAsync({
|
||||
@@ -58,7 +67,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
|
||||
})),
|
||||
});
|
||||
},
|
||||
getItemId: (code) => code.code_value,
|
||||
getItemId: (code: CodeInfo) => code.code_value,
|
||||
});
|
||||
|
||||
// 새 코드 생성
|
||||
@@ -158,72 +167,87 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 코드 목록 */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* 코드 목록 (무한 스크롤) */}
|
||||
<div className="h-96 overflow-y-auto" onScroll={handleScroll}>
|
||||
{isLoading ? (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : filteredCodes.length === 0 ? (
|
||||
<div className="p-4 text-center text-gray-500">
|
||||
{searchTerm ? "검색 결과가 없습니다." : "코드가 없습니다."}
|
||||
{codes.length === 0 ? "코드가 없습니다." : "검색 결과가 없습니다."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2">
|
||||
<DndContext {...dragAndDrop.dndContextProps}>
|
||||
<SortableContext
|
||||
items={filteredCodes.map((code) => code.code_value)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{filteredCodes.map((code) => (
|
||||
<SortableCodeItem
|
||||
key={code.code_value}
|
||||
code={code}
|
||||
categoryCode={categoryCode}
|
||||
onEdit={() => handleEditCode(code)}
|
||||
onDelete={() => handleDeleteCode(code)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{dragAndDrop.activeItem ? (
|
||||
<div className="cursor-grabbing rounded-lg border border-gray-300 bg-white p-3 shadow-lg">
|
||||
{(() => {
|
||||
const activeCode = dragAndDrop.activeItem;
|
||||
if (!activeCode) return null;
|
||||
return (
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-gray-900">{activeCode.code_name}</h3>
|
||||
<Badge
|
||||
variant={activeCode.is_active === "Y" ? "default" : "secondary"}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
activeCode.is_active === "Y"
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-600",
|
||||
)}
|
||||
>
|
||||
{activeCode.is_active === "Y" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">{activeCode.code_value}</p>
|
||||
{activeCode.description && (
|
||||
<p className="mt-1 text-sm text-gray-500">{activeCode.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<>
|
||||
<div className="p-2">
|
||||
<DndContext {...dragAndDrop.dndContextProps}>
|
||||
<SortableContext
|
||||
items={filteredCodes.map((code) => code.code_value)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{filteredCodes.map((code, index) => (
|
||||
<SortableCodeItem
|
||||
key={`${code.code_value}-${index}`}
|
||||
code={code}
|
||||
categoryCode={categoryCode}
|
||||
onEdit={() => handleEditCode(code)}
|
||||
onDelete={() => handleDeleteCode(code)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{dragAndDrop.activeItem ? (
|
||||
<div className="cursor-grabbing rounded-lg border border-gray-300 bg-white p-3 shadow-lg">
|
||||
{(() => {
|
||||
const activeCode = dragAndDrop.activeItem;
|
||||
if (!activeCode) return null;
|
||||
return (
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-gray-900">{activeCode.code_name}</h3>
|
||||
<Badge
|
||||
variant={activeCode.is_active === "Y" ? "default" : "secondary"}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
activeCode.is_active === "Y"
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-600",
|
||||
)}
|
||||
>
|
||||
{activeCode.is_active === "Y" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">{activeCode.code_value}</p>
|
||||
{activeCode.description && (
|
||||
<p className="mt-1 text-sm text-gray-500">{activeCode.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
{/* 무한 스크롤 로딩 인디케이터 */}
|
||||
{isFetchingNextPage && (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<LoadingSpinner size="sm" />
|
||||
<span className="ml-2 text-sm text-gray-500">코드를 더 불러오는 중...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 모든 코드 로드 완료 메시지 */}
|
||||
{!hasNextPage && codes.length > 0 && (
|
||||
<div className="py-4 text-center text-sm text-gray-500">모든 코드를 불러왔습니다.</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user