diff options
Diffstat (limited to 'lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx')
| -rw-r--r-- | lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx | 436 |
1 files changed, 436 insertions, 0 deletions
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<DocumentClassOption[]>([]) + const [loading, setLoading] = useState(true) + const [newOptionRows, setNewOptionRows] = useState<NewOptionRow[]>([]) + const [editingOption, setEditingOption] = useState<DocumentClassOption | null>(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 ( + <Sheet open={open} onOpenChange={onOpenChange}> + <SheetContent className="w-[600px] sm:max-w-[600px] overflow-y-auto"> + <SheetHeader> + <SheetTitle>하위 옵션 관리</SheetTitle> + <SheetDescription> + {comboBoxOption.description} ({comboBoxOption.code})의 하위 옵션을 관리합니다. + </SheetDescription> + </SheetHeader> + + <div className="space-y-4 mt-6"> + {/* 새 하위 옵션 추가 */} + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <h4 className="text-sm font-medium">새 하위 옵션 추가</h4> + <Button + type="button" + variant="outline" + size="sm" + onClick={addNewRow} + className="h-8" + > + <Plus className="h-4 w-4 mr-1" /> + 옵션 추가 + </Button> + </div> + + {newOptionRows.length > 0 && ( + <div className="border rounded-lg overflow-hidden"> + <Table> + <TableHeader> + <TableRow className="bg-muted/30"> + <TableHead className="w-[40%]">옵션 값 *</TableHead> + <TableHead className="w-[30%]">옵션 코드</TableHead> + <TableHead className="w-[20%]">순서</TableHead> + <TableHead className="w-[10%]"></TableHead> + </TableRow> + </TableHeader> + <TableBody> + {newOptionRows.map((row) => ( + <TableRow key={row.id} className="hover:bg-muted/30"> + <TableCell> + <Input + value={row.optionValue} + onChange={(e) => updateNewRow(row.id, "optionValue", e.target.value)} + placeholder="옵션 값" + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + </TableCell> + <TableCell> + <Input + value={row.optionCode} + onChange={(e) => updateNewRow(row.id, "optionCode", e.target.value)} + placeholder="옵션 코드 (선택)" + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + </TableCell> + <TableCell> + <Input + type="number" + value={row.sortOrder} + onChange={(e) => updateNewRow(row.id, "sortOrder", parseInt(e.target.value) || 0)} + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + </TableCell> + <TableCell> + <Button + onClick={() => removeNewRow(row.id)} + size="sm" + variant="ghost" + className="h-6 w-6 p-0" + > + <X className="h-3 w-3" /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + <div className="p-3 border-t"> + <Button + onClick={handleSaveNewOptions} + size="sm" + className="h-8" + > + <Save className="h-4 w-4 mr-1" /> + 저장 + </Button> + </div> + </div> + )} + </div> + + {/* 기존 하위 옵션 목록 */} + <div className="space-y-2"> + <h4 className="text-sm font-medium">기존 하위 옵션</h4> + <div className="border rounded-lg overflow-hidden"> + <Table> + <TableHeader> + <TableRow className="bg-muted/30"> + <TableHead className="w-[35%]">옵션 값</TableHead> + <TableHead className="w-[25%]">옵션 코드</TableHead> + <TableHead className="w-[15%]">순서</TableHead> + <TableHead className="w-[15%]">상태</TableHead> + <TableHead className="w-[10%]">작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {loading ? ( + <TableRow> + <TableCell colSpan={5} className="text-center py-8"> + 로딩 중... + </TableCell> + </TableRow> + ) : options.length === 0 ? ( + <TableRow> + <TableCell colSpan={5} className="text-center text-muted-foreground py-8"> + 등록된 하위 옵션이 없습니다. + </TableCell> + </TableRow> + ) : ( + options.map((option) => ( + <TableRow key={option.id} className="hover:bg-muted/30"> + <TableCell className="text-sm"> + {editingOption?.id === option.id ? ( + <Input + value={editingOption.optionValue} + onChange={(e) => setEditingOption(prev => prev ? { ...prev, optionValue: e.target.value } : null)} + placeholder="옵션 값" + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + ) : ( + option.optionValue + )} + </TableCell> + <TableCell className="text-sm"> + {editingOption?.id === option.id ? ( + <Input + value={editingOption.optionCode || ""} + onChange={(e) => setEditingOption(prev => prev ? { ...prev, optionCode: e.target.value } : null)} + placeholder="옵션 코드" + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + ) : ( + option.optionCode || "-" + )} + </TableCell> + <TableCell className="text-sm"> + {editingOption?.id === option.id ? ( + <Input + type="number" + value={editingOption.sortOrder} + onChange={(e) => setEditingOption(prev => prev ? { ...prev, sortOrder: parseInt(e.target.value) || 0 } : null)} + className="border-0 focus-visible:ring-1 bg-transparent h-8" + /> + ) : ( + option.sortOrder + )} + </TableCell> + <TableCell className="text-sm"> + <span className={`px-2 py-1 rounded text-xs ${ + option.isActive + ? "bg-green-100 text-green-800" + : "bg-red-100 text-red-800" + }`}> + {option.isActive ? "활성" : "비활성"} + </span> + </TableCell> + <TableCell className="text-sm"> + {editingOption?.id === option.id ? ( + <div className="flex gap-1"> + <Button + onClick={() => handleUpdateOption(editingOption)} + size="sm" + variant="outline" + className="h-6 px-2 text-xs" + > + 저장 + </Button> + <Button + onClick={() => setEditingOption(null)} + size="sm" + variant="ghost" + className="h-6 px-2 text-xs" + > + 취소 + </Button> + </div> + ) : ( + <div className="flex gap-1"> + <Button + onClick={() => setEditingOption(option)} + size="sm" + variant="outline" + className="h-6 px-2 text-xs" + > + 수정 + </Button> + <Button + onClick={() => handleDeleteOption(option.id)} + size="sm" + variant="ghost" + className="h-6 px-2 text-xs text-red-600 hover:text-red-700" + > + <Trash2 className="h-3 w-3" /> + </Button> + </div> + )} + </TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + </div> + </div> + </div> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file |
