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.json b/frontend/package.json index 1f1f8c971..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": { diff --git a/frontend/src/components/ContactUs/ContactUs.style.ts b/frontend/src/components/ContactUs/ContactUs.style.ts new file mode 100644 index 000000000..bf562eb14 --- /dev/null +++ b/frontend/src/components/ContactUs/ContactUs.style.ts @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +export const ContactUSButton = styled.button` + cursor: pointer; +`; + +export const Form = styled.form` + display: flex; + flex-direction: column; + gap: 1.25rem; +`; diff --git a/frontend/src/components/ContactUs/ContactUs.tsx b/frontend/src/components/ContactUs/ContactUs.tsx new file mode 100644 index 000000000..4a07e051f --- /dev/null +++ b/frontend/src/components/ContactUs/ContactUs.tsx @@ -0,0 +1,111 @@ +import { useInput, useInputWithValidate, useToggle } from '@/hooks'; +import { useToast } from '@/hooks/useToast'; +import { validateEmail } from '@/service/validates'; +import { theme } from '@/style/theme'; + +import { Button, Input, Modal, Text, Textarea } from '..'; +import * as S from './ContactUs.style'; + +const ContactUs = () => { + const [isModalOpen, toggleModal] = useToggle(); + const [message, handleMessage, resetMessage] = useInput(''); + const { + value: email, + handleChange: handleEmail, + resetValue: resetEmail, + errorMessage: emailErrorMessage, + } = useInputWithValidate('', validateEmail); + + const { failAlert, successAlert } = useToast(); + + const isValidContents = message.trim().length !== 0; + + const handleSubmit = (e: React.FormEvent | React.MouseEvent) => { + e.preventDefault(); + + if (!isValidContents || emailErrorMessage) { + return; + } + + submitForm(); + }; + + const submitForm = async () => { + const res = await sendData(); + + if (!res.ok) { + failAlert('보내기에 실패했습니다. 계속 실패한다면 이메일로 제보 부탁드립니다.'); + + return; + } + + successSubmit(); + successAlert('보내기 완료! 소중한 의견 감사합니다:)'); + }; + + const sendData = () => { + const URL = process.env.GOOGLE_URL || ''; + + return fetch(URL, { + method: 'POST', + mode: 'no-cors', + body: JSON.stringify({ message, email }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }; + + const successSubmit = () => { + resetForm(); + toggleModal(); + }; + + const resetForm = () => { + resetEmail(); + resetMessage(); + }; + + return ( + <> + + + 문의하기 + + + + 문의하기 + + + + 질문/피드백을 편하게 남겨주세요! 여러분의 의견은 더 나은 서비스를 만드는 데 큰 도움이 됩니다.
+ 이미지 등을 함께 보내실 경우 codezap2024@gmail.com으로 직접 이메일을 보내실 수 있습니다. +
+ + + 답변이 필요하시면 아래 이메일 주소를 남겨주세요. 이메일은 오직 답변을 위해서만 사용됩니다 :) + + + 이메일 (선택) + + {emailErrorMessage} + +
+
+ + + + +
+ + ); +}; + +export default ContactUs; diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx index 0d275c604..d6959bcd9 100644 --- a/frontend/src/components/Footer/Footer.tsx +++ b/frontend/src/components/Footer/Footer.tsx @@ -1,4 +1,4 @@ -import { Text } from '@/components'; +import { ContactUs, Text } from '@/components'; import { useAuth } from '@/hooks/authentication'; import * as S from './Footer.style'; @@ -19,11 +19,8 @@ const Footer = () => { {' '} © All rights reserved. - - - 문의 : - {' '} - codezap2024@gmail.com{' '} + + ); diff --git a/frontend/src/components/Header/Header.style.ts b/frontend/src/components/Header/Header.style.ts index 763bb6175..b36c8663f 100644 --- a/frontend/src/components/Header/Header.style.ts +++ b/frontend/src/components/Header/Header.style.ts @@ -17,7 +17,7 @@ export const HeaderContainer = styled.nav` padding: 0 2rem; background: white; - border-bottom: 2px solid ${theme.color.light.secondary_200}; + border-bottom: 1px solid ${theme.color.light.secondary_300}; @media (max-width: 768px) { padding: 0 1rem; @@ -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..1b478e32f 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 { Button, ContactUs, 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); @@ -56,6 +56,7 @@ const Header = ({ headerRef }: { headerRef: React.RefObject }) = {!isChecking && isLogin && } + }) = weight='bold' hoverStyle='none' onClick={handleTemplateUploadButton} + aria-description='템플릿 작성 페이지로 이동됩니다.' > - 새 템플릿 + 새 템플릿 {!isChecking && isLogin ? : } - @@ -83,24 +92,39 @@ const Header = ({ headerRef }: { headerRef: React.RefObject }) = ); }; -const Logo = () => ( - - - - 코드잽 - - -); +const Logo = () => { + const location = useLocation(); + const isLandingPage = location.pathname === '/'; -const NavOption = ({ route, name }: { route: string; name: string }) => ( - - - - {name} - - - -); + return ( + + + + + 코드잽 + + + + ); +}; + +const NavOption = ({ route, name }: { route: string; name: string }) => { + const location = useLocation(); + const isCurrentPage = location.pathname === route; + + return ( + + + + {name} + + + + ); +}; const LogoutButton = () => { const { mutateAsync } = useLogoutMutation(); @@ -125,7 +149,7 @@ const LoginButton = () => ( ); const HeaderMenuButton = ({ menuOpen, toggleMenu }: { menuOpen: boolean; toggleMenu: () => void }) => ( - + ); diff --git a/frontend/src/components/Input/Input.tsx b/frontend/src/components/Input/Input.tsx index 4bab9f54e..dd7798c3f 100644 --- a/frontend/src/components/Input/Input.tsx +++ b/frontend/src/components/Input/Input.tsx @@ -1,12 +1,6 @@ -import { - Children, - HTMLAttributes, - InputHTMLAttributes, - isValidElement, - LabelHTMLAttributes, - PropsWithChildren, - ReactNode, -} from 'react'; +import { HTMLAttributes, InputHTMLAttributes, LabelHTMLAttributes, PropsWithChildren } from 'react'; + +import { getChildOfType, getChildrenWithoutTypes } from '@/utils'; import * as S from './Input.style'; @@ -29,18 +23,6 @@ export interface AdornmentProps extends HTMLAttributes { export interface HelperTextProps extends HTMLAttributes {} -const getChildOfType = (children: ReactNode, type: unknown) => { - const childrenArray = Children.toArray(children); - - return childrenArray.find((child) => isValidElement(child) && child.type === type); -}; - -const getChildrenWithoutTypes = (children: ReactNode, types: unknown[]) => { - const childrenArray = Children.toArray(children); - - return childrenArray.filter((child) => !(isValidElement(child) && types.includes(child.type))); -}; - const TextField = ({ ...rests }: TextFieldProps) => ; const Label = ({ children, ...rests }: PropsWithChildren) => {children}; diff --git a/frontend/src/components/Modal/Modal.tsx b/frontend/src/components/Modal/Modal.tsx index 6d6f74f53..db2805232 100644 --- a/frontend/src/components/Modal/Modal.tsx +++ b/frontend/src/components/Modal/Modal.tsx @@ -1,6 +1,9 @@ import { HTMLAttributes, PropsWithChildren, ReactNode } from 'react'; import { createPortal } from 'react-dom'; +import { usePressESC } from '@/hooks/usePressESC'; +import { useScrollDisable } from '@/hooks/useScrollDisable'; + import { theme } from '../../style/theme'; import Heading from '../Heading/Heading'; import * as S from './Modal.style'; @@ -14,6 +17,9 @@ export interface BaseProps extends HTMLAttributes { } const Base = ({ isOpen, toggleModal, size = 'small', children, ...props }: PropsWithChildren) => { + usePressESC(isOpen, toggleModal); + useScrollDisable(isOpen); + if (!isOpen) { return null; } 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 = () => ( +