From 33942ba9fc35e4621bc11fc4a04b7e940c9a29e3 Mon Sep 17 00:00:00 2001 From: fuse <76121068+silvertae@users.noreply.github.com> Date: Mon, 14 Aug 2023 00:01:38 +0900 Subject: [PATCH] =?UTF-8?q?[Team#05]=20[FE]=203=EC=A3=BC=EC=B0=A8=20PR=20(?= =?UTF-8?q?#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #chore: 불필요한 주석 제거 * #26 feat: Context와 useAuth 커스텀 훅을 이용한 로그인 라우팅 기능 구현 * #26 chore: handleLogin 함수 간단히 수정 * #26 chore: axios 설치 및 인스턴스 생성 * #26 feat: useRefreshToken 커스텀 훅 추가 * #26 feat: useAxiosPrivate 훅 추가 및 로그아웃 기능 추가 * #26 feat: signup 기능 추가 * #26 chore: 간단한 코드 수정 * #26 chore: 안 쓰는 import 삭제 * #26 feat: TextInput 컴포넌트에 type prop 추가 - 비밀번호를 입력받는 경우에는 type="password"로 설정해야 하므로 TextInput 컴포넌트에 type prop을 추가 * #26 feat: 로그인 시 userName을 응답으로 받아오도록 추가하고 관련 코드 수정 * #26 fix: CORS 문제 해결중 * #26 chore: axios baseURL 원래대로 수정 * #26 feat: Context와 useAuth 커스텀 훅을 이용한 로그인 라우팅 기능 구현 * #26 chore: handleLogin 함수 간단히 수정 * #26 chore: axios 설치 및 인스턴스 생성 * #26 feat: useRefreshToken 커스텀 훅 추가 * #26 feat: useAxiosPrivate 훅 추가 및 로그아웃 기능 추가 * #26 feat: signup 기능 추가 * #26 chore: 간단한 코드 수정 * #26 chore: 안 쓰는 import 삭제 * #26 feat: TextInput 컴포넌트에 type prop 추가 - 비밀번호를 입력받는 경우에는 type="password"로 설정해야 하므로 TextInput 컴포넌트에 type prop을 추가 * #26 feat: 로그인 시 userName을 응답으로 받아오도록 추가하고 관련 코드 수정 * #26 fix: CORS 문제 해결중 * #26 chore: axios baseURL 원래대로 수정 * #26 fix: vite cofig 파일에 proxy 설정 추가해서 CORS 오류 해결 * #26 fix: api 명세서에 맞게 바뀐 부분 수정 * #26 feat: 바뀐 로그아웃 api 명세에 맞게 요청과 AuthContext 수정 - 더 이상 AuthContext에 유저 비밀번호를 저장하지 않음 - 더 이상 로그아웃 요청에 유저 이메일과 비밀번호를 보내지 않음 * #26 feat: 로그인 페이지 이메일, 비밀번호 입력 인풋에 대한 유효성 검사 추가 - 로그인 버튼 활성화, 비활성화 기능 추가 필요 * #26 feat: GitHub OAuth 로그인 기능 구현 및 Callback 페이지 생성 * #chore: MSW 서비스워커 설정 수정 * #49 feat: Options 페이지 레이아웃 작업 시작 및 tab 레이아웃 구성 * #49 chore 구조수정 * #53 refactor: CommentElements 컴포넌트에서 리뷰어 피드백 반영 * #26 feat: 버튼 컴포넌트에서 ghost 버튼일 때 selected 스타일 추가 - ghost 버튼일 때 selected 스타일 추가 - selected를 props를 받으면 글자랑 아이콘색, 폰트 굵기 변경 * #50 refactor: 기존 TextInput 컴포넌트 props, state 수정 * #50 feat: 아이디, 비밀번호 검증 기능 추가 - Login 페이지 컴포넌트에서 input 값을 상태로 관리하도록 변경 - 적합한 형식의 아이디와 비밀번호가 입력되어야 버튼이 활성화되는 기능 추가 * chore: labels, milestones 이름, url 수정 * feat: issue 목록 레이아웃 * #chore: 배포시 필요한 환경변수 파일을 위해서 .gitignore 수정 * feat: options 내부 labels 컴포넌트 작업 및 자잘한 수정 * feat: 라벨 페이지에서 이슈목록을 조회하는 get 요청에 대한 mockData 설정 추가 * refactor: useAxiosPrivate 조건 수정 및 Options 페이지에서 활용하도록 변경 * chore: components 디렉토리 구조 변경 * chore: .env 파일 gitignore에 다시 추가 * chore: 이름수정 * refactor: ColorCodeInput 내부 구조 수정 defaultValue -> value로 변경 * hotfix: 버튼 컴포넌트 누락된 selected 스타일 추가 --------- Co-authored-by: Cat Hanbit --- frontend/.gitignore | 2 - frontend/package-lock.json | 27 +++- frontend/package.json | 4 +- frontend/src/App.tsx | 9 +- frontend/src/asset/icons/check_box/active.svg | 4 + .../src/asset/icons/check_box/disable.svg | 4 + .../src/asset/icons/check_box/initial.svg | 3 + frontend/src/asset/icons/label.svg | 3 + frontend/src/asset/icons/milestone.svg | 3 + frontend/src/asset/icons/trash.svg | 6 + frontend/src/components/BaseButton.tsx | 110 ---------------- frontend/src/components/ButtonLarge.tsx | 14 -- frontend/src/components/ButtonSmall.tsx | 9 -- frontend/src/components/Layout.tsx | 11 ++ .../src/components/common/ColorCodeInput.tsx | 35 ++--- .../src/components/common/CommentElements.tsx | 11 +- .../src/components/common/DropdownPanel.tsx | 8 +- .../src/components/common/InformationTag.tsx | 2 +- frontend/src/components/common/TabButton.tsx | 92 ++++++------- frontend/src/components/common/TextInput.tsx | 43 +++--- .../components/common/button/BaseButton.tsx | 44 +++++-- .../components/common/button/ButtonLarge.tsx | 9 +- .../components/common/button/ButtonSmall.tsx | 9 +- frontend/src/components/issues/IssueTable.tsx | 124 ++++++++++++++++++ frontend/src/components/label/Label.tsx | 25 ++++ frontend/src/components/label/Labels.tsx | 103 +++++++++++++++ frontend/src/components/landmark/Header.tsx | 19 +++ frontend/src/components/landmark/Main.tsx | 14 ++ frontend/src/components/landmark/Toolbar.tsx | 12 ++ frontend/src/constant/CheckBox.tsx | 7 + frontend/src/context/AuthProvider.tsx | 2 +- frontend/src/design/Icons.ts | 19 +++ frontend/src/hooks/useAxiosPrivate.ts | 5 +- frontend/src/hooks/useRefreshToken.ts | 7 +- frontend/src/main.tsx | 2 +- frontend/src/mocks/handlers.ts | 58 ++++++-- frontend/src/pages/Components.tsx | 37 +++--- frontend/src/pages/GitHubCallback.tsx | 76 +++++++++++ frontend/src/pages/Issues.tsx | 63 +++++++++ frontend/src/pages/Login.tsx | 59 +++++++-- frontend/src/pages/Main.tsx | 30 +++-- frontend/src/pages/Options.tsx | 87 ++++++++++++ frontend/vite.config.ts | 9 ++ 43 files changed, 917 insertions(+), 303 deletions(-) create mode 100644 frontend/src/asset/icons/check_box/active.svg create mode 100644 frontend/src/asset/icons/check_box/disable.svg create mode 100644 frontend/src/asset/icons/check_box/initial.svg create mode 100644 frontend/src/asset/icons/label.svg create mode 100644 frontend/src/asset/icons/milestone.svg create mode 100644 frontend/src/asset/icons/trash.svg delete mode 100644 frontend/src/components/BaseButton.tsx delete mode 100644 frontend/src/components/ButtonLarge.tsx delete mode 100644 frontend/src/components/ButtonSmall.tsx create mode 100644 frontend/src/components/Layout.tsx create mode 100644 frontend/src/components/issues/IssueTable.tsx create mode 100644 frontend/src/components/label/Label.tsx create mode 100644 frontend/src/components/label/Labels.tsx create mode 100644 frontend/src/components/landmark/Header.tsx create mode 100644 frontend/src/components/landmark/Main.tsx create mode 100644 frontend/src/components/landmark/Toolbar.tsx create mode 100644 frontend/src/constant/CheckBox.tsx create mode 100644 frontend/src/pages/GitHubCallback.tsx create mode 100644 frontend/src/pages/Issues.tsx create mode 100644 frontend/src/pages/Options.tsx diff --git a/frontend/.gitignore b/frontend/.gitignore index 50c8dda2a..a547bf36d 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,5 +22,3 @@ dist-ssr *.njsproj *.sln *.sw? - -.env diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 60792b6d5..115d17130 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,13 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@types/uuid": "^9.0.2", "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.2", - "styled-components": "^6.0.5" + "styled-components": "^6.0.5", + "uuid": "^9.0.0" }, "devDependencies": { "@types/react": "^18.2.15", @@ -2843,6 +2845,11 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" }, + "node_modules/@types/uuid": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", + "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", @@ -6465,6 +6472,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.6.tgz", @@ -8532,6 +8547,11 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" }, + "@types/uuid": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", + "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==" + }, "@typescript-eslint/eslint-plugin": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", @@ -11036,6 +11056,11 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, "vite": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index c8c047289..7e4f34a2c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,11 +10,13 @@ "preview": "vite preview" }, "dependencies": { + "@types/uuid": "^9.0.2", "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.2", - "styled-components": "^6.0.5" + "styled-components": "^6.0.5", + "uuid": "^9.0.0" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4c138524d..452bf500c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,7 +7,7 @@ import { Routes, Route } from 'react-router-dom'; import Components from './pages/Components'; import Login from './pages/Login'; import Register from './pages/Register'; -import Main from './pages/Main'; +import Callback from './pages/GitHubCallback'; import AddIssue from './pages/AddIssue'; import LogoDarkLarge from './asset/logo/logo_dark_large.svg'; @@ -16,6 +16,9 @@ import LogoLightLarge from './asset/logo/logo_light_large.svg'; import LogoLightMedium from './asset/logo/logo_light_medium.svg'; import { AppContext } from './main'; import RequireAuth from './routes/RequireAuth'; +import Options from './pages/Options'; +import Main from './pages/Main'; +import Issues from './pages/Issues'; function App() { const [isLight, setIsLight] = useState(true); @@ -39,10 +42,12 @@ function App() { {/* public routes */} + } /> } /> } /> } /> - + } /> + } /> {/* protected routes */} }> } /> diff --git a/frontend/src/asset/icons/check_box/active.svg b/frontend/src/asset/icons/check_box/active.svg new file mode 100644 index 000000000..089bb581b --- /dev/null +++ b/frontend/src/asset/icons/check_box/active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/asset/icons/check_box/disable.svg b/frontend/src/asset/icons/check_box/disable.svg new file mode 100644 index 000000000..d0d72aa8f --- /dev/null +++ b/frontend/src/asset/icons/check_box/disable.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/asset/icons/check_box/initial.svg b/frontend/src/asset/icons/check_box/initial.svg new file mode 100644 index 000000000..9a221dd91 --- /dev/null +++ b/frontend/src/asset/icons/check_box/initial.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/asset/icons/label.svg b/frontend/src/asset/icons/label.svg new file mode 100644 index 000000000..43ba5fac1 --- /dev/null +++ b/frontend/src/asset/icons/label.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/asset/icons/milestone.svg b/frontend/src/asset/icons/milestone.svg new file mode 100644 index 000000000..93652e7e5 --- /dev/null +++ b/frontend/src/asset/icons/milestone.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/asset/icons/trash.svg b/frontend/src/asset/icons/trash.svg new file mode 100644 index 000000000..4a1918d7e --- /dev/null +++ b/frontend/src/asset/icons/trash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/components/BaseButton.tsx b/frontend/src/components/BaseButton.tsx deleted file mode 100644 index 8ed438e5c..000000000 --- a/frontend/src/components/BaseButton.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { styled } from 'styled-components'; -import Icons from '../design/Icons'; - -type ButtonProps = React.HTMLAttributes & { - type: 'button' | 'submit' | 'reset'; - iconName?: keyof typeof Icons; - children?: React.ReactNode; - disabled?: boolean; - flexible?: boolean; - outline?: boolean; - ghost?: boolean; - onClick?: () => void; -}; - -export default function Button(props: ButtonProps) { - const { - type, - iconName, - children, - disabled, - flexible, - outline, - ghost, - ...rest - } = props; - const hasIcon = iconName !== undefined; - const Icon = Icons[iconName ?? 'default']; - - return ( - - {hasIcon && } - {children} - - ); -} - -type StyledButtonProps = { - $flexible?: boolean; - $outline?: boolean; - $ghost?: boolean; -}; - -const RealButton = styled.button` - width: ${({ $flexible }) => ($flexible ? 'auto' : '184px')}; - height: 48px; - padding: 0 24px; - display: inline-flex; - justify-content: center; - align-items: center; - border: none; - border-radius: ${({ theme }) => theme.objectStyles.radius.medium}; - background: ${({ theme }) => theme.color.brand.surface.default}; - color: ${({ theme }) => theme.color.brand.text.default}; - ${({ theme }) => theme.font.available.medium[16]} - cursor: pointer; - - svg { - fill: ${({ theme }) => theme.color.brand.text.default}; - stroke: ${({ theme }) => theme.color.brand.text.default}; - } - - &:hover { - opacity: ${({ theme }) => theme.objectStyles.opacity.hover}; - } - - &:active { - opacity: ${({ theme }) => theme.objectStyles.opacity.press}; - } - - &:disabled { - opacity: ${({ theme }) => theme.objectStyles.opacity.disabled}; - } - - ${({ theme, $outline }) => - $outline && - ` - background: transparent; - border: 1px solid ${theme.color.brand.border.default}; - color: ${theme.color.brand.text.weak}; - - svg { - fill: ${theme.color.brand.text.weak}; - stroke: ${theme.color.brand.text.weak}; - } - `} - - ${({ theme, $ghost }) => - $ghost && - ` - background: transparent; - border: none; - color: ${theme.color.neutral.text.default}; - - svg { - fill: ${theme.color.neutral.text.default}; - stroke: ${theme.color.neutral.text.default}; - } - `} -`; - -const TextLabel = styled.span` - padding: 0 8px; - text-align: center; -`; diff --git a/frontend/src/components/ButtonLarge.tsx b/frontend/src/components/ButtonLarge.tsx deleted file mode 100644 index f02eab744..000000000 --- a/frontend/src/components/ButtonLarge.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { styled } from 'styled-components'; -import Button from './BaseButton'; - -const ButtonLarge = styled(Button)` - width: ${({ flexible }) => (flexible ? 'auto' : '240px')}; - height: 56px; - - svg { - width: 24px; - height: 24px; - } -`; - -export default ButtonLarge; diff --git a/frontend/src/components/ButtonSmall.tsx b/frontend/src/components/ButtonSmall.tsx deleted file mode 100644 index a7e980402..000000000 --- a/frontend/src/components/ButtonSmall.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { styled } from 'styled-components'; -import Button from './BaseButton'; - -const ButtonSmall = styled(Button)` - width: ${({ flexible }) => (flexible ? 'auto' : '128px')}; - height: 40px; -`; - -export default ButtonSmall; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 000000000..ef0d4b110 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,11 @@ +import { styled } from 'styled-components'; + +export default function Layout({ children }: { children: React.ReactNode }) { + return {children}; +} + +const Container = styled.div` + padding: 0 80px; + max-width: 1280px; + margin: auto; +`; diff --git a/frontend/src/components/common/ColorCodeInput.tsx b/frontend/src/components/common/ColorCodeInput.tsx index 02463a8a6..ae309f485 100644 --- a/frontend/src/components/common/ColorCodeInput.tsx +++ b/frontend/src/components/common/ColorCodeInput.tsx @@ -4,26 +4,29 @@ import { useState } from 'react'; export default function ColorCodeInput({ label }: { label: string }) { const [colorCode, setColorCode] = useState(getRandomColor()); - const [key, setKey] = useState(Date.now()); + const [inputValue, setInputValue] = useState(colorCode); + const [isInvalid, setIsInvalid] = useState(false); const submitHandler = (e: React.FormEvent) => { e.preventDefault(); - const form = e.target as typeof e.target & { - 'color-code': { value: string }; - }; - const colorCode = form['color-code'].value; - if (isValidColorCode(colorCode)) { - setColorCode(colorCode); + if (isValidColorCode(inputValue)) { + setColorCode(inputValue); + } else { + setIsInvalid(true); } - } + }; - const refresh = () => { - setKey(Date.now()); - } + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + if (isInvalid && isValidColorCode(e.target.value)) { + setIsInvalid(false); + } + }; const refreshBtnHandler = () => { - setColorCode(getRandomColor()); - refresh(); + const newRandomColor = getRandomColor(); + setColorCode(newRandomColor); + setInputValue(newRandomColor); }; return ( @@ -31,14 +34,12 @@ export default function ColorCodeInput({ label }: { label: string }) { diff --git a/frontend/src/components/common/CommentElements.tsx b/frontend/src/components/common/CommentElements.tsx index 23b279fdd..e27b78292 100644 --- a/frontend/src/components/common/CommentElements.tsx +++ b/frontend/src/components/common/CommentElements.tsx @@ -36,9 +36,11 @@ export default function Comment(props: CommentProps) { }; const handleChange = (e: React.ChangeEvent) => { - if (isEdit) { - setTextValue(e.target.value); + if (!isEdit) { + return; } + setTextValue(e.target.value); + return; }; @@ -72,12 +74,13 @@ export default function Comment(props: CommentProps) { - {!isEdit && {textValue}} - {isEdit && ( + {isEdit ? ( + ) : ( + {textValue} )} {isEdit && ( diff --git a/frontend/src/components/common/DropdownPanel.tsx b/frontend/src/components/common/DropdownPanel.tsx index 3de6b02b8..c307e44cf 100644 --- a/frontend/src/components/common/DropdownPanel.tsx +++ b/frontend/src/components/common/DropdownPanel.tsx @@ -1,5 +1,6 @@ import { styled } from 'styled-components'; import Icons from '../../design/Icons'; +import { v4 as uuidV4 } from 'uuid'; enum Option { Available, @@ -17,15 +18,16 @@ export default function DropdownPanel({ label }: { label: string }) {
{label}
{dummy.map(([text, option]) => { + const key = uuidV4() const Icon = Icons.userImageSmall; return ( -
  • - +
  • + {text} {option ? : } diff --git a/frontend/src/components/common/InformationTag.tsx b/frontend/src/components/common/InformationTag.tsx index 35c36fe73..dbf9632cf 100644 --- a/frontend/src/components/common/InformationTag.tsx +++ b/frontend/src/components/common/InformationTag.tsx @@ -15,7 +15,7 @@ export default function InformationTag({ iconName?: keyof typeof Icons; children?: React.ReactNode; }) { - const Icon = iconName && Icons[iconName]; + const Icon = iconName && Icons[iconName] as React.FunctionComponent>; return ( diff --git a/frontend/src/components/common/TabButton.tsx b/frontend/src/components/common/TabButton.tsx index 2fa123c20..4d3ca68d4 100644 --- a/frontend/src/components/common/TabButton.tsx +++ b/frontend/src/components/common/TabButton.tsx @@ -1,66 +1,66 @@ import { styled } from 'styled-components'; import { useState } from 'react'; import Button from './button/BaseButton'; +import { v4 as uuidV4 } from 'uuid'; +import Icons from '../../design/Icons'; -enum ButtonActive { - Default, - Left, - Right, -} - -const { Default, Left, Right } = ButtonActive; - -export default function TabButton() { - const [buttonActive, setButtonActive] = useState(Default); +export default function TabButton({ + tabs, +}: { + tabs: { + iconName: keyof typeof Icons; + text: string; + event: () => void; + }[]; +}) { + const [buttonActive, setButtonActive] = useState(1); return ( - - - + {tabs.map(({ iconName, text, event }, index) => ( + + ))} ); } -const Tab = styled.div<{ $buttonActive: ButtonActive }>` +const Tab = styled.div<{ $buttonActive: number | undefined }>` display: inline-flex; border: 1px solid ${({ theme }) => theme.color.neutral.border.default}; border-radius: ${({ theme }) => theme.objectStyles.radius.medium}; overflow: hidden; + width: 320px; - button { + & > button { + width: 100%; + height: 40px; box-sizing: content-box; + border-right: 1px solid ${({ theme }) => theme.color.neutral.border.default}; } - button:first-child { - background-color: ${({ theme, $buttonActive }) => { - return $buttonActive === Left - ? theme.color.neutral.surface.bold - : theme.color.neutral.surface.default; - }}; - } - button:last-child { - background-color: ${({ theme, $buttonActive }) => { - return $buttonActive === Right - ? theme.color.neutral.surface.bold - : theme.color.neutral.surface.default; - }}; + + & > button:last-child { + border: 0; } -`; -const Vertical = styled.div` - width: 1px; - background-color: ${({ theme }) => theme.color.neutral.border.default}; + ${({ theme, $buttonActive }) => { + if (!$buttonActive) { + return ``; + } + return ` + button:nth-child(${$buttonActive}) { + background: ${theme.color.neutral.surface.bold}; + } + `; + }} `; diff --git a/frontend/src/components/common/TextInput.tsx b/frontend/src/components/common/TextInput.tsx index b08b3fc4f..ecacdd196 100644 --- a/frontend/src/components/common/TextInput.tsx +++ b/frontend/src/components/common/TextInput.tsx @@ -5,71 +5,64 @@ type TextInputProps = React.HTMLAttributes & { size: 'tall' | 'short'; id: string; name: string; - type?: string; + value: string; labelName: string; + type?: string; disabled?: boolean; placeholder?: string; helpText?: string; - validationFunc?: (value: string) => boolean; + hasError?: boolean; }; export default function TextInput(props: TextInputProps) { const [isFocused, setIsFocused] = useState(false); - const [isError, setIsError] = useState(false); - const [inputValue, setInputValue] = useState(''); const { id, name, size, type, + value, labelName, disabled, placeholder, helpText, - validationFunc, + hasError, + ...rest } = props; const hasHelpText = !!helpText; const handleBlur = () => { setIsFocused(false); - - if (validationFunc && !validationFunc(inputValue) && inputValue) { - setIsError(true); - } else { - setIsError(false); - } }; const handleFocus = () => { setIsFocused(true); }; - const handleChange = (e: React.ChangeEvent) => { - setInputValue(e.target.value); - }; - return ( - - {(isFocused || inputValue) && } + + {(isFocused || value) && } - {hasHelpText && {helpText}} + {...rest}> + {hasHelpText && ( + + {helpText} + + )} ); } type InputContainerProps = { $size: 'tall' | 'short'; - $focused?: boolean; $disabled?: boolean; $hasError?: boolean; }; @@ -119,7 +112,7 @@ const Label = styled.label<{ $size: 'tall' | 'short' }>` ${({ theme }) => theme.font.display.medium[12]} `; -const StyledTextInput = styled.input<{ $focused?: boolean }>` +const StyledTextInput = styled.input` padding: 0; border: none; outline: none; @@ -133,12 +126,12 @@ const StyledTextInput = styled.input<{ $focused?: boolean }>` } `; -const Caption = styled.span<{ $hasError: boolean }>` +const Caption = styled.span<{ $isFocused: boolean; $hasError?: boolean }>` position: absolute; bottom: -18px; left: 16px; - color: ${({ theme, $hasError }) => - $hasError + color: ${({ theme, $isFocused, $hasError }) => + !$isFocused && $hasError ? theme.color.danger.text.default : theme.color.neutral.text.weak}; ${({ theme }) => theme.font.display.medium[12]} diff --git a/frontend/src/components/common/button/BaseButton.tsx b/frontend/src/components/common/button/BaseButton.tsx index df0ce2b15..24f0f9a65 100644 --- a/frontend/src/components/common/button/BaseButton.tsx +++ b/frontend/src/components/common/button/BaseButton.tsx @@ -9,6 +9,7 @@ type ButtonProps = React.HTMLAttributes & { flexible?: boolean; outline?: boolean; ghost?: boolean; + selected?: boolean; onClick?: () => void; }; @@ -21,10 +22,13 @@ export default function Button(props: ButtonProps) { flexible, outline, ghost, + selected, ...rest } = props; const hasIcon = iconName !== undefined; - const Icon = Icons[iconName ?? 'default']; + const Icon = Icons[iconName ?? 'default'] as React.FunctionComponent< + React.SVGProps + >; return ( {hasIcon && } - {children} + {children} ); } @@ -44,11 +49,12 @@ type StyledButtonProps = { $flexible?: boolean; $outline?: boolean; $ghost?: boolean; + $selected?: boolean; }; const RealButton = styled.button` width: ${({ $flexible }) => ($flexible ? 'auto' : '184px')}; - height: 48px; + min-height: 48px; padding: 0 16px; display: inline-flex; justify-content: center; @@ -102,28 +108,48 @@ const RealButton = styled.button` } `} - ${({ theme, $ghost, $flexible }) => + ${({ theme, $ghost, $flexible, $selected }) => $ghost && ` + min-height: 32px; padding: ${$flexible ? '0' : '0 16px'}; + gap: 4px; background-color: transparent; border: none; border-radius: 0; - color: ${theme.color.neutral.text.default}; + color: ${ + $selected + ? theme.color.neutral.text.strong + : theme.color.neutral.text.default + }; + ${$selected && theme.font.selected.bold[16]}; svg { path { - stroke: ${theme.color.neutral.text.default}; + stroke: ${ + $selected + ? theme.color.neutral.text.strong + : theme.color.neutral.text.default + }; } rect { - fill: ${theme.color.neutral.text.default}; + fill: ${ + $selected + ? theme.color.neutral.text.strong + : theme.color.neutral.text.default + }; } } `} `; -const TextLabel = styled.span` - padding: 0 8px; +const TextLabel = styled.span<{ $ghost?: boolean }>` text-align: center; + ${({ $ghost }) => { + if (!$ghost) { + return 'padding: 0 8px;'; + } + return 'padding: 0;'; + }} `; diff --git a/frontend/src/components/common/button/ButtonLarge.tsx b/frontend/src/components/common/button/ButtonLarge.tsx index 8d736b4b8..bc603cb59 100644 --- a/frontend/src/components/common/button/ButtonLarge.tsx +++ b/frontend/src/components/common/button/ButtonLarge.tsx @@ -1,10 +1,15 @@ import { styled } from 'styled-components'; import Button from './BaseButton'; -const ButtonLarge = styled(Button)` +const ButtonLarge = styled(Button)<{ ghost?: boolean; selected?: boolean }>` width: ${({ flexible }) => (flexible ? 'auto' : '240px')}; - height: 56px; + min-height: 56px; ${({ theme }) => theme.font.available.medium[20]} + ${({ ghost }) => (ghost ? 'min-height: 32px;' : '')} + ${({ theme, ghost, selected }) => + selected && ghost + ? theme.font.selected.bold[20] + : theme.font.available.medium[20]}; svg { width: 24px; diff --git a/frontend/src/components/common/button/ButtonSmall.tsx b/frontend/src/components/common/button/ButtonSmall.tsx index b1d42096f..90f42ea95 100644 --- a/frontend/src/components/common/button/ButtonSmall.tsx +++ b/frontend/src/components/common/button/ButtonSmall.tsx @@ -1,10 +1,15 @@ import { styled } from 'styled-components'; import Button from './BaseButton'; -const ButtonSmall = styled(Button)` +const ButtonSmall = styled(Button)<{ ghost?: boolean; selected?: boolean }>` width: ${({ flexible }) => (flexible ? 'auto' : '128px')}; - height: 40px; + min-height: 40px; ${({ theme }) => theme.font.available.medium[12]} + ${({ ghost }) => (ghost ? `min-height: 32px;` : '')} + ${({ theme, ghost, selected }) => + selected && ghost + ? theme.font.selected.bold[12] + : theme.font.available.medium[12]}; `; export default ButtonSmall; diff --git a/frontend/src/components/issues/IssueTable.tsx b/frontend/src/components/issues/IssueTable.tsx new file mode 100644 index 000000000..277ac2ad8 --- /dev/null +++ b/frontend/src/components/issues/IssueTable.tsx @@ -0,0 +1,124 @@ +import { styled } from 'styled-components'; +import { useState } from 'react'; +import Icons from '../../design/Icons'; +import CheckBox from '../../constant/CheckBox'; +import Button from '../common/button/BaseButton'; +import DropdownIndicator from '../common/DropdownIndicator'; + +const { initial, active } = CheckBox; + +export default function IssueTable() { + const [totalCheckBox, setTotalCheckBox] = useState(initial); + + function totalCheckBoxHandler(e: React.MouseEvent) { + const input = e.target as HTMLInputElement; + if (input.checked) { + setTotalCheckBox(active); + return; + } + setTotalCheckBox(initial); + } + + return ( + + + + + + + + +
  • + +
  • +
  • + +
  • + + +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
    + + + + + + + ); +} + +const Container = styled.article` + background-color: ${({ theme }) => theme.color.neutral.surface.default}; + border: 1px solid ${({ theme }) => theme.color.neutral.border.default}; + overflow: hidden; + border-radius: ${({ theme }) => theme.objectStyles.radius.large}; + & > h3 { + &:first-child { + border-bottom: 1px solid + ${({ theme }) => theme.color.neutral.border.default}; + } + &:last-child { + border-bottom: 0; + } + } +`; + +const TotalCheckBox = styled.label` + & > input { + display: none; + } +`; + +function CheckboxIcon({ checkBox }: { checkBox: CheckBox }) { + const Icon = Icons.checkBox[checkBox]; + return ; +} + +const IssueTableHeading = styled.h3` + padding: 16px 32px; + display: flex; + align-items: center; + gap: 32px; +`; + +const Buttons = styled.div` + width: 100%; + display: flex; + justify-content: space-between; + & > ul { + display: flex; + gap: 24px; + & > li { + display: flex; + align-items: center; + } + } +`; + +const Left = styled.ul``; + +const Right = styled.ul``; + +const IssueTableBody = styled.ul``; + +const IssueTableItem = styled.li``; diff --git a/frontend/src/components/label/Label.tsx b/frontend/src/components/label/Label.tsx new file mode 100644 index 000000000..a799dfdae --- /dev/null +++ b/frontend/src/components/label/Label.tsx @@ -0,0 +1,25 @@ +import { styled } from 'styled-components'; +import InformationTag from '../common/InformationTag'; + +export default function Label({ + textColor, + backgroundColor, + name, +}: { + textColor: string; + backgroundColor: string; + name: string; +}) { + return ( + + {name} + + ); +} + +const Container = styled.span<{ $textColor: string; $backgroundColor: string }>` + display: inline-block; + border-radius: ${({ theme }) => theme.objectStyles.radius.large}; + color: #${({ $textColor }) => $textColor}; + background: #${({ $backgroundColor }) => $backgroundColor}; +`; diff --git a/frontend/src/components/label/Labels.tsx b/frontend/src/components/label/Labels.tsx new file mode 100644 index 000000000..301da67bd --- /dev/null +++ b/frontend/src/components/label/Labels.tsx @@ -0,0 +1,103 @@ +import { styled } from 'styled-components'; +import Label from './Label'; +import ButtonSmall from '../common/button/ButtonSmall'; + +export default function Labels({ + data, +}: { + data: { + id: number; + textColor: string; + backgroundColor: string; + name: string; + description: string; + }[]; +}) { + return ( + +
    {data.length}개의 레이블
    + + {data.map(({ id, textColor, backgroundColor, name, description }) => ( + + + + {description} + +
  • + + 편집 + +
  • +
  • + + 삭제 + +
  • +
    +
    + ))} + +
    + ); +} +const Header = styled.h2` + padding: 20px 32px; + background: ${({ theme }) => theme.color.neutral.surface.default}; + border-bottom: 1px solid ${({ theme }) => theme.color.neutral.border.default}; + ${({ theme }) => theme.font.display.bold[16]} +`; + +const Body = styled.ul``; + +const LabelsItem = styled.li` + display: flex; + align-items: center; + gap: 32px; + padding: 0 32px; + height: 96px; + border-bottom: 1px solid ${({ theme }) => theme.color.neutral.border.default}; + background: ${({ theme }) => theme.color.neutral.surface.strong}; + &:last-child { + border: 0; + } +`; + +const LabelArea = styled.div` + width: 176px; +`; + +const Description = styled.div` + width: 870px; + color: ${({ theme }) => theme.color.neutral.text.weak}; + ${({ theme }) => theme.font.display.medium[16]} +`; + +const RightButton = styled.menu` + display: flex; + gap: 24px; + min-width: 106px; + & > li { + flex-grow: 1; + & > button { + width: 100%; + } + &:nth-child(2) > button { + color: ${({ theme }) => theme.color.danger.text.default}; + path { + stroke: ${({ theme }) => theme.color.danger.text.default}; + } + + rect { + fill: ${({ theme }) => theme.color.danger.text.default}; + } + } + } +`; + +const Container = styled.article` + border-radius: ${({ theme }) => theme.objectStyles.radius.large}; + border: 1px solid ${({ theme }) => theme.color.neutral.border.default}; + overflow: hidden; + margin: auto; +`; diff --git a/frontend/src/components/landmark/Header.tsx b/frontend/src/components/landmark/Header.tsx new file mode 100644 index 000000000..e31f2929a --- /dev/null +++ b/frontend/src/components/landmark/Header.tsx @@ -0,0 +1,19 @@ +import { styled } from 'styled-components'; + +export default function Header({ children }: { children: React.ReactNode }) { + return ( + +

    헤더

    + {children} +
    + ); +} + +const Container = styled.header` + height: 94px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32px; +`; diff --git a/frontend/src/components/landmark/Main.tsx b/frontend/src/components/landmark/Main.tsx new file mode 100644 index 000000000..6bc100d40 --- /dev/null +++ b/frontend/src/components/landmark/Main.tsx @@ -0,0 +1,14 @@ +import { styled } from 'styled-components'; + +export default function Main({ children }: { children: React.ReactNode }) { + return ( + +

    메인

    + {children} +
    + ); +} + +const Container = styled.main` + +`; diff --git a/frontend/src/components/landmark/Toolbar.tsx b/frontend/src/components/landmark/Toolbar.tsx new file mode 100644 index 000000000..bebc15c25 --- /dev/null +++ b/frontend/src/components/landmark/Toolbar.tsx @@ -0,0 +1,12 @@ +import { styled } from 'styled-components'; + +export default function Toolbar({ children }: { children: React.ReactNode }) { + return {children}; +} + +const Container = styled.div` + display: flex; + justify-content: space-between; + margin-bottom: 24px; + align-items: center; +`; diff --git a/frontend/src/constant/CheckBox.tsx b/frontend/src/constant/CheckBox.tsx new file mode 100644 index 000000000..598ade44a --- /dev/null +++ b/frontend/src/constant/CheckBox.tsx @@ -0,0 +1,7 @@ +enum CheckBox { + initial, + disable, + active, +} + +export default CheckBox; diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx index c9f791b60..0b9bafb1a 100644 --- a/frontend/src/context/AuthProvider.tsx +++ b/frontend/src/context/AuthProvider.tsx @@ -2,8 +2,8 @@ import { useState, createContext, ReactElement } from 'react'; export type AuthUser = { userId: string; - pwd: string; userName: string; + profileImg: string; accessToken: string; } | null; diff --git a/frontend/src/design/Icons.ts b/frontend/src/design/Icons.ts index 7f8898e29..32fc6b08c 100644 --- a/frontend/src/design/Icons.ts +++ b/frontend/src/design/Icons.ts @@ -10,6 +10,16 @@ import { ReactComponent as PaperClip } from '../asset/icons/paperclip.svg'; import { ReactComponent as Edit } from '../asset/icons/edit.svg'; import { ReactComponent as Smile } from '../asset/icons/smile.svg'; import { ReactComponent as Search } from '../asset/icons/search.svg'; +import { ReactComponent as Label } from '../asset/icons/label.svg'; +import { ReactComponent as MileStone } from '../asset/icons/milestone.svg'; +import { ReactComponent as CheckBoxInitial } from '../asset/icons/check_box/initial.svg'; +import { ReactComponent as CheckBoxDisable } from '../asset/icons/check_box/disable.svg'; +import { ReactComponent as CheckBoxActive } from '../asset/icons/check_box/active.svg'; +import { ReactComponent as Archive } from '../asset/icons/archive.svg'; +import { ReactComponent as Trash } from '../asset/icons/trash.svg'; +import CheckBox from '../constant/CheckBox'; + +const { initial, disable, active } = CheckBox; const Icons = { plus: Plus, @@ -21,10 +31,19 @@ const Icons = { checkOnCircle: CheckOnCircle, checkOffCircle: CheckOffCircle, paperClip: PaperClip, + label: Label, + milestone: MileStone, edit: Edit, smile: Smile, search: Search, default: Plus, + archive: Archive, + trash: Trash, + checkBox: { + [initial]: CheckBoxInitial, + [disable]: CheckBoxDisable, + [active]: CheckBoxActive, + }, } as const; export default Icons; diff --git a/frontend/src/hooks/useAxiosPrivate.ts b/frontend/src/hooks/useAxiosPrivate.ts index d8534e03d..5051653a8 100644 --- a/frontend/src/hooks/useAxiosPrivate.ts +++ b/frontend/src/hooks/useAxiosPrivate.ts @@ -10,8 +10,9 @@ const useAxiosPrivate = () => { useEffect(() => { const requestIntercept = axiosPrivate.interceptors.request.use( (config) => { - if (!config.headers['Authorization']) { - config.headers['Authorization'] = `Bearer ${auth?.accessToken}`; + const accessToken = localStorage.getItem('accessToken'); + if (!config.headers['Authorization'] && accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}`; } return config; }, diff --git a/frontend/src/hooks/useRefreshToken.ts b/frontend/src/hooks/useRefreshToken.ts index bfb780507..785b5b80c 100644 --- a/frontend/src/hooks/useRefreshToken.ts +++ b/frontend/src/hooks/useRefreshToken.ts @@ -10,6 +10,7 @@ const useRefreshToken = () => { '/api/reissue/token', JSON.stringify({ refreshToken: localStorage.getItem('refreshToken') }), { + headers: { 'Content-Type': 'application/json' }, withCredentials: true, } ); @@ -21,13 +22,13 @@ const useRefreshToken = () => { const newAuth: AuthUser = { userId: prev.userId, - pwd: prev.pwd, userName: prev.userName, - accessToken: response.data.accessToken, + profileImg: prev.profileImg, + accessToken: response.data.message.accessToken, }; return newAuth; }); - localStorage.setItem('accessToken', response.data.accessToken); + localStorage.setItem('accessToken', response.data.message.accessToken); return response.data.accessToken; }; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 1ccb4cbae..430ff5c46 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; -import { worker } from './mocks/browser'; +import { worker } from './mocks/browser.ts'; import { BrowserRouter } from 'react-router-dom'; import { AuthProvider } from './context/AuthProvider.tsx'; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 10cd44057..60ba312c2 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -6,7 +6,7 @@ export const handlers = [ ctx.status(200), ctx.json({ statusCode: 200, - messages: '요청 성공', + message: '요청 성공', }) ); }), @@ -16,23 +16,32 @@ export const handlers = [ rest.post('/api/reissue/token', (req, res, ctx) => { return res(ctx.status(200), ctx.json(newAccessToken)); }), - rest.post('/api/login/github?code={}', (req, res, ctx) => { - return res(ctx.status(200), ctx.json(successGitHubLogin)); + rest.get('/api/login/github', (req, res, ctx) => { + const code = req.url.searchParams.get('code'); + + if (code) { + return res(ctx.status(200), ctx.json(successGitHubLogin)); + } else { + return res(ctx.status(400), ctx.json({ message: '잘못된 코드 값' })); + } }), rest.post('/api/logout', (req, res, ctx) => { return res( ctx.status(200), ctx.json({ statusCode: 200, - messages: '요청 성공', + message: '요청 성공', }) ); }), + rest.get('/api/labels', (req, res, ctx) => { + return res(ctx.status(200), ctx.json(labelData)); + }), ]; const successLogin = { statusCode: 200, - messages: { + message: { accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', refreshToken: @@ -44,13 +53,16 @@ const successLogin = { }; const newAccessToken = { - accessToken: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkxlbyBLaW0iLCJpYXQiOjE1MTYyMzkwMjJ9.ZfseO7je1qHjBQgT122YZ-OvCMXUQ5NOkVZM8k9P2eU', + statusCode: 200, + message: { + accessToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkxlbyBLaW0iLCJpYXQiOjE1MTYyMzkwMjJ9.ZfseO7je1qHjBQgT122YZ-OvCMXUQ5NOkVZM8k9P2eU', + }, }; const successGitHubLogin = { statusCode: 200, - messages: { + message: { accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', refreshToken: @@ -60,3 +72,33 @@ const successGitHubLogin = { userProfileImg: 'https://f1.kina.or.kr/2020/11/jtqgmmu4i3.jpg', }, }; + +const labelData = { + statusCode: 200, + message: { + milestoneCount: 2, + labels: [ + { + id: 1, + textColor: 'AAA333', + backgroundColor: 'FFF3FF', + name: 'feat', + description: '기능 추가', + }, + { + id: 2, + textColor: 'FEFEFE', + backgroundColor: '333333', + name: 'fix', + description: '버그 수정', + }, + { + id: 3, + textColor: 'AAA333', + backgroundColor: 'FFF3FF', + name: 'refactor', + description: '리팩터링', + }, + ], + }, +}; diff --git a/frontend/src/pages/Components.tsx b/frontend/src/pages/Components.tsx index 1385e1add..3b6378f2b 100644 --- a/frontend/src/pages/Components.tsx +++ b/frontend/src/pages/Components.tsx @@ -3,16 +3,16 @@ import Button from '../components/common/button/BaseButton'; import ButtonLarge from '../components/common/button/ButtonLarge'; import ButtonSmall from '../components/common/button/ButtonSmall'; import ColorCodeInput from '../components/common/ColorCodeInput'; -import TabButton from '../components/common/TabButton'; -import InformationTag from '../components/common/InformationTag'; -import TextInput from '../components/common/TextInput'; -import ProgressIndicator from '../components/common/ProgressIndicator'; -import DropdownIndicator from '../components/common/DropdownIndicator'; -import DropdownPanel from '../components/common/DropdownPanel'; -import TextArea from '../components/common/TextArea'; -import SideBar from '../components/common/SideBar'; -import FilterBar from '../components/common/FilterBar'; -import Comment from '../components/common/CommentElements'; +// import TabButton from '../components/common/TabButton'; +// import InformationTag from '../components/common/InformationTag'; +// import TextInput from '../components/common/TextInput'; +// import ProgressIndicator from '../components/common/ProgressIndicator'; +// import DropdownIndicator from '../components/common/DropdownIndicator'; +// import DropdownPanel from '../components/common/DropdownPanel'; +// import TextArea from '../components/common/TextArea'; +// import SideBar from '../components/common/SideBar'; +// import FilterBar from '../components/common/FilterBar'; +// import Comment from '../components/common/CommentElements'; import { AppContext } from '../main'; import { useContext } from 'react'; @@ -29,16 +29,19 @@ export default function Components() { BUTTON + + - - BUTTON - - BUTTON + S BUTTON - + {/* Label @@ -74,7 +77,7 @@ export default function Components() { comment="오늘 점심 뭐 먹죠?" /> - + */} ); diff --git a/frontend/src/pages/GitHubCallback.tsx b/frontend/src/pages/GitHubCallback.tsx new file mode 100644 index 000000000..0b19c3a65 --- /dev/null +++ b/frontend/src/pages/GitHubCallback.tsx @@ -0,0 +1,76 @@ +import { useEffect } from 'react'; +import { styled, keyframes } from 'styled-components'; +import { useNavigate } from 'react-router-dom'; +import axios from '../api/axios'; +import useAuth from '../hooks/useAuth'; + +export default function Callback() { + const { login } = useAuth(); + const navigate = useNavigate(); + + useEffect(() => { + const getAccessToken = async () => { + const codeParam = new URLSearchParams(window.location.search).get('code'); + + if (!codeParam) { + navigate('/login'); + return; + } + + try { + const res = await axios.get(`/api/login/github?code=${codeParam}`, { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + }); + + if (res.status === 200) { + localStorage.setItem('accessToken', res.data.message.accessToken); + localStorage.setItem('refreshToken', res.data.message.refreshToken); + login({ + userId: res.data.message.userId, + userName: res.data.message.userName, + profileImg: res.data.message.userProfileImg, + accessToken: res.data.message.accessToken, + }); + navigate('/'); + } + } catch (err) { + console.error(err); + navigate('/login'); + } + }; + + getAccessToken(); + }, []); + + return ( + + + + ); +} + +const spinner = keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +`; + +const Container = styled.div` + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +`; + +const Loader = styled.div` + width: 50px; + height: 50px; + border: 10px solid #f3f3f3; + border-top: 10px solid #383636; + border-radius: 50%; + animation: ${spinner} 1.5s linear infinite; +`; diff --git a/frontend/src/pages/Issues.tsx b/frontend/src/pages/Issues.tsx new file mode 100644 index 000000000..8fbef3421 --- /dev/null +++ b/frontend/src/pages/Issues.tsx @@ -0,0 +1,63 @@ +import { useContext } from 'react'; +import { AppContext } from '../main'; +import Header from '../components/landmark/Header'; +import IssueTable from '../components/issues/IssueTable'; +import Main from '../components/landmark/Main'; +import { Link } from 'react-router-dom'; +import ContextLogo from '../types/ContextLogo'; +import FilterBar from '../components/common/FilterBar'; +import Toolbar from '../components/landmark/Toolbar'; +import { styled } from 'styled-components'; +import TabButton from '../components/common/TabButton'; +import Layout from '../components/Layout'; +import Button from '../components/common/button/BaseButton'; + +export default function Issues() { + const { util } = useContext(AppContext); + const logo = (util.getLogoByTheme() as ContextLogo).medium; + return ( + +
    + + 이슈트래커 + +
    + {/* profile */} + popopo +
    +
    + + + + {}, + }, + { + iconName: 'milestone', + text: '마일스톤', + event: () => {}, + }, + ]} + /> + + + +
    + +
    + {/* */} +
    + ); +} + +const ActionGroup = styled.div` + display: flex; + align-items: center; + gap: 16px; +`; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index ef1ff2cb8..3f5cbbb80 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { useState, useContext } from 'react'; import { styled } from 'styled-components'; import axios from '../api/axios'; import { useNavigate, useLocation } from 'react-router-dom'; @@ -10,6 +10,9 @@ import ButtonLarge from '../components/common/button/ButtonLarge'; import Button from '../components/common/button/BaseButton'; export default function Login() { + const [userId, setUserId] = useState(''); + const [password, setPassword] = useState(''); + const { login } = useAuth(); const { util } = useContext(AppContext); const logo = (util.getLogoByTheme() as ContextLogo).large; @@ -21,6 +24,19 @@ export default function Login() { import.meta.env.VITE_CLIENT_ID }`; + const validateUserEmail = (value: string) => { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return emailRegex.test(value); + }; + + const validateUserPassword = (value: string) => { + const passwordRegex = /^[a-zA-Z0-9!@#$%^&*|'"~;:₩\\?]{6,12}$/; + return passwordRegex.test(value); + }; + + const isValidate = + validateUserEmail(userId) && validateUserPassword(password); + const handleLogin = async (userId: string, password: string) => { try { const res = await axios.post( @@ -33,13 +49,13 @@ export default function Login() { ); if (res.status === 200) { - localStorage.setItem('accessToken', res.data.messages.accessToken); - localStorage.setItem('refreshToken', res.data.messages.refreshToken); + localStorage.setItem('accessToken', res.data.message.accessToken); + localStorage.setItem('refreshToken', res.data.message.refreshToken); login({ - userId: userId, - pwd: password, - userName: res.data.messages.userName, - accessToken: res.data.messages.accessToken, + userId: res.data.message.userId, + userName: res.data.message.userName, + profileImg: res.data.message.userProfileImg, + accessToken: res.data.message.accessToken, }); } @@ -60,6 +76,15 @@ export default function Login() { handleLogin(userId, password); }; + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + if (name === 'userId') { + setUserId(value); + } else if (name === 'password') { + setPassword(value); + } + }; + return (

    로그인 페이지

    @@ -71,30 +96,38 @@ export default function Login() { type="button" outline onClick={() => { - window.location.assign( - GITHUB_LOGIN_URL + `&redirect_uri=localhost:5173` - ); + window.location.assign(GITHUB_LOGIN_URL); }}> GitHub 계정으로 로그인 or - 아이디로 로그인 + + 아이디로 로그인 + navigate('/register')}> 회원가입 @@ -118,7 +151,7 @@ const Container = styled.article` } fieldset { - margin-bottom: 16px; + margin-bottom: 24px; } `; diff --git a/frontend/src/pages/Main.tsx b/frontend/src/pages/Main.tsx index 01680f278..5e66a73fd 100644 --- a/frontend/src/pages/Main.tsx +++ b/frontend/src/pages/Main.tsx @@ -15,28 +15,32 @@ export default function Main() { } try { - const res = await axiosPrivate.post( - '/api/logout', - JSON.stringify({ id: auth.userId, password: auth.pwd }), - { - headers: { 'Content-Type': 'application/json' }, - withCredentials: true, - } - ); + const res = await axiosPrivate.post('/api/logout', null, { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + }); if (res.status === 200) { localStorage.clear(); logout(); + navigate('/login'); + } else { + console.error('로그아웃 실패:', res.status); } - navigate('/login'); } catch (err) { - console.error(err); + console.error('로그아웃 에러:', err); } }; return ( <>

    main

    + + ); diff --git a/frontend/src/pages/Options.tsx b/frontend/src/pages/Options.tsx new file mode 100644 index 000000000..45fd390a2 --- /dev/null +++ b/frontend/src/pages/Options.tsx @@ -0,0 +1,87 @@ +import React, { useContext, useEffect, useState } from 'react'; +import Header from '../components/landmark/Header'; +import { Link } from 'react-router-dom'; +import { AppContext } from '../main'; +import ContextLogo from '../types/ContextLogo'; +import Main from '../components/landmark/Main'; +import Toolbar from '../components/landmark/Toolbar'; +import TabButtonComponent from '../components/common/TabButton'; +import Layout from '../components/Layout'; +import Button from '../components/common/button/BaseButton'; +import Labels from '../components/label/Labels'; +import useAxiosPrivate from '../hooks/useAxiosPrivate'; + +enum Option { + labels, + milestones, +} + +const { labels, milestones } = Option; + +const TabButton = React.memo(TabButtonComponent); + +export default function Options() { + const { util } = useContext(AppContext); + const [activeOption, setActiveOption] = useState