1. oninput 이용해 메뉴 검색창 기능 만들기
✅ onInput 과 onChange의 차이
- oninput
: input 태그 내부의 값이 변경 될 때마다 이벤트 발생
- onchange
: input 태그의 포커스를 벗어났을때(입력 종료 시) 이벤트 발생
어드민 페이지의 사이드 바 내부 메뉴(페이지) 수가 많아짐에 따라, 특정 메뉴를 찾는 데 시간이 오래 걸렸다.
개발 및 테스트를 하면서도 불편함을 많이 느꼈는데 유저(운영자)의 불편함은 말할 수도 없겠지🤨 그래서 이번 기회에 메뉴 검색 기능을 추가해 보았다.
유저가 input에 입력한 값이 변경될 때마다, 즉각적으로 검색어와 일치하는 메뉴는 보이고 그 외의 메뉴는 보이지 않도록 구현하고 싶었다. 따라서 위의 두 개 이벤트 중 oninput
을 이용했다.
또한 input
에 v-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 정의된 필드 타입에 따라 컬럼 우클릭시 노출되는 컨텍스트 메뉴 변경 (코드 타입인 경우 코드 변경 메뉴 노출)