iipbs_cs #8

Merged
chanii8 merged 20 commits from iipbs_cs into main 2025-03-17 18:10:32 +09:00
99 changed files with 3123 additions and 664 deletions
+4 -3
View File
@@ -4,8 +4,9 @@
# dependencies
node_modules
# build
.next
dist
# misc
.DS_Store
# env
.env*
+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;
+1 -1
View File
@@ -5,7 +5,7 @@ import {
REFRESH_TOKEN,
SERVER_API_URL,
SITE_ID,
} from "@/shared/config/env";
} from "@/shared/config";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
-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*",
};
-12
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(
@@ -30,20 +27,11 @@ const nextConfig: NextConfig = {
"import.meta.env.VITE_AUTH_USER_ID": JSON.stringify(
process.env.NEXT_PUBLIC_AUTH_USER_ID,
),
"process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID),
}),
);
return config;
},
async rewrites() {
return [
{
source: "/server/:path*",
destination: `${serverApiUrl}/:path*`,
},
];
},
};
export default nextConfig;
+9 -1
View File
@@ -11,14 +11,22 @@
"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",
"zustand": "^5.0.3"
},
"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 "./providers";
const App = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
<html lang="ko">
<body>
<QueryProvider>
<CookieProvider>
<AuthProvider>{children}</AuthProvider>
</CookieProvider>
</QueryProvider>
<AppProvider>{children}</AppProvider>
</body>
</html>
);
@@ -0,0 +1,19 @@
"use client";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthWrapper } from "@/entities/auth";
import { QueryProvider, StoreProvider } from "@/shared/providers";
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
<StoreProvider>
<QueryProvider>
<ConfirmProvider>
<AuthWrapper>{children}</AuthWrapper>
</ConfirmProvider>
</QueryProvider>
</StoreProvider>
);
};
export { AppProvider };
+1
View File
@@ -0,0 +1 @@
export { AppProvider } from "./app-provider";
+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];
}
}
}
@@ -1,6 +1,11 @@
import { AxiosError } from "axios";
import { ENDPOINT, JWT_EXPIRATION_TIME } from "@/shared/config/api";
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env";
import {
ENDPOINT,
JWT_EXPIRATION_TIME,
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
} from "@/shared/config";
import type { LoginService, ILogin } from "../model/login-interface";
import { tokenManager } from "../lib/token-manager";
@@ -31,7 +36,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 +47,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 +65,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}`);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
export { LoginApi } from "./api/login-service";
export { AuthWrapper } from "./ui/auth-wrapper";
@@ -1,5 +1,5 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env";
import { getCookie, setCookie, removeCookie } from "@/shared/lib/cookie";
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
export class TokenManager {
private static instance: TokenManager;
-1
View File
@@ -1 +0,0 @@
export { LoginApi } from "./api/login-service";
@@ -0,0 +1,46 @@
"use client";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { client } from "@/shared/api";
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
import { getCookie, useUserStore } from "@/shared/lib";
import type { IUserInfo } from "@/shared/store";
import { LoginApi } from "../api/login-service";
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const { setUserInfo } = useUserStore((state) => state);
useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: async () => {
try {
const userId = getCookie(AUTH_USER_ID);
if (!userId) {
throw new Error("Missing required authentication tokens");
}
const response = await client.get<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
);
setUserInfo(response.data);
return response;
} catch (error) {
console.error(error);
try {
await LoginApi.silentRefresh();
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
setUserInfo(null);
router.push("/auth/login");
}
}
},
});
return <>{children}</>;
};
export { AuthWrapper };
@@ -0,0 +1,14 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { CodeService, ICode } from "../model/code-interface";
export class CodeApiService implements CodeService {
public async getCode(upCd: string): Promise<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";
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 };
@@ -1,20 +0,0 @@
import { client } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config/api";
import type {
ISiteQuery,
ISiteInfo,
SiteService,
} from "../model/site-interface";
export class SiteApiService implements SiteService {
public async siteInfo(query: ISiteQuery): Promise<ISiteInfo> {
const response = await client.get<ISiteInfo>(
`${ENDPOINT.CMS_SERVICE.SITES}`,
{ params: query },
);
return response.data;
}
}
export const SiteApi = new SiteApiService();
-1
View File
@@ -1 +0,0 @@
export { SiteApi } from "./api/site-service";
@@ -1,49 +0,0 @@
export interface ISiteQuery {
siteNm: string;
siteKndCd: string;
useYn: string;
page: number;
size: number;
sort: string[];
}
export interface ISite {
siteId: string;
siteNm: string;
siteKindCode: string;
bscSiteYn: string;
useYn: string;
}
export interface ISiteInfo {
totalPages: number;
totalElements: number;
number: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
size: number;
content: ISite[];
pageable: {
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
};
numberOfElements: number;
first: boolean;
last: boolean;
empty: boolean;
}
export interface SiteService {
siteInfo(query: ISiteQuery): Promise<ISiteInfo>;
}
@@ -1,15 +0,0 @@
import { client } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config/api";
import type { IUser, IUserInfo, UserService } from "../model/user-interface";
export class UsersApiService implements UserService {
public async userInfo(payload: IUser): Promise<IUserInfo> {
const response = await client.get<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/${payload.userUniqId}`,
);
return response.data;
}
}
export const UserApi = new UsersApiService();
-1
View File
@@ -1 +0,0 @@
export { useUser } from "./lib/use-user";
-20
View File
@@ -1,20 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config/api";
import { AUTH_USER_ID } from "@/shared/config/env";
import { useCookie } from "@/shared/provider";
import { UserApi } from "../api/user-service";
const useUser = () => {
const { getCookie } = useCookie();
const { data } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: () =>
UserApi.userInfo({
userUniqId: getCookie(AUTH_USER_ID) ?? "",
}),
});
return { userInfo: data };
};
export { useUser };
@@ -2,7 +2,7 @@
import Image from "next/image";
import { images } from "@common/assets";
import { LoginApi } from "@/entities/auth/login";
import { LoginApi } from "@/entities/auth";
import "./login.css";
function LoginPage() {
@@ -0,0 +1,54 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
siteFormSchema,
type ISiteInfo,
type SiteService,
type ISiteDetail,
type ISiteParams,
type SiteFormData,
} from "../model/site-interface";
export class SiteApiService implements SiteService {
public async siteInfo(params: ISiteParams): Promise<ISiteInfo> {
const response = await client.get<ISiteInfo>(
`${ENDPOINT.CMS_SERVICE.SITES}`,
{ 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;
}
}
export const SiteApi = new SiteApiService();
+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";
@@ -0,0 +1,87 @@
import { z } from "zod";
export interface ISiteParams {
siteNm: string;
siteKndCd: string;
useYn: string;
page: number;
size: number;
sort: string[];
}
export interface ISite {
siteId: string;
siteNm: string;
siteKindCode: string;
bscSiteYn: string;
siteAddr: string;
useYn: string;
regDt: string;
rgTrNm: string;
}
export interface ISiteInfo {
totalPages: number;
totalElements: number;
number: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
size: number;
content: ISite[];
pageable: {
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
};
numberOfElements: number;
first: boolean;
last: boolean;
empty: boolean;
}
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>;
}
@@ -1,5 +1,6 @@
import dayjs from "dayjs";
import { createColumnHelper } from "@tanstack/react-table";
import type { ISite } from "@/entities/site/model/site-interface";
import type { ISite } from "./site-interface";
const columnHelper = createColumnHelper<ISite>();
@@ -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");
},
}),
];
+54
View File
@@ -0,0 +1,54 @@
"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 } from "../api/site-service";
import { siteFormSchema, type SiteFormData } from "../model/site-interface";
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 { ENDPOINT } from "@/shared/config";
import { SiteApi } from "../api/site-service";
import {
siteFormSchema,
type ISiteDetail,
type SiteFormData,
} from "../model/site-interface";
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 {
FormItem,
FormControl,
FormField,
Form,
FormMessage,
} from "@/shared/ui/form";
import { type ISiteDetail, type SiteFormData } from "../model/site-interface";
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
@@ -7,14 +7,21 @@ import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { Button, Input, DataTable } from "@common/components";
import { SiteApi } from "@/entities/site/api/site-service";
import { ENDPOINT } from "@/shared/config/api";
import { columns } from "../model/__mocks__";
import { ENDPOINT } from "@/shared/config";
import { SiteApi } from "../api/site-service";
import { type ISite, type ISiteParams } from "../model/site-interface";
import { columns } from "../model/table-columns";
import { SitePageLayout } from "./layout";
import type { ISiteQuery } from "@/entities/site/model/site-interface";
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
@@ -0,0 +1 @@
export { client } from "./client";
+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
@@ -0,0 +1,2 @@
export * from "./api";
export * from "./env";
-2
View File
@@ -1,5 +1,3 @@
"use client";
import Cookies from "js-cookie";
export function getCookie(name: string): string | undefined {
+2
View File
@@ -0,0 +1,2 @@
export { getCookie, setCookie, removeCookie } from "./cookie";
export { useUserStore } from "./use-user-store";
+14
View File
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import { type UserStore } from "@/shared/store/user-store";
import { StoreContext } from "../providers/store-provider";
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useUserStore must be used within StoreProvider");
}
return useStore(storeContext.userStore, selector);
};
@@ -1,97 +0,0 @@
"use client";
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 {
regDt: string;
mdfcnDt: string;
rgtr: string;
mdfr: string;
userUnqId: string;
userId: string;
userName: string;
email: string;
encryptedPassword: string;
refreshToken: string;
userStateCode: string;
lastLoginDate: string;
loginFailCount: number;
pswdHnt: string;
pswdAns: string;
mphn: string;
telno: string;
zip: string;
addr: string;
daddr: string;
laeSeCd: string;
ogdpSeCd: string;
aprvYn: string;
autzrId: string;
expryYmd: string;
lastPswdChgDt: string;
googleId: string;
kakaoId: string;
naverId: string;
isSocialUser: boolean;
hasPassword: boolean;
}
export interface AuthContextType {
userInfo: IUserInfo | null;
}
export const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [userInfo, setUserInfo] = useState<IUserInfo | null>(null);
useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: async () => {
try {
const response = await client.get<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/${getCookie(AUTH_USER_ID)}`,
);
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);
}
}
}
}
},
});
const authContextValue: AuthContextType = {
userInfo,
};
return (
<AuthContext.Provider value={authContextValue}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
@@ -1,38 +0,0 @@
"use client";
import { createContext, useContext } from "react";
import { getCookie, setCookie, removeCookie } from "../lib/cookie";
export interface CookieContextType {
getCookie: (name: string) => string | undefined;
setCookie: (
name: string,
value: string,
options: Cookies.CookieAttributes,
) => void;
removeCookie: (name: string, options: Cookies.CookieAttributes) => void;
}
export const CookieContext = createContext<CookieContextType | null>(null);
export function CookieProvider({ children }: { children: React.ReactNode }) {
const cookieContextValue: CookieContextType = {
getCookie,
setCookie,
removeCookie,
};
return (
<CookieContext.Provider value={cookieContextValue}>
{children}
</CookieContext.Provider>
);
}
export function useCookie(): CookieContextType {
const context = useContext(CookieContext);
if (!context) {
throw new Error("useCookie must be used within a CookieProvider");
}
return context;
}
-3
View File
@@ -1,3 +0,0 @@
export { AuthProvider, useAuth } from "./auth-provider";
export { CookieProvider, useCookie } from "./cookie-provider";
export { QueryProvider } from "./query-provider";
+2
View File
@@ -0,0 +1,2 @@
export { QueryProvider } from "./query-provider";
export { StoreProvider } from "./store-provider";
@@ -1,5 +1,3 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
@@ -0,0 +1,27 @@
"use client";
import { createContext, useRef } from "react";
import { createUserStore } from "@/shared/store/user-store";
type StoreContextType = {
userStore: ReturnType<typeof createUserStore>;
};
export const StoreContext = createContext<StoreContextType | undefined>(
undefined,
);
export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
const storeRef = useRef<StoreContextType | null>(null);
if (storeRef.current === null) {
storeRef.current = {
userStore: createUserStore(),
};
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
);
};
+1
View File
@@ -0,0 +1 @@
export { createUserStore, type IUserInfo } from "./user-store";
@@ -1,6 +1,4 @@
export interface IUser {
userUniqId: string;
}
import { createStore } from "zustand/vanilla";
export interface IUserInfo {
regDt: string;
@@ -36,6 +34,23 @@ export interface IUserInfo {
hasPassword: boolean;
}
export interface UserService {
userInfo: (payload: IUser) => Promise<IUserInfo>;
}
export type UserState = {
userInfo: IUserInfo | null;
};
export type UserActions = {
setUserInfo: (userInfo: IUserInfo | null) => void;
};
export type UserStore = UserState & UserActions;
export const defaultInitState: UserState = {
userInfo: null,
};
export const createUserStore = (initState: UserState = defaultInitState) => {
return createStore<UserStore>()((set) => ({
...initState,
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
}));
};
+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,
};
@@ -23,6 +23,7 @@
--border: #e5e5e5;
--input: #e5e5e5;
--label: #666666;
--placeholder: #a2a2a2;
--ring: #0a0a0a;
--chart-1: #2094fa;
--chart-2: #00dfe5;
@@ -31,6 +32,18 @@
--chart-5: #ff7865;
--chart-6: #e65db5;
--radius: 1rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--field: #fff;
--form: #fff;
--container: #f1f6f9;
--title: #4c5e6f;
}
[data-mode="dark"] {
--background: #0a0a0a;
@@ -52,6 +65,7 @@
--border: #262626;
--input: #262626;
--label: #f1f1f1;
--placeholder: #737373;
--ring: #d4d4d4;
--chart-1: #66b3ff;
--chart-2: #66f0f2;
@@ -60,5 +74,17 @@
--chart-5: #ffbcad;
--chart-6: #f4a0d3;
--radius: 1rem;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--field: #5b6269;
--form: #1a1a1a;
--container: #0a0a0a;
--title: #fff;
}
}
+4
View File
@@ -34,10 +34,14 @@
"@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-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-visually-hidden": "^1.1.2",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-table": "^8.21.2",
"@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;
};
@@ -0,0 +1,21 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}
@@ -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>
),
};
@@ -0,0 +1,29 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "../../lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
@@ -0,0 +1,140 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "../../lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 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-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
@@ -0,0 +1,779 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react";
import { useIsMobile } from "../../hooks/use-mobile";
import { cn } from "../../lib/utils";
import { Button } from "../button";
import { Input } from "../input";
import { Separator } from "../separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "../sheet";
import { Skeleton } from "../skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref,
) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className,
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
},
);
SidebarProvider.displayName = "SidebarProvider";
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref,
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className,
)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)}
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
},
);
Sidebar.displayName = "Sidebar";
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
});
SidebarTrigger.displayName = "SidebarTrigger";
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
});
SidebarRail.displayName = "SidebarRail";
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex w-full flex-1 flex-col bg-background",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
{...props}
/>
);
});
SidebarInset.displayName = "SidebarInset";
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className,
)}
{...props}
/>
);
});
SidebarInput.displayName = "SidebarInput";
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
});
SidebarHeader.displayName = "SidebarHeader";
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
});
SidebarFooter.displayName = "SidebarFooter";
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
);
});
SidebarSeparator.displayName = "SidebarSeparator";
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
});
SidebarContent.displayName = "SidebarContent";
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
});
SidebarGroup.displayName = "SidebarGroup";
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div";
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
});
SidebarGroupLabel.displayName = "SidebarGroupLabel";
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
});
SidebarGroupAction.displayName = "SidebarGroupAction";
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
));
SidebarGroupContent.displayName = "SidebarGroupContent";
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
));
SidebarMenu.displayName = "SidebarMenu";
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
));
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref,
) => {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
);
},
);
SidebarMenuButton.displayName = "SidebarMenuButton";
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className,
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = "SidebarMenuAction";
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
));
SidebarMenuBadge.displayName = "SidebarMenuBadge";
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
));
SidebarMenuSub.displayName = "SidebarMenuSub";
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />);
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};
@@ -0,0 +1,15 @@
import { cn } from "../../lib/utils";
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton };
@@ -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>
@@ -0,0 +1,28 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "../../lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -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,
+240 -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,18 @@ 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
zustand:
specifier: ^5.0.3
version: 5.0.3(@types/react@18.3.18)(react@18.3.1)
devDependencies:
'@eslint/eslintrc':
specifier: ^3
@@ -148,6 +172,12 @@ 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-separator':
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)
'@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 +190,12 @@ 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-tooltip':
specifier: ^1.1.8
version: 1.1.8(@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 +1152,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 +2131,66 @@ 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-separator@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-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==}
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/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:
@@ -2174,6 +2288,37 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-tooltip@1.1.8(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==}
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/primitive': 1.1.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-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-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-presence': 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-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-controllable-state': 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)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
@@ -2269,6 +2414,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 +2629,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 +3775,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 +4164,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 +5559,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 +5811,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 +6608,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 +8042,25 @@ packages:
/zod@3.24.2:
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
dev: true
/zustand@5.0.3(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': ^18.3.18
immer: '>=9.0.6'
react: ^18.3.1
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
dependencies:
'@types/react': 18.3.18
react: 18.3.1
dev: false
+15
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,20 @@ export default {
4: "var(--chart-4)",
5: "var(--chart-5)",
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
field: "var(--field)",
form: "var(--form)",
container: "var(--container)",
title: "var(--title)",
},
keyframes: {
"accordion-down": {