본문 바로가기
Developer/TIL

TIL : 250328금 (메뉴 검색창 만들기, 즐겨찾기 기능 구현, 프로그래머스 유연근무제)

by 청량리 물냉면 2025. 3. 29.
반응형

1. oninput 이용해 메뉴 검색창 기능 만들기

✅ onInput 과 onChange의 차이

- oninput: input 태그 내부의 값이 변경 될 때마다 이벤트 발생

- onchange: input 태그의 포커스를 벗어났을때(입력 종료 시) 이벤트 발생

 

어드민 페이지의 사이드 바 내부 메뉴(페이지) 수가 많아짐에 따라, 특정 메뉴를 찾는 데 시간이 오래 걸렸다.

개발 및 테스트를 하면서도 불편함을 많이 느꼈는데 유저(운영자)의 불편함은 말할 수도 없겠지🤨 그래서 이번 기회에 메뉴 검색 기능을 추가해 보았다.

유저가 input에 입력한 값이 변경될 때마다, 즉각적으로 검색어와 일치하는 메뉴는 보이고 그 외의 메뉴는 보이지 않도록 구현하고 싶었다. 따라서 위의 두 개 이벤트 중 oninput을 이용했다. 

또한 inputv-model을 추가해서 스크립트에서도 input 필드내부의 값을 사용할 수 있도록 했다.

const search = () => {
const menuItems = document.querySelectorAll(".menu label");
menuItems.forEach((item) => {
const text = item.textContent.toLowerCase().trim();
if (text.includes(searchInput.value.toLowerCase().trim())) {
item.parentElement.style.display = "block";
} else {
item.parentElement.style.display = "none";
}
});
};

 

위와 같이 querySelector를 이용해서 직접 돔 조작을 하는 방식으로 짰는데, vue에서(React도 마찬가지) 직접 DOM조작을 하는 코드는 그리 좋지 않다고 알고있다.

하지만 다른 방법을 모르겠다.🙄

 

 

2. 프로그래머스 Lv.1 유연근무제

function solution(schedules, timelogs, startday) {
var answer = 0;
for(let i = 0; i < schedules.length; i++){
let chul = 0;
for(let j = 0; j < timelogs[i].length; j++){
let dayOfWeek = (startday + j - 1) % 7 + 1;
if(dayOfWeek === 6 || dayOfWeek === 7) continue;
let sigan = schedules[i] + 10;
// 10분을 더한 시각까지 출근을 완료했는지 체크
if(sigan % 100 >= 60) {
sigan = ((sigan / 100 | 0) + 1) * 100 + (sigan % 100 - 60);
}
if(timelogs[i][j] <= sigan) chul++;
}
if(chul >= 5) answer++;
}
return answer;
}

 

 

 

3. 페이지 즐겨찾기 기능 구현

✅ 객체를 순회하면서 특정 속성에 따라 필터링하는 방법 

📌 Object.values() + filter() 사용

const bookmarkMenuItems2 = ref(Object.values(bookmarkMenuItems.value).filter(item => item.isFilled));
  • Object.values(bookmarkMenuItems.value): 객체의 값을 배열로 변환
  • .filter(item => item.isFilled): isFilled가 true인 것만 남김

 

✅ 즐겨찾기 버튼을 누를 시, 즉각적으로 즐겨찾기 항목의 리스트를 변경하도록 구현 -> 성능 문제???

기존에는 유저가 즐겨찾기 버튼을 누를 때마다

const toggleStar = (menuKey) => {
bookmarkMenuItems.value[menuKey].isFilled = !bookmarkMenuItems.value[menuKey].isFilled;
// 변경 즉시 리스트 업데이트
bookmarkMenuItems2.value = Object.values(bookmarkMenuItems.value).filter(item => item.isFilled);
};

이런 식으로 필터링을 해주었다. 

그런데 filter가 전체 배열을 한번 순회하는 메서드이다 보니, 성능적인 측면에서 과연 괜찮은가 하는 의문이 들었다.

생각해 보니 추가 시에는 굳이 filter 사용을 하지 않아도 될 것 같았고, 삭제 시에도 기존 배열을 수정하면 될 것 같아서 아래와 같이 코드를 수정했다. 

const toggleStar = (menuKey) => {
bookmarkMenuItems.value[menuKey].isFilled = !bookmarkMenuItems.value[menuKey].isFilled;
if (item.isFilled) {
// 별이 채워지면 리스트에 추가
bookmarkMenuItems2.value.push(item);
} else {
// 별이 해제되면 리스트에서 제거
bookmarkMenuItems2.value = bookmarkMenuItems2.value.filter((i) => i !== item);
}
};

 

 

 

✅ 옵셔널 체이닝

bookmarkMenuItems[menu.name]["isFilled"] ? "★" : "☆"

bookmarkMenuItems[menu.name]이 undefined일 때 에러 없이 "☆" 유지하는 방법

<button @click="toggleStar(menu.name)">
{{ bookmarkMenuItems[menu.name]?.isFilled ? "★" : "☆" }}
</button>

👍 ?.을 사용하면 bookmarkMenuItems[menu.name]undefined라도 안전하게 "☆" 반환
👍 isFilled이 true면 "★", false 또는 undefined면 "☆" return

 

🔹 대체 방법 (null 병합 연산자 ?? 사용)

{{ (bookmarkMenuItems[menu.name]?.isFilled ?? false) ? "★" : "☆" }}

 

👍?? false를 추가하면 undefined일 때 false로 처리되어 "☆"이 반환된다.

 

 

✅  localstorage에서 js로 불러 값을 불러올 때

  • localStorage는 문자열(String)만 저장 가능
  • object → string 변환: JSON.stringify(object)
  • string → object 변환: JSON.parse(string)
  • JSON.parse()를 하지 않으면 문자열 그대로 사용 가능하지만, forEach() 같은 배열 메서드는 동작하지 않음

예제 코드

1️⃣ object → string 변환 후 저장

const user = { name: "Alice", age: 25 };
localStorage.setItem("user", JSON.stringify(user));

2️⃣ string → object 변환 후 사용

const userData = JSON.parse(localStorage.getItem("user"));
console.log(userData.name); // Alice

JSON.parse()를 하지 않은 경우 문제 발생

const userData = localStorage.getItem("user");
console.log(userData.name); // undefined

JSON.parse() 후 배열 메서드 사용 예제

const users = [{ name: "Alice" }, { name: "Bob" }];
localStorage.setItem("users", JSON.stringify(users));
const storedUsers = JSON.parse(localStorage.getItem("users"));
storedUsers.forEach(user => console.log(user.name));
// Alice
// Bob

이렇게 사용하면 forEach(), map(), filter() 등 배열 메서드도 문제없이 사용 가능하다.

 

 

최종 코드는 아래와 같다. 

<template>
<transition name="fade-slide">
<div
class="sidebar"
:class="{ hidden: !sideMenuStore.isMenuVisible }"
v-show="sideMenuStore.isMenuVisible">
<div class="sidebar-header" @click="toConsoleRoot">
<h3>{{ side_title }}</h3>
<div class="close" @click="sideMenuStore.toggleSideMenu"></div>
</div>
<input
type="text"
class="search"
v-model="searchInput"
placeholder="검색어를 입력하세요."
:oninput="search" />
<div class="sidebar-menu">
<ul>
<li class="has-submenu" :class="{ active: isActive('bookmark') }">
<div @click="toggleSubMenu('bookmark')" class="submenu-wrap">
<span style="color: #f1c40f">즐겨찾기</span>
</div>
<ul class="submenu" v-if="isSubMenuOpen['bookmark']">
<li
v-for="(menu, index) in activeBookmarks"
:key="index"
class="menu"
:class="{ active: isActiveItem(menu.name) }">
<button
type="button"
@click="handleChangeButton(menu)"
:style="starStyle(menu)"
:key="menu.name">
{{ bookmarks[menu.name]["isMarked"] ? "★" : "☆" }}
</button>
<label v-on:click="movePage(menu)">{{ menu.label }}</label>
</li>
</ul>
</li>
<li class="has-submenu" :class="{ active: isActive('ㅌ') }">
<div @click="toggleSubMenu('trs')" class="submenu-wrap">
<span>ㅇㅇ</span>
</div>
<ul class="submenu" v-if="isSubMenuOpen['ㅌ']">
<li
v-for="(menu, index) in MENU_ITEMS.transMenuItems"
:key="index"
class="menu"
:class="{ active: isActiveItem(menu.name) }">
<button
type="button"
@click="handleChangeButton(menu)"
:style="starStyle(menu)"
:key="menu.name">
{{ bookmarks[menu.name]["isMarked"] ? "★" : "☆" }}
</button>
<label v-on:click="movePage(menu)">
{{ menu.label }}
</label>
</li>
</ul>
</li>
...
</ul>
</li>
</ul>
</div>
</div>
</transition>
</template>
<script>
...
export default defineComponent({
name: "SideMenu",
setup() {
const searchInput = ref("");
const bookmarks = ref(JSON.parse(localStorage.getItem("bookmark")) || {});
const activeBookmarks = ref(
Object.values(bookmarks.value).filter((item) => item.isMarked) || [],
);
...
const search = () => {
const menuItems = document.querySelectorAll(".menu label");
menuItems.forEach((item) => {
const text = item.textContent.toLowerCase().trim();
if (text.includes(searchInput.value.toLowerCase().trim())) {
item.parentElement.style.display = "block";
} else {
item.parentElement.style.display = "none";
}
});
};
const handleChangeButton = (menu) => {
const bookmarkMenu = bookmarks.value[menu.name];
if (!bookmarkMenu) {
bookmarks.value[menu.name] = { ...menu, isMarked: false };
}
bookmarkMenu["isMarked"] = !bookmarkMenu["isMarked"];
localStorage.setItem("bookmark", JSON.stringify(bookmarks.value));
if (bookmarkMenu["isMarked"]) {
activeBookmarks.value.push(bookmarkMenu);
} else {
activeBookmarks.value = activeBookmarks.value.filter((item) => item.name !== menu.name);
}
};
const starStyle = (menu) => {
if (!bookmarks.value[menu.name]) {
bookmarks.value[menu.name] = { ...menu, isMarked: false };
}
return {
backgroundColor: "inherit",
color: bookmarks.value[menu.name]["isMarked"] ? "yellow" : "white",
padding: 0,
};
};
return {
...
};
},
});
</script>
<style scoped>
...
.search {
width: 80%;
height: 25px;
border-radius: 5px;
padding-left: 10px;
background-color: white;
}
.search:focus {
border: #5fcadb solid 1px;
outline: none;
}
label > text {
cursor: pointer;
}
.menu {
display: flex;
flex-direction: row;
gap: 3px;
align-content: center;
}
.menu button {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
...
</style>

짜고 보니 중복이 너무 많아서, 중복 제거를 위한 리팩토링 진행 중이다.

 

 

 

 

2025년 3월 4주차 작업 목록

1. WEB 서비스 점검 모달 구현 확인 및 마무리 작업

2. 카테고리 변경 모달 작성 및 카테고리 수정 API 연결, input filed 컴포넌트화 (수정 기능 추가 확인 필요)
2. ADMIN 거래소 수수료 집계 페이지
- 계좌 타입 필드 관련 selectbox null 에러 수정
- 코드 수정 모달에서, 코드 입력 => 전체삭제 => 다시 모달창 열 경우 내부에 빈 input field가 생성되는 에러 수정
3. ADMIN 정의된 필드 타입에 따라 컬럼 우클릭시 노출되는 컨텍스트 메뉴 변경 (코드 타입인 경우 코드 변경 메뉴 노출)

반응형