From f541687c0087da58c1d0528a236c22359a8c4d4f Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 13 Mar 2025 17:42:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D/=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20fo?= =?UTF-8?q?rm=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/package.json | 7 +- apps/cs/src/entities/site/api/site-service.ts | 15 +- apps/cs/src/entities/site/index.ts | 1 + .../src/entities/site/model/site-interface.ts | 20 +- apps/cs/src/pages/site/model/form-schema.ts | 17 + apps/cs/src/pages/site/ui/register-page.tsx | 561 +++++++++++------- apps/cs/src/pages/site/ui/site-page.tsx | 5 +- apps/cs/src/shared/ui/form.tsx | 177 ++++++ .../common/components/src/ui/label/index.tsx | 2 +- pnpm-lock.yaml | 38 +- 10 files changed, 616 insertions(+), 227 deletions(-) create mode 100644 apps/cs/src/pages/site/model/form-schema.ts create mode 100644 apps/cs/src/shared/ui/form.tsx diff --git a/apps/cs/package.json b/apps/cs/package.json index c9c024291..05c224ea3 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -11,6 +11,9 @@ "dependencies": { "@common/assets": "workspace:*", "@common/components": "workspace:*", + "@hookform/resolvers": "^4.1.3", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", "@tanstack/react-query": "^5.67.1", "@tanstack/react-table": "^8.21.2", "axios": "^1.8.1", @@ -19,7 +22,9 @@ "js-cookie": "^3.0.5", "next": "15.1.7", "react-chartjs-2": "^5.3.0", - "tailwindcss-animate": "^1.0.7" + "react-hook-form": "^7.54.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/apps/cs/src/entities/site/api/site-service.ts b/apps/cs/src/entities/site/api/site-service.ts index ee2910e18..6091ecdca 100644 --- a/apps/cs/src/entities/site/api/site-service.ts +++ b/apps/cs/src/entities/site/api/site-service.ts @@ -1,17 +1,18 @@ import { client } from "@/shared/api/client"; import { ENDPOINT } from "@/shared/config/api"; import type { - ISiteQuery, ISiteInfo, SiteService, ISiteDetail, + ISiteParams, + ISiteData, } from "../model/site-interface"; export class SiteApiService implements SiteService { - public async siteInfo(query: ISiteQuery): Promise { + public async siteInfo(params: ISiteParams): Promise { const response = await client.get( `${ENDPOINT.CMS_SERVICE.SITES}`, - { params: query }, + { params }, ); return response.data; } @@ -22,6 +23,14 @@ export class SiteApiService implements SiteService { ); return response.data; } + + public async modifySite(siteId: string, data: ISiteData): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, + data, + ); + return response.data; + } } export const SiteApi = new SiteApiService(); diff --git a/apps/cs/src/entities/site/index.ts b/apps/cs/src/entities/site/index.ts index 92ccd0ff7..47f9d7b50 100644 --- a/apps/cs/src/entities/site/index.ts +++ b/apps/cs/src/entities/site/index.ts @@ -1 +1,2 @@ export { SiteApi } from "./api/site-service"; +export * from "./model/site-interface"; diff --git a/apps/cs/src/entities/site/model/site-interface.ts b/apps/cs/src/entities/site/model/site-interface.ts index 005652b1a..4f7879f74 100644 --- a/apps/cs/src/entities/site/model/site-interface.ts +++ b/apps/cs/src/entities/site/model/site-interface.ts @@ -1,4 +1,4 @@ -export interface ISiteQuery { +export interface ISiteParams { siteNm: string; siteKndCd: string; useYn: string; @@ -47,20 +47,25 @@ export interface ISiteInfo { empty: boolean; } -export interface ISiteDetail { - siteId: string; +export interface ISiteData { siteNm: string; siteExpln: string; siteKndCd: string; - siteKindNm: string; siteAddr: string; - faxNumber: string; + faxNo: string; telNo: string; lwndCn: string; - basicSiteYn: string; + bscSiteYn: string; bkmkIcon: string; useYn: string; siteSkn: string; +} + +export interface ISiteDetail extends ISiteData { + siteId: string; + siteKindNm: string; + basicSiteYn: string; + faxNumber: string; regDt: string; rgTr: string; mdfcnDt: string; @@ -70,6 +75,7 @@ export interface ISiteDetail { } export interface SiteService { - siteInfo(query: ISiteQuery): Promise; + siteInfo(query: ISiteParams): Promise; siteDetail(siteId: string): Promise; + modifySite(siteId: string, data: ISiteData): Promise; } diff --git a/apps/cs/src/pages/site/model/form-schema.ts b/apps/cs/src/pages/site/model/form-schema.ts new file mode 100644 index 000000000..edafd5253 --- /dev/null +++ b/apps/cs/src/pages/site/model/form-schema.ts @@ -0,0 +1,17 @@ +import type { ISiteData } from "@/entities/site"; + +const siteFormSchema: ISiteData = { + siteNm: "", + siteExpln: "", + siteKndCd: "", + siteAddr: "", + faxNo: "", + telNo: "", + lwndCn: "", + bscSiteYn: "", + bkmkIcon: "", + useYn: "", + siteSkn: "", +}; + +export { siteFormSchema }; diff --git a/apps/cs/src/pages/site/ui/register-page.tsx b/apps/cs/src/pages/site/ui/register-page.tsx index 48347e823..31e435f6d 100644 --- a/apps/cs/src/pages/site/ui/register-page.tsx +++ b/apps/cs/src/pages/site/ui/register-page.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; -import { SiteApi } from "@/entities/site"; -import { ENDPOINT } from "@/shared/config/api"; +import { useForm } from "react-hook-form"; +import { useQuery } from "@tanstack/react-query"; import { Button, Input, @@ -16,29 +16,49 @@ import { TableHead, TableRow, } from "@common/components"; -import { useQuery } from "@tanstack/react-query"; +import { SiteApi, type ISiteDetail } from "@/entities/site"; import { CodeSelect } from "@/entities/code"; +import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; +import { ENDPOINT } from "@/shared/config/api"; import { SitePageLayout } from "./layout"; import { IPManagementDialog } from "./ip-management-dialog"; import { DomainManagementDialog, type Domain, } from "./domain-management-dialog"; -import type { ISiteDetail } from "@/entities/site/model/site-interface"; +import { siteFormSchema } from "../model/form-schema"; +type CreateFormData = typeof siteFormSchema & { + siteId: string; +}; +type ModifyFormData = typeof siteFormSchema; +type FormData = CreateFormData | ModifyFormData; type ViewState = Partial; function RegisterPage({ siteId }: { siteId: string }) { + const isModifyMode = !!siteId; + const router = useRouter(); const { data: siteDetail } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId], queryFn: () => SiteApi.siteDetail(siteId), select: (data) => { + const newData = { + ...data, + faxNo: data.faxNumber, + bscSiteYn: data.basicSiteYn, + }; handleViewStateChange("siteKndCd")(data.siteKndCd); handleViewStateChange("siteSkn")(data.siteSkn); - return data; + return newData; }, - enabled: !!siteId, + enabled: isModifyMode, + }); + + const form = useForm({ + defaultValues: isModifyMode + ? siteFormSchema + : { ...siteFormSchema, siteId: "" }, }); const [viewState, setViewState] = useState({}); @@ -54,213 +74,332 @@ function RegisterPage({ siteId }: { siteId: string }) { ); }; + const onSubmit = (data: FormData) => { + console.log(data); + }; + return ( - -
-
-

사이트 등록/수정

-
-
- - - - - - - - 사이트 코드 - - - handleViewStateChange("siteId")(e.target.value) - } - /> - - - - 사이트 구분 - -
- - {siteId && ( - + +
+
+
+

+ 사이트 등록/수정 +

+
+
+
+ + + + + + + 사이트 코드 + + ( + + + + + + )} /> - )} - - - - - 사이트 디자인 - - - - - - 사이트명 - - - handleViewStateChange("siteNm")(e.target.value) - } - /> - - - - 사이트 설명 - - - handleViewStateChange("siteExpln")(e.target.value) - } - /> - - - - 사이트 도메인 - -
-
- {representativeDomain} -
- -
-
-
- - 사이트 하단 표기 주소 - - - handleViewStateChange("siteAddr")(e.target.value) - } - /> - - - - 사이트 하단 표기 전화번호 - - - handleViewStateChange("telNo")(e.target.value) - } - /> - - - - 사이트 하단 표기 팩스번호 - - - handleViewStateChange("faxNumber")(e.target.value) - } - /> - - - - 사이트 하단 표기 내용 - - - handleViewStateChange("lwndCn")(e.target.value) - } - /> - - - - 북마크 아이콘 - - - handleViewStateChange("bkmkIcon")(e.target.value) - } - /> - - - - 기본 사이트 여부 - - -
- - -
-
- - -
-
-
-
- - 사용 여부 - - -
- - -
-
- - -
-
-
-
-
-
-
-
-
- - - -
-
+ + + + 사이트 구분 + +
+ ( + + + { + handleViewStateChange("siteKndCd")(value); + field.onChange(value); + }} + /> + + + )} + /> + {isModifyMode && ( + + )} +
+
+
+ + 사이트 디자인 + + ( + + + + + + )} + /> + + + + 사이트명 + + ( + + + + + + )} + /> + + + + 사이트 설명 + + ( + + + + + + )} + /> + + + + 사이트 도메인 + +
+
+ {representativeDomain} +
+ +
+
+
+ + 사이트 하단 표기 주소 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 전화번호 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 팩스번호 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 내용 + + ( + + + + + + )} + /> + + + + 북마크 아이콘 + + ( + + + + + + )} + /> + + + + 기본 사이트 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + + + +
+ + + +
+ + + ); } diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index 46e957985..4b095caff 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -7,8 +7,7 @@ import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { Button, Input, DataTable } from "@common/components"; -import { SiteApi } from "@/entities/site/api/site-service"; -import type { ISite, ISiteQuery } from "@/entities/site/model/site-interface"; +import { SiteApi, type ISite, type ISiteParams } from "@/entities/site"; import { ENDPOINT } from "@/shared/config/api"; import { columns } from "../model/table-columns"; import { SitePageLayout } from "./layout"; @@ -21,7 +20,7 @@ function SitePage() { enabled: false, }); - const [siteQuery, setSiteQuery] = useState({ + const [siteQuery, setSiteQuery] = useState({ siteNm: "", siteKndCd: "", useYn: "", diff --git a/apps/cs/src/shared/ui/form.tsx b/apps/cs/src/shared/ui/form.tsx new file mode 100644 index 000000000..73830edd1 --- /dev/null +++ b/apps/cs/src/shared/ui/form.tsx @@ -0,0 +1,177 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + FormProvider, + useFormContext, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form"; +import { cn, Label } from "@common/components"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +