summaryrefslogtreecommitdiff
path: root/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx
diff options
context:
space:
mode:
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.tsx436
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