프로젝트

토이 프로젝트 나만의 반려식물 만들기(진행 중)

jyee 2025. 2. 16. 13:07
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
반응형