feat: 사용자 설문 폼 작업
This commit is contained in:
@@ -14,6 +14,7 @@ import type {
|
||||
ArticleList,
|
||||
ArticleParams,
|
||||
UserSurvey,
|
||||
UserSurveyFormData,
|
||||
} from "../model/survey-interface";
|
||||
|
||||
export class SurveyApiService implements SurveyService {
|
||||
@@ -97,6 +98,10 @@ export class SurveyApiService implements SurveyService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveUserSurvey(data: UserSurveyFormData): Promise<void> {
|
||||
await client.post(`${ENDPOINT.CMS_SERVICE.SURVEY}/user`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const SurveyApi = new SurveyApiService();
|
||||
|
||||
@@ -89,6 +89,24 @@ export const articleSchema = z.object({
|
||||
mode: z.string(),
|
||||
});
|
||||
|
||||
export const userSurveyFormSchema = z.object({
|
||||
srvySn: z.number(),
|
||||
rspdnts: z.object({
|
||||
rspdntNm: z.string(),
|
||||
gndrCd: z.string(),
|
||||
crTypeCd: z.string(),
|
||||
brdt: z.string(),
|
||||
}),
|
||||
rspnsRslts: z.array(
|
||||
z.object({
|
||||
srvyQitemNo: z.number(),
|
||||
qstnTypeCd: z.string(),
|
||||
ansCn: z.string(),
|
||||
etcAnsCn: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type SurveyList = PageableList<Survey>;
|
||||
export type SurveyFormData = z.infer<typeof surveyFormSchema>;
|
||||
export type QuestionList = PageableList<Question>;
|
||||
@@ -100,6 +118,7 @@ export type UserSurvey = SurveyDetail & {
|
||||
articles: Article[];
|
||||
})[];
|
||||
};
|
||||
export type UserSurveyFormData = z.infer<typeof userSurveyFormSchema>;
|
||||
|
||||
export interface SurveyService {
|
||||
getSurveyList(params: SurveyParams): Promise<SurveyList>;
|
||||
@@ -114,4 +133,5 @@ export interface SurveyService {
|
||||
getArticleList(params: ArticleParams): Promise<ArticleList>;
|
||||
saveArticleList(data: Article[]): Promise<void>;
|
||||
getUserSurvey(srvySn: number): Promise<UserSurvey>;
|
||||
saveUserSurvey(data: UserSurveyFormData): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,209 +1,377 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useForm, type Control } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { SurveyApi } from "../api/survey-service";
|
||||
import type { Article } from "../model/survey-interface";
|
||||
import { useState } from "react";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
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 { data: userSurvey } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, srvySn],
|
||||
queryFn: () => SurveyApi.getUserSurvey(parseInt(srvySn)),
|
||||
});
|
||||
|
||||
const { mutate: saveUserSurvey } = useMutation({
|
||||
mutationFn: (data: UserSurveyFormData) => SurveyApi.saveUserSurvey(data),
|
||||
onSuccess: () => {
|
||||
alert("설문이 제출되었습니다.", window.close);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<UserSurveyFormData>({
|
||||
resolver: zodResolver(userSurveyFormSchema),
|
||||
values: {
|
||||
srvySn: parseInt(srvySn),
|
||||
rspdnts: {
|
||||
rspdntNm: "",
|
||||
gndrCd: "",
|
||||
crTypeCd: "",
|
||||
brdt: "",
|
||||
},
|
||||
rspnsRslts:
|
||||
userSurvey?.qitems.map((item) => ({
|
||||
srvyQitemNo: item.srvyQitemNo,
|
||||
qstnTypeCd: item.qstnTypeCd,
|
||||
ansCn: "",
|
||||
etcAnsCn: "",
|
||||
})) || [],
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: UserSurveyFormData) => {
|
||||
saveUserSurvey(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-[#f1f6f9] p-4">
|
||||
<div className="card card-border !m-0 max-h-full overflow-auto">
|
||||
<div className="card-header sticky -top-2 z-10 bg-background">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{userSurvey?.srvyTtl}
|
||||
</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button>제출</Button>
|
||||
<Button color="white" onClick={() => window.close()}>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div>
|
||||
<h2 className="text-[1.3rem] font-bold text-foreground">
|
||||
설문 참여자 정보
|
||||
</h2>
|
||||
<div className="my-4 flex gap-4">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span>응답자명</span>
|
||||
<Input
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="이름을 입력해주세요"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span>직업</span>
|
||||
<CodeSelect upCd="" className="w-full" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span>생년월일</span>
|
||||
<Input type="date" className="!text-[1.3rem]" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span>성별</span>
|
||||
<RadioGroup name="gndrCd" className="h-[3.2rem]">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="M" id="M" />
|
||||
<Label htmlFor="M">남자</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="F" id="F" />
|
||||
<Label htmlFor="F">여자</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card-header sticky -top-2 z-10 bg-background">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{userSurvey?.srvyTtl}
|
||||
</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit">제출</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
onClick={() => window.close()}
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
<div className="h-[8rem] overflow-auto rounded-sm bg-accent p-4">
|
||||
{userSurvey?.srvyWrtGdCn}
|
||||
</div>
|
||||
<div className="mt-10 flex flex-col gap-10">
|
||||
{userSurvey?.qitems.map((item) => (
|
||||
<div className="flex items-start gap-4" key={item.srvyQitemNo}>
|
||||
<span className="flex size-8 items-center justify-center rounded-full bg-primary text-[1.3rem] font-medium text-primary-foreground">
|
||||
{item.qstnSeq}
|
||||
</span>
|
||||
{item.qstnTypeCd === "RADIO" && (
|
||||
<RadioArticle
|
||||
name={`r-${item.srvyQitemNo}`}
|
||||
qstnCn={item.qstnCn}
|
||||
articles={item.articles}
|
||||
/>
|
||||
)}
|
||||
{item.qstnTypeCd === "CHECKBOX" && (
|
||||
<CheckboxArticle
|
||||
name={`chk-${item.srvyQitemNo}`}
|
||||
qstnCn={item.qstnCn}
|
||||
maxChcCnt={item.maxChcCnt}
|
||||
articles={item.articles}
|
||||
/>
|
||||
)}
|
||||
{item.qstnTypeCd === "INPUT" && (
|
||||
<TextareaArticle
|
||||
qstnCn={item.qstnCn}
|
||||
article={item.articles[0]}
|
||||
/>
|
||||
)}
|
||||
<div className="card">
|
||||
<div>
|
||||
<h2 className="text-[1.3rem] font-bold text-foreground">
|
||||
설문 참여자 정보
|
||||
</h2>
|
||||
<div className="my-4 flex gap-4">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span>응답자명</span>
|
||||
<Input
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="이름을 입력해주세요"
|
||||
{...form.register("rspdnts.rspdntNm")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span>직업</span>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rspdnts.crTypeCd"
|
||||
render={({ field }) => (
|
||||
<CodeSelect
|
||||
upCd="CRTYPECD"
|
||||
className="w-full"
|
||||
onValueChange={field.onChange}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span>생년월일</span>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rspdnts.brdt"
|
||||
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"));
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span>성별</span>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rspdnts.gndrCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
className="h-[3.2rem]"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="M" id="M" />
|
||||
<Label htmlFor="M">남자</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="F" id="F" />
|
||||
<Label htmlFor="F">여자</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
<div className="h-[8rem] overflow-auto rounded-sm bg-accent p-4">
|
||||
{userSurvey?.srvyWrtGdCn}
|
||||
</div>
|
||||
<div className="mt-10 flex flex-col gap-10">
|
||||
{userSurvey?.qitems.map((item, qIndex) => (
|
||||
<div
|
||||
className="flex items-start gap-4"
|
||||
key={item.srvyQitemNo}
|
||||
>
|
||||
<span className="flex size-8 items-center justify-center rounded-full bg-primary text-[1.3rem] font-medium text-primary-foreground">
|
||||
{item.qstnSeq}
|
||||
</span>
|
||||
{item.qstnTypeCd === "RADIO" && (
|
||||
<RadioArticle
|
||||
control={form.control}
|
||||
qstnCn={item.qstnCn}
|
||||
index={qIndex}
|
||||
articles={item.articles}
|
||||
/>
|
||||
)}
|
||||
{item.qstnTypeCd === "CHECKBOX" && (
|
||||
<CheckboxArticle
|
||||
control={form.control}
|
||||
qstnCn={item.qstnCn}
|
||||
index={qIndex}
|
||||
maxChcCnt={item.maxChcCnt}
|
||||
articles={item.articles}
|
||||
/>
|
||||
)}
|
||||
{item.qstnTypeCd === "INPUT" && (
|
||||
<TextareaArticle
|
||||
control={form.control}
|
||||
qstnCn={item.qstnCn}
|
||||
index={qIndex}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioArticle({
|
||||
name,
|
||||
control,
|
||||
qstnCn,
|
||||
index: i,
|
||||
articles,
|
||||
}: {
|
||||
name: string;
|
||||
control: Control<UserSurveyFormData>;
|
||||
qstnCn: string;
|
||||
index: number;
|
||||
articles: Article[];
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-4 text-2xl font-bold text-foreground">{qstnCn}</p>
|
||||
<RadioGroup name={name}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{articles.map((article, index) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={article.srvyArtclNo}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={article.srvyArtclNo?.toString() ?? ""}
|
||||
id={`${name}-${index}`}
|
||||
/>
|
||||
<Label htmlFor={`${name}-${index}`} className="!text-[1.4rem]">
|
||||
{article.artclCn}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<FormField
|
||||
control={control}
|
||||
name={`rspnsRslts.${i}.ansCn`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup onValueChange={field.onChange} {...field}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{articles.map((article, j) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={article.srvyArtclNo}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={article.srvyArtclNo?.toString() ?? ""}
|
||||
id={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
className="!text-[1.4rem]"
|
||||
>
|
||||
{article.artclCn}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckboxArticle({
|
||||
name,
|
||||
control,
|
||||
qstnCn,
|
||||
index: i,
|
||||
maxChcCnt,
|
||||
articles,
|
||||
}: {
|
||||
name: string;
|
||||
control: Control<UserSurveyFormData>;
|
||||
index: number;
|
||||
qstnCn: string;
|
||||
maxChcCnt: number;
|
||||
articles: Article[];
|
||||
}) {
|
||||
const [selectedValues, setSelectedValues] = useState<number[]>([]);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const handleCheckboxChange = (value: number) => (checked: boolean) => {
|
||||
if (checked && selectedValues.length >= maxChcCnt) {
|
||||
alert("최대 선택 개수를 초과했습니다.");
|
||||
return;
|
||||
}
|
||||
if (selectedValues.includes(value)) {
|
||||
setSelectedValues((prev) => prev.filter((item) => item !== value));
|
||||
return;
|
||||
}
|
||||
setSelectedValues((prev) => [...prev, value]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-4 text-2xl font-bold text-foreground">
|
||||
{qstnCn} (최대 {maxChcCnt}개 선택)
|
||||
</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
{articles.map((article, index) => (
|
||||
<div
|
||||
className="flex h-[1.8rem] items-center gap-2"
|
||||
key={article.srvyArtclNo}
|
||||
>
|
||||
<Checkbox
|
||||
id={`${name}-${index}`}
|
||||
checked={selectedValues.includes(article.srvyArtclNo ?? 0)}
|
||||
onCheckedChange={handleCheckboxChange(article.srvyArtclNo ?? 0)}
|
||||
/>
|
||||
<Label htmlFor={`${name}-${index}`} className="!text-[1.4rem]">
|
||||
{article.artclCn}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<FormField
|
||||
control={control}
|
||||
name={`rspnsRslts.${i}.ansCn`}
|
||||
render={({ field }) => {
|
||||
const selectedValues = field.value?.split(",") || [];
|
||||
const handleCheckboxChange =
|
||||
(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}
|
||||
>
|
||||
<Checkbox
|
||||
id={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
checked={selectedValues.includes(value)}
|
||||
onCheckedChange={handleCheckboxChange(value)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`rspnsRslts.${i}.ansCn-${j}`}
|
||||
className="!text-[1.4rem]"
|
||||
>
|
||||
{article.artclCn}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextareaArticle({ qstnCn }: { qstnCn: string; article?: Article }) {
|
||||
function TextareaArticle({
|
||||
control,
|
||||
qstnCn,
|
||||
index: i,
|
||||
}: {
|
||||
control: Control<UserSurveyFormData>;
|
||||
qstnCn: string;
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<p className="mb-4 text-2xl font-bold text-foreground">{qstnCn}</p>
|
||||
<Textarea className="!text-[1.3rem]" placeholder="답변을 입력해주세요" />
|
||||
<FormField
|
||||
control={control}
|
||||
name={`rspnsRslts.${i}.ansCn`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="답변을 입력해주세요"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user