layout.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import type { Metadata } from "next";
  2. import { Geist, Geist_Mono, Archivo_Black } from "next/font/google";
  3. import Script from "next/script";
  4. import React from "react";
  5. import "./globals.scss";
  6. import { SignalRProvider } from '@/contexts/signalrProvider';
  7. import { AuthProvider } from "@/contexts/authProvider";
  8. import { MemberProvider } from "@/contexts/memberProvider";
  9. import { ConfigProvider } from "@/contexts/configProvider";
  10. import { CartProvider } from "@/contexts/cartProvider";
  11. import { ThemeProvider } from "@/contexts/themeProvider";
  12. import { Toaster } from "@/components/ui/sonner";
  13. import { getAccessToken, getSignalRChatUrl, getSignalRAppUrl } from "@/lib/utils/server";
  14. import { fetchConfig } from "@/lib/api/system";
  15. import { getInitialMember } from "@/lib/api/auth";
  16. const geistSans = Geist({
  17. variable: "--font-geist-sans",
  18. subsets: ["latin"],
  19. });
  20. const geistMono = Geist_Mono({
  21. variable: "--font-geist-mono",
  22. subsets: ["latin"],
  23. });
  24. const archivoBlack = Archivo_Black({
  25. variable: "--font-brand",
  26. subsets: ["latin"],
  27. weight: "400",
  28. display: "swap",
  29. });
  30. type ColorSchemeEnum = 'normal' | 'light' | 'dark' | 'light dark' | 'dark light' | 'only light';
  31. function parseMetaAdds(html: string | null | undefined)
  32. {
  33. if (!html) return { other: {} as Record<string, string>, themeColor: [] as { media?: string; color: string }[], colorScheme: undefined as ColorSchemeEnum | undefined };
  34. const other: Record<string, string> = {};
  35. const themeColor: { media?: string; color: string }[] = [];
  36. const validColorSchemes: ColorSchemeEnum[] = ['normal', 'light', 'dark', 'light dark', 'dark light', 'only light'];
  37. let colorScheme: ColorSchemeEnum | undefined;
  38. const tagRegex = /<meta\s+([^>]+)\/?>/gi;
  39. const attrRegex = /(\w[\w-]*)=["']([^"']*?)["']/gi;
  40. let tagMatch;
  41. while ((tagMatch = tagRegex.exec(html)) !== null) {
  42. const attrs: Record<string, string> = {};
  43. let attrMatch;
  44. while ((attrMatch = attrRegex.exec(tagMatch[1])) !== null) {
  45. attrs[attrMatch[1].toLowerCase()] = attrMatch[2];
  46. }
  47. const name = attrs['name'];
  48. const content = attrs['content'];
  49. if (!name || !content) continue;
  50. if (name === 'theme-color') {
  51. themeColor.push(attrs['media'] ? { media: attrs['media'], color: content } : { color: content });
  52. } else if (name === 'color-scheme') {
  53. if (validColorSchemes.includes(content as ColorSchemeEnum)) {
  54. colorScheme = content as ColorSchemeEnum;
  55. }
  56. } else {
  57. other[name] = content;
  58. }
  59. }
  60. return { other, themeColor, colorScheme };
  61. }
  62. export async function generateMetadata(): Promise<Metadata> {
  63. const config = (await fetchConfig())?.data;
  64. const metaAdds = parseMetaAdds(config?.meta?.adds);
  65. return {
  66. title: config?.basic?.siteName ?? '개미투자',
  67. description: config?.meta?.description ?? '',
  68. keywords: config?.meta?.keywords ?? '',
  69. authors: config?.meta?.author ? [{ name: config.meta.author }] : undefined,
  70. applicationName: config?.meta.applicationName,
  71. generator: config?.meta.generator,
  72. robots: config?.meta.robots,
  73. ...(metaAdds.themeColor.length > 0 && { themeColor: metaAdds.themeColor }),
  74. ...(metaAdds.colorScheme && { colorScheme: metaAdds.colorScheme }),
  75. other: {
  76. 'naver-site-verification': '18dc0d1c5cb466a765e5f5093f94638ed6537d1c',
  77. ...metaAdds.other,
  78. },
  79. };
  80. }
  81. export default async function RootLayout({
  82. children,
  83. }: Readonly<{
  84. children: React.ReactNode;
  85. }>) {
  86. const accessToken = await getAccessToken();
  87. const signalRChatUrl = await getSignalRChatUrl();
  88. const signalRAppUrl = await getSignalRAppUrl();
  89. const config = (await fetchConfig())?.data;
  90. const initialMember = await getInitialMember();
  91. return (
  92. <html lang="ko" suppressHydrationWarning>
  93. <head>
  94. <link rel="manifest" href="/manifest.json" />
  95. <Script src="https://www.googletagmanager.com/gtag/js?id=G-DY6YFW4CTM" strategy="afterInteractive" />
  96. <Script id="gtag-init" strategy="afterInteractive">
  97. {`
  98. window.dataLayer = window.dataLayer || [];
  99. function gtag(){dataLayer.push(arguments);}
  100. gtag('js', new Date());
  101. gtag('config', 'G-DY6YFW4CTM');
  102. `}
  103. </Script>
  104. </head>
  105. <body className={`${geistSans.variable} ${geistMono.variable} ${archivoBlack.variable} antialiased`}>
  106. <ThemeProvider>
  107. <SignalRProvider accessToken={accessToken} signalRChatUrl={signalRChatUrl} signalRAppUrl={signalRAppUrl}>
  108. <AuthProvider initialAuthenticated={!!initialMember}>
  109. <MemberProvider initialMember={initialMember}>
  110. <ConfigProvider initialConfig={config}>
  111. <CartProvider>
  112. {children}
  113. </CartProvider>
  114. </ConfigProvider>
  115. </MemberProvider>
  116. </AuthProvider>
  117. </SignalRProvider>
  118. <Toaster />
  119. </ThemeProvider>
  120. </body>
  121. </html>
  122. );
  123. }