Merge commit 'cd03d6f42ebb9e3e9ceda9d9681d298cea9ca8ff' into iipbs/common/main

This commit is contained in:
truthwing
2025-03-17 11:20:28 +09:00
20 changed files with 508 additions and 348 deletions
+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>;
}
+19
View File
@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from "next/server";
const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
if (pathname.startsWith("/server")) {
return NextResponse.rewrite(
`${serverApiUrl}${pathname.replace("/server", "")}`,
);
}
return NextResponse.next();
}
export const config = {
matcher: "/server/:path*",
};
-11
View File
@@ -1,9 +1,6 @@
import type { NextConfig } from "next";
import webpack from "webpack";
const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3000";
const nextConfig: NextConfig = {
webpack: (config) => {
config.plugins.push(
@@ -36,14 +33,6 @@ const nextConfig: NextConfig = {
return config;
},
async rewrites() {
return [
{
source: "/server/:path*",
destination: `${serverApiUrl}/:path*`,
},
];
},
};
export default nextConfig;
+1
View File
@@ -11,6 +11,7 @@
"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",
+12 -5
View File
@@ -25,6 +25,15 @@ export class SiteApiService implements SiteService {
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>(
@@ -34,11 +43,9 @@ export class SiteApiService implements SiteService {
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,
public async deleteSite(siteId: string): Promise<void> {
const response = await client.delete<void>(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
);
return response.data;
}
@@ -1,3 +1,5 @@
"use client";
import Image from "next/image";
import { images } from "@common/assets";
import { LoginApi } from "@/entities/auth/login";
+16 -5
View File
@@ -40,11 +40,17 @@ function ModifyPage({ siteId }: { siteId: string }) {
const { mutateAsync: modifySite } = useMutation({
mutationFn: (data: SiteFormData) => SiteApi.modifySite(siteId, data),
onSuccess: () => {
setIsModifySuccess(true);
setMessage("수정되었습니다.");
},
});
const [isModifySuccess, setIsModifySuccess] = useState(false);
const { mutateAsync: deleteSite } = useMutation({
mutationFn: () => SiteApi.deleteSite(siteId),
onSuccess: () => {
setMessage("삭제되었습니다.");
},
});
const [message, setMessage] = useState("");
const form = useForm<SiteFormData>({
resolver: zodResolver(siteFormSchema),
@@ -64,11 +70,16 @@ function ModifyPage({ siteId }: { siteId: string }) {
return (
<SitePageLayout>
<SiteForm form={form} siteId={siteId} handleSave={modifySite} />
<Dialog open={isModifySuccess} onOpenChange={() => router.back()}>
<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]">수정되었습니다.</p>
<p className="text-center text-[1.4rem]">{message}</p>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
+349 -308
View File
@@ -1,8 +1,13 @@
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,
@@ -32,10 +37,12 @@ type SiteFormProps = {
form: UseFormReturn<SiteFormData>;
siteId?: string;
handleSave: (data: SiteFormData) => void;
handleDelete: () => void;
};
function SiteForm({ form, siteId, handleSave }: SiteFormProps) {
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>("");
@@ -56,334 +63,368 @@ function SiteForm({ form, siteId, handleSave }: SiteFormProps) {
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">
<>
<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="siteKndCd"
name="siteId"
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
<Input
{...field}
upCd="SITEKNDCD"
onValueChange={(value) => {
handleViewStateChange("siteKndCd")(value);
field.onChange(value);
}}
className="w-[12rem] !text-[1.3rem]"
disabled={isModifyMode}
/>
</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 && (
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 도메인</TableHead>
<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} />
<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="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>
<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>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button size="lg" color="white" onClick={() => router.back()}>
목록
</Button>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red">
삭제
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button
size="lg"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
)}
</div>
</form>
</Form>
<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>
</>
);
}
+2 -2
View File
@@ -1,6 +1,6 @@
"use client";
import { BaseLayout } from "@common/components/ui";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthProvider } from "./auth-provider";
import { CookieProvider } from "./cookie-provider";
import { QueryProvider } from "./query-provider";
@@ -10,7 +10,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
<QueryProvider>
<CookieProvider>
<AuthProvider>
<BaseLayout>{children}</BaseLayout>
<ConfirmProvider>{children}</ConfirmProvider>
</AuthProvider>
</CookieProvider>
</QueryProvider>
@@ -1 +1,2 @@
export * from "./client";
export * from "./user";
+5 -7
View File
@@ -1,4 +1,3 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "../config";
import { client } from "./client";
@@ -36,10 +35,9 @@ export interface IUser {
hasPassword: boolean;
}
export const useUsersQuery = ({ userUniqId }: { userUniqId: string }) => {
return useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: () =>
client.get<IUser>(`${ENDPOINT.USER_SERVICE.USERS}/${userUniqId}`),
});
export const getUserInfo = async (userUniqId: string) => {
const response = await client.get<IUser>(
`${ENDPOINT.USER_SERVICE.USERS}/${userUniqId}`,
);
return response.data;
};
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { images } from "@common/assets";
import { useUsersQuery } from "../../api";
import { AUTH_USER_ID } from "../../config";
import { getUserInfo } from "../../api";
import { AUTH_USER_ID, ENDPOINT } from "../../config";
import { getToken } from "../../lib";
import {
DropdownMenu,
@@ -10,9 +11,12 @@ import {
} from "../dropdown-menu";
import { Button } from "../button";
export function Header() {
const { data: userInfo } = useUsersQuery({
userUniqId: getToken(AUTH_USER_ID) ?? "",
export function Header({ viewOnly }: { viewOnly?: boolean }) {
const userUniqId = getToken(AUTH_USER_ID) ?? "";
const { data: userInfo } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: () => getUserInfo(userUniqId),
enabled: !viewOnly,
});
return (
@@ -32,7 +36,7 @@ export function Header() {
<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>
@@ -1 +1,2 @@
export { BaseLayout } from "./base";
export { VBaseLayout } from "./vbase";
@@ -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>
);
}
+48
View File
@@ -42,6 +42,9 @@ importers:
'@common/components':
specifier: workspace:*
version: link:../../packages/common/components
'@frontend-opensource/use-react-hooks':
specifier: ^1.1.4
version: 1.1.4
'@hookform/resolvers':
specifier: ^4.1.3
version: 4.1.3(react-hook-form@7.54.2)
@@ -1140,6 +1143,15 @@ packages:
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
dev: false
/@frontend-opensource/use-react-hooks@1.1.4:
resolution: {integrity: sha512-otCSyciL5MVCNNYmdDdg2lEL6rbZ3XX1meYQCzapUSC4V6qx2YxMk1kUbhwEM/+vdxhYzBHsVBGD0rZWl5cgrw==}
dependencies:
async-wave: 1.8.6
fast-deep-equal: 3.1.3
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@hookform/resolvers@4.1.3(react-hook-form@7.54.2):
resolution: {integrity: sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==}
peerDependencies:
@@ -3703,6 +3715,17 @@ packages:
engines: {node: '>= 0.4'}
dev: true
/async-wave@1.8.6:
resolution: {integrity: sha512-wqWvr0Mrc1TrY92hN9MV0Xebxa6kt+T7QtjZ7FEMv+RUBa/Hfpz/UOgz0rB+7uqVK8TuBSXykOyU/k9gogdLkw==}
dependencies:
is-promise: 4.0.0
lodash.add: 3.7.0
lodash.clonedeep: 4.5.0
lodash.isequal: 4.5.0
lodash.isfunction: 3.0.9
lodash.size: 4.2.0
dev: false
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false
@@ -5476,6 +5499,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
/is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
dev: false
/is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -5724,10 +5751,31 @@ packages:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: true
/lodash.add@3.7.0:
resolution: {integrity: sha512-/uxUZkB6vYiPoGSXADJJE+Ee4W2vU+bxZOFdvJdx+mDgv+qyAO76sWD84Q4FxsMgTjC1+gVFC9YNhdG243QRDw==}
dev: false
/lodash.clonedeep@4.5.0:
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
dev: false
/lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
dev: false
/lodash.isfunction@3.0.9:
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
dev: false
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
/lodash.size@4.2.0:
resolution: {integrity: sha512-wbu3SF1XC5ijqm0piNxw59yCbuUf2kaShumYBLWUrcCvwh6C8odz6SY/wGVzCWTQTFL/1Ygbvqg2eLtspUVVAQ==}
dev: false
/lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
dev: true