느린우체통
프로젝트 일정
SR 10/25 ~ 11/7
1차 기획회의
2차 기획회의
3차 기획회의
4차 기획회의
목업 구현 11/8 ~ 11/13
기능 구현 11/14 ~ 11/17
발표준비 11/17 ~ 11/18
발표 11/19
소개
'언제 어디서든 누군가에게 지금의 감정과 기분을 기록해 미래의 누군가에게 마음을 전달할 수는 없을까?'라는 궁금증에서 시작해 예약메일을 전송하는 '느린 우체통'을 기획하게 되었습니다.
느린 우체통은 최대 1년 후까지 날짜를 선택해 텍스트와 이미지 등을 첨부해 예약메일을 전송할 수 있는 서비스입니다. 홈페이지에 가입해 예약메일을 전송하면, 즉시 수신자에게 알림메일이 전달되며, 예약된 날짜에도 알림메일을 전송합니다. 수신자는 홈페이지에 가입하여야 예약메일을 읽어볼 수 있습니다.
기능 및 구현
홈화면
회원가입
로그인 및 비밀번호 찾기
소셜로그인
카카오 로그인
버튼 클릭시 회원가입이 되어있지 않다면, 닉네임 정보제공 동의(필수) 및 카카오톡 이메일, 비밀번호를 입력한 후 자동 로그인됩니다. 카카오 로그인
버튼 클릭시 회원가입이 되어있다면, 자동 로그인됩니다. 메일작성
예약이 완료된 편지는 전송 취소가 불가합니다. 편지를 보내시겠습니까?
라는 내용의 확인 모달창을 띄워줍니다. 알림메일 발송
OO님께서 예약전송한 편지가 OOOO-OO-OO일에 도착할 예정입니다
라는 내용의 알림메일이 전송됩니다. OO님께서 OOOO-OO-OO일에 예약한 메일이 도착했습니다
라는 내용의 알림메일이 전송됩니다.예약메일 발송
마이페이지
받은편지함
보낸편지함
맡은 역할
이번 프로젝트에서는 모든 팀원이 풀스택으로 참여했고, 저는 메일작성 페이지와 마이페이지를 구현했습니다.
메일 작성 구현
메일 작성페이지에서는 CKEditor5와 connect-multiparty를 사용해 메일작성과 이미지 업로드 기능을 구현했습니다. 유저가 로컬 이미지를 첨부한 경우, 서버의 uploads 폴더에 저장됩니다.
안내메일 구현
전송하기 버튼 클릭시 수신자 이메일에게 안내메일을 전송합니다. 메일전송은 nodemailer를 사용해 구현했으며, 메일 내용은 campain monitor에서 이메일 템플릿을 수정해 사용했습니다.
예약메일 구현
예약메일 서비스인만큼, 예약된 날짜가 되면 수신자에게 예약안내 메일을 전송합니다. 매일 같은 시간에 예약메일을 전송하는 기능은 node-schedule을 사용해 구현했습니다. 서버의 index.js에서 매일 0시 0분에 데이터베이스에서 오늘 날짜로 예약된 메일 데이터를 가져옵니다. 각각의 데이터의 이름, 수신자이메일, 전송날짜 정보를 가져와 arrivalAlert 함수에 인자로 전달합니다. arrivalAlert 함수에서는 nodemailer를 사용해 인자로 받아온 정보를 활용해 안내메일을 전송합니다.
// index.js
const rule = new schedule.RecurrenceRule();
rule.hour = 0;
rule.minute = 0;
schedule.scheduleJob(rule, async function sendAlertMail() {
const realTime = getDateStr(new Date());
try {
console.log('it works');
const sql = `SELECT users.name, mails.created_at, mails.receiverEmail FROM mails INNER JOIN users ON users.email=mails.writerEmail WHERE reserved_at=?`;
const [rows, fields, error] = await db.query(sql, [realTime]);
if (error) {
console.log(error);
} else {
for (let i = 0; i < rows.length; i++) {
arrivalAlert(
rows[i]['name'],
rows[i]['receiverEmail'],
rows[i]['created_at']
);
console.log('sending alert mail');
}
}
} catch (error) {
throw error;
}
});
module.exports = async (req, res) => {
try {
const receiverEmail = req.query.receiverEmail;
const sql =
'SELECT id, title, reserved_at FROM mails WHERE receiverEmail = ? ORDER BY reserved_at';
const params = [receiverEmail];
const [rows, fields, err] = await db.query(sql, params);
if (err) {
console.log(err);
return res.status(404).send('실패');
} else {
return res.status(200).json({ data: rows });
}
} catch (err) {
throw err;
}
};
개인정보 수정 구현
자체가입 유저는 비밀번호를 변경할 수 있으며, 비밀번호 유효성 검사를 거쳐 10자리이상 15자리 이하이면서 새로운 비밀번호와 확인 비밀번호가 일치하는 경우에만 변경이 가능합니다. 두 조건 중 하나라도 만족하지 않는다면, 입력한 항목을 확인하라는 알림창이 뜹니다.
비밀번호 변경시 crypto 모듈을 이용해 서버에서 새로운 salt를 생성해 유저가 입력한 비밀번호와 salt를 합해 암호화된 비밀번호를 생성하고, 데이터베이스의 salt와 암호화된 비밀번호를 업데이트합니다.
소셜로그인 유저는 비밀번호 변경이 불가합니다. redux로 관리되는 oauth여부를 useSelector로 가져와, oauth가 true인 경우, 비밀번호 변경 컴포넌트가 보이지 않도록 구현했습니다.
회원탈퇴
자체가입 유저는 회원탈퇴 시 비밀번호를 입력합니다. 비밀번호를 입력해 확인 버튼 클릭 시, 서버에서 데이터베이스로부터 암호화된 salt와 암호화된 비밀번호를 가져옵니다. 이후, 유저가 입력한 비밀번호를 가져온 salt로 암호화해 데이터베이스에서 가져온 암호화된 비밀번호와 일치하는지 확인합니다.
일치하지 않는다면, 비밀번호를 확인하라는 알림창이 뜹니다.
일치한다면, 회원탈퇴되고, 홈페이지로 이동합니다.
다음은 자체가입 유저 회원탈퇴 로직입니다.
const db = require('../../db');
const crypto = require('crypto');
module.exports = async (req, res) => {
try {
const { email, password } = req.body;
//console.log(email, password);
const sql1 = 'SELECT salt,password as decoded FROM users WHERE email=?';
const params1 = [email];
const [row1, field1, err1] = await db.query(sql1, params1);
if (err1) {
console.log(err1);
return res.status(404).json({ message: 'error' });
}
//console.log(row); [{salt:'111', decoded:'helloworld'}]
const hashPassword = crypto
.createHash('sha512')
.update(password + row1[0]['salt'])
.digest('hex');
if (row1[0]['decoded'] === hashPassword) {
const sql2 = 'DELETE FROM users WHERE password=?';
const params2 = [hashPassword];
const [row2, field2, err2] = await db.query(sql2, params2);
if (err2) {
console.log(err2);
return res.status(404).json({ message: 'error' });
} else {
console.log('탈퇴됨');
return res.status(200).json({ message: 'success' });
}
} else {
console.log('비밀번호 틀림');
return res.json({ message: 'not authorized' });
}
} catch (err) {
throw err;
}
};
tech-stack
이번 프로젝트를 진행하며
지난 다가치 프로젝트 때와는 다르게, 팀규칙부터 commit, lint, pr 규칙 등을 세부적으로 정했습니다. 해야할 모든 태스크를 미리 태스크카드로 만들어두었고, 매일 어떤 태스크카드를 하고있는지, 끝냈는지 등을 칸반보드로 In-Progress, Done으로 구분해 다른 팀원들과 진행사항을 공유했습니다.
커밋 메시지와 pr 내용을 작성하는 것이 처음에는 익숙하지 않았지만, 함께 팀원들과 merge할때 어떤 변경내역이 있는지 한눈에 확인할 수 있어 유용하다고 느꼈습니다.
이번 프로젝트에서는 팀원 4명이 모두 풀스택을 맡았습니다. 각자 페이지를 맡아 목업과 기능을 구현했습니다. 팀원 모두가 프론트와 백을 경험해볼 수 있었습니다. 자신이 맡은 페이지에 대해 더욱 책임감을 갖고 마무리할 수 있어 좋았습니다. 하지만, api를 작성할때와 css에 있어서 통일성이 부족하다는 느낌을 받았습니다. 다음 프로젝트 때에는 프론트와 백을 구분해 프로젝트를 진행할 예정입니다.
이번 프로젝트는 코드스테이츠에서 주어진 시간보다 조금 일찍 시작했습니다. 이미 팀원이 정해진 상태였고, 지난 다가치 토이 프로젝트를 마친 후 먼저 아이디어 회의를 시작했기 때문에 코드스테이츠 스케줄보다 일주일 정도 먼저 시작했습니다.
그래서 코드스테이츠로부터 SR단계에 대한 공지를 받았을때, 와이어프레임과 IA 등 이미 완성된 부분이 있어 시간을 단축할 수 있었지만, 필수로 해야 하는 부분들에 대해 (ex. gitbook등을 활용한 api작성) 추가적으로 진행했습니다.
그래서 조금 일찍 목업구현을 시작했는데, SR 과정을 충분히 하지 못했다는 생각이 들었습니다. 특히 메일작성 페이지에서는 구현해야할 기능이 많고, 사용할 스택 등이 여러개가 필요했지만 이에 대해 충분히 고민하는 시간을 갖지 못한 것 같습니다.
그래서 기능구현을 하며, CKEditor4에서 CKEditor5로 변경하는 등 중간에 수정하는 일들이 발생했습니다.
그래서 다음 프로젝트 때에는 SR 단계 동안 사용할 스택에 대해서도 충분히 찾아보고 고민하는 시간을 갖고, 로직을 미리 생각해 둔 뒤부터 목업 구현을 진행하려고 합니다.