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,41 @@
1
+ import 'server-only';
2
+
3
+ import { forbidden, notFound, redirect } from 'next/navigation';
4
+
5
+ import { APP_ERROR_CODES, normalizeAppError } from '../../../core/error/app-error';
6
+ import { routePaths } from '../../../core/routes/route-paths';
7
+
8
+ export type HandleServerAppErrorOptions = {
9
+ unauthorizedRedirectTo?: string | false;
10
+ notFoundAs404?: boolean;
11
+ forbiddenAs403?: boolean;
12
+ };
13
+
14
+ /**
15
+ * 서버 컴포넌트나 서버 액션에서 AppError를
16
+ * Next.js navigation 동작으로 바꿔야 할 때 선택적으로 사용하는 예시입니다.
17
+ */
18
+ export function handleServerAppError(
19
+ error: unknown,
20
+ {
21
+ unauthorizedRedirectTo = routePaths.login,
22
+ notFoundAs404 = true,
23
+ forbiddenAs403 = false,
24
+ }: HandleServerAppErrorOptions = {},
25
+ ): never {
26
+ const appError = normalizeAppError(error);
27
+
28
+ if (appError.code === APP_ERROR_CODES.UNAUTHORIZED && unauthorizedRedirectTo) {
29
+ redirect(unauthorizedRedirectTo);
30
+ }
31
+
32
+ if (appError.code === APP_ERROR_CODES.NOT_FOUND && notFoundAs404) {
33
+ notFound();
34
+ }
35
+
36
+ if (appError.code === APP_ERROR_CODES.FORBIDDEN && forbiddenAs403) {
37
+ forbidden();
38
+ }
39
+
40
+ throw appError;
41
+ }
@@ -0,0 +1,20 @@
1
+ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
2
+ import type { ReactNode } from 'react';
3
+
4
+ import { getQueryClient } from '../../../core/query/query-client';
5
+
6
+ export type QueryHydrationProps = {
7
+ children: ReactNode;
8
+ prefetch: (queryClient: ReturnType<typeof getQueryClient>) => Promise<void>;
9
+ };
10
+
11
+ /**
12
+ * 서버 prefetch 후 HydrationBoundary로 넘기는 템플릿 예시입니다.
13
+ * 서버에서 미리 채운 query를 클라이언트로 넘겨야 할 때 선택적으로 사용하세요.
14
+ */
15
+ export async function QueryHydration({ children, prefetch }: QueryHydrationProps) {
16
+ const queryClient = getQueryClient();
17
+ await prefetch(queryClient);
18
+
19
+ return <HydrationBoundary state={dehydrate(queryClient)}>{children}</HydrationBoundary>;
20
+ }
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import { QueryClientProvider } from '@tanstack/react-query';
4
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
5
+ import type { ReactNode } from 'react';
6
+
7
+ import { getQueryClient } from '../../../core/query/query-client';
8
+
9
+ export type QueryProvidersProps = {
10
+ children: ReactNode;
11
+ /**
12
+ * 템플릿 확장 포인트입니다.
13
+ * handleClientAppError 같은 전역 에러 핸들러를 필요할 때만 주입해서 사용하세요.
14
+ */
15
+ onGlobalError?: (error: unknown) => void;
16
+ };
17
+
18
+ /**
19
+ * TanStack Query provider 예시입니다.
20
+ * 템플릿 기본값만 담고 있으니 프로젝트 정책에 맞게 옵션을 확장해서 사용하세요.
21
+ */
22
+ export function QueryProviders({ children, onGlobalError }: QueryProvidersProps) {
23
+ const queryClient = getQueryClient({
24
+ onGlobalError,
25
+ });
26
+
27
+ return (
28
+ <QueryClientProvider client={queryClient}>
29
+ {children}
30
+ {process.env.NODE_ENV === 'development' ? (
31
+ <ReactQueryDevtools initialIsOpen={false} />
32
+ ) : null}
33
+ </QueryClientProvider>
34
+ );
35
+ }
@@ -0,0 +1,24 @@
1
+ import { executeRequest, type RequestOptions } from './request';
2
+
3
+ export type ClientRequestOptions = Omit<RequestOptions, 'credentials'>;
4
+
5
+ function requestWithCredentials(credentials: RequestCredentials) {
6
+ return function executeClientRequest<T>(options: ClientRequestOptions) {
7
+ return executeRequest<T>({
8
+ ...(options as RequestOptions),
9
+ credentials,
10
+ });
11
+ };
12
+ }
13
+
14
+ /**
15
+ * 인증 쿠키가 필요 없는 클라이언트 요청 래퍼 예시입니다.
16
+ * 프로젝트 정책에 맞는 공통 옵션이 있다면 여기서 확장해서 사용하세요.
17
+ */
18
+ export const requestClient = requestWithCredentials('omit');
19
+
20
+ /**
21
+ * 인증 쿠키를 함께 보내는 클라이언트 요청 래퍼 예시입니다.
22
+ * 실제 인증 전략에 맞게 credentials 정책을 조정해서 사용하세요.
23
+ */
24
+ export const requestClientAuth = requestWithCredentials('include');
@@ -0,0 +1,64 @@
1
+ import 'server-only';
2
+
3
+ import { cookies } from 'next/headers';
4
+
5
+ import { executeRequest, type RequestOptions } from './request';
6
+
7
+ export type ServerRequestOptions = Omit<RequestOptions, 'headers'> & {
8
+ headers?: HeadersInit;
9
+ };
10
+
11
+ const AUTH_COOKIE_NAMES = ['session', 'access_token', 'refresh_token'] as const;
12
+
13
+ export type ServerAuthRequestOptions = ServerRequestOptions & {
14
+ authCookieNames?: readonly string[];
15
+ };
16
+
17
+ function mergeCookieHeader(headers: HeadersInit | undefined, cookieHeader: string) {
18
+ const nextHeaders = new Headers(headers);
19
+ const existingCookieHeader = nextHeaders.get('Cookie');
20
+
21
+ nextHeaders.set(
22
+ 'Cookie',
23
+ existingCookieHeader ? `${existingCookieHeader}; ${cookieHeader}` : cookieHeader,
24
+ );
25
+
26
+ return nextHeaders;
27
+ }
28
+
29
+ function createAuthCookieHeader(
30
+ cookieNames: readonly string[],
31
+ cookieStore: Awaited<ReturnType<typeof cookies>>,
32
+ ) {
33
+ return cookieNames
34
+ .map((cookieName) => cookieStore.get(cookieName))
35
+ .filter((cookie) => cookie !== undefined)
36
+ .map((cookie) => `${cookie.name}=${cookie.value}`)
37
+ .join('; ');
38
+ }
39
+
40
+ /**
41
+ * 인증 정보가 필요 없는 서버 요청 래퍼 예시입니다.
42
+ * 서버 공통 헤더나 캐시 정책이 필요하면 여기서 확장해서 사용하세요.
43
+ */
44
+ export function requestServer<T>(options: ServerRequestOptions) {
45
+ return executeRequest<T>({
46
+ ...options,
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Next 서버 쿠키를 자동 수집해서 Cookie 헤더로 합쳐 전달합니다.
52
+ */
53
+ export async function requestServerAuth<T>({
54
+ authCookieNames = AUTH_COOKIE_NAMES,
55
+ ...options
56
+ }: ServerAuthRequestOptions) {
57
+ const cookieStore = await cookies();
58
+ const cookieHeader = createAuthCookieHeader(authCookieNames, cookieStore);
59
+
60
+ return executeRequest<T>({
61
+ ...options,
62
+ headers: cookieHeader ? mergeCookieHeader(options.headers, cookieHeader) : options.headers,
63
+ });
64
+ }
@@ -0,0 +1,52 @@
1
+ import {
2
+ executeRequest as executeSharedRequest,
3
+ type JsonObject,
4
+ type JsonValue,
5
+ type RequestData,
6
+ type RequestOptions,
7
+ type RequestParamValue,
8
+ type RequestParams,
9
+ } from '../../../core/request/request-core';
10
+
11
+ function assertUrl(value: string, name: string) {
12
+ try {
13
+ return new URL(value).toString().replace(/\/$/, '');
14
+ } catch {
15
+ throw new Error(`${name} 환경 변수는 유효한 URL이어야 합니다.`);
16
+ }
17
+ }
18
+
19
+ function resolveBaseUrl(baseUrl?: string) {
20
+ if (baseUrl !== undefined) {
21
+ return baseUrl;
22
+ }
23
+
24
+ if (process.env.API_BASE_URL) {
25
+ return assertUrl(process.env.API_BASE_URL, 'API_BASE_URL');
26
+ }
27
+
28
+ if (process.env.NEXT_PUBLIC_API_BASE_URL) {
29
+ return assertUrl(process.env.NEXT_PUBLIC_API_BASE_URL, 'NEXT_PUBLIC_API_BASE_URL');
30
+ }
31
+
32
+ return undefined;
33
+ }
34
+
35
+ export type {
36
+ JsonObject,
37
+ JsonValue,
38
+ RequestData,
39
+ RequestOptions,
40
+ RequestParamValue,
41
+ RequestParams,
42
+ };
43
+
44
+ /**
45
+ * Next wrapper는 서버/클라이언트 공용 baseUrl 해석만 맡고,
46
+ * 나머지 요청 본체는 공통 request 본체를 그대로 사용합니다.
47
+ */
48
+ export function executeRequest<T>(options: RequestOptions): Promise<T> {
49
+ return executeSharedRequest<T>(options, {
50
+ resolveBaseUrl,
51
+ });
52
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { useModalStore } from '../../../core/ui/modal-store';
4
+
5
+ export function GlobalModal() {
6
+ const { isOpen, title, description, closeModal } = useModalStore();
7
+
8
+ if (!isOpen) {
9
+ return null;
10
+ }
11
+
12
+ return (
13
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
14
+ <div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-lg">
15
+ <h2 className="text-lg font-semibold text-zinc-950">{title}</h2>
16
+ {description ? <p className="mt-2 text-sm leading-6 text-zinc-600">{description}</p> : null}
17
+ <div className="mt-6 flex justify-end">
18
+ <button
19
+ type="button"
20
+ onClick={closeModal}
21
+ className="rounded-lg bg-zinc-950 px-4 py-2 text-sm font-medium text-white"
22
+ >
23
+ 닫기
24
+ </button>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,34 @@
1
+ import { useEffect } from 'react';
2
+ import { useLocation, useNavigate } from 'react-router-dom';
3
+
4
+ import { AUTH_UNAUTHORIZED_EVENT } from '../../../core/auth/auth-events';
5
+ import { routePaths } from '../../../core/routes/route-paths';
6
+
7
+ /**
8
+ * AppProviders에 렌더링해두면 unauthorized 이벤트를 받아
9
+ * 로그인 페이지로 이동시킬 수 있는 템플릿 예시입니다.
10
+ */
11
+ export function AuthErrorListener() {
12
+ const location = useLocation();
13
+ const navigate = useNavigate();
14
+
15
+ useEffect(() => {
16
+ function handleUnauthorized() {
17
+ if (location.pathname === routePaths.login) {
18
+ return;
19
+ }
20
+
21
+ navigate(routePaths.login, {
22
+ replace: true,
23
+ });
24
+ }
25
+
26
+ window.addEventListener(AUTH_UNAUTHORIZED_EVENT, handleUnauthorized);
27
+
28
+ return () => {
29
+ window.removeEventListener(AUTH_UNAUTHORIZED_EVENT, handleUnauthorized);
30
+ };
31
+ }, [location.pathname, navigate]);
32
+
33
+ return null;
34
+ }
@@ -0,0 +1,31 @@
1
+ import { QueryClientProvider } from '@tanstack/react-query';
2
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
3
+ import type { ReactNode } from 'react';
4
+
5
+ import { getQueryClient } from '../../../core/query/query-client';
6
+
7
+ export type QueryProvidersProps = {
8
+ children: ReactNode;
9
+ /**
10
+ * 템플릿 확장 포인트입니다.
11
+ * handleClientAppError 같은 전역 에러 핸들러를 필요할 때만 주입해서 사용하세요.
12
+ */
13
+ onGlobalError?: (error: unknown) => void;
14
+ };
15
+
16
+ /**
17
+ * TanStack Query provider 예시입니다.
18
+ * 템플릿 기본값만 담고 있으니 프로젝트 정책에 맞게 옵션을 확장해서 사용하세요.
19
+ */
20
+ export function QueryProviders({ children, onGlobalError }: QueryProvidersProps) {
21
+ const queryClient = getQueryClient({
22
+ onGlobalError,
23
+ });
24
+
25
+ return (
26
+ <QueryClientProvider client={queryClient}>
27
+ {children}
28
+ {import.meta.env.DEV ? <ReactQueryDevtools initialIsOpen={false} /> : null}
29
+ </QueryClientProvider>
30
+ );
31
+ }
@@ -0,0 +1,24 @@
1
+ import { executeRequest, type RequestOptions } from './request';
2
+
3
+ export type ClientRequestOptions = Omit<RequestOptions, 'credentials'>;
4
+
5
+ function requestWithCredentials(credentials: RequestCredentials) {
6
+ return function executeClientRequest<T>(options: ClientRequestOptions) {
7
+ return executeRequest<T>({
8
+ ...(options as RequestOptions),
9
+ credentials,
10
+ });
11
+ };
12
+ }
13
+
14
+ /**
15
+ * 인증 쿠키가 필요 없는 클라이언트 요청 래퍼 예시입니다.
16
+ * 프로젝트 정책에 맞는 공통 옵션이 있다면 여기서 확장해서 사용하세요.
17
+ */
18
+ export const requestClient = requestWithCredentials('omit');
19
+
20
+ /**
21
+ * 인증 쿠키를 함께 보내는 클라이언트 요청 래퍼 예시입니다.
22
+ * 실제 인증 전략에 맞게 credentials 정책을 조정해서 사용하세요.
23
+ */
24
+ export const requestClientAuth = requestWithCredentials('include');
@@ -0,0 +1,51 @@
1
+ import {
2
+ executeRequest as executeSharedRequest,
3
+ type JsonObject,
4
+ type JsonValue,
5
+ type RequestData,
6
+ type RequestOptions,
7
+ type RequestParamValue,
8
+ type RequestParams,
9
+ } from '../../../core/request/request-core';
10
+
11
+ function assertUrl(value: string, name: string) {
12
+ try {
13
+ return new URL(value).toString().replace(/\/$/, '');
14
+ } catch {
15
+ throw new Error(`${name} 환경 변수는 유효한 URL이어야 합니다.`);
16
+ }
17
+ }
18
+
19
+ function resolveBaseUrl(baseUrl?: string) {
20
+ if (baseUrl !== undefined) {
21
+ return baseUrl;
22
+ }
23
+
24
+ const env = import.meta.env as Record<string, string | undefined>;
25
+ const value = env.VITE_API_BASE_URL;
26
+
27
+ if (!value) {
28
+ return undefined;
29
+ }
30
+
31
+ return assertUrl(value, 'VITE_API_BASE_URL');
32
+ }
33
+
34
+ export type {
35
+ JsonObject,
36
+ JsonValue,
37
+ RequestData,
38
+ RequestOptions,
39
+ RequestParamValue,
40
+ RequestParams,
41
+ };
42
+
43
+ /**
44
+ * React standalone wrapper는 Vite env 해석만 맡고,
45
+ * 실제 요청 직렬화/에러 처리 로직은 공통 request 본체를 재사용합니다.
46
+ */
47
+ export function executeRequest<T>(options: RequestOptions): Promise<T> {
48
+ return executeSharedRequest<T>(options, {
49
+ resolveBaseUrl,
50
+ });
51
+ }
@@ -0,0 +1,27 @@
1
+ import { useModalStore } from '../../../core/ui/modal-store';
2
+
3
+ export function GlobalModal() {
4
+ const { isOpen, title, description, closeModal } = useModalStore();
5
+
6
+ if (!isOpen) {
7
+ return null;
8
+ }
9
+
10
+ return (
11
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
12
+ <div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-lg">
13
+ <h2 className="text-lg font-semibold text-zinc-950">{title}</h2>
14
+ {description ? <p className="mt-2 text-sm leading-6 text-zinc-600">{description}</p> : null}
15
+ <div className="mt-6 flex justify-end">
16
+ <button
17
+ type="button"
18
+ onClick={closeModal}
19
+ className="rounded-lg bg-zinc-950 px-4 py-2 text-sm font-medium text-white"
20
+ >
21
+ 닫기
22
+ </button>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,38 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # Dependencies
4
+ node_modules
5
+ .pnp
6
+ .pnp.js
7
+
8
+ # Local env files
9
+ .env
10
+ .env.local
11
+ .env.development.local
12
+ .env.test.local
13
+ .env.production.local
14
+
15
+ # Testing
16
+ coverage
17
+
18
+ # Turbo
19
+ .turbo
20
+
21
+ # Vercel
22
+ .vercel
23
+
24
+ # Build Outputs
25
+ .next/
26
+ out/
27
+ build
28
+ dist
29
+
30
+
31
+ # Debug
32
+ npm-debug.log*
33
+ yarn-debug.log*
34
+ yarn-error.log*
35
+
36
+ # Misc
37
+ .DS_Store
38
+ *.pem