본문 바로가기
프로젝트

토이 프로젝트 나만의 반려식물 만들기

by jyee 2025. 2. 16.
728x90
반응형

이전부터 만들어 보고 싶었던!!!! 푸망처럼 나만의 맞춤형 테스트를 만들고 싶었다!

예전부터 생각은 했지만 어떤 주제로 할까 계속 고민이 많았다.

처음에는 세계문학 책들 중 취향을 찾아서 맞춤형 책들을 추천하는 식으로 하려고 했는데 내가 관련 책들을 많이 안 읽어서 잘 모르기도 하고 데이터 수집해서 기획하기도 번거로워서 계속 미루고만 있었다. (하지만 올해 안에 이 주제로도 꼭 만들어야지)

 

🌱 이 테스트를 만들게 된 이유

그러다 작년부터 함께한 식물 모임에서 기획 아이디어가 떠올랐다.
우리 모임은 반려 식물을 키우며 자연과 함께 힐링하는 활동을 중심으로 하는데 모임원들이 어떤 식물을 키울지 고민하는 시간이 꽤 길게 걸린다는 걸 느꼈다.

 

여기서 만들게 보자고 결심하게 된 이유가 총 세 가지가 있는데, 

 

1️⃣ 나에게 맞는 반려식물을 찾기 어려운 초보들을 위해!

  • 검색해도 정보가 너무 많아서 고르기 힘들다.
  • 나처럼 귀찮지만 식물을 키우고 싶은 사람들에게 도움을 주고 싶었다.

2️⃣ 더 이상 식물을 죽이고 싶지 않다!

  • 나한테 맞는 식물을 미리 알면 더 오래 잘 키울 수 있지 않을까? 
  • 클릭 몇 번으로 추천받는 맞춤형 테스트가 있으면 좋겠다고 생각했다.

3️⃣ 모임원이 보내준 짤에 충격!

  • 🌿 그동안 내가 죽인 반려식물들에게 사죄하는 마음으로 만든다...🥲

그럼 만들기 시작~!!!


 

🌱프로젝트 목표와 기본 화면 구성 

이 프로젝트의 핵심은 간단한 클릭만으로 나에게 맞는 식물을 추천 받을 수 있는 테스트를 만드는 것이고 

크게 3단계로 나눠서 구현된다.

  • 시작화면 : 간단한 소개와 시작 버튼
  • Q&A화면 : 나에게 맞는 식물들을 찾기 위한 질문들
  • 결과 화면: 최종 결과를 보여주는 화면

🌱 HTML 기본 구조 만들기 

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="keywords" content="반려식물 찾기" />
    <meta name="description" content="반려식물 찾기" />

    <!-- sns share -->
    <meta property="og:url" content="https://twlovetype.netlify.app" />
    <meta property="og:title" content="반려식물 찾기" />
    <meta property="og:type" content="website" />
    <meta property="og:image" content="" />
    <meta property="og:description" content="나만의 반려식물 찾기" />

    <!-- favicon -->
    <link rel="shortcut icon" href="img/heart.ico" />
    <link rel="apple-touch-icon-precomposed" href="img/heart.ico" />

    <title>반려식물 찾기</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
      crossorigin="anonymous"
    />
    <link rel="stylesheet" href="./css/default.css" />
    <link rel="stylesheet" href="./css/main.css" />
    <link rel="stylesheet" href="./css/qna.css" />
    <link rel="stylesheet" href="./css/result.css" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Dongle&family=Inconsolata:wght@400;700&family=Jua&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="./css/animation.css" />
    <script src="https://developers.kakao.com/sdk/js/kakao.min.js"></script>
    <script>
      Kakao.init("cdb1b728841c80290ca5471e86f63392");
      Kakao.isInitialized();
    </script>
  </head>

  <body>
    <div class="container">
      <section id="main" class="mx-auto mt-5 mb-5 py-5 px-5">
        <div class="col-lg-6 col-md-8 col-sm-10 mx-auto">
          <img src="./img/introchorok.png" alt="introchorok" class="img-fluid" />
        </div>
        <p>
          나만의 반려식물 찾기<br />
          아래 시작하기 눌러주세요
        </p>
        <button
          type="button"
          class="btn btn-outline-info mb-4"
          onclick="js:begin()"
        >
          시작하기
        </button>
      </section>

      <section id="qna">
        <div class="qBox my-5 py-3 mx-auto"></div>
        <div class="answerBox mx-auto"></div>

        <!-- <div class="status mx-auto">
          <div class="statusBar"></div>
        </div> -->
      </section>

      <section id="result" class="mx-auto mt-5 mb-5 py-5 px-5">
        <h1>두근두근💞 결과는?</h1>
        <div class="resultname"></div>
        <div
          id="resultImg"
          class="my-3 col-lg-6 col-md-8 col-sm-10 col-12 mx-auto"
        ></div>
        <div class="resultDesc"></div>
        <button
          type="button"
          class="kakao mt-3 py-2 px-2"
          onclick="js:kakaoShare()"
        >
          공유하기
        </button>
      </section>
      <script src="./js/data.js" charset="utf-8"></script>
      <script src="./js/start.js" charset="utf-8"></script>
      <script src="./js/share.js"></script>
    </div>
  </body>
</html>

 

  • <head> 태그 안에 있는 내용(메타데이터, SEO, SNS공유태그, CSS&폰트 불러오기, 부트스트랩, 구글폰트 외부 리소스 추가) 
  • 메인컨텐츠 영역<body> 부분 #main /  #qna / #result 로 나눔
  • script연결 data.js(질문과 결과 데이터)  / start.js(테스트 진행로직) / share.js(sns공유기능)

 

🌱 JavaScript 기능 구현

 

data.js

이런 식으로 가장 보편적인 키울 수 있는 거주환경에

맞춤형 질문 위주로 해야하기에 관련 정보들을 모아서 질문지 리스트를 만들었다. 

 

qnaList에서 크게 type을 객관식 문항에 맞춰 1, 2, 3, 4로 하였다.

Type 1 (강한 채광 , 높은 습도, 성장 빠름)  

Type 2 (중간 채광, 보통 습도 , 관리 쉬움)

Type 3 ( 반음지, 건조에도 강함, 초보자용)

Type 4 (저조도, 건조환경, 초극저관리)  

 

infoList는 해당 결과값이다. 

이제 여기서 사용자가 어떤 type을 많이 눌렀는지 계산하고 해당 type의 식물 중 랜덤으로 결과값을 나오게 하려고 한다. 


start.js

const main = document.querySelector("#main");
const qna = document.querySelector("#qna");
const result = document.querySelector("#result");

const endPoint = 12;
const select = Array(12).fill(0); 

let typeCount = {
  1: 0, 
  2: 0,  
  3: 0,  
  4: 0   
};

// 점수 계산 함수
function calResult() {
  console.log('Type counts:', typeCount);

  // 가장 높은 count를 가진 type 찾기
  let maxCount = 0;
  let maxTypes = [];
  
  for (let type in typeCount) {
    if (typeCount[type] > maxCount) {
      maxCount = typeCount[type];
      maxTypes = [parseInt(type)];
    } else if (typeCount[type] === maxCount) {
      maxTypes.push(parseInt(type));
    }
  }
  
  // 최대 count를 가진 type이 여러 개인 경우 랜덤 선택
  const selectedType = maxTypes[Math.floor(Math.random() * maxTypes.length)];
  
  const possiblePlants = infoList.filter(plant => 
    plant.type.includes(selectedType)
  );
  
  // 가능한 식물들 중 랜덤 선택
  const selectedPlant = possiblePlants[Math.floor(Math.random() * possiblePlants.length)];
  
  return infoList.findIndex(plant => plant.name === selectedPlant.name);
}

// 답변 추가 함수
function addAnswer(answerText, qIdx, idx) {
  var a = document.querySelector(".answerBox");
  var answer = document.createElement("button");
  answer.classList.add("answerList", "my-3", "py-3", "mx-auto", "fadeIn");

  a.appendChild(answer);
  answer.innerHTML = answerText;

  answer.addEventListener(
    "click",
    function () {
      var children = document.querySelectorAll(".answerList");
      for (let i = 0; i < children.length; i++) {
        children[i].disabled = true;
        children[i].style.WebkitAnimation = "fadeOut 0.7s";
        children[i].style.animation = "fadeOut 0.7s";
      }

      setTimeout(() => {
        // type 카운트 증가
        const types = qnaList[qIdx].a[idx].type;
        types.forEach(type => {
          typeCount[type] = (typeCount[type] || 0) + 1;
        });
        
        for (let i = 0; i < children.length; i++) {
          children[i].style.display = "none";
        }

        goNext(++qIdx);
      }, 450);
    },
    false
  );
}

function setResult() {
  let point = calResult();

  if (!infoList[point]) {
    console.error("올바른 결과값을 찾을 수 없습니다.");
    return;
  }

  const resultName = document.querySelector('.resultname');
  resultName.innerHTML = infoList[point].name;

  const resultImg = document.createElement('img');
  const imgDiv = document.querySelector('#resultImg');
  resultImg.src = infoList[point].image;  // infoList에서 image 경로 가져오기
  resultImg.alt = infoList[point].name;  // alt 속성에 식물 이름 넣기
  resultImg.classList.add('img-fluid');
  imgDiv.appendChild(resultImg);

  const resultDesc = document.querySelector('.resultDesc');
  resultDesc.innerHTML = infoList[point].desc;
}

// 결과 페이지로 이동
function goResult() {
  qna.style.WebkitAnimation = "fadeOut 1s";
  qna.style.animation = "fadeOut 1s";
  
  setTimeout(() => {
    qna.style.display = "none";
    result.style.display = "block";
    result.style.WebkitAnimation = "fadeIn 1s";
    result.style.animation = "fadeIn 1s";

    setResult(); // 결과 설정 호출
  }, 450);
}

// 다음 질문으로 이동
function goNext(qIdx) {
  if (qIdx === endPoint) {
    goResult();
    return;
  }

  var q = document.querySelector(".qBox");
  q.innerHTML = qnaList[qIdx].q;

  for (let i in qnaList[qIdx].a) {
    addAnswer(qnaList[qIdx].a[i].answer, qIdx, i);
  }

  var status = document.querySelector(".statusBar");
  status.style.width = (100 / endPoint) * (qIdx + 1) + "%";
}

// 테스트 시작
function begin() {
  main.style.WebkitAnimation = "fadeOut 1s";
  main.style.animation = "fadeOut 1s";

  setTimeout(() => {
    main.style.display = "none";
    qna.style.display = "block";
    qna.style.WebkitAnimation = "fadeIn 1s";
    qna.style.animation = "fadeIn 1s";

    let qIdx = 0;
    goNext(qIdx);
  }, 450);
}

 

1. calResult 함수 - 점수 계산과 식물 추천

calResult 함수는 사용자 선택을 기반으로 가장 적합한 식물을 추천

  • typeCount 객체는 각 타입(1, 2, 3, 4)의 선택 횟수를 기록
  • 각 타입에 대한 선택 횟수를 비교하여 가장 많이 선택된 타입을 찾고, 같은 횟수를 선택한 타입이 여러 개 있을 경우 랜덤으로 하나를 선택
  • 선택된 타입에 해당하는 식물들만 필터링하여, 그 중 랜덤으로 하나를 추천
  • 추천된 식물은 infoList에서 해당 식물의 인덱스를 반환

2. addAnswer 함수 - 답변 추가 및 처리

사용자가 질문에 대한 답을 선택할 때마다 실행

  • 사용자가 선택한 답변을 화면에 표시합니다.
  • 답변 버튼을 클릭하면 버튼들이 사라지고, 클릭한 답변에 해당하는 타입을 typeCount 객체에 기록.
  • 이후 goNext 함수를 호출

3. setResult 함수 - 추천된 식물 결과 표시

setResult 함수는 추천된 식물 정보를 화면에 표시

  • calResult 함수로 계산된 인덱스를 사용하여, infoList에서 식물의 이름, 설명, 이미지 가져오기
  • 추천된 식물의 이름, 설명, 이미지를 결과 화면에 표시

4. goResult 함수 - 결과 화면으로 이동

사용자가 모든 질문에 답을 마친 후 결과 화면으로 이동하는 역할

  • qna 화면을 페이드 아웃 애니메이션으로 숨기고, result 화면을 페이드 인 애니메이션으로 표시
  • 결과 화면으로 이동하면서 setResult 함수를 호출하여 추천된 식물 정보를 업데이트

5. goNext 함수 - 다음 질문으로 이동

현재 질문에 대한 답변을 완료한 후, 다음 질문으로 넘어가는 함수

  • qIdx가 endPoint(총 질문 수)와 일치하면 goResult 함수를 호출하여 결과 페이지로 이동
  • 그렇지 않으면 qnaList에서 현재 질문에 해당하는 내용과 답변을 가져와서 화면에 표시
  • 상태 표시바(statusBar)는 진행 상태를 시각적으로 보여줌(이건 할까말까 고민중)

6. begin 함수 - 퀴즈 시작

퀴즈를 시작할 때 호출되는 함수

  • main 화면을 페이드 아웃하여 숨기고, qna 화면을 페이드 인하여 표시한다,
  • 첫 번째 질문을 표시하고, goNext(0)을 호출하여 퀴즈 진행을 시작됨.

어찌저찌 스무스하게 클릭되며 넘어가긴 한다..!

 

결과값도 내가 원하는대로 type에 맞춰 나오고 있긴한데 계산이 제대로 되는건지 확인이 더 필요할거 같다

일단은 전체적인 틀과 로직은 어느정도 되었기에 

이제 결과값에 이미지 넣기랑 해당 식물의 설명 글과 같은 디자인을 좀 더 꾸미고 카카오톡 공유하기 기능을 추가하는걸 넣어야 할거 같다.  

 


 

728x90
반응형

'프로젝트' 카테고리의 다른 글

[toyproject] Audio API  (0) 2023.11.25
[toyproject] text to voice converter 만들기  (0) 2023.11.21