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

2025. 3. 29. 14:44·Developer/TIL
반응형

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 정의된 필드 타입에 따라 컬럼 우클릭시 노출되는 컨텍스트 메뉴 변경 (코드 타입인 경우 코드 변경 메뉴 노출)

반응형
저작자표시 비영리 변경금지 (새창열림)

'Developer > TIL' 카테고리의 다른 글

TIL : 250402수 (falsy값 처리(?., ??, || 언제 써야 할까?), setTimeout 0ms 비동기 처리, 프로그래머스 완주하지 못한 선수, for...in vs for...of 차이점 정리, 프로그래머스 실패율)  (0) 2025.04.02
TIL : 250401화 (:active, 즐겨찾기 기능 css 변경 진행중-event.target, menuRefs, white-space, 프로그래머스 문자열 내림차순으로 배치하기)  (0) 2025.04.01
TIL : 250331월 (프로그래머스 해시, 윤년, 같은 숫자는 싫어, 문자열 내 마음대로 정렬하기(sort()함수에 대해 알아보기))  (1) 2025.03.31
[TIL] 250327목 (isNaN, Number.isNaN / split, splice, slice / reduce / map vs reduce)  (1) 2025.03.27
[TIL] 250326수 (자바스크립트 BFS, BFS가 필요한 문제 패턴, PGM 음양마스터, selectbox 데이터 null값 뜨는 문제 수정)  (1) 2025.03.26
[TIL] 20250325화 (자바스크립트의 배열, 연결리스트, 완전탐색(모의고사), reduce와 filter의 시간복잡도 계산)  (1) 2025.03.25
'Developer/TIL' 카테고리의 다른 글
  • TIL : 250401화 (:active, 즐겨찾기 기능 css 변경 진행중-event.target, menuRefs, white-space, 프로그래머스 문자열 내림차순으로 배치하기)
  • TIL : 250331월 (프로그래머스 해시, 윤년, 같은 숫자는 싫어, 문자열 내 마음대로 정렬하기(sort()함수에 대해 알아보기))
  • [TIL] 250327목 (isNaN, Number.isNaN / split, splice, slice / reduce / map vs reduce)
  • [TIL] 250326수 (자바스크립트 BFS, BFS가 필요한 문제 패턴, PGM 음양마스터, selectbox 데이터 null값 뜨는 문제 수정)
청량리 물냉면
청량리 물냉면
프로그래밍 공부를 하고 있습니다. 공부 내용 정리 겸 정보 공유를 목적으로 합니다.
    반응형
  • 청량리 물냉면
    노력중인 블로그
    청량리 물냉면
  • 전체
    오늘
    어제
    • 분류 전체보기 (506)
      • 프로그래밍 (41)
        • Programming (1)
        • C | C++ (6)
        • Java (28)
        • Python (5)
      • 웹 프로그래밍 (2)
        • HTML | CSS (5)
        • JavaScript | TypeScript (41)
        • React (25)
        • Vue.js (0)
        • Next.js (18)
        • Spring & Spring Boot (13)
        • JSP & Servlet (1)
        • DB (4)
      • 웹 프로젝트 (77)
        • 웹 프로젝트 (22)
        • 🥨스낵몰 (3)
        • 👨‍👨‍👧‍👧소셜 가계부 (26)
        • 🌜꿈 일기장 (11)
        • 🔮포트폴리오 사이트 (11)
        • 🏃‍♂️팀 프로젝트: 일정관리 프로그램 (0)
        • 📈팀 프로젝트: AI기반 주식 분석 플랫폼 (0)
        • 😺Just Meow It: 조언 사이트 (2)
        • 📕Workly: 교대근무 다이어리 (1)
      • 앱 프로그래밍 (26)
        • Flutter (24)
        • Kotlin (2)
      • Problem Solving (166)
        • 백준 (52)
        • 프로그래머스 (79)
        • SWEA (29)
      • Computer Science (40)
        • 알고리즘 (14)
        • 컴퓨터 네트워크 (18)
        • 이산수학 (8)
      • Developer (47)
        • 후기 (4)
        • 자료정리 (4)
        • 취업 | 취준 (9)
        • SSAFY (1)
        • 웹개발 교육 프로그램 (9)
        • TIL (20)
  • 블로그 메뉴

    • 홈
    • Github
  • 공지사항

    • 프로그래밍 공부 중😊
  • 인기 글

  • 태그

    파이썬
    뉴렉처
    d3
    컴퓨터네트워크
    블로그 제작
    리액트
    React
    알고리즘
    AWS
    Til
    자바스크립트
    플러터
    프로그래머스
    공식문서
    강의내용정리
    SWEA
    자바
    mysql
    백준
    bfs
    spring boot
    ZeroCho
    클론 프로젝트
    프로젝트
    타입스크립트
    구현
    Next.js
    포트폴리오
    웹사이트
    Jiraynor Programming
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
청량리 물냉면
TIL : 250328금 (메뉴 검색창 만들기, 즐겨찾기 기능 구현, 프로그래머스 유연근무제)
상단으로

티스토리툴바