Merge commit 'e16ff92e72f6346ee36d0f1b7dd9d6039fa1a2d2' into iipbs/wr/main_merge

This commit is contained in:
truthwing
2025-03-17 17:19:24 +09:00
72 changed files with 1867 additions and 427 deletions
+11 -1
View File
@@ -62,7 +62,10 @@ npx create-next-app@latest ./apps/as --typescript --eslint --tailwind --app
{
// 기타 설정 ...
"paths": {
"@common/components": ["../../packages/common/components/src"], // 추가
"@common/components/*": ["../../../packages/common/components/src/*"],
"@common/components": ["../../../packages/common/components/src"],
"@common/network/*": ["../../../packages/common/network/*"],
"@common/network": ["../../../packages/common/network/"]
}
}
```
@@ -174,3 +177,10 @@ pnpm install
## 링크
- 컴포넌트 가이드(스토리북): http://www.onasys.co.kr/design/template
## API 서버
http://dev.onasys.co.kr:8000
## Swagger
http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=attach-service
+2
View File
@@ -0,0 +1,2 @@
NEXT_PUBLIC_PORT=3000
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
-3
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
"use client";
import { BaseLayout } from "@common/components/ui";
export default function WithBaseLayout({
children,
}: {
children: React.ReactNode;
}) {
return <BaseLayout>{children}</BaseLayout>;
}
@@ -0,0 +1,10 @@
import { ModifyPage as VModifyPage } from "@/pages/site";
export default async function ModifyPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <VModifyPage siteId={id} />;
}
@@ -0,0 +1,2 @@
import { CreatePage } from "@/pages/site";
export default CreatePage;
-2
View File
@@ -1,2 +0,0 @@
import { RegisterPage } from "@/pages/site";
export default RegisterPage;
+19
View File
@@ -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*",
};
-11
View File
@@ -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;
+8 -1
View File
@@ -11,14 +11,21 @@
"dependencies": {
"@common/assets": "workspace:*",
"@common/components": "workspace:*",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@hookform/resolvers": "^4.1.3",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"next": "15.1.7",
"react-chartjs-2": "^5.3.0",
"tailwindcss-animate": "^1.0.7"
"react-hook-form": "^7.54.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+2 -6
View File
@@ -1,16 +1,12 @@
import "./styles/base.css";
import "./styles/components.css";
import { AuthProvider, CookieProvider, QueryProvider } from "@/shared/provider";
import { AppProvider } from "@/shared/provider";
const App = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
<html lang="ko">
<body>
<QueryProvider>
<CookieProvider>
<AuthProvider>{children}</AuthProvider>
</CookieProvider>
</QueryProvider>
<AppProvider>{children}</AppProvider>
</body>
</html>
);
+10 -1
View File
@@ -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];
}
}
}
@@ -31,7 +31,7 @@ export class LoginApiService implements LoginService {
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("[login] Missing required authentication tokens");
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
@@ -42,11 +42,9 @@ export class LoginApiService implements LoginService {
);
} catch (error) {
if (error instanceof AxiosError) {
throw new Error(
`Login failed: ${error.response?.data?.message || error.message}`,
);
throw new Error(`Login failed: ${error.response?.data?.message}`);
}
throw error;
throw new Error(`Login failed: ${error}`);
}
}
@@ -62,22 +60,19 @@ export class LoginApiService implements LoginService {
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error(
"[silentRefresh] Missing required authentication tokens",
);
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
} catch (error) {
console.warn("Token has expired, logging out.");
tokenManager.clearAuthToken();
if (error instanceof AxiosError) {
throw new Error(
`Silent refresh failed: ${error.response?.data?.message || error.message}`,
`Silent refresh failed: ${error.response?.data?.message}`,
);
}
throw error;
throw new Error(`Silent refresh failed: ${error}`);
}
}
}
@@ -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]">
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
{code?.map((item) => (
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
{item.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { CodeSelect };
+40 -6
View File
@@ -1,18 +1,52 @@
import { client } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config/api";
import type {
ISiteQuery,
ISiteInfo,
SiteService,
import {
siteFormSchema,
type ISiteInfo,
type SiteService,
type ISiteDetail,
type ISiteParams,
type SiteFormData,
} from "../model/site-interface";
export class SiteApiService implements SiteService {
public async siteInfo(query: ISiteQuery): Promise<ISiteInfo> {
public async siteInfo(params: ISiteParams): Promise<ISiteInfo> {
const response = await client.get<ISiteInfo>(
`${ENDPOINT.CMS_SERVICE.SITES}`,
{ params: query },
{ params },
);
return response.data;
}
public async siteDetail(siteId: string): Promise<ISiteDetail> {
const response = await client.get<ISiteDetail>(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
);
return response.data;
}
public async createSite(data: SiteFormData): Promise<void> {
const validatedData = siteFormSchema.parse(data);
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.SITES}`,
validatedData,
);
return response.data;
}
public async modifySite(siteId: string, data: SiteFormData): Promise<void> {
const validatedData = siteFormSchema.parse(data);
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
validatedData,
);
return response.data;
}
public async deleteSite(siteId: string): Promise<void> {
const response = await client.delete<void>(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
);
return response.data;
}
}
+1
View File
@@ -1 +1,2 @@
export { SiteApi } from "./api/site-service";
export * from "./model/site-interface";
@@ -1,4 +1,6 @@
export interface ISiteQuery {
import { z } from "zod";
export interface ISiteParams {
siteNm: string;
siteKndCd: string;
useYn: string;
@@ -12,7 +14,10 @@ export interface ISite {
siteNm: string;
siteKindCode: string;
bscSiteYn: string;
siteAddr: string;
useYn: string;
regDt: string;
rgTrNm: string;
}
export interface ISiteInfo {
@@ -44,6 +49,39 @@ export interface ISiteInfo {
empty: boolean;
}
export interface SiteService {
siteInfo(query: ISiteQuery): Promise<ISiteInfo>;
export interface ISiteDetail extends SiteFormData {
siteId: string;
siteKindNm: string;
basicSiteYn: string;
faxNumber: string;
regDt: string;
rgTr: string;
mdfcnDt: string;
mdFr: string;
rgTrNm: string;
mdFrNm: string;
}
export const siteFormSchema = z.object({
siteId: z.string().max(2).nonempty(),
siteNm: z.string().max(100).nonempty(),
siteExpln: z.string().max(1000).optional(),
siteKndCd: z.string().max(10).nonempty(),
siteAddr: z.string().max(2000).optional(),
faxNo: z.string().max(100).optional(),
telNo: z.string().max(1000).optional(),
lwndCn: z.string().max(2000).optional(),
bscSiteYn: z.string(),
bkmkIcon: z.string().max(100).optional(),
useYn: z.string(),
siteSkn: z.string().max(10).optional(),
});
export type SiteFormData = z.infer<typeof siteFormSchema>;
export interface SiteService {
siteInfo(query: ISiteParams): Promise<ISiteInfo>;
siteDetail(siteId: string): Promise<ISiteDetail>;
modifySite(siteId: string, data: SiteFormData): Promise<void>;
createSite(data: SiteFormData): Promise<void>;
}
+2 -1
View File
@@ -1,2 +1,3 @@
export { RegisterPage } from "./ui/register-page";
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { SitePage } from "./ui/site-page";
@@ -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");
},
}),
];
+53
View File
@@ -0,0 +1,53 @@
"use client";
import { useState } from "react";
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 { SitePageLayout } from "./layout";
import { SiteForm } from "./site-form";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
} from "@common/components/ui";
function CreatePage() {
const router = useRouter();
const { mutateAsync: createSite } = useMutation({
mutationFn: (data: SiteFormData) => SiteApi.createSite(data),
onSuccess: () => {
setIsCreateSuccess(true);
},
});
const [isCreateSuccess, setIsCreateSuccess] = useState(false);
const form = useForm<SiteFormData>({
resolver: zodResolver(siteFormSchema),
});
return (
<SitePageLayout>
<SiteForm form={form} handleSave={createSite} />
<Dialog open={isCreateSuccess} onOpenChange={() => router.back()}>
<DialogContent aria-describedby={undefined}>
<div className="py-10">
<p className="text-center text-[1.4rem]">등록되었습니다.</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button>확인</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</SitePageLayout>
);
}
export { CreatePage };
@@ -0,0 +1,210 @@
import { useState } from "react";
import {
Dialog,
DialogTrigger,
Button,
DialogContent,
DialogHeader,
DialogTitle,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Input,
Checkbox,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
DialogFooter,
DialogClose,
} from "@common/components";
export type Domain = {
id: number;
domainUrl: string;
isRepresentative: boolean;
isActive: "true" | "false";
};
type DomainManagementDialogProps = {
triggerDisabled?: boolean;
handleSave: (rows: Domain[]) => void;
};
function DomainManagementDialog({
triggerDisabled,
handleSave,
}: DomainManagementDialogProps) {
const [rows, setRows] = useState<Domain[]>([]);
const [rowSelection, setRowSelection] = useState<Domain["id"][]>([]);
const onSave = () => {
handleSave(rows);
};
const handleAddDomain = () => {
const newDomain: Domain = {
id: Date.now(),
domainUrl: "",
isRepresentative: rows.length === 0,
isActive: "true",
};
setRows([...rows, newDomain]);
};
const handleDeleteDomain = () => {
const filteredRows = rows.filter((row) => !rowSelection.includes(row.id));
const hasRepresentative = filteredRows.some((row) => row.isRepresentative);
let updatedRows = filteredRows;
if (!hasRepresentative && filteredRows.length > 0) {
updatedRows = filteredRows.map((row, index) =>
index === 0 ? { ...row, isRepresentative: true } : row,
);
}
setRows(updatedRows);
setRowSelection([]);
};
const handleDomainUrlChange = (id: number, value: string) => {
setRows(
rows.map((row) => (row.id === id ? { ...row, domainUrl: value } : row)),
);
};
const handleRepresentativeChange = (id: number) => {
setRows(
rows.map((row) => ({
...row,
isRepresentative: row.id === id,
})),
);
};
const handleActiveChange = (id: number, value: "true" | "false") => {
setRows(
rows.map((row) => (row.id === id ? { ...row, isActive: value } : row)),
);
};
const toggleRowSelection = (id: number) => {
setRowSelection((prev) => {
if (prev.includes(id)) {
return prev.filter((item) => item !== id);
} else {
return [...prev, id];
}
});
};
return (
<Dialog>
<DialogTrigger asChild>
<Button disabled={triggerDisabled}>도메인 관리</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[60rem]">
<DialogHeader>
<DialogTitle>도메인 관리</DialogTitle>
</DialogHeader>
<div className="mb-4 flex justify-end gap-2">
<Button size="sm" color="white" onClick={handleAddDomain}>
항목 추가
</Button>
<Button
size="sm"
color="red"
onClick={handleDeleteDomain}
disabled={rowSelection.length === 0}
>
항목 삭제
</Button>
</div>
<div className="card card-border">
<Table variant="secondary">
<colgroup>
<col width="8%" />
<col width="12%" />
<col width="40%" />
<col width="20%" />
<col width="20%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>NO</TableHead>
<TableHead>선택</TableHead>
<TableHead>도메인URL</TableHead>
<TableHead>대표URL</TableHead>
<TableHead>사용여부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row, idx) => (
<TableRow key={row.id}>
<TableCell align="center">{idx + 1}</TableCell>
<TableCell align="center">
<Checkbox
checked={rowSelection.includes(row.id)}
onCheckedChange={() => toggleRowSelection(row.id)}
className="cursor-pointer"
/>
</TableCell>
<TableCell align="center">
<Input
value={row.domainUrl}
onChange={(e) =>
handleDomainUrlChange(row.id, e.target.value)
}
className="!text-[1.3rem]"
/>
</TableCell>
<TableCell align="center">
<Checkbox
checked={row.isRepresentative}
onCheckedChange={() => handleRepresentativeChange(row.id)}
className="cursor-pointer"
/>
</TableCell>
<TableCell align="center">
<Select
value={row.isActive}
onValueChange={(value: "true" | "false") =>
handleActiveChange(row.id, value)
}
>
<SelectTrigger className="mx-auto w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">예</SelectItem>
<SelectItem value="false">아니오</SelectItem>
</SelectContent>
</Select>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button size="lg" onClick={onSave}>
저장
</Button>
</DialogClose>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { DomainManagementDialog };
@@ -1,5 +1,3 @@
"use client";
import { useState } from "react";
import {
Dialog,
@@ -18,6 +16,8 @@ import {
RadioGroup,
RadioGroupItem,
Checkbox,
DialogFooter,
DialogClose,
} from "@common/components";
type IP = {
@@ -26,11 +26,15 @@ type IP = {
allowed: boolean;
};
function IPManagementDialog() {
type IPManagementDialogProps = {
triggerDisabled?: boolean;
};
function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) {
const [rows, setRows] = useState<IP[]>([]);
const [rowSelection, setRowSelection] = useState<IP["id"][]>([]);
const handleSave = () => {};
const onSave = () => {};
const handleAddIP = () => {
const newIP: IP = {
@@ -72,9 +76,9 @@ function IPManagementDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>IP 관리</Button>
<Button disabled={triggerDisabled}>IP 관리</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogContent className="sm:max-w-[60rem]">
<DialogHeader>
<DialogTitle>IP 관리</DialogTitle>
</DialogHeader>
@@ -82,9 +86,6 @@ function IPManagementDialog() {
<Button size="sm" color="green">
엑셀 업로드
</Button>
<Button size="sm" onClick={handleSave}>
저장
</Button>
<Button size="sm" color="white" onClick={handleAddIP}>
항목 추가
</Button>
@@ -98,7 +99,7 @@ function IPManagementDialog() {
</Button>
</div>
<div className="card card-border">
<Table>
<Table variant="secondary">
<colgroup>
<col width="10%" />
<col width="10%" />
@@ -156,6 +157,18 @@ function IPManagementDialog() {
</TableBody>
</Table>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button size="lg" onClick={onSave}>
저장
</Button>
</DialogClose>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
+6 -10
View File
@@ -1,15 +1,11 @@
import { BaseLayout } from "@common/components";
function SitePageLayout({ children }: { children: React.ReactNode }) {
return (
<BaseLayout>
<div className="h-auto min-h-full bg-[#f1f6f9]">
<h2 className="px-[1.8rem] pt-[2rem] text-[2.4rem] font-semibold text-[#4c5e6f]">
사이트 관리
</h2>
{children}
</div>
</BaseLayout>
<div className="h-auto min-h-full bg-container">
<h2 className="px-[1.8rem] pt-[2rem] text-[2.4rem] font-semibold text-title">
사이트 관리
</h2>
{children}
</div>
);
}
+95
View File
@@ -0,0 +1,95 @@
"use client";
import { useEffect, useState } from "react";
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 {
SiteApi,
siteFormSchema,
type ISiteDetail,
type SiteFormData,
} from "@/entities/site";
import { ENDPOINT } from "@/shared/config/api";
import { SitePageLayout } from "./layout";
import { SiteForm } from "./site-form";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
} from "@common/components/ui";
function ModifyPage({ siteId }: { siteId: string }) {
const isModifyMode = !!siteId;
const router = useRouter();
const { data: siteDetail } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
queryFn: () => SiteApi.siteDetail(siteId),
select: (data) => ({
...data,
faxNo: data.faxNumber,
bscSiteYn: data.basicSiteYn,
}),
enabled: isModifyMode,
});
const { mutateAsync: modifySite } = useMutation({
mutationFn: (data: SiteFormData) => SiteApi.modifySite(siteId, data),
onSuccess: () => {
setMessage("수정되었습니다.");
},
});
const { mutateAsync: deleteSite } = useMutation({
mutationFn: () => SiteApi.deleteSite(siteId),
onSuccess: () => {
setMessage("삭제되었습니다.");
},
});
const [message, setMessage] = useState("");
const form = useForm<SiteFormData>({
resolver: zodResolver(siteFormSchema),
});
useEffect(() => {
if (!siteDetail) return;
const keys = Object.keys(siteFormSchema.shape);
for (const key of keys) {
form.setValue(
key as keyof SiteFormData,
siteDetail[key as keyof ISiteDetail] ?? "",
);
}
}, [siteDetail]);
return (
<SitePageLayout>
<SiteForm
form={form}
siteId={siteId}
handleSave={modifySite}
handleDelete={deleteSite}
/>
<Dialog open={!!message} onOpenChange={() => router.back()}>
<DialogContent aria-describedby={undefined}>
<div className="py-10">
<p className="text-center text-[1.4rem]">{message}</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button>확인</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</SitePageLayout>
);
}
export { ModifyPage };
-199
View File
@@ -1,199 +0,0 @@
"use client";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Input,
Label,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import { SitePageLayout } from "./layout";
import { IPManagementDialog } from "./ip-management-dialog";
function RegisterPage() {
return (
<SitePageLayout>
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-[#666]">사이트 등록/수정</h3>
</div>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>사이트 코드</TableHead>
<TableCell className="border">
<Input className="w-[12rem] !text-[1.3rem]" />
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 구분</TableHead>
<TableCell className="border">
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
color="white"
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>
<IPManagementDialog />
</div>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 디자인</TableHead>
<TableCell className="border">
<DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
color="white"
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>
</TableRow>
<TableRow>
<TableHead>사이트명</TableHead>
<TableCell className="border">
<Input
className="!text-[1.3rem]"
placeholder="사이트명을 입력하십시오."
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 설명</TableHead>
<TableCell className="border">
<Input
className="!text-[1.3rem]"
placeholder="사이트 설명을 입력하십시오."
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 하단 표기 주소</TableHead>
<TableCell className="border">
<Input
className="!text-[1.3rem]"
placeholder="사이트 하단 주소를 입력하십시오."
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 하단 표기 전화번호</TableHead>
<TableCell className="border">
<Input
className="!text-[1.3rem]"
placeholder="사이트 하단 전화번호를 입력하십시오."
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 하단 표기 팩스번호</TableHead>
<TableCell className="border">
<Input
className="!text-[1.3rem]"
placeholder="사이트 하단 팩스번호를 입력하십시오."
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 하단 표기 내용</TableHead>
<TableCell className="border">
<Input
className="!text-[1.3rem]"
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>북마크 아이콘</TableHead>
<TableCell className="border">
<Input
className="!text-[1.3rem]"
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>기본 사이트 여부</TableHead>
<TableCell className="border">
<RadioGroup defaultValue="comfortable">
<div className="flex items-center space-x-2">
<RadioGroupItem value="default" id="r1" />
<Label htmlFor="r1">예</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="comfortable" id="r2" />
<Label htmlFor="r2">아니오</Label>
</div>
</RadioGroup>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableCell className="border">
<RadioGroup defaultValue="comfortable">
<div className="flex items-center space-x-2">
<RadioGroupItem value="default" id="r1" />
<Label htmlFor="r1">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="comfortable" id="r2" />
<Label htmlFor="r2">사용안함</Label>
</div>
</RadioGroup>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button size="lg">저장</Button>
<Button size="lg" color="white">
취소
</Button>
<Button size="lg" color="red">
삭제
</Button>
</div>
</SitePageLayout>
);
}
export { RegisterPage };
+431
View File
@@ -0,0 +1,431 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { type UseFormReturn } from "react-hook-form";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
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<SiteFormData>;
siteId?: string;
handleSave: (data: SiteFormData) => void;
handleDelete: () => void;
};
function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
const router = useRouter();
const { message, confirm, onConfirm, onCancel } = useConfirm();
const [viewState, setViewState] = useState<Partial<ISiteDetail>>({});
const [representativeDomain, setRepresentativeDomain] = useState<string>("");
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);
};
const onDelete = async () => {
if (await confirm("정말 삭제하시겠습니까?")) {
handleDelete();
}
};
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">
사이트 등록/수정
</h3>
</div>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>사이트 코드</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="w-[12rem] !text-[1.3rem]"
disabled={isModifyMode}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 구분</TableHead>
<TableCell className="border">
<div className="flex gap-2">
<FormField
control={form.control}
name="siteKndCd"
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
{...field}
upCd="SITEKNDCD"
onValueChange={(value) => {
handleViewStateChange("siteKndCd")(value);
field.onChange(value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isModifyMode && (
<IPManagementDialog
triggerDisabled={viewState.siteKndCd === "O"}
/>
)}
</div>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 디자인</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="siteSkn"
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
{...field}
upCd="SITESKINCD"
onValueChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="siteNm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
placeholder="사이트명을 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 설명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="siteExpln"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
placeholder="사이트 설명을 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
{isModifyMode && (
<TableRow>
<TableHead>사이트 도메인</TableHead>
<TableCell className="border">
<div className="flex gap-2">
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
{representativeDomain}
</div>
<DomainManagementDialog
handleSave={handleDomainSave}
/>
</div>
</TableCell>
</TableRow>
)}
<TableRow>
<TableHead>사이트 하단 표기 주소</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="siteAddr"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
placeholder="사이트 하단 주소를 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 하단 표기 전화번호</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="telNo"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
placeholder="사이트 하단 전화번호를 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 하단 표기 팩스번호</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="faxNo"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
placeholder="사이트 하단 팩스번호를 입력하십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 하단 표기 내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="lwndCn"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>북마크 아이콘</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="bkmkIcon"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
className="!text-[1.3rem]"
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>기본 사이트 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="bscSiteYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="bscSiteYn"
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="bscSiteY" />
<Label htmlFor="bscSiteY">예</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="bscSiteN" />
<Label htmlFor="bscSiteN">아니오</Label>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="useYn"
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="useY" />
<Label htmlFor="useY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="useN" />
<Label htmlFor="useN">사용안함</Label>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button
size="lg"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
<Dialog open={!!message} onOpenChange={onCancel}>
<DialogContent aria-describedby={undefined}>
<div className="py-10">
<p className="text-center text-[1.4rem]">{message}</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button color="red" onClick={onConfirm}>
삭제
</Button>
</DialogClose>
<DialogClose asChild>
<Button color="white" onClick={onCancel}>
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export { SiteForm };
+21 -16
View File
@@ -2,19 +2,26 @@
import "@common/assets/styles/grid.css";
import { type ISite, type ISiteParams } from "@/entities/site";
import { SiteApi } from "@/entities/site/api/site-service";
import type { ISiteQuery } from "@/entities/site/model/site-interface";
import { ENDPOINT } from "@/shared/config/api";
import { Button, DataTable, Input } from "@common/components";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { columns } from "../model/__mocks__";
import { useRouter } from "next/router";
import { useState } from "react";
import { columns } from "../model/table-columns";
import { SitePageLayout } from "./layout";
function SitePage() {
const [siteQuery, setSiteQuery] = useState<ISiteQuery>({
const router = useRouter();
const { data: siteInfo, refetch } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
queryFn: () => SiteApi.siteInfo(siteQuery),
enabled: false,
});
const [siteQuery, setSiteQuery] = useState<ISiteParams>({
siteNm: "",
siteKndCd: "",
useYn: "",
@@ -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<HTMLFormElement>) => {
@@ -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() {
<Button onClick={() => router.push("/site/register")}>등록</Button>
</div>
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
<DataTable table={table} />
<DataTable table={table} onRowClick={handleRowClick} />
</div>
</div>
</div>
+1
View File
@@ -10,6 +10,7 @@ export const ENDPOINT = {
},
CMS_SERVICE: {
SITES: "/cms-service/api/v1/sites",
CODE: "/cms-service/api/v1/code/cache",
},
};
-2
View File
@@ -1,5 +1,3 @@
"use client";
import Cookies from "js-cookie";
export function getCookie(name: string): string | undefined {
@@ -0,0 +1,20 @@
"use client";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthProvider } from "./auth-provider";
import { CookieProvider } from "./cookie-provider";
import { QueryProvider } from "./query-provider";
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
<QueryProvider>
<CookieProvider>
<AuthProvider>
<ConfirmProvider>{children}</ConfirmProvider>
</AuthProvider>
</CookieProvider>
</QueryProvider>
);
};
export { AppProvider };
+15 -11
View File
@@ -1,12 +1,12 @@
"use client";
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 { AxiosError } from "axios";
import { LoginApi } from "@/entities/auth/login";
interface IUserInfo {
@@ -50,28 +50,32 @@ export interface AuthContextType {
export const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const [userInfo, setUserInfo] = useState<IUserInfo | null>(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<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/${getCookie(AUTH_USER_ID)}`,
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
);
setUserInfo(response.data);
return response;
} catch (error) {
console.error(error);
if (error instanceof AxiosError) {
if (error.response?.status === 401) {
try {
await LoginApi.silentRefresh();
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
setUserInfo(null);
}
}
try {
await LoginApi.silentRefresh();
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
setUserInfo(null);
router.push("/auth/login");
}
}
},
+3 -3
View File
@@ -1,3 +1,3 @@
export { AuthProvider, useAuth } from "./auth-provider";
export { CookieProvider, useCookie } from "./cookie-provider";
export { QueryProvider } from "./query-provider";
export { AppProvider } from "./app-provider";
export { useAuth } from "./auth-provider";
export { useCookie } from "./cookie-provider";
@@ -1,5 +1,3 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
+175
View File
@@ -0,0 +1,175 @@
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<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
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 <FormField>");
}
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<FormItemContextValue>(
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[1rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};
+32 -1
View File
@@ -1,7 +1,38 @@
import type { NextConfig } from "next";
import webpack from "webpack";
const nextConfig: NextConfig = {
/* config options here */
webpack: (config) => {
config.plugins.push(
new webpack.DefinePlugin({
"import.meta.env.VITE_PORT": JSON.stringify(
process.env.NEXT_PUBLIC_PORT,
),
"import.meta.env.VITE_PROXY_HOST": JSON.stringify(
process.env.NEXT_PUBLIC_PROXY_HOST,
),
"import.meta.env.VITE_SERVER_API_URL": JSON.stringify(
process.env.NEXT_PUBLIC_SERVER_API_URL,
),
"import.meta.env.VITE_SITE_ID": JSON.stringify(process.env.SITE_ID),
"import.meta.env.VITE_CLAIM_NAME": JSON.stringify(
process.env.NEXT_PUBLIC_CLAIM_NAME,
),
"import.meta.env.VITE_ACCESS_TOKEN": JSON.stringify(
process.env.NEXT_PUBLIC_ACCESS_TOKEN,
),
"import.meta.env.VITE_REFRESH_TOKEN": JSON.stringify(
process.env.NEXT_PUBLIC_REFRESH_TOKEN,
),
"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),
}),
);
return config;
},
};
export default nextConfig;
@@ -23,6 +23,7 @@
--border: #e5e5e5;
--input: #e5e5e5;
--label: #666666;
--placeholder: #a2a2a2;
--ring: #0a0a0a;
--chart-1: #2094fa;
--chart-2: #00dfe5;
@@ -31,6 +32,10 @@
--chart-5: #ff7865;
--chart-6: #e65db5;
--radius: 1rem;
--field: #fff;
--form: #fff;
--container: #f1f6f9;
--title: #4c5e6f;
}
[data-mode="dark"] {
--background: #0a0a0a;
@@ -52,6 +57,7 @@
--border: #262626;
--input: #262626;
--label: #f1f1f1;
--placeholder: #737373;
--ring: #d4d4d4;
--chart-1: #66b3ff;
--chart-2: #66f0f2;
@@ -60,5 +66,9 @@
--chart-5: #ffbcad;
--chart-6: #f4a0d3;
--radius: 1rem;
--field: #5b6269;
--form: #1a1a1a;
--container: #0a0a0a;
--title: #fff;
}
}
+2
View File
@@ -34,10 +34,12 @@
"@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",
"@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",
@@ -1 +1,2 @@
export * from "./client";
export * from "./user";
+5 -7
View File
@@ -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<IUser>(`${ENDPOINT.USER_SERVICE.USERS}/${userUniqId}`),
});
export const getUserInfo = async (userUniqId: string) => {
const response = await client.get<IUser>(
`${ENDPOINT.USER_SERVICE.USERS}/${userUniqId}`,
);
return response.data;
};
@@ -1,5 +1,3 @@
"use client";
import "./accordion.css";
import * as React from "react";
@@ -22,7 +22,7 @@ const meta: Meta<typeof Bar> = {
description: {
component: `
- [Docs](https://react-chartjs-2.js.org/)
`,
`,
},
source: {
code: `
@@ -1,5 +1,3 @@
"use client";
import * as React from "react";
import { ko } from "date-fns/locale";
import { ChevronLeft, ChevronRight } from "lucide-react";
@@ -14,7 +14,7 @@ const meta: Meta<typeof DataTable> = {
description: {
component: `
- [Docs](https://tanstack.com/table/v8/docs/introduction)
`,
`,
},
source: {
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 (
<Table className="w-full">
<THead table={table} />
<tbody>
{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) => {
const { meta } = cell.column.columnDef;
return (
@@ -1,5 +1,3 @@
"use client";
import "./date-picker.css";
import { format } from "date-fns";
@@ -26,7 +26,7 @@ export const Default: Story = {
<DialogTrigger asChild>
<Button>다이얼로그 열기</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogContent className="sm:max-w-[42.5rem]">
<DialogHeader>
<DialogTitle>다이얼로그 제목</DialogTitle>
</DialogHeader>
@@ -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";
@@ -36,14 +37,17 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
<VisuallyHidden asChild>
<DialogTitle>Dialog</DialogTitle>
</VisuallyHidden>
{children}
<DialogPrimitive.Close className="absolute right-6 top-6 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="size-6" />
<X className="size-6 text-muted-foreground" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
@@ -71,7 +75,7 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"-mx-6 flex flex-col-reverse border-t border-t-[#eaeef5] px-6 pt-6 sm:flex-row sm:justify-end sm:space-x-2",
"-mx-6 flex flex-col-reverse border-t border-t-border px-6 pt-6 sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
@@ -86,7 +90,7 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"-mx-6 border-b border-b-[#eaeef5] px-6 pb-6 text-[1.4rem] font-semibold leading-none tracking-tight",
"-mx-6 border-b border-b-border px-6 pb-6 text-[1.4rem] font-semibold leading-none tracking-tight text-foreground",
className,
)}
{...props}
@@ -1,8 +1,7 @@
"use client";
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,
@@ -12,27 +11,32 @@ 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 (
<header className="sticky top-0 box-border flex h-[5rem] w-full items-center justify-between border-b border-solid border-border">
<header className="sticky top-0 box-border flex h-[5rem] w-full items-center justify-between border-b border-solid border-border bg-background">
<div className="flex items-center">
<div className="MobMenuBtn">
<img src={images.menuBtn} />
</div>
<h1 className="flex items-center gap-4">
<img src={images.logo} className="h-[3rem]" alt="logo" />
<span className="text-[1.8rem] font-semibold">통합플랫폼관리</span>
<span className="text-[1.8rem] font-semibold text-foreground">
통합플랫폼관리
</span>
</h1>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label">
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
{userInfo?.data.userName}
{userInfo?.userName}
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</DropdownMenuTrigger>
@@ -40,7 +44,7 @@ export function Header() {
<DropdownMenuItem>프로필 설정</DropdownMenuItem>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">최종접속일</p>
<p>{userInfo?.data.lastLoginDate}</p>
<p>{userInfo?.lastLoginDate}</p>
</div>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">접속 IP</p>
@@ -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";
@@ -3,14 +3,15 @@ import * as React from "react";
import { cn } from "../../lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
({ className, type, value, ...props }, ref) => {
return (
<input
type={type}
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",
"flex h-[3.2rem] w-full rounded-md border border-input bg-form 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-placeholder 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,
)}
value={value ?? ""}
ref={ref}
{...props}
/>
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
"text-[1.3rem] font-medium leading-none text-label peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
@@ -1,7 +1,9 @@
export function Footer() {
return (
<footer className="flex h-[5rem] w-full shrink-0 items-center border-t border-solid border-border p-4">
<p>© 2025 Ona And All. All Rights Reserved.</p>
<footer className="flex h-[5rem] w-full shrink-0 items-center border-t border-solid border-border bg-background p-4">
<p className="text-foreground">
© 2025 Ona And All. All Rights Reserved.
</p>
</footer>
);
}
@@ -1 +1,2 @@
export { BaseLayout } from "./base";
export { VBaseLayout } from "./vbase";
@@ -348,7 +348,7 @@ function IPManagementDialog() {
<DialogTrigger asChild>
<Button>IP 관리</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogContent className="sm:max-w-[60rem]">
<DialogHeader>
<DialogTitle>IP 관리</DialogTitle>
</DialogHeader>
@@ -2,7 +2,7 @@ export function LNB() {
return (
<div
id="lnb"
className="!w-[22rem] overflow-auto border-r border-solid border-border"
className="!w-[22rem] overflow-auto border-r border-solid border-border bg-background"
>
<ul>
<li id="m_dashboard">
@@ -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 (
<QueryClientProvider client={queryClient}>
<div>
<Header viewOnly />
<div className="flex h-[calc(100vh-50px)]">
<LNB />
<div className="flex flex-1 flex-col">
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
{children}
</div>
<Footer />
</div>
</div>
</div>
</QueryClientProvider>
);
}
@@ -13,7 +13,6 @@ import {
} from "chart.js";
import { Line } from "react-chartjs-2";
import { fakerKO as faker } from "@faker-js/faker";
import { getCssVariableValue } from "../../lib/utils";
const meta: Meta<typeof Line> = {
title: "Chart/Line",
@@ -23,7 +22,7 @@ const meta: Meta<typeof Line> = {
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)",
},
],
};
@@ -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<typeof Pie> = {
title: "Chart/Pie",
@@ -19,7 +11,7 @@ const meta: Meta<typeof Pie> = {
description: {
component: `
- [Docs](https://react-chartjs-2.js.org/)
`,
`,
},
source: {
code: `
@@ -83,42 +75,31 @@ type Story = StoryObj<typeof Pie>;
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,
},
@@ -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;
@@ -0,0 +1,157 @@
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-form 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}
>
{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-[18rem]">
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">옵션 1</SelectItem>
<SelectItem value="option2">옵션 2</SelectItem>
<SelectItem value="option3">옵션 3</SelectItem>
</SelectContent>
</Select>
),
};
@@ -13,7 +13,11 @@ const Table = React.forwardRef<HTMLTableElement, TableProps>(
<div className="relative w-full">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className, variant)}
className={cn(
"table-ui w-full caption-bottom text-sm",
className,
variant,
)}
{...props}
/>
</div>
@@ -63,7 +67,7 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
"hover:bg-muted/50 border-b transition-colors data-[state=selected]:bg-muted",
"border-b bg-background transition-colors data-[state=selected]:bg-muted",
className,
)}
{...props}
@@ -89,7 +93,10 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("align-middle [&:has([role=checkbox])]:pr-0", className)}
className={cn(
"align-middle text-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
@@ -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] bg-[#fff] 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 {
.table-ui th {
@apply bg-[#5987bf] px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2] text-[#fff];
}
.table-ui 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];
}
.table-ui.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];
}
.table-ui.secondary td {
@apply border border-border;
}
.line td {
@apply border-b border-b-[#e0e6ea];
.table-ui.line th {
@apply border-0 border-b-2 border-t border-b-[#d4e0ea] border-t-[#d4e0ea] bg-[#fff] text-[#555];
}
.table-ui.line td {
@apply border-b border-b-border;
}
[data-mode="dark"] {
.table-ui th {
@apply bg-[#5e7b9c] text-[#fff];
}
.table-ui.secondary th {
@apply bg-[#1a1c1d];
}
.table-ui.line th {
@apply bg-[#0a0a0a];
}
}
}
@@ -21,7 +21,7 @@ type Story = StoryObj<typeof Tabs>;
export const Default: Story = {
render: () => (
<Tabs defaultValue="account" className="w-[400px]">
<Tabs defaultValue="account" className="w-[40rem]">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
@@ -38,7 +38,7 @@ export const Default: Story = {
export const Underline: Story = {
render: () => (
<Tabs defaultValue="account" className="w-[400px]" variant="underline">
<Tabs defaultValue="account" className="w-[40rem]" variant="underline">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
@@ -55,7 +55,7 @@ export const Underline: Story = {
export const Outline: Story = {
render: () => (
<Tabs defaultValue="account" className="w-[400px]" variant="outline">
<Tabs defaultValue="account" className="w-[40rem]" variant="outline">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
@@ -19,7 +19,7 @@ const meta: Meta<typeof Tree> = {
description: {
component: `
- [Docs](https://github.com/minop1205/react-dnd-treeview/blob/next/README.md)
`,
`,
},
},
},
+2 -2
View File
@@ -28,12 +28,12 @@ class Client {
if (!token || !userId) {
throw new AxiosError(
"[Client] Missing required authentication tokens",
"The authentication token is invalid.",
"UNAUTHORIZED",
config,
null,
{
data: { message: "Missing required authentication tokens" },
data: { message: "The authentication token is invalid." },
status: 401,
statusText: "Unauthorized",
headers: this.axiosInstance.defaults.headers,
+158 -1
View File
@@ -42,6 +42,18 @@ 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)
'@radix-ui/react-label':
specifier: ^2.1.2
version: 2.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-slot':
specifier: ^1.1.2
version: 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@tanstack/react-query':
specifier: ^5.67.1
version: 5.67.1(react@18.3.1)
@@ -54,6 +66,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
@@ -63,9 +78,15 @@ importers:
react-chartjs-2:
specifier: ^5.3.0
version: 5.3.0(chart.js@4.4.8)(react@18.3.1)
react-hook-form:
specifier: ^7.54.2
version: 7.54.2(react@18.3.1)
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.17)
zod:
specifier: ^3.24.2
version: 3.24.2
devDependencies:
'@eslint/eslintrc':
specifier: ^3
@@ -148,6 +169,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)
@@ -160,6 +184,9 @@ importers:
'@radix-ui/react-tabs':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-visually-hidden':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@tanstack/react-query':
specifier: ^5.67.1
version: 5.67.1(react@18.3.1)
@@ -1116,6 +1143,24 @@ 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:
react-hook-form: ^7.0.0
dependencies:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.54.2(react@18.3.1)
dev: false
/@humanfs/core@0.19.1:
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -2077,6 +2122,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 +2354,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
@@ -2464,6 +2569,10 @@ packages:
engines: {node: '>=18'}
dev: true
/@standard-schema/utils@0.3.0:
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
dev: false
/@storybook/addon-actions@8.6.0(storybook@8.6.0):
resolution: {integrity: sha512-lI03stcI28wN8trjp7zx0PzN1NRxSpU/551slzEhSM0tghcLxcfLZR/wh4Jp0T68Obj7z//YXPttZ9PBFajs6w==}
peerDependencies:
@@ -3606,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
@@ -3984,6 +4104,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:
@@ -5375,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'}
@@ -5623,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
@@ -6399,6 +6548,15 @@ packages:
react: 18.3.1
scheduler: 0.23.2
/react-hook-form@7.54.2(react@18.3.1):
resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^18.3.1
dependencies:
react: 18.3.1
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -7824,4 +7982,3 @@ packages:
/zod@3.24.2:
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
dev: true
+5
View File
@@ -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)",
@@ -49,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": {