feat: 설문 제출 기능
This commit is contained in:
@@ -92,18 +92,23 @@ export const articleSchema = z.object({
|
||||
export const userSurveyFormSchema = z.object({
|
||||
srvySn: z.number(),
|
||||
rspdnts: z.object({
|
||||
rspdntNm: z.string(),
|
||||
gndrCd: z.string(),
|
||||
crTypeCd: z.string(),
|
||||
brdt: z.string(),
|
||||
rspdntNm: z.string().min(1),
|
||||
gndrCd: z.string().min(1),
|
||||
crTypeCd: z.string().min(1),
|
||||
brdt: z.string().min(8),
|
||||
}),
|
||||
rspnsRslts: z.array(
|
||||
z.object({
|
||||
srvyQitemNo: z.number(),
|
||||
qstnTypeCd: z.string(),
|
||||
ansCn: z.string(),
|
||||
etcAnsCn: z.string(),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
srvyQitemNo: z.number(),
|
||||
qstnTypeCd: z.string(),
|
||||
ansCn: z.string(),
|
||||
etcAnsCn: z.string(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.etcAnsCn.trim().length > 0 || data.ansCn.trim().length > 0,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useForm, type Control } from "react-hook-form";
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
useForm,
|
||||
useWatch,
|
||||
type Control,
|
||||
type UseFormReturn,
|
||||
} from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
cn,
|
||||
Button,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
@@ -16,18 +23,19 @@ import {
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { SurveyApi } from "../api/survey-service";
|
||||
import {
|
||||
userSurveyFormSchema,
|
||||
type Article,
|
||||
type UserSurveyFormData,
|
||||
} from "../model/survey-interface";
|
||||
import dayjs from "dayjs";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
|
||||
function UserSurveyArticle({ srvySn }: { srvySn: string }) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [brdt, setBrdt] = useState("");
|
||||
|
||||
const { data: userSurvey } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, srvySn],
|
||||
queryFn: () => SurveyApi.getUserSurvey(parseInt(srvySn)),
|
||||
@@ -60,8 +68,20 @@ function UserSurveyArticle({ srvySn }: { srvySn: string }) {
|
||||
},
|
||||
});
|
||||
|
||||
const { success } = userSurveyFormSchema.safeParse(form.getValues());
|
||||
|
||||
const onSubmit = (data: UserSurveyFormData) => {
|
||||
saveUserSurvey(data);
|
||||
const updatedData = {
|
||||
...data,
|
||||
rspnsRslts: data.rspnsRslts.map((field) => {
|
||||
if (field.qstnTypeCd === "RADIO" && field.ansCn === "0") {
|
||||
return { ...field, ansCn: "" };
|
||||
}
|
||||
return field;
|
||||
}),
|
||||
};
|
||||
|
||||
saveUserSurvey(updatedData);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -74,7 +94,9 @@ function UserSurveyArticle({ srvySn }: { srvySn: string }) {
|
||||
{userSurvey?.srvyTtl}
|
||||
</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit">제출</Button>
|
||||
<Button type="submit" disabled={!success}>
|
||||
제출
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
@@ -121,18 +143,17 @@ function UserSurveyArticle({ srvySn }: { srvySn: string }) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
mode="single"
|
||||
className="w-full justify-start px-4"
|
||||
selected={
|
||||
field.value
|
||||
? dayjs(field.value).toDate()
|
||||
: undefined
|
||||
}
|
||||
onSelect={(date) => {
|
||||
field.onChange(dayjs(date).format("YYYYMMDD"));
|
||||
}}
|
||||
<Input
|
||||
{...field}
|
||||
type="date"
|
||||
className="w-full !text-[1.3rem]"
|
||||
value={brdt}
|
||||
onChange={(e) => {
|
||||
setBrdt(e.target.value);
|
||||
field.onChange(
|
||||
dayjs(e.target.value).format("YYYYMMDD"),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@@ -191,7 +212,7 @@ function UserSurveyArticle({ srvySn }: { srvySn: string }) {
|
||||
)}
|
||||
{item.qstnTypeCd === "CHECK" && (
|
||||
<CheckArticle
|
||||
control={form.control}
|
||||
form={form}
|
||||
qstnCn={item.qstnCn}
|
||||
index={qIndex}
|
||||
maxChcCnt={item.maxChcCnt}
|
||||
@@ -240,19 +261,41 @@ function RadioArticle({
|
||||
<div className="flex flex-col gap-3">
|
||||
{articles.map((article, j) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
className="flex flex-col gap-3"
|
||||
key={article.srvyArtclNo}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={article.srvyArtclNo?.toString() ?? ""}
|
||||
id={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
className="!text-[1.4rem]"
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={article.srvyArtclNo}
|
||||
>
|
||||
{article.artclCn}
|
||||
</Label>
|
||||
<RadioGroupItem
|
||||
value={article.srvyArtclNo?.toString() ?? ""}
|
||||
id={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
className="!text-[1.4rem]"
|
||||
>
|
||||
{article.artclCn}
|
||||
</Label>
|
||||
</div>
|
||||
{article.srvyArtclNo === 0 && (
|
||||
<FormField
|
||||
control={control}
|
||||
name={`rspnsRslts.${i}.etcAnsCn`}
|
||||
render={({ field: etcField }) => (
|
||||
<Input
|
||||
{...etcField}
|
||||
value={etcField.value ?? ""}
|
||||
className={cn(
|
||||
"hidden w-[32rem] !text-[1.3rem]",
|
||||
field.value === "0" && "block",
|
||||
)}
|
||||
placeholder="기타 내용을 입력해주세요"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -266,13 +309,13 @@ function RadioArticle({
|
||||
}
|
||||
|
||||
function CheckArticle({
|
||||
control,
|
||||
form,
|
||||
qstnCn,
|
||||
index: i,
|
||||
maxChcCnt,
|
||||
articles,
|
||||
}: {
|
||||
control: Control<UserSurveyFormData>;
|
||||
form: UseFormReturn<UserSurveyFormData>;
|
||||
index: number;
|
||||
qstnCn: string;
|
||||
maxChcCnt: number;
|
||||
@@ -280,50 +323,74 @@ function CheckArticle({
|
||||
}) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const selectedValues = useWatch({
|
||||
control: form.control,
|
||||
name: `rspnsRslts.${i}.ansCn`,
|
||||
});
|
||||
|
||||
const filteredValues =
|
||||
selectedValues?.split(",").filter((v) => v.trim() !== "") || [];
|
||||
|
||||
const [isEtcChecked, setIsEtcChecked] = useState(false);
|
||||
|
||||
const handleCheckedChange = (value: string) => (checked: boolean) => {
|
||||
const values = filteredValues;
|
||||
|
||||
if (checked) {
|
||||
values.push(value);
|
||||
} else {
|
||||
const valueIndex = values.indexOf(value);
|
||||
if (valueIndex !== -1) values.splice(valueIndex, 1);
|
||||
}
|
||||
|
||||
form.setValue(`rspnsRslts.${i}.ansCn`, values.join(","));
|
||||
};
|
||||
|
||||
const validMaxCheckedCount = () => {
|
||||
let checkedCount = 0;
|
||||
if (isEtcChecked) checkedCount += 1;
|
||||
checkedCount += filteredValues.length;
|
||||
return checkedCount < maxChcCnt;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-4 text-2xl font-bold text-foreground">
|
||||
{qstnCn} (최대 {maxChcCnt}개 선택)
|
||||
</p>
|
||||
<FormField
|
||||
control={control}
|
||||
control={form.control}
|
||||
name={`rspnsRslts.${i}.ansCn`}
|
||||
render={({ field }) => {
|
||||
const selectedValues = field.value?.split(",") || [];
|
||||
const handleCheckedChange = (value: string) => (checked: boolean) => {
|
||||
const values = [...selectedValues];
|
||||
|
||||
if (checked) {
|
||||
if (values.length >= maxChcCnt) {
|
||||
alert("최대 선택 개수를 초과했습니다.");
|
||||
return;
|
||||
}
|
||||
values.push(value);
|
||||
} else {
|
||||
const valueIndex = values.indexOf(value);
|
||||
if (valueIndex !== -1) {
|
||||
values.splice(valueIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
field.onChange(values.filter((v) => v.trim() !== "").join(","));
|
||||
};
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="flex flex-col gap-3">
|
||||
{articles.map((article, j) => {
|
||||
const value = article.srvyArtclNo?.toString() ?? "";
|
||||
return (
|
||||
<div
|
||||
className="flex h-[1.8rem] items-center gap-2"
|
||||
key={article.srvyArtclNo}
|
||||
>
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="flex flex-col gap-3">
|
||||
{articles.map((article, j) => {
|
||||
const srvyArtclNo = article.srvyArtclNo?.toString() ?? "";
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-3"
|
||||
key={article.srvyArtclNo}
|
||||
>
|
||||
<div className="flex h-[1.8rem] items-center gap-2">
|
||||
<Checkbox
|
||||
id={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
checked={selectedValues.includes(value)}
|
||||
onCheckedChange={handleCheckedChange(value)}
|
||||
checked={
|
||||
article.srvyArtclNo === 0
|
||||
? isEtcChecked
|
||||
: selectedValues?.split(",").includes(srvyArtclNo)
|
||||
}
|
||||
onCheckedChange={(checked: boolean) => {
|
||||
if (checked && !validMaxCheckedCount()) {
|
||||
alert("최대 선택 개수를 초과했습니다.");
|
||||
return;
|
||||
}
|
||||
if (article.srvyArtclNo === 0) {
|
||||
setIsEtcChecked(checked);
|
||||
} else {
|
||||
handleCheckedChange(srvyArtclNo)(checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
@@ -332,13 +399,30 @@ function CheckArticle({
|
||||
{article.artclCn}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
{article.srvyArtclNo === 0 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`rspnsRslts.${i}.etcAnsCn`}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className={cn(
|
||||
"hidden w-[32rem] !text-[1.3rem]",
|
||||
isEtcChecked && "block",
|
||||
)}
|
||||
placeholder="기타 내용을 입력해주세요"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user