feat: 설문 제출 기능

This commit is contained in:
2025-06-05 13:37:14 +09:00
parent 3a1053733e
commit a72ff34c19
2 changed files with 171 additions and 82 deletions
@@ -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>
);