본문 바로가기
Developer/TIL

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

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

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

반응형