Skip to content

Commit

Permalink
✨ feat : 모집글 작성 API 및 모집글 내 이미지 업로드
Browse files Browse the repository at this point in the history
- 모집글 작성 API - postJobPosting
- 이미지 업로드 API (단일) - uploadPostImageAPI

ref : #18
  • Loading branch information
odukong committed Nov 12, 2024
1 parent 242f733 commit f6c2501
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 144 deletions.
8 changes: 7 additions & 1 deletion src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -15,6 +20,7 @@ export {
signupCEOAPI, // 사업자유저 회원가입 API
loginAPI, // 로그인 API
postJobPosting, //모집글 작성 API
uploadPostImageAPI, // 모집글 작성 중 이미지 업로드 API
updateJobPosting, //모집글 수정 API
getPostById, // 모집글 수정시 postData 불러오기 API
postListAPI, // 알바리스트 불러오기(홈) API
Expand Down
84 changes: 62 additions & 22 deletions src/api/postingAPI.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
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: "",
data: null,
};

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: "",
Expand All @@ -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;
Expand All @@ -54,23 +59,58 @@ 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;
};

/*---- 게시글 데이터 호출 ----*/
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;
}
};
return response;
};
5 changes: 2 additions & 3 deletions src/components/posting/AddressInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
Expand All @@ -36,4 +35,4 @@ const AddressInput = ({ label, value, onChange }) => {
);
};

export default AddressInput;
export default AddressInput;
44 changes: 29 additions & 15 deletions src/components/posting/PhotoUpload.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef } from "react";
import React, { useState, useRef, useEffect } from "react";
import {
StyledLabel,
UploadContainer,
Expand All @@ -9,47 +9,61 @@ 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 (
<div>
{label && <StyledLabel>{label} (선택)</StyledLabel>}
<UploadText>사진이 있으면 더 많은 사람들이 확인해요.</UploadText>
<UploadContainer>
{photos.map((photo, index) => (
{selectedPhotos.map((photo, index) => (
<PreviewContainer key={index}>
<PreviewImage src={URL.createObjectURL(photo)} alt={`preview-${index}`} />
<PreviewImage src={photo} alt={`preview-${index}`} />
<RemoveButton onClick={() => removePhoto(index)}></RemoveButton>
</PreviewContainer>
))}
{photos.length < maxPhotos && (
{selectedPhotos.length < maxPhotos && (
<UploadBox onClick={handleUploadClick}>
<UploadIcon>📷</UploadIcon>
<span>{`${photos.length}/${maxPhotos}`}</span>
<span>{`${selectedPhotos.length}/${maxPhotos}`}</span>
<input
type="file"
multiple
accept="image/*"
onChange={handlePhotoUpload}
ref={fileInputRef}
Expand All @@ -62,4 +76,4 @@ const PhotoUpload = ({ label }) => {
);
};

export default PhotoUpload;
export default PhotoUpload;
59 changes: 30 additions & 29 deletions src/components/posting/Tag.jsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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]);
}
};

Expand All @@ -55,12 +54,8 @@ const Tag = ({
{allTags.slice(0, showAll ? allTags.length : 5).map((tag, index) => (
<TagButton
key={index}
selected={effectiveSelectedTags.includes(tag)}
selected={internalSelectedTags.includes(tag)}
onClick={() => handleTagClick(tag)}
disabled={
!effectiveSelectedTags.includes(tag) &&
effectiveSelectedTags.length >= maxSelectable
}
>
{tag}
</TagButton>
Expand All @@ -78,7 +73,13 @@ const Tag = ({
<AddTagButton onClick={handleAddTag}>추가</AddTagButton>
</TagInputWrapper>
)}
<div style={{ display: "flex", justifyContent: "space-between", marginTop: "8px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: "8px",
}}
>
{allTags.length > 5 && (
<MoreButton onClick={toggleShowAll}>
{showAll ? "접기 ▲" : "더보기 ▼"}
Expand All @@ -96,4 +97,4 @@ const Tag = ({
);
};

export default Tag;
export default Tag;
Loading

0 comments on commit f6c2501

Please sign in to comment.