diff --git a/package-lock.json b/package-lock.json index 4fb7df25..7ecb2ee3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@tanstack/react-query-devtools": "^5.64.1", "@uiw/react-md-editor": "^4.0.5", "axios": "^1.7.9", + "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -41,6 +42,7 @@ "@storybook/test-runner": "^0.21.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", + "@types/crypto-js": "^4.2.2", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/styled-components": "^5.1.34", @@ -3745,6 +3747,13 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5818,6 +5827,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", diff --git a/package.json b/package.json index 2c8b312a..b785325d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@tanstack/react-query-devtools": "^5.64.1", "@uiw/react-md-editor": "^4.0.5", "axios": "^1.7.9", + "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -49,6 +50,7 @@ "@storybook/test-runner": "^0.21.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", + "@types/crypto-js": "^4.2.2", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/styled-components": "^5.1.34", diff --git a/src/components/applyComponents/careersComponent/CareersComponent.tsx b/src/components/applyComponents/careersComponent/CareersComponent.tsx index 655503c8..6f8ab5b7 100644 --- a/src/components/applyComponents/careersComponent/CareersComponent.tsx +++ b/src/components/applyComponents/careersComponent/CareersComponent.tsx @@ -38,6 +38,7 @@ const CareersComponent = ({ size='primary' schema='primary' radius='primary' + type='button' onClick={() => appendCareers({ name: '', diff --git a/src/components/applyComponents/careersComponent/careersInputComponent/CareersComponentInput.styled.ts b/src/components/applyComponents/careersComponent/careersInputComponent/CareersComponentInput.styled.ts index f9d89e81..5217b795 100644 --- a/src/components/applyComponents/careersComponent/careersInputComponent/CareersComponentInput.styled.ts +++ b/src/components/applyComponents/careersComponent/careersInputComponent/CareersComponentInput.styled.ts @@ -13,12 +13,12 @@ const dateStyle = css` export const CareerInput = styled.input` ${basicStyle} padding: 10px; - font-size: 16px; + font-size: ${({ theme }) => theme.heading.semiSmall.fontSize}; &:nth-child(1), &:nth-child(4) { flex: 2; - font-size: ${({ theme }) => theme.heading.small.fontSize}; + font-size: ${({ theme }) => theme.heading.semiSmall.fontSize}; } &:nth-child(2), @@ -29,13 +29,13 @@ export const CareerInput = styled.input` @media screen and ${({ theme }) => theme.mediaQuery.tablet} { width: 100%; margin-bottom: 12px; - font-size: 14px; + font-size: ${({ theme }) => theme.heading.small.fontSize}; } `; export const FormError = styled.p` margin-top: 0.3px; - font-size: 1rem; + font-size: ${({ theme }) => theme.heading.small.fontSize}; color: ${({ theme }) => theme.color.red}; position: absolute; top: 100%; diff --git a/src/components/applyComponents/phoneComponent/PhoneComponent.styled.ts b/src/components/applyComponents/phoneComponent/PhoneComponent.styled.ts index 52b0919b..caecef9a 100644 --- a/src/components/applyComponents/phoneComponent/PhoneComponent.styled.ts +++ b/src/components/applyComponents/phoneComponent/PhoneComponent.styled.ts @@ -10,13 +10,13 @@ export const PhoneInputContainer = styled.div` export const Dash = styled.span` align-self: center; - font-size: 25px; + font-size: ${({ theme }) => theme.heading.semiLarge.fontSize}; color: #888; `; export const FormError = styled.p` margin-top: 0.3px; - font-size: 0.9rem; + font-size: ${({ theme }) => theme.heading.small.fontSize}; color: ${({ theme }) => theme.color.red}; position: absolute; top: 115%; diff --git a/src/components/applyComponents/phoneComponent/PhoneComponent.tsx b/src/components/applyComponents/phoneComponent/PhoneComponent.tsx index 9db54192..adaee0ab 100644 --- a/src/components/applyComponents/phoneComponent/PhoneComponent.tsx +++ b/src/components/applyComponents/phoneComponent/PhoneComponent.tsx @@ -33,7 +33,15 @@ const PhoneComponent = ({ control, errors }: PhoneComponentProps) => { /> {errors.phone && ( - {String(errors?.phone[0]?.message)} + + {String( + errors?.phone[0]?.message + ? errors?.phone[0]?.message + : errors?.phone[1]?.message + ? errors?.phone[1].message + : errors?.phone[2]?.message + )} + )} ); diff --git a/src/components/applyComponents/phoneComponent/phoneComponentInput/PhoneComponentInput.styled.ts b/src/components/applyComponents/phoneComponent/phoneComponentInput/PhoneComponentInput.styled.ts index d0e56279..7a722b4e 100644 --- a/src/components/applyComponents/phoneComponent/phoneComponentInput/PhoneComponentInput.styled.ts +++ b/src/components/applyComponents/phoneComponent/phoneComponentInput/PhoneComponentInput.styled.ts @@ -6,9 +6,9 @@ export const PhoneInput = styled.input` border: 1px solid ${({ theme }) => theme.color.border}; border-radius: ${({ theme }) => theme.borderRadius.primary}; text-align: center; - font-size: 17px; + font-size: ${({ theme }) => theme.heading.semiSmall.fontSize}; @media screen and ${({ theme }) => theme.mediaQuery.tablet} { - font-size: 15px; + font-size: ${({ theme }) => theme.heading.small.fontSize}; } `; diff --git a/src/components/common/positionButton/PositionButton.tsx b/src/components/common/positionButton/PositionButton.tsx index 6d6141c3..c0cdea11 100644 --- a/src/components/common/positionButton/PositionButton.tsx +++ b/src/components/common/positionButton/PositionButton.tsx @@ -15,7 +15,11 @@ export default function PositionButton({ }: PositionButtonProps) { return ( - + {position} diff --git a/src/components/projectFormComponents/editor/MarkdownEditor.styled.ts b/src/components/projectFormComponents/editor/MarkdownEditor.styled.ts index cdd3b482..7ca7cc0d 100644 --- a/src/components/projectFormComponents/editor/MarkdownEditor.styled.ts +++ b/src/components/projectFormComponents/editor/MarkdownEditor.styled.ts @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const StyledMDEditor = styled(MDEditor)` border: 1px solid ${({ theme }) => theme.color.border}; - border-radius: ${({ theme }) => theme.borderRadius.primary}; + border-radius: ${({ theme }) => theme.borderRadius.large}; background-color: ${({ theme }) => theme.color.white}; padding: 10px; `; diff --git a/src/components/projectFormComponents/inputComponent/inputComponent.styled.ts b/src/components/projectFormComponents/inputComponent/inputComponent.styled.ts index d052beab..372ea499 100644 --- a/src/components/projectFormComponents/inputComponent/inputComponent.styled.ts +++ b/src/components/projectFormComponents/inputComponent/inputComponent.styled.ts @@ -79,4 +79,8 @@ export const FormError = styled.p` top: 115%; left: 5px; white-space: nowrap; + + @media screen and ${({ theme }) => theme.mediaQuery.tablet} { + font-size: ${({ theme }) => theme.heading.small.tabletFontSize}; + } `; diff --git a/src/components/projectFormComponents/projectInformationInput/ProjectInformationInput.styled.ts b/src/components/projectFormComponents/projectInformationInput/ProjectInformationInput.styled.ts index 7ec4c008..5aaa430f 100644 --- a/src/components/projectFormComponents/projectInformationInput/ProjectInformationInput.styled.ts +++ b/src/components/projectFormComponents/projectInformationInput/ProjectInformationInput.styled.ts @@ -6,13 +6,19 @@ export const InfoRow = styled.div` margin-bottom: 1.8rem; display: flex; - label { - font-size: 1rem; - font-weight: bold; - color: #333; - } - p { font-size: 0.8rem; } `; + +export const InfoLabel = styled.label` + font-size: ${({ theme }) => theme.heading.small.fontSize}; + font-weight: bold; + color: #333; +`; + +export const welcomeSprout = styled.p` + font-size: 1rem; + font-weight: bold; + color: ${({ theme }) => theme.color.placeholder}; +`; diff --git a/src/components/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx b/src/components/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx index 12800b74..2271d5ef 100644 --- a/src/components/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx +++ b/src/components/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx @@ -44,7 +44,7 @@ const ProjectInformationInput = ({ {PROJECT_DATA.map((input, index) => ( <> - + {input.label} + {input.type === 'checkbox' && ( + "새싹 멤버도 환영해요 !!" + )} ))} - + 진행 방식 - + 모집 분야 - + 사용 언어 { setSelectedMethod(idx); - setValue('field', idx); + setValue('field', idx + 1); }; useEffect(() => { diff --git a/src/components/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.styled.ts b/src/components/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.styled.ts index 726a10ae..989261db 100644 --- a/src/components/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.styled.ts +++ b/src/components/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.styled.ts @@ -36,7 +36,7 @@ export const PositionButtonFeat = styled(PositionButton)<{ } .name { - font-size: 0.7rem; + font-size: 0.8rem; font-weight: 200; color: ${({ isSelected, theme }) => isSelected ? theme.color.white : theme.color.primary}; diff --git a/src/hooks/useUpdateProject.ts b/src/hooks/useUpdateProject.ts index 94fb4571..f4c252d6 100644 --- a/src/hooks/useUpdateProject.ts +++ b/src/hooks/useUpdateProject.ts @@ -4,6 +4,7 @@ import { FormData } from '../models/createProject'; import { MODAL_MESSAGE } from '../constants/modalMessage'; import { ROUTES } from '../constants/routes'; import { useNavigate } from 'react-router-dom'; +import { managedProjectKey } from './queries/keys'; interface UseUpdateProjectProps { id: number; @@ -18,7 +19,7 @@ const useUpdateProject = ({ id, handleModalOpen }: UseUpdateProjectProps) => { mutationFn: (formData: FormData) => putProject(formData, id), onSuccess: () => { queryClient.invalidateQueries({ - queryKey: ['projectDataAll', id], + queryKey: [managedProjectKey.detail, id], exact: true, }); handleModalOpen(MODAL_MESSAGE.ModifyProjectSuccess); diff --git a/src/pages/apply/Apply.styled.ts b/src/pages/apply/Apply.styled.ts index 0e894941..4726ca8a 100644 --- a/src/pages/apply/Apply.styled.ts +++ b/src/pages/apply/Apply.styled.ts @@ -8,37 +8,37 @@ export const Container = styled.div` font-family: Arial, sans-serif; @media screen and ${({ theme }) => theme.mediaQuery.tablet} { - padding: 25px; + padding: 20px; } `; export const Title = styled.h1` - font-size: 32px; + font-size: 2.3rem; font-weight: bold; margin-bottom: 50px; @media screen and ${({ theme }) => theme.mediaQuery.tablet} { - font-size: 27px; + font-size: 1.8rem; } `; export const Subtitle = styled.h2` - font-size: 25px; + font-size: 1.5rem; color: ${({ theme }) => theme.color.primary}; margin-bottom: 20px; @media screen and ${({ theme }) => theme.mediaQuery.tablet} { - font-size: 22px; + font-size: 1.4rem; } `; export const Dates = styled.p` - font-size: 20px; + font-size: ${({ theme }) => theme.heading.small.fontSize}; color: ${({ theme }) => theme.color.placeholder}; margin-bottom: 50px; @media screen and ${({ theme }) => theme.mediaQuery.tablet} { - font-size: 16px; + font-size: ${({ theme }) => theme.heading.small.fontSize}; } `; @@ -56,12 +56,12 @@ export const Section = styled.div` `; export const Label = styled.label` - font-size: 20px; + font-size: 1.2rem; font-weight: bold; color: ${({ theme }) => theme.color.black}; @media screen and ${({ theme }) => theme.mediaQuery.tablet} { - font-size: 18px; + font-size: 1.1rem; } `; diff --git a/src/pages/apply/Apply.tsx b/src/pages/apply/Apply.tsx index 6b017214..7d2e56a6 100644 --- a/src/pages/apply/Apply.tsx +++ b/src/pages/apply/Apply.tsx @@ -13,6 +13,8 @@ import LoadingSpinner from '../../components/common/loadingSpinner/LoadingSpinne import Modal from '../../components/common/modal/Modal'; import { useModal } from '../../hooks/useModal'; import useApplyProject from '../../hooks/useApplyProject'; +import useAuthStore from '../../store/authStore'; +import { useEffect } from 'react'; const ApplyScheme = z.object({ email: z @@ -26,12 +28,30 @@ const ApplyScheme = z.object({ wantToSay: z.string().optional(), careers: z .array( - z.object({ - name: z.string(), - periodStart: z.string(), - periodEnd: z.string(), - role: z.string(), - }) + z + .object({ + name: z.string().nonempty({ message: '경력명을 입력해주세요.' }), + periodStart: z + .string() + .nonempty({ message: '시작 날짜를 입력해주세요.' }) + .refine((date) => !isNaN(Date.parse(date)), { + message: '유효한 날짜를 입력해주세요.', + }), + periodEnd: z + .string() + .nonempty({ message: '종료 날짜를 입력해주세요.' }) + .refine((date) => !isNaN(Date.parse(date)), { + message: '유효한 날짜를 입력해주세요.', + }), + role: z.string().nonempty({ message: '역할을 입력해주세요.' }), + }) + .refine( + (data) => new Date(data.periodStart) < new Date(data.periodEnd), + { + message: '시작 날짜는 종료 날짜보다 이전이어야 합니다.', + path: ['periodStart'], + } + ) ) .optional(), }); @@ -44,10 +64,12 @@ const Apply = () => { const { isOpen, handleModalOpen, handleModalClose, message } = useModal(); const { data: projectData, isLoading, isFetching } = useGetProjectData(id); const { applyProject } = useApplyProject({ id, handleModalOpen }); + const userEmail = useAuthStore((state) => state.userData?.email); const { handleSubmit: onSubmitHandler, formState: { errors }, control, + setValue, } = useForm({ resolver: zodResolver(ApplyScheme), defaultValues: { @@ -58,6 +80,10 @@ const Apply = () => { }, }); + useEffect(() => { + if (userEmail) setValue('email', userEmail); + }, [userEmail, setValue]); + const { fields: fieldsCareers, append: appendCareers } = useFieldArray({ name: 'careers', control, @@ -75,7 +101,11 @@ const Apply = () => { }; if (!projectData) { - return
데이터가 없습니다.
; + return ( + + {message} + + ); } if (isLoading) return ; @@ -102,7 +132,7 @@ const Apply = () => { - 전화번호 + 휴대폰 전화번호 diff --git a/src/pages/createProject/CreateProject.styled.ts b/src/pages/createProject/CreateProject.styled.ts index c2324642..ca8b21b1 100644 --- a/src/pages/createProject/CreateProject.styled.ts +++ b/src/pages/createProject/CreateProject.styled.ts @@ -42,7 +42,6 @@ export const Section = styled.div` `; export const SectionInput = styled.div` - margin-bottom: 40px; padding: 20px; border: 1px solid ${({ theme }) => theme.color.border}; border-radius: ${({ theme }) => theme.borderRadius.primary}; diff --git a/src/pages/createProject/CreateProject.tsx b/src/pages/createProject/CreateProject.tsx index fe300b4f..1885626c 100644 --- a/src/pages/createProject/CreateProject.tsx +++ b/src/pages/createProject/CreateProject.tsx @@ -9,6 +9,7 @@ import { useState } from 'react'; import Modal from '../../components/common/modal/Modal'; import { useModal } from '../../hooks/useModal'; import useCreateProject from '../../hooks/useCreateProject'; +import LoadingSpinner from '../../components/common/loadingSpinner/LoadingSpinner'; export const createProjectScheme = z.object({ startDate: z @@ -76,7 +77,7 @@ const CreateProject = () => { const [isSubmit, setIsSubmit] = useState(false); const { isOpen, message, handleModalClose, handleModalOpen } = useModal(); - const { createProject } = useCreateProject({ + const { createProject, isLoading } = useCreateProject({ handleModalOpen, setIsSubmit, }); @@ -99,6 +100,8 @@ const CreateProject = () => { createProject(formData); }; + if (isLoading) return ; + return ( 프로젝트 생성 diff --git a/src/pages/projectDetail/ProjectDetail.tsx b/src/pages/projectDetail/ProjectDetail.tsx index dd5ab25c..e09e8356 100644 --- a/src/pages/projectDetail/ProjectDetail.tsx +++ b/src/pages/projectDetail/ProjectDetail.tsx @@ -9,8 +9,6 @@ import Avatar from '../../components/common/avatar/Avatar'; import { EyeIcon } from '@heroicons/react/24/outline'; import useAuthStore from '../../store/authStore'; import { ROUTES } from '../../constants/routes'; -import { useModal } from '../../hooks/useModal'; -import Modal from '../../components/common/modal/Modal'; import LoadingSpinner from '../../components/common/loadingSpinner/LoadingSpinner'; const ProjectDetail = () => { @@ -18,7 +16,6 @@ const ProjectDetail = () => { const id = Number(projectId); const navigate = useNavigate(); const { data, isLoading, isFetching } = useGetProjectData(id); - const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); const { userData } = useAuthStore((state) => state); if (isLoading) return ; @@ -28,12 +25,7 @@ const ProjectDetail = () => { } const handleApplyClick = () => { - if (userData?.id === data.User.id) { - handleModalOpen('본인의 프로젝트는 지원할 수 없습니다.'); - return; - } else { - navigate(`${ROUTES.apply}/${id}`); - } + navigate(`${ROUTES.apply}/${id}`); }; const handleMovetoUserPage = () => { @@ -70,20 +62,19 @@ const ProjectDetail = () => { - + {userData?.id !== data.User.id ? ( + + ) : null}
- - {message} -
); }; diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 28da8a1b..f04beb0c 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import { decryptData, encryptData } from '../util/cryptoUtils'; export interface UserData { id: number; @@ -13,7 +14,7 @@ interface AuthState { storeLogin: ( accessToken: string, refreshToken: string, - userData?: UserData + userData: UserData ) => void; storeLogout: () => void; } @@ -24,6 +25,11 @@ const initialUserData: UserData = { nickname: '', }; +export const getStoredUserData = () => { + const encryptedData = localStorage.getItem('userData'); + return encryptedData ? decryptData(encryptedData) : null; +}; + export const getTokens = () => { const accessToken = localStorage.getItem('accessToken'); const refreshToken = localStorage.getItem('refreshToken'); @@ -50,9 +56,10 @@ const useAuthStore = create( storeLogin: ( accessToken: string, refreshToken: string, - userData?: UserData + userData: UserData ) => { setTokens(accessToken, refreshToken); + localStorage.setItem('userData', encryptData(userData)); set({ isLoggedIn: true, userData }); }, storeLogout: () => { diff --git a/src/util/cryptoUtils.ts b/src/util/cryptoUtils.ts new file mode 100644 index 00000000..5a650fcf --- /dev/null +++ b/src/util/cryptoUtils.ts @@ -0,0 +1,17 @@ +import CryptoJS from 'crypto-js'; +import { UserData } from '../store/authStore'; + +export const encryptData = (data: UserData) => { + return CryptoJS.AES.encrypt( + JSON.stringify(data), + `${import.meta.env.CRYPTO_SECRET_KEY}` + ).toString(); +}; + +export const decryptData = (cipherText: string) => { + const bytes = CryptoJS.AES.decrypt( + cipherText, + `${import.meta.env.CRYPTO_SECRET_KEY}` + ); + return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); +};