React, Node, MongoDB, Express
Library bcrypt cloudinary cookie-parser cors dotenv express express-fileupload jsonwebtoken mongoose validator
⭐Git Configuration
git init git remote add origin https://github.com/MangwonCassie/JobSeekingApp.git
⭐⭐⭐⭐⭐(node_modules/ in .gitignore)⭐⭐⭐⭐
git add .
git commit -m "commit message"
git push -u origin master
- 함수를 인수로 받아 express 미들웨어구조에 맞게 함수를 구성하고, Promise를 이용해서 비동기 함수 구현, 비동기 작업이 실패하면 catch를 통해 next로 함수에 오류를 전달하여 express의 오류처리 미들웨어로 이동시킴.
export const catchAsyncError = (theFunction) => {
return (req, res, next)=>{
Promise.resolve(theFunction(req, res, next)).catch(next);
}
}
[추가 참고 자료:] (https://velog.io/@tastestar/Express-error-handling)
- statusCode는 Express.js에서 내장된 메서드인 res.status()의 매개변수를 이용해서 res.status(statusCode)로 응답상태 코드를 설정하고, res.cookie 메서드를 사용하여 클라이언트에게 jwt를 담을 쿠키를 설정한다.
- 이 때 json형식으로 응답을 보내터 클라이언트에게 성공 및 "사용자 정보"와 함께 "토큰"을 보낸다.
- JSON Web Token(JWT)을 생성할 때 사용되는 시크릿 키(secretOrPrivateKey)를 정의하는 것은 jsonwebtoken 라이브러리를 사용하여 JWT를 생성할 때 필요하므로 env파일에 JWT_SECRET_KEY=임의로 지정
- env 파일 s 오타 같은 거 나지 않도록 주의 (에러 사진)
- 로그아웃 로직에서는 쿠키의 token을 빈 문자열로 설정하여 브라우저에서 해당 쿠키를 삭제하고, 만료일을 현재 시간으로 설정하여 쿠키를 즉시 만료시켜 로그아웃으로 처리합니다.
-코드
export const logout = catchAsyncError(async (req, res, next) => {
res.status(201).cookie("token", "", {
httpOnly: true,
expires: new Date(Date.now()),
})
.json({
success: true,
message: "User Logged Out Successfully"
})
})
- +를 사용하지 않으면 해당 필드는 기본적으로 조회되지 않습니다. 즉, 해당 필드는 결과에 포함되지 않습니다. 따라서 +를 사용하여 해당 필드를 명시적으로 선택하여 조회해야 합니다. 그렇지 않으면 해당 필드에 접근할 때 undefined가 반환되거나 오류가 발생할 수 있습니다.
- 관련 코드
export const login = catchAsyncError(async (req, res, next) => {
const { email, password, role } = req.body;
if (!email || !password || !role) {
return next(
new ErrorHandler("Please provide email ,password and role.", 400)
);
}
const user = await User.findOne({ email }).select("password");;
if (!user) {
return next(new ErrorHandler("Invalid Email or password"));
}
const isPasswordMatched = await user.comparePassword(password);
if (!isPasswordMatched) {
return next(new ErrorHandler("Invalid Email Or Password.", 400));
}
if (user.role !== role) {
return next(
new ErrorHandler(`User with provided email and ${role}, user.role ${user.role} not found!`, 404)
);
}
sendToken(user, 201, res, "User Logged In!");
});
- 해당 오류
- .env 파일 설정
CLOUDINARY_CLIENT_NAME=fffffff
CLOUDINARY_CLIENT_API=541233sss86232323
CLOUDINARY_CLIENT_SECRET=ubOH3VHq임의 코드
- server.js 설정
import app from "./app.js";
import cloudinary from "cloudinary";
cloudinary.v2.config({
cloud_name: process.env.CLOUDINARY_CLIENT_NAME,
api_key: process.env.CLOUDINARY_CLIENT_API,
api_secret: process.env.CLOUDINARY_CLIENT_SECRET,
});
app.listen(process.env.PORT, () => {
console.log(`server is running ${process.env.PORT}`);
});
-cloudinary resume 업로드 로직
export const postApplication = catchAsyncError(async (req, res, next) => {
const { role } = req.user;
if (role === "Employer") {
return next(
new ErrorHandler("Employer not allowed to access this resource.", 400)
);
}
if (!req.files || Object.keys(req.files).length === 0) {
return next(new ErrorHandler("Resume File Required!", 400));
}
const { resume } = req.files;
const allowedFormats = ["image/png", "image/jpeg", "image/webp"];
if (!allowedFormats.includes(resume.mimetype)) {
return next(
new ErrorHandler("Invalid file type. Please upload a PNG file.", 400)
);
}
const cloudinaryResponse = await cloudinary.uploader.upload(
resume.tempFilePath
);
if (!cloudinaryResponse || cloudinaryResponse.error) {
console.error(
"Cloudinary Error:",
cloudinaryResponse.error || "Unknown Cloudinary error"
);
return next(new ErrorHandler("Failed to upload Resume to Cloudinary", 500));
}
const { name, email, coverLetter, phone, address, jobId } = req.body;
console.log("Job ID:", jobId);
const applicantID = {
user: req.user._id,
role: "Job Seeker",
};
if (!jobId) {
return next(new ErrorHandler("Job not found!", 404));
}
const jobDetails = await Job.findById(jobId);
if (!jobDetails) {
return next(new ErrorHandler("Job not found!", 404));
}
const employerID = {
user: jobDetails.postedBy,
role: "Employer",
};
if (
!name ||
!email ||
!coverLetter ||
!phone ||
!address ||
!applicantID ||
!employerID ||
!resume
) {
return next(new ErrorHandler("Please fill all fields.", 400));
}
const application = await Application.create({
name,
email,
coverLetter,
phone,
address,
applicantID,
employerID,
resume: {
public_id: cloudinaryResponse.public_id,
url: cloudinaryResponse.secure_url,
},
});
res.status(200).json({
success: true,
message: "Application Submitted!",
application,
});
});
-
일반적으로 파일 업로드를 처리하는 경우, 클라이언트 측에서 파일을 서버에 전송할 때는 파일을 multipart/form-data 형식으로 보냅니다. 이때 클라이언트가 전송한 파일은 서버의 라우트 핸들러에서 해당 요청(request) 객체의 req.files 속성에 자동으로 포함됩니다.
-
이는 주로 파일 업로드를 위해 사용되는 라이브러리(예: multer)가 이 역할을 수행하기 때문입니다. multer 브라우저가 보낸 파일 데이터를 파싱하고 req.files에 적절한 형식으로 저장합니다.
-
Object.keys() 메서드는 주어진 객체의 열거 가능한 속성 이름들을 배열로 반환합니다. 이 배열은 객체의 속성 이름(key)을 포함하게 됩니다. 따라서 Object.keys(obj).length는 해당 객체 obj의 속성의 개수를 나타냅니다.
-
{ resume } = req.files; // req.files 객체에서 resume 속성에 해당하는 파일을 가져온 것.
-image/png: PNG 형식의 이미지 파일을 나타냅니다., image/jpeg: JPEG 형식의 이미지 파일을 나타냅니다., image/webp: WebP 형식의 이미지 파일을 나타냅니다.
-!allowedFormats.includes(resume.mimetype) 조건문은 resume 파일의 mimetype이 allowedFormats 배열에 포함되지 않을 때를 조건문으로 표현한 것입니다. -
cloudinary.uploader.upload()는 cloudinary를 사용하여 resume 파일을 클라우드에 업로드합니다.
-
resume.tempFilePath는 업로드할 파일의 임시 경로입니다.
-
업로드된 파일의 정보를 cloudinaryResponse 변수에 할당
-
applicationID 객체를 만들 때 req.user._id는 현재 요청을 보내는 사용자 ID를 나타내는데 로그인 후 Express 세션 미들웨어가 사용자 정보를 세션에 저장하고 "요청 객체(req)에 사용자정보를 추가한다."
-axios 요청에서 Accept 헤더를 명시적으로 설정하지 않으면 서버가 HTML 대신 JSON 응답을 보내더라도, 클라이언트가 HTML로 응답을 받아들일 수 있음.
- 관련 코드
const Jobs = () => {
const [jobs, setJobs] = useState([]);
const { isAuthorized } = useContext(Context);
const navigateTo = useNavigate();
useEffect(() => {
try {
axios
.get("http://localhost:4000/api/v1/job/getall", {
withCredentials: true,
headers: {
"Accept": "application/json",
}
})
.then((res) => {
console.log("res.status", res.status);
console.log("Res.data", res.data); // res.data 출력
console.log("Res.data.data", res.data.data); // res.data 출력
console.log("Res.data.jobs", res.data.jobs); // res.data 출력
setJobs(res.data);
});
} catch (error) {
console.log(error);
}
}, []);
if (!isAuthorized) {
navigateTo("/");
}
- console.log로 res.status는 200인 것을 확인했지만 res.data는 html형식, res.data.jobs 는 undefined가 뜬다는 사실 확인
- post요청이면 "Content-Type": "application/json" 까지 설정해줘야하지만 getall api는 get요청이므로 accept 헤더 타입만 기입해주면 됨.
![post job 500 error 해결](https://github.com/MangwonCassie/JobSeek/assets/129250487/e61d78ff-ce3f-4927-bec7-426b3bd0efa7)
- post job api 모델스키마와 controller 순서 맞춰야함.
- BE
``` app.use( cors({ origin: [process.env.FRONTEND_URL, "http://localhost:5173", "http://localhost:5173/", "http://127.0.0.1:5173/", "*"], method: ["GET", "POST", "DELETE", "PUT"], credentials: true, }) ) ```
-FE
"proxy": {
"/api": {
"target": "http://localhost:4000",
"changeOrigin": true,
"secure": false
}
}
-package.json 설정해도 localhost 말고 127로 시작하는 주소로 api 요청해야 rest api 동작하는 경우 있음
const App = () => {
const { isAuthorized, setIsAuthorized, user, setUser } = useContext(Context);
useEffect(() => {
const fetchUser = async () => {
try {
const response = await axios.get(
"http://127.0.0.1:4000/api/v1/user/getuser",
{
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
withCredentials: true,
}
);
setUser(response.data.user);
setIsAuthorized(true);
} catch (error) {
console.log("App에서 user 가져왔나요?", user);
setIsAuthorized(false);
}
};
fetchUser();
}, [isAuthorized]);
-BEFORE (BE 부분)
- get 요청 put 요청으로 변경
- logout 시 빈객체를 전달
- BE 파트 clearCookie로 쉽게 코드 리팩토링
export const logout = catchAsyncError(async (req, res, next) => {
res
.status(201)
.cookie("token", "", {
httpOnly: true,
expires: new Date(Date.now()),
})
.json({
success: true,
message: "Logged Out Successfully.",
});
});
-AFTER (BE) 부분
export const logout = catchAsyncError(async (req, res, next) => {
res.clearCookie("token").status(201).json({
success: true,
message: "Logged Out Successfully.",
});
});
- BEFORE (FE 부분)
const handleLogout = async () => {
try {
const response = await axios.get(
"/api/v1/user/logout",
{
withCredentials: true,
}
);
toast.success(response.data.message);
setIsAuthorized(false);
navigateTo("/login");
} catch (error) {
toast.error(error.response.data.message);
setIsAuthorized(true);
}
};
- AFTER (FE 부분)
const handleLogout = async () => {
try {
const response = await axios.post(
"/api/v1/user/logout",
{}, // 빈 객체 전달
{
withCredentials: true,
}
);
toast.success(response.data.message);
setIsAuthorized(false);
navigateTo("/login");
} catch (error) {
toast.error(error.response.data.message);
setIsAuthorized(true);
}
};