diff --git a/src/api/index.js b/src/api/index.js index 19e2ec2..c76a47e 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -5,7 +5,12 @@ import { signupCEOAPI, } from "./signupAPI"; import { loginAPI } from "./loginAPI"; -import{ postJobPosting, updateJobPosting, getPostById } from "./postingAPI"; +import { + postJobPosting, + updateJobPosting, + getPostById, + uploadPostImageAPI, +} from "./postingAPI"; import { postListAPI, searchPostListAPI } from "./homeAPI"; export { @@ -15,6 +20,7 @@ export { signupCEOAPI, // 사업자유저 회원가입 API loginAPI, // 로그인 API postJobPosting, //모집글 작성 API + uploadPostImageAPI, // 모집글 작성 중 이미지 업로드 API updateJobPosting, //모집글 수정 API getPostById, // 모집글 수정시 postData 불러오기 API postListAPI, // 알바리스트 불러오기(홈) API diff --git a/src/api/postingAPI.js b/src/api/postingAPI.js index 8955885..ee024ce 100644 --- a/src/api/postingAPI.js +++ b/src/api/postingAPI.js @@ -1,8 +1,7 @@ -import axios from "axios"; import { privateAxios } from "../utils/customAxios"; /*---- 게시글 등록 ----*/ -export const postJobPosting = async (accessToken, payload) => { +export const postJobPosting = async (accessToken, dispatch, body) => { const response = { isSuccess: false, message: "", @@ -10,26 +9,29 @@ export const postJobPosting = async (accessToken, payload) => { }; try { - const result = await privateAxios(accessToken).post("/api/v1/post", payload); // POST로 변경 + const result = await privateAxios(accessToken, dispatch).post( + "/api/v1/post", + body + ); if (result.status === 200) { response.isSuccess = true; response.message = result.data.message; - response.data = result.data.data; } } catch (err) { response.isSuccess = false; - response.message = err.response?.data?.message || err.message; - - if (err.response?.status === 401) { - alert("인증이 필요합니다. 다시 로그인해주세요."); - } + response.message = err; } return response; }; /*---- 게시글 수정 ----*/ -export const updateJobPosting = async (accessToken, postId, postData, userId) => { +export const updateJobPosting = async ( + accessToken, + postId, + postData, + userId +) => { const response = { isSuccess: false, message: "", @@ -38,14 +40,17 @@ export const updateJobPosting = async (accessToken, postId, postData, userId) => try { const body = { - postId:postId, - userId:userId, + postId: postId, + userId: userId, storeName: "", // 수정 불가 workPlaceAddress: "", // 수정 불가 postData, }; - const result = await privateAxios(accessToken).patch(`/api/v1/post/${postId}`, body); + const result = await privateAxios(accessToken).patch( + `/api/v1/post/${postId}`, + body + ); if (result.status === 200) { response.isSuccess = true; @@ -54,7 +59,7 @@ export const updateJobPosting = async (accessToken, postId, postData, userId) => } } catch (err) { response.isSuccess = false; - response.message = err.response?.data?.message || err.message; + response.message = err.response.data; } return response; @@ -62,15 +67,50 @@ export const updateJobPosting = async (accessToken, postId, postData, userId) => /*---- 게시글 데이터 호출 ----*/ export const getPostById = async (postId, accessToken) => { + const response = { + isSuccess: false, + message: "", + data: null, + }; try { - const response = await axios.get(`http://43.203.223.82:8080/api/v1/post/${postId}`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - return response.data; + const result = await privateAxios(accessToken).get( + `/api/v1/post/${postId}` + ); + if (result.status === 200) { + response.isSuccess = true; + response.message = result.data.message; + response.data = result.data; + } } catch (error) { console.error("Error fetching post data:", error); - throw error; + response.message = error.response.data; + } + return response; +}; + +/**------ 이미지 업로드 (개별) ------ */ +export const uploadPostImageAPI = async (accessToken, dispatch, file) => { + const response = { + isSuccess: false, + message: "", + imageUrl: "", + }; + + let formData = new FormData(); + formData.append("image", file); + + try { + const result = await privateAxios(accessToken, dispatch).post( + `/api/v1/post/upload-post-image`, + formData + ); + if (result.status === 200) { + response.isSuccess = true; + response.message = result.data.message; + response.imageUrl = result.data.data; + } + } catch (error) { + response.message = error.response.data; } -}; \ No newline at end of file + return response; +}; diff --git a/src/components/posting/AddressInput.jsx b/src/components/posting/AddressInput.jsx index cc8f089..03c1ef8 100644 --- a/src/components/posting/AddressInput.jsx +++ b/src/components/posting/AddressInput.jsx @@ -14,11 +14,10 @@ const AddressInput = ({ label, value, onChange }) => { } new window.daum.Postcode({ oncomplete: function (data) { - onChange(data.address); + onChange(`${data.sido} ${data.sigungu} ${data.bname}`); }, }).open(); }; - return (
@@ -36,4 +35,4 @@ const AddressInput = ({ label, value, onChange }) => { ); }; -export default AddressInput; \ No newline at end of file +export default AddressInput; diff --git a/src/components/posting/PhotoUpload.jsx b/src/components/posting/PhotoUpload.jsx index 9ac1070..77f27b5 100644 --- a/src/components/posting/PhotoUpload.jsx +++ b/src/components/posting/PhotoUpload.jsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { StyledLabel, UploadContainer, @@ -9,27 +9,42 @@ import { PreviewImage, RemoveButton, } from "../../styles/posting/PhotoUploadStyles"; +import { uploadPostImageAPI } from "../../api"; +import getAccessToken from "../../utils/getAccessToken"; +import { useDispatch } from "react-redux"; -const PhotoUpload = ({ label }) => { - const [photos, setPhotos] = useState([]); - const maxPhotos = 10; +const PhotoUpload = ({ label, selectedPhotos, setSelectedPhotos }) => { + const accessToken = getAccessToken(); + const dispatch = useDispatch(); + const maxPhotos = 10; const fileInputRef = useRef(null); + // 업로드 const handlePhotoUpload = (e) => { - const files = Array.from(e.target.files); - const newPhotos = [...photos, ...files].slice(0, maxPhotos); - setPhotos(newPhotos); + const file = e.target.files[0]; + if (file) { + uploadPostImageAPI(accessToken, dispatch, file).then((res) => { + if (res.isSuccess) { + const newPhotos = [...selectedPhotos, res.imageUrl.imageUrl]; + setSelectedPhotos(newPhotos); // 부모로 상태 전달 + } else { + alert(res.message); + } + }); + } }; const handleUploadClick = () => { - if (photos.length < maxPhotos) { + if (selectedPhotos.length < maxPhotos) { fileInputRef.current.click(); } }; + // 삭제 const removePhoto = (index) => { - setPhotos(photos.filter((_, i) => i !== index)); + const newPhotos = selectedPhotos.filter((_, i) => i !== index); + setSelectedPhotos(newPhotos); // 부모로 상태 전달 }; return ( @@ -37,19 +52,18 @@ const PhotoUpload = ({ label }) => { {label && {label} (선택)} 사진이 있으면 더 많은 사람들이 확인해요. - {photos.map((photo, index) => ( + {selectedPhotos.map((photo, index) => ( - + removePhoto(index)}>✕ ))} - {photos.length < maxPhotos && ( + {selectedPhotos.length < maxPhotos && ( 📷 - {`${photos.length}/${maxPhotos}`} + {`${selectedPhotos.length}/${maxPhotos}`} { ); }; -export default PhotoUpload; \ No newline at end of file +export default PhotoUpload; diff --git a/src/components/posting/Tag.jsx b/src/components/posting/Tag.jsx index 7ca5cc3..ddbcf79 100644 --- a/src/components/posting/Tag.jsx +++ b/src/components/posting/Tag.jsx @@ -1,21 +1,21 @@ import React, { useState, useEffect } from "react"; -import { - StyledLabel, - TagContainer, - TagButton, - MoreButton, - AddTagButton, - TagInputWrapper, - TagInputField +import { + StyledLabel, + TagContainer, + TagButton, + MoreButton, + AddTagButton, + TagInputWrapper, + TagInputField, } from "../../styles/posting/TagStyles"; -const Tag = ({ - label, - tags = [], - maxSelectable = 1, - selectedTags, - setSelectedTags, - onTagsUpdate +const Tag = ({ + label, + tags = [], + maxSelectable, + selectedTags, + setSelectedTags, + onTagsUpdate, }) => { const [internalSelectedTags, setInternalSelectedTags] = useState([]); const [showAll, setShowAll] = useState(false); @@ -27,14 +27,13 @@ const Tag = ({ onTagsUpdate && onTagsUpdate(allTags); // 태그 변경 시 상위로 업데이트 }, [allTags, onTagsUpdate]); - const effectiveSelectedTags = selectedTags ?? internalSelectedTags; - const handleSetSelectedTags = setSelectedTags ?? setInternalSelectedTags; - const handleTagClick = (tag) => { - if (effectiveSelectedTags.includes(tag)) { - handleSetSelectedTags(effectiveSelectedTags.filter((t) => t !== tag)); - } else if (effectiveSelectedTags.length < maxSelectable) { - handleSetSelectedTags([...effectiveSelectedTags, tag]); + if (internalSelectedTags.includes(tag)) { + setInternalSelectedTags(internalSelectedTags.filter((t) => t !== tag)); + setSelectedTags(internalSelectedTags.filter((t) => t !== tag)); + } else if (internalSelectedTags.length < maxSelectable) { + setInternalSelectedTags([...internalSelectedTags, tag]); + setSelectedTags([...internalSelectedTags, tag]); } }; @@ -55,12 +54,8 @@ const Tag = ({ {allTags.slice(0, showAll ? allTags.length : 5).map((tag, index) => ( handleTagClick(tag)} - disabled={ - !effectiveSelectedTags.includes(tag) && - effectiveSelectedTags.length >= maxSelectable - } > {tag} @@ -78,7 +73,13 @@ const Tag = ({ 추가 )} -
+
{allTags.length > 5 && ( {showAll ? "접기 ▲" : "더보기 ▼"} @@ -96,4 +97,4 @@ const Tag = ({ ); }; -export default Tag; \ No newline at end of file +export default Tag; diff --git a/src/pages/recruitment/Posting.jsx b/src/pages/recruitment/Posting.jsx index a9ed9a9..b62fb06 100644 --- a/src/pages/recruitment/Posting.jsx +++ b/src/pages/recruitment/Posting.jsx @@ -1,17 +1,34 @@ import React, { useState, useEffect } from "react"; -import { PageContainer, ContentContainer, FixedButtonContainer } from "../../styles/posting/PostingStyles"; -import { InputField, Tag, Toggle, WeekdayPicker, WorkTimePicker, PayPicker, AddressInput, PhotoUpload, DescriptionInput, PhoneInput, Button } from "../../components"; +import { + PageContainer, + ContentContainer, + FixedButtonContainer, +} from "../../styles/posting/PostingStyles"; +import { + InputField, + Tag, + Toggle, + WeekdayPicker, + WorkTimePicker, + PayPicker, + AddressInput, + PhotoUpload, + DescriptionInput, + PhoneInput, + Button, +} from "../../components"; import "../../styles/posting/Posting.css"; import { POSTING_UPMU_TAG } from "../../constants"; import { postJobPosting, updateJobPosting, getPostById } from "../../api"; import { useNavigate, useLocation } from "react-router-dom"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import getAccessToken from "../../utils/getAccessToken"; // 일반 함수로 가져와야 오류 안뜸!! import { createPayload } from "../../utils/posting/payloadHelper"; // 분리된 payload 생성 함수 import { validateForm } from "../../utils/posting/validationHelper"; // 분리된 유효성 검증 함수 import { parseAddress, convertDays } from "../../utils/posting/formatHelper"; // 분리된 주소 및 요일 변환 함수 const Posting = () => { + const dispatch = useDispatch(); const accessToken = getAccessToken(); const location = useLocation(); const navigate = useNavigate(); @@ -37,7 +54,8 @@ const Posting = () => { applyNumber: "", isNumberPublic: true, description: "", - workPeriod: "1개월 이상" + workPeriod: "1개월 이상", + imageUrlList: [], }); const handleChange = (key, value) => { @@ -98,7 +116,9 @@ const Posting = () => { workTags: [workType], workDays, workTime: { - start: `${workStartHour}:${workStartMinute.toString().padStart(2, "0")}`, + start: `${workStartHour}:${workStartMinute + .toString() + .padStart(2, "0")}`, end: `${workEndHour}:${workEndTimeMinute.toString().padStart(2, "0")}`, }, pay, @@ -110,34 +130,45 @@ const Posting = () => { }); }; - const handleSubmit = async () => { + const handleSubmit = () => { const allValid = Object.values(validStates).every((isValid) => isValid); if (!allValid) { alert("모든 필드를 올바르게 입력해주세요."); return; } - - const payload = createPayload(formData, postId, userId, parseAddress, convertDays); - console.log("Payload:", payload); + const payload = createPayload( + formData, + postId, + userId, + parseAddress, + convertDays + ); if (!validateForm(payload, formData)) { - console.log("Validation failed. Aborting API call."); return; } try { - const response = mode === "create" - ? await postJobPosting(accessToken, payload) - : await updateJobPosting(accessToken, postId, payload.postData); - - if (response.isSuccess) { - alert(mode === "create" ? "게시글이 성공적으로 등록되었습니다." : "게시글이 성공적으로 수정되었습니다."); - navigate("/home"); + if (mode !== "modify") { + postJobPosting(accessToken, dispatch, payload).then((res) => { + if (res.isSuccess) { + alert("게시글이 성공적으로 등록되었습니다."); + navigate("/home"); + } else { + alert(res.message); + } + }); } else { - alert(`${mode === "create" ? "등록" : "수정"} 실패: ${response.message}`); + updateJobPosting(accessToken, postId, postData, userId).then((res) => { + if (res.isSuccess) { + alert("게시글이 성공적으로 수정되었습니다."); + navigate("/home"); + } else { + alert(res.message); + } + }); } } catch (error) { - console.error("Error during submission:", error); alert("제출 과정에서 오류가 발생했습니다. 다시 시도해주세요."); } }; @@ -145,7 +176,9 @@ const Posting = () => { return ( -
{mode === "modify" ? "게시글 수정" : "어떤 알바를 구하고 계신가요?"}
+
+ {mode === "modify" ? "게시글 수정" : "어떤 알바를 구하고 계신가요?"} +
{ label="제목" placeholder="공고 내용을 요약해주세요." onChange={(value) => handleChange("title", value)} - onValidityChange={(isValid) => handleValidityChange("title", isValid)} + onValidityChange={(isValid) => + handleValidityChange("title", isValid) + } />
@@ -192,7 +227,10 @@ const Posting = () => { { - handleChange("workTime", { start: timeData.start, end: timeData.end }); + handleChange("workTime", { + start: timeData.start, + end: timeData.end, + }); handleChange("isNegotiable", timeData.isNegotiable); }} /> @@ -207,13 +245,21 @@ const Posting = () => { />
- + + handleChange("imageUrlList", photos) + } + />
handleChange("description", value)} - onValidityChange={(isValid) => handleValidityChange("description", isValid)} + onValidityChange={(isValid) => + handleValidityChange("description", isValid) + } />
@@ -231,7 +277,9 @@ const Posting = () => { handleValidityChange("applyNumber", isValid)} + onValidityChange={(isValid) => + handleValidityChange("applyNumber", isValid) + } />
@@ -251,4 +299,4 @@ const Posting = () => { ); }; -export default Posting; \ No newline at end of file +export default Posting; diff --git a/src/utils/posting/formatHelper.js b/src/utils/posting/formatHelper.js index 2462b36..e804e33 100644 --- a/src/utils/posting/formatHelper.js +++ b/src/utils/posting/formatHelper.js @@ -1,17 +1,27 @@ export const parseAddress = (address) => { - const [doName = "", siName = "", detailName = ""] = address.split(" "); - return { doName, siName, detailName }; + const cityPattern = /[가-힣]+시|[가-힣]+구/; // 시, 구 + + // '시도' 부분 추출 + const doName = address.split(" ")[0]; + + // '시군구' 부분 추출 (시도 뒤에 오는 시나 구) + const cityMatch = address.replace(doName, "").match(cityPattern); + const siName = cityMatch ? cityMatch[0] : ""; + + // 나머지는 '동/읍/면' + const detailName = address.replace(doName, "").replace(siName, "").trim(); + return { doName, siName, detailName }; +}; + +export const convertDays = (days) => { + const dayMap = { + 월: "MONDAY", + 화: "TUESDAY", + 수: "WEDNESDAY", + 목: "THURSDAY", + 금: "FRIDAY", + 토: "SATURDAY", + 일: "SUNDAY", }; - - export const convertDays = (days) => { - const dayMap = { - 월: "MONDAY", - 화: "TUESDAY", - 수: "WEDNESDAY", - 목: "THURSDAY", - 금: "FRIDAY", - 토: "SATURDAY", - 일: "SUNDAY", - }; - return days.map((day) => dayMap[day]).join(", "); - }; \ No newline at end of file + return days.map((day) => dayMap[day]); +}; diff --git a/src/utils/posting/payloadHelper.js b/src/utils/posting/payloadHelper.js index 448863a..33287fb 100644 --- a/src/utils/posting/payloadHelper.js +++ b/src/utils/posting/payloadHelper.js @@ -1,33 +1,41 @@ -export const createPayload = (formData, postId, userId, parseAddress, convertDays) => { - const { doName, siName, detailName } = parseAddress(formData.workLocation); - const workDays = convertDays(formData.workDays); - const [startHour, startMinute] = formData.workTime.start.split(":").map(Number); - const [endHour, endMinute] = formData.workTime.end.split(":").map(Number); - - return { - postId: postId || 0, - userId, - storeName: formData.storeName, - workPlaceAddress: formData.workLocation, - postData: { - doName, - siName, - detailName, - workType: formData.workTags[0] || "기타", - title: formData.title, - content: formData.description, - pay: parseInt(formData.pay, 10), - workStartHour: startHour, - workStartMinute: startMinute, - workEndHour: endHour, - workEndTimeMinute: endMinute, - isNegotiable: formData.isNegotiable || false, - applyNumber: formData.applyNumber, - workDays, - isShortTermJob: formData.workPeriod === "단기", - payType: formData.payType, - isNumberPublic: formData.isNumberPublic, - imageUrlList: [""], - }, - }; - }; \ No newline at end of file +export const createPayload = ( + formData, + postId, + userId, + parseAddress, + convertDays +) => { + const { doName, siName, detailName } = parseAddress(formData.workLocation); + const workDays = convertDays(formData.workDays); + const [startHour, startMinute] = formData.workTime.start + .split(":") + .map(Number); + const [endHour, endMinute] = formData.workTime.end.split(":").map(Number); + + return { + postId: postId || 0, + userId: userId, + storeName: formData.storeName, + workPlaceAddress: formData.workLocation, + postData: { + doName: doName, + siName: siName, + detailName: detailName, + workType: formData.workTags[0] || "기타", + title: formData.title, + content: formData.description, + pay: parseInt(formData.pay, 10), + workStartHour: startHour, + workStartMinute: startMinute, + workEndHour: endHour, + workEndTimeMinute: endMinute, + isNegotiable: formData.isNegotiable || false, + applyNumber: formData.applyNumber, + workDays: workDays, + isShortTermJob: formData.workPeriod === "단기", + payType: formData.payType, + isNumberPublic: formData.isNumberPublic, + imageUrlList: formData.imageUrlList, + }, + }; +};