From fefca6304eefea94f41057f9f934b0e19ceb54bb Mon Sep 17 00:00:00 2001
From: 0-Zz-ang <s1998319@gmail.com>
Date: Fri, 22 Aug 2025 13:47:37 +0900
Subject: (박서영)Compliance 설문/응답 리스트 생성
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../questions/compliance-question-edit-sheet.tsx   | 572 +++++++++++++++++++++
 1 file changed, 572 insertions(+)
 create mode 100644 lib/compliance/questions/compliance-question-edit-sheet.tsx

(limited to 'lib/compliance/questions/compliance-question-edit-sheet.tsx')

diff --git a/lib/compliance/questions/compliance-question-edit-sheet.tsx b/lib/compliance/questions/compliance-question-edit-sheet.tsx
new file mode 100644
index 00000000..064cafc1
--- /dev/null
+++ b/lib/compliance/questions/compliance-question-edit-sheet.tsx
@@ -0,0 +1,572 @@
+"use client";
+
+import * as React from "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 {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from "@/components/ui/sheet";
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import { Textarea } from "@/components/ui/textarea";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import { Edit, Plus, Trash2 } from "lucide-react";
+import { 
+  updateComplianceQuestion,
+  getComplianceQuestionOptions,
+  createComplianceQuestionOption,
+  deleteComplianceQuestionOption,
+  getSelectableParentQuestions,
+} from "@/lib/compliance/services";
+import { QUESTION_TYPES } from "@/db/schema/compliance";
+import { toast } from "sonner";
+import { useRouter } from "next/navigation";
+import { complianceQuestions } from "@/db/schema/compliance";
+
+const questionSchema = z.object({
+  questionNumber: z.string().min(1, "질문 번호를 입력하세요"),
+  questionText: z.string().min(1, "질문 내용을 입력하세요"),
+  questionType: z.string().min(1, "질문 유형을 선택하세요"),
+  isRequired: z.boolean(),
+  hasDetailText: z.boolean(),
+  hasFileUpload: z.boolean(),
+  isConditional: z.boolean(),
+  parentQuestionId: z.number().optional(),
+  conditionalValue: z.string().optional(),
+});
+
+type QuestionFormData = z.infer<typeof questionSchema>;
+
+interface ComplianceQuestionEditDialogProps {
+  question: typeof complianceQuestions.$inferSelect;
+  onSuccess?: () => void;
+}
+
+export function ComplianceQuestionEditSheet({ 
+  question, 
+  onSuccess 
+}: ComplianceQuestionEditDialogProps) {
+  const [open, setOpen] = React.useState(false);
+  const [isLoading, setIsLoading] = React.useState(false);
+  const router = useRouter();
+  const [options, setOptions] = React.useState<Array<{ id: number; optionValue: string; optionText: string; allowsOtherInput: boolean; displayOrder: number }>>([]);
+  const [newOptionValue, setNewOptionValue] = React.useState("");
+  const [newOptionText, setNewOptionText] = React.useState("");
+  const [newOptionOther, setNewOptionOther] = React.useState(false);
+  const [parentOptions, setParentOptions] = React.useState<Array<{ id: number; optionValue: string; optionText: string }>>([]);
+  const [selectableParents, setSelectableParents] = React.useState<Array<{ id: number; questionNumber: string; questionText: string; questionType: string }>>([]);
+  const [parentQuestionId, setParentQuestionId] = React.useState<number | null>(question.parentQuestionId || null);
+  const [showOptionForm, setShowOptionForm] = React.useState(false);
+
+  const form = useForm<QuestionFormData>({
+    resolver: zodResolver(questionSchema),
+    defaultValues: {
+      questionNumber: question.questionNumber,
+      questionText: question.questionText,
+      questionType: question.questionType,
+      isRequired: question.isRequired,
+      hasDetailText: question.hasDetailText,
+      hasFileUpload: question.hasFileUpload,
+      isConditional: !!question.parentQuestionId,
+      parentQuestionId: question.parentQuestionId || undefined,
+      conditionalValue: question.conditionalValue || "",
+    },
+  });
+
+  const isSelectionType = React.useMemo(() => {
+    return [QUESTION_TYPES.RADIO, QUESTION_TYPES.CHECKBOX, QUESTION_TYPES.DROPDOWN].includes((form.getValues("questionType") || "").toUpperCase() as any);
+  }, [form]);
+
+  const loadOptions = React.useCallback(async () => {
+    if (!isSelectionType) return;
+    try {
+      const data = await getComplianceQuestionOptions(question.id);
+      setOptions(data);
+    } catch (e) {
+      console.error("loadOptions error", e);
+    }
+  }, [isSelectionType, question.id]);
+
+  React.useEffect(() => {
+    if (open) {
+      loadOptions();
+    }
+  }, [open, loadOptions]);
+
+  // 선택 가능한 부모 질문들 로드 (조건부 질문용)
+  React.useEffect(() => {
+    const loadSelectableParents = async () => {
+      if (!open) return;
+      try {
+        // 현재 질문과 같은 템플릿의 선택형 질문들만 가져오기
+        const data = await getSelectableParentQuestions(question.templateId, question.id);
+        setSelectableParents(data);
+      } catch (e) {
+        console.error("loadSelectableParents error", e);
+        setSelectableParents([]);
+      }
+    };
+    loadSelectableParents();
+  }, [open, question.templateId, question.id]);
+
+  // 부모 질문의 옵션 로드 (조건부 질문용)
+  React.useEffect(() => {
+    const loadParentOptions = async () => {
+      if (!open) return;
+      if (!parentQuestionId) {
+        setParentOptions([]);
+        return;
+      }
+      try {
+        const data = await getComplianceQuestionOptions(parentQuestionId);
+        setParentOptions(data.map((o: any) => ({ id: o.id, optionValue: o.optionValue, optionText: o.optionText })));
+      } catch (e) {
+        console.error("loadParentOptions error", e);
+        setParentOptions([]);
+      }
+    };
+    loadParentOptions();
+  }, [open, parentQuestionId]);
+
+  const onSubmit = async (data: QuestionFormData) => {
+    try {
+      setIsLoading(true);
+      
+      // 조건부 질문 관련 데이터 처리
+      const updateData = {
+        ...data,
+        parentQuestionId: data.isConditional ? parentQuestionId : null,
+        conditionalValue: data.isConditional ? data.conditionalValue : undefined,
+      };
+      
+      // isConditional과 parentQuestionId는 제거 (스키마에 없음)
+      delete (updateData as any).isConditional;
+      
+      await updateComplianceQuestion(question.id, updateData);
+
+      toast.success("질문이 성공적으로 수정되었습니다.");
+      setOpen(false);
+      
+      // 페이지 새로고침
+      router.refresh();
+      
+      if (onSuccess) {
+        onSuccess();
+      }
+    } catch (error) {
+      console.error("Error updating question:", error);
+      
+      // 중복 질문번호 오류 처리
+      if (error instanceof Error && error.message === "DUPLICATE_QUESTION_NUMBER") {
+        form.setError("questionNumber", {
+          type: "manual",
+          message: "이미 사용 중인 질문번호입니다."
+        });
+        toast.error("이미 사용 중인 질문번호입니다.");
+      } else {
+        toast.error("질문 수정 중 오류가 발생했습니다.");
+      }
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <Sheet open={open} onOpenChange={setOpen}>
+      <SheetTrigger asChild>
+        <Button variant="ghost" size="sm">
+          <Edit className="h-4 w-4" />
+        </Button>
+      </SheetTrigger>
+      <SheetContent className="sm:max-w-[500px] overflow-y-auto">
+        <SheetHeader>
+          <SheetTitle>질문 수정</SheetTitle>
+          <SheetDescription>
+            질문 내용을 수정합니다.
+          </SheetDescription>
+        </SheetHeader>
+        
+        <Form {...form}>
+          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+            <FormField
+              control={form.control}
+              name="questionNumber"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>질문 번호</FormLabel>
+                  <FormControl>
+                    <Input placeholder="Q1" {...field} />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="questionText"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>질문 내용</FormLabel>
+                  <FormControl>
+                    <Textarea 
+                      placeholder="질문 내용을 입력하세요" 
+                      className="min-h-[100px]"
+                      {...field} 
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="questionType"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>질문 유형</FormLabel>
+                  <Select onValueChange={field.onChange} defaultValue={(field.value || "").toUpperCase()}>
+                    <FormControl>
+                      <SelectTrigger>
+                        <SelectValue placeholder="질문 유형을 선택하세요" />
+                      </SelectTrigger>
+                    </FormControl>
+                    <SelectContent>
+                      {Object.entries(QUESTION_TYPES).map(([key, value]) => (
+                        <SelectItem key={key} value={value}>
+                          {value}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            {isSelectionType && (
+              <div className="space-y-3">
+                <div className="flex items-center justify-between">
+                  <div className="text-sm font-medium">옵션 관리</div>
+                  <Button
+                    type="button"
+                    variant="outline"
+                    size="sm"
+                    onClick={() => {
+                      setNewOptionValue("");
+                      setNewOptionText("");
+                      setNewOptionOther(false);
+                      // 옵션 추가 모드 활성화
+                      setShowOptionForm(true);
+                    }}
+                  >
+                    <Plus className="h-4 w-4 mr-1" />
+                    옵션 추가
+                  </Button>
+                </div>
+
+                {/* 옵션 추가 폼 */}
+                {showOptionForm && (
+                  <div className="space-y-3 p-3 border rounded-lg bg-muted/50">
+                    <div className="grid grid-cols-2 gap-3">
+                      <div>
+                        <Input 
+                          value={newOptionValue} 
+                          onChange={(e) => setNewOptionValue(e.target.value)} 
+                          placeholder="option_value (예: YES)" 
+                        />
+                      </div>
+                      <div>
+                        <Input 
+                          value={newOptionText} 
+                          onChange={(e) => setNewOptionText(e.target.value)} 
+                          placeholder="option_text (표시 라벨)" 
+                        />
+                      </div>
+                    </div>
+                    <div className="flex items-center justify-between">
+                      <div className="flex items-center gap-2">
+                        <Checkbox 
+                          checked={newOptionOther} 
+                          onCheckedChange={(v) => setNewOptionOther(Boolean(v))} 
+                        />
+                        <span className="text-sm text-muted-foreground">기타 허용</span>
+                      </div>
+                      <div className="flex gap-2">
+                        <Button
+                          type="button"
+                          variant="outline"
+                          size="sm"
+                          onClick={async () => {
+                            if (!newOptionValue || !newOptionText) {
+                              toast.error("option_value와 option_text를 입력하세요.");
+                              return;
+                            }
+                            try {
+                              await createComplianceQuestionOption({
+                                questionId: question.id,
+                                optionValue: newOptionValue.toUpperCase(),
+                                optionText: newOptionText,
+                                allowsOtherInput: newOptionOther,
+                                displayOrder: (options?.length || 0) + 1,
+                              });
+                              setNewOptionValue("");
+                              setNewOptionText("");
+                              setNewOptionOther(false);
+                              setShowOptionForm(false);
+                              await loadOptions();
+                              toast.success("옵션이 추가되었습니다.");
+                            } catch (e) {
+                              console.error(e);
+                              toast.error("옵션 추가 중 오류가 발생했습니다.");
+                            }
+                          }}
+                        >
+                          등록
+                        </Button>
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => {
+                            setShowOptionForm(false);
+                            setNewOptionValue("");
+                            setNewOptionText("");
+                            setNewOptionOther(false);
+                          }}
+                        >
+                          취소
+                        </Button>
+                      </div>
+                    </div>
+                  </div>
+                )}
+
+                <div className="space-y-2">
+                  {options.length === 0 ? (
+                    <div className="text-xs text-muted-foreground">등록된 옵션이 없습니다.</div>
+                  ) : (
+                    options.map((opt) => (
+                      <div key={opt.id} className="flex items-center gap-3 rounded border p-2">
+                        <div className="text-xs text-muted-foreground w-10">#{opt.displayOrder}</div>
+                        <div className="text-sm font-mono">{opt.optionValue}</div>
+                        <div className="text-sm flex-1">{opt.optionText}</div>
+                        {opt.allowsOtherInput && <Badge variant="secondary">기타 허용</Badge>}
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="icon"
+                          onClick={async () => {
+                            try {
+                              await deleteComplianceQuestionOption(opt.id);
+                              await loadOptions();
+                              toast.success("옵션이 삭제되었습니다.");
+                            } catch (e) {
+                              console.error(e);
+                              toast.error("옵션 삭제 중 오류가 발생했습니다.");
+                            }
+                          }}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    ))
+                  )}
+                </div>
+              </div>
+            )}
+
+            <div className="grid grid-cols-3 gap-4">
+              <FormField
+                control={form.control}
+                name="isRequired"
+                render={({ field }) => (
+                  <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                    <FormControl>
+                      <Checkbox
+                        checked={field.value}
+                        onCheckedChange={field.onChange}
+                      />
+                    </FormControl>
+                    <div className="space-y-1 leading-none">
+                      <FormLabel>필수 질문</FormLabel>
+                      <FormDescription>
+                        응답자가 반드시 답변해야 하는 질문
+                      </FormDescription>
+                    </div>
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="hasDetailText"
+                render={({ field }) => (
+                  <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                    <FormControl>
+                      <Checkbox
+                        checked={field.value}
+                        onCheckedChange={field.onChange}
+                      />
+                    </FormControl>
+                    <div className="space-y-1 leading-none">
+                      <FormLabel>상세 설명</FormLabel>
+                      <FormDescription>
+                        추가 설명 입력 가능
+                      </FormDescription>
+                    </div>
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="hasFileUpload"
+                render={({ field }) => (
+                  <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                    <FormControl>
+                      <Checkbox
+                        checked={field.value}
+                        onCheckedChange={field.onChange}
+                      />
+                    </FormControl>
+                    <div className="space-y-1 leading-none">
+                      <FormLabel>파일 업로드</FormLabel>
+                      <FormDescription>
+                        파일 첨부 가능
+                      </FormDescription>
+                    </div>
+                  </FormItem>
+                )}
+              />
+            </div>
+
+            {/* 조건부 질문 체크박스 */}
+            <FormField
+              control={form.control}
+              name="isConditional"
+              render={({ field }) => (
+                <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                  <FormControl>
+                    <Checkbox
+                      checked={field.value}
+                      onCheckedChange={field.onChange}
+                    />
+                  </FormControl>
+                  <div className="space-y-1 leading-none">
+                    <FormLabel>조건부 질문</FormLabel>
+                    <FormDescription>
+                      특정 조건에 따라 표시되는 질문
+                    </FormDescription>
+                  </div>
+                </FormItem>
+              )}
+            />
+
+            {/* 조건부 질문일 때만 부모 질문과 조건값 표시 */}
+            {form.watch("isConditional") && (
+              <div className="space-y-2">
+                {/* 조건 질문 선택 */}
+                <div>
+                  <FormLabel>조건 질문</FormLabel>
+                  <Select onValueChange={(v) => setParentQuestionId(Number(v))} value={String(parentQuestionId || "")}>
+                    <SelectTrigger>
+                      <SelectValue placeholder="조건 기준 질문을 선택하세요">
+                        {parentQuestionId ? (
+                          <div className="truncate max-w-[300px] text-left">
+                            {selectableParents.find(p => p.id === parentQuestionId)?.questionText}
+                          </div>
+                        ) : null}
+                      </SelectValue>
+                    </SelectTrigger>
+                    <SelectContent>
+                      {selectableParents.map((p) => (
+                        <SelectItem key={p.id} value={String(p.id)}>
+                          {p.questionText}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+
+                {/* 조건값 선택 */}
+                <FormField
+                  control={form.control}
+                  name="conditionalValue"
+                  render={({ field }) => (
+                    <FormItem className="space-y-1">
+                      <FormLabel>조건값</FormLabel>
+                      {parentOptions.length > 0 ? (
+                        <>
+                          <Select onValueChange={field.onChange} defaultValue={(field.value || "").toString()}>
+                            <SelectTrigger>
+                              <SelectValue placeholder="조건값을 선택하세요" />
+                            </SelectTrigger>
+                            <SelectContent>
+                              {parentOptions.map((opt) => (
+                                <SelectItem key={opt.id} value={opt.optionValue}>
+                                  {opt.optionValue}
+                                </SelectItem>
+                              ))}
+                            </SelectContent>
+                          </Select>
+                        </>
+                      ) : (
+                        <>
+                          <FormControl>
+                            <Input placeholder="먼저 부모 질문을 선택하세요" disabled />
+                          </FormControl>
+                          <FormDescription>조건 질문을 선택하세요.</FormDescription>
+                        </>
+                      )}
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+              </div>
+            )}
+
+            <SheetFooter>
+              <Button 
+                type="button" 
+                variant="outline" 
+                onClick={() => setOpen(false)}
+                disabled={isLoading}
+              >
+                취소
+              </Button>
+              <Button type="submit" disabled={isLoading}>
+                {isLoading ? "수정 중..." : "질문 수정"}
+              </Button>
+            </SheetFooter>
+          </form>
+        </Form>
+      </SheetContent>
+    </Sheet>
+  );
+}
-- 
cgit v1.2.3

