@mandujs/core 0.13.0 → 0.13.2
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.ko.md +4 -4
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +9 -0
- package/src/config/validate.ts +12 -0
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- 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/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/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- 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 +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- 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/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- 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 +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +46 -46
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +24 -1
- package/src/filling/deps.ts +238 -238
- package/src/filling/index.ts +4 -0
- package/src/filling/sse-catchup.test.ts +56 -0
- package/src/filling/sse-catchup.ts +67 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/index.ts +3 -3
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- 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/presets/index.ts +291 -291
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +6 -1
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-scanner.ts +497 -497
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/escape.ts +44 -0
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +257 -0
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +16 -21
- package/src/runtime/streaming-ssr.ts +24 -33
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- 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 +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
package/src/client/hooks.ts
CHANGED
|
@@ -1,267 +1,267 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Router Hooks 🪝
|
|
3
|
-
* React hooks for client-side routing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect, useCallback, useSyncExternalStore } from "react";
|
|
7
|
-
import {
|
|
8
|
-
subscribe,
|
|
9
|
-
getRouterState,
|
|
10
|
-
getCurrentRoute,
|
|
11
|
-
getLoaderData,
|
|
12
|
-
getNavigationState,
|
|
13
|
-
navigate,
|
|
14
|
-
type RouteInfo,
|
|
15
|
-
type NavigationState,
|
|
16
|
-
type NavigateOptions,
|
|
17
|
-
} from "./router";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 라우터 상태 전체 접근
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```tsx
|
|
24
|
-
* const { currentRoute, loaderData, navigation } = useRouterState();
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export function useRouterState() {
|
|
28
|
-
return useSyncExternalStore(
|
|
29
|
-
subscribe,
|
|
30
|
-
getRouterState,
|
|
31
|
-
getRouterState // SSR에서도 동일
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 현재 라우트 정보
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```tsx
|
|
40
|
-
* const route = useRoute();
|
|
41
|
-
* console.log(route?.id, route?.params);
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export function useRoute(): RouteInfo | null {
|
|
45
|
-
const state = useRouterState();
|
|
46
|
-
return state.currentRoute;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* URL 파라미터 접근
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```tsx
|
|
54
|
-
* // URL: /users/123
|
|
55
|
-
* const { id } = useParams<{ id: string }>();
|
|
56
|
-
* console.log(id); // "123"
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
|
|
60
|
-
const route = useRoute();
|
|
61
|
-
return (route?.params ?? {}) as T;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 현재 경로명
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```tsx
|
|
69
|
-
* const pathname = usePathname();
|
|
70
|
-
* console.log(pathname); // "/users/123"
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
73
|
-
export function usePathname(): string {
|
|
74
|
-
const [pathname, setPathname] = useState(() =>
|
|
75
|
-
typeof window !== "undefined" ? window.location.pathname : "/"
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
const handleChange = () => {
|
|
80
|
-
setPathname(window.location.pathname);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
window.addEventListener("popstate", handleChange);
|
|
84
|
-
|
|
85
|
-
// 라우터 상태 변경 구독
|
|
86
|
-
const unsubscribe = subscribe(() => {
|
|
87
|
-
setPathname(window.location.pathname);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
return () => {
|
|
91
|
-
window.removeEventListener("popstate", handleChange);
|
|
92
|
-
unsubscribe();
|
|
93
|
-
};
|
|
94
|
-
}, []);
|
|
95
|
-
|
|
96
|
-
return pathname;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 현재 검색 파라미터 (쿼리 스트링)
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* ```tsx
|
|
104
|
-
* // URL: /search?q=hello&page=2
|
|
105
|
-
* const searchParams = useSearchParams();
|
|
106
|
-
* console.log(searchParams.get("q")); // "hello"
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
|
-
export function useSearchParams(): URLSearchParams {
|
|
110
|
-
const [searchParams, setSearchParams] = useState(() =>
|
|
111
|
-
typeof window !== "undefined"
|
|
112
|
-
? new URLSearchParams(window.location.search)
|
|
113
|
-
: new URLSearchParams()
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
const handleChange = () => {
|
|
118
|
-
setSearchParams(new URLSearchParams(window.location.search));
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
window.addEventListener("popstate", handleChange);
|
|
122
|
-
|
|
123
|
-
const unsubscribe = subscribe(() => {
|
|
124
|
-
setSearchParams(new URLSearchParams(window.location.search));
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
return () => {
|
|
128
|
-
window.removeEventListener("popstate", handleChange);
|
|
129
|
-
unsubscribe();
|
|
130
|
-
};
|
|
131
|
-
}, []);
|
|
132
|
-
|
|
133
|
-
return searchParams;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Loader 데이터 접근
|
|
138
|
-
*
|
|
139
|
-
* @example
|
|
140
|
-
* ```tsx
|
|
141
|
-
* interface UserData { name: string; email: string; }
|
|
142
|
-
* const data = useLoaderData<UserData>();
|
|
143
|
-
* ```
|
|
144
|
-
*/
|
|
145
|
-
export function useLoaderData<T = unknown>(): T | undefined {
|
|
146
|
-
const state = useRouterState();
|
|
147
|
-
return state.loaderData as T | undefined;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* 네비게이션 상태 (로딩 여부)
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* ```tsx
|
|
155
|
-
* const { state, location } = useNavigation();
|
|
156
|
-
*
|
|
157
|
-
* if (state === "loading") {
|
|
158
|
-
* return <Spinner />;
|
|
159
|
-
* }
|
|
160
|
-
* ```
|
|
161
|
-
*/
|
|
162
|
-
export function useNavigation(): NavigationState {
|
|
163
|
-
const state = useRouterState();
|
|
164
|
-
return state.navigation;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* 프로그래매틱 네비게이션
|
|
169
|
-
*
|
|
170
|
-
* @example
|
|
171
|
-
* ```tsx
|
|
172
|
-
* const navigate = useNavigate();
|
|
173
|
-
*
|
|
174
|
-
* const handleClick = () => {
|
|
175
|
-
* navigate("/dashboard");
|
|
176
|
-
* };
|
|
177
|
-
*
|
|
178
|
-
* const handleSubmit = () => {
|
|
179
|
-
* navigate("/success", { replace: true });
|
|
180
|
-
* };
|
|
181
|
-
* ```
|
|
182
|
-
*/
|
|
183
|
-
export function useNavigate(): (to: string, options?: NavigateOptions) => Promise<void> {
|
|
184
|
-
return useCallback((to: string, options?: NavigateOptions) => {
|
|
185
|
-
return navigate(to, options);
|
|
186
|
-
}, []);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* 라우터 통합 훅 (편의용)
|
|
191
|
-
*
|
|
192
|
-
* @example
|
|
193
|
-
* ```tsx
|
|
194
|
-
* const {
|
|
195
|
-
* pathname,
|
|
196
|
-
* params,
|
|
197
|
-
* searchParams,
|
|
198
|
-
* navigate,
|
|
199
|
-
* isNavigating
|
|
200
|
-
* } = useRouter();
|
|
201
|
-
* ```
|
|
202
|
-
*/
|
|
203
|
-
export function useRouter() {
|
|
204
|
-
const pathname = usePathname();
|
|
205
|
-
const params = useParams();
|
|
206
|
-
const searchParams = useSearchParams();
|
|
207
|
-
const navigation = useNavigation();
|
|
208
|
-
const navigateFn = useNavigate();
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
/** 현재 경로명 */
|
|
212
|
-
pathname,
|
|
213
|
-
/** URL 파라미터 */
|
|
214
|
-
params,
|
|
215
|
-
/** 검색 파라미터 (쿼리 스트링) */
|
|
216
|
-
searchParams,
|
|
217
|
-
/** 네비게이션 함수 */
|
|
218
|
-
navigate: navigateFn,
|
|
219
|
-
/** 네비게이션 중 여부 */
|
|
220
|
-
isNavigating: navigation.state === "loading",
|
|
221
|
-
/** 네비게이션 상태 상세 */
|
|
222
|
-
navigation,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* 특정 경로와 현재 경로 일치 여부
|
|
228
|
-
*
|
|
229
|
-
* @example
|
|
230
|
-
* ```tsx
|
|
231
|
-
* const isActive = useMatch("/about");
|
|
232
|
-
* const isUsersPage = useMatch("/users/:id");
|
|
233
|
-
* ```
|
|
234
|
-
*/
|
|
235
|
-
export function useMatch(pattern: string): boolean {
|
|
236
|
-
const pathname = usePathname();
|
|
237
|
-
|
|
238
|
-
// 간단한 패턴 매칭 (파라미터 고려)
|
|
239
|
-
const regexStr = pattern
|
|
240
|
-
.replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, "[^/]+")
|
|
241
|
-
.replace(/\//g, "\\/");
|
|
242
|
-
|
|
243
|
-
const regex = new RegExp(`^${regexStr}$`);
|
|
244
|
-
return regex.test(pathname);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* 뒤로 가기
|
|
249
|
-
*/
|
|
250
|
-
export function useGoBack(): () => void {
|
|
251
|
-
return useCallback(() => {
|
|
252
|
-
if (typeof window !== "undefined") {
|
|
253
|
-
window.history.back();
|
|
254
|
-
}
|
|
255
|
-
}, []);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* 앞으로 가기
|
|
260
|
-
*/
|
|
261
|
-
export function useGoForward(): () => void {
|
|
262
|
-
return useCallback(() => {
|
|
263
|
-
if (typeof window !== "undefined") {
|
|
264
|
-
window.history.forward();
|
|
265
|
-
}
|
|
266
|
-
}, []);
|
|
267
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Router Hooks 🪝
|
|
3
|
+
* React hooks for client-side routing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useCallback, useSyncExternalStore } from "react";
|
|
7
|
+
import {
|
|
8
|
+
subscribe,
|
|
9
|
+
getRouterState,
|
|
10
|
+
getCurrentRoute,
|
|
11
|
+
getLoaderData,
|
|
12
|
+
getNavigationState,
|
|
13
|
+
navigate,
|
|
14
|
+
type RouteInfo,
|
|
15
|
+
type NavigationState,
|
|
16
|
+
type NavigateOptions,
|
|
17
|
+
} from "./router";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 라우터 상태 전체 접근
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { currentRoute, loaderData, navigation } = useRouterState();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function useRouterState() {
|
|
28
|
+
return useSyncExternalStore(
|
|
29
|
+
subscribe,
|
|
30
|
+
getRouterState,
|
|
31
|
+
getRouterState // SSR에서도 동일
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 현재 라우트 정보
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* const route = useRoute();
|
|
41
|
+
* console.log(route?.id, route?.params);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function useRoute(): RouteInfo | null {
|
|
45
|
+
const state = useRouterState();
|
|
46
|
+
return state.currentRoute;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* URL 파라미터 접근
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* // URL: /users/123
|
|
55
|
+
* const { id } = useParams<{ id: string }>();
|
|
56
|
+
* console.log(id); // "123"
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
|
|
60
|
+
const route = useRoute();
|
|
61
|
+
return (route?.params ?? {}) as T;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 현재 경로명
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const pathname = usePathname();
|
|
70
|
+
* console.log(pathname); // "/users/123"
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function usePathname(): string {
|
|
74
|
+
const [pathname, setPathname] = useState(() =>
|
|
75
|
+
typeof window !== "undefined" ? window.location.pathname : "/"
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const handleChange = () => {
|
|
80
|
+
setPathname(window.location.pathname);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
window.addEventListener("popstate", handleChange);
|
|
84
|
+
|
|
85
|
+
// 라우터 상태 변경 구독
|
|
86
|
+
const unsubscribe = subscribe(() => {
|
|
87
|
+
setPathname(window.location.pathname);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener("popstate", handleChange);
|
|
92
|
+
unsubscribe();
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
return pathname;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 현재 검색 파라미터 (쿼리 스트링)
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* // URL: /search?q=hello&page=2
|
|
105
|
+
* const searchParams = useSearchParams();
|
|
106
|
+
* console.log(searchParams.get("q")); // "hello"
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function useSearchParams(): URLSearchParams {
|
|
110
|
+
const [searchParams, setSearchParams] = useState(() =>
|
|
111
|
+
typeof window !== "undefined"
|
|
112
|
+
? new URLSearchParams(window.location.search)
|
|
113
|
+
: new URLSearchParams()
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const handleChange = () => {
|
|
118
|
+
setSearchParams(new URLSearchParams(window.location.search));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
window.addEventListener("popstate", handleChange);
|
|
122
|
+
|
|
123
|
+
const unsubscribe = subscribe(() => {
|
|
124
|
+
setSearchParams(new URLSearchParams(window.location.search));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
window.removeEventListener("popstate", handleChange);
|
|
129
|
+
unsubscribe();
|
|
130
|
+
};
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
return searchParams;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Loader 데이터 접근
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```tsx
|
|
141
|
+
* interface UserData { name: string; email: string; }
|
|
142
|
+
* const data = useLoaderData<UserData>();
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export function useLoaderData<T = unknown>(): T | undefined {
|
|
146
|
+
const state = useRouterState();
|
|
147
|
+
return state.loaderData as T | undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 네비게이션 상태 (로딩 여부)
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* const { state, location } = useNavigation();
|
|
156
|
+
*
|
|
157
|
+
* if (state === "loading") {
|
|
158
|
+
* return <Spinner />;
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export function useNavigation(): NavigationState {
|
|
163
|
+
const state = useRouterState();
|
|
164
|
+
return state.navigation;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 프로그래매틱 네비게이션
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```tsx
|
|
172
|
+
* const navigate = useNavigate();
|
|
173
|
+
*
|
|
174
|
+
* const handleClick = () => {
|
|
175
|
+
* navigate("/dashboard");
|
|
176
|
+
* };
|
|
177
|
+
*
|
|
178
|
+
* const handleSubmit = () => {
|
|
179
|
+
* navigate("/success", { replace: true });
|
|
180
|
+
* };
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function useNavigate(): (to: string, options?: NavigateOptions) => Promise<void> {
|
|
184
|
+
return useCallback((to: string, options?: NavigateOptions) => {
|
|
185
|
+
return navigate(to, options);
|
|
186
|
+
}, []);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 라우터 통합 훅 (편의용)
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```tsx
|
|
194
|
+
* const {
|
|
195
|
+
* pathname,
|
|
196
|
+
* params,
|
|
197
|
+
* searchParams,
|
|
198
|
+
* navigate,
|
|
199
|
+
* isNavigating
|
|
200
|
+
* } = useRouter();
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export function useRouter() {
|
|
204
|
+
const pathname = usePathname();
|
|
205
|
+
const params = useParams();
|
|
206
|
+
const searchParams = useSearchParams();
|
|
207
|
+
const navigation = useNavigation();
|
|
208
|
+
const navigateFn = useNavigate();
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
/** 현재 경로명 */
|
|
212
|
+
pathname,
|
|
213
|
+
/** URL 파라미터 */
|
|
214
|
+
params,
|
|
215
|
+
/** 검색 파라미터 (쿼리 스트링) */
|
|
216
|
+
searchParams,
|
|
217
|
+
/** 네비게이션 함수 */
|
|
218
|
+
navigate: navigateFn,
|
|
219
|
+
/** 네비게이션 중 여부 */
|
|
220
|
+
isNavigating: navigation.state === "loading",
|
|
221
|
+
/** 네비게이션 상태 상세 */
|
|
222
|
+
navigation,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 특정 경로와 현재 경로 일치 여부
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```tsx
|
|
231
|
+
* const isActive = useMatch("/about");
|
|
232
|
+
* const isUsersPage = useMatch("/users/:id");
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export function useMatch(pattern: string): boolean {
|
|
236
|
+
const pathname = usePathname();
|
|
237
|
+
|
|
238
|
+
// 간단한 패턴 매칭 (파라미터 고려)
|
|
239
|
+
const regexStr = pattern
|
|
240
|
+
.replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, "[^/]+")
|
|
241
|
+
.replace(/\//g, "\\/");
|
|
242
|
+
|
|
243
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
244
|
+
return regex.test(pathname);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 뒤로 가기
|
|
249
|
+
*/
|
|
250
|
+
export function useGoBack(): () => void {
|
|
251
|
+
return useCallback(() => {
|
|
252
|
+
if (typeof window !== "undefined") {
|
|
253
|
+
window.history.back();
|
|
254
|
+
}
|
|
255
|
+
}, []);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 앞으로 가기
|
|
260
|
+
*/
|
|
261
|
+
export function useGoForward(): () => void {
|
|
262
|
+
return useCallback(() => {
|
|
263
|
+
if (typeof window !== "undefined") {
|
|
264
|
+
window.history.forward();
|
|
265
|
+
}
|
|
266
|
+
}, []);
|
|
267
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Client Module 🏝️
|
|
3
|
-
* 클라이언트 사이드 hydration 및 라우팅을 위한 API
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Client Module 🏝️
|
|
3
|
+
* 클라이언트 사이드 hydration 및 라우팅을 위한 API
|
|
4
4
|
*
|
|
5
5
|
* @example
|
|
6
6
|
* ```typescript
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* return <Link href="/about">About</Link>;
|
|
24
24
|
* }
|
|
25
25
|
* ```
|
|
26
|
-
*/
|
|
27
|
-
import "./globals";
|
|
26
|
+
*/
|
|
27
|
+
import "./globals";
|
|
28
28
|
|
|
29
29
|
// Island API
|
|
30
30
|
export {
|
package/src/client/island.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Hydration을 위한 클라이언트 사이드 컴포넌트 정의
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ReactNode } from "react";
|
|
7
|
-
import { getServerData as getGlobalServerData } from "./window-state";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import { getServerData as getGlobalServerData } from "./window-state";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Island 정의 타입
|
|
@@ -118,12 +118,12 @@ export function island<TServerData, TSetupResult = TServerData>(
|
|
|
118
118
|
* SSR 데이터에 안전하게 접근하는 훅
|
|
119
119
|
* 서버 데이터가 없는 경우 fallback 반환
|
|
120
120
|
*/
|
|
121
|
-
export function useServerData<T>(key: string, fallback: T): T {
|
|
122
|
-
if (typeof window === "undefined") return fallback;
|
|
123
|
-
|
|
124
|
-
const data = getGlobalServerData<T>(key);
|
|
125
|
-
return data === undefined ? fallback : data;
|
|
126
|
-
}
|
|
121
|
+
export function useServerData<T>(key: string, fallback: T): T {
|
|
122
|
+
if (typeof window === "undefined") return fallback;
|
|
123
|
+
|
|
124
|
+
const data = getGlobalServerData<T>(key);
|
|
125
|
+
return data === undefined ? fallback : data;
|
|
126
|
+
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* Hydration 상태를 추적하는 훅
|