feat: 설문 관리 문항 작업

This commit is contained in:
2025-05-30 18:39:23 +09:00
parent e8c20e44ad
commit a7adb4d1d9
14 changed files with 939 additions and 26 deletions
@@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { useState } from "react";
import dynamic from "next/dynamic";
import { type Path, type PathValue, type UseFormReturn } from "react-hook-form";
import type { Path, PathValue, UseFormReturn } from "react-hook-form";
import {
Checkbox,
DatePicker,
+59 -3
View File
@@ -1,11 +1,18 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
SurveyParams,
SurveyList,
SurveyService,
SurveyFormData,
SurveyList,
SurveyParams,
SurveyDetail,
SurveyFormData,
Question,
QuestionList,
QuestionParams,
QuestionFormData,
Article,
ArticleList,
ArticleParams,
} from "../model/survey-interface";
export class SurveyApiService implements SurveyService {
@@ -33,6 +40,55 @@ export class SurveyApiService implements SurveyService {
): Promise<void> {
await client.put(`${ENDPOINT.CMS_SERVICE.SURVEY}/${srvySn}`, data);
}
public async getQuestionList(params: QuestionParams): Promise<QuestionList> {
const response = await client.get<QuestionList>(
`${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}`,
{ params },
);
return response.data;
}
public async getQuestionInfo(srvyQitemNo: number): Promise<Question> {
const response = await client.get<Question>(
`${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}/${srvyQitemNo}`,
);
return response.data;
}
public async createQuestion(
data: QuestionFormData & { srvySn: number },
): Promise<void> {
await client.post(ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, data);
}
public async updateQuestion(
srvyQitemNo: number,
data: QuestionFormData,
): Promise<void> {
await client.put(
`${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}/${srvyQitemNo}`,
data,
);
}
public async deleteQuestion(srvyQitemNo: number): Promise<void> {
await client.delete(
`${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}/${srvyQitemNo}`,
);
}
public async getArticleList(params: ArticleParams): Promise<ArticleList> {
const response = await client.get<ArticleList>(
`${ENDPOINT.CMS_SERVICE.SURVEY_ARTICLE}`,
{ params },
);
return response.data;
}
public async saveArticleList(data: Article[]): Promise<void> {
await client.post(ENDPOINT.CMS_SERVICE.SURVEY_ARTICLE, data);
}
}
export const SurveyApi = new SurveyApiService();
@@ -0,0 +1,116 @@
import { memo } from "react";
import { useQueryClient } from "@tanstack/react-query";
import type { ColumnDef } from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button } from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import type { Question } from "./survey-interface";
import { SurveyApi } from "../api/survey-service";
import { QuestionEditDialog } from "../ui/question-edit-dialog";
import { ArticleManagementDialog } from "../ui/article-management-dialog";
const QUESTION_TYPE = {
RADIO: "객관식",
CHECK: "다중응답",
INPUT: "주관식",
} as const;
export const columns: ColumnDef<Question>[] = [
{
accessorKey: "qstnSeq",
header: "문항순번",
meta: {
width: 80,
},
},
{
accessorKey: "qstnCn",
header: "문항내용",
},
{
accessorKey: "qstnTypeCd",
header: "문항유형",
cell: ({ row }) => (
<span className="text-center">
{QUESTION_TYPE[row.original.qstnTypeCd]}
</span>
),
meta: {
width: 100,
},
},
{
accessorKey: "maxChcCnt",
header: "최대선택갯수",
meta: {
width: 100,
},
},
{
accessorKey: "etcAnsYn",
header: "기타답변여부",
meta: {
width: 100,
},
},
{
id: "action",
header: "관리",
cell: ({ row }) => (
<ActionCell
srvySn={row.original.srvySn}
srvyQitemNo={row.original.srvyQitemNo}
qstnCn={row.original.qstnCn}
/>
),
meta: {
width: 240,
},
},
];
type ActionCellProps = {
srvySn: number;
srvyQitemNo: number;
qstnCn: string;
};
const ActionCell = memo(
({ srvySn, srvyQitemNo, qstnCn }: ActionCellProps) => {
const queryClient = useQueryClient();
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const onDelete = async () => {
if (await confirm("문항을 삭제하시겠습니까?")) {
try {
await SurveyApi.deleteQuestion(srvyQitemNo);
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvySn.toString()],
});
alert("삭제되었습니다.");
} catch (error) {
console.error(error);
}
}
};
return (
<div className="flex justify-center gap-2">
<QuestionEditDialog srvySn={srvySn} srvyQitemNo={srvyQitemNo} />
<Button size="sm" color="red" onClick={onDelete}>
삭제
</Button>
<ArticleManagementDialog srvyQitemNo={srvyQitemNo} qstnCn={qstnCn} />
</div>
);
},
(prevProps, nextProps) => {
return prevProps.srvyQitemNo === nextProps.srvyQitemNo;
},
);
ActionCell.displayName = "ActionCell";
export { ActionCell };
@@ -1,6 +1,15 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export const QUESTION_TYPE_CODE = {
RADIO: "RADIO",
CHECK: "CHECK",
INPUT: "INPUT",
} as const;
export type QuestionTypeCode =
(typeof QUESTION_TYPE_CODE)[keyof typeof QUESTION_TYPE_CODE];
export interface SurveyParams extends PageableParams {
keywordType?: string;
keyword?: string;
@@ -11,6 +20,24 @@ export interface SurveyParams extends PageableParams {
delYn?: string;
}
export interface QuestionParams extends PageableParams {
keywordType?: string;
keyword?: string;
srvySn?: number;
qstnTypeCd?: QuestionTypeCode;
qstnCn?: string;
etcAnsYn?: string;
useYn?: string;
}
export interface ArticleParams extends PageableParams {
keywordType?: string;
keyword?: string;
srvyQitemNo?: number;
artclCn?: string;
delYn?: string;
}
export interface Survey {
srvySn: number;
srvyTtl: string;
@@ -26,6 +53,19 @@ export interface SurveyDetail extends Survey {
srvyWrtGdCn: string;
}
export interface Question {
srvyQitemNo: number;
srvySn: number;
qstnSeq: number;
qstnTypeCd: QuestionTypeCode;
qstnTypeNm: string;
qstnCn: string;
maxChcCnt: number;
etcAnsYn: string;
regDt: string;
rgTrNm: string;
}
export const surveyFormSchema = z.object({
srvyTtl: z.string(),
srvyPrps: z.string(),
@@ -33,12 +73,39 @@ export const surveyFormSchema = z.object({
srvyWrtGdCn: z.string().nullish(),
});
export const questionFormSchema = z.object({
qstnSeq: z.number(),
qstnTypeCd: z.nativeEnum(QUESTION_TYPE_CODE),
qstnCn: z.string(),
maxChcCnt: z.number().nullish(),
etcAnsYn: z.string().nullish(),
});
export const articleSchema = z.object({
srvyArtclNo: z.number().nullish(),
srvyQitemNo: z.number(),
artclSeq: z.number(),
artclCn: z.string(),
mode: z.string(),
});
export type SurveyList = PageableList<Survey>;
export type SurveyFormData = z.infer<typeof surveyFormSchema>;
export type QuestionList = PageableList<Question>;
export type QuestionFormData = z.infer<typeof questionFormSchema>;
export type Article = z.infer<typeof articleSchema>;
export type ArticleList = PageableList<Article>;
export interface SurveyService {
getSurveyList(params: SurveyParams): Promise<SurveyList>;
getSurveyInfo(srvySn: number): Promise<SurveyDetail>;
createSurvey(data: SurveyFormData): Promise<void>;
updateSurvey(srvySn: number, data: SurveyFormData): Promise<void>;
getQuestionList(params: QuestionParams): Promise<QuestionList>;
getQuestionInfo(srvyQitemNo: number): Promise<Question>;
createQuestion(data: QuestionFormData): Promise<void>;
updateQuestion(srvyQitemNo: number, data: QuestionFormData): Promise<void>;
deleteQuestion(srvyQitemNo: number): Promise<void>;
getArticleList(params: ArticleParams): Promise<ArticleList>;
saveArticleList(data: Article[]): Promise<void>;
}
@@ -44,7 +44,7 @@ export const columns: ColumnDef<Survey>[] = [
header: "설문제목",
},
{
accessorKey: "srvyTrgt",
accessorKey: "srvyTrgtNm",
header: "설문대상",
},
{
@@ -0,0 +1,281 @@
import { z } from "zod";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, useFieldArray } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Dialog,
DialogTrigger,
Button,
DialogContent,
DialogHeader,
DialogTitle,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Input,
DialogFooter,
DialogClose,
Checkbox,
} from "@common/components";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import { ENDPOINT } from "@/shared/config";
import { SurveyApi } from "../api/survey-service";
import { articleSchema, type Article } from "../model/survey-interface";
import { useAlertStore } from "@/shared/lib/use-alert-store";
type ArticleFormData = {
articles: Article[];
};
function ArticleManagementDialog({
srvyQitemNo,
qstnCn,
}: {
srvyQitemNo: number;
qstnCn: string;
}) {
const queryClient = useQueryClient();
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false);
const { data: articleList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_ARTICLE, srvyQitemNo],
queryFn: () => SurveyApi.getArticleList({ srvyQitemNo }),
select: (data) => {
const articles = data.content.map((item) => ({
...item,
mode: "U",
}));
initialForm();
return articles;
},
enabled: !!srvyQitemNo && !!open,
});
const { mutateAsync } = useMutation({
mutationFn: (body: Article[]) => SurveyApi.saveArticleList(body),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_ARTICLE, srvyQitemNo],
});
alert("저장되었습니다.");
},
});
const form = useForm<ArticleFormData>({
resolver: zodResolver(
z.object({
articles: articleSchema.array(),
}),
),
values: {
articles: articleList ?? [],
},
});
const { fields, append, remove, update } = useFieldArray({
control: form.control,
name: "articles",
});
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const initialForm = () => {
form.reset();
setSelectedRows([]);
};
const onSubmit = async (data: ArticleFormData) => {
if (await confirm("저장하시겠습니까?")) {
await mutateAsync(data.articles);
alert("저장되었습니다.");
}
};
const handleAddArticle = () => {
const maxSeq =
fields.length > 0
? Math.max(...fields.map((field) => field.artclSeq))
: 0;
append({
srvyArtclNo: null,
srvyQitemNo,
artclSeq: maxSeq + 1,
artclCn: "",
mode: "C",
});
};
const handleSelectAll = () => {
if (selectedRows.length === fields.length) {
setSelectedRows([]);
} else {
setSelectedRows(fields.map((_, index) => index));
}
};
const handleSelectRow = (i: number) => {
setSelectedRows((prev) =>
prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i],
);
};
const handleDeleteArticle = async () => {
if (selectedRows.length === 0) return;
const sortedIndices = [...selectedRows].sort((a, b) => b - a);
sortedIndices.forEach((i) => {
const field = fields[i];
if (field?.srvyArtclNo) {
update(i, {
...field,
mode: "D",
});
} else {
remove(i);
}
});
setSelectedRows([]);
};
const isAllSelected =
fields.length > 0 && selectedRows.length === fields.length;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size="sm" color="white">
답변관리
</Button>
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>답변 항목 관리</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
>
<div className="mb-4 flex items-center justify-between">
<div className="text-2xl font-bold">{qstnCn}</div>
<div className="flex gap-2">
<Button type="button" size="sm" onClick={handleAddArticle}>
항목 추가
</Button>
<Button
type="button"
size="sm"
color="red"
onClick={handleDeleteArticle}
disabled={selectedRows.length === 0}
>
항목 삭제
</Button>
</div>
</div>
<div className="card card-border !mx-0 max-h-[28.5rem] overflow-auto">
<Table variant="secondary">
<colgroup>
<col width="8%" />
<col width="16%" />
<col width="auto" />
</colgroup>
<TableHeader className="sticky -top-[0.6rem]">
<TableRow>
<TableHead className="!px-0">
<Checkbox
checked={isAllSelected}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>답변 순번</TableHead>
<TableHead>답변 내용</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, i) => {
if (field.mode === "D") return null;
return (
<TableRow key={field.id}>
<TableCell align="center" className="!px-0">
<Checkbox
checked={selectedRows.includes(i)}
onCheckedChange={() => handleSelectRow(i)}
/>
</TableCell>
<TableCell align="center">
<FormField
control={form.control}
name={`articles.${i}.artclSeq`}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
type="number"
className="!text-[1.3rem]"
value={field.value?.toString() ?? ""}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
<TableCell align="center">
<FormField
control={form.control}
name={`articles.${i}.artclCn`}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
value={field.value ?? ""}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
<DialogFooter className="!justify-center">
<Button type="submit" size="lg">
저장
</Button>
<DialogClose asChild>
<Button type="button" size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
export { ArticleManagementDialog };
+47 -18
View File
@@ -14,7 +14,9 @@ import { PostingApi, type PostPayload } from "@/entities/posting";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import { SurveyApi } from "../api/survey-service";
import { columns } from "../model/posting-table-columns";
import { columns as postingColumns } from "../model/posting-table-columns";
import { columns as questionColumns } from "../model/question-table-columns";
import { QuestionAddDialog } from "./question-add-dialog";
function DetailPage({ srvySn }: { srvySn: string }) {
const router = useRouter();
@@ -24,6 +26,7 @@ function DetailPage({ srvySn }: { srvySn: string }) {
const alert = useAlertStore((state) => state.setMessage);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [questionAddDialogOpen, setQuestionAddDialogOpen] = useState(false);
const { data: surveyInfo } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, srvySn],
@@ -36,6 +39,11 @@ function DetailPage({ srvySn }: { srvySn: string }) {
PostingApi.getPostingHistory({ pstgTrgtNo: parseInt(srvySn) }),
});
const { data: questionList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvySn],
queryFn: () => SurveyApi.getQuestionList({ srvySn: parseInt(srvySn) }),
});
const { mutateAsync: savePostingHistory } = useMutation({
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
onSuccess: () => {
@@ -46,9 +54,20 @@ function DetailPage({ srvySn }: { srvySn: string }) {
},
});
const table = useReactTable({
const postingTable = useReactTable({
data: postHistory?.content ?? [],
columns,
columns: postingColumns,
state: {
rowSelection,
},
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
});
const questionTable = useReactTable({
data: questionList?.content ?? [],
columns: questionColumns,
state: {
rowSelection,
},
@@ -58,7 +77,7 @@ function DetailPage({ srvySn }: { srvySn: string }) {
});
const handleDeletePostingHistory = async () => {
const data = table.getFilteredSelectedRowModel().rows.map((row) => ({
const data = postingTable.getFilteredSelectedRowModel().rows.map((row) => ({
...row.original,
mode: "D",
}));
@@ -71,7 +90,7 @@ function DetailPage({ srvySn }: { srvySn: string }) {
return (
<div className="mt-6 flex flex-col">
<div className="card card-border !mb-0 !mt-6">
<div className="card card-border !mb-0">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">설문 상세</h3>
<div className="flex gap-2">
@@ -101,26 +120,36 @@ function DetailPage({ srvySn }: { srvySn: string }) {
</li>
<li className="flex items-center gap-8 py-6">
<span className="w-[16rem] text-right text-label">설문 대상</span>
<p className="text-2xl font-bold">{surveyInfo?.srvyTrgt}</p>
<p className="text-2xl font-bold">{surveyInfo?.srvyTrgtNm}</p>
</li>
</ul>
</div>
<div className="card card-border">
<div className="card-header">
<div className="card-header flex justify-between">
<h3 className="text-2xl font-medium text-label">사이트 게시 이력</h3>
<Button
color="red"
disabled={!postingTable.getFilteredSelectedRowModel().rows.length}
onClick={handleDeletePostingHistory}
>
이력삭제
</Button>
</div>
<div className="card">
<div className="mb-4 flex justify-end">
<Button
size="sm"
color="red"
disabled={!table.getFilteredSelectedRowModel().rows.length}
onClick={handleDeletePostingHistory}
>
이력삭제
</Button>
</div>
<DataTable table={table} />
<DataTable table={postingTable} />
</div>
</div>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">설문 문항</h3>
<QuestionAddDialog
srvySn={parseInt(srvySn)}
open={questionAddDialogOpen}
onOpenChange={setQuestionAddDialogOpen}
/>
</div>
<div className="card">
<DataTable table={questionTable} />
</div>
</div>
</div>
@@ -0,0 +1,84 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@common/components";
import { useAlertStore } from "@/shared/lib";
import {
questionFormSchema,
type QuestionFormData,
} from "../model/survey-interface";
import { QuestionForm } from "./question-form";
import { SurveyApi } from "../api/survey-service";
import { ENDPOINT } from "@/shared/config";
type QuestionAddDialogProps = {
srvySn: number;
open?: boolean;
onOpenChange?: (open: boolean) => void;
};
function QuestionAddDialog({
srvySn,
open,
onOpenChange,
...props
}: QuestionAddDialogProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createQuestion } = useMutation({
mutationFn: (data: QuestionFormData) =>
SurveyApi.createQuestion({ ...data, srvySn }),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvySn.toString()],
});
alert("문항이 추가되었습니다.");
onOpenChange?.(false);
},
});
const form = useForm<QuestionFormData>({
resolver: zodResolver(questionFormSchema),
defaultValues: { maxChcCnt: 0, etcAnsYn: "N" },
});
return (
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
<DialogTrigger asChild>
<Button>문항추가</Button>
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>설문 문항 추가</DialogTitle>
</DialogHeader>
<QuestionForm
form={form}
handleSave={createQuestion}
actionSlot={
<DialogFooter className="!justify-center">
<Button size="lg" type="submit">
저장
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
}
/>
</DialogContent>
</Dialog>
);
}
export { QuestionAddDialog };
@@ -0,0 +1,98 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import {
questionFormSchema,
type QuestionTypeCode,
type QuestionFormData,
} from "../model/survey-interface";
import { SurveyApi } from "../api/survey-service";
import { QuestionForm } from "./question-form";
type QuestionEditDialogProps = {
srvySn: number;
srvyQitemNo: number;
};
function QuestionEditDialog({
srvySn,
srvyQitemNo,
...props
}: QuestionEditDialogProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false);
const { data: questionInfo } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvyQitemNo],
queryFn: () => SurveyApi.getQuestionInfo(srvyQitemNo),
enabled: open,
});
const { mutateAsync: updateQuestion } = useMutation({
mutationFn: (data: QuestionFormData) =>
SurveyApi.updateQuestion(srvyQitemNo, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvySn.toString()],
});
alert("문항이 수정되었습니다.");
setOpen(false);
},
});
const form = useForm<QuestionFormData>({
resolver: zodResolver(questionFormSchema),
values: {
...questionInfo,
qstnSeq: questionInfo?.qstnSeq ?? 0,
qstnTypeCd: questionInfo?.qstnTypeCd as QuestionTypeCode,
qstnCn: questionInfo?.qstnCn ?? "",
},
});
return (
<Dialog open={open} onOpenChange={setOpen} {...props}>
<DialogTrigger asChild>
<Button size="sm">문항수정</Button>
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>설문 문항 수정</DialogTitle>
</DialogHeader>
<QuestionForm
form={form}
actionSlot={
<DialogFooter className="!justify-center">
<Button size="lg" type="submit">
저장
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
}
handleSave={updateQuestion}
/>
</DialogContent>
</Dialog>
);
}
export { QuestionEditDialog };
@@ -0,0 +1,176 @@
import type { UseFormReturn } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Input,
Label,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Textarea,
} from "@common/components";
import { CodeSelect } from "@/entities/code";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import type { QuestionFormData } from "../model/survey-interface";
type QuestionFormProps = {
form: UseFormReturn<QuestionFormData>;
actionSlot?: React.ReactNode;
handleSave: (data: QuestionFormData) => Promise<void>;
};
function QuestionForm({ form, actionSlot, handleSave }: QuestionFormProps) {
const qstnTypeCd = form.watch("qstnTypeCd");
const { confirm } = useConfirm();
const onSubmit = async (data: QuestionFormData) => {
if (await confirm("저장하시겠습니까?")) handleSave(data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>문항순번</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="qstnSeq"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
type="number"
value={field.value ?? 0}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
}
className="w-[12rem] !text-[1.3rem]"
placeholder="문항순번을 입력하십시오."
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>문항유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="qstnTypeCd"
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
{...field}
upCd="SRVYTYPE"
value={field.value ?? ""}
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>문항내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="qstnCn"
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="문항내용을 입력하십시오."
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
{qstnTypeCd === "CHECK" && (
<TableRow>
<TableHead>최대선택갯수</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="maxChcCnt"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
type="number"
value={field.value ?? 0}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
}
className="w-[12rem] !text-[1.3rem]"
placeholder="답변 최대 선택 갯수"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
{(qstnTypeCd === "RADIO" || qstnTypeCd === "CHECK") && (
<TableRow>
<TableHead>기타답변여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="etcAnsYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
name="etcAnsYn"
value={field.value?.toString() ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="etcY" />
<Label htmlFor="etcY">예</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="etcN" />
<Label htmlFor="etcN">아니오</Label>
</div>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{actionSlot}
</form>
</Form>
);
}
export { QuestionForm };
+2 -1
View File
@@ -101,7 +101,8 @@ function SurveyForm({ form, ...props }: SurveyFormProps) {
<FormItem>
<FormControl>
<CodeSelect
upCd="SRVYTARGET"
upCd="SRVYTRGT"
value={field.value ?? ""}
onValueChange={field.onChange}
/>
</FormControl>
+2 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
@@ -43,6 +43,7 @@ function SurveyPage() {
page: pagination.pageIndex,
size: pagination.pageSize,
}),
placeholderData: keepPreviousData,
});
const table = useReactTable({
+2
View File
@@ -41,6 +41,8 @@ export const ENDPOINT = {
POPUPS: "/cms-service/api/v1/popups",
SCHEDULES: "/cms-service/api/v1/schdls",
SURVEY: "/cms-service/api/v1/srvy",
SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem",
SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl",
},
BOARD_SERVICE: {
BBS: "/board-service/api/v1/bbs",
+3 -1
View File
@@ -17,7 +17,9 @@ function AlertDialog() {
<Dialog open={!!message} onOpenChange={handleOpenChange}>
<DialogContent aria-describedby={undefined}>
<div className="py-10">
<p className="text-center text-[1.4rem] text-foreground">{message}</p>
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
{message}
</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>