Skip to content

Commit

Permalink
Feat/#81 구인 글 상세 화면 수정 및 API 연결 (#83)
Browse files Browse the repository at this point in the history
* 7, 8주차 산출물 (#70)

* Update README.md

* Refactor/#47 6주차 코드리뷰 리팩토링 (#48)

* refactor: separate style and prop-related constants

* refactor: remove lambda function

* refactor: restructure SignUp and RecruitmentHeader components

* feat: separate Button and Text component of SignIn

* feat: add SignUpText component

* refactor: remove auth page's barrel file

* Feat/#42, #43 지원자 목록 페이지 및 팝업 구현 (#51)

* feat: 지원자 목록 페이지 구현

* feat: 지원자 목록 페이지 스토리북 생성

* refactor: ApplicantList 테이블 컴포넌트 분리

* feat: Applicants path 설정

* feat: 지원자 목록 페이지에 계약 관련 팝업 추가

* fix: 이미지 경로 수정 및 불필요한 태그 제거

* refactor: MyAccount 페이지 구조 변경 및 CompanyRecruitments로 파일명 변경

- 기존 MyAccount 페이지를 CompanyRecruitments로 이름 변경
- 새로운 MyAccount 페이지 구현을 위해 기존 페이지의 역할 변경

* refactor: visaRegistration 관련 파일 구조 변경

- visaRegistration 페이지를 src/pages/employee에서 src/pages로 이동
- 관련 기능을 src/features/employee/visaRegistration에서 src/features/visaRegistration으로 이동

* feat: Table 컴포넌트 구현

* feat: 변경된 고용주 마이페이지 구현

* refactor: 회사 관련 공통 기능을 features/companies로 이동 및 CompanyInfo 수정

* refactor: CompanyRecruitments 페이지 이름을 MyCompany로 변경

* feat: EmployerMyAccount path 설정

* refactor: 불필요한 코드 삭제 및 폴더명 일관성 있게 변경

* Feat/#50 Select 컴포넌트 구현 (#53)

* feat: add Select component

* feat: add useGlobalSelect and useSelect

* refactor: move directory

* refactor: EmployerMyAccount 페이지에서 mock 데이터 분리 및 코드 정리

* refactor: visaRegistration 및 applicants 페이지의 mock, style 파일 분리

* refactor: RecruitmentList 컴포넌트 리팩토링 및 RecruitmentsTable 분리

* refactor: CompanyInfo 반응형 디자인 수정

* refactor: 외국인 번호 및 비자 발급 일자 등록 페이지 스타일 수정

* refactor: Header 컴포넌트의 닉네임 버튼을 사용자 프로필 이미지로 변경

* Refactor/#54 Modal 컴포넌트 재설계 (#55)

* chore: add loadable component package

* feat: implement modal management system with context and dynamic loading

* refactor: 코드 리뷰 반영

- formValid를 useMemo로 관리
- validateForeignerNumber 함수를 별도 파일로 분리

* Feat/#56 메인 페이지 API 연동 (#57)

* chore: setting mockServiceWorker

* feat: add useFetchRecruitments hooks and recruitmentsMockHandler

* feat: add useFetchSlides hooks and slidesMockHandler

* feat: add Spinner component

* feat: add AsyncBoundary component

* chore: add msw-storybook-addon

* Feat/#58 OAuth 구글 로그인 구현 (#59)

* feat: add useGoogleOAuth hook

* feat: add Loading page

* chore: add MemoryRouter to decorators

* Feat/#60 가입자 정보 선택 API 연동 (#61)

* feat: add useRegister hook

* fix: change role prop value

* style: Button 컴포넌트 Props 이름변경 theme->design

* feat: 근로자마이페이지 아이콘 설정

* feat: 근로자 마이페이지 구현

* feat: 근로자마이페이지 라우터 설정

* style: Button props 이름 변경

* feat: msw 세팅 및 API path 작성

* feat: 구인글 등록 API 연결 및 msw 세팅

* fix: 구인글 업로드 mock 핸들러 수정

* feat: 근로자 마이페이지 mock 핸들러 추가

* feat: 근로자 마이페이지 API 연결 및 msw 설정

* feat: 이력서 페이지 구현 (#63)

- react-hook-form 을 사용했습니다.
- api 명세서에 맞게 이름,주소,번호,경력,자기소개,한국어실력을 필수값으로 받게 했습니다.

Co-authored-by: kangkibong <[email protected]>

* fix: change button prop

* feat: add GitHub Actions workflow for linting and type checking

---------

Co-authored-by: yimsebin <[email protected]>
Co-authored-by: Kim Jian <[email protected]>
Co-authored-by: KimJi-An <[email protected]>
Co-authored-by: LEE YONGJIN <[email protected]>

* docs: update README.md

* docs: update README.md

* docs: update README.md

* refactor: 구인글 상세 정보 반환 페이지 수정
- 데이터 타입을 api 명세에 맞게 변경했습니다.
- recruitmentsDetail 컴포넌트의 스타일을 변경했습니다.
- 타입을 별도로 분리했습니다.

* feat: 구인글 상세화면 api 연동(msw)
- api 명세서에 맞게 요청 로직을 구현했습니다.
- msw를 통해 반환 값을 받을 수 있게 했습니다.

---------

Co-authored-by: kangkibong <[email protected]>
Co-authored-by: yimsebin <[email protected]>
Co-authored-by: Kim Jian <[email protected]>
Co-authored-by: KimJi-An <[email protected]>
  • Loading branch information
5 people authored Oct 30, 2024
1 parent 684a542 commit ed7cd75
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 108 deletions.
2 changes: 2 additions & 0 deletions src/apis/apiPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const APIPath = {
getForeigner: `${BASE_URL}/visa/:userId`,
setVisa: `${BASE_URL}/visa`,
apply: '/api/application/',
recruitmentsDetail: '/api/recruitments/:postId',
};

export const getDynamicAPIPath = {
Expand All @@ -21,4 +22,5 @@ export const getDynamicAPIPath = {
getMyApplicants: (recruitmentId: number) =>
APIPath.getMyApplicants.replace(':recruitmentId', recruitmentId.toString()),
getForeigner: (userId: number) => APIPath.getForeigner.replace(':userId', userId.toString()),
recruitmentsDetail: (postId: string) => APIPath.recruitmentsDetail.replace(':postId', postId.toString()),
};
File renamed without changes
34 changes: 34 additions & 0 deletions src/apis/recruitmentsDetail/recruitmentsDetailList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import cat from './cat.jpg';

export const recruitmentDetailList = {
koreanTitle: '김밥천국 채용 (1년 계약직)',
vietnameseTitle: 'hi',
companyScale: '대기업',
area: '대구 달서구',
requestedCareer: '경력 1~2년',
imageUrl: cat,
detailedDescription: '김밥 만들기 알바',
majorBusiness: [
{ id: 1, text: '김밥 만들기' },
{ id: 2, text: '재료 전처리' },
{ id: 3, text: '마감 청소' },
],
eligibilityCriteria: [
{ id: 1, text: '비자를 가진 사람' },
{ id: 2, text: '주 2회 이상 근무 가능한 사람' },
],
preferredConditions: [
{ id: 1, text: '한국어 의사 소통이 잘 되는 사람' },
{ id: 2, text: '지각하지 않는 사람' },
{ id: 3, text: '김밥에 있는 오이를 빼지 않는 사람' },
],
employerName: '이재용',
companyName: '삼성전자',
koreanDetailedDescription: '김밥 만들기 아르바이트',
vietnameseDetailedDescription: 'hi',
workDuration: '3달',
workDays: '주 2회',
workType: '파트타임',
workHours: '오전 11시 ~ 오후 1시',
salary: '최저시급',
};
7 changes: 7 additions & 0 deletions src/apis/recruitmentsDetail/recruitmentsDetailMockHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { APIPath } from '@/apis/apiPath';
import { recruitmentDetailList } from './recruitmentsDetailList';
import { http, HttpResponse } from 'msw';

export const recruitmentsDetailMockHandler = [
http.get(APIPath.recruitmentsDetail, () => HttpResponse.json(recruitmentDetailList)),
];
15 changes: 15 additions & 0 deletions src/apis/recruitmentsDetail/useRecruitmentsDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getDynamicAPIPath } from '@/apis/apiPath';
import { clientInstance } from '@/apis/instance';
import { useQuery } from '@tanstack/react-query';
import { type RecruitProps } from '@/pages/recruit/RecruitType';

const getRecruitmentsDetail = async (postId: string) => {
const res = await clientInstance.get(getDynamicAPIPath.recruitmentsDetail(postId));
return res.data;
};

export const useGetRecruitmentsDetail = (postId: string) =>
useQuery<RecruitProps, Error>({
queryKey: ['recruitmentsDetail', postId],
queryFn: () => getRecruitmentsDetail(postId),
});
2 changes: 2 additions & 0 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { myApplicantsMockHandler } from '@/apis/applicants/mocks/myApplicantsMoc
import { foreignerMockHandler } from '@/apis/applicants/mocks/foreignerMockHandler';
import { visaMockHandler } from '@/apis/applicants/mocks/visaMockHandler';
import { postApplyMockHandler } from '@apis/apply/postApply.mock';
import { recruitmentsDetailMockHandler } from '@apis/recruitmentsDetail/recruitmentsDetailMockHandler';

export const handlers = [
...recruitmentsMockHandler,
Expand All @@ -22,4 +23,5 @@ export const handlers = [
...foreignerMockHandler,
...visaMockHandler,
...postApplyMockHandler,
...recruitmentsDetailMockHandler,
];
17 changes: 5 additions & 12 deletions src/pages/recruit/RecruitCard.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { Button } from '@/components/common';
import { Icon } from '@components/common';
import styled from '@emotion/styled';
import type { RecruitCardProps } from './RecruitType';

export interface RecruitCardProps {
title: string;
companySize: string;
area: string;
requestedCareer: string;
companyImage?: string;
}

export default function RecruitCard({ title, companySize, area, requestedCareer, companyImage }: RecruitCardProps) {
export default function RecruitCard({ koreanTitle, companyScale, area, requestedCareer, imageUrl }: RecruitCardProps) {
return (
<RecruitContainer>
<CompanyImg alt="a" src={companyImage} />
<CompanyImg alt="companyImg" src={imageUrl} />
<Info_Div>
<Info_p>
<p>{title}</p>
<p>{`${companySize} | ${area} | ${requestedCareer}`}</p>
<p>{koreanTitle}</p>
<p>{`${companyScale} | ${area} | ${requestedCareer}`}</p>
</Info_p>
<Info_Btn>
<CustomBtn background="#0a65cc">지원하기</CustomBtn>
Expand Down
103 changes: 61 additions & 42 deletions src/pages/recruit/RecruitDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,84 @@
import { ReactNode } from 'react';
import { type SectionProps, type RecruitDetailProps } from './RecruitType';
import styled from '@emotion/styled';
import { Flex } from '@/components/common';

interface Item {
id: number;
text: string;
}
export interface RecruitDetailProps {
detailedDescription: string;
majorBusiness: Item[];
eligibilityCriteria: Item[];
preferredConditions: Item[];
companyImage?: string;
}

const SectionWithTitle = ({ title, children }: { title: string; children: ReactNode }) => (
const SectionWithTitle = ({ title, children }: SectionProps) => (
<Section>
<TitleP>{title}</TitleP>
{children}
<Flex
direction="column"
gap={{ y: '15px' }}
css={{ border: '1px solid #e9e9e9', padding: '50px 30px', borderRadius: '3px', fontSize: '20px' }}
>
{children}
</Flex>
</Section>
);

const SectionConditions = ({ title, children }: SectionProps) => (
<Flex gap={{ x: '10px' }} alignItems="center">
<p css={{ fontSize: '18px', color: '#5E6670' }}>{title}</p>
<p css={{ fontSize: '20px' }}>{children}</p>
</Flex>
);

export default function RecruitDetail({
detailedDescription,
majorBusiness,
eligibilityCriteria,
preferredConditions,
companyImage,
employerName,
companyName,
workDuration,
workDays,
workType,
workHours,
salary,
}: RecruitDetailProps) {
return (
<RecruitDetailContainer>
<CompanyImg src={companyImage} />
<SectionWithTitle title="상세 설명">
<p>{detailedDescription}</p>
<SectionWithTitle title="근무조건">
<Flex
css={{
'@media (max-width: 1024px)': {
flexDirection: 'column',
},
}}
>
<Flex direction="column" gap={{ y: '15px' }}>
<SectionConditions title="급여">{salary}</SectionConditions>
<SectionConditions title="근무기간">{workDuration}</SectionConditions>
<SectionConditions title="근무요일">{workDays}</SectionConditions>
<SectionConditions title="근무시간">{workHours}</SectionConditions>
</Flex>
<Flex
direction="column"
gap={{ y: '15px' }}
css={{
'@media (max-width: 1024px)': {
marginTop: '15px',
},
}}
>
<SectionConditions title="대표">{employerName}</SectionConditions>
<SectionConditions title="회사명">{companyName}</SectionConditions>
<SectionConditions title="고용형태">{workType}</SectionConditions>
</Flex>
</Flex>
</SectionWithTitle>
<SectionWithTitle title="주요 업무">
{majorBusiness.map((data) => {
<SectionWithTitle title="지원자격">
{eligibilityCriteria.map((data) => {
return <li key={data.id}>{data.text}</li>;
})}
</SectionWithTitle>
<SectionWithTitle title="지원 자격">
{eligibilityCriteria.map((data) => {
<SectionWithTitle title="상세설명">{detailedDescription}</SectionWithTitle>
<SectionWithTitle title="주요업무">
{majorBusiness.map((data) => {
return <li key={data.id}>{data.text}</li>;
})}
</SectionWithTitle>
<SectionWithTitle title="우대 사항">

<SectionWithTitle title="우대사항">
{preferredConditions.map((data) => {
return <li key={data.id}>{data.text}</li>;
})}
Expand All @@ -53,32 +88,16 @@ export default function RecruitDetail({
}

const RecruitDetailContainer = styled.div`
width: 80%;
width: 70%;
max-width: 1325px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 50px;
gap: 70px;
margin-bottom: 50px;
`;

const CompanyImg = styled.img<React.ImgHTMLAttributes<HTMLImageElement>>`
width: 100%;
height: 380px;
border-radius: 20px;
object-fit: cover;
margin-bottom: 30px;
@media (max-width: 768px) {
height: 300px;
margin-bottom: 25px;
}
@media (max-width: 480px) {
height: 220px;
margin-bottom: 15px;
}
`;

const Section = styled.div`
width: 100%;
display: flex;
Expand Down
38 changes: 38 additions & 0 deletions src/pages/recruit/RecruitType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ReactNode } from 'react';

export interface RecruitCardProps {
koreanTitle: string;
vietnameseTitle?: string;
companyScale: string;
area: string;
requestedCareer: string;
imageUrl: string;
}

export interface Item {
id: number;
text: string;
}

export interface RecruitDetailProps {
salary: string;
workDuration: string;
workDays: string;
workType: string;
workHours: string;
detailedDescription: string;
majorBusiness: Item[];
eligibilityCriteria: Item[];
preferredConditions: Item[];
employerName: string;
companyName: string;
koreanDetailedDescription: string;
vietnameseDetailedDescription: string;
}

export type RecruitProps = RecruitCardProps & RecruitDetailProps;

export interface SectionProps {
title: string;
children: ReactNode;
}
55 changes: 14 additions & 41 deletions src/pages/recruit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,25 @@ import styled from '@emotion/styled';
import RecruitCard from './RecruitCard';
import RecruitDetail from './RecruitDetail';
import Layout from '@/features/layout';
import cat from './cat.jpg';

const CardinitialData = {
title: '김밥천국 채용 (1년 계약직)',
companySize: '대기업',
area: '대구 달서구',
requestedCareer: '경력 1~2년',
companyImage: cat,
};
const DeatilinitialData = {
detailedDescription: '김밥 만들기 아르바이트',
majorBusiness: [
{ id: 1, text: '김밥 만들기' },
{ id: 2, text: '재료 전처리' },
{ id: 3, text: '마감 청소' },
],
eligibilityCriteria: [
{ id: 1, text: '비자를 가진 사람' },
{ id: 2, text: '주 2회 이상 근무 가능한 사람' },
],
preferredConditions: [
{ id: 1, text: '한국어 의사 소통이 잘 되는 사람' },
{ id: 2, text: '지각하지 않는 사람' },
{ id: 3, text: '김밥에 있는 오이를 빼지 않는 사람' },
],
};
import { useParams } from 'react-router-dom';
import { Spinner } from '@/components/common';
import { useGetRecruitmentsDetail } from '@/apis/recruitmentsDetail/useRecruitmentsDetail';

export default function Recruit() {
const { postId = '1' } = useParams();

const { data, isLoading } = useGetRecruitmentsDetail(postId);

if (isLoading || !data) {
return <Spinner />;
}

return (
<Layout>
<Container>
<RecruitCard
title={CardinitialData.title}
companySize={CardinitialData.companySize}
area={CardinitialData.area}
requestedCareer={CardinitialData.requestedCareer}
companyImage={cat}
/>
<RecruitCard {...data} />
<Divider />
<RecruitDetail
detailedDescription={DeatilinitialData.detailedDescription}
majorBusiness={DeatilinitialData.majorBusiness}
eligibilityCriteria={DeatilinitialData.eligibilityCriteria}
preferredConditions={DeatilinitialData.preferredConditions}
companyImage={cat}
/>
<RecruitDetail {...data} />
</Container>
</Layout>
);
Expand All @@ -64,7 +37,7 @@ const Divider = styled.div`
width: 100%;
height: 1px;
opacity: 0.7;
margin: 100px 0;
margin: 70px 0;
padding: 0 100px;
max-width: 1320px;
background-color: #e9e9e9;
Expand Down
Loading

0 comments on commit ed7cd75

Please sign in to comment.