From 0588bf0d7f15a286693983079b8c0c5f976de533 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 12 Mar 2025 16:56:47 +0900 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=83=81=EC=84=B8(=EC=88=98=EC=A0=95)=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B0=94=EC=9D=B8?= =?UTF-8?q?=EB=94=A9=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/app/site/register/[id]/page.tsx | 10 ++ apps/cs/app/site/register/page.tsx | 2 - apps/cs/package.json | 1 + apps/cs/src/entities/code/api/code-service.ts | 14 ++ apps/cs/src/entities/code/index.ts | 1 + .../src/entities/code/model/code-interface.ts | 9 + apps/cs/src/entities/code/ui/code-select.tsx | 45 +++++ apps/cs/src/entities/site/api/site-service.ts | 7 + .../src/entities/site/model/site-interface.ts | 26 +++ .../{__mocks__.tsx => table-columns.tsx} | 17 +- .../pages/site/ui/ip-management-dialog.tsx | 8 +- apps/cs/src/pages/site/ui/register-page.tsx | 100 ++++++----- apps/cs/src/pages/site/ui/site-page.tsx | 31 ++-- apps/cs/src/shared/config/api.ts | 1 + packages/common/assets/src/styles/base.css | 2 + packages/common/components/package.json | 1 + .../src/ui/bar-chart/chart.stories.tsx | 2 +- .../src/ui/data-table/data-table.stories.tsx | 2 +- .../components/src/ui/data-table/index.tsx | 13 +- packages/common/components/src/ui/index.ts | 1 + .../common/components/src/ui/input/index.tsx | 2 +- .../src/ui/line-chart/chart.stories.tsx | 31 ++-- .../src/ui/pie-chart/chart.stories.tsx | 53 ++---- .../common/components/src/ui/select/index.tsx | 159 ++++++++++++++++++ .../src/ui/select/select.stories.tsx | 41 +++++ .../common/components/src/ui/table/index.tsx | 2 +- .../common/components/src/ui/table/table.css | 2 +- .../components/src/ui/tree/tree.stories.tsx | 2 +- pnpm-lock.yaml | 70 ++++++++ tailwind.config.preset.ts | 1 + 30 files changed, 529 insertions(+), 127 deletions(-) create mode 100644 apps/cs/app/site/register/[id]/page.tsx delete mode 100644 apps/cs/app/site/register/page.tsx create mode 100644 apps/cs/src/entities/code/api/code-service.ts create mode 100644 apps/cs/src/entities/code/index.ts create mode 100644 apps/cs/src/entities/code/model/code-interface.ts create mode 100644 apps/cs/src/entities/code/ui/code-select.tsx rename apps/cs/src/pages/site/model/{__mocks__.tsx => table-columns.tsx} (74%) create mode 100644 packages/common/components/src/ui/select/index.tsx create mode 100644 packages/common/components/src/ui/select/select.stories.tsx diff --git a/apps/cs/app/site/register/[id]/page.tsx b/apps/cs/app/site/register/[id]/page.tsx new file mode 100644 index 000000000..b1f4d3a16 --- /dev/null +++ b/apps/cs/app/site/register/[id]/page.tsx @@ -0,0 +1,10 @@ +import { RegisterPage as ViewRegisterPage } from "@/pages/site"; + +export default async function RegisterPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + return ; +} diff --git a/apps/cs/app/site/register/page.tsx b/apps/cs/app/site/register/page.tsx deleted file mode 100644 index a11d0b60c..000000000 --- a/apps/cs/app/site/register/page.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { RegisterPage } from "@/pages/site"; -export default RegisterPage; diff --git a/apps/cs/package.json b/apps/cs/package.json index 1797b4828..c9c024291 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -15,6 +15,7 @@ "@tanstack/react-table": "^8.21.2", "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", diff --git a/apps/cs/src/entities/code/api/code-service.ts b/apps/cs/src/entities/code/api/code-service.ts new file mode 100644 index 000000000..3a789f2aa --- /dev/null +++ b/apps/cs/src/entities/code/api/code-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config/api"; +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/cs/src/entities/code/index.ts b/apps/cs/src/entities/code/index.ts new file mode 100644 index 000000000..0b6633f4b --- /dev/null +++ b/apps/cs/src/entities/code/index.ts @@ -0,0 +1 @@ +export { CodeSelect } from "./ui/code-select"; diff --git a/apps/cs/src/entities/code/model/code-interface.ts b/apps/cs/src/entities/code/model/code-interface.ts new file mode 100644 index 000000000..0d066e6da --- /dev/null +++ b/apps/cs/src/entities/code/model/code-interface.ts @@ -0,0 +1,9 @@ +export interface ICode { + lwpoCd: string; + upCd: string; + lwpoCdNm: string; +} + +export interface CodeService { + getCode(upCd: string): Promise; +} diff --git a/apps/cs/src/entities/code/ui/code-select.tsx b/apps/cs/src/entities/code/ui/code-select.tsx new file mode 100644 index 000000000..88c8a1368 --- /dev/null +++ b/apps/cs/src/entities/code/ui/code-select.tsx @@ -0,0 +1,45 @@ +import { useQuery } from "@tanstack/react-query"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config/api"; +import { CodeApi } from "../api/code-service"; + +type CodeSelectProps = { + upCd: string; + value?: string; + defaultValue?: string; + onValueChange?: (value: string) => void; +}; + +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/cs/src/entities/site/api/site-service.ts b/apps/cs/src/entities/site/api/site-service.ts index 89ba82350..ee2910e18 100644 --- a/apps/cs/src/entities/site/api/site-service.ts +++ b/apps/cs/src/entities/site/api/site-service.ts @@ -4,6 +4,7 @@ import type { ISiteQuery, ISiteInfo, SiteService, + ISiteDetail, } from "../model/site-interface"; export class SiteApiService implements SiteService { @@ -12,7 +13,13 @@ export class SiteApiService implements SiteService { `${ENDPOINT.CMS_SERVICE.SITES}`, { params: query }, ); + return response.data; + } + public async siteDetail(siteId: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, + ); return response.data; } } diff --git a/apps/cs/src/entities/site/model/site-interface.ts b/apps/cs/src/entities/site/model/site-interface.ts index 8ba2da298..005652b1a 100644 --- a/apps/cs/src/entities/site/model/site-interface.ts +++ b/apps/cs/src/entities/site/model/site-interface.ts @@ -12,7 +12,10 @@ export interface ISite { siteNm: string; siteKindCode: string; bscSiteYn: string; + siteAddr: string; useYn: string; + regDt: string; + rgTrNm: string; } export interface ISiteInfo { @@ -44,6 +47,29 @@ export interface ISiteInfo { empty: boolean; } +export interface ISiteDetail { + siteId: string; + siteNm: string; + siteExpln: string; + siteKndCd: string; + siteKindNm: string; + siteAddr: string; + faxNumber: string; + telNo: string; + lwndCn: string; + basicSiteYn: string; + bkmkIcon: string; + useYn: string; + siteSkn: string; + regDt: string; + rgTr: string; + mdfcnDt: string; + mdFr: string; + rgTrNm: string; + mdFrNm: string; +} + export interface SiteService { siteInfo(query: ISiteQuery): Promise; + siteDetail(siteId: string): Promise; } diff --git a/apps/cs/src/pages/site/model/__mocks__.tsx b/apps/cs/src/pages/site/model/table-columns.tsx similarity index 74% rename from apps/cs/src/pages/site/model/__mocks__.tsx rename to apps/cs/src/pages/site/model/table-columns.tsx index 2b55da7d8..ddf211aea 100644 --- a/apps/cs/src/pages/site/model/__mocks__.tsx +++ b/apps/cs/src/pages/site/model/table-columns.tsx @@ -1,3 +1,4 @@ +import dayjs from "dayjs"; import { createColumnHelper } from "@tanstack/react-table"; import type { ISite } from "@/entities/site/model/site-interface"; @@ -16,8 +17,8 @@ const columns = [ id: "siteNm", header: "사이트명", }), - columnHelper.accessor("siteKindCode", { - id: "unknown1", + columnHelper.accessor("siteAddr", { + id: "siteAddr", header: "대표 URL", }), columnHelper.accessor("bscSiteYn", { @@ -30,13 +31,17 @@ const columns = [ header: "사용 여부", cell: (info) => (info.getValue() ? "Y" : "N"), }), - columnHelper.accessor("siteId", { - id: "unknown2", + columnHelper.accessor("rgTrNm", { + id: "rgTrNm", header: "등록자", }), - columnHelper.accessor("siteId", { - id: "ununknown3", + columnHelper.accessor("regDt", { + id: "regDt", header: "등록일", + cell: (info) => { + const date = dayjs(info.getValue()); + return date.format("YYYY-MM-DD"); + }, }), ]; diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx index 04b71afc7..9f637e3be 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -26,7 +26,11 @@ type IP = { allowed: boolean; }; -function IPManagementDialog() { +type IPManagementDialogProps = { + triggerDisabled?: boolean; +}; + +function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) { const [rows, setRows] = useState([]); const [rowSelection, setRowSelection] = useState([]); @@ -72,7 +76,7 @@ function IPManagementDialog() { return ( - + diff --git a/apps/cs/src/pages/site/ui/register-page.tsx b/apps/cs/src/pages/site/ui/register-page.tsx index 3c35ae8e0..0257176ab 100644 --- a/apps/cs/src/pages/site/ui/register-page.tsx +++ b/apps/cs/src/pages/site/ui/register-page.tsx @@ -1,11 +1,10 @@ "use client"; +import { useState } from "react"; +import { SiteApi } from "@/entities/site"; +import { ENDPOINT } from "@/shared/config/api"; import { Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, Input, Label, RadioGroup, @@ -16,10 +15,24 @@ import { TableHead, TableRow, } from "@common/components"; +import { useQuery } from "@tanstack/react-query"; +import { CodeSelect } from "@/entities/code"; import { SitePageLayout } from "./layout"; import { IPManagementDialog } from "./ip-management-dialog"; -function RegisterPage() { +function RegisterPage({ siteId }: { siteId: string }) { + const { data: siteDetail } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId], + queryFn: () => SiteApi.siteDetail(siteId), + select: (data) => { + setSiteKndCd(data.siteKndCd); + return data; + }, + enabled: !!siteId, + }); + const [siteKndCd, setSiteKndCd] = useState(""); + const [, setSiteSkinCd] = useState(""); + return (
@@ -36,54 +49,38 @@ function RegisterPage() { 사이트 코드 - + 사이트 구분
- - - - - - - 내부 - 외부 - - - - + { + setSiteKndCd(value); + }} + /> +
사이트 디자인 - - - - - - - 라이트 - 다크 - - - + { + setSiteSkinCd(value); + }} + /> @@ -92,6 +89,7 @@ function RegisterPage() { @@ -101,6 +99,7 @@ function RegisterPage() { @@ -110,6 +109,7 @@ function RegisterPage() { @@ -119,6 +119,7 @@ function RegisterPage() { @@ -128,6 +129,7 @@ function RegisterPage() { @@ -137,6 +139,7 @@ function RegisterPage() { @@ -146,13 +149,19 @@ function RegisterPage() { 기본 사이트 여부 - + { + console.log(value); + }} + >
@@ -167,7 +176,12 @@ function RegisterPage() { 사용 여부 - + { + console.log(value); + }} + >
diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index c13284ce8..46e957985 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -8,12 +8,19 @@ import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { Button, Input, DataTable } from "@common/components"; import { SiteApi } from "@/entities/site/api/site-service"; +import type { ISite, ISiteQuery } from "@/entities/site/model/site-interface"; import { ENDPOINT } from "@/shared/config/api"; -import { columns } from "../model/__mocks__"; +import { columns } from "../model/table-columns"; import { SitePageLayout } from "./layout"; -import type { ISiteQuery } from "@/entities/site/model/site-interface"; function SitePage() { + const router = useRouter(); + const { data: siteInfo, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SITES], + queryFn: () => SiteApi.siteInfo(siteQuery), + enabled: false, + }); + const [siteQuery, setSiteQuery] = useState({ siteNm: "", siteKndCd: "", @@ -23,10 +30,10 @@ function SitePage() { sort: [], }); - const { data: siteInfo, refetch } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.SITES], - queryFn: () => SiteApi.siteInfo(siteQuery), - enabled: false, + const table = useReactTable({ + data: siteInfo?.content ?? [], + columns, + getCoreRowModel: getCoreRowModel(), }); const handleSubmit = (e: React.FormEvent) => { @@ -34,15 +41,13 @@ function SitePage() { refetch(); }; - const router = useRouter(); - const table = useReactTable({ - data: siteInfo?.content ?? [], - columns, - getCoreRowModel: getCoreRowModel(), - }); + const handleRowClick = (row: ISite) => { + router.push(`/site/register/${row.siteId}`); + }; useEffect(() => { refetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( @@ -71,7 +76,7 @@ function SitePage() {
- +
diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 052eea56a..bab8cac7b 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -10,6 +10,7 @@ export const ENDPOINT = { }, CMS_SERVICE: { SITES: "/cms-service/api/v1/sites", + CODE: "/cms-service/api/v1/code/cache", }, }; diff --git a/packages/common/assets/src/styles/base.css b/packages/common/assets/src/styles/base.css index bd9accda9..2d73b09df 100644 --- a/packages/common/assets/src/styles/base.css +++ b/packages/common/assets/src/styles/base.css @@ -23,6 +23,7 @@ --border: #e5e5e5; --input: #e5e5e5; --label: #666666; + --placeholder: #a2a2a2; --ring: #0a0a0a; --chart-1: #2094fa; --chart-2: #00dfe5; @@ -52,6 +53,7 @@ --border: #262626; --input: #262626; --label: #f1f1f1; + --placeholder: #737373; --ring: #d4d4d4; --chart-1: #66b3ff; --chart-2: #66f0f2; diff --git a/packages/common/components/package.json b/packages/common/components/package.json index b9b9f4f40..2bfdb6ba6 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -34,6 +34,7 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-radio-group": "^1.2.2", + "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", diff --git a/packages/common/components/src/ui/bar-chart/chart.stories.tsx b/packages/common/components/src/ui/bar-chart/chart.stories.tsx index 60bd0906f..97fa3be49 100644 --- a/packages/common/components/src/ui/bar-chart/chart.stories.tsx +++ b/packages/common/components/src/ui/bar-chart/chart.stories.tsx @@ -22,7 +22,7 @@ const meta: Meta = { description: { component: ` - [Docs](https://react-chartjs-2.js.org/) - `, +`, }, source: { code: ` diff --git a/packages/common/components/src/ui/data-table/data-table.stories.tsx b/packages/common/components/src/ui/data-table/data-table.stories.tsx index d1a4c2142..62237cc07 100644 --- a/packages/common/components/src/ui/data-table/data-table.stories.tsx +++ b/packages/common/components/src/ui/data-table/data-table.stories.tsx @@ -14,7 +14,7 @@ const meta: Meta = { description: { component: ` - [Docs](https://tanstack.com/table/v8/docs/introduction) - `, +`, }, source: { code: ` diff --git a/packages/common/components/src/ui/data-table/index.tsx b/packages/common/components/src/ui/data-table/index.tsx index 4b0ba901b..8dc30c701 100644 --- a/packages/common/components/src/ui/data-table/index.tsx +++ b/packages/common/components/src/ui/data-table/index.tsx @@ -36,15 +36,22 @@ function THead({ table }: { table: TableType }) { ); } -type TableProps = { table: TableType }; +type TableProps = { + table: TableType; + onRowClick?: (row: T) => void; +}; -function DataTable({ table }: TableProps) { +function DataTable({ table, onRowClick }: TableProps) { return ( {table.getRowModel().rows.map((row) => ( - + onRowClick?.(row.original)} + className={onRowClick ? "cursor-pointer hover:bg-muted" : ""} + > {row.getVisibleCells().map((cell) => { const { meta } = cell.column.columnDef; return ( diff --git a/packages/common/components/src/ui/index.ts b/packages/common/components/src/ui/index.ts index cdc88e163..438c18dbd 100644 --- a/packages/common/components/src/ui/index.ts +++ b/packages/common/components/src/ui/index.ts @@ -15,6 +15,7 @@ export * from "./layout"; export * from "./loading"; export * from "./popover"; export * from "./radio-group"; +export * from "./select"; export * from "./slider"; export * from "./switch"; export * from "./table"; diff --git a/packages/common/components/src/ui/input/index.tsx b/packages/common/components/src/ui/input/index.tsx index bcb03d621..70bb9827c 100644 --- a/packages/common/components/src/ui/input/index.tsx +++ b/packages/common/components/src/ui/input/index.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>( = { title: "Chart/Line", @@ -23,7 +22,7 @@ const meta: Meta = { description: { component: ` - [Docs](https://react-chartjs-2.js.org/) - `, +`, }, source: { code: ` @@ -107,7 +106,7 @@ export const Default: Story = { LineElement, Title, Tooltip, - Legend + Legend, ); const options: ChartOptions<"line"> = { @@ -124,29 +123,29 @@ export const Default: Story = { }; const labels = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July' + "January", + "February", + "March", + "April", + "May", + "June", + "July", ]; const data = { labels, datasets: [ { - label: 'Dataset 1', + label: "Dataset 1", data: labels.map(() => faker.number.int({ min: -1000, max: 1000 })), - borderColor: 'rgb(255, 99, 132)', - backgroundColor: 'rgba(255, 99, 132, 0.5)', + borderColor: "rgb(255, 99, 132)", + backgroundColor: "rgba(255, 99, 132, 0.5)", }, { - label: 'Dataset 2', + label: "Dataset 2", data: labels.map(() => faker.number.int({ min: -1000, max: 1000 })), - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgba(53, 162, 235, 0.5)', + borderColor: "rgb(53, 162, 235)", + backgroundColor: "rgba(53, 162, 235, 0.5)", }, ], }; diff --git a/packages/common/components/src/ui/pie-chart/chart.stories.tsx b/packages/common/components/src/ui/pie-chart/chart.stories.tsx index 199878356..0e50d2e22 100644 --- a/packages/common/components/src/ui/pie-chart/chart.stories.tsx +++ b/packages/common/components/src/ui/pie-chart/chart.stories.tsx @@ -1,15 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { - Chart as ChartJS, - ArcElement, - Tooltip, - Legend, - type ChartOptions, -} from "chart.js"; +import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js"; import { Pie } from "react-chartjs-2"; -import { fakerKO as faker } from "@faker-js/faker"; -import { getCssVariableValue } from "../../lib/utils"; const meta: Meta = { title: "Chart/Pie", @@ -19,7 +11,7 @@ const meta: Meta = { description: { component: ` - [Docs](https://react-chartjs-2.js.org/) - `, +`, }, source: { code: ` @@ -83,42 +75,31 @@ type Story = StoryObj; export const Default: Story = { render: () => { - ChartJS.register( - ArcElement, - Tooltip, - Legend, - ); + ChartJS.register(ArcElement, Tooltip, Legend); - const labels = [ - 'Red', - 'Blue', - 'Yellow', - 'Green', - 'Purple', - 'Orange' - ]; + const labels = ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"]; const data = { labels, datasets: [ { - label: '# of Votes', + label: "# of Votes", data: [12, 19, 3, 5, 2, 3], backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)', + "rgba(255, 99, 132, 0.2)", + "rgba(54, 162, 235, 0.2)", + "rgba(255, 206, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(153, 102, 255, 0.2)", + "rgba(255, 159, 64, 0.2)", ], borderColor: [ - 'rgba(255, 99, 132, 1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)', + "rgba(255, 99, 132, 1)", + "rgba(54, 162, 235, 1)", + "rgba(255, 206, 86, 1)", + "rgba(75, 192, 192, 1)", + "rgba(153, 102, 255, 1)", + "rgba(255, 159, 64, 1)", ], borderWidth: 1, }, diff --git a/packages/common/components/src/ui/select/index.tsx b/packages/common/components/src/ui/select/index.tsx new file mode 100644 index 000000000..ffd63bb9a --- /dev/null +++ b/packages/common/components/src/ui/select/index.tsx @@ -0,0 +1,159 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "../../lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/packages/common/components/src/ui/select/select.stories.tsx b/packages/common/components/src/ui/select/select.stories.tsx new file mode 100644 index 000000000..41846d17d --- /dev/null +++ b/packages/common/components/src/ui/select/select.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "."; + +const meta: Meta = { + title: "Elements/Select", + component: Select, + parameters: { + docs: { + description: { + component: ` +- [Docs](https://ui.shadcn.com/docs/components/select) +- [API Reference](https://www.radix-ui.com/primitives/docs/components/select#api-reference) +`, + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + ), +}; diff --git a/packages/common/components/src/ui/table/index.tsx b/packages/common/components/src/ui/table/index.tsx index 425fdba0c..e96ea7e4d 100644 --- a/packages/common/components/src/ui/table/index.tsx +++ b/packages/common/components/src/ui/table/index.tsx @@ -63,7 +63,7 @@ const TableRow = React.forwardRef< = { description: { component: ` - [Docs](https://github.com/minop1205/react-dnd-treeview/blob/next/README.md) - `, +`, }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1c113792..e99e6c0c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: chart.js: specifier: ^4.4.8 version: 4.4.8 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -148,6 +151,9 @@ importers: '@radix-ui/react-radio-group': specifier: ^1.2.2 version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.6 + version: 2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-slider': specifier: ^1.2.3 version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) @@ -2077,6 +2083,46 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-select@2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==} + peerDependencies: + '@types/react': ^18.3.18 + '@types/react-dom': ^18.3.5 + react: ^18.3.1 + react-dom: ^18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.2(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.3(@types/react@18.3.18)(react@18.3.1) + dev: false + /@radix-ui/react-slider@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==} peerDependencies: @@ -2269,6 +2315,26 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} + peerDependencies: + '@types/react': ^18.3.18 + '@types/react-dom': ^18.3.5 + react: ^18.3.1 + react-dom: ^18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/rect@1.1.0: resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} dev: false @@ -3984,6 +4050,10 @@ packages: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} dev: false + /dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dev: false + /debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: diff --git a/tailwind.config.preset.ts b/tailwind.config.preset.ts index dc8cf1743..ee341683e 100644 --- a/tailwind.config.preset.ts +++ b/tailwind.config.preset.ts @@ -41,6 +41,7 @@ export default { border: "var(--border)", input: "var(--input)", label: "var(--label)", + placeholder: "var(--placeholder)", ring: "var(--ring)", chart: { 1: "var(--chart-1)", -- 2.47.1.windows.1 From b64ff7c081d4b634039c0b19f53717d95d951b94 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 13 Mar 2025 13:25:51 +0900 Subject: [PATCH 02/19] =?UTF-8?q?design:=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=8B=A4=ED=81=AC=20=ED=85=8C?= =?UTF-8?q?=EB=A7=88=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/assets/src/styles/base.css | 8 +++ .../common/components/src/ui/dialog/index.tsx | 8 +-- .../common/components/src/ui/header/index.tsx | 6 ++- .../common/components/src/ui/input/index.tsx | 2 +- .../components/src/ui/layout/footer.tsx | 6 ++- .../common/components/src/ui/layout/lnb.tsx | 2 +- .../common/components/src/ui/select/index.tsx | 2 +- .../common/components/src/ui/table/index.tsx | 7 ++- .../common/components/src/ui/table/table.css | 50 ++++++++++++------- tailwind.config.preset.ts | 4 ++ 10 files changed, 65 insertions(+), 30 deletions(-) diff --git a/packages/common/assets/src/styles/base.css b/packages/common/assets/src/styles/base.css index 2d73b09df..8a33518bc 100644 --- a/packages/common/assets/src/styles/base.css +++ b/packages/common/assets/src/styles/base.css @@ -32,6 +32,10 @@ --chart-5: #ff7865; --chart-6: #e65db5; --radius: 1rem; + --field: #fff; + --form: #fff; + --container: #f1f6f9; + --title: #4c5e6f; } [data-mode="dark"] { --background: #0a0a0a; @@ -62,5 +66,9 @@ --chart-5: #ffbcad; --chart-6: #f4a0d3; --radius: 1rem; + --field: #5b6269; + --form: #1a1a1a; + --container: #0a0a0a; + --title: #fff; } } diff --git a/packages/common/components/src/ui/dialog/index.tsx b/packages/common/components/src/ui/dialog/index.tsx index 1186ce636..b2ba92d05 100644 --- a/packages/common/components/src/ui/dialog/index.tsx +++ b/packages/common/components/src/ui/dialog/index.tsx @@ -36,14 +36,14 @@ const DialogContent = React.forwardRef< {children} - + Close @@ -71,7 +71,7 @@ const DialogFooter = ({ }: React.HTMLAttributes) => (
+

logo - 통합플랫폼관리 + + 통합플랫폼관리 +

diff --git a/packages/common/components/src/ui/input/index.tsx b/packages/common/components/src/ui/input/index.tsx index 70bb9827c..963ef1947 100644 --- a/packages/common/components/src/ui/input/index.tsx +++ b/packages/common/components/src/ui/input/index.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>( -

© 2025 Ona And All. All Rights Reserved.

+
+

+ © 2025 Ona And All. All Rights Reserved. +

); } diff --git a/packages/common/components/src/ui/layout/lnb.tsx b/packages/common/components/src/ui/layout/lnb.tsx index 761633414..f7d3a9be8 100644 --- a/packages/common/components/src/ui/layout/lnb.tsx +++ b/packages/common/components/src/ui/layout/lnb.tsx @@ -2,7 +2,7 @@ export function LNB() { return (
  • diff --git a/packages/common/components/src/ui/select/index.tsx b/packages/common/components/src/ui/select/index.tsx index ffd63bb9a..77e8ec11a 100644 --- a/packages/common/components/src/ui/select/index.tsx +++ b/packages/common/components/src/ui/select/index.tsx @@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "bg-form flex h-[3.2rem] w-full items-center justify-between whitespace-nowrap rounded-sm border border-input px-4 py-2 text-[1.3rem] text-label ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1", className, )} {...props} diff --git a/packages/common/components/src/ui/table/index.tsx b/packages/common/components/src/ui/table/index.tsx index e96ea7e4d..34505c898 100644 --- a/packages/common/components/src/ui/table/index.tsx +++ b/packages/common/components/src/ui/table/index.tsx @@ -63,7 +63,7 @@ const TableRow = React.forwardRef<
(({ className, ...props }, ref) => (
)); diff --git a/packages/common/components/src/ui/table/table.css b/packages/common/components/src/ui/table/table.css index 7736b04f0..53e7ea616 100644 --- a/packages/common/components/src/ui/table/table.css +++ b/packages/common/components/src/ui/table/table.css @@ -1,22 +1,38 @@ -th { - @apply bg-[#5987bf] px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2] text-[#fff]; -} -td { - @apply border-b border-b-[#e0e6ea] px-2 py-[1.2rem] text-[1.3rem] font-normal leading-[1.2] text-[#666]; -} +@tailwind components; -.secondary th { - @apply border border-[#e0e6ea] bg-[#eaf3fb] text-[#647784]; -} +@layer components { + th { + @apply bg-[#5987bf] px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2] text-[#fff]; + } + td { + @apply border-b border-b-border px-2 py-[1.2rem] text-[1.3rem] font-normal leading-[1.2] text-[#666]; + } -.secondary td { - @apply border border-[#e0e6ea]; -} + .secondary th { + @apply border border-border bg-[#eaf3fb] text-[#647784]; + } -.line th { - @apply border-0 border-b-2 border-t border-b-[#d4e0ea] border-t-[#d4e0ea] bg-[#fff] text-[#555]; -} + .secondary td { + @apply border border-border; + } -.line td { - @apply border-b border-b-[#e0e6ea]; + .line th { + @apply border-0 border-b-2 border-t border-b-[#d4e0ea] border-t-[#d4e0ea] bg-[#fff] text-[#555]; + } + + .line td { + @apply border-b border-b-border; + } + + [data-mode="dark"] { + th { + @apply bg-[#5e7b9c] text-[#fff]; + } + .secondary th { + @apply bg-[#1a1c1d]; + } + .line th { + @apply bg-[#0a0a0a]; + } + } } diff --git a/tailwind.config.preset.ts b/tailwind.config.preset.ts index ee341683e..9eb83e4d7 100644 --- a/tailwind.config.preset.ts +++ b/tailwind.config.preset.ts @@ -50,6 +50,10 @@ export default { 4: "var(--chart-4)", 5: "var(--chart-5)", }, + field: "var(--field)", + form: "var(--form)", + container: "var(--container)", + title: "var(--title)", }, keyframes: { "accordion-down": { -- 2.47.1.windows.1 From 64a615258da071b85b1a50f59dc4d3d662302ffa Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 13 Mar 2025 13:28:33 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D/=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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/site/register/page.tsx | 2 + apps/cs/src/app/styles/components.css | 11 +- apps/cs/src/entities/code/ui/code-select.tsx | 2 +- .../site/ui/domain-management-dialog.tsx | 212 ++++++++++++++++++ .../pages/site/ui/ip-management-dialog.tsx | 21 +- apps/cs/src/pages/site/ui/layout.tsx | 4 +- apps/cs/src/pages/site/ui/register-page.tsx | 106 ++++++--- 7 files changed, 323 insertions(+), 35 deletions(-) create mode 100644 apps/cs/app/site/register/page.tsx create mode 100644 apps/cs/src/pages/site/ui/domain-management-dialog.tsx diff --git a/apps/cs/app/site/register/page.tsx b/apps/cs/app/site/register/page.tsx new file mode 100644 index 000000000..a11d0b60c --- /dev/null +++ b/apps/cs/app/site/register/page.tsx @@ -0,0 +1,2 @@ +import { RegisterPage } from "@/pages/site"; +export default RegisterPage; diff --git a/apps/cs/src/app/styles/components.css b/apps/cs/src/app/styles/components.css index 4d5caaf96..ec137cf25 100644 --- a/apps/cs/src/app/styles/components.css +++ b/apps/cs/src/app/styles/components.css @@ -5,7 +5,7 @@ @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; } .card-border { - @apply shadow-blur border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)]; + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; } .card-header { @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; @@ -13,4 +13,13 @@ .card-footer { @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; } + + [data-mode="dark"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } } diff --git a/apps/cs/src/entities/code/ui/code-select.tsx b/apps/cs/src/entities/code/ui/code-select.tsx index 88c8a1368..5d73ee1d3 100644 --- a/apps/cs/src/entities/code/ui/code-select.tsx +++ b/apps/cs/src/entities/code/ui/code-select.tsx @@ -28,7 +28,7 @@ function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) { return ( + + + + + + + + + + NO + 선택 + 도메인URL + 대표URL + 사용여부 + + + + {rows.map((row, idx) => ( + + {idx + 1} + + toggleRowSelection(row.id)} + className="cursor-pointer" + /> + + + + handleDomainUrlChange(row.id, e.target.value) + } + className="!text-[1.3rem]" + /> + + + handleRepresentativeChange(row.id)} + className="cursor-pointer" + /> + + + + + + ))} + +
+ + + + + + + + + + + + ); +} + +export { DomainManagementDialog }; diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx index 9f637e3be..1390efd45 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -18,6 +18,8 @@ import { RadioGroup, RadioGroupItem, Checkbox, + DialogFooter, + DialogClose, } from "@common/components"; type IP = { @@ -34,7 +36,7 @@ function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) { const [rows, setRows] = useState([]); const [rowSelection, setRowSelection] = useState([]); - const handleSave = () => {}; + const onSave = () => {}; const handleAddIP = () => { const newIP: IP = { @@ -86,9 +88,6 @@ function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) { - @@ -102,7 +101,7 @@ function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) {
- +
@@ -160,6 +159,18 @@ function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) {
+ + + + + + + + ); diff --git a/apps/cs/src/pages/site/ui/layout.tsx b/apps/cs/src/pages/site/ui/layout.tsx index a5d85dee3..c9b12a1f1 100644 --- a/apps/cs/src/pages/site/ui/layout.tsx +++ b/apps/cs/src/pages/site/ui/layout.tsx @@ -3,8 +3,8 @@ import { BaseLayout } from "@common/components"; function SitePageLayout({ children }: { children: React.ReactNode }) { return ( -
-

+
+

사이트 관리

{children} diff --git a/apps/cs/src/pages/site/ui/register-page.tsx b/apps/cs/src/pages/site/ui/register-page.tsx index 0257176ab..48347e823 100644 --- a/apps/cs/src/pages/site/ui/register-page.tsx +++ b/apps/cs/src/pages/site/ui/register-page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useRouter } from "next/navigation"; import { SiteApi } from "@/entities/site"; import { ENDPOINT } from "@/shared/config/api"; import { @@ -19,25 +20,45 @@ import { useQuery } from "@tanstack/react-query"; import { CodeSelect } from "@/entities/code"; import { SitePageLayout } from "./layout"; import { IPManagementDialog } from "./ip-management-dialog"; +import { + DomainManagementDialog, + type Domain, +} from "./domain-management-dialog"; +import type { ISiteDetail } from "@/entities/site/model/site-interface"; + +type ViewState = Partial; function RegisterPage({ siteId }: { siteId: string }) { + const router = useRouter(); const { data: siteDetail } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId], queryFn: () => SiteApi.siteDetail(siteId), select: (data) => { - setSiteKndCd(data.siteKndCd); + handleViewStateChange("siteKndCd")(data.siteKndCd); + handleViewStateChange("siteSkn")(data.siteSkn); return data; }, enabled: !!siteId, }); - const [siteKndCd, setSiteKndCd] = useState(""); - const [, setSiteSkinCd] = useState(""); + + const [viewState, setViewState] = useState({}); + const [representativeDomain, setRepresentativeDomain] = useState(""); + + const handleViewStateChange = (key: keyof ISiteDetail) => (value: string) => { + setViewState({ ...viewState, [key]: value }); + }; + + const handleDomainSave = (rows: Domain[]) => { + setRepresentativeDomain( + rows.find((row) => row.isRepresentative)?.domainUrl || "", + ); + }; return (
-

사이트 등록/수정

+

사이트 등록/수정

@@ -53,6 +74,9 @@ function RegisterPage({ siteId }: { siteId: string }) { className="w-[12rem] !text-[1.3rem]" defaultValue={siteDetail?.siteId} disabled={!!siteId} + onChange={(e) => + handleViewStateChange("siteId")(e.target.value) + } /> @@ -63,11 +87,13 @@ function RegisterPage({ siteId }: { siteId: string }) { { - setSiteKndCd(value); - }} + onValueChange={handleViewStateChange("siteKndCd")} /> - + {siteId && ( + + )} @@ -77,9 +103,7 @@ function RegisterPage({ siteId }: { siteId: string }) { { - setSiteSkinCd(value); - }} + onValueChange={handleViewStateChange("siteSkn")} /> @@ -90,6 +114,9 @@ function RegisterPage({ siteId }: { siteId: string }) { className="!text-[1.3rem]" placeholder="사이트명을 입력하십시오." defaultValue={siteDetail?.siteNm} + onChange={(e) => + handleViewStateChange("siteNm")(e.target.value) + } /> @@ -100,9 +127,23 @@ function RegisterPage({ siteId }: { siteId: string }) { className="!text-[1.3rem]" placeholder="사이트 설명을 입력하십시오." defaultValue={siteDetail?.siteExpln} + onChange={(e) => + handleViewStateChange("siteExpln")(e.target.value) + } /> + + 사이트 도메인 + +
+
+ {representativeDomain} +
+ +
+
+
사이트 하단 표기 주소 @@ -110,6 +151,9 @@ function RegisterPage({ siteId }: { siteId: string }) { className="!text-[1.3rem]" placeholder="사이트 하단 주소를 입력하십시오." defaultValue={siteDetail?.siteAddr} + onChange={(e) => + handleViewStateChange("siteAddr")(e.target.value) + } /> @@ -120,6 +164,9 @@ function RegisterPage({ siteId }: { siteId: string }) { className="!text-[1.3rem]" placeholder="사이트 하단 전화번호를 입력하십시오." defaultValue={siteDetail?.telNo} + onChange={(e) => + handleViewStateChange("telNo")(e.target.value) + } /> @@ -130,6 +177,9 @@ function RegisterPage({ siteId }: { siteId: string }) { className="!text-[1.3rem]" placeholder="사이트 하단 팩스번호를 입력하십시오." defaultValue={siteDetail?.faxNumber} + onChange={(e) => + handleViewStateChange("faxNumber")(e.target.value) + } /> @@ -140,6 +190,9 @@ function RegisterPage({ siteId }: { siteId: string }) { className="!text-[1.3rem]" placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)" defaultValue={siteDetail?.lwndCn} + onChange={(e) => + handleViewStateChange("lwndCn")(e.target.value) + } /> @@ -150,6 +203,9 @@ function RegisterPage({ siteId }: { siteId: string }) { className="!text-[1.3rem]" placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오." defaultValue={siteDetail?.bkmkIcon} + onChange={(e) => + handleViewStateChange("bkmkIcon")(e.target.value) + } /> @@ -157,18 +213,17 @@ function RegisterPage({ siteId }: { siteId: string }) { 기본 사이트 여부 { - console.log(value); - }} + onValueChange={handleViewStateChange("basicSiteYn")} >
- - + +
- - + +
@@ -177,18 +232,17 @@ function RegisterPage({ siteId }: { siteId: string }) { 사용 여부 { - console.log(value); - }} + onValueChange={handleViewStateChange("useYn")} >
- - + +
- - + +
@@ -199,7 +253,7 @@ function RegisterPage({ siteId }: { siteId: string }) {
-
- - - - - - - 사이트 코드 - - - handleViewStateChange("siteId")(e.target.value) - } - /> - - - - 사이트 구분 - -
- - {siteId && ( - + +
+
+
+

+ 사이트 등록/수정 +

+
+
+
+ + + + + + + 사이트 코드 + + ( + + + + + + )} /> - )} - - - - - 사이트 디자인 - - - - - - 사이트명 - - - handleViewStateChange("siteNm")(e.target.value) - } - /> - - - - 사이트 설명 - - - handleViewStateChange("siteExpln")(e.target.value) - } - /> - - - - 사이트 도메인 - -
-
- {representativeDomain} -
- -
-
-
- - 사이트 하단 표기 주소 - - - handleViewStateChange("siteAddr")(e.target.value) - } - /> - - - - 사이트 하단 표기 전화번호 - - - handleViewStateChange("telNo")(e.target.value) - } - /> - - - - 사이트 하단 표기 팩스번호 - - - handleViewStateChange("faxNumber")(e.target.value) - } - /> - - - - 사이트 하단 표기 내용 - - - handleViewStateChange("lwndCn")(e.target.value) - } - /> - - - - 북마크 아이콘 - - - handleViewStateChange("bkmkIcon")(e.target.value) - } - /> - - - - 기본 사이트 여부 - - -
- - -
-
- - -
-
-
-
- - 사용 여부 - - -
- - -
-
- - -
-
-
-
-
-
-
-
-
- - - -
-
+ + + + 사이트 구분 + +
+ ( + + + { + handleViewStateChange("siteKndCd")(value); + field.onChange(value); + }} + /> + + + )} + /> + {isModifyMode && ( + + )} +
+
+
+ + 사이트 디자인 + + ( + + + + + + )} + /> + + + + 사이트명 + + ( + + + + + + )} + /> + + + + 사이트 설명 + + ( + + + + + + )} + /> + + + + 사이트 도메인 + +
+
+ {representativeDomain} +
+ +
+
+
+ + 사이트 하단 표기 주소 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 전화번호 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 팩스번호 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 내용 + + ( + + + + + + )} + /> + + + + 북마크 아이콘 + + ( + + + + + + )} + /> + + + + 기본 사이트 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ +

+ + +
+ + + +
+ +
+ ); } diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index 46e957985..4b095caff 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -7,8 +7,7 @@ import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { Button, Input, DataTable } from "@common/components"; -import { SiteApi } from "@/entities/site/api/site-service"; -import type { ISite, ISiteQuery } from "@/entities/site/model/site-interface"; +import { SiteApi, type ISite, type ISiteParams } from "@/entities/site"; import { ENDPOINT } from "@/shared/config/api"; import { columns } from "../model/table-columns"; import { SitePageLayout } from "./layout"; @@ -21,7 +20,7 @@ function SitePage() { enabled: false, }); - const [siteQuery, setSiteQuery] = useState({ + const [siteQuery, setSiteQuery] = useState({ siteNm: "", siteKndCd: "", useYn: "", diff --git a/apps/cs/src/shared/ui/form.tsx b/apps/cs/src/shared/ui/form.tsx new file mode 100644 index 000000000..73830edd1 --- /dev/null +++ b/apps/cs/src/shared/ui/form.tsx @@ -0,0 +1,177 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + FormProvider, + useFormContext, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form"; +import { cn, Label } from "@common/components"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +
-
- - - -
- - - - ); -} - -export { RegisterPage }; diff --git a/apps/cs/src/pages/site/ui/site-form.tsx b/apps/cs/src/pages/site/ui/site-form.tsx new file mode 100644 index 000000000..8bbeee156 --- /dev/null +++ b/apps/cs/src/pages/site/ui/site-form.tsx @@ -0,0 +1,386 @@ +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { type UseFormReturn } from "react-hook-form"; +import { + Button, + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { CodeSelect } from "@/entities/code"; +import { type ISiteDetail, type SiteFormData } from "@/entities/site"; +import { + FormItem, + FormControl, + FormField, + Form, + FormMessage, +} from "@/shared/ui/form"; +import { IPManagementDialog } from "./ip-management-dialog"; +import { + DomainManagementDialog, + type Domain, +} from "./domain-management-dialog"; + +type SiteFormProps = { + form: UseFormReturn; + siteId?: string; + handleSave: (data: SiteFormData) => void; +}; + +function SiteForm({ form, siteId, handleSave }: SiteFormProps) { + const router = useRouter(); + + const [viewState, setViewState] = useState>({}); + const [representativeDomain, setRepresentativeDomain] = useState(""); + + const isModifyMode = !!siteId; + + const handleViewStateChange = (key: keyof ISiteDetail) => (value: string) => { + setViewState({ ...viewState, [key]: value }); + }; + + const handleDomainSave = (rows: Domain[]) => { + setRepresentativeDomain( + rows.find((row) => row.isRepresentative)?.domainUrl || "", + ); + }; + + const onSubmit = (data: SiteFormData) => { + handleSave(data); + }; + + return ( +
+ +
+
+

+ 사이트 등록/수정 +

+
+
+ + + + + + + + 사이트 코드 + + ( + + + + + + + )} + /> + + + + 사이트 구분 + +
+ ( + + + { + handleViewStateChange("siteKndCd")(value); + field.onChange(value); + }} + /> + + + + )} + /> + {isModifyMode && ( + + )} +
+
+
+ + 사이트 디자인 + + ( + + + + + + + )} + /> + + + + 사이트명 + + ( + + + + + + + )} + /> + + + + 사이트 설명 + + ( + + + + + + + )} + /> + + + + 사이트 도메인 + +
+
+ {representativeDomain} +
+ +
+
+
+ + 사이트 하단 표기 주소 + + ( + + + + + + + )} + /> + + + + 사이트 하단 표기 전화번호 + + ( + + + + + + + )} + /> + + + + 사이트 하단 표기 팩스번호 + + ( + + + + + + + )} + /> + + + + 사이트 하단 표기 내용 + + ( + + + + + + + )} + /> + + + + 북마크 아이콘 + + ( + + + + + + + )} + /> + + + + 기본 사이트 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+
+
+
+
+
+
+ + + +
+
+ + ); +} + +export { SiteForm }; diff --git a/apps/cs/src/shared/ui/form.tsx b/apps/cs/src/shared/ui/form.tsx index 6d379038f..bd579d263 100644 --- a/apps/cs/src/shared/ui/form.tsx +++ b/apps/cs/src/shared/ui/form.tsx @@ -154,7 +154,7 @@ const FormMessage = React.forwardRef<

{body} -- 2.47.1.windows.1 From 5505838bc58c84daebf961426e8dc6b64ddad9df Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 14 Mar 2025 18:13:25 +0900 Subject: [PATCH 09/19] =?UTF-8?q?design:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=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/site/ui/domain-management-dialog.tsx | 4 ++-- apps/cs/src/pages/site/ui/ip-management-dialog.tsx | 2 +- packages/common/components/package.json | 1 + packages/common/components/src/ui/dialog/dialog.stories.tsx | 2 +- packages/common/components/src/ui/dialog/index.tsx | 4 ++++ packages/common/components/src/ui/layout/layout.stories.tsx | 2 +- .../common/components/src/ui/radio-group/radio-group.css | 2 +- packages/common/components/src/ui/select/select.stories.tsx | 2 +- packages/common/components/src/ui/tabs/tabs.stories.tsx | 6 +++--- pnpm-lock.yaml | 3 +++ 10 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx index 1faee4c0c..c675f92d9 100644 --- a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx @@ -107,7 +107,7 @@ function DomainManagementDialog({ - + 도메인 관리 @@ -176,7 +176,7 @@ function DomainManagementDialog({ handleActiveChange(row.id, value) } > - + diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx index f4b29c648..06d9638b3 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -78,7 +78,7 @@ function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) { - + IP 관리 diff --git a/packages/common/components/package.json b/packages/common/components/package.json index 2bfdb6ba6..7c04950f7 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -39,6 +39,7 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-visually-hidden": "^1.1.2", "@tanstack/react-query": "^5.67.1", "@tanstack/react-table": "^8.21.2", "@types/js-cookie": "^3.0.6", diff --git a/packages/common/components/src/ui/dialog/dialog.stories.tsx b/packages/common/components/src/ui/dialog/dialog.stories.tsx index 24c0cdbda..80d14cff2 100644 --- a/packages/common/components/src/ui/dialog/dialog.stories.tsx +++ b/packages/common/components/src/ui/dialog/dialog.stories.tsx @@ -26,7 +26,7 @@ export const Default: Story = { - + 다이얼로그 제목 diff --git a/packages/common/components/src/ui/dialog/index.tsx b/packages/common/components/src/ui/dialog/index.tsx index b2ba92d05..1b6ff30f9 100644 --- a/packages/common/components/src/ui/dialog/index.tsx +++ b/packages/common/components/src/ui/dialog/index.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { X } from "lucide-react"; import { cn } from "../../lib/utils"; @@ -41,6 +42,9 @@ const DialogContent = React.forwardRef< )} {...props} > + + Dialog + {children} diff --git a/packages/common/components/src/ui/layout/layout.stories.tsx b/packages/common/components/src/ui/layout/layout.stories.tsx index f8a1e999d..bd73f98d9 100644 --- a/packages/common/components/src/ui/layout/layout.stories.tsx +++ b/packages/common/components/src/ui/layout/layout.stories.tsx @@ -348,7 +348,7 @@ function IPManagementDialog() { - + IP 관리 diff --git a/packages/common/components/src/ui/radio-group/radio-group.css b/packages/common/components/src/ui/radio-group/radio-group.css index 2becbf2cf..b055b281a 100644 --- a/packages/common/components/src/ui/radio-group/radio-group.css +++ b/packages/common/components/src/ui/radio-group/radio-group.css @@ -5,7 +5,7 @@ @apply flex flex-row gap-4 data-[orientation="vertical"]:flex-col data-[orientation="vertical"]:gap-2; } button[role="radio"] { - @apply aspect-square size-[18px] rounded-full bg-[url(../../../public/assets/images/checkbox_off.png)] bg-contain text-primary ring-offset-background focus: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-[url(../../../public/assets/images/checkbox_on.png)]; + @apply aspect-square size-[1.8rem] rounded-full bg-[url(../../../public/assets/images/checkbox_off.png)] bg-contain text-primary ring-offset-background focus: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-[url(../../../public/assets/images/checkbox_on.png)]; } button[role="radio"] + label { @apply text-[1.3rem] font-medium text-label; diff --git a/packages/common/components/src/ui/select/select.stories.tsx b/packages/common/components/src/ui/select/select.stories.tsx index 41846d17d..bbcf03cfd 100644 --- a/packages/common/components/src/ui/select/select.stories.tsx +++ b/packages/common/components/src/ui/select/select.stories.tsx @@ -28,7 +28,7 @@ type Story = StoryObj; export const Default: Story = { render: () => ( - - - - )} - /> - - - - 사이트 구분 - -

+ <> +
+ +
+
+

+ 사이트 등록/수정 +

+
+
+ + + + + + + + 사이트 코드 + ( - { - handleViewStateChange("siteKndCd")(value); - field.onChange(value); - }} + className="w-[12rem] !text-[1.3rem]" + disabled={isModifyMode} /> )} /> - {isModifyMode && ( - - )} - - - - - 사이트 디자인 - - ( - - - - - - - )} - /> - - - - 사이트명 - - ( - - - - - - - )} - /> - - - - 사이트 설명 - - ( - - - - - - - )} - /> - - - {isModifyMode && ( + + - 사이트 도메인 + 사이트 구분
-
- {representativeDomain} -
- + ( + + + { + handleViewStateChange("siteKndCd")(value); + field.onChange(value); + }} + /> + + + + )} + /> + {isModifyMode && ( + + )}
- )} - - 사이트 하단 표기 주소 - - ( - - - - - - - )} - /> - - - - 사이트 하단 표기 전화번호 - - ( - - - - - - - )} - /> - - - - 사이트 하단 표기 팩스번호 - - ( - - - - - - - )} - /> - - - - 사이트 하단 표기 내용 - - ( - - - - - - - )} - /> - - - - 북마크 아이콘 - - ( - - - - - - - )} - /> - - - - 기본 사이트 여부 - - ( - - - -
- - -
-
- - -
-
-
- -
- )} - /> -
-
- - 사용 여부 - - ( - - - -
- - -
-
- - -
-
-
- -
- )} - /> -
-
-
-
+ + 사이트 디자인 + + ( + + + + + + + )} + /> + + + + 사이트명 + + ( + + + + + + + )} + /> + + + + 사이트 설명 + + ( + + + + + + + )} + /> + + + {isModifyMode && ( + + 사이트 도메인 + +
+
+ {representativeDomain} +
+ +
+
+
+ )} + + 사이트 하단 표기 주소 + + ( + + + + + + + )} + /> + + + + 사이트 하단 표기 전화번호 + + ( + + + + + + + )} + /> + + + + 사이트 하단 표기 팩스번호 + + ( + + + + + + + )} + /> + + + + 사이트 하단 표기 내용 + + ( + + + + + + + )} + /> + + + + 북마크 아이콘 + + ( + + + + + + + )} + /> + + + + 기본 사이트 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+
+ + +
-
-
- - - {isModifyMode && ( - - )} -
- - + + {isModifyMode && ( + + )} + + + + + +
+

{message}

+
+ + + + + + + + +
+
+ ); } diff --git a/apps/cs/src/shared/provider/app-provider.tsx b/apps/cs/src/shared/provider/app-provider.tsx index 6c1d45410..49e9ead2a 100644 --- a/apps/cs/src/shared/provider/app-provider.tsx +++ b/apps/cs/src/shared/provider/app-provider.tsx @@ -1,5 +1,6 @@ "use client"; +import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; import { BaseLayout } from "@common/components/ui"; import { AuthProvider } from "./auth-provider"; import { CookieProvider } from "./cookie-provider"; @@ -10,7 +11,9 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { - {children} + + {children} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5fe9f771..58b9fe203 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: '@common/components': specifier: workspace:* version: link:../../packages/common/components + '@frontend-opensource/use-react-hooks': + specifier: ^1.1.4 + version: 1.1.4 '@hookform/resolvers': specifier: ^4.1.3 version: 4.1.3(react-hook-form@7.54.2) @@ -1140,6 +1143,15 @@ packages: resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} dev: false + /@frontend-opensource/use-react-hooks@1.1.4: + resolution: {integrity: sha512-otCSyciL5MVCNNYmdDdg2lEL6rbZ3XX1meYQCzapUSC4V6qx2YxMk1kUbhwEM/+vdxhYzBHsVBGD0rZWl5cgrw==} + dependencies: + async-wave: 1.8.6 + fast-deep-equal: 3.1.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@hookform/resolvers@4.1.3(react-hook-form@7.54.2): resolution: {integrity: sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==} peerDependencies: @@ -3703,6 +3715,17 @@ packages: engines: {node: '>= 0.4'} dev: true + /async-wave@1.8.6: + resolution: {integrity: sha512-wqWvr0Mrc1TrY92hN9MV0Xebxa6kt+T7QtjZ7FEMv+RUBa/Hfpz/UOgz0rB+7uqVK8TuBSXykOyU/k9gogdLkw==} + dependencies: + is-promise: 4.0.0 + lodash.add: 3.7.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.size: 4.2.0 + dev: false + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false @@ -5476,6 +5499,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + /is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + dev: false + /is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -5724,10 +5751,31 @@ packages: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} dev: true + /lodash.add@3.7.0: + resolution: {integrity: sha512-/uxUZkB6vYiPoGSXADJJE+Ee4W2vU+bxZOFdvJdx+mDgv+qyAO76sWD84Q4FxsMgTjC1+gVFC9YNhdG243QRDw==} + dev: false + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: false + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + dev: false + + /lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.size@4.2.0: + resolution: {integrity: sha512-wbu3SF1XC5ijqm0piNxw59yCbuUf2kaShumYBLWUrcCvwh6C8odz6SY/wGVzCWTQTFL/1Ygbvqg2eLtspUVVAQ==} + dev: false + /lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: true -- 2.47.1.windows.1 From 594dbd9fc77c46fa29900695359d3421c787642b Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 17 Mar 2025 10:00:41 +0900 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20base=20layout=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/app/(with-base-layout)/layout.tsx | 11 +++++++++++ apps/cs/app/{ => (with-base-layout)}/site/page.tsx | 0 .../site/register/[id]/page.tsx | 0 .../{ => (with-base-layout)}/site/register/page.tsx | 0 apps/cs/src/pages/auth/login/ui/login-page.tsx | 2 ++ apps/cs/src/shared/provider/app-provider.tsx | 5 +---- 6 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/layout.tsx rename apps/cs/app/{ => (with-base-layout)}/site/page.tsx (100%) rename apps/cs/app/{ => (with-base-layout)}/site/register/[id]/page.tsx (100%) rename apps/cs/app/{ => (with-base-layout)}/site/register/page.tsx (100%) diff --git a/apps/cs/app/(with-base-layout)/layout.tsx b/apps/cs/app/(with-base-layout)/layout.tsx new file mode 100644 index 000000000..cbcaee847 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/layout.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { BaseLayout } from "@common/components/ui"; + +export default function WithBaseLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/apps/cs/app/site/page.tsx b/apps/cs/app/(with-base-layout)/site/page.tsx similarity index 100% rename from apps/cs/app/site/page.tsx rename to apps/cs/app/(with-base-layout)/site/page.tsx diff --git a/apps/cs/app/site/register/[id]/page.tsx b/apps/cs/app/(with-base-layout)/site/register/[id]/page.tsx similarity index 100% rename from apps/cs/app/site/register/[id]/page.tsx rename to apps/cs/app/(with-base-layout)/site/register/[id]/page.tsx diff --git a/apps/cs/app/site/register/page.tsx b/apps/cs/app/(with-base-layout)/site/register/page.tsx similarity index 100% rename from apps/cs/app/site/register/page.tsx rename to apps/cs/app/(with-base-layout)/site/register/page.tsx diff --git a/apps/cs/src/pages/auth/login/ui/login-page.tsx b/apps/cs/src/pages/auth/login/ui/login-page.tsx index 39a8b7411..c8eb1e2f9 100644 --- a/apps/cs/src/pages/auth/login/ui/login-page.tsx +++ b/apps/cs/src/pages/auth/login/ui/login-page.tsx @@ -1,3 +1,5 @@ +"use client"; + import Image from "next/image"; import { images } from "@common/assets"; import { LoginApi } from "@/entities/auth/login"; diff --git a/apps/cs/src/shared/provider/app-provider.tsx b/apps/cs/src/shared/provider/app-provider.tsx index 49e9ead2a..175cc6aaa 100644 --- a/apps/cs/src/shared/provider/app-provider.tsx +++ b/apps/cs/src/shared/provider/app-provider.tsx @@ -1,7 +1,6 @@ "use client"; import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; -import { BaseLayout } from "@common/components/ui"; import { AuthProvider } from "./auth-provider"; import { CookieProvider } from "./cookie-provider"; import { QueryProvider } from "./query-provider"; @@ -11,9 +10,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { - - {children} - + {children} -- 2.47.1.windows.1 From 6e3b9aaa37510a519305e3480f25b04359280e8b Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 17 Mar 2025 10:08:09 +0900 Subject: [PATCH 13/19] =?UTF-8?q?refactor:=20rewrites=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EB=AF=B8=EB=93=A4=EC=9B=A8=EC=96=B4=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/middleware.ts | 19 +++++++++++++++++++ apps/cs/next.config.ts | 11 ----------- 2 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 apps/cs/middleware.ts diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts new file mode 100644 index 000000000..e7b03dddc --- /dev/null +++ b/apps/cs/middleware.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from "next/server"; + +const serverApiUrl = + process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000"; + +export function middleware(req: NextRequest) { + const pathname = req.nextUrl.pathname; + if (pathname.startsWith("/server")) { + return NextResponse.rewrite( + `${serverApiUrl}${pathname.replace("/server", "")}`, + ); + } + + return NextResponse.next(); +} + +export const config = { + matcher: "/server/:path*", +}; diff --git a/apps/cs/next.config.ts b/apps/cs/next.config.ts index fd2a3a156..5e3560133 100644 --- a/apps/cs/next.config.ts +++ b/apps/cs/next.config.ts @@ -1,9 +1,6 @@ import type { NextConfig } from "next"; import webpack from "webpack"; -const serverApiUrl = - process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3000"; - const nextConfig: NextConfig = { webpack: (config) => { config.plugins.push( @@ -36,14 +33,6 @@ const nextConfig: NextConfig = { return config; }, - async rewrites() { - return [ - { - source: "/server/:path*", - destination: `${serverApiUrl}/:path*`, - }, - ]; - }, }; export default nextConfig; -- 2.47.1.windows.1 From 05f3e48ed690a6583bf31bfd2956c7fdac5e0fe0 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 17 Mar 2025 11:03:57 +0900 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20view=20only=20layout=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/components/src/api/index.ts | 1 + packages/common/components/src/api/user.ts | 12 ++++---- .../common/components/src/ui/header/index.tsx | 18 +++++++----- .../common/components/src/ui/layout/index.ts | 1 + .../common/components/src/ui/layout/vbase.tsx | 28 +++++++++++++++++++ 5 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 packages/common/components/src/ui/layout/vbase.tsx diff --git a/packages/common/components/src/api/index.ts b/packages/common/components/src/api/index.ts index 7616f9e53..6674d19fa 100644 --- a/packages/common/components/src/api/index.ts +++ b/packages/common/components/src/api/index.ts @@ -1 +1,2 @@ +export * from "./client"; export * from "./user"; diff --git a/packages/common/components/src/api/user.ts b/packages/common/components/src/api/user.ts index 478143e09..36e39461d 100644 --- a/packages/common/components/src/api/user.ts +++ b/packages/common/components/src/api/user.ts @@ -1,4 +1,3 @@ -import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "../config"; import { client } from "./client"; @@ -36,10 +35,9 @@ export interface IUser { hasPassword: boolean; } -export const useUsersQuery = ({ userUniqId }: { userUniqId: string }) => { - return useQuery({ - queryKey: [ENDPOINT.USER_SERVICE.USERS], - queryFn: () => - client.get(`${ENDPOINT.USER_SERVICE.USERS}/${userUniqId}`), - }); +export const getUserInfo = async (userUniqId: string) => { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/${userUniqId}`, + ); + return response.data; }; diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 5baceda02..55bca0be6 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -1,6 +1,7 @@ +import { useQuery } from "@tanstack/react-query"; import { images } from "@common/assets"; -import { useUsersQuery } from "../../api"; -import { AUTH_USER_ID } from "../../config"; +import { getUserInfo } from "../../api"; +import { AUTH_USER_ID, ENDPOINT } from "../../config"; import { getToken } from "../../lib"; import { DropdownMenu, @@ -10,9 +11,12 @@ import { } from "../dropdown-menu"; import { Button } from "../button"; -export function Header() { - const { data: userInfo } = useUsersQuery({ - userUniqId: getToken(AUTH_USER_ID) ?? "", +export function Header({ viewOnly }: { viewOnly?: boolean }) { + const userUniqId = getToken(AUTH_USER_ID) ?? ""; + const { data: userInfo } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS], + queryFn: () => getUserInfo(userUniqId), + enabled: !viewOnly, }); return ( @@ -32,7 +36,7 @@ export function Header() { @@ -40,7 +44,7 @@ export function Header() { 프로필 설정

최종접속일

-

{userInfo?.data.lastLoginDate}

+

{userInfo?.lastLoginDate}

접속 IP

diff --git a/packages/common/components/src/ui/layout/index.ts b/packages/common/components/src/ui/layout/index.ts index 96148b44a..30c069b90 100644 --- a/packages/common/components/src/ui/layout/index.ts +++ b/packages/common/components/src/ui/layout/index.ts @@ -1 +1,2 @@ export { BaseLayout } from "./base"; +export { VBaseLayout } from "./vbase"; diff --git a/packages/common/components/src/ui/layout/vbase.tsx b/packages/common/components/src/ui/layout/vbase.tsx new file mode 100644 index 000000000..5a4ca8df4 --- /dev/null +++ b/packages/common/components/src/ui/layout/vbase.tsx @@ -0,0 +1,28 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { LNB } from "./lnb"; +import { Header } from "../header"; +import { Footer } from "./footer"; + +import "./common.css"; + +const queryClient = new QueryClient(); + +export function VBaseLayout({ children }: { children: React.ReactNode }) { + return ( + +
+
+
+ +
+
+ {children} +
+
+
+
+
+
+ ); +} -- 2.47.1.windows.1 From babf7055b465e97c82e5bac54d2f4b03604eefdb Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 17 Mar 2025 11:09:58 +0900 Subject: [PATCH 15/19] =?UTF-8?q?chore:=20env=20=ED=8C=8C=EC=9D=BC=20git?= =?UTF-8?q?=EC=97=90=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 --- apps/cs/.env.local | 2 ++ apps/cs/.gitignore | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) create mode 100644 apps/cs/.env.local diff --git a/.gitignore b/.gitignore index bfdd87b0a..29f6a6643 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,3 @@ node_modules # misc .DS_Store - -# env -.env* \ No newline at end of file diff --git a/apps/cs/.env.local b/apps/cs/.env.local new file mode 100644 index 000000000..0648024ff --- /dev/null +++ b/apps/cs/.env.local @@ -0,0 +1,2 @@ +NEXT_PUBLIC_PORT=3000 +NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000 diff --git a/apps/cs/.gitignore b/apps/cs/.gitignore index 5ef6a5207..5340dd9a1 100644 --- a/apps/cs/.gitignore +++ b/apps/cs/.gitignore @@ -30,9 +30,6 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* - # vercel .vercel -- 2.47.1.windows.1 From 39bf03c4f5f72847aae4b55ac4f533adc744e7f3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 17 Mar 2025 13:27:19 +0900 Subject: [PATCH 16/19] =?UTF-8?q?refactor:=20provider=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EB=B3=80=EA=B2=BD=20&=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20public=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/app/api/proxy/[...url]/route.ts | 2 +- apps/cs/next.config.ts | 1 - apps/cs/src/app/index.tsx | 2 +- .../providers}/app-provider.tsx | 4 +- apps/cs/src/app/providers/index.ts | 1 + .../providers}/query-provider.tsx | 0 .../auth/{login => }/api/login-service.ts | 9 +++- apps/cs/src/entities/auth/index.ts | 2 + .../auth/lib}/auth-provider.tsx | 19 +++------ .../auth/{login => }/lib/token-manager.ts | 4 +- apps/cs/src/entities/auth/login/index.ts | 1 - .../auth/{login => }/model/login-interface.ts | 0 apps/cs/src/entities/code/api/code-service.ts | 4 +- apps/cs/src/entities/code/ui/code-select.tsx | 2 +- apps/cs/src/entities/site/index.ts | 2 - apps/cs/src/entities/user/api/user-service.ts | 15 ------- apps/cs/src/entities/user/index.ts | 1 - apps/cs/src/entities/user/lib/use-user.ts | 20 --------- .../src/entities/user/model/user-interface.ts | 41 ------------------- .../cs/src/pages/auth/login/ui/login-page.tsx | 2 +- .../site/api/site-service.ts | 4 +- .../site/model/site-interface.ts | 0 .../cs/src/pages/site/model/table-columns.tsx | 2 +- apps/cs/src/pages/site/ui/create-page.tsx | 3 +- apps/cs/src/pages/site/ui/modify-page.tsx | 6 +-- apps/cs/src/pages/site/ui/site-form.tsx | 6 +-- apps/cs/src/pages/site/ui/site-page.tsx | 5 ++- apps/cs/src/shared/api/index.ts | 1 + apps/cs/src/shared/config/index.ts | 2 + .../{provider => lib}/cookie-provider.tsx | 12 +----- apps/cs/src/shared/lib/index.ts | 2 + apps/cs/src/shared/provider/index.ts | 3 -- 32 files changed, 46 insertions(+), 132 deletions(-) rename apps/cs/src/{shared/provider => app/providers}/app-provider.tsx (81%) create mode 100644 apps/cs/src/app/providers/index.ts rename apps/cs/src/{shared/provider => app/providers}/query-provider.tsx (100%) rename apps/cs/src/entities/auth/{login => }/api/login-service.ts (94%) create mode 100644 apps/cs/src/entities/auth/index.ts rename apps/cs/src/{shared/provider => entities/auth/lib}/auth-provider.tsx (80%) rename apps/cs/src/entities/auth/{login => }/lib/token-manager.ts (94%) delete mode 100644 apps/cs/src/entities/auth/login/index.ts rename apps/cs/src/entities/auth/{login => }/model/login-interface.ts (100%) delete mode 100644 apps/cs/src/entities/site/index.ts delete mode 100644 apps/cs/src/entities/user/api/user-service.ts delete mode 100644 apps/cs/src/entities/user/index.ts delete mode 100644 apps/cs/src/entities/user/lib/use-user.ts delete mode 100644 apps/cs/src/entities/user/model/user-interface.ts rename apps/cs/src/{entities => pages}/site/api/site-service.ts (93%) rename apps/cs/src/{entities => pages}/site/model/site-interface.ts (100%) create mode 100644 apps/cs/src/shared/api/index.ts create mode 100644 apps/cs/src/shared/config/index.ts rename apps/cs/src/shared/{provider => lib}/cookie-provider.tsx (67%) create mode 100644 apps/cs/src/shared/lib/index.ts delete mode 100644 apps/cs/src/shared/provider/index.ts diff --git a/apps/cs/app/api/proxy/[...url]/route.ts b/apps/cs/app/api/proxy/[...url]/route.ts index 5397e5fe6..d3092c5c1 100644 --- a/apps/cs/app/api/proxy/[...url]/route.ts +++ b/apps/cs/app/api/proxy/[...url]/route.ts @@ -5,7 +5,7 @@ import { REFRESH_TOKEN, SERVER_API_URL, SITE_ID, -} from "@/shared/config/env"; +} from "@/shared/config"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; diff --git a/apps/cs/next.config.ts b/apps/cs/next.config.ts index 5e3560133..7c1a12675 100644 --- a/apps/cs/next.config.ts +++ b/apps/cs/next.config.ts @@ -27,7 +27,6 @@ const nextConfig: NextConfig = { "import.meta.env.VITE_AUTH_USER_ID": JSON.stringify( process.env.NEXT_PUBLIC_AUTH_USER_ID, ), - "process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID), }), ); diff --git a/apps/cs/src/app/index.tsx b/apps/cs/src/app/index.tsx index eb1d7f7c7..d1d5e56e1 100644 --- a/apps/cs/src/app/index.tsx +++ b/apps/cs/src/app/index.tsx @@ -1,6 +1,6 @@ import "./styles/base.css"; import "./styles/components.css"; -import { AppProvider } from "@/shared/provider"; +import { AppProvider } from "./providers"; const App = ({ children }: Readonly<{ children: React.ReactNode }>) => { return ( diff --git a/apps/cs/src/shared/provider/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx similarity index 81% rename from apps/cs/src/shared/provider/app-provider.tsx rename to apps/cs/src/app/providers/app-provider.tsx index 175cc6aaa..7bd2ec683 100644 --- a/apps/cs/src/shared/provider/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -1,8 +1,8 @@ "use client"; import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; -import { AuthProvider } from "./auth-provider"; -import { CookieProvider } from "./cookie-provider"; +import { AuthProvider } from "@/entities/auth"; +import { CookieProvider } from "@/shared/lib"; import { QueryProvider } from "./query-provider"; const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { diff --git a/apps/cs/src/app/providers/index.ts b/apps/cs/src/app/providers/index.ts new file mode 100644 index 000000000..15f99b7fd --- /dev/null +++ b/apps/cs/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProvider } from "./app-provider"; diff --git a/apps/cs/src/shared/provider/query-provider.tsx b/apps/cs/src/app/providers/query-provider.tsx similarity index 100% rename from apps/cs/src/shared/provider/query-provider.tsx rename to apps/cs/src/app/providers/query-provider.tsx diff --git a/apps/cs/src/entities/auth/login/api/login-service.ts b/apps/cs/src/entities/auth/api/login-service.ts similarity index 94% rename from apps/cs/src/entities/auth/login/api/login-service.ts rename to apps/cs/src/entities/auth/api/login-service.ts index 8c14547a0..c1968fe36 100644 --- a/apps/cs/src/entities/auth/login/api/login-service.ts +++ b/apps/cs/src/entities/auth/api/login-service.ts @@ -1,6 +1,11 @@ import { AxiosError } from "axios"; -import { ENDPOINT, JWT_EXPIRATION_TIME } from "@/shared/config/api"; -import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env"; +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"; diff --git a/apps/cs/src/entities/auth/index.ts b/apps/cs/src/entities/auth/index.ts new file mode 100644 index 000000000..97579d0a5 --- /dev/null +++ b/apps/cs/src/entities/auth/index.ts @@ -0,0 +1,2 @@ +export { LoginApi } from "./api/login-service"; +export { AuthProvider } from "./lib/auth-provider"; diff --git a/apps/cs/src/shared/provider/auth-provider.tsx b/apps/cs/src/entities/auth/lib/auth-provider.tsx similarity index 80% rename from apps/cs/src/shared/provider/auth-provider.tsx rename to apps/cs/src/entities/auth/lib/auth-provider.tsx index 60b273641..551cfa79d 100644 --- a/apps/cs/src/shared/provider/auth-provider.tsx +++ b/apps/cs/src/entities/auth/lib/auth-provider.tsx @@ -2,12 +2,11 @@ import { useRouter } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; -import { createContext, useContext, useState } from "react"; -import { client } from "../api/client"; -import { ENDPOINT } from "../config/api"; -import { AUTH_USER_ID } from "../config/env"; -import { getCookie } from "../lib/cookie"; -import { LoginApi } from "@/entities/auth/login"; +import { createContext, useState } from "react"; +import { client } from "@/shared/api"; +import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; +import { getCookie } from "@/shared/lib"; +import { LoginApi } from "../api/login-service"; interface IUserInfo { regDt: string; @@ -91,11 +90,3 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { ); }; - -export const useAuth = (): AuthContextType => { - const context = useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; -}; diff --git a/apps/cs/src/entities/auth/login/lib/token-manager.ts b/apps/cs/src/entities/auth/lib/token-manager.ts similarity index 94% rename from apps/cs/src/entities/auth/login/lib/token-manager.ts rename to apps/cs/src/entities/auth/lib/token-manager.ts index 4429c7d1b..0e761486a 100644 --- a/apps/cs/src/entities/auth/login/lib/token-manager.ts +++ b/apps/cs/src/entities/auth/lib/token-manager.ts @@ -1,5 +1,5 @@ -import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env"; -import { getCookie, setCookie, removeCookie } from "@/shared/lib/cookie"; +import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config"; +import { getCookie, setCookie, removeCookie } from "@/shared/lib"; export class TokenManager { private static instance: TokenManager; diff --git a/apps/cs/src/entities/auth/login/index.ts b/apps/cs/src/entities/auth/login/index.ts deleted file mode 100644 index 8c7981ac9..000000000 --- a/apps/cs/src/entities/auth/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginApi } from "./api/login-service"; diff --git a/apps/cs/src/entities/auth/login/model/login-interface.ts b/apps/cs/src/entities/auth/model/login-interface.ts similarity index 100% rename from apps/cs/src/entities/auth/login/model/login-interface.ts rename to apps/cs/src/entities/auth/model/login-interface.ts diff --git a/apps/cs/src/entities/code/api/code-service.ts b/apps/cs/src/entities/code/api/code-service.ts index 3a789f2aa..0d1055f6a 100644 --- a/apps/cs/src/entities/code/api/code-service.ts +++ b/apps/cs/src/entities/code/api/code-service.ts @@ -1,5 +1,5 @@ -import { client } from "@/shared/api/client"; -import { ENDPOINT } from "@/shared/config/api"; +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; import type { CodeService, ICode } from "../model/code-interface"; export class CodeApiService implements CodeService { diff --git a/apps/cs/src/entities/code/ui/code-select.tsx b/apps/cs/src/entities/code/ui/code-select.tsx index 5d73ee1d3..00b3157aa 100644 --- a/apps/cs/src/entities/code/ui/code-select.tsx +++ b/apps/cs/src/entities/code/ui/code-select.tsx @@ -6,7 +6,7 @@ import { SelectTrigger, SelectValue, } from "@common/components"; -import { ENDPOINT } from "@/shared/config/api"; +import { ENDPOINT } from "@/shared/config"; import { CodeApi } from "../api/code-service"; type CodeSelectProps = { diff --git a/apps/cs/src/entities/site/index.ts b/apps/cs/src/entities/site/index.ts deleted file mode 100644 index 47f9d7b50..000000000 --- a/apps/cs/src/entities/site/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SiteApi } from "./api/site-service"; -export * from "./model/site-interface"; diff --git a/apps/cs/src/entities/user/api/user-service.ts b/apps/cs/src/entities/user/api/user-service.ts deleted file mode 100644 index 41319bc1f..000000000 --- a/apps/cs/src/entities/user/api/user-service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { client } from "@/shared/api/client"; -import { ENDPOINT } from "@/shared/config/api"; -import type { IUser, IUserInfo, UserService } from "../model/user-interface"; - -export class UsersApiService implements UserService { - public async userInfo(payload: IUser): Promise { - const response = await client.get( - `${ENDPOINT.USER_SERVICE.USERS}/${payload.userUniqId}`, - ); - - return response.data; - } -} - -export const UserApi = new UsersApiService(); diff --git a/apps/cs/src/entities/user/index.ts b/apps/cs/src/entities/user/index.ts deleted file mode 100644 index 3076aa261..000000000 --- a/apps/cs/src/entities/user/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useUser } from "./lib/use-user"; diff --git a/apps/cs/src/entities/user/lib/use-user.ts b/apps/cs/src/entities/user/lib/use-user.ts deleted file mode 100644 index 97cd2f034..000000000 --- a/apps/cs/src/entities/user/lib/use-user.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { ENDPOINT } from "@/shared/config/api"; -import { AUTH_USER_ID } from "@/shared/config/env"; -import { useCookie } from "@/shared/provider"; -import { UserApi } from "../api/user-service"; - -const useUser = () => { - const { getCookie } = useCookie(); - const { data } = useQuery({ - queryKey: [ENDPOINT.USER_SERVICE.USERS], - queryFn: () => - UserApi.userInfo({ - userUniqId: getCookie(AUTH_USER_ID) ?? "", - }), - }); - - return { userInfo: data }; -}; - -export { useUser }; diff --git a/apps/cs/src/entities/user/model/user-interface.ts b/apps/cs/src/entities/user/model/user-interface.ts deleted file mode 100644 index cba4a6206..000000000 --- a/apps/cs/src/entities/user/model/user-interface.ts +++ /dev/null @@ -1,41 +0,0 @@ -export interface IUser { - userUniqId: string; -} - -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 interface UserService { - userInfo: (payload: IUser) => Promise; -} diff --git a/apps/cs/src/pages/auth/login/ui/login-page.tsx b/apps/cs/src/pages/auth/login/ui/login-page.tsx index c8eb1e2f9..d5e12966a 100644 --- a/apps/cs/src/pages/auth/login/ui/login-page.tsx +++ b/apps/cs/src/pages/auth/login/ui/login-page.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { images } from "@common/assets"; -import { LoginApi } from "@/entities/auth/login"; +import { LoginApi } from "@/entities/auth"; import "./login.css"; function LoginPage() { diff --git a/apps/cs/src/entities/site/api/site-service.ts b/apps/cs/src/pages/site/api/site-service.ts similarity index 93% rename from apps/cs/src/entities/site/api/site-service.ts rename to apps/cs/src/pages/site/api/site-service.ts index 1c9d34361..22363de68 100644 --- a/apps/cs/src/entities/site/api/site-service.ts +++ b/apps/cs/src/pages/site/api/site-service.ts @@ -1,5 +1,5 @@ -import { client } from "@/shared/api/client"; -import { ENDPOINT } from "@/shared/config/api"; +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; import { siteFormSchema, type ISiteInfo, diff --git a/apps/cs/src/entities/site/model/site-interface.ts b/apps/cs/src/pages/site/model/site-interface.ts similarity index 100% rename from apps/cs/src/entities/site/model/site-interface.ts rename to apps/cs/src/pages/site/model/site-interface.ts diff --git a/apps/cs/src/pages/site/model/table-columns.tsx b/apps/cs/src/pages/site/model/table-columns.tsx index ddf211aea..35c084896 100644 --- a/apps/cs/src/pages/site/model/table-columns.tsx +++ b/apps/cs/src/pages/site/model/table-columns.tsx @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import { createColumnHelper } from "@tanstack/react-table"; -import type { ISite } from "@/entities/site/model/site-interface"; +import type { ISite } from "./site-interface"; const columnHelper = createColumnHelper(); diff --git a/apps/cs/src/pages/site/ui/create-page.tsx b/apps/cs/src/pages/site/ui/create-page.tsx index 10d2d9030..a79c16022 100644 --- a/apps/cs/src/pages/site/ui/create-page.tsx +++ b/apps/cs/src/pages/site/ui/create-page.tsx @@ -5,7 +5,8 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { useMutation } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; -import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site"; +import { SiteApi } from "../api/site-service"; +import { siteFormSchema, type SiteFormData } from "../model/site-interface"; import { SitePageLayout } from "./layout"; import { SiteForm } from "./site-form"; import { diff --git a/apps/cs/src/pages/site/ui/modify-page.tsx b/apps/cs/src/pages/site/ui/modify-page.tsx index 982f12472..8c3bbd093 100644 --- a/apps/cs/src/pages/site/ui/modify-page.tsx +++ b/apps/cs/src/pages/site/ui/modify-page.tsx @@ -5,13 +5,13 @@ import { useRouter } from "next/navigation"; 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 { SiteApi } from "../api/site-service"; import { - SiteApi, siteFormSchema, type ISiteDetail, type SiteFormData, -} from "@/entities/site"; -import { ENDPOINT } from "@/shared/config/api"; +} from "../model/site-interface"; import { SitePageLayout } from "./layout"; import { SiteForm } from "./site-form"; import { diff --git a/apps/cs/src/pages/site/ui/site-form.tsx b/apps/cs/src/pages/site/ui/site-form.tsx index 2178befca..c554a868c 100644 --- a/apps/cs/src/pages/site/ui/site-form.tsx +++ b/apps/cs/src/pages/site/ui/site-form.tsx @@ -19,7 +19,6 @@ import { TableRow, } from "@common/components"; import { CodeSelect } from "@/entities/code"; -import { type ISiteDetail, type SiteFormData } from "@/entities/site"; import { FormItem, FormControl, @@ -27,6 +26,7 @@ import { Form, FormMessage, } from "@/shared/ui/form"; +import { type ISiteDetail, type SiteFormData } from "../model/site-interface"; import { IPManagementDialog } from "./ip-management-dialog"; import { DomainManagementDialog, @@ -37,7 +37,7 @@ type SiteFormProps = { form: UseFormReturn; siteId?: string; handleSave: (data: SiteFormData) => void; - handleDelete: () => void; + handleDelete?: () => void; }; function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { @@ -65,7 +65,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { const onDelete = async () => { if (await confirm("정말 삭제하시겠습니까?")) { - handleDelete(); + handleDelete?.(); } }; diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index 4b095caff..1e026c6ec 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -7,8 +7,9 @@ import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { Button, Input, DataTable } from "@common/components"; -import { SiteApi, type ISite, type ISiteParams } from "@/entities/site"; -import { ENDPOINT } from "@/shared/config/api"; +import { ENDPOINT } from "@/shared/config"; +import { SiteApi } from "../api/site-service"; +import { type ISite, type ISiteParams } from "../model/site-interface"; import { columns } from "../model/table-columns"; import { SitePageLayout } from "./layout"; diff --git a/apps/cs/src/shared/api/index.ts b/apps/cs/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/cs/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/cs/src/shared/config/index.ts b/apps/cs/src/shared/config/index.ts new file mode 100644 index 000000000..cd83447d5 --- /dev/null +++ b/apps/cs/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./env"; diff --git a/apps/cs/src/shared/provider/cookie-provider.tsx b/apps/cs/src/shared/lib/cookie-provider.tsx similarity index 67% rename from apps/cs/src/shared/provider/cookie-provider.tsx rename to apps/cs/src/shared/lib/cookie-provider.tsx index c50050cf5..f844d4f8a 100644 --- a/apps/cs/src/shared/provider/cookie-provider.tsx +++ b/apps/cs/src/shared/lib/cookie-provider.tsx @@ -1,7 +1,7 @@ "use client"; -import { createContext, useContext } from "react"; -import { getCookie, setCookie, removeCookie } from "../lib/cookie"; +import { createContext } from "react"; +import { getCookie, setCookie, removeCookie } from "./cookie"; export interface CookieContextType { getCookie: (name: string) => string | undefined; @@ -28,11 +28,3 @@ export function CookieProvider({ children }: { children: React.ReactNode }) { ); } - -export function useCookie(): CookieContextType { - const context = useContext(CookieContext); - if (!context) { - throw new Error("useCookie must be used within a CookieProvider"); - } - return context; -} diff --git a/apps/cs/src/shared/lib/index.ts b/apps/cs/src/shared/lib/index.ts new file mode 100644 index 000000000..0f4be9c9b --- /dev/null +++ b/apps/cs/src/shared/lib/index.ts @@ -0,0 +1,2 @@ +export { CookieProvider } from "./cookie-provider"; +export { getCookie, setCookie, removeCookie } from "./cookie"; diff --git a/apps/cs/src/shared/provider/index.ts b/apps/cs/src/shared/provider/index.ts deleted file mode 100644 index 6b1832501..000000000 --- a/apps/cs/src/shared/provider/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { AppProvider } from "./app-provider"; -export { useAuth } from "./auth-provider"; -export { useCookie } from "./cookie-provider"; -- 2.47.1.windows.1 From c81735b8b897b59b44786a7d59b5b2977e22a570 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 17 Mar 2025 14:50:18 +0900 Subject: [PATCH 17/19] =?UTF-8?q?feat:=20user=20store=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?(zustand)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/package.json | 3 +- apps/cs/src/app/providers/app-provider.tsx | 19 ++-- apps/cs/src/entities/auth/index.ts | 2 +- .../src/entities/auth/lib/auth-provider.tsx | 92 ------------------- apps/cs/src/entities/auth/ui/auth-wrapper.tsx | 46 ++++++++++ apps/cs/src/shared/lib/cookie-provider.tsx | 30 ------ apps/cs/src/shared/lib/index.ts | 2 +- apps/cs/src/shared/lib/use-user-store.ts | 14 +++ apps/cs/src/shared/providers/index.ts | 2 + .../providers/query-provider.tsx | 0 .../src/shared/providers/store-provider.tsx | 27 ++++++ apps/cs/src/shared/store/index.ts | 1 + apps/cs/src/shared/store/user-store.ts | 56 +++++++++++ pnpm-lock.yaml | 25 +++++ 14 files changed, 184 insertions(+), 135 deletions(-) delete mode 100644 apps/cs/src/entities/auth/lib/auth-provider.tsx create mode 100644 apps/cs/src/entities/auth/ui/auth-wrapper.tsx delete mode 100644 apps/cs/src/shared/lib/cookie-provider.tsx create mode 100644 apps/cs/src/shared/lib/use-user-store.ts create mode 100644 apps/cs/src/shared/providers/index.ts rename apps/cs/src/{app => shared}/providers/query-provider.tsx (100%) create mode 100644 apps/cs/src/shared/providers/store-provider.tsx create mode 100644 apps/cs/src/shared/store/index.ts create mode 100644 apps/cs/src/shared/store/user-store.ts diff --git a/apps/cs/package.json b/apps/cs/package.json index 402f0d414..4b400a285 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -25,7 +25,8 @@ "react-chartjs-2": "^5.3.0", "react-hook-form": "^7.54.2", "tailwindcss-animate": "^1.0.7", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index 7bd2ec683..cf477dcc0 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -1,19 +1,18 @@ "use client"; import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; -import { AuthProvider } from "@/entities/auth"; -import { CookieProvider } from "@/shared/lib"; -import { QueryProvider } from "./query-provider"; +import { AuthWrapper } from "@/entities/auth"; +import { QueryProvider, StoreProvider } from "@/shared/providers"; const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { return ( - - - - {children} - - - + + + + {children} + + + ); }; diff --git a/apps/cs/src/entities/auth/index.ts b/apps/cs/src/entities/auth/index.ts index 97579d0a5..dc3fb96c9 100644 --- a/apps/cs/src/entities/auth/index.ts +++ b/apps/cs/src/entities/auth/index.ts @@ -1,2 +1,2 @@ export { LoginApi } from "./api/login-service"; -export { AuthProvider } from "./lib/auth-provider"; +export { AuthWrapper } from "./ui/auth-wrapper"; diff --git a/apps/cs/src/entities/auth/lib/auth-provider.tsx b/apps/cs/src/entities/auth/lib/auth-provider.tsx deleted file mode 100644 index 551cfa79d..000000000 --- a/apps/cs/src/entities/auth/lib/auth-provider.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useQuery } from "@tanstack/react-query"; -import { createContext, useState } from "react"; -import { client } from "@/shared/api"; -import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; -import { getCookie } from "@/shared/lib"; -import { LoginApi } from "../api/login-service"; - -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 interface AuthContextType { - userInfo: IUserInfo | null; -} - -export const AuthContext = createContext(null); - -export const AuthProvider = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); - const [userInfo, setUserInfo] = useState(null); - - useQuery({ - queryKey: [ENDPOINT.USER_SERVICE.USERS], - queryFn: async () => { - try { - 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}`, - ); - setUserInfo(response.data); - return response; - } catch (error) { - console.error(error); - try { - await LoginApi.silentRefresh(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - router.push("/auth/login"); - } - } - }, - }); - - const authContextValue: AuthContextType = { - userInfo, - }; - - return ( - - {children} - - ); -}; diff --git a/apps/cs/src/entities/auth/ui/auth-wrapper.tsx b/apps/cs/src/entities/auth/ui/auth-wrapper.tsx new file mode 100644 index 000000000..ba175e697 --- /dev/null +++ b/apps/cs/src/entities/auth/ui/auth-wrapper.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { client } from "@/shared/api"; +import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; +import { getCookie, useUserStore } from "@/shared/lib"; +import type { IUserInfo } from "@/shared/store"; +import { LoginApi } from "../api/login-service"; + +const AuthWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const { setUserInfo } = useUserStore((state) => state); + + useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS], + queryFn: async () => { + try { + 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}`, + ); + setUserInfo(response.data); + return response; + } catch (error) { + console.error(error); + try { + await LoginApi.silentRefresh(); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + setUserInfo(null); + router.push("/auth/login"); + } + } + }, + }); + + return <>{children}; +}; + +export { AuthWrapper }; diff --git a/apps/cs/src/shared/lib/cookie-provider.tsx b/apps/cs/src/shared/lib/cookie-provider.tsx deleted file mode 100644 index f844d4f8a..000000000 --- a/apps/cs/src/shared/lib/cookie-provider.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import { createContext } from "react"; -import { getCookie, setCookie, removeCookie } from "./cookie"; - -export interface CookieContextType { - getCookie: (name: string) => string | undefined; - setCookie: ( - name: string, - value: string, - options: Cookies.CookieAttributes, - ) => void; - removeCookie: (name: string, options: Cookies.CookieAttributes) => void; -} - -export const CookieContext = createContext(null); - -export function CookieProvider({ children }: { children: React.ReactNode }) { - const cookieContextValue: CookieContextType = { - getCookie, - setCookie, - removeCookie, - }; - - return ( - - {children} - - ); -} diff --git a/apps/cs/src/shared/lib/index.ts b/apps/cs/src/shared/lib/index.ts index 0f4be9c9b..259525de6 100644 --- a/apps/cs/src/shared/lib/index.ts +++ b/apps/cs/src/shared/lib/index.ts @@ -1,2 +1,2 @@ -export { CookieProvider } from "./cookie-provider"; export { getCookie, setCookie, removeCookie } from "./cookie"; +export { useUserStore } from "./use-user-store"; diff --git a/apps/cs/src/shared/lib/use-user-store.ts b/apps/cs/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..f4bf9763a --- /dev/null +++ b/apps/cs/src/shared/lib/use-user-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import { type UserStore } from "@/shared/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/cs/src/shared/providers/index.ts b/apps/cs/src/shared/providers/index.ts new file mode 100644 index 000000000..5d9f096a3 --- /dev/null +++ b/apps/cs/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { QueryProvider } from "./query-provider"; +export { StoreProvider } from "./store-provider"; diff --git a/apps/cs/src/app/providers/query-provider.tsx b/apps/cs/src/shared/providers/query-provider.tsx similarity index 100% rename from apps/cs/src/app/providers/query-provider.tsx rename to apps/cs/src/shared/providers/query-provider.tsx diff --git a/apps/cs/src/shared/providers/store-provider.tsx b/apps/cs/src/shared/providers/store-provider.tsx new file mode 100644 index 000000000..ec913f235 --- /dev/null +++ b/apps/cs/src/shared/providers/store-provider.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { createContext, useRef } from "react"; +import { createUserStore } from "@/shared/store/user-store"; + +type StoreContextType = { + userStore: ReturnType; +}; + +export const StoreContext = createContext( + undefined, +); + +export const StoreProvider = ({ children }: { children: React.ReactNode }) => { + const storeRef = useRef(null); + if (storeRef.current === null) { + storeRef.current = { + userStore: createUserStore(), + }; + } + + return ( + + {children} + + ); +}; diff --git a/apps/cs/src/shared/store/index.ts b/apps/cs/src/shared/store/index.ts new file mode 100644 index 000000000..4dae19ced --- /dev/null +++ b/apps/cs/src/shared/store/index.ts @@ -0,0 +1 @@ +export { createUserStore, type IUserInfo } from "./user-store"; diff --git a/apps/cs/src/shared/store/user-store.ts b/apps/cs/src/shared/store/user-store.ts new file mode 100644 index 000000000..00379fb1f --- /dev/null +++ b/apps/cs/src/shared/store/user-store.ts @@ -0,0 +1,56 @@ +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/pnpm-lock.yaml b/pnpm-lock.yaml index 58b9fe203..484efdd1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: zod: specifier: ^3.24.2 version: 3.24.2 + zustand: + specifier: ^5.0.3 + version: 5.0.3(@types/react@18.3.18)(react@18.3.1) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -7982,3 +7985,25 @@ packages: /zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + + /zustand@5.0.3(@types/react@18.3.18)(react@18.3.1): + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': ^18.3.18 + immer: '>=9.0.6' + react: ^18.3.1 + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + dependencies: + '@types/react': 18.3.18 + react: 18.3.1 + dev: false -- 2.47.1.windows.1 From 574286139c5783de2da4b244b3a720623fa14311 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 17 Mar 2025 15:24:58 +0900 Subject: [PATCH 18/19] =?UTF-8?q?feat:=20sidebar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/assets/src/styles/base.css | 16 + packages/common/components/package.json | 2 + .../components/src/hooks/use-mobile.tsx | 21 + .../components/src/ui/separator/index.tsx | 29 + .../common/components/src/ui/sheet/index.tsx | 140 ++++ .../components/src/ui/sidebar/index.tsx | 779 ++++++++++++++++++ .../components/src/ui/skeleton/index.tsx | 15 + .../components/src/ui/tooltip/index.tsx | 28 + pnpm-lock.yaml | 57 ++ tailwind.config.preset.ts | 10 + 10 files changed, 1097 insertions(+) create mode 100644 packages/common/components/src/hooks/use-mobile.tsx create mode 100644 packages/common/components/src/ui/separator/index.tsx create mode 100644 packages/common/components/src/ui/sheet/index.tsx create mode 100644 packages/common/components/src/ui/sidebar/index.tsx create mode 100644 packages/common/components/src/ui/skeleton/index.tsx create mode 100644 packages/common/components/src/ui/tooltip/index.tsx diff --git a/packages/common/assets/src/styles/base.css b/packages/common/assets/src/styles/base.css index 8a33518bc..a99b29843 100644 --- a/packages/common/assets/src/styles/base.css +++ b/packages/common/assets/src/styles/base.css @@ -32,6 +32,14 @@ --chart-5: #ff7865; --chart-6: #e65db5; --radius: 1rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; --field: #fff; --form: #fff; --container: #f1f6f9; @@ -66,6 +74,14 @@ --chart-5: #ffbcad; --chart-6: #f4a0d3; --radius: 1rem; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; --field: #5b6269; --form: #1a1a1a; --container: #0a0a0a; diff --git a/packages/common/components/package.json b/packages/common/components/package.json index 7c04950f7..7556b9738 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -35,10 +35,12 @@ "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-visually-hidden": "^1.1.2", "@tanstack/react-query": "^5.67.1", "@tanstack/react-table": "^8.21.2", diff --git a/packages/common/components/src/hooks/use-mobile.tsx b/packages/common/components/src/hooks/use-mobile.tsx new file mode 100644 index 000000000..a93d58393 --- /dev/null +++ b/packages/common/components/src/hooks/use-mobile.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined, + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + + return !!isMobile; +} diff --git a/packages/common/components/src/ui/separator/index.tsx b/packages/common/components/src/ui/separator/index.tsx new file mode 100644 index 000000000..79b37ace3 --- /dev/null +++ b/packages/common/components/src/ui/separator/index.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "../../lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref, + ) => ( + + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/packages/common/components/src/ui/sheet/index.tsx b/packages/common/components/src/ui/sheet/index.tsx new file mode 100644 index 000000000..10a35d74c --- /dev/null +++ b/packages/common/components/src/ui/sheet/index.tsx @@ -0,0 +1,140 @@ +"use client"; + +import * as React from "react"; +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; + +import { cn } from "../../lib/utils"; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + }, +); + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetHeader.displayName = "SheetHeader"; + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetFooter.displayName = "SheetFooter"; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/packages/common/components/src/ui/sidebar/index.tsx b/packages/common/components/src/ui/sidebar/index.tsx new file mode 100644 index 000000000..eee581419 --- /dev/null +++ b/packages/common/components/src/ui/sidebar/index.tsx @@ -0,0 +1,779 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; +import { PanelLeft } from "lucide-react"; + +import { useIsMobile } from "../../hooks/use-mobile"; +import { cn } from "../../lib/utils"; +import { Button } from "../button"; +import { Input } from "../input"; +import { Separator } from "../separator"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "../sheet"; +import { Skeleton } from "../skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "../tooltip"; + +const SIDEBAR_COOKIE_NAME = "sidebar_state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContextProps = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); + } + + return context; +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref, + ) => { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open], + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + ], + ); + + return ( + + +
+ {children} +
+
+
+ ); + }, +); +SidebarProvider.displayName = "SidebarProvider"; + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref, + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === "none") { + return ( +
+ {children} +
+ ); + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ); + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ); + }, +); +Sidebar.displayName = "Sidebar"; + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +}); +SidebarTrigger.displayName = "SidebarTrigger"; + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( +