summaryrefslogtreecommitdiff
path: root/lib/compliance
diff options
context:
space:
mode:
Diffstat (limited to 'lib/compliance')
-rw-r--r--lib/compliance/questions/compliance-question-create-dialog.tsx173
-rw-r--r--lib/compliance/questions/compliance-question-edit-sheet.tsx230
-rw-r--r--lib/compliance/services.ts35
3 files changed, 257 insertions, 181 deletions
diff --git a/lib/compliance/questions/compliance-question-create-dialog.tsx b/lib/compliance/questions/compliance-question-create-dialog.tsx
index c0e050ab..b05c2e0d 100644
--- a/lib/compliance/questions/compliance-question-create-dialog.tsx
+++ b/lib/compliance/questions/compliance-question-create-dialog.tsx
@@ -45,9 +45,16 @@ const questionSchema = z.object({
questionText: z.string().min(1, "질문 내용을 입력하세요"),
questionType: z.string().min(1, "질문 유형을 선택하세요"),
isRequired: z.boolean(),
+ isConditional: z.boolean(),
hasDetailText: z.boolean(),
hasFileUpload: z.boolean(),
conditionalValue: z.string().optional(),
+}).refine((data) => {
+ // 필수 질문이거나 조건부 질문이어야 함
+ return data.isRequired || data.isConditional;
+}, {
+ message: "필수 질문 또는 조건부 질문 중 하나는 선택해야 합니다.",
+ path: ["isRequired", "isConditional"]
});
type QuestionFormData = z.infer<typeof questionSchema>;
@@ -72,6 +79,7 @@ export function ComplianceQuestionCreateDialog({
questionText: "",
questionType: "",
isRequired: false,
+ isConditional: false,
hasDetailText: false,
hasFileUpload: false,
conditionalValue: "",
@@ -132,10 +140,16 @@ export function ComplianceQuestionCreateDialog({
// 새로운 질문의 displayOrder는 기존 질문 개수 + 1
const currentQuestionsCount = await getComplianceQuestionsCount(templateId);
+ // 디버깅을 위한 로그
+ console.log("Form data:", data);
+ console.log("isConditional:", form.watch("isConditional"));
+ console.log("parentQuestionId:", parentQuestionId);
+ console.log("Final parentQuestionId:", form.watch("isConditional") && parentQuestionId ? Number(parentQuestionId) : null);
+
const newQuestion = await createComplianceQuestion({
templateId,
...data,
- parentQuestionId: data.isConditional && parentQuestionId ? Number(parentQuestionId) : null,
+ parentQuestionId: form.watch("isConditional") && parentQuestionId ? Number(parentQuestionId) : null,
displayOrder: currentQuestionsCount + 1,
});
@@ -204,9 +218,13 @@ export function ComplianceQuestionCreateDialog({
템플릿에 새로운 질문을 추가합니다.
</DialogDescription>
</DialogHeader>
+
+
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+
+
<FormField
control={form.control}
name="questionNumber"
@@ -221,6 +239,58 @@ export function ComplianceQuestionCreateDialog({
)}
/>
+ {/* 필수 질문과 조건부 질문 체크박스 */}
+ <div className="grid grid-cols-2 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="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>
+ )}
+ />
+ </div>
+
+ {/* 유효성 검사 에러 메시지 */}
+ {(form.formState.errors.isRequired || form.formState.errors.isConditional) && (
+ <div className="text-sm text-destructive">
+ {form.formState.errors.isRequired?.message || form.formState.errors.isConditional?.message || "필수 질문 또는 조건부 질문 중 하나는 선택해야 합니다."}
+ </div>
+ )}
+
<FormField
control={form.control}
name="questionText"
@@ -231,6 +301,7 @@ export function ComplianceQuestionCreateDialog({
<Textarea
placeholder="질문 내용을 입력하세요"
className="min-h-[100px]"
+ disabled={!form.watch("isRequired") && !form.watch("isConditional")}
{...field}
/>
</FormControl>
@@ -245,7 +316,7 @@ export function ComplianceQuestionCreateDialog({
render={({ field }) => (
<FormItem>
<FormLabel>질문 유형</FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <Select onValueChange={field.onChange} defaultValue={field.value} disabled={!form.watch("isRequired") && !form.watch("isConditional")}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="질문 유형을 선택하세요" />
@@ -266,13 +337,14 @@ export function ComplianceQuestionCreateDialog({
{/* 옵션 관리 (선택형 질문일 때만) */}
{isSelectionType && (
- <div className="space-y-3">
+ <div className={`space-y-3 ${(!form.watch("isRequired") && !form.watch("isConditional")) ? "opacity-50 pointer-events-none" : ""}`}>
<div className="flex items-center justify-between">
<div className="text-sm font-medium">옵션 관리</div>
<Button
type="button"
variant="outline"
size="sm"
+ disabled={!form.watch("isRequired") && !form.watch("isConditional")}
onClick={() => {
setNewOptionValue("");
setNewOptionText("");
@@ -386,95 +458,7 @@ export function ComplianceQuestionCreateDialog({
</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") && (
@@ -550,7 +534,10 @@ export function ComplianceQuestionCreateDialog({
>
취소
</Button>
- <Button type="submit" disabled={isLoading}>
+ <Button
+ type="submit"
+ disabled={isLoading || (!form.watch("isRequired") && !form.watch("isConditional"))}
+ >
{isLoading ? "추가 중..." : "질문 추가"}
</Button>
</DialogFooter>
diff --git a/lib/compliance/questions/compliance-question-edit-sheet.tsx b/lib/compliance/questions/compliance-question-edit-sheet.tsx
index 064cafc1..e5fc6242 100644
--- a/lib/compliance/questions/compliance-question-edit-sheet.tsx
+++ b/lib/compliance/questions/compliance-question-edit-sheet.tsx
@@ -15,6 +15,14 @@ import {
SheetTrigger,
} from "@/components/ui/sheet";
import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
Form,
FormControl,
FormDescription,
@@ -81,6 +89,8 @@ export function ComplianceQuestionEditSheet({
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 [showOptionsDeleteDialog, setShowOptionsDeleteDialog] = React.useState(false);
+ const [pendingQuestionTypeChange, setPendingQuestionTypeChange] = React.useState<string | null>(null);
const form = useForm<QuestionFormData>({
resolver: zodResolver(questionSchema),
@@ -156,6 +166,12 @@ export function ComplianceQuestionEditSheet({
try {
setIsLoading(true);
+ // 디버깅을 위한 로그
+ console.log("Edit form data:", data);
+ console.log("Current isConditional:", data.isConditional);
+ console.log("Current parentQuestionId:", parentQuestionId);
+ console.log("Current conditionalValue:", data.conditionalValue);
+
// 조건부 질문 관련 데이터 처리
const updateData = {
...data,
@@ -166,6 +182,8 @@ export function ComplianceQuestionEditSheet({
// isConditional과 parentQuestionId는 제거 (스키마에 없음)
delete (updateData as any).isConditional;
+ console.log("Final updateData:", updateData);
+
await updateComplianceQuestion(question.id, updateData);
toast.success("질문이 성공적으로 수정되었습니다.");
@@ -226,6 +244,51 @@ export function ComplianceQuestionEditSheet({
)}
/>
+ {/* 필수 질문과 조건부 질문 체크박스 */}
+ <div className="grid grid-cols-2 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="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>
+ )}
+ />
+ </div>
+
<FormField
control={form.control}
name="questionText"
@@ -250,7 +313,22 @@ export function ComplianceQuestionEditSheet({
render={({ field }) => (
<FormItem>
<FormLabel>질문 유형</FormLabel>
- <Select onValueChange={field.onChange} defaultValue={(field.value || "").toUpperCase()}>
+ <Select
+ onValueChange={(newValue) => {
+ const currentType = field.value;
+ const isCurrentSelectionType = [QUESTION_TYPES.RADIO, QUESTION_TYPES.CHECKBOX, QUESTION_TYPES.DROPDOWN].includes((currentType || "").toUpperCase() as any);
+ const isNewSelectionType = [QUESTION_TYPES.RADIO, QUESTION_TYPES.CHECKBOX, QUESTION_TYPES.DROPDOWN].includes(newValue.toUpperCase() as any);
+
+ // 선택형에서 비선택형으로 변경하고 기존 옵션이 있는 경우
+ if (isCurrentSelectionType && !isNewSelectionType && options.length > 0) {
+ setPendingQuestionTypeChange(newValue);
+ setShowOptionsDeleteDialog(true);
+ } else {
+ field.onChange(newValue);
+ }
+ }}
+ defaultValue={(field.value || "").toUpperCase()}
+ >
<FormControl>
<SelectTrigger>
<SelectValue placeholder="질문 유형을 선택하세요" />
@@ -401,93 +479,6 @@ export function ComplianceQuestionEditSheet({
</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">
@@ -567,6 +558,69 @@ export function ComplianceQuestionEditSheet({
</form>
</Form>
</SheetContent>
+
+ {/* 옵션 삭제 확인 다이얼로그 */}
+ <Dialog open={showOptionsDeleteDialog} onOpenChange={setShowOptionsDeleteDialog}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>옵션 삭제 확인</DialogTitle>
+ <DialogDescription>
+ 질문 유형을 변경하면 기존 옵션들이 모두 삭제됩니다. 계속하시겠습니까?
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="py-4">
+ <div className="bg-muted p-4 rounded-lg">
+ <h4 className="font-medium mb-2">삭제될 옵션들:</h4>
+ {options.map((option, index) => (
+ <div key={option.id} className="text-sm text-muted-foreground mb-1">
+ <strong>옵션 {index + 1}:</strong> {option.optionValue} - {option.optionText}
+ </div>
+ ))}
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => {
+ setShowOptionsDeleteDialog(false);
+ setPendingQuestionTypeChange(null);
+ }}
+ >
+ 취소
+ </Button>
+ <Button
+ type="button"
+ variant="destructive"
+ onClick={async () => {
+ try {
+ // 옵션들을 삭제
+ for (const option of options) {
+ await deleteComplianceQuestionOption(option.id);
+ }
+ setOptions([]);
+
+ // 질문 유형 변경
+ if (pendingQuestionTypeChange) {
+ form.setValue("questionType", pendingQuestionTypeChange);
+ }
+
+ toast.success("옵션이 삭제되고 질문 유형이 변경되었습니다.");
+ setShowOptionsDeleteDialog(false);
+ setPendingQuestionTypeChange(null);
+ } catch (error) {
+ console.error("Error deleting options:", error);
+ toast.error("옵션 삭제 중 오류가 발생했습니다.");
+ }
+ }}
+ >
+ 옵션 삭제 및 유형 변경
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
</Sheet>
);
}
diff --git a/lib/compliance/services.ts b/lib/compliance/services.ts
index 03fae071..de67598b 100644
--- a/lib/compliance/services.ts
+++ b/lib/compliance/services.ts
@@ -174,6 +174,12 @@ export async function getComplianceSurveyTemplates(input: {
// 특정 템플릿 조회
export async function getComplianceSurveyTemplate(templateId: number) {
try {
+ // templateId 유효성 검사 추가
+ if (!templateId || isNaN(templateId) || templateId <= 0) {
+ console.error(`Invalid templateId: ${templateId}`);
+ return null;
+ }
+
const [template] = await db
.select()
.from(complianceSurveyTemplates)
@@ -214,6 +220,12 @@ export async function updateComplianceSurveyTemplate(templateId: number, data: {
// 템플릿의 질문들 조회
export async function getComplianceQuestions(templateId: number) {
try {
+ // templateId 유효성 검사 추가
+ if (!templateId || isNaN(templateId) || templateId <= 0) {
+ console.error(`Invalid templateId: ${templateId}`);
+ return [];
+ }
+
const questions = await db
.select()
.from(complianceQuestions)
@@ -481,6 +493,12 @@ export async function deleteComplianceQuestionOption(optionId: number) {
// 템플릿의 응답들 조회
export async function getComplianceResponses(templateId: number) {
try {
+ // templateId 유효성 검사 추가
+ if (!templateId || isNaN(templateId) || templateId <= 0) {
+ console.error(`Invalid templateId: ${templateId}`);
+ return [];
+ }
+
const responses = await db
.select()
.from(complianceResponses)
@@ -497,6 +515,12 @@ export async function getComplianceResponses(templateId: number) {
// 템플릿의 응답들과 답변들을 함께 조회 (페이지네이션 포함)
export async function getComplianceResponsesWithPagination(templateId: number) {
try {
+ // templateId 유효성 검사 추가
+ if (!templateId || isNaN(templateId) || templateId <= 0) {
+ console.error(`Invalid templateId: ${templateId}`);
+ return { data: [], pageCount: 0 };
+ }
+
const responses = await db
.select({
id: complianceResponses.id,
@@ -544,6 +568,17 @@ export async function getComplianceResponsesWithPagination(templateId: number) {
// 템플릿별 응답 통계 조회
export async function getComplianceResponseStats(templateId: number) {
try {
+ // templateId 유효성 검사 추가
+ if (!templateId || isNaN(templateId) || templateId <= 0) {
+ console.error(`Invalid templateId: ${templateId}`);
+ return {
+ inProgress: 0,
+ completed: 0,
+ reviewed: 0,
+ total: 0
+ };
+ }
+
const responses = await db
.select({
status: complianceResponses.status,