dn-react-router-toolkit 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.
- package/README.md +1 -0
- package/dist/cjs/auth-kit/apple_auth.js +60 -0
- package/dist/cjs/auth-kit/auth_service.js +204 -0
- package/dist/cjs/auth-kit/client/google_login_button.js +25 -0
- package/dist/cjs/auth-kit/client/redirect_page.js +21 -0
- package/dist/cjs/auth-kit/google_auth.js +65 -0
- package/dist/cjs/auth-kit/jwt.js +53 -0
- package/dist/cjs/auth-kit/kakao_auth.js +38 -0
- package/dist/cjs/auth-kit/repository.js +2 -0
- package/dist/cjs/auth-kit/with_auth.js +46 -0
- package/dist/cjs/cn.js +6 -0
- package/dist/cjs/components/index.js +18 -0
- package/dist/cjs/components/modal/fullscreen_container.js +64 -0
- package/dist/cjs/components/modal/hooks.js +78 -0
- package/dist/cjs/components/modal/index.js +19 -0
- package/dist/cjs/components/modal/modal.js +91 -0
- package/dist/cjs/components/styles.js +10 -0
- package/dist/cjs/date.js +31 -0
- package/dist/cjs/file-kit/cdn.js +9 -0
- package/dist/cjs/file-kit/client/drop_file_input.js +195 -0
- package/dist/cjs/file-kit/client/file_uploader.js +78 -0
- package/dist/cjs/file-kit/file_service.js +29 -0
- package/dist/cjs/file-kit/object_storage.js +50 -0
- package/dist/cjs/file-kit/repository.js +2 -0
- package/dist/cjs/file-kit/responsive_image.js +78 -0
- package/dist/cjs/http-kit/index.js +17 -0
- package/dist/cjs/http-kit/response.js +34 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/route/api/auth/login/[provider]/route.js +36 -0
- package/dist/cjs/route/api/auth/login/route.js +35 -0
- package/dist/cjs/route/api/auth/logout/route.js +22 -0
- package/dist/cjs/route/api/auth/refresh/route.js +23 -0
- package/dist/cjs/route/api/auth/route.js +12 -0
- package/dist/cjs/route/api/files/[fileId]/route.js +20 -0
- package/dist/cjs/route/api/files/route.js +34 -0
- package/dist/cjs/route/auth/callback/[provider]/route.js +35 -0
- package/dist/cjs/route/index.js +80 -0
- package/dist/cjs/seo-kit/index.js +19 -0
- package/dist/cjs/seo-kit/loader.js +17 -0
- package/dist/cjs/seo-kit/seo.js +286 -0
- package/dist/cjs/seo-kit/seo_loader.js +19 -0
- package/dist/cjs/singleton.js +12 -0
- package/dist/cjs/slug.js +10 -0
- package/dist/esm/auth-kit/apple_auth.d.ts +15 -0
- package/dist/esm/auth-kit/apple_auth.js +56 -0
- package/dist/esm/auth-kit/auth_service.d.ts +67 -0
- package/dist/esm/auth-kit/auth_service.js +197 -0
- package/dist/esm/auth-kit/client/google_login_button.d.ts +6 -0
- package/dist/esm/auth-kit/client/google_login_button.js +19 -0
- package/dist/esm/auth-kit/client/redirect_page.d.ts +2 -0
- package/dist/esm/auth-kit/client/redirect_page.js +15 -0
- package/dist/esm/auth-kit/google_auth.d.ts +18 -0
- package/dist/esm/auth-kit/google_auth.js +61 -0
- package/dist/esm/auth-kit/jwt.d.ts +15 -0
- package/dist/esm/auth-kit/jwt.js +49 -0
- package/dist/esm/auth-kit/kakao_auth.d.ts +15 -0
- package/dist/esm/auth-kit/kakao_auth.js +34 -0
- package/dist/esm/auth-kit/repository.d.ts +40 -0
- package/dist/esm/auth-kit/repository.js +1 -0
- package/dist/esm/auth-kit/with_auth.d.ts +12 -0
- package/dist/esm/auth-kit/with_auth.js +43 -0
- package/dist/esm/cn.d.ts +1 -0
- package/dist/esm/cn.js +3 -0
- package/dist/esm/components/index.d.ts +2 -0
- package/dist/esm/components/index.js +2 -0
- package/dist/esm/components/modal/fullscreen_container.d.ts +5 -0
- package/dist/esm/components/modal/fullscreen_container.js +57 -0
- package/dist/esm/components/modal/hooks.d.ts +15 -0
- package/dist/esm/components/modal/hooks.js +69 -0
- package/dist/esm/components/modal/index.d.ts +3 -0
- package/dist/esm/components/modal/index.js +3 -0
- package/dist/esm/components/modal/modal.d.ts +10 -0
- package/dist/esm/components/modal/modal.js +55 -0
- package/dist/esm/components/styles.d.ts +7 -0
- package/dist/esm/components/styles.js +7 -0
- package/dist/esm/date.d.ts +1 -0
- package/dist/esm/date.js +24 -0
- package/dist/esm/file-kit/cdn.d.ts +3 -0
- package/dist/esm/file-kit/cdn.js +5 -0
- package/dist/esm/file-kit/client/drop_file_input.d.ts +31 -0
- package/dist/esm/file-kit/client/drop_file_input.js +158 -0
- package/dist/esm/file-kit/client/file_uploader.d.ts +11 -0
- package/dist/esm/file-kit/client/file_uploader.js +74 -0
- package/dist/esm/file-kit/file_service.d.ts +23 -0
- package/dist/esm/file-kit/file_service.js +25 -0
- package/dist/esm/file-kit/object_storage.d.ts +13 -0
- package/dist/esm/file-kit/object_storage.js +46 -0
- package/dist/esm/file-kit/repository.d.ts +14 -0
- package/dist/esm/file-kit/repository.js +1 -0
- package/dist/esm/file-kit/responsive_image.d.ts +17 -0
- package/dist/esm/file-kit/responsive_image.js +70 -0
- package/dist/esm/http-kit/index.d.ts +1 -0
- package/dist/esm/http-kit/index.js +1 -0
- package/dist/esm/http-kit/response.d.ts +17 -0
- package/dist/esm/http-kit/response.js +28 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/route/api/auth/login/[provider]/route.d.ts +10 -0
- package/dist/esm/route/api/auth/login/[provider]/route.js +32 -0
- package/dist/esm/route/api/auth/login/route.d.ts +6 -0
- package/dist/esm/route/api/auth/login/route.js +31 -0
- package/dist/esm/route/api/auth/logout/route.d.ts +6 -0
- package/dist/esm/route/api/auth/logout/route.js +18 -0
- package/dist/esm/route/api/auth/refresh/route.d.ts +4 -0
- package/dist/esm/route/api/auth/refresh/route.js +19 -0
- package/dist/esm/route/api/auth/route.d.ts +4 -0
- package/dist/esm/route/api/auth/route.js +8 -0
- package/dist/esm/route/api/files/[fileId]/route.d.ts +8 -0
- package/dist/esm/route/api/files/[fileId]/route.js +16 -0
- package/dist/esm/route/api/files/route.d.ts +6 -0
- package/dist/esm/route/api/files/route.js +30 -0
- package/dist/esm/route/auth/callback/[provider]/route.d.ts +11 -0
- package/dist/esm/route/auth/callback/[provider]/route.js +31 -0
- package/dist/esm/route/index.d.ts +22 -0
- package/dist/esm/route/index.js +76 -0
- package/dist/esm/seo-kit/index.d.ts +3 -0
- package/dist/esm/seo-kit/index.js +3 -0
- package/dist/esm/seo-kit/loader.d.ts +5 -0
- package/dist/esm/seo-kit/loader.js +14 -0
- package/dist/esm/seo-kit/seo.d.ts +100 -0
- package/dist/esm/seo-kit/seo.js +280 -0
- package/dist/esm/seo-kit/seo_loader.d.ts +12 -0
- package/dist/esm/seo-kit/seo_loader.js +13 -0
- package/dist/esm/singleton.d.ts +1 -0
- package/dist/esm/singleton.js +9 -0
- package/dist/esm/slug.d.ts +1 -0
- package/dist/esm/slug.js +6 -0
- package/package.json +81 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FcGoogle } from "react-icons/fc";
|
|
3
|
+
import { useSearchParams } from "react-router";
|
|
4
|
+
export function GoogleLoginButton({ className = "max-w-[300px] w-full", GOOGLE_CLIENT_ID, GOOGLE_REDIRECT_URI, }) {
|
|
5
|
+
const [searchParams] = useSearchParams();
|
|
6
|
+
const redirectUrl = searchParams.get("redirectUrl") || "";
|
|
7
|
+
const googleClientId = GOOGLE_CLIENT_ID || "";
|
|
8
|
+
const googleRedirectUrl = GOOGLE_REDIRECT_URI || "";
|
|
9
|
+
const href = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
10
|
+
href.searchParams.append("client_id", googleClientId);
|
|
11
|
+
href.searchParams.append("redirect_uri", googleRedirectUrl);
|
|
12
|
+
href.searchParams.append("response_type", "code");
|
|
13
|
+
href.searchParams.append("scope", "email profile");
|
|
14
|
+
href.searchParams.append("state", redirectUrl);
|
|
15
|
+
return (React.createElement("div", { className: className },
|
|
16
|
+
React.createElement("a", { href: href.toString(), className: "relative flex items-center border border-neutral-300 px-4 py-3 no-underline hover:no-underline bg-white hover:bg-neutral-100 rounded" },
|
|
17
|
+
React.createElement(FcGoogle, null),
|
|
18
|
+
React.createElement("p", { className: "text-[14px] text-black absolute left-0 right-0 text-center" }, "\uAD6C\uAE00\uB85C \uACC4\uC18D\uD558\uAE30"))));
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { useNavigate } from "react-router";
|
|
4
|
+
export function RedirectPage() {
|
|
5
|
+
const navigate = useNavigate();
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
8
|
+
const redirectUrl = searchParams.get("redirectUrl") ||
|
|
9
|
+
new URL("/", window.location.origin).toString();
|
|
10
|
+
if (redirectUrl) {
|
|
11
|
+
navigate(redirectUrl);
|
|
12
|
+
}
|
|
13
|
+
}, [navigate]);
|
|
14
|
+
return React.createElement(React.Fragment, null);
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AuthService } from "./auth_service";
|
|
2
|
+
export declare class GoogleAuth {
|
|
3
|
+
AUTH: AuthService;
|
|
4
|
+
GOOGLE_CLIENT_ID: string;
|
|
5
|
+
GOOGLE_CLIENT_SECRET: string;
|
|
6
|
+
GOOGLE_REDIRECT_URI: string;
|
|
7
|
+
constructor(AUTH: AuthService, GOOGLE_CLIENT_ID: string, GOOGLE_CLIENT_SECRET: string, GOOGLE_REDIRECT_URI: string);
|
|
8
|
+
signIn(code: string): Promise<{
|
|
9
|
+
user: {
|
|
10
|
+
id: string;
|
|
11
|
+
role: string;
|
|
12
|
+
name: string;
|
|
13
|
+
refreshToken: string | null;
|
|
14
|
+
};
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BAD_REQUEST, createJsonResponse, INTERNAL_SERVER_ERROR, } from "../http-kit/response";
|
|
2
|
+
export class GoogleAuth {
|
|
3
|
+
constructor(AUTH, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI) {
|
|
4
|
+
this.GOOGLE_CLIENT_ID = GOOGLE_CLIENT_ID;
|
|
5
|
+
this.GOOGLE_CLIENT_SECRET = GOOGLE_CLIENT_SECRET;
|
|
6
|
+
this.GOOGLE_REDIRECT_URI = GOOGLE_REDIRECT_URI;
|
|
7
|
+
this.AUTH = AUTH;
|
|
8
|
+
}
|
|
9
|
+
async signIn(code) {
|
|
10
|
+
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
14
|
+
},
|
|
15
|
+
body: new URLSearchParams({
|
|
16
|
+
code,
|
|
17
|
+
client_id: this.GOOGLE_CLIENT_ID,
|
|
18
|
+
client_secret: this.GOOGLE_CLIENT_SECRET,
|
|
19
|
+
redirect_uri: this.GOOGLE_REDIRECT_URI,
|
|
20
|
+
grant_type: "authorization_code",
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
if (!tokenRes.ok) {
|
|
24
|
+
if (process.env.NODE_ENV === "development") {
|
|
25
|
+
throw INTERNAL_SERVER_ERROR(`구글 토큰 발급에 실패했습니다. status: ${tokenRes.status}, message: ${await tokenRes.text()}`);
|
|
26
|
+
}
|
|
27
|
+
throw INTERNAL_SERVER_ERROR("인증에 실패했습니다.");
|
|
28
|
+
}
|
|
29
|
+
const { access_token } = await tokenRes.json();
|
|
30
|
+
const userRes = await fetch("https://www.googleapis.com/oauth2/v1/userinfo", {
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${access_token}`,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
if (!userRes.ok) {
|
|
36
|
+
throw BAD_REQUEST("유저 정보를 가져오는데 실패했습니다.");
|
|
37
|
+
}
|
|
38
|
+
const { id, email, name, picture } = await userRes.json();
|
|
39
|
+
const user = await this.AUTH.findUser("google", {
|
|
40
|
+
id,
|
|
41
|
+
email,
|
|
42
|
+
name,
|
|
43
|
+
picture,
|
|
44
|
+
});
|
|
45
|
+
if (!user) {
|
|
46
|
+
throw createJsonResponse(404)({
|
|
47
|
+
provider: "google",
|
|
48
|
+
id,
|
|
49
|
+
name,
|
|
50
|
+
picture,
|
|
51
|
+
message: "사용자를 찾을 수 없습니다.",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const { accessToken, refreshToken } = await this.AUTH.issueTokenPair(user);
|
|
55
|
+
return {
|
|
56
|
+
user,
|
|
57
|
+
accessToken,
|
|
58
|
+
refreshToken,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type JWTPayload } from "jose";
|
|
2
|
+
export declare class JWTManager {
|
|
3
|
+
siteOrigin?: string;
|
|
4
|
+
constructor(siteOrigin?: string);
|
|
5
|
+
verify(token: string, secret: string): Promise<JWTPayload | undefined>;
|
|
6
|
+
verifyAccessToken(token: string): Promise<JWTPayload | undefined>;
|
|
7
|
+
verifyRefreshToken(token: string): Promise<JWTPayload | undefined>;
|
|
8
|
+
sign(payload: JWTPayload, secret: string, { expiresIn }?: {
|
|
9
|
+
expiresIn?: string;
|
|
10
|
+
}): Promise<string>;
|
|
11
|
+
signAccessToken(payload: JWTPayload): Promise<string>;
|
|
12
|
+
signRefreshToken(payload: JWTPayload): Promise<string>;
|
|
13
|
+
decode(token: string): JWTPayload;
|
|
14
|
+
getExpirationTime(token: string): Date;
|
|
15
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SignJWT, decodeJwt, jwtVerify } from "jose";
|
|
2
|
+
export class JWTManager {
|
|
3
|
+
constructor(siteOrigin) {
|
|
4
|
+
this.siteOrigin = siteOrigin;
|
|
5
|
+
}
|
|
6
|
+
async verify(token, secret) {
|
|
7
|
+
try {
|
|
8
|
+
const result = await jwtVerify(token, new TextEncoder().encode(secret));
|
|
9
|
+
return result.payload;
|
|
10
|
+
}
|
|
11
|
+
catch (_) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async verifyAccessToken(token) {
|
|
16
|
+
return this.verify(token, process.env.ACCESS_TOKEN_SECRET);
|
|
17
|
+
}
|
|
18
|
+
async verifyRefreshToken(token) {
|
|
19
|
+
return this.verify(token, process.env.REFRESH_TOKEN_SECRET);
|
|
20
|
+
}
|
|
21
|
+
async sign(payload, secret, { expiresIn } = {}) {
|
|
22
|
+
let builder = new SignJWT(payload)
|
|
23
|
+
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
|
|
24
|
+
.setIssuedAt();
|
|
25
|
+
if (this.siteOrigin) {
|
|
26
|
+
builder = builder.setIssuer(this.siteOrigin);
|
|
27
|
+
}
|
|
28
|
+
if (expiresIn) {
|
|
29
|
+
builder = builder.setExpirationTime(expiresIn);
|
|
30
|
+
}
|
|
31
|
+
return builder.sign(new TextEncoder().encode(secret));
|
|
32
|
+
}
|
|
33
|
+
async signAccessToken(payload) {
|
|
34
|
+
return this.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
|
|
35
|
+
expiresIn: process.env.ACCESS_TOKEN_EXPIRES_IN || "10s",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async signRefreshToken(payload) {
|
|
39
|
+
return this.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
|
|
40
|
+
expiresIn: process.env.REFRESH_TOKEN_EXPIRES_IN || "1d",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
decode(token) {
|
|
44
|
+
return decodeJwt(token);
|
|
45
|
+
}
|
|
46
|
+
getExpirationTime(token) {
|
|
47
|
+
return new Date(Number(decodeJwt(token).exp) * 1000);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AuthService } from "./auth_service";
|
|
2
|
+
export declare class KakaoAuth {
|
|
3
|
+
AUTH: AuthService;
|
|
4
|
+
constructor(AUTH: AuthService);
|
|
5
|
+
signIn(kakaoAccessToken: string): Promise<{
|
|
6
|
+
user: {
|
|
7
|
+
id: string;
|
|
8
|
+
role: string;
|
|
9
|
+
name: string;
|
|
10
|
+
refreshToken: string | null;
|
|
11
|
+
};
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createJsonResponse } from "../http-kit";
|
|
2
|
+
export class KakaoAuth {
|
|
3
|
+
constructor(AUTH) {
|
|
4
|
+
this.AUTH = AUTH;
|
|
5
|
+
}
|
|
6
|
+
async signIn(kakaoAccessToken) {
|
|
7
|
+
const userRes = await fetch("https://kapi.kakao.com/v2/user/me", {
|
|
8
|
+
headers: {
|
|
9
|
+
Authorization: `Bearer ${kakaoAccessToken}`,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
if (!userRes.ok) {
|
|
13
|
+
throw new Error("유저 정보를 가져오는데 실패했습니다.");
|
|
14
|
+
}
|
|
15
|
+
const { id, kakao_account } = await userRes.json();
|
|
16
|
+
const { email, profile } = kakao_account;
|
|
17
|
+
const payload = {
|
|
18
|
+
id,
|
|
19
|
+
email,
|
|
20
|
+
name: profile.nickname,
|
|
21
|
+
picture: profile.thumbnail_image_url,
|
|
22
|
+
};
|
|
23
|
+
const user = await this.AUTH.findUser("kakao", payload);
|
|
24
|
+
if (!user) {
|
|
25
|
+
throw createJsonResponse(404)(Object.assign(Object.assign({ provider: "kakao" }, payload), { message: "사용자를 찾을 수 없습니다." }));
|
|
26
|
+
}
|
|
27
|
+
const { accessToken, refreshToken } = await this.AUTH.issueTokenPair(user);
|
|
28
|
+
return {
|
|
29
|
+
user,
|
|
30
|
+
accessToken,
|
|
31
|
+
refreshToken,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface AuthRepository {
|
|
2
|
+
findCredentialById(id: string): Promise<{
|
|
3
|
+
password: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
} | undefined>;
|
|
6
|
+
findUserById(userId: string): Promise<{
|
|
7
|
+
id: string;
|
|
8
|
+
role: string;
|
|
9
|
+
name: string;
|
|
10
|
+
refreshToken: string | null;
|
|
11
|
+
} | undefined>;
|
|
12
|
+
updateUserRefreshToken(userId: string, hashedRefreshToken: string | null): Promise<void>;
|
|
13
|
+
createUser(userData: {
|
|
14
|
+
id: string;
|
|
15
|
+
role: string;
|
|
16
|
+
profileImageId: string | null;
|
|
17
|
+
name: string;
|
|
18
|
+
email: string;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
id: string;
|
|
21
|
+
role: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}>;
|
|
24
|
+
findThirdPartyAuth(provider: string, providerId: string): Promise<{
|
|
25
|
+
userId: string;
|
|
26
|
+
} | undefined>;
|
|
27
|
+
createThirdPartyAuth(authData: {
|
|
28
|
+
id: string;
|
|
29
|
+
provider: string;
|
|
30
|
+
userId: string;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
createFile(fileData: {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
type: string;
|
|
36
|
+
key: string;
|
|
37
|
+
size: number;
|
|
38
|
+
metadata: Record<string, any>;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
|
|
2
|
+
import { AuthService } from "./auth_service";
|
|
3
|
+
import { JWTManager } from "./jwt";
|
|
4
|
+
import type { JWTPayload } from "jose";
|
|
5
|
+
type Fn<T extends LoaderFunctionArgs | ActionFunctionArgs> = (arg: T) => Promise<unknown> | unknown;
|
|
6
|
+
type InputFN = (auth?: JWTPayload) => Fn<LoaderFunctionArgs> | Fn<ActionFunctionArgs>;
|
|
7
|
+
export type WithAuthHandler<T extends LoaderFunctionArgs | ActionFunctionArgs> = (fn: InputFN) => Fn<T>;
|
|
8
|
+
export declare function createWithAuthHandler<T extends LoaderFunctionArgs | ActionFunctionArgs>({ JWT_MANAGER, AUTH, }: {
|
|
9
|
+
JWT_MANAGER: JWTManager;
|
|
10
|
+
AUTH: AuthService;
|
|
11
|
+
}): WithAuthHandler<T>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, } from "./auth_service";
|
|
2
|
+
export function createWithAuthHandler({ JWT_MANAGER, AUTH, }) {
|
|
3
|
+
return function (fn) {
|
|
4
|
+
return async function (arg) {
|
|
5
|
+
const cookie = arg.request.headers.get("cookie");
|
|
6
|
+
const accessToken = await ACCESS_TOKEN_COOKIE.parse(cookie);
|
|
7
|
+
if (accessToken) {
|
|
8
|
+
const payload = await JWT_MANAGER.verifyAccessToken(accessToken);
|
|
9
|
+
if (payload) {
|
|
10
|
+
return fn(payload)(arg);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const refreshToken = await REFRESH_TOKEN_COOKIE.parse(cookie);
|
|
14
|
+
if (refreshToken) {
|
|
15
|
+
try {
|
|
16
|
+
const refreshedAccessToken = await AUTH.refreshAccessToken(refreshToken);
|
|
17
|
+
const setCookieHeader = await AUTH.getRefreshTokenSetCookie(refreshedAccessToken);
|
|
18
|
+
return new Response("Temporary Redirect", {
|
|
19
|
+
status: 307,
|
|
20
|
+
headers: {
|
|
21
|
+
"Set-Cookie": setCookieHeader,
|
|
22
|
+
Location: arg.request.url,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (e instanceof Error) {
|
|
28
|
+
console.log(e.message);
|
|
29
|
+
}
|
|
30
|
+
const setCookieHeader = await AUTH.getRefreshTokenSetCookie(null);
|
|
31
|
+
return new Response("Temporary Redirect", {
|
|
32
|
+
status: 307,
|
|
33
|
+
headers: {
|
|
34
|
+
"Set-Cookie": setCookieHeader,
|
|
35
|
+
Location: arg.request.url,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return fn(undefined)(arg);
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/esm/cn.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cn(...classes: (string | false | undefined)[]): string;
|
package/dist/esm/cn.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type DetailedHTMLProps, type HTMLAttributes } from "react";
|
|
3
|
+
export type Props = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
|
4
|
+
export default function FullscreenContainer({ className, children, ...props }: Props): React.JSX.Element;
|
|
5
|
+
export declare function useFullscreen(ref: React.RefObject<HTMLDivElement | null>): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { cn } from "../../cn";
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { useEffect, useRef, } from "react";
|
|
15
|
+
export default function FullscreenContainer(_a) {
|
|
16
|
+
var { className, children } = _a, props = __rest(_a, ["className", "children"]);
|
|
17
|
+
const containerRef = useRef(null);
|
|
18
|
+
useFullscreen(containerRef);
|
|
19
|
+
return (React.createElement("div", Object.assign({ ref: containerRef }, props, { className: cn("fixed left-0 right-0 top-0 bottom-0", className) }), children));
|
|
20
|
+
}
|
|
21
|
+
export function useFullscreen(ref) {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const container = ref.current;
|
|
24
|
+
if (!container) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const scroll = () => {
|
|
28
|
+
if (scrollY > 0) {
|
|
29
|
+
window.scrollTo({
|
|
30
|
+
top: 0,
|
|
31
|
+
behavior: "instant",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const resize = () => {
|
|
36
|
+
if (!visualViewport) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
container.classList.remove("bottom-0");
|
|
40
|
+
container.style.height = `${visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height}px`;
|
|
41
|
+
if (visualViewport.height < window.innerHeight) {
|
|
42
|
+
window.addEventListener("scroll", scroll);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
window.removeEventListener("scroll", scroll);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
resize();
|
|
49
|
+
visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener("resize", resize);
|
|
50
|
+
scroll();
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener("scroll", scroll);
|
|
53
|
+
visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener("resize", resize);
|
|
54
|
+
};
|
|
55
|
+
}, [ref]);
|
|
56
|
+
return "fixed inset-0";
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ModalProps } from "./modal";
|
|
2
|
+
import React from "react";
|
|
3
|
+
export declare function useModalRoot(): HTMLElement | null;
|
|
4
|
+
export declare function useModalPortal(): ((children: React.ReactNode) => null) | ((children: React.ReactNode) => React.ReactPortal);
|
|
5
|
+
export type ModalContextProps = {
|
|
6
|
+
open: () => void;
|
|
7
|
+
close: () => void;
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare const ModalContext: React.Context<ModalContextProps>;
|
|
11
|
+
export declare function useModal(): {
|
|
12
|
+
open: () => void;
|
|
13
|
+
close: () => void;
|
|
14
|
+
Modal: (props: Omit<ModalProps, "open">) => React.JSX.Element;
|
|
15
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createContext, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import Modal from "./modal";
|
|
4
|
+
import React from "react";
|
|
5
|
+
export function useModalRoot() {
|
|
6
|
+
const [container, setContainer] = useState(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const container = document.getElementById("modal-root");
|
|
9
|
+
if (container) {
|
|
10
|
+
setContainer(container);
|
|
11
|
+
}
|
|
12
|
+
return () => {
|
|
13
|
+
setContainer(null);
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
return container;
|
|
17
|
+
}
|
|
18
|
+
export function useModalPortal() {
|
|
19
|
+
const container = useModalRoot();
|
|
20
|
+
if (!container) {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
22
|
+
return (children) => null;
|
|
23
|
+
}
|
|
24
|
+
return (children) => {
|
|
25
|
+
return createPortal(children, container);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
export const ModalContext = createContext({});
|
|
30
|
+
export function useModal() {
|
|
31
|
+
const ch2 = useRef(null);
|
|
32
|
+
const open = () => {
|
|
33
|
+
var _a;
|
|
34
|
+
(_a = ch2.current) === null || _a === void 0 ? void 0 : _a.call(ch2, true);
|
|
35
|
+
};
|
|
36
|
+
const close = () => {
|
|
37
|
+
var _a;
|
|
38
|
+
(_a = ch2.current) === null || _a === void 0 ? void 0 : _a.call(ch2, false);
|
|
39
|
+
};
|
|
40
|
+
const Component = useCallback(function Component(props) {
|
|
41
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
42
|
+
const createPortal = useModalPortal();
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
ch2.current = (v) => {
|
|
45
|
+
setIsOpen(v);
|
|
46
|
+
};
|
|
47
|
+
return () => {
|
|
48
|
+
ch2.current = null;
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (isOpen) {
|
|
53
|
+
// document.body.style.overflow = "hidden";
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// document.body.style.overflow = "";
|
|
57
|
+
}
|
|
58
|
+
return () => {
|
|
59
|
+
// document.body.style.overflow = "";
|
|
60
|
+
};
|
|
61
|
+
}, [isOpen]);
|
|
62
|
+
return (React.createElement(ModalContext.Provider, { value: { open, close, isOpen } }, createPortal(React.createElement(Modal, Object.assign({}, props, { open: isOpen, onBackgroundClick: close })))));
|
|
63
|
+
}, []);
|
|
64
|
+
return {
|
|
65
|
+
open,
|
|
66
|
+
close,
|
|
67
|
+
Modal: Component,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
export type ModalProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
3
|
+
open: boolean;
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
onBackgroundClick?: () => void;
|
|
6
|
+
embeded?: boolean;
|
|
7
|
+
always?: boolean;
|
|
8
|
+
background?: string;
|
|
9
|
+
};
|
|
10
|
+
export default function Modal({ open, className, background, onBackgroundClick, children, ...props }: ModalProps): React.JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import React, { useEffect, useRef, } from "react";
|
|
13
|
+
import { useFullscreen } from "./fullscreen_container";
|
|
14
|
+
import { cn } from "../../cn";
|
|
15
|
+
export default function Modal(_a) {
|
|
16
|
+
var { open, className, background = "bg-gray-500/25", onBackgroundClick, children } = _a, props = __rest(_a, ["open", "className", "background", "onBackgroundClick", "children"]);
|
|
17
|
+
const containerRef = useRef(null);
|
|
18
|
+
const fullscreen = useFullscreen(containerRef);
|
|
19
|
+
const [mounted, setMounted] = React.useState(false);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (open) {
|
|
22
|
+
// document.body.style.overflow = "hidden";
|
|
23
|
+
setMounted(true);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// document.body.style.overflow = "";
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
setMounted(false);
|
|
29
|
+
}, 200);
|
|
30
|
+
}
|
|
31
|
+
return () => {
|
|
32
|
+
// document.body.style.overflow = "";
|
|
33
|
+
};
|
|
34
|
+
}, [open]);
|
|
35
|
+
const onBackgroundMouseDown = (event) => {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
onBackgroundClick === null || onBackgroundClick === void 0 ? void 0 : onBackgroundClick();
|
|
38
|
+
};
|
|
39
|
+
const onModalMouseDown = (event) => {
|
|
40
|
+
event.stopPropagation();
|
|
41
|
+
};
|
|
42
|
+
const onBackgroundTouchStart = (event) => {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
onBackgroundClick === null || onBackgroundClick === void 0 ? void 0 : onBackgroundClick();
|
|
45
|
+
};
|
|
46
|
+
const onModalTouchStart = (event) => {
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
};
|
|
49
|
+
return (React.createElement("div", { ref: containerRef, className: cn(fullscreen, "z-[999] flex flex-col justify-center transition-bg duration-200 md:px-6 overflow-hidden print:px-0 print:block print:overflow-auto", open && mounted
|
|
50
|
+
? cn(background, "ease-out")
|
|
51
|
+
: "bg-transparent ease-in pointer-events-none"), onMouseDown: onBackgroundMouseDown, onTouchStart: onBackgroundTouchStart },
|
|
52
|
+
React.createElement("div", { className: cn("relative flex-1 flex flex-col justify-end md:justify-center items-center transition-[opacity transform] duration-200 print:block", open && mounted
|
|
53
|
+
? "translate-y-[0px] opacity-100 ease-out"
|
|
54
|
+
: "translate-y-[100%] md:translate-y-[50px] opacity-0 ease-in") }, (open || mounted) && (React.createElement("div", Object.assign({}, props, { className: cn(className, "w-full max-h-[92vh] print:w-[unset] print:max-h-[unset] rounded-tl-lg rounded-tr-lg md:rounded-lg overflow-auto print:rounded-none"), onMouseDown: onModalMouseDown, onTouchStart: onModalTouchStart }), children)))));
|
|
55
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const inputBackgroundStyle = "text-[16px] border border-neutral-200 outline-none appearance-none disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-neutral-400";
|
|
2
|
+
export declare const formInputStyle = "w-full px-3 text-[16px] border border-neutral-200 outline-none appearance-none disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-neutral-400 h-[40px]";
|
|
3
|
+
export declare const baseButtonStyle = "transition-colors text-sm font-semibold cursor-pointer flex items-center justify-center rounded-lg whitespace-nowrap text-ellipsis overflow-hidden";
|
|
4
|
+
export declare const primaryButtonStyle = "transition-colors text-sm font-semibold cursor-pointer flex items-center justify-center rounded-lg whitespace-nowrap text-ellipsis overflow-hidden px-4 h-[40px] bg-gradient-to-br from-primary-400 to-primary-500 text-white transition-colors";
|
|
5
|
+
export declare const disabledButtonStyle = "transition-colors text-sm font-semibold cursor-pointer flex items-center justify-center rounded-lg whitespace-nowrap text-ellipsis overflow-hidden h-[40px] px-4 bg-neutral-200 text-neutral-400";
|
|
6
|
+
export declare const outlineButtonStyle = "transition-colors text-sm font-semibold cursor-pointer flex items-center justify-center rounded-lg whitespace-nowrap text-ellipsis overflow-hidden h-[40px] px-4 bg-white border border-neutral-300 hover:bg-neutral-50";
|
|
7
|
+
export declare const dangerousButtonStyle = "transition-colors text-sm font-semibold cursor-pointer flex items-center justify-center rounded-lg whitespace-nowrap text-ellipsis overflow-hidden h-[40px] px-4 bg-white border hover:bg-red-50 text-red-600";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const inputBackgroundStyle = "text-[16px] border border-neutral-200 outline-none appearance-none disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-neutral-400";
|
|
2
|
+
export const formInputStyle = `w-full px-3 ${inputBackgroundStyle} h-[40px]`;
|
|
3
|
+
export const baseButtonStyle = "transition-colors text-sm font-semibold cursor-pointer flex items-center justify-center rounded-lg whitespace-nowrap text-ellipsis overflow-hidden";
|
|
4
|
+
export const primaryButtonStyle = `${baseButtonStyle} px-4 h-[40px] bg-gradient-to-br from-primary-400 to-primary-500 text-white transition-colors`;
|
|
5
|
+
export const disabledButtonStyle = `${baseButtonStyle} h-[40px] px-4 bg-neutral-200 text-neutral-400`;
|
|
6
|
+
export const outlineButtonStyle = `${baseButtonStyle} h-[40px] px-4 bg-white border border-neutral-300 hover:bg-neutral-50`;
|
|
7
|
+
export const dangerousButtonStyle = `${baseButtonStyle} h-[40px] px-4 bg-white border hover:bg-red-50 text-red-600`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const formatHumanDateTime: (value: string | Date) => string;
|
package/dist/esm/date.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import moment from "moment-timezone";
|
|
2
|
+
export const formatHumanDateTime = (value) => {
|
|
3
|
+
const date = moment(value).tz("Asia/Seoul");
|
|
4
|
+
const now = moment().tz("Asia/Seoul");
|
|
5
|
+
if (date.isAfter(now.clone().subtract(1, "minute"))) {
|
|
6
|
+
return "방금 전";
|
|
7
|
+
}
|
|
8
|
+
if (date.isAfter(now.clone().subtract(1, "hour"))) {
|
|
9
|
+
const diff = now.diff(date, "minute");
|
|
10
|
+
return `${diff}분 전`;
|
|
11
|
+
}
|
|
12
|
+
if (date.isAfter(now.clone().subtract(6, "hours"))) {
|
|
13
|
+
const diff = now.diff(date, "hour");
|
|
14
|
+
return `${diff}시간 전`;
|
|
15
|
+
}
|
|
16
|
+
if (date.isAfter(now.clone().subtract(1, "week"))) {
|
|
17
|
+
const diff = now.diff(date, "day");
|
|
18
|
+
return `${diff}일 전`;
|
|
19
|
+
}
|
|
20
|
+
if (date.isSame(now, "year")) {
|
|
21
|
+
return date.format("M월 D일");
|
|
22
|
+
}
|
|
23
|
+
return date.format("YYYY년 M월 D일");
|
|
24
|
+
};
|