From 50ae0b8f02c034e60d4cbb504620dfa1575a836f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Jul 2025 09:19:42 +0000 Subject: (박서영) 설계 document Numbering Rule 개발-최겸 업로드 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/document-class-options-sheet.tsx | 436 +++++++++++++++++++++ 1 file changed, 436 insertions(+) create mode 100644 lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx (limited to 'lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx') diff --git a/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx b/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx new file mode 100644 index 00000000..8585d9a3 --- /dev/null +++ b/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx @@ -0,0 +1,436 @@ +"use client" + +import * as React from "react" +import { useState, useEffect } from "react" +import { Plus, Trash2, Save, X } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { toast } from "sonner" +import { + getDocumentClassOptions, + createDocumentClassOption, + updateDocumentClassOption, + deleteDocumentClassOption +} from "../service" + +interface DocumentClassOptionsSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + comboBoxOption: { + id: number + code: string + description: string + } + onSuccess: () => void +} + +interface DocumentClassOption { + id: number + comboBoxSettingId: number + optionValue: string + optionCode: string | null + sortOrder: number + isActive: boolean + createdAt: Date + updatedAt: Date +} + +interface NewOptionRow { + id: string + optionValue: string + optionCode: string + sortOrder: number +} + +export function DocumentClassOptionsSheet({ + open, + onOpenChange, + comboBoxOption, + onSuccess +}: DocumentClassOptionsSheetProps) { + const [options, setOptions] = useState([]) + const [loading, setLoading] = useState(true) + const [newOptionRows, setNewOptionRows] = useState([]) + const [editingOption, setEditingOption] = useState(null) + + // 하위 옵션 목록 로드 + const loadOptions = async () => { + try { + setLoading(true) + const result = await getDocumentClassOptions(comboBoxOption.id) + if (result.success && result.data) { + setOptions(result.data) + } else { + toast.error("하위 옵션 목록을 불러오는데 실패했습니다.") + } + } catch (error) { + console.error("하위 옵션 로드 실패:", error) + toast.error("하위 옵션 목록을 불러오는데 실패했습니다.") + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (open) { + loadOptions() + } + }, [open, comboBoxOption.id]) + + // 새 행 추가 + const addNewRow = () => { + const newRow: NewOptionRow = { + id: `new-${Date.now()}-${Math.random()}`, + optionValue: "", + optionCode: "", + sortOrder: options.length + newOptionRows.length + 1, + } + setNewOptionRows(prev => [...prev, newRow]) + } + + // 새 행 삭제 + const removeNewRow = (id: string) => { + setNewOptionRows(prev => prev.filter(row => row.id !== id)) + } + + // 새 행 업데이트 + const updateNewRow = (id: string, field: keyof NewOptionRow, value: string | number) => { + setNewOptionRows(prev => + prev.map(row => + row.id === id ? { ...row, [field]: value } : row + ) + ) + } + + // 새 하위 옵션 저장 + const handleSaveNewOptions = async () => { + const validRows = newOptionRows.filter(row => row.optionValue.trim()) + + if (validRows.length === 0) { + toast.error("최소 하나의 옵션 값을 입력해주세요.") + return + } + + try { + let successCount = 0 + let errorCount = 0 + + for (const row of validRows) { + try { + const result = await createDocumentClassOption({ + comboBoxSettingId: comboBoxOption.id, + optionValue: row.optionValue.trim(), + optionCode: row.optionCode.trim() || undefined, + sortOrder: row.sortOrder, + }) + + if (result.success) { + successCount++ + } else { + errorCount++ + } + } catch (error) { + console.error("하위 옵션 추가 실패:", error) + errorCount++ + } + } + + if (successCount > 0) { + toast.success(`${successCount}개의 하위 옵션이 추가되었습니다.${errorCount > 0 ? ` (${errorCount}개 실패)` : ''}`) + setNewOptionRows([]) + await loadOptions() + onSuccess() + } else { + toast.error("모든 하위 옵션 추가에 실패했습니다.") + } + } catch (error) { + console.error("하위 옵션 추가 실패:", error) + toast.error("하위 옵션 추가에 실패했습니다.") + } + } + + // 기존 하위 옵션 수정 + const handleUpdateOption = async (option: DocumentClassOption) => { + if (!option.optionValue.trim()) { + toast.error("옵션 값은 필수 입력 항목입니다.") + return + } + + try { + const result = await updateDocumentClassOption({ + id: option.id, + optionValue: option.optionValue.trim(), + optionCode: option.optionCode || undefined, + sortOrder: option.sortOrder, + isActive: option.isActive, + }) + + if (result.success) { + await loadOptions() + setEditingOption(null) + toast.success("하위 옵션이 수정되었습니다.") + } else { + toast.error("하위 옵션 수정에 실패했습니다.") + } + } catch (error) { + console.error("하위 옵션 수정 실패:", error) + toast.error("하위 옵션 수정에 실패했습니다.") + } + } + + // 하위 옵션 삭제 + const handleDeleteOption = async (optionId: number) => { + if (!confirm("정말로 이 하위 옵션을 삭제하시겠습니까?")) { + return + } + + try { + const result = await deleteDocumentClassOption(optionId) + if (result.success) { + await loadOptions() + toast.success("하위 옵션이 삭제되었습니다.") + } else { + toast.error("하위 옵션 삭제에 실패했습니다.") + } + } catch (error) { + console.error("하위 옵션 삭제 실패:", error) + toast.error("하위 옵션 삭제에 실패했습니다.") + } + } + + return ( + + + + 하위 옵션 관리 + + {comboBoxOption.description} ({comboBoxOption.code})의 하위 옵션을 관리합니다. + + + +
+ {/* 새 하위 옵션 추가 */} +
+
+

새 하위 옵션 추가

+ +
+ + {newOptionRows.length > 0 && ( +
+ + + + 옵션 값 * + 옵션 코드 + 순서 + + + + + {newOptionRows.map((row) => ( + + + updateNewRow(row.id, "optionValue", e.target.value)} + placeholder="옵션 값" + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + + + updateNewRow(row.id, "optionCode", e.target.value)} + placeholder="옵션 코드 (선택)" + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + + + updateNewRow(row.id, "sortOrder", parseInt(e.target.value) || 0)} + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + + + + + + ))} + +
+
+ +
+
+ )} +
+ + {/* 기존 하위 옵션 목록 */} +
+

기존 하위 옵션

+
+ + + + 옵션 값 + 옵션 코드 + 순서 + 상태 + 작업 + + + + {loading ? ( + + + 로딩 중... + + + ) : options.length === 0 ? ( + + + 등록된 하위 옵션이 없습니다. + + + ) : ( + options.map((option) => ( + + + {editingOption?.id === option.id ? ( + setEditingOption(prev => prev ? { ...prev, optionValue: e.target.value } : null)} + placeholder="옵션 값" + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + ) : ( + option.optionValue + )} + + + {editingOption?.id === option.id ? ( + setEditingOption(prev => prev ? { ...prev, optionCode: e.target.value } : null)} + placeholder="옵션 코드" + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + ) : ( + option.optionCode || "-" + )} + + + {editingOption?.id === option.id ? ( + setEditingOption(prev => prev ? { ...prev, sortOrder: parseInt(e.target.value) || 0 } : null)} + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + ) : ( + option.sortOrder + )} + + + + {option.isActive ? "활성" : "비활성"} + + + + {editingOption?.id === option.id ? ( +
+ + +
+ ) : ( +
+ + +
+ )} +
+
+ )) + )} +
+
+
+
+
+
+
+ ) +} \ No newline at end of file -- cgit v1.2.3