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,
+ },
+ };
+};