diff --git a/src/assets/icons/RightArrowIcon.svg b/src/assets/icons/RightArrowIcon.svg new file mode 100644 index 00000000..4773e4c9 --- /dev/null +++ b/src/assets/icons/RightArrowIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Application/ApplicationCard.tsx b/src/components/Application/ApplicationCard.tsx new file mode 100644 index 00000000..39d68c7e --- /dev/null +++ b/src/components/Application/ApplicationCard.tsx @@ -0,0 +1,89 @@ +import RightArrowIcon from '@/assets/icons/RightArrowIcon.svg?react'; +import Tag from '@/components/Common/Tag'; +import { APPLICATION_STEP } from '@/constants/application'; +import { + AppicationItemType, + ApplicationStepType, +} from '@/types/application/applicationItem'; +import { useNavigate } from 'react-router-dom'; + +const statusStyler = (status: ApplicationStepType) => { + switch (status) { + case APPLICATION_STEP.APPLICATION_SUCCESS: + return 'bg-[#C7C6F6]'; + case APPLICATION_STEP.RESUME_REJECTED: + return 'bg-[#FFC6C0]'; + case APPLICATION_STEP.APPLICATION_REJECTED: + return 'bg-[#FFC6C0]'; + case APPLICATION_STEP.PENDING: + return 'bg-[#BDBDBD]'; + default: + return 'bg-[#FEF387]'; + } +}; + +type ApplicationCardType = { + applicationData: AppicationItemType; +}; + +const ApplicationCard = ({ applicationData }: ApplicationCardType) => { + const navigate = useNavigate(); + + return ( +
+
+
+

+ {applicationData.step.replace(/_/g, ' ').toLowerCase()} +

+
+
+ +
+
+
+
+
+

+ {applicationData.title} +

+

+ {applicationData.address_name} +

+
+
+
+ +

+ {applicationData.duration_of_days} Days After +

+
+
+
+ {/* TODO: 각각 공고의 id로 이동하기, 지원 상태 상세로 이동하기 */} + + +
+
+ ); +}; + +export default ApplicationCard; diff --git a/src/components/Application/ApplicationCardList.tsx b/src/components/Application/ApplicationCardList.tsx new file mode 100644 index 00000000..c1ed8a59 --- /dev/null +++ b/src/components/Application/ApplicationCardList.tsx @@ -0,0 +1,23 @@ +import ApplicationCard from '@/components/Application/ApplicationCard'; +import { AppicationItemType } from '@/types/application/applicationItem'; + +type ApplicationCardListType = { + applicationListData: AppicationItemType[]; +}; + +const ApplicationCardList = ({ + applicationListData, +}: ApplicationCardListType) => { + return ( +
+ {applicationListData.map((data) => ( + + ))} +
+ ); +}; + +export default ApplicationCardList; diff --git a/src/components/PostSearch/PostSearchSortDropdown.tsx b/src/components/Common/SearchSortDropdown.tsx similarity index 89% rename from src/components/PostSearch/PostSearchSortDropdown.tsx rename to src/components/Common/SearchSortDropdown.tsx index f0030107..ec7d2dd4 100644 --- a/src/components/PostSearch/PostSearchSortDropdown.tsx +++ b/src/components/Common/SearchSortDropdown.tsx @@ -10,7 +10,7 @@ type DropdownProps = { // DropdownModal 컴포넌트: 드롭다운 옵션을 표시하는 모달 const DropdownModal = ({ options, value, onSelect }: DropdownProps) => { return ( -
+
{options.map((option, index) => (
{ ); }; -const PostSearchSortDropdown = ({ - options, - value, - onSelect, -}: DropdownProps) => { +const SearchSortDropdown = ({ options, value, onSelect }: DropdownProps) => { const [isOpen, setIsOpen] = useState(false); const onSelectOption = (option: string) => { @@ -63,4 +59,4 @@ const PostSearchSortDropdown = ({
); }; -export default PostSearchSortDropdown; +export default SearchSortDropdown; diff --git a/src/components/PostSearch/PostSearchResult.tsx b/src/components/PostSearch/PostSearchResult.tsx index 650e8507..e5eba338 100644 --- a/src/components/PostSearch/PostSearchResult.tsx +++ b/src/components/PostSearch/PostSearchResult.tsx @@ -2,7 +2,7 @@ import NoSearchResultImg from '@/assets/images/NoSearchResultImg.png'; import { JobPostingItemType } from '@/types/common/jobPostingItem'; import JobPostingCard from '@/components/Common/JobPostingCard'; import { useState } from 'react'; -import PostSearchSortDropdown from '@/components/PostSearch/PostSearchSortDropdown'; +import SearchSortDropdown from '@/components/Common/SearchSortDropdown'; // 공고 목록 더미데이터 const JOB_POSTING_LIST: JobPostingItemType[] = [ @@ -59,7 +59,7 @@ const PostSearchResult = () => {

Search Result

- setSelectedSort(sort as SortType)} diff --git a/src/components/Profile/EditProfilePicture.tsx b/src/components/Profile/EditProfilePicture.tsx index 36044ba7..254f7102 100644 --- a/src/components/Profile/EditProfilePicture.tsx +++ b/src/components/Profile/EditProfilePicture.tsx @@ -68,8 +68,6 @@ const EditProfilePicture = ({ onChange={handlePictureChange} style={{ display: 'none' }} /> - - {imagePreviewUrl &&

Profile picture has been changed.

}
); }; diff --git a/src/components/Profile/ProfileCard.tsx b/src/components/Profile/ProfileCard.tsx index f6438d81..f5653247 100644 --- a/src/components/Profile/ProfileCard.tsx +++ b/src/components/Profile/ProfileCard.tsx @@ -25,7 +25,10 @@ const ProfileCard = ({ data }: ProfileCardProps) => { {data.first_name} {data.last_name}
{/* 생년월일 */} -
{data.birth}
+
+ {data.birth.replace(/-/g, '.')} +
+ {/* 교육 정보 */} {data.school_name === '' ? ( diff --git a/src/constants/application.ts b/src/constants/application.ts new file mode 100644 index 00000000..05cd1dfa --- /dev/null +++ b/src/constants/application.ts @@ -0,0 +1,25 @@ +export const enum APPLICATION_STEP { + RESUME_UNDER_REVIEW = 'RESUME_UNDER_REVIEW', + WAITING_FOR_INTERVIEW = 'WAITING_FOR_INTERVIEW', + FILLING_OUT_DOCUMENTS = 'FILLING_OUT_DOCUMENTS', + DOCUMENT_UNDER_REVIEW = 'DOCUMENT_UNDER_REVIEW', + APPLICATION_IN_PROGRESS = 'APPLICATION_IN_PROGRESS', + APPLICATION_SUCCESS = 'APPLICATION_SUCCESS', + APPLICATION_REJECTED = 'APPLICATION_REJECTED', + RESUME_REJECTED = 'RESUME_REJECTED', + PENDING = 'PENDING', + REGISTRATION_RESULTS = 'REGISTRATION_RESULTS', +} + +export const APPLICATION_SORT_TYPE = { + ASCENDING: 'Ascending', + DESCENDING: 'Descending', +} as const; + +export const APPLICATION_STATUS_TYPE = { + INPROGRESS: 'Inprogress', + APPLICATION_SUCCESSFUL: 'Applicatioin successful', + APPLICATION_REJECTED: 'Applicatioin rejected', + RESUME_REJECTED: 'resume rejected', + PENDING: 'pending', +} as const; diff --git a/src/constants/profile.ts b/src/constants/profile.ts index 14f3c3f1..73349895 100644 --- a/src/constants/profile.ts +++ b/src/constants/profile.ts @@ -5,4 +5,31 @@ export const enum IconType { NOTIFICATION = 'NOTIFICATION', LANGUAGE = 'LANGUAGE', LOGOUT = 'LOGOUT', +} + +export const enum GenderType { + MALE='Maile', + FEMALE='Femail', + NONE='None', +} + +export const enum VisaType{ + D_2_1='D-2-1', + D_2_2='D-2-2', + D_2_3='D-2-3', + D_2_4='D-2-4', + D_2_6='D-2-6', + D_2_7='D-2-7', + D_2_8='D-2-8', + D_4_1='D-4-1', + D_4_7='D-4-7', + F_2='F-2' +} + +export const enum NationalityType{ + SOUTH_KOREA= 'South Korea', + JAPAN= 'Japan', + CHINA= 'China', + VIETNAME= 'Vietname', + UZBEKISTAN= 'Uzbekistan', } \ No newline at end of file diff --git a/src/pages/Application/ApplicationPage.tsx b/src/pages/Application/ApplicationPage.tsx new file mode 100644 index 00000000..583b6ea2 --- /dev/null +++ b/src/pages/Application/ApplicationPage.tsx @@ -0,0 +1,95 @@ +import ApplicationCardList from '@/components/Application/ApplicationCardList'; +import BaseHeader from '@/components/Common/Header/BaseHeader'; +import SearchSortDropdown from '@/components/Common/SearchSortDropdown'; +import { + APPLICATION_SORT_TYPE, + APPLICATION_STATUS_TYPE, +} from '@/constants/application'; +import { AppicationItemType } from '@/types/application/applicationItem'; +import { useState } from 'react'; + +type SortType = + (typeof APPLICATION_SORT_TYPE)[keyof typeof APPLICATION_SORT_TYPE]; + +type StatusType = + (typeof APPLICATION_STATUS_TYPE)[keyof typeof APPLICATION_STATUS_TYPE]; + +// 더미데이터 +const APPLICATION_LIST_DATA: AppicationItemType[] = [ + { + job_posting_id: 1, + user_owner_job_posting_id: 1, + icon_img_url: 'https://example.com/icon1.png', + title: 'Frontend Developer', + address_name: '123 Tech Avenue, Seoul', + step: 'RESUME_UNDER_REVIEW', + hourly_rate: 25, + duration_of_days: 90, + }, + { + job_posting_id: 2, + user_owner_job_posting_id: 2, + icon_img_url: 'https://example.com/icon2.png', + title: 'Backend Developer', + address_name: '456 Code Street, Busan', + step: 'PENDING', + hourly_rate: 30, + duration_of_days: 120, + }, + { + job_posting_id: 3, + user_owner_job_posting_id: 3, + icon_img_url: 'https://example.com/icon3.png', + title: 'Full Stack Developer', + address_name: '789 Web Road, Incheon', + step: 'APPLICATION_REJECTED', + hourly_rate: 28, + duration_of_days: 60, + }, + { + job_posting_id: 4, + user_owner_job_posting_id: 4, + icon_img_url: 'https://example.com/icon3.png', + title: 'Full Stack Developer', + address_name: '789 Web Road, Incheon', + step: 'APPLICATION_SUCCESS', + hourly_rate: 28, + duration_of_days: 60, + }, +]; + +const ApplicationPage = () => { + const [selectedSort, setSelectedSort] = useState( + APPLICATION_SORT_TYPE.ASCENDING, + ); + const [selectedStatus, setSelectedStatus] = useState( + APPLICATION_STATUS_TYPE.INPROGRESS, + ); + + return ( + <> + +
+
+ setSelectedSort(sort as SortType)} + /> + setSelectedStatus(sort as StatusType)} + /> +
+ +
+ + ); +}; + +export default ApplicationPage; diff --git a/src/pages/Profile/EditProfilePage.tsx b/src/pages/Profile/EditProfilePage.tsx index 105302c8..9783ae42 100644 --- a/src/pages/Profile/EditProfilePage.tsx +++ b/src/pages/Profile/EditProfilePage.tsx @@ -1,25 +1,35 @@ import Button from '@/components/Common/Button'; +import Dropdown from '@/components/Common/Dropdown'; import BaseHeader from '@/components/Common/Header/BaseHeader'; import Input from '@/components/Common/Input'; +import RadioButton from '@/components/Information/RadioButton'; import EditProfilePicture from '@/components/Profile/EditProfilePicture'; import { buttonTypeKeys } from '@/constants/components'; -import { UserEditProfileDataType } from '@/types/api/profile'; +import { GenderType, NationalityType, VisaType } from '@/constants/profile'; +import { UserProfileDetailDataType } from '@/types/api/profile'; import { InputType } from '@/types/common/input'; +import { transformToEditProfileData } from '@/utils/editProfileData'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { country, phone, visa } from '@/constants/information'; const EditProfilePage = () => { const navigate = useNavigate(); - const [userData, setUserData] = useState( - undefined, - ); + const [userData, setUserData] = useState< + UserProfileDetailDataType | undefined + >(undefined); const [profileImage, setProfileImage] = useState(null); + const [phoneNum, setPhoneNum] = useState({ + start: '', + middle: '', + end: '', + }); const handleInputChange = - (field: keyof UserEditProfileDataType) => (value: string) => { + (field: keyof UserProfileDetailDataType) => (value: string) => { setUserData((prevData) => { if (!prevData) { - return { [field]: value } as UserEditProfileDataType; + return { [field]: value } as UserProfileDetailDataType; } return { ...prevData, @@ -36,24 +46,69 @@ const EditProfilePage = () => { } }; - const handleSubmit = () => { - // API - 3.5 (유학생) 프로필 수정 - console.log('file 변경 - ' + profileImage); + const handleSubmit = async (): Promise => { + // TODO : API - 3.5 (유학생) 프로필 수정 + if (!userData) return; navigate('/profile'); + + // get -> patch 데이터 변환 + const transformedData = transformToEditProfileData( + userData, + phoneNum, + profileImage, + Boolean(profileImage), // 이미지 변경 여부 확인 + ); + // console.log('transformedData: ', transformedData); + try { + const formData = new FormData(); + + // 이미지가 있을 경우 FormData에 추가 + if (transformedData.image) { + formData.append('image', transformedData.image); + } + + // JSON 데이터를 Blob으로 변환 후 FormData에 추가 + formData.append( + 'body', + new Blob([JSON.stringify(transformedData.body)], { + type: 'application/json', + }), + ); + /* + const response = await axios.patch('/api/v1/users', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + console.log('API 성공:', response.data); + */ + navigate('/profile'); + } catch (error) { + console.error('API 호출 중 에러 발생:', error); + } }; + // 전화번호를 3개의 파트로 구분 useEffect(() => { + if (userData?.phone_number) { + const [start, middle, end] = userData.phone_number.split('-'); + setPhoneNum({ start, middle, end }); + } + }, [userData]); + + useEffect(() => { + // TODO : API - 3.1 (유학생) 유저 프로필 조회하기 setUserData({ profile_img_url: 'https://images.pexels.com/photos/1458926/pexels-photo-1458926.jpeg?cs=srgb&dl=pexels-poodles2doodles-1458926.jpg&fm=jpg', first_name: 'Hyeona', last_name: 'Seol', - birth: '0000.00.00', - gender: 'Female', - nationality: 'Korea', - visa_satus: 'D_2_7', - telephone_number: '010-1111-2222', + birth: '2001-02-09', + gender: GenderType.FEMALE, + nationality: NationalityType.SOUTH_KOREA, + visa: VisaType.D_2_1, + phone_number: '010-1111-2222', }); }, []); @@ -72,65 +127,155 @@ const EditProfilePage = () => { profileImgUrl={userData.profile_img_url} onImageUpdate={setProfileImage} /> - - - - - - - -
-
+ {/* 성 작성 */} +
+
+ Last Name +
+ +
+
+
+ Gender +
+
+ + setUserData({ + ...userData, + gender: value as GenderType, + }) + } + isOn={userData.gender === GenderType.FEMALE} + /> + + setUserData({ + ...userData, + gender: value as GenderType, + }) + } + isOn={userData.gender === GenderType.MALE} + /> + + setUserData({ + ...userData, + gender: value as GenderType, + }) + } + isOn={userData.gender === GenderType.NONE} + /> +
+
+ {/* 생년월일 선택 */} +
+
+ Date of birth +
+ setUserData({ ...userData, birth: value })} />
+ {/* 국적 선택 */} +
+
+ Nationality +
+ + setUserData({ ...userData, nationality: value }) + } + /> +
+ {/* 비자 선택 */} +
+
+ Visa Status +
+ + setUserData({ ...userData, visa: value as VisaType }) + } + /> +
+ {/* 전화번호 선택, dropdown으로 앞 번호를, 중간 번호와 뒷 번호는 각각 input으로 입력 받음 */} +
+
+ Telephone No. +
+
+
+ + setPhoneNum({ ...phoneNum, start: value }) + } + /> +
+ + setPhoneNum({ ...phoneNum, middle: value }) + } + canDelete={false} + /> + setPhoneNum({ ...phoneNum, end: value })} + canDelete={false} + /> +
+
+
+
+
) : ( diff --git a/src/pages/Profile/ProfilePage.tsx b/src/pages/Profile/ProfilePage.tsx index 2c7f25ce..a1524bbd 100644 --- a/src/pages/Profile/ProfilePage.tsx +++ b/src/pages/Profile/ProfilePage.tsx @@ -56,7 +56,7 @@ const ProfilePage = () => { 'https://images.pexels.com/photos/1458926/pexels-photo-1458926.jpeg?cs=srgb&dl=pexels-poodles2doodles-1458926.jpg&fm=jpg', first_name: 'Hyeona', last_name: 'Seol', - birth: '0000.00.00', + birth: '0000-00-00', school_name: 'Dongguk University', grade: 3, gpa: 3.5, diff --git a/src/router.tsx b/src/router.tsx index a1389f45..75f12a26 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,5 +1,9 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom'; + +import ScrollToTop from '@/components/Common/ScrollToTop'; +import Navbar from '@/components/Common/Navbar'; + import HomePage from '@/pages/Home/HomePage'; import SigninPage from '@/pages/Signin/SigninPage'; import SignupPage from '@/pages/Signup/SignupPage'; @@ -12,8 +16,8 @@ import LanguageSettingPage from '@/pages/Profile/LanguageSettingPage'; import EditProfilePage from '@/pages/Profile/EditProfilePage'; import PostDetailPage from '@/pages/PostDetail/PostDetailPage'; import PostApplyPage from '@/pages/PostApply/PostApplyPage'; -import ScrollToTop from '@/components/Common/ScrollToTop'; -import Navbar from '@/components/Common/Navbar'; +import ApplicationPage from '@/pages/Application/ApplicationPage'; +import WriteDocumentsPage from '@/pages/WriteDocuments/WriteDocumentsPage'; const Layout = () => { const location = useLocation(); @@ -38,20 +42,29 @@ const Router = () => { }> } /> - } /> + } /> } /> + } /> + } /> + } /> } /> + } /> } /> } /> + } /> } /> + + } /> + + } /> diff --git a/src/types/api/profile.ts b/src/types/api/profile.ts index 8d00fd6d..4792fedc 100644 --- a/src/types/api/profile.ts +++ b/src/types/api/profile.ts @@ -1,3 +1,5 @@ +import { GenderType, VisaType } from "@/constants/profile"; + export type UserProfileData = { user_information: UserInformationType language_level: LanguageLevelType @@ -38,12 +40,28 @@ export type ApplicationCountType = { } export type UserEditProfileDataType ={ - profile_img_url: string; + image?: File; // multipart-form-data + body: UserEditBodyType; +} + +export type UserEditBodyType = { first_name: string; last_name: string; - birth: string; - gender: string; + birth: string; // yyyy-MM-dd + gender: GenderType; // Enum(MALE, FEMALE, NONE) + nationality: string; + visa: VisaType; // Enum(D_2_1, D_2_2, D_2_3, D_2_4, D_2_6, D_2_7, D_2_8, D_4_1, D_4_7, F_2) + phone_number: string; + is_profile_img_changed: boolean; +} + +export type UserProfileDetailDataType = { + profile_img_url: string, + first_name: string; + last_name: string; + birth: string; // yyyy-MM-dd + gender: GenderType; // Enum(MALE, FEMALE, NONE) nationality: string; - visa_satus: string; - telephone_number: string; + visa: VisaType; // Enum(D_2_1, D_2_2, D_2_3, D_2_4, D_2_6, D_2_7, D_2_8, D_4_1, D_4_7, F_2) + phone_number: string; } \ No newline at end of file diff --git a/src/types/application/applicationItem.ts b/src/types/application/applicationItem.ts new file mode 100644 index 00000000..f852e515 --- /dev/null +++ b/src/types/application/applicationItem.ts @@ -0,0 +1,22 @@ +export type ApplicationStepType = + | 'RESUME_UNDER_REVIEW' + | 'WAITING_FOR_INTERVIEW' + | 'FILLING_OUT_DOCUMENTS' + | 'DOCUMENT_UNDER_REVIEW' + | 'APPLICATION_IN_PROGRESS' + | 'APPLICATION_SUCCESS' + | 'APPLICATION_REJECTED' + | 'RESUME_REJECTED' + | 'PENDING' + | 'REGISTRATION_RESULTS'; + +export type AppicationItemType = { + job_posting_id: number; + user_owner_job_posting_id: number; + icon_img_url: string; + title: string; + address_name: string; + step: ApplicationStepType; + hourly_rate: number; + duration_of_days: number; +}; diff --git a/src/utils/editProfileData.ts b/src/utils/editProfileData.ts new file mode 100644 index 00000000..f4238945 --- /dev/null +++ b/src/utils/editProfileData.ts @@ -0,0 +1,25 @@ +import { GenderType, NationalityType, VisaType } from "@/constants/profile"; +import { UserEditProfileDataType, UserProfileDetailDataType } from "@/types/api/profile"; + +// GET 데이터를 PATCH 요청 데이터로 변환 +export const transformToEditProfileData = ( + userData: UserProfileDetailDataType, + phoneNum: { start: string; middle: string; end: string }, + profileImage: File | null, + isProfileImgChanged: boolean, +): UserEditProfileDataType => { + return { + image: profileImage || undefined, + body: { + first_name: userData.first_name, + last_name: userData.last_name, + birth: userData.birth.replace(/\//g, '-'), + gender: userData.gender.toUpperCase() as GenderType, + nationality: userData.nationality.toUpperCase().replace(/ /g, '_') as NationalityType, + visa: userData.visa.replace(/-/g, '_') as VisaType, + // phone_number 통합 + phone_number: `${phoneNum.start}-${phoneNum.middle}-${phoneNum.end}`, + is_profile_img_changed: isProfileImgChanged, + }, + }; +}; \ No newline at end of file