$\colorbox{#e54c5c}{\textsf{\textbf{\color{white}GitHub}}}$

https://github.com/thelight0804/massi_dot_com

$\colorbox{#e54c5c}{\textsf{\textbf{\color{white}Japanese page}}}$

Massi.com

$\colorbox{#e54c5c}{\textsf{\textbf{\color{white}Reference}}}$

Firebase

React

https://github.com/jgudo/ecommerce-react

**https://ai.google.dev/gemini-api**

$\colorbox{#e54c5c}{\textsf{\textbf{\color{white}Process}}}$

process

$$ \huge\textsf{\textbf{\color{#e54c5c}맛있닷컴 (Massi.com)}} $$

<aside> <img src="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0fb2db70-d069-479e-8cd8-3f44fba669d3/blank.png" alt="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0fb2db70-d069-479e-8cd8-3f44fba669d3/blank.png" width="40px" /> 음식점 리뷰 사이트에서 손님이 남긴 리뷰에 대해 AI가 자동으로 답변을 생성하는 것을 목표로 제작되었습니다.

</aside>

<aside> 👉 日本語はこちらをご覧ください。

</aside>

맛있닷컴

https://github.com/thelight0804/massi_dot_com

Untitled

Untitled

Untitled

Untitled

⚠️ 주의!


<aside> ⛔ Firebase 공격으로 인한 과금을 방지하기 위해 읽기 권한을 제한했습니다. 모든 서비스를 사용하고 싶으시다면 ‘[email protected]’으로 문의 바랍니다.

</aside>

<aside> ⛔ To prevent charges due to Firebase attacks, we have restricted read permissions. If you wish to use all services, please contact ‘[email protected]

</aside>

🖥️ 아키텍처


image01.png

🛠️ 기술 스택


Firebase Frontend AI generator
Firestore React Google Gemini
Storage Tailwind CSS
Authentication Redux
Hosting React Router

Firestore

Firesotre는 실시간 데이터베이스 기능을 제공하며, 데이터가 클라우드에 저장하여 실시간으로 데이터를 동기화하여 여러 사용자 간에 상태를 즉시 공유할 수 있습니다. 식당ㆍ유저ㆍ리뷰 데이터를 저장하는데 Firesotre를 사용했습니다.

데이터 가져오기 (Read)

getRestaurants = async (itemsCount) => {
  const restaurants = []; // 식당 데이터 배열
  try {
    const q = **query(collection(this.db, "restaurant"), limit(itemsCount))**; // 데이터 가져오는 쿼리
    const querySnapshot = await **getDocs(q)**; // 쿼리 실행
    querySnapshot.forEach((doc) => {
      // 식당 데이터 배열에 추가
      restaurants.push({
        id: doc.id,
        ...doc.data(),
      });
    });
  } catch (e) {
    console.error('Firebase.getRestaurant: ', e);
  }
  return restaurants; // 식당 데이터 배열 반환
}

query() 함수에서 가져올 데이터를 컬렉션(collection)에서 선택하여 쿼리를 생성합니다. 이후에 getDocs() 함수에서 쿼리를 실행하고 데이터를 가져옵니다.

데이터 추가 (Write)

addRestaurant = async (value) => {
  try {
    // 메뉴 데이터에 이미지 제거
    var menu = value.menu.map(item => ({ ...item, image: null }));

    // Firestore에 식당 데이터 추가
    const docRef = await **addDoc(collection(this.db, "restaurant")**, {
      //... 코드 생략
      menu: menu, // 이미지를 제거한 메뉴 데이터 추가
      uid: value.uid, // 사용자 ID 추가
    });
    return docRef.id; // 등록된 식당 ID 반환
  } catch (e) {
    alert("식당 등록에 실패했습니다.\\n다시 시도해주세요.");
  }
}

addDoc() 함수에서 컬렉션(collection)에 문서(document)를 추가합니다.

Storage

Storage는 Firestore에는 저장할 수 없는 파일 유형을 저장할 수 있는 데이터베이스 서비스입니다. 이미지는 Storage에 저장하고 Firestore에는 이미지 경로를 저장하는 방식을 사용했습니다.

파일 저장

storeImage = async (file, path) => {
  // 임의의 파일명 생성
  const **filename** = 'file_' + Date.now() + '_' + Math.floor(Math.random() * 1000);

  // storage reference 생성
  const **storageRef** = ref(this.storage, path + filename);

  try {
    // 파일 업로드
    **uploadBytesResumable**(storageRef, file).then((snapshot) => {
      return getDownloadURL(storageRef);
    })
  } catch (e) {
    alert("이미지 업로드에 실패했습니다.\\n다시 시도해주세요.");
  }
}
}

파일명은 현재 시간과 난수를 조합하여 생성합니다. ref() 함수를 통해 이미지를 저장할 경로를 지정한 후 uploadBytesResumable() 함수에서 파일을 업로드합니다.

Authentication

Firebase Authentication은 사용자가 안전하게 로그인하고 인증할 수 있는 기능을 제공합니다. 이메일 & 비밀번호로 로그인을 구성했으며, 클라이언트의 localStorage에 로그인 정보를 저장하여 사용자가 웹 사이트를 재접속해도 로그인 상태를 유지합니다.

회원가입

signUp = async (**values**) =>
  await **createUserWithEmailAndPassword**(this.auth, values.email, values.password)
    .then((userCredential) => {
      const user = userCredential.user;
      return user.uid;
    })
    .catch((error) => {
      return error;
    });

사용자의 정보를 values 매개변수에 받아, createUserWithEmailAndPassword() 함수를 통해 회원가입을 진행합니다.

로그인 & 로그아웃

// 로그인
signIn = async (email, password) => 
  signInWithEmailAndPassword(this.auth, email, password)

// 로그아웃
signOut = async () => {
  try { await signOut(this.auth); }
  catch (error) { return error; }
}

Hosting

Firebase Hosting을 사용하여 웹 서비스를 배포했습니다. Firebase Hosting은 안정적이고 빠른 웹 호스팅을 제공받을 수 있으며, SSL 인증서를 기본 제공하여 안전된 연결을 제공합니다.

Untitled

Untitled

Tailwind CSS

Tailwind CSS는클래스 기반의 스타일 지정을 통해 웹 애플리케이션의 디자인을 쉽게 구축하고 관리할 수 있는 CSS 프레임워크입니다. CSS를 작성하는 대신 클래스를 HTML 요소에 적용하여 디자인을 적용하고, 컴포넌트를 조합하여 디자인을 구성했습니다.

예시 코드

const RestaurantItem = () => {
  return (
    <button
      **className**="flex w-full items-center truncate border-b-2 border-stone-100 hover:bg-red-100 md:m-5 md:w-1/3 md:border-2"
      //...
    >
      <img **className**="m-4 h-28 w-28 rounded-md" ... />
      <div>
        <h1 **className**="text-left text-lg font-bold">...</h1>
        <div **className**="flex items-center">
          //... 코드 생략
        </div>
    </button>
  );
};

className 속성에 여러 Tailwind 클래스를 공백으로 구분하여 추가합니다. 스타일을 빠르게 적용할 수 있다는 장점이 있었지만, 매번 Tailwind 공식 문서에서 필요한 속성을 검색해야 하고 JSX가 복잡해 보이는 단점이 있었습니다. 각 프로젝트의 성향에 맞게 선택하는 것이 좋을 거 같다고 생각합니다.

Google Gemini API

Google Gemini API는 Google의 차세대 언어 모델을 기반으로 한 인공지능 API로, 텍스트 생성, 분석, 번역 등 다양한 자연어 처리 기능을 제공합니다. 손님이 남긴 리뷰에 대한 답글을 자동으로 완성해주는 주요 기능에서 사용했습니다.

프롬프트

const generateReply = async (reply) => {
  setIsGeminiLoading(true);
  const **prompt** = `
    당신은 '${reply.restaurantName}'의 사장님입니다.\\n
    '${reply.userName}'님께 감사 인사를 전하고 싶습니다.\\n
    '${reply.review}'에 대한 답글을 작성해주세요.\\n
    손님이 주문한 메뉴는 '${reply.eatenMenu}'입니다.\\n
    손님들의 리뷰에 대해 공감과 감사의 표현을 포함하여 정중하고 성의 있게 답변합니다.\\n
    문제점이 지적된 리뷰에 대해서는 사과와 함께 문제를 해결하기 위한 방안을 제시하고, 긍정적인 리뷰에 대해서는 감사의 인사를 전합니다. \\n
    이모지를 적절히 사용하세요.\\n
    답변에 식당 이름을 반드시 포함하고, 손님 이름을 반드시 포함하세요.\\n
    하나의 답변만 생성하세요.\\n
  `;
  try {
    const content = await gemini.generateText(prompt);
    return content;
  } catch (e) {
    //... 코드 생략
  }
};

Google Gemini가 적절한 답글을 생성할 수 있도록, 답변 작성 가이드라인을 프롬프트로 정의했습니다. 프롬프트에는 손님의 리뷰 내용, 식당 이름, 주문한 메뉴 등을 포함하여, reply 매개변수로 받은 식당 정보를 답글에 반영합니다. 개선 사항으로, 사장님이 원하는 방식의 답글을 생성할 수 있도록 프롬프트를 변경하는 기능을 추가로 구현할 수 있습니다.

👥 팀원