From a510779ea18ed18bd4cc3482141d2d572d2b6c5f Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 21 Sep 2026 17:24:01 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[as]=20feat:=20AI=20=EB=A7=8C=EC=A1=B1?= =?UTF-8?q?=EB=8F=84=20=ED=8F=89=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/pwrk-cnsltn-doc-evl-service.ts | 19 ++ .../pwrk-cnsltn-doc-evl/api/queries.ts | 11 + .../src/entities/pwrk-cnsltn-doc-evl/index.ts | 10 + .../model/pwrk-cnsltn-doc-evl-interface.ts | 12 + .../pwrk-cnsltn-doc-evl-payload-schema.ts | 12 + .../lib/pwrk-cnsltn-doc-evl-schema.ts | 32 +++ .../components/pwrk-cnsltn-doc-evl-dialog.tsx | 136 ++++++++++++ .../components/pwrk-cnsltn-doc-mng-dialog.tsx | 209 ++++++++++-------- apps/as/src/shared/config/api.ts | 1 + 9 files changed, 347 insertions(+), 95 deletions(-) create mode 100644 apps/as/src/entities/pwrk-cnsltn-doc-evl/api/pwrk-cnsltn-doc-evl-service.ts create mode 100644 apps/as/src/entities/pwrk-cnsltn-doc-evl/api/queries.ts create mode 100644 apps/as/src/entities/pwrk-cnsltn-doc-evl/index.ts create mode 100644 apps/as/src/entities/pwrk-cnsltn-doc-evl/model/pwrk-cnsltn-doc-evl-interface.ts create mode 100644 apps/as/src/entities/pwrk-cnsltn-doc-evl/model/pwrk-cnsltn-doc-evl-payload-schema.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/lib/pwrk-cnsltn-doc-evl-schema.ts create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-evl-dialog.tsx diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-evl/api/pwrk-cnsltn-doc-evl-service.ts b/apps/as/src/entities/pwrk-cnsltn-doc-evl/api/pwrk-cnsltn-doc-evl-service.ts new file mode 100644 index 000000000..54b3c8b2c --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-evl/api/pwrk-cnsltn-doc-evl-service.ts @@ -0,0 +1,19 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + PwrkCnsltnDocEvlId, + PwrkCnsltnDocEvlService, +} from "../model/pwrk-cnsltn-doc-evl-interface"; +import type { PwrkCnsltnDocEvlSaveRequestDto } from "../model/pwrk-cnsltn-doc-evl-payload-schema"; + +const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_DOC_EVL; + +async function save(body: PwrkCnsltnDocEvlSaveRequestDto): Promise { + const response = await client.post(BASE, body); + return response.data; +} + +export const pwrkCnsltnDocEvlService = { + save, +} satisfies PwrkCnsltnDocEvlService; diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-evl/api/queries.ts b/apps/as/src/entities/pwrk-cnsltn-doc-evl/api/queries.ts new file mode 100644 index 000000000..dcd696ac8 --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-evl/api/queries.ts @@ -0,0 +1,11 @@ +import { useMutation } from "@tanstack/react-query"; + +import type { PwrkCnsltnDocEvlSaveRequestDto } from "../model/pwrk-cnsltn-doc-evl-payload-schema"; + +import { pwrkCnsltnDocEvlService } from "./pwrk-cnsltn-doc-evl-service"; + +export function useSavePwrkCnsltnDocEvl() { + return useMutation({ + mutationFn: (body: PwrkCnsltnDocEvlSaveRequestDto) => pwrkCnsltnDocEvlService.save(body), + }); +} diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-evl/index.ts b/apps/as/src/entities/pwrk-cnsltn-doc-evl/index.ts new file mode 100644 index 000000000..4b8ee4d9c --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-evl/index.ts @@ -0,0 +1,10 @@ +export { pwrkCnsltnDocEvlService } from "./api/pwrk-cnsltn-doc-evl-service"; +export { useSavePwrkCnsltnDocEvl } from "./api/queries"; +export type { + PwrkCnsltnDocEvlId, + PwrkCnsltnDocEvlService, +} from "./model/pwrk-cnsltn-doc-evl-interface"; +export { + type PwrkCnsltnDocEvlSaveRequestDto, + pwrkCnsltnDocEvlSaveRequestSchema, +} from "./model/pwrk-cnsltn-doc-evl-payload-schema"; diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-evl/model/pwrk-cnsltn-doc-evl-interface.ts b/apps/as/src/entities/pwrk-cnsltn-doc-evl/model/pwrk-cnsltn-doc-evl-interface.ts new file mode 100644 index 000000000..e2735e028 --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-evl/model/pwrk-cnsltn-doc-evl-interface.ts @@ -0,0 +1,12 @@ +import type { PwrkCnsltnDocEvlSaveRequestDto } from "./pwrk-cnsltn-doc-evl-payload-schema"; + +/** 백엔드 PwrkCnsltnDocEvlId (IdClass) */ +export interface PwrkCnsltnDocEvlId { + pwrkCnsltnDocMng: string; + pwrkCnsltnDocEvlUuid: string; +} + +/** Controller public 메서드명·시그니처와 동일 (save만) */ +export interface PwrkCnsltnDocEvlService { + save(body: PwrkCnsltnDocEvlSaveRequestDto): Promise; +} diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-evl/model/pwrk-cnsltn-doc-evl-payload-schema.ts b/apps/as/src/entities/pwrk-cnsltn-doc-evl/model/pwrk-cnsltn-doc-evl-payload-schema.ts new file mode 100644 index 000000000..5418c7c63 --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-evl/model/pwrk-cnsltn-doc-evl-payload-schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +/** 백엔드 PwrkCnsltnDocEvlSaveRequestDto */ +export const pwrkCnsltnDocEvlSaveRequestSchema = z.object({ + pwrkCnsltnDocNo: z.string().min(1), + pwrkCnsltnDocEvlUuid: z.string().uuid().optional(), + pwrkCnsltnDocEvlCn: z.string().max(2000).optional(), + pwrkCnsltnDocEvlScr: z.number().optional(), + pwrkCnsltnDocEvlDt: z.string().optional(), +}); + +export type PwrkCnsltnDocEvlSaveRequestDto = z.infer; diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/pwrk-cnsltn-doc-evl-schema.ts b/apps/as/src/pages/fnrs-cnsltn/lib/pwrk-cnsltn-doc-evl-schema.ts new file mode 100644 index 000000000..569c4f9c2 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/pwrk-cnsltn-doc-evl-schema.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +import { pwrkCnsltnDocEvlSaveRequestSchema } from "@/entities/pwrk-cnsltn-doc-evl"; + +export const PWRK_CNSLTN_DOC_EVL_SCR_OPTIONS = [ + { value: "5", label: "매우 만족" }, + { value: "4", label: "만족" }, + { value: "3", label: "보통" }, + { value: "2", label: "불만족" }, + { value: "1", label: "매우 불만족" }, +] as const; + +export const pwrkCnsltnDocEvlFormSchema = z.object({ + pwrkCnsltnDocEvlScr: z.enum(["1", "2", "3", "4", "5"], { + required_error: "만족도를 선택해 주세요.", + invalid_type_error: "만족도를 선택해 주세요.", + }), + pwrkCnsltnDocEvlCn: z.string().max(2000).optional(), +}); + +export type PwrkCnsltnDocEvlFormValues = z.infer; + +export function buildPwrkCnsltnDocEvlSavePayload( + pwrkCnsltnDocNo: string, + form: PwrkCnsltnDocEvlFormValues, +) { + return pwrkCnsltnDocEvlSaveRequestSchema.parse({ + pwrkCnsltnDocNo, + pwrkCnsltnDocEvlScr: Number(form.pwrkCnsltnDocEvlScr), + pwrkCnsltnDocEvlCn: form.pwrkCnsltnDocEvlCn?.trim() || undefined, + }); +} diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-evl-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-evl-dialog.tsx new file mode 100644 index 000000000..3696faf7c --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-evl-dialog.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { zodResolver } from "@hookform/resolvers/zod"; + +import { useAlertStore } from "@/shared/lib"; +import { useSavePwrkCnsltnDocEvl } from "@/entities/pwrk-cnsltn-doc-evl"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Textarea, +} from "@common/components"; + +import { + buildPwrkCnsltnDocEvlSavePayload, + PWRK_CNSLTN_DOC_EVL_SCR_OPTIONS, + pwrkCnsltnDocEvlFormSchema, + type PwrkCnsltnDocEvlFormValues, +} from "../../lib/pwrk-cnsltn-doc-evl-schema"; + +interface PwrkCnsltnDocEvlDialogProps { + open: boolean; + pwrkCnsltnDocNo: string; + onClose: () => void; +} + +const DEFAULT_VALUES: Partial = { + pwrkCnsltnDocEvlScr: undefined, + pwrkCnsltnDocEvlCn: "", +}; + +function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDocEvlDialogProps) { + const alert = useAlertStore((state) => state.setMessage); + const { mutateAsync: saveEvl, isPending } = useSavePwrkCnsltnDocEvl(); + + const { handleSubmit, reset, setValue, watch, register } = useForm({ + resolver: zodResolver(pwrkCnsltnDocEvlFormSchema), + defaultValues: DEFAULT_VALUES, + }); + + const pwrkCnsltnDocEvlScr = watch("pwrkCnsltnDocEvlScr"); + + useEffect(() => { + if (!open) return; + reset(DEFAULT_VALUES); + }, [open, reset]); + + function handleOpenChange(nextOpen: boolean) { + if (!nextOpen) onClose(); + } + + async function onSubmit(values: PwrkCnsltnDocEvlFormValues) { + try { + const payload = buildPwrkCnsltnDocEvlSavePayload(pwrkCnsltnDocNo, values); + await saveEvl(payload); + alert("AI 초안 만족도 평가를 등록했습니다."); + onClose(); + } catch (error) { + console.error("AI 초안 만족도 평가 등록 실패:", error); + alert("AI 초안 만족도 평가 등록에 실패했습니다."); + } + } + + return ( + + + + AI 초안 만족도 평가 + + +
+
+ + +
+ +
+ +