From d6f754eccb3fbaa19d02971a893b1e4e3ee3dac7 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 17 Apr 2025 15:46:37 +0900 Subject: [PATCH 01/44] rename: content -> contents --- .../{content-table-colums.tsx => contents-table-colums.tsx} | 0 apps/cs/src/pages/menu/ui/content-search-dialog.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename apps/cs/src/pages/menu/model/{content-table-colums.tsx => contents-table-colums.tsx} (100%) diff --git a/apps/cs/src/pages/menu/model/content-table-colums.tsx b/apps/cs/src/pages/menu/model/contents-table-colums.tsx similarity index 100% rename from apps/cs/src/pages/menu/model/content-table-colums.tsx rename to apps/cs/src/pages/menu/model/contents-table-colums.tsx diff --git a/apps/cs/src/pages/menu/ui/content-search-dialog.tsx b/apps/cs/src/pages/menu/ui/content-search-dialog.tsx index a3910eda9..5f2a25f83 100644 --- a/apps/cs/src/pages/menu/ui/content-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/content-search-dialog.tsx @@ -17,7 +17,7 @@ import { } from "@common/components/ui"; import { ENDPOINT } from "@/shared/config"; import { ContentsApi, type Contents } from "@/entities/contents"; -import { columns } from "../model/content-table-colums"; +import { columns } from "../model/contents-table-colums"; type ContentSearchDialogProps = { handleContentSelect: (content: Contents) => void; From aaa6e72723591ed48193caa8ba1c6af1d134400a Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 17 Apr 2025 16:45:43 +0900 Subject: [PATCH 02/44] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contents/model/contents-interface.ts | 8 +- .../pages/contents/model/table-columns.tsx | 83 +++++++++++-------- .../src/pages/contents/ui/contents-page.tsx | 9 +- .../components/src/ui/data-table/index.tsx | 38 +++++++-- 4 files changed, 89 insertions(+), 49 deletions(-) diff --git a/apps/cs/src/entities/contents/model/contents-interface.ts b/apps/cs/src/entities/contents/model/contents-interface.ts index d5a9f833d..7eddaad30 100644 --- a/apps/cs/src/entities/contents/model/contents-interface.ts +++ b/apps/cs/src/entities/contents/model/contents-interface.ts @@ -12,10 +12,14 @@ export const contentsSchema = z.object({ contsTtl: z.string(), aprvContsVer: z.number(), useYn: z.string(), - regDt: z.string(), mdfcnDt: z.string(), - rgTrNm: z.string(), + mdfr: z.string(), mdfrNm: z.string(), + aprvDt: z.string(), + autzrNm: z.string(), + workAprvCd: z.string(), + workAprvNm: z.string(), + workContsVer: z.number(), }); export const contentsFormSchema = z.object({ diff --git a/apps/cs/src/pages/contents/model/table-columns.tsx b/apps/cs/src/pages/contents/model/table-columns.tsx index 8790c2176..f2de567e8 100644 --- a/apps/cs/src/pages/contents/model/table-columns.tsx +++ b/apps/cs/src/pages/contents/model/table-columns.tsx @@ -1,47 +1,58 @@ import dayjs from "dayjs"; -import { type ColumnDef } from "@tanstack/react-table"; +import { createColumnHelper } from "@tanstack/react-table"; import type { Contents } from "@/entities/contents"; -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +export const columns = [ + columnHelper.display({ id: "no", header: "NO", cell: ({ row }) => row.index + 1, - }, - { - accessorKey: "contsTtl", + }), + columnHelper.accessor("contsTtl", { + id: "contsTtl", header: "콘텐츠명", - }, - { - accessorKey: "aprvContsVer", - header: "승인여부", - }, - { - accessorKey: "rgTrNm", - header: "승인자", - }, - { - accessorKey: "regDt", - header: "승인일자", - cell: ({ row }) => { - const date = dayjs(row.original.regDt); - return date.format("YYYY-MM-DD"); - }, - }, - { - accessorKey: "useYn", + }), + columnHelper.group({ + id: "workInfo", + header: "작성정보", + columns: [ + columnHelper.accessor("workContsVer", { + header: "작성버전", + }), + columnHelper.accessor("workAprvNm", { + header: "작성상태", + }), + ], + }), + columnHelper.group({ + id: "aprvInfo", + header: "승인정보", + columns: [ + columnHelper.accessor("aprvContsVer", { + header: "승인버전", + }), + columnHelper.accessor("autzrNm", { + header: "승인자", + }), + columnHelper.accessor("aprvDt", { + header: "승인일자", + cell: ({ row }) => dayjs(row.original.aprvDt).format("YYYY-MM-DD"), + }), + ], + }), + columnHelper.accessor("useYn", { + id: "useYn", header: "사용여부", - }, - { - accessorKey: "mdfrNm", + }), + columnHelper.accessor("mdfrNm", { + id: "mdfrNm", header: "최종수정자", - }, - { - accessorKey: "mdfcnDt", + }), + columnHelper.accessor("mdfcnDt", { + id: "mdfcnDt", header: "최종수정일", - cell: ({ row }) => { - const date = dayjs(row.original.mdfcnDt); - return date.format("YYYY-MM-DD"); - }, - }, + cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"), + }), ]; diff --git a/apps/cs/src/pages/contents/ui/contents-page.tsx b/apps/cs/src/pages/contents/ui/contents-page.tsx index 2687b2606..c81621008 100644 --- a/apps/cs/src/pages/contents/ui/contents-page.tsx +++ b/apps/cs/src/pages/contents/ui/contents-page.tsx @@ -89,8 +89,13 @@ function ContentsPage() { 등록 -
- +
+
diff --git a/packages/common/components/src/ui/data-table/index.tsx b/packages/common/components/src/ui/data-table/index.tsx index ac82afde9..bcb072bb1 100644 --- a/packages/common/components/src/ui/data-table/index.tsx +++ b/packages/common/components/src/ui/data-table/index.tsx @@ -13,11 +13,11 @@ import { cn } from "../../lib"; import { Button } from "../button"; import { Table, TableCell, TableHead, TableHeader, TableRow } from "../table"; import type { HTMLAttributes } from "react"; - declare module "@tanstack/react-table" { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { - align: "left" | "center" | "right"; + align?: "left" | "center" | "right"; + rowSpan?: number; } } @@ -70,15 +70,35 @@ function THead({ table }: { table: TableInstance }) { {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { + const columnRelativeDepth = header.depth - header.column.depth; + if ( + !header.isPlaceholder && + columnRelativeDepth > 1 && + header.id === header.column.id + ) { + return null; + } + + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = (leafs?.[leafs.length - 1]?.depth ?? 0) - header.depth; + } + const { meta } = header.column.columnDef; + return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} + + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} ); })} From 41dbb368c308a50dac69f4308c54e0422e23fcfd Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 17 Apr 2025 17:20:12 +0900 Subject: [PATCH 03/44] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contents/register/page.tsx | 2 + apps/cs/middleware.ts | 5 +- .../entities/contents/api/contents-service.ts | 2 +- apps/cs/src/entities/contents/index.ts | 7 +- .../contents/model/contents-interface.ts | 8 +- apps/cs/src/pages/contents/index.ts | 1 + .../src/pages/contents/ui/contents-form.tsx | 198 ++++++++++++++++++ apps/cs/src/pages/contents/ui/create-page.tsx | 43 ++++ apps/cs/src/pages/template/ui/create-page.tsx | 3 +- apps/cs/src/shared/config/api.ts | 1 + 10 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/contents/register/page.tsx create mode 100644 apps/cs/src/pages/contents/ui/contents-form.tsx create mode 100644 apps/cs/src/pages/contents/ui/create-page.tsx diff --git a/apps/cs/app/(with-base-layout)/contents/register/page.tsx b/apps/cs/app/(with-base-layout)/contents/register/page.tsx new file mode 100644 index 000000000..fc33228e2 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/contents/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/contents"; +export default CreatePage; diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts index 923069eb9..d56c1f8ab 100644 --- a/apps/cs/middleware.ts +++ b/apps/cs/middleware.ts @@ -13,7 +13,7 @@ export function middleware(req: NextRequest) { const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ queryParams ? `?${queryParams}` : "" }`; - console.log("Rewriting to >>>>", newUrl); + console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } @@ -26,6 +26,9 @@ export function middleware(req: NextRequest) { if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); + console.log( + `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`, + ); return NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, }); diff --git a/apps/cs/src/entities/contents/api/contents-service.ts b/apps/cs/src/entities/contents/api/contents-service.ts index 8d73d2bf0..06f141fae 100644 --- a/apps/cs/src/entities/contents/api/contents-service.ts +++ b/apps/cs/src/entities/contents/api/contents-service.ts @@ -26,7 +26,7 @@ export class ContentsApiService implements ContentsService { public async createContents(data: ContentsFormData): Promise { const response = await client.post( - `${ENDPOINT.CMS_SERVICE.CONTENTS}`, + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`, data, ); return response.data; diff --git a/apps/cs/src/entities/contents/index.ts b/apps/cs/src/entities/contents/index.ts index 6c612996c..5df94d667 100644 --- a/apps/cs/src/entities/contents/index.ts +++ b/apps/cs/src/entities/contents/index.ts @@ -1,2 +1,7 @@ export { ContentsApi } from "./api/contents-service"; -export type { Contents, ContentsParams } from "./model/contents-interface"; +export { + contentsFormSchema, + type Contents, + type ContentsParams, + type ContentsFormData, +} from "./model/contents-interface"; diff --git a/apps/cs/src/entities/contents/model/contents-interface.ts b/apps/cs/src/entities/contents/model/contents-interface.ts index 7eddaad30..7a7c6dcf0 100644 --- a/apps/cs/src/entities/contents/model/contents-interface.ts +++ b/apps/cs/src/entities/contents/model/contents-interface.ts @@ -23,9 +23,15 @@ export const contentsSchema = z.object({ }); export const contentsFormSchema = z.object({ + contsSn: z.number().nullish(), contsTtl: z.string(), + contsCn: z.string(), + contsRmrk: z.string().nullish(), + aprvCd: z.string().nullish(), + cclCd: z.string().nullish(), + koglCd: z.string().nullish(), + tag: z.string().nullish(), useYn: z.string(), - rgtr: z.string().nullish(), }); export type Contents = z.infer; diff --git a/apps/cs/src/pages/contents/index.ts b/apps/cs/src/pages/contents/index.ts index c14f174e2..f6dc4a528 100644 --- a/apps/cs/src/pages/contents/index.ts +++ b/apps/cs/src/pages/contents/index.ts @@ -1 +1,2 @@ export { ContentsPage } from "./ui/contents-page"; +export { CreatePage } from "./ui/create-page"; diff --git a/apps/cs/src/pages/contents/ui/contents-form.tsx b/apps/cs/src/pages/contents/ui/contents-form.tsx new file mode 100644 index 000000000..0294dff74 --- /dev/null +++ b/apps/cs/src/pages/contents/ui/contents-form.tsx @@ -0,0 +1,198 @@ +import dynamic from "next/dynamic"; +import { type UseFormReturn } from "react-hook-form"; +import { + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { FormWrapper } from "@/widgets/form-wrapper"; +import { FormItem, FormControl, FormField } from "@/shared/ui/form"; +import type { ContentsFormData } from "@/entities/contents"; + +const Editor = dynamic( + () => import("@/entities/editor").then((module) => module.Editor), + { ssr: false }, +); + +interface ContentsFormProps { + form: UseFormReturn; + contsSn?: number; + handleSave: (data: ContentsFormData) => Promise; + handleDelete?: () => Promise; +} + +function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) { + return ( + +
+ + + + + + + + 콘텐츠 제목 + + ( + + + + + + )} + /> + + + + 콘텐츠 내용 + + ( + + + + + + )} + /> + + + + CCL + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 공공누리 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 태그 + + ( + + + + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ ); +} + +export { ContentsForm }; diff --git a/apps/cs/src/pages/contents/ui/create-page.tsx b/apps/cs/src/pages/contents/ui/create-page.tsx new file mode 100644 index 000000000..947ea10c9 --- /dev/null +++ b/apps/cs/src/pages/contents/ui/create-page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; +import { useMutation } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + ContentsApi, + contentsFormSchema, + type ContentsFormData, +} from "@/entities/contents"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { ContentsForm } from "./contents-form"; + +function CreatePage() { + const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createContents } = useMutation({ + mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(contentsFormSchema), + defaultValues: { + useYn: "Y", + }, + }); + + return ( +
+
+

콘텐츠 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/template/ui/create-page.tsx b/apps/cs/src/pages/template/ui/create-page.tsx index 8057ddd00..f3b445809 100644 --- a/apps/cs/src/pages/template/ui/create-page.tsx +++ b/apps/cs/src/pages/template/ui/create-page.tsx @@ -1,7 +1,7 @@ "use client"; -import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; import { useMutation } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { TemplateApi } from "../api/template-service"; @@ -15,6 +15,7 @@ import { TemplateForm } from "./template-form"; function CreatePage() { const router = useRouter(); const { setMessage: alert } = useAlertStore((state) => state); + const { mutateAsync: createTemplate } = useMutation({ mutationFn: (data: TemplateFormData) => TemplateApi.createTemplate(data), onSuccess: () => { diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 2b98fb4e5..1a77bbb10 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -26,6 +26,7 @@ export const ENDPOINT = { MENU_ROLES: "/cms-service/api/v1/menu-roles", MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", IMAGES: "/cms-service/api/v1/images", BANNERS: "/cms-service/api/v1/bnrs", From 30a1c532d37314094c886255743b5b7ca8d2a10d Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 17 Apr 2025 17:31:44 +0900 Subject: [PATCH 04/44] =?UTF-8?q?chore:=20steiger=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/attachment/index.ts | 4 ++++ apps/cs/src/pages/file/model/table-columns.tsx | 2 +- apps/cs/src/pages/file/ui/file-page.tsx | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/cs/src/entities/attachment/index.ts b/apps/cs/src/entities/attachment/index.ts index 182ef9fd6..385425537 100644 --- a/apps/cs/src/entities/attachment/index.ts +++ b/apps/cs/src/entities/attachment/index.ts @@ -1 +1,5 @@ export { AttachmentApi } from "./api/attachment-service"; +export type { + AttachmentParams, + Attachment, +} from "./model/attachment-interface"; diff --git a/apps/cs/src/pages/file/model/table-columns.tsx b/apps/cs/src/pages/file/model/table-columns.tsx index 2a73bf54a..0bc7e5c23 100644 --- a/apps/cs/src/pages/file/model/table-columns.tsx +++ b/apps/cs/src/pages/file/model/table-columns.tsx @@ -3,7 +3,7 @@ import dayjs from "dayjs"; import { type ColumnDef } from "@tanstack/react-table"; import { Button } from "@common/components"; -import type { Attachment } from "@/entities/attachment/model/attachment-interface"; +import type { Attachment } from "@/entities/attachment"; export const createColumns = ( handleDeleteAttachment: (atflId: string) => void, diff --git a/apps/cs/src/pages/file/ui/file-page.tsx b/apps/cs/src/pages/file/ui/file-page.tsx index c896531bb..4f6b79781 100644 --- a/apps/cs/src/pages/file/ui/file-page.tsx +++ b/apps/cs/src/pages/file/ui/file-page.tsx @@ -9,8 +9,7 @@ import { } from "@tanstack/react-table"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button, DataTable, Input, Label } from "@common/components"; -import type { AttachmentParams } from "@/entities/attachment/model/attachment-interface"; -import { AttachmentApi } from "@/entities/attachment/api/attachment-service"; +import { AttachmentApi, type AttachmentParams } from "@/entities/attachment"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { ENDPOINT } from "@/shared/config"; import { createColumns } from "../model/table-columns"; From 9665189c95d0cff12db7171165c2a7fc5e8d2111 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 17 Apr 2025 17:36:06 +0900 Subject: [PATCH 05/44] =?UTF-8?q?chore:=20steiger=20=EB=B0=8F=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EC=97=90=EB=9F=AC=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/{contents => content}/api/contents-service.ts | 0 apps/cs/src/entities/{contents => content}/index.ts | 0 .../{contents => content}/model/contents-interface.ts | 0 apps/cs/src/pages/contents/model/table-columns.tsx | 2 +- apps/cs/src/pages/contents/ui/contents-form.tsx | 2 +- apps/cs/src/pages/contents/ui/contents-page.tsx | 2 +- apps/cs/src/pages/contents/ui/create-page.tsx | 2 +- apps/cs/src/pages/menu/model/contents-table-colums.tsx | 6 +++--- apps/cs/src/pages/menu/ui/content-search-dialog.tsx | 2 +- apps/cs/src/pages/menu/ui/type-menu-form.tsx | 2 +- 10 files changed, 9 insertions(+), 9 deletions(-) rename apps/cs/src/entities/{contents => content}/api/contents-service.ts (100%) rename apps/cs/src/entities/{contents => content}/index.ts (100%) rename apps/cs/src/entities/{contents => content}/model/contents-interface.ts (100%) diff --git a/apps/cs/src/entities/contents/api/contents-service.ts b/apps/cs/src/entities/content/api/contents-service.ts similarity index 100% rename from apps/cs/src/entities/contents/api/contents-service.ts rename to apps/cs/src/entities/content/api/contents-service.ts diff --git a/apps/cs/src/entities/contents/index.ts b/apps/cs/src/entities/content/index.ts similarity index 100% rename from apps/cs/src/entities/contents/index.ts rename to apps/cs/src/entities/content/index.ts diff --git a/apps/cs/src/entities/contents/model/contents-interface.ts b/apps/cs/src/entities/content/model/contents-interface.ts similarity index 100% rename from apps/cs/src/entities/contents/model/contents-interface.ts rename to apps/cs/src/entities/content/model/contents-interface.ts diff --git a/apps/cs/src/pages/contents/model/table-columns.tsx b/apps/cs/src/pages/contents/model/table-columns.tsx index f2de567e8..43dab6d51 100644 --- a/apps/cs/src/pages/contents/model/table-columns.tsx +++ b/apps/cs/src/pages/contents/model/table-columns.tsx @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import { createColumnHelper } from "@tanstack/react-table"; -import type { Contents } from "@/entities/contents"; +import type { Contents } from "@/entities/content"; const columnHelper = createColumnHelper(); diff --git a/apps/cs/src/pages/contents/ui/contents-form.tsx b/apps/cs/src/pages/contents/ui/contents-form.tsx index 0294dff74..0bbd122de 100644 --- a/apps/cs/src/pages/contents/ui/contents-form.tsx +++ b/apps/cs/src/pages/contents/ui/contents-form.tsx @@ -13,7 +13,7 @@ import { } from "@common/components"; import { FormWrapper } from "@/widgets/form-wrapper"; import { FormItem, FormControl, FormField } from "@/shared/ui/form"; -import type { ContentsFormData } from "@/entities/contents"; +import type { ContentsFormData } from "@/entities/content"; const Editor = dynamic( () => import("@/entities/editor").then((module) => module.Editor), diff --git a/apps/cs/src/pages/contents/ui/contents-page.tsx b/apps/cs/src/pages/contents/ui/contents-page.tsx index c81621008..dd47313e7 100644 --- a/apps/cs/src/pages/contents/ui/contents-page.tsx +++ b/apps/cs/src/pages/contents/ui/contents-page.tsx @@ -13,7 +13,7 @@ import { ContentsApi, type Contents, type ContentsParams, -} from "@/entities/contents"; +} from "@/entities/content"; import { ENDPOINT } from "@/shared/config"; import { columns } from "../model/table-columns"; diff --git a/apps/cs/src/pages/contents/ui/create-page.tsx b/apps/cs/src/pages/contents/ui/create-page.tsx index 947ea10c9..8f7c662f3 100644 --- a/apps/cs/src/pages/contents/ui/create-page.tsx +++ b/apps/cs/src/pages/contents/ui/create-page.tsx @@ -8,7 +8,7 @@ import { ContentsApi, contentsFormSchema, type ContentsFormData, -} from "@/entities/contents"; +} from "@/entities/content"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { ContentsForm } from "./contents-form"; diff --git a/apps/cs/src/pages/menu/model/contents-table-colums.tsx b/apps/cs/src/pages/menu/model/contents-table-colums.tsx index 8670058bc..a96434c39 100644 --- a/apps/cs/src/pages/menu/model/contents-table-colums.tsx +++ b/apps/cs/src/pages/menu/model/contents-table-colums.tsx @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { ColumnDef } from "@tanstack/react-table"; import { Checkbox } from "@common/components/ui"; -import type { Contents } from "@/entities/contents"; +import type { Contents } from "@/entities/content"; export const columns: ColumnDef[] = [ { @@ -19,10 +19,10 @@ export const columns: ColumnDef[] = [ header: "승인자", }, { - accessorKey: "regDt", + accessorKey: "aprvDt", header: "승인일자", cell: ({ row }) => { - const date = dayjs(row.original.regDt); + const date = dayjs(row.original.aprvDt); return date.format("YYYY-MM-DD"); }, }, diff --git a/apps/cs/src/pages/menu/ui/content-search-dialog.tsx b/apps/cs/src/pages/menu/ui/content-search-dialog.tsx index 5f2a25f83..73e9b7263 100644 --- a/apps/cs/src/pages/menu/ui/content-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/content-search-dialog.tsx @@ -16,7 +16,7 @@ import { Input, } from "@common/components/ui"; import { ENDPOINT } from "@/shared/config"; -import { ContentsApi, type Contents } from "@/entities/contents"; +import { ContentsApi, type Contents } from "@/entities/content"; import { columns } from "../model/contents-table-colums"; type ContentSearchDialogProps = { diff --git a/apps/cs/src/pages/menu/ui/type-menu-form.tsx b/apps/cs/src/pages/menu/ui/type-menu-form.tsx index 07b7ca4fe..6af89b27c 100644 --- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx @@ -15,7 +15,7 @@ import { } from "@common/components"; import { FormItem, FormControl, FormField } from "@/shared/ui/form"; import type { BoardPopup } from "@/entities/board"; -import type { Contents } from "@/entities/contents"; +import type { Contents } from "@/entities/content"; import type { User } from "../model/organization-interface"; import type { MenuFormData, MenuType } from "../model/menu-interface"; import { OrganizationSearchDialog } from "./organization-search-dialog"; From 6c014fe0cedf2c862b7efb9d3ab2d68750e9826f Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Apr 2025 14:40:39 +0900 Subject: [PATCH 06/44] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contents/register/[sequential]/page.tsx | 10 ++ apps/cs/public/images/img_cc_by.png | Bin 0 -> 4498 bytes apps/cs/public/images/img_cc_by_nc.png | Bin 0 -> 5637 bytes apps/cs/public/images/img_cc_by_nc_nd.png | Bin 0 -> 6651 bytes apps/cs/public/images/img_cc_by_nc_sa.png | Bin 0 -> 6694 bytes apps/cs/public/images/img_cc_by_nd.png | Bin 0 -> 5510 bytes apps/cs/public/images/img_cc_by_sa.png | Bin 0 -> 5668 bytes apps/cs/public/images/img_opentype01.png | Bin 0 -> 3246 bytes apps/cs/public/images/img_opentype02.png | Bin 0 -> 3628 bytes apps/cs/public/images/img_opentype03.png | Bin 0 -> 3605 bytes apps/cs/public/images/img_opentype04.png | Bin 0 -> 4117 bytes .../src/entities/code/model/code-interface.ts | 1 + .../entities/content/api/contents-service.ts | 26 +++- apps/cs/src/entities/content/index.ts | 1 + .../content/model/contents-interface.ts | 57 +++++++- apps/cs/src/pages/contents/index.ts | 1 + .../src/pages/contents/ui/contents-form.tsx | 89 ++++++++++-- apps/cs/src/pages/contents/ui/modify-page.tsx | 127 ++++++++++++++++++ .../cs/src/pages/file/model/table-columns.tsx | 15 +++ 19 files changed, 304 insertions(+), 23 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx create mode 100755 apps/cs/public/images/img_cc_by.png create mode 100755 apps/cs/public/images/img_cc_by_nc.png create mode 100755 apps/cs/public/images/img_cc_by_nc_nd.png create mode 100755 apps/cs/public/images/img_cc_by_nc_sa.png create mode 100755 apps/cs/public/images/img_cc_by_nd.png create mode 100755 apps/cs/public/images/img_cc_by_sa.png create mode 100755 apps/cs/public/images/img_opentype01.png create mode 100755 apps/cs/public/images/img_opentype02.png create mode 100755 apps/cs/public/images/img_opentype03.png create mode 100755 apps/cs/public/images/img_opentype04.png create mode 100644 apps/cs/src/pages/contents/ui/modify-page.tsx diff --git a/apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx b/apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx new file mode 100644 index 000000000..aa9320b8f --- /dev/null +++ b/apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/contents"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ sequential: string }>; +}) { + const { sequential } = await params; + return ; +} diff --git a/apps/cs/public/images/img_cc_by.png b/apps/cs/public/images/img_cc_by.png new file mode 100755 index 0000000000000000000000000000000000000000..628cc2bfd3fdd655056b888ff2e516eb66c45812 GIT binary patch literal 4498 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 literal 0 HcmV?d00001 diff --git a/apps/cs/public/images/img_cc_by_nc_nd.png b/apps/cs/public/images/img_cc_by_nc_nd.png new file mode 100755 index 0000000000000000000000000000000000000000..9d8ccb18dc00d7898c8a25181af55e2ed2143ed0 GIT binary patch literal 6651 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 z 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 C&0xI% literal 0 HcmV?d00001 diff --git a/apps/cs/public/images/img_cc_by_nc_sa.png b/apps/cs/public/images/img_cc_by_nc_sa.png new file mode 100755 index 0000000000000000000000000000000000000000..00f402c9e1a34e5d88e949b5cf5d31cd17b20fc5 GIT binary patch literal 6694 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 literal 0 HcmV?d00001 diff --git a/apps/cs/public/images/img_cc_by_nd.png b/apps/cs/public/images/img_cc_by_nd.png new file mode 100755 index 0000000000000000000000000000000000000000..0da64acd8a24fed8fd21782589ba77e69fab6d8b GIT binary patch literal 5510 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 Ow^dWtR4RRB5%OP9)_QaR literal 0 HcmV?d00001 diff --git a/apps/cs/public/images/img_opentype01.png b/apps/cs/public/images/img_opentype01.png new file mode 100755 index 0000000000000000000000000000000000000000..456c7c6bb211a83deb3ca26e155020c6a1ac1c53 GIT binary patch literal 3246 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 NfwytQmZFcG{2yPxrMmzC literal 0 HcmV?d00001 diff --git a/apps/cs/public/images/img_opentype02.png b/apps/cs/public/images/img_opentype02.png new file mode 100755 index 0000000000000000000000000000000000000000..a6c5bfb3229e319b6d8e4a61982061f7aecc7145 GIT binary patch literal 3628 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 MMwYmHhNq+c58HlKYXATM literal 0 HcmV?d00001 diff --git a/apps/cs/public/images/img_opentype04.png b/apps/cs/public/images/img_opentype04.png new file mode 100755 index 0000000000000000000000000000000000000000..41d284cabcdccc032c0fee538260079c4d75d374 GIT binary patch literal 4117 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 E0FhT$uK)l5 literal 0 HcmV?d00001 diff --git a/apps/cs/src/entities/code/model/code-interface.ts b/apps/cs/src/entities/code/model/code-interface.ts index 0d066e6da..4562e2559 100644 --- a/apps/cs/src/entities/code/model/code-interface.ts +++ b/apps/cs/src/entities/code/model/code-interface.ts @@ -2,6 +2,7 @@ export interface ICode { lwpoCd: string; upCd: string; lwpoCdNm: string; + unit: string; } export interface CodeService { diff --git a/apps/cs/src/entities/content/api/contents-service.ts b/apps/cs/src/entities/content/api/contents-service.ts index 06f141fae..eedc25c54 100644 --- a/apps/cs/src/entities/content/api/contents-service.ts +++ b/apps/cs/src/entities/content/api/contents-service.ts @@ -4,8 +4,10 @@ import type { ContentsService, ContentsParams, ContentsList, - Contents, ContentsFormData, + ContentsVersionParams, + ContentsVersionList, + ContentsDetail, } from "../model/contents-interface"; export class ContentsApiService implements ContentsService { @@ -17,9 +19,22 @@ export class ContentsApiService implements ContentsService { return response.data; } - public async getContentsInfo(contsSn: number): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`, + public async getContentsVersionList( + params: ContentsVersionParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`, + { params }, + ); + return response.data; + } + + public async getLatestContentsInfo( + contsSn: number, + contsVer: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, ); return response.data; } @@ -34,10 +49,11 @@ export class ContentsApiService implements ContentsService { public async updateContents( contsSn: number, + contsVer: number, data: ContentsFormData, ): Promise { const response = await client.put( - `${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`, + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, data, ); return response.data; diff --git a/apps/cs/src/entities/content/index.ts b/apps/cs/src/entities/content/index.ts index 5df94d667..6bf6ebd45 100644 --- a/apps/cs/src/entities/content/index.ts +++ b/apps/cs/src/entities/content/index.ts @@ -1,5 +1,6 @@ export { ContentsApi } from "./api/contents-service"; export { + CONTENTS_APPROVAL_STATUS, contentsFormSchema, type Contents, type ContentsParams, diff --git a/apps/cs/src/entities/content/model/contents-interface.ts b/apps/cs/src/entities/content/model/contents-interface.ts index 7a7c6dcf0..773cdc161 100644 --- a/apps/cs/src/entities/content/model/contents-interface.ts +++ b/apps/cs/src/entities/content/model/contents-interface.ts @@ -1,12 +1,38 @@ import { z } from "zod"; import type { PageableList, PageableParams } from "@/shared/model"; +export const CONTENTS_APPROVAL_STATUS = { + DRAFT: "01", + REQUESTED: "02", + APPROVED: "99", +} as const; + +export type ContentsApprovalStatus = + (typeof CONTENTS_APPROVAL_STATUS)[keyof typeof CONTENTS_APPROVAL_STATUS]; + export interface ContentsParams extends PageableParams { contsTtl?: string; aprvContsVer?: number; useYn?: string; } +export interface ContentsVersionParams extends PageableParams { + contsSn?: number; + contsCn?: string; + aprvCd?: string; + cclCd?: string; + koglCd?: string; + tag?: string; + useYn?: string; +} + +export interface ContentsVersion { + contsSn: number; + contsVer: number; + contsCn: string; + useYn: string; +} + export const contentsSchema = z.object({ contsSn: z.number(), contsTtl: z.string(), @@ -22,26 +48,47 @@ export const contentsSchema = z.object({ workContsVer: z.number(), }); -export const contentsFormSchema = z.object({ - contsSn: z.number().nullish(), +export const contentsDetailSchema = z.object({ + contsSn: z.number(), + contsVer: z.number(), contsTtl: z.string(), contsCn: z.string(), contsRmrk: z.string().nullish(), - aprvCd: z.string().nullish(), + aprvCd: z.string(), cclCd: z.string().nullish(), koglCd: z.string().nullish(), tag: z.string().nullish(), + aprvDt: z.string(), + autzr: z.string(), useYn: z.string(), }); +export const contentsFormSchema = contentsDetailSchema.omit({ + contsVer: true, + aprvDt: true, + autzr: true, +}); + export type Contents = z.infer; +export type ContentsDetail = z.infer; export type ContentsFormData = z.infer; export type ContentsList = PageableList; +export type ContentsVersionList = PageableList; export interface ContentsService { getContentsList(params: ContentsParams): Promise; - getContentsInfo(contsSn: number): Promise; + getContentsVersionList( + params: ContentsVersionParams, + ): Promise; + getLatestContentsInfo( + contsSn: number, + contsVer: number, + ): Promise; createContents(data: ContentsFormData): Promise; - updateContents(contsSn: number, data: ContentsFormData): Promise; + updateContents( + contsSn: number, + contsVer: number, + data: ContentsFormData, + ): Promise; deleteContents(contsSn: number): Promise; } diff --git a/apps/cs/src/pages/contents/index.ts b/apps/cs/src/pages/contents/index.ts index f6dc4a528..2bb0d4fb2 100644 --- a/apps/cs/src/pages/contents/index.ts +++ b/apps/cs/src/pages/contents/index.ts @@ -1,2 +1,3 @@ export { ContentsPage } from "./ui/contents-page"; export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; diff --git a/apps/cs/src/pages/contents/ui/contents-form.tsx b/apps/cs/src/pages/contents/ui/contents-form.tsx index 0bbd122de..d61084646 100644 --- a/apps/cs/src/pages/contents/ui/contents-form.tsx +++ b/apps/cs/src/pages/contents/ui/contents-form.tsx @@ -14,6 +14,10 @@ import { import { FormWrapper } from "@/widgets/form-wrapper"; import { FormItem, FormControl, FormField } from "@/shared/ui/form"; import type { ContentsFormData } from "@/entities/content"; +import { useQuery } from "@tanstack/react-query"; +import { ENDPOINT } from "@/shared/config"; +import { CodeApi } from "@/entities/code/api/code-service"; +import Image from "next/image"; const Editor = dynamic( () => import("@/entities/editor").then((module) => module.Editor), @@ -23,11 +27,27 @@ const Editor = dynamic( interface ContentsFormProps { form: UseFormReturn; contsSn?: number; + disabledFields?: Partial>; handleSave: (data: ContentsFormData) => Promise; handleDelete?: () => Promise; } -function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) { +function ContentsForm({ + form, + contsSn, + disabledFields, + ...props +}: ContentsFormProps) { + const { data: cclCode } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "CCLCD"], + queryFn: () => CodeApi.getCode("CCLCD"), + }); + + const { data: koglCode } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "KOGLCD"], + queryFn: () => CodeApi.getCode("KOGLCD"), + }); + return (
@@ -51,6 +71,7 @@ function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) { value={field.value ?? ""} className="!text-[1.3rem]" placeholder="콘텐츠 제목을 입력하십시오." + disabled={disabledFields?.contsTtl} /> @@ -93,13 +114,33 @@ function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) { onValueChange={field.onChange} >
- - -
-
- - + +
+ {cclCode?.map((code) => ( +
+ + +
+ ))} @@ -123,13 +164,35 @@ function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) { onValueChange={field.onChange} >
- - -
-
- - + +
+ {koglCode?.map((code) => ( +
+ + +
+ ))} diff --git a/apps/cs/src/pages/contents/ui/modify-page.tsx b/apps/cs/src/pages/contents/ui/modify-page.tsx new file mode 100644 index 000000000..52241a5cc --- /dev/null +++ b/apps/cs/src/pages/contents/ui/modify-page.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { + CONTENTS_APPROVAL_STATUS, + ContentsApi, + contentsFormSchema, + type ContentsFormData, +} from "@/entities/content"; +import { ContentsForm } from "./contents-form"; +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@common/components/ui"; + +function ModifyPage({ sequential }: { sequential: string }) { + const { setMessage: alert } = useAlertStore((state) => state); + + const [contentsVersion, setContentsVersion] = useState(0); + const [disabledFields, setDisabledFields] = useState< + Partial> + >({}); + + const { data: contentsVersionList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, sequential], + queryFn: () => + ContentsApi.getContentsVersionList({ + contsSn: Number(sequential), + page: 0, + size: 100, + }), + }); + + const { data: contentsDetail } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contentsVersion], + queryFn: () => + ContentsApi.getLatestContentsInfo(Number(sequential), contentsVersion), + enabled: !!contentsVersion, + }); + + const { mutateAsync: createContents } = useMutation({ + mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), + onSuccess: () => { + alert("수정되었습니다."); + }, + }); + + const { mutateAsync: updateContents } = useMutation({ + mutationFn: (data: ContentsFormData) => + ContentsApi.updateContents( + Number(sequential), + contentsDetail?.contsVer ?? 0, + data, + ), + onSuccess: () => { + alert("수정되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(contentsFormSchema), + values: contentsDetail, + }); + + const isApprovedContents = + contentsDetail?.aprvCd === CONTENTS_APPROVAL_STATUS.APPROVED; + + useEffect(() => { + if (!contentsVersionList?.content?.length) return; + + const latestContent = contentsVersionList?.content[0]; + if (!latestContent) return; + + if (latestContent?.contsVer > 1) { + setDisabledFields({ + contsTtl: true, + }); + } + setContentsVersion(latestContent?.contsVer ?? 0); + }, [contentsVersionList?.content]); + + return ( +
+
+

템플릿 수정

+
+ +
+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/file/model/table-columns.tsx b/apps/cs/src/pages/file/model/table-columns.tsx index 0bc7e5c23..2606397c9 100644 --- a/apps/cs/src/pages/file/model/table-columns.tsx +++ b/apps/cs/src/pages/file/model/table-columns.tsx @@ -28,6 +28,10 @@ export const createColumns = ( { accessorKey: "atflFlieSz", header: "파일크기", + cell: ({ row }) => { + const size = row.getValue("atflFlieSz"); + return size ? formatFileSize(Number(size)) : "0 Bytes"; + }, }, { @@ -60,3 +64,14 @@ export const createColumns = ( ), }, ]; + +const formatFileSize = (bytes: number): string => { + console.log("bytes: ", bytes); + if (bytes === 0) return "0 Bytes"; + + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +}; From 1b0e310513ec44b0db31f78932918e87fce9919e Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Apr 2025 14:52:23 +0900 Subject: [PATCH 07/44] =?UTF-8?q?chore:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=BB=AC=EB=9F=BC=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/contents/model/table-columns.tsx | 14 ++++++++++++-- apps/cs/src/pages/contents/ui/modify-page.tsx | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/cs/src/pages/contents/model/table-columns.tsx b/apps/cs/src/pages/contents/model/table-columns.tsx index 43dab6d51..19d98a48b 100644 --- a/apps/cs/src/pages/contents/model/table-columns.tsx +++ b/apps/cs/src/pages/contents/model/table-columns.tsx @@ -20,6 +20,8 @@ export const columns = [ columns: [ columnHelper.accessor("workContsVer", { header: "작성버전", + cell: ({ row }) => + row.original.workContsVer ? `rev. ${row.original.workContsVer}` : "", }), columnHelper.accessor("workAprvNm", { header: "작성상태", @@ -32,13 +34,18 @@ export const columns = [ columns: [ columnHelper.accessor("aprvContsVer", { header: "승인버전", + cell: ({ row }) => + row.original.aprvContsVer ? `rev. ${row.original.aprvContsVer}` : "", }), columnHelper.accessor("autzrNm", { header: "승인자", }), columnHelper.accessor("aprvDt", { header: "승인일자", - cell: ({ row }) => dayjs(row.original.aprvDt).format("YYYY-MM-DD"), + cell: ({ row }) => + row.original.aprvDt + ? dayjs(row.original.aprvDt).format("YYYY-MM-DD") + : "", }), ], }), @@ -53,6 +60,9 @@ export const columns = [ columnHelper.accessor("mdfcnDt", { id: "mdfcnDt", header: "최종수정일", - cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"), + cell: ({ row }) => + row.original.mdfcnDt + ? dayjs(row.original.mdfcnDt).format("YYYY-MM-DD") + : "", }), ]; diff --git a/apps/cs/src/pages/contents/ui/modify-page.tsx b/apps/cs/src/pages/contents/ui/modify-page.tsx index 52241a5cc..e37cd1563 100644 --- a/apps/cs/src/pages/contents/ui/modify-page.tsx +++ b/apps/cs/src/pages/contents/ui/modify-page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; @@ -22,6 +22,7 @@ import { } from "@common/components/ui"; function ModifyPage({ sequential }: { sequential: string }) { + const queryClient = useQueryClient(); const { setMessage: alert } = useAlertStore((state) => state); const [contentsVersion, setContentsVersion] = useState(0); @@ -50,6 +51,9 @@ function ModifyPage({ sequential }: { sequential: string }) { mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), onSuccess: () => { alert("수정되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, sequential], + }); }, }); From c6814a65f9761e137bd84bd04d7eeb5d207c0908 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Apr 2025 16:04:22 +0900 Subject: [PATCH 08/44] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/content/api/contents-service.ts | 9 +++++-- .../content/model/contents-interface.ts | 2 +- apps/cs/src/pages/contents/ui/modify-page.tsx | 25 ++++++++++++------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/apps/cs/src/entities/content/api/contents-service.ts b/apps/cs/src/entities/content/api/contents-service.ts index eedc25c54..792ffd92e 100644 --- a/apps/cs/src/entities/content/api/contents-service.ts +++ b/apps/cs/src/entities/content/api/contents-service.ts @@ -59,8 +59,13 @@ export class ContentsApiService implements ContentsService { return response.data; } - public async deleteContents(contsSn: number): Promise { - await client.delete(`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`); + public async deleteContents( + contsSn: number, + contsVer: number, + ): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, + ); } } diff --git a/apps/cs/src/entities/content/model/contents-interface.ts b/apps/cs/src/entities/content/model/contents-interface.ts index 773cdc161..b77c28742 100644 --- a/apps/cs/src/entities/content/model/contents-interface.ts +++ b/apps/cs/src/entities/content/model/contents-interface.ts @@ -90,5 +90,5 @@ export interface ContentsService { contsVer: number, data: ContentsFormData, ): Promise; - deleteContents(contsSn: number): Promise; + deleteContents(contsSn: number, contsVer: number): Promise; } diff --git a/apps/cs/src/pages/contents/ui/modify-page.tsx b/apps/cs/src/pages/contents/ui/modify-page.tsx index e37cd1563..ab11d62ee 100644 --- a/apps/cs/src/pages/contents/ui/modify-page.tsx +++ b/apps/cs/src/pages/contents/ui/modify-page.tsx @@ -34,7 +34,7 @@ function ModifyPage({ sequential }: { sequential: string }) { queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, sequential], queryFn: () => ContentsApi.getContentsVersionList({ - contsSn: Number(sequential), + contsSn: contsSn, page: 0, size: 100, }), @@ -42,8 +42,7 @@ function ModifyPage({ sequential }: { sequential: string }) { const { data: contentsDetail } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contentsVersion], - queryFn: () => - ContentsApi.getLatestContentsInfo(Number(sequential), contentsVersion), + queryFn: () => ContentsApi.getLatestContentsInfo(contsSn, contentsVersion), enabled: !!contentsVersion, }); @@ -59,21 +58,28 @@ function ModifyPage({ sequential }: { sequential: string }) { const { mutateAsync: updateContents } = useMutation({ mutationFn: (data: ContentsFormData) => - ContentsApi.updateContents( - Number(sequential), - contentsDetail?.contsVer ?? 0, - data, - ), + ContentsApi.updateContents(contsSn, contentsDetail?.contsVer ?? 0, data), onSuccess: () => { alert("수정되었습니다."); }, }); + const { mutateAsync: deleteContents } = useMutation({ + mutationFn: () => ContentsApi.deleteContents(contsSn, contentsVersion), + onSuccess: () => { + alert("삭제되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, sequential], + }); + }, + }); + const form = useForm({ resolver: zodResolver(contentsFormSchema), values: contentsDetail, }); + const contsSn = Number(sequential); const isApprovedContents = contentsDetail?.aprvCd === CONTENTS_APPROVAL_STATUS.APPROVED; @@ -120,9 +126,10 @@ function ModifyPage({ sequential }: { sequential: string }) {
); From fa183f9d92d032371848997f17afb8c852100296 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Apr 2025 16:56:20 +0900 Subject: [PATCH 09/44] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EC=9A=94=EC=B2=AD=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/content/api/contents-service.ts | 9 +++ apps/cs/src/entities/content/index.ts | 2 +- .../content/model/contents-interface.ts | 15 +++-- apps/cs/src/pages/contents/ui/modify-page.tsx | 57 ++++++++++++++----- apps/cs/src/shared/ui/confirm-dialog.tsx | 4 +- 5 files changed, 66 insertions(+), 21 deletions(-) diff --git a/apps/cs/src/entities/content/api/contents-service.ts b/apps/cs/src/entities/content/api/contents-service.ts index 792ffd92e..355a04b1a 100644 --- a/apps/cs/src/entities/content/api/contents-service.ts +++ b/apps/cs/src/entities/content/api/contents-service.ts @@ -67,6 +67,15 @@ export class ContentsApiService implements ContentsService { `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, ); } + + public async requestApprove( + contsSn: number, + contsVer: number, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`, + ); + } } export const ContentsApi = new ContentsApiService(); diff --git a/apps/cs/src/entities/content/index.ts b/apps/cs/src/entities/content/index.ts index 6bf6ebd45..2446dd716 100644 --- a/apps/cs/src/entities/content/index.ts +++ b/apps/cs/src/entities/content/index.ts @@ -1,6 +1,6 @@ export { ContentsApi } from "./api/contents-service"; export { - CONTENTS_APPROVAL_STATUS, + CONTENTS_APPROVE_STATUS, contentsFormSchema, type Contents, type ContentsParams, diff --git a/apps/cs/src/entities/content/model/contents-interface.ts b/apps/cs/src/entities/content/model/contents-interface.ts index b77c28742..0048b0906 100644 --- a/apps/cs/src/entities/content/model/contents-interface.ts +++ b/apps/cs/src/entities/content/model/contents-interface.ts @@ -1,14 +1,14 @@ import { z } from "zod"; import type { PageableList, PageableParams } from "@/shared/model"; -export const CONTENTS_APPROVAL_STATUS = { +export const CONTENTS_APPROVE_STATUS = { DRAFT: "01", REQUESTED: "02", APPROVED: "99", } as const; -export type ContentsApprovalStatus = - (typeof CONTENTS_APPROVAL_STATUS)[keyof typeof CONTENTS_APPROVAL_STATUS]; +export type ContentsApproveStatus = + (typeof CONTENTS_APPROVE_STATUS)[keyof typeof CONTENTS_APPROVE_STATUS]; export interface ContentsParams extends PageableParams { contsTtl?: string; @@ -19,7 +19,7 @@ export interface ContentsParams extends PageableParams { export interface ContentsVersionParams extends PageableParams { contsSn?: number; contsCn?: string; - aprvCd?: string; + aprvCd?: ContentsApproveStatus; cclCd?: string; koglCd?: string; tag?: string; @@ -54,7 +54,11 @@ export const contentsDetailSchema = z.object({ contsTtl: z.string(), contsCn: z.string(), contsRmrk: z.string().nullish(), - aprvCd: z.string(), + aprvCd: z.enum([ + CONTENTS_APPROVE_STATUS.DRAFT, + CONTENTS_APPROVE_STATUS.REQUESTED, + CONTENTS_APPROVE_STATUS.APPROVED, + ]), cclCd: z.string().nullish(), koglCd: z.string().nullish(), tag: z.string().nullish(), @@ -91,4 +95,5 @@ export interface ContentsService { data: ContentsFormData, ): Promise; deleteContents(contsSn: number, contsVer: number): Promise; + requestApprove(contsSn: number, contsVer: number): Promise; } diff --git a/apps/cs/src/pages/contents/ui/modify-page.tsx b/apps/cs/src/pages/contents/ui/modify-page.tsx index ab11d62ee..82738dfff 100644 --- a/apps/cs/src/pages/contents/ui/modify-page.tsx +++ b/apps/cs/src/pages/contents/ui/modify-page.tsx @@ -1,13 +1,14 @@ "use client"; import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { - CONTENTS_APPROVAL_STATUS, + CONTENTS_APPROVE_STATUS, ContentsApi, contentsFormSchema, type ContentsFormData, @@ -19,9 +20,13 @@ import { SelectValue, SelectContent, SelectItem, + Button, } from "@common/components/ui"; function ModifyPage({ sequential }: { sequential: string }) { + const contsSn = Number(sequential); + + const { confirm } = useConfirm(); const queryClient = useQueryClient(); const { setMessage: alert } = useAlertStore((state) => state); @@ -31,7 +36,7 @@ function ModifyPage({ sequential }: { sequential: string }) { >({}); const { data: contentsVersionList } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, sequential], + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], queryFn: () => ContentsApi.getContentsVersionList({ contsSn: contsSn, @@ -51,14 +56,14 @@ function ModifyPage({ sequential }: { sequential: string }) { onSuccess: () => { alert("수정되었습니다."); queryClient.invalidateQueries({ - queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, sequential], + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], }); }, }); const { mutateAsync: updateContents } = useMutation({ mutationFn: (data: ContentsFormData) => - ContentsApi.updateContents(contsSn, contentsDetail?.contsVer ?? 0, data), + ContentsApi.updateContents(contsSn, contentsVersion, data), onSuccess: () => { alert("수정되었습니다."); }, @@ -69,7 +74,17 @@ function ModifyPage({ sequential }: { sequential: string }) { onSuccess: () => { alert("삭제되었습니다."); queryClient.invalidateQueries({ - queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, sequential], + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], + }); + }, + }); + + const { mutateAsync: requestApprove } = useMutation({ + mutationFn: () => ContentsApi.requestApprove(contsSn, contentsVersion), + onSuccess: () => { + alert("승인 요청이 완료되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contentsVersion], }); }, }); @@ -79,9 +94,18 @@ function ModifyPage({ sequential }: { sequential: string }) { values: contentsDetail, }); - const contsSn = Number(sequential); + const isDraftContents = + contentsDetail?.aprvCd === CONTENTS_APPROVE_STATUS.DRAFT; + const isRequestedContents = + contentsDetail?.aprvCd === CONTENTS_APPROVE_STATUS.REQUESTED; const isApprovedContents = - contentsDetail?.aprvCd === CONTENTS_APPROVAL_STATUS.APPROVED; + contentsDetail?.aprvCd === CONTENTS_APPROVE_STATUS.APPROVED; + + const onRequestApprove = async () => { + const message = + "승인 요청하면 더이상 내용을 수정할 수 없습니다.\n요청하시겠습니까?"; + if (await confirm(message)) requestApprove(); + }; useEffect(() => { if (!contentsVersionList?.content?.length) return; @@ -89,11 +113,7 @@ function ModifyPage({ sequential }: { sequential: string }) { const latestContent = contentsVersionList?.content[0]; if (!latestContent) return; - if (latestContent?.contsVer > 1) { - setDisabledFields({ - contsTtl: true, - }); - } + if (latestContent?.contsVer > 1) setDisabledFields({ contsTtl: true }); setContentsVersion(latestContent?.contsVer ?? 0); }, [contentsVersionList?.content]); @@ -101,7 +121,7 @@ function ModifyPage({ sequential }: { sequential: string }) {

템플릿 수정

-
+
+ {isDraftContents && ( + + )} + {isRequestedContents && ( + <> + + + + )}
-

{message}

+

+ {message} +

From 8927fb88b45853fd11ae42a996c6cdb92d232d12 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 18 Apr 2025 18:19:36 +0900 Subject: [PATCH 10/44] =?UTF-8?q?feat:=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/content/api/contents-service.ts | 9 +++ .../content/model/contents-interface.ts | 1 + apps/cs/src/entities/editor/ui/editor.tsx | 1 + .../src/pages/contents/ui/contents-form.tsx | 13 +++- apps/cs/src/pages/contents/ui/modify-page.tsx | 71 +++++++++++-------- 5 files changed, 64 insertions(+), 31 deletions(-) diff --git a/apps/cs/src/entities/content/api/contents-service.ts b/apps/cs/src/entities/content/api/contents-service.ts index 355a04b1a..00d2a8c67 100644 --- a/apps/cs/src/entities/content/api/contents-service.ts +++ b/apps/cs/src/entities/content/api/contents-service.ts @@ -76,6 +76,15 @@ export class ContentsApiService implements ContentsService { `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`, ); } + + public async approveContents( + contsSn: number, + contsVer: number, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`, + ); + } } export const ContentsApi = new ContentsApiService(); diff --git a/apps/cs/src/entities/content/model/contents-interface.ts b/apps/cs/src/entities/content/model/contents-interface.ts index 0048b0906..72456149b 100644 --- a/apps/cs/src/entities/content/model/contents-interface.ts +++ b/apps/cs/src/entities/content/model/contents-interface.ts @@ -96,4 +96,5 @@ export interface ContentsService { ): Promise; deleteContents(contsSn: number, contsVer: number): Promise; requestApprove(contsSn: number, contsVer: number): Promise; + approveContents(contsSn: number, contsVer: number): Promise; } diff --git a/apps/cs/src/entities/editor/ui/editor.tsx b/apps/cs/src/entities/editor/ui/editor.tsx index b5ef02153..797a6c1bf 100644 --- a/apps/cs/src/entities/editor/ui/editor.tsx +++ b/apps/cs/src/entities/editor/ui/editor.tsx @@ -49,6 +49,7 @@ const Editor = ({ ...props }: IAllProps) => { }} init={{ ...baseConfig.init, + disabled: props.disabled, images_upload_handler: imagesUploadHandler, file_picker_callback: filePickerCallback, }} diff --git a/apps/cs/src/pages/contents/ui/contents-form.tsx b/apps/cs/src/pages/contents/ui/contents-form.tsx index d61084646..823d30173 100644 --- a/apps/cs/src/pages/contents/ui/contents-form.tsx +++ b/apps/cs/src/pages/contents/ui/contents-form.tsx @@ -27,7 +27,8 @@ const Editor = dynamic( interface ContentsFormProps { form: UseFormReturn; contsSn?: number; - disabledFields?: Partial>; + isEditable?: boolean; + isContsTtlDisabled?: boolean; handleSave: (data: ContentsFormData) => Promise; handleDelete?: () => Promise; } @@ -35,7 +36,8 @@ interface ContentsFormProps { function ContentsForm({ form, contsSn, - disabledFields, + isEditable, + isContsTtlDisabled, ...props }: ContentsFormProps) { const { data: cclCode } = useQuery({ @@ -71,7 +73,7 @@ function ContentsForm({ value={field.value ?? ""} className="!text-[1.3rem]" placeholder="콘텐츠 제목을 입력하십시오." - disabled={disabledFields?.contsTtl} + disabled={!isEditable || isContsTtlDisabled} /> @@ -91,6 +93,7 @@ function ContentsForm({ @@ -112,6 +115,7 @@ function ContentsForm({ name="cclCd" value={field.value ?? ""} onValueChange={field.onChange} + disabled={!isEditable} >
@@ -162,6 +166,7 @@ function ContentsForm({ name="koglCd" value={field.value ?? ""} onValueChange={field.onChange} + disabled={!isEditable} >
@@ -214,6 +219,7 @@ function ContentsForm({ value={field.value ?? ""} className="!text-[1.3rem]" placeholder="태그를 입력하십시오." + disabled={!isEditable} /> @@ -235,6 +241,7 @@ function ContentsForm({ name="useYn" value={field.value ?? ""} onValueChange={field.onChange} + disabled={!isEditable} >
diff --git a/apps/cs/src/pages/contents/ui/modify-page.tsx b/apps/cs/src/pages/contents/ui/modify-page.tsx index 82738dfff..e4b765f24 100644 --- a/apps/cs/src/pages/contents/ui/modify-page.tsx +++ b/apps/cs/src/pages/contents/ui/modify-page.tsx @@ -24,16 +24,12 @@ import { } from "@common/components/ui"; function ModifyPage({ sequential }: { sequential: string }) { - const contsSn = Number(sequential); - const { confirm } = useConfirm(); const queryClient = useQueryClient(); const { setMessage: alert } = useAlertStore((state) => state); - const [contentsVersion, setContentsVersion] = useState(0); - const [disabledFields, setDisabledFields] = useState< - Partial> - >({}); + const contsSn = Number(sequential); + const [contsVer, setContsVer] = useState(0); const { data: contentsVersionList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], @@ -46,14 +42,15 @@ function ModifyPage({ sequential }: { sequential: string }) { }); const { data: contentsDetail } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contentsVersion], - queryFn: () => ContentsApi.getLatestContentsInfo(contsSn, contentsVersion), - enabled: !!contentsVersion, + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsVer], + queryFn: () => ContentsApi.getLatestContentsInfo(contsSn, contsVer), + enabled: !!contsVer, }); const { mutateAsync: createContents } = useMutation({ mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), onSuccess: () => { + setContsVer(0); alert("수정되었습니다."); queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], @@ -63,15 +60,16 @@ function ModifyPage({ sequential }: { sequential: string }) { const { mutateAsync: updateContents } = useMutation({ mutationFn: (data: ContentsFormData) => - ContentsApi.updateContents(contsSn, contentsVersion, data), + ContentsApi.updateContents(contsSn, contsVer, data), onSuccess: () => { alert("수정되었습니다."); }, }); const { mutateAsync: deleteContents } = useMutation({ - mutationFn: () => ContentsApi.deleteContents(contsSn, contentsVersion), + mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer), onSuccess: () => { + setContsVer(0); alert("삭제되었습니다."); queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], @@ -80,11 +78,21 @@ function ModifyPage({ sequential }: { sequential: string }) { }); const { mutateAsync: requestApprove } = useMutation({ - mutationFn: () => ContentsApi.requestApprove(contsSn, contentsVersion), + mutationFn: () => ContentsApi.requestApprove(contsSn, contsVer), onSuccess: () => { alert("승인 요청이 완료되었습니다."); queryClient.invalidateQueries({ - queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contentsVersion], + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS], + }); + }, + }); + + const { mutateAsync: approveContents } = useMutation({ + mutationFn: () => ContentsApi.approveContents(contsSn, contsVer), + onSuccess: () => { + alert("승인되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS], }); }, }); @@ -94,12 +102,9 @@ function ModifyPage({ sequential }: { sequential: string }) { values: contentsDetail, }); - const isDraftContents = - contentsDetail?.aprvCd === CONTENTS_APPROVE_STATUS.DRAFT; - const isRequestedContents = - contentsDetail?.aprvCd === CONTENTS_APPROVE_STATUS.REQUESTED; - const isApprovedContents = - contentsDetail?.aprvCd === CONTENTS_APPROVE_STATUS.APPROVED; + const approveStatus = contentsDetail?.aprvCd; + const isContsTtlDisabled = + (contentsVersionList?.content?.[0]?.contsVer ?? 0) > 1; const onRequestApprove = async () => { const message = @@ -107,14 +112,19 @@ function ModifyPage({ sequential }: { sequential: string }) { if (await confirm(message)) requestApprove(); }; + const onApprove = async () => { + const message = "승인하시겠습니까?"; + if (await confirm(message)) approveContents(); + }; + useEffect(() => { + if (contsVer) return; if (!contentsVersionList?.content?.length) return; const latestContent = contentsVersionList?.content[0]; if (!latestContent) return; - if (latestContent?.contsVer > 1) setDisabledFields({ contsTtl: true }); - setContentsVersion(latestContent?.contsVer ?? 0); + setContsVer(latestContent?.contsVer ?? 0); }, [contentsVersionList?.content]); return ( @@ -123,9 +133,9 @@ function ModifyPage({ sequential }: { sequential: string }) {

템플릿 수정

- {isDraftContents && ( + {approveStatus === CONTENTS_APPROVE_STATUS.DRAFT && ( )} - {isRequestedContents && ( + {approveStatus === CONTENTS_APPROVE_STATUS.REQUESTED && ( <> - + )} @@ -156,8 +166,13 @@ function ModifyPage({ sequential }: { sequential: string }) {
From 665c8fd7aeda0787ebda3c764b0fb98271452414 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 21 Apr 2025 10:07:47 +0900 Subject: [PATCH 11/44] =?UTF-8?q?chore:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/menu/lib/utils.ts | 6 +++--- apps/cs/src/pages/menu/model/organization-interface.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index b1dbafe5f..1c31a1f92 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -112,8 +112,8 @@ export const generateOrganizationTreeData = ( ): NodeModel[] => { return organizations.reduce[]>((acc, org) => { const orgNode: NodeModel = { - id: org.ognzId, - parent: org.upOgnzId ?? 0, + id: org.ognzSn, + parent: org.upOgnzSn ?? 0, text: org.ognzNm, data: org, droppable: false, @@ -129,7 +129,7 @@ export const generateOrganizationTreeData = ( acc.push( ...org.users.map((user) => ({ id: user.userUnqId, - parent: org.ognzId, + parent: org.ognzSn, text: user.userNm, data: user, droppable: false, diff --git a/apps/cs/src/pages/menu/model/organization-interface.ts b/apps/cs/src/pages/menu/model/organization-interface.ts index 86a8b8e31..082437110 100644 --- a/apps/cs/src/pages/menu/model/organization-interface.ts +++ b/apps/cs/src/pages/menu/model/organization-interface.ts @@ -6,8 +6,8 @@ export interface User { } export interface Organization { - ognzId: string; - upOgnzId: string; + ognzSn: number; + upOgnzSn: number; ognzNm: string; users: User[]; children: Organization[]; From 6290634171f9b3c46fe3a7179bc6fdf796cf7a25 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 21 Apr 2025 10:44:30 +0900 Subject: [PATCH 12/44] =?UTF-8?q?chore:=20organization=20->=20user-organiz?= =?UTF-8?q?ation=EC=9C=BC=EB=A1=9C=20=EB=A6=AC=EB=84=A4=EC=9D=B4=EB=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/menu/api/organization-service.ts | 17 ----- .../menu/api/user-organization-service.ts | 17 +++++ apps/cs/src/pages/menu/lib/utils.ts | 72 ++++++++++--------- ...face.ts => user-organization-interface.ts} | 8 +-- .../menu/ui/organization-search-dialog.tsx | 19 ++--- apps/cs/src/pages/menu/ui/type-menu-form.tsx | 2 +- 6 files changed, 72 insertions(+), 63 deletions(-) delete mode 100644 apps/cs/src/pages/menu/api/organization-service.ts create mode 100644 apps/cs/src/pages/menu/api/user-organization-service.ts rename apps/cs/src/pages/menu/model/{organization-interface.ts => user-organization-interface.ts} (51%) diff --git a/apps/cs/src/pages/menu/api/organization-service.ts b/apps/cs/src/pages/menu/api/organization-service.ts deleted file mode 100644 index 9409f81b5..000000000 --- a/apps/cs/src/pages/menu/api/organization-service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { - Organization, - OrganizationService, -} from "../model/organization-interface"; - -export class OrganizationApiService implements OrganizationService { - public async getOrganizationUserTreeList(): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`, - ); - return response.data; - } -} - -export const OrganizationApi = new OrganizationApiService(); diff --git a/apps/cs/src/pages/menu/api/user-organization-service.ts b/apps/cs/src/pages/menu/api/user-organization-service.ts new file mode 100644 index 000000000..daa6e2ffe --- /dev/null +++ b/apps/cs/src/pages/menu/api/user-organization-service.ts @@ -0,0 +1,17 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + UserOrganization, + UserOrganizationService, +} from "../model/user-organization-interface"; + +export class UserOrganizationApiService implements UserOrganizationService { + public async getUserOrganizationTreeList(): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`, + ); + return response.data; + } +} + +export const UserOrganizationApi = new UserOrganizationApiService(); diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index 1c31a1f92..274a0a057 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -1,6 +1,9 @@ import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; import type { TreeMenu, TreeNode } from "../model/menu-interface"; -import type { Organization, User } from "../model/organization-interface"; +import type { + UserOrganization, + User, +} from "../model/user-organization-interface"; export const generateMenuTreeData = ( data: TreeMenu[], @@ -107,44 +110,47 @@ export const handleNodeTextChange = callback(newTree); }; -export const generateOrganizationTreeData = ( - organizations: Organization[], -): NodeModel[] => { - return organizations.reduce[]>((acc, org) => { - const orgNode: NodeModel = { - id: org.ognzSn, - parent: org.upOgnzSn ?? 0, - text: org.ognzNm, - data: org, - droppable: false, - }; +export const generateUserOrganizationTreeData = ( + organizations: UserOrganization[], +): NodeModel[] => { + return organizations.reduce[]>( + (acc, org) => { + const orgNode: NodeModel = { + id: org.ognzSn, + parent: org.upOgnzSn ?? 0, + text: org.ognzNm, + data: org, + droppable: false, + }; - acc.push(orgNode); + acc.push(orgNode); - if (org.children?.length) { - acc.push(...generateOrganizationTreeData(org.children)); - } + if (org.children?.length) { + acc.push(...generateUserOrganizationTreeData(org.children)); + } - if (org.users?.length) { - acc.push( - ...org.users.map((user) => ({ - id: user.userUnqId, - parent: org.ognzSn, - text: user.userNm, - data: user, - droppable: false, - })), - ); - } + if (org.users?.length) { + acc.push( + ...org.users.map((user) => ({ + id: user.userUnqId, + parent: org.ognzSn, + text: user.userNm, + data: user, + droppable: false, + })), + ); + } - return acc; - }, []); + return acc; + }, + [], + ); }; -export const filterOrganizationTreeDataByKeyword = ( - treeData: NodeModel[], +export const filterUserOrganizationTreeDataByKeyword = ( + treeData: NodeModel[], keyword: string, -): NodeModel[] => { +): NodeModel[] => { return Array.from( treeData.reduce((matchedNodes, node) => { if (!node.text.includes(keyword)) return matchedNodes; @@ -160,6 +166,6 @@ export const filterOrganizationTreeDataByKeyword = ( } return matchedNodes; - }, new Set>()), + }, new Set>()), ); }; diff --git a/apps/cs/src/pages/menu/model/organization-interface.ts b/apps/cs/src/pages/menu/model/user-organization-interface.ts similarity index 51% rename from apps/cs/src/pages/menu/model/organization-interface.ts rename to apps/cs/src/pages/menu/model/user-organization-interface.ts index 082437110..c788b44e0 100644 --- a/apps/cs/src/pages/menu/model/organization-interface.ts +++ b/apps/cs/src/pages/menu/model/user-organization-interface.ts @@ -5,14 +5,14 @@ export interface User { jbgdSeCd: string; } -export interface Organization { +export interface UserOrganization { ognzSn: number; upOgnzSn: number; ognzNm: string; users: User[]; - children: Organization[]; + children: UserOrganization[]; } -export interface OrganizationService { - getOrganizationUserTreeList(): Promise; +export interface UserOrganizationService { + getUserOrganizationTreeList(): Promise; } diff --git a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx index ed825508c..41749b26b 100644 --- a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx @@ -20,14 +20,17 @@ import { } from "@common/components/ui"; import { cn } from "@common/components/lib"; import { ENDPOINT } from "@/shared/config"; -import { OrganizationApi } from "../api/organization-service"; -import type { Organization, User } from "../model/organization-interface"; +import { UserOrganizationApi } from "../api/user-organization-service"; +import type { + UserOrganization, + User, +} from "../model/user-organization-interface"; import { - filterOrganizationTreeDataByKeyword, - generateOrganizationTreeData, + filterUserOrganizationTreeDataByKeyword, + generateUserOrganizationTreeData, } from "../lib/utils"; -type TreeModel = Organization | User; +type TreeModel = UserOrganization | User; interface OrganizationSearchDialogProps { handleUserSelect: (user: User) => void; @@ -44,8 +47,8 @@ function OrganizationSearchDialog({ const { data: treeData } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION], - queryFn: () => OrganizationApi.getOrganizationUserTreeList(), - select: (data) => generateOrganizationTreeData(data), + queryFn: () => UserOrganizationApi.getUserOrganizationTreeList(), + select: (data) => generateUserOrganizationTreeData(data), }); const handleSelectNode = (node: NodeModel) => { @@ -79,7 +82,7 @@ function OrganizationSearchDialog({ + +
+
+ ( + + additionalNodeForm.setValue("ognzNm", e.target.value) + } + onSave={onSaveAdditionalNode} + onCancel={resetAdditionalNode} + {...props} + /> + )} + handleSelectNode={handleNodeSelect} + handleDropNode={() => {}} + updateNodeName={async () => {}} + /> +
+
+
+
+ ); +} + +export { OrganizationPage }; diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 1a77bbb10..49379f521 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -27,6 +27,7 @@ export const ENDPOINT = { MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", CONTENTS: "/cms-service/api/v1/contents", CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", IMAGES: "/cms-service/api/v1/images", BANNERS: "/cms-service/api/v1/bnrs", diff --git a/apps/cs/src/widgets/dnd-tree/index.ts b/apps/cs/src/widgets/dnd-tree/index.ts new file mode 100644 index 000000000..ca1c20f8d --- /dev/null +++ b/apps/cs/src/widgets/dnd-tree/index.ts @@ -0,0 +1,2 @@ +export { DndTree } from "./ui/dnd-tree"; +export { EditableNode } from "./ui/editable-node"; diff --git a/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx b/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx new file mode 100644 index 000000000..b909f1bfb --- /dev/null +++ b/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx @@ -0,0 +1,103 @@ +import { useEffect, useState } from "react"; +import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview"; +import { Button } from "@common/components/ui"; +import { cn } from "@common/components/lib"; +import { EditableNode } from "./editable-node"; + +interface TreeData extends Record { + children: unknown[]; +} + +type CustomNodeProps = { + node: NodeModel; + depth: number; + isOpen: boolean; + isSelected: boolean; + isAdditionalParent: boolean; + treeMethods: TreeMethods | null; + onToggle: (id: NodeModel["id"]) => void; + onSelect: (node: NodeModel) => void; + updateNodeName: (nodeName: string) => Promise; +}; + +function CustomNode({ + node, + depth, + isOpen, + isSelected, + isAdditionalParent, + treeMethods, + onToggle, + onSelect, + updateNodeName, +}: CustomNodeProps) { + const { id, text, data } = node; + const indent = depth * 10 + 5; + + const [editMode, setEditMode] = useState(false); + const [nodeName, setNodeName] = useState(text); + + const handleToggle = () => onToggle(id); + const handleSelect = () => onSelect(node); + + const handleSave = async () => { + await updateNodeName(nodeName); + setEditMode(false); + }; + + const handleCancel = () => { + setEditMode(false); + setNodeName(text); + }; + + useEffect(() => { + if (isAdditionalParent) treeMethods?.open(id); + }, [isAdditionalParent]); + + return ( +
+ {editMode ? ( + setNodeName(e.target.value)} + onSave={handleSave} + onCancel={handleCancel} + /> + ) : ( + <> +
+ {data && data.children.length > 0 && ( + { + e.stopPropagation(); + handleToggle(); + }} + /> + )} + {text} +
+ + + )} +
+ ); +} + +export { CustomNode }; diff --git a/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx b/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx new file mode 100644 index 000000000..98386f09c --- /dev/null +++ b/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx @@ -0,0 +1,123 @@ +"use client"; + +import React, { type ReactElement, useRef } from "react"; +import { DndProvider } from "react-dnd"; +import { + Tree, + MultiBackend, + getBackendOptions, + type NodeModel, + type TreeMethods, + type DropOptions, +} from "@minoru/react-dnd-treeview"; +import { Button } from "@common/components/ui"; +import { CustomNode } from "./custom-node"; + +interface TreeData extends Record { + children: unknown[]; +} + +type EditableNodeProps = { depth: number }; + +type TreeProps = { + tree: NodeModel[]; + selectedNodeId: number; + renderEditableNode: (props: EditableNodeProps) => ReactElement; + handleSelectNode: (node: NodeModel) => void; + handleDropNode: (newTree: NodeModel[], options: DropOptions) => void; + updateNodeName: (nodeName: string) => Promise; +}; + +function DndTree({ + tree, + selectedNodeId, + renderEditableNode, + handleSelectNode, + handleDropNode, + updateNodeName, +}: TreeProps) { + const treeMethods = useRef(null); + + const isAdditionalParent = (node: NodeModel): boolean => { + const additionalNode = tree.find((n) => n.id === -1); + if (!additionalNode) return false; + return additionalNode?.parent === node.id; + }; + + return ( + <> +
+ + , { depth, isOpen, onToggle }) => { + if (node.id === -1) return renderEditableNode({ depth }); + return ( + + ); + }} + onDrop={handleDropNode} + classes={{ + root: "p-4", + container: "flex flex-col", + dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm", + placeholder: "relative", + }} + sort={false} + dropTargetOffset={5} + insertDroppableFirst={false} + canDrop={(_, { dragSource, dropTargetId }) => { + if (dragSource?.parent === dropTargetId) { + return true; + } + }} + placeholderRender={(_, { depth }) => { + return ( +
+ ); + }} + /> + +
+
+ + +
+ + ); +} + +export { DndTree }; diff --git a/apps/cs/src/widgets/dnd-tree/ui/editable-node.tsx b/apps/cs/src/widgets/dnd-tree/ui/editable-node.tsx new file mode 100644 index 000000000..9ada835e2 --- /dev/null +++ b/apps/cs/src/widgets/dnd-tree/ui/editable-node.tsx @@ -0,0 +1,44 @@ +import type { ComponentProps } from "react"; +import { Button, Input } from "@common/components/ui"; + +type EditableNodeProps = { + depth?: number; + onSave: () => void; + onCancel: () => void; +} & ComponentProps; + +function EditableNode({ + depth, + onSave, + onCancel, + ...props +}: EditableNodeProps) { + return ( +
+ +
+ + +
+
+ ); +} + +export { EditableNode }; From e719834c800532bb8adcc031686d57f862bf7c4b Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 21 Apr 2025 18:35:13 +0900 Subject: [PATCH 14/44] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/api/organization-service.ts | 11 +++++++++++ .../organization/model/organization-interface.ts | 1 + .../pages/organization/ui/organization-page.tsx | 15 ++++++++++++++- apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx | 4 ++-- apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx | 2 +- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apps/cs/src/entities/organization/api/organization-service.ts b/apps/cs/src/entities/organization/api/organization-service.ts index 3a362df19..ba66c5b76 100644 --- a/apps/cs/src/entities/organization/api/organization-service.ts +++ b/apps/cs/src/entities/organization/api/organization-service.ts @@ -1,6 +1,7 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import { + type Organization, type OrganizationFormData, type OrganizationService, type OrganizationTree, @@ -28,6 +29,16 @@ export class OrganizationApiService implements OrganizationService { `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, ); } + + public async updateOrganizationName( + ognzSn: number, + name: string, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}/${name}`, + ); + return response.data; + } } export const OrganizationApi = new OrganizationApiService(); diff --git a/apps/cs/src/entities/organization/model/organization-interface.ts b/apps/cs/src/entities/organization/model/organization-interface.ts index ad8167708..d22ce2dc1 100644 --- a/apps/cs/src/entities/organization/model/organization-interface.ts +++ b/apps/cs/src/entities/organization/model/organization-interface.ts @@ -23,4 +23,5 @@ export interface OrganizationService { getOrganizationsTreeList(): Promise; createOrganization(organization: OrganizationFormData): Promise; deleteOrganization(ognzSn: number): Promise; + updateOrganizationName(ognzSn: number, name: string): Promise; } diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index 5a767ba6b..a7766b756 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -55,6 +55,17 @@ function OrganizationPage() { }, }); + const { mutateAsync: updateOrganizationName } = useMutation({ + mutationFn: ({ ognzSn, name }: { ognzSn: number; name: string }) => + OrganizationApi.updateOrganizationName(ognzSn, name), + onSuccess: () => { + alert("저장되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + }); + }, + }); + const additionalNodeForm = useForm({ resolver: zodResolver(organizationFormSchema), defaultValues: { @@ -145,7 +156,9 @@ function OrganizationPage() { )} handleSelectNode={handleNodeSelect} handleDropNode={() => {}} - updateNodeName={async () => {}} + updateNodeName={async (id, name) => { + await updateOrganizationName({ ognzSn: id as number, name }); + }} />
diff --git a/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx b/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx index b909f1bfb..ccd5ce75e 100644 --- a/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx +++ b/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx @@ -17,7 +17,7 @@ type CustomNodeProps = { treeMethods: TreeMethods | null; onToggle: (id: NodeModel["id"]) => void; onSelect: (node: NodeModel) => void; - updateNodeName: (nodeName: string) => Promise; + updateNodeName: (id: NodeModel["id"], nodeName: string) => Promise; }; function CustomNode({ @@ -41,7 +41,7 @@ function CustomNode({ const handleSelect = () => onSelect(node); const handleSave = async () => { - await updateNodeName(nodeName); + await updateNodeName(id, nodeName); setEditMode(false); }; diff --git a/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx b/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx index 98386f09c..1a0a36816 100644 --- a/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx +++ b/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx @@ -25,7 +25,7 @@ type TreeProps = { renderEditableNode: (props: EditableNodeProps) => ReactElement; handleSelectNode: (node: NodeModel) => void; handleDropNode: (newTree: NodeModel[], options: DropOptions) => void; - updateNodeName: (nodeName: string) => Promise; + updateNodeName: (id: NodeModel["id"], nodeName: string) => Promise; }; function DndTree({ From c6c5eff11ef2d5bd67e38f75c29423d838dac548 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 21 Apr 2025 19:36:21 +0900 Subject: [PATCH 15/44] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=ED=8F=BC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/api/organization-service.ts | 24 ++- .../model/organization-interface.ts | 4 +- .../organization/ui/organization-form.tsx | 200 ++++++++++++++++++ .../organization/ui/organization-page.tsx | 53 ++++- apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx | 16 +- 5 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 apps/cs/src/pages/organization/ui/organization-form.tsx diff --git a/apps/cs/src/entities/organization/api/organization-service.ts b/apps/cs/src/entities/organization/api/organization-service.ts index ba66c5b76..3f8701045 100644 --- a/apps/cs/src/entities/organization/api/organization-service.ts +++ b/apps/cs/src/entities/organization/api/organization-service.ts @@ -15,13 +15,15 @@ export class OrganizationApiService implements OrganizationService { return response.data; } - public async createOrganization( - organization: OrganizationFormData, - ): Promise { - await client.post( - `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, - organization, + public async getOrganizationInfo(ognzSn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, ); + return response.data; + } + + public async createOrganization(form: OrganizationFormData): Promise { + await client.post(`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, form); } public async deleteOrganization(ognzSn: number): Promise { @@ -39,6 +41,16 @@ export class OrganizationApiService implements OrganizationService { ); return response.data; } + + public async updateOrganization( + ognzSn: number, + form: OrganizationFormData, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, + form, + ); + } } export const OrganizationApi = new OrganizationApiService(); diff --git a/apps/cs/src/entities/organization/model/organization-interface.ts b/apps/cs/src/entities/organization/model/organization-interface.ts index d22ce2dc1..755bcd77f 100644 --- a/apps/cs/src/entities/organization/model/organization-interface.ts +++ b/apps/cs/src/entities/organization/model/organization-interface.ts @@ -21,7 +21,9 @@ export type OrganizationTree = Organization & { export interface OrganizationService { getOrganizationsTreeList(): Promise; - createOrganization(organization: OrganizationFormData): Promise; + getOrganizationInfo(ognzSn: number): Promise; + createOrganization(form: OrganizationFormData): Promise; deleteOrganization(ognzSn: number): Promise; updateOrganizationName(ognzSn: number, name: string): Promise; + updateOrganization(ognzSn: number, form: OrganizationFormData): Promise; } diff --git a/apps/cs/src/pages/organization/ui/organization-form.tsx b/apps/cs/src/pages/organization/ui/organization-form.tsx new file mode 100644 index 000000000..9ff0fac22 --- /dev/null +++ b/apps/cs/src/pages/organization/ui/organization-form.tsx @@ -0,0 +1,200 @@ +"use client"; + +import type { UseFormReturn } from "react-hook-form"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import type { OrganizationFormData } from "@/entities/organization"; +import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; + +type OrganizationFormProps = { + form: UseFormReturn; + ognzSn?: number; + handleCreate: (data: OrganizationFormData) => Promise; + handleUpdate: ({ + ognzSn, + data, + }: { + ognzSn: number; + data: OrganizationFormData; + }) => Promise; + handleDelete?: (ognzSn: number) => Promise; +}; + +function OrganizationForm({ + form, + ognzSn, + handleCreate, + handleUpdate, + handleDelete, +}: OrganizationFormProps) { + const isModifyMode = !!ognzSn; + + const { confirm } = useConfirm(); + + const onSubmit = async (data: OrganizationFormData) => { + if (isModifyMode) { + await onUpdate(data); + } else { + await onCreate(data); + } + }; + + const onCreate = async (data: OrganizationFormData) => { + if (await confirm("저장하시겠습니까?")) { + handleCreate(data); + } + }; + + const onUpdate = async (data: OrganizationFormData) => { + if (await confirm("수정하시겠습니까?")) { + handleUpdate({ ognzSn: ognzSn!, data }); + } + }; + + const onDelete = async () => { + if (await confirm("삭제하시겠습니까?")) { + handleDelete?.(ognzSn!); + } + }; + + return ( +
+ +
+ + + + + + + + 조직명 + + ( + + + + + + )} + /> + + + + 조직설명 + + ( + + + + + + )} + /> + + + + 정렬순서 + + ( + + + + field.onChange(Number(e.target.value)) + } + className="!text-[1.3rem]" + placeholder="정렬순서를 입력해주세요" + /> + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ + +
+
+ + ); +} + +export { OrganizationForm }; diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index a7766b756..d2a6d8128 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -17,6 +17,7 @@ import { } from "@/entities/organization"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { OrganizationForm } from "./organization-form"; function OrganizationPage() { const queryClient = useQueryClient(); @@ -34,9 +35,19 @@ function OrganizationPage() { select: (data) => generateOrganizationTreeData(data), }); + const { data: organizationInfo } = useQuery({ + queryKey: [ + ENDPOINT.CMS_SERVICE.ORGANIZATIONS, + selectedOrganization?.ognzSn, + ], + queryFn: () => + OrganizationApi.getOrganizationInfo(selectedOrganization!.ognzSn!), + enabled: !!selectedOrganization?.ognzSn, + }); + const { mutateAsync: createOrganization } = useMutation({ - mutationFn: (organization: OrganizationFormData) => - OrganizationApi.createOrganization(organization), + mutationFn: (form: OrganizationFormData) => + OrganizationApi.createOrganization(form), onSuccess: () => { alert("저장되었습니다."); queryClient.invalidateQueries({ @@ -49,6 +60,7 @@ function OrganizationPage() { mutationFn: (ognzSn: number) => OrganizationApi.deleteOrganization(ognzSn), onSuccess: () => { alert("삭제되었습니다."); + setSelectedOrganization(null); queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], }); @@ -66,6 +78,32 @@ function OrganizationPage() { }, }); + const { mutateAsync: updateOrganization } = useMutation({ + mutationFn: ({ + ognzSn, + data, + }: { + ognzSn: number; + data: OrganizationFormData; + }) => OrganizationApi.updateOrganization(ognzSn, data), + onSuccess: () => { + alert("수정되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + }); + }, + }); + + const form = useForm({ + resolver: zodResolver(organizationFormSchema), + defaultValues: { + ognzNm: "", + sortSeq: 1, + useYn: "Y", + }, + values: organizationInfo, + }); + const additionalNodeForm = useForm({ resolver: zodResolver(organizationFormSchema), defaultValues: { @@ -159,10 +197,19 @@ function OrganizationPage() { updateNodeName={async (id, name) => { await updateOrganizationName({ ognzSn: id as number, name }); }} + classes={{ wrapper: "max-h-[calc(100vh-29rem)]" }} />
-
+
+ +
); } diff --git a/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx b/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx index 1a0a36816..c2aab22c0 100644 --- a/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx +++ b/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx @@ -11,6 +11,7 @@ import { type DropOptions, } from "@minoru/react-dnd-treeview"; import { Button } from "@common/components/ui"; +import { cn } from "@common/components/lib"; import { CustomNode } from "./custom-node"; interface TreeData extends Record { @@ -19,9 +20,12 @@ interface TreeData extends Record { type EditableNodeProps = { depth: number }; -type TreeProps = { +type DndTreeProps = { tree: NodeModel[]; selectedNodeId: number; + classes?: { + wrapper: string; + }; renderEditableNode: (props: EditableNodeProps) => ReactElement; handleSelectNode: (node: NodeModel) => void; handleDropNode: (newTree: NodeModel[], options: DropOptions) => void; @@ -31,11 +35,12 @@ type TreeProps = { function DndTree({ tree, selectedNodeId, + classes, renderEditableNode, handleSelectNode, handleDropNode, updateNodeName, -}: TreeProps) { +}: DndTreeProps) { const treeMethods = useRef(null); const isAdditionalParent = (node: NodeModel): boolean => { @@ -46,7 +51,12 @@ function DndTree({ return ( <> -
+
Date: Mon, 21 Apr 2025 19:44:12 +0900 Subject: [PATCH 16/44] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=ED=8F=BC=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/ui/organization-form.tsx | 239 +++++++++--------- .../organization/ui/organization-page.tsx | 7 +- 2 files changed, 130 insertions(+), 116 deletions(-) diff --git a/apps/cs/src/pages/organization/ui/organization-form.tsx b/apps/cs/src/pages/organization/ui/organization-form.tsx index 9ff0fac22..a0ab0e4e5 100644 --- a/apps/cs/src/pages/organization/ui/organization-form.tsx +++ b/apps/cs/src/pages/organization/ui/organization-form.tsx @@ -71,126 +71,135 @@ function OrganizationForm({ return (
-
- - - - - - - - 조직명 - - ( - - - - - - )} - /> - - - - 조직설명 - - ( - - - - - - )} - /> - - - - 정렬순서 - - ( - - - - field.onChange(Number(e.target.value)) - } - className="!text-[1.3rem]" - placeholder="정렬순서를 입력해주세요" - /> - - - )} - /> - - - - 사용여부 - - ( - - - -
- - -
-
- - -
-
-
-
- )} - /> -
-
-
-
+
+
+

+ {isModifyMode ? "조직도 수정" : "조직도 등록"} +

+
+
+ + + + + + + + 조직명 + + ( + + + + + + )} + /> + + + + 조직설명 + + ( + + + + + + )} + /> + + + + 정렬순서 + + ( + + + + field.onChange(Number(e.target.value)) + } + className="!text-[1.3rem]" + placeholder="정렬순서를 입력해주세요" + /> + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
- + {isModifyMode && ( + + )}
diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index d2a6d8128..96fe6da61 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -101,7 +101,12 @@ function OrganizationPage() { sortSeq: 1, useYn: "Y", }, - values: organizationInfo, + values: { + ognzNm: organizationInfo?.ognzNm ?? "", + ognzExpln: organizationInfo?.ognzExpln ?? "", + sortSeq: organizationInfo?.sortSeq ?? 1, + useYn: organizationInfo?.useYn ?? "Y", + }, }); const additionalNodeForm = useForm({ From 5d942f735ed0a8fcf5f7c5daf0fd16c3840292f4 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 21 Apr 2025 20:19:50 +0900 Subject: [PATCH 17/44] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=20dnd=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/organization/index.ts | 2 +- .../cs/src/entities/organization/lib/utils.ts | 56 ++++++++++++++++++- .../model/organization-interface.ts | 2 +- .../organization/ui/organization-page.tsx | 5 +- .../src/widgets/dnd-tree/ui/custom-node.tsx | 4 +- apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx | 2 +- 6 files changed, 63 insertions(+), 8 deletions(-) diff --git a/apps/cs/src/entities/organization/index.ts b/apps/cs/src/entities/organization/index.ts index e04563805..7a064d06c 100644 --- a/apps/cs/src/entities/organization/index.ts +++ b/apps/cs/src/entities/organization/index.ts @@ -4,4 +4,4 @@ export { type OrganizationTree, type OrganizationFormData, } from "./model/organization-interface"; -export { generateOrganizationTreeData } from "./lib/utils"; +export { generateOrganizationTreeData, handleDropNode } from "./lib/utils"; diff --git a/apps/cs/src/entities/organization/lib/utils.ts b/apps/cs/src/entities/organization/lib/utils.ts index d923fc35b..0022b44f1 100644 --- a/apps/cs/src/entities/organization/lib/utils.ts +++ b/apps/cs/src/entities/organization/lib/utils.ts @@ -1,4 +1,4 @@ -import type { NodeModel } from "@minoru/react-dnd-treeview"; +import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; import type { OrganizationTree } from "../model/organization-interface"; export const generateOrganizationTreeData = ( @@ -21,3 +21,57 @@ export const generateOrganizationTreeData = ( return acc; }, []); }; + +export const generateNewOrganizationTreeData = ( + treeData: NodeModel[], + options: DropOptions, +): OrganizationTree | null => { + const { dragSourceId, dragSource, dropTargetId } = options; + + if (!dragSource?.data) return null; + + const siblings = treeData.filter((node) => node.parent === dropTargetId); + + const dragSourceIndex = siblings.findIndex( + (node) => node.id === dragSourceId, + ); + + const prevSibling = siblings[dragSourceIndex - 1]; + + const newSortSeq = + dragSourceIndex === 0 ? 1 : (prevSibling?.data?.sortSeq ?? 0) + 1; + + const childNodes = treeData.filter( + (node) => node.parent === dragSourceId && node.data, + ); + + const treeNode: OrganizationTree = { + ...dragSource.data, + upOgnzSn: dropTargetId === 0 ? null : (dropTargetId as number), + sortSeq: newSortSeq, + }; + + const result: OrganizationTree = { + ...treeNode, + children: + childNodes?.map((child) => ({ + ...child.data, + ognzNm: child.data?.ognzNm ?? "", + upOgnzSn: child.data?.upOgnzSn === 0 ? null : (dragSourceId as number), + sortSeq: child.data?.sortSeq ?? 1, + })) ?? [], + }; + + return result; +}; + +export const handleDropNode = + (callback: (newTree: OrganizationTree) => void) => + ( + newTree: NodeModel[], + options: DropOptions, + ) => { + const newTreeData = generateNewOrganizationTreeData(newTree, options); + if (!newTreeData) return; + callback(newTreeData); + }; diff --git a/apps/cs/src/entities/organization/model/organization-interface.ts b/apps/cs/src/entities/organization/model/organization-interface.ts index 755bcd77f..0d25b0b09 100644 --- a/apps/cs/src/entities/organization/model/organization-interface.ts +++ b/apps/cs/src/entities/organization/model/organization-interface.ts @@ -16,7 +16,7 @@ export const organizationFormSchema = organizationSchema.omit({ export type Organization = z.infer; export type OrganizationFormData = z.infer; export type OrganizationTree = Organization & { - children: OrganizationTree[]; + children?: OrganizationTree[]; }; export interface OrganizationService { diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index 96fe6da61..4c8663f87 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -10,8 +10,9 @@ import { Button } from "@common/components/ui"; import { DndTree, EditableNode } from "@/widgets/dnd-tree"; import { OrganizationApi, - generateOrganizationTreeData, organizationFormSchema, + generateOrganizationTreeData, + handleDropNode, type OrganizationFormData, type OrganizationTree, } from "@/entities/organization"; @@ -198,7 +199,7 @@ function OrganizationPage() { /> )} handleSelectNode={handleNodeSelect} - handleDropNode={() => {}} + handleDropNode={handleDropNode(console.log)} updateNodeName={async (id, name) => { await updateOrganizationName({ ognzSn: id as number, name }); }} diff --git a/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx b/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx index ccd5ce75e..8e124be62 100644 --- a/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx +++ b/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx @@ -5,7 +5,7 @@ import { cn } from "@common/components/lib"; import { EditableNode } from "./editable-node"; interface TreeData extends Record { - children: unknown[]; + children?: unknown[]; } type CustomNodeProps = { @@ -72,7 +72,7 @@ function CustomNode({ ) : ( <>
- {data && data.children.length > 0 && ( + {data && data.children && data.children.length > 0 && ( { - children: unknown[]; + children?: unknown[]; } type EditableNodeProps = { depth: number }; From 837f448a7fce8b85aaa113d2a79e55e87d272f21 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 22 Apr 2025 09:48:28 +0900 Subject: [PATCH 18/44] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=20dnd=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/api/organization-service.ts | 18 +++++++--- apps/cs/src/entities/organization/index.ts | 1 + .../cs/src/entities/organization/lib/utils.ts | 35 ++++++++++--------- .../model/organization-interface.ts | 6 ++-- .../organization/ui/organization-page.tsx | 13 ++++++- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/apps/cs/src/entities/organization/api/organization-service.ts b/apps/cs/src/entities/organization/api/organization-service.ts index 3f8701045..456606076 100644 --- a/apps/cs/src/entities/organization/api/organization-service.ts +++ b/apps/cs/src/entities/organization/api/organization-service.ts @@ -5,6 +5,7 @@ import { type OrganizationFormData, type OrganizationService, type OrganizationTree, + type OrganizationTreeForDnd, } from "../model/organization-interface"; export class OrganizationApiService implements OrganizationService { @@ -22,8 +23,8 @@ export class OrganizationApiService implements OrganizationService { return response.data; } - public async createOrganization(form: OrganizationFormData): Promise { - await client.post(`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, form); + public async createOrganization(data: OrganizationFormData): Promise { + await client.post(`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, data); } public async deleteOrganization(ognzSn: number): Promise { @@ -44,11 +45,20 @@ export class OrganizationApiService implements OrganizationService { public async updateOrganization( ognzSn: number, - form: OrganizationFormData, + data: OrganizationFormData, ): Promise { await client.put( `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, - form, + data, + ); + } + + public async updateOrganizationByDnd( + data: OrganizationTreeForDnd, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/dnd/tree`, + data, ); } } diff --git a/apps/cs/src/entities/organization/index.ts b/apps/cs/src/entities/organization/index.ts index 7a064d06c..6b4cb0357 100644 --- a/apps/cs/src/entities/organization/index.ts +++ b/apps/cs/src/entities/organization/index.ts @@ -3,5 +3,6 @@ export { organizationFormSchema, type OrganizationTree, type OrganizationFormData, + type OrganizationTreeForDnd, } from "./model/organization-interface"; export { generateOrganizationTreeData, handleDropNode } from "./lib/utils"; diff --git a/apps/cs/src/entities/organization/lib/utils.ts b/apps/cs/src/entities/organization/lib/utils.ts index 0022b44f1..45591fc9e 100644 --- a/apps/cs/src/entities/organization/lib/utils.ts +++ b/apps/cs/src/entities/organization/lib/utils.ts @@ -1,5 +1,8 @@ import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; -import type { OrganizationTree } from "../model/organization-interface"; +import type { + OrganizationTree, + OrganizationTreeForDnd, +} from "../model/organization-interface"; export const generateOrganizationTreeData = ( data: OrganizationTree[], @@ -22,41 +25,39 @@ export const generateOrganizationTreeData = ( }, []); }; -export const generateNewOrganizationTreeData = ( +export const generateDndOrganizationTreeData = ( treeData: NodeModel[], options: DropOptions, -): OrganizationTree | null => { +): OrganizationTreeForDnd | null => { const { dragSourceId, dragSource, dropTargetId } = options; - if (!dragSource?.data) return null; const siblings = treeData.filter((node) => node.parent === dropTargetId); - const dragSourceIndex = siblings.findIndex( (node) => node.id === dragSourceId, ); - const prevSibling = siblings[dragSourceIndex - 1]; - const newSortSeq = dragSourceIndex === 0 ? 1 : (prevSibling?.data?.sortSeq ?? 0) + 1; - const childNodes = treeData.filter( - (node) => node.parent === dragSourceId && node.data, - ); - - const treeNode: OrganizationTree = { - ...dragSource.data, + const treeNode: OrganizationTreeForDnd = { + ognzSn: dragSource.data.ognzSn, + ognzNm: dragSource.data.ognzNm, + ognzExpln: dragSource.data.ognzExpln, upOgnzSn: dropTargetId === 0 ? null : (dropTargetId as number), sortSeq: newSortSeq, }; - const result: OrganizationTree = { + const childNodes = treeData.filter( + (node) => node.parent === dragSourceId && node.data, + ); + const result: OrganizationTreeForDnd = { ...treeNode, children: childNodes?.map((child) => ({ - ...child.data, + ognzSn: child.data?.ognzSn, ognzNm: child.data?.ognzNm ?? "", + ognzExpln: child.data?.ognzExpln, upOgnzSn: child.data?.upOgnzSn === 0 ? null : (dragSourceId as number), sortSeq: child.data?.sortSeq ?? 1, })) ?? [], @@ -66,12 +67,12 @@ export const generateNewOrganizationTreeData = ( }; export const handleDropNode = - (callback: (newTree: OrganizationTree) => void) => + (callback: (newTree: OrganizationTreeForDnd) => void) => ( newTree: NodeModel[], options: DropOptions, ) => { - const newTreeData = generateNewOrganizationTreeData(newTree, options); + const newTreeData = generateDndOrganizationTreeData(newTree, options); if (!newTreeData) return; callback(newTreeData); }; diff --git a/apps/cs/src/entities/organization/model/organization-interface.ts b/apps/cs/src/entities/organization/model/organization-interface.ts index 0d25b0b09..ab0983092 100644 --- a/apps/cs/src/entities/organization/model/organization-interface.ts +++ b/apps/cs/src/entities/organization/model/organization-interface.ts @@ -18,12 +18,14 @@ export type OrganizationFormData = z.infer; export type OrganizationTree = Organization & { children?: OrganizationTree[]; }; +export type OrganizationTreeForDnd = Omit; export interface OrganizationService { getOrganizationsTreeList(): Promise; getOrganizationInfo(ognzSn: number): Promise; - createOrganization(form: OrganizationFormData): Promise; + createOrganization(data: OrganizationFormData): Promise; deleteOrganization(ognzSn: number): Promise; updateOrganizationName(ognzSn: number, name: string): Promise; - updateOrganization(ognzSn: number, form: OrganizationFormData): Promise; + updateOrganization(ognzSn: number, data: OrganizationFormData): Promise; + updateOrganizationByDnd(data: OrganizationTreeForDnd): Promise; } diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index 4c8663f87..0be3770cc 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -15,6 +15,7 @@ import { handleDropNode, type OrganizationFormData, type OrganizationTree, + type OrganizationTreeForDnd, } from "@/entities/organization"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; @@ -95,6 +96,16 @@ function OrganizationPage() { }, }); + const { mutateAsync: updateOrganizationByDnd } = useMutation({ + mutationFn: (data: OrganizationTreeForDnd) => + OrganizationApi.updateOrganizationByDnd(data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + }); + }, + }); + const form = useForm({ resolver: zodResolver(organizationFormSchema), defaultValues: { @@ -199,7 +210,7 @@ function OrganizationPage() { /> )} handleSelectNode={handleNodeSelect} - handleDropNode={handleDropNode(console.log)} + handleDropNode={handleDropNode(updateOrganizationByDnd)} updateNodeName={async (id, name) => { await updateOrganizationName({ ognzSn: id as number, name }); }} From 0263ad8672c243c02fda98af13b3b0a1cb56b609 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 22 Apr 2025 09:54:31 +0900 Subject: [PATCH 19/44] =?UTF-8?q?chore:=20user-organization=20entities=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/user-organization-service.ts | 0 apps/cs/src/entities/user-organization/index.ts | 5 +++++ .../model/user-organization-interface.ts | 0 apps/cs/src/pages/menu/lib/utils.ts | 5 +---- .../src/pages/menu/ui/organization-search-dialog.tsx | 10 +++++----- apps/cs/src/pages/menu/ui/type-menu-form.tsx | 4 ++-- 6 files changed, 13 insertions(+), 11 deletions(-) rename apps/cs/src/{pages/menu => entities/user-organization}/api/user-organization-service.ts (100%) create mode 100644 apps/cs/src/entities/user-organization/index.ts rename apps/cs/src/{pages/menu => entities/user-organization}/model/user-organization-interface.ts (100%) diff --git a/apps/cs/src/pages/menu/api/user-organization-service.ts b/apps/cs/src/entities/user-organization/api/user-organization-service.ts similarity index 100% rename from apps/cs/src/pages/menu/api/user-organization-service.ts rename to apps/cs/src/entities/user-organization/api/user-organization-service.ts diff --git a/apps/cs/src/entities/user-organization/index.ts b/apps/cs/src/entities/user-organization/index.ts new file mode 100644 index 000000000..47463958b --- /dev/null +++ b/apps/cs/src/entities/user-organization/index.ts @@ -0,0 +1,5 @@ +export { UserOrganizationApi } from "./api/user-organization-service"; +export type { + User, + UserOrganization, +} from "./model/user-organization-interface"; diff --git a/apps/cs/src/pages/menu/model/user-organization-interface.ts b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts similarity index 100% rename from apps/cs/src/pages/menu/model/user-organization-interface.ts rename to apps/cs/src/entities/user-organization/model/user-organization-interface.ts diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index 274a0a057..09dd2a8a9 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -1,9 +1,6 @@ import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; +import type { UserOrganization, User } from "@/entities/user-organization"; import type { TreeMenu, TreeNode } from "../model/menu-interface"; -import type { - UserOrganization, - User, -} from "../model/user-organization-interface"; export const generateMenuTreeData = ( data: TreeMenu[], diff --git a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx index 41749b26b..505532296 100644 --- a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx @@ -19,12 +19,12 @@ import { Input, } from "@common/components/ui"; import { cn } from "@common/components/lib"; +import { + UserOrganizationApi, + type User, + type UserOrganization, +} from "@/entities/user-organization"; import { ENDPOINT } from "@/shared/config"; -import { UserOrganizationApi } from "../api/user-organization-service"; -import type { - UserOrganization, - User, -} from "../model/user-organization-interface"; import { filterUserOrganizationTreeDataByKeyword, generateUserOrganizationTreeData, diff --git a/apps/cs/src/pages/menu/ui/type-menu-form.tsx b/apps/cs/src/pages/menu/ui/type-menu-form.tsx index bd992bd70..085506ce6 100644 --- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx @@ -13,10 +13,10 @@ import { TableHead, TableRow, } from "@common/components"; -import { FormItem, FormControl, FormField } from "@/shared/ui/form"; import type { BoardPopup } from "@/entities/board"; import type { Contents } from "@/entities/content"; -import type { User } from "../model/user-organization-interface"; +import type { User } from "@/entities/user-organization"; +import { FormItem, FormControl, FormField } from "@/shared/ui/form"; import type { MenuFormData, MenuType } from "../model/menu-interface"; import { OrganizationSearchDialog } from "./organization-search-dialog"; import { ContentSearchDialog } from "./content-search-dialog"; From 086a81aa4ba4a38fde6d782874e50d4cbd0c5375 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 22 Apr 2025 17:46:22 +0900 Subject: [PATCH 20/44] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=EC=86=8C=EC=86=8D=EC=9B=90=20=ED=8F=BC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/code/index.ts | 2 + .../api/user-organization-service.ts | 28 ++- .../src/entities/user-organization/index.ts | 9 +- .../model/user-organization-interface.ts | 71 +++++++- .../menu/ui/organization-search-dialog.tsx | 4 +- .../role-authorizations-table-columns.tsx | 6 +- .../model/additional-user-table-columns.tsx | 39 +++++ .../model/user-organization-table-columns.tsx | 91 ++++++++++ .../ui/additional-user-dialog.tsx | 110 ++++++++++++ .../organization/ui/organization-page.tsx | 2 + .../ui/user-organization-section.tsx | 160 ++++++++++++++++++ 11 files changed, 507 insertions(+), 15 deletions(-) create mode 100644 apps/cs/src/pages/organization/model/additional-user-table-columns.tsx create mode 100644 apps/cs/src/pages/organization/model/user-organization-table-columns.tsx create mode 100644 apps/cs/src/pages/organization/ui/additional-user-dialog.tsx create mode 100644 apps/cs/src/pages/organization/ui/user-organization-section.tsx diff --git a/apps/cs/src/entities/code/index.ts b/apps/cs/src/entities/code/index.ts index 0b6633f4b..dc74bbc47 100644 --- a/apps/cs/src/entities/code/index.ts +++ b/apps/cs/src/entities/code/index.ts @@ -1 +1,3 @@ +export { CodeApi } from "./api/code-service"; +export type { ICode } from "./model/code-interface"; export { CodeSelect } from "./ui/code-select"; diff --git a/apps/cs/src/entities/user-organization/api/user-organization-service.ts b/apps/cs/src/entities/user-organization/api/user-organization-service.ts index daa6e2ffe..cbd17363f 100644 --- a/apps/cs/src/entities/user-organization/api/user-organization-service.ts +++ b/apps/cs/src/entities/user-organization/api/user-organization-service.ts @@ -1,17 +1,39 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { - UserOrganization, + TreeUserOrganization, + UserOrganizationParams, + UserOrganizationList, UserOrganizationService, + SearchUserParams, + SearchUserList, } from "../model/user-organization-interface"; export class UserOrganizationApiService implements UserOrganizationService { - public async getUserOrganizationTreeList(): Promise { - const response = await client.get( + public async getUserOrganizationList( + params: UserOrganizationParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`, + { params }, + ); + return response.data; + } + + public async getUserOrganizationTreeList(): Promise { + const response = await client.get( `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`, ); return response.data; } + + public async searchUser(params: SearchUserParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/user-popup`, + { params }, + ); + return response.data; + } } export const UserOrganizationApi = new UserOrganizationApiService(); diff --git a/apps/cs/src/entities/user-organization/index.ts b/apps/cs/src/entities/user-organization/index.ts index 47463958b..df4ed2321 100644 --- a/apps/cs/src/entities/user-organization/index.ts +++ b/apps/cs/src/entities/user-organization/index.ts @@ -1,5 +1,8 @@ export { UserOrganizationApi } from "./api/user-organization-service"; -export type { - User, - UserOrganization, +export { + userOrganizationFormSchema, + type TreeUserOrganization, + type UserOrganizationFormData, + type User, + type SearchUser, } from "./model/user-organization-interface"; diff --git a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts index c788b44e0..bbacd1a10 100644 --- a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts +++ b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts @@ -1,18 +1,81 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface UserOrganizationParams extends PageableParams { + keywordType?: string; + keyword?: string; + ognzSn?: number; + userUnqId?: string; + jbpsSeCd?: string; + jbgdSeCd?: string; +} + +export interface SearchUserParams extends PageableParams { + userNm?: string; + ogdpSeCd?: string; +} + export interface User { + ognzSn: number; + userUnqId: string; + userUnqNm: string; + jbpsSeCd: string; + jbpsSeNm: string; + jbgdSeCd: string; + jbgdSeNm: string; + sortSeq: number; + mdfcnDt: string; + mdfr: string; + mdfrNm: string; + mode: string; +} + +export interface SearchUser { + userUnqId: string; + userNm: string; + ogdpSeCd: string; + ogdpSeNm: string; + regDt: string; +} + +export interface TreeUser { userUnqId: string; userNm: string; jbpsSeCd: string; jbgdSeCd: string; } -export interface UserOrganization { +export interface TreeUserOrganization { ognzSn: number; upOgnzSn: number; ognzNm: string; - users: User[]; - children: UserOrganization[]; + users: TreeUser[]; + children: TreeUserOrganization[]; } +export const userOrganizationFormSchema = z.object({ + userOrganization: z.array( + z.object({ + mode: z.string(), + ognzSn: z.number(), + userUnqId: z.string(), + jbpsSeCd: z.string(), + jbgdSeCd: z.string(), + sortSeq: z.number(), + }), + ), +}); + +export type UserOrganizationList = PageableList; +export type SearchUserList = PageableList; +export type UserOrganizationFormData = z.infer< + typeof userOrganizationFormSchema +>; + export interface UserOrganizationService { - getUserOrganizationTreeList(): Promise; + getUserOrganizationList( + params: UserOrganizationParams, + ): Promise; + getUserOrganizationTreeList(): Promise; + searchUser(params: SearchUserParams): Promise; } diff --git a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx index 505532296..8478533fa 100644 --- a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx @@ -22,7 +22,7 @@ import { cn } from "@common/components/lib"; import { UserOrganizationApi, type User, - type UserOrganization, + type TreeUserOrganization, } from "@/entities/user-organization"; import { ENDPOINT } from "@/shared/config"; import { @@ -30,7 +30,7 @@ import { generateUserOrganizationTreeData, } from "../lib/utils"; -type TreeModel = UserOrganization | User; +type TreeModel = TreeUserOrganization | User; interface OrganizationSearchDialogProps { handleUserSelect: (user: User) => void; diff --git a/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx b/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx index e9a9a6d4d..c5517824b 100644 --- a/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx +++ b/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx @@ -1,7 +1,7 @@ -import { type ColumnDef } from "@tanstack/react-table"; -import type { RoleAuthorizations } from "./role-authorizations-interface"; -import { Checkbox } from "@common/components/ui"; +import type { ColumnDef } from "@tanstack/react-table"; import type { UseFormReturn } from "react-hook-form"; +import { Checkbox } from "@common/components/ui"; +import type { RoleAuthorizations } from "./role-authorizations-interface"; import type { RoleAuthorizationsFormData } from "./role-authorizations-interface"; export const createColumns = ( diff --git a/apps/cs/src/pages/organization/model/additional-user-table-columns.tsx b/apps/cs/src/pages/organization/model/additional-user-table-columns.tsx new file mode 100644 index 000000000..0b6e1e1e3 --- /dev/null +++ b/apps/cs/src/pages/organization/model/additional-user-table-columns.tsx @@ -0,0 +1,39 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@common/components/ui"; +import type { SearchUser } from "@/entities/user-organization"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + accessorKey: "userNm", + header: "이름", + }, + { + accessorKey: "ogdpSeNm", + header: "소속", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD HH:mm:ss"), + }, +]; diff --git a/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx b/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx new file mode 100644 index 000000000..2378c92d2 --- /dev/null +++ b/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx @@ -0,0 +1,91 @@ +import dayjs from "dayjs"; +import type { UseFormReturn } from "react-hook-form"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox, Select } from "@common/components/ui"; +import type { + User, + UserOrganizationFormData, +} from "@/entities/user-organization"; + +export const createColumns = ( + form: UseFormReturn, + renderJobPositionSelect: ( + props: React.ComponentPropsWithoutRef, + ) => React.ReactNode, + renderJobGradeSelect: ( + props: React.ComponentPropsWithoutRef, + ) => React.ReactNode, +): ColumnDef[] => { + return [ + { + id: "select", + header: ({ table }) => ( + + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + id: "no", + header: "NO", + cell: ({ row }) => row.index + 1, + }, + { + accessorKey: "userUnqNm", + header: "이름", + }, + { + accessorKey: "jbpsSeCd", + header: "직위", + cell: ({ row }) => + renderJobPositionSelect({ + defaultValue: row.original.jbpsSeCd, + value: form.watch(`userOrganization.${row.index}.jbpsSeCd`), + onValueChange: (value) => { + const mode = form.watch(`userOrganization.${row.index}.mode`); + if (mode === "R") + form.setValue(`userOrganization.${row.index}.mode`, "U"); + form.setValue(`userOrganization.${row.index}.jbpsSeCd`, value); + }, + }), + }, + { + accessorKey: "jbgdSeCd", + header: "직급", + cell: ({ row }) => + renderJobGradeSelect({ + defaultValue: row.original.jbgdSeCd, + value: form.watch(`userOrganization.${row.index}.jbgdSeCd`), + onValueChange: (value) => { + const mode = form.watch(`userOrganization.${row.index}.mode`); + if (mode === "R") + form.setValue(`userOrganization.${row.index}.mode`, "U"); + form.setValue(`userOrganization.${row.index}.jbgdSeCd`, value); + }, + }), + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => { + if (!row.original.mdfcnDt) return ""; + return dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"); + }, + }, + ]; +}; diff --git a/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx b/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx new file mode 100644 index 000000000..c9181c578 --- /dev/null +++ b/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx @@ -0,0 +1,110 @@ +import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type RowSelectionState, +} from "@tanstack/react-table"; +import { + Button, + DataTable, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + Input, + DialogFooter, + DialogClose, +} from "@common/components/ui"; +import { + UserOrganizationApi, + type SearchUser, +} from "@/entities/user-organization"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/additional-user-table-columns"; + +type AdditionalUserDialogProps = { + onAddUsers: (user: SearchUser[]) => void; +} & React.ComponentProps; + +function AdditionalUserDialog({ + onAddUsers, + ...props +}: AdditionalUserDialogProps) { + const [open, setOpen] = useState(false); + const [userNm, setUserNm] = useState(""); + const [rowSelection, setRowSelection] = useState({}); + + const { data: userList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION], + queryFn: () => UserOrganizationApi.searchUser({ userNm }), + enabled: false, + }); + + const table = useReactTable({ + data: userList?.content ?? [], + columns, + state: { rowSelection }, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + }); + + const handleAddUsers = () => { + const selectedRows = table.getSelectedRowModel().rows; + const userData = selectedRows.map((row) => row.original); + if (!userData) return; + onAddUsers(userData); + }; + + const handleCancel = () => { + setRowSelection({}); + setUserNm(""); + }; + + useEffect(() => { + if (open) refetch(); + }, [open]); + + return ( + + + + + + + 콘텐츠 조회 + +
+
+ setUserNm(e.target.value)} + /> + +
+
+ +
+
+ + + + + + +
+
+ ); +} + +export { AdditionalUserDialog }; diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index 0be3770cc..a0979db53 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -20,6 +20,7 @@ import { import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { OrganizationForm } from "./organization-form"; +import { UserOrganizationSection } from "./user-organization-section"; function OrganizationPage() { const queryClient = useQueryClient(); @@ -226,6 +227,7 @@ function OrganizationPage() { handleUpdate={updateOrganization} handleDelete={deleteOrganization} /> +
); diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx new file mode 100644 index 000000000..3bdfe4f19 --- /dev/null +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { useReactTable, getCoreRowModel } from "@tanstack/react-table"; +import { cn } from "@common/components/lib"; +import { Button, DataTable } from "@common/components/ui"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components/ui"; +import { + UserOrganizationApi, + userOrganizationFormSchema, + type UserOrganizationFormData, + type User, + type SearchUser, +} from "@/entities/user-organization"; +import { CodeApi, type ICode } from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { createColumns } from "../model/user-organization-table-columns"; +import { AdditionalUserDialog } from "./additional-user-dialog"; + +type UserOrganizationSectionProps = { + ognzSn?: number | null; +}; + +function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { + const [userList, setUserList] = useState([]); + + const { data } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn], + queryFn: () => + UserOrganizationApi.getUserOrganizationList({ ognzSn: ognzSn! }), + enabled: !!ognzSn, + placeholderData: keepPreviousData, + }); + + const { data: jobPositionCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "JPBSCLCD"], + queryFn: () => CodeApi.getCode("JPBSCLCD"), + }); + + const { data: jobGradeCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "JDGDCLCD"], + queryFn: () => CodeApi.getCode("JDGDCLCD"), + }); + + const form = useForm({ + resolver: zodResolver(userOrganizationFormSchema), + values: { + userOrganization: [ + ...(data?.content ?? []).map((user) => ({ + mode: user.mode, + ognzSn: user.ognzSn, + userUnqId: user.userUnqId, + jbpsSeCd: user.jbpsSeCd, + jbgdSeCd: user.jbgdSeCd, + sortSeq: user.sortSeq, + })), + ], + }, + }); + + const { append } = useFieldArray({ + control: form.control, + name: "userOrganization", + }); + + const table = useReactTable({ + data: data?.content ?? [], + columns: createColumns( + form, + (props) => , + (props) => , + ), + getCoreRowModel: getCoreRowModel(), + }); + + const handleAddUsers = (users: SearchUser[]) => { + const newForm = users.map((user) => ({ + mode: "C", + ognzSn: ognzSn!, + userUnqId: user.userUnqId, + jbpsSeCd: "", + jbgdSeCd: "", + sortSeq: 0, + })); + append(newForm); + + const newUserList = users.map((user) => ({ + ognzSn: ognzSn!, + userUnqId: user.userUnqId, + userUnqNm: user.userNm, + jbpsSeCd: "", + jbpsSeNm: "", + jbgdSeCd: "", + jbgdSeNm: "", + sortSeq: 0, + mdfcnDt: "", + mdfr: "", + mdfrNm: "", + mode: "C", + })); + setUserList([...newUserList, ...userList]); + }; + + useEffect(() => { + if (data?.content) setUserList(data.content); + }, [data]); + + return ( +
+
+

소속원 목록

+
+ + + +
+
+
+ +
+
+ ); +} + +type CodeSelectProps = React.ComponentPropsWithoutRef & { + codeData?: ICode[]; +}; +const CodeSelect = ({ codeData, ...props }: CodeSelectProps) => { + if (!codeData) return null; + + const defaultValueText = + codeData.find((item) => item.lwpoCd === props.defaultValue)?.lwpoCdNm ?? + "선택"; + + return ( + + ); +}; + +export { UserOrganizationSection }; From 14bb8e9e604ca250ec2ff85267b8546d0a9a5715 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 22 Apr 2025 20:13:12 +0900 Subject: [PATCH 21/44] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=EC=86=8C=EC=86=8D=EC=9B=90=20=ED=8F=BC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/entities/user-organization/index.ts | 2 ++ .../model/user-organization-interface.ts | 23 +++++++++++-------- apps/cs/src/pages/menu/lib/utils.ts | 19 ++++++++------- .../menu/ui/organization-search-dialog.tsx | 8 +++---- apps/cs/src/pages/menu/ui/type-menu-form.tsx | 4 ++-- .../organization/ui/organization-page.tsx | 4 +++- .../ui/user-organization-section.tsx | 13 ++++++----- 7 files changed, 42 insertions(+), 31 deletions(-) diff --git a/apps/cs/src/entities/user-organization/index.ts b/apps/cs/src/entities/user-organization/index.ts index df4ed2321..19cd76682 100644 --- a/apps/cs/src/entities/user-organization/index.ts +++ b/apps/cs/src/entities/user-organization/index.ts @@ -2,7 +2,9 @@ export { UserOrganizationApi } from "./api/user-organization-service"; export { userOrganizationFormSchema, type TreeUserOrganization, + type TreeUser, type UserOrganizationFormData, + type UserOrganizationPayloadData, type User, type SearchUser, } from "./model/user-organization-interface"; diff --git a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts index bbacd1a10..f9198549b 100644 --- a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts +++ b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts @@ -53,21 +53,24 @@ export interface TreeUserOrganization { children: TreeUserOrganization[]; } +export const userOrganizationPayloadSchema = z.object({ + mode: z.string(), + ognzSn: z.number(), + userUnqId: z.string(), + jbpsSeCd: z.string(), + jbgdSeCd: z.string(), + sortSeq: z.number(), +}); + export const userOrganizationFormSchema = z.object({ - userOrganization: z.array( - z.object({ - mode: z.string(), - ognzSn: z.number(), - userUnqId: z.string(), - jbpsSeCd: z.string(), - jbgdSeCd: z.string(), - sortSeq: z.number(), - }), - ), + userOrganization: z.array(userOrganizationPayloadSchema), }); export type UserOrganizationList = PageableList; export type SearchUserList = PageableList; +export type UserOrganizationPayloadData = z.infer< + typeof userOrganizationPayloadSchema +>; export type UserOrganizationFormData = z.infer< typeof userOrganizationFormSchema >; diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index 09dd2a8a9..f6d13be33 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -1,5 +1,8 @@ import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; -import type { UserOrganization, User } from "@/entities/user-organization"; +import type { + TreeUserOrganization, + TreeUser, +} from "@/entities/user-organization"; import type { TreeMenu, TreeNode } from "../model/menu-interface"; export const generateMenuTreeData = ( @@ -108,11 +111,11 @@ export const handleNodeTextChange = }; export const generateUserOrganizationTreeData = ( - organizations: UserOrganization[], -): NodeModel[] => { - return organizations.reduce[]>( + organizations: TreeUserOrganization[], +): NodeModel[] => { + return organizations.reduce[]>( (acc, org) => { - const orgNode: NodeModel = { + const orgNode: NodeModel = { id: org.ognzSn, parent: org.upOgnzSn ?? 0, text: org.ognzNm, @@ -145,9 +148,9 @@ export const generateUserOrganizationTreeData = ( }; export const filterUserOrganizationTreeDataByKeyword = ( - treeData: NodeModel[], + treeData: NodeModel[], keyword: string, -): NodeModel[] => { +): NodeModel[] => { return Array.from( treeData.reduce((matchedNodes, node) => { if (!node.text.includes(keyword)) return matchedNodes; @@ -163,6 +166,6 @@ export const filterUserOrganizationTreeDataByKeyword = ( } return matchedNodes; - }, new Set>()), + }, new Set>()), ); }; diff --git a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx index 8478533fa..14eccbf98 100644 --- a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx @@ -21,8 +21,8 @@ import { import { cn } from "@common/components/lib"; import { UserOrganizationApi, - type User, type TreeUserOrganization, + type TreeUser, } from "@/entities/user-organization"; import { ENDPOINT } from "@/shared/config"; import { @@ -30,10 +30,10 @@ import { generateUserOrganizationTreeData, } from "../lib/utils"; -type TreeModel = TreeUserOrganization | User; +type TreeModel = TreeUserOrganization | TreeUser; interface OrganizationSearchDialogProps { - handleUserSelect: (user: User) => void; + handleUserSelect: (user: TreeUser) => void; } function OrganizationSearchDialog({ @@ -53,7 +53,7 @@ function OrganizationSearchDialog({ const handleSelectNode = (node: NodeModel) => { setSelectedNode(node); - handleUserSelect(node.data as User); + handleUserSelect(node.data as TreeUser); }; useEffect(() => { diff --git a/apps/cs/src/pages/menu/ui/type-menu-form.tsx b/apps/cs/src/pages/menu/ui/type-menu-form.tsx index 085506ce6..1f0483d1b 100644 --- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx @@ -15,7 +15,7 @@ import { } from "@common/components"; import type { BoardPopup } from "@/entities/board"; import type { Contents } from "@/entities/content"; -import type { User } from "@/entities/user-organization"; +import type { TreeUser } from "@/entities/user-organization"; import { FormItem, FormControl, FormField } from "@/shared/ui/form"; import type { MenuFormData, MenuType } from "../model/menu-interface"; import { OrganizationSearchDialog } from "./organization-search-dialog"; @@ -37,7 +37,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { form.setValue("menuLnkgSnNm", content.contsTtl); }; - const handleUserSelect = (user: User) => { + const handleUserSelect = (user: TreeUser) => { form.setValue("menuPicUserUnqId", user.userUnqId); form.setValue("menuPicUserUnqIdNm", user.userNm); }; diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index a0979db53..fb920dbfc 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -227,7 +227,9 @@ function OrganizationPage() { handleUpdate={updateOrganization} handleDelete={deleteOrganization} /> - + {selectedOrganization?.ognzSn && ( + + )}
); diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx index 3bdfe4f19..7e7eaa2a0 100644 --- a/apps/cs/src/pages/organization/ui/user-organization-section.tsx +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -31,7 +31,7 @@ type UserOrganizationSectionProps = { function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { const [userList, setUserList] = useState([]); - const { data } = useQuery({ + const { data: userOrganizationList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn], queryFn: () => UserOrganizationApi.getUserOrganizationList({ ognzSn: ognzSn! }), @@ -53,7 +53,7 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { resolver: zodResolver(userOrganizationFormSchema), values: { userOrganization: [ - ...(data?.content ?? []).map((user) => ({ + ...userList.map((user) => ({ mode: user.mode, ognzSn: user.ognzSn, userUnqId: user.userUnqId, @@ -64,14 +64,13 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { ], }, }); - const { append } = useFieldArray({ control: form.control, name: "userOrganization", }); const table = useReactTable({ - data: data?.content ?? [], + data: userList, columns: createColumns( form, (props) => , @@ -109,8 +108,9 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { }; useEffect(() => { - if (data?.content) setUserList(data.content); - }, [data]); + if (userOrganizationList?.content) + setUserList(userOrganizationList.content); + }, [userOrganizationList]); return (
@@ -134,6 +134,7 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { type CodeSelectProps = React.ComponentPropsWithoutRef & { codeData?: ICode[]; }; + const CodeSelect = ({ codeData, ...props }: CodeSelectProps) => { if (!codeData) return null; From d1677af87b4f96b9a54e8ba780e9779805cb0263 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 23 Apr 2025 04:18:25 +0900 Subject: [PATCH 22/44] =?UTF-8?q?refactor:=20=EC=A1=B0=EC=A7=81=EB=8F=84?= =?UTF-8?q?=20=EC=86=8C=EC=86=8D=EC=9B=90=20=ED=8F=BC=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/user-organization-interface.ts | 14 +- .../model/user-organization-table-columns.tsx | 91 ------- .../ui/user-organization-section.tsx | 224 ++++++++++++++---- 3 files changed, 186 insertions(+), 143 deletions(-) delete mode 100644 apps/cs/src/pages/organization/model/user-organization-table-columns.tsx diff --git a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts index f9198549b..d194f7a68 100644 --- a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts +++ b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts @@ -16,18 +16,18 @@ export interface SearchUserParams extends PageableParams { } export interface User { + mode: string; ognzSn: number; userUnqId: string; - userUnqNm: string; + userUnqNm?: string; jbpsSeCd: string; - jbpsSeNm: string; + jbpsSeNm?: string; jbgdSeCd: string; - jbgdSeNm: string; + jbgdSeNm?: string; sortSeq: number; - mdfcnDt: string; - mdfr: string; - mdfrNm: string; - mode: string; + mdfcnDt?: string; + mdfr?: string; + mdfrNm?: string; } export interface SearchUser { diff --git a/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx b/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx deleted file mode 100644 index 2378c92d2..000000000 --- a/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import dayjs from "dayjs"; -import type { UseFormReturn } from "react-hook-form"; -import type { ColumnDef } from "@tanstack/react-table"; -import { Checkbox, Select } from "@common/components/ui"; -import type { - User, - UserOrganizationFormData, -} from "@/entities/user-organization"; - -export const createColumns = ( - form: UseFormReturn, - renderJobPositionSelect: ( - props: React.ComponentPropsWithoutRef, - ) => React.ReactNode, - renderJobGradeSelect: ( - props: React.ComponentPropsWithoutRef, - ) => React.ReactNode, -): ColumnDef[] => { - return [ - { - id: "select", - header: ({ table }) => ( - - table.toggleAllPageRowsSelected(!!checked) - } - className="h-4 w-4 rounded border-gray-300" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!checked)} - className="h-4 w-4 rounded border-gray-300" - /> - ), - }, - { - id: "no", - header: "NO", - cell: ({ row }) => row.index + 1, - }, - { - accessorKey: "userUnqNm", - header: "이름", - }, - { - accessorKey: "jbpsSeCd", - header: "직위", - cell: ({ row }) => - renderJobPositionSelect({ - defaultValue: row.original.jbpsSeCd, - value: form.watch(`userOrganization.${row.index}.jbpsSeCd`), - onValueChange: (value) => { - const mode = form.watch(`userOrganization.${row.index}.mode`); - if (mode === "R") - form.setValue(`userOrganization.${row.index}.mode`, "U"); - form.setValue(`userOrganization.${row.index}.jbpsSeCd`, value); - }, - }), - }, - { - accessorKey: "jbgdSeCd", - header: "직급", - cell: ({ row }) => - renderJobGradeSelect({ - defaultValue: row.original.jbgdSeCd, - value: form.watch(`userOrganization.${row.index}.jbgdSeCd`), - onValueChange: (value) => { - const mode = form.watch(`userOrganization.${row.index}.mode`); - if (mode === "R") - form.setValue(`userOrganization.${row.index}.mode`, "U"); - form.setValue(`userOrganization.${row.index}.jbgdSeCd`, value); - }, - }), - }, - { - accessorKey: "mdfrNm", - header: "최종수정자", - }, - { - accessorKey: "mdfcnDt", - header: "최종수정일", - cell: ({ row }) => { - if (!row.original.mdfcnDt) return ""; - return dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"); - }, - }, - ]; -}; diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx index 7e7eaa2a0..38be359f0 100644 --- a/apps/cs/src/pages/organization/ui/user-organization-section.tsx +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -1,10 +1,18 @@ -import { useEffect, useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; +import dayjs from "dayjs"; +import React, { useEffect, useState, useCallback, memo } from "react"; +import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery, keepPreviousData } from "@tanstack/react-query"; -import { useReactTable, getCoreRowModel } from "@tanstack/react-table"; -import { cn } from "@common/components/lib"; -import { Button, DataTable } from "@common/components/ui"; +import { + Button, + Checkbox, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@common/components/ui"; import { Select, SelectContent, @@ -21,20 +29,22 @@ import { } from "@/entities/user-organization"; import { CodeApi, type ICode } from "@/entities/code"; import { ENDPOINT } from "@/shared/config"; -import { createColumns } from "../model/user-organization-table-columns"; import { AdditionalUserDialog } from "./additional-user-dialog"; type UserOrganizationSectionProps = { - ognzSn?: number | null; + ognzSn: number; }; function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { const [userList, setUserList] = useState([]); + const [selectedUsers, setSelectedUsers] = useState([]); + + const isAllSelected = + userList.length > 0 && selectedUsers.length === userList.length; const { data: userOrganizationList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn], - queryFn: () => - UserOrganizationApi.getUserOrganizationList({ ognzSn: ognzSn! }), + queryFn: () => UserOrganizationApi.getUserOrganizationList({ ognzSn }), enabled: !!ognzSn, placeholderData: keepPreviousData, }); @@ -52,37 +62,26 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { const form = useForm({ resolver: zodResolver(userOrganizationFormSchema), values: { - userOrganization: [ - ...userList.map((user) => ({ - mode: user.mode, - ognzSn: user.ognzSn, - userUnqId: user.userUnqId, - jbpsSeCd: user.jbpsSeCd, - jbgdSeCd: user.jbgdSeCd, - sortSeq: user.sortSeq, - })), - ], + userOrganization: userList.map((user) => ({ + mode: user.mode, + ognzSn: user.ognzSn, + userUnqId: user.userUnqId, + jbpsSeCd: user.jbpsSeCd, + jbgdSeCd: user.jbgdSeCd, + sortSeq: user.sortSeq, + })), }, }); - const { append } = useFieldArray({ - control: form.control, - name: "userOrganization", - }); - const table = useReactTable({ - data: userList, - columns: createColumns( - form, - (props) => , - (props) => , - ), - getCoreRowModel: getCoreRowModel(), + const { append } = useFieldArray({ + name: "userOrganization", + control: form.control, }); const handleAddUsers = (users: SearchUser[]) => { const newForm = users.map((user) => ({ mode: "C", - ognzSn: ognzSn!, + ognzSn, userUnqId: user.userUnqId, jbpsSeCd: "", jbgdSeCd: "", @@ -91,51 +90,184 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { append(newForm); const newUserList = users.map((user) => ({ - ognzSn: ognzSn!, + mode: "C", + ognzSn, userUnqId: user.userUnqId, userUnqNm: user.userNm, jbpsSeCd: "", - jbpsSeNm: "", jbgdSeCd: "", - jbgdSeNm: "", sortSeq: 0, - mdfcnDt: "", - mdfr: "", - mdfrNm: "", - mode: "C", })); setUserList([...newUserList, ...userList]); }; + const handleToggleSelectUser = useCallback((user: User) => { + setSelectedUsers((prevSelected) => { + const isSelected = prevSelected.some( + (item) => item.userUnqId === user.userUnqId, + ); + + if (isSelected) { + return prevSelected.filter((item) => item.userUnqId !== user.userUnqId); + } else { + return [...prevSelected, user]; + } + }); + }, []); + + const handleToggleSelectAll = () => { + if (isAllSelected) { + setSelectedUsers([]); + } else { + setSelectedUsers([...userList]); + } + }; + + const handleDeleteUsers = () => { + if (selectedUsers.length === 0) return; + + const selectedUserIds = selectedUsers.map((user) => user.userUnqId); + + setUserList((prevUsers) => + prevUsers.filter((user) => !selectedUserIds.includes(user.userUnqId)), + ); + setSelectedUsers([]); + }; + + const isUserSelected = useCallback( + (userUnqId: string) => + selectedUsers.some((item) => item.userUnqId === userUnqId), + [selectedUsers], + ); + useEffect(() => { - if (userOrganizationList?.content) + if (userOrganizationList?.content) { setUserList(userOrganizationList.content); + } }, [userOrganizationList]); return ( -
+

소속원 목록

-
- + + + + + + + NO + 이름 + 직위 + 직급 + 최종수정자 + 최종수정일 + + + + {userList.map((user, index) => ( + + + handleToggleSelectUser(user)} + /> + + + + ))} + +
); } +type UserTableRowProps = { + form: UseFormReturn; + user: User; + index: number; + jobPositionCodes?: ICode[]; + jobGradeCodes?: ICode[]; +}; + +const UserTableRow = memo( + ({ + form, + user, + index, + jobPositionCodes, + jobGradeCodes, + }: UserTableRowProps) => ( + <> + {index + 1} + {user.userUnqNm} + + { + const mode = form.watch(`userOrganization.${index}.mode`); + if (mode === "R") { + form.setValue(`userOrganization.${index}.mode`, "U"); + } + form.setValue(`userOrganization.${index}.jbpsSeCd`, value); + }} + {...form.register(`userOrganization.${index}.jbpsSeCd`)} + /> + + + { + const mode = form.watch(`userOrganization.${index}.mode`); + if (mode === "R") { + form.setValue(`userOrganization.${index}.mode`, "U"); + } + form.setValue(`userOrganization.${index}.jbgdSeCd`, value); + }} + {...form.register(`userOrganization.${index}.jbgdSeCd`)} + /> + + {user.mdfrNm} + + {dayjs(user.mdfcnDt).format("YYYY-MM-DD")} + + + ), +); + +UserTableRow.displayName = "UserTableRow"; + type CodeSelectProps = React.ComponentPropsWithoutRef & { codeData?: ICode[]; }; -const CodeSelect = ({ codeData, ...props }: CodeSelectProps) => { +const CodeSelect = memo(({ codeData, ...props }: CodeSelectProps) => { if (!codeData) return null; const defaultValueText = @@ -156,6 +288,8 @@ const CodeSelect = ({ codeData, ...props }: CodeSelectProps) => { ); -}; +}); + +CodeSelect.displayName = "CodeSelect"; export { UserOrganizationSection }; From aa8edc71c7bf321bde1c76a5d4cd69274ad5959a Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 23 Apr 2025 04:41:39 +0900 Subject: [PATCH 23/44] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/organization/ui/additional-user-dialog.tsx | 4 +++- .../pages/organization/ui/user-organization-section.tsx | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx b/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx index c9181c578..48e6dc219 100644 --- a/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx +++ b/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx @@ -100,7 +100,9 @@ function AdditionalUserDialog({ 취소 - + + + diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx index 38be359f0..c53afe04b 100644 --- a/apps/cs/src/pages/organization/ui/user-organization-section.tsx +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -98,7 +98,13 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { jbgdSeCd: "", sortSeq: 0, })); - setUserList([...newUserList, ...userList]); + const filteredUserList = userList.filter( + (existingUser) => + !newUserList.some( + (newUser) => newUser.userUnqId === existingUser.userUnqId, + ), + ); + setUserList([...newUserList, ...filteredUserList]); }; const handleToggleSelectUser = useCallback((user: User) => { From f6bcf8ebfe3ede8e9db11ea62f4939e9f36c7001 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 24 Apr 2025 11:10:17 +0900 Subject: [PATCH 24/44] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=EB=8F=84=20?= =?UTF-8?q?=EC=86=8C=EC=86=8D=EC=9B=90=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/user-organization-service.ts | 12 ++ .../model/user-organization-interface.ts | 1 + .../ui/additional-user-dialog.tsx | 2 + .../ui/user-organization-section.tsx | 118 +++++++++++------- 4 files changed, 91 insertions(+), 42 deletions(-) diff --git a/apps/cs/src/entities/user-organization/api/user-organization-service.ts b/apps/cs/src/entities/user-organization/api/user-organization-service.ts index cbd17363f..bbb7abeef 100644 --- a/apps/cs/src/entities/user-organization/api/user-organization-service.ts +++ b/apps/cs/src/entities/user-organization/api/user-organization-service.ts @@ -5,8 +5,10 @@ import type { UserOrganizationParams, UserOrganizationList, UserOrganizationService, + UserOrganizationPayloadData, SearchUserParams, SearchUserList, + User, } from "../model/user-organization-interface"; export class UserOrganizationApiService implements UserOrganizationService { @@ -34,6 +36,16 @@ export class UserOrganizationApiService implements UserOrganizationService { ); return response.data; } + + public async saveUserOrganization( + data: UserOrganizationPayloadData[], + ): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`, + data, + ); + return response.data; + } } export const UserOrganizationApi = new UserOrganizationApiService(); diff --git a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts index d194f7a68..d7901c86b 100644 --- a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts +++ b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts @@ -81,4 +81,5 @@ export interface UserOrganizationService { ): Promise; getUserOrganizationTreeList(): Promise; searchUser(params: SearchUserParams): Promise; + saveUserOrganization(data: UserOrganizationPayloadData[]): Promise; } diff --git a/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx b/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx index 48e6dc219..0dc63dcb0 100644 --- a/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx +++ b/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx @@ -54,7 +54,9 @@ function AdditionalUserDialog({ const selectedRows = table.getSelectedRowModel().rows; const userData = selectedRows.map((row) => row.original); if (!userData) return; + onAddUsers(userData); + table.setRowSelection({}); }; const handleCancel = () => { diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx index c53afe04b..d21d57be2 100644 --- a/apps/cs/src/pages/organization/ui/user-organization-section.tsx +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -1,8 +1,14 @@ import dayjs from "dayjs"; import React, { useEffect, useState, useCallback, memo } from "react"; import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { + useQuery, + useMutation, + useQueryClient, + keepPreviousData, +} from "@tanstack/react-query"; import { Button, Checkbox, @@ -24,11 +30,13 @@ import { UserOrganizationApi, userOrganizationFormSchema, type UserOrganizationFormData, + type UserOrganizationPayloadData, type User, type SearchUser, } from "@/entities/user-organization"; import { CodeApi, type ICode } from "@/entities/code"; import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; import { AdditionalUserDialog } from "./additional-user-dialog"; type UserOrganizationSectionProps = { @@ -36,6 +44,10 @@ type UserOrganizationSectionProps = { }; function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { + const queryClient = useQueryClient(); + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + const [userList, setUserList] = useState([]); const [selectedUsers, setSelectedUsers] = useState([]); @@ -59,17 +71,29 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { queryFn: () => CodeApi.getCode("JDGDCLCD"), }); + const { mutate: saveUserOrganization } = useMutation({ + mutationFn: (data: UserOrganizationPayloadData[]) => + UserOrganizationApi.saveUserOrganization(data), + onSuccess: () => { + alert("저장되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn], + }); + }, + }); + const form = useForm({ resolver: zodResolver(userOrganizationFormSchema), values: { - userOrganization: userList.map((user) => ({ - mode: user.mode, - ognzSn: user.ognzSn, - userUnqId: user.userUnqId, - jbpsSeCd: user.jbpsSeCd, - jbgdSeCd: user.jbgdSeCd, - sortSeq: user.sortSeq, - })), + userOrganization: + userOrganizationList?.content.map((user) => ({ + mode: user.mode, + ognzSn: user.ognzSn, + userUnqId: user.userUnqId, + jbpsSeCd: user.jbpsSeCd, + jbgdSeCd: user.jbgdSeCd, + sortSeq: user.sortSeq, + })) ?? [], }, }); @@ -79,30 +103,25 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { }); const handleAddUsers = (users: SearchUser[]) => { - const newForm = users.map((user) => ({ + const generateUserData = (user: SearchUser) => ({ mode: "C", ognzSn, userUnqId: user.userUnqId, jbpsSeCd: "", jbgdSeCd: "", sortSeq: 0, - })); - append(newForm); + }); + + const newFormData = users.map(generateUserData); + append(newFormData); const newUserList = users.map((user) => ({ - mode: "C", - ognzSn, - userUnqId: user.userUnqId, + ...generateUserData(user), userUnqNm: user.userNm, - jbpsSeCd: "", - jbgdSeCd: "", - sortSeq: 0, })); + const existingUserIds = new Set(newUserList.map((user) => user.userUnqId)); const filteredUserList = userList.filter( - (existingUser) => - !newUserList.some( - (newUser) => newUser.userUnqId === existingUser.userUnqId, - ), + (user) => !existingUserIds.has(user.userUnqId), ); setUserList([...newUserList, ...filteredUserList]); }; @@ -130,16 +149,29 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { }; const handleDeleteUsers = () => { - if (selectedUsers.length === 0) return; - const selectedUserIds = selectedUsers.map((user) => user.userUnqId); - + const updatedFormData = form.getValues("userOrganization").map((item) => { + if (selectedUserIds.includes(item.userUnqId)) { + return { ...item, mode: "D" }; + } + return item; + }); + form.setValue("userOrganization", updatedFormData); setUserList((prevUsers) => prevUsers.filter((user) => !selectedUserIds.includes(user.userUnqId)), ); setSelectedUsers([]); }; + const handleSaveUsers = async () => { + if (await confirm("저장하시겠습니까?")) { + const filteredData = form + .watch("userOrganization") + .filter((item) => item.mode !== "R"); + saveUserOrganization(filteredData); + } + }; + const isUserSelected = useCallback( (userUnqId: string) => selectedUsers.some((item) => item.userUnqId === userUnqId), @@ -166,7 +198,9 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { > 선택삭제 - +
@@ -196,12 +230,12 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { onCheckedChange={() => handleToggleSelectUser(user)} /> - ))} @@ -212,28 +246,28 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { ); } -type UserTableRowProps = { +type UserCellsProps = { form: UseFormReturn; user: User; index: number; - jobPositionCodes?: ICode[]; - jobGradeCodes?: ICode[]; + jobPositionCodeData?: ICode[]; + jobGradeCodeData?: ICode[]; }; -const UserTableRow = memo( +const UserCells = memo( ({ form, user, index, - jobPositionCodes, - jobGradeCodes, - }: UserTableRowProps) => ( + jobPositionCodeData, + jobGradeCodeData, + }: UserCellsProps) => ( <> {index + 1} {user.userUnqNm} { const mode = form.watch(`userOrganization.${index}.mode`); @@ -247,7 +281,7 @@ const UserTableRow = memo( { const mode = form.watch(`userOrganization.${index}.mode`); @@ -267,7 +301,7 @@ const UserTableRow = memo( ), ); -UserTableRow.displayName = "UserTableRow"; +UserCells.displayName = "UserCells"; type CodeSelectProps = React.ComponentPropsWithoutRef & { codeData?: ICode[]; @@ -276,14 +310,14 @@ type CodeSelectProps = React.ComponentPropsWithoutRef & { const CodeSelect = memo(({ codeData, ...props }: CodeSelectProps) => { if (!codeData) return null; - const defaultValueText = + const placeholderText = codeData.find((item) => item.lwpoCd === props.defaultValue)?.lwpoCdNm ?? "선택"; return ( + updateFormValue(index, "sortSeq")(Number(e.target.value)) } - form.setValue(`userOrganization.${index}.jbpsSeCd`, value); - }} - {...form.register(`userOrganization.${index}.jbpsSeCd`)} - /> - - - { - const mode = form.watch(`userOrganization.${index}.mode`); - if (mode === "R") { - form.setValue(`userOrganization.${index}.mode`, "U"); - } - form.setValue(`userOrganization.${index}.jbgdSeCd`, value); - }} - {...form.register(`userOrganization.${index}.jbgdSeCd`)} - /> - - {user.mdfrNm} - - {dayjs(user.mdfcnDt).format("YYYY-MM-DD")} - - - ), + className="w-[6rem] !text-[1.3rem]" + /> + + {user.mdfrNm} + + {dayjs(user.mdfcnDt).format("YYYY-MM-DD")} + + + ); + }, ); UserCells.displayName = "UserCells"; From 9b9a116abd5d1eaa2c8dec5a5fd5101f3b5d1872 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 24 Apr 2025 14:45:43 +0900 Subject: [PATCH 27/44] =?UTF-8?q?design:=20=EC=86=8C=EC=86=8D=EC=9B=90=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/ui/organization-form.tsx | 54 +++++++++---------- .../ui/user-organization-section.tsx | 4 +- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/apps/cs/src/pages/organization/ui/organization-form.tsx b/apps/cs/src/pages/organization/ui/organization-form.tsx index a0ab0e4e5..4ddb94a57 100644 --- a/apps/cs/src/pages/organization/ui/organization-form.tsx +++ b/apps/cs/src/pages/organization/ui/organization-form.tsx @@ -81,7 +81,9 @@ function OrganizationForm({ - + + + @@ -104,6 +106,29 @@ function OrganizationForm({ )} /> + 정렬순서 + + ( + + + + field.onChange(Number(e.target.value)) + } + className="!text-[1.3rem]" + placeholder="정렬순서를 입력해주세요" + /> + + + )} + /> + 조직설명 @@ -125,33 +150,6 @@ function OrganizationForm({ )} /> - - - 정렬순서 - - ( - - - - field.onChange(Number(e.target.value)) - } - className="!text-[1.3rem]" - placeholder="정렬순서를 입력해주세요" - /> - - - )} - /> - - - 사용여부 -
+
- + Date: Thu, 24 Apr 2025 16:23:46 +0900 Subject: [PATCH 28/44] =?UTF-8?q?chore:=20=EC=A1=B0=EC=A7=81=20=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=20dnd=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/organization/api/organization-service.ts | 2 +- .../src/entities/organization/model/organization-interface.ts | 2 +- apps/cs/src/pages/organization/ui/organization-page.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cs/src/entities/organization/api/organization-service.ts b/apps/cs/src/entities/organization/api/organization-service.ts index 456606076..ae7fc44ac 100644 --- a/apps/cs/src/entities/organization/api/organization-service.ts +++ b/apps/cs/src/entities/organization/api/organization-service.ts @@ -54,7 +54,7 @@ export class OrganizationApiService implements OrganizationService { } public async updateOrganizationByDnd( - data: OrganizationTreeForDnd, + data: OrganizationTreeForDnd[], ): Promise { await client.put( `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/dnd/tree`, diff --git a/apps/cs/src/entities/organization/model/organization-interface.ts b/apps/cs/src/entities/organization/model/organization-interface.ts index ab0983092..a63b85571 100644 --- a/apps/cs/src/entities/organization/model/organization-interface.ts +++ b/apps/cs/src/entities/organization/model/organization-interface.ts @@ -27,5 +27,5 @@ export interface OrganizationService { deleteOrganization(ognzSn: number): Promise; updateOrganizationName(ognzSn: number, name: string): Promise; updateOrganization(ognzSn: number, data: OrganizationFormData): Promise; - updateOrganizationByDnd(data: OrganizationTreeForDnd): Promise; + updateOrganizationByDnd(data: OrganizationTreeForDnd[]): Promise; } diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index fb920dbfc..545488305 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -99,7 +99,7 @@ function OrganizationPage() { const { mutateAsync: updateOrganizationByDnd } = useMutation({ mutationFn: (data: OrganizationTreeForDnd) => - OrganizationApi.updateOrganizationByDnd(data), + OrganizationApi.updateOrganizationByDnd([data]), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], From 4f6e335e7770399426380b4900ec58a5d6606711 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 24 Apr 2025 16:44:47 +0900 Subject: [PATCH 29/44] =?UTF-8?q?design:=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/organization/ui/user-organization-section.tsx | 2 +- packages/common/components/src/ui/checkbox/checkbox.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx index 55e94d143..74a367750 100644 --- a/apps/cs/src/pages/organization/ui/user-organization-section.tsx +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -237,7 +237,7 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { -
+
diff --git a/packages/common/components/src/ui/checkbox/checkbox.css b/packages/common/components/src/ui/checkbox/checkbox.css index c25f8d8e4..b1f1af872 100644 --- a/packages/common/components/src/ui/checkbox/checkbox.css +++ b/packages/common/components/src/ui/checkbox/checkbox.css @@ -2,13 +2,13 @@ @layer components { button[role="checkbox"] { - @apply size-[1.5rem] shrink-0 overflow-hidden rounded-[0.5rem] border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground; + @apply size-[1.5rem] shrink-0 overflow-hidden rounded-[0.5rem] border border-primary bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground; } button[role="checkbox"] > span { @apply flex size-full items-center justify-center text-current; } button[role="checkbox"] + label { - @apply text-label text-[1.3rem] font-medium; + @apply text-[1.3rem] font-medium text-label; } button[role="checkbox"]:disabled + label { @apply cursor-not-allowed opacity-70; From 4284ed87ea5695918c0c33081d0059a5e120dbce Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 24 Apr 2025 17:57:36 +0900 Subject: [PATCH 30/44] rename: common/feature -> common/features --- apps/common/{feature => features}/.gitignore | 0 apps/common/{feature => features}/.prettierrc | 0 .../app/(with-base-layout)/board/page.tsx | 0 .../app/(with-base-layout)/layout.tsx | 0 .../app/(with-base-layout)/loading.tsx | 0 .../app/api/proxy/[...url]/route.ts | 0 .../{feature => features}/app/auth/login/page.tsx | 0 apps/common/{feature => features}/app/layout.tsx | 0 apps/common/{feature => features}/eslint.config.mjs | 0 apps/common/{feature => features}/middleware.ts | 0 apps/common/{feature => features}/next.config.ts | 0 apps/common/{feature => features}/package.json | 0 apps/common/{feature => features}/pages/.gitkeep | 0 .../common/{feature => features}/postcss.config.mjs | 0 .../public/images/bg_login.jpg | Bin apps/common/{feature => features}/src/app/index.tsx | 0 .../src/app/providers/app-provider.tsx | 0 .../src/app/providers/index.ts | 0 .../{feature => features}/src/app/styles/base.css | 0 .../src/app/styles/components.css | 0 .../entities/attachment/api/attachment-service.ts | 0 .../src/entities/attachment/index.ts | 0 .../attachment/model/attachment-interface.ts | 0 .../src/entities/auth/api/login-service.ts | 0 .../src/entities/auth/index.ts | 0 .../src/entities/auth/lib/auth-wrapper.tsx | 0 .../src/entities/auth/lib/token-manager.ts | 0 .../src/entities/auth/model/login-interface.ts | 0 .../src/entities/board/api/board-service.ts | 0 .../src/entities/board/index.ts | 0 .../src/entities/board/model/board-interface.ts | 0 .../src/entities/code/@x/posting.ts | 0 .../src/entities/code/api/code-service.ts | 0 .../src/entities/code/index.ts | 0 .../src/entities/code/model/code-interface.ts | 0 .../src/entities/code/ui/code-select.tsx | 0 .../src/entities/editor/api/editor-service.ts | 0 .../src/entities/editor/index.ts | 0 .../src/entities/editor/lib/blob-to-base64.ts | 0 .../src/entities/editor/lib/generate-file-info.ts | 0 .../src/entities/editor/model/editor-base-config.ts | 0 .../src/entities/editor/model/editor-interface.ts | 0 .../src/entities/editor/ui/editor.tsx | 0 .../menu-authority/api/menu-authority-service.ts | 0 .../src/entities/menu-authority/index.ts | 0 .../model/menu-authority-interface.ts | 0 .../model/menu-authority-table-columns.tsx | 0 .../ui/management-authority-dialog.tsx | 0 .../src/entities/menu/api/menu-service.ts | 0 .../src/entities/menu/index.ts | 0 .../src/entities/menu/lib/menu-wrapper.tsx | 0 .../src/entities/menu/lib/utils.ts | 0 .../src/entities/menu/model/menu-interface.ts | 0 .../src/entities/posting/api/posting-service.ts | 0 .../src/entities/posting/index.ts | 0 .../src/entities/posting/model/posting-interface.ts | 0 .../src/entities/posting/ui/posting-dialog.tsx | 0 .../src/entities/site/@x/posting.ts | 0 .../src/entities/site/api/site-service.ts | 0 .../src/entities/site/index.ts | 0 .../src/entities/site/model/site-interface.ts | 0 .../src/pages/auth/login/index.ts | 0 .../src/pages/auth/login/ui/login-page.tsx | 0 .../src/pages/auth/login/ui/login.css | 0 .../{feature => features}/src/pages/board/index.ts | 0 .../src/pages/board/ui/board-page.tsx | 0 .../{feature => features}/src/shared/api/client.ts | 0 .../{feature => features}/src/shared/api/index.ts | 0 .../{feature => features}/src/shared/config/api.ts | 0 .../{feature => features}/src/shared/config/env.ts | 0 .../src/shared/config/index.ts | 0 .../{feature => features}/src/shared/lib/cookie.ts | 0 .../{feature => features}/src/shared/lib/index.ts | 0 .../src/shared/lib/use-alert-store.ts | 0 .../src/shared/lib/use-attachment-store.ts | 0 .../src/shared/lib/use-menu-store.ts | 0 .../{feature => features}/src/shared/lib/use-tus.ts | 0 .../src/shared/lib/use-user-store.ts | 0 .../src/shared/model/api-interface.ts | 0 .../{feature => features}/src/shared/model/index.ts | 0 .../src/shared/providers/index.ts | 0 .../src/shared/providers/store-provider.tsx | 0 .../src/shared/store/alert-store.ts | 0 .../src/shared/store/attachment-store.ts | 0 .../{feature => features}/src/shared/store/index.ts | 0 .../src/shared/store/menu-store.ts | 0 .../src/shared/store/user-store.ts | 0 .../src/shared/ui/alert-dialog.tsx | 0 .../src/shared/ui/confirm-dialog.tsx | 0 .../{feature => features}/src/shared/ui/form.tsx | 0 .../src/shared/ui/page-layout.tsx | 0 .../src/shared/ui/skeleton-page.tsx | 0 .../src/widgets/form-wrapper/index.ts | 0 .../src/widgets/form-wrapper/ui/form-wrapper.tsx | 0 .../common/{feature => features}/tailwind.config.ts | 0 apps/common/{feature => features}/tsconfig.json | 0 96 files changed, 0 insertions(+), 0 deletions(-) rename apps/common/{feature => features}/.gitignore (100%) rename apps/common/{feature => features}/.prettierrc (100%) rename apps/common/{feature => features}/app/(with-base-layout)/board/page.tsx (100%) rename apps/common/{feature => features}/app/(with-base-layout)/layout.tsx (100%) rename apps/common/{feature => features}/app/(with-base-layout)/loading.tsx (100%) rename apps/common/{feature => features}/app/api/proxy/[...url]/route.ts (100%) rename apps/common/{feature => features}/app/auth/login/page.tsx (100%) rename apps/common/{feature => features}/app/layout.tsx (100%) rename apps/common/{feature => features}/eslint.config.mjs (100%) rename apps/common/{feature => features}/middleware.ts (100%) rename apps/common/{feature => features}/next.config.ts (100%) rename apps/common/{feature => features}/package.json (100%) rename apps/common/{feature => features}/pages/.gitkeep (100%) rename apps/common/{feature => features}/postcss.config.mjs (100%) rename apps/common/{feature => features}/public/images/bg_login.jpg (100%) rename apps/common/{feature => features}/src/app/index.tsx (100%) rename apps/common/{feature => features}/src/app/providers/app-provider.tsx (100%) rename apps/common/{feature => features}/src/app/providers/index.ts (100%) rename apps/common/{feature => features}/src/app/styles/base.css (100%) rename apps/common/{feature => features}/src/app/styles/components.css (100%) rename apps/common/{feature => features}/src/entities/attachment/api/attachment-service.ts (100%) rename apps/common/{feature => features}/src/entities/attachment/index.ts (100%) rename apps/common/{feature => features}/src/entities/attachment/model/attachment-interface.ts (100%) rename apps/common/{feature => features}/src/entities/auth/api/login-service.ts (100%) rename apps/common/{feature => features}/src/entities/auth/index.ts (100%) rename apps/common/{feature => features}/src/entities/auth/lib/auth-wrapper.tsx (100%) rename apps/common/{feature => features}/src/entities/auth/lib/token-manager.ts (100%) rename apps/common/{feature => features}/src/entities/auth/model/login-interface.ts (100%) rename apps/common/{feature => features}/src/entities/board/api/board-service.ts (100%) rename apps/common/{feature => features}/src/entities/board/index.ts (100%) rename apps/common/{feature => features}/src/entities/board/model/board-interface.ts (100%) rename apps/common/{feature => features}/src/entities/code/@x/posting.ts (100%) rename apps/common/{feature => features}/src/entities/code/api/code-service.ts (100%) rename apps/common/{feature => features}/src/entities/code/index.ts (100%) rename apps/common/{feature => features}/src/entities/code/model/code-interface.ts (100%) rename apps/common/{feature => features}/src/entities/code/ui/code-select.tsx (100%) rename apps/common/{feature => features}/src/entities/editor/api/editor-service.ts (100%) rename apps/common/{feature => features}/src/entities/editor/index.ts (100%) rename apps/common/{feature => features}/src/entities/editor/lib/blob-to-base64.ts (100%) rename apps/common/{feature => features}/src/entities/editor/lib/generate-file-info.ts (100%) rename apps/common/{feature => features}/src/entities/editor/model/editor-base-config.ts (100%) rename apps/common/{feature => features}/src/entities/editor/model/editor-interface.ts (100%) rename apps/common/{feature => features}/src/entities/editor/ui/editor.tsx (100%) rename apps/common/{feature => features}/src/entities/menu-authority/api/menu-authority-service.ts (100%) rename apps/common/{feature => features}/src/entities/menu-authority/index.ts (100%) rename apps/common/{feature => features}/src/entities/menu-authority/model/menu-authority-interface.ts (100%) rename apps/common/{feature => features}/src/entities/menu-authority/model/menu-authority-table-columns.tsx (100%) rename apps/common/{feature => features}/src/entities/menu-authority/ui/management-authority-dialog.tsx (100%) rename apps/common/{feature => features}/src/entities/menu/api/menu-service.ts (100%) rename apps/common/{feature => features}/src/entities/menu/index.ts (100%) rename apps/common/{feature => features}/src/entities/menu/lib/menu-wrapper.tsx (100%) rename apps/common/{feature => features}/src/entities/menu/lib/utils.ts (100%) rename apps/common/{feature => features}/src/entities/menu/model/menu-interface.ts (100%) rename apps/common/{feature => features}/src/entities/posting/api/posting-service.ts (100%) rename apps/common/{feature => features}/src/entities/posting/index.ts (100%) rename apps/common/{feature => features}/src/entities/posting/model/posting-interface.ts (100%) rename apps/common/{feature => features}/src/entities/posting/ui/posting-dialog.tsx (100%) rename apps/common/{feature => features}/src/entities/site/@x/posting.ts (100%) rename apps/common/{feature => features}/src/entities/site/api/site-service.ts (100%) rename apps/common/{feature => features}/src/entities/site/index.ts (100%) rename apps/common/{feature => features}/src/entities/site/model/site-interface.ts (100%) rename apps/common/{feature => features}/src/pages/auth/login/index.ts (100%) rename apps/common/{feature => features}/src/pages/auth/login/ui/login-page.tsx (100%) rename apps/common/{feature => features}/src/pages/auth/login/ui/login.css (100%) rename apps/common/{feature => features}/src/pages/board/index.ts (100%) rename apps/common/{feature => features}/src/pages/board/ui/board-page.tsx (100%) rename apps/common/{feature => features}/src/shared/api/client.ts (100%) rename apps/common/{feature => features}/src/shared/api/index.ts (100%) rename apps/common/{feature => features}/src/shared/config/api.ts (100%) rename apps/common/{feature => features}/src/shared/config/env.ts (100%) rename apps/common/{feature => features}/src/shared/config/index.ts (100%) rename apps/common/{feature => features}/src/shared/lib/cookie.ts (100%) rename apps/common/{feature => features}/src/shared/lib/index.ts (100%) rename apps/common/{feature => features}/src/shared/lib/use-alert-store.ts (100%) rename apps/common/{feature => features}/src/shared/lib/use-attachment-store.ts (100%) rename apps/common/{feature => features}/src/shared/lib/use-menu-store.ts (100%) rename apps/common/{feature => features}/src/shared/lib/use-tus.ts (100%) rename apps/common/{feature => features}/src/shared/lib/use-user-store.ts (100%) rename apps/common/{feature => features}/src/shared/model/api-interface.ts (100%) rename apps/common/{feature => features}/src/shared/model/index.ts (100%) rename apps/common/{feature => features}/src/shared/providers/index.ts (100%) rename apps/common/{feature => features}/src/shared/providers/store-provider.tsx (100%) rename apps/common/{feature => features}/src/shared/store/alert-store.ts (100%) rename apps/common/{feature => features}/src/shared/store/attachment-store.ts (100%) rename apps/common/{feature => features}/src/shared/store/index.ts (100%) rename apps/common/{feature => features}/src/shared/store/menu-store.ts (100%) rename apps/common/{feature => features}/src/shared/store/user-store.ts (100%) rename apps/common/{feature => features}/src/shared/ui/alert-dialog.tsx (100%) rename apps/common/{feature => features}/src/shared/ui/confirm-dialog.tsx (100%) rename apps/common/{feature => features}/src/shared/ui/form.tsx (100%) rename apps/common/{feature => features}/src/shared/ui/page-layout.tsx (100%) rename apps/common/{feature => features}/src/shared/ui/skeleton-page.tsx (100%) rename apps/common/{feature => features}/src/widgets/form-wrapper/index.ts (100%) rename apps/common/{feature => features}/src/widgets/form-wrapper/ui/form-wrapper.tsx (100%) rename apps/common/{feature => features}/tailwind.config.ts (100%) rename apps/common/{feature => features}/tsconfig.json (100%) diff --git a/apps/common/feature/.gitignore b/apps/common/features/.gitignore similarity index 100% rename from apps/common/feature/.gitignore rename to apps/common/features/.gitignore diff --git a/apps/common/feature/.prettierrc b/apps/common/features/.prettierrc similarity index 100% rename from apps/common/feature/.prettierrc rename to apps/common/features/.prettierrc diff --git a/apps/common/feature/app/(with-base-layout)/board/page.tsx b/apps/common/features/app/(with-base-layout)/board/page.tsx similarity index 100% rename from apps/common/feature/app/(with-base-layout)/board/page.tsx rename to apps/common/features/app/(with-base-layout)/board/page.tsx diff --git a/apps/common/feature/app/(with-base-layout)/layout.tsx b/apps/common/features/app/(with-base-layout)/layout.tsx similarity index 100% rename from apps/common/feature/app/(with-base-layout)/layout.tsx rename to apps/common/features/app/(with-base-layout)/layout.tsx diff --git a/apps/common/feature/app/(with-base-layout)/loading.tsx b/apps/common/features/app/(with-base-layout)/loading.tsx similarity index 100% rename from apps/common/feature/app/(with-base-layout)/loading.tsx rename to apps/common/features/app/(with-base-layout)/loading.tsx diff --git a/apps/common/feature/app/api/proxy/[...url]/route.ts b/apps/common/features/app/api/proxy/[...url]/route.ts similarity index 100% rename from apps/common/feature/app/api/proxy/[...url]/route.ts rename to apps/common/features/app/api/proxy/[...url]/route.ts diff --git a/apps/common/feature/app/auth/login/page.tsx b/apps/common/features/app/auth/login/page.tsx similarity index 100% rename from apps/common/feature/app/auth/login/page.tsx rename to apps/common/features/app/auth/login/page.tsx diff --git a/apps/common/feature/app/layout.tsx b/apps/common/features/app/layout.tsx similarity index 100% rename from apps/common/feature/app/layout.tsx rename to apps/common/features/app/layout.tsx diff --git a/apps/common/feature/eslint.config.mjs b/apps/common/features/eslint.config.mjs similarity index 100% rename from apps/common/feature/eslint.config.mjs rename to apps/common/features/eslint.config.mjs diff --git a/apps/common/feature/middleware.ts b/apps/common/features/middleware.ts similarity index 100% rename from apps/common/feature/middleware.ts rename to apps/common/features/middleware.ts diff --git a/apps/common/feature/next.config.ts b/apps/common/features/next.config.ts similarity index 100% rename from apps/common/feature/next.config.ts rename to apps/common/features/next.config.ts diff --git a/apps/common/feature/package.json b/apps/common/features/package.json similarity index 100% rename from apps/common/feature/package.json rename to apps/common/features/package.json diff --git a/apps/common/feature/pages/.gitkeep b/apps/common/features/pages/.gitkeep similarity index 100% rename from apps/common/feature/pages/.gitkeep rename to apps/common/features/pages/.gitkeep diff --git a/apps/common/feature/postcss.config.mjs b/apps/common/features/postcss.config.mjs similarity index 100% rename from apps/common/feature/postcss.config.mjs rename to apps/common/features/postcss.config.mjs diff --git a/apps/common/feature/public/images/bg_login.jpg b/apps/common/features/public/images/bg_login.jpg similarity index 100% rename from apps/common/feature/public/images/bg_login.jpg rename to apps/common/features/public/images/bg_login.jpg diff --git a/apps/common/feature/src/app/index.tsx b/apps/common/features/src/app/index.tsx similarity index 100% rename from apps/common/feature/src/app/index.tsx rename to apps/common/features/src/app/index.tsx diff --git a/apps/common/feature/src/app/providers/app-provider.tsx b/apps/common/features/src/app/providers/app-provider.tsx similarity index 100% rename from apps/common/feature/src/app/providers/app-provider.tsx rename to apps/common/features/src/app/providers/app-provider.tsx diff --git a/apps/common/feature/src/app/providers/index.ts b/apps/common/features/src/app/providers/index.ts similarity index 100% rename from apps/common/feature/src/app/providers/index.ts rename to apps/common/features/src/app/providers/index.ts diff --git a/apps/common/feature/src/app/styles/base.css b/apps/common/features/src/app/styles/base.css similarity index 100% rename from apps/common/feature/src/app/styles/base.css rename to apps/common/features/src/app/styles/base.css diff --git a/apps/common/feature/src/app/styles/components.css b/apps/common/features/src/app/styles/components.css similarity index 100% rename from apps/common/feature/src/app/styles/components.css rename to apps/common/features/src/app/styles/components.css diff --git a/apps/common/feature/src/entities/attachment/api/attachment-service.ts b/apps/common/features/src/entities/attachment/api/attachment-service.ts similarity index 100% rename from apps/common/feature/src/entities/attachment/api/attachment-service.ts rename to apps/common/features/src/entities/attachment/api/attachment-service.ts diff --git a/apps/common/feature/src/entities/attachment/index.ts b/apps/common/features/src/entities/attachment/index.ts similarity index 100% rename from apps/common/feature/src/entities/attachment/index.ts rename to apps/common/features/src/entities/attachment/index.ts diff --git a/apps/common/feature/src/entities/attachment/model/attachment-interface.ts b/apps/common/features/src/entities/attachment/model/attachment-interface.ts similarity index 100% rename from apps/common/feature/src/entities/attachment/model/attachment-interface.ts rename to apps/common/features/src/entities/attachment/model/attachment-interface.ts diff --git a/apps/common/feature/src/entities/auth/api/login-service.ts b/apps/common/features/src/entities/auth/api/login-service.ts similarity index 100% rename from apps/common/feature/src/entities/auth/api/login-service.ts rename to apps/common/features/src/entities/auth/api/login-service.ts diff --git a/apps/common/feature/src/entities/auth/index.ts b/apps/common/features/src/entities/auth/index.ts similarity index 100% rename from apps/common/feature/src/entities/auth/index.ts rename to apps/common/features/src/entities/auth/index.ts diff --git a/apps/common/feature/src/entities/auth/lib/auth-wrapper.tsx b/apps/common/features/src/entities/auth/lib/auth-wrapper.tsx similarity index 100% rename from apps/common/feature/src/entities/auth/lib/auth-wrapper.tsx rename to apps/common/features/src/entities/auth/lib/auth-wrapper.tsx diff --git a/apps/common/feature/src/entities/auth/lib/token-manager.ts b/apps/common/features/src/entities/auth/lib/token-manager.ts similarity index 100% rename from apps/common/feature/src/entities/auth/lib/token-manager.ts rename to apps/common/features/src/entities/auth/lib/token-manager.ts diff --git a/apps/common/feature/src/entities/auth/model/login-interface.ts b/apps/common/features/src/entities/auth/model/login-interface.ts similarity index 100% rename from apps/common/feature/src/entities/auth/model/login-interface.ts rename to apps/common/features/src/entities/auth/model/login-interface.ts diff --git a/apps/common/feature/src/entities/board/api/board-service.ts b/apps/common/features/src/entities/board/api/board-service.ts similarity index 100% rename from apps/common/feature/src/entities/board/api/board-service.ts rename to apps/common/features/src/entities/board/api/board-service.ts diff --git a/apps/common/feature/src/entities/board/index.ts b/apps/common/features/src/entities/board/index.ts similarity index 100% rename from apps/common/feature/src/entities/board/index.ts rename to apps/common/features/src/entities/board/index.ts diff --git a/apps/common/feature/src/entities/board/model/board-interface.ts b/apps/common/features/src/entities/board/model/board-interface.ts similarity index 100% rename from apps/common/feature/src/entities/board/model/board-interface.ts rename to apps/common/features/src/entities/board/model/board-interface.ts diff --git a/apps/common/feature/src/entities/code/@x/posting.ts b/apps/common/features/src/entities/code/@x/posting.ts similarity index 100% rename from apps/common/feature/src/entities/code/@x/posting.ts rename to apps/common/features/src/entities/code/@x/posting.ts diff --git a/apps/common/feature/src/entities/code/api/code-service.ts b/apps/common/features/src/entities/code/api/code-service.ts similarity index 100% rename from apps/common/feature/src/entities/code/api/code-service.ts rename to apps/common/features/src/entities/code/api/code-service.ts diff --git a/apps/common/feature/src/entities/code/index.ts b/apps/common/features/src/entities/code/index.ts similarity index 100% rename from apps/common/feature/src/entities/code/index.ts rename to apps/common/features/src/entities/code/index.ts diff --git a/apps/common/feature/src/entities/code/model/code-interface.ts b/apps/common/features/src/entities/code/model/code-interface.ts similarity index 100% rename from apps/common/feature/src/entities/code/model/code-interface.ts rename to apps/common/features/src/entities/code/model/code-interface.ts diff --git a/apps/common/feature/src/entities/code/ui/code-select.tsx b/apps/common/features/src/entities/code/ui/code-select.tsx similarity index 100% rename from apps/common/feature/src/entities/code/ui/code-select.tsx rename to apps/common/features/src/entities/code/ui/code-select.tsx diff --git a/apps/common/feature/src/entities/editor/api/editor-service.ts b/apps/common/features/src/entities/editor/api/editor-service.ts similarity index 100% rename from apps/common/feature/src/entities/editor/api/editor-service.ts rename to apps/common/features/src/entities/editor/api/editor-service.ts diff --git a/apps/common/feature/src/entities/editor/index.ts b/apps/common/features/src/entities/editor/index.ts similarity index 100% rename from apps/common/feature/src/entities/editor/index.ts rename to apps/common/features/src/entities/editor/index.ts diff --git a/apps/common/feature/src/entities/editor/lib/blob-to-base64.ts b/apps/common/features/src/entities/editor/lib/blob-to-base64.ts similarity index 100% rename from apps/common/feature/src/entities/editor/lib/blob-to-base64.ts rename to apps/common/features/src/entities/editor/lib/blob-to-base64.ts diff --git a/apps/common/feature/src/entities/editor/lib/generate-file-info.ts b/apps/common/features/src/entities/editor/lib/generate-file-info.ts similarity index 100% rename from apps/common/feature/src/entities/editor/lib/generate-file-info.ts rename to apps/common/features/src/entities/editor/lib/generate-file-info.ts diff --git a/apps/common/feature/src/entities/editor/model/editor-base-config.ts b/apps/common/features/src/entities/editor/model/editor-base-config.ts similarity index 100% rename from apps/common/feature/src/entities/editor/model/editor-base-config.ts rename to apps/common/features/src/entities/editor/model/editor-base-config.ts diff --git a/apps/common/feature/src/entities/editor/model/editor-interface.ts b/apps/common/features/src/entities/editor/model/editor-interface.ts similarity index 100% rename from apps/common/feature/src/entities/editor/model/editor-interface.ts rename to apps/common/features/src/entities/editor/model/editor-interface.ts diff --git a/apps/common/feature/src/entities/editor/ui/editor.tsx b/apps/common/features/src/entities/editor/ui/editor.tsx similarity index 100% rename from apps/common/feature/src/entities/editor/ui/editor.tsx rename to apps/common/features/src/entities/editor/ui/editor.tsx diff --git a/apps/common/feature/src/entities/menu-authority/api/menu-authority-service.ts b/apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts similarity index 100% rename from apps/common/feature/src/entities/menu-authority/api/menu-authority-service.ts rename to apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts diff --git a/apps/common/feature/src/entities/menu-authority/index.ts b/apps/common/features/src/entities/menu-authority/index.ts similarity index 100% rename from apps/common/feature/src/entities/menu-authority/index.ts rename to apps/common/features/src/entities/menu-authority/index.ts diff --git a/apps/common/feature/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts similarity index 100% rename from apps/common/feature/src/entities/menu-authority/model/menu-authority-interface.ts rename to apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts diff --git a/apps/common/feature/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx similarity index 100% rename from apps/common/feature/src/entities/menu-authority/model/menu-authority-table-columns.tsx rename to apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx diff --git a/apps/common/feature/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx similarity index 100% rename from apps/common/feature/src/entities/menu-authority/ui/management-authority-dialog.tsx rename to apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx diff --git a/apps/common/feature/src/entities/menu/api/menu-service.ts b/apps/common/features/src/entities/menu/api/menu-service.ts similarity index 100% rename from apps/common/feature/src/entities/menu/api/menu-service.ts rename to apps/common/features/src/entities/menu/api/menu-service.ts diff --git a/apps/common/feature/src/entities/menu/index.ts b/apps/common/features/src/entities/menu/index.ts similarity index 100% rename from apps/common/feature/src/entities/menu/index.ts rename to apps/common/features/src/entities/menu/index.ts diff --git a/apps/common/feature/src/entities/menu/lib/menu-wrapper.tsx b/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx similarity index 100% rename from apps/common/feature/src/entities/menu/lib/menu-wrapper.tsx rename to apps/common/features/src/entities/menu/lib/menu-wrapper.tsx diff --git a/apps/common/feature/src/entities/menu/lib/utils.ts b/apps/common/features/src/entities/menu/lib/utils.ts similarity index 100% rename from apps/common/feature/src/entities/menu/lib/utils.ts rename to apps/common/features/src/entities/menu/lib/utils.ts diff --git a/apps/common/feature/src/entities/menu/model/menu-interface.ts b/apps/common/features/src/entities/menu/model/menu-interface.ts similarity index 100% rename from apps/common/feature/src/entities/menu/model/menu-interface.ts rename to apps/common/features/src/entities/menu/model/menu-interface.ts diff --git a/apps/common/feature/src/entities/posting/api/posting-service.ts b/apps/common/features/src/entities/posting/api/posting-service.ts similarity index 100% rename from apps/common/feature/src/entities/posting/api/posting-service.ts rename to apps/common/features/src/entities/posting/api/posting-service.ts diff --git a/apps/common/feature/src/entities/posting/index.ts b/apps/common/features/src/entities/posting/index.ts similarity index 100% rename from apps/common/feature/src/entities/posting/index.ts rename to apps/common/features/src/entities/posting/index.ts diff --git a/apps/common/feature/src/entities/posting/model/posting-interface.ts b/apps/common/features/src/entities/posting/model/posting-interface.ts similarity index 100% rename from apps/common/feature/src/entities/posting/model/posting-interface.ts rename to apps/common/features/src/entities/posting/model/posting-interface.ts diff --git a/apps/common/feature/src/entities/posting/ui/posting-dialog.tsx b/apps/common/features/src/entities/posting/ui/posting-dialog.tsx similarity index 100% rename from apps/common/feature/src/entities/posting/ui/posting-dialog.tsx rename to apps/common/features/src/entities/posting/ui/posting-dialog.tsx diff --git a/apps/common/feature/src/entities/site/@x/posting.ts b/apps/common/features/src/entities/site/@x/posting.ts similarity index 100% rename from apps/common/feature/src/entities/site/@x/posting.ts rename to apps/common/features/src/entities/site/@x/posting.ts diff --git a/apps/common/feature/src/entities/site/api/site-service.ts b/apps/common/features/src/entities/site/api/site-service.ts similarity index 100% rename from apps/common/feature/src/entities/site/api/site-service.ts rename to apps/common/features/src/entities/site/api/site-service.ts diff --git a/apps/common/feature/src/entities/site/index.ts b/apps/common/features/src/entities/site/index.ts similarity index 100% rename from apps/common/feature/src/entities/site/index.ts rename to apps/common/features/src/entities/site/index.ts diff --git a/apps/common/feature/src/entities/site/model/site-interface.ts b/apps/common/features/src/entities/site/model/site-interface.ts similarity index 100% rename from apps/common/feature/src/entities/site/model/site-interface.ts rename to apps/common/features/src/entities/site/model/site-interface.ts diff --git a/apps/common/feature/src/pages/auth/login/index.ts b/apps/common/features/src/pages/auth/login/index.ts similarity index 100% rename from apps/common/feature/src/pages/auth/login/index.ts rename to apps/common/features/src/pages/auth/login/index.ts diff --git a/apps/common/feature/src/pages/auth/login/ui/login-page.tsx b/apps/common/features/src/pages/auth/login/ui/login-page.tsx similarity index 100% rename from apps/common/feature/src/pages/auth/login/ui/login-page.tsx rename to apps/common/features/src/pages/auth/login/ui/login-page.tsx diff --git a/apps/common/feature/src/pages/auth/login/ui/login.css b/apps/common/features/src/pages/auth/login/ui/login.css similarity index 100% rename from apps/common/feature/src/pages/auth/login/ui/login.css rename to apps/common/features/src/pages/auth/login/ui/login.css diff --git a/apps/common/feature/src/pages/board/index.ts b/apps/common/features/src/pages/board/index.ts similarity index 100% rename from apps/common/feature/src/pages/board/index.ts rename to apps/common/features/src/pages/board/index.ts diff --git a/apps/common/feature/src/pages/board/ui/board-page.tsx b/apps/common/features/src/pages/board/ui/board-page.tsx similarity index 100% rename from apps/common/feature/src/pages/board/ui/board-page.tsx rename to apps/common/features/src/pages/board/ui/board-page.tsx diff --git a/apps/common/feature/src/shared/api/client.ts b/apps/common/features/src/shared/api/client.ts similarity index 100% rename from apps/common/feature/src/shared/api/client.ts rename to apps/common/features/src/shared/api/client.ts diff --git a/apps/common/feature/src/shared/api/index.ts b/apps/common/features/src/shared/api/index.ts similarity index 100% rename from apps/common/feature/src/shared/api/index.ts rename to apps/common/features/src/shared/api/index.ts diff --git a/apps/common/feature/src/shared/config/api.ts b/apps/common/features/src/shared/config/api.ts similarity index 100% rename from apps/common/feature/src/shared/config/api.ts rename to apps/common/features/src/shared/config/api.ts diff --git a/apps/common/feature/src/shared/config/env.ts b/apps/common/features/src/shared/config/env.ts similarity index 100% rename from apps/common/feature/src/shared/config/env.ts rename to apps/common/features/src/shared/config/env.ts diff --git a/apps/common/feature/src/shared/config/index.ts b/apps/common/features/src/shared/config/index.ts similarity index 100% rename from apps/common/feature/src/shared/config/index.ts rename to apps/common/features/src/shared/config/index.ts diff --git a/apps/common/feature/src/shared/lib/cookie.ts b/apps/common/features/src/shared/lib/cookie.ts similarity index 100% rename from apps/common/feature/src/shared/lib/cookie.ts rename to apps/common/features/src/shared/lib/cookie.ts diff --git a/apps/common/feature/src/shared/lib/index.ts b/apps/common/features/src/shared/lib/index.ts similarity index 100% rename from apps/common/feature/src/shared/lib/index.ts rename to apps/common/features/src/shared/lib/index.ts diff --git a/apps/common/feature/src/shared/lib/use-alert-store.ts b/apps/common/features/src/shared/lib/use-alert-store.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-alert-store.ts rename to apps/common/features/src/shared/lib/use-alert-store.ts diff --git a/apps/common/feature/src/shared/lib/use-attachment-store.ts b/apps/common/features/src/shared/lib/use-attachment-store.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-attachment-store.ts rename to apps/common/features/src/shared/lib/use-attachment-store.ts diff --git a/apps/common/feature/src/shared/lib/use-menu-store.ts b/apps/common/features/src/shared/lib/use-menu-store.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-menu-store.ts rename to apps/common/features/src/shared/lib/use-menu-store.ts diff --git a/apps/common/feature/src/shared/lib/use-tus.ts b/apps/common/features/src/shared/lib/use-tus.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-tus.ts rename to apps/common/features/src/shared/lib/use-tus.ts diff --git a/apps/common/feature/src/shared/lib/use-user-store.ts b/apps/common/features/src/shared/lib/use-user-store.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-user-store.ts rename to apps/common/features/src/shared/lib/use-user-store.ts diff --git a/apps/common/feature/src/shared/model/api-interface.ts b/apps/common/features/src/shared/model/api-interface.ts similarity index 100% rename from apps/common/feature/src/shared/model/api-interface.ts rename to apps/common/features/src/shared/model/api-interface.ts diff --git a/apps/common/feature/src/shared/model/index.ts b/apps/common/features/src/shared/model/index.ts similarity index 100% rename from apps/common/feature/src/shared/model/index.ts rename to apps/common/features/src/shared/model/index.ts diff --git a/apps/common/feature/src/shared/providers/index.ts b/apps/common/features/src/shared/providers/index.ts similarity index 100% rename from apps/common/feature/src/shared/providers/index.ts rename to apps/common/features/src/shared/providers/index.ts diff --git a/apps/common/feature/src/shared/providers/store-provider.tsx b/apps/common/features/src/shared/providers/store-provider.tsx similarity index 100% rename from apps/common/feature/src/shared/providers/store-provider.tsx rename to apps/common/features/src/shared/providers/store-provider.tsx diff --git a/apps/common/feature/src/shared/store/alert-store.ts b/apps/common/features/src/shared/store/alert-store.ts similarity index 100% rename from apps/common/feature/src/shared/store/alert-store.ts rename to apps/common/features/src/shared/store/alert-store.ts diff --git a/apps/common/feature/src/shared/store/attachment-store.ts b/apps/common/features/src/shared/store/attachment-store.ts similarity index 100% rename from apps/common/feature/src/shared/store/attachment-store.ts rename to apps/common/features/src/shared/store/attachment-store.ts diff --git a/apps/common/feature/src/shared/store/index.ts b/apps/common/features/src/shared/store/index.ts similarity index 100% rename from apps/common/feature/src/shared/store/index.ts rename to apps/common/features/src/shared/store/index.ts diff --git a/apps/common/feature/src/shared/store/menu-store.ts b/apps/common/features/src/shared/store/menu-store.ts similarity index 100% rename from apps/common/feature/src/shared/store/menu-store.ts rename to apps/common/features/src/shared/store/menu-store.ts diff --git a/apps/common/feature/src/shared/store/user-store.ts b/apps/common/features/src/shared/store/user-store.ts similarity index 100% rename from apps/common/feature/src/shared/store/user-store.ts rename to apps/common/features/src/shared/store/user-store.ts diff --git a/apps/common/feature/src/shared/ui/alert-dialog.tsx b/apps/common/features/src/shared/ui/alert-dialog.tsx similarity index 100% rename from apps/common/feature/src/shared/ui/alert-dialog.tsx rename to apps/common/features/src/shared/ui/alert-dialog.tsx diff --git a/apps/common/feature/src/shared/ui/confirm-dialog.tsx b/apps/common/features/src/shared/ui/confirm-dialog.tsx similarity index 100% rename from apps/common/feature/src/shared/ui/confirm-dialog.tsx rename to apps/common/features/src/shared/ui/confirm-dialog.tsx diff --git a/apps/common/feature/src/shared/ui/form.tsx b/apps/common/features/src/shared/ui/form.tsx similarity index 100% rename from apps/common/feature/src/shared/ui/form.tsx rename to apps/common/features/src/shared/ui/form.tsx diff --git a/apps/common/feature/src/shared/ui/page-layout.tsx b/apps/common/features/src/shared/ui/page-layout.tsx similarity index 100% rename from apps/common/feature/src/shared/ui/page-layout.tsx rename to apps/common/features/src/shared/ui/page-layout.tsx diff --git a/apps/common/feature/src/shared/ui/skeleton-page.tsx b/apps/common/features/src/shared/ui/skeleton-page.tsx similarity index 100% rename from apps/common/feature/src/shared/ui/skeleton-page.tsx rename to apps/common/features/src/shared/ui/skeleton-page.tsx diff --git a/apps/common/feature/src/widgets/form-wrapper/index.ts b/apps/common/features/src/widgets/form-wrapper/index.ts similarity index 100% rename from apps/common/feature/src/widgets/form-wrapper/index.ts rename to apps/common/features/src/widgets/form-wrapper/index.ts diff --git a/apps/common/feature/src/widgets/form-wrapper/ui/form-wrapper.tsx b/apps/common/features/src/widgets/form-wrapper/ui/form-wrapper.tsx similarity index 100% rename from apps/common/feature/src/widgets/form-wrapper/ui/form-wrapper.tsx rename to apps/common/features/src/widgets/form-wrapper/ui/form-wrapper.tsx diff --git a/apps/common/feature/tailwind.config.ts b/apps/common/features/tailwind.config.ts similarity index 100% rename from apps/common/feature/tailwind.config.ts rename to apps/common/features/tailwind.config.ts diff --git a/apps/common/feature/tsconfig.json b/apps/common/features/tsconfig.json similarity index 100% rename from apps/common/feature/tsconfig.json rename to apps/common/features/tsconfig.json From 44b820c62e6c6616cd5de8a27b35cdc0b3898315 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 28 Apr 2025 11:23:54 +0900 Subject: [PATCH 31/44] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/app/(with-base-layout)/bbs/page.tsx | 2 + .../src/entities/board/api/board-service.ts | 10 ++ apps/cs/src/entities/board/index.ts | 6 +- .../entities/board/model/board-interface.ts | 35 ++++- apps/cs/src/pages/bbs/index.ts | 1 + apps/cs/src/pages/bbs/model/table-columns.tsx | 41 +++++ apps/cs/src/pages/bbs/ui/bbs-page.tsx | 143 ++++++++++++++++++ 7 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/bbs/page.tsx create mode 100644 apps/cs/src/pages/bbs/index.ts create mode 100644 apps/cs/src/pages/bbs/model/table-columns.tsx create mode 100644 apps/cs/src/pages/bbs/ui/bbs-page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/page.tsx b/apps/cs/app/(with-base-layout)/bbs/page.tsx new file mode 100644 index 000000000..d9aab376a --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/page.tsx @@ -0,0 +1,2 @@ +import { BbsPage } from "@/pages/bbs"; +export default BbsPage; diff --git a/apps/cs/src/entities/board/api/board-service.ts b/apps/cs/src/entities/board/api/board-service.ts index 9cf29599b..2ba0f55fd 100644 --- a/apps/cs/src/entities/board/api/board-service.ts +++ b/apps/cs/src/entities/board/api/board-service.ts @@ -1,12 +1,22 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { + BoardList, + BoardParams, BoardPopupList, BoardPopupParams, BoardService, } from "../model/board-interface"; export class BoardApiService implements BoardService { + public async getBoardList(params: BoardParams): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + { params }, + ); + return response.data; + } + public async getBoardPopupList( params: BoardPopupParams, ): Promise { diff --git a/apps/cs/src/entities/board/index.ts b/apps/cs/src/entities/board/index.ts index 2173bb4a0..73229695f 100644 --- a/apps/cs/src/entities/board/index.ts +++ b/apps/cs/src/entities/board/index.ts @@ -1,2 +1,6 @@ export { BoardApi } from "./api/board-service"; -export { type BoardPopup } from "./model/board-interface"; +export { + type Board, + type BoardParams, + type BoardPopup, +} from "./model/board-interface"; diff --git a/apps/cs/src/entities/board/model/board-interface.ts b/apps/cs/src/entities/board/model/board-interface.ts index 942840d74..ccfebdaed 100644 --- a/apps/cs/src/entities/board/model/board-interface.ts +++ b/apps/cs/src/entities/board/model/board-interface.ts @@ -9,13 +9,34 @@ export const BOARD_TYPE = { export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE]; +export interface BoardParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number; + bbsTypeCd?: string; + bbsNm?: string; + bbsExpln?: string; + rfrncUseYn?: string; + atflUseYn?: string; + atflMaxCnt?: number; + atflMaxFileSz?: number; + ansUseYn?: string; + cmntUseYn?: string; + dgstfnEvlUseYn?: string; + smsUseYn?: string; + upndFixUseYn?: string; + pstgPrdUseYn?: string; + popupUseYn?: string; + delYn?: string; +} + export interface BoardPopupParams extends PageableParams { bbsNm?: string; } -export interface BoardPopup { +export interface Board { bbsId: number; - bbsTypeCd: BoardType; + bbsTypeCd: string; bbsTypeNm: string; bbsNm: string; bbsExpln: string; @@ -24,6 +45,7 @@ export interface BoardPopup { atflMaxCnt: number; atflMaxFileSz: number; ansUseYn: string; + ansUseAuthrt: string; cmntUseYn: string; dgstfnEvlUseYn: string; smsUseYn: string; @@ -31,12 +53,19 @@ export interface BoardPopup { pstgPrdUseYn: string; popupUseYn: string; delYn: string; - regDt: string; + useYn: string; + mdfcnDt: string; + mdfrNm: string; +} + +export interface BoardPopup extends Board { pstCnt: number; } +export type BoardList = PageableList; export type BoardPopupList = PageableList; export interface BoardService { + getBoardList: (params: BoardParams) => Promise; getBoardPopupList: (params: BoardPopupParams) => Promise; } diff --git a/apps/cs/src/pages/bbs/index.ts b/apps/cs/src/pages/bbs/index.ts new file mode 100644 index 000000000..acdf5e213 --- /dev/null +++ b/apps/cs/src/pages/bbs/index.ts @@ -0,0 +1 @@ +export { BbsPage } from "./ui/bbs-page"; diff --git a/apps/cs/src/pages/bbs/model/table-columns.tsx b/apps/cs/src/pages/bbs/model/table-columns.tsx new file mode 100644 index 000000000..697f953a7 --- /dev/null +++ b/apps/cs/src/pages/bbs/model/table-columns.tsx @@ -0,0 +1,41 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@common/components/ui"; +import type { Board } from "@/entities/board/model/board-interface"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row }) => row.index + 1, + }, + { + accessorKey: "bbsNm", + header: "게시판명", + }, + { + accessorKey: "bbsTypeNm", + header: "게시판유형", + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => + row.original.mdfcnDt + ? dayjs(row.original.mdfcnDt).format("YYYY-MM-DD") + : "", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + id: "actions", + header: "관리", + cell: () => , + }, +]; diff --git a/apps/cs/src/pages/bbs/ui/bbs-page.tsx b/apps/cs/src/pages/bbs/ui/bbs-page.tsx new file mode 100644 index 000000000..393ee957b --- /dev/null +++ b/apps/cs/src/pages/bbs/ui/bbs-page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; +import { + Button, + DataTable, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { CodeSelect } from "@/entities/code"; +import { BoardApi, type BoardParams } from "@/entities/board"; +import { ENDPOINT } from "@/shared/config"; +import { type Board } from "@/entities/board"; +import { columns } from "../model/table-columns"; + +function BbsPage() { + const router = useRouter(); + + const [rowSelection, setRowSelection] = useState({}); + const [boardParams, setBoardParams] = useState({ + bbsTypeCd: "", + bbsNm: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: boardList, refetch } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.BBS], + queryFn: () => + BoardApi.getBoardList({ + ...boardParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + enabled: false, + }); + + const table = useReactTable({ + data: boardList?.content ?? [], + columns, + state: { + pagination, + rowSelection, + }, + rowCount: boardList?.totalElements ?? 0, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleRowClick = (row: Board) => {}; + + useEffect(() => { + refetch(); + }, [pagination]); + + return ( +
+
+
+ + + setBoardParams({ ...boardParams, bbsTypeCd: value }) + } + /> + + + setBoardParams({ ...boardParams, bbsNm: e.target.value }) + } + /> + + +
+
+
+
+

게시판 목록

+
+ + +
+
+
+
+ +
+
+
+ ); +} + +export { BbsPage }; From 4bcbff714a3ce3ae981db7697849293159317c79 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 28 Apr 2025 15:21:26 +0900 Subject: [PATCH 32/44] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D,=20=EC=88=98=EC=A0=95=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bbs/register/[id]/page.tsx | 10 + .../(with-base-layout)/bbs/register/page.tsx | 2 + .../src/entities/board/api/board-service.ts | 29 ++ apps/cs/src/entities/board/index.ts | 2 + .../entities/board/model/board-interface.ts | 25 + apps/cs/src/pages/bbs/index.ts | 2 + apps/cs/src/pages/bbs/ui/bbs-form.tsx | 447 ++++++++++++++++++ apps/cs/src/pages/bbs/ui/bbs-page.tsx | 4 +- apps/cs/src/pages/bbs/ui/create-page.tsx | 55 +++ apps/cs/src/pages/bbs/ui/modify-page.tsx | 45 ++ .../widgets/form-wrapper/ui/form-wrapper.tsx | 2 +- 11 files changed, 621 insertions(+), 2 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/bbs/register/page.tsx create mode 100644 apps/cs/src/pages/bbs/ui/bbs-form.tsx create mode 100644 apps/cs/src/pages/bbs/ui/create-page.tsx create mode 100644 apps/cs/src/pages/bbs/ui/modify-page.tsx diff --git a/apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx b/apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx new file mode 100644 index 000000000..07cc78b6f --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/bbs"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/bbs/register/page.tsx b/apps/cs/app/(with-base-layout)/bbs/register/page.tsx new file mode 100644 index 000000000..3e74aef97 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/bbs"; +export default CreatePage; diff --git a/apps/cs/src/entities/board/api/board-service.ts b/apps/cs/src/entities/board/api/board-service.ts index 2ba0f55fd..150ff677a 100644 --- a/apps/cs/src/entities/board/api/board-service.ts +++ b/apps/cs/src/entities/board/api/board-service.ts @@ -1,6 +1,8 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { + Board, + BoardFormData, BoardList, BoardParams, BoardPopupList, @@ -26,6 +28,33 @@ export class BoardApiService implements BoardService { ); return response.data; } + + public async getBoardInfo(bbsId: string): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`, + ); + return response.data; + } + + public async createBoard(data: BoardFormData): Promise { + const response = await client.post( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + data, + ); + return response.data; + } + + public async updateBoard(bbsId: string, data: BoardFormData): Promise { + const response = await client.put( + `${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`, + data, + ); + return response.data; + } + + public async deleteBoard(bbsId: string): Promise { + await client.delete(`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`); + } } export const BoardApi = new BoardApiService(); diff --git a/apps/cs/src/entities/board/index.ts b/apps/cs/src/entities/board/index.ts index 73229695f..f9ef1b610 100644 --- a/apps/cs/src/entities/board/index.ts +++ b/apps/cs/src/entities/board/index.ts @@ -1,6 +1,8 @@ export { BoardApi } from "./api/board-service"; export { + boardFormSchema, type Board, + type BoardFormData, type BoardParams, type BoardPopup, } from "./model/board-interface"; diff --git a/apps/cs/src/entities/board/model/board-interface.ts b/apps/cs/src/entities/board/model/board-interface.ts index ccfebdaed..1ed8f1d9f 100644 --- a/apps/cs/src/entities/board/model/board-interface.ts +++ b/apps/cs/src/entities/board/model/board-interface.ts @@ -1,3 +1,4 @@ +import { z } from "zod"; import type { PageableList, PageableParams } from "@/shared/model"; export const BOARD_TYPE = { @@ -62,10 +63,34 @@ export interface BoardPopup extends Board { pstCnt: number; } +export const boardFormSchema = z.object({ + bbsTypeCd: z.string(), + bbsNm: z.string(), + bbsExpln: z.string(), + rfrncUseYn: z.string(), + atflUseYn: z.string(), + atflMaxCnt: z.number().nullish(), + atflMaxFileSz: z.number().nullish(), + ansUseYn: z.string(), + ansUseAuthrt: z.string().nullish(), + cmntUseYn: z.string(), + dgstfnEvlUseYn: z.string(), + smsUseYn: z.string(), + upndFixUseYn: z.string(), + pstgPrdUseYn: z.string(), + popupUseYn: z.string(), + useYn: z.string(), +}); + export type BoardList = PageableList; export type BoardPopupList = PageableList; +export type BoardFormData = z.infer; export interface BoardService { getBoardList: (params: BoardParams) => Promise; getBoardPopupList: (params: BoardPopupParams) => Promise; + getBoardInfo: (bbsId: string) => Promise; + createBoard: (data: BoardFormData) => Promise; + updateBoard: (bbsId: string, data: BoardFormData) => Promise; + deleteBoard: (bbsId: string) => Promise; } diff --git a/apps/cs/src/pages/bbs/index.ts b/apps/cs/src/pages/bbs/index.ts index acdf5e213..bed839012 100644 --- a/apps/cs/src/pages/bbs/index.ts +++ b/apps/cs/src/pages/bbs/index.ts @@ -1 +1,3 @@ export { BbsPage } from "./ui/bbs-page"; +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; diff --git a/apps/cs/src/pages/bbs/ui/bbs-form.tsx b/apps/cs/src/pages/bbs/ui/bbs-form.tsx new file mode 100644 index 000000000..ac2b5ebd5 --- /dev/null +++ b/apps/cs/src/pages/bbs/ui/bbs-form.tsx @@ -0,0 +1,447 @@ +"use client"; + +import type { UseFormReturn } from "react-hook-form"; +import { + Input, + Label, + RadioGroup, + RadioGroupItem, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { FormWrapper } from "@/widgets/form-wrapper"; +import { CodeSelect } from "@/entities/code"; +import type { Board, BoardFormData } from "@/entities/board"; +import { FormControl, FormField, FormItem } from "@/shared/ui/form"; + +type BbsFormProps = { + form: UseFormReturn; + bbsId?: string; + handleSave: (data: BoardFormData) => Promise; +}; + +function BbsForm({ form, bbsId, ...props }: BbsFormProps) { + return ( + +
+
+ + + + + + + 게시판명 + + ( + + + + + + )} + /> + + + + 게시판 설명 + + ( + + + + + + )} + /> + + + + 게시판 유형 + + ( + + + + + + )} + /> + + + + 참조 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 첨부파일 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 첨부파일 최대 개수 + + ( + + + { + const value = e.target.value; + if (isNaN(Number(value))) return; + field.onChange(value === "" ? null : Number(value)); + }} + className="w-[12rem] !text-[1.3rem]" + placeholder="최대 개수 입력" + disabled={form.watch("atflUseYn") === "N"} + /> + + + )} + /> + + + + 첨부파일 최대 크기 + + ( + + + { + const value = e.target.value; + if (isNaN(Number(value))) return; + field.onChange(value === "" ? null : Number(value)); + }} + className="w-[12rem] !text-[1.3rem]" + placeholder="최대 크기 입력" + disabled={form.watch("atflUseYn") === "N"} + /> + + + )} + /> + + + + 답글 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 답글 사용 권한 + + ( + + + + + + )} + /> + + + + 댓글 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 게시물평가 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 팝업 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + SMS알림 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 상단고정 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 게시기간 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ ); +} + +export { BbsForm }; diff --git a/apps/cs/src/pages/bbs/ui/bbs-page.tsx b/apps/cs/src/pages/bbs/ui/bbs-page.tsx index 393ee957b..89562cb9c 100644 --- a/apps/cs/src/pages/bbs/ui/bbs-page.tsx +++ b/apps/cs/src/pages/bbs/ui/bbs-page.tsx @@ -70,7 +70,9 @@ function BbsPage() { refetch(); }; - const handleRowClick = (row: Board) => {}; + const handleRowClick = (row: Board) => { + router.push(`/bbs/register/${row.bbsId}`); + }; useEffect(() => { refetch(); diff --git a/apps/cs/src/pages/bbs/ui/create-page.tsx b/apps/cs/src/pages/bbs/ui/create-page.tsx new file mode 100644 index 000000000..bb710c33b --- /dev/null +++ b/apps/cs/src/pages/bbs/ui/create-page.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { + BoardApi, + boardFormSchema, + type BoardFormData, +} from "@/entities/board"; +import { BbsForm } from "./bbs-form"; + +function CreatePage() { + const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createBoard } = useMutation({ + mutationFn: (data: BoardFormData) => BoardApi.createBoard(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(boardFormSchema), + defaultValues: { + bbsTypeCd: "", + bbsNm: "", + bbsExpln: "", + rfrncUseYn: "Y", + atflUseYn: "Y", + ansUseYn: "Y", + cmntUseYn: "Y", + dgstfnEvlUseYn: "Y", + smsUseYn: "Y", + upndFixUseYn: "Y", + pstgPrdUseYn: "Y", + popupUseYn: "Y", + useYn: "Y", + }, + }); + + return ( +
+
+

게시판 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/bbs/ui/modify-page.tsx b/apps/cs/src/pages/bbs/ui/modify-page.tsx new file mode 100644 index 000000000..f411cbd5c --- /dev/null +++ b/apps/cs/src/pages/bbs/ui/modify-page.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { ENDPOINT } from "@/shared/config"; +import { + BoardApi, + boardFormSchema, + type BoardFormData, +} from "@/entities/board"; +import { BbsForm } from "./bbs-form"; + +function ModifyPage({ bbsId }: { bbsId: string }) { + const { setMessage: alert } = useAlertStore((state) => state); + + const { data: boardInfo } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId], + queryFn: () => BoardApi.getBoardInfo(bbsId), + }); + + const { mutateAsync: updateBoard } = useMutation({ + mutationFn: (data: BoardFormData) => BoardApi.updateBoard(bbsId, data), + onSuccess: () => { + alert("수정되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(boardFormSchema), + values: boardInfo, + }); + + return ( +
+
+

게시판 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/widgets/form-wrapper/ui/form-wrapper.tsx b/apps/cs/src/widgets/form-wrapper/ui/form-wrapper.tsx index 5b6edb3bc..071b299ee 100644 --- a/apps/cs/src/widgets/form-wrapper/ui/form-wrapper.tsx +++ b/apps/cs/src/widgets/form-wrapper/ui/form-wrapper.tsx @@ -54,7 +54,7 @@ function FormWrapper({ - {isModifyMode && ( + {isModifyMode && handleDelete && ( From 22233a9cba8cb7f90a0fa6b29ca1082e1277486d Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 29 Apr 2025 10:52:09 +0900 Subject: [PATCH 33/44] =?UTF-8?q?remove:=20feature=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/features/.gitignore | 40 --- apps/common/features/.prettierrc | 3 - .../app/(with-base-layout)/board/page.tsx | 2 - .../app/(with-base-layout)/layout.tsx | 35 -- .../app/(with-base-layout)/loading.tsx | 2 - .../features/app/api/proxy/[...url]/route.ts | 100 ------ apps/common/features/app/auth/login/page.tsx | 2 - apps/common/features/app/layout.tsx | 8 - apps/common/features/eslint.config.mjs | 21 -- apps/common/features/middleware.ts | 37 -- apps/common/features/next.config.ts | 5 - apps/common/features/package.json | 50 --- apps/common/features/pages/.gitkeep | 0 apps/common/features/postcss.config.mjs | 8 - .../features/public/images/bg_login.jpg | Bin 135228 -> 0 bytes apps/common/features/src/app/index.tsx | 18 - .../src/app/providers/app-provider.tsx | 28 -- .../features/src/app/providers/index.ts | 1 - apps/common/features/src/app/styles/base.css | 11 - .../features/src/app/styles/components.css | 129 ------- .../attachment/api/attachment-service.ts | 43 --- .../features/src/entities/attachment/index.ts | 1 - .../attachment/model/attachment-interface.ts | 22 -- .../src/entities/auth/api/login-service.ts | 85 ----- .../features/src/entities/auth/index.ts | 2 - .../src/entities/auth/lib/auth-wrapper.tsx | 49 --- .../src/entities/auth/lib/token-manager.ts | 59 ---- .../entities/auth/model/login-interface.ts | 9 - .../src/entities/board/api/board-service.ts | 21 -- .../features/src/entities/board/index.ts | 2 - .../entities/board/model/board-interface.ts | 42 --- .../features/src/entities/code/@x/posting.ts | 1 - .../src/entities/code/api/code-service.ts | 14 - .../features/src/entities/code/index.ts | 1 - .../src/entities/code/model/code-interface.ts | 9 - .../src/entities/code/ui/code-select.tsx | 42 --- .../src/entities/editor/api/editor-service.ts | 19 -- .../features/src/entities/editor/index.ts | 1 - .../src/entities/editor/lib/blob-to-base64.ts | 10 - .../entities/editor/lib/generate-file-info.ts | 16 - .../editor/model/editor-base-config.ts | 37 -- .../entities/editor/model/editor-interface.ts | 20 -- .../src/entities/editor/ui/editor.tsx | 60 ---- .../api/menu-authority-service.ts | 32 -- .../src/entities/menu-authority/index.ts | 1 - .../model/menu-authority-interface.ts | 41 --- .../model/menu-authority-table-columns.tsx | 117 ------- .../ui/management-authority-dialog.tsx | 101 ------ .../src/entities/menu/api/menu-service.ts | 14 - .../features/src/entities/menu/index.ts | 1 - .../src/entities/menu/lib/menu-wrapper.tsx | 29 -- .../features/src/entities/menu/lib/utils.ts | 20 -- .../src/entities/menu/model/menu-interface.ts | 23 -- .../entities/posting/api/posting-service.ts | 31 -- .../features/src/entities/posting/index.ts | 7 - .../posting/model/posting-interface.ts | 50 --- .../entities/posting/ui/posting-dialog.tsx | 323 ------------------ .../features/src/entities/site/@x/posting.ts | 2 - .../src/entities/site/api/site-service.ts | 51 --- .../features/src/entities/site/index.ts | 7 - .../src/entities/site/model/site-interface.ts | 59 ---- .../features/src/pages/auth/login/index.ts | 1 - .../src/pages/auth/login/ui/login-page.tsx | 129 ------- .../src/pages/auth/login/ui/login.css | 311 ----------------- apps/common/features/src/pages/board/index.ts | 1 - .../src/pages/board/ui/board-page.tsx | 5 - apps/common/features/src/shared/api/client.ts | 8 - apps/common/features/src/shared/api/index.ts | 1 - apps/common/features/src/shared/config/api.ts | 40 --- apps/common/features/src/shared/config/env.ts | 13 - .../features/src/shared/config/index.ts | 2 - apps/common/features/src/shared/lib/cookie.ts | 29 -- apps/common/features/src/shared/lib/index.ts | 3 - .../src/shared/lib/use-alert-store.ts | 14 - .../src/shared/lib/use-attachment-store.ts | 14 - .../features/src/shared/lib/use-menu-store.ts | 14 - .../common/features/src/shared/lib/use-tus.ts | 64 ---- .../features/src/shared/lib/use-user-store.ts | 14 - .../src/shared/model/api-interface.ts | 34 -- .../common/features/src/shared/model/index.ts | 1 - .../features/src/shared/providers/index.ts | 1 - .../src/shared/providers/store-provider.tsx | 38 --- .../features/src/shared/store/alert-store.ts | 25 -- .../src/shared/store/attachment-store.ts | 25 -- .../common/features/src/shared/store/index.ts | 4 - .../features/src/shared/store/menu-store.ts | 23 -- .../features/src/shared/store/user-store.ts | 56 --- .../features/src/shared/ui/alert-dialog.tsx | 32 -- .../features/src/shared/ui/confirm-dialog.tsx | 46 --- apps/common/features/src/shared/ui/form.tsx | 175 ---------- .../features/src/shared/ui/page-layout.tsx | 22 -- .../features/src/shared/ui/skeleton-page.tsx | 12 - .../src/widgets/form-wrapper/index.ts | 1 - .../widgets/form-wrapper/ui/form-wrapper.tsx | 68 ---- apps/common/features/tailwind.config.ts | 19 -- apps/common/features/tsconfig.json | 48 --- 96 files changed, 3267 deletions(-) delete mode 100644 apps/common/features/.gitignore delete mode 100644 apps/common/features/.prettierrc delete mode 100644 apps/common/features/app/(with-base-layout)/board/page.tsx delete mode 100644 apps/common/features/app/(with-base-layout)/layout.tsx delete mode 100644 apps/common/features/app/(with-base-layout)/loading.tsx delete mode 100644 apps/common/features/app/api/proxy/[...url]/route.ts delete mode 100644 apps/common/features/app/auth/login/page.tsx delete mode 100644 apps/common/features/app/layout.tsx delete mode 100644 apps/common/features/eslint.config.mjs delete mode 100644 apps/common/features/middleware.ts delete mode 100644 apps/common/features/next.config.ts delete mode 100644 apps/common/features/package.json delete mode 100644 apps/common/features/pages/.gitkeep delete mode 100644 apps/common/features/postcss.config.mjs delete mode 100755 apps/common/features/public/images/bg_login.jpg delete mode 100644 apps/common/features/src/app/index.tsx delete mode 100644 apps/common/features/src/app/providers/app-provider.tsx delete mode 100644 apps/common/features/src/app/providers/index.ts delete mode 100644 apps/common/features/src/app/styles/base.css delete mode 100644 apps/common/features/src/app/styles/components.css delete mode 100644 apps/common/features/src/entities/attachment/api/attachment-service.ts delete mode 100644 apps/common/features/src/entities/attachment/index.ts delete mode 100644 apps/common/features/src/entities/attachment/model/attachment-interface.ts delete mode 100644 apps/common/features/src/entities/auth/api/login-service.ts delete mode 100644 apps/common/features/src/entities/auth/index.ts delete mode 100644 apps/common/features/src/entities/auth/lib/auth-wrapper.tsx delete mode 100644 apps/common/features/src/entities/auth/lib/token-manager.ts delete mode 100644 apps/common/features/src/entities/auth/model/login-interface.ts delete mode 100644 apps/common/features/src/entities/board/api/board-service.ts delete mode 100644 apps/common/features/src/entities/board/index.ts delete mode 100644 apps/common/features/src/entities/board/model/board-interface.ts delete mode 100644 apps/common/features/src/entities/code/@x/posting.ts delete mode 100644 apps/common/features/src/entities/code/api/code-service.ts delete mode 100644 apps/common/features/src/entities/code/index.ts delete mode 100644 apps/common/features/src/entities/code/model/code-interface.ts delete mode 100644 apps/common/features/src/entities/code/ui/code-select.tsx delete mode 100644 apps/common/features/src/entities/editor/api/editor-service.ts delete mode 100644 apps/common/features/src/entities/editor/index.ts delete mode 100644 apps/common/features/src/entities/editor/lib/blob-to-base64.ts delete mode 100644 apps/common/features/src/entities/editor/lib/generate-file-info.ts delete mode 100644 apps/common/features/src/entities/editor/model/editor-base-config.ts delete mode 100644 apps/common/features/src/entities/editor/model/editor-interface.ts delete mode 100644 apps/common/features/src/entities/editor/ui/editor.tsx delete mode 100644 apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts delete mode 100644 apps/common/features/src/entities/menu-authority/index.ts delete mode 100644 apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts delete mode 100644 apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx delete mode 100644 apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx delete mode 100644 apps/common/features/src/entities/menu/api/menu-service.ts delete mode 100644 apps/common/features/src/entities/menu/index.ts delete mode 100644 apps/common/features/src/entities/menu/lib/menu-wrapper.tsx delete mode 100644 apps/common/features/src/entities/menu/lib/utils.ts delete mode 100644 apps/common/features/src/entities/menu/model/menu-interface.ts delete mode 100644 apps/common/features/src/entities/posting/api/posting-service.ts delete mode 100644 apps/common/features/src/entities/posting/index.ts delete mode 100644 apps/common/features/src/entities/posting/model/posting-interface.ts delete mode 100644 apps/common/features/src/entities/posting/ui/posting-dialog.tsx delete mode 100644 apps/common/features/src/entities/site/@x/posting.ts delete mode 100644 apps/common/features/src/entities/site/api/site-service.ts delete mode 100644 apps/common/features/src/entities/site/index.ts delete mode 100644 apps/common/features/src/entities/site/model/site-interface.ts delete mode 100644 apps/common/features/src/pages/auth/login/index.ts delete mode 100644 apps/common/features/src/pages/auth/login/ui/login-page.tsx delete mode 100755 apps/common/features/src/pages/auth/login/ui/login.css delete mode 100644 apps/common/features/src/pages/board/index.ts delete mode 100644 apps/common/features/src/pages/board/ui/board-page.tsx delete mode 100644 apps/common/features/src/shared/api/client.ts delete mode 100644 apps/common/features/src/shared/api/index.ts delete mode 100644 apps/common/features/src/shared/config/api.ts delete mode 100644 apps/common/features/src/shared/config/env.ts delete mode 100644 apps/common/features/src/shared/config/index.ts delete mode 100644 apps/common/features/src/shared/lib/cookie.ts delete mode 100644 apps/common/features/src/shared/lib/index.ts delete mode 100644 apps/common/features/src/shared/lib/use-alert-store.ts delete mode 100644 apps/common/features/src/shared/lib/use-attachment-store.ts delete mode 100644 apps/common/features/src/shared/lib/use-menu-store.ts delete mode 100644 apps/common/features/src/shared/lib/use-tus.ts delete mode 100644 apps/common/features/src/shared/lib/use-user-store.ts delete mode 100644 apps/common/features/src/shared/model/api-interface.ts delete mode 100644 apps/common/features/src/shared/model/index.ts delete mode 100644 apps/common/features/src/shared/providers/index.ts delete mode 100644 apps/common/features/src/shared/providers/store-provider.tsx delete mode 100644 apps/common/features/src/shared/store/alert-store.ts delete mode 100644 apps/common/features/src/shared/store/attachment-store.ts delete mode 100644 apps/common/features/src/shared/store/index.ts delete mode 100644 apps/common/features/src/shared/store/menu-store.ts delete mode 100644 apps/common/features/src/shared/store/user-store.ts delete mode 100644 apps/common/features/src/shared/ui/alert-dialog.tsx delete mode 100644 apps/common/features/src/shared/ui/confirm-dialog.tsx delete mode 100644 apps/common/features/src/shared/ui/form.tsx delete mode 100644 apps/common/features/src/shared/ui/page-layout.tsx delete mode 100644 apps/common/features/src/shared/ui/skeleton-page.tsx delete mode 100644 apps/common/features/src/widgets/form-wrapper/index.ts delete mode 100644 apps/common/features/src/widgets/form-wrapper/ui/form-wrapper.tsx delete mode 100644 apps/common/features/tailwind.config.ts delete mode 100644 apps/common/features/tsconfig.json diff --git a/apps/common/features/.gitignore b/apps/common/features/.gitignore deleted file mode 100644 index e8d8437ee..000000000 --- a/apps/common/features/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -.env* \ No newline at end of file diff --git a/apps/common/features/.prettierrc b/apps/common/features/.prettierrc deleted file mode 100644 index b4bfed357..000000000 --- a/apps/common/features/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["prettier-plugin-tailwindcss"] -} diff --git a/apps/common/features/app/(with-base-layout)/board/page.tsx b/apps/common/features/app/(with-base-layout)/board/page.tsx deleted file mode 100644 index 9f8a68c08..000000000 --- a/apps/common/features/app/(with-base-layout)/board/page.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { BoardPage } from "@/pages/board"; -export default BoardPage; diff --git a/apps/common/features/app/(with-base-layout)/layout.tsx b/apps/common/features/app/(with-base-layout)/layout.tsx deleted file mode 100644 index c3254a935..000000000 --- a/apps/common/features/app/(with-base-layout)/layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { usePathname, useRouter } from "next/navigation"; -import { useQueryClient } from "@tanstack/react-query"; -import type { MenuItem } from "@common/components/api"; -import { BaseLayout } from "@common/components/ui"; -import { useMenuStore } from "@/shared/lib"; -import { PageLayout } from "@/shared/ui/page-layout"; - -export default function WithBaseLayout({ - children, -}: { - children: React.ReactNode; -}) { - const router = useRouter(); - const pathname = usePathname(); - const queryClient = useQueryClient(); - - const { setCurrentMenuInfo } = useMenuStore((state) => state); - - const handleMenuClick = (item: MenuItem) => { - setCurrentMenuInfo(item); - router.push(item.urlPath); - }; - - return ( - - {children} - - ); -} diff --git a/apps/common/features/app/(with-base-layout)/loading.tsx b/apps/common/features/app/(with-base-layout)/loading.tsx deleted file mode 100644 index 3a3b15932..000000000 --- a/apps/common/features/app/(with-base-layout)/loading.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { SkeletonPage } from "@/shared/ui/skeleton-page"; -export default SkeletonPage; diff --git a/apps/common/features/app/api/proxy/[...url]/route.ts b/apps/common/features/app/api/proxy/[...url]/route.ts deleted file mode 100644 index d3092c5c1..000000000 --- a/apps/common/features/app/api/proxy/[...url]/route.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - ACCESS_TOKEN, - AUTH_USER_ID, - CLAIM_NAME, - REFRESH_TOKEN, - SERVER_API_URL, - SITE_ID, -} from "@/shared/config"; -import { cookies } from "next/headers"; -import { NextResponse } from "next/server"; - -async function handler( - request: Request, - params: { url: string[] }, - method: "GET" | "POST" | "PUT" | "DELETE", -) { - const cookieStore = await cookies(); - const headers: Record = { - "Content-Type": "application/json", - "X-Site-Id": SITE_ID, - }; - let body = {}; - const isRefreshHandler = params.url.includes("refresh"); - - // silent refresh - if (isRefreshHandler) { - const refreshToken = cookieStore.get(REFRESH_TOKEN); - headers[CLAIM_NAME] = refreshToken?.value ?? ""; - - if (!headers[CLAIM_NAME]) { - return NextResponse.json({ message: "Can't refresh" }, { status: 401 }); - } - } - - try { - if (!isRefreshHandler) { - body = await request.json(); - } - - const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, { - method, - headers, - body: JSON.stringify(body), - }); - - if (!result.ok) { - return NextResponse.json( - { message: "Invalid Credentials" }, - { status: 401 }, - ); - } - - const accessToken = result.headers.get(ACCESS_TOKEN); - const refreshToken = result.headers.get(REFRESH_TOKEN); - const userId = result.headers.get(AUTH_USER_ID); - - if (accessToken && userId) { - const response = NextResponse.json({ - [ACCESS_TOKEN]: accessToken, - [AUTH_USER_ID]: userId, - }); - - if (refreshToken) { - cookieStore.set(REFRESH_TOKEN, refreshToken, { - httpOnly: true, - sameSite: "lax", - path: "/", - }); - } - - return response; - } - - return NextResponse.json( - { message: "Invalid Credentials" }, - { status: 401 }, - ); - } catch (error) { - return NextResponse.json( - { message: "Server Error", error }, - { status: 500 }, - ); - } -} - -export async function POST( - request: Request, - props: { params: Promise<{ url: string[] }> }, -) { - const params = await props.params; - return handler(request, params, "POST"); -} - -export async function PUT( - request: Request, - props: { params: Promise<{ url: string[] }> }, -) { - const params = await props.params; - return handler(request, params, "PUT"); -} diff --git a/apps/common/features/app/auth/login/page.tsx b/apps/common/features/app/auth/login/page.tsx deleted file mode 100644 index 8f9e6adc6..000000000 --- a/apps/common/features/app/auth/login/page.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { LoginPage } from "@/pages/auth/login"; -export default LoginPage; diff --git a/apps/common/features/app/layout.tsx b/apps/common/features/app/layout.tsx deleted file mode 100644 index aa2049dcc..000000000 --- a/apps/common/features/app/layout.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { Metadata } from "next"; -import { App } from "@/app"; - -export const metadata: Metadata = { - title: "유역하수도 통합 정보플랫폼", -}; - -export default App; diff --git a/apps/common/features/eslint.config.mjs b/apps/common/features/eslint.config.mjs deleted file mode 100644 index 9109635e9..000000000 --- a/apps/common/features/eslint.config.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const compat = new FlatCompat({ - baseDirectory: __dirname, -}); - -const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), - { - rules: { - "react-hooks/exhaustive-deps": "off", - }, - }, -]; - -export default eslintConfig; diff --git a/apps/common/features/middleware.ts b/apps/common/features/middleware.ts deleted file mode 100644 index 5b38dc5a8..000000000 --- a/apps/common/features/middleware.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; - -const serverApiUrl = process.env.NEXT_PUBLIC_SERVER_API_URL; - -export function middleware(req: NextRequest) { - const pathname = req.nextUrl.pathname; - const searchParams = req.nextUrl.searchParams; - - if (pathname.startsWith("/server")) { - const queryParams = searchParams.toString(); - const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ - queryParams ? `?${queryParams}` : "" - }`; - return NextResponse.rewrite(new URL(newUrl)); - } - - if (pathname.startsWith("/attach-service")) { - const requestHeaders = new Headers(req.headers); - const token = req.cookies.get(CLAIM_NAME)?.value; - const userId = req.cookies.get(AUTH_USER_ID)?.value; - const newUrl = `${serverApiUrl}${pathname}`; - - if (token) requestHeaders.set(CLAIM_NAME, token); - if (userId) requestHeaders.set(AUTH_USER_ID, userId); - - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - - return NextResponse.next(); -} - -export const config = { - matcher: ["/server/:path*", "/attach-service/:path*"], -}; diff --git a/apps/common/features/next.config.ts b/apps/common/features/next.config.ts deleted file mode 100644 index cb651cdc0..000000000 --- a/apps/common/features/next.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = {}; - -export default nextConfig; diff --git a/apps/common/features/package.json b/apps/common/features/package.json deleted file mode 100644 index bf44a814c..000000000 --- a/apps/common/features/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "feature", - "version": "0.0.0", - "private": true, - "scripts": { - "dev": "next dev -p 13000", - "build": "next build", - "start": "next start -p 13000", - "lint": "next lint" - }, - "dependencies": { - "@common/assets": "workspace:*", - "@common/components": "workspace:*", - "@frontend-opensource/use-react-hooks": "^1.1.4", - "@hookform/resolvers": "^4.1.3", - "@minoru/react-dnd-treeview": "^3.5.0", - "@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", - "@tinymce/tinymce-react": "^6.1.0", - "axios": "^1.8.1", - "chart.js": "^4.4.8", - "dayjs": "^1.11.13", - "js-cookie": "^3.0.5", - "next": "15.1.7", - "react-chartjs-2": "^5.3.0", - "react-dnd": "^16.0.1", - "react-hook-form": "^7.54.2", - "tailwindcss-animate": "^1.0.7", - "tus-js-client": "^4.3.1", - "zod": "^3.24.2", - "zustand": "^5.0.3" - }, - "devDependencies": { - "@eslint/eslintrc": "^3", - "@faker-js/faker": "^9.5.0", - "@types/js-cookie": "^3.0.6", - "@types/node": "^22.13.1", - "@types/webpack": "^5.28.5", - "eslint": "^9", - "eslint-config-next": "15.1.7", - "postcss": "^8", - "prettier": "^3.4.2", - "prettier-plugin-tailwindcss": "^0.6.11", - "steiger": "^0.5.5", - "tailwindcss": "^3.4.1", - "typescript": "^5" - } -} diff --git a/apps/common/features/pages/.gitkeep b/apps/common/features/pages/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/common/features/postcss.config.mjs b/apps/common/features/postcss.config.mjs deleted file mode 100644 index 1a69fd2a4..000000000 --- a/apps/common/features/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; diff --git a/apps/common/features/public/images/bg_login.jpg b/apps/common/features/public/images/bg_login.jpg deleted file mode 100755 index b7793d16a07f4870260d6fd16f31a9ef158c90a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135228 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 z 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 diff --git a/apps/common/features/src/app/index.tsx b/apps/common/features/src/app/index.tsx deleted file mode 100644 index 5a73f2fdd..000000000 --- a/apps/common/features/src/app/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import "./styles/base.css"; -import "./styles/components.css"; -import { AppProvider } from "./providers"; -import { cookies } from "next/headers"; - -const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => { - const theme = (await cookies()).get("site-theme-key"); - - return ( - - - {children} - - - ); -}; - -export { App }; diff --git a/apps/common/features/src/app/providers/app-provider.tsx b/apps/common/features/src/app/providers/app-provider.tsx deleted file mode 100644 index 6fd488b90..000000000 --- a/apps/common/features/src/app/providers/app-provider.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; -import { AuthWrapper } from "@/entities/auth"; -import { MenuWrapper } from "@/entities/menu"; -import { StoreProvider } from "@/shared/providers"; -import { AlertDialog } from "@/shared/ui/alert-dialog"; -import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; - -const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { - const queryClient = new QueryClient(); - return ( - - - - - {children} - - - - - - - ); -}; - -export { AppProvider }; diff --git a/apps/common/features/src/app/providers/index.ts b/apps/common/features/src/app/providers/index.ts deleted file mode 100644 index 15f99b7fd..000000000 --- a/apps/common/features/src/app/providers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AppProvider } from "./app-provider"; diff --git a/apps/common/features/src/app/styles/base.css b/apps/common/features/src/app/styles/base.css deleted file mode 100644 index e24111afe..000000000 --- a/apps/common/features/src/app/styles/base.css +++ /dev/null @@ -1,11 +0,0 @@ -@import "@common/assets/styles/base.css"; -@import "@common/assets/styles/diveicon.css"; - -@tailwind base; -@tailwind utilities; - -html, -body { - font-family: "Noto Sans KR", sans-serif; - font-size: 10px; -} diff --git a/apps/common/features/src/app/styles/components.css b/apps/common/features/src/app/styles/components.css deleted file mode 100644 index 5b9742c0f..000000000 --- a/apps/common/features/src/app/styles/components.css +++ /dev/null @@ -1,129 +0,0 @@ -.card { - position: relative; - margin: 0.6rem; - box-sizing: border-box; - min-height: 2rem; - border-radius: 0.5rem; - padding: 0.6rem; - font-size: 1.3rem; - color: #666; -} - -.card_width10 { - width: calc(10% - 12px); -} - -.card_width20 { - width: calc(20% - 12px); -} - -.card_width25 { - width: calc(25% - 12px); -} - -.card_width30 { - width: calc(30% - 12px); -} - -.card_width33 { - width: calc(33.333% - 12px); -} - -.card_width40 { - width: calc(40% - 12px); -} - -.card_width45 { - width: calc(45% - 12px); -} - -.card_width50 { - width: calc(50% - 12px); -} - -.card_width60 { - width: calc(60% - 12px); -} - -.card_width66 { - width: calc(66.666% - 12px); -} - -.card_width70 { - width: calc(70% - 12px); -} - -.card_width75 { - width: calc(75% - 12px); -} - -.card_width80 { - width: calc(80% - 12px); -} - -.card_width90 { - width: calc(90% - 12px); -} - -.card_width100 { - width: calc(100% - 12px); -} - -.card_nopadding { - padding: 0 !important; -} - -.card_nomargin { - width: 100%; - margin: 0 !important; - padding: 0 !important; -} - -.topline { - border-top: 1px solid #ccc; -} - -.bottomline { - border-bottom: 1px solid #e6eef3; - background: #fff; -} - -.card-border { - border: 1px solid #dbe2e6; - background-color: rgba(255, 255, 255, 0.6); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); -} - -.card-header { - margin-top: -0.6rem; - margin-left: -0.6rem; - margin-right: -0.6rem; - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid #e1e7eb; - padding: 1rem; -} - -.card-footer { - margin-left: -0.6rem; - margin-right: -0.6rem; - margin-top: 0.5rem; - display: flex; - align-items: center; - justify-content: center; - border-top: 1px solid #e1e7eb; - padding-top: 1rem; - padding-bottom: 1rem; -} - -/* 블랙 테마 */ -[data-theme="black"] { - .card-border { - background-color: rgba(255, 255, 255, 0.065); - border-color: #38383a; - } - .card-header { - border-bottom-color: #3f4042; - } -} diff --git a/apps/common/features/src/entities/attachment/api/attachment-service.ts b/apps/common/features/src/entities/attachment/api/attachment-service.ts deleted file mode 100644 index c930b7012..000000000 --- a/apps/common/features/src/entities/attachment/api/attachment-service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { - Attachment, - AttachmentService, -} from "../model/attachment-interface"; - -export class AttachmentApiService implements AttachmentService { - public async getAttachments(atflCd: string): Promise { - const response = await client.get( - `${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`, - ); - return response.data; - } - - public async deleteAttachment(atflId: string): Promise { - await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`); - } - - public async deleteAttachmentByAtflCd(atflCd: string): Promise { - await client.delete( - `${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/atflCd/${atflCd}`, - ); - } - - public async getImageUrl(atflId: string): Promise { - if (!atflId) return ""; - const response = await fetch(`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${atflId}`); - const blob = await response.blob(); - return URL.createObjectURL(blob); - } - - public async getImageUrlByAtflCd(atflCd: string): Promise { - if (!atflCd) return ""; - const response = await fetch( - `${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`, - ); - const blob = await response.blob(); - return URL.createObjectURL(blob); - } -} - -export const AttachmentApi = new AttachmentApiService(); diff --git a/apps/common/features/src/entities/attachment/index.ts b/apps/common/features/src/entities/attachment/index.ts deleted file mode 100644 index 182ef9fd6..000000000 --- a/apps/common/features/src/entities/attachment/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AttachmentApi } from "./api/attachment-service"; diff --git a/apps/common/features/src/entities/attachment/model/attachment-interface.ts b/apps/common/features/src/entities/attachment/model/attachment-interface.ts deleted file mode 100644 index 5986a96d1..000000000 --- a/apps/common/features/src/entities/attachment/model/attachment-interface.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface Attachment { - atflCd: string; - atflSn: number; - atflId: string; - physFileNm: string; - orgnlFileNm: string; - atflFlieSz: number; - atflType: string; - dwnldNocs: number; - delYn: string; - lnkgDmnId: string; - lnkgDmnNm: string; - regDt: string; -} - -export interface AttachmentService { - getAttachments: (atflCd: string) => Promise; - deleteAttachment: (atflId: string) => Promise; - deleteAttachmentByAtflCd: (atflCd: string) => Promise; - getImageUrl: (atflId: string) => Promise; - getImageUrlByAtflCd: (atflCd: string) => Promise; -} diff --git a/apps/common/features/src/entities/auth/api/login-service.ts b/apps/common/features/src/entities/auth/api/login-service.ts deleted file mode 100644 index c1968fe36..000000000 --- a/apps/common/features/src/entities/auth/api/login-service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { AxiosError } from "axios"; -import { - ENDPOINT, - JWT_EXPIRATION_TIME, - ACCESS_TOKEN, - AUTH_USER_ID, - CLAIM_NAME, -} from "@/shared/config"; -import type { LoginService, ILogin } from "../model/login-interface"; -import { tokenManager } from "../lib/token-manager"; - -export class LoginApiService implements LoginService { - private static instance: LoginApiService; - - private constructor() {} - - public static getInstance(): LoginApiService { - if (!LoginApiService.instance) { - LoginApiService.instance = new LoginApiService(); - } - return LoginApiService.instance; - } - - public async login(payload: ILogin): Promise { - try { - const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { - method: "POST", - body: JSON.stringify({ - userId: payload.userId, - password: payload.password, - }), - }); - - const data = await response.json(); - const accessToken = data[ACCESS_TOKEN]; - const tokenId = data[AUTH_USER_ID]; - - if (!accessToken || !tokenId) { - throw new Error("Missing required authentication tokens"); - } - - tokenManager.setToken(CLAIM_NAME, accessToken); - tokenManager.setToken(AUTH_USER_ID, tokenId); - tokenManager.setupRefreshTimer( - () => this.silentRefresh(), - JWT_EXPIRATION_TIME, - ); - } catch (error) { - if (error instanceof AxiosError) { - throw new Error(`Login failed: ${error.response?.data?.message}`); - } - throw new Error(`Login failed: ${error}`); - } - } - - public async silentRefresh(): Promise { - try { - const response = await fetch( - `/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, - { method: "PUT" }, - ); - - const data = await response.json(); - const accessToken = data[ACCESS_TOKEN]; - const tokenId = data[AUTH_USER_ID]; - - if (!accessToken || !tokenId) { - throw new Error("Missing required authentication tokens"); - } - - tokenManager.setToken(CLAIM_NAME, accessToken); - tokenManager.setToken(AUTH_USER_ID, tokenId); - } catch (error) { - tokenManager.clearAuthToken(); - if (error instanceof AxiosError) { - throw new Error( - `Silent refresh failed: ${error.response?.data?.message}`, - ); - } - throw new Error(`Silent refresh failed: ${error}`); - } - } -} - -export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/common/features/src/entities/auth/index.ts b/apps/common/features/src/entities/auth/index.ts deleted file mode 100644 index 97e142225..000000000 --- a/apps/common/features/src/entities/auth/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { LoginApi } from "./api/login-service"; -export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/common/features/src/entities/auth/lib/auth-wrapper.tsx b/apps/common/features/src/entities/auth/lib/auth-wrapper.tsx deleted file mode 100644 index 705c03231..000000000 --- a/apps/common/features/src/entities/auth/lib/auth-wrapper.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { client } from "@/shared/api"; -import type { IUserInfo } from "@/shared/store"; -import { getCookie, useUserStore } from "@/shared/lib"; -import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; -import { LoginApi } from "../api/login-service"; - -const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); - const { userInfo, setUserInfo } = useUserStore((state) => state); - - async function authCheck() { - const userId = getCookie(AUTH_USER_ID); - if (!userId) { - throw new Error("Missing required authentication tokens"); - } - - const response = await client.get( - `${ENDPOINT.USER_SERVICE.USERS}/${userId}`, - ); - if (!userInfo) setUserInfo(response.data); - } - - useEffect(() => { - (async () => { - try { - await authCheck(); - } catch (error) { - console.error(error); - - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - router.push("/auth/login"); - } - } - })(); - }, []); - - return <>{children}; -}; - -export { AuthWrapper }; diff --git a/apps/common/features/src/entities/auth/lib/token-manager.ts b/apps/common/features/src/entities/auth/lib/token-manager.ts deleted file mode 100644 index 0e761486a..000000000 --- a/apps/common/features/src/entities/auth/lib/token-manager.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config"; -import { getCookie, setCookie, removeCookie } from "@/shared/lib"; - -export class TokenManager { - private static instance: TokenManager; - private refreshTimer: NodeJS.Timeout | null = null; - - private constructor() {} - - public static getInstance(): TokenManager { - if (!TokenManager.instance) { - TokenManager.instance = new TokenManager(); - } - return TokenManager.instance; - } - - public getToken(key: string): string | undefined { - return getCookie(key); - } - - public setToken(key: string, value: string): void { - setCookie(key, value, { - secure: window.location.protocol === "https:", - sameSite: "lax", - }); - } - - public removeToken(key: string): void { - removeCookie(key); - } - - public setupRefreshTimer( - callback: () => Promise, - expirationTime: number, - ): void { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - } - - this.refreshTimer = setTimeout(() => { - callback().catch((error) => { - console.error("Silent refresh failed:", error); - this.clearAuthToken(); - }); - }, expirationTime - 60000); - } - - public clearAuthToken(): void { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - this.refreshTimer = null; - } - - this.removeToken(CLAIM_NAME); - this.removeToken(AUTH_USER_ID); - } -} - -export const tokenManager = TokenManager.getInstance(); diff --git a/apps/common/features/src/entities/auth/model/login-interface.ts b/apps/common/features/src/entities/auth/model/login-interface.ts deleted file mode 100644 index 5aaf53044..000000000 --- a/apps/common/features/src/entities/auth/model/login-interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface ILogin { - userId: string; - password: string; -} - -export interface LoginService { - login(payload: ILogin): Promise; - silentRefresh(): Promise; -} diff --git a/apps/common/features/src/entities/board/api/board-service.ts b/apps/common/features/src/entities/board/api/board-service.ts deleted file mode 100644 index 9cf29599b..000000000 --- a/apps/common/features/src/entities/board/api/board-service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { - BoardPopupList, - BoardPopupParams, - BoardService, -} from "../model/board-interface"; - -export class BoardApiService implements BoardService { - public async getBoardPopupList( - params: BoardPopupParams, - ): Promise { - const response = await client.get( - `${ENDPOINT.BOARD_SERVICE.BBS}`, - { params }, - ); - return response.data; - } -} - -export const BoardApi = new BoardApiService(); diff --git a/apps/common/features/src/entities/board/index.ts b/apps/common/features/src/entities/board/index.ts deleted file mode 100644 index 2173bb4a0..000000000 --- a/apps/common/features/src/entities/board/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BoardApi } from "./api/board-service"; -export { type BoardPopup } from "./model/board-interface"; diff --git a/apps/common/features/src/entities/board/model/board-interface.ts b/apps/common/features/src/entities/board/model/board-interface.ts deleted file mode 100644 index 942840d74..000000000 --- a/apps/common/features/src/entities/board/model/board-interface.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { PageableList, PageableParams } from "@/shared/model"; - -export const BOARD_TYPE = { - BBSNORMAL: "BBSNORMAL", - BBSFAQ: "BBSFAQ", - BBSIMAGE: "BBSIMAGE", - BBSMOVIE: "BBSMOVIE", -} as const; - -export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE]; - -export interface BoardPopupParams extends PageableParams { - bbsNm?: string; -} - -export interface BoardPopup { - bbsId: number; - bbsTypeCd: BoardType; - bbsTypeNm: string; - bbsNm: string; - bbsExpln: string; - rfrncUseYn: string; - atflUseYn: string; - atflMaxCnt: number; - atflMaxFileSz: number; - ansUseYn: string; - cmntUseYn: string; - dgstfnEvlUseYn: string; - smsUseYn: string; - upndFixUseYn: string; - pstgPrdUseYn: string; - popupUseYn: string; - delYn: string; - regDt: string; - pstCnt: number; -} - -export type BoardPopupList = PageableList; - -export interface BoardService { - getBoardPopupList: (params: BoardPopupParams) => Promise; -} diff --git a/apps/common/features/src/entities/code/@x/posting.ts b/apps/common/features/src/entities/code/@x/posting.ts deleted file mode 100644 index 0525684d9..000000000 --- a/apps/common/features/src/entities/code/@x/posting.ts +++ /dev/null @@ -1 +0,0 @@ -export { CodeSelect } from "../ui/code-select"; diff --git a/apps/common/features/src/entities/code/api/code-service.ts b/apps/common/features/src/entities/code/api/code-service.ts deleted file mode 100644 index 0d1055f6a..000000000 --- a/apps/common/features/src/entities/code/api/code-service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { CodeService, ICode } from "../model/code-interface"; - -export class CodeApiService implements CodeService { - public async getCode(upCd: string): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`, - ); - return response.data; - } -} - -export const CodeApi = new CodeApiService(); diff --git a/apps/common/features/src/entities/code/index.ts b/apps/common/features/src/entities/code/index.ts deleted file mode 100644 index 0b6633f4b..000000000 --- a/apps/common/features/src/entities/code/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CodeSelect } from "./ui/code-select"; diff --git a/apps/common/features/src/entities/code/model/code-interface.ts b/apps/common/features/src/entities/code/model/code-interface.ts deleted file mode 100644 index 0d066e6da..000000000 --- a/apps/common/features/src/entities/code/model/code-interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface ICode { - lwpoCd: string; - upCd: string; - lwpoCdNm: string; -} - -export interface CodeService { - getCode(upCd: string): Promise; -} diff --git a/apps/common/features/src/entities/code/ui/code-select.tsx b/apps/common/features/src/entities/code/ui/code-select.tsx deleted file mode 100644 index bd175a8a9..000000000 --- a/apps/common/features/src/entities/code/ui/code-select.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@common/components"; -import { ENDPOINT } from "@/shared/config"; -import { CodeApi } from "../api/code-service"; - -type CodeSelectProps = { - upCd: string; -} & React.ComponentPropsWithoutRef; - -function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) { - const { data: code } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd], - queryFn: () => CodeApi.getCode(upCd), - enabled: !!upCd, - }); - - const defaultValueText = - code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? "선택"; - - return ( - - ); -} - -export { CodeSelect }; diff --git a/apps/common/features/src/entities/editor/api/editor-service.ts b/apps/common/features/src/entities/editor/api/editor-service.ts deleted file mode 100644 index 3e2fc75a4..000000000 --- a/apps/common/features/src/entities/editor/api/editor-service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { client } from "@/shared/api"; -import type { - EditorService, - FileInfo, - UploadFile, -} from "../model/editor-interface"; -import { ENDPOINT } from "@/shared/config"; - -export class EditorApiService implements EditorService { - async uploadImage(data: UploadFile): Promise { - const response = await client.post( - ENDPOINT.ATTACH_SERVICE.EDITOR_UPLOAD, - data, - ); - return response.data; - } -} - -export const EditorApi = new EditorApiService(); diff --git a/apps/common/features/src/entities/editor/index.ts b/apps/common/features/src/entities/editor/index.ts deleted file mode 100644 index 1ae82fb4f..000000000 --- a/apps/common/features/src/entities/editor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Editor } from "./ui/editor"; diff --git a/apps/common/features/src/entities/editor/lib/blob-to-base64.ts b/apps/common/features/src/entities/editor/lib/blob-to-base64.ts deleted file mode 100644 index 2f0e68c0f..000000000 --- a/apps/common/features/src/entities/editor/lib/blob-to-base64.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const blobToBase64 = (blob: Blob): Promise => { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = () => { - const base64 = reader.result as string; - resolve(base64.split(",")[1] || ""); - }; - reader.readAsDataURL(blob); - }); -}; diff --git a/apps/common/features/src/entities/editor/lib/generate-file-info.ts b/apps/common/features/src/entities/editor/lib/generate-file-info.ts deleted file mode 100644 index c36ed3577..000000000 --- a/apps/common/features/src/entities/editor/lib/generate-file-info.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { blobToBase64 } from "./blob-to-base64"; - -export const generateFileInfo = async (blobInfo: { - blob: () => Blob; - filename: () => string; -}) => { - const blob = blobInfo.blob(); - const base64 = await blobToBase64(blob); - return { - fieldName: `${blob.type.split("/")[1]}`, - fileType: blob.type, - fileBase64: base64, - originalName: blobInfo.filename(), - size: blob.size, - }; -}; diff --git a/apps/common/features/src/entities/editor/model/editor-base-config.ts b/apps/common/features/src/entities/editor/model/editor-base-config.ts deleted file mode 100644 index 1b97ba9a0..000000000 --- a/apps/common/features/src/entities/editor/model/editor-base-config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { SERVER_API_URL } from "@/shared/config"; - -export const editorBaseConfig = { - apiKey: process.env.NEXT_PUBLIC_TINY_API_KEY, - init: { - language: "ko_KR", - height: 500, - plugins: [ - "advlist", - "autolink", - "lists", - "link", - "image", - "charmap", - "preview", - "anchor", - "searchreplace", - "visualblocks", - "code", - "codesample", - "fullscreen", - "insertdatetime", - "media", - "table", - "code", - "help", - "wordcount", - "save", - ], - toolbar: [ - "undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table", - "link image", - ], - images_upload_base_path: SERVER_API_URL, - file_picker_types: "image", - }, -}; diff --git a/apps/common/features/src/entities/editor/model/editor-interface.ts b/apps/common/features/src/entities/editor/model/editor-interface.ts deleted file mode 100644 index ed6b4a987..000000000 --- a/apps/common/features/src/entities/editor/model/editor-interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface UploadFile { - fieldName: string; - fileType: string; - fileBase64: string; - originalName: string; - size: number; -} - -export interface FileInfo { - originalFileName: string; - message: string; - fileType: string; - size: number; - uploaded: number; - url: string; -} - -export interface EditorService { - uploadImage(data: UploadFile): Promise; -} diff --git a/apps/common/features/src/entities/editor/ui/editor.tsx b/apps/common/features/src/entities/editor/ui/editor.tsx deleted file mode 100644 index b5ef02153..000000000 --- a/apps/common/features/src/entities/editor/ui/editor.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useRef } from "react"; -import { - Editor as TinyEditor, - type Editor as TinyEditorInstance, - type IAllProps, -} from "@tinymce/tinymce-react"; -import { editorBaseConfig as baseConfig } from "../model/editor-base-config"; -import { generateFileInfo } from "../lib/generate-file-info"; -import { EditorApi } from "../api/editor-service"; - -const Editor = ({ ...props }: IAllProps) => { - const editorRef = useRef(null); - - const imagesUploadHandler = (blobInfo: { - blob: () => Blob; - filename: () => string; - }) => { - return new Promise(async (resolve) => { - const fileInfo = await generateFileInfo(blobInfo); - const data = await EditorApi.uploadImage(fileInfo); - resolve(data.url); - }); - }; - - const filePickerCallback = ( - callback: (base64: string, meta: { title: string }) => void, - ) => { - const input = document.createElement("input"); - input.setAttribute("type", "file"); - input.setAttribute("accept", "image/*"); - input.onchange = async () => { - const file = input.files?.[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = async () => { - const base64 = reader.result as string; - callback(base64, { title: file.name }); - }; - reader.readAsDataURL(file); - }; - input.click(); - }; - - return ( - { - editorRef.current = editor; - }} - init={{ - ...baseConfig.init, - images_upload_handler: imagesUploadHandler, - file_picker_callback: filePickerCallback, - }} - {...props} - /> - ); -}; - -export { Editor }; diff --git a/apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts b/apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts deleted file mode 100644 index f9b01b4d8..000000000 --- a/apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { - MenuAuthorityList, - MenuAuthorityParams, - MenuAuthorityPayloadData, - MenuAuthorityService, -} from "../model/menu-authority-interface"; - -export class MenuAuthorityApiService implements MenuAuthorityService { - public async getMenuAuthorityList( - params: MenuAuthorityParams, - ): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, - { params }, - ); - return response.data; - } - - public async saveMenuAuthority( - data: MenuAuthorityPayloadData[], - ): Promise { - const response = await client.post( - `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, - data, - ); - return response.data; - } -} - -export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/common/features/src/entities/menu-authority/index.ts b/apps/common/features/src/entities/menu-authority/index.ts deleted file mode 100644 index 07433a098..000000000 --- a/apps/common/features/src/entities/menu-authority/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts deleted file mode 100644 index d6775838c..000000000 --- a/apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from "zod"; -import type { PageableList, PageableParams } from "@/shared/model"; - -export interface MenuAuthorityParams extends PageableParams { - groupId?: string; - menuCd?: number; - useYn?: string; -} - -export const menuAuthoritySchema = z.object({ - groupId: z.string(), - groupNm: z.string(), - menuCd: z.number(), - mngAuthrtYn: z.string(), - inqAuthrtYn: z.string(), - regAuthrtYn: z.string(), - mdfcnAuthrtYn: z.string(), - delAuthrtYn: z.string(), - useYn: z.string(), - mode: z.string(), -}); - -export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({ - groupNm: true, -}); - -export const menuAuthorityFormSchema = z.object({ - menuAuthority: z.array(menuAuthorityPayloadSchema), -}); - -export type MenuAuthority = z.infer; -export type MenuAuthorityList = PageableList; -export type MenuAuthorityPayloadData = z.infer< - typeof menuAuthorityPayloadSchema ->; -export type MenuAuthorityFormData = z.infer; - -export interface MenuAuthorityService { - getMenuAuthorityList(params: MenuAuthorityParams): Promise; - saveMenuAuthority(data: MenuAuthorityPayloadData[]): Promise; -} diff --git a/apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx deleted file mode 100644 index 1a9863e15..000000000 --- a/apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import type { ColumnDef } from "@tanstack/react-table"; -import type { UseFormReturn } from "react-hook-form"; -import { Checkbox } from "@common/components/ui"; -import type { - MenuAuthority, - MenuAuthorityFormData, - MenuAuthorityPayloadData, -} from "./menu-authority-interface"; - -export const createColumns = ( - form: UseFormReturn, -): ColumnDef[] => { - const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; - - const handleCheckedChange = - (i: number, key: keyof MenuAuthorityPayloadData) => (checked: boolean) => { - if (getCurrentRow(i)?.mode === "R") { - form.setValue(`menuAuthority.${i}.mode`, "U"); - } - form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N"); - }; - - return [ - { - accessorKey: "groupNm", - header: "그룹명", - }, - { - accessorKey: "mngAuthrtYn", - header: "관리권한", - cell: ({ row }) => { - return ( - { - if (checked) { - handleCheckedChange( - row.index, - "regAuthrtYn", - )(checked as boolean); - handleCheckedChange( - row.index, - "mdfcnAuthrtYn", - )(checked as boolean); - handleCheckedChange( - row.index, - "delAuthrtYn", - )(checked as boolean); - handleCheckedChange( - row.index, - "inqAuthrtYn", - )(checked as boolean); - } - handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean); - }} - /> - ); - }, - }, - { - accessorKey: "regAuthrtYn", - header: "등록권한", - cell: ({ row }) => { - return ( - { - if (checked) { - handleCheckedChange( - row.index, - "inqAuthrtYn", - )(checked as boolean); - } - handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean); - }} - /> - ); - }, - }, - { - accessorKey: "mdfcnAuthrtYn", - header: "수정권한 ", - cell: ({ row }) => { - return ( - - ); - }, - }, - { - accessorKey: "delAuthrtYn", - header: "삭제권한", - cell: ({ row }) => { - return ( - - ); - }, - }, - { - accessorKey: "inqAuthrtYn", - header: "조회권한", - cell: ({ row }) => { - return ( - - ); - }, - }, - ]; -}; diff --git a/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx deleted file mode 100644 index 62915e2ed..000000000 --- a/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; -import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { - Button, - DataTable, - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@common/components/ui"; -import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; -import { - menuAuthorityFormSchema, - type MenuAuthorityFormData, - type MenuAuthorityPayloadData, -} from "../model/menu-authority-interface"; -import { createColumns } from "../model/menu-authority-table-columns"; -import { MenuAuthorityApi } from "../api/menu-authority-service"; - -type ManagementAuthorityDialogProps = { - menuCd?: number; -} & React.ComponentProps; - -function ManagementAuthorityDialog({ - menuCd, - ...props -}: ManagementAuthorityDialogProps) { - const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); - - const { data: menuAuthorityList } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd], - queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }), - enabled: !!menuCd, - }); - - const { mutateAsync: saveMenuAuthority } = useMutation({ - mutationFn: (data: MenuAuthorityPayloadData[]) => - MenuAuthorityApi.saveMenuAuthority(data), - onSuccess: () => { - alert("저장되었습니다."); - }, - }); - - const form = useForm({ - resolver: zodResolver(menuAuthorityFormSchema), - values: { - menuAuthority: menuAuthorityList?.content ?? [], - }, - }); - - const table = useReactTable({ - data: menuAuthorityList?.content ?? [], - columns: createColumns(form), - getCoreRowModel: getCoreRowModel(), - }); - - const handleSave = async () => { - if (await confirm("저장하시겠습니까?")) { - const data = form.getValues("menuAuthority"); - const filteredData = data.filter((item) => item.mode !== "R"); - await saveMenuAuthority(filteredData); - } - }; - - return ( - - - - - - - 그룹별 메뉴 권한 - - - - - - - - - - - ); -} - -export { ManagementAuthorityDialog }; diff --git a/apps/common/features/src/entities/menu/api/menu-service.ts b/apps/common/features/src/entities/menu/api/menu-service.ts deleted file mode 100644 index b6c1510cd..000000000 --- a/apps/common/features/src/entities/menu/api/menu-service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { client } from "@common/components/api"; -import { ENDPOINT } from "@/shared/config"; -import type { MenuItem, MenuService } from "../model/menu-interface"; - -export class MenuApiService implements MenuService { - async getMenuRoles(siteId: string): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, - ); - return response.data; - } -} - -export const MenuApi = new MenuApiService(); diff --git a/apps/common/features/src/entities/menu/index.ts b/apps/common/features/src/entities/menu/index.ts deleted file mode 100644 index 1fa9dd218..000000000 --- a/apps/common/features/src/entities/menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MenuWrapper } from "./lib/menu-wrapper"; diff --git a/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx b/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx deleted file mode 100644 index d473548af..000000000 --- a/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { usePathname } from "next/navigation"; -import { SITE_ID } from "@/shared/config"; -import { useMenuStore } from "@/shared/lib"; -import { MenuApi } from "../api/menu-service"; -import { getCurrentMenu } from "./utils"; - -const MenuWrapper = ({ children }: { children: React.ReactNode }) => { - const pathname = usePathname(); - const { setCurrentMenuInfo } = useMenuStore((state) => state); - - useEffect(() => { - (async () => { - const menuRoles = await MenuApi.getMenuRoles(SITE_ID); - if (!menuRoles) return; - - const currentMenu = getCurrentMenu(menuRoles, pathname ?? ""); - if (!currentMenu) return; - - setCurrentMenuInfo(currentMenu); - })(); - }, []); - - return <>{children}; -}; - -export { MenuWrapper }; diff --git a/apps/common/features/src/entities/menu/lib/utils.ts b/apps/common/features/src/entities/menu/lib/utils.ts deleted file mode 100644 index 1cf4514d9..000000000 --- a/apps/common/features/src/entities/menu/lib/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { MenuItem } from "../model/menu-interface"; - -export const getCurrentMenu = ( - menus: MenuItem[], - pathname: string, -): MenuItem | null => { - for (const menu of menus) { - if ( - menu.urlPath !== "" && - (menu.urlPath === pathname || pathname.startsWith(menu.urlPath)) - ) - return menu; - - if (menu.children) { - const children = getCurrentMenu(menu.children, pathname); - if (children) return children; - } - } - return null; -}; diff --git a/apps/common/features/src/entities/menu/model/menu-interface.ts b/apps/common/features/src/entities/menu/model/menu-interface.ts deleted file mode 100644 index 12c7d007a..000000000 --- a/apps/common/features/src/entities/menu/model/menu-interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface MenuItem { - menuCd: number; - menuKornNm: string; - menuEngNm: string; - parentId: number | null; - sortSeq: number; - iconNm?: string; - children: MenuItem[] | null; - levelNo: number; - urlPath: string; - menuLnkgTypeCd: string; - menuLnkgSn: number; - menuPath: string; - qrcd: string; - useYn: string; - otptYn: string; - newWndwYn: string; - requiredUrlPath: boolean; -} - -export interface MenuService { - getMenuRoles(siteId: string): Promise; -} diff --git a/apps/common/features/src/entities/posting/api/posting-service.ts b/apps/common/features/src/entities/posting/api/posting-service.ts deleted file mode 100644 index 2fafd6557..000000000 --- a/apps/common/features/src/entities/posting/api/posting-service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { PageableList } from "@/shared/model"; -import type { - PostService, - PostingParams, - Post, - PostPayloadData, -} from "../model/posting-interface"; - -export class PostingApiService implements PostService { - public async getPostingHistory( - params: PostingParams, - ): Promise> { - const response = await client.get>( - `${ENDPOINT.CMS_SERVICE.POSTINGS}`, - { params }, - ); - return response.data; - } - - public async savePosting(data: PostPayloadData[]): Promise { - const response = await client.post( - `${ENDPOINT.CMS_SERVICE.POSTINGS}`, - data, - ); - return response.data; - } -} - -export const PostingApi = new PostingApiService(); diff --git a/apps/common/features/src/entities/posting/index.ts b/apps/common/features/src/entities/posting/index.ts deleted file mode 100644 index c6029d757..000000000 --- a/apps/common/features/src/entities/posting/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { PostingApi } from "./api/posting-service"; -export { - POSTING_TYPE, - type Post, - type PostPayloadData, -} from "./model/posting-interface"; -export { PostingDialog } from "./ui/posting-dialog"; diff --git a/apps/common/features/src/entities/posting/model/posting-interface.ts b/apps/common/features/src/entities/posting/model/posting-interface.ts deleted file mode 100644 index b14b2ab66..000000000 --- a/apps/common/features/src/entities/posting/model/posting-interface.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { PageableList, PageableParams } from "@/shared/model"; -import { z } from "zod"; - -export const POSTING_TYPE = { - POPUP: "POPUP", - BANNER: "BANNER", - IMAGE: "IMAGE", -} as const; - -export type PostingType = (typeof POSTING_TYPE)[keyof typeof POSTING_TYPE]; - -export interface PostingParams extends PageableParams { - pstgTrgtNo: number; -} - -export const postSchema = z.object({ - siteId: z.string(), - siteNm: z.string(), - pstgTypeCd: z.string(), - pstgTypeNm: z.string(), - pstgTrgtNo: z.number(), - pstgBgngDt: z.string().optional(), - pstgEndDt: z.string().optional(), - pstgPrdSeCd: z.string(), - pstgPrdSeNm: z.string(), - delYn: z.string(), -}); - -export const postPayloadSchema = postSchema - .extend({ - mode: z.string(), - }) - .omit({ - siteNm: true, - pstgTypeNm: true, - pstgPrdSeNm: true, - }); - -export const postingFormSchema = z.object({ - pstg: z.array(postPayloadSchema), -}); - -export type Post = z.infer; -export type PostPayloadData = z.infer; -export type PostingFormData = z.infer; - -export interface PostService { - getPostingHistory: (params: PostingParams) => Promise>; - savePosting: (data: PostPayloadData[]) => Promise; -} diff --git a/apps/common/features/src/entities/posting/ui/posting-dialog.tsx b/apps/common/features/src/entities/posting/ui/posting-dialog.tsx deleted file mode 100644 index 9e27a4485..000000000 --- a/apps/common/features/src/entities/posting/ui/posting-dialog.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import dayjs from "dayjs"; -import { useState } from "react"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useFieldArray, useForm } from "react-hook-form"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { - Dialog, - DialogTrigger, - Button, - DialogContent, - DialogHeader, - DialogTitle, - Badge, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, - Checkbox, - Select, - DateRangePicker, - DialogClose, - DialogFooter, -} from "@common/components/ui"; -import { cn } from "@common/components/lib"; -import { CodeSelect } from "@/entities/code/@x/posting"; -import { SiteApi, type Site } from "@/entities/site/@x/posting"; -import type { PageableList } from "@/shared/model"; -import { ENDPOINT } from "@/shared/config"; -import { PostingApi } from "../api/posting-service"; -import { - postingFormSchema, - type PostingFormData, - type PostingType, - type PostPayloadData, -} from "../model/posting-interface"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; - -type DateRange = { - from: string; - to?: string; -}; - -type PostingTarget = { - pstgTrgtNm: string; - pstgTrgtNo: number; -}; - -type PostingDialogProps = { - postingType: PostingType; - postingTargets: PostingTarget[]; -}; - -const POSTING_TYPE_LABEL = { - POPUP: "팝업", - BANNER: "배너", - IMAGE: "이미지", -} as const; - -function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { - const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); - const [selectedRows, setSelectedRows] = useState([]); - const [rememberPeriodCode, setRememberPeriodCode] = useState< - Record - >({}); - const [rememberPeriodDate, setRememberPeriodDate] = useState< - Record - >({}); - - const { data: siteList } = useQuery>({ - queryKey: [ENDPOINT.CMS_SERVICE.SITES], - queryFn: () => SiteApi.getSiteList({}), - }); - - const { mutateAsync: savePosting } = useMutation({ - mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data), - onSuccess: () => { - alert("저장되었습니다."); - }, - }); - - const form = useForm({ - resolver: zodResolver(postingFormSchema), - }); - const { fields, append, replace } = useFieldArray({ - control: form.control, - name: "pstg", - }); - - const isFormValid = - fields.length && fields.every((field) => field.pstgPrdSeCd !== ""); - - const isAllSelected = - siteList && selectedRows.length === siteList.content.length; - - const postingPeriod = (siteId: string) => { - const periodCode = Number(rememberPeriodCode[siteId]); - - if (periodCode === 99) return "상시"; - - return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs() - .add(periodCode, "day") - .format("YYYY-MM-DD")}`; - }; - - const handleOpenChange = (open: boolean) => { - if (open) return true; - replace([]); - setSelectedRows([]); - setRememberPeriodCode({}); - setRememberPeriodDate({}); - }; - - const handleAppendFields = (i: number) => { - const siteId = siteList?.content[i]?.siteId ?? ""; - const newFields = postingTargets.map((target) => ({ - siteId, - pstgTypeCd: postingType, - pstgTrgtNo: target.pstgTrgtNo, - pstgBgngDt: rememberPeriodDate[siteId]?.from ?? "", - pstgEndDt: rememberPeriodDate[siteId]?.to ?? "", - pstgPrdSeCd: rememberPeriodCode[siteId] ?? "", - delYn: "N", - mode: "C", - })); - append(newFields); - }; - - const handleUpdateFields = - (siteId: string) => (obj: Record) => { - const updatedFields = fields.map((field) => - "siteId" in field && field.siteId === siteId - ? { ...field, ...obj } - : field, - ); - replace(updatedFields); - }; - - const handleRemoveFields = (siteId: string) => { - const filteredFields = fields.filter( - (field) => "siteId" in field && field.siteId !== siteId, - ); - replace(filteredFields); - }; - - const handleSelectRow = (i: number) => { - const isSelected = selectedRows.includes(i); - setSelectedRows((prev) => - isSelected ? prev.filter((j) => j !== i) : [...prev, i], - ); - - if (isSelected) { - const siteId = siteList?.content[i]?.siteId; - if (!siteId) return; - handleRemoveFields(siteId); - return; - } - - handleAppendFields(i); - }; - - const handleSelectAll = () => { - if (selectedRows.length === (siteList?.content.length ?? 0)) { - replace([]); - setSelectedRows([]); - } else { - replace([]); - setSelectedRows(siteList?.content.map((_, i) => i) ?? []); - siteList?.content.forEach((_, i) => handleAppendFields(i)); - } - }; - - const handlePeriodCodeChange = (siteId: string) => (value: string) => { - const from = dayjs().format("YYYY-MM-DD 00:00:00"); - const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59"); - - setRememberPeriodCode((prev) => ({ - ...prev, - [siteId]: value, - })); - setRememberPeriodDate((prev) => ({ - ...prev, - [siteId]: { from, to }, - })); - handleUpdateFields(siteId)({ - pstgPrdSeCd: value, - pstgBgngDt: from, - pstgEndDt: to, - }); - }; - - const handlePeriodDateChange = - (siteId: string) => (value: DateRange | undefined) => { - const from = dayjs(value?.from).format("YYYY-MM-DD 00:00:00"); - const to = dayjs(value?.to).format("YYYY-MM-DD 23:59:59"); - setRememberPeriodDate((prev) => ({ - ...prev, - [siteId]: { from, to }, - })); - handleUpdateFields(siteId)({ - pstgBgngDt: from, - pstgEndDt: to, - }); - }; - - const handleSave = async () => { - if (await confirm("저장하시겠습니까?")) { - const data = form.getValues("pstg"); - await savePosting(data); - } - }; - - return ( - - - - - - - 사이트 반영 - -
-

- 선택된 {POSTING_TYPE_LABEL[postingType]} -

-
- {postingTargets.map((target) => ( - - {target.pstgTrgtNm} - - ))} -
-
-
- - - - - - - - - - - - 사이트명 - 게시 구분 - - - - {siteList?.content.map((site, i) => ( - - - handleSelectRow(i)} - /> - - {site.siteNm} - -
- - {!!Number(rememberPeriodCode[site.siteId]) && ( - - 게시 기간: {postingPeriod(site.siteId)} - - )} - {Number(rememberPeriodCode[site.siteId]) === 0 && ( - - handlePeriodDateChange(site.siteId)( - date as DateRange | undefined, - ) - } - disabled={!selectedRows.includes(i)} - /> - )} -
-
-
- ))} -
-
-
- - - - - - -
-
- ); -} - -function PostingPeriodSelect({ - ...props -}: React.ComponentPropsWithoutRef) { - return ; -} - -export { PostingDialog }; diff --git a/apps/common/features/src/entities/site/@x/posting.ts b/apps/common/features/src/entities/site/@x/posting.ts deleted file mode 100644 index 6470b3cb2..000000000 --- a/apps/common/features/src/entities/site/@x/posting.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SiteApi } from "../api/site-service"; -export type { Site } from "../model/site-interface"; diff --git a/apps/common/features/src/entities/site/api/site-service.ts b/apps/common/features/src/entities/site/api/site-service.ts deleted file mode 100644 index a5d9d7cd6..000000000 --- a/apps/common/features/src/entities/site/api/site-service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import { - siteFormSchema, - type SiteService, - type SiteList, - type SiteDetail, - type SiteParams, - type SiteFormData, -} from "../model/site-interface"; - -export class SiteApiService implements SiteService { - public async getSiteList(params?: SiteParams): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.SITES}`, - { params }, - ); - return response.data; - } - - public async siteDetail(siteId: string): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, - ); - return response.data; - } - - public async createSite(data: SiteFormData): Promise { - const validatedData = siteFormSchema.parse(data); - const response = await client.post( - `${ENDPOINT.CMS_SERVICE.SITES}`, - validatedData, - ); - return response.data; - } - - public async updateSite(siteId: string, data: SiteFormData): Promise { - const validatedData = siteFormSchema.parse(data); - const response = await client.put( - `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, - validatedData, - ); - return response.data; - } - - public async deleteSite(siteId: string): Promise { - await client.delete(`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`); - } -} - -export const SiteApi = new SiteApiService(); diff --git a/apps/common/features/src/entities/site/index.ts b/apps/common/features/src/entities/site/index.ts deleted file mode 100644 index c7b437a64..000000000 --- a/apps/common/features/src/entities/site/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { SiteApi } from "./api/site-service"; -export { - siteFormSchema, - type SiteParams, - type Site, - type SiteFormData, -} from "./model/site-interface"; diff --git a/apps/common/features/src/entities/site/model/site-interface.ts b/apps/common/features/src/entities/site/model/site-interface.ts deleted file mode 100644 index 8846f59b9..000000000 --- a/apps/common/features/src/entities/site/model/site-interface.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { z } from "zod"; -import type { PageableParams, PageableList } from "@/shared/model"; - -export interface SiteParams extends PageableParams { - siteNm?: string; - siteKndCd?: string; - useYn?: string; -} - -export interface Site { - siteId: string; - siteNm: string; - siteKindCode: string; - bscSiteYn: string; - siteAddr: string; - useYn: string; - regDt: string; - rgTrNm: string; - dmnAddr: string; -} - -export interface SiteDetail extends SiteFormData { - siteId: string; - siteKindNm: string; - basicSiteYn: string; - faxNumber: string; - regDt: string; - rgTr: string; - mdfcnDt: string; - mdFr: string; - rgTrNm: string; - mdfrNm: string; -} - -export const siteFormSchema = z.object({ - siteId: z.string().max(2).nonempty(), - siteNm: z.string().max(100).nonempty(), - siteExpln: z.string().max(1000).nullish(), - siteKndCd: z.string().max(10).nonempty(), - siteAddr: z.string().max(2000).nullish(), - faxNo: z.string().max(100).nullish(), - telNo: z.string().max(1000).nullish(), - lwndCn: z.string().max(2000).nullish(), - bscSiteYn: z.string(), - bkmkIcon: z.string().max(100).nullish(), - useYn: z.string(), - siteSkn: z.string().max(10).nullish(), - dmnAddr: z.string().nullish(), -}); - -export type SiteFormData = z.infer; -export type SiteList = PageableList; - -export interface SiteService { - getSiteList(params: SiteParams): Promise; - siteDetail(siteId: string): Promise; - updateSite(siteId: string, data: SiteFormData): Promise; - createSite(data: SiteFormData): Promise; -} diff --git a/apps/common/features/src/pages/auth/login/index.ts b/apps/common/features/src/pages/auth/login/index.ts deleted file mode 100644 index 2093c3d8d..000000000 --- a/apps/common/features/src/pages/auth/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginPage } from "./ui/login-page"; diff --git a/apps/common/features/src/pages/auth/login/ui/login-page.tsx b/apps/common/features/src/pages/auth/login/ui/login-page.tsx deleted file mode 100644 index 4700a691b..000000000 --- a/apps/common/features/src/pages/auth/login/ui/login-page.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import Image from "next/image"; -import { images } from "@common/assets"; -import { LoginApi } from "@/entities/auth"; -import "./login.css"; - -function LoginPage() { - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - const formData = new FormData(e.target as HTMLFormElement); - const userId = formData.get("id") as string; - const password = formData.get("password") as string; - - await LoginApi.login({ userId, password }); - window.location.href = "/dashboard"; - }; - - return ( -
-
-
-

- 하수도정보시스템 -
- CMS 통합관리시스템 -

-

하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.

-
-
- logo -
-
-
-
-
    -
  • -

    ID

    -
    - - -
    -
  • -
  • -

    PASSWORD

    -
    - - -
    -
  • -
-
-
- - -
- {/* 비밀번호 또는 아이디를 잃어버리셨나요? */} -
- -
-
-

copyright © 2023 KECO. All rights reserved.

-
- -

- - 주소 : (22689) 인천광역시 서구 환경로 42(경서동 - 종합환경연구단지) - - - 전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723, - 3722 - - 팩스 :032-590-3729 -

-
-
- -
- ); -} - -export { LoginPage }; diff --git a/apps/common/features/src/pages/auth/login/ui/login.css b/apps/common/features/src/pages/auth/login/ui/login.css deleted file mode 100755 index 831cc13a9..000000000 --- a/apps/common/features/src/pages/auth/login/ui/login.css +++ /dev/null @@ -1,311 +0,0 @@ -/** -* OLYM Cascading Style Sheets -* File Name : login.css -* Description : 로그인 스타일시트 -* Author : Web business Team / Choi Hyun Mi -* Email : desingtj@olym.co.kr -* Date : 2023.07.20 -* Update : 2023.07.26 -* Copyright(c) 2023 OLYM Communications. All Rights Reserved. -*/ -html, -body { - background: #fff; -} -.fx { - display: flex; - -ms-display: flex; - -webkit-display: flex; - box-sizing: border-box; -} -.login { - width: 100%; - height: 100vh; -} -.login .left_box { - width: 100%; - background: url(../../../../../public/images/bg_login.jpg) no-repeat - center/cover; - flex-direction: column; - justify-content: space-between; - padding: 7% 5% 5% 5%; -} -.login .left_box .text h1 { - font-size: 53px; - color: #fff; -} -.login .left_box .text h1 span { - font-weight: 800; -} -.login .left_box .text p { - margin-top: 40px; - font-size: 18px; - color: #fff; -} -.login .login_box { - width: 100%; - flex-direction: column; - justify-content: center; - padding: 4% 12% 4% 6%; -} -.login .login_box > ul { - flex-direction: column; - gap: 40px; -} -.login .login_box > ul > li h2 { - font-size: 22px; - font-weight: 600; - color: #2640a5; - margin-bottom: 14px; -} -.login .login_box > ul > li .input_st { - position: relative; -} -.login .login_box > ul > li .input_st .diveicon { - position: absolute; - top: 50%; - left: 28px; - transform: translateY(-50%); - font-family: "Xeicon"; - font-size: 25px; - color: #003288; -} -.login .login_box > ul > li input { - width: 100%; - padding: 20px 20px 20px 70px; - background: #f7f7f7; - box-sizing: border-box; - border: none; - border-radius: 5px; - font-size: 18px; - font-weight: 700; - color: #000; - height: auto; -} -.login .login_box > ul > li input:focus { - outline: 1px solid #3c5fe5; -} -.login .login_box > ul > li input::placeholder { - color: #afabab; - font-weight: 500; -} -.login .login_box .bottom_box { - justify-content: space-between; - gap: 20px; - padding: 50px 0 45px 0; -} -.login .login_box .bottom_box .checks { - display: flex; - align-items: center; - gap: 8px; -} -.login .login_box .bottom_box .checks input[type="checkbox"] { - width: 18px; - height: 18px; -} -.login .login_box .bottom_box .checks label { - font-size: 14px; -} -.login .login_box .bottom_box .pw_message { - align-items: center; - gap: 5px; - color: #636363; - font-weight: 500; - border-bottom: 1px solid #ccc; - padding-bottom: 5px; -} -.login .login_box .bottom_box .pw_message i { - color: #327af5; - font-size: 20px; -} -.login .login_box .login_btn { - position: relative; - border-radius: 5px; - padding: 24px 15px; - background: linear-gradient( - to left, - #1d3969 0%, - #264078 10%, - #172558 40%, - #162254 50%, - #111c43 60%, - #111c42 100% - ); - overflow: hidden; - justify-content: center; - align-items: center; - gap: 10px; - color: #fff; - cursor: pointer; -} -.login .login_box .login_btn span { - font-family: "Montserrat"; - font-size: 19px; - font-weight: 700; - position: relative; -} -.login .login_box .login_btn i { - font-size: 22px; - transform: translateY(-2px); -} -.login .login_box .login_btn:before { - display: block; - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0; - background: linear-gradient( - to right, - #1d3969 0%, - #264078 10%, - #172558 40%, - #162254 50%, - #111c43 60%, - #111c42 100% - ); - transition: opacity 0.5s; -} -.login .login_box .login_btn:hover::before { - opacity: 1; -} -.login .footer .copy { - margin-top: 36px; - justify-content: space-between; - align-items: center; - gap: 3%; -} -.login .footer .copy p { - font-size: 13px; - color: #d4d4d4; - width: 44%; - text-align: center; -} -.login .footer .copy::before, -.login .login_box .copy::after { - content: ""; - width: 20%; - height: 1px; - background: #d4d4d4; -} -.login .footer .fot_btn { - gap: 20px; - margin: 30px 0 10px 0; -} -.login .footer .fot_btn a { - color: #333; - font-weight: 700; - font-size: 13px; -} -.login .footer .fot_btn a:hover { - color: #003288; -} -.login .footer .addr { - color: #666; - font-size: 13px; - gap: 3px 20px; - flex-wrap: wrap; -} -/* 비밀번호 변경 팝업창 */ -.login .modal .modal_container { - width: 750px; -} -.login .pw_desc { - padding: 10px 10px 30px 10px; - color: #666; -} -.login .pw_desc p { - font-size: 1.25rem; - color: #000; - font-weight: 700; - margin-bottom: 10px; -} -.login .pw_desc span { - color: #f60; - font-weight: 700; -} -.login .modal .form_write { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - padding: 20px 50px; -} -@media (max-width: 1024px) { - .login { - flex-direction: column; - height: auto; - } - .login .left_box { - padding: 30px; - flex-direction: column-reverse; - gap: 20px; - } - .login .left_box .text h1 { - font-size: 30px; - line-height: 1.2em; - } - .login .left_box .text p { - margin-top: 20px; - font-size: 14px; - } - .login .left_box .img img { - height: 40px; - } - .login .login_box { - padding: 30px; - } - .login .login_box > ul { - gap: 20px; - } - .login .login_box > ul > li h2 { - margin-bottom: 7px; - } - .login .login_box > ul > li .input_st::after { - left: 15px; - font-size: 20px; - } - .login .login_box > ul > li input { - padding: 15px 10px 15px 50px; - font-size: 16px; - } - .login .login_box .bottom_box { - flex-direction: column; - align-items: flex-start; - padding: 30px 0; - } - .login .login_box .bottom_box .id_save label { - font-size: 14px; - } - .login .login_box .login_btn { - padding: 15px; - } - .login .login_box .login_btn span { - font-size: 16px; - } - .login .login_box .login_btn i { - font-size: 18px; - transform: translateY(-1px); - } - .login .login_box .copy p { - width: 100%; - } - .login .login_box .copy::before, - .login .login_box .copy::after { - display: none; - } - .login .footer .addr { - font-size: 11px; - gap: 3px 10px; - } - .login .modal .modal_container { - width: 90%; - } - .login .pw_desc { - padding: 0 0 20px 0; - } - .login .modal .form_write { - padding: 10px; - } -} -/* // login */ diff --git a/apps/common/features/src/pages/board/index.ts b/apps/common/features/src/pages/board/index.ts deleted file mode 100644 index 483a2f96b..000000000 --- a/apps/common/features/src/pages/board/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BoardPage } from "./ui/board-page"; diff --git a/apps/common/features/src/pages/board/ui/board-page.tsx b/apps/common/features/src/pages/board/ui/board-page.tsx deleted file mode 100644 index 15960674e..000000000 --- a/apps/common/features/src/pages/board/ui/board-page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -function BoardPage() { - return
게시판 관리
; -} - -export { BoardPage }; diff --git a/apps/common/features/src/shared/api/client.ts b/apps/common/features/src/shared/api/client.ts deleted file mode 100644 index 709bb44a5..000000000 --- a/apps/common/features/src/shared/api/client.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Client } from "@common/network"; -import { BASE_URL } from "../config/api"; -import { SITE_ID } from "../config/env"; - -export const client = Client.getInstance({ - baseURL: BASE_URL, - siteId: SITE_ID, -}); diff --git a/apps/common/features/src/shared/api/index.ts b/apps/common/features/src/shared/api/index.ts deleted file mode 100644 index b7b9d6121..000000000 --- a/apps/common/features/src/shared/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { client } from "./client"; diff --git a/apps/common/features/src/shared/config/api.ts b/apps/common/features/src/shared/config/api.ts deleted file mode 100644 index 94d2e728d..000000000 --- a/apps/common/features/src/shared/config/api.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { PROXY_HOST } from "./env"; - -export const BASE_URL = `${PROXY_HOST}/server`; - -export const ENDPOINT = { - ATTACH_SERVICE: { - TUS_UPLOAD: "/attach-service/api/v1/tus/upload", - EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", - IMAGES: "/attach-service/api/v1/images", - ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", - }, - USER_SERVICE: { - LOGIN: "/user-service/login", - USERS: "/user-service/api/v1/users", - SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", - AUTHRTS: "/user-service/api/v1/authorizations", - ROLES_AUTHRTS: "/user-service/api/v1/role-authorizations", - }, - CMS_SERVICE: { - SITES: "/cms-service/api/v1/sites", - IPS: "/cms-service/api/v1/site-ips", - DOMAINS: "/cms-service/api/v1/site-dmns", - POSTINGS: "/cms-service/api/v1/site-pstgs", - CODE: "/cms-service/api/v1/code/cache", - MENUS: "/cms-service/api/v1/menus", - MENU_ROLES: "/cms-service/api/v1/menu-roles", - MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", - CONTENTS: "/cms-service/api/v1/contents", - USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", - IMAGES: "/cms-service/api/v1/images", - BANNERS: "/cms-service/api/v1/bnrs", - TEMPLATE: "/cms-service/api/v1/tmplt", - TERMS: "/cms-service/api/v1/trms", - }, - BOARD_SERVICE: { - BBS: "/board-service/api/v1/bbs", - }, -}; - -export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/common/features/src/shared/config/env.ts b/apps/common/features/src/shared/config/env.ts deleted file mode 100644 index f0b4c83b1..000000000 --- a/apps/common/features/src/shared/config/env.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; -export const PROXY_HOST = - process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; - -export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; -export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS"; - -export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; -export const ACCESS_TOKEN = - process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; -export const REFRESH_TOKEN = - process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; -export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; diff --git a/apps/common/features/src/shared/config/index.ts b/apps/common/features/src/shared/config/index.ts deleted file mode 100644 index cd83447d5..000000000 --- a/apps/common/features/src/shared/config/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./api"; -export * from "./env"; diff --git a/apps/common/features/src/shared/lib/cookie.ts b/apps/common/features/src/shared/lib/cookie.ts deleted file mode 100644 index 8f5d0011b..000000000 --- a/apps/common/features/src/shared/lib/cookie.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Cookies from "js-cookie"; - -export function getCookie(name: string): string | undefined { - if (typeof window === "undefined") { - return undefined; - } - return Cookies.get(name); -} - -export function setCookie( - name: string, - value: string, - options?: Cookies.CookieAttributes, -): void { - if (typeof window === "undefined") { - return; - } - Cookies.set(name, value, options); -} - -export function removeCookie( - name: string, - options?: Cookies.CookieAttributes, -): void { - if (typeof window === "undefined") { - return; - } - Cookies.remove(name, options); -} diff --git a/apps/common/features/src/shared/lib/index.ts b/apps/common/features/src/shared/lib/index.ts deleted file mode 100644 index bfbe51702..000000000 --- a/apps/common/features/src/shared/lib/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { getCookie, setCookie, removeCookie } from "./cookie"; -export { useMenuStore } from "./use-menu-store"; -export { useUserStore } from "./use-user-store"; diff --git a/apps/common/features/src/shared/lib/use-alert-store.ts b/apps/common/features/src/shared/lib/use-alert-store.ts deleted file mode 100644 index c21a00ed3..000000000 --- a/apps/common/features/src/shared/lib/use-alert-store.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useStore } from "zustand"; -import { useContext } from "react"; -import type { AlertStore } from "../store/alert-store"; -import { StoreContext } from "../providers/store-provider"; - -export const useAlertStore = (selector: (store: AlertStore) => T): T => { - const storeContext = useContext(StoreContext); - - if (!storeContext) { - throw new Error("useAlertStore must be used within StoreProvider"); - } - - return useStore(storeContext.alertStore, selector); -}; diff --git a/apps/common/features/src/shared/lib/use-attachment-store.ts b/apps/common/features/src/shared/lib/use-attachment-store.ts deleted file mode 100644 index 285699cc6..000000000 --- a/apps/common/features/src/shared/lib/use-attachment-store.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useStore } from "zustand"; -import { useContext } from "react"; -import type { AttachmentStore } from "../store/attachment-store"; -import { StoreContext } from "../providers/store-provider"; - -export const useAttachmentStore = ( - selector: (store: AttachmentStore) => T, -): T => { - const store = useContext(StoreContext); - if (!store) { - throw new Error("useAttachmentStore must be used within StoreProvider"); - } - return useStore(store.attachmentStore, selector); -}; diff --git a/apps/common/features/src/shared/lib/use-menu-store.ts b/apps/common/features/src/shared/lib/use-menu-store.ts deleted file mode 100644 index 66e41c76c..000000000 --- a/apps/common/features/src/shared/lib/use-menu-store.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useStore } from "zustand"; -import { useContext } from "react"; -import type { MenuStore } from "../store/menu-store"; -import { StoreContext } from "../providers/store-provider"; - -export const useMenuStore = (selector: (store: MenuStore) => T): T => { - const storeContext = useContext(StoreContext); - - if (!storeContext) { - throw new Error("useMenuStore must be used within StoreProvider"); - } - - return useStore(storeContext.menuStore, selector); -}; diff --git a/apps/common/features/src/shared/lib/use-tus.ts b/apps/common/features/src/shared/lib/use-tus.ts deleted file mode 100644 index fdaeb1f65..000000000 --- a/apps/common/features/src/shared/lib/use-tus.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect, useState } from "react"; -import { type HttpResponse, Upload } from "tus-js-client"; -import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "../config"; -import { useAttachmentStore } from "./use-attachment-store"; -import { getCookie } from "./cookie"; - -export const useTus = () => { - const { attachmentFileCode, setAttachmentFileCode } = useAttachmentStore( - (state) => state, - ); - - const [file, setFile] = useState(null); - const [upload, setUpload] = useState(null); - const [response, setResponse] = useState(null); - const [progress, setProgress] = useState(0); - - const metadata = { - filename: file?.name ?? "", - filetype: file?.type ?? "", - atflCd: attachmentFileCode, - }; - - const createUpload = () => { - return new Upload(file!, { - endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`, - chunkSize: 10 * 1024 * 1024, - headers: { - [CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "", - [AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "", - }, - metadata, - onError(error) { - throw new Error(`Failed because: ${error}`); - }, - onProgress(bytesUploaded, bytesTotal) { - const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); - setProgress(Number(percentage)); - }, - onAfterResponse(req, res) { - const fileInfo = res.getHeader("X-File-Info"); - if (fileInfo === undefined || fileInfo === null) return; - - setAttachmentFileCode(JSON.parse(fileInfo)?.atflCd ?? ""); - setResponse(res); - }, - }); - }; - - useEffect(() => { - if (!file) return; - - const upload = createUpload(); - upload.start(); - setUpload(upload); - }, [file]); - - return { - file, - upload, - response, - progress, - setFile, - }; -}; diff --git a/apps/common/features/src/shared/lib/use-user-store.ts b/apps/common/features/src/shared/lib/use-user-store.ts deleted file mode 100644 index 748caed4c..000000000 --- a/apps/common/features/src/shared/lib/use-user-store.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useStore } from "zustand"; -import { useContext } from "react"; -import type { UserStore } from "../store/user-store"; -import { StoreContext } from "../providers/store-provider"; - -export const useUserStore = (selector: (store: UserStore) => T): T => { - const storeContext = useContext(StoreContext); - - if (!storeContext) { - throw new Error("useUserStore must be used within StoreProvider"); - } - - return useStore(storeContext.userStore, selector); -}; diff --git a/apps/common/features/src/shared/model/api-interface.ts b/apps/common/features/src/shared/model/api-interface.ts deleted file mode 100644 index 133c0c401..000000000 --- a/apps/common/features/src/shared/model/api-interface.ts +++ /dev/null @@ -1,34 +0,0 @@ -interface SortConfig { - empty: boolean; - sorted: boolean; - unsorted: boolean; -} - -interface PageableConfig { - sort: SortConfig; - offset: number; - pageNumber: number; - pageSize: number; - paged: boolean; - unpaged: boolean; -} - -export interface PageableList { - content: T[]; - pageable: PageableConfig; - sort: SortConfig; - totalPages: number; - totalElements: number; - number: number; - numberOfElements: number; - size: number; - first: boolean; - last: boolean; - empty: boolean; -} - -export interface PageableParams { - page?: number; - size?: number; - sort?: string[]; -} diff --git a/apps/common/features/src/shared/model/index.ts b/apps/common/features/src/shared/model/index.ts deleted file mode 100644 index cf589d200..000000000 --- a/apps/common/features/src/shared/model/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./api-interface"; diff --git a/apps/common/features/src/shared/providers/index.ts b/apps/common/features/src/shared/providers/index.ts deleted file mode 100644 index f2e092170..000000000 --- a/apps/common/features/src/shared/providers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { StoreProvider } from "./store-provider"; diff --git a/apps/common/features/src/shared/providers/store-provider.tsx b/apps/common/features/src/shared/providers/store-provider.tsx deleted file mode 100644 index a0ec17b61..000000000 --- a/apps/common/features/src/shared/providers/store-provider.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -import { createContext, useRef } from "react"; -import { - createAlertStore, - createAttachmentStore, - createMenuStore, - createUserStore, -} from "../store"; - -type StoreContextType = { - alertStore: ReturnType; - attachmentStore: ReturnType; - menuStore: ReturnType; - userStore: ReturnType; -}; - -export const StoreContext = createContext( - undefined, -); - -export const StoreProvider = ({ children }: { children: React.ReactNode }) => { - const storeRef = useRef(null); - if (storeRef.current === null) { - storeRef.current = { - alertStore: createAlertStore(), - attachmentStore: createAttachmentStore(), - menuStore: createMenuStore(), - userStore: createUserStore(), - }; - } - - return ( - - {children} - - ); -}; diff --git a/apps/common/features/src/shared/store/alert-store.ts b/apps/common/features/src/shared/store/alert-store.ts deleted file mode 100644 index 3bba65848..000000000 --- a/apps/common/features/src/shared/store/alert-store.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createStore } from "zustand/vanilla"; - -export type AlertState = { - message: string; - callback?: () => void; -}; - -export type AlertActions = { - setMessage: (message: string, callback?: () => void) => void; -}; - -export type AlertStore = AlertState & AlertActions; - -export const defaultInitState: AlertState = { - message: "", - callback: undefined, -}; - -export const createAlertStore = (initState: AlertState = defaultInitState) => { - return createStore()((set) => ({ - ...initState, - setMessage: (message: string, callback?: () => void) => - set(() => ({ message, callback })), - })); -}; diff --git a/apps/common/features/src/shared/store/attachment-store.ts b/apps/common/features/src/shared/store/attachment-store.ts deleted file mode 100644 index faa700949..000000000 --- a/apps/common/features/src/shared/store/attachment-store.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createStore } from "zustand/vanilla"; - -export type AttachmentState = { - attachmentFileCode: string; -}; - -export type AttachmentActions = { - setAttachmentFileCode: (attachmentFileCode: string) => void; -}; - -export type AttachmentStore = AttachmentState & AttachmentActions; - -export const defaultInitState: AttachmentState = { - attachmentFileCode: "", -}; - -export const createAttachmentStore = ( - initState: AttachmentState = defaultInitState, -) => { - return createStore()((set) => ({ - ...initState, - setAttachmentFileCode: (attachmentFileCode: string) => - set({ attachmentFileCode }), - })); -}; diff --git a/apps/common/features/src/shared/store/index.ts b/apps/common/features/src/shared/store/index.ts deleted file mode 100644 index e26f6781e..000000000 --- a/apps/common/features/src/shared/store/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { createAlertStore } from "./alert-store"; -export { createAttachmentStore } from "./attachment-store"; -export { createMenuStore } from "./menu-store"; -export { createUserStore, type IUserInfo } from "./user-store"; diff --git a/apps/common/features/src/shared/store/menu-store.ts b/apps/common/features/src/shared/store/menu-store.ts deleted file mode 100644 index f0c6340c9..000000000 --- a/apps/common/features/src/shared/store/menu-store.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createStore } from "zustand/vanilla"; -import type { MenuItem } from "@common/components/api"; - -export type MenuState = { - currentMenuInfo: MenuItem | null; -}; - -export type MenuActions = { - setCurrentMenuInfo: (menu: MenuItem) => void; -}; - -export type MenuStore = MenuState & MenuActions; - -export const defaultInitState: MenuState = { - currentMenuInfo: null, -}; - -export const createMenuStore = (initState: MenuState = defaultInitState) => { - return createStore()((set) => ({ - ...initState, - setCurrentMenuInfo: (menu: MenuItem) => set({ currentMenuInfo: menu }), - })); -}; diff --git a/apps/common/features/src/shared/store/user-store.ts b/apps/common/features/src/shared/store/user-store.ts deleted file mode 100644 index 00379fb1f..000000000 --- a/apps/common/features/src/shared/store/user-store.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createStore } from "zustand/vanilla"; - -export interface IUserInfo { - regDt: string; - mdfcnDt: string; - rgtr: string; - mdfr: string; - userUnqId: string; - userId: string; - userName: string; - email: string; - encryptedPassword: string; - refreshToken: string; - userStateCode: string; - lastLoginDate: string; - loginFailCount: number; - pswdHnt: string; - pswdAns: string; - mphn: string; - telno: string; - zip: string; - addr: string; - daddr: string; - laeSeCd: string; - ogdpSeCd: string; - aprvYn: string; - autzrId: string; - expryYmd: string; - lastPswdChgDt: string; - googleId: string; - kakaoId: string; - naverId: string; - isSocialUser: boolean; - hasPassword: boolean; -} - -export type UserState = { - userInfo: IUserInfo | null; -}; - -export type UserActions = { - setUserInfo: (userInfo: IUserInfo | null) => void; -}; - -export type UserStore = UserState & UserActions; - -export const defaultInitState: UserState = { - userInfo: null, -}; - -export const createUserStore = (initState: UserState = defaultInitState) => { - return createStore()((set) => ({ - ...initState, - setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), - })); -}; diff --git a/apps/common/features/src/shared/ui/alert-dialog.tsx b/apps/common/features/src/shared/ui/alert-dialog.tsx deleted file mode 100644 index 728276713..000000000 --- a/apps/common/features/src/shared/ui/alert-dialog.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { - Dialog, - DialogContent, - DialogFooter, - DialogClose, - Button, -} from "@common/components/ui"; -import { useAlertStore } from "../lib/use-alert-store"; - -function AlertDialog() { - const { message, callback, setMessage } = useAlertStore((state) => state); - const handleOpenChange = () => { - setMessage(""); - callback?.(); - }; - return ( - - -
-

{message}

-
- - - - - -
-
- ); -} - -export { AlertDialog }; diff --git a/apps/common/features/src/shared/ui/confirm-dialog.tsx b/apps/common/features/src/shared/ui/confirm-dialog.tsx deleted file mode 100644 index 4223d197e..000000000 --- a/apps/common/features/src/shared/ui/confirm-dialog.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogClose, - Button, - type ButtonProps, -} from "@common/components/ui"; - -type ConfirmDialogProps = { - open?: boolean; - confirmButtonProps?: ButtonProps & { label?: string }; - cancelButtonProps?: ButtonProps & { label?: string }; -}; - -function ConfirmDialog({ - open, - confirmButtonProps, - cancelButtonProps, -}: ConfirmDialogProps) { - const { message, isOpen, onConfirm, onCancel } = useConfirm(); - return ( - - -
-

{message}

-
- - - - - - - - -
-
- ); -} - -export { ConfirmDialog }; diff --git a/apps/common/features/src/shared/ui/form.tsx b/apps/common/features/src/shared/ui/form.tsx deleted file mode 100644 index bd579d263..000000000 --- a/apps/common/features/src/shared/ui/form.tsx +++ /dev/null @@ -1,175 +0,0 @@ -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 ( -