[251023] fix: 일반검색 수정

🕐 커밋 시간: 2025. 10. 23. 10:25:47

📊 변경 통계:
  • 총 파일: 4개
  • 추가: +33줄
  • 삭제: -12줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchInpuOverlay.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.module.less

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript):
     Added: SpotlightContainerDecorator()
    🔄 Modified: clearAllTimers()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.module.less (unknown):
     Added: scale()
This commit is contained in:
2025-10-23 10:25:48 +09:00
parent 4ffacbdef9
commit 33c34b6f44
4 changed files with 120 additions and 195 deletions

View File

@@ -1,73 +1,65 @@
import React, {
useCallback,
useEffect,
useMemo,
} from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { getSearch } from '../../actions/searchActions';
import TFullPopup from '../../components/TFullPopup/TFullPopup';
import TInput, {
ICONS,
KINDS,
} from '../../components/TInput/TInput';
import TInput, { ICONS, KINDS } from '../../components/TInput/TInput';
import css from './SearchInputOverlay.module.less';
const OverlayContainer = SpotlightContainerDecorator(
{
enterTo: "default-element",
restrict: "self-only", // 포커스를 overlay 내부로만 제한
},
"div"
);
// const OverlayContainer = SpotlightContainerDecorator(
// {
// enterTo: "default-element",
// restrict: "self-only", // 포커스를 overlay 내부로만 제한
// },
// "div"
// );
const SpottableKeyword = Spottable("div");
const SpottableKeyword = Spottable('div');
const SearchInputOverlay = ({
isVisible,
onClose,
searchQuery = "",
searchQuery = '',
onSearchChange,
onSearchSubmit,
// onSearchSubmit,
handleClick,
}) => {
const dispatch = useDispatch();
const recentResultSearches = useMemo(
() => [
"Puppy food",
"Dog toy",
'Puppy food',
'Dog toy',
"Mather's Day",
"Gift",
"Easter Day",
"Royal Canin puppy food2",
"Shark",
'Gift',
'Easter Day',
'Royal Canin puppy food2',
'Shark',
],
[]
);
// Input 창에서 API 호출 핸들러 (돋보기 아이콘 클릭 시에만)
const handleSearchSubmit = useCallback(
(query) => {
console.log("[SearchInputOverlay] Search submit:", searchQuery);
console.log('[SearchInputOverlay] Search submit:', searchQuery);
if (searchQuery && searchQuery.trim()) {
dispatch(
getSearch({
service: "com.lgshop.app",
service: 'com.lgshop.app',
query: query,
domain: "theme,show,item",
domain: 'theme,show,item',
})
);
// API 호출 후 Input 박스로 포커스 이동
setTimeout(() => {
onClose();
Spotlight.focus("voice-overlay-input-box");
Spotlight.focus('voice-overlay-input-box');
}, 100);
}
},
@@ -76,7 +68,7 @@ const SearchInputOverlay = ({
// Input 창에서 엔터키 핸들러 (API 호출하지 않음)
const handleInputKeyDown = useCallback((e) => {
if (e.key === "Enter" || e.keyCode === 13) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
// Enter 키로는 API 호출하지 않음
// 돋보기 아이콘 클릭/Enter로만 API 호출
@@ -85,7 +77,7 @@ const SearchInputOverlay = ({
const handleDimClick = useCallback(
(e) => {
console.log("[VoiceInputOverlay] dimBackground clicked");
console.log('[VoiceInputOverlay] dimBackground clicked');
onClose();
},
[onClose]
@@ -93,11 +85,9 @@ const SearchInputOverlay = ({
useEffect(() => {
if (isVisible) {
const input = document.querySelector(
`[data-spotlight-id="input-field-box"] > input`
);
console.log("###input", input);
console.log("###input.value", input.value);
const input = document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
console.log('###input', input);
console.log('###input.value', input.value);
if (input) {
input.focus();
}
@@ -110,7 +100,7 @@ const SearchInputOverlay = ({
onClose={onClose}
noAutoDismiss={true}
spotlightRestrict="self-only"
spotlightId={"search_input_overlay_container"}
spotlightId={'search_input_overlay_container'}
noAnimation={false}
scrimType="transparent"
className={css.tFullPopupWrapper}
@@ -132,7 +122,7 @@ const SearchInputOverlay = ({
onIconClick={() => {
handleSearchSubmit(searchQuery);
}}
spotlightId={"search_overlay_input_box"}
spotlightId={'search_overlay_input_box'}
spotlightBoxDisabled
/>
</div>

View File

@@ -1,37 +1,19 @@
// src/views/SearchPanel/SearchPanel.new.jsx
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import micIcon from '../../../assets/images/searchpanel/image-mic.png';
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
import hotPicksBrandImage
from '../../../assets/images/searchpanel/img-search-hotpicks.png';
import {
sendLogGNB,
sendLogTotalRecommend,
} from '../../actions/logActions';
import hotPicksBrandImage from '../../../assets/images/searchpanel/img-search-hotpicks.png';
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
import {
popPanel,
pushPanel,
updatePanel,
} from '../../actions/panelActions';
import { popPanel, pushPanel, updatePanel } from '../../actions/panelActions';
import {
getSearch,
getSearchMain,
@@ -48,64 +30,43 @@ import {
// } from '../../actions/toastActions';
import TBody from '../../components/TBody/TBody';
import TPanel from '../../components/TPanel/TPanel';
import TVerticalPagenator
from '../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList
from '../../components/TVirtualGridList/TVirtualGridList';
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
import usePrevious from '../../hooks/usePrevious';
import {
LOG_CONTEXT_NAME,
LOG_MENU,
LOG_MESSAGE_ID,
panel_names,
} from '../../utils/Config';
import NoSearchResults from './NoSearchResults/NoSearchResults';
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
// import NoSearchResults from './NoSearchResults/NoSearchResults';
import SearchInputOverlay from './SearchInpuOverlay';
import css from './SearchPanel.new.module.less';
import SearchResultsNew from './SearchResults.new';
import TInput, {
ICONS,
KINDS,
} from './TInput/TInput';
import VoiceInputOverlay, {
VOICE_MODES,
} from './VoiceInputOverlay/VoiceInputOverlay';
import TInput, { ICONS, KINDS } from './TInput/TInput';
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';
const ContainerBasic = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
// 검색 입력 영역 컨테이너
const InputContainer = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const InputContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
// 콘텐츠 섹션 컨테이너
const SectionContainer = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const SectionContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
// Spottable 컴포넌트들
const SpottableMicButton = Spottable("div");
const SpottableKeyword = Spottable("div");
const SpottableProduct = Spottable("div");
const SpottableMicButton = Spottable('div');
const SpottableKeyword = Spottable('div');
const SpottableProduct = Spottable('div');
const ITEMS_PER_PAGE = 9;
// Spotlight ID 상수
const SPOTLIGHT_IDS = {
SEARCH_INPUT_LAYER: "search-input-layer",
SEARCH_INPUT_BOX: "search-input-box",
MICROPHONE_BUTTON: "microphone-button",
RECENT_SEARCHES_SECTION: "recent-searches-section",
TOP_SEARCHES_SECTION: "top-searches-section",
POPULAR_BRANDS_SECTION: "popular-brands-section",
HOT_PICKS_SECTION: "hot-picks-section",
SEARCH_VERTICAL_PAGENATOR: "search_verticalPagenator",
SEARCH_INPUT_LAYER: 'search-input-layer',
SEARCH_INPUT_BOX: 'search-input-box',
MICROPHONE_BUTTON: 'microphone-button',
RECENT_SEARCHES_SECTION: 'recent-searches-section',
TOP_SEARCHES_SECTION: 'top-searches-section',
POPULAR_BRANDS_SECTION: 'popular-brands-section',
HOT_PICKS_SECTION: 'hot-picks-section',
SEARCH_VERTICAL_PAGENATOR: 'search_verticalPagenator',
};
export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
@@ -121,21 +82,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 0hun: 패널 전역 상태
const panels = useSelector((state) => state.panels.panels);
// 0hun: 음성 검색 결과에 대한 전역 상태
const shopperHouseData = useSelector(
(state) => state.search.shopperHouseData
);
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
// 0hun: 검색 메인, Hot Picks for you 영역에 대한 전역 상태 값
const hotPicksForYou = useSelector(
(state) => state.search.searchMainData.hotPicksForYou
);
const hotPicksForYou = useSelector((state) => state.search.searchMainData.hotPicksForYou);
// 0hun: 검색 메인, Popular Brands 영역에 대한 전역 상태 값
const popularBrands = useSelector(
(state) => state.search.searchMainData.popularBrands
);
const popularBrands = useSelector((state) => state.search.searchMainData.popularBrands);
// 0hun: 검색 메인, Top Searchs 영역에 대한 전역 상태 값
const topSearchs = useSelector(
(state) => state.search.searchMainData.topSearchs
);
const topSearchs = useSelector((state) => state.search.searchMainData.topSearchs);
/**
* states
@@ -143,9 +96,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 0hun: 초기 포커스 유무를 나타내는 Boolean 상태
const [firstSpot, setFirstSpot] = useState(false);
// 0hun: 검색어 상태
const [searchQuery, setSearchQuery] = useState(
panelInfo.searchVal ? panelInfo.searchVal : null
);
const [searchQuery, setSearchQuery] = useState(panelInfo.searchVal ? panelInfo.searchVal : null);
// 0hun: 검색 컨테이너 포커스 position 상태 값
const [position, setPosition] = useState(null);
// 0hun: 가상 키보드 Display 유무 Boolean 값
@@ -159,9 +110,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// TInput의 입력 모드 상태 (webOS 키보드가 뜨는지 여부)
const [isInputModeActive, setIsInputModeActive] = useState(false);
// 0hun: 현재 포커스된 container의 spotlightId를 관리하는 상태 값
const [focusedContainerId, setFocusedContainerId] = useState(
panelInfo?.focusedContainerId
);
const [focusedContainerId, setFocusedContainerId] = useState(panelInfo?.focusedContainerId);
/**
* refs
@@ -177,10 +126,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
* memoized variables
*/
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
const recentSearches = useMemo(
() => ["Puppy food", "Dog toy", "Fitness"],
[]
);
const recentSearches = useMemo(() => ['Puppy food', 'Dog toy', 'Fitness'], []);
// Voice overlay suggestions (동적으로 변경 가능)
const voiceSuggestions = useMemo(
@@ -216,7 +162,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}
// Enter 키 처리
if (e.key === "Enter") {
if (e.key === 'Enter') {
// e.preventDefault();
if (showVirtualKeyboard) {
// 가상 키보드가 열려있으면 검색 실행하고 키보드 닫기
@@ -230,32 +176,26 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 방향키 처리 - Spotlight 네비게이션 허용
const arrowKeys = [
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Left",
"Right",
"Up",
"Down",
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown',
'Left',
'Right',
'Up',
'Down',
];
if (arrowKeys.includes(e.key)) {
// 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지
if (
position === 0 &&
(e.key === "Left" || e.key === "ArrowLeft") &&
!searchQuery
) {
if (position === 0 && (e.key === 'Left' || e.key === 'ArrowLeft') && !searchQuery) {
e.preventDefault();
return;
}
// 오른쪽 화살표 키 처리 - 포커스 이동 허용
if (e.key === "ArrowRight" || e.key === "Right") {
if (e.key === 'ArrowRight' || e.key === 'Right') {
// 커서가 텍스트 끝에 있을 때만 포커스 이동 허용
const input = document.querySelector(
`[data-spotlight-id="input-field-box"] > input`
);
const input = document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
if (input && position === input.value.length) {
// 커서가 텍스트 끝에 있으면 포커스 이동 허용
return;
@@ -274,8 +214,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
*/
const handleInputModeChange = useCallback((isActive) => {
console.log(
"[SearchPanel] TInput 입력 모드:",
isActive ? "활성화 (키보드 표시)" : "비활성화 (키보드 숨김)"
'[SearchPanel] TInput 입력 모드:',
isActive ? '활성화 (키보드 표시)' : '비활성화 (키보드 숨김)'
);
setIsInputModeActive(isActive);
}, []);
@@ -303,9 +243,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
if (query.trim()) {
dispatch(
getSearch({
domain: "theme,show,item",
domain: 'theme,show,item',
query: query,
service: "com.lgshop.app",
service: 'com.lgshop.app',
})
);
@@ -328,9 +268,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
* 0hun: keyUp 이벤트 핸들러, keyUp 시 `position`의 상태값 변경하는 함수
*/
const cursorPosition = useCallback(() => {
const input = document.querySelector(
`[data-spotlight-id="input-field-box"] > input`
);
const input = document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
if (input) {
setPosition(input.selectionStart);
@@ -346,7 +284,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}
console.log(
"🖱️ [DEBUG][SearchPanel] onClickMic called, current isVoiceOverlayVisible:",
'🖱️ [DEBUG][SearchPanel] onClickMic called, current isVoiceOverlayVisible:',
isVoiceOverlayVisible
);
setIsVoiceOverlayVisible(true);
@@ -367,14 +305,20 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
return;
}
if (searchQuery === null || searchQuery === "") {
// SearchInputOverlay가 열려있으면 먼저 닫기
if (isSearchOverlayVisible) {
setIsSearchOverlayVisible(false);
return;
}
if (searchQuery === null || searchQuery === '') {
dispatch(popPanel(panel_names.SEARCH_PANEL));
} else {
setSearchQuery("");
setSearchQuery('');
dispatch(resetSearch());
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}
}, [isVoiceOverlayVisible, searchQuery]);
}, [isVoiceOverlayVisible, isSearchOverlayVisible, searchQuery]);
/**
* 0hun: paginator 내부 아이템들의 onFocus 이벤트, containerId인자로 받는다
@@ -393,7 +337,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
Spotlight.resume();
setFirstSpot(true);
if (panelInfo.currentSpot) {
if (panels[panels.length - 1]?.name === "searchpanel") {
if (panels[panels.length - 1]?.name === 'searchpanel') {
Spotlight.focus(panelInfo.currentSpot);
}
}
@@ -435,6 +379,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
*/
const handleSearchOverlayClose = useCallback(() => {
setIsSearchOverlayVisible(false);
// VoiceInputOverlay와 동일하게 닫힐 때 TInput으로 포커스 이동
setTimeout(() => {
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}, 150); // Overlay 닫히는 시간을 고려한 지연
}, []);
/**
@@ -442,7 +390,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
*/
const handleVoiceOverlayClose = useCallback(() => {
console.log(
"🚪 [DEBUG][SearchPanel] handleVoiceOverlayClose called, setting isVoiceOverlayVisible to FALSE"
'🚪 [DEBUG][SearchPanel] handleVoiceOverlayClose called, setting isVoiceOverlayVisible to FALSE'
);
// ✨ Redux 정리는 VoiceInputOverlay.handleClose()에서 처리함
@@ -481,7 +429,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
*/
const handleMicKeyDown = useCallback(
(e) => {
if (e.key === "Enter") {
if (e.key === 'Enter') {
onClickMic();
}
},
@@ -523,27 +471,17 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
{...rest}
>
<div className={css.productImageWrapper}>
<img
src={bgImgPath}
alt={curationNm}
className={css.productImage}
/>
<img src={bgImgPath} alt={curationNm} className={css.productImage} />
</div>
<div className={css.productInfo}>
{showBrandLogo && (
<div className={css.productBrandWrapper}>
<img
src={patncLogoPath}
alt={patncNm}
className={css.brandLogo}
/>
<img src={patncLogoPath} alt={patncNm} className={css.brandLogo} />
</div>
)}
<div className={css.productDetails}>
{showBrandName && <div className={css.brandName}>{patncNm}</div>}
{showProductTitle && (
<div className={css.productTitle}>{curationNm}</div>
)}
{showProductTitle && <div className={css.productTitle}>{curationNm}</div>}
</div>
</div>
</SpottableProduct>
@@ -680,7 +618,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
dispatch(
sendLogTotalRecommend({
query: searchQuery,
searchType: searchPerformed ? "query" : "keyword",
searchType: searchPerformed ? 'query' : 'keyword',
result: result,
contextName: LOG_CONTEXT_NAME.SEARCH,
messageId: LOG_MESSAGE_ID.SEARCH_ITEM,
@@ -734,11 +672,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, []);
return (
<TPanel
className={css.container}
handleCancel={onCancel}
spotlightId={spotlightId}
>
<TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}>
<TBody
className={css.tBody}
scrollable
@@ -762,10 +696,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
className={classNames(
css.inputContainer,
inputFocus === true && css.inputFocus,
searchDatas &&
css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
(isVoiceOverlayVisible || isSearchOverlayVisible) &&
css.hidden
searchDatas && css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
(isVoiceOverlayVisible || isSearchOverlayVisible) && css.hidden
)}
data-wheel-point="true"
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
@@ -806,11 +738,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
>
<div className={css.microphoneCircle}>
<img
src={micIcon}
alt="Microphone"
className={css.microphoneIcon}
/>
<img src={micIcon} alt="Microphone" className={css.microphoneIcon} />
</div>
</SpottableMicButton>
@@ -876,9 +804,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator} />
<div className={css.sectionTitle}>
Your Recent Searches
</div>
<div className={css.sectionTitle}>Your Recent Searches</div>
</div>
<div className={css.keywordList}>
{recentSearches.map((keyword, index) => (
@@ -959,9 +885,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator} />
<div className={css.sectionTitle}>
Hot Picks for You
</div>
<div className={css.sectionTitle}>Hot Picks for You</div>
</div>
<div className={css.productList}>
{hotPicksForYou && hotPicksForYou.length > 0 && (

View File

@@ -35,6 +35,7 @@ const OverlayContainer = SpotlightContainerDecorator(
);
const SpottableMicButton = Spottable('div');
const SpottableDebugButton = Spottable('div');
// Debug mode constant - 항상 디버그 화면 표시
const DEBUG_MODE = true;
@@ -1483,13 +1484,14 @@ const VoiceInputOverlay = ({
// Memoize debug toggle button
const debugToggleButton = useMemo(() => {
return (
<button
<SpottableDebugButton
className={classNames(css.debugToggleButton, showDashboard && css.active)}
onClick={handleToggleDashboard}
title={showDashboard ? 'Close Debug Dashboard' : 'Open Debug Dashboard'}
spotlightId="debug-toggle-button"
>
<span className={css.debugToggleIcon}></span>
</button>
</SpottableDebugButton>
);
}, [showDashboard, handleToggleDashboard]);

View File

@@ -493,6 +493,15 @@
transform: scale(1.05);
}
// Spotlight 포커스 스타일 (붉은색 테두리)
&:focus {
outline: none;
border-color: #ff0000;
border-width: 6px;
box-shadow: 0 0 0 3px rgba(255, 0, 0, 0.3);
transform: scale(1.05);
}
&.active {
background: #FFB81C; // 활성화 상태 (노란색)
border-color: #FFB81C;