Skip to content

Commit

Permalink
feat: add api base code (incomplete)
Browse files Browse the repository at this point in the history
  • Loading branch information
aube-dev committed Sep 14, 2024
1 parent 0030e90 commit 53ed9a1
Show file tree
Hide file tree
Showing 33 changed files with 931 additions and 452 deletions.
50 changes: 50 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ module.exports = {
],
format: ['PascalCase'],
},
{
selector: 'typeParameter',
format: ['PascalCase'],
prefix: ['_'],
filter: '^_',
},
{
selector: ['enumMember'],
format: ['UPPER_CASE'],
Expand All @@ -117,6 +123,12 @@ module.exports = {
selector: ['objectLiteralProperty'],
format: null,
},
{
selector: 'variable',
format: ['camelCase'],
prefix: ['api_', 'unstable_'],
filter: '(^api_|^unstable_)',
},
],
},
},
Expand All @@ -128,6 +140,24 @@ module.exports = {
node: true,
},
},

{
files: ['./server/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
// https://github.com/vitejs/vite/issues/10063
group: ['@/*', '@server/*', 'app/*', 'server/*'],
message: 'server 디렉토리 안에서는 상대 경로를 사용해 주세요.',
},
],
},
],
},
},
],

rules: {
Expand All @@ -142,5 +172,25 @@ module.exports = {
],
'arrow-body-style': ['warn', 'as-needed'],
'prettier/prettier': 'warn',
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['..*'],
message: '다른 경로에 있는 모듈은 절대 경로로 불러와 주세요.',
},
{
group: ['app/*', 'server/*'],
message: '`@/`, `@server` 등 올바른 절대 경로를 사용해 주세요.',
},
{
group: ['@remix-run/react'],
importNames: ['useFetcher'],
message: '`@/hooks/useTypedFetcher`를 사용해 주세요.',
},
],
},
],
},
};
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createRemixStub } from '@remix-run/testing';
import type { Preview } from '@storybook/react';
import '../app/root.css';
import '@/root.css';

const preview: Preview = {
parameters: {
Expand Down
22 changes: 22 additions & 0 deletions app/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Api } from '@server';

export const api_loginWithKakao = new Api<
{ code: string; state: string },
{
userId: number;
accessToken: string;
accessTokenExpiresAt: string;
refreshToken: string;
refreshTokenExpiresAt: string;
}
>({
method: 'POST',
endpoint: '/login/oauth2/code/kakao',
needToLogin: false,
request: (variables) => ({
queryParams: {
code: variables.code,
state: variables.state,
},
}),
});
10 changes: 5 additions & 5 deletions app/components/KakaoLoginButton/KakaoLoginButton.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import React, { useEffect, useState } from 'react';
import useEnv from '@/hooks/useEnv';
import { getUUID } from '@/utils/random';

const KakaoLoginButton: React.FC = () => {
const env = useEnv();
const [kakaoAuthUrl, setKakaoAuthUrl] = useState('');

useEffect(() => {
const userUUID = getUUID();
const redirectUri = window.location.origin + '/oauth/kakao';
const redirectUri =
window.location.origin.replace('localhost', '127.0.0.1') + '/oauth/kakao';
setKakaoAuthUrl(
`https://kauth.kakao.com/oauth/authorize?client_id=${env.KAKAO_CLIENT_ID}&redirect_uri=${redirectUri}&response_type=code&state=${userUUID}`,
`${env.API_URL}/oauth2/authorization/kakao?redirect_to=${redirectUri}`,
);
}, [env.KAKAO_CLIENT_ID]);
// setKakaoAuthUrl('http://146.56.161.252:8080/oauth2/authorization/kakao');
}, [env.API_URL]);

return (
<a href={kakaoAuthUrl}>
Expand Down
2 changes: 1 addition & 1 deletion app/contexts/EnvContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext } from 'react';
import { type ClientEnv } from '@server/constants/env';
import { type ClientEnv } from '@server';

export const EnvContext = createContext<ClientEnv | null>(null);
2 changes: 1 addition & 1 deletion app/hooks/useEnv.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from 'react';
import { EnvContext } from '@/contexts/EnvContext';
import { type ClientEnv, clientEnvSchema } from '@server/constants/env';
import { type ClientEnv, clientEnvSchema } from '@server';

/**
* 클라이언트용 환경 변수를 얻는 hook
Expand Down
16 changes: 16 additions & 0 deletions app/hooks/useErrorToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { create } from 'zustand';
import { type FrontendErrorResponse } from '@server';

interface ErrorToastStore {
error: FrontendErrorResponse<unknown> | null;
setError: (newError: FrontendErrorResponse<unknown>) => void;
clearError: () => void;
}

const useErrorToast = create<ErrorToastStore>()((set) => ({
error: null,
setError: (newError) => set({ error: newError }),
clearError: () => set({ error: null }),
}));

export default useErrorToast;
37 changes: 37 additions & 0 deletions app/hooks/useTypedFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type TypedResponse } from '@remix-run/cloudflare';
// `useTypedFetcher`를 사용하라는 규칙인데 이 파일이 바로 그 구현체이므로 무시
// eslint-disable-next-line no-restricted-imports
import { useFetcher } from '@remix-run/react';
import { useEffect } from 'react';
import useErrorToast from './useErrorToast';
import {
type FrontendErrorResponse,
type FrontendSuccessResponse,
type JsonValue,
} from '@server';

const useTypedFetcher = <
T extends (
...params: unknown[]
) => Promise<
| TypedResponse<FrontendSuccessResponse<JsonValue>>
| TypedResponse<FrontendErrorResponse<JsonValue>>
>,
>(
...params: Parameters<typeof useFetcher<T>>
) => {
const fetcher = useFetcher<T>(...params);
const { setError } = useErrorToast();

useEffect(() => {
if (fetcher.data !== undefined && !fetcher.data.isSuccess) {
setError(fetcher.data);
}
// `fetcher.data` 외 다른 것이 변할 때는 실행되면 안 됨
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher.data]);

return fetcher;
};

export default useTypedFetcher;
60 changes: 59 additions & 1 deletion app/root.css.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { globalFontFace, globalStyle } from '@vanilla-extract/css';
import { globalFontFace, globalStyle, style } from '@vanilla-extract/css';
import { textStyle } from './styles/text.css';
import { themeVars } from './styles/theme.css';
import { rgba } from './utils/style';
import SUITVariable from '@/assets/SUIT-Variable.woff2';

globalFontFace('SUIT-Variable', {
Expand All @@ -25,3 +28,58 @@ globalStyle('body', {
globalStyle('h1, h2, h3, h4, h5, h6, p', {
margin: 0,
});

export const loadingToast = style({
position: 'fixed',
bottom: '32px',
left: '50%',
transform: 'translate(-50%, 0)',
backgroundColor: '#121212',
borderRadius: '12px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingRight: '12px',
gap: '10px',
width: '180px',
height: '50px',
});

export const loadingLottie = style({
width: '43px',
height: '43px',
});

export const loadingToastText = style([
textStyle.subtitle2SB,
{ color: themeVars.color.grayscale.white.hex },
]);

export const errorToastWrap = style({
position: 'fixed',
bottom: '32px',
left: '50%',
transform: 'translate(-50%, 0)',
padding: '0 24px',
width: '100%',
maxWidth: '648px',
});

export const errorToast = style({
backgroundColor: rgba(themeVars.color.system.caution.rgb, 0.7),
borderRadius: '12px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '12px 24px',
});

export const errorToastText = style([
textStyle.subtitle2SB,
{
color: themeVars.color.grayscale.white.hex,
textAlign: 'center',
wordBreak: 'keep-all',
},
]);
47 changes: 46 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useFetchers,
useLoaderData,
useRouteError,
} from '@remix-run/react';
import { useEffect, useRef } from 'react';
import useErrorToast from './hooks/useErrorToast';
import * as styles from './root.css';
import { EnvContext } from '@/contexts/EnvContext';

import './root.css';
import '@/styles/theme.css';

export const loader = async (args: LoaderFunctionArgs) =>
Expand All @@ -19,6 +24,24 @@ export const loader = async (args: LoaderFunctionArgs) =>

const App = () => {
const data = useLoaderData<typeof loader>();
const fetchers = useFetchers();
const { error, clearError } = useErrorToast();
const errorToastRemovalTimeoutIdRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
if (error) {
if (errorToastRemovalTimeoutIdRef.current) {
clearTimeout(errorToastRemovalTimeoutIdRef.current);
}
const timeoutId = setTimeout(() => {
clearError();
}, 5000);
errorToastRemovalTimeoutIdRef.current = timeoutId;
}

// `error` 외 다른 것이 바뀔 때는 호출되지 않아야 함
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);

return (
<html lang="ko">
Expand All @@ -31,6 +54,21 @@ const App = () => {
<body>
<EnvContext.Provider value={data.env}>
<Outlet />
{fetchers.length > 0 && (
<div className={styles.loadingToast}>
<div className={styles.loadingLottie}>
<DotLottieReact src="/loading-lottie.json" loop autoplay />
</div>
<span className={styles.loadingToastText}>서버 통신 중...</span>
</div>
)}
{error && (
<div className={styles.errorToastWrap}>
<div className={styles.errorToast}>
<span className={styles.errorToastText}>{error.message}</span>
</div>
</div>
)}
</EnvContext.Provider>
<ScrollRestoration />
<Scripts />
Expand All @@ -39,4 +77,11 @@ const App = () => {
);
};

export const ErrorBoundary = () => {
const error = useRouteError();
console.log('ErrorBoundary', error);

return <p>Error</p>;
};

export default App;
20 changes: 14 additions & 6 deletions app/routes/oauth.kakao.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { type LoaderFunctionArgs } from '@remix-run/cloudflare';

export const loader = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');

console.log(code, state);
const cookieHeader = request.headers;
console.log(cookieHeader);

return null;
};

const KakaoRedirect = () => <div>카카오 로그인 중...</div>;
const KakaoRedirect = () => {
console.log('kakaoredirect');

return (
<div>
{/* <p className={textStyle.headline1B}>{fetcher.state}</p>
<fetcher.Form method="POST">
<button type="submit">test submit</button>
</fetcher.Form> */}
</div>
);
};

export default KakaoRedirect;
7 changes: 4 additions & 3 deletions functions/[[path]].ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import {
clientEnvSchema,
type ContextEnv,
serverEnvSchema,
} from '@server/constants/env';
import { getLoadContext, makeAuthSession } from '@server/utils/cloudflare';
getLoadContext,
makeAuthSession,
} from '@server';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - the server build file is generated by `remix vite:build`
// eslint-disable-next-line import/no-unresolved, import/order
// eslint-disable-next-line import/no-unresolved, import/order, no-restricted-imports
import * as build from '../build/server';

export const onRequest = async (
Expand Down
Loading

0 comments on commit 53ed9a1

Please sign in to comment.