From eb933ec144c4762c54d02ade6290b5fba0af0ef0 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 16:40:01 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=B5=9C=EA=B7=BC=20=EB=B3=B8=20?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0=20=EC=A0=80=EC=9E=A5=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/recentlyViewed/postRecentlyViewed.ts | 24 +++++++++++++++++++ src/hooks/useAddToCombination.ts | 9 +++++++ 2 files changed, 33 insertions(+) create mode 100644 src/apis/recentlyViewed/postRecentlyViewed.ts diff --git a/src/apis/recentlyViewed/postRecentlyViewed.ts b/src/apis/recentlyViewed/postRecentlyViewed.ts new file mode 100644 index 0000000..b110230 --- /dev/null +++ b/src/apis/recentlyViewed/postRecentlyViewed.ts @@ -0,0 +1,24 @@ +import { axiosInstance } from '@/apis/axios/axios'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { queryKey } from '@/constants/queryKey'; +import type { CommonResponse } from '@/types/common'; + +// 최근 본 기기 기록 API +export const postRecentlyViewed = async (deviceId: number): Promise => { + const { data } = await axiosInstance.post( + `/api/recently-viewed/${deviceId}` + ); + return data; +}; + +// 최근 본 기기 기록 Mutation +export const usePostRecentlyViewed = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (deviceId: number) => postRecentlyViewed(deviceId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [queryKey.RECENTLY_VIEWED] }); + }, + }); +}; diff --git a/src/hooks/useAddToCombination.ts b/src/hooks/useAddToCombination.ts index 07ef541..5b09a1d 100644 --- a/src/hooks/useAddToCombination.ts +++ b/src/hooks/useAddToCombination.ts @@ -6,6 +6,7 @@ import { useGetCombos } from '@/apis/combo/getCombos'; import { useGetCombo } from '@/apis/combo/getComboId'; import { usePostComboDevice } from '@/apis/combo/postComboDevices'; import { useGetUserProfile } from '@/apis/mypage/getUserProfile'; +import { usePostRecentlyViewed } from '@/apis/recentlyViewed/postRecentlyViewed'; import { hasAccessToken, hasCompletedOnboarding } from '@/utils/authStorage'; interface UseAddToCombinationParams { @@ -33,12 +34,20 @@ export const useAddToCombination = ({ // API hooks const { data: combos = [] } = useGetCombos(); const { mutate: addDeviceToCombo, isPending: isAddingDevice } = usePostComboDevice(); + const { mutate: recordRecentlyViewed } = usePostRecentlyViewed(); const [selectedCombinationId, setSelectedCombinationId] = useState(null); const [showAllDevices, setShowAllDevices] = useState(false); const [showSaveCompleteModal, setShowSaveCompleteModal] = useState(false); const [isFadingOut, setIsFadingOut] = useState(false); + // 모달 열릴 때 최근 본 기기 기록 (로그인 상태에서만) + useEffect(() => { + if (isLoggedIn && selectedProductId) { + recordRecentlyViewed(Number(selectedProductId)); + } + }, [selectedProductId]); + // 선택된 조합의 상세 정보 조회 const { data: comboDetail } = useGetCombo(selectedCombinationId); From bd9fd6d3ca407213ea12818aa5bde9a27ee4ff27 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 16:50:20 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20alert=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useAddToCombination.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/hooks/useAddToCombination.ts b/src/hooks/useAddToCombination.ts index 5b09a1d..dc5ac19 100644 --- a/src/hooks/useAddToCombination.ts +++ b/src/hooks/useAddToCombination.ts @@ -61,12 +61,7 @@ export const useAddToCombination = ({ // 에러 핸들러 (공통) const handleComboError = (error: unknown) => { - const axiosError = error as { response?: { status?: number } }; - if (axiosError?.response?.status === 400) { - alert('이미 조합에 추가된 기기입니다.'); - } else { - console.error('기기 추가 실패:', error); - } + console.error('기기 추가 실패:', error); }; /* 내 조합에 담기 */ From 2ed00dae7e8588ddd4b51e6c237c82baf6b354a5 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 17:11:46 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EA=B8=B0=EA=B8=B0=EA=B2=80?= =?UTF-8?q?=EC=83=89=20flow,=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=A1=B0=ED=95=A9=EC=97=90=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Combination/CombinationDeviceCard.tsx | 10 +++++++++- src/components/MyPage/CombinationCard.tsx | 10 +++++++++- src/components/MyPage/CombinationDetailView.tsx | 10 +++++++++- src/hooks/useAddToCombination.ts | 12 ------------ src/pages/devices/DeviceSearchPage.tsx | 16 ++++++++++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/components/Combination/CombinationDeviceCard.tsx b/src/components/Combination/CombinationDeviceCard.tsx index 9472899..95521f7 100644 --- a/src/components/Combination/CombinationDeviceCard.tsx +++ b/src/components/Combination/CombinationDeviceCard.tsx @@ -92,7 +92,15 @@ const CombinationDeviceCard = ({ key={device.deviceId} className={`bg-white rounded-card shadow-[0_0_4px_rgba(0,0,0,0.1)] p-12 ${deviceCardWidth} flex items-center gap-12`} > -
+
+ {device.imageUrl && ( + {device.name} + )} +

{device.name}

{device.brandName}

diff --git a/src/components/MyPage/CombinationCard.tsx b/src/components/MyPage/CombinationCard.tsx index 2f67f52..59b7451 100644 --- a/src/components/MyPage/CombinationCard.tsx +++ b/src/components/MyPage/CombinationCard.tsx @@ -143,7 +143,15 @@ const CombinationCard = ({ key={device.deviceId} className="bg-white rounded-card shadow-[0_0_4px_rgba(0,0,0,0.1)] p-12 w-244 flex items-center gap-12" > -
+
+ {device.imageUrl && ( + {device.name} + )} +

{device.name}

{device.brandName}

diff --git a/src/components/MyPage/CombinationDetailView.tsx b/src/components/MyPage/CombinationDetailView.tsx index 40ff6be..3d6090a 100644 --- a/src/components/MyPage/CombinationDetailView.tsx +++ b/src/components/MyPage/CombinationDetailView.tsx @@ -19,6 +19,7 @@ interface Device { name: string; brandName?: string; deviceType?: string; + imageUrl?: string; } interface EvaluationCard { @@ -151,7 +152,14 @@ const CombinationDetailView = ({ onClick={() => window.open(`/devices?productId=${device.deviceId}`, '_blank')} className={`bg-white rounded-card shadow-[0_0_4px_rgba(0,0,0,0.1)] p-12 w-244 flex items-center gap-12 border cursor-pointer hover:shadow-[0_0_7px_#57a0ff] transition-shadow ${selectedDevices.includes(device.deviceId) ? 'border-blue-600' : 'border-transparent'}`} > -
+
+ {device.imageUrl && ( + {device.name} + )} {/* 호버 오버레이 */}
diff --git a/src/hooks/useAddToCombination.ts b/src/hooks/useAddToCombination.ts index dc5ac19..995cb6d 100644 --- a/src/hooks/useAddToCombination.ts +++ b/src/hooks/useAddToCombination.ts @@ -173,18 +173,6 @@ export const useAddToCombination = ({ ? combinationDevices.some(device => device.deviceId === Number(selectedProductId)) : false; - /* 모달 열렸을 때 y 스크롤 방지 */ - useEffect(() => { - if (selectedProductId) { - document.documentElement.style.overflowY = 'hidden'; - } else { - document.documentElement.style.overflowY = 'auto'; - } - return () => { - document.documentElement.style.overflowY = 'auto'; - }; - }, [selectedProductId]); - return { modalView, setModalView, diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index ec861b6..2b219dd 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -22,6 +22,7 @@ import { mapSearchDeviceToProduct } from '@/utils/mapSearchDevice'; import { useDeviceSearch } from '@/hooks/useDeviceSearch'; import { useScrollState } from '@/hooks/useScrollState'; import { useAddToCombination } from '@/hooks/useAddToCombination'; +import { useEffect } from 'react'; const DeviceSearchPage = () => { const [searchParams, setSearchParams] = useSearchParams(); @@ -48,6 +49,21 @@ const DeviceSearchPage = () => { : null; const selectedProduct = selectedDevice ? mapSearchDeviceToProduct(selectedDevice) : null; + /* 모달 열림 상태 확인 및 스크롤 잠금 */ + const isModalOpen = !!selectedProduct || combo.showSaveCompleteModal; + + useEffect(() => { + if (isModalOpen) { + document.documentElement.style.overflowY = 'hidden'; + } else { + document.documentElement.style.overflowY = ''; + } + + return () => { + document.documentElement.style.overflowY = ''; + }; + }, [isModalOpen]); + return (
From 7b446a96c418ce55e67b3e04befa01d63bbf434b Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 17:30:41 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EA=B8=B0=EA=B8=B0=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EB=B3=B4=EA=B8=B0=EC=97=90=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeviceSearch/DeviceDetailModal.tsx | 16 ++++++++++++++-- src/pages/devices/DeviceSearchPage.tsx | 17 ++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/DeviceSearch/DeviceDetailModal.tsx b/src/components/DeviceSearch/DeviceDetailModal.tsx index 0b87b5c..f8d32ad 100644 --- a/src/components/DeviceSearch/DeviceDetailModal.tsx +++ b/src/components/DeviceSearch/DeviceDetailModal.tsx @@ -2,6 +2,8 @@ import type { Product } from '@/types/product'; import type { SearchDevice } from '@/types/devices'; import PrimaryButton from '@/components/Button/PrimaryButton'; import XIcon from '@/assets/icons/X.svg?react'; +import RoundedLifestyleTag from '@/components/Lifestyle/RoundedLifestyleTag'; +import { getDeviceLifestyleTags } from '@/utils/tag/deviceLifestyleTags'; interface DeviceDetailModalProps { product: Product; @@ -18,6 +20,8 @@ const DeviceDetailModal = ({ isProfileLoading, onClose, }: DeviceDetailModalProps) => { + const rawTags = getDeviceLifestyleTags(device); + return (
{/* Close Button - 카드 바깥 */} @@ -66,9 +70,9 @@ const DeviceDetailModal = ({
{/* Right Section - Specs */} -
+
{/* Product Info Table */} -
+

모델명

{product.name}

@@ -116,6 +120,14 @@ const DeviceDetailModal = ({ )}
+ {/* Lifestyle Tags */} + {rawTags.length > 0 && ( +
+ {rawTags.map((tag) => ( + + ))} +
+ )}
diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index 2b219dd..4476869 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -49,18 +49,25 @@ const DeviceSearchPage = () => { : null; const selectedProduct = selectedDevice ? mapSearchDeviceToProduct(selectedDevice) : null; - /* 모달 열림 상태 확인 및 스크롤 잠금 */ - const isModalOpen = !!selectedProduct || combo.showSaveCompleteModal; + /* 모달 열림 상태 확인 및 스크롤 잠금 (회색 배경이 보일 때와 동일한 조건) */ + const isModalOpen = (!!selectedProduct && !combo.showSaveCompleteModal) || combo.showSaveCompleteModal; useEffect(() => { if (isModalOpen) { - document.documentElement.style.overflowY = 'hidden'; + const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; + document.documentElement.style.overflow = 'hidden'; + document.body.style.overflow = 'hidden'; + document.body.style.paddingRight = `${scrollBarWidth}px`; } else { - document.documentElement.style.overflowY = ''; + document.documentElement.style.overflow = ''; + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; } return () => { - document.documentElement.style.overflowY = ''; + document.documentElement.style.overflow = ''; + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; }; }, [isModalOpen]); From 00e1de5fa876457ec117056ead91e704ad11f93a Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 17:57:40 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Combination/CombinationDeviceCard.tsx | 33 +++++-- src/components/Combination/CombinationTag.tsx | 6 +- .../DeviceSearch/CombinationSelectModal.tsx | 94 +++++++++++++------ src/types/combo/combo.ts | 6 ++ 4 files changed, 99 insertions(+), 40 deletions(-) diff --git a/src/components/Combination/CombinationDeviceCard.tsx b/src/components/Combination/CombinationDeviceCard.tsx index 95521f7..2437dca 100644 --- a/src/components/Combination/CombinationDeviceCard.tsx +++ b/src/components/Combination/CombinationDeviceCard.tsx @@ -1,6 +1,9 @@ import { useState } from 'react'; import StarIcon from '@/assets/icons/star.svg?react'; import type { ComboListItem, ComboDevice } from '@/types/combo/combo'; +import type { CombinationStatus } from '@/constants/combination'; +import CombinationTag from '@/components/Combination/CombinationTag'; +import { useComboEvaluation } from '@/apis/combo/getComboEvaluation'; type CombinationDeviceCardProps = { combination: ComboListItem; @@ -29,6 +32,9 @@ const CombinationDeviceCard = ({ }: CombinationDeviceCardProps) => { const [internalExpanded, setInternalExpanded] = useState(false); + // 평가 데이터 조회 (마이페이지와 동일한 데이터 소스) + const { data: evaluation } = useComboEvaluation(combination.comboId); + const isControlled = expanded !== undefined; const showAllDevices = isControlled ? expanded : internalExpanded; @@ -60,7 +66,7 @@ const CombinationDeviceCard = ({ return (
{/* 조합 정보 */} -
+
{/* 조합명 */}
{/* 조합 번호 */} @@ -73,18 +79,24 @@ const CombinationDeviceCard = ({ {combination.isPinned && }
- {/* Tags */} -
- - 기기 {combination.deviceCount}개 - - - ₩{combination.totalPrice.toLocaleString()} - + {/* 평가 태그 */} +
+ + +
- {/* 기기 그리드 */} + {/* 기기 그리드 - 태그와의 마진 24px (mt-24) */}
{displayedDevices.map((device) => ( @@ -143,3 +155,4 @@ const CombinationDeviceCard = ({ }; export default CombinationDeviceCard; + diff --git a/src/components/Combination/CombinationTag.tsx b/src/components/Combination/CombinationTag.tsx index 7b60b9e..b1f4b0c 100644 --- a/src/components/Combination/CombinationTag.tsx +++ b/src/components/Combination/CombinationTag.tsx @@ -12,6 +12,10 @@ type CombinationTagProps = { }; const CombinationTag = ({ name, status, className = '' }: CombinationTagProps) => { + // status가 유효하지 않을 경우를 대비한 안전한 스타일 추출 + const statusStyle = COMBINATION_STATUS_STYLE_MAP[status] || COMBINATION_STATUS_STYLE_MAP['-']; + const displayStatus = status || '-'; + return ( {name}: - {status} + {displayStatus} ); }; diff --git a/src/components/DeviceSearch/CombinationSelectModal.tsx b/src/components/DeviceSearch/CombinationSelectModal.tsx index a75cdc3..4c9a129 100644 --- a/src/components/DeviceSearch/CombinationSelectModal.tsx +++ b/src/components/DeviceSearch/CombinationSelectModal.tsx @@ -1,8 +1,11 @@ import type { ComboListItem } from '@/types/combo/combo'; +import type { CombinationStatus } from '@/constants/combination'; +import CombinationTag from '@/components/Combination/CombinationTag'; import BackIcon from '@/assets/icons/back.svg?react'; import XIcon from '@/assets/icons/X.svg?react'; import StarIcon from '@/assets/icons/star.svg?react'; import MoreIcon from '@/assets/icons/more.svg?react'; +import { useComboEvaluation } from '@/apis/combo/getComboEvaluation'; interface CombinationSelectModalProps { combos: ComboListItem[]; @@ -11,6 +14,61 @@ interface CombinationSelectModalProps { onClose: () => void; } +/** + * 개별 조합 아이템 컴포넌트 + * 마이페이지 상세 정보와 동일한 평가 데이터를 가져와서 표시합니다. + */ +const CombinationSelectItem = ({ + comboItem, + index, + onSelect, +}: { + comboItem: ComboListItem; + index: number; + onSelect: (id: number) => void; +}) => { + // 각 조합의 평가 정보를 상세 API에서 가져옴 (마이페이지와 동일한 데이터 소스) + const { data: evaluation } = useComboEvaluation(comboItem.comboId); + + return ( + + ); +}; + const CombinationSelectModal = ({ combos, onSelectCombination, @@ -48,36 +106,12 @@ const CombinationSelectModal = ({ {/* Combination List */}
{combos.map((comboItem, index) => ( - + comboItem={comboItem} + index={index} + onSelect={onSelectCombination} + /> ))}
@@ -86,3 +120,5 @@ const CombinationSelectModal = ({ }; export default CombinationSelectModal; + + diff --git a/src/types/combo/combo.ts b/src/types/combo/combo.ts index c3212b1..b79c737 100644 --- a/src/types/combo/combo.ts +++ b/src/types/combo/combo.ts @@ -21,6 +21,9 @@ export type ComboListItem = { pinnedAt: string | null; totalPrice: number; currentTotalScore: number; + connectivityGrade?: string; + convenienceGrade?: string; + lifestyleGrade?: string; deviceCount: number; createdAt: string; updatedAt: string; @@ -35,6 +38,9 @@ export type ComboDetail = { pinnedAt: string | null; totalPrice: number; currentTotalScore: number; + connectivityGrade?: string; + convenienceGrade?: string; + lifestyleGrade?: string; evaluatedAt: string | null; createdAt: string; updatedAt: string; From 55fe7804247265f903e0f4fc8be77878101a38ab Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 18:24:56 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=EC=97=90=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeviceSearch/DeviceDetailModal.tsx | 4 +- src/utils/tag/deviceLifestyleTags.ts | 107 ++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/utils/tag/deviceLifestyleTags.ts diff --git a/src/components/DeviceSearch/DeviceDetailModal.tsx b/src/components/DeviceSearch/DeviceDetailModal.tsx index f8d32ad..b9153b4 100644 --- a/src/components/DeviceSearch/DeviceDetailModal.tsx +++ b/src/components/DeviceSearch/DeviceDetailModal.tsx @@ -20,7 +20,7 @@ const DeviceDetailModal = ({ isProfileLoading, onClose, }: DeviceDetailModalProps) => { - const rawTags = getDeviceLifestyleTags(device); + const rawTags: string[] = getDeviceLifestyleTags(device); return (
@@ -123,7 +123,7 @@ const DeviceDetailModal = ({ {/* Lifestyle Tags */} {rawTags.length > 0 && (
- {rawTags.map((tag) => ( + {rawTags.map((tag: string) => ( ))}
diff --git a/src/utils/tag/deviceLifestyleTags.ts b/src/utils/tag/deviceLifestyleTags.ts new file mode 100644 index 0000000..77c54b4 --- /dev/null +++ b/src/utils/tag/deviceLifestyleTags.ts @@ -0,0 +1,107 @@ +import type { SearchDevice } from '@/types/devices'; + +/** + * 기기 상세 정보의 specifications를 바탕으로 라이프스타일 스타일 태그를 생성합니다. + */ +export const getDeviceLifestyleTags = (device: SearchDevice): string[] => { + const { deviceType, specifications: specs } = device; + if (!specs) return []; + + const tags: string[] = []; + + switch (deviceType) { + case 'SMARTPHONE': + case 'TABLET': + if (specs.storageGb) tags.push(`${specs.storageGb}GB`); + if (specs.screenInch) tags.push(`${specs.screenInch}인치`); + break; + + case 'LAPTOP': + if (specs.cpu) tags.push(String(specs.cpu)); + if (specs.screenInch) tags.push(`${specs.screenInch}인치`); + break; + + case 'SMARTWATCH': + if (specs.caseSizeMm) tags.push(`${specs.caseSizeMm}mm`); + if (specs.hasCellular !== undefined) { + // 1이면 셀룰러, 0이면 GPS + tags.push(Number(specs.hasCellular) === 1 ? '셀룰러' : 'GPS'); + } + break; + + case 'AUDIO': + if (Number(specs.hasAnc) === 1) tags.push('ANC'); + if (specs.totalBatteryLifeHours) tags.push(`${specs.totalBatteryLifeHours}시간`); + break; + + case 'KEYBOARD': + if (specs.switchType) { + const switchMap: Record = { + BLUE: '청축', + RED: '적축', + BROWN: '갈축', + SCISSOR: '펜타그래프', + }; + const mapped = switchMap[String(specs.switchType).toUpperCase()]; + if (mapped) tags.push(mapped); + } + if (specs.connectionType) { + tags.push(formatConnectionType(String(specs.connectionType))); + } + break; + + case 'MOUSE': + if (specs.mouseType) { + const mouseMap: Record = { + VERTICAL: '버티컬', + TRACKBALL: '트랙볼', + }; + const mapped = mouseMap[String(specs.mouseType).toUpperCase()]; + if (mapped) tags.push(mapped); + } + if (specs.connectionType) { + tags.push(formatConnectionType(String(specs.connectionType))); + } + break; + + case 'CHARGER': + if (specs.totalPowerW) tags.push(`${specs.totalPowerW}W`); + if (Array.isArray(specs.portConfiguration)) { + const portConfig = formatPortConfiguration(specs.portConfiguration); + if (portConfig) tags.push(portConfig); + } + break; + + default: + break; + } + + return tags.filter(Boolean); +}; + +const formatConnectionType = (type: string): string => { + const t = type.toUpperCase(); + if (t === 'BLUETOOTH') return '블루투스'; + if (t === 'WIRED') return '유선'; + // DONGLE이나 기타 무선 방식은 '무선'으로 표기 + return '무선'; +}; + +const formatPortConfiguration = (ports: string[]): string => { + let cCount = 0; + let aCount = 0; + + ports.forEach((port) => { + const p = port.toUpperCase(); + if (p.includes('USB_C')) cCount++; + else if (p.includes('USB_A')) aCount++; + else if (p.includes('C')) cCount++; + else if (p.includes('A')) aCount++; + }); + + const result = []; + if (cCount > 0) result.push(`${cCount}C`); + if (aCount > 0) result.push(`${aCount}A`); + + return result.join(''); +};