토이 프로젝트 나만의 반려식물 만들기(진행 중)
이전부터 만들어 보고 싶었던!!!! 푸망처럼 나만의 맞춤형 테스트를 만들고 싶었다!
예전부터 생각은 했지만 어떤 주제로 할까 계속 고민이 많았다.
처음에는 세계문학 책들 중 취향을 찾아서 맞춤형 책들을 추천하는 식으로 하려고 했는데 내가 관련 책들을 많이 안 읽어서 잘 모르기도 하고 데이터 수집해서 기획하기도 번거로워서 계속 미루고만 있었다. (하지만 올해 안에 이 주제로도 꼭 만들어야지)
🌱 이 테스트를 만들게 된 이유
그러다 작년부터 함께한 식물 모임에서 기획 아이디어가 떠올랐다.
우리 모임은 반려 식물을 키우며 자연과 함께 힐링하는 활동을 중심으로 한다.
그런데 모임원들이 어떤 식물을 키울지 고민하는 시간이 꽤 길었다는 걸 깨달았다.
언제까지 미룰 셈인가!!
여기서 만들게 된 이유가 총 세 가지가 있는데,
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에 맞춰 나오고 있긴한데 계산이 제대로 되는건지 확인이 더 필요할거 같다
일단은 전체적인 틀과 로직은 어느정도 되었기에
이제 결과값에 이미지 넣기랑 해당 식물의 설명 글과 같은 디자인을 좀 더 꾸미고 카카오톡 공유하기 기능을 추가하는걸 넣어야 할거 같다.