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,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type FileUploaderOptions } from "./file_uploader";
|
|
3
|
+
export type FileInputProps = Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "defaultValue" | "onChange"> & {
|
|
4
|
+
container?: string;
|
|
5
|
+
accept?: string;
|
|
6
|
+
multiple?: boolean;
|
|
7
|
+
name?: string;
|
|
8
|
+
hideMessage?: boolean;
|
|
9
|
+
draggingClassName?: (value: boolean) => string;
|
|
10
|
+
};
|
|
11
|
+
export type FileItem<T> = {
|
|
12
|
+
key: string;
|
|
13
|
+
item?: T;
|
|
14
|
+
};
|
|
15
|
+
type Props<T> = {
|
|
16
|
+
defaultValue?: T | T[];
|
|
17
|
+
options?: FileUploaderOptions;
|
|
18
|
+
uploadFile?: (file: File, options?: FileUploaderOptions) => Promise<T>;
|
|
19
|
+
onFileInput?: (files: File[]) => void;
|
|
20
|
+
onFileUploaded?: (file: T) => Promise<void>;
|
|
21
|
+
onChange?: (files: FileItem<T>[]) => void;
|
|
22
|
+
limit?: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function useDropFileInput<T>({ defaultValue, options, uploadFile, onFileInput, onFileUploaded, limit, }?: Props<T>): {
|
|
25
|
+
fileIds: string[];
|
|
26
|
+
files: FileItem<T>[];
|
|
27
|
+
setFiles: React.Dispatch<React.SetStateAction<FileItem<T>[]>>;
|
|
28
|
+
Component: ({ className, container, draggingClassName, name, hideMessage, children, ...props }: FileInputProps) => React.JSX.Element;
|
|
29
|
+
};
|
|
30
|
+
export declare function DropFileMessageBox(): React.JSX.Element;
|
|
31
|
+
export default useDropFileInput;
|
|
@@ -0,0 +1,158 @@
|
|
|
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, { useState, useCallback, useRef, useEffect, useMemo, } from "react";
|
|
13
|
+
import { v4 } from "uuid";
|
|
14
|
+
import { cn } from "../../cn";
|
|
15
|
+
export function useDropFileInput({ defaultValue, options, uploadFile, onFileInput, onFileUploaded, limit, } = {}) {
|
|
16
|
+
const [files, setFiles] = useState(defaultValue
|
|
17
|
+
? (Array.isArray(defaultValue) ? defaultValue : [defaultValue])
|
|
18
|
+
.map((v) => {
|
|
19
|
+
return {
|
|
20
|
+
key: v4(),
|
|
21
|
+
item: v,
|
|
22
|
+
};
|
|
23
|
+
})
|
|
24
|
+
.slice(0, limit ? limit : Infinity)
|
|
25
|
+
: []);
|
|
26
|
+
const fileRef = useRef([]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
fileRef.current = files;
|
|
29
|
+
}, [files]);
|
|
30
|
+
const Component = useCallback(function Component(_a) {
|
|
31
|
+
var { className, container = "border border-dashed border-neutral-300 rounded flex items-center justify-center", draggingClassName, name, hideMessage = false, children } = _a, props = __rest(_a, ["className", "container", "draggingClassName", "name", "hideMessage", "children"]);
|
|
32
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
33
|
+
const handleDragEnter = useCallback((e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
e.stopPropagation();
|
|
36
|
+
setIsDragging(true);
|
|
37
|
+
}, []);
|
|
38
|
+
const handleDragLeave = useCallback((e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
e.stopPropagation();
|
|
41
|
+
setIsDragging(false);
|
|
42
|
+
}, []);
|
|
43
|
+
const handleDragOver = useCallback((e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
}, []);
|
|
47
|
+
const handleFiles = useCallback(async (files) => {
|
|
48
|
+
if (limit && fileRef.current.length >= limit) {
|
|
49
|
+
alert(`파일은 최대 ${limit}개 업로드할 수 있습니다.`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const filteredFiles = files.filter((file) => {
|
|
53
|
+
// if (!props.accept) {
|
|
54
|
+
// return true;
|
|
55
|
+
// }
|
|
56
|
+
// const accepts = props.accept.split(",");
|
|
57
|
+
// for (const accept of accepts) {
|
|
58
|
+
// if (file.type.startsWith(accept)) {
|
|
59
|
+
// return true;
|
|
60
|
+
// }
|
|
61
|
+
// if (file.name.endsWith(accept)) {
|
|
62
|
+
// return true;
|
|
63
|
+
// }
|
|
64
|
+
// }
|
|
65
|
+
// return false;
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
if (files.length !== filteredFiles.length) {
|
|
69
|
+
alert(`${props.accept} 형식의 파일만 업로드할 수 있습니다.`);
|
|
70
|
+
}
|
|
71
|
+
const limitedFiles = filteredFiles.slice(0, limit ? limit - fileRef.current.length : Infinity);
|
|
72
|
+
if (limitedFiles.length === 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (onFileInput) {
|
|
76
|
+
onFileInput(limitedFiles);
|
|
77
|
+
}
|
|
78
|
+
for (const file of limitedFiles) {
|
|
79
|
+
const fileItem = {
|
|
80
|
+
key: v4(),
|
|
81
|
+
};
|
|
82
|
+
setFiles((prevFiles) => [...prevFiles, fileItem]);
|
|
83
|
+
uploadFile === null || uploadFile === void 0 ? void 0 : uploadFile(file, options).then(async (item) => {
|
|
84
|
+
await (onFileUploaded === null || onFileUploaded === void 0 ? void 0 : onFileUploaded(item));
|
|
85
|
+
setFiles((prevFiles) => prevFiles.map((f) => {
|
|
86
|
+
if (f.key === fileItem.key) {
|
|
87
|
+
return Object.assign(Object.assign({}, f), { item });
|
|
88
|
+
}
|
|
89
|
+
return f;
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}, [props.accept]);
|
|
94
|
+
const handleDrop = useCallback((e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
setIsDragging(false);
|
|
98
|
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
99
|
+
handleFiles(Array.from(e.dataTransfer.files));
|
|
100
|
+
e.dataTransfer.clearData();
|
|
101
|
+
}
|
|
102
|
+
}, [handleFiles]);
|
|
103
|
+
const inputRef = useRef(null);
|
|
104
|
+
const handleClick = useCallback(() => {
|
|
105
|
+
var _a;
|
|
106
|
+
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
107
|
+
}, []);
|
|
108
|
+
const handleKeyDown = useCallback((e) => {
|
|
109
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
110
|
+
handleClick();
|
|
111
|
+
}
|
|
112
|
+
}, [handleClick]);
|
|
113
|
+
const handleChange = useCallback((e) => {
|
|
114
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
115
|
+
handleFiles(Array.from(e.target.files));
|
|
116
|
+
e.target.value = "";
|
|
117
|
+
}
|
|
118
|
+
}, [handleFiles]);
|
|
119
|
+
return (React.createElement("div", { className: cn(className, container, (draggingClassName === null || draggingClassName === void 0 ? void 0 : draggingClassName(isDragging)) ||
|
|
120
|
+
(isDragging ? "bg-neutral-300/25" : "hover:bg-neutral-300/25"), "transition-colors cursor-pointer"), onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, onClick: handleClick, onChange: handleChange, onKeyDown: handleKeyDown, tabIndex: 0, role: "button" },
|
|
121
|
+
React.createElement("input", Object.assign({}, props, { defaultValue: "", type: "file", hidden: true, ref: inputRef })),
|
|
122
|
+
React.createElement("input", { name: name, hidden: true, readOnly: true, value: files
|
|
123
|
+
.map((file) => {
|
|
124
|
+
if (file.item &&
|
|
125
|
+
typeof file.item === "object" &&
|
|
126
|
+
"id" in file.item) {
|
|
127
|
+
return file.item.id;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
})
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(",") }),
|
|
133
|
+
children ||
|
|
134
|
+
(!(hideMessage && !isDragging) && React.createElement(DropFileMessageBox, null))));
|
|
135
|
+
}, [limit, fileRef, files, options, uploadFile, onFileInput, onFileUploaded]);
|
|
136
|
+
const loadedFileIds = files
|
|
137
|
+
.map((file) => {
|
|
138
|
+
if (file.item && typeof file.item === "object" && "id" in file.item) {
|
|
139
|
+
return file.item.id;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
})
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
const loadedFileIdsString = loadedFileIds.join(",");
|
|
145
|
+
const fileIds = useMemo(() => loadedFileIdsString.split(",").filter(Boolean), [loadedFileIdsString]);
|
|
146
|
+
return {
|
|
147
|
+
fileIds,
|
|
148
|
+
files,
|
|
149
|
+
setFiles,
|
|
150
|
+
Component,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function DropFileMessageBox() {
|
|
154
|
+
return (React.createElement("div", { className: "text-sm pointer-events-none flex justify-center items-center" },
|
|
155
|
+
React.createElement("div", { className: "flex flex-col items-center" },
|
|
156
|
+
React.createElement("span", null, "\uD30C\uC77C\uC744 \uC5EC\uAE30\uB85C \uB04C\uC5B4\uB2E4 \uB193\uAC70\uB098 \uD074\uB9AD\uD574\uC11C \uC120\uD0DD\uD574 \uC8FC\uC138\uC694"))));
|
|
157
|
+
}
|
|
158
|
+
export default useDropFileInput;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type FileUploaderOptions = {
|
|
2
|
+
metadata?: Record<string, unknown>;
|
|
3
|
+
};
|
|
4
|
+
export declare class FileUploader {
|
|
5
|
+
endpoint: string;
|
|
6
|
+
constructor(endpoint?: string);
|
|
7
|
+
formatSize: (size: number) => string;
|
|
8
|
+
uploadFile(file: File, options?: FileUploaderOptions): Promise<any>;
|
|
9
|
+
uploadBlob(blob: Blob, name?: string, options?: FileUploaderOptions): Promise<any>;
|
|
10
|
+
deleteFile(fileId: string): Promise<boolean>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export class FileUploader {
|
|
2
|
+
constructor(endpoint = "/api/files") {
|
|
3
|
+
this.formatSize = (size) => {
|
|
4
|
+
if (size < 1024) {
|
|
5
|
+
return `${size} B`;
|
|
6
|
+
}
|
|
7
|
+
if (size < 1024 * 1024) {
|
|
8
|
+
return `${(size / 1024).toFixed(2)} KB`;
|
|
9
|
+
}
|
|
10
|
+
return `${(size / (1024 * 1024)).toFixed(2)} MB`;
|
|
11
|
+
};
|
|
12
|
+
this.endpoint = endpoint;
|
|
13
|
+
}
|
|
14
|
+
uploadFile(file, options = {}) {
|
|
15
|
+
return this.uploadBlob(file, file.name, options);
|
|
16
|
+
}
|
|
17
|
+
async uploadBlob(blob, name = "blob", options = {}) {
|
|
18
|
+
const { type, size } = blob;
|
|
19
|
+
const metadataForMedia = await new Promise((resolve, reject) => {
|
|
20
|
+
if (blob.type.startsWith("image/")) {
|
|
21
|
+
const img = new Image();
|
|
22
|
+
img.src = URL.createObjectURL(blob);
|
|
23
|
+
img.onload = () => {
|
|
24
|
+
resolve(Object.assign(Object.assign({}, options.metadata), { width: img.width, height: img.height }));
|
|
25
|
+
};
|
|
26
|
+
img.onerror = reject;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (blob.type.startsWith("video/")) {
|
|
30
|
+
const video = document.createElement("video");
|
|
31
|
+
video.src = URL.createObjectURL(blob);
|
|
32
|
+
video.onloadedmetadata = () => {
|
|
33
|
+
resolve(Object.assign(Object.assign({}, options.metadata), { width: video.videoWidth, height: video.videoHeight, duration: video.duration }));
|
|
34
|
+
};
|
|
35
|
+
video.onerror = reject;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
resolve(options.metadata || {});
|
|
39
|
+
});
|
|
40
|
+
const res1 = await fetch(this.endpoint, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
name: name.replace(/ /g, "_"),
|
|
44
|
+
type,
|
|
45
|
+
size,
|
|
46
|
+
metadata: metadataForMedia,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
if (!res1.ok) {
|
|
50
|
+
const message = await res1.json();
|
|
51
|
+
throw new Error(message);
|
|
52
|
+
}
|
|
53
|
+
const result = await res1.json();
|
|
54
|
+
const { signedUrl, file } = result;
|
|
55
|
+
const res2 = await fetch(signedUrl, {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
body: blob,
|
|
58
|
+
});
|
|
59
|
+
if (!res2.ok) {
|
|
60
|
+
throw new Error(await res2.text());
|
|
61
|
+
}
|
|
62
|
+
return file;
|
|
63
|
+
}
|
|
64
|
+
async deleteFile(fileId) {
|
|
65
|
+
const res = await fetch(`${this.endpoint}/${fileId}`, {
|
|
66
|
+
method: "DELETE",
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const { message } = await res.json();
|
|
70
|
+
throw new Error(message);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ObjectStorage } from "./object_storage";
|
|
2
|
+
import { type FileRepository } from "./repository";
|
|
3
|
+
type Params = {
|
|
4
|
+
userId?: string;
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
size: number;
|
|
8
|
+
metadata?: Record<string, any>;
|
|
9
|
+
};
|
|
10
|
+
export declare class FileService<TFile> {
|
|
11
|
+
prefix: string;
|
|
12
|
+
repository: FileRepository<TFile>;
|
|
13
|
+
OBJECT_STORAGE: ObjectStorage;
|
|
14
|
+
constructor(prefix: string | undefined, { repository, OBJECT_STORAGE, }: {
|
|
15
|
+
repository: FileRepository<TFile>;
|
|
16
|
+
OBJECT_STORAGE: ObjectStorage;
|
|
17
|
+
});
|
|
18
|
+
generateSignedUrl({ userId, name, type, size, metadata }: Params): Promise<{
|
|
19
|
+
file: Awaited<TFile>;
|
|
20
|
+
signedUrl: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { v4 } from "uuid";
|
|
2
|
+
export class FileService {
|
|
3
|
+
constructor(prefix = "user", { repository, OBJECT_STORAGE, }) {
|
|
4
|
+
this.prefix = prefix;
|
|
5
|
+
this.repository = repository;
|
|
6
|
+
this.OBJECT_STORAGE = OBJECT_STORAGE;
|
|
7
|
+
}
|
|
8
|
+
async generateSignedUrl({ userId, name, type, size, metadata = {} }) {
|
|
9
|
+
const id = v4();
|
|
10
|
+
const key = `${this.prefix}/${id}/${name}`;
|
|
11
|
+
const file = await this.repository.createFile({
|
|
12
|
+
id,
|
|
13
|
+
userId,
|
|
14
|
+
name,
|
|
15
|
+
type,
|
|
16
|
+
size,
|
|
17
|
+
metadata,
|
|
18
|
+
key,
|
|
19
|
+
});
|
|
20
|
+
const signedUrl = await this.OBJECT_STORAGE.generateSignedUrl(key, {
|
|
21
|
+
contentType: type,
|
|
22
|
+
});
|
|
23
|
+
return { file, signedUrl };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class ObjectStorage {
|
|
2
|
+
private client;
|
|
3
|
+
private bucketName;
|
|
4
|
+
constructor(bucketName: string);
|
|
5
|
+
generateSignedUrl(key: string, { contentType, expiresIn, }?: {
|
|
6
|
+
contentType?: string;
|
|
7
|
+
expiresIn?: number;
|
|
8
|
+
}): Promise<string>;
|
|
9
|
+
find(key: string): Promise<Uint8Array<ArrayBufferLike> | undefined>;
|
|
10
|
+
put(key: string, buffer: Buffer, { contentType }?: {
|
|
11
|
+
contentType?: string;
|
|
12
|
+
}): Promise<import("@aws-sdk/client-s3").PutObjectCommandOutput>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { GetObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
|
|
2
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3
|
+
export class ObjectStorage {
|
|
4
|
+
constructor(bucketName) {
|
|
5
|
+
this.client = new S3Client({
|
|
6
|
+
region: "ap-northeast-2",
|
|
7
|
+
});
|
|
8
|
+
this.bucketName = bucketName;
|
|
9
|
+
}
|
|
10
|
+
async generateSignedUrl(key, { contentType, expiresIn = 3600, } = {}) {
|
|
11
|
+
const command = new PutObjectCommand({
|
|
12
|
+
Bucket: this.bucketName,
|
|
13
|
+
Key: key,
|
|
14
|
+
ContentType: contentType,
|
|
15
|
+
});
|
|
16
|
+
const signedUrl = await getSignedUrl(this.client, command, {
|
|
17
|
+
expiresIn,
|
|
18
|
+
});
|
|
19
|
+
return signedUrl;
|
|
20
|
+
}
|
|
21
|
+
async find(key) {
|
|
22
|
+
try {
|
|
23
|
+
const command = new GetObjectCommand({
|
|
24
|
+
Bucket: this.bucketName,
|
|
25
|
+
Key: key,
|
|
26
|
+
});
|
|
27
|
+
const { Body } = await this.client.send(command);
|
|
28
|
+
if (!Body) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
return Body.transformToByteArray();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async put(key, buffer, { contentType } = {}) {
|
|
38
|
+
const command = new PutObjectCommand({
|
|
39
|
+
Bucket: this.bucketName,
|
|
40
|
+
Key: key,
|
|
41
|
+
ContentType: contentType,
|
|
42
|
+
Body: buffer,
|
|
43
|
+
});
|
|
44
|
+
return await this.client.send(command);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface FileRepository<TFile> {
|
|
2
|
+
findFileById(fileId: string): Promise<TFile | undefined>;
|
|
3
|
+
hasPermission(file: TFile, userId?: string): boolean;
|
|
4
|
+
createFile(fileData: {
|
|
5
|
+
id: string;
|
|
6
|
+
userId?: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
size: number;
|
|
10
|
+
metadata: Record<string, any>;
|
|
11
|
+
key: string;
|
|
12
|
+
}): Promise<TFile>;
|
|
13
|
+
deleteFile(fileId: string): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export type ResponsiveImageProps = Omit<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "src" | "srcSet" | "alt"> & {
|
|
3
|
+
alt: string;
|
|
4
|
+
src?: string;
|
|
5
|
+
file?: {
|
|
6
|
+
key: string;
|
|
7
|
+
};
|
|
8
|
+
ratio?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const createResponsiveImage: (cdn: (key: string | undefined, options?: {
|
|
11
|
+
width?: number;
|
|
12
|
+
}) => string | undefined) => React.FC<ResponsiveImageProps>;
|
|
13
|
+
export default createResponsiveImage;
|
|
14
|
+
export declare function generateSrcSet(image: HTMLImageElement | string, ratio?: number, props?: {
|
|
15
|
+
width?: string | number;
|
|
16
|
+
height?: string | number;
|
|
17
|
+
}): string | undefined;
|
|
@@ -0,0 +1,70 @@
|
|
|
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 from "react";
|
|
13
|
+
const sizes = [
|
|
14
|
+
64, 128, 256, 320, 480, 640, 768, 1080, 1200, 1536, 1920, 2560, 3840,
|
|
15
|
+
];
|
|
16
|
+
export const createResponsiveImage = (cdn) => {
|
|
17
|
+
const Component = (_a) => {
|
|
18
|
+
var { alt, file, ratio } = _a, props = __rest(_a, ["alt", "file", "ratio"]);
|
|
19
|
+
const src = cdn(file === null || file === void 0 ? void 0 : file.key) || props.src || "#";
|
|
20
|
+
return (React.createElement("img", Object.assign({}, props, { src: src, alt: alt, srcSet: generateSrcSet(src, ratio, props) })));
|
|
21
|
+
};
|
|
22
|
+
return Component;
|
|
23
|
+
};
|
|
24
|
+
export default createResponsiveImage;
|
|
25
|
+
const generateSrc = (src, width, height, ratio, image = {}) => {
|
|
26
|
+
const searchParams = new URLSearchParams();
|
|
27
|
+
if (image.width) {
|
|
28
|
+
searchParams.set("w", image.width.toString());
|
|
29
|
+
}
|
|
30
|
+
if (width) {
|
|
31
|
+
searchParams.set("w", width.toString());
|
|
32
|
+
if (ratio) {
|
|
33
|
+
searchParams.set("h", Math.round(width / ratio).toString());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (image.height) {
|
|
37
|
+
searchParams.set("h", image.height.toString());
|
|
38
|
+
}
|
|
39
|
+
if (height) {
|
|
40
|
+
searchParams.set("h", height.toString());
|
|
41
|
+
}
|
|
42
|
+
const search = searchParams.toString() ? `?${searchParams.toString()}` : "";
|
|
43
|
+
const origin = process.env.NEXT_PUBLIC_CDN_ORIGIN || "";
|
|
44
|
+
if (!src.includes(origin)) {
|
|
45
|
+
return src;
|
|
46
|
+
}
|
|
47
|
+
return `${encodeURI(decodeURI(src))}${search}`;
|
|
48
|
+
};
|
|
49
|
+
export function generateSrcSet(image, ratio, props = {}) {
|
|
50
|
+
const src = typeof image === "string" ? image : image.src;
|
|
51
|
+
const isGif = src.endsWith(".gif");
|
|
52
|
+
if (isGif) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (props.width) {
|
|
56
|
+
return [1, 2, 3]
|
|
57
|
+
.map((scale) => {
|
|
58
|
+
const genWidth = Number(props.width) * scale;
|
|
59
|
+
return `${generateSrc(src, genWidth, props.height
|
|
60
|
+
? Number(props.height) * scale
|
|
61
|
+
: ratio
|
|
62
|
+
? Math.round(genWidth / ratio)
|
|
63
|
+
: undefined)} ${scale}x`;
|
|
64
|
+
})
|
|
65
|
+
.join(", ");
|
|
66
|
+
}
|
|
67
|
+
return sizes
|
|
68
|
+
.map((size) => `${generateSrc(src, size, undefined, ratio, props)} ${size}w`)
|
|
69
|
+
.join(", ");
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./response";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./response";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const createJsonResponse: (status: number) => (data?: unknown, init?: ResponseInit) => Response;
|
|
2
|
+
export declare const OK: (data?: unknown, init?: ResponseInit) => Response;
|
|
3
|
+
export declare const CREATED: (data?: unknown, init?: ResponseInit) => Response;
|
|
4
|
+
export declare const ACCEPTED: (data?: unknown, init?: ResponseInit) => Response;
|
|
5
|
+
export declare const NO_CONTENT: (init?: ResponseInit) => Response;
|
|
6
|
+
export declare const createException: (status: number, defaultMessage?: unknown) => (message?: unknown, init?: ResponseInit) => Response;
|
|
7
|
+
export declare const BAD_REQUEST: (message?: unknown, init?: ResponseInit) => Response;
|
|
8
|
+
export declare const UNAUTHORIZED: (message?: unknown, init?: ResponseInit) => Response;
|
|
9
|
+
export declare const FORBIDDEN: (message?: unknown, init?: ResponseInit) => Response;
|
|
10
|
+
export declare const NOT_FOUND: (message?: unknown, init?: ResponseInit) => Response;
|
|
11
|
+
export declare const METHOD_NOT_ALLOWED: (message?: unknown, init?: ResponseInit) => Response;
|
|
12
|
+
export declare const NOT_ACCEPTABLE: (message?: unknown, init?: ResponseInit) => Response;
|
|
13
|
+
export declare const REQUEST_TIMEOUT: (message?: unknown, init?: ResponseInit) => Response;
|
|
14
|
+
export declare const CONFLICT: (message?: unknown, init?: ResponseInit) => Response;
|
|
15
|
+
export declare const UNPROCESSABLE_ENTITY: (message?: unknown, init?: ResponseInit) => Response;
|
|
16
|
+
export declare const TOO_MANY_REQUESTS: (message?: unknown, init?: ResponseInit) => Response;
|
|
17
|
+
export declare const INTERNAL_SERVER_ERROR: (message?: unknown, init?: ResponseInit) => Response;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const createJsonResponse = (status) => {
|
|
2
|
+
return (data = {}, init) => {
|
|
3
|
+
return Response.json(data, Object.assign({ status }, init));
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
Response;
|
|
7
|
+
export const OK = createJsonResponse(200);
|
|
8
|
+
export const CREATED = createJsonResponse(201);
|
|
9
|
+
export const ACCEPTED = createJsonResponse(202);
|
|
10
|
+
export const NO_CONTENT = (init) => {
|
|
11
|
+
return new Response(null, Object.assign(Object.assign({}, init), { status: 204 }));
|
|
12
|
+
};
|
|
13
|
+
export const createException = (status, defaultMessage = "오류가 발생했습니다.") => {
|
|
14
|
+
return (message = defaultMessage, init) => {
|
|
15
|
+
return createJsonResponse(status)({ message }, init);
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export const BAD_REQUEST = createException(400, "요청이 올바르지 않습니다.");
|
|
19
|
+
export const UNAUTHORIZED = createException(401, "인증이 필요합니다.");
|
|
20
|
+
export const FORBIDDEN = createException(403, "권한이 없습니다.");
|
|
21
|
+
export const NOT_FOUND = createException(404, "요청한 리소스를 찾을 수 없습니다.");
|
|
22
|
+
export const METHOD_NOT_ALLOWED = createException(405, "메서드를 사용할 수 없습니다.");
|
|
23
|
+
export const NOT_ACCEPTABLE = createException(406, "요청한 형식을 사용할 수 없습니다.");
|
|
24
|
+
export const REQUEST_TIMEOUT = createException(408, "요청 시간이 초과되었습니다.");
|
|
25
|
+
export const CONFLICT = createException(409, "요청이 충돌했습니다.");
|
|
26
|
+
export const UNPROCESSABLE_ENTITY = createException(422, "처리할 수 없는 엔티티입니다.");
|
|
27
|
+
export const TOO_MANY_REQUESTS = createException(429, "요청이 너무 많습니다.");
|
|
28
|
+
export const INTERNAL_SERVER_ERROR = createException(500, "예기치 못한 오류가 발생했습니다.");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GoogleAuth } from "../../../../../auth-kit/google_auth";
|
|
2
|
+
import { AppleAuth } from "../../../../../auth-kit/apple_auth";
|
|
3
|
+
import { KakaoAuth } from "../../../../../auth-kit/kakao_auth";
|
|
4
|
+
export declare const loginThirdPartyHandler: (request: Request, { provider, }: {
|
|
5
|
+
provider: string;
|
|
6
|
+
}, { GOOGLE_AUTH, APPLE_AUTH, KAKAO_AUTH, }: {
|
|
7
|
+
GOOGLE_AUTH: GoogleAuth;
|
|
8
|
+
APPLE_AUTH: AppleAuth;
|
|
9
|
+
KAKAO_AUTH: KakaoAuth;
|
|
10
|
+
}) => Promise<Response>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BAD_REQUEST, CREATED, INTERNAL_SERVER_ERROR, } from "../../../../../http-kit";
|
|
2
|
+
export const loginThirdPartyHandler = async (request, { provider, }, { GOOGLE_AUTH, APPLE_AUTH, KAKAO_AUTH, }) => {
|
|
3
|
+
const { code } = await request.json();
|
|
4
|
+
if (!code) {
|
|
5
|
+
return BAD_REQUEST("코드가 없습니다.");
|
|
6
|
+
}
|
|
7
|
+
const getThirdPartyAuth = (provider) => {
|
|
8
|
+
switch (provider) {
|
|
9
|
+
case "google":
|
|
10
|
+
return GOOGLE_AUTH;
|
|
11
|
+
case "apple":
|
|
12
|
+
return APPLE_AUTH;
|
|
13
|
+
case "kakao":
|
|
14
|
+
return KAKAO_AUTH;
|
|
15
|
+
default:
|
|
16
|
+
throw BAD_REQUEST("지원하지 않는 인증 제공자입니다.");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
const { accessToken, refreshToken } = await getThirdPartyAuth(provider).signIn(code);
|
|
21
|
+
return CREATED({
|
|
22
|
+
accessToken,
|
|
23
|
+
refreshToken,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof Response) {
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
throw INTERNAL_SERVER_ERROR();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AuthService } from "../../../../auth-kit/auth_service";
|
|
2
|
+
import { JWTManager } from "../../../../auth-kit/jwt";
|
|
3
|
+
export declare const loginCredentialHandler: (request: Request, { AUTH, JWT_MANAGER, }: {
|
|
4
|
+
AUTH: AuthService;
|
|
5
|
+
JWT_MANAGER: JWTManager;
|
|
6
|
+
}) => Promise<Response>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CREATED, UNAUTHORIZED } from "../../../../http-kit";
|
|
2
|
+
export const loginCredentialHandler = async (request, { AUTH, JWT_MANAGER, }) => {
|
|
3
|
+
const searchParams = new URL(request.url).searchParams;
|
|
4
|
+
const { id, password } = await request.json();
|
|
5
|
+
try {
|
|
6
|
+
const { accessToken, refreshToken } = await AUTH.signIn({
|
|
7
|
+
id,
|
|
8
|
+
password,
|
|
9
|
+
});
|
|
10
|
+
if (searchParams.get("type") === "json") {
|
|
11
|
+
return CREATED({ accessToken, refreshToken });
|
|
12
|
+
}
|
|
13
|
+
const [accessTokenSetCookie, refreshTokenSetCookie] = await Promise.all([
|
|
14
|
+
AUTH.getAccessTokenSetCookie(accessToken),
|
|
15
|
+
AUTH.getRefreshTokenSetCookie(refreshToken),
|
|
16
|
+
]);
|
|
17
|
+
const payload = JWT_MANAGER.decode(accessToken);
|
|
18
|
+
const headers = new Headers();
|
|
19
|
+
headers.append("Set-Cookie", accessTokenSetCookie);
|
|
20
|
+
headers.append("Set-Cookie", refreshTokenSetCookie);
|
|
21
|
+
return CREATED(payload, {
|
|
22
|
+
headers,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
if (e instanceof Error) {
|
|
27
|
+
return UNAUTHORIZED(e.message);
|
|
28
|
+
}
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AuthService } from "../../../../auth-kit/auth_service";
|
|
2
|
+
import { type AuthRepository } from "../../../../auth-kit/repository";
|
|
3
|
+
export declare const logoutHandler: (request: Request, { AUTH, repository }: {
|
|
4
|
+
AUTH: AuthService;
|
|
5
|
+
repository: AuthRepository;
|
|
6
|
+
}) => Promise<Response>;
|