feat: 사이트 상세(수정) 페이지 데이터 바인딩 작업

This commit is contained in:
2025-03-12 16:56:47 +09:00
parent 108e2a8746
commit 0588bf0d7f
30 changed files with 529 additions and 127 deletions
+10
View File
@@ -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 <ViewRegisterPage siteId={id} />;
}
-2
View File
@@ -1,2 +0,0 @@
import { RegisterPage } from "@/pages/site";
export default RegisterPage;
+1
View File
@@ -15,6 +15,7 @@
"@tanstack/react-table": "^8.21.2", "@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1", "axios": "^1.8.1",
"chart.js": "^4.4.8", "chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"next": "15.1.7", "next": "15.1.7",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
@@ -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<ICode[]> {
const response = await client.get<ICode[]>(
`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`,
);
return response.data;
}
}
export const CodeApi = new CodeApiService();
+1
View File
@@ -0,0 +1 @@
export { CodeSelect } from "./ui/code-select";
@@ -0,0 +1,9 @@
export interface ICode {
lwpoCd: string;
upCd: string;
lwpoCdNm: string;
}
export interface CodeService {
getCode(upCd: string): Promise<ICode[]>;
}
@@ -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 (
<Select {...props}>
<SelectTrigger className="w-[12rem] text-[1.3rem] text-label">
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
{code?.map((item) => (
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
{item.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { CodeSelect };
@@ -4,6 +4,7 @@ import type {
ISiteQuery, ISiteQuery,
ISiteInfo, ISiteInfo,
SiteService, SiteService,
ISiteDetail,
} from "../model/site-interface"; } from "../model/site-interface";
export class SiteApiService implements SiteService { export class SiteApiService implements SiteService {
@@ -12,7 +13,13 @@ export class SiteApiService implements SiteService {
`${ENDPOINT.CMS_SERVICE.SITES}`, `${ENDPOINT.CMS_SERVICE.SITES}`,
{ params: query }, { params: query },
); );
return response.data;
}
public async siteDetail(siteId: string): Promise<ISiteDetail> {
const response = await client.get<ISiteDetail>(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
);
return response.data; return response.data;
} }
} }
@@ -12,7 +12,10 @@ export interface ISite {
siteNm: string; siteNm: string;
siteKindCode: string; siteKindCode: string;
bscSiteYn: string; bscSiteYn: string;
siteAddr: string;
useYn: string; useYn: string;
regDt: string;
rgTrNm: string;
} }
export interface ISiteInfo { export interface ISiteInfo {
@@ -44,6 +47,29 @@ export interface ISiteInfo {
empty: boolean; 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 { export interface SiteService {
siteInfo(query: ISiteQuery): Promise<ISiteInfo>; siteInfo(query: ISiteQuery): Promise<ISiteInfo>;
siteDetail(siteId: string): Promise<ISiteDetail>;
} }
@@ -1,3 +1,4 @@
import dayjs from "dayjs";
import { createColumnHelper } from "@tanstack/react-table"; import { createColumnHelper } from "@tanstack/react-table";
import type { ISite } from "@/entities/site/model/site-interface"; import type { ISite } from "@/entities/site/model/site-interface";
@@ -16,8 +17,8 @@ const columns = [
id: "siteNm", id: "siteNm",
header: "사이트명", header: "사이트명",
}), }),
columnHelper.accessor("siteKindCode", { columnHelper.accessor("siteAddr", {
id: "unknown1", id: "siteAddr",
header: "대표 URL", header: "대표 URL",
}), }),
columnHelper.accessor("bscSiteYn", { columnHelper.accessor("bscSiteYn", {
@@ -30,13 +31,17 @@ const columns = [
header: "사용 여부", header: "사용 여부",
cell: (info) => (info.getValue() ? "Y" : "N"), cell: (info) => (info.getValue() ? "Y" : "N"),
}), }),
columnHelper.accessor("siteId", { columnHelper.accessor("rgTrNm", {
id: "unknown2", id: "rgTrNm",
header: "등록자", header: "등록자",
}), }),
columnHelper.accessor("siteId", { columnHelper.accessor("regDt", {
id: "ununknown3", id: "regDt",
header: "등록일", header: "등록일",
cell: (info) => {
const date = dayjs(info.getValue());
return date.format("YYYY-MM-DD");
},
}), }),
]; ];
@@ -26,7 +26,11 @@ type IP = {
allowed: boolean; allowed: boolean;
}; };
function IPManagementDialog() { type IPManagementDialogProps = {
triggerDisabled?: boolean;
};
function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) {
const [rows, setRows] = useState<IP[]>([]); const [rows, setRows] = useState<IP[]>([]);
const [rowSelection, setRowSelection] = useState<IP["id"][]>([]); const [rowSelection, setRowSelection] = useState<IP["id"][]>([]);
@@ -72,7 +76,7 @@ function IPManagementDialog() {
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button>IP 관리</Button> <Button disabled={triggerDisabled}>IP 관리</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[600px]"> <DialogContent className="sm:max-w-[600px]">
<DialogHeader> <DialogHeader>
+57 -43
View File
@@ -1,11 +1,10 @@
"use client"; "use client";
import { useState } from "react";
import { SiteApi } from "@/entities/site";
import { ENDPOINT } from "@/shared/config/api";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Input, Input,
Label, Label,
RadioGroup, RadioGroup,
@@ -16,10 +15,24 @@ import {
TableHead, TableHead,
TableRow, TableRow,
} from "@common/components"; } from "@common/components";
import { useQuery } from "@tanstack/react-query";
import { CodeSelect } from "@/entities/code";
import { SitePageLayout } from "./layout"; import { SitePageLayout } from "./layout";
import { IPManagementDialog } from "./ip-management-dialog"; 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<string>("");
const [, setSiteSkinCd] = useState<string>("");
return ( return (
<SitePageLayout> <SitePageLayout>
<div className="card card-border !mt-6"> <div className="card card-border !mt-6">
@@ -36,54 +49,38 @@ function RegisterPage() {
<TableRow> <TableRow>
<TableHead>사이트 코드</TableHead> <TableHead>사이트 코드</TableHead>
<TableCell className="border"> <TableCell className="border">
<Input className="w-[12rem] !text-[1.3rem]" /> <Input
className="w-[12rem] !text-[1.3rem]"
defaultValue={siteDetail?.siteId}
disabled={!!siteId}
/>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableHead>사이트 구분</TableHead> <TableHead>사이트 구분</TableHead>
<TableCell className="border"> <TableCell className="border">
<div className="flex gap-2"> <div className="flex gap-2">
<DropdownMenu> <CodeSelect
<DropdownMenu> upCd="SITEKNDCD"
<DropdownMenuTrigger asChild> defaultValue={siteDetail?.siteKndCd}
<Button onValueChange={(value) => {
color="white" setSiteKndCd(value);
className="group w-[12rem] justify-between px-4" }}
> />
선택 <IPManagementDialog triggerDisabled={siteKndCd === "O"} />
<i className="diveicon di-chevron-down translate-y-0.5 transition-transform group-data-[state=open]:translate-y-0 group-data-[state=open]:rotate-180" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[12rem]">
<DropdownMenuItem>내부</DropdownMenuItem>
<DropdownMenuItem>외부</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenu>
<IPManagementDialog />
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableHead>사이트 디자인</TableHead> <TableHead>사이트 디자인</TableHead>
<TableCell className="border"> <TableCell className="border">
<DropdownMenu> <CodeSelect
<DropdownMenu> upCd="SITESKINCD"
<DropdownMenuTrigger asChild> defaultValue={siteDetail?.siteSkn}
<Button onValueChange={(value) => {
color="white" setSiteSkinCd(value);
className="group w-[12rem] justify-between px-4" }}
> />
선택
<i className="diveicon di-chevron-down translate-y-0.5 transition-transform group-data-[state=open]:translate-y-0 group-data-[state=open]:rotate-180" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[12rem]">
<DropdownMenuItem>라이트</DropdownMenuItem>
<DropdownMenuItem>다크</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenu>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
@@ -92,6 +89,7 @@ function RegisterPage() {
<Input <Input
className="!text-[1.3rem]" className="!text-[1.3rem]"
placeholder="사이트명을 입력하십시오." placeholder="사이트명을 입력하십시오."
defaultValue={siteDetail?.siteNm}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -101,6 +99,7 @@ function RegisterPage() {
<Input <Input
className="!text-[1.3rem]" className="!text-[1.3rem]"
placeholder="사이트 설명을 입력하십시오." placeholder="사이트 설명을 입력하십시오."
defaultValue={siteDetail?.siteExpln}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -110,6 +109,7 @@ function RegisterPage() {
<Input <Input
className="!text-[1.3rem]" className="!text-[1.3rem]"
placeholder="사이트 하단 주소를 입력하십시오." placeholder="사이트 하단 주소를 입력하십시오."
defaultValue={siteDetail?.siteAddr}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -119,6 +119,7 @@ function RegisterPage() {
<Input <Input
className="!text-[1.3rem]" className="!text-[1.3rem]"
placeholder="사이트 하단 전화번호를 입력하십시오." placeholder="사이트 하단 전화번호를 입력하십시오."
defaultValue={siteDetail?.telNo}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -128,6 +129,7 @@ function RegisterPage() {
<Input <Input
className="!text-[1.3rem]" className="!text-[1.3rem]"
placeholder="사이트 하단 팩스번호를 입력하십시오." placeholder="사이트 하단 팩스번호를 입력하십시오."
defaultValue={siteDetail?.faxNumber}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -137,6 +139,7 @@ function RegisterPage() {
<Input <Input
className="!text-[1.3rem]" className="!text-[1.3rem]"
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)" placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
defaultValue={siteDetail?.lwndCn}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -146,13 +149,19 @@ function RegisterPage() {
<Input <Input
className="!text-[1.3rem]" className="!text-[1.3rem]"
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오." placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
defaultValue={siteDetail?.bkmkIcon}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableHead>기본 사이트 여부</TableHead> <TableHead>기본 사이트 여부</TableHead>
<TableCell className="border"> <TableCell className="border">
<RadioGroup defaultValue="comfortable"> <RadioGroup
defaultValue={siteDetail?.basicSiteYn}
onValueChange={(value) => {
console.log(value);
}}
>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RadioGroupItem value="default" id="r1" /> <RadioGroupItem value="default" id="r1" />
<Label htmlFor="r1">예</Label> <Label htmlFor="r1">예</Label>
@@ -167,7 +176,12 @@ function RegisterPage() {
<TableRow> <TableRow>
<TableHead>사용 여부</TableHead> <TableHead>사용 여부</TableHead>
<TableCell className="border"> <TableCell className="border">
<RadioGroup defaultValue="comfortable"> <RadioGroup
defaultValue={siteDetail?.useYn}
onValueChange={(value) => {
console.log(value);
}}
>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RadioGroupItem value="default" id="r1" /> <RadioGroupItem value="default" id="r1" />
<Label htmlFor="r1">사용</Label> <Label htmlFor="r1">사용</Label>
+18 -13
View File
@@ -8,12 +8,19 @@ import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { Button, Input, DataTable } from "@common/components"; import { Button, Input, DataTable } from "@common/components";
import { SiteApi } from "@/entities/site/api/site-service"; 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 { ENDPOINT } from "@/shared/config/api";
import { columns } from "../model/__mocks__"; import { columns } from "../model/table-columns";
import { SitePageLayout } from "./layout"; import { SitePageLayout } from "./layout";
import type { ISiteQuery } from "@/entities/site/model/site-interface";
function SitePage() { 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<ISiteQuery>({ const [siteQuery, setSiteQuery] = useState<ISiteQuery>({
siteNm: "", siteNm: "",
siteKndCd: "", siteKndCd: "",
@@ -23,10 +30,10 @@ function SitePage() {
sort: [], sort: [],
}); });
const { data: siteInfo, refetch } = useQuery({ const table = useReactTable({
queryKey: [ENDPOINT.CMS_SERVICE.SITES], data: siteInfo?.content ?? [],
queryFn: () => SiteApi.siteInfo(siteQuery), columns,
enabled: false, getCoreRowModel: getCoreRowModel(),
}); });
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
@@ -34,15 +41,13 @@ function SitePage() {
refetch(); refetch();
}; };
const router = useRouter(); const handleRowClick = (row: ISite) => {
const table = useReactTable({ router.push(`/site/register/${row.siteId}`);
data: siteInfo?.content ?? [], };
columns,
getCoreRowModel: getCoreRowModel(),
});
useEffect(() => { useEffect(() => {
refetch(); refetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (
@@ -71,7 +76,7 @@ function SitePage() {
<Button onClick={() => router.push("/site/register")}>등록</Button> <Button onClick={() => router.push("/site/register")}>등록</Button>
</div> </div>
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0"> <div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
<DataTable table={table} /> <DataTable table={table} onRowClick={handleRowClick} />
</div> </div>
</div> </div>
</div> </div>
+1
View File
@@ -10,6 +10,7 @@ export const ENDPOINT = {
}, },
CMS_SERVICE: { CMS_SERVICE: {
SITES: "/cms-service/api/v1/sites", SITES: "/cms-service/api/v1/sites",
CODE: "/cms-service/api/v1/code/cache",
}, },
}; };
@@ -23,6 +23,7 @@
--border: #e5e5e5; --border: #e5e5e5;
--input: #e5e5e5; --input: #e5e5e5;
--label: #666666; --label: #666666;
--placeholder: #a2a2a2;
--ring: #0a0a0a; --ring: #0a0a0a;
--chart-1: #2094fa; --chart-1: #2094fa;
--chart-2: #00dfe5; --chart-2: #00dfe5;
@@ -52,6 +53,7 @@
--border: #262626; --border: #262626;
--input: #262626; --input: #262626;
--label: #f1f1f1; --label: #f1f1f1;
--placeholder: #737373;
--ring: #d4d4d4; --ring: #d4d4d4;
--chart-1: #66b3ff; --chart-1: #66b3ff;
--chart-2: #66f0f2; --chart-2: #66f0f2;
+1
View File
@@ -34,6 +34,7 @@
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-radio-group": "^1.2.2", "@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-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.1.3",
@@ -22,7 +22,7 @@ const meta: Meta<typeof Bar> = {
description: { description: {
component: ` component: `
- [Docs](https://react-chartjs-2.js.org/) - [Docs](https://react-chartjs-2.js.org/)
`, `,
}, },
source: { source: {
code: ` code: `
@@ -14,7 +14,7 @@ const meta: Meta<typeof DataTable> = {
description: { description: {
component: ` component: `
- [Docs](https://tanstack.com/table/v8/docs/introduction) - [Docs](https://tanstack.com/table/v8/docs/introduction)
`, `,
}, },
source: { source: {
code: ` code: `
@@ -36,15 +36,22 @@ function THead<T>({ table }: { table: TableType<T> }) {
); );
} }
type TableProps<T> = { table: TableType<T> }; type TableProps<T> = {
table: TableType<T>;
onRowClick?: (row: T) => void;
};
function DataTable<T>({ table }: TableProps<T>) { function DataTable<T>({ table, onRowClick }: TableProps<T>) {
return ( return (
<Table className="w-full"> <Table className="w-full">
<THead table={table} /> <THead table={table} />
<tbody> <tbody>
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows.map((row) => (
<TableRow key={row.id}> <TableRow
key={row.id}
onClick={() => onRowClick?.(row.original)}
className={onRowClick ? "cursor-pointer hover:bg-muted" : ""}
>
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
const { meta } = cell.column.columnDef; const { meta } = cell.column.columnDef;
return ( return (
@@ -15,6 +15,7 @@ export * from "./layout";
export * from "./loading"; export * from "./loading";
export * from "./popover"; export * from "./popover";
export * from "./radio-group"; export * from "./radio-group";
export * from "./select";
export * from "./slider"; export * from "./slider";
export * from "./switch"; export * from "./switch";
export * from "./table"; export * from "./table";
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-[3.2rem] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "placeholder:text-placeholder flex h-[3.2rem] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className, className,
)} )}
ref={ref} ref={ref}
@@ -13,7 +13,6 @@ import {
} from "chart.js"; } from "chart.js";
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import { fakerKO as faker } from "@faker-js/faker"; import { fakerKO as faker } from "@faker-js/faker";
import { getCssVariableValue } from "../../lib/utils";
const meta: Meta<typeof Line> = { const meta: Meta<typeof Line> = {
title: "Chart/Line", title: "Chart/Line",
@@ -23,7 +22,7 @@ const meta: Meta<typeof Line> = {
description: { description: {
component: ` component: `
- [Docs](https://react-chartjs-2.js.org/) - [Docs](https://react-chartjs-2.js.org/)
`, `,
}, },
source: { source: {
code: ` code: `
@@ -107,7 +106,7 @@ export const Default: Story = {
LineElement, LineElement,
Title, Title,
Tooltip, Tooltip,
Legend Legend,
); );
const options: ChartOptions<"line"> = { const options: ChartOptions<"line"> = {
@@ -124,29 +123,29 @@ export const Default: Story = {
}; };
const labels = [ const labels = [
'January', "January",
'February', "February",
'March', "March",
'April', "April",
'May', "May",
'June', "June",
'July' "July",
]; ];
const data = { const data = {
labels, labels,
datasets: [ datasets: [
{ {
label: 'Dataset 1', label: "Dataset 1",
data: labels.map(() => faker.number.int({ min: -1000, max: 1000 })), data: labels.map(() => faker.number.int({ min: -1000, max: 1000 })),
borderColor: 'rgb(255, 99, 132)', borderColor: "rgb(255, 99, 132)",
backgroundColor: 'rgba(255, 99, 132, 0.5)', backgroundColor: "rgba(255, 99, 132, 0.5)",
}, },
{ {
label: 'Dataset 2', label: "Dataset 2",
data: labels.map(() => faker.number.int({ min: -1000, max: 1000 })), data: labels.map(() => faker.number.int({ min: -1000, max: 1000 })),
borderColor: 'rgb(53, 162, 235)', borderColor: "rgb(53, 162, 235)",
backgroundColor: 'rgba(53, 162, 235, 0.5)', backgroundColor: "rgba(53, 162, 235, 0.5)",
}, },
], ],
}; };
@@ -1,15 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
Chart as ChartJS,
ArcElement,
Tooltip,
Legend,
type ChartOptions,
} from "chart.js";
import { Pie } from "react-chartjs-2"; import { Pie } from "react-chartjs-2";
import { fakerKO as faker } from "@faker-js/faker";
import { getCssVariableValue } from "../../lib/utils";
const meta: Meta<typeof Pie> = { const meta: Meta<typeof Pie> = {
title: "Chart/Pie", title: "Chart/Pie",
@@ -19,7 +11,7 @@ const meta: Meta<typeof Pie> = {
description: { description: {
component: ` component: `
- [Docs](https://react-chartjs-2.js.org/) - [Docs](https://react-chartjs-2.js.org/)
`, `,
}, },
source: { source: {
code: ` code: `
@@ -83,42 +75,31 @@ type Story = StoryObj<typeof Pie>;
export const Default: Story = { export const Default: Story = {
render: () => { render: () => {
ChartJS.register( ChartJS.register(ArcElement, Tooltip, Legend);
ArcElement,
Tooltip,
Legend,
);
const labels = [ const labels = ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"];
'Red',
'Blue',
'Yellow',
'Green',
'Purple',
'Orange'
];
const data = { const data = {
labels, labels,
datasets: [ datasets: [
{ {
label: '# of Votes', label: "# of Votes",
data: [12, 19, 3, 5, 2, 3], data: [12, 19, 3, 5, 2, 3],
backgroundColor: [ backgroundColor: [
'rgba(255, 99, 132, 0.2)', "rgba(255, 99, 132, 0.2)",
'rgba(54, 162, 235, 0.2)', "rgba(54, 162, 235, 0.2)",
'rgba(255, 206, 86, 0.2)', "rgba(255, 206, 86, 0.2)",
'rgba(75, 192, 192, 0.2)', "rgba(75, 192, 192, 0.2)",
'rgba(153, 102, 255, 0.2)', "rgba(153, 102, 255, 0.2)",
'rgba(255, 159, 64, 0.2)', "rgba(255, 159, 64, 0.2)",
], ],
borderColor: [ borderColor: [
'rgba(255, 99, 132, 1)', "rgba(255, 99, 132, 1)",
'rgba(54, 162, 235, 1)', "rgba(54, 162, 235, 1)",
'rgba(255, 206, 86, 1)', "rgba(255, 206, 86, 1)",
'rgba(75, 192, 192, 1)', "rgba(75, 192, 192, 1)",
'rgba(153, 102, 255, 1)', "rgba(153, 102, 255, 1)",
'rgba(255, 159, 64, 1)', "rgba(255, 159, 64, 1)",
], ],
borderWidth: 1, borderWidth: 1,
}, },
@@ -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<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-[3.2rem] w-full items-center justify-between whitespace-nowrap rounded-sm border border-input bg-transparent px-4 py-2 text-sm 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}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-3 pl-4 pr-8 text-[1.2rem] outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from ".";
const meta: Meta<typeof Select> = {
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<typeof Select>;
export const Default: Story = {
render: () => (
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">옵션 1</SelectItem>
<SelectItem value="option2">옵션 2</SelectItem>
<SelectItem value="option3">옵션 3</SelectItem>
</SelectContent>
</Select>
),
};
@@ -63,7 +63,7 @@ const TableRow = React.forwardRef<
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
"hover:bg-muted/50 border-b transition-colors data-[state=selected]:bg-muted", "border-b bg-white transition-colors data-[state=selected]:bg-muted",
className, className,
)} )}
{...props} {...props}
@@ -2,7 +2,7 @@ th {
@apply bg-[#5987bf] px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2] text-[#fff]; @apply bg-[#5987bf] px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2] text-[#fff];
} }
td { td {
@apply border-b border-b-[#e0e6ea] bg-[#fff] px-2 py-[1.2rem] text-[1.3rem] font-normal leading-[1.2] text-[#666]; @apply border-b border-b-[#e0e6ea] px-2 py-[1.2rem] text-[1.3rem] font-normal leading-[1.2] text-[#666];
} }
.secondary th { .secondary th {
@@ -19,7 +19,7 @@ const meta: Meta<typeof Tree> = {
description: { description: {
component: ` component: `
- [Docs](https://github.com/minop1205/react-dnd-treeview/blob/next/README.md) - [Docs](https://github.com/minop1205/react-dnd-treeview/blob/next/README.md)
`, `,
}, },
}, },
}, },
+70
View File
@@ -54,6 +54,9 @@ importers:
chart.js: chart.js:
specifier: ^4.4.8 specifier: ^4.4.8
version: 4.4.8 version: 4.4.8
dayjs:
specifier: ^1.11.13
version: 1.11.13
js-cookie: js-cookie:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
@@ -148,6 +151,9 @@ importers:
'@radix-ui/react-radio-group': '@radix-ui/react-radio-group':
specifier: ^1.2.2 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) 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': '@radix-ui/react-slider':
specifier: ^1.2.3 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) 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) react-dom: 18.3.1(react@18.3.1)
dev: false 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): /@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==} resolution: {integrity: sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==}
peerDependencies: peerDependencies:
@@ -2269,6 +2315,26 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false 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: /@radix-ui/rect@1.1.0:
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
dev: false dev: false
@@ -3984,6 +4050,10 @@ packages:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
dev: false dev: false
/dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
dev: false
/debug@3.2.7: /debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies: peerDependencies:
+1
View File
@@ -41,6 +41,7 @@ export default {
border: "var(--border)", border: "var(--border)",
input: "var(--input)", input: "var(--input)",
label: "var(--label)", label: "var(--label)",
placeholder: "var(--placeholder)",
ring: "var(--ring)", ring: "var(--ring)",
chart: { chart: {
1: "var(--chart-1)", 1: "var(--chart-1)",