@akanjs/next 0.0.50 → 0.0.51
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/bootCsr.tsx +180 -0
- package/createNextMiddleware.ts +45 -0
- package/createRobotPage.ts +14 -0
- package/createSitemapPage.ts +6 -0
- package/lazy.ts +9 -0
- package/makePageProto.tsx +117 -0
- package/package.json +1 -1
- package/types.ts +7 -0
- package/useCamera.tsx +94 -0
- package/useCodepush.tsx +98 -0
- package/useContact.tsx +45 -0
- package/useCsrValues.ts +613 -0
- package/useDebounce.ts +23 -0
- package/useFetch.ts +24 -0
- package/useGeoLocation.tsx +21 -0
- package/useHistory.ts +55 -0
- package/useInterval.ts +20 -0
- package/useLocation.ts +65 -0
- package/usePurchase.tsx +155 -0
- package/usePushNoti.tsx +39 -0
- package/useThrottle.ts +17 -0
- package/index.js +0 -60
package/bootCsr.tsx
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { device, initAuth, storage } from "@akanjs/client";
|
|
3
|
+
import {
|
|
4
|
+
type CsrConfig,
|
|
5
|
+
csrContext,
|
|
6
|
+
DEFAULT_BOTTOM_INSET,
|
|
7
|
+
DEFAULT_TOP_INSET,
|
|
8
|
+
defaultPageState,
|
|
9
|
+
type PageState,
|
|
10
|
+
type PathRoute,
|
|
11
|
+
type Route,
|
|
12
|
+
type RouteGuide,
|
|
13
|
+
} from "@akanjs/client";
|
|
14
|
+
import { Logger } from "@akanjs/common";
|
|
15
|
+
import React, { ReactNode } from "react"; // import React 꼭 필요함. 안그러면 csr에서 에러남
|
|
16
|
+
import * as ReactDOM from "react-dom/client";
|
|
17
|
+
|
|
18
|
+
import { useCsrValues } from "./useCsrValues";
|
|
19
|
+
|
|
20
|
+
const supportLanguages = ["en", "ko"] as const;
|
|
21
|
+
|
|
22
|
+
export const bootCsr = async (
|
|
23
|
+
context: Record<string, () => Promise<unknown>>,
|
|
24
|
+
rootPath: string,
|
|
25
|
+
entryPath = "/route"
|
|
26
|
+
) => {
|
|
27
|
+
// 1. Collect Device Information
|
|
28
|
+
const [, jwt] = await Promise.all([device.init({ supportLanguages }), storage.getItem("jwt")]);
|
|
29
|
+
if (jwt) initAuth({ jwt });
|
|
30
|
+
Logger.verbose(`Set default language: ${device.lang}`);
|
|
31
|
+
|
|
32
|
+
// 2. Create Route Map
|
|
33
|
+
const pages: { [key: string]: { default: { csrConfig?: CsrConfig } } } = {};
|
|
34
|
+
await Promise.all(
|
|
35
|
+
Object.entries(context).map(async ([key, value]) => {
|
|
36
|
+
pages[key] = (await value()) as { default: { csrConfig?: CsrConfig } };
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
const getPageState = (csrConfig?: CsrConfig) => {
|
|
40
|
+
const { transition, safeArea, topInset, bottomInset, gesture, cache }: CsrConfig = csrConfig ?? {};
|
|
41
|
+
const pageState: PageState = {
|
|
42
|
+
transition: transition ?? "none",
|
|
43
|
+
topSafeArea: safeArea === false ? 0 : device.topSafeArea,
|
|
44
|
+
bottomSafeArea: safeArea === false ? 0 : device.bottomSafeArea,
|
|
45
|
+
// topSafeArea: safeArea === false || device.info.platform === "android" ? 0 : device.topSafeArea,
|
|
46
|
+
// bottomSafeArea: safeArea === false || device.info.platform === "android" ? 0 : device.bottomSafeArea,
|
|
47
|
+
topInset: topInset === true ? DEFAULT_TOP_INSET : topInset === false ? 0 : (topInset ?? 0),
|
|
48
|
+
bottomInset: bottomInset === true ? DEFAULT_BOTTOM_INSET : bottomInset === false ? 0 : (bottomInset ?? 0),
|
|
49
|
+
gesture: gesture ?? true,
|
|
50
|
+
cache: cache ?? false,
|
|
51
|
+
};
|
|
52
|
+
return pageState;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const routeMap = new Map<string, Route>();
|
|
56
|
+
routeMap.set("/", { path: "/", children: new Map() });
|
|
57
|
+
for (const filePath of Object.keys(pages)) {
|
|
58
|
+
const fileName = /\.\/(.*)\.tsx$/.exec(filePath)?.[1];
|
|
59
|
+
|
|
60
|
+
if (!fileName) continue;
|
|
61
|
+
const fileType: "page" | "layout" | null = fileName.endsWith("page")
|
|
62
|
+
? "page"
|
|
63
|
+
: fileName.endsWith("layout")
|
|
64
|
+
? "layout"
|
|
65
|
+
: null;
|
|
66
|
+
if (!fileType) continue;
|
|
67
|
+
const pathSegments = [
|
|
68
|
+
"/",
|
|
69
|
+
...fileName
|
|
70
|
+
.split("/")
|
|
71
|
+
.slice(0, -1)
|
|
72
|
+
.map((segment) => `/${segment.replace(/\[(.*?)\]/g, ":$1")}`),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const targetRouteMap = pathSegments.slice(0, -1).reduce((rMap: Map<string, Route>, path: string) => {
|
|
76
|
+
if (!rMap.has(path)) rMap.set(path, { path, children: new Map() });
|
|
77
|
+
return rMap.get(path)?.children;
|
|
78
|
+
}, routeMap);
|
|
79
|
+
if (!targetRouteMap) continue;
|
|
80
|
+
const targetPath = pathSegments[pathSegments.length - 1];
|
|
81
|
+
|
|
82
|
+
targetRouteMap.set(targetPath, {
|
|
83
|
+
// action: pages[path]?.action,
|
|
84
|
+
// ErrorBoundary: pages[path]?.ErrorBoundary,
|
|
85
|
+
...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
|
|
86
|
+
...(fileType === "layout"
|
|
87
|
+
? { Layout: pages[filePath].default }
|
|
88
|
+
: {
|
|
89
|
+
Page: pages[filePath].default,
|
|
90
|
+
pageState: getPageState(pages[filePath].default.csrConfig),
|
|
91
|
+
csrConfig: pages[filePath].default.csrConfig,
|
|
92
|
+
}),
|
|
93
|
+
} as Route);
|
|
94
|
+
}
|
|
95
|
+
const pathname = window.location.pathname;
|
|
96
|
+
const initialPath = device.lang + entryPath;
|
|
97
|
+
window.document.body.style.overflow = "hidden";
|
|
98
|
+
|
|
99
|
+
const getPathRoutes = (
|
|
100
|
+
route: Route,
|
|
101
|
+
parentRootLayouts: (
|
|
102
|
+
| (({ children, params, searchParams }) => ReactNode)
|
|
103
|
+
| (({ children, params, searchParams }) => Promise<ReactNode>)
|
|
104
|
+
)[] = [],
|
|
105
|
+
parentLayouts: (
|
|
106
|
+
| (({ children, params, searchParams }) => ReactNode)
|
|
107
|
+
| (({ children, params, searchParams }) => Promise<ReactNode>)
|
|
108
|
+
)[] = [],
|
|
109
|
+
parentPaths: string[] = []
|
|
110
|
+
): PathRoute[] => {
|
|
111
|
+
const parentPath = parentPaths.filter((path) => path !== "/").join("");
|
|
112
|
+
const currentPathSegment = /^\/\(.*\)$/.test(route.path) ? "" : route.path;
|
|
113
|
+
const isRoot = ["/", "/:lang"].includes(parentPath + currentPathSegment) && parentRootLayouts.length < 2;
|
|
114
|
+
const path = parentPath + currentPathSegment;
|
|
115
|
+
const pathSegments = [...parentPaths, ...(currentPathSegment ? [currentPathSegment] : [])];
|
|
116
|
+
const RootLayouts = [...parentRootLayouts, ...(isRoot && route.Layout ? [route.Layout] : [])];
|
|
117
|
+
const Layouts = [...parentLayouts, ...(!isRoot && route.Layout ? [route.Layout] : [])];
|
|
118
|
+
return [
|
|
119
|
+
...(route.Page
|
|
120
|
+
? [
|
|
121
|
+
{
|
|
122
|
+
path,
|
|
123
|
+
pathSegments,
|
|
124
|
+
Page: route.Page,
|
|
125
|
+
RootLayouts,
|
|
126
|
+
Layouts,
|
|
127
|
+
pageState: route.pageState ?? defaultPageState,
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
: []),
|
|
131
|
+
...(route.children.size
|
|
132
|
+
? [...route.children.values()].flatMap((child) => getPathRoutes(child, RootLayouts, Layouts, pathSegments))
|
|
133
|
+
: []),
|
|
134
|
+
];
|
|
135
|
+
};
|
|
136
|
+
const rootRoute = routeMap.get("/");
|
|
137
|
+
if (!rootRoute) throw new Error("No root route");
|
|
138
|
+
const pathRoutes = getPathRoutes(rootRoute);
|
|
139
|
+
const routeGuide: RouteGuide = { pathSegment: "/", children: {} };
|
|
140
|
+
pathRoutes.forEach((pathRoute) => {
|
|
141
|
+
const pathSegments = pathRoute.pathSegments.slice(1);
|
|
142
|
+
pathSegments.reduce((routeGuide, pathSegment, index) => {
|
|
143
|
+
const child = routeGuide.children[pathSegment] as RouteGuide | undefined;
|
|
144
|
+
routeGuide.children[pathSegment] = {
|
|
145
|
+
...(child ?? {}),
|
|
146
|
+
pathSegment,
|
|
147
|
+
...(index === pathSegments.length - 1 ? { pathRoute } : {}),
|
|
148
|
+
children: (child?.children as { [key: string]: RouteGuide } | undefined) ?? {},
|
|
149
|
+
} as RouteGuide;
|
|
150
|
+
return routeGuide.children[pathSegment];
|
|
151
|
+
}, routeGuide);
|
|
152
|
+
});
|
|
153
|
+
const RouterProvider = () => {
|
|
154
|
+
const csrValues = useCsrValues(routeGuide, pathRoutes);
|
|
155
|
+
const { location } = csrValues;
|
|
156
|
+
return (
|
|
157
|
+
<csrContext.Provider value={csrValues}>
|
|
158
|
+
{location.pathRoute.RootLayouts.reduceRight(
|
|
159
|
+
(children, Layout: any) => {
|
|
160
|
+
return (
|
|
161
|
+
<Layout params={location.params} searchParams={location.searchParams}>
|
|
162
|
+
{children}
|
|
163
|
+
</Layout>
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
<></>
|
|
167
|
+
)}
|
|
168
|
+
</csrContext.Provider>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
if (pathname !== `/${initialPath}`) {
|
|
172
|
+
window.location.replace(initialPath);
|
|
173
|
+
return;
|
|
174
|
+
} else {
|
|
175
|
+
const el = document.getElementById("root");
|
|
176
|
+
if (!el) throw new Error("No root element");
|
|
177
|
+
const root = ReactDOM.createRoot(el);
|
|
178
|
+
root.render(<RouterProvider />);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { logo } from "@akanjs/base";
|
|
2
|
+
import { Logger } from "@akanjs/common";
|
|
3
|
+
import { match as matchLocale } from "@formatjs/intl-localematcher";
|
|
4
|
+
import Negotiator from "negotiator";
|
|
5
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
6
|
+
|
|
7
|
+
const i18n = { defaultLocale: "en", locales: ["en", "ko"] };
|
|
8
|
+
const basePaths = process.env.basePaths ? process.env.basePaths.split(",") : [];
|
|
9
|
+
|
|
10
|
+
function getLocale(request: NextRequest): string | undefined {
|
|
11
|
+
if (!request.headers.get("accept-language")) return i18n.defaultLocale;
|
|
12
|
+
const negotiatorHeaders: Record<string, string> = {};
|
|
13
|
+
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
|
|
14
|
+
try {
|
|
15
|
+
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
|
|
16
|
+
return matchLocale(languages, i18n.locales, i18n.defaultLocale);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return i18n.defaultLocale;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const createNextMiddleware = () => {
|
|
22
|
+
Logger.rawLog(logo, "console");
|
|
23
|
+
const middleware = (request: NextRequest) => {
|
|
24
|
+
const pathname = request.nextUrl.pathname;
|
|
25
|
+
const pathnameIsMissingLocale = i18n.locales.every(
|
|
26
|
+
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
|
|
27
|
+
);
|
|
28
|
+
if (pathnameIsMissingLocale)
|
|
29
|
+
return NextResponse.redirect(
|
|
30
|
+
new URL(`/${getLocale(request)}/${request.nextUrl.href.split("/").slice(3).join("/")}`, request.url)
|
|
31
|
+
);
|
|
32
|
+
const splits = pathname.split("/");
|
|
33
|
+
const locale = splits[1];
|
|
34
|
+
const basePath = basePaths.includes(splits[2]) ? splits[2] : null;
|
|
35
|
+
const headers = new Headers(request.headers);
|
|
36
|
+
const searchParams = new URLSearchParams(request.nextUrl.search);
|
|
37
|
+
const searchParamJwt = searchParams.get("jwt");
|
|
38
|
+
headers.set("x-locale", locale);
|
|
39
|
+
headers.set("x-path", "/" + splits.slice(2).join("/"));
|
|
40
|
+
if (basePath) headers.set("x-base-path", basePath);
|
|
41
|
+
if (searchParamJwt) headers.set("jwt", searchParamJwt);
|
|
42
|
+
return NextResponse.next({ request: { headers } });
|
|
43
|
+
};
|
|
44
|
+
return middleware;
|
|
45
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MetadataRoute } from "next";
|
|
2
|
+
|
|
3
|
+
export const createRobotPage = (clientHttpUri: string, config?: MetadataRoute.Robots): MetadataRoute.Robots => {
|
|
4
|
+
return {
|
|
5
|
+
...(config ?? {}),
|
|
6
|
+
rules: {
|
|
7
|
+
userAgent: "*",
|
|
8
|
+
allow: "/",
|
|
9
|
+
disallow: "/admin/",
|
|
10
|
+
...(config?.rules ?? {}),
|
|
11
|
+
},
|
|
12
|
+
sitemap: `${clientHttpUri}/sitemap.xml`,
|
|
13
|
+
};
|
|
14
|
+
};
|
package/lazy.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//! next build를 위해서 lint 무시
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
3
|
+
import dynamic, { DynamicOptions } from "next/dynamic";
|
|
4
|
+
import { ComponentType } from "react";
|
|
5
|
+
|
|
6
|
+
export const lazy = <T extends ComponentType<any>>(
|
|
7
|
+
loader: (x?: string) => Promise<{ default: T } | T>,
|
|
8
|
+
option?: DynamicOptions<T>
|
|
9
|
+
) => (dynamic as any)(loader, option ?? {}) as unknown as T;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { baseClientEnv } from "@akanjs/base";
|
|
2
|
+
import { getHeader } from "@akanjs/client";
|
|
3
|
+
import { Logger, pathGet } from "@akanjs/common";
|
|
4
|
+
import { type Translation, type TransMessage } from "@akanjs/dictionary";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
const getPageInfo = (): { locale: string; path: string } => {
|
|
8
|
+
if (baseClientEnv.side !== "server") {
|
|
9
|
+
// client side, has window object
|
|
10
|
+
return {
|
|
11
|
+
locale: window.location.pathname.split("/")[1] ?? "en",
|
|
12
|
+
path: "/" + window.location.pathname.split("/").slice(2).join("/"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const locale = getHeader("x-locale") ?? "en";
|
|
16
|
+
const path = getHeader("x-path") ?? "/";
|
|
17
|
+
return { locale, path };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const langIdx: Record<string, number> = { en: 0, ko: 1, zhChs: 2, zhCht: 3 };
|
|
21
|
+
const dictionary: { [key: string]: { [key: string]: string } } = {};
|
|
22
|
+
const translator = (lang: string, key: string, param?: Record<string, string | number>) => {
|
|
23
|
+
const idx = langIdx[lang];
|
|
24
|
+
try {
|
|
25
|
+
const msg = pathGet(key, dictionary)?.[idx] as string | undefined;
|
|
26
|
+
if (!msg) {
|
|
27
|
+
Logger.error(`No translation for ${key}`);
|
|
28
|
+
return key;
|
|
29
|
+
}
|
|
30
|
+
return param ? msg.replace(/{([^}]+)}/g, (_, key: string) => param[key] as string) : msg;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return key;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
translator.rich = (lang: string, key: string, param?: Record<string, string>) => {
|
|
36
|
+
const idx = langIdx[lang];
|
|
37
|
+
const msg = pathGet(key, dictionary)?.[idx] as string | undefined;
|
|
38
|
+
if (!msg) {
|
|
39
|
+
Logger.error(`No translation for ${key}`);
|
|
40
|
+
return key;
|
|
41
|
+
}
|
|
42
|
+
return param ? msg.replace(/{([^}]+)}/g, (_, key: string) => param[key]) : msg;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const makePageProto = <Locale extends { [key: string]: { [key: string]: Translation } }>(locales: Locale[]) => {
|
|
46
|
+
locales.forEach((locale) => {
|
|
47
|
+
Object.keys(locale).forEach((key: string) => (dictionary[key] = Object.assign(dictionary[key] ?? {}, locale[key])));
|
|
48
|
+
});
|
|
49
|
+
return () => {
|
|
50
|
+
const { locale, path } = getPageInfo();
|
|
51
|
+
const lang = locale;
|
|
52
|
+
const l = (key: TransMessage<Locale>, param?: { [key: string]: string | number }) => translator(lang, key, param);
|
|
53
|
+
l.rich = (key: TransMessage<Locale>, param?: { [key: string]: string | number }) =>
|
|
54
|
+
(
|
|
55
|
+
<span
|
|
56
|
+
dangerouslySetInnerHTML={{
|
|
57
|
+
__html: translator.rich(lang, key, {
|
|
58
|
+
...param,
|
|
59
|
+
// strong: (chunks: string) => `<b>${chunks}</b>`,
|
|
60
|
+
// "bg-primary": (chunks: string) => `<span className="bg-primary text-base-100">${chunks}</span>`,
|
|
61
|
+
// primary: (chunks: string) => `<span className="bg-base-100 text-primary">${chunks}</span>`,
|
|
62
|
+
br: `<br />`,
|
|
63
|
+
}),
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
) as ReactNode;
|
|
67
|
+
l.field = <ModelKey extends keyof Locale>(model: ModelKey, field: keyof Locale[ModelKey]) => {
|
|
68
|
+
const key = `${model as string}.${field as string}` as unknown as TransMessage<Locale>;
|
|
69
|
+
return l(key);
|
|
70
|
+
};
|
|
71
|
+
l.desc = <ModelKey extends keyof Locale>(model: ModelKey, field: keyof Locale[ModelKey]) => {
|
|
72
|
+
const key = `${model as string}.desc-${field as string}` as unknown as TransMessage<Locale>;
|
|
73
|
+
return l(key);
|
|
74
|
+
};
|
|
75
|
+
l.enum = <ModelKey extends keyof Locale>(model: ModelKey, field: keyof Locale[ModelKey], value: string) => {
|
|
76
|
+
const key = `${model as string}.enum-${field as string}-${value}` as unknown as TransMessage<Locale>;
|
|
77
|
+
return l(key);
|
|
78
|
+
};
|
|
79
|
+
l.enumdesc = <ModelKey extends keyof Locale>(model: ModelKey, field: keyof Locale[ModelKey], value: string) => {
|
|
80
|
+
const key = `${model as string}.enumdesc-${field as string}-${value}` as unknown as TransMessage<Locale>;
|
|
81
|
+
return l(key);
|
|
82
|
+
};
|
|
83
|
+
l.api = <ModelKey extends keyof Locale>(model: ModelKey, endpoint: keyof Locale[ModelKey]) => {
|
|
84
|
+
const key = `${model as string}.api-${endpoint as string}` as unknown as TransMessage<Locale>;
|
|
85
|
+
return l(key);
|
|
86
|
+
};
|
|
87
|
+
l.apidesc = <ModelKey extends keyof Locale>(model: ModelKey, endpoint: keyof Locale[ModelKey]) => {
|
|
88
|
+
const key = `${model as string}.apidesc-${endpoint as string}` as unknown as TransMessage<Locale>;
|
|
89
|
+
return l(key);
|
|
90
|
+
};
|
|
91
|
+
l.arg = <ModelKey extends keyof Locale>(model: ModelKey, endpoint: keyof Locale[ModelKey], arg: string) => {
|
|
92
|
+
const key = `${model as string}.arg-${endpoint as string}-${arg}` as unknown as TransMessage<Locale>;
|
|
93
|
+
return l(key);
|
|
94
|
+
};
|
|
95
|
+
l.argdesc = <ModelKey extends keyof Locale>(model: ModelKey, endpoint: keyof Locale[ModelKey], arg: string) => {
|
|
96
|
+
const key = `${model as string}.argdesc-${endpoint as string}-${arg}` as unknown as TransMessage<Locale>;
|
|
97
|
+
return l(key);
|
|
98
|
+
};
|
|
99
|
+
l.qry = <ModelKey extends keyof Locale>(model: ModelKey, queryKey: keyof Locale[ModelKey]) => {
|
|
100
|
+
const key = `${model as string}.qry-${queryKey as string}` as unknown as TransMessage<Locale>;
|
|
101
|
+
return l(key);
|
|
102
|
+
};
|
|
103
|
+
l.qrydesc = <ModelKey extends keyof Locale>(model: ModelKey, queryKey: keyof Locale[ModelKey]) => {
|
|
104
|
+
const key = `${model as string}.qrydesc-${queryKey as string}` as unknown as TransMessage<Locale>;
|
|
105
|
+
return l(key);
|
|
106
|
+
};
|
|
107
|
+
l.qarg = <ModelKey extends keyof Locale>(model: ModelKey, queryKey: keyof Locale[ModelKey], arg: string) => {
|
|
108
|
+
const key = `${model as string}.qarg-${queryKey as string}-${arg}` as unknown as TransMessage<Locale>;
|
|
109
|
+
return l(key);
|
|
110
|
+
};
|
|
111
|
+
l.qargdesc = <ModelKey extends keyof Locale>(model: ModelKey, queryKey: keyof Locale[ModelKey], arg: string) => {
|
|
112
|
+
const key = `${model as string}.qargdesc-${queryKey as string}-${arg}` as unknown as TransMessage<Locale>;
|
|
113
|
+
return l(key);
|
|
114
|
+
};
|
|
115
|
+
return { path, l, lang };
|
|
116
|
+
};
|
|
117
|
+
};
|
package/package.json
CHANGED
package/types.ts
ADDED
package/useCamera.tsx
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { device } from "@akanjs/client";
|
|
3
|
+
import { Camera, CameraResultType, CameraSource, PermissionStatus } from "@capacitor/camera";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
export const useCamera = () => {
|
|
7
|
+
const [permissions, setPermissions] = useState<PermissionStatus>({ camera: "prompt", photos: "prompt" });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 최초로 킬 경우 권한은 prompt 상태이다.
|
|
11
|
+
* prompt 상태일 경우 권한을 요청한다.
|
|
12
|
+
* 권한이 denied 상태일 경우 설정으로 이동한다.
|
|
13
|
+
* 이후 state의 permission을 업데이트해야한다.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
const checkPermission = async (type: "photos" | "camera" | "all") => {
|
|
17
|
+
try {
|
|
18
|
+
if (type === "photos") {
|
|
19
|
+
if (permissions.photos === "prompt") {
|
|
20
|
+
const { photos } = await Camera.requestPermissions();
|
|
21
|
+
setPermissions((prev) => ({ ...prev, photos }));
|
|
22
|
+
} else if (permissions.photos === "denied") {
|
|
23
|
+
location.assign("app-settings:");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
} else if (type === "camera") {
|
|
27
|
+
if (permissions.camera === "prompt") {
|
|
28
|
+
const { camera } = await Camera.requestPermissions();
|
|
29
|
+
setPermissions((prev) => ({ ...prev, camera }));
|
|
30
|
+
} else if (permissions.camera === "denied") {
|
|
31
|
+
location.assign("app-settings:");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
if (permissions.camera === "prompt" || permissions.photos === "prompt") {
|
|
36
|
+
const permissions = await Camera.requestPermissions();
|
|
37
|
+
setPermissions(permissions);
|
|
38
|
+
} else if (permissions.camera === "denied" || permissions.photos === "denied") {
|
|
39
|
+
location.assign("app-settings:");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
//
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getPhoto = async (src: "prompt" | "camera" | "photos" = "prompt") => {
|
|
49
|
+
const source =
|
|
50
|
+
device.info.platform !== "web"
|
|
51
|
+
? src === "prompt"
|
|
52
|
+
? CameraSource.Prompt
|
|
53
|
+
: src === "camera"
|
|
54
|
+
? CameraSource.Camera
|
|
55
|
+
: CameraSource.Photos
|
|
56
|
+
: CameraSource.Photos;
|
|
57
|
+
const permission = src === "prompt" ? "all" : src === "camera" ? "camera" : "photos";
|
|
58
|
+
void checkPermission(permission);
|
|
59
|
+
try {
|
|
60
|
+
const photo = await Camera.getPhoto({
|
|
61
|
+
quality: 100,
|
|
62
|
+
source,
|
|
63
|
+
allowEditing: false,
|
|
64
|
+
resultType: CameraResultType.DataUrl,
|
|
65
|
+
promptLabelHeader: "프로필 사진을 올려주세요",
|
|
66
|
+
promptLabelPhoto: "앨범에서 선택하기",
|
|
67
|
+
promptLabelPicture: "사진 찍기",
|
|
68
|
+
promptLabelCancel: "취소",
|
|
69
|
+
});
|
|
70
|
+
return photo;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (e === "User cancelled photos app") return;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const pickImage = async () => {
|
|
77
|
+
void checkPermission("photos");
|
|
78
|
+
const photo = await Camera.pickImages({
|
|
79
|
+
quality: 90,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return photo;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
void (async () => {
|
|
87
|
+
if (device.info.platform !== "web") {
|
|
88
|
+
const permissions = await Camera.checkPermissions();
|
|
89
|
+
setPermissions(permissions);
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
}, []);
|
|
93
|
+
return { permissions, getPhoto, pickImage, checkPermission };
|
|
94
|
+
};
|
package/useCodepush.tsx
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { mergeVersion, splitVersion } from "@akanjs/common";
|
|
3
|
+
import type { ProtoAppInfo, ProtoFile } from "@akanjs/constant";
|
|
4
|
+
import { App } from "@capacitor/app";
|
|
5
|
+
import { Device } from "@capacitor/device";
|
|
6
|
+
import { CapacitorUpdater } from "@capgo/capacitor-updater";
|
|
7
|
+
import axios from "axios";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
|
|
10
|
+
export const useCodepush = ({ serverUrl, branch }: { serverUrl: string; branch: "debug" | "develop" | "main" }) => {
|
|
11
|
+
const [update, setUpdate] = useState(false);
|
|
12
|
+
const [version, setVersion] = useState("");
|
|
13
|
+
|
|
14
|
+
const initialize = async () => {
|
|
15
|
+
await CapacitorUpdater.notifyAppReady();
|
|
16
|
+
};
|
|
17
|
+
const checkNewRelease = async () => {
|
|
18
|
+
//*appInfo 정의
|
|
19
|
+
const info = await Device.getInfo();
|
|
20
|
+
const app = await App.getInfo();
|
|
21
|
+
const pluginVersion = await CapacitorUpdater.getPluginVersion();
|
|
22
|
+
const { deviceId } = await CapacitorUpdater.getDeviceId();
|
|
23
|
+
const { bundle: version, native } = await CapacitorUpdater.current();
|
|
24
|
+
const builtInversion = await CapacitorUpdater.getBuiltinVersion();
|
|
25
|
+
const appId = app.id;
|
|
26
|
+
const platform = info.platform;
|
|
27
|
+
|
|
28
|
+
window.alert(
|
|
29
|
+
`getBuildinVersion:${builtInversion.version}\ncurrent.bundle:${version.version}\ncurrennt.native:${native}`
|
|
30
|
+
);
|
|
31
|
+
/**
|
|
32
|
+
* "version_name": "builtin",
|
|
33
|
+
* "version_code": "1",
|
|
34
|
+
* "app_id": "com.lu.app",
|
|
35
|
+
* "plugin_version": "5.6.9",
|
|
36
|
+
* "version_build": "1.0",
|
|
37
|
+
* "is_prod": true,
|
|
38
|
+
* "version_os": "17.0.1",
|
|
39
|
+
* "is_emulator": true,
|
|
40
|
+
* "custom_id": "",
|
|
41
|
+
* "device_id": "C77000B1-7D28-4697-ADE0-74452F47C350",
|
|
42
|
+
* "platform": "ios",
|
|
43
|
+
* "defaultChannel": ""
|
|
44
|
+
*/
|
|
45
|
+
const { major, minor, patch } = splitVersion(version.version === "builtin" ? app.version : version.version);
|
|
46
|
+
const appName = process.env.NEXT_PUBLIC_APP_NAME ?? "";
|
|
47
|
+
|
|
48
|
+
const appInfo: ProtoAppInfo = {
|
|
49
|
+
appId,
|
|
50
|
+
appName,
|
|
51
|
+
deviceId: deviceId,
|
|
52
|
+
platform: platform as "ios" | "android",
|
|
53
|
+
branch,
|
|
54
|
+
isEmulator: info.isVirtual,
|
|
55
|
+
major: parseInt(major),
|
|
56
|
+
minor: parseInt(minor),
|
|
57
|
+
patch: parseInt(patch),
|
|
58
|
+
buildNum: app.build, //앱내 빌드시 버전 횟수 모르면 고한테 물어보기
|
|
59
|
+
versionOs: info.osVersion,
|
|
60
|
+
};
|
|
61
|
+
//fix lu to akasys
|
|
62
|
+
const url = serverUrl.replace("lu", "akasys");
|
|
63
|
+
const release = (
|
|
64
|
+
await axios.post<(ProtoAppInfo & { appBuild: string }) | null>(`${url}/release/codepush`, {
|
|
65
|
+
data: { ...appInfo },
|
|
66
|
+
})
|
|
67
|
+
).data;
|
|
68
|
+
if (!release) return;
|
|
69
|
+
const file = (await axios.get<ProtoFile>(`${url}/file/file/${release.appBuild}`)).data;
|
|
70
|
+
|
|
71
|
+
return { release: release, bundleFile: file };
|
|
72
|
+
//* fetch로 서버에게 내 AppInfo 전달.
|
|
73
|
+
// return await fetch.requestRelease();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const codepush = async () => {
|
|
77
|
+
//* isNeedUpdate로 업데이트 필요한지 확인
|
|
78
|
+
const isNewRelease = await checkNewRelease();
|
|
79
|
+
if (!isNewRelease) return;
|
|
80
|
+
const { release, bundleFile } = isNewRelease;
|
|
81
|
+
setUpdate(true);
|
|
82
|
+
const bundle = await CapacitorUpdater.download({
|
|
83
|
+
url: bundleFile.url,
|
|
84
|
+
version: mergeVersion(release.major, release.minor, release.patch),
|
|
85
|
+
});
|
|
86
|
+
//* 해제한 파일로 업데이트
|
|
87
|
+
await CapacitorUpdater.set(bundle);
|
|
88
|
+
};
|
|
89
|
+
const getVersion = async () => {
|
|
90
|
+
return await CapacitorUpdater.getBuiltinVersion();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const statManager = async () => {
|
|
94
|
+
// 업데이트 통계 서버에 전달
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return { update, version, initialize, checkNewRelease, codepush, statManager };
|
|
98
|
+
};
|
package/useContact.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { device } from "@akanjs/client";
|
|
3
|
+
import { Contacts, PermissionStatus } from "@capacitor-community/contacts";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
export const useContact = () => {
|
|
7
|
+
const [permissions, setPermissions] = useState<PermissionStatus>({ contacts: "prompt" });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 최초로 킬 경우 권한은 prompt 상태이다.
|
|
11
|
+
* prompt 상태일 경우 권한을 요청한다.
|
|
12
|
+
* 권한이 denied 상태일 경우 설정으로 이동한다.
|
|
13
|
+
* 이후 state의 permission을 업데이트해야한다.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
const checkPermission = async () => {
|
|
17
|
+
try {
|
|
18
|
+
if (permissions.contacts === "prompt") {
|
|
19
|
+
const { contacts } = await Contacts.requestPermissions();
|
|
20
|
+
setPermissions((prev) => ({ ...prev, contacts }));
|
|
21
|
+
} else if (permissions.contacts === "denied") {
|
|
22
|
+
location.assign("app-settings:");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
//
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getContacts = async () => {
|
|
31
|
+
await checkPermission();
|
|
32
|
+
const { contacts } = await Contacts.getContacts({ projection: { name: true, phones: true } });
|
|
33
|
+
return contacts;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
void (async () => {
|
|
38
|
+
if (device.info.platform === "web") return;
|
|
39
|
+
const permissions = await Contacts.checkPermissions();
|
|
40
|
+
setPermissions(permissions);
|
|
41
|
+
})();
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
return { permissions, getContacts, checkPermission };
|
|
45
|
+
};
|