From 9958ea72b8c6f05889b8e9dc1e0ed3896b1eb880 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 1 Jun 2026 16:41:18 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EC=BD=94=EB=93=9C=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code-management/page.tsx | 1 + apps/as/src/pages/code-management/index.ts | 1 + .../model/acntg-se-table-columns.tsx | 61 +++++++ .../model/cnstp-se-table-columns.tsx | 61 +++++++ .../ui/acntg-se-form-dialog.tsx | 151 ++++++++++++++++++ .../code-management/ui/acntg-se-section.tsx | 144 +++++++++++++++++ .../ui/cnstp-se-form-dialog.tsx | 151 ++++++++++++++++++ .../code-management/ui/cnstp-se-section.tsx | 147 +++++++++++++++++ .../ui/code-management-page.tsx | 30 ++++ 9 files changed, 747 insertions(+) create mode 100644 apps/as/app/(with-base-layout)/code-management/page.tsx create mode 100644 apps/as/src/pages/code-management/index.ts create mode 100644 apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx create mode 100644 apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx create mode 100644 apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx create mode 100644 apps/as/src/pages/code-management/ui/acntg-se-section.tsx create mode 100644 apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx create mode 100644 apps/as/src/pages/code-management/ui/cnstp-se-section.tsx create mode 100644 apps/as/src/pages/code-management/ui/code-management-page.tsx diff --git a/apps/as/app/(with-base-layout)/code-management/page.tsx b/apps/as/app/(with-base-layout)/code-management/page.tsx new file mode 100644 index 000000000..7a4daf43f --- /dev/null +++ b/apps/as/app/(with-base-layout)/code-management/page.tsx @@ -0,0 +1 @@ +export { CodeManagementPage as default } from "@/pages/code-management"; diff --git a/apps/as/src/pages/code-management/index.ts b/apps/as/src/pages/code-management/index.ts new file mode 100644 index 000000000..3b00522e0 --- /dev/null +++ b/apps/as/src/pages/code-management/index.ts @@ -0,0 +1 @@ +export { CodeManagementPage } from "./ui/code-management-page"; diff --git a/apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx b/apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx new file mode 100644 index 000000000..2e4cc96f0 --- /dev/null +++ b/apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { createColumnHelper } from "@tanstack/react-table"; + +import type { AcntgSeMngResponseDto } from "@/entities/acntg"; +import { Button } from "@common/components"; + +const columnHelper = createColumnHelper(); + +interface CreateAcntgSeColumnsOptions { + onEdit: (row: AcntgSeMngResponseDto) => void; + onDelete: (row: AcntgSeMngResponseDto) => void; +} + +function formatUseYn(value: string | undefined) { + if (value === "Y") return "사용"; + if (value === "N") return "미사용"; + return "-"; +} + +function formatOptional(value: string | undefined) { + return value?.trim() ? value : "-"; +} + +export function createAcntgSeColumns({ onEdit, onDelete }: CreateAcntgSeColumnsOptions) { + return [ + columnHelper.accessor("acntgSeCd", { + header: "코드", + meta: { align: "center" }, + }), + columnHelper.accessor("acntgSeCdNm", { + header: "코드명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("expln", { + header: "설명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("useYn", { + header: "사용여부", + meta: { align: "center" }, + cell: ({ getValue }) => formatUseYn(getValue()), + }), + columnHelper.display({ + id: "actions", + header: "관리", + cell: ({ row }) => ( +
+ + +
+ ), + }), + ]; +} diff --git a/apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx b/apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx new file mode 100644 index 000000000..45dcadff8 --- /dev/null +++ b/apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { createColumnHelper } from "@tanstack/react-table"; + +import type { CnstpSeMngResponseDto } from "@/entities/cnstp"; +import { Button } from "@common/components"; + +const columnHelper = createColumnHelper(); + +interface CreateCnstpSeColumnsOptions { + onEdit: (row: CnstpSeMngResponseDto) => void; + onDelete: (row: CnstpSeMngResponseDto) => void; +} + +function formatUseYn(value: string | undefined) { + if (value === "Y") return "사용"; + if (value === "N") return "미사용"; + return "-"; +} + +function formatOptional(value: string | undefined) { + return value?.trim() ? value : "-"; +} + +export function createCnstpSeColumns({ onEdit, onDelete }: CreateCnstpSeColumnsOptions) { + return [ + columnHelper.accessor("cnstpSeCd", { + header: "코드", + meta: { align: "center" }, + }), + columnHelper.accessor("cnstpSeCdNm", { + header: "코드명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("expln", { + header: "설명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("useYn", { + header: "사용여부", + meta: { align: "center" }, + cell: ({ getValue }) => formatUseYn(getValue()), + }), + columnHelper.display({ + id: "actions", + header: "관리", + cell: ({ row }) => ( +
+ + +
+ ), + }), + ]; +} diff --git a/apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx b/apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx new file mode 100644 index 000000000..5d6afc414 --- /dev/null +++ b/apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { useAcntgSeMng } from "@/entities/acntg"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, + RadioGroup, + RadioGroupItem, + Skeleton, +} from "@common/components"; + +export interface AcntgSeFormValues { + acntgSeCd: string; + acntgSeCdNm: string; + expln: string; + useYn: "Y" | "N"; +} + +interface AcntgSeFormDialogProps { + mode: "create" | "edit"; + open: boolean; + acntgSeCd?: string | null; + onOpenChange: (open: boolean) => void; + isSubmitting?: boolean; + onSubmit: (values: AcntgSeFormValues) => void; +} + +function AcntgSeFormDialog({ + mode, + open, + acntgSeCd, + onOpenChange, + isSubmitting, + onSubmit, +}: AcntgSeFormDialogProps) { + const detailQuery = useAcntgSeMng(mode === "edit" ? acntgSeCd : null); + + const { register, handleSubmit, reset, setValue, watch } = useForm({ + defaultValues: { + acntgSeCd: "", + acntgSeCdNm: "", + expln: "", + useYn: "Y", + }, + }); + + const useYn = watch("useYn"); + + useEffect(() => { + if (!open) return; + + if (mode === "edit") { + if (!detailQuery.data) return; + reset({ + acntgSeCd: detailQuery.data.acntgSeCd, + acntgSeCdNm: detailQuery.data.acntgSeCdNm ?? "", + expln: detailQuery.data.expln ?? "", + useYn: detailQuery.data.useYn === "N" ? "N" : "Y", + }); + return; + } + + reset({ + acntgSeCd: "", + acntgSeCdNm: "", + expln: "", + useYn: "Y", + }); + }, [open, mode, detailQuery.data, reset]); + + const isLoading = mode === "edit" && detailQuery.isPending; + + return ( + + + + {mode === "create" ? "회계코드 등록" : "회계코드 수정"} + + {isLoading ? ( +
+ + + +
+ ) : ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + setValue("useYn", v as "Y" | "N")} + className="flex gap-6" + > +
+ + +
+
+ + +
+
+
+ + + + +
+ )} +
+
+ ); +} + +export { AcntgSeFormDialog }; diff --git a/apps/as/src/pages/code-management/ui/acntg-se-section.tsx b/apps/as/src/pages/code-management/ui/acntg-se-section.tsx new file mode 100644 index 000000000..f0daf9d4f --- /dev/null +++ b/apps/as/src/pages/code-management/ui/acntg-se-section.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; + +import { useDataTable } from "@/shared/lib"; +import { + type AcntgSeMngResponseDto, + useAcntgSeMng, + useAcntgSeMngPage, + useDeleteAcntgSeMng, + useSaveAcntgSeMng, + useUpdateAcntgSeMng, +} from "@/entities/acntg"; +import { Button } from "@common/components"; + +import { createAcntgSeColumns } from "../model/acntg-se-table-columns"; + +import { AcntgSeFormDialog, type AcntgSeFormValues } from "./acntg-se-form-dialog"; + +interface AcntgSeSectionProps { + pagination: PaginationState; + onPaginationChange: (pagination: PaginationState) => void; +} + +function AcntgSeSection({ pagination, onPaginationChange }: AcntgSeSectionProps) { + const { confirm } = useConfirm(); + const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null); + const [editingCd, setEditingCd] = useState(null); + + const pageQuery = useAcntgSeMngPage({ + page: pagination.pageIndex, + size: pagination.pageSize, + }); + + const detailQuery = useAcntgSeMng(dialogMode === "edit" ? editingCd : null); + const saveMutation = useSaveAcntgSeMng(); + const updateMutation = useUpdateAcntgSeMng(); + const deleteMutation = useDeleteAcntgSeMng(); + + const handleDelete = useCallback( + async (row: AcntgSeMngResponseDto) => { + if (!(await confirm("삭제하시겠습니까?"))) return; + await deleteMutation.mutateAsync(row.acntgSeCd); + }, + [confirm, deleteMutation], + ); + + const columns = useMemo( + () => + createAcntgSeColumns({ + onEdit: (row) => { + setEditingCd(row.acntgSeCd); + setDialogMode("edit"); + }, + onDelete: handleDelete, + }), + [handleDelete], + ); + + const { tableComponent } = useDataTable({ + query: pageQuery, + tableOptions: { + columns, + state: { pagination }, + onPaginationChange: (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + onPaginationChange(next); + }, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const handleFormSubmit = async (values: AcntgSeFormValues) => { + if (!values.acntgSeCd.trim() || !values.acntgSeCdNm.trim()) return; + + if (dialogMode === "create") { + if (!(await confirm("저장하시겠습니까?"))) return; + await saveMutation.mutateAsync({ + acntgSeCd: values.acntgSeCd.trim(), + acntgSeCdNm: values.acntgSeCdNm.trim(), + expln: values.expln.trim() || undefined, + useYn: values.useYn, + cdLvNo: 1, + }); + } else if (dialogMode === "edit" && detailQuery.data) { + if (!(await confirm("수정하시겠습니까?"))) return; + await updateMutation.mutateAsync({ + acntgSeCd: detailQuery.data.acntgSeCd, + body: { + acntgSeCd: detailQuery.data.acntgSeCd, + acntgSeCdNm: values.acntgSeCdNm.trim(), + expln: values.expln.trim() || undefined, + useYn: values.useYn, + upAcntgSeCd: detailQuery.data.upAcntgSeCd, + cdLvNo: detailQuery.data.cdLvNo, + }, + }); + } + + setDialogMode(null); + setEditingCd(null); + }; + + const handleOpenCreate = () => { + setEditingCd(null); + setDialogMode("create"); + }; + + return ( +
+
+

회계코드 관리

+ +
+ {tableComponent} + {dialogMode ? ( + { + if (!open) { + setDialogMode(null); + setEditingCd(null); + } + }} + isSubmitting={saveMutation.isPending || updateMutation.isPending} + onSubmit={handleFormSubmit} + /> + ) : null} +
+ ); +} + +export { AcntgSeSection }; diff --git a/apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx b/apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx new file mode 100644 index 000000000..362728cb9 --- /dev/null +++ b/apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { useCnstpSeMng } from "@/entities/cnstp"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, + RadioGroup, + RadioGroupItem, + Skeleton, +} from "@common/components"; + +export interface CnstpSeFormValues { + cnstpSeCd: string; + cnstpSeCdNm: string; + expln: string; + useYn: "Y" | "N"; +} + +interface CnstpSeFormDialogProps { + mode: "create" | "edit"; + open: boolean; + cnstpSeCd?: string | null; + onOpenChange: (open: boolean) => void; + isSubmitting?: boolean; + onSubmit: (values: CnstpSeFormValues) => void; +} + +function CnstpSeFormDialog({ + mode, + open, + cnstpSeCd, + onOpenChange, + isSubmitting, + onSubmit, +}: CnstpSeFormDialogProps) { + const detailQuery = useCnstpSeMng(mode === "edit" ? cnstpSeCd : null); + + const { register, handleSubmit, reset, setValue, watch } = useForm({ + defaultValues: { + cnstpSeCd: "", + cnstpSeCdNm: "", + expln: "", + useYn: "Y", + }, + }); + + const useYn = watch("useYn"); + + useEffect(() => { + if (!open) return; + + if (mode === "edit") { + if (!detailQuery.data) return; + reset({ + cnstpSeCd: detailQuery.data.cnstpSeCd, + cnstpSeCdNm: detailQuery.data.cnstpSeCdNm ?? "", + expln: detailQuery.data.expln ?? "", + useYn: detailQuery.data.useYn === "N" ? "N" : "Y", + }); + return; + } + + reset({ + cnstpSeCd: "", + cnstpSeCdNm: "", + expln: "", + useYn: "Y", + }); + }, [open, mode, detailQuery.data, reset]); + + const isLoading = mode === "edit" && detailQuery.isPending; + + return ( + + + + {mode === "create" ? "공종코드 등록" : "공종코드 수정"} + + {isLoading ? ( +
+ + + +
+ ) : ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + setValue("useYn", v as "Y" | "N")} + className="flex gap-6" + > +
+ + +
+
+ + +
+
+
+ + + + +
+ )} +
+
+ ); +} + +export { CnstpSeFormDialog }; diff --git a/apps/as/src/pages/code-management/ui/cnstp-se-section.tsx b/apps/as/src/pages/code-management/ui/cnstp-se-section.tsx new file mode 100644 index 000000000..23b9d792f --- /dev/null +++ b/apps/as/src/pages/code-management/ui/cnstp-se-section.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; + +import { useDataTable } from "@/shared/lib"; +import { + type CnstpSeMngResponseDto, + useCnstpSeMng, + useCnstpSeMngPage, + useDeleteCnstpSeMng, + useSaveCnstpSeMng, + useUpdateCnstpSeMng, +} from "@/entities/cnstp"; +import { Button } from "@common/components"; + +import { createCnstpSeColumns } from "../model/cnstp-se-table-columns"; + +import { CnstpSeFormDialog, type CnstpSeFormValues } from "./cnstp-se-form-dialog"; + +const DEFAULT_PAGE_INDEX = 0; +const DEFAULT_PAGE_SIZE = 10; + +interface CnstpSeSectionProps { + pagination: PaginationState; + onPaginationChange: (pagination: PaginationState) => void; +} + +function CnstpSeSection({ pagination, onPaginationChange }: CnstpSeSectionProps) { + const { confirm } = useConfirm(); + const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null); + const [editingCd, setEditingCd] = useState(null); + + const pageQuery = useCnstpSeMngPage({ + page: pagination.pageIndex, + size: pagination.pageSize, + }); + + const detailQuery = useCnstpSeMng(dialogMode === "edit" ? editingCd : null); + const saveMutation = useSaveCnstpSeMng(); + const updateMutation = useUpdateCnstpSeMng(); + const deleteMutation = useDeleteCnstpSeMng(); + + const handleDelete = useCallback( + async (row: CnstpSeMngResponseDto) => { + if (!(await confirm("삭제하시겠습니까?"))) return; + await deleteMutation.mutateAsync(row.cnstpSeCd); + }, + [confirm, deleteMutation], + ); + + const columns = useMemo( + () => + createCnstpSeColumns({ + onEdit: (row) => { + setEditingCd(row.cnstpSeCd); + setDialogMode("edit"); + }, + onDelete: handleDelete, + }), + [handleDelete], + ); + + const { tableComponent } = useDataTable({ + query: pageQuery, + tableOptions: { + columns, + state: { pagination }, + onPaginationChange: (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + onPaginationChange(next); + }, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const handleFormSubmit = async (values: CnstpSeFormValues) => { + if (!values.cnstpSeCd.trim()) return; + + if (dialogMode === "create") { + if (!(await confirm("저장하시겠습니까?"))) return; + await saveMutation.mutateAsync({ + cnstpSeCd: values.cnstpSeCd.trim(), + cnstpSeCdNm: values.cnstpSeCdNm.trim() || undefined, + expln: values.expln.trim() || undefined, + useYn: values.useYn, + cdLvNo: 1, + }); + } else if (dialogMode === "edit" && detailQuery.data) { + if (!(await confirm("수정하시겠습니까?"))) return; + await updateMutation.mutateAsync({ + cnstpSeCd: detailQuery.data.cnstpSeCd, + body: { + cnstpSeCd: detailQuery.data.cnstpSeCd, + cnstpSeCdNm: values.cnstpSeCdNm.trim() || undefined, + expln: values.expln.trim() || undefined, + useYn: values.useYn, + upCnstpCd: detailQuery.data.upCnstpCd, + cdLvNo: detailQuery.data.cdLvNo, + }, + }); + } + + setDialogMode(null); + setEditingCd(null); + }; + + const handleOpenCreate = () => { + setEditingCd(null); + setDialogMode("create"); + }; + + return ( +
+
+

공종코드 관리

+ +
+ {tableComponent} + {dialogMode ? ( + { + if (!open) { + setDialogMode(null); + setEditingCd(null); + } + }} + isSubmitting={saveMutation.isPending || updateMutation.isPending} + onSubmit={handleFormSubmit} + /> + ) : null} +
+ ); +} + +export { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE }; diff --git a/apps/as/src/pages/code-management/ui/code-management-page.tsx b/apps/as/src/pages/code-management/ui/code-management-page.tsx new file mode 100644 index 000000000..95e7dcd9f --- /dev/null +++ b/apps/as/src/pages/code-management/ui/code-management-page.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useState } from "react"; + +import type { PaginationState } from "@tanstack/react-table"; + +import { AcntgSeSection } from "./acntg-se-section"; +import { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE } from "./cnstp-se-section"; + +function CodeManagementPage() { + const [cnstpPagination, setCnstpPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); + const [acntgPagination, setAcntgPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); + + return ( +
+
+ + +
+
+ ); +} + +export { CodeManagementPage };