심부릉 앱 파트너십 가입 step1과 step2를 구현하였다
화면은 피그마로 먼저 설계하였다
백엔드 디렉토리 구조
프로젝트 구현 전에 팀원들과 백엔드 디렉토리 구조를 미리 정의해두었다
BACK/
├── node_modules/
├── server/
│ ├── config/
│ ├── controller/
│ ├── middleware/
│ ├── query/
│ ├── router/
│ └── uploads/
├── .env
├── .gitignore
├── .prettierrc
├── app.js
├── package-lock.json
└── package.json
1. 미들웨어 (파일 업로드 및 주민등록번호 암호화)
파일명: partnershipMiddleware.js
미들웨어 폴더에는 다음과 같이 업로드 설정 생성 함수와 주민등록 암호화 함수를 만들어 export를 통해 다른 파일에서 가져다 쓰기 편하게 분리해 두었다
import bcrypt from 'bcrypt';
import multer from 'multer';
import path from 'path';
- bcrypt: 주민등록번호를 암호화하는 라이브러리.
- multer: 파일 업로드를 처리하는 미들웨어.
- path: 파일 경로와 확장자를 다루기 위한 Node.js 기본 모듈.
파일 이름 생성 함수
const generateFilename = (file) => Date.now() + path.extname(file.originalname);
기능: 업로드된 파일의 이름을 현재 시간 기반으로 생성하고 확장자 유지.
파일 업로드 설정 함수
export function createUploader(uploadPath = "uploads") {
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadPath),
filename: (req, file, cb) => cb(null, generateFilename(file)),
});
- destination: 업로드된 파일이 저장될 폴더 경로를 설정합니다.
- filename: 파일명을 generateFilename 함수로 생성합니다.
파일 필터와 업로드 제한
return multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowedExtensions = /jpeg|jpg|png/;
const ext = path.extname(file.originalname).toLowerCase();
if (allowedExtensions.test(ext)) {
cb(null, true);
} else {
cb(new Error('이미지 파일만 업로드 가능합니다.'));
}
},
});
}
- 파일 크기 제한: 5MB까지 업로드 가능.
- 파일 필터: JPEG, JPG, PNG 확장자만 허용.
- 에러 처리: 다른 확장자일 경우 에러를 발생시킵니다
주민등록번호 암호화 함수
export async function encryptSSN(ssn, saltRounds = 10) {
try {
return await bcrypt.hash(ssn, saltRounds);
} catch (error) {
throw new Error("암호화 과정에서 문제가 발생했습니다.");
}
}
- bcrypt.hash: 주민등록번호를 안전하게 암호화합니다.
- saltRounds: 암호화 강도(기본 10).
미들웨어 전체코드
import bcrypt from 'bcrypt';
import multer from 'multer';
import path from 'path';
// 공통 함수로 파일 이름 생성
const generateFilename = (file) => Date.now() + path.extname(file.originalname);
// 업로드 설정 생성 함수
export function createUploader(uploadPath = "uploads") {
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadPath),
filename: (req, file, cb) => cb(null, generateFilename(file)),
});
return multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowedExtensions = /jpeg|jpg|png/;
const ext = path.extname(file.originalname).toLowerCase();
if (allowedExtensions.test(ext)) {
cb(null, true);
} else {
cb(new Error('이미지 파일만 업로드 가능합니다.'));
}
},
});
}
// 주민등록번호 암호화 함수
export async function encryptSSN(ssn, saltRounds = 10) {
try {
return await bcrypt.hash(ssn, saltRounds);
} catch (error) {
throw new Error("암호화 과정에서 문제가 발생했습니다.");
}
}
2. 라우터
파일명: partnerRouter.js
라우터도 관리하기 편하도록 따로 분리해 두었다
import express from 'express';
import { partnerController } from '../controller/partnerController.js';
import { createUploader } from "../middleware/partnershipMiddleware.js";
- express: 라우터를 설정하기 위해 사용.
- partnerController: 요청을 처리하는 컨트롤러 파일.
- createUploader: 파일 업로드 미들웨어.
파일 업로드 미들웨어 설정
const idPhotoUpload = createUploader('uploads/ids');
const facePhotoUpload = createUploader('uploads/faces');
- idPhotoUpload: 신분증 사진을 uploads/ids 폴더에 저장.
- facePhotoUpload: 정면 사진을 uploads/faces 폴더에 저장.
1단계 - 신분증 사진과 개인정보 저장
router.post("/step1", idPhotoUpload.single("idPhoto"), partnerController.applyPartnership1);
- idPhotoUpload.single("idPhoto"): idPhoto라는 필드에서 파일 하나를 업로드합니다.
- applyPartnership1: 업로드된 파일과 데이터 처리를 컨트롤러로 위임.
2단계 - 정면 사진 업로드
router.post("/step2", facePhotoUpload.single("facePhoto"), partnerController.applyPartnership2);
- facePhotoUpload.single("facePhoto"): facePhoto 필드에서 파일 하나를 업로드합니다.
- applyPartnership2: 기존 데이터에 정면 사진을 추가로 저장하는 컨트롤러 함수.
라우터 전체 코드
import express from 'express';
import { partnerController } from '../controller/partnerController.js'
import { createUploader } from "../middleware/partnershipMiddleware.js";
const router = express.Router()
// 사진 업로드 설정
const idPhotoUpload = createUploader('uploads/ids');
const facePhotoUpload = createUploader('uploads/faces');
// 파트너십 가입 요청 1단계: 신분증 사진과 개인정보 저장
router.post("/step1", idPhotoUpload.single("idPhoto"), partnerController.applyPartnership1)
// // 2단계: 정면 사진 업로드
router.post("/step2", facePhotoUpload.single("facePhoto"), partnerController.applyPartnership2)
export default router;
3. 컨트롤러
파일명: partnerController.js
1단계 - applyPartnership1
기능: 주민등록번호를 암호화하고 신분증 사진 경로와 함께 데이터베이스에 저장합니다.
applyPartnership1: async (req, res) => {
try {
const { name, ssn } = req.body;
req.body: 클라이언트에서 보낸 name과 ssn 가져오기.
if (!name || !ssn || !req.file) {
return res.status(400).json({
code: "400",
msg: "필수 입력값을 모두 입력해주세요",
});
}
필수 입력값 검증: name, ssn, req.file이 없으면 400 에러 반환.
const hashedSSN = await encryptSSN(ssn, 10);
const idPhotoPath = req.file.path;
- 주민등록번호 암호화: encryptSSN 함수 사용.
- 파일 경로 저장: req.file.path에서 업로드된 경로를 가져옵니다.
const newPartnership = new PartnershipSchema({
name,
ssn: hashedSSN,
idPhotoPath,
});
await newPartnership.save();
데이터 생성: name, hashedSSN, idPhotoPath를 MongoDB에 저장.
res.status(201).json({
code: "200",
msg: "정보 제출 성공",
partnerId: newPartnership._id,
});
응답 반환: 성공 메시지와 partnerId 반환.
2단계 - applyPartnership2
userId: 클라이언트에서 userId를 가져옵니다.
const updatedPartnership = await PartnershipSchema.findOneAndUpdate(
{ _id: userId },
{ $set: updateData },
{ new: true }
);
기존 데이터 업데이트:
- 조건: _id가 userId인 문서를 찾음.
- $set: facePhotoPath와 updatedAt 필드 업데이트.
res.status(200).json({
code: "200",
msg: "정면 사진 업로드가 완료되었습니다.",
data: updatedPartnership,
});
응답 반환: 성공 메시지와 업데이트된 데이터를 반환.
import { encryptSSN } from "../middleware/partnershipMiddleware.js"
import PartnershipSchema from '../query/PartnershipQuery.js'; // Partnership 모델 가져오기
// 1단계: 주민등록번호,신분증사진,데이터 저장
export const partnerController = {
applyPartnership1: async (req, res) => {
try {
const { name, ssn } = req.body;
// 필수 입력값 확인
if (!name || !ssn || !req.file) {
return res.status(400).json({
code: "400",
msg: "필수 입력값을 모두 입력해주세요",
})
}
//모든 형식 검증은 프론트에서 해도 됨
// 주민등록번호 형식 검증
const ssnRegex = /^\d{6}-\d{7}$/
if (!ssnRegex.test(ssn)) {
return res.status(400).json({
code: "401",
msg: "주민등록번호 형식이 유효하지 않습니다",
})
}
// 주민등록번호 암호화
const hashedSSN = await encryptSSN(ssn, 10)
// 신분증 사진 경로
const idPhotoPath = req.file.path;
// 파트너십 데이터 생성
const newPartnership = new PartnershipSchema({
// userId: req.user._id, // 인증된 사용자 ID
name,
ssn: hashedSSN,
idPhotoPath,
});
// 데이터베이스에 저장
await newPartnership.save()
res.status(201).json({
code: "200",
msg: "정보 제출 성공",
partnerId: newPartnership._id,
});
} catch (err) {
console.error(err)
res.status(500).json({
code: "500",
msg: "서버 오류가 발생했습니다",
})
}
},
// 2단계: 정면 사진 업로드 처리
applyPartnership2: async (req, res) => {
try {
//const { userId } = req.body;
// 요청 데이터 검증
if (!req.file) {
console.error("요청 데이터가 올바르지 않습니다.", {file: req.file });
return res.status(400).json({ code: "400", msg: "모든 필드를 입력해야 합니다." });
}
const updateData = {
facePhotoPath: req.file.path,
updatedAt: new Date(),
};
// 기존 파트너십 데이터 업데이트
const updatedPartnership = await PartnershipSchema.findOneAndUpdate(
//{ _id: userId }, // 검색 조건
{ $set: updateData }, // $set 연산으로 facePhotoPath 추가
{ new: true } // 업데이트된 문서 반환
);
if (!updatedPartnership) {
console.error("파트너십 데이터가 존재하지 않습니다.", { userId });
return res.status(404).json({
code: "404",
msg: "파트너십 데이터가 존재하지 않습니다.",
});
}
res.status(200).json({
code: "200",
msg: "정면 사진 업로드가 완료되었습니다.",
data: updatedPartnership,
});
} catch (err) {
console.error("서버 오류 발생:", err);
res.status(500).json({
code: "500",
msg: "서버 오류가 발생했습니다.",
});
}
},
}
컨트롤러에서 주석처리된 부분은 현재 회원가입쪽은 다른 팀원이 개발 진행중이기에 주석으로 처리해두었다.
mongoDB를 사용해 데이터를 추가하면서 저장하려니까 파트너십 가입을 피그마에서 만든 화면처럼 분리해서 만들고 데이터를 저장하려니 스키마에 필요한 필드를 미리 정의해두고 default값을 null로 한 다음 %set연산으로 추가하는 방식이 필요했다
프로젝트를 진행하면서 페이지 하나 만드는데 생각보다 쉽지 않았고 여러 상황을 고려해서 만들어야 했다
사용하는 DB의 특성을 고려하면서 진행하니 코드를 고치는 일은 많이 줄어들었다
코드를 짜기전에 항상 메모장을 켜서 어떤 점을 고려하며 코드를 짜야하는지 정리하고나서 구현했던게 도움이 많이 됬던것 같다
'프로젝트' 카테고리의 다른 글
심부릉 앱 DB 설계 (2) | 2024.12.02 |
---|---|
심부릉 앱 DB (0) | 2024.11.30 |
심부릉 앱 화면 기획 (0) | 2024.11.30 |
심부름 앱 기획 (6) | 2024.11.30 |