diff --git a/.github/workflows/frontend_cd.yml b/.github/workflows/frontend_cd.yml deleted file mode 100644 index 7d2abc37c..000000000 --- a/.github/workflows/frontend_cd.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Frontend CD - -on: - push: - branches: - - main - - dev/fe - -jobs: - build: - runs-on: - - self-hosted - - spring - - develop - env: - frontend-directory: ./frontend - steps: - - uses: actions/checkout@v4 - - - name: Node.js 설정 - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 환경 파일 생성 - run: | - if [ "${{ github.ref_name }}" == "main" ]; then - echo "REACT_APP_API_URL=${{ secrets.REACT_APP_API_URL }}" > ${{ env.frontend-directory }}/.env.production - else - echo "REACT_APP_API_URL=${{ secrets.REACT_APP_BETA_API_URL }}" > ${{ env.frontend-directory }}/.env.production - fi - - echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> ${{ env.frontend-directory }}/.env.production - echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> ${{ env.frontend-directory }}/.env.sentry-build-plugin - - - name: 환경 파일 권한 설정 - run: chmod 644 ${{ env.frontend-directory }}/.env.* - - - name: 의존성 설치 - run: npm install - working-directory: ${{ env.frontend-directory }} - - - name: 빌드 실행 - run: npm run build - working-directory: ${{ env.frontend-directory }} - - - name: 아티팩트 업로드 - uses: actions/upload-artifact@v4 - with: - name: code-zap-front - path: ${{ env.frontend-directory }}/dist/** - - deploy: - needs: build - runs-on: - - self-hosted - - spring - - develop - steps: - - name: 아티팩트 디렉토리 생성 - run: | - rm -rf ${{ secrets.FRONT_DIRECTORY }} - mkdir ${{ secrets.FRONT_DIRECTORY }} - - name: 아티팩트 다운로드 - uses: actions/download-artifact@v4 - with: - name: code-zap-front - path: ${{ secrets.FRONT_DIRECTORY }} - - name: S3로 이동 - run: | - if [ "${{ github.ref_name }}" == "main" ]; then - aws s3 cp --recursive ${{ secrets.FRONT_DIRECTORY }} s3://techcourse-project-2024/code-zap - else - aws s3 cp --recursive ${{ secrets.FRONT_DIRECTORY }} s3://techcourse-project-2024/code-zap-staging - fi diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d898aaa7d..47f7ed2ca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -71,6 +71,7 @@ "undici": "^6.19.2", "util": "^0.12.5", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" } @@ -4331,6 +4332,12 @@ "node": ">=18" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true + }, "node_modules/@remix-run/router": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", @@ -9397,6 +9404,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -9817,6 +9830,12 @@ "webpack": "^4 || ^5" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -12010,6 +12029,21 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -15943,6 +15977,15 @@ "node": ">=0.4.0" } }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -16619,6 +16662,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -18566,6 +18618,20 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -20025,6 +20091,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -20976,6 +21051,86 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7ea3db807..04a0ce40d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "code-zap", - "version": "1.1.2", + "version": "1.1.3", "description": "", "main": "index.js", "scripts": { @@ -10,12 +10,16 @@ "dev": "webpack-dev-server --config webpack.dev.js --open", "tsc": "tsc --noEmit", "build": "webpack --mode production --config webpack.prod.js", + "build:report": "webpack-bundle-analyzer --port 8888 dist/bundle-stats.json", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "lint:style": "stylelint '**/style.ts' --fix" }, "keywords": [], "author": "", + "sideEffects": [ + "./src/routes/router.tsx" + ], "license": "ISC", "dependencies": { "@emotion/react": "^11.11.4", @@ -80,6 +84,7 @@ "undici": "^6.19.2", "util": "^0.12.5", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" }, diff --git a/frontend/playwright/tests/search.spec.ts b/frontend/playwright/tests/search.spec.ts index cf37599fb..aeaa61ced 100644 --- a/frontend/playwright/tests/search.spec.ts +++ b/frontend/playwright/tests/search.spec.ts @@ -12,7 +12,7 @@ test('검색창에 `검색테스트`를 입력하면 `검색테스트`가 내용 await searchTemplates({ page, keyword }); - await waitForSuccess({ page, apiUrl: '/templates?keyword' }); + await waitForSuccess({ page, apiUrl: '/templates/login?keyword' }); await expect(page.getByRole('link', { name: /검색테스트/ })).toBeVisible(); }); @@ -23,6 +23,6 @@ test('검색창에 `ㅁㅅㅌㅇ`를 입력할 경우 `검색 결과가 없습 await searchTemplates({ page, keyword }); - await waitForSuccess({ page, apiUrl: '/templates?keyword' }); + await waitForSuccess({ page, apiUrl: '/templates/login?keyword' }); await expect(page.locator('div').filter({ hasText: /^검색 결과가 없습니다\.$/ })).toBeVisible(); }); diff --git a/frontend/playwright/tests/templates.actions.ts b/frontend/playwright/tests/templates.actions.ts index 9824a6645..d722c4c3f 100644 --- a/frontend/playwright/tests/templates.actions.ts +++ b/frontend/playwright/tests/templates.actions.ts @@ -35,7 +35,7 @@ export const uploadTemplateToCodezap = async ({ } // 파일명 입력 - await page.getByPlaceholder('파일명.js').fill(fileName); + await page.getByPlaceholder('파일명.[확장자]').fill(fileName); // 코드 입력 await page diff --git a/frontend/playwright/tests/templates.spec.ts b/frontend/playwright/tests/templates.spec.ts index c1f04a18f..774454c6e 100644 --- a/frontend/playwright/tests/templates.spec.ts +++ b/frontend/playwright/tests/templates.spec.ts @@ -35,7 +35,7 @@ test('템플릿 제목, 설명, 파일명, 소스코드, 태그를 입력하고 tag: testTitle, }); - const templateCard = page.getByRole('link', { name: `testTitle` }).first(); + const templateCard = page.getByRole('link', { name: testTitle }).first(); await expect(templateCard).toBeVisible(); } catch (error) { @@ -50,7 +50,7 @@ test('템플릿 카드를 누르면 템플릿 제목, 설명, 작성자, 생성 }) => { await page.goto('/my-templates'); // 템플릿 목록 - await waitForSuccess({ page, apiUrl: '/templates' }); + await waitForSuccess({ page, apiUrl: '/templates/login?keyword' }); const templateCard = page.getByRole('link', { name: '상세조회테스트' }); diff --git a/frontend/public/index.html b/frontend/public/index.html index ff20b8126..27a121bd5 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,6 +1,8 @@ + + void; + currentValue: Category; + handleCurrentValue: (newValue: Category) => void; + getOptionLabel: (category: Category) => string; + createNewCategory: (categoryName: string) => Promise; + dropdownRef: React.MutableRefObject; + isPending: boolean; +} + +const CategoryDropdown = ({ + options, + isOpen, + toggleDropdown, + currentValue, + handleCurrentValue, + getOptionLabel, + createNewCategory, + dropdownRef, + isPending, +}: Props) => { + const { + value: categoryInputValue, + errorMessage: categoryInputErrorMessage, + handleChange: handleCategoryInputChange, + } = useInputWithValidate('', validateCategoryName); + + const handleNewCategory = async (e: React.KeyboardEvent) => { + if (!(e.target instanceof HTMLInputElement) || e.key !== 'Enter' || e.nativeEvent.isComposing === true) { + return; + } + + if (categoryInputErrorMessage !== '') { + return; + } + + const inputValue = e.target.value; + + if (inputValue === '') { + return; + } + + await createNewCategory(inputValue); + + e.target.value = ''; + }; + + return ( + <> + + + } + /> + + ); +}; + +export default CategoryDropdown; diff --git a/frontend/src/components/CategoryGuide/CategoryGuide.tsx b/frontend/src/components/CategoryGuide/CategoryGuide.tsx new file mode 100644 index 000000000..3484a6842 --- /dev/null +++ b/frontend/src/components/CategoryGuide/CategoryGuide.tsx @@ -0,0 +1,23 @@ +import { Guide, Text } from '@/components'; +import { theme } from '@/style/theme'; + +interface Props { + isOpen: boolean; + categoryErrorMessage: string; +} + +const CategoryGuide = ({ isOpen, categoryErrorMessage }: Props) => { + const isError = categoryErrorMessage !== ''; + + return ( + + {isError ? ( + {categoryErrorMessage} + ) : ( + 엔터로 카테고리를 등록해요 + )} + + ); +}; + +export default CategoryGuide; diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx index 4cc6b91c3..0d275c604 100644 --- a/frontend/src/components/Footer/Footer.tsx +++ b/frontend/src/components/Footer/Footer.tsx @@ -1,23 +1,32 @@ import { Text } from '@/components'; +import { useAuth } from '@/hooks/authentication'; import * as S from './Footer.style'; -const Footer = () => ( - - - Copyright{' '} - - Codezap - {' '} - © All rights reserved. - - - - 문의 : - {' '} - codezap2024@gmail.com{' '} - - -); +const Footer = () => { + const { isChecking } = useAuth(); + + if (isChecking) { + return null; + } + + return ( + + + Copyright{' '} + + Codezap + {' '} + © All rights reserved. + + + + 문의 : + {' '} + codezap2024@gmail.com{' '} + + + ); +}; export default Footer; diff --git a/frontend/src/components/Header/Header.style.ts b/frontend/src/components/Header/Header.style.ts index 763bb6175..eebcf31fa 100644 --- a/frontend/src/components/Header/Header.style.ts +++ b/frontend/src/components/Header/Header.style.ts @@ -99,7 +99,7 @@ export const MobileMenuContainer = styled.div` } `; -export const HamburgerIconWrapper = styled.div` +export const HamburgerIconWrapper = styled.button` display: none; @media (max-width: 768px) { diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 065b14156..afd2ff704 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,10 +1,10 @@ import { useEffect } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { CodeZapLogo, HamburgerIcon, PlusIcon } from '@/assets/images'; import { Button, Flex, Heading, Text } from '@/components'; import { ToastContext } from '@/contexts'; -import { useCustomContext, useToggle } from '@/hooks'; +import { useCustomContext, useCustomNavigate, useToggle } from '@/hooks'; import { useAuth } from '@/hooks/authentication/useAuth'; import { usePressESC } from '@/hooks/usePressESC'; import { useScrollDisable } from '@/hooks/useScrollDisable'; @@ -19,7 +19,7 @@ const Header = ({ headerRef }: { headerRef: React.RefObject }) = const [menuOpen, toggleMenu] = useToggle(); const { failAlert } = useCustomContext(ToastContext); const location = useLocation(); - const navigate = useNavigate(); + const navigate = useCustomNavigate(); useScrollDisable(menuOpen); usePressESC(menuOpen, toggleMenu); @@ -64,16 +64,24 @@ const Header = ({ headerRef }: { headerRef: React.RefObject }) = weight='bold' hoverStyle='none' onClick={handleTemplateUploadButton} + aria-description='템플릿 작성 페이지로 이동됩니다.' > - 새 템플릿 + 새 템플릿 {!isChecking && isLogin ? : } - @@ -125,7 +133,7 @@ const LoginButton = () => ( ); const HeaderMenuButton = ({ menuOpen, toggleMenu }: { menuOpen: boolean; toggleMenu: () => void }) => ( - + ); diff --git a/frontend/src/components/Input/Input.style.ts b/frontend/src/components/Input/Input.style.ts index 5d1b6cfa1..67d9d637b 100644 --- a/frontend/src/components/Input/Input.style.ts +++ b/frontend/src/components/Input/Input.style.ts @@ -2,6 +2,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { theme } from '@/style/theme'; + import type { BaseProps, TextFieldProps } from './Input'; const sizes = { @@ -144,6 +145,12 @@ export const Adornment = styled.div` width: 1.5rem; height: 1.5rem; `} + + ${({ as }) => + as === 'button' && + css` + cursor: pointer; + `} `; export const HelperText = styled.span` diff --git a/frontend/src/components/Input/Input.tsx b/frontend/src/components/Input/Input.tsx index 41ea5cc13..4bab9f54e 100644 --- a/frontend/src/components/Input/Input.tsx +++ b/frontend/src/components/Input/Input.tsx @@ -23,7 +23,9 @@ export interface TextFieldProps extends InputHTMLAttributes { export interface LabelProps extends LabelHTMLAttributes {} -export interface AdornmentProps extends HTMLAttributes {} +export interface AdornmentProps extends HTMLAttributes { + as?: 'div' | 'button'; +} export interface HelperTextProps extends HTMLAttributes {} @@ -43,12 +45,15 @@ const TextField = ({ ...rests }: TextFieldProps) => ; const Label = ({ children, ...rests }: PropsWithChildren) => {children}; -const Adornment = ({ children, ...rests }: PropsWithChildren) => ( - - {children} - -); +const Adornment = ({ children, as, ...rests }: PropsWithChildren) => { + const buttonProps = as === 'button' ? { type: 'button' } : {}; + return ( + + {children} + + ); +}; const HelperText = ({ children, ...rests }: PropsWithChildren) => ( {children} ); diff --git a/frontend/src/components/LoadingFallback/LoadingFallback.style.ts b/frontend/src/components/LoadingFallback/LoadingFallback.style.ts new file mode 100644 index 000000000..721106631 --- /dev/null +++ b/frontend/src/components/LoadingFallback/LoadingFallback.style.ts @@ -0,0 +1,10 @@ +import styled from '@emotion/styled'; + +export const FallbackContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + + width: 100vw; + height: 100vh; +`; diff --git a/frontend/src/components/LoadingFallback/LoadingFallback.tsx b/frontend/src/components/LoadingFallback/LoadingFallback.tsx new file mode 100644 index 000000000..baab22323 --- /dev/null +++ b/frontend/src/components/LoadingFallback/LoadingFallback.tsx @@ -0,0 +1,10 @@ +import LoadingBall from '../LoadingBall/LoadingBall'; +import * as S from './LoadingFallback.style'; + +const LoadingFallback = () => ( + + + +); + +export default LoadingFallback; diff --git a/frontend/src/components/NewCategoryInput/NewCategoryInput.tsx b/frontend/src/components/NewCategoryInput/NewCategoryInput.tsx new file mode 100644 index 000000000..f84dd4919 --- /dev/null +++ b/frontend/src/components/NewCategoryInput/NewCategoryInput.tsx @@ -0,0 +1,33 @@ +import { Input, LoadingBall } from '@/components'; +import { useLoaderDelay } from '@/hooks'; +import { theme } from '@/style/theme'; + +interface Props { + value: string; + onEnterDown: (e: React.KeyboardEvent) => void; + onChange: (e: React.ChangeEvent, compareValue?: string) => void; + isPending: boolean; +} + +const NewCategoryInput = ({ value, onChange, onEnterDown, isPending }: Props) => { + const showLoader = useLoaderDelay(isPending, 700); + + return ( + + {showLoader ? ( + + ) : ( + + )} + + ); +}; + +export default NewCategoryInput; diff --git a/frontend/src/components/NonmemberAlerter/NonmemberAlerter.tsx b/frontend/src/components/NonmemberAlerter/NonmemberAlerter.tsx index 15aba2677..6aa154551 100644 --- a/frontend/src/components/NonmemberAlerter/NonmemberAlerter.tsx +++ b/frontend/src/components/NonmemberAlerter/NonmemberAlerter.tsx @@ -1,6 +1,5 @@ -import { useNavigate } from 'react-router-dom'; - import { Button, Flex, Modal, Text } from '@/components'; +import { useCustomNavigate } from '@/hooks'; import { END_POINTS } from '@/routes'; import { theme } from '@/style/theme'; @@ -11,7 +10,7 @@ interface Props { } const NonmemberAlerter = ({ isOpen, content, toggleModal }: Props) => { - const navigate = useNavigate(); + const navigate = useCustomNavigate(); const handleLoginButtonClick = () => { navigate(END_POINTS.LOGIN); diff --git a/frontend/src/components/ScreenReaderOnly/ScreenReaderOnly.tsx b/frontend/src/components/ScreenReaderOnly/ScreenReaderOnly.tsx new file mode 100644 index 000000000..3a49d12f3 --- /dev/null +++ b/frontend/src/components/ScreenReaderOnly/ScreenReaderOnly.tsx @@ -0,0 +1,21 @@ +const ScreenReaderOnly = () => ( + }>{children} +); + const router = createBrowserRouter([ { - errorElement: , - element: , + errorElement: ( + + + + ), + element: ( + + + + ), children: [ { path: END_POINTS.HOME, - element: , + element: ( + + + + ), }, { path: END_POINTS.MY_TEMPLATES, element: ( - + }> + }> + + + ), }, { path: END_POINTS.TEMPLATES_EXPLORE, - element: , + element: ( + + + + ), }, { path: END_POINTS.TEMPLATE, - element: , + element: ( + + + + ), }, { path: END_POINTS.TEMPLATES_UPLOAD, element: ( - + + + ), }, @@ -52,7 +83,9 @@ const router = createBrowserRouter([ path: END_POINTS.SIGNUP, element: ( - + + + ), }, @@ -60,13 +93,19 @@ const router = createBrowserRouter([ path: END_POINTS.LOGIN, element: ( - + + + ), }, { path: '*', - element: , + element: ( + + + + ), }, ], }, diff --git a/frontend/src/service/index.ts b/frontend/src/service/index.ts index 927da8040..81faf1ecc 100644 --- a/frontend/src/service/index.ts +++ b/frontend/src/service/index.ts @@ -2,6 +2,6 @@ export { validateName, validatePassword, validateConfirmPassword, - validateFileName, + validateFilename, validateSourceCode, } from './validates'; diff --git a/frontend/src/service/validates.ts b/frontend/src/service/validates.ts index 53f9f46b2..53d075378 100644 --- a/frontend/src/service/validates.ts +++ b/frontend/src/service/validates.ts @@ -26,15 +26,15 @@ export const validatePassword = (password: string) => { export const validateConfirmPassword = (password: string, confirmPassword: string) => password === confirmPassword ? '' : '비밀번호가 일치하지 않습니다.'; -export const validateFileName = (fileName: string) => { +export const validateFilename = (filename: string) => { const MAX_LENGTH = 255; const invalidChars = /[<>:"/\\|?*]/; - if (fileName.length > MAX_LENGTH) { + if (filename.length > MAX_LENGTH) { return `파일명의 길이는 ${MAX_LENGTH}자 이내로 입력해주세요!`; } - if (invalidChars.test(fileName)) { + if (invalidChars.test(filename)) { return '특수 문자 (<, >, :, ", /, , |, ?, *)는 사용할 수 없습니다!'; } @@ -46,7 +46,7 @@ export const validateSourceCode = (sourceCode: string) => { const currentByteSize = getByteSize(sourceCode); if (currentByteSize > MAX_CONTENT_SIZE) { - return `소스코드는 최대 ${MAX_CONTENT_SIZE} 바이트까지 입력할 수 있습니다!.`; + return `소스코드는 최대 ${MAX_CONTENT_SIZE} 바이트까지 입력할 수 있습니다!`; } return ''; diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index 79afbbf00..d50713e0f 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -2,6 +2,5 @@ export { formatRelativeTime } from './formatRelativeTime'; export { formatWithK } from './formatWithK'; export { getByteSize } from './getByteSize'; export { getLanguageByFilename } from './getLanguageByFileName'; -export { removeAllWhitespace } from './removeAllWhitespace'; export { remToPx } from './remToPx'; export { scroll } from './scroll'; diff --git a/frontend/src/utils/removeAllWhitespace.ts b/frontend/src/utils/removeAllWhitespace.ts deleted file mode 100644 index 376c8dbbb..000000000 --- a/frontend/src/utils/removeAllWhitespace.ts +++ /dev/null @@ -1 +0,0 @@ -export const removeAllWhitespace = (str: string) => str.replace(/\s/g, ''); diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 01923dd9f..6f4429637 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -84,5 +84,4 @@ module.exports = { }, extensions: ['.tsx', '.ts', '.js'], }, - devtool: 'source-map', }; diff --git a/frontend/webpack.prod.js b/frontend/webpack.prod.js index a62146e97..f56735d20 100644 --- a/frontend/webpack.prod.js +++ b/frontend/webpack.prod.js @@ -1,6 +1,5 @@ const { merge } = require('webpack-merge'); const Dotenv = require('dotenv-webpack'); - const common = require('./webpack.common.js'); module.exports = () => { @@ -15,5 +14,10 @@ module.exports = () => { ignoreStub: true, }), ], + optimization: { + splitChunks: { + chunks: 'all', + }, + }, }); };