@uniai-fe/next-providers 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 UNIAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This project includes third-party software governed by additional licenses,
26
+ including Apache License 2.0. Refer to `THIRD_PARTY_NOTICES.md` for the full
27
+ text of those notices and any required attributions.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # client / next-providers
2
+
3
+ 상태관리 도구 \<Provider /> 및 보조 도구 커스텀 등 관리
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@uniai-fe/next-providers",
3
+ "version": "0.1.1",
4
+ "description": "Next.js State Providers for UNIAI FE Projects",
5
+ "type": "module",
6
+ "private": false,
7
+ "sideEffects": false,
8
+ "license": "MIT",
9
+ "homepage": "https://www.uniai.co.kr/",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "packageManager": "pnpm@10.22.0",
14
+ "engines": {
15
+ "node": ">=22",
16
+ "pnpm": ">=10"
17
+ },
18
+ "author": {
19
+ "name": "GraffitoRyu",
20
+ "email": "yth4135@naver.com",
21
+ "url": "https://github.com/GraffitoRyu"
22
+ },
23
+ "files": [
24
+ "src"
25
+ ],
26
+ "scripts": {
27
+ "lint": "eslint . --max-warnings=0",
28
+ "typecheck": "tsc --project tsconfig.build.json --noEmit",
29
+ "build": "pnpm typecheck",
30
+ "dev": "tsc --project tsconfig.build.json --watch --noEmit",
31
+ "module:lint": "pnpm lint",
32
+ "module:typecheck": "pnpm typecheck",
33
+ "module:build": "pnpm build",
34
+ "next-providers:build": "pnpm run build",
35
+ "next-providers:dev": "pnpm run dev"
36
+ },
37
+ "main": "./src/index.tsx",
38
+ "module": "./src/index.tsx",
39
+ "exports": {
40
+ ".": "./src/index.tsx"
41
+ },
42
+ "peerDependencies": {
43
+ "next": ">= 15",
44
+ "react": ">= 19",
45
+ "react-dom": ">= 19",
46
+ "@tanstack/react-query": ">= 5",
47
+ "jotai": ">= 2",
48
+ "styled-components": ">= 6",
49
+ "airtable": ">= 0.12"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "optional": {
53
+ "@tanstack/react-query": true,
54
+ "jotai": true,
55
+ "styled-components": true
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@uniai-fe/eslint-config": "workspace:*",
60
+ "@uniai-fe/tsconfig": "workspace:*",
61
+ "@uniai-fe/next-devkit": "workspace:*",
62
+ "@uniai-fe/util-functions": "workspace:*",
63
+ "@uniai-fe/util-jotai": "workspace:*",
64
+ "@uniai-fe/ui-legacy": "workspace:*",
65
+ "@uniai-fe/i18n": "workspace:*",
66
+ "@tanstack/react-query": "^5.90.8",
67
+ "@types/node": "^24.10.1",
68
+ "@types/react": "^19.2.4",
69
+ "@types/react-dom": "^19.2.3",
70
+ "airtable": "^0.12.2",
71
+ "eslint": "^9.39.1",
72
+ "jotai": "^2.15.1",
73
+ "jotai-tanstack-query": "^0.11.0",
74
+ "next": "^15.5.6",
75
+ "prettier": "^3.6.2",
76
+ "react": "^19.2.0",
77
+ "react-dom": "^19.2.0",
78
+ "styled-components": "^6.1.19",
79
+ "typescript": "~5.9.3"
80
+ }
81
+ }
@@ -0,0 +1,57 @@
1
+ import { Suspense } from "react";
2
+ import { Modal } from "@uniai-fe/ui-legacy";
3
+
4
+ import StyledComponentsRegistry from "./lib/StyledComponentsRegistry";
5
+ import AirtableProvider from "./provider/AirtableProvider";
6
+ import { JotaiProvider } from "./provider/JotaiProvider";
7
+ import { ReactQueryProvider } from "./provider/ReactQueryProvider";
8
+ import RouterEventDetector from "./lib/RouterEventsDetector";
9
+ import SetQueryCookie from "./lib/SetQueryCookie";
10
+ import SystemThemeChecker from "./lib/SystemThemeChecker";
11
+ import type { SystemLanguageType } from "@uniai-fe/next-devkit/types";
12
+
13
+ /**
14
+ * Next.js Root Layout에 적용할 Provider 및 유틸리티 도구 집합
15
+ * @component
16
+ * @param {object} props
17
+ * @param {SystemLanguageType} [props.locale] 언어설정; "ko", "en"
18
+ * @param {object} [props.systemThemeOptions] 시스템 테마 체크 옵션
19
+ * @param {string[]} [props.systemThemeOptions.exceptPaths] 테마 체크 제외 경로
20
+ * @param {React.ReactNode} props.children
21
+ * @desc
22
+ * - React Query
23
+ * - Jotai
24
+ * - Apollo GraphQL
25
+ * - Airtable
26
+ * - Modal / Draggable Modal
27
+ * - System Theme Checker
28
+ * - Router Event Detector
29
+ */
30
+ export default function NextRoots({
31
+ locale = "ko",
32
+ systemThemeOptions,
33
+ children,
34
+ }: {
35
+ locale?: SystemLanguageType;
36
+ systemThemeOptions?: { exceptPaths: string[] };
37
+ children: React.ReactNode;
38
+ }) {
39
+ return (
40
+ <ReactQueryProvider>
41
+ <JotaiProvider>
42
+ <SetQueryCookie />
43
+ <StyledComponentsRegistry>
44
+ <AirtableProvider locale={locale}>
45
+ {children}
46
+ <Modal.Basic.Provider />
47
+ <Modal.Draggable.Provider />
48
+ </AirtableProvider>
49
+ <SystemThemeChecker exceptPaths={systemThemeOptions?.exceptPaths} />
50
+ <Suspense fallback={null}>
51
+ <RouterEventDetector />
52
+ </Suspense>
53
+ </StyledComponentsRegistry>
54
+ </JotaiProvider>
55
+ </ReactQueryProvider>
56
+ );
57
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,3 @@
1
+ import NextRoots from "./NextRoots";
2
+
3
+ export { NextRoots };
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import { useEffect } from "react";
5
+ import { useSetAtom } from "jotai";
6
+
7
+ import { modalState, modalDraggableState } from "@uniai-fe/ui-legacy";
8
+
9
+ /**
10
+ * 경로변경 이벤트 감지
11
+ * @component
12
+ * @desc
13
+ * - 최상위 layout에서, 경로 변경을 감지
14
+ */
15
+ export default function RouterEventDetector() {
16
+ const resetModalStack = useSetAtom(modalState);
17
+ const resetDraggableModalStack = useSetAtom(modalDraggableState);
18
+
19
+ const pathname = usePathname();
20
+
21
+ // 경로 변경 감지
22
+ useEffect(() => {
23
+ // 모달 스택 초기화
24
+ resetModalStack([]);
25
+ // 드래거블 모달 스택 초기화
26
+ resetDraggableModalStack([]);
27
+ }, [pathname, resetDraggableModalStack, resetModalStack]);
28
+
29
+ return null;
30
+ }
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect } from "react";
4
+ import { setServerState } from "@uniai-fe/util-jotai";
5
+
6
+ /**
7
+ * 쿠키 요청값 업데이트
8
+ * @component
9
+ * @desc
10
+ * - 클라이언트에서 정보를 받아 서버 쿠키로 요청이 필요한 경우 사용
11
+ */
12
+ export default function SetQueryCookie() {
13
+ /**
14
+ * 언어설정 쿠키 요청값 업데이트
15
+ */
16
+ const setLocaleCookie = useCallback(async () => {
17
+ if (typeof window === "undefined") return;
18
+ const locale = localStorage.getItem("systemLocale");
19
+ await setServerState("systemLocale", locale, "ko");
20
+ }, []);
21
+
22
+ // 클라이언트에서 실행
23
+ useEffect(() => {
24
+ setLocaleCookie();
25
+ }, [setLocaleCookie]);
26
+
27
+ return null;
28
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useServerInsertedHTML } from "next/navigation";
5
+ import { ServerStyleSheet, StyleSheetManager } from "styled-components";
6
+
7
+ /**
8
+ * styled-components의 SSR 활용을 위한 레지스트리 구성요소
9
+ * @component
10
+ * @desc
11
+ * - 최상위 layout에서 등록
12
+ */
13
+ export default function StyledComponentsRegistry({
14
+ children,
15
+ }: {
16
+ children: React.ReactNode;
17
+ }) {
18
+ // Only create stylesheet once with lazy initial state
19
+ // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
20
+ const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
21
+
22
+ useServerInsertedHTML(() => {
23
+ const styles = styledComponentsStyleSheet.getStyleElement();
24
+ styledComponentsStyleSheet.instance.clearTag();
25
+ return <>{styles}</>;
26
+ });
27
+
28
+ if (typeof window !== "undefined") return <>{children}</>;
29
+
30
+ return (
31
+ <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
32
+ {children}
33
+ </StyleSheetManager>
34
+ );
35
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import { useEffect } from "react";
5
+ import { useAtomValue } from "jotai";
6
+ import { systemThemeState } from "@uniai-fe/util-jotai";
7
+
8
+ /**
9
+ * 서비스 테마관리 공급자
10
+ * @component
11
+ * @desc
12
+ * - 최상위 layout에서 관리
13
+ */
14
+ export default function SystemThemeChecker({
15
+ serviceType = "farm",
16
+ exceptPaths = [],
17
+ }: {
18
+ serviceType?: string;
19
+ exceptPaths?: string[];
20
+ }) {
21
+ const systemTheme = useAtomValue(systemThemeState);
22
+ const pathname = usePathname();
23
+
24
+ // const locale = useLocale();
25
+
26
+ useEffect(() => {
27
+ if (!systemTheme[serviceType] || exceptPaths.includes(pathname)) {
28
+ document.documentElement.classList.remove(`dark-mode`);
29
+ return;
30
+ }
31
+ document.documentElement.classList.remove(`dark-mode`);
32
+ if (systemTheme[serviceType] === "dark")
33
+ document.documentElement.classList.add(
34
+ `${systemTheme[serviceType]}-mode`,
35
+ );
36
+ }, [exceptPaths, pathname, serviceType, systemTheme]);
37
+
38
+ return null;
39
+ }
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import {
3
+ dehydrate,
4
+ HydrationBoundary,
5
+ QueryClient,
6
+ } from "@tanstack/react-query";
7
+ import { getAirtableData } from "@uniai-fe/i18n/api";
8
+ import type { SystemLanguageType } from "@uniai-fe/next-devkit/types";
9
+
10
+ /**
11
+ * 다국어 지원을 위한 Airtable 데이터 호출 공급자
12
+ * @component
13
+ * @property {SystemLanguageType} locale "ko", "en", "jp", string
14
+ * @property {React.ReactNode} children
15
+ * @see https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data
16
+ * @see https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#nesting-server-components
17
+ * @desc
18
+ * - 데이터를 서버에서 미리 불러오기 위해, 서버 컴포넌트로 구성
19
+ */
20
+ export default async function AirtableProvider({
21
+ locale: serverLocale,
22
+ children,
23
+ }: {
24
+ locale: SystemLanguageType;
25
+ children: React.ReactNode;
26
+ }) {
27
+ // 서버는 항상 새로운 client를 선언해야 함
28
+ const serverQueryClient = new QueryClient();
29
+
30
+ // 데이터 호출
31
+ await serverQueryClient.prefetchQuery({
32
+ queryKey: ["airtable", serverLocale],
33
+ queryFn: () => getAirtableData(serverLocale),
34
+ });
35
+
36
+ return (
37
+ <HydrationBoundary state={dehydrate(serverQueryClient)}>
38
+ {children}
39
+ </HydrationBoundary>
40
+ );
41
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { Provider } from "jotai";
4
+ import { useHydrateAtoms } from "jotai/react/utils";
5
+ import { queryClientAtom } from "jotai-tanstack-query";
6
+ import { getQueryClient } from "./ReactQueryProvider";
7
+
8
+ export function JotaiHydrateAtoms({ children }: { children: React.ReactNode }) {
9
+ useHydrateAtoms([[queryClientAtom, getQueryClient()]]);
10
+ return children;
11
+ }
12
+
13
+ /**
14
+ * Jotai 상태 공급자
15
+ * @component
16
+ */
17
+ export function JotaiProvider({ children }: { children: React.ReactNode }) {
18
+ return (
19
+ <Provider>
20
+ <JotaiHydrateAtoms>{children}</JotaiHydrateAtoms>
21
+ </Provider>
22
+ );
23
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import {
4
+ defaultShouldDehydrateQuery,
5
+ isServer,
6
+ QueryClient,
7
+ QueryClientProvider,
8
+ } from "@tanstack/react-query";
9
+
10
+ /**
11
+ * 클라이언트 단에서의 React Query client
12
+ * @type {QueryClient | undefined}
13
+ */
14
+ let browserQueryClient: QueryClient | undefined = undefined;
15
+
16
+ /**
17
+ * React Query client 생성
18
+ * - 서비스 전역에 적용할 쿼리옵션을 사전에 지정하여 생성
19
+ * @return {QueryClient}
20
+ */
21
+ function makeQueryClient(): QueryClient {
22
+ return new QueryClient({
23
+ // react-query 전역 설정
24
+ defaultOptions: {
25
+ queries: {
26
+ // onError: queryErrorHandler,
27
+ staleTime: 600000,
28
+ gcTime: 900000, // 기존 cacheTime에서 명칭 변경, 원래 "가비지 콜렉션 시간"을 의미하였음.
29
+ refetchOnMount: false,
30
+ refetchOnReconnect: false,
31
+ refetchOnWindowFocus: false,
32
+ retry: false,
33
+ },
34
+ dehydrate: {
35
+ // include pending queries in dehydration
36
+ shouldDehydrateQuery: query =>
37
+ defaultShouldDehydrateQuery(query) ||
38
+ query.state.status === "pending",
39
+ },
40
+ },
41
+ });
42
+ }
43
+
44
+ /**
45
+ * React Query client 생성
46
+ * @return {QueryClient}
47
+ */
48
+ export function getQueryClient(): QueryClient {
49
+ if (isServer) {
50
+ // Server: always make a new query client
51
+ return makeQueryClient();
52
+ } else {
53
+ // Browser: make a new query client if we don't already have one
54
+ // This is very important, so we don't re-make a new client if React
55
+ // suspends during the initial render. This may not be needed if we
56
+ // have a suspense boundary BELOW the creation of the query client
57
+ if (!browserQueryClient) browserQueryClient = makeQueryClient();
58
+ return browserQueryClient;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * ReactQuery 상태 공급자
64
+ * @component
65
+ * @see https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data
66
+ * @see https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#nesting-server-components
67
+ */
68
+ export function ReactQueryProvider({
69
+ children,
70
+ }: {
71
+ children: React.ReactNode;
72
+ }) {
73
+ // NOTE: Avoid useState when initializing the query client if you don't
74
+ // have a suspense boundary between this and the code that may
75
+ // suspend because React will throw away the client on the initial
76
+ // render if it suspends and there is no boundary
77
+ const queryClient = getQueryClient();
78
+
79
+ return (
80
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
81
+ );
82
+ }