@mandujs/core 0.53.2 → 0.54.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.
- package/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -380
- package/src/diagnose/checks.ts +832 -720
- package/src/diagnose/index.ts +17 -16
- package/src/diagnose/run.ts +93 -91
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
package/src/client/hooks.ts
CHANGED
|
@@ -1,362 +1,362 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Router Hooks 🪝
|
|
3
|
-
* React hooks for client-side routing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect, useCallback, useMemo, useSyncExternalStore } from "react";
|
|
7
|
-
import {
|
|
8
|
-
subscribe,
|
|
9
|
-
getRouterState,
|
|
10
|
-
navigate,
|
|
11
|
-
submitAction,
|
|
12
|
-
type RouteInfo,
|
|
13
|
-
type NavigationState,
|
|
14
|
-
type NavigateOptions,
|
|
15
|
-
type ActionResult,
|
|
16
|
-
} from "./router";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 라우터 상태 전체 접근
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```tsx
|
|
23
|
-
* const { currentRoute, loaderData, navigation } = useRouterState();
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export function useRouterState() {
|
|
27
|
-
return useSyncExternalStore(
|
|
28
|
-
subscribe,
|
|
29
|
-
getRouterState,
|
|
30
|
-
getRouterState // SSR에서도 동일
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 현재 라우트 정보
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```tsx
|
|
39
|
-
* const route = useRoute();
|
|
40
|
-
* console.log(route?.id, route?.params);
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
export function useRoute(): RouteInfo | null {
|
|
44
|
-
const state = useRouterState();
|
|
45
|
-
return state.currentRoute;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* URL 파라미터 접근
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```tsx
|
|
53
|
-
* // URL: /users/123
|
|
54
|
-
* const { id } = useParams<{ id: string }>();
|
|
55
|
-
* console.log(id); // "123"
|
|
56
|
-
* ```
|
|
57
|
-
*/
|
|
58
|
-
export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
|
|
59
|
-
const route = useRoute();
|
|
60
|
-
return (route?.params ?? {}) as T;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 현재 경로명
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```tsx
|
|
68
|
-
* const pathname = usePathname();
|
|
69
|
-
* console.log(pathname); // "/users/123"
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
export function usePathname(): string {
|
|
73
|
-
const [pathname, setPathname] = useState(() =>
|
|
74
|
-
typeof window !== "undefined" ? window.location.pathname : "/"
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
const handleChange = () => {
|
|
79
|
-
setPathname(window.location.pathname);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
window.addEventListener("popstate", handleChange);
|
|
83
|
-
|
|
84
|
-
// 라우터 상태 변경 구독
|
|
85
|
-
const unsubscribe = subscribe(() => {
|
|
86
|
-
setPathname(window.location.pathname);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
return () => {
|
|
90
|
-
window.removeEventListener("popstate", handleChange);
|
|
91
|
-
unsubscribe();
|
|
92
|
-
};
|
|
93
|
-
}, []);
|
|
94
|
-
|
|
95
|
-
return pathname;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 현재 검색 파라미터 (쿼리 스트링)
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* ```tsx
|
|
103
|
-
* // URL: /search?q=hello&page=2
|
|
104
|
-
* const searchParams = useSearchParams();
|
|
105
|
-
* console.log(searchParams.get("q")); // "hello"
|
|
106
|
-
* ```
|
|
107
|
-
*/
|
|
108
|
-
export function useSearchParams(): URLSearchParams {
|
|
109
|
-
const [searchParams, setSearchParams] = useState(() =>
|
|
110
|
-
typeof window !== "undefined"
|
|
111
|
-
? new URLSearchParams(window.location.search)
|
|
112
|
-
: new URLSearchParams()
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
const handleChange = () => {
|
|
117
|
-
setSearchParams(new URLSearchParams(window.location.search));
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
window.addEventListener("popstate", handleChange);
|
|
121
|
-
|
|
122
|
-
const unsubscribe = subscribe(() => {
|
|
123
|
-
setSearchParams(new URLSearchParams(window.location.search));
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
return () => {
|
|
127
|
-
window.removeEventListener("popstate", handleChange);
|
|
128
|
-
unsubscribe();
|
|
129
|
-
};
|
|
130
|
-
}, []);
|
|
131
|
-
|
|
132
|
-
return searchParams;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Loader 데이터 접근
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* ```tsx
|
|
140
|
-
* interface UserData { name: string; email: string; }
|
|
141
|
-
* const data = useLoaderData<UserData>();
|
|
142
|
-
* ```
|
|
143
|
-
*/
|
|
144
|
-
export function useLoaderData<T = unknown>(): T | undefined {
|
|
145
|
-
const state = useRouterState();
|
|
146
|
-
return state.loaderData as T | undefined;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* 네비게이션 상태 (로딩 여부)
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* ```tsx
|
|
154
|
-
* const { state, location } = useNavigation();
|
|
155
|
-
*
|
|
156
|
-
* if (state === "loading") {
|
|
157
|
-
* return <Spinner />;
|
|
158
|
-
* }
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
export function useNavigation(): NavigationState {
|
|
162
|
-
const state = useRouterState();
|
|
163
|
-
return state.navigation;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* 프로그래매틱 네비게이션
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* ```tsx
|
|
171
|
-
* const navigate = useNavigate();
|
|
172
|
-
*
|
|
173
|
-
* const handleClick = () => {
|
|
174
|
-
* navigate("/dashboard");
|
|
175
|
-
* };
|
|
176
|
-
*
|
|
177
|
-
* const handleSubmit = () => {
|
|
178
|
-
* navigate("/success", { replace: true });
|
|
179
|
-
* };
|
|
180
|
-
* ```
|
|
181
|
-
*/
|
|
182
|
-
export function useNavigate(): (to: string, options?: NavigateOptions) => Promise<void> {
|
|
183
|
-
return useCallback((to: string, options?: NavigateOptions) => {
|
|
184
|
-
return navigate(to, options);
|
|
185
|
-
}, []);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* 라우터 통합 훅 (편의용)
|
|
190
|
-
*
|
|
191
|
-
* @example
|
|
192
|
-
* ```tsx
|
|
193
|
-
* const {
|
|
194
|
-
* pathname,
|
|
195
|
-
* params,
|
|
196
|
-
* searchParams,
|
|
197
|
-
* navigate,
|
|
198
|
-
* isNavigating
|
|
199
|
-
* } = useRouter();
|
|
200
|
-
* ```
|
|
201
|
-
*/
|
|
202
|
-
export function useRouter() {
|
|
203
|
-
const pathname = usePathname();
|
|
204
|
-
const params = useParams();
|
|
205
|
-
const searchParams = useSearchParams();
|
|
206
|
-
const navigation = useNavigation();
|
|
207
|
-
const navigateFn = useNavigate();
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
/** 현재 경로명 */
|
|
211
|
-
pathname,
|
|
212
|
-
/** URL 파라미터 */
|
|
213
|
-
params,
|
|
214
|
-
/** 검색 파라미터 (쿼리 스트링) */
|
|
215
|
-
searchParams,
|
|
216
|
-
/** 네비게이션 함수 */
|
|
217
|
-
navigate: navigateFn,
|
|
218
|
-
/** 네비게이션 중 여부 */
|
|
219
|
-
isNavigating: navigation.state === "loading",
|
|
220
|
-
/** 네비게이션 상태 상세 */
|
|
221
|
-
navigation,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* 특정 경로와 현재 경로 일치 여부
|
|
227
|
-
*
|
|
228
|
-
* @example
|
|
229
|
-
* ```tsx
|
|
230
|
-
* const isActive = useMatch("/about");
|
|
231
|
-
* const isUsersPage = useMatch("/users/:id");
|
|
232
|
-
* ```
|
|
233
|
-
*/
|
|
234
|
-
export function useMatch(pattern: string): boolean {
|
|
235
|
-
const pathname = usePathname();
|
|
236
|
-
|
|
237
|
-
const regex = useMemo(() => {
|
|
238
|
-
// 파라미터를 플레이스홀더로 치환 → 나머지 특수문자 이스케이프 → 플레이스홀더 복원
|
|
239
|
-
const PLACEHOLDER = "\x00PARAM\x00";
|
|
240
|
-
const withPlaceholders = pattern.replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, PLACEHOLDER);
|
|
241
|
-
const escaped = withPlaceholders.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
|
-
const regexStr = escaped.replace(
|
|
243
|
-
new RegExp(PLACEHOLDER.replace(/\x00/g, "\\x00"), "g"),
|
|
244
|
-
"[^/]+"
|
|
245
|
-
);
|
|
246
|
-
return new RegExp(`^${regexStr}$`);
|
|
247
|
-
}, [pattern]);
|
|
248
|
-
|
|
249
|
-
return regex.test(pathname);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* 뒤로 가기
|
|
254
|
-
*/
|
|
255
|
-
export function useGoBack(): () => void {
|
|
256
|
-
return useCallback(() => {
|
|
257
|
-
if (typeof window !== "undefined") {
|
|
258
|
-
window.history.back();
|
|
259
|
-
}
|
|
260
|
-
}, []);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* 앞으로 가기
|
|
265
|
-
*/
|
|
266
|
-
export function useGoForward(): () => void {
|
|
267
|
-
return useCallback(() => {
|
|
268
|
-
if (typeof window !== "undefined") {
|
|
269
|
-
window.history.forward();
|
|
270
|
-
}
|
|
271
|
-
}, []);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Action 데이터 접근
|
|
276
|
-
* filling.action()의 결과 데이터
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* ```tsx
|
|
280
|
-
* const actionData = useActionData<{ created: boolean }>();
|
|
281
|
-
* if (actionData?.created) { ... }
|
|
282
|
-
* ```
|
|
283
|
-
*/
|
|
284
|
-
export function useActionData<T = unknown>(): T | undefined {
|
|
285
|
-
const state = useRouterState();
|
|
286
|
-
return state.actionData as T | undefined;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Action 제출 함수
|
|
291
|
-
*
|
|
292
|
-
* @example
|
|
293
|
-
* ```tsx
|
|
294
|
-
* const submit = useSubmit();
|
|
295
|
-
* const handleCreate = () => submit("/api/todos", { title: "New" }, "create");
|
|
296
|
-
* ```
|
|
297
|
-
*/
|
|
298
|
-
export function useSubmit(): (
|
|
299
|
-
url: string,
|
|
300
|
-
data: FormData | Record<string, unknown>,
|
|
301
|
-
actionName?: string,
|
|
302
|
-
method?: string
|
|
303
|
-
) => Promise<ActionResult> {
|
|
304
|
-
return useCallback(
|
|
305
|
-
(url: string, data: FormData | Record<string, unknown>, actionName = "default", method = "POST") => {
|
|
306
|
-
return submitAction(url, data, actionName, method);
|
|
307
|
-
},
|
|
308
|
-
[]
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Mandu 통합 상태 훅
|
|
314
|
-
* 라우트, 데이터, 네비게이션, 액션 상태를 하나의 객체로 제공
|
|
315
|
-
*
|
|
316
|
-
* @example
|
|
317
|
-
* ```tsx
|
|
318
|
-
* const {
|
|
319
|
-
* url, params, routeId,
|
|
320
|
-
* loaderData, actionData,
|
|
321
|
-
* navigation,
|
|
322
|
-
* navigate, submit,
|
|
323
|
-
* } = useMandu();
|
|
324
|
-
* ```
|
|
325
|
-
*/
|
|
326
|
-
export function useMandu() {
|
|
327
|
-
const state = useRouterState();
|
|
328
|
-
const navigateFn = useNavigate();
|
|
329
|
-
const submitFn = useSubmit();
|
|
330
|
-
|
|
331
|
-
// URL은 라우터 상태가 실제로 바뀔 때만 재생성한다.
|
|
332
|
-
// query-only navigation도 반영되도록 상태 전체를 의존성으로 사용한다.
|
|
333
|
-
const url = useMemo(
|
|
334
|
-
() => typeof window !== "undefined" ? new URL(window.location.href) : null,
|
|
335
|
-
[state]
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
/** 현재 URL (브라우저) */
|
|
340
|
-
url,
|
|
341
|
-
/** URL 파라미터 */
|
|
342
|
-
params: (state.currentRoute?.params ?? {}) as Record<string, string>,
|
|
343
|
-
/** 현재 라우트 ID */
|
|
344
|
-
routeId: state.currentRoute?.id ?? "",
|
|
345
|
-
/** 라우트 패턴 */
|
|
346
|
-
pattern: state.currentRoute?.pattern ?? "",
|
|
347
|
-
/** Loader 데이터 */
|
|
348
|
-
loaderData: state.loaderData,
|
|
349
|
-
/** 마지막 Action 결과 */
|
|
350
|
-
actionData: state.actionData,
|
|
351
|
-
/** 네비게이션 상태 */
|
|
352
|
-
navigation: state.navigation,
|
|
353
|
-
/** 네비게이션 중 여부 */
|
|
354
|
-
isNavigating: state.navigation.state === "loading",
|
|
355
|
-
/** Action 제출 중 여부 */
|
|
356
|
-
isSubmitting: state.navigation.state === "submitting",
|
|
357
|
-
/** 프로그래매틱 네비게이션 */
|
|
358
|
-
navigate: navigateFn,
|
|
359
|
-
/** Action 제출 */
|
|
360
|
-
submit: submitFn,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Router Hooks 🪝
|
|
3
|
+
* React hooks for client-side routing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useCallback, useMemo, useSyncExternalStore } from "react";
|
|
7
|
+
import {
|
|
8
|
+
subscribe,
|
|
9
|
+
getRouterState,
|
|
10
|
+
navigate,
|
|
11
|
+
submitAction,
|
|
12
|
+
type RouteInfo,
|
|
13
|
+
type NavigationState,
|
|
14
|
+
type NavigateOptions,
|
|
15
|
+
type ActionResult,
|
|
16
|
+
} from "./router";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 라우터 상태 전체 접근
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const { currentRoute, loaderData, navigation } = useRouterState();
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function useRouterState() {
|
|
27
|
+
return useSyncExternalStore(
|
|
28
|
+
subscribe,
|
|
29
|
+
getRouterState,
|
|
30
|
+
getRouterState // SSR에서도 동일
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 현재 라우트 정보
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* const route = useRoute();
|
|
40
|
+
* console.log(route?.id, route?.params);
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function useRoute(): RouteInfo | null {
|
|
44
|
+
const state = useRouterState();
|
|
45
|
+
return state.currentRoute;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* URL 파라미터 접근
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* // URL: /users/123
|
|
54
|
+
* const { id } = useParams<{ id: string }>();
|
|
55
|
+
* console.log(id); // "123"
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
|
|
59
|
+
const route = useRoute();
|
|
60
|
+
return (route?.params ?? {}) as T;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 현재 경로명
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```tsx
|
|
68
|
+
* const pathname = usePathname();
|
|
69
|
+
* console.log(pathname); // "/users/123"
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function usePathname(): string {
|
|
73
|
+
const [pathname, setPathname] = useState(() =>
|
|
74
|
+
typeof window !== "undefined" ? window.location.pathname : "/"
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const handleChange = () => {
|
|
79
|
+
setPathname(window.location.pathname);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
window.addEventListener("popstate", handleChange);
|
|
83
|
+
|
|
84
|
+
// 라우터 상태 변경 구독
|
|
85
|
+
const unsubscribe = subscribe(() => {
|
|
86
|
+
setPathname(window.location.pathname);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
window.removeEventListener("popstate", handleChange);
|
|
91
|
+
unsubscribe();
|
|
92
|
+
};
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
return pathname;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 현재 검색 파라미터 (쿼리 스트링)
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* // URL: /search?q=hello&page=2
|
|
104
|
+
* const searchParams = useSearchParams();
|
|
105
|
+
* console.log(searchParams.get("q")); // "hello"
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function useSearchParams(): URLSearchParams {
|
|
109
|
+
const [searchParams, setSearchParams] = useState(() =>
|
|
110
|
+
typeof window !== "undefined"
|
|
111
|
+
? new URLSearchParams(window.location.search)
|
|
112
|
+
: new URLSearchParams()
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const handleChange = () => {
|
|
117
|
+
setSearchParams(new URLSearchParams(window.location.search));
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
window.addEventListener("popstate", handleChange);
|
|
121
|
+
|
|
122
|
+
const unsubscribe = subscribe(() => {
|
|
123
|
+
setSearchParams(new URLSearchParams(window.location.search));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
window.removeEventListener("popstate", handleChange);
|
|
128
|
+
unsubscribe();
|
|
129
|
+
};
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
return searchParams;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Loader 데이터 접근
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```tsx
|
|
140
|
+
* interface UserData { name: string; email: string; }
|
|
141
|
+
* const data = useLoaderData<UserData>();
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function useLoaderData<T = unknown>(): T | undefined {
|
|
145
|
+
const state = useRouterState();
|
|
146
|
+
return state.loaderData as T | undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 네비게이션 상태 (로딩 여부)
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```tsx
|
|
154
|
+
* const { state, location } = useNavigation();
|
|
155
|
+
*
|
|
156
|
+
* if (state === "loading") {
|
|
157
|
+
* return <Spinner />;
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function useNavigation(): NavigationState {
|
|
162
|
+
const state = useRouterState();
|
|
163
|
+
return state.navigation;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 프로그래매틱 네비게이션
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```tsx
|
|
171
|
+
* const navigate = useNavigate();
|
|
172
|
+
*
|
|
173
|
+
* const handleClick = () => {
|
|
174
|
+
* navigate("/dashboard");
|
|
175
|
+
* };
|
|
176
|
+
*
|
|
177
|
+
* const handleSubmit = () => {
|
|
178
|
+
* navigate("/success", { replace: true });
|
|
179
|
+
* };
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export function useNavigate(): (to: string, options?: NavigateOptions) => Promise<void> {
|
|
183
|
+
return useCallback((to: string, options?: NavigateOptions) => {
|
|
184
|
+
return navigate(to, options);
|
|
185
|
+
}, []);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 라우터 통합 훅 (편의용)
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```tsx
|
|
193
|
+
* const {
|
|
194
|
+
* pathname,
|
|
195
|
+
* params,
|
|
196
|
+
* searchParams,
|
|
197
|
+
* navigate,
|
|
198
|
+
* isNavigating
|
|
199
|
+
* } = useRouter();
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export function useRouter() {
|
|
203
|
+
const pathname = usePathname();
|
|
204
|
+
const params = useParams();
|
|
205
|
+
const searchParams = useSearchParams();
|
|
206
|
+
const navigation = useNavigation();
|
|
207
|
+
const navigateFn = useNavigate();
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
/** 현재 경로명 */
|
|
211
|
+
pathname,
|
|
212
|
+
/** URL 파라미터 */
|
|
213
|
+
params,
|
|
214
|
+
/** 검색 파라미터 (쿼리 스트링) */
|
|
215
|
+
searchParams,
|
|
216
|
+
/** 네비게이션 함수 */
|
|
217
|
+
navigate: navigateFn,
|
|
218
|
+
/** 네비게이션 중 여부 */
|
|
219
|
+
isNavigating: navigation.state === "loading",
|
|
220
|
+
/** 네비게이션 상태 상세 */
|
|
221
|
+
navigation,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 특정 경로와 현재 경로 일치 여부
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```tsx
|
|
230
|
+
* const isActive = useMatch("/about");
|
|
231
|
+
* const isUsersPage = useMatch("/users/:id");
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export function useMatch(pattern: string): boolean {
|
|
235
|
+
const pathname = usePathname();
|
|
236
|
+
|
|
237
|
+
const regex = useMemo(() => {
|
|
238
|
+
// 파라미터를 플레이스홀더로 치환 → 나머지 특수문자 이스케이프 → 플레이스홀더 복원
|
|
239
|
+
const PLACEHOLDER = "\x00PARAM\x00";
|
|
240
|
+
const withPlaceholders = pattern.replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, PLACEHOLDER);
|
|
241
|
+
const escaped = withPlaceholders.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
|
+
const regexStr = escaped.replace(
|
|
243
|
+
new RegExp(PLACEHOLDER.replace(/\x00/g, "\\x00"), "g"),
|
|
244
|
+
"[^/]+"
|
|
245
|
+
);
|
|
246
|
+
return new RegExp(`^${regexStr}$`);
|
|
247
|
+
}, [pattern]);
|
|
248
|
+
|
|
249
|
+
return regex.test(pathname);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 뒤로 가기
|
|
254
|
+
*/
|
|
255
|
+
export function useGoBack(): () => void {
|
|
256
|
+
return useCallback(() => {
|
|
257
|
+
if (typeof window !== "undefined") {
|
|
258
|
+
window.history.back();
|
|
259
|
+
}
|
|
260
|
+
}, []);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 앞으로 가기
|
|
265
|
+
*/
|
|
266
|
+
export function useGoForward(): () => void {
|
|
267
|
+
return useCallback(() => {
|
|
268
|
+
if (typeof window !== "undefined") {
|
|
269
|
+
window.history.forward();
|
|
270
|
+
}
|
|
271
|
+
}, []);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Action 데이터 접근
|
|
276
|
+
* filling.action()의 결과 데이터
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```tsx
|
|
280
|
+
* const actionData = useActionData<{ created: boolean }>();
|
|
281
|
+
* if (actionData?.created) { ... }
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
export function useActionData<T = unknown>(): T | undefined {
|
|
285
|
+
const state = useRouterState();
|
|
286
|
+
return state.actionData as T | undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Action 제출 함수
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```tsx
|
|
294
|
+
* const submit = useSubmit();
|
|
295
|
+
* const handleCreate = () => submit("/api/todos", { title: "New" }, "create");
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export function useSubmit(): (
|
|
299
|
+
url: string,
|
|
300
|
+
data: FormData | Record<string, unknown>,
|
|
301
|
+
actionName?: string,
|
|
302
|
+
method?: string
|
|
303
|
+
) => Promise<ActionResult> {
|
|
304
|
+
return useCallback(
|
|
305
|
+
(url: string, data: FormData | Record<string, unknown>, actionName = "default", method = "POST") => {
|
|
306
|
+
return submitAction(url, data, actionName, method);
|
|
307
|
+
},
|
|
308
|
+
[]
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Mandu 통합 상태 훅
|
|
314
|
+
* 라우트, 데이터, 네비게이션, 액션 상태를 하나의 객체로 제공
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```tsx
|
|
318
|
+
* const {
|
|
319
|
+
* url, params, routeId,
|
|
320
|
+
* loaderData, actionData,
|
|
321
|
+
* navigation,
|
|
322
|
+
* navigate, submit,
|
|
323
|
+
* } = useMandu();
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
export function useMandu() {
|
|
327
|
+
const state = useRouterState();
|
|
328
|
+
const navigateFn = useNavigate();
|
|
329
|
+
const submitFn = useSubmit();
|
|
330
|
+
|
|
331
|
+
// URL은 라우터 상태가 실제로 바뀔 때만 재생성한다.
|
|
332
|
+
// query-only navigation도 반영되도록 상태 전체를 의존성으로 사용한다.
|
|
333
|
+
const url = useMemo(
|
|
334
|
+
() => typeof window !== "undefined" ? new URL(window.location.href) : null,
|
|
335
|
+
[state]
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
/** 현재 URL (브라우저) */
|
|
340
|
+
url,
|
|
341
|
+
/** URL 파라미터 */
|
|
342
|
+
params: (state.currentRoute?.params ?? {}) as Record<string, string>,
|
|
343
|
+
/** 현재 라우트 ID */
|
|
344
|
+
routeId: state.currentRoute?.id ?? "",
|
|
345
|
+
/** 라우트 패턴 */
|
|
346
|
+
pattern: state.currentRoute?.pattern ?? "",
|
|
347
|
+
/** Loader 데이터 */
|
|
348
|
+
loaderData: state.loaderData,
|
|
349
|
+
/** 마지막 Action 결과 */
|
|
350
|
+
actionData: state.actionData,
|
|
351
|
+
/** 네비게이션 상태 */
|
|
352
|
+
navigation: state.navigation,
|
|
353
|
+
/** 네비게이션 중 여부 */
|
|
354
|
+
isNavigating: state.navigation.state === "loading",
|
|
355
|
+
/** Action 제출 중 여부 */
|
|
356
|
+
isSubmitting: state.navigation.state === "submitting",
|
|
357
|
+
/** 프로그래매틱 네비게이션 */
|
|
358
|
+
navigate: navigateFn,
|
|
359
|
+
/** Action 제출 */
|
|
360
|
+
submit: submitFn,
|
|
361
|
+
};
|
|
362
|
+
}
|