OpenAI를 활용한 외국어 회화 연습 웹 사이트 (API)

Hyun·2023년 5월 20일
0

프로젝트

목록 보기
1/2
post-thumbnail

이번 프로젝트를 진행하면서 총 8개의 API를 생성하였고 각 API에 대한 로직에 대해 설명을 하겠다.

1. 암호화 함수

const crypto = require("crypto");
// 솔트 생성
const createSalt = () => {
    return new Promise((resolve, reject) => {
      crypto.randomBytes(64, (err, buf) => {
        if (err) reject(err);
        resolve(buf.toString("base64"));
      });
    });
  };
  // 암호비번 생성
  const createCryptoPassword = async (plainPassword) => {
    console.log(">>>>>>>>>>", plainPassword);
    const salt = await createSalt();
    return new Promise((resolve, reject) => {
      crypto.pbkdf2(plainPassword, salt, 10000, 64, "sha512", (err, key) => {
        if (err) {
          console.log("createCryptoPassword 에서 에러 발생");
          reject(err);
        }
        resolve({ password: key.toString("base64"), salt });
      });
    });
  };
  // 비밀번호 검증
  const getCryptoPassword = (plainPassword, salt) => {
    return new Promise((resolve, reject) => {
      crypto.pbkdf2(plainPassword, salt, 10000, 64, "sha512", (err, key) => {
        if (err) {
          console.log("getCryptoPassword 에서 에러 발생");
          reject(err);
        }
        resolve({ password: key.toString("base64"), salt });
      });
    });
  };
module.exports = {createSalt, createCryptoPassword, getCryptoPassword} 
  • 사용자가 전달한 암호는 createCryptoPassword에서 createSalt로 생성한 Salt값을 활용하여 같이 sha-512 해쉬 알고리즘으로 해쉬화가 된다.

  • getCryptoPassword는 사용자가 입력한 암호와 DB에 저장되어 있는 Salt값을 이용하여 위와 동일하게 해쉬화 한다.

2. POST

1) /signup : 회원가입

exports.post_signup = async (req, res) => {
  let cryptoPassword = await createCryptoPassword(req.body.password);
  try{
    const result = await models.USER.findOrCreate({
      where : {
        email: req.body.email,
      },
      defaults : {
        name: req.body.name,
        email: req.body.email,
        password: cryptoPassword.password,
        salt: cryptoPassword.salt,
        gender: req.body.gender,
        telephone: req.body.telephone,
      }
    })
    if(result.at(-1) === false){
      console.log('fail')
      res.send('fail')
    }
    else{
      console.log('suc')
      res.send('success')
    }
  }
  catch(error){
    res.status(500).send("Error");
  }
};
  • Front-End 측에서 회원가입을 위해 post로 signup을 호출시 req.body에는 총 5개의 정보를 받게되고 사용자가 입력한 비밀번호는 createCryptoPassword로 해쉬화 되고 입력한 이메일이 DB에 존재할시 Front-End 측에 'fail'을 send하게 되고 존재 하지 않으면 받은 정보와 해쉬화 된 암호 및 Salt를 DB에 저장한다.

2) /signup/checkemail : 이메일 중복 체크

exports.checkEmail = async (req, res) => {
  let inputEmail = req.body.email
  try{
    const result = await models.USER.findOne({
      where : {
        email : inputEmail
      }
    })
    if(result !== null){ // 중복
      res.send(false)
    }
    else{ // 중복x
      res.send(true)
    }
  }
  catch{
    res.status(500).send("Error");
  }
};

결과

  • findOne의 결과값은 존재할시 값을 반환하고 없을시 null을 반환한다.
    즉, result의 값이 null이 아니면 중복되는 이메일이 존재한다.
    Front-End 측에서 전달받은 사용자가 입력한 이메일이 DB에 존재하면 중복이라는 의미이기 때문에, Front-End측에 메시지를 send한다.

3) /login : 로그인

exports.post_login = async (req, res) => {
  try{
     // 입력받은 아이디를 가진 사람을 찾아 salt와 입력한 비밀번호를 조합하며 저장된 비번과 같은지 확인
    const result = await models.USER.findOne({
      where:{
        email: req.body.email,
      }
    });
    const getCry = await getCryptoPassword(req.body.password, result.salt);
    if(getCry.password === result.password) {
      res.send({msg: 'success', userid : result.id, email : result.email}); // 성공메시지와 유저아디 반환
    }
    else {
    // 로그인 틀림
      res.send("fail");
    }
  }
  catch{
    res.status(500).send("Error");
  }
};

결과

  • Front-End 측에서 전달받은 이메일과 같은 유저를 DB에서 찾는다.
    그 다음 전달받은 암호 평문과 찾은 유저가 가진 Salt값을 이용하여 getCryptoPassword 함수를 실행하여 반환값이 DB에 저장된 해쉬화된 암호와 일치할 시 옳바른 유저이기 때문에 Front-End측으로 성공을 send 한다.

4) /room/make/:userid : 방 생성

exports.room = async (req, res) => {
  const userId = req.params.userid // 가입한 사람 id나 kakaoId
  try{
    const result = await models.USER.findOne({ // params랑 user의 id가 같은 사람 찾기
      where : {
        id : userId
      }
    })
    if(result !== null){ // params랑 user의 id가 같은 사람 발견
      await models.ROOM.create({
        id : userId,
        situation : req.body.situation,
        accent : req.body.accent,
        language : req.body.language,
      })
    }
    else{ // 다르면 카카오
      await models.ROOM.create({
        kakaoId : userId,
        situation : req.body.situation,
        accent : req.body.accent,
        language : req.body.language,
      })
    }
    res.send('success')
  }
  catch{
    res.status(500).send("Error");
  }
};

결과

  • Front-End에서 전달받은 params를 먼저 user 테이블ID와 비교를 하고 일치하는 값이 있으면 ROOM 테이블id에 저장되고, user 테이블에서 ID가 존재하지 않으면 Kakao 테이블에 저장된 kakaoId와 비교하여 Room 테이블kakaoId에 저장되며 Front-End 측에서 전달받은 언어,상황,말투로 방이 생성된다.

5) /kakao : OAuth로 받아온 kakao 유저 정보 저장

exports.kakao2 = async (req, res) => {
  try {
    const result = await models.KAKAO.findOrCreate({
      where : {
        kakaoId : req.body.id.toString()
      },
      defaults : {
        kakaoId : req.body.id.toString(),
        kakao_nickname : req.body.nickname
      }
    })
    if(result.at(-1) === false){ // 중복되면 
      res.send('fail')
    }
    else{ // 중복 안되면
      res.send('success')
    }
  } catch (error) {
    res.status(500).send("Error");
  }
};
  • Front-End 측에서 전달받은 idKAKAO 테이블에서 찾는다.
    findOrCreate는 중복 시 생성에 실패하기 때문에 반환값의 끝값이 False가 된다. 따라서 .at() 메소드를 사용하여 끝값을 뽑아내어 중복여부를 체크하여 Front측에 send 해 주었다.

6) /chat/:userid/:roomid : GPT와 대화

exports.runGPT35 = async (req, res) => {
  // MSG 정보 가져오기
  const userid = req.params.userid;
  const roomid = req.params.roomid;
  const result = await models.MSG.findAll({
    raw: true,
    where: {
      room_id: roomid,
      user_id: userid,
    },
  });
  if (result.length > 0) {
    // MSG 테이블이 비어있지 않다면
    let newMsg = [];
    for (let i = 0; i < result.length; i++) {
      newMsg.push({ role: result[i].part_id, content: result[i].content });
    }
    // 과거내역 불러오기
    const response = await openai.createChatCompletion({
      model: "gpt-3.5-turbo",
      messages: [...newMsg, { role: "user", content: req.body.msg }],
    });
    await models.MSG.create({
      part_id: "user",
      content: req.body.msg,
      room_id: roomid,
      user_id: userid,
    });
    await models.MSG.create({
      part_id: response.data.choices[0].message.role,
      content: response.data.choices[0].message.content,
      room_id: roomid,
      user_id: userid,
    });
    res.send(response.data.choices[0].message.content); // 답변 반환
  } else {
    // MSG 테이블이 비었다면 ROOM에 저장된 세팅 값으로 gpt 세팅
    const settings = await models.ROOM.findOne({
      raw: true,
      where: {
        room_id: roomid,
      },
    });
    const situation = settings.situation;
    const accent = settings.accent;
    const language = settings.language;
    const msg = `Let's play a role play. you can play any role in ${situation}.
                   but you must use ${language} and please speak with ${accent} accent.`;
    const response = await openai.createChatCompletion({
      model: "gpt-3.5-turbo",
      messages: [
        { role: "system", content: msg },
        { role: "user", content: req.body.msg },
      ],
    });
    // DB 추가
    await models.MSG.create({
      part_id: "system",
      content: msg,
      room_id: roomid,
      user_id: userid,
    });
    await models.MSG.create({
      part_id: "user",
      content: req.body.msg,
      room_id: roomid,
      user_id: userid,
    });
    await models.MSG.create({
      part_id: response.data.choices[0].message.role,
      content: response.data.choices[0].message.content,
      room_id: roomid,
      user_id: userid,
    });
    res.send(response.data.choices[0].message.content); // 답변 반환 
    }
 };

결과

  • Front-End 측에서 전달받은 userIdroomIDMSG 테이블을 조회한다.

  • MSG 테이블에 해당하는 값 존재O: newMSG라는 빈 배열에 해당하는 값들을 전부 넣어 GPT가 과거 무슨 대화를 나누었는지 알게 하고 전달받은 msg를 gpt에게 물어 답변과 msgMSG 테이블에 저장한다.

  • MSG 테이블에 해당하는 값 존재X: Room 테이블에서 해당하는 RoomID를 찾아 Room에 저장된 언어, 상황, 말투를 반환받아 gpt의 기본 prompt를 구성한다.
    그렇게 세팅된 초기값으로 전달받은 msg를 gpt에게 전달하여 답변을 받고 MSG 테이블에 저장한다.

3. GET

1) /room/all/:userid : 한 유저가 가진 모든 방 번호 가져오기

exports.allRoom = async(req,res)=>{
  const userId = req.params.userid
  try{
    const result = await models.ROOM.findAll({
      raw :true,
      where : {
        [Op.or] : [{id : userId},{kakaoId : userId}]
      }
    })
    let roomIds = []
    for(let i=0; i<result.length; i++){
      roomIds.push(result[i].room_id)
    }
    res.send(roomIds)
  }
  catch{
    res.status(500).send("Error");
  }
}

결과

  • Front-End 측에서 전달받은 paramsROOM 테이블id와 kakaoId를 비교하여 둘 중 하나와 일치하면 roomIds라는 빈 배열에 해당 결과값을 담아 Front측으로 전달한다.

2) /msg/:userid/:roomid : 한 유저가 가진 어떤 방에 대한 과거 대화내역 받아오기

exports.msg = async (req, res) => {
  const userid = req.params.userid;
  const roomid = req.params.roomid;
  const result = await models.MSG.findAll({
    raw: true,
    where: {
      room_id: roomid,
      user_id: userid,
    },
  });
  let newMsg = [];
  for (let i = 0; i < result.length; i++) {
    const { msg_id, room_id, user_id, ...others } = result[i];
    newMsg.push(others);
  }
  res.send(newMsg);
};

결과

  • Front-End 측에서 전달받은 params로 MSG 테이블의 user_idroom_id를 비교하여 둘 다 일치하는 해당 결과값을 result로 받아 newMSG에 객체 분해 할당하여 필요한 msg(part_id, content)...others를 받아 Front측에 전달한다.

0개의 댓글