From c9ca2428bc481e989851ecbb243741d087ce7cd2 Mon Sep 17 00:00:00 2001 From: YIMSEBIN Date: Fri, 15 Nov 2024 04:12:47 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EB=B2=88=EC=97=AD=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../translator/Contract/contractData.ts | 23 +++++++++++-- .../translator/PostNotice/postNoticeData.ts | 32 +++++++++++++++++++ .../RegisterCompany/registerCompanyData.ts | 14 ++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/assets/translator/Contract/contractData.ts b/src/assets/translator/Contract/contractData.ts index dcf8813..7eac42c 100644 --- a/src/assets/translator/Contract/contractData.ts +++ b/src/assets/translator/Contract/contractData.ts @@ -14,7 +14,17 @@ export const contractData = { SENTENCE2: "이 계약에서 정하지 않은 사항은 '근로기준법'에서 정하는 바에 따른다.", SIGN: '서명하기', SUBMIT: '제출하기', - ERROR: '* 근로계약서에 서명해주세요!', + ERROR: { + WORKING_PLACE: '근무장소를 작성해주세요.', + RESPONSIBILITIES: '상세 업무 내용을 작성해주세요.', + WORKING_HOURS: '근로일 및 근로일별 근로시간을 작성해주세요.', + DAY_OFF: '주휴일에 대한 정보를 작성해주세요.', + SALARY: '임금을 작성해주세요.', + ANNUAL_PAID_LEAVE: '연차유급휴가에 대한 정보를 작성해주세요.', + RULE: '취업규칙을 작성해주세요.', + NUMBER: '숫자로 작성해주세요.', + SIGN: '* 근로계약서에 서명해주세요!', + }, }, [Languages.VE]: { CONTRACT: 'Hợp đồng lao động', @@ -30,6 +40,15 @@ export const contractData = { SENTENCE2: 'Các điều khoản không được quy định trong hợp đồng này sẽ được điều chỉnh theo "Luật lao động".', SIGN: 'Ký tên', SUBMIT: 'Gửi đi', - ERROR: '* Vui lòng ký vào hợp đồng lao động!', + ERROR: { + WORKING_PLACE: 'Vui lòng điền nơi làm việc.', + RESPONSIBILITIES: 'Vui lòng mô tả chi tiết công việc.', + WORKING_HOURS: 'Vui lòng ghi rõ ngày làm việc và giờ làm việc hàng ngày.', + DAY_OFF: 'Vui lòng ghi thông tin về ngày nghỉ hàng tuần.', + SALARY: 'Vui lòng điền thông tin lương.', + ANNUAL_PAID_LEAVE: 'Vui lòng ghi thông tin về nghỉ phép có lương hàng năm.', + RULE: 'Vui lòng điền quy tắc làm việc.', + SIGN: '* Vui lòng ký vào hợp đồng lao động!', + }, }, }; diff --git a/src/assets/translator/PostNotice/postNoticeData.ts b/src/assets/translator/PostNotice/postNoticeData.ts index e47a097..bc20723 100644 --- a/src/assets/translator/PostNotice/postNoticeData.ts +++ b/src/assets/translator/PostNotice/postNoticeData.ts @@ -17,6 +17,22 @@ export const postNoticeData = { REQUESTED_CAREER: '지원조건', ELIGIBILITY_CRITERIA: '비자조건', PREFERRED_CONDITIONS: '우대사항', + ERROR: { + NOTICE_TITLE: '구인글 제목을 입력해주세요.', + COMPANY_NAME: '회사명을 입력해주세요.', + EMPLOYER_NAME: '고용주 이름을 입력해주세요.', + COMPANY_SCALE: '회사 규모를 입력해주세요.', + AREA: '지역을 입력해주세요.', + SALARY: '급여를 입력해주세요.', + MAJOR_BUSINESS: '주요 업무 내용을 입력해주세요.', + WORKDURATION: '근무 기간을 입력해주세요.', + WORKDAYS: '근무 요일을 입력해주세요.', + WORKHOURS: '근무 시간을 입력해주세요.', + WORKTYPE: '고용 형태를 입력해주세요.', + REQUESTED_CAREER: '지원조건을 입력해주세요.', + ELIGIBILITY_CRITERIA: '비자 자격 요건을 입력해주세요.', + PREFERRED_CONDITIONS: '우대사항을 입력해주세요.', + }, SUBMIT: '등록하기', }, [Languages.VE]: { @@ -35,6 +51,22 @@ export const postNoticeData = { REQUESTED_CAREER: 'Yêu cầu kinh nghiệm', ELIGIBILITY_CRITERIA: 'Điều kiện về visa', PREFERRED_CONDITIONS: 'Ưu tiên', + ERROR: { + NOTICE_TITLE: 'Vui lòng nhập tiêu đề bài đăng tuyển dụng.', + COMPANY_NAME: 'Vui lòng nhập tên công ty.', + EMPLOYER_NAME: 'Vui lòng nhập tên nhà tuyển dụng.', + COMPANY_SCALE: 'Vui lòng nhập quy mô công ty.', + AREA: 'Vui lòng nhập khu vực.', + SALARY: 'Vui lòng nhập mức lương.', + MAJOR_BUSINESS: 'Vui lòng nhập nội dung công việc chính.', + WORKDURATION: 'Vui lòng nhập thời gian làm việc.', + WORKDAYS: 'Vui lòng nhập ngày làm việc.', + WORKHOURS: 'Vui lòng nhập giờ làm việc.', + WORKTYPE: 'Vui lòng nhập hình thức làm việc.', + REQUESTED_CAREER: 'Vui lòng nhập điều kiện ứng tuyển.', + ELIGIBILITY_CRITERIA: 'Vui lòng nhập yêu cầu về điều kiện visa.', + PREFERRED_CONDITIONS: 'Vui lòng nhập điều kiện ưu tiên.', + }, SUBMIT: 'Đăng ký', }, }; diff --git a/src/assets/translator/RegisterCompany/registerCompanyData.ts b/src/assets/translator/RegisterCompany/registerCompanyData.ts index 63d31bf..718289e 100644 --- a/src/assets/translator/RegisterCompany/registerCompanyData.ts +++ b/src/assets/translator/RegisterCompany/registerCompanyData.ts @@ -9,6 +9,13 @@ export const registerCompanyData = { BRAND: '브랜드', REVENUE_PERYEAR: '연 평균 매출액', SUBMIT: '등록하기', + ERROR: { + COMPANYNAME: '회사명을 입력해주세요.', + INDUSTRY_OCCUPATION: '산업/직종을 입력해주세요.', + BRAND: '브랜드명을 입력해주세요.', + REVENUE_PERYEAR: '연 매출을 입력해주세요.', + NUMBER: '숫자로 입력해주세요.', + }, }, [Languages.VE]: { TITLE: 'Đăng ký công ty', @@ -18,5 +25,12 @@ export const registerCompanyData = { BRAND: 'Thương hiệu', REVENUE_PERYEAR: 'Doanh thu hàng năm', SUBMIT: 'Đăng ký', + ERROR: { + COMPANYNAME: 'Vui lòng nhập tên công ty.', + INDUSTRY_OCCUPATION: 'Vui lòng nhập ngành nghề.', + BRAND: 'Vui lòng nhập tên thương hiệu.', + REVENUE_PERYEAR: 'Vui lòng nhập doanh thu hàng năm.', + NUMBER: 'Vui lòng nhập bằng số.', + }, }, }; From d7d38524295c9a329abf2d5d09ef89ea6fd0c22c Mon Sep 17 00:00:00 2001 From: YIMSEBIN Date: Fri, 15 Nov 2024 04:14:32 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EA=B7=BC=EB=A1=9C=EC=9E=90=20?= =?UTF-8?q?=EA=B7=BC=EB=A1=9C=EA=B3=84=EC=95=BD=EC=84=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/employeeContract.test.tsx | 66 ++++++++ .../components/ContractSection.tsx | 153 ++++++++++++++++++ .../EmployeeContract/EmployeeContract.tsx | 135 +--------------- 3 files changed, 222 insertions(+), 132 deletions(-) create mode 100644 src/features/contract/EmployeeContract/__test__/employeeContract.test.tsx create mode 100644 src/features/contract/EmployeeContract/components/ContractSection.tsx diff --git a/src/features/contract/EmployeeContract/__test__/employeeContract.test.tsx b/src/features/contract/EmployeeContract/__test__/employeeContract.test.tsx new file mode 100644 index 0000000..94cf154 --- /dev/null +++ b/src/features/contract/EmployeeContract/__test__/employeeContract.test.tsx @@ -0,0 +1,66 @@ +import { fireEvent, screen } from '@testing-library/react'; +import { server } from '@/mocks/server'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { renderWithProviders } from '@/__test__/test-utils'; +import ContractSection from '../components/ContractSection'; +import { useNavigate } from 'react-router-dom'; +import ROUTE_PATH from '@/routes/path'; + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + return { + ...(await vi.importActual('react-router-dom')), + useNavigate: vi.fn(), + }; +}); + +describe('employeeContract', () => { + beforeAll(() => { + server.listen(); + vi.mocked(useNavigate).mockImplementation(() => mockNavigate); + }); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const renderContract = () => { + return renderWithProviders(); + }; + + it('근로계약서 데이터 잘 불려와짐', async () => { + renderContract(); + + expect(screen.getByText('contract.WORKING_PLACE')).toBeInTheDocument(); + expect(screen.getByText('contract.RESPONSIBILITIES')).toBeInTheDocument(); + expect(screen.getByText('contract.WORKING_HOURS')).toBeInTheDocument(); + expect(screen.getByText('contract.DAY_OFF')).toBeInTheDocument(); + expect(screen.getByText('contract.SALARY')).toBeInTheDocument(); + expect(screen.getByText('contract.ANNUAL_PAID_LEAVE')).toBeInTheDocument(); + expect(screen.getByText('contract.RULE')).toBeInTheDocument(); + }); + + it('서명하지 않고 제출하면 메세지를 띄운다.', async () => { + const { getByRole } = renderContract(); + + fireEvent.click(getByRole('button', { name: 'contract.SUBMIT' })); + + await vi.waitFor(() => { + const errorMessage = screen.getByText('contract.ERROR.SIGN'); + expect(errorMessage).toBeInTheDocument(); + }); + }); + + it('서명 후 제출한다.', async () => { + const { getByRole } = renderContract(); + + fireEvent.click(getByRole('button', { name: 'contract.SIGN' })); + fireEvent.click(getByRole('button', { name: 'contract.SUBMIT' })); + + await vi.waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(ROUTE_PATH.HOME); + }); + }); +}); diff --git a/src/features/contract/EmployeeContract/components/ContractSection.tsx b/src/features/contract/EmployeeContract/components/ContractSection.tsx new file mode 100644 index 0000000..f44d939 --- /dev/null +++ b/src/features/contract/EmployeeContract/components/ContractSection.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react'; +import { useGetMyContract } from '@/apis/contract/hooks/useGetMyContract'; +import { Button, Typo } from '@/components/common'; +import ROUTE_PATH from '@/routes/path'; +import styled from '@emotion/styled'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import { usePostSignEmployeeContract } from '@/apis/contract/hooks/usePostEmployeeSign'; + +export type ContractResponseData = { + salary: string; + workingHours: string; + dayOff: string; + annualPaidLeave: string; + workingPlace: string; + responsibilities: string; + rule: string; +}; + +export default function ContractSection() { + const { t } = useTranslation(); + const { applyId } = useParams(); + const applicationId = Number(applyId); + const { data: contract } = useGetMyContract(applicationId); + const mutation = usePostSignEmployeeContract(); + const navigate = useNavigate(); + const contractData: ContractResponseData = contract || {}; + + const [isSigned, setIsSigned] = useState(false); + const [showSignError, setShowSignError] = useState(false); + + const handlePostSignEmployeeContract = () => { + if (!isSigned) { + setShowSignError(true); + return; + } + mutation.mutate( + { applyId: applicationId }, + { + onSuccess: () => { + navigate(ROUTE_PATH.HOME); + }, + onError: () => { + alert('값이 정상적으로 저장되지 않았습니다.'); + }, + }, + ); + }; + + const handleSign = () => { + setIsSigned(!isSigned); + setShowSignError(false); + }; + + return ( + <> + + + {t('contract.WORKING_PLACE')} + {contractData.workingPlace} + + + {t('contract.RESPONSIBILITIES')} + {contractData.responsibilities} + + + {t('contract.WORKING_HOURS')} + {contractData.workingHours} + + + {t('contract.DAY_OFF')} + {contractData.dayOff} + + + {t('contract.SALARY')} + {contractData.salary} + + + {t('contract.ANNUAL_PAID_LEAVE')} + {contractData.annualPaidLeave} + + + {t('contract.RULE')} + {contractData.rule} + + + + {t('contract.SENTENCE1')} + + + {t('contract.SENTENCE2')} + + + + {t('contract.SIGN')} + {isSigned && } + + + + {showSignError && !isSigned && {t('contract.ERROR.SIGN')}} + + ); +} + +const InputWrapper = styled.div` + margin-top: 28px; +`; + +const InputContainer = styled.div` + width: 700px; + display: flex; + align-items: center; + justify-content: space-between; + margin: 24px 0; +`; + +const ButtonWrapper = styled.div` + width: 700px; + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 52px; +`; + +const SignButton = styled(Button)<{ isSigned: boolean }>` + width: 150px; + background-color: ${({ isSigned }) => (isSigned ? '#3960d7' : 'white')}; + color: ${({ isSigned }) => (isSigned ? 'white' : 'black')}; + padding: 10px 0; + display: flex; + align-items: center; + transition: + background-color 0.3s, + color 0.3s; +`; + +const TypoWrapper = styled.span<{ isSigned: boolean }>` + margin-right: ${({ isSigned }) => (isSigned ? '8px' : '0')}; + transition: margin-right 0.3s; +`; + +const CheckIcon = styled.span` + transition: opacity 0.3s; + opacity: 1; +`; + +const ErrorText = styled.span` + color: red; + font-size: 12px; + margin-top: 8px; +`; diff --git a/src/pages/contract/EmployeeContract/EmployeeContract.tsx b/src/pages/contract/EmployeeContract/EmployeeContract.tsx index b35fcab..a1f462e 100644 --- a/src/pages/contract/EmployeeContract/EmployeeContract.tsx +++ b/src/pages/contract/EmployeeContract/EmployeeContract.tsx @@ -1,12 +1,8 @@ -import { useState } from 'react'; -import { useGetMyContract } from '@/apis/contract/hooks/useGetMyContract'; -import { Button, Flex, Typo } from '@/components/common'; +import { Flex, Typo } from '@/components/common'; import Layout from '@/features/layout'; -import ROUTE_PATH from '@/routes/path'; import styled from '@emotion/styled'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; -import { usePostSignEmployeeContract } from '@/apis/contract/hooks/usePostEmployeeSign'; +import ContractSection from '@/features/contract/EmployeeContract/components/ContractSection'; export type ContractResponseData = { salary: string; @@ -20,38 +16,6 @@ export type ContractResponseData = { export default function EmployeeContract() { const { t } = useTranslation(); - const { applyId } = useParams(); - const applicationId = Number(applyId); - const { data: contract } = useGetMyContract(applicationId); - const mutation = usePostSignEmployeeContract(); - const navigate = useNavigate(); - const contractData: ContractResponseData = contract || {}; - - const [isSigned, setIsSigned] = useState(false); - const [showSignError, setShowSignError] = useState(false); - - const handlePostSignEmployeeContract = () => { - if (!isSigned) { - setShowSignError(true); - return; - } - mutation.mutate( - { applyId: applicationId }, - { - onSuccess: () => { - navigate(ROUTE_PATH.HOME); - }, - onError: () => { - alert('값이 정상적으로 저장되지 않았습니다.'); - }, - }, - ); - }; - - const handleSign = () => { - setIsSigned(!isSigned); - setShowSignError(false); - }; return ( @@ -62,52 +26,7 @@ export default function EmployeeContract() { {t('contract.CONTRACT')} - - - {t('contract.WORKING_PLACE')} - {contractData.workingPlace} - - - {t('contract.RESPONSIBILITIES')} - {contractData.responsibilities} - - - {t('contract.WORKING_HOURS')} - {contractData.workingHours} - - - {t('contract.DAY_OFF')} - {contractData.dayOff} - - - {t('contract.SALARY')} - {contractData.salary} - - - {t('contract.ANNUAL_PAID_LEAVE')} - {contractData.annualPaidLeave} - - - {t('contract.RULE')} - {contractData.rule} - - - - {t('contract.SENTENCE1')} - - - {t('contract.SENTENCE2')} - - - - {t('contract.SIGN')} - {isSigned && } - - - - {showSignError && !isSigned && {t('contract.ERROR')}} + @@ -125,51 +44,3 @@ const LineWrapper = styled.div` rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px; `; - -const InputWrapper = styled.div` - margin-top: 28px; -`; - -const InputContainer = styled.div` - width: 700px; - display: flex; - align-items: center; - justify-content: space-between; - margin: 24px 0; -`; - -const ButtonWrapper = styled.div` - width: 700px; - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 52px; -`; - -const SignButton = styled(Button)<{ isSigned: boolean }>` - width: 150px; - background-color: ${({ isSigned }) => (isSigned ? '#3960d7' : 'white')}; - color: ${({ isSigned }) => (isSigned ? 'white' : 'black')}; - padding: 10px 0; - display: flex; - align-items: center; - transition: - background-color 0.3s, - color 0.3s; -`; - -const TypoWrapper = styled.span<{ isSigned: boolean }>` - margin-right: ${({ isSigned }) => (isSigned ? '8px' : '0')}; - transition: margin-right 0.3s; -`; - -const CheckIcon = styled.span` - transition: opacity 0.3s; - opacity: 1; -`; - -const ErrorText = styled.span` - color: red; - font-size: 12px; - margin-top: 8px; -`; From 77522d2da86a051dfa5d003ce3eb61e794ce4c95 Mon Sep 17 00:00:00 2001 From: YIMSEBIN Date: Fri, 15 Nov 2024 04:15:27 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=EA=B3=A0=EC=9A=A9=EC=A3=BC=20?= =?UTF-8?q?=EA=B7=BC=EB=A1=9C=EA=B3=84=EC=95=BD=EC=84=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/employerContract.test.tsx | 113 +++++++++ .../components/ContractRegistrationForm.tsx | 236 ++++++++++++++++++ .../EmployerContract/EmployerContract.tsx | 191 +------------- 3 files changed, 354 insertions(+), 186 deletions(-) create mode 100644 src/features/contract/EmployerContract/__test__/employerContract.test.tsx create mode 100644 src/features/contract/EmployerContract/components/ContractRegistrationForm.tsx diff --git a/src/features/contract/EmployerContract/__test__/employerContract.test.tsx b/src/features/contract/EmployerContract/__test__/employerContract.test.tsx new file mode 100644 index 0000000..3c4d5be --- /dev/null +++ b/src/features/contract/EmployerContract/__test__/employerContract.test.tsx @@ -0,0 +1,113 @@ +import { renderWithProviders } from '@/__test__/test-utils'; +import { screen } from '@testing-library/react'; +import { server } from '@/mocks/server'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import { useNavigate } from 'react-router-dom'; +import ROUTE_PATH from '@/routes/path'; +import ContractRegistrationForm from '../components/ContractRegistrationForm'; + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + return { + ...(await vi.importActual('react-router-dom')), + useNavigate: vi.fn(), + }; +}); + +describe('RegisterContract', () => { + beforeAll(() => { + server.listen(); + vi.mocked(useNavigate).mockImplementation(() => mockNavigate); + }); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const renderRegisterContract = () => { + return renderWithProviders(); + }; + + it('근로계약서 정보를 입력하고 서명 후 폼을 제출한다', async () => { + const { getByLabelText, getByRole } = renderRegisterContract(); + + const workingPlaceInput = getByLabelText('contract.WORKING_PLACE'); + const responsibilitiesInput = getByLabelText('contract.RESPONSIBILITIES'); + const workingHoursInput = getByLabelText('contract.WORKING_HOURS'); + const dayOffInput = getByLabelText('contract.DAY_OFF'); + const salaryInput = getByLabelText('contract.SALARY'); + const annualPaidLeaveInput = getByLabelText('contract.ANNUAL_PAID_LEAVE'); + const ruleInput = getByLabelText('contract.RULE'); + + fireEvent.change(workingPlaceInput, { target: { value: 'OO회사' } }); + fireEvent.change(responsibilitiesInput, { target: { value: '옷 포장' } }); + fireEvent.change(workingHoursInput, { target: { value: '주중 10:00-19:00' } }); + fireEvent.change(dayOffInput, { target: { value: '주말' } }); + fireEvent.change(salaryInput, { target: { value: 10000 } }); + fireEvent.change(annualPaidLeaveInput, { target: { value: '추후 협의' } }); + fireEvent.change(ruleInput, { target: { value: '열심히 일하기' } }); + + fireEvent.click(getByRole('button', { name: 'contract.SIGN' })); + fireEvent.click(getByRole('button', { name: 'contract.SUBMIT' })); + + await vi.waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(ROUTE_PATH.HOME); + }); + }); + + it('서명하지 않고 제출하면 메세지를 띄운다.', async () => { + const { getByLabelText, getByRole } = renderRegisterContract(); + + const workingPlaceInput = getByLabelText('contract.WORKING_PLACE'); + const responsibilitiesInput = getByLabelText('contract.RESPONSIBILITIES'); + const workingHoursInput = getByLabelText('contract.WORKING_HOURS'); + const dayOffInput = getByLabelText('contract.DAY_OFF'); + const salaryInput = getByLabelText('contract.SALARY'); + const annualPaidLeaveInput = getByLabelText('contract.ANNUAL_PAID_LEAVE'); + const ruleInput = getByLabelText('contract.RULE'); + + fireEvent.change(workingPlaceInput, { target: { value: 'OO회사' } }); + fireEvent.change(responsibilitiesInput, { target: { value: '옷 포장' } }); + fireEvent.change(workingHoursInput, { target: { value: '주중 10:00-19:00' } }); + fireEvent.change(dayOffInput, { target: { value: '주말' } }); + fireEvent.change(salaryInput, { target: { value: 10000 } }); + fireEvent.change(annualPaidLeaveInput, { target: { value: '추후 협의' } }); + fireEvent.change(ruleInput, { target: { value: '열심히 일하기' } }); + + fireEvent.click(getByRole('button', { name: 'contract.SUBMIT' })); + + await vi.waitFor(() => { + const errorMessage = screen.getByText('contract.ERROR.SIGN'); + expect(errorMessage).toBeInTheDocument(); + }); + }); + + it('데이터를 입력하지 않고 제출하면 메세지를 띄운다.', async () => { + const { getByLabelText, getByRole } = renderRegisterContract(); + + const workingPlaceInput = getByLabelText('contract.WORKING_PLACE'); + const responsibilitiesInput = getByLabelText('contract.RESPONSIBILITIES'); + const workingHoursInput = getByLabelText('contract.WORKING_HOURS'); + const dayOffInput = getByLabelText('contract.DAY_OFF'); + const salaryInput = getByLabelText('contract.SALARY'); + const annualPaidLeaveInput = getByLabelText('contract.ANNUAL_PAID_LEAVE'); + + fireEvent.change(workingPlaceInput, { target: { value: 'OO회사' } }); + fireEvent.change(responsibilitiesInput, { target: { value: '옷 포장' } }); + fireEvent.change(workingHoursInput, { target: { value: '주중 10:00-19:00' } }); + fireEvent.change(dayOffInput, { target: { value: '주말' } }); + fireEvent.change(salaryInput, { target: { value: 10000 } }); + fireEvent.change(annualPaidLeaveInput, { target: { value: '추후 협의' } }); + + fireEvent.click(getByRole('button', { name: 'contract.SIGN' })); + fireEvent.click(getByRole('button', { name: 'contract.SUBMIT' })); + + await vi.waitFor(() => { + const errorMessage = screen.getByText('contract.ERROR.RULE'); + expect(errorMessage).toBeInTheDocument(); + }); + }); +}); diff --git a/src/features/contract/EmployerContract/components/ContractRegistrationForm.tsx b/src/features/contract/EmployerContract/components/ContractRegistrationForm.tsx new file mode 100644 index 0000000..9c18202 --- /dev/null +++ b/src/features/contract/EmployerContract/components/ContractRegistrationForm.tsx @@ -0,0 +1,236 @@ +import { useFetchPostContract } from '@/apis/contract/hooks/usePostContract'; +import { Button, Input, Typo } from '@/components/common'; +import ROUTE_PATH from '@/routes/path'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; + +const default_inputs = { + salary: 0, + workingHours: '', + dayOff: '', + annualPaidLeave: '', + workingPlace: '', + responsibilities: '', + rule: '', +}; + +export default function ContractRegistrationForm() { + const { t } = useTranslation(); + const { applyId: applicationId } = useParams(); + const mutation = useFetchPostContract(); + const navigate = useNavigate(); + + const [isSigned, setIsSigned] = useState(false); + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + const [inputs, setInputs] = useState({ ...default_inputs }); + + const { salary, workingHours, dayOff, annualPaidLeave, workingPlace, responsibilities, rule } = inputs; + + const onChange = (e: React.ChangeEvent) => { + const { value, name } = e.target; + if (name === 'salary') { + if (!/^\d*$/.test(value)) { + setErrors({ + ...errors, + revenuePerYear: t('registerCompany.ERROR.NUMBER'), + }); + return; + } + } + setInputs({ + ...inputs, + [name]: value, + }); + setErrors({ + ...errors, + [name]: '', + }); + }; + + const handlePostContract = () => { + const newErrors: { [key: string]: string } = {}; + + if (!salary) newErrors.salary = t('contract.ERROR.SALARY'); + if (!workingHours) newErrors.workingHours = t('contract.ERROR.WORKING_HOURS'); + if (!dayOff) newErrors.dayOff = t('contract.ERROR.DAY_OFF'); + if (!annualPaidLeave) newErrors.annualPaidLeave = t('contract.ERROR.ANNUAL_PAID_LEAVE'); + if (!workingPlace) newErrors.workingPlace = t('contract.ERROR.WORKING_PLACE'); + if (!responsibilities) newErrors.responsibilities = t('contract.ERROR.RESPONSIBILITIES'); + if (!rule) newErrors.rule = t('contract.ERROR.RULE'); + if (!isSigned) newErrors.sign = t('contract.ERROR.SIGN'); + + setErrors(newErrors); + + if (Object.keys(newErrors).length > 0) return; + + const postData = { ...inputs, applyId: Number(`${applicationId}`) }; + + mutation.mutate(postData, { + onSuccess: () => { + navigate(ROUTE_PATH.HOME); + }, + onError: () => { + alert('값이 정상적으로 저장되지 않았습니다.'); + }, + }); + }; + + const handleSign = () => { + setIsSigned(!isSigned); + }; + + return ( + <> + + + + {errors.workingPlace && {errors.workingPlace}} + + + + {errors.responsibilities && {errors.responsibilities}} + + + + {errors.workingHours && {errors.workingHours}} + + + + {errors.dayOff && {errors.dayOff}} + + + + {errors.salary && {errors.salary}} + + + + {errors.annualPaidLeave && {errors.annualPaidLeave}} + + + + {errors.rule && {errors.rule}} + + + + {t('contract.SENTENCE1')} + + + {t('contract.SENTENCE2')} + + + + {t('contract.SIGN')} + {isSigned && } + + + + {!isSigned && errors.sign && {errors.sign}} + + ); +} + +const InputWrapper = styled.div` + margin-top: 28px; +`; + +const InputContainer = styled.div` + width: 700px; + display: flex; + align-items: center; + justify-content: space-between; + margin: 24px 0; + flex-flow: wrap; +`; + +const ButtonWrapper = styled.div` + width: 700px; + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 52px; +`; + +const SignButton = styled(Button)<{ isSigned: boolean }>` + width: 150px; + background-color: ${({ isSigned }) => (isSigned ? '#3960d7' : 'white')}; + color: ${({ isSigned }) => (isSigned ? 'white' : 'black')}; + padding: 10px 0; + display: flex; + align-items: center; + transition: + background-color 0.3s, + color 0.3s; +`; + +const TypoWrapper = styled.span<{ isSigned: boolean }>` + margin-right: ${({ isSigned }) => (isSigned ? '8px' : '0')}; + transition: margin-right 0.3s; +`; + +const CheckIcon = styled.span` + transition: opacity 0.3s; + opacity: 1; +`; + +const ErrorText = styled.span` + width: 100%; + color: red; + font-size: 12px; + margin-top: 8px; +`; + +const InputStyle = { width: '450px', height: '48px' }; diff --git a/src/pages/contract/EmployerContract/EmployerContract.tsx b/src/pages/contract/EmployerContract/EmployerContract.tsx index f25dfcc..d210bf5 100644 --- a/src/pages/contract/EmployerContract/EmployerContract.tsx +++ b/src/pages/contract/EmployerContract/EmployerContract.tsx @@ -1,61 +1,12 @@ -import { useFetchPostContract } from '@/apis/contract/hooks/usePostContract'; -import { Button, Flex, Input, Typo } from '@/components/common'; +import { Flex } from '@/components/common'; +import Title from '@/components/common/Title'; +import ContractRegistrationForm from '@/features/contract/EmployerContract/components/ContractRegistrationForm'; import Layout from '@/features/layout'; -import ROUTE_PATH from '@/routes/path'; import styled from '@emotion/styled'; -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; export default function EmployerContract() { const { t } = useTranslation(); - const { applyId: applicationId } = useParams(); - const mutation = useFetchPostContract(); - const navigate = useNavigate(); - - const [isSigned, setIsSigned] = useState(false); - const [showSignError, setShowSignError] = useState(false); - - const [inputs, setInputs] = useState({ - salary: 0, - workingHours: '', - dayOff: '', - annualPaidLeave: '', - workingPlace: '', - responsibilities: '', - rule: '', - applyId: Number(`${applicationId}`), - }); - - const { salary, workingHours, dayOff, annualPaidLeave, workingPlace, responsibilities, rule } = inputs; - - const onChange = (e: React.ChangeEvent) => { - const { value, name } = e.target; - setInputs({ - ...inputs, - [name]: value, - }); - }; - - const handlePostContract = () => { - if (!isSigned) { - setShowSignError(true); - return; - } - mutation.mutate(inputs, { - onSuccess: () => { - navigate(ROUTE_PATH.HOME); - }, - onError: () => { - alert('값이 정상적으로 저장되지 않았습니다.'); - }, - }); - }; - - const handleSign = () => { - setIsSigned(!isSigned); - setShowSignError(false); - }; return ( @@ -63,90 +14,8 @@ export default function EmployerContract() { - - {t('contract.CONTRACT')} - - - - - - - - - - - - - - - - - - - - - - - - - - {t('contract.SENTENCE1')} - - - {t('contract.SENTENCE2')} - - - - {t('contract.SIGN')} - {isSigned && } - - - - {showSignError && !isSigned && {t('contract.ERROR')}} + + <ContractRegistrationForm /> </Flex> </LineWrapper> </Flex> @@ -164,53 +33,3 @@ const LineWrapper = styled.div` rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px; `; - -const InputWrapper = styled.div` - margin-top: 28px; -`; - -const InputContainer = styled.div` - width: 700px; - display: flex; - align-items: center; - justify-content: space-between; - margin: 24px 0; -`; - -const ButtonWrapper = styled.div` - width: 700px; - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 52px; -`; - -const SignButton = styled(Button)<{ isSigned: boolean }>` - width: 150px; - background-color: ${({ isSigned }) => (isSigned ? '#3960d7' : 'white')}; - color: ${({ isSigned }) => (isSigned ? 'white' : 'black')}; - padding: 10px 0; - display: flex; - align-items: center; - transition: - background-color 0.3s, - color 0.3s; -`; - -const TypoWrapper = styled.span<{ isSigned: boolean }>` - margin-right: ${({ isSigned }) => (isSigned ? '8px' : '0')}; - transition: margin-right 0.3s; -`; - -const CheckIcon = styled.span` - transition: opacity 0.3s; - opacity: 1; -`; - -const ErrorText = styled.span` - color: red; - font-size: 12px; - margin-top: 8px; -`; - -const InputStyle = { width: '450px', height: '48px' }; From e406015354b6c85fb0f683eb86f0cf43ab71f8f7 Mon Sep 17 00:00:00 2001 From: YIMSEBIN <jij09123@gmail.com> Date: Fri, 15 Nov 2024 04:16:14 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EA=B5=AC=EC=9D=B8=EA=B8=80=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/PostNoticeForm.test.tsx | 112 +++++++ .../postNotice/components/PostNoticeForm.tsx | 290 ++++++++++++++++++ src/pages/postNotice/PostNotice.tsx | 268 +--------------- 3 files changed, 405 insertions(+), 265 deletions(-) create mode 100644 src/features/postNotice/__test__/PostNoticeForm.test.tsx create mode 100644 src/features/postNotice/components/PostNoticeForm.tsx diff --git a/src/features/postNotice/__test__/PostNoticeForm.test.tsx b/src/features/postNotice/__test__/PostNoticeForm.test.tsx new file mode 100644 index 0000000..f893763 --- /dev/null +++ b/src/features/postNotice/__test__/PostNoticeForm.test.tsx @@ -0,0 +1,112 @@ +import { renderWithProviders } from '@/__test__/test-utils'; +import { screen } from '@testing-library/react'; +import { server } from '@/mocks/server'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import { useNavigate } from 'react-router-dom'; +import ROUTE_PATH from '@/routes/path'; +import PostNoticeForm from '../components/PostNoticeForm'; + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + return { + ...(await vi.importActual('react-router-dom')), + useNavigate: vi.fn(), + }; +}); + +describe('PostNoticeForm', () => { + beforeAll(() => { + server.listen(); + vi.mocked(useNavigate).mockImplementation(() => mockNavigate); + }); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const renderRegisterContract = () => { + return renderWithProviders(<PostNoticeForm />); + }; + + it('구인글 정보를 입력하고 서명 후 폼을 제출한다', async () => { + const { getByLabelText, getByRole } = renderRegisterContract(); + + const titleInput = getByLabelText('postNotice.NOTICE_TITLE'); + const companyScaleInput = getByLabelText('postNotice.COMPANY_SCALE'); + const areaInput = getByLabelText('postNotice.AREA'); + const salaryInput = getByLabelText('postNotice.SALARY'); + const workDurationInput = getByLabelText('postNotice.WORKDURATION'); + const workDaysInput = getByLabelText('postNotice.WORKDAYS'); + const workTypeInput = getByLabelText('postNotice.WORKTYPE'); + const workHoursInput = getByLabelText('postNotice.WORKHOURS'); + const requestedCareerInput = getByLabelText('postNotice.REQUESTED_CAREER'); + const majorBusinessInput = getByLabelText('postNotice.MAJOR_BUSINESS'); + const eligibilityCriteriaInput = getByLabelText('postNotice.ELIGIBILITY_CRITERIA'); + const preferredConditionsInput = getByLabelText('postNotice.PREFERRED_CONDITIONS'); + const employerNameInput = getByLabelText('postNotice.EMPLOYER_NAME'); + const companyNameInput = getByLabelText('postNotice.COMPANY_NAME'); + + fireEvent.change(titleInput, { target: { value: '제목' } }); + fireEvent.change(companyScaleInput, { target: { value: '회사규모' } }); + fireEvent.change(areaInput, { target: { value: '회사위치' } }); + fireEvent.change(salaryInput, { target: { value: 100000 } }); + fireEvent.change(workDurationInput, { target: { value: '근무기간' } }); + fireEvent.change(workDaysInput, { target: { value: '근무요일' } }); + fireEvent.change(workTypeInput, { target: { value: '고용형태' } }); + fireEvent.change(workHoursInput, { target: { value: '근무시간' } }); + fireEvent.change(requestedCareerInput, { target: { value: '지원조건' } }); + fireEvent.change(majorBusinessInput, { target: { value: '업무내용' } }); + fireEvent.change(eligibilityCriteriaInput, { target: { value: '비자조건' } }); + fireEvent.change(preferredConditionsInput, { target: { value: '우대사항' } }); + fireEvent.change(employerNameInput, { target: { value: '담당자명' } }); + fireEvent.change(companyNameInput, { target: { value: '회사명' } }); + + fireEvent.click(getByRole('button', { name: 'postNotice.SUBMIT' })); + + await vi.waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(ROUTE_PATH.HOME); + }); + }); + + it('데이터를 입력하지 않고 제출하면 메세지를 띄운다.', async () => { + const { getByLabelText, getByRole } = renderRegisterContract(); + + const titleInput = getByLabelText('postNotice.NOTICE_TITLE'); + const companyScaleInput = getByLabelText('postNotice.COMPANY_SCALE'); + const areaInput = getByLabelText('postNotice.AREA'); + const salaryInput = getByLabelText('postNotice.SALARY'); + const workDurationInput = getByLabelText('postNotice.WORKDURATION'); + const workDaysInput = getByLabelText('postNotice.WORKDAYS'); + const workTypeInput = getByLabelText('postNotice.WORKTYPE'); + const workHoursInput = getByLabelText('postNotice.WORKHOURS'); + const requestedCareerInput = getByLabelText('postNotice.REQUESTED_CAREER'); + const majorBusinessInput = getByLabelText('postNotice.MAJOR_BUSINESS'); + const eligibilityCriteriaInput = getByLabelText('postNotice.ELIGIBILITY_CRITERIA'); + const preferredConditionsInput = getByLabelText('postNotice.PREFERRED_CONDITIONS'); + const employerNameInput = getByLabelText('postNotice.EMPLOYER_NAME'); + + fireEvent.change(titleInput, { target: { value: '제목' } }); + fireEvent.change(companyScaleInput, { target: { value: '회사규모' } }); + fireEvent.change(areaInput, { target: { value: '회사위치' } }); + fireEvent.change(salaryInput, { target: { value: 100000 } }); + fireEvent.change(workDurationInput, { target: { value: '근무기간' } }); + fireEvent.change(workDaysInput, { target: { value: '근무요일' } }); + fireEvent.change(workTypeInput, { target: { value: '고용형태' } }); + fireEvent.change(workHoursInput, { target: { value: '근무시간' } }); + fireEvent.change(requestedCareerInput, { target: { value: '지원조건' } }); + fireEvent.change(majorBusinessInput, { target: { value: '업무내용' } }); + fireEvent.change(eligibilityCriteriaInput, { target: { value: '비자조건' } }); + fireEvent.change(preferredConditionsInput, { target: { value: '우대사항' } }); + fireEvent.change(employerNameInput, { target: { value: '담당자명' } }); + + fireEvent.click(getByRole('button', { name: 'postNotice.SUBMIT' })); + + await vi.waitFor(() => { + const errorMessage = screen.getByText('postNotice.ERROR.COMPANY_NAME'); + expect(errorMessage).toBeInTheDocument(); + }); + }); +}); diff --git a/src/features/postNotice/components/PostNoticeForm.tsx b/src/features/postNotice/components/PostNoticeForm.tsx new file mode 100644 index 0000000..0f85429 --- /dev/null +++ b/src/features/postNotice/components/PostNoticeForm.tsx @@ -0,0 +1,290 @@ +import { usePostNotice } from '@/apis/postNotice/hooks/usePostNotice'; +import { Button, Input } from '@/components/common'; +import ROUTE_PATH from '@/routes/path'; +import { NoticeRequestData } from '@/types'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; + +const default_inputs: NoticeRequestData = { + title: '', + companyScale: '', + area: '', + salary: 0, + workDuration: '', + workDays: '', + workType: '', + workHours: '', + requestedCareer: '', + majorBusiness: '', + eligibilityCriteria: '', + preferredConditions: '', + employerName: '', + companyName: '', + companyId: 0, +}; + +export default function PostNoticeForm() { + const { t } = useTranslation(); + const mutation = usePostNotice(); + const navigate = useNavigate(); + const { companyId: curCompanyId } = useParams(); + + const [inputs, setInputs] = useState({ ...default_inputs }); + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + const { + title, + companyScale, + area, + salary, + workDuration, + workDays, + workType, + workHours, + requestedCareer, + majorBusiness, + eligibilityCriteria, + preferredConditions, + employerName, + companyName, + } = inputs; + + const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const { value, name } = e.target; + + if (name === 'salary') { + if (!/^\d*$/.test(value)) { + setErrors({ + ...errors, + salary: '숫자로 입력해주세요.', + }); + return; + } + } + + setInputs({ + ...inputs, + [name]: name === 'salary' ? Number(value) : value, + }); + setErrors({ + ...errors, + [name]: '', + }); + }; + + const handlePostNotice = () => { + const newErrors: { [key: string]: string } = {}; + + if (!title) newErrors.title = t('postNotice.ERROR.NOTICE_TITLE'); + if (!companyScale) newErrors.companyScale = t('postNotice.ERROR.COMPANY_SCALE'); + if (!area) newErrors.area = t('postNotice.ERROR.AREA'); + if (!salary) newErrors.salary = t('postNotice.ERROR.SALARY'); + if (!workDuration) newErrors.workDuration = t('postNotice.ERROR.WORKDURATION'); + if (!workDays) newErrors.workDays = t('postNotice.ERROR.WORKDAYS'); + if (!workType) newErrors.workType = t('postNotice.ERROR.WORKTYPE'); + if (!workHours) newErrors.workHours = t('postNotice.ERROR.WORKHOURS'); + if (!requestedCareer) newErrors.requestedCareer = t('postNotice.ERROR.REQUESTED_CAREER'); + if (!majorBusiness) newErrors.majorBusiness = t('postNotice.ERROR.MAJOR_BUSINESS'); + if (!eligibilityCriteria) newErrors.eligibilityCriteria = t('postNotice.ERROR.ELIGIBILITY_CRITERIA'); + if (!preferredConditions) newErrors.preferredConditions = t('postNotice.ERROR.PREFERRED_CONDITIONS'); + if (!employerName) newErrors.employerName = t('postNotice.ERROR.EMPLOYER_NAME'); + if (!companyName) newErrors.companyName = t('postNotice.ERROR.COMPANY_NAME'); + + setErrors(newErrors); + if (Object.keys(newErrors).length > 0) return; + + inputs.companyId = Number(curCompanyId); + + mutation.mutate(inputs, { + onSuccess: () => { + navigate(ROUTE_PATH.HOME); + }, + onError: () => { + alert('값이 정상적으로 저장되지 않았습니다.'); + }, + }); + }; + + return ( + <> + <InputContainer> + <Input + aria-label={t('postNotice.NOTICE_TITLE')} + label={t('postNotice.NOTICE_TITLE')} + name="title" + style={InputStyle} + value={title} + onChange={onChange} + /> + {errors.title && <ErrorText>{errors.title}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.COMPANY_NAME')} + label={t('postNotice.COMPANY_NAME')} + name="companyName" + style={InputStyle} + value={companyName} + onChange={onChange} + /> + {errors.companyName && <ErrorText>{errors.companyName}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.EMPLOYER_NAME')} + label={t('postNotice.EMPLOYER_NAME')} + name="employerName" + style={InputStyle} + value={employerName} + onChange={onChange} + /> + {errors.employerName && <ErrorText>{errors.employerName}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.COMPANY_SCALE')} + label={t('postNotice.COMPANY_SCALE')} + name="companyScale" + style={InputStyle} + value={companyScale} + onChange={onChange} + /> + {errors.companyScale && <ErrorText>{errors.companyScale}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.AREA')} + label={t('postNotice.AREA')} + name="area" + style={InputStyle} + value={area} + onChange={onChange} + /> + {errors.area && <ErrorText>{errors.area}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.SALARY')} + label={t('postNotice.SALARY')} + name="salary" + style={InputStyle} + value={salary} + onChange={onChange} + /> + {errors.salary && <ErrorText>{errors.salary}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.MAJOR_BUSINESS')} + label={t('postNotice.MAJOR_BUSINESS')} + name="majorBusiness" + style={InputStyle} + value={majorBusiness} + onChange={onChange} + /> + {errors.majorBusiness && <ErrorText>{errors.majorBusiness}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.WORKDURATION')} + label={t('postNotice.WORKDURATION')} + labelStyle={{ width: '100px', textAlign: 'left' }} + name="workDuration" + style={InputStyle} + value={workDuration} + onChange={onChange} + /> + {errors.workDuration && <ErrorText>{errors.workDuration}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.WORKDAYS')} + label={t('postNotice.WORKDAYS')} + name="workDays" + style={InputStyle} + value={workDays} + onChange={onChange} + /> + {errors.workDays && <ErrorText>{errors.workDays}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.WORKHOURS')} + label={t('postNotice.WORKHOURS')} + name="workHours" + style={InputStyle} + value={workHours} + onChange={onChange} + /> + {errors.workHours && <ErrorText>{errors.workHours}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.WORKTYPE')} + label={t('postNotice.WORKTYPE')} + name="workType" + style={InputStyle} + value={workType} + onChange={onChange} + /> + {errors.workType && <ErrorText>{errors.workType}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.REQUESTED_CAREER')} + label={t('postNotice.REQUESTED_CAREER')} + name="requestedCareer" + style={InputStyle} + value={requestedCareer} + onChange={onChange} + /> + {errors.requestedCareer && <ErrorText>{errors.requestedCareer}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.ELIGIBILITY_CRITERIA')} + label={t('postNotice.ELIGIBILITY_CRITERIA')} + name="eligibilityCriteria" + style={InputStyle} + value={eligibilityCriteria} + onChange={onChange} + /> + {errors.eligibilityCriteria && <ErrorText>{errors.eligibilityCriteria}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('postNotice.PREFERRED_CONDITIONS')} + label={t('postNotice.PREFERRED_CONDITIONS')} + name="preferredConditions" + style={InputStyle} + value={preferredConditions} + onChange={onChange} + /> + {errors.preferredConditions && <ErrorText>{errors.preferredConditions}</ErrorText>} + </InputContainer> + <Button onClick={handlePostNotice} design="default" style={{ marginTop: '52px' }}> + {t('postNotice.SUBMIT')} + </Button> + </> + ); +} + +const InputContainer = styled.div` + width: 700px; + align-items: start; + margin-top: 32px; + display: flex; + align-items: center; + justify-content: space-between; + flex-flow: wrap; +`; + +const InputStyle = { width: '600px', height: '48px', marginTop: '12px' }; + +const ErrorText = styled.span` + color: red; + font-size: 12px; + margin-top: 5px; +`; diff --git a/src/pages/postNotice/PostNotice.tsx b/src/pages/postNotice/PostNotice.tsx index fb88e98..f67b3cd 100644 --- a/src/pages/postNotice/PostNotice.tsx +++ b/src/pages/postNotice/PostNotice.tsx @@ -1,112 +1,11 @@ -import { usePostNotice } from '@/apis/postNotice/hooks/usePostNotice'; -import { Button, Flex, Input, Typo } from '@/components/common'; +import { Flex, Typo } from '@/components/common'; import Layout from '@/features/layout'; -import ROUTE_PATH from '@/routes/path'; -import { NoticeRequestData } from '@/types'; +import PostNoticeForm from '@/features/postNotice/components/PostNoticeForm'; import styled from '@emotion/styled'; -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; - -const default_inputs: NoticeRequestData = { - title: '', - companyScale: '', - area: '', - salary: 0, - workDuration: '', - workDays: '', - workType: '', - workHours: '', - requestedCareer: '', - majorBusiness: '', - eligibilityCriteria: '', - preferredConditions: '', - employerName: '', - companyName: '', - companyId: 0, -}; export default function PostNotice() { const { t } = useTranslation(); - const mutation = usePostNotice(); - const navigate = useNavigate(); - const { companyId: curCompanyId } = useParams(); - - const [inputs, setInputs] = useState({ ...default_inputs }); - const [errors, setErrors] = useState<{ [key: string]: string }>({}); - - const { - title, - companyScale, - area, - salary, - workDuration, - workDays, - workType, - workHours, - requestedCareer, - majorBusiness, - eligibilityCriteria, - preferredConditions, - employerName, - companyName, - } = inputs; - - const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { - const { value, name } = e.target; - - if (name === 'salary') { - if (!/^\d*$/.test(value)) { - setErrors({ - ...errors, - salary: '숫자로 입력해주세요.', - }); - return; - } - } - - setInputs({ - ...inputs, - [name]: name === 'salary' ? Number(value) : value, - }); - setErrors({ - ...errors, - [name]: '', - }); - }; - - const handlePostNotice = () => { - const newErrors: { [key: string]: string } = {}; - - if (!title) newErrors.title = '구인글 제목을 입력해주세요.'; - if (!companyScale) newErrors.companyScale = '회사 규모를 입력해주세요.'; - if (!area) newErrors.area = '지역을 입력해주세요.'; - if (!salary) newErrors.salary = '급여를 입력해주세요.'; - if (!workDuration) newErrors.workDuration = '근무 기간을 입력해주세요.'; - if (!workDays) newErrors.workDays = '근무 요일을 입력해주세요.'; - if (!workType) newErrors.workType = '근무 형태를 입력해주세요.'; - if (!workHours) newErrors.workHours = '근무 시간을 입력해주세요.'; - if (!requestedCareer) newErrors.requestedCareer = '요구 경력을 입력해주세요.'; - if (!majorBusiness) newErrors.majorBusiness = '주요 사업 내용을 입력해주세요.'; - if (!eligibilityCriteria) newErrors.eligibilityCriteria = '자격 요건을 입력해주세요.'; - if (!preferredConditions) newErrors.preferredConditions = '우대 조건을 입력해주세요.'; - if (!employerName) newErrors.employerName = '고용주 이름을 입력해주세요.'; - if (!companyName) newErrors.companyName = '회사명을 입력해주세요.'; - - setErrors(newErrors); - if (Object.keys(newErrors).length > 0) return; - - inputs.companyId = Number(curCompanyId); - - mutation.mutate(inputs, { - onSuccess: () => { - navigate(ROUTE_PATH.HOME); - }, - onError: () => { - alert('값이 정상적으로 저장되지 않았습니다.'); - }, - }); - }; return ( <Layout> @@ -117,150 +16,7 @@ export default function PostNotice() { <Typo element="h1" size="24px" style={{ fontWeight: 'bold', marginBottom: '24px' }}> {t('postNotice.TITLE')} </Typo> - <InputContainer> - <Input - label={t('postNotice.NOTICE_TITLE')} - name="title" - style={InputStyle} - value={title} - onChange={onChange} - ></Input> - {errors.title && <ErrorText>{errors.title}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.COMPANY_NAME')} - name="companyName" - style={InputStyle} - value={companyName} - onChange={onChange} - ></Input> - {errors.companyName && <ErrorText>{errors.companyName}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.EMPLOYER_NAME')} - name="employerName" - style={InputStyle} - value={employerName} - onChange={onChange} - ></Input> - {errors.employerName && <ErrorText>{errors.employerName}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.COMPANY_SCALE')} - name="companyScale" - style={InputStyle} - value={companyScale} - onChange={onChange} - ></Input> - {errors.companyScale && <ErrorText>{errors.companyScale}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.AREA')} - name="area" - style={InputStyle} - value={area} - onChange={onChange} - ></Input> - {errors.area && <ErrorText>{errors.area}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.SALARY')} - name="salary" - style={InputStyle} - value={salary} - onChange={onChange} - ></Input> - {errors.salary && <ErrorText>{errors.salary}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.MAJOR_BUSINESS')} - name="majorBusiness" - style={InputStyle} - value={majorBusiness} - onChange={onChange} - ></Input> - {errors.majorBusiness && <ErrorText>{errors.majorBusiness}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.WORKDURATION')} - labelStyle={{ width: '100px', textAlign: 'left' }} - name="workDuration" - style={InputStyle} - value={workDuration} - onChange={onChange} - ></Input> - {errors.workDuration && <ErrorText>{errors.workDuration}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.WORKDAYS')} - name="workDays" - style={InputStyle} - value={workDays} - onChange={onChange} - ></Input> - {errors.workDays && <ErrorText>{errors.workDays}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.WORKHOURS')} - name="workHours" - style={InputStyle} - value={workHours} - onChange={onChange} - ></Input> - {errors.workHours && <ErrorText>{errors.workHours}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.WORKTYPE')} - name="workType" - style={InputStyle} - value={workType} - onChange={onChange} - ></Input> - {errors.workType && <ErrorText>{errors.workType}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.REQUESTED_CAREER')} - name="requestedCareer" - style={InputStyle} - value={requestedCareer} - onChange={onChange} - ></Input> - {errors.requestedCareer && <ErrorText>{errors.requestedCareer}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.ELIGIBILITY_CRITERIA')} - name="eligibilityCriteria" - style={InputStyle} - value={eligibilityCriteria} - onChange={onChange} - ></Input> - {errors.eligibilityCriteria && <ErrorText>{errors.eligibilityCriteria}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('postNotice.PREFERRED_CONDITIONS')} - name="preferredConditions" - style={InputStyle} - value={preferredConditions} - onChange={onChange} - ></Input> - {errors.preferredConditions && <ErrorText>{errors.preferredConditions}</ErrorText>} - </InputContainer> - <Button onClick={handlePostNotice} design="default" style={{ marginTop: '52px' }}> - {t('postNotice.SUBMIT')} - </Button> + <PostNoticeForm /> </Flex> </LineWrapper> </Flex> @@ -278,21 +34,3 @@ const LineWrapper = styled.div` rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px; `; - -const InputContainer = styled.div` - width: 700px; - align-items: start; - margin-top: 32px; - display: flex; - align-items: center; - justify-content: space-between; - flex-flow: wrap; -`; - -const InputStyle = { width: '600px', height: '48px', marginTop: '12px' }; - -const ErrorText = styled.span` - color: red; - font-size: 12px; - margin-top: 5px; -`; From a78dcb52d9e855593f45ac23f1b7fb38ed60a128 Mon Sep 17 00:00:00 2001 From: YIMSEBIN <jij09123@gmail.com> Date: Fri, 15 Nov 2024 04:17:13 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=ED=9A=8C=EC=82=AC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/registerCompany.test.tsx | 72 ++++++ .../components/CompanyRegistrationForm.tsx | 223 ++++++++++++++++++ src/pages/registerCompany/RegisterCompany.tsx | 213 +---------------- 3 files changed, 298 insertions(+), 210 deletions(-) create mode 100644 src/features/registerCompany/__test__/registerCompany.test.tsx create mode 100644 src/features/registerCompany/components/CompanyRegistrationForm.tsx diff --git a/src/features/registerCompany/__test__/registerCompany.test.tsx b/src/features/registerCompany/__test__/registerCompany.test.tsx new file mode 100644 index 0000000..eadd52a --- /dev/null +++ b/src/features/registerCompany/__test__/registerCompany.test.tsx @@ -0,0 +1,72 @@ +import { renderWithProviders } from '@/__test__/test-utils'; +import { screen } from '@testing-library/react'; +import { server } from '@/mocks/server'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import { useNavigate } from 'react-router-dom'; +import ROUTE_PATH from '@/routes/path'; +import CompanyRegistrationForm from '../components/CompanyRegistrationForm'; + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + return { + ...(await vi.importActual('react-router-dom')), + useNavigate: vi.fn(), + }; +}); + +describe('RegisterCompany', () => { + beforeAll(() => { + server.listen(); + vi.mocked(useNavigate).mockImplementation(() => mockNavigate); + }); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const renderRegisterCompany = () => { + return renderWithProviders(<CompanyRegistrationForm />); + }; + + it('회사 정보를 입력하고 폼을 제출한다', async () => { + const { getByLabelText, getByRole } = renderRegisterCompany(); + + const companyNameInput = getByLabelText('registerCompany.COMPANYNAME'); + const brandInput = getByLabelText('registerCompany.BRAND'); + const industryInput = getByLabelText('registerCompany.INDUSTRY_OCCUPATION'); + const revenueInput = getByLabelText('registerCompany.REVENUE_PERYEAR'); + + fireEvent.change(companyNameInput, { target: { value: 'OO회사' } }); + fireEvent.change(brandInput, { target: { value: 'OO브랜드' } }); + fireEvent.change(industryInput, { target: { value: '의류' } }); + fireEvent.change(revenueInput, { target: { value: 100000 } }); + + fireEvent.click(getByRole('button', { name: 'registerCompany.SUBMIT' })); + + await vi.waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(ROUTE_PATH.HOME); + }); + }); + + it('데이터를 입력하지 않고 제출하면 메세지를 띄운다.', async () => { + const { getByLabelText, getByRole } = renderRegisterCompany(); + + const companyNameInput = getByLabelText('registerCompany.COMPANYNAME'); + const brandInput = getByLabelText('registerCompany.BRAND'); + const industryInput = getByLabelText('registerCompany.INDUSTRY_OCCUPATION'); + + fireEvent.change(companyNameInput, { target: { value: 'OO회사' } }); + fireEvent.change(brandInput, { target: { value: 'OO브랜드' } }); + fireEvent.change(industryInput, { target: { value: '의류' } }); + + fireEvent.click(getByRole('button', { name: 'registerCompany.SUBMIT' })); + + await vi.waitFor(() => { + const errorMessage = screen.getByText('registerCompany.ERROR.REVENUE_PERYEAR'); + expect(errorMessage).toBeInTheDocument(); + }); + }); +}); diff --git a/src/features/registerCompany/components/CompanyRegistrationForm.tsx b/src/features/registerCompany/components/CompanyRegistrationForm.tsx new file mode 100644 index 0000000..853cf34 --- /dev/null +++ b/src/features/registerCompany/components/CompanyRegistrationForm.tsx @@ -0,0 +1,223 @@ +import { CompanyRequestData, usePostCompany } from '@/apis/registerCompany/hooks/useRegisterCompany'; +import { Button, Flex, Input } from '@/components/common'; +import ROUTE_PATH from '@/routes/path'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +const default_inputs = { + name: '', + industryOccupation: '', + brand: '', + revenuePerYear: 0, + logoImage: undefined as File | undefined, +}; + +export default function CompanyRegistrationForm() { + const { t } = useTranslation(); + const mutation = usePostCompany(); + const navigate = useNavigate(); + const [inputs, setInputs] = useState<CompanyRequestData>({ ...default_inputs }); + const [file, setFile] = useState<File | null>(null); + const [accountImg, setAccountImg] = useState(''); + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + const { name, industryOccupation, brand, revenuePerYear } = inputs; + + const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const { value, name } = e.target; + + if (name === 'revenuePerYear') { + if (!/^\d*$/.test(value)) { + setErrors({ + ...errors, + revenuePerYear: t('registerCompany.ERROR.NUMBER'), + }); + return; + } + } + + setInputs({ + ...inputs, + [name]: name === 'revenuePerYear' ? Number(value) : value, + }); + setErrors({ + ...errors, + [name]: '', + }); + }; + + const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const reader = new FileReader(); + const newfile = e.target.files?.[0]; + if (newfile) { + reader.readAsDataURL(newfile); + reader.onload = () => { + if (typeof reader.result === 'string') { + setAccountImg(reader.result); + } + }; + setFile(newfile); + setErrors({ ...errors, logoImage: '' }); + } + }; + + const handlePostCompany = () => { + const newErrors: { [key: string]: string } = {}; + + if (!name) newErrors.name = t('registerCompany.ERROR.COMPANYNAME'); + if (!industryOccupation) newErrors.industryOccupation = t('registerCompany.ERROR.INDUSTRY_OCCUPATION'); + if (!brand) newErrors.brand = t('registerCompany.ERROR.BRAND'); + if (!revenuePerYear) newErrors.revenuePerYear = t('registerCompany.ERROR.REVENUE_PERYEAR'); + + setErrors(newErrors); + + if (Object.keys(newErrors).length > 0) return; + + const formData = new FormData(); + + const companyRequest = { + name: inputs.name, + industryOccupation: inputs.industryOccupation, + brand: inputs.brand, + revenuePerYear: inputs.revenuePerYear, + }; + + formData.append('companyRequest', JSON.stringify(companyRequest)); + + if (file) { + formData.append('logoImage', file); + } + + mutation.mutate(formData, { + onSuccess: () => { + navigate(ROUTE_PATH.HOME); + }, + onError: () => { + alert('submit error'); + }, + }); + }; + + return ( + <> + <InputWrapper> + <Flex direction="row" justifyContent="space-between" alignItems="center"> + <Flex + direction="column" + justifyContent="space-between" + alignItems="center" + style={{ marginRight: '30px', width: 'auto' }} + > + <InputContainer style={{ width: 'auto' }}> + <Input + aria-label={t('registerCompany.COMPANYNAME')} + label={t('registerCompany.COMPANYNAME')} + labelStyle={{ width: '150px', fontWeight: 'bold' }} + name="name" + value={name} + onChange={onChange} + style={{ width: '270px', height: '48px' }} + /> + {errors.name && <ErrorText>{errors.name}</ErrorText>} + </InputContainer> + <InputContainer style={{ width: 'auto' }}> + <Input + aria-label={t('registerCompany.BRAND')} + label={t('registerCompany.BRAND')} + labelStyle={{ width: '150px', fontWeight: 'bold' }} + name="brand" + value={brand} + onChange={onChange} + style={{ width: '270px', height: '48px' }} + /> + {errors.brand && <ErrorText>{errors.brand}</ErrorText>} + </InputContainer> + </Flex> + <Flex direction="column" justifyContent="space-between" alignItems="center" style={{ width: 'auto' }}> + <label htmlFor="imgChangeBtn" style={{ fontWeight: 'bold' }}> + {t('registerCompany.LOGOIMAGE')} + </label> + <ImgInputLabel + htmlFor="imgChangeBtn" + userImgUrl={accountImg} + style={{ width: '250px', height: '100px', border: '4px dashed #e4e5e8', marginTop: '10px' }} + ></ImgInputLabel> + {errors.logoImage && <ErrorText>{errors.logoImage}</ErrorText>} + <ImgInput + type="file" + accept="image/*" + id="imgChangeBtn" + onChange={onFileChange} + style={{ display: 'none' }} + /> + </Flex> + </Flex> + <Flex direction="column" justifyContent="space-between" alignItems="center"> + <InputContainer> + <Input + aria-label={t('registerCompany.INDUSTRY_OCCUPATION')} + label={t('registerCompany.INDUSTRY_OCCUPATION')} + labelStyle={{ width: '150px', fontWeight: 'bold' }} + name="industryOccupation" + value={industryOccupation} + onChange={onChange} + style={{ width: '550px', height: '48px' }} + ></Input> + {errors.industryOccupation && <ErrorText>{errors.industryOccupation}</ErrorText>} + </InputContainer> + <InputContainer> + <Input + aria-label={t('registerCompany.REVENUE_PERYEAR')} + label={t('registerCompany.REVENUE_PERYEAR')} + labelStyle={{ width: '150px', fontWeight: 'bold' }} + name="revenuePerYear" + value={revenuePerYear} + onChange={onChange} + style={{ width: '550px', height: '48px' }} + ></Input> + {errors.revenuePerYear && <ErrorText>{errors.revenuePerYear}</ErrorText>} + </InputContainer> + </Flex> + </InputWrapper> + <ButtonWrapper> + <Button design="default" onClick={handlePostCompany}> + {t('registerCompany.SUBMIT')} + </Button> + </ButtonWrapper> + </> + ); +} +const InputWrapper = styled.div` + width: 100%; + margin-top: 28px; +`; + +const InputContainer = styled.div` + width: 100%; + display: flex; + align-items: center; + flex-flow: wrap; + margin: 12px 0; +`; + +const ButtonWrapper = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: center; + margin-top: 52px; +`; + +const ImgInputLabel = styled.label<{ userImgUrl: string }>` + background-image: url(${({ userImgUrl }) => userImgUrl}); +`; + +const ImgInput = styled.input``; + +const ErrorText = styled.span` + color: red; + font-size: 12px; + margin-top: 5px; +`; diff --git a/src/pages/registerCompany/RegisterCompany.tsx b/src/pages/registerCompany/RegisterCompany.tsx index fc3d392..70c47a7 100644 --- a/src/pages/registerCompany/RegisterCompany.tsx +++ b/src/pages/registerCompany/RegisterCompany.tsx @@ -1,106 +1,12 @@ -import { CompanyRequestData, usePostCompany } from '@/apis/registerCompany/hooks/useRegisterCompany'; -import { Button, Flex, Input } from '@/components/common'; +import { Flex } from '@/components/common'; import Title from '@/components/common/Title'; import Layout from '@/features/layout'; -import ROUTE_PATH from '@/routes/path'; +import CompanyRegistrationForm from '@/features/registerCompany/components/CompanyRegistrationForm'; import styled from '@emotion/styled'; -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; - -const default_inputs = { - name: '', - industryOccupation: '', - brand: '', - revenuePerYear: 0, - logoImage: undefined as File | undefined, -}; export default function RegisterCompany() { const { t } = useTranslation(); - const mutation = usePostCompany(); - const navigate = useNavigate(); - const [inputs, setInputs] = useState<CompanyRequestData>({ ...default_inputs }); - const [file, setFile] = useState<File | null>(null); - const [accountImg, setAccountImg] = useState(''); - const [errors, setErrors] = useState<{ [key: string]: string }>({}); - - const { name, industryOccupation, brand, revenuePerYear } = inputs; - - const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { - const { value, name } = e.target; - - if (name === 'revenuePerYear') { - if (!/^\d*$/.test(value)) { - setErrors({ - ...errors, - revenuePerYear: '숫자로 입력해주세요.', - }); - return; - } - } - - setInputs({ - ...inputs, - [name]: name === 'revenuePerYear' ? Number(value) : value, - }); - setErrors({ - ...errors, - [name]: '', - }); - }; - - const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { - const reader = new FileReader(); - const newfile = e.target.files?.[0]; - if (newfile) { - reader.readAsDataURL(newfile); - reader.onload = () => { - if (typeof reader.result === 'string') { - setAccountImg(reader.result); - } - }; - setFile(newfile); - setErrors({ ...errors, logoImage: '' }); - } - }; - - const handlePostCompany = () => { - const newErrors: { [key: string]: string } = {}; - - if (!name) newErrors.name = '회사명을 입력해주세요.'; - if (!industryOccupation) newErrors.industryOccupation = '산업/직종을 입력해주세요.'; - if (!brand) newErrors.brand = '브랜드명을 입력해주세요.'; - if (!revenuePerYear) newErrors.revenuePerYear = '연 매출을 입력해주세요.'; - - setErrors(newErrors); - - if (Object.keys(newErrors).length > 0) return; - - const formData = new FormData(); - - const companyRequest = { - name: inputs.name, - industryOccupation: inputs.industryOccupation, - brand: inputs.brand, - revenuePerYear: inputs.revenuePerYear, - }; - - formData.append('companyRequest', JSON.stringify(companyRequest)); - - if (file) { - formData.append('logoImage', file); - } - - mutation.mutate(formData, { - onSuccess: () => { - navigate(ROUTE_PATH.HOME); - }, - onError: () => { - alert('값이 정상적으로 저장되지 않았습니다.'); - }, - }); - }; return ( <Layout> @@ -108,93 +14,13 @@ export default function RegisterCompany() { <LineWrapper style={{ backgroundColor: 'white' }}> <Flex direction="column" justifyContent="space-between" alignItems="center" style={{ width: '700px' }}> <Title text={t('registerCompany.TITLE')} /> - <InputWrapper> - <Flex direction="row" justifyContent="space-between" alignItems="center"> - <Flex - direction="column" - justifyContent="space-between" - alignItems="center" - style={{ marginRight: '30px', width: 'auto' }} - > - <InputContainer style={{ width: 'auto' }}> - <Input - label={t('registerCompany.COMPANYNAME')} - labelStyle={{ width: '150px', fontWeight: 'bold' }} - name="name" - value={name} - onChange={onChange} - style={{ width: '270px', height: '48px' }} - /> - {errors.name && <ErrorText>{errors.name}</ErrorText>} - </InputContainer> - <InputContainer style={{ width: 'auto' }}> - <Input - label={t('registerCompany.BRAND')} - labelStyle={{ width: '150px', fontWeight: 'bold' }} - name="brand" - value={brand} - onChange={onChange} - style={{ width: '270px', height: '48px' }} - /> - {errors.brand && <ErrorText>{errors.brand}</ErrorText>} - </InputContainer> - </Flex> - <Flex direction="column" justifyContent="space-between" alignItems="center" style={{ width: 'auto' }}> - <label htmlFor="imgChangeBtn" style={{ fontWeight: 'bold' }}> - {t('registerCompany.LOGOIMAGE')} - </label> - <ImgInputLabel - htmlFor="imgChangeBtn" - userImgUrl={accountImg} - style={{ width: '250px', height: '100px', border: '4px dashed #e4e5e8', marginTop: '10px' }} - ></ImgInputLabel> - {errors.logoImage && <ErrorText>{errors.logoImage}</ErrorText>} - <ImgInput - type="file" - accept="image/*" - id="imgChangeBtn" - onChange={onFileChange} - style={{ display: 'none' }} - /> - </Flex> - </Flex> - <Flex direction="column" justifyContent="space-between" alignItems="center"> - <InputContainer> - <Input - label={t('registerCompany.INDUSTRY_OCCUPATION')} - labelStyle={{ width: '150px', fontWeight: 'bold' }} - name="industryOccupation" - value={industryOccupation} - onChange={onChange} - style={{ width: '550px', height: '48px' }} - ></Input> - {errors.industryOccupation && <ErrorText>{errors.industryOccupation}</ErrorText>} - </InputContainer> - <InputContainer> - <Input - label={t('registerCompany.REVENUE_PERYEAR')} - labelStyle={{ width: '150px', fontWeight: 'bold' }} - name="revenuePerYear" - value={revenuePerYear} - onChange={onChange} - style={{ width: '550px', height: '48px' }} - ></Input> - {errors.revenuePerYear && <ErrorText>{errors.revenuePerYear}</ErrorText>} - </InputContainer> - </Flex> - </InputWrapper> - <ButtonWrapper> - <Button design="default" onClick={handlePostCompany}> - {t('registerCompany.SUBMIT')} - </Button> - </ButtonWrapper> + <CompanyRegistrationForm /> </Flex> </LineWrapper> </Flex> </Layout> ); } - const LineWrapper = styled.div` border: 1px solid #e9e9e9; border-radius: 3px; @@ -204,36 +30,3 @@ const LineWrapper = styled.div` rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px; `; - -const InputWrapper = styled.div` - width: 100%; - margin-top: 28px; -`; - -const InputContainer = styled.div` - width: 100%; - display: flex; - align-items: center; - flex-flow: wrap; - margin: 12px 0; -`; - -const ButtonWrapper = styled.div` - width: 100%; - display: flex; - align-items: center; - justify-content: center; - margin-top: 52px; -`; - -const ImgInputLabel = styled.label<{ userImgUrl: string }>` - background-image: url(${({ userImgUrl }) => userImgUrl}); -`; - -const ImgInput = styled.input``; - -const ErrorText = styled.span` - color: red; - font-size: 12px; - margin-top: 5px; -`; From a70f18de9009027c0f66a2297ac4dcfaa31f94c8 Mon Sep 17 00:00:00 2001 From: YIMSEBIN <jij09123@gmail.com> Date: Fri, 15 Nov 2024 04:18:07 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EA=B7=BC=EB=A1=9C=EC=9E=90=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../employee/__test__/MyRecruitList.test.tsx | 24 ++++++ .../employee/__test__/ProfileSection.test.tsx | 79 +++++++++++++++++++ src/pages/myPage/employee/EmployeeMyPage.tsx | 1 + 3 files changed, 104 insertions(+) create mode 100644 src/features/myPage/employee/__test__/MyRecruitList.test.tsx create mode 100644 src/features/myPage/employee/__test__/ProfileSection.test.tsx diff --git a/src/features/myPage/employee/__test__/MyRecruitList.test.tsx b/src/features/myPage/employee/__test__/MyRecruitList.test.tsx new file mode 100644 index 0000000..e855c0d --- /dev/null +++ b/src/features/myPage/employee/__test__/MyRecruitList.test.tsx @@ -0,0 +1,24 @@ +import { screen } from '@testing-library/react'; +import { server } from '@/mocks/server'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import EmployeeMyPage from '@/pages/myPage/employee/EmployeeMyPage'; +import { renderWithProviders } from '@/__test__/test-utils'; + +describe('MyRecruitList', () => { + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('공고 리스트 데이터 잘 불려와짐', async () => { + renderWithProviders(<EmployeeMyPage />); + + const recruitCards = await screen.findAllByRole('button', { + name: /근로계약서 서명하기|근로계약서 다운로드|지원서 검토중|채용 마감/, + }); + expect(recruitCards).toHaveLength(4); + }); +}); diff --git a/src/features/myPage/employee/__test__/ProfileSection.test.tsx b/src/features/myPage/employee/__test__/ProfileSection.test.tsx new file mode 100644 index 0000000..ac92a29 --- /dev/null +++ b/src/features/myPage/employee/__test__/ProfileSection.test.tsx @@ -0,0 +1,79 @@ +import { renderWithProviders } from '@/__test__/test-utils'; +import { screen } from '@testing-library/react'; +import { server } from '@/mocks/server'; +import { UserData } from '@/types'; +import { userLocalStorage } from '@/utils/storage'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import EmployeeProfile from '../components/EmployeeProfile'; +import ButtonGroup from '../components/ButtonGroup'; + +const mockEmployeeUser = { type: 'employee', profileImage: 'userProfileImage', name: '근로자이름' } as UserData; + +const mockRequiredFieldCheckFalse = { + resumeExistence: false, + visaExistence: false, + foreignerIdNumberExistence: false, + signExistence: false, +}; + +const mockRequiredFieldCheckTrue = { + resumeExistence: true, + visaExistence: true, + foreignerIdNumberExistence: true, + signExistence: true, +}; + +let mockRequiredFieldCheck = mockRequiredFieldCheckFalse; + +vi.mock('@/apis/recruitmentsDetail/useRequiredFieldCheck', () => { + return { + useGetRequiredFieldCheck: () => ({ + data: mockRequiredFieldCheck, + }), + }; +}); + +describe('ProfileSection', () => { + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('사용자 이름을 프로필 UI에 띄운다.', () => { + vi.spyOn(userLocalStorage, 'getUser').mockReturnValue(mockEmployeeUser); + + renderWithProviders(<EmployeeProfile />); + + const buttonText = screen.getByText(mockEmployeeUser.name); + expect(buttonText).toBeInTheDocument(); + }); + + it('이력서/서명/비자 등록 X -> 이력서/서명/비자 등록 버튼을 띄운다.', () => { + mockRequiredFieldCheck = mockRequiredFieldCheckFalse; + + renderWithProviders(<ButtonGroup />); + + const resumeButton = screen.getByText('employeeMyPage.REGISTER_RESUME'); + const signButton = screen.getByText('employeeMyPage.REGISTER_SIGN'); + const visaButton = screen.getByText('employeeMyPage.REGISTER_VISA'); + expect(resumeButton).toBeInTheDocument(); + expect(signButton).toBeInTheDocument(); + expect(visaButton).toBeInTheDocument(); + }); + + it('이력서/서명/비자 등록 O -> 이력서/서명/비자 등록 완료 버튼을 띄운다.', () => { + mockRequiredFieldCheck = mockRequiredFieldCheckTrue; + + renderWithProviders(<ButtonGroup />); + + const resumeButton = screen.getByText('employeeMyPage.COMPLETE_RESUME'); + const signButton = screen.getByText('employeeMyPage.COMPLETE_SIGN'); + const visaButton = screen.getByText('employeeMyPage.COMPLETE_VISA'); + expect(resumeButton).toBeInTheDocument(); + expect(signButton).toBeInTheDocument(); + expect(visaButton).toBeInTheDocument(); + }); +}); diff --git a/src/pages/myPage/employee/EmployeeMyPage.tsx b/src/pages/myPage/employee/EmployeeMyPage.tsx index 666a563..b27df6d 100644 --- a/src/pages/myPage/employee/EmployeeMyPage.tsx +++ b/src/pages/myPage/employee/EmployeeMyPage.tsx @@ -6,6 +6,7 @@ import { useGetMyApplication } from '@/apis/employee/hooks/useGetMyApplication'; import { useTranslation } from 'react-i18next'; import ProfileSection from '@/features/myPage/employee/components/ProfileSection'; import { css } from '@emotion/react'; + export default function EmployeeMyPage() { const { t } = useTranslation(); const { data: myRecruitList, isLoading } = useGetMyApplication(); From 65cf440d6b361797046c015f52790f612f1bb05e Mon Sep 17 00:00:00 2001 From: YIMSEBIN <jij09123@gmail.com> Date: Fri, 15 Nov 2024 04:46:15 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=ED=9A=8C=EC=82=AC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RegisterCompany/registerCompanyData.ts | 2 ++ .../components/CompanyRegistrationForm.tsx | 30 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/assets/translator/RegisterCompany/registerCompanyData.ts b/src/assets/translator/RegisterCompany/registerCompanyData.ts index 718289e..1292254 100644 --- a/src/assets/translator/RegisterCompany/registerCompanyData.ts +++ b/src/assets/translator/RegisterCompany/registerCompanyData.ts @@ -9,6 +9,7 @@ export const registerCompanyData = { BRAND: '브랜드', REVENUE_PERYEAR: '연 평균 매출액', SUBMIT: '등록하기', + SUBMIT_CHECK: '회사를 등록하시겠습니까?', ERROR: { COMPANYNAME: '회사명을 입력해주세요.', INDUSTRY_OCCUPATION: '산업/직종을 입력해주세요.', @@ -25,6 +26,7 @@ export const registerCompanyData = { BRAND: 'Thương hiệu', REVENUE_PERYEAR: 'Doanh thu hàng năm', SUBMIT: 'Đăng ký', + SUBMIT_CHECK: 'Bạn có muốn đăng ký công ty không?', ERROR: { COMPANYNAME: 'Vui lòng nhập tên công ty.', INDUSTRY_OCCUPATION: 'Vui lòng nhập ngành nghề.', diff --git a/src/features/registerCompany/components/CompanyRegistrationForm.tsx b/src/features/registerCompany/components/CompanyRegistrationForm.tsx index 853cf34..3a106b8 100644 --- a/src/features/registerCompany/components/CompanyRegistrationForm.tsx +++ b/src/features/registerCompany/components/CompanyRegistrationForm.tsx @@ -1,5 +1,6 @@ import { CompanyRequestData, usePostCompany } from '@/apis/registerCompany/hooks/useRegisterCompany'; -import { Button, Flex, Input } from '@/components/common'; +import { Button, Flex, Input, Modal } from '@/components/common'; +import useToggle from '@/hooks/useToggle'; import ROUTE_PATH from '@/routes/path'; import styled from '@emotion/styled'; import { useState } from 'react'; @@ -17,6 +18,7 @@ const default_inputs = { export default function CompanyRegistrationForm() { const { t } = useTranslation(); const mutation = usePostCompany(); + const [isToggle, toggle] = useToggle(); const navigate = useNavigate(); const [inputs, setInputs] = useState<CompanyRequestData>({ ...default_inputs }); const [file, setFile] = useState<File | null>(null); @@ -63,7 +65,7 @@ export default function CompanyRegistrationForm() { } }; - const handlePostCompany = () => { + const onClickSubmitButton = () => { const newErrors: { [key: string]: string } = {}; if (!name) newErrors.name = t('registerCompany.ERROR.COMPANYNAME'); @@ -74,7 +76,10 @@ export default function CompanyRegistrationForm() { setErrors(newErrors); if (Object.keys(newErrors).length > 0) return; + toggle(); + }; + const onPostCompany = () => { const formData = new FormData(); const companyRequest = { @@ -182,10 +187,17 @@ export default function CompanyRegistrationForm() { </Flex> </InputWrapper> <ButtonWrapper> - <Button design="default" onClick={handlePostCompany}> + <Button design="default" onClick={onClickSubmitButton}> {t('registerCompany.SUBMIT')} </Button> </ButtonWrapper> + {isToggle && ( + <Modal + textChildren={<ModalContainer>{t('registerCompany.SUBMIT_CHECK')}</ModalContainer>} + buttonChildren={<CustomBtn onClick={onPostCompany}>{t('registerCompany.SUBMIT')}</CustomBtn>} + onClose={toggle} + /> + )} </> ); } @@ -221,3 +233,15 @@ const ErrorText = styled.span` font-size: 12px; margin-top: 5px; `; + +const CustomBtn = styled(Button)` + background: #0a65cc; + color: white; + border: 1px solid #e4e5e8; + align-self: center; +`; + +const ModalContainer = styled.div` + font-size: 24px; + margin: 30px 30px; +`; From 208d71922960498a1a4fe05c47b5113f65caaeac Mon Sep 17 00:00:00 2001 From: YIMSEBIN <jij09123@gmail.com> Date: Fri, 15 Nov 2024 04:54:53 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EA=B5=AC=EC=9D=B8=EA=B8=80=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8B=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../translator/PostNotice/postNoticeData.ts | 2 ++ .../postNotice/components/PostNoticeForm.tsx | 33 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/assets/translator/PostNotice/postNoticeData.ts b/src/assets/translator/PostNotice/postNoticeData.ts index bc20723..6e52f55 100644 --- a/src/assets/translator/PostNotice/postNoticeData.ts +++ b/src/assets/translator/PostNotice/postNoticeData.ts @@ -33,6 +33,7 @@ export const postNoticeData = { ELIGIBILITY_CRITERIA: '비자 자격 요건을 입력해주세요.', PREFERRED_CONDITIONS: '우대사항을 입력해주세요.', }, + SUBMIT_CHECK: '구인글을 등록하시겠습니까?', SUBMIT: '등록하기', }, [Languages.VE]: { @@ -67,6 +68,7 @@ export const postNoticeData = { ELIGIBILITY_CRITERIA: 'Vui lòng nhập yêu cầu về điều kiện visa.', PREFERRED_CONDITIONS: 'Vui lòng nhập điều kiện ưu tiên.', }, + SUBMIT_CHECK: 'Bạn có muốn đăng bài tuyển dụng không?', SUBMIT: 'Đăng ký', }, }; diff --git a/src/features/postNotice/components/PostNoticeForm.tsx b/src/features/postNotice/components/PostNoticeForm.tsx index 0f85429..0d2956c 100644 --- a/src/features/postNotice/components/PostNoticeForm.tsx +++ b/src/features/postNotice/components/PostNoticeForm.tsx @@ -1,5 +1,6 @@ import { usePostNotice } from '@/apis/postNotice/hooks/usePostNotice'; -import { Button, Input } from '@/components/common'; +import { Button, Input, Modal } from '@/components/common'; +import useToggle from '@/hooks/useToggle'; import ROUTE_PATH from '@/routes/path'; import { NoticeRequestData } from '@/types'; import styled from '@emotion/styled'; @@ -33,6 +34,8 @@ export default function PostNoticeForm() { const [inputs, setInputs] = useState({ ...default_inputs }); const [errors, setErrors] = useState<{ [key: string]: string }>({}); + const [isToggle, toggle] = useToggle(); + inputs.companyId = Number(curCompanyId); const { title, @@ -74,7 +77,7 @@ export default function PostNoticeForm() { }); }; - const handlePostNotice = () => { + const onClickSubmitButton = () => { const newErrors: { [key: string]: string } = {}; if (!title) newErrors.title = t('postNotice.ERROR.NOTICE_TITLE'); @@ -94,9 +97,10 @@ export default function PostNoticeForm() { setErrors(newErrors); if (Object.keys(newErrors).length > 0) return; + toggle(); + }; - inputs.companyId = Number(curCompanyId); - + const onPostNotice = () => { mutation.mutate(inputs, { onSuccess: () => { navigate(ROUTE_PATH.HOME); @@ -264,9 +268,16 @@ export default function PostNoticeForm() { /> {errors.preferredConditions && <ErrorText>{errors.preferredConditions}</ErrorText>} </InputContainer> - <Button onClick={handlePostNotice} design="default" style={{ marginTop: '52px' }}> + <Button onClick={onClickSubmitButton} design="default" style={{ marginTop: '52px' }}> {t('postNotice.SUBMIT')} </Button> + {isToggle && ( + <Modal + textChildren={<ModalContainer>{t('postNotice.SUBMIT_CHECK')}</ModalContainer>} + buttonChildren={<CustomBtn onClick={onPostNotice}>{t('postNotice.SUBMIT')}</CustomBtn>} + onClose={toggle} + /> + )} </> ); } @@ -288,3 +299,15 @@ const ErrorText = styled.span` font-size: 12px; margin-top: 5px; `; + +const CustomBtn = styled(Button)` + background: #0a65cc; + color: white; + border: 1px solid #e4e5e8; + align-self: center; +`; + +const ModalContainer = styled.div` + font-size: 24px; + margin: 30px 30px; +`; From 7b947d09ad2780f289b12d2431a2c70113351c66 Mon Sep 17 00:00:00 2001 From: YIMSEBIN <jij09123@gmail.com> Date: Fri, 15 Nov 2024 04:59:39 +0900 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=EA=B3=A0=EC=9A=A9=EC=A3=BC=20?= =?UTF-8?q?=EA=B7=BC=EB=A1=9C=EA=B3=84=EC=95=BD=EC=84=9C=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8B=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../translator/Contract/contractData.ts | 2 ++ .../components/ContractRegistrationForm.tsx | 31 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/assets/translator/Contract/contractData.ts b/src/assets/translator/Contract/contractData.ts index 7eac42c..0e0da42 100644 --- a/src/assets/translator/Contract/contractData.ts +++ b/src/assets/translator/Contract/contractData.ts @@ -13,6 +13,7 @@ export const contractData = { SENTENCE1: '사용자와 근로자는 각자가 근로계약, 취업규칙, 단체협약을 지키고 성실하게 이행하여야 한다.', SENTENCE2: "이 계약에서 정하지 않은 사항은 '근로기준법'에서 정하는 바에 따른다.", SIGN: '서명하기', + SUBMIT_CHECK: '정말 제출하시겠습니까?', SUBMIT: '제출하기', ERROR: { WORKING_PLACE: '근무장소를 작성해주세요.', @@ -39,6 +40,7 @@ export const contractData = { 'Người sử dụng lao động và người lao động cần tuân thủ hợp đồng lao động, quy tắc lao động và thỏa thuận tập thể một cách nghiêm túc.', SENTENCE2: 'Các điều khoản không được quy định trong hợp đồng này sẽ được điều chỉnh theo "Luật lao động".', SIGN: 'Ký tên', + SUBMIT_CHECK: 'Bạn có chắc chắn muốn nộp không?', SUBMIT: 'Gửi đi', ERROR: { WORKING_PLACE: 'Vui lòng điền nơi làm việc.', diff --git a/src/features/contract/EmployerContract/components/ContractRegistrationForm.tsx b/src/features/contract/EmployerContract/components/ContractRegistrationForm.tsx index 9c18202..a4ef6a0 100644 --- a/src/features/contract/EmployerContract/components/ContractRegistrationForm.tsx +++ b/src/features/contract/EmployerContract/components/ContractRegistrationForm.tsx @@ -1,5 +1,6 @@ import { useFetchPostContract } from '@/apis/contract/hooks/usePostContract'; -import { Button, Input, Typo } from '@/components/common'; +import { Button, Input, Modal, Typo } from '@/components/common'; +import useToggle from '@/hooks/useToggle'; import ROUTE_PATH from '@/routes/path'; import styled from '@emotion/styled'; import { useState } from 'react'; @@ -22,9 +23,9 @@ export default function ContractRegistrationForm() { const mutation = useFetchPostContract(); const navigate = useNavigate(); + const [isToggle, toggle] = useToggle(); const [isSigned, setIsSigned] = useState(false); const [errors, setErrors] = useState<{ [key: string]: string }>({}); - const [inputs, setInputs] = useState({ ...default_inputs }); const { salary, workingHours, dayOff, annualPaidLeave, workingPlace, responsibilities, rule } = inputs; @@ -50,7 +51,7 @@ export default function ContractRegistrationForm() { }); }; - const handlePostContract = () => { + const onClickSubmitButton = () => { const newErrors: { [key: string]: string } = {}; if (!salary) newErrors.salary = t('contract.ERROR.SALARY'); @@ -65,7 +66,10 @@ export default function ContractRegistrationForm() { setErrors(newErrors); if (Object.keys(newErrors).length > 0) return; + toggle(); + }; + const onPostContract = () => { const postData = { ...inputs, applyId: Number(`${applicationId}`) }; mutation.mutate(postData, { @@ -174,11 +178,18 @@ export default function ContractRegistrationForm() { <TypoWrapper isSigned={isSigned}>{t('contract.SIGN')}</TypoWrapper> {isSigned && <CheckIcon>✅</CheckIcon>} </SignButton> - <Button design="default" onClick={handlePostContract}> + <Button design="default" onClick={onClickSubmitButton}> {t('contract.SUBMIT')} </Button> </ButtonWrapper> {!isSigned && errors.sign && <ErrorText>{errors.sign}</ErrorText>} + {isToggle && ( + <Modal + textChildren={<ModalContainer>{t('contract.SUBMIT_CHECK')}</ModalContainer>} + buttonChildren={<CustomBtn onClick={onPostContract}>{t('contract.SUBMIT')}</CustomBtn>} + onClose={toggle} + /> + )} </> ); } @@ -234,3 +245,15 @@ const ErrorText = styled.span` `; const InputStyle = { width: '450px', height: '48px' }; + +const CustomBtn = styled(Button)` + background: #0a65cc; + color: white; + border: 1px solid #e4e5e8; + align-self: center; +`; + +const ModalContainer = styled.div` + font-size: 24px; + margin: 30px 30px; +`;