create-web-0to1 0.1.0

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.
Files changed (193) hide show
  1. package/README.md +68 -0
  2. package/internal/engine/create-feature-crud-script.ts +134 -0
  3. package/internal/engine/create-feature-crud.template.mjs +601 -0
  4. package/internal/engine/create-feature-script.ts +142 -0
  5. package/internal/engine/generator-engine.ts +546 -0
  6. package/internal/engine/standalone-feature-preset.ts +34 -0
  7. package/internal/meta/preset-plan.ts +41 -0
  8. package/internal/meta/runtime-copy-plan.ts +220 -0
  9. package/internal/meta/runtime-layout.ts +262 -0
  10. package/internal/meta/scaffold-manifest.ts +169 -0
  11. package/internal/meta/standalone-dependency-manifest.ts +75 -0
  12. package/package.json +45 -0
  13. package/scripts/create-app.mjs +1612 -0
  14. package/source/core/auth/auth-events.ts +13 -0
  15. package/source/core/error/app-error.ts +85 -0
  16. package/source/core/error/handle-app-error.client.ts +35 -0
  17. package/source/core/lib/dayjs.ts +25 -0
  18. package/source/core/query/query-client.ts +126 -0
  19. package/source/core/request/request-core.ts +210 -0
  20. package/source/core/routes/route-paths.ts +4 -0
  21. package/source/core/ui/button.tsx +24 -0
  22. package/source/core/ui/modal-store.ts +32 -0
  23. package/source/core/ui/text-input-field.tsx +36 -0
  24. package/source/core/utils/build-query-string.ts +30 -0
  25. package/source/core/utils/format/date.ts +41 -0
  26. package/source/core/utils/format/index.ts +3 -0
  27. package/source/core/utils/format/number.ts +13 -0
  28. package/source/core/utils/format/text.ts +15 -0
  29. package/source/core/utils/schema-utils.ts +27 -0
  30. package/source/wrappers/monorepo/core/internal.ts +21 -0
  31. package/source/wrappers/monorepo/core/src/index.ts +4 -0
  32. package/source/wrappers/monorepo/core-next/src/auth.client.ts +1 -0
  33. package/source/wrappers/monorepo/core-next/src/auth.server.ts +94 -0
  34. package/source/wrappers/monorepo/core-next/src/bootstrap.client.tsx +21 -0
  35. package/source/wrappers/monorepo/core-next/src/bootstrap.tsx +18 -0
  36. package/source/wrappers/monorepo/core-next/src/index.ts +1 -0
  37. package/source/wrappers/monorepo/core-react/src/app-providers.tsx +36 -0
  38. package/source/wrappers/monorepo/core-react/src/auth.ts +42 -0
  39. package/source/wrappers/monorepo/core-react/src/hydration.tsx +21 -0
  40. package/source/wrappers/monorepo/core-react/src/index.ts +7 -0
  41. package/source/wrappers/monorepo/core-react/src/provider.tsx +49 -0
  42. package/source/wrappers/monorepo/core-react/src/query-client.ts +48 -0
  43. package/source/wrappers/monorepo/core-react/src/query-error-handler.ts +62 -0
  44. package/source/wrappers/monorepo/core-react/src/query-keys.ts +22 -0
  45. package/source/wrappers/monorepo/request/core-fetch.ts +27 -0
  46. package/source/wrappers/monorepo/request/core-request.ts +93 -0
  47. package/source/wrappers/next/auth/auth-error-listener.tsx +34 -0
  48. package/source/wrappers/next/error/handle-app-error.server.ts +41 -0
  49. package/source/wrappers/next/query/hydration.tsx +20 -0
  50. package/source/wrappers/next/query/providers.tsx +35 -0
  51. package/source/wrappers/next/request/request.client.ts +24 -0
  52. package/source/wrappers/next/request/request.server.ts +64 -0
  53. package/source/wrappers/next/request/request.ts +52 -0
  54. package/source/wrappers/next/ui/global-modal.tsx +29 -0
  55. package/source/wrappers/react/auth/auth-error-listener.tsx +34 -0
  56. package/source/wrappers/react/query/providers.tsx +31 -0
  57. package/source/wrappers/react/request/request.client.ts +24 -0
  58. package/source/wrappers/react/request/request.ts +51 -0
  59. package/source/wrappers/react/ui/global-modal.tsx +27 -0
  60. package/templates/monorepo/.dockerignore +38 -0
  61. package/templates/monorepo/README.md +292 -0
  62. package/templates/monorepo/_gitignore +38 -0
  63. package/templates/monorepo/_npmrc +1 -0
  64. package/templates/monorepo/apps/project/Dockerfile +32 -0
  65. package/templates/monorepo/apps/project/eslint.config.mjs +4 -0
  66. package/templates/monorepo/apps/project/index.html +14 -0
  67. package/templates/monorepo/apps/project/index.ts +15 -0
  68. package/templates/monorepo/apps/project/package.json +21 -0
  69. package/templates/monorepo/apps/project/tsconfig.json +9 -0
  70. package/templates/monorepo/apps/project/vite.config.ts +6 -0
  71. package/templates/monorepo/apps/web/Dockerfile +43 -0
  72. package/templates/monorepo/apps/web/README.md +111 -0
  73. package/templates/monorepo/apps/web/_gitignore +36 -0
  74. package/templates/monorepo/apps/web/app/favicon.ico +0 -0
  75. package/templates/monorepo/apps/web/app/global-error.tsx +12 -0
  76. package/templates/monorepo/apps/web/app/globals.css +0 -0
  77. package/templates/monorepo/apps/web/app/layout.tsx +28 -0
  78. package/templates/monorepo/apps/web/app/page.tsx +7 -0
  79. package/templates/monorepo/apps/web/app/providers.tsx +25 -0
  80. package/templates/monorepo/apps/web/eslint.config.js +4 -0
  81. package/templates/monorepo/apps/web/next-env.d.ts +6 -0
  82. package/templates/monorepo/apps/web/next.config.js +4 -0
  83. package/templates/monorepo/apps/web/package.json +31 -0
  84. package/templates/monorepo/apps/web/public/file-text.svg +3 -0
  85. package/templates/monorepo/apps/web/public/globe.svg +10 -0
  86. package/templates/monorepo/apps/web/public/next.svg +1 -0
  87. package/templates/monorepo/apps/web/public/turborepo-dark.svg +19 -0
  88. package/templates/monorepo/apps/web/public/turborepo-light.svg +19 -0
  89. package/templates/monorepo/apps/web/public/vercel.svg +10 -0
  90. package/templates/monorepo/apps/web/public/window.svg +3 -0
  91. package/templates/monorepo/apps/web/tsconfig.json +20 -0
  92. package/templates/monorepo/package.json +24 -0
  93. package/templates/monorepo/packages/core/eslint.config.mjs +4 -0
  94. package/templates/monorepo/packages/core/package.json +32 -0
  95. package/templates/monorepo/packages/core/tsconfig.json +8 -0
  96. package/templates/monorepo/packages/core-next/eslint.config.mjs +13 -0
  97. package/templates/monorepo/packages/core-next/package.json +43 -0
  98. package/templates/monorepo/packages/core-next/tsconfig.json +8 -0
  99. package/templates/monorepo/packages/core-react/eslint.config.mjs +4 -0
  100. package/templates/monorepo/packages/core-react/package.json +34 -0
  101. package/templates/monorepo/packages/core-react/tsconfig.json +8 -0
  102. package/templates/monorepo/packages/eslint-config/README.md +3 -0
  103. package/templates/monorepo/packages/eslint-config/base.js +57 -0
  104. package/templates/monorepo/packages/eslint-config/next.js +22 -0
  105. package/templates/monorepo/packages/eslint-config/package.json +25 -0
  106. package/templates/monorepo/packages/eslint-config/react-internal.js +33 -0
  107. package/templates/monorepo/packages/typescript-config/base.json +19 -0
  108. package/templates/monorepo/packages/typescript-config/nextjs.json +12 -0
  109. package/templates/monorepo/packages/typescript-config/package.json +9 -0
  110. package/templates/monorepo/packages/typescript-config/react-library.json +7 -0
  111. package/templates/monorepo/packages/ui/eslint.config.mjs +4 -0
  112. package/templates/monorepo/packages/ui/package.json +26 -0
  113. package/templates/monorepo/packages/ui/src/button.tsx +20 -0
  114. package/templates/monorepo/packages/ui/src/card.tsx +27 -0
  115. package/templates/monorepo/packages/ui/src/code.tsx +11 -0
  116. package/templates/monorepo/packages/ui/tsconfig.json +8 -0
  117. package/templates/monorepo/pnpm-workspace.yaml +9 -0
  118. package/templates/monorepo/turbo/generators/config.js +1336 -0
  119. package/templates/monorepo/turbo/generators/templates/next-app/Dockerfile.tpl +30 -0
  120. package/templates/monorepo/turbo/generators/templates/next-app/README.md.tpl +118 -0
  121. package/templates/monorepo/turbo/generators/templates/next-app/app/global-error.tsx.tpl +12 -0
  122. package/templates/monorepo/turbo/generators/templates/next-app/app/globals.css.tpl +1 -0
  123. package/templates/monorepo/turbo/generators/templates/next-app/app/layout.tsx.tpl +29 -0
  124. package/templates/monorepo/turbo/generators/templates/next-app/app/page.tsx.tpl +7 -0
  125. package/templates/monorepo/turbo/generators/templates/next-app/app/providers.tsx.tpl +25 -0
  126. package/templates/monorepo/turbo/generators/templates/next-app/eslint.config.js.tpl +4 -0
  127. package/templates/monorepo/turbo/generators/templates/next-app/next.config.js.tpl +6 -0
  128. package/templates/monorepo/turbo/generators/templates/next-app/tsconfig.json.tpl +18 -0
  129. package/templates/monorepo/turbo/generators/templates/vite-app/Dockerfile.tpl +22 -0
  130. package/templates/monorepo/turbo/generators/templates/vite-app/README.plain.md.tpl +90 -0
  131. package/templates/monorepo/turbo/generators/templates/vite-app/README.react.md.tpl +107 -0
  132. package/templates/monorepo/turbo/generators/templates/vite-app/eslint.config.mjs.tpl +4 -0
  133. package/templates/monorepo/turbo/generators/templates/vite-app/index.html.tpl +12 -0
  134. package/templates/monorepo/turbo/generators/templates/vite-app/index.ts.tpl +22 -0
  135. package/templates/monorepo/turbo/generators/templates/vite-app/tsconfig.json.tpl +9 -0
  136. package/templates/monorepo/turbo/generators/templates/vite-app/vite.config.ts.tpl +6 -0
  137. package/templates/monorepo/turbo.json +28 -0
  138. package/templates/next/.env.example +2 -0
  139. package/templates/next/.prettierignore +9 -0
  140. package/templates/next/.prettierrc.json +9 -0
  141. package/templates/next/README.md +246 -0
  142. package/templates/next/_gitignore +44 -0
  143. package/templates/next/eslint.config.mjs +51 -0
  144. package/templates/next/next.config.ts +7 -0
  145. package/templates/next/package.json +24 -0
  146. package/templates/next/postcss.config.mjs +7 -0
  147. package/templates/next/scripts/create-feature-crud.mjs +5 -0
  148. package/templates/next/scripts/create-feature.mjs +5 -0
  149. package/templates/next/src/app/error.tsx +33 -0
  150. package/templates/next/src/app/globals.css +35 -0
  151. package/templates/next/src/app/layout.tsx +39 -0
  152. package/templates/next/src/app/login/page.tsx +17 -0
  153. package/templates/next/src/app/page.tsx +32 -0
  154. package/templates/next/src/app/providers.tsx +20 -0
  155. package/templates/next/tsconfig.json +34 -0
  156. package/templates/react/.env.example +1 -0
  157. package/templates/react/.prettierignore +10 -0
  158. package/templates/react/.prettierrc.json +9 -0
  159. package/templates/react/README.md +250 -0
  160. package/templates/react/_gitignore +31 -0
  161. package/templates/react/eslint.config.mjs +64 -0
  162. package/templates/react/package.json +19 -0
  163. package/templates/react/scripts/create-feature-crud.mjs +5 -0
  164. package/templates/react/scripts/create-feature.mjs +5 -0
  165. package/templates/react/src/app/app.tsx +15 -0
  166. package/templates/react/src/app/error-boundary.tsx +59 -0
  167. package/templates/react/src/app/frame.tsx +32 -0
  168. package/templates/react/src/app/globals.css +43 -0
  169. package/templates/react/src/app/not-found-page.tsx +23 -0
  170. package/templates/react/src/app/providers.tsx +16 -0
  171. package/templates/react/src/app/router.tsx +62 -0
  172. package/templates/react/src/main.tsx +12 -0
  173. package/templates/react/src/pages/index/page.tsx +36 -0
  174. package/templates/react/src/pages/login/page.tsx +18 -0
  175. package/templates/react/tsconfig.app.json +30 -0
  176. package/templates/react/tsconfig.json +4 -0
  177. package/templates/react/tsconfig.node.json +24 -0
  178. package/templates/react/vite.config.ts +14 -0
  179. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/00-/354/240/204/353/260/230/354/240/201/354/235/270-/355/217/264/353/215/224/352/265/254/354/241/260.md +150 -0
  180. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/01-/352/265/254/354/241/260/354/231/200-/353/235/274/354/232/260/355/214/205.md +186 -0
  181. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/02-/354/204/234/353/262/204/354/231/200-/355/201/264/353/235/274/354/235/264/354/226/270/355/212/270.md +86 -0
  182. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/03-/354/203/201/355/203/234/352/264/200/353/246/254.md +84 -0
  183. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/04-API/354/231/200-/353/215/260/354/235/264/355/204/260.md +199 -0
  184. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/05-/354/227/220/353/237/254/354/231/200-UI-/354/203/201/355/203/234.md +159 -0
  185. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/06-/355/217/274.md +116 -0
  186. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/07-/354/212/244/355/203/200/354/235/274/353/247/201/352/263/274-/354/240/221/352/267/274/354/204/261.md +73 -0
  187. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/08-/353/204/244/354/235/264/353/260/215-/354/204/244/354/240/225-/355/217/254/353/247/267/355/214/205.md +98 -0
  188. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/09-/353/235/274/354/232/260/355/212/270-/354/240/225/354/235/230.md +169 -0
  189. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/10-/354/273/244/353/260/213-/354/273/250/353/262/244/354/205/230.md +64 -0
  190. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/11-/352/270/260/355/203/200-/354/233/220/354/271/231.md +187 -0
  191. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/12-/354/213/244/353/254/264-/353/215/260/354/235/264/355/204/260-/355/214/250/355/204/264.md +302 -0
  192. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/13-/354/204/261/353/212/245-/354/233/220/354/271/231.md +175 -0
  193. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/README.md +39 -0
@@ -0,0 +1,15 @@
1
+ export function truncateText(value: string, maxLength: number, suffix = '...') {
2
+ if (value.length <= maxLength) {
3
+ return value;
4
+ }
5
+
6
+ return `${value.slice(0, maxLength)}${suffix}`;
7
+ }
8
+
9
+ export function fallbackText(value: string | null | undefined, empty = '-') {
10
+ if (!value || value.trim() === '') {
11
+ return empty;
12
+ }
13
+
14
+ return value;
15
+ }
@@ -0,0 +1,27 @@
1
+ import { ApiError, APP_ERROR_CODES, getAppErrorMessage } from '../error/app-error';
2
+
3
+ type SchemaLike<TOutput> = {
4
+ safeParse: (
5
+ value: unknown,
6
+ ) => { success: true; data: TOutput } | { success: false };
7
+ };
8
+
9
+ /**
10
+ * 런타임 데이터 검증 실패를 템플릿 공통 AppError로 통일합니다.
11
+ */
12
+ export function parseWithSchema<TOutput>(
13
+ schema: SchemaLike<TOutput>,
14
+ payload: unknown,
15
+ ): TOutput {
16
+ const result = schema.safeParse(payload);
17
+
18
+ if (!result.success) {
19
+ throw new ApiError(
20
+ getAppErrorMessage(APP_ERROR_CODES.INVALID_RESPONSE),
21
+ 500,
22
+ APP_ERROR_CODES.INVALID_RESPONSE,
23
+ );
24
+ }
25
+
26
+ return result.data;
27
+ }
@@ -0,0 +1,21 @@
1
+ type CoreConfig = {
2
+ siteId: string;
3
+ baseUrl: string;
4
+ };
5
+
6
+ const coreConfig: CoreConfig = {
7
+ siteId: '',
8
+ baseUrl: '',
9
+ };
10
+
11
+ export function initCore(siteId: string, baseUrl?: string) {
12
+ coreConfig.siteId = siteId;
13
+
14
+ if (baseUrl !== undefined) {
15
+ coreConfig.baseUrl = baseUrl;
16
+ }
17
+ }
18
+
19
+ export function getCoreConfig() {
20
+ return { ...coreConfig };
21
+ }
@@ -0,0 +1,4 @@
1
+ export * from '../../../../core/error/app-error';
2
+ export * from '../internal';
3
+ export * from '../../request/core-request';
4
+ export * from '../../../../core/utils/schema-utils';
@@ -0,0 +1 @@
1
+ export { withClientAuth } from '../../core-react/src/auth';
@@ -0,0 +1,94 @@
1
+ import 'server-only';
2
+
3
+ import { cookies } from 'next/headers';
4
+
5
+ const DEFAULT_AUTH_COOKIE_NAMES = [
6
+ 'session',
7
+ 'access_token',
8
+ 'refresh_token',
9
+ ] as const;
10
+
11
+ export type WithServerAuthOptions = {
12
+ authCookieNames?: readonly string[];
13
+ };
14
+
15
+ type RequestOptionsLike = {
16
+ headers?: HeadersInit;
17
+ };
18
+
19
+ function isRequestOptionsLike(value: unknown): value is RequestOptionsLike {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
21
+ }
22
+
23
+ function mergeCookieHeader(
24
+ headers: HeadersInit | undefined,
25
+ cookieHeader: string,
26
+ ) {
27
+ const nextHeaders = new Headers(headers);
28
+ const existingCookieHeader = nextHeaders.get('Cookie');
29
+
30
+ nextHeaders.set(
31
+ 'Cookie',
32
+ existingCookieHeader
33
+ ? `${existingCookieHeader}; ${cookieHeader}`
34
+ : cookieHeader,
35
+ );
36
+
37
+ return nextHeaders;
38
+ }
39
+
40
+ function createAuthCookieHeader(
41
+ cookieNames: readonly string[],
42
+ cookieStore: Awaited<ReturnType<typeof cookies>>,
43
+ ) {
44
+ return cookieNames
45
+ .map((cookieName) => cookieStore.get(cookieName))
46
+ .filter((cookie) => cookie !== undefined)
47
+ .map((cookie) => `${cookie.name}=${cookie.value}`)
48
+ .join('; ');
49
+ }
50
+
51
+ function withServerAuthHeaders(args: unknown[], cookieHeader: string) {
52
+ if (!cookieHeader) {
53
+ return args;
54
+ }
55
+
56
+ const nextArgs = [...args];
57
+ const lastArg = nextArgs.at(-1);
58
+
59
+ if (!isRequestOptionsLike(lastArg)) {
60
+ nextArgs.push({
61
+ headers: mergeCookieHeader(undefined, cookieHeader),
62
+ });
63
+
64
+ return nextArgs;
65
+ }
66
+
67
+ nextArgs[nextArgs.length - 1] = {
68
+ ...lastArg,
69
+ headers: mergeCookieHeader(lastArg.headers, cookieHeader),
70
+ };
71
+
72
+ return nextArgs;
73
+ }
74
+
75
+ type AsyncRequestFunction = (...args: unknown[]) => Promise<unknown>;
76
+
77
+ /**
78
+ * Next 서버에서 현재 요청 쿠키를 읽어 request 함수에 그대로 주입합니다.
79
+ */
80
+ export function withServerAuth<TRequestFunction extends AsyncRequestFunction>(
81
+ requestFunction: TRequestFunction,
82
+ options: WithServerAuthOptions = {},
83
+ ): TRequestFunction {
84
+ const { authCookieNames = DEFAULT_AUTH_COOKIE_NAMES } = options;
85
+
86
+ return (async (...args: Parameters<TRequestFunction>) => {
87
+ const cookieStore = await cookies();
88
+ const cookieHeader = createAuthCookieHeader(authCookieNames, cookieStore);
89
+
90
+ return requestFunction(
91
+ ...(withServerAuthHeaders(args, cookieHeader) as Parameters<TRequestFunction>),
92
+ );
93
+ }) as TRequestFunction;
94
+ }
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import { initCore } from '../../core/internal';
4
+
5
+ export type CoreBootstrapClientProps = {
6
+ siteId: string;
7
+ baseUrl?: string;
8
+ };
9
+
10
+ /**
11
+ * 클라이언트에서도 동일한 core 설정을 다시 초기화해서
12
+ * hydration 이후 request wrapper가 같은 값을 읽도록 맞춥니다.
13
+ */
14
+ export function CoreBootstrapClient({
15
+ siteId,
16
+ baseUrl,
17
+ }: CoreBootstrapClientProps) {
18
+ initCore(siteId, baseUrl);
19
+
20
+ return null;
21
+ }
@@ -0,0 +1,18 @@
1
+ import type { JSX } from 'react';
2
+
3
+ import { initCore } from '../../core/internal';
4
+ import { CoreBootstrapClient } from './bootstrap.client';
5
+
6
+ export type CoreBootstrapProps = {
7
+ siteId: string;
8
+ baseUrl?: string;
9
+ };
10
+
11
+ export function CoreBootstrap({
12
+ siteId,
13
+ baseUrl,
14
+ }: CoreBootstrapProps): JSX.Element {
15
+ initCore(siteId, baseUrl);
16
+
17
+ return <CoreBootstrapClient siteId={siteId} baseUrl={baseUrl} />;
18
+ }
@@ -0,0 +1 @@
1
+ export * from '../../core-react/src/index';
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import type { QueryClient } from '@tanstack/react-query';
4
+ import type { ReactNode } from 'react';
5
+
6
+ import {
7
+ CoreReactProvider,
8
+ type CoreReactProviderProps,
9
+ } from './provider';
10
+ import { createClientQueryErrorHandler } from './query-error-handler';
11
+
12
+ export type AppProvidersProps = {
13
+ children: ReactNode;
14
+ queryClient?: QueryClient;
15
+ defaultOptions?: CoreReactProviderProps['defaultOptions'];
16
+ onGlobalError?: CoreReactProviderProps['onGlobalError'];
17
+ };
18
+
19
+ export function AppProviders({
20
+ children,
21
+ queryClient,
22
+ defaultOptions,
23
+ onGlobalError,
24
+ }: AppProvidersProps) {
25
+ return (
26
+ <CoreReactProvider
27
+ queryClient={queryClient}
28
+ defaultOptions={defaultOptions}
29
+ onGlobalError={
30
+ onGlobalError ?? createClientQueryErrorHandler()
31
+ }
32
+ >
33
+ {children}
34
+ </CoreReactProvider>
35
+ );
36
+ }
@@ -0,0 +1,42 @@
1
+ type RequestOptionsLike = {
2
+ credentials?: RequestCredentials;
3
+ };
4
+
5
+ function isRequestOptionsLike(value: unknown): value is RequestOptionsLike {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+
9
+ function withClientAuthOptions(args: unknown[]) {
10
+ const nextArgs = [...args];
11
+ const lastArg = nextArgs.at(-1);
12
+
13
+ if (!isRequestOptionsLike(lastArg)) {
14
+ nextArgs.push({
15
+ credentials: 'include' satisfies RequestCredentials,
16
+ });
17
+
18
+ return nextArgs;
19
+ }
20
+
21
+ nextArgs[nextArgs.length - 1] = {
22
+ ...lastArg,
23
+ credentials: 'include' satisfies RequestCredentials,
24
+ };
25
+
26
+ return nextArgs;
27
+ }
28
+
29
+ type AsyncRequestFunction = (...args: unknown[]) => Promise<unknown>;
30
+
31
+ /**
32
+ * monorepo 클라이언트 요청 함수에 credentials: include를 덧씌우는 helper입니다.
33
+ */
34
+ export function withClientAuth<TRequestFunction extends AsyncRequestFunction>(
35
+ requestFunction: TRequestFunction,
36
+ ): TRequestFunction {
37
+ return ((...args: Parameters<TRequestFunction>) => {
38
+ return requestFunction(
39
+ ...(withClientAuthOptions(args) as Parameters<TRequestFunction>),
40
+ );
41
+ }) as TRequestFunction;
42
+ }
@@ -0,0 +1,21 @@
1
+ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
2
+ import type { ReactNode } from 'react';
3
+
4
+ import { getCoreQueryClient } from './query-client';
5
+
6
+ export type CoreQueryHydrationProps = {
7
+ children: ReactNode;
8
+ prefetch: (
9
+ queryClient: ReturnType<typeof getCoreQueryClient>,
10
+ ) => Promise<void>;
11
+ };
12
+
13
+ export async function CoreQueryHydration({
14
+ children,
15
+ prefetch,
16
+ }: CoreQueryHydrationProps) {
17
+ const queryClient = getCoreQueryClient();
18
+ await prefetch(queryClient);
19
+
20
+ return <HydrationBoundary state={dehydrate(queryClient)}>{children}</HydrationBoundary>;
21
+ }
@@ -0,0 +1,7 @@
1
+ export * from './app-providers';
2
+ export * from './auth';
3
+ export * from './hydration';
4
+ export * from './provider';
5
+ export * from './query-client';
6
+ export * from './query-error-handler';
7
+ export * from './query-keys';
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import { QueryClientProvider, type QueryClient } from '@tanstack/react-query';
4
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
5
+ import type { ReactNode } from 'react';
6
+
7
+ import {
8
+ type CoreQueryClientOptions,
9
+ getCoreQueryClient,
10
+ } from './query-client';
11
+
12
+ export type CoreReactProviderProps = {
13
+ children: ReactNode;
14
+ queryClient?: QueryClient;
15
+ onGlobalError?: (error: unknown) => void;
16
+ defaultOptions?: CoreQueryClientOptions['defaultOptions'];
17
+ };
18
+
19
+ function isDevelopmentEnvironment() {
20
+ // Vite 앱 타입체크에서도 안전하게 동작하도록 process 전역을 직접 참조하지 않습니다.
21
+ const globalProcess = (globalThis as { process?: { env?: { NODE_ENV?: string } } })
22
+ .process;
23
+
24
+ return globalProcess?.env?.NODE_ENV === 'development';
25
+ }
26
+
27
+ export function CoreReactProvider({
28
+ children,
29
+ queryClient,
30
+ onGlobalError,
31
+ defaultOptions,
32
+ }: CoreReactProviderProps) {
33
+ // 외부에서 client를 주입하지 않으면 monorepo 기본 규칙으로 생성합니다.
34
+ const resolvedQueryClient =
35
+ queryClient ??
36
+ getCoreQueryClient({
37
+ onGlobalError,
38
+ defaultOptions,
39
+ });
40
+
41
+ return (
42
+ <QueryClientProvider client={resolvedQueryClient}>
43
+ {children}
44
+ {isDevelopmentEnvironment() ? (
45
+ <ReactQueryDevtools initialIsOpen={false} />
46
+ ) : null}
47
+ </QueryClientProvider>
48
+ );
49
+ }
@@ -0,0 +1,48 @@
1
+ import type { DefaultOptions } from '@tanstack/react-query';
2
+
3
+ import {
4
+ createSharedQueryClient,
5
+ getSharedQueryClient,
6
+ mergeQueryClientDefaultOptions,
7
+ makeSharedQueryClient,
8
+ type SharedQueryClientOptions,
9
+ } from '../../../../core/query/query-client';
10
+
11
+ const monorepoDefaultOptions: DefaultOptions = {
12
+ queries: {
13
+ refetchOnWindowFocus: false,
14
+ },
15
+ };
16
+
17
+ export type CoreQueryClientOptions = Pick<SharedQueryClientOptions, 'onGlobalError'> & {
18
+ defaultOptions?: DefaultOptions;
19
+ };
20
+
21
+ function mergeMonorepoDefaultOptions(defaultOptions?: DefaultOptions): DefaultOptions {
22
+ return mergeQueryClientDefaultOptions(monorepoDefaultOptions, defaultOptions);
23
+ }
24
+
25
+ /**
26
+ * monorepo 전용 기본값을 공통 QueryClient 옵션 형태로 정규화합니다.
27
+ */
28
+ function toSharedOptions({
29
+ onGlobalError,
30
+ defaultOptions,
31
+ }: CoreQueryClientOptions = {}): SharedQueryClientOptions {
32
+ return {
33
+ onGlobalError,
34
+ defaultOptions: mergeMonorepoDefaultOptions(defaultOptions),
35
+ };
36
+ }
37
+
38
+ export function makeCoreQueryClient(options: CoreQueryClientOptions = {}) {
39
+ return makeSharedQueryClient(toSharedOptions(options));
40
+ }
41
+
42
+ export function getCoreQueryClient(options: CoreQueryClientOptions = {}) {
43
+ return getSharedQueryClient(toSharedOptions(options));
44
+ }
45
+
46
+ export function createCoreQueryClient(options: CoreQueryClientOptions = {}) {
47
+ return createSharedQueryClient(toSharedOptions(options));
48
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ APP_ERROR_CODES,
3
+ type AppError,
4
+ isAppError,
5
+ normalizeAppError,
6
+ } from '../../../../core/error/app-error';
7
+
8
+ export type ClientQueryErrorHandlerOptions = {
9
+ logger?: (error: AppError) => void;
10
+ onError?: (error: AppError) => void;
11
+ onUnauthorized?: (error: AppError) => void;
12
+ onForbidden?: (error: AppError) => void;
13
+ onNotFound?: (error: AppError) => void;
14
+ onNetworkError?: (error: AppError) => void;
15
+ onUnknownError?: (error: AppError) => void;
16
+ };
17
+
18
+ export function handleClientQueryError(
19
+ error: unknown,
20
+ {
21
+ logger,
22
+ onError,
23
+ onUnauthorized,
24
+ onForbidden,
25
+ onNotFound,
26
+ onNetworkError,
27
+ onUnknownError,
28
+ }: ClientQueryErrorHandlerOptions = {},
29
+ ) {
30
+ const appError = isAppError(error) ? error : normalizeAppError(error);
31
+
32
+ logger?.(appError);
33
+ onError?.(appError);
34
+
35
+ switch (appError.code) {
36
+ case APP_ERROR_CODES.UNAUTHORIZED:
37
+ onUnauthorized?.(appError);
38
+ return;
39
+ case APP_ERROR_CODES.FORBIDDEN:
40
+ onForbidden?.(appError);
41
+ return;
42
+ case APP_ERROR_CODES.NOT_FOUND:
43
+ onNotFound?.(appError);
44
+ return;
45
+ case APP_ERROR_CODES.NETWORK_ERROR:
46
+ onNetworkError?.(appError);
47
+ return;
48
+ default:
49
+ onUnknownError?.(appError);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * monorepo 앱은 query 전역 에러 정책을 provider에 쉽게 주입할 수 있게 helper를 유지합니다.
55
+ */
56
+ export function createClientQueryErrorHandler(
57
+ options: ClientQueryErrorHandlerOptions = {},
58
+ ) {
59
+ return (error: unknown) => {
60
+ handleClientQueryError(error, options);
61
+ };
62
+ }
@@ -0,0 +1,22 @@
1
+ type QueryKeyValue =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | null
6
+ | undefined
7
+ | Record<string, unknown>
8
+ | readonly unknown[];
9
+
10
+ export function createQueryKeys<const TScope extends string>(scope: TScope) {
11
+ return {
12
+ all: [scope] as const,
13
+ lists: () => [scope, 'list'] as const,
14
+ list: <TParams extends QueryKeyValue>(params?: TParams) =>
15
+ params === undefined
16
+ ? ([scope, 'list'] as const)
17
+ : ([scope, 'list', params] as const),
18
+ details: () => [scope, 'detail'] as const,
19
+ detail: <TId extends QueryKeyValue>(id: TId) =>
20
+ [scope, 'detail', id] as const,
21
+ };
22
+ }
@@ -0,0 +1,27 @@
1
+ import { executeRequest, type RequestOptions } from './core-request';
2
+
3
+ export type CoreFetchOptions = Omit<RequestOptions, 'method' | 'url'> & {
4
+ method?: RequestOptions['method'];
5
+ };
6
+
7
+ export type CoreFetchFunction = <T = unknown>(
8
+ endpoint: string,
9
+ options?: CoreFetchOptions,
10
+ ) => Promise<T>;
11
+
12
+ /**
13
+ * monorepo 패키지 공개 API는 endpoint + options 형태를 유지하고,
14
+ * 실제 요청 본체는 core-request wrapper로 위임합니다.
15
+ */
16
+ export const coreFetch: CoreFetchFunction = async <T = unknown>(
17
+ endpoint: string,
18
+ options: CoreFetchOptions = {},
19
+ ): Promise<T> => {
20
+ const { method = 'GET', ...rest } = options;
21
+
22
+ return executeRequest<T>({
23
+ method,
24
+ url: endpoint,
25
+ ...rest,
26
+ });
27
+ };
@@ -0,0 +1,93 @@
1
+ import {
2
+ APP_ERROR_CODES,
3
+ type AppError,
4
+ getAppErrorMessage,
5
+ } from '../../../core/error/app-error';
6
+ import {
7
+ createRequestHeaders,
8
+ executeRequest as executeSharedRequest,
9
+ type JsonObject,
10
+ type JsonValue,
11
+ type RequestData,
12
+ type RequestOptions as SharedRequestOptions,
13
+ type RequestParamValue,
14
+ type RequestParams,
15
+ } from '../../../core/request/request-core';
16
+ import { getCoreConfig } from '../core/internal';
17
+
18
+ export type AuthMode = 'auto' | 'none';
19
+
20
+ export type RequestOptions = SharedRequestOptions & {
21
+ siteId?: string;
22
+ auth?: AuthMode;
23
+ };
24
+
25
+ function isBrowser() {
26
+ return typeof window !== 'undefined';
27
+ }
28
+
29
+ function resolveSiteId(siteId?: string) {
30
+ const resolvedSiteId = siteId ?? getCoreConfig().siteId;
31
+
32
+ if (resolvedSiteId) {
33
+ return resolvedSiteId;
34
+ }
35
+
36
+ throw {
37
+ code: APP_ERROR_CODES.INVALID_CONFIGURATION,
38
+ message: getAppErrorMessage(APP_ERROR_CODES.INVALID_CONFIGURATION),
39
+ status: 500,
40
+ } satisfies AppError;
41
+ }
42
+
43
+ function shouldAttachAuth(
44
+ requestUrl: string,
45
+ baseUrl: string | undefined,
46
+ auth: AuthMode,
47
+ ) {
48
+ if (auth === 'none' || !baseUrl) {
49
+ return false;
50
+ }
51
+
52
+ return new URL(requestUrl).origin === new URL(baseUrl).origin;
53
+ }
54
+
55
+ export type {
56
+ JsonObject,
57
+ JsonValue,
58
+ RequestData,
59
+ RequestParamValue,
60
+ RequestParams,
61
+ };
62
+
63
+ /**
64
+ * monorepo wrapper는 siteId, X-Site-Id, same-origin auth 같은
65
+ * 배포 형태별 규칙만 주입합니다.
66
+ */
67
+ export function executeRequest<T>({
68
+ siteId,
69
+ auth = 'auto',
70
+ ...options
71
+ }: RequestOptions): Promise<T> {
72
+ const resolvedSiteId = resolveSiteId(siteId);
73
+
74
+ return executeSharedRequest<T>(options, {
75
+ resolveBaseUrl: (baseUrl) => baseUrl ?? getCoreConfig().baseUrl,
76
+ createHeaders: ({ data, headers }) => {
77
+ const nextHeaders = createRequestHeaders(data, headers);
78
+
79
+ nextHeaders.set('X-Site-Id', resolvedSiteId);
80
+
81
+ return nextHeaders;
82
+ },
83
+ resolveCredentials: ({ requestUrl, resolvedBaseUrl }) => {
84
+ if (!isBrowser()) {
85
+ return undefined;
86
+ }
87
+
88
+ return shouldAttachAuth(requestUrl, resolvedBaseUrl, auth)
89
+ ? 'include'
90
+ : undefined;
91
+ },
92
+ });
93
+ }
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { usePathname, useRouter } from 'next/navigation';
5
+
6
+ import { AUTH_UNAUTHORIZED_EVENT } from '../../../core/auth/auth-events';
7
+ import { routePaths } from '../../../core/routes/route-paths';
8
+
9
+ /**
10
+ * AppProviders에 렌더링해두면 unauthorized 이벤트를 받아
11
+ * 로그인 페이지로 이동시킬 수 있는 템플릿 예시입니다.
12
+ */
13
+ export function AuthErrorListener() {
14
+ const pathname = usePathname();
15
+ const router = useRouter();
16
+
17
+ useEffect(() => {
18
+ function handleUnauthorized() {
19
+ if (pathname === routePaths.login) {
20
+ return;
21
+ }
22
+
23
+ router.replace(routePaths.login);
24
+ }
25
+
26
+ window.addEventListener(AUTH_UNAUTHORIZED_EVENT, handleUnauthorized);
27
+
28
+ return () => {
29
+ window.removeEventListener(AUTH_UNAUTHORIZED_EVENT, handleUnauthorized);
30
+ };
31
+ }, [pathname, router]);
32
+
33
+ return null;
34
+ }