Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 로그인 기능 구현 #203

Merged
merged 21 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c0eb17e
chore: react-router-dom 라이브러리 설정 및 셋팅 (#193)
turtle601 Jul 28, 2023
c259975
chore: naver, kakao 아이콘 셋팅 (#193)
turtle601 Jul 28, 2023
cc8fc40
feat: api 관련 상수 선언 (#193)
turtle601 Jul 28, 2023
c4190d9
feat: 네이버, 카카오 로그인 버튼 구현 (#193)
turtle601 Jul 28, 2023
97445a3
feat: Oauth 기능 구현 (#193)
turtle601 Jul 28, 2023
3e34ca9
feat: 구글 로그인 버튼 ui 구현 (#193)
turtle601 Jul 29, 2023
38b01c3
feat: 내 정보 아이콘 ui 구현 (#193)
turtle601 Jul 29, 2023
c387492
feat: InfoDropDown컴포넌트 구현 (#193)
turtle601 Jul 29, 2023
59187a6
feat: Modal 컴포넌트 구현 (#193)
turtle601 Jul 29, 2023
eee7e13
feat: LoginModalContnet 컴포넌트 구현 (#193)
turtle601 Jul 29, 2023
941cc82
feat: Header에 InfoButton 컴포넌트 적용 및 기능 구현 (#193)
turtle601 Jul 29, 2023
46ebf24
refactor: Modal 컴포넌트 가운데에 정렬이 되도록 수정 (#193)
turtle601 Jul 29, 2023
929cc2e
refactor: useBooleanstate 훅 활용 (#193)
turtle601 Jul 29, 2023
5f60d9e
refactor: 불필요한 파일 삭제 (#193)
turtle601 Jul 29, 2023
9213391
chore: .vscode 파일 수정 (#193)
turtle601 Jul 29, 2023
e6ee4c4
Merge branch '193-feat-카카오-로그인-기능-구현' of https://github.com/woowacour…
turtle601 Jul 29, 2023
f1a6b2d
refactor: Oauth 타입 분리 및 적용 (#193)
turtle601 Jul 31, 2023
b704ab9
refactor: DropDown 리스트 box-shadow 적용 (#193)
turtle601 Jul 31, 2023
1b7a93d
refactor: box shadow 변수 사용 (#193)
turtle601 Jul 31, 2023
286583d
Squashed commit of the following:
turtle601 Jul 31, 2023
4746b1d
refactor: BASE_URL 값 수정 (#193)
turtle601 Jul 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",

"stylelint.enable": true,

"stylelint.config": null,
"stylelint.validate": ["css", "scss", "typescript", "typescriptreact"]
}
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"main": "index.tsx",
"license": "MIT",
"scripts": {
"start": "webpack serve --open --mode development",
"start": "webpack serve --open --mode development --port 3000",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"start:prod": "webpack serve --open --mode production",
"build": "webpack --mode production",
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
Expand Down Expand Up @@ -32,6 +32,7 @@
"msw": "^1.2.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2",
"storybook": "^7.0.25",
"styled-components": "^6.0.2",
"ts-loader": "^9.4.4",
Expand Down
1 change: 1 addition & 0 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ <h2>경고!</h2>
<div>현재 사용 중인 브라우저는 스크립트를 지원하지 않거나, 해당 기능이 활성화 되어 있지 않습니다.</div>
</noscript>
<div id="root"></div>
<div id="modal"></div>
</body>
</html>
15 changes: 13 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import MainPage from './pages/MainPage';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import OauthRedirectPage from '~/pages/OauthRedirectPage';
import MainPage from '~/pages/MainPage';

function App() {
return <MainPage />;
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<MainPage />} />
<Route path="/oauth/redirect/kakao" element={<OauthRedirectPage type="kakao" />} />
<Route path="/oauth/redirect/naver" element={<OauthRedirectPage type="naver" />} />
<Route path="/oauth/redirect/google" element={<OauthRedirectPage type="google" />} />
</Routes>
</BrowserRouter>
);
}

export default App;
3 changes: 3 additions & 0 deletions frontend/src/assets/icons/etc/menu.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/assets/icons/etc/user.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/assets/icons/oauth/google.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions frontend/src/assets/icons/oauth/kakao.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions frontend/src/assets/icons/oauth/naver.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 31 additions & 4 deletions frontend/src/components/@common/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
import React from 'react';
import { styled } from 'styled-components';
import Logo from '~/assets/logo.png';
import { Modal, ModalContent } from '~/components/@common/Modal';
import InfoDropDown from '~/components/InfoDropDown';
import LoginModalContent from '~/components/LoginModalContent';
import useBooleanState from '~/hooks/useBooleanState';

const options = [
{ id: 1, value: '로그인' },
{ id: 2, value: '회원가입' },
];

function Header() {
const { value: isModalOpen, setTrue: openModal, setFalse: closeModal } = useBooleanState(false);

const handleInfoDropDown = (event: React.MouseEvent<HTMLElement>) => {
const currentOption = event.currentTarget.dataset.name;

if (currentOption === '로그인') openModal();
};

return (
<StyledHeader>
<StyledLogo alt="셀럽잇 로고" src={Logo} />
</StyledHeader>
<>
<StyledHeader>
<StyledLogo alt="셀럽잇 로고" src={Logo} />
<InfoDropDown options={options} externalOnClick={handleInfoDropDown} isOpen={isModalOpen} />
</StyledHeader>
<Modal>
<ModalContent isShow={isModalOpen} title="로그인 및 회원 가입" closeModal={closeModal}>
<LoginModalContent />
</ModalContent>
</Modal>
</>
);
}

export default Header;

const StyledHeader = styled.header`
display: flex;
justify-content: space-between;
align-items: center;

position: sticky;
top: 0;
z-index: 10;
z-index: 20;
Comment on lines -20 to +47
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z-index를 올려줘야만 하는 일이 있었나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넹 ㅋㅋ Navbar의 sticky랑 Header의 sticky가 중복되서 로그인 dropdown이 가려지는 현상이 발생하더라구요
그래서 수정했습니다!!


width: 100%;
height: 80px;
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/components/@common/InfoButton/InfoButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Meta, StoryObj } from '@storybook/react';
import InfoButton from './InfoButton';

const meta: Meta<typeof InfoButton> = {
title: 'InfoButton',
component: InfoButton,
};

export default meta;

type Story = StoryObj<typeof InfoButton>;

export const Default: Story = {
args: {},
};
47 changes: 47 additions & 0 deletions frontend/src/components/@common/InfoButton/InfoButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import styled, { css } from 'styled-components';

import Menu from '~/assets/icons/etc/menu.svg';
import User from '~/assets/icons/etc/user.svg';

interface InfoButtonProps {
isShow?: boolean;
}

function InfoButton({ isShow = false }: InfoButtonProps) {
return (
<StyledInfoButton isShow={isShow}>
<Menu />
<User />
</StyledInfoButton>
Comment on lines +12 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 각각의 아이콘이 한 버튼으로 동작하게 해 놓은 걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아뇨 디자인 시안에서 보면 아이콘에 관계없이 아이콘을 감싸는 버튼을 클릭 시 dropdown이 동작하게 되어있습니다.
그래서 그런 식으로 구현했어요!!

);
}

export default InfoButton;

const StyledInfoButton = styled.button<InfoButtonProps>`
display: flex;
justify-content: space-between;
align-items: center;

width: 77px;

padding: 0.5rem 0.5rem 0.5rem 1.2rem;

border: 1px solid #ddd;
border-radius: 21px;
background: transparent;

cursor: pointer;

${({ isShow }) =>
isShow &&
css`
box-shadow: 0 1px 2px rgb(0 0 0 / 15%);
`}

&:hover {
box-shadow: 0 1px 2px rgb(0 0 0 / 15%);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common에 등록되어있는 shaow 속성을 사용하면 좋겠어요!


transition: box-shadow 0.2s ease-in-out;
}
`;
3 changes: 3 additions & 0 deletions frontend/src/components/@common/InfoButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import InfoButton from '~/components/@common/InfoButton/InfoButton';

export default InfoButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import LoginButton from './LoginButton';

const meta: Meta<typeof LoginButton> = {
title: 'Oauth/LoginButton',
component: LoginButton,
decorators: [
Story => (
<BrowserRouter>
<Routes>
<Route path="/*" element={<Story />} />
</Routes>
</BrowserRouter>
),
],
};

export default meta;

type Story = StoryObj<typeof LoginButton>;

export const Google: Story = {
args: { type: 'google' },
};

export const KaKao: Story = {
args: { type: 'kakao' },
};

export const Naver: Story = {
args: { type: 'naver' },
};
73 changes: 73 additions & 0 deletions frontend/src/components/@common/LoginButton/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Link } from 'react-router-dom';
import styled, { css } from 'styled-components';
import React from 'react';
import { OAUTH_BUTTON_MESSAGE, OAUTH_LINK } from '~/constants/api';

import KaKao from '~/assets/icons/oauth/kakao.svg';
import Naver from '~/assets/icons/oauth/naver.svg';
import Google from '~/assets/icons/oauth/google.svg';

interface LoginButtonProps {
type: 'google' | 'kakao' | 'naver';
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다음과 같은 타입은 oauth.types.ts 로 빼줘도 좋을 것 같네요~!
물론 여기에서는 Props로 쓰였지만요..!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋아요!!

type Domain = 'google' | 'kakao' | 'naver';

으로 분리하면 좋을 거 같네요


const LoginIcon: Record<string, React.ReactNode> = {
naver: <Naver />,
kakao: <KaKao />,
google: <Google />,
};

function LoginButton({ type }: LoginButtonProps) {
return (
<StyledLoginButtonWrapper type={type} to={OAUTH_LINK[type]} target="_blank">
<div>{LoginIcon[type]}</div>
<StyledLoginButtonText>{OAUTH_BUTTON_MESSAGE[type]}</StyledLoginButtonText>
</StyledLoginButtonWrapper>
);
}

export default LoginButton;

const StyledLoginButtonWrapper = styled(Link)<LoginButtonProps>`
display: flex;

width: 100%;
height: fit-content;

padding: 2.3rem 1.3rem;

border-radius: 12px;

font-size: 1.4rem;
font-weight: 600;
text-decoration: none;

${({ type }) =>
type === 'naver' &&
css`
background: #03c759;

color: #fff;
`}

${({ type }) =>
type === 'kakao' &&
css`
background: #fee500;
`}

${({ type }) =>
type === 'google' &&
css`
border: 1px solid var(--gray-3);
`}

cursor: pointer;
transition: box-shadow 0.2s cubic-bezier(0.2, 0, 0, 1), transform 0.1s cubic-bezier(0.2, 0, 0, 1);
`;

const StyledLoginButtonText = styled.span`
margin: 0 auto;

color: inherit;
`;
3 changes: 3 additions & 0 deletions frontend/src/components/@common/LoginButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import LoginButton from '~/components/@common/LoginButton/LoginButton';

export default LoginButton;
11 changes: 11 additions & 0 deletions frontend/src/components/@common/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createPortal } from 'react-dom';

interface ModalProps {
children: React.ReactNode;
}

function Modal({ children }: ModalProps) {
return createPortal(children, document.querySelector('#modal'));
}

export default Modal;
20 changes: 20 additions & 0 deletions frontend/src/components/@common/Modal/ModalContent.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react';
import ModalContent from './ModalContent';

const meta: Meta<typeof ModalContent> = {
title: 'ModalContent',
component: ModalContent,
};

export default meta;

type Story = StoryObj<typeof ModalContent>;

export const LoginModal: Story = {
args: {
isShow: true,
title: '로그인 또는 회원가입',
closeModal: () => {},
children: '모달 내용',
},
};
Loading
Loading