diff --git a/src/agent/ApiClient.ts b/src/agent/ApiClient.ts index a34e140..77d0048 100644 --- a/src/agent/ApiClient.ts +++ b/src/agent/ApiClient.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; export const initApiClient = () => { axios.defaults.baseURL = process.env.API_BASE_URL; @@ -6,7 +6,11 @@ export const initApiClient = () => { export const apiClient = () => { return { - get: (url: string, params?: Params) => axios.get(url, { params }), + get: ( + url: string, + params?: Params, + ): Promise & { statusCode: number }> => + axios.get(url, { params }), post: (url: string, data: Body) => axios.post(url, data), put: (url: string, data: Body) => axios.put(url, data), delete: (url: string) => axios.delete(url), diff --git a/src/assets/icon/hourglass.svg b/src/assets/icon/hourglass.svg new file mode 100644 index 0000000..26cfef8 --- /dev/null +++ b/src/assets/icon/hourglass.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/warning.svg b/src/assets/icon/warning.svg new file mode 100644 index 0000000..4ae7705 --- /dev/null +++ b/src/assets/icon/warning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/client/renderer.tsx b/src/client/renderer.tsx index 407f89a..4d75382 100644 --- a/src/client/renderer.tsx +++ b/src/client/renderer.tsx @@ -2,22 +2,15 @@ import React from 'react'; import { renderToString } from 'react-dom/server'; import { Store } from 'redux'; import { App } from './view/App'; +import { NotFoundPage } from './view/pages/404'; +import { ExpiredPage } from './view/pages/expired'; -export const renderer = ({ - assetPath, - store, -}: { - assetPath: string; - store: Store; -}) => { - try { - const content = renderToString(); - return ` +const defaultHtml = (assetPath: string, content: string) => ` - + 반다라트 @@ -28,20 +21,29 @@ export const renderer = ({
${content}
- - `; + `; + +export const renderer = ({ + assetPath, + store, +}: { + assetPath: string; + store: Store; +}) => { + try { + const content = renderToString(); + return defaultHtml(assetPath, content); } catch (e) { - console.error(e); - return ` - - - 반다라트 - - - -
에러가 발생했어요!!
- - `; + return renderNotFound(assetPath); } }; + +export const renderNotFound = (assetPath: string) => { + const content = renderToString(); + return defaultHtml(assetPath, content); +}; + +export const renderExpired = (assetPath: string) => { + const content = renderToString(); + return defaultHtml(assetPath, content); +}; diff --git a/src/client/view/App.tsx b/src/client/view/App.tsx index 3518ecc..0544c34 100644 --- a/src/client/view/App.tsx +++ b/src/client/view/App.tsx @@ -3,9 +3,7 @@ import { initApiClient } from '../../agent/ApiClient'; import { BandalartSharePage } from './pages/share'; import { Provider } from 'react-redux'; import { Store } from 'redux'; -import { css, cx } from '@linaria/core'; -import { theme } from './theme'; -import { EnvContextProvider } from './components/context/EnvContext'; +import { DefaultContainer } from './components/_common/DefaultContainer'; initApiClient(); @@ -17,69 +15,9 @@ type AppProps = { export const App = ({ store, assetPath }: AppProps) => { return ( -
- - - -
+ + +
); }; - -const globalStyle = css` - font-family: - 'Pretendard Variable', - Pretendard, - -apple-system, - BlinkMacSystemFont, - system-ui, - Roboto, - 'Helvetica Neue', - 'Segoe UI', - 'Apple SD Gothic Neo', - 'Noto Sans KR', - 'Malgun Gothic', - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - sans-serif; - - background-color: var(--color-50); - - ${theme}; - - :global(html) { - html { - box-sizing: border-box; - } - - body { - margin: 0; - } - - *, - *::before, - *::after { - box-sizing: inherit; - } - - h1, - h2, - h3, - h4, - h5, - h6, - p { - margin: 0; - } - - ul { - list-style: none; - padding: 0; - margin: 0; - margin-block: 0; - margin-inline: 0; - padding-inline-start: 0; - } - } -`; diff --git a/src/client/view/components/_common/DefaultContainer.tsx b/src/client/view/components/_common/DefaultContainer.tsx new file mode 100644 index 0000000..b959bac --- /dev/null +++ b/src/client/view/components/_common/DefaultContainer.tsx @@ -0,0 +1,15 @@ +import { cx } from '@linaria/core'; +import { globalStyle } from '../../styles/globalStyles'; +import React, { ReactNode } from 'react'; +import { EnvContextProvider } from '../context/EnvContext'; + +type Props = { + children: ReactNode; + assetPath: string; +}; + +export const DefaultContainer = ({ assetPath, children }: Props) => ( +
+ {children} +
+); diff --git a/src/client/view/components/_common/Icon.tsx b/src/client/view/components/_common/Icon.tsx index 38172eb..e74e969 100644 --- a/src/client/view/components/_common/Icon.tsx +++ b/src/client/view/components/_common/Icon.tsx @@ -3,20 +3,27 @@ import { EnvContext } from '../context/EnvContext'; import { LinariaClassName } from '@linaria/core'; export type IconProps = { - className: LinariaClassName; + className?: LinariaClassName; alt: string; iconName: IconName; + size?: number; }; -type IconName = 'check'; +const IconNameList = ['check', 'warning', 'hourglass'] as const; -export const Icon = ({ className, alt, iconName }: IconProps) => { +export type IconName = (typeof IconNameList)[number]; + +export const Icon = ({ className, alt, iconName, size }: IconProps) => { const { assetPath } = useContext(EnvContext); return ( {alt} ); }; diff --git a/src/client/view/components/template/WarningTemplate.tsx b/src/client/view/components/template/WarningTemplate.tsx new file mode 100644 index 0000000..60f9909 --- /dev/null +++ b/src/client/view/components/template/WarningTemplate.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Icon, IconName } from '../_common/Icon'; +import { css } from '@linaria/core'; + +type Props = { + iconName: IconName; + title: string; + description?: string; + iconSize?: number; +}; + +export const WarningTemplate = ({ + title, + description, + iconName, + iconSize, +}: Props) => ( +
+
+ +
+

{title}

+

{description}

+
+); + +const container = css` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +const iconWrapper = css` + height: 80px; + width: 80px; + margin-bottom: 10px; + display: flex; + justify-content: center; + align-items: center; +`; + +const titleStyle = css` + color: var(--color-600); + font-size: 20px; + font-weight: 600; + letter-spacing: -0.4px; + text-align: center; +`; + +const descriptionStyle = css` + margin-top: 6px; + color: var(--color-400); + font-size: 12px; + font-weight: 500; + letter-spacing: -0.24px; +`; diff --git a/src/client/view/pages/404/index.tsx b/src/client/view/pages/404/index.tsx new file mode 100644 index 0000000..822fbca --- /dev/null +++ b/src/client/view/pages/404/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { WarningTemplate } from '../../components/template/WarningTemplate'; +import { DefaultContainer } from '../../components/_common/DefaultContainer'; +import { css } from '@linaria/core'; + +export const NotFoundPage = ({ assetPath }: { assetPath: string }) => ( + +
+ +
+
+); + +const container = css` + height: 100dvh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; diff --git a/src/client/view/pages/expired/index.tsx b/src/client/view/pages/expired/index.tsx new file mode 100644 index 0000000..f5b96b5 --- /dev/null +++ b/src/client/view/pages/expired/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { WarningTemplate } from '../../components/template/WarningTemplate'; +import { DefaultContainer } from '../../components/_common/DefaultContainer'; +import { css } from '@linaria/core'; + +export const ExpiredPage = ({ assetPath }: { assetPath: string }) => ( + +
+ +
+
+); + +const container = css` + height: 100dvh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; diff --git a/src/client/view/styles/globalStyles.ts b/src/client/view/styles/globalStyles.ts new file mode 100644 index 0000000..ee80b91 --- /dev/null +++ b/src/client/view/styles/globalStyles.ts @@ -0,0 +1,60 @@ +import { css } from '@linaria/core'; +import { theme } from '../theme'; + +export const globalStyle = css` + font-family: + 'Pretendard Variable', + Pretendard, + -apple-system, + BlinkMacSystemFont, + system-ui, + Roboto, + 'Helvetica Neue', + 'Segoe UI', + 'Apple SD Gothic Neo', + 'Noto Sans KR', + 'Malgun Gothic', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + sans-serif; + + background-color: var(--color-50); + + ${theme}; + + :global(html) { + html { + box-sizing: border-box; + } + + body { + margin: 0; + } + + *, + *::before, + *::after { + box-sizing: inherit; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p { + margin: 0; + } + + ul { + list-style: none; + padding: 0; + margin: 0; + margin-block: 0; + margin-inline: 0; + padding-inline-start: 0; + } + } +`; diff --git a/src/index.ts b/src/index.ts index 2e094af..ba9ae2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import koa from 'koa'; import serve from 'koa-static'; import { configDotenv } from 'dotenv'; -import viewRouter from './server/route/viewRoute'; +import viewRouter, { fallback } from './server/route/viewRoute'; configDotenv(); @@ -13,6 +13,8 @@ const app = new koa(); app.use(viewRouter.routes()).use(viewRouter.allowedMethods()); app.use(serve(__dirname + '/public')); +app.use(fallback); + app.listen(PORT, () => { console.log(`Server is running on http://${HOST}:${PORT}`); console.log(`Quick link -> http://${HOST}:${PORT}/share/Ha63U`); diff --git a/src/server/route/viewRoute.ts b/src/server/route/viewRoute.ts index 1ec81a2..ea0a9f3 100644 --- a/src/server/route/viewRoute.ts +++ b/src/server/route/viewRoute.ts @@ -2,8 +2,10 @@ import Router from '@koa/router'; import { initApiClient } from '../../agent/ApiClient'; import { getSharedBandalartDetailByKey } from '../../agent/shares/getSharedBandalartDetailByKey'; import { getSharedBandalartCells } from '../../agent/shares/getSharedBandalartCells'; -import { renderer } from '../../client/renderer'; +import { renderer, renderExpired, renderNotFound } from '../../client/renderer'; import { createStore } from '../../client/stores/createStore'; +import { Context } from 'koa'; +import axios from 'axios'; const viewRouter = new Router(); @@ -22,6 +24,16 @@ viewRouter.get('/share/:key', async (ctx) => { }); } catch (e) { console.error(e); + if (axios.isAxiosError(e) && e.response) { + switch (e.response.status) { + case 400: + ctx.response.body = renderExpired(process.env.ASSET_PATH ?? ''); + break; + case 404: + ctx.response.body = renderNotFound(process.env.ASSET_PATH ?? ''); + break; + } + } } }); @@ -30,3 +42,6 @@ viewRouter.get('/health', (ctx) => { }); export default viewRouter; +export const fallback = (ctx: Context) => { + ctx.response.body = renderNotFound(process.env.ASSET_PATH ?? ''); +};