diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-26 09:57:24 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-26 09:57:24 +0000 |
| commit | 8b23b471638a155fd1bfa3a8c853b26d9315b272 (patch) | |
| tree | 47353e9dd342011cb2f1dcd24b09661707a8421b /lib/docu-list-rule | |
| parent | d62368d2b68d73da895977e60a18f9b1286b0545 (diff) | |
(대표님) 권한관리, 문서업로드, rfq첨부, SWP문서룰 등
(최겸) 입찰
Diffstat (limited to 'lib/docu-list-rule')
4 files changed, 332 insertions, 204 deletions
diff --git a/lib/docu-list-rule/number-type-configs/service.ts b/lib/docu-list-rule/number-type-configs/service.ts index c29af464..b644c43a 100644 --- a/lib/docu-list-rule/number-type-configs/service.ts +++ b/lib/docu-list-rule/number-type-configs/service.ts @@ -166,12 +166,12 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { } } -// Number Type Config 생성 export async function createNumberTypeConfig(input: { documentNumberTypeId: number codeGroupId: number | null sdq: number description?: string + delimiter?: string remark?: string }) { try { @@ -198,6 +198,7 @@ export async function createNumberTypeConfig(input: { codeGroupId: input.codeGroupId, sdq: input.sdq, description: input.description, + delimiter: input.delimiter, remark: input.remark, }) .returning({ id: documentNumberTypeConfigs.id }) @@ -218,12 +219,12 @@ export async function createNumberTypeConfig(input: { } } -// Number Type Config 수정 export async function updateNumberTypeConfig(input: { id: number codeGroupId: number | null sdq: number description?: string + delimiter?: string remark?: string }) { try { @@ -263,6 +264,7 @@ export async function updateNumberTypeConfig(input: { codeGroupId: input.codeGroupId, sdq: input.sdq, description: input.description, + delimiter: input.delimiter, remark: input.remark, updatedAt: new Date(), }) @@ -284,7 +286,6 @@ export async function updateNumberTypeConfig(input: { } } } - // Number Type Config 순서 변경 (간단한 방식) export async function updateNumberTypeConfigOrder(input: { id: number diff --git a/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx index cd2d6fc8..ad3478ff 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx @@ -2,6 +2,9 @@ import * as React from "react" import { Loader2 } from "lucide-react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" import { toast } from "sonner" import { Button } from "@/components/ui/button" @@ -14,6 +17,14 @@ import { DialogTitle, } from "@/components/ui/dialog" import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Select, SelectContent, SelectItem, @@ -21,18 +32,30 @@ import { SelectValue, } from "@/components/ui/select" import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { updateNumberTypeConfig, getActiveCodeGroups } from "@/lib/docu-list-rule/number-type-configs/service" import { NumberTypeConfig } from "@/lib/docu-list-rule/types" +const formSchema = z.object({ + codeGroupId: z.string().min(1, "Code Group을 선택해주세요."), + sdq: z.string().min(1, "순서를 입력해주세요.").refine( + (val) => !isNaN(Number(val)) && Number(val) > 0, + { message: "순서는 1 이상의 숫자여야 합니다." } + ), + description: z.string().optional(), + delimiter: z.string().max(10).optional(), + remark: z.string().optional(), +}) + +type FormData = z.infer<typeof formSchema> + interface NumberTypeConfigsEditDialogProps { open: boolean onOpenChange: (open: boolean) => void data: NumberTypeConfig | null onSuccess?: () => void - existingConfigs?: NumberTypeConfig[] // 기존 configs 목록 추가 + existingConfigs?: NumberTypeConfig[] selectedProjectId?: number | null } @@ -41,29 +64,35 @@ export function NumberTypeConfigsEditDialog({ onOpenChange, data, onSuccess, - existingConfigs = [], // 기본값 추가 + existingConfigs = [], selectedProjectId, }: NumberTypeConfigsEditDialogProps) { const [isLoading, setIsLoading] = React.useState(false) const [codeGroups, setCodeGroups] = React.useState<{ id: number; description: string }[]>([]) - const [formData, setFormData] = React.useState({ - codeGroupId: "", - sdq: "", - description: "", - remark: "" + + const form = useForm<FormData>({ + resolver: zodResolver(formSchema), + defaultValues: { + codeGroupId: "", + sdq: "", + description: "", + delimiter: "", + remark: "", + }, }) // 데이터가 변경될 때 폼 초기화 React.useEffect(() => { if (data) { - setFormData({ - codeGroupId: data.codeGroupId?.toString() || "", // null 체크 추가 + form.reset({ + codeGroupId: data.codeGroupId?.toString() || "", sdq: data.sdq.toString(), description: data.description || "", + delimiter: data.delimiter || "", remark: data.remark || "" }) } - }, [data]) + }, [data, form]) // Code Groups 로드 React.useEffect(() => { @@ -79,21 +108,23 @@ export function NumberTypeConfigsEditDialog({ })() }, [selectedProjectId]) - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!data || !formData.codeGroupId || !formData.sdq) { - toast.error("필수 필드를 모두 입력해주세요.") + const onSubmit = async (values: FormData) => { + if (!data) { + toast.error("데이터를 찾을 수 없습니다.") return } - const newSdq = parseInt(formData.sdq) + const newSdq = parseInt(values.sdq) // 순서 중복 검증 (현재 수정 중인 항목 제외) const existingSdq = existingConfigs.find(config => config.sdq === newSdq && config.id !== data.id ) if (existingSdq) { - toast.error(`순서 ${newSdq}번은 이미 사용 중입니다. 다른 순서를 입력해주세요.`) + form.setError("sdq", { + type: "manual", + message: `순서 ${newSdq}번은 이미 사용 중입니다. 다른 순서를 입력해주세요.` + }) return } @@ -101,11 +132,11 @@ export function NumberTypeConfigsEditDialog({ try { const result = await updateNumberTypeConfig({ id: data.id, - codeGroupId: parseInt(formData.codeGroupId), - + codeGroupId: parseInt(values.codeGroupId), sdq: newSdq, - description: formData.description || undefined, - remark: formData.remark || undefined, + description: values.description || undefined, + delimiter: values.delimiter || undefined, + remark: values.remark || undefined, }) if (result.success) { @@ -135,91 +166,127 @@ export function NumberTypeConfigsEditDialog({ <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> </DialogDescription> </DialogHeader> - <form onSubmit={handleSubmit} className="space-y-4"> - <div className="grid gap-4 py-2"> - <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="codeGroup" className="text-right"> - Code Group <span className="text-red-500">*</span> - </Label> - <div className="col-span-3"> - <Select - value={formData.codeGroupId} - onValueChange={(value) => setFormData(prev => ({ ...prev, codeGroupId: value }))} - > - <SelectTrigger> - <SelectValue placeholder="Code Group 선택" /> - </SelectTrigger> - <SelectContent> - {codeGroups.map((codeGroup) => ( - <SelectItem key={codeGroup.id} value={codeGroup.id.toString()}> - {codeGroup.description} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - </div> - <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="sdq" className="text-right"> - 순서 <span className="text-red-500">*</span> - </Label> - <div className="col-span-3"> - <Input - id="sdq" - type="number" - value={formData.sdq} - onChange={(e) => setFormData(prev => ({ ...prev, sdq: e.target.value }))} - min="1" - /> - </div> - </div> - <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="description" className="text-right"> - Description - </Label> - <div className="col-span-3"> - <Input - id="description" - value={formData.description} - onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} - placeholder="예: PROJECT NO" - /> - </div> - </div> - <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="remark" className="text-right"> - Remark - </Label> - <div className="col-span-3"> - <Textarea - id="remark" - value={formData.remark} - onChange={(e) => setFormData(prev => ({ ...prev, remark: e.target.value }))} - placeholder="비고 사항" - rows={3} - /> - </div> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <div className="grid gap-4 py-2"> + <FormField + control={form.control} + name="codeGroupId" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right"> + Code Group <span className="text-red-500">*</span> + </FormLabel> + <div className="col-span-3"> + <FormControl> + <Select + value={field.value} + onValueChange={field.onChange} + > + <SelectTrigger> + <SelectValue placeholder="Code Group 선택" /> + </SelectTrigger> + <SelectContent> + {codeGroups.map((codeGroup) => ( + <SelectItem key={codeGroup.id} value={codeGroup.id.toString()}> + {codeGroup.description} + </SelectItem> + ))} + </SelectContent> + </Select> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="sdq" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right"> + 순서 <span className="text-red-500">*</span> + </FormLabel> + <div className="col-span-3"> + <FormControl> + <Input {...field} type="number" min="1" /> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right">Description</FormLabel> + <div className="col-span-3"> + <FormControl> + <Input {...field} placeholder="예: PROJECT NO" /> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="delimiter" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right">Delimiter</FormLabel> + <div className="col-span-3"> + <FormControl> + <Input {...field} placeholder="예: -, _, /" maxLength={10} /> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="remark" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right">Remark</FormLabel> + <div className="col-span-3"> + <FormControl> + <Textarea {...field} placeholder="비고 사항" rows={3} /> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> </div> - </div> - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => onOpenChange(false)} - disabled={isLoading} - > - 취소 - </Button> - <Button - type="submit" - disabled={isLoading} - > - {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - {isLoading ? "수정 중..." : "수정"} - </Button> - </DialogFooter> - </form> + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + type="submit" + disabled={isLoading} + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {isLoading ? "수정 중..." : "수정"} + </Button> + </DialogFooter> + </form> + </Form> </DialogContent> </Dialog> ) -}
\ No newline at end of file +}
\ No newline at end of file diff --git a/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx index 572d05cd..243dff73 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx @@ -2,6 +2,9 @@ import * as React from "react" import { Plus, Loader2 } from "lucide-react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" import { Button } from "@/components/ui/button" import { Dialog, @@ -13,6 +16,14 @@ import { DialogTrigger, } from "@/components/ui/dialog" import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Select, SelectContent, SelectItem, @@ -20,7 +31,6 @@ import { SelectValue, } from "@/components/ui/select" import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { toast } from "sonner" @@ -28,6 +38,15 @@ import { createNumberTypeConfig, getActiveCodeGroups } from "@/lib/docu-list-rul import { DeleteNumberTypeConfigsDialog } from "@/lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog" import { NumberTypeConfig } from "@/lib/docu-list-rule/types" +const formSchema = z.object({ + codeGroupId: z.string().min(1, "Code Group을 선택해주세요."), + description: z.string().optional(), + delimiter: z.string().max(10).optional(), + remark: z.string().optional(), +}) + +type FormData = z.infer<typeof formSchema> + interface NumberTypeConfigsToolbarActionsProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any table: any @@ -46,15 +65,23 @@ export function NumberTypeConfigsToolbarActions({ }: NumberTypeConfigsToolbarActionsProps) { const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) - const [formData, setFormData] = React.useState({ codeGroupId: "", description: "", remark: "" }) const [codeGroups, setCodeGroups] = React.useState<{ id: number; description: string }[]>([]) const [allOptions, setAllOptions] = React.useState<{ id: string; name: string }[]>([]) + const form = useForm<FormData>({ + resolver: zodResolver(formSchema), + defaultValues: { + codeGroupId: "", + description: "", + delimiter: "", + remark: "", + }, + }) + const loadCodeGroups = React.useCallback(async () => { try { const result = await getActiveCodeGroups(selectedProjectId || undefined) if (result.success && result.data) { - // 이미 추가된 Code Group들을 제외하고 필터링 const usedCodeGroupIds = configsData.map(config => config.codeGroupId) const availableCodeGroups = result.data.filter(codeGroup => @@ -86,17 +113,23 @@ export function NumberTypeConfigsToolbarActions({ combineOptions() }, [combineOptions]) - // 다이얼로그가 열릴 때마다 Code Groups 다시 로드 + // 다이얼로그가 열릴 때마다 Code Groups 다시 로드 및 폼 리셋 React.useEffect(() => { if (isAddDialogOpen) { loadCodeGroups() + form.reset() } - }, [isAddDialogOpen, loadCodeGroups, configsData]) + }, [isAddDialogOpen, loadCodeGroups, configsData, form]) - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!selectedNumberType || !formData.codeGroupId) { - toast.error("필수 필드를 모두 입력해주세요.") + const getNextSdq = () => { + if (configsData.length === 0) return 1 + const maxSdq = Math.max(...configsData.map(config => config.sdq)) + return maxSdq + 1 + } + + const onSubmit = async (values: FormData) => { + if (!selectedNumberType) { + toast.error("Number Type을 선택해주세요.") return } @@ -105,21 +138,21 @@ export function NumberTypeConfigsToolbarActions({ try { // Code Group ID 추출 - const codeGroupId = parseInt(formData.codeGroupId.replace('cg_', '')) + const codeGroupId = parseInt(values.codeGroupId.replace('cg_', '')) const result = await createNumberTypeConfig({ documentNumberTypeId: selectedNumberType, codeGroupId: codeGroupId, - sdq: sdq, - description: formData.description || undefined, - remark: formData.remark || undefined, + description: values.description || undefined, + delimiter: values.delimiter || undefined, + remark: values.remark || undefined, }) if (result.success) { toast.success("Number Type Config가 성공적으로 추가되었습니다.") setIsAddDialogOpen(false) - setFormData({ codeGroupId: "", description: "", remark: "" }) + form.reset() onSuccess?.() } else { toast.error(result.error || "추가에 실패했습니다.") @@ -132,12 +165,6 @@ export function NumberTypeConfigsToolbarActions({ } } - const getNextSdq = () => { - if (configsData.length === 0) return 1 - const maxSdq = Math.max(...configsData.map(config => config.sdq)) - return maxSdq + 1 - } - return ( <div className="flex items-center gap-2"> {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} @@ -170,84 +197,116 @@ export function NumberTypeConfigsToolbarActions({ <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> </DialogDescription> </DialogHeader> - <form onSubmit={handleSubmit} className="space-y-4"> - <div className="grid gap-4 py-2"> - <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="codeGroup" className="text-right"> - Code Group <span className="text-red-500">*</span> - </Label> - <div className="col-span-3"> - <Select - value={formData.codeGroupId} - onValueChange={(value) => setFormData(prev => ({ ...prev, codeGroupId: value }))} - > - <SelectTrigger> - <SelectValue placeholder="Code Group 선택" /> - </SelectTrigger> - <SelectContent> - {allOptions.length > 0 ? ( - allOptions.map((option) => ( - <SelectItem key={option.id} value={option.id}> - {option.name} - </SelectItem> - )) - ) : ( - <div className="px-2 py-1.5 text-sm text-muted-foreground"> - 사용 가능한 옵션이 없습니다. - </div> - )} - </SelectContent> - </Select> - </div> - </div> - <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="description" className="text-right"> - Description - </Label> - <div className="col-span-3"> - <Input - id="description" - value={formData.description} - onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} - placeholder="예: PROJECT NO" - /> - </div> - </div> - <div className="grid grid-cols-4 items-center gap-4"> - <Label htmlFor="remark" className="text-right"> - Remark - </Label> - <div className="col-span-3"> - <Textarea - id="remark" - value={formData.remark} - onChange={(e) => setFormData(prev => ({ ...prev, remark: e.target.value }))} - placeholder="비고 사항" - rows={3} - /> - </div> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <div className="grid gap-4 py-2"> + <FormField + control={form.control} + name="codeGroupId" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right"> + Code Group <span className="text-red-500">*</span> + </FormLabel> + <div className="col-span-3"> + <FormControl> + <Select + value={field.value} + onValueChange={field.onChange} + > + <SelectTrigger> + <SelectValue placeholder="Code Group 선택" /> + </SelectTrigger> + <SelectContent> + {allOptions.length > 0 ? ( + allOptions.map((option) => ( + <SelectItem key={option.id} value={option.id}> + {option.name} + </SelectItem> + )) + ) : ( + <div className="px-2 py-1.5 text-sm text-muted-foreground"> + 사용 가능한 옵션이 없습니다. + </div> + )} + </SelectContent> + </Select> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right">Description</FormLabel> + <div className="col-span-3"> + <FormControl> + <Input {...field} placeholder="예: PROJECT NO" /> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="delimiter" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right">Delimiter</FormLabel> + <div className="col-span-3"> + <FormControl> + <Input {...field} placeholder="예: -, _, /" maxLength={10} /> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="remark" + render={({ field }) => ( + <FormItem className="grid grid-cols-4 items-center gap-4 space-y-0"> + <FormLabel className="text-right">Remark</FormLabel> + <div className="col-span-3"> + <FormControl> + <Textarea {...field} placeholder="비고 사항" rows={3} /> + </FormControl> + <FormMessage className="col-start-2 col-span-3" /> + </div> + </FormItem> + )} + /> </div> - </div> - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => setIsAddDialogOpen(false)} - disabled={isLoading} - > - 취소 - </Button> - <Button - type="submit" - disabled={isLoading} - > - {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - {isLoading ? "추가 중..." : "추가"} - </Button> - </DialogFooter> - </form> + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setIsAddDialogOpen(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + type="submit" + disabled={isLoading} + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {isLoading ? "추가 중..." : "추가"} + </Button> + </DialogFooter> + </form> + </Form> </DialogContent> </Dialog> </div> ) -}
\ No newline at end of file +}
\ No newline at end of file diff --git a/lib/docu-list-rule/types.ts b/lib/docu-list-rule/types.ts index ef3f90d3..2baa72a5 100644 --- a/lib/docu-list-rule/types.ts +++ b/lib/docu-list-rule/types.ts @@ -6,6 +6,7 @@ export interface NumberTypeConfig { codeGroupId: number | null sdq: number description: string | null + delimiter:string | null remark: string | null isActive: boolean | null createdAt: Date |
