-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathRouter.js
More file actions
179 lines (153 loc) · 4.47 KB
/
Router.js
File metadata and controls
179 lines (153 loc) · 4.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
/**
* 간단한 SPA 라우터
*/
import { createObserver } from "./createObserver.js";
export class Router {
#routes;
#route;
#observer = createObserver();
#baseUrl;
constructor(baseUrl = "") {
this.#routes = new Map();
this.#route = null;
this.#baseUrl = baseUrl.replace(/\/$/, "");
if (typeof window === "undefined") {
return;
}
window.addEventListener("popstate", () => {
this.#route = this.#findRoute();
this.#observer.notify();
});
}
get baseUrl() {
return this.#baseUrl;
}
get query() {
return Router.parseQuery(typeof window !== "undefined" ? window.location.search : {});
}
set query(newQuery) {
const newUrl = Router.getUrl(newQuery, this.#baseUrl);
this.push(newUrl);
}
get params() {
return this.#route?.params ?? {};
}
get route() {
return this.#route;
}
get target() {
return this.#route?.handler;
}
subscribe(fn) {
this.#observer.subscribe(fn);
}
/**
* 라우트 등록
* @param {string} path - 경로 패턴 (예: "/product/:id")
* @param {Function} handler - 라우트 핸들러
*/
addRoute(path, handler) {
// 경로 패턴을 정규식으로 변환
const paramNames = [];
const regexPath = path
.replace(/:\w+/g, (match) => {
paramNames.push(match.slice(1)); // ':id' -> 'id'
return "([^/]+)";
})
.replace(/\//g, "\\/");
const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`);
this.#routes.set(path, {
regex,
paramNames,
handler,
});
}
#findRoute(url = typeof window !== "undefined" ? window.location.pathname : "/") {
const { pathname } = new URL(url, typeof window !== "undefined" ? window.location.origin : "/");
for (const [routePath, route] of this.#routes) {
const match = pathname.match(route.regex);
if (match) {
// 매치된 파라미터들을 객체로 변환
const params = {};
route.paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
return {
...route,
params,
path: routePath,
};
}
}
return null;
}
/**
* 네비게이션 실행
* @param {string} url - 이동할 경로
*/
push(url) {
try {
// baseUrl이 없으면 자동으로 붙여줌
let fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url);
const prevFullUrl = `${typeof window !== "undefined" ? window.location.pathname : "/"}${
typeof window !== "undefined" ? window.location.search : ""
}`;
// 히스토리 업데이트
if (prevFullUrl !== fullUrl) {
window.history.pushState(null, "", fullUrl);
}
this.#route = this.#findRoute(fullUrl);
this.#observer.notify();
} catch (error) {
console.error("라우터 네비게이션 오류:", error);
}
}
/**
* 라우터 시작
*/
start() {
this.#route = this.#findRoute();
this.#observer.notify();
}
/**
* 쿼리 파라미터를 객체로 파싱
* @param {string} search - location.search 또는 쿼리 문자열
* @returns {Object} 파싱된 쿼리 객체
*/
static parseQuery = (search = typeof window !== "undefined" ? window.location.search : "") => {
const params = new URLSearchParams(search);
const query = {};
for (const [key, value] of params) {
query[key] = value;
}
return query;
};
/**
* 객체를 쿼리 문자열로 변환
* @param {Object} query - 쿼리 객체
* @returns {string} 쿼리 문자열
*/
static stringifyQuery = (query) => {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined && value !== "") {
params.set(key, String(value));
}
}
return params.toString();
};
static getUrl = (newQuery, baseUrl = "") => {
const currentQuery = Router.parseQuery();
const updatedQuery = { ...currentQuery, ...newQuery };
// 빈 값들 제거
Object.keys(updatedQuery).forEach((key) => {
if (updatedQuery[key] === null || updatedQuery[key] === undefined || updatedQuery[key] === "") {
delete updatedQuery[key];
}
});
const queryString = Router.stringifyQuery(updatedQuery);
return `${baseUrl}${typeof window !== "undefined" ? window.location.pathname.replace(baseUrl, "") : "/"}${
queryString ? `?${queryString}` : ""
}`;
};
}