From ed2e025f65420683a5c36f52e064919803c0733e Mon Sep 17 00:00:00 2001 From: ujeong Date: Thu, 28 May 2026 17:29:41 +0900 Subject: [PATCH 001/137] =?UTF-8?q?docs:=201=EC=A3=BC=EC=B0=A8=20=EB=B3=B4?= =?UTF-8?q?=EA=B3=A0=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- report/week1.md | 260 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 report/week1.md diff --git a/report/week1.md b/report/week1.md new file mode 100644 index 00000000..51745490 --- /dev/null +++ b/report/week1.md @@ -0,0 +1,260 @@ +## **미니멀 SNS 서비스** + +> **서비스명 후보: 캐디 +팀명: Ellipsis +팀원: 아오, 구름, 조디악, 투핸더 +작성일: 2026년 5월 28일 +발표 예정일: 2026년 5월 29일** +> + +--- + +## **A. 기획 결과물** + +## **1. 팀 정보** + +| **항목** | **내용** | +| --- | --- | +| **팀 이름** | ellipsis | +| **팀 멤버** | 구름, 아오, 조디악, 투핸더 | +| **한 줄 소개** | 사용자의 데스크톱 위에 작은 펫을 띄워, 나와 친구의 상태를 귀엽고 가볍게 공유하는 미니멀 SNS 서비스를 만듭니다. | + +--- + +## **2. 문제 정의** + +| **구분** | **내용** | +| --- | --- | +| **누가** | PC로 장시간 작업하거나 학습하는 사용자, 가까운 친구·동료와 부담 없이 연결되어 있고 싶은 사용자 | +| **어떤 상황에서** | 개발, 공부, 업무 등으로 오랜 시간 PC 앞에 머무르며, 상대방의 상태를 가볍게 확인하거나 나의 현재 상태를 자연스럽게 공유하고 싶은 상황에서 | +| **어떤 불편을 겪는가** | 기존 메신저·SNS는 상대 상태 확인을 위해 별도 앱을 열어야 하고, 단순히 "작업 중인지" 정도만 알고 싶은 상황에서도 채팅을 보내는 것은 부담스럽다. 또한 macOS와 Windows는 사용자가 자연스럽게 받아들이는 데스크톱 경험이 달라, 하나의 UI를 모든 OS에 동일하게 적용하면 각 플랫폼의 사용 맥락과 어긋날 수 있다. | + +--- + +## **3. 솔루션** + +> **macOS에서는 메뉴바에 사는 펫, Windows에서는 바탕화면에 사는 펫을 통해 나와 친구의 상태를 귀엽고 부담 없이 공유합니다.** +> + +OS별 사용 맥락에 맞춰 macOS에서는 메뉴바에 작은 펫 아이콘을 표시하고, Windows에서는 바탕화면 위에 작은 펫을 띄웁니다. 사용자는 이 펫을 통해 자신의 상태를 표현하고, 친구의 상태를 확인하며, 간단한 리액션으로 부담 없이 상호작용할 수 있습니다. + +**프로젝트 설계 배경** + +최근 "셋로그"처럼 편집 과정을 제거하고 일상 공유에만 집중한 미니멀 SNS 서비스가 활발히 사용되고 있다. 이는 *적은 스트레스로 콘텐츠를 업로드하고 싶은* 사용자 니즈가 실재함을 보여준다. 이 현상을 토대로 **"미니멀 SNS 서비스를 제작하면 사용자를 이끌어낼 수 있다"** 는 가설을 검증하고자 한다. + +--- + +## **4. 기술/플랫폼 선택 + 근거** + +**선택 기술: Tauri v2** + +| **선택 이유** | **상세 설명** | +| --- | --- | +| **크로스 플랫폼 + OS별 UI 분기** | 단일 코드베이스로 Windows·macOS를 모두 지원하면서, 공통 도메인 로직은 공유하고 OS별 UI만 분기하여 구현 가능 | +| **macOS 메뉴바 앱 구현** | Tauri System Tray를 활용해 macOS 메뉴바에 작은 펫 아이콘을 표시하고, 클릭 시 상태·친구 정보 패널 제공 (RunCat 등 메뉴바 유틸리티 경험과 일치) | +| **Windows 바탕화면 펫 구현** | 투명하고 프레임 없는 창, 항상 위에 떠 있는 창 등 창 제어 기능으로 Windows 바탕화면 위 작은 펫 구현 가능 | +| **경량성** | Rust 기반 백엔드와 OS WebView 활용으로 Electron 대비 메모리·CPU 사용량이 적어, 장시간 백그라운드 실행에 적합 | +| **생산성 + 네이티브** | 프론트엔드는 React/TypeScript로 빠르게 구현하고, OS 밀착 기능은 Rust로 처리 | + +**예상 기술 스택** + +| **레이어** | **기술** | +| --- | --- | +| Desktop Client | Tauri v2 | +| Frontend | React, TypeScript, Vite | +| Local State | Zustand 또는 Jotai | +| Local Storage | Tauri Store Plugin | +| Window State | Tauri Window State Plugin | +| macOS UI | Tauri System Tray, Menu Bar Panel | +| Windows UI | Transparent Window, Frameless Window, Tray | +| Realtime | WebSocket | +| Backend | Spring Boot | +| Database | PostgreSQL | +| Presence Cache | Redis | +| Animation | PNG Sprite | + +--- + +## **5. 고려했지만 선택하지 않은 기술** + +| **기술** | **장점** | **선택하지 않은 이유** | +| --- | --- | --- | +| **Electron** | 투명 창·트레이 등 기능이 성숙, 구현 사례 풍부 | 장시간 백그라운드 실행 앱 특성상 앱 크기·메모리 사용량이 부담 | +| **Flutter Desktop** | UI·애니메이션 강점, Windows/macOS 데스크톱 지원 | macOS 메뉴바 앱·Windows 투명 오버레이·클릭 처리 등 OS별 세부 동작 구현 시 별도 패키지 의존도가 높아짐 | +| **OS별 네이티브 개발** | 각 플랫폼에 최적화된 완성도 높은 경험 | MVP 단계에서 두 플랫폼 별도 개발 시 비용 과대, 공통 로직 중복 구현 필요 | +| **모든 OS에서 동일한 바탕화면 펫 UI** | 제품 컨셉 설명이 쉽고, 두 OS에서 같은 화면 제공 가능 | macOS에서는 메뉴바 유틸리티 경험이 더 자연스럽고, 바탕화면에 표시되는 펫이 오히려 작업 흐름을 방해할 수 있다고 판단하여 OS별 UI로 피봇 | + +--- + +## **6. MVP 범위** + +## **✅ 만들 것** + +**공통 기능** + +- 내 펫 정보 관리 +- 내 상태 설정 (온라인 / 작업 중 / 쉬는 중 / 자리 비움 / 방해 금지) +- 친구 목록 확인 +- 친구 초대 및 수락 +- 친구 상태 확인 +- 친구에게 간단한 체팅 혹은 반응 보내기 + - 이모지 송신 + - 채팅 히스토리는 없어야 함 +- WebSocket 기반 실시간 상태 동기화 +- 로컬 설정 저장 + +**macOS MVP** + +- 상단 제어바에 작은 펫 아이콘 표시 +- 내 상태에 따라 메뉴바 펫 아이콘 변경 +- 메뉴바 아이콘 클릭 시 작은 정보 패널 표시 +- 패널에서 내 상태 확인·변경 / 친구 상태 확인 / 리액션 전송 +- 설정 화면 열기 + +**Windows MVP** + +- 윈도우 오버레이로 작은 펫 표시 +- 펫 드래그 이동 및 위치 저장·재실행 시 복원 +- 펫 클릭 시 간단한 리액션 +- 친구 상태에 따라 펫 모습 변경 +- 트레이 메뉴(하단제어바) (설정 열기 / 펫 숨기기 / 종료) + +## **❌ MVP에서 만들지 않는 기능** + +- 로그인 +- LLM 기반 AI 대화 +- 복잡한 채팅 기능 +- 정교한 펫 커스터마이징, 펫 만들기 +- 공식 OS 위젯 +- 활성 앱 이름·창 제목 분석(로컬에서만) & 정확한 CPU/RAM 수치 표시 + - 소프트웨어 자원/하드웨어 자원 구분 +- per-pixel click-through 같은 고급 클릭 통과 기능 +- App Store 배포(불가능할 수도 있음) +- 스트레칭 알림 +- 모바일/웨어러블 연동? + +> **MVP에서는 "OS별로 자연스러운 위치에 펫이 존재하고, 이를 통해 상태 공유와 가벼운 상호작용이 가능한가" 를 검증하는 데 집중합니다.** +> + +--- + +## **7. 검증 방법 초안** + +## **성공 기준** + +- 펫을 방해 요소가 아닌 **귀여운 동반자**로 느끼는가 +- 친구 펫/상태를 통한 리액션이 채팅보다 **부담 없는 소통 방식**으로 느껴지는가 +- OS별로 다른 UI를 제공하는 것이 사용자에게 어색하지 않고 자연스럽게 받아들여지는가 +- 장시간 PC 작업 중 스트레칭 제안이 유용하거나 긍정적으로 느껴지는가 +- macOS와 Windows 각각에서 핵심 기능이 안정적으로 동작하는가 + +## **실패 기준** + +- 펫이 화면에서 **방해된다고 느끼는** 사용자가 다수인 경우 +- 사용자가 친구 상태 공유 기능의 **필요성을 느끼지 못하는** 경우 +- 리액션 기능이 **너무 가볍거나 의미 없다고** 느껴지는 경우 +- Tauri에서 메뉴바 패널 또는 투명 창 구현이 불안정할 경우 + +## **검증 방식** + +- macOS / Windows 각각에서 프로토타입 사용 테스트 +- 사용자 인터뷰 및 짧은 설문 + - 실제로 스트레칭 하셨나요? 이 기능이 유용하다고 느끼셨나요? + +> 정량적인 부분은 의견을 받거나 회고를 통해서 평가하기, 서버 사용량이나 로그를 따서 정량적인 활성도 확인하기, 구글 애널리틱스, 파이어베이스 이벤트 등등 +> + +**핵심 질문** + +- macOS 사용자는 메뉴바에 있는 작은 펫으로도 충분히 매력을 느끼는가? +- Windows 사용자는 바탕화면 위의 펫을 방해 요소가 아닌 동반자로 느끼는가? +- 친구 상태를 텍스트가 아닌 펫으로 표현하는 방식이 직관적인가? +- 채팅 없이 리액션만으로도 의미 있는 가벼운 소통이 가능한가? + +**핵심 지표** + +| **지표** | **설명** | +| --- | --- | +| 앱 재실행률 | 자발적으로 앱을 다시 켜는지 여부 | +| 사용자별 macOS 메뉴바 아이콘 클릭 횟수 | 메뉴바 펫에 대한 관심도 | +| Windows 펫 위치 이동 여부 | 사용자가 펫을 자기 방식으로 배치하는지 | +| 상태 변경 사용 횟수 | 상태 기능을 실제로 활용하는지 | +| 친구 초대/수락 수 | 소셜 기능 진입률 | +| 리액션 전송 횟수 | 핵심 상호작용 활성화 정도 | +| 강제 종료(의도적인 종료)·삭제 여부 | 추적 가능 여부는 미확정이나 추적 희망 | + +--- + +## **8. 발표 자료 링크** + +- **발표 예정일:** 2026년 5월 29일 +- **발표 자료 링크:** 미정 (추후 업데이트 예정) + +--- + +## **B. 회고** + +## **1. 가장 어려웠던 의사결정 + 어떻게 풀었는지** + +**의사결정 ①: 데스크톱 앱 구현 기술 스택 선택** + +이 서비스는 일반적인 웹 서비스가 아니라, 사용자의 바탕화면 위에 작은 펫이 항상 존재해야 하는 서비스다. 그렇기에, 데스크톱 특화 기능 구현 가능성과 하드웨어 성능 부담이 적은 기술을 선택해야 했다. + +초기에는 Electron, Tauri, Flutter Desktop, OS별 네이티브 개발 네 가지 옵션을 함께 검토했다. + +- **Electron:** 기능과 생태계가 안정적이나, 장시간 켜두는 앱 특성상 메모리·앱 크기 부담 +- **Flutter Desktop:** UI·애니메이션 강점이 있으나, 오버레이 창 제어에서 추가 구현 부담 예상 +- **OS별 네이티브:** 완성도 높지만 두 플랫폼 개발 비용이 MVP 규모에 비해 과도 + +결국 **크로스 플랫폼 지원 + 경량 실행 + 웹 프론트엔드 생산성 + Rust를 통한 네이티브 접근성**을 모두 고려해 Tauri를 선택했다. 단, Tauri에서도 OS별 창 제어 제약이 존재할 수 있으므로 초기 개발 단계에서 **투명 창과 항상 위 창의 안정성을 먼저 검증**하기로 했다. + +--- + +**의사결정 ②: 모든 OS에서 동일한 UI vs. OS별 다른 UX 제공** ← *기획 과정에서 변경된 핵심 결정* + +**macOS와 Windows 사용자가 자연스럽게 받아들이는 데스크톱 경험이 다르다**는 점이 문제로 떠올랐다. + +- **macOS:** RunCat처럼 제어바에 상주하여 표시하는 경험에 익숙하다. 바탕화면 위에 펫을 계속 띄우는 방식은 귀엽기보다 작업 흐름을 방해하는 요소가 될 수 있다. +- **Windows:** 작업표시줄(하단 메뉴바)에서 표시할 수 있는 범위가 적다. + +이 차이를 무시하고 동일한 UI를 적용하면 한 쪽 OS에서 사용자 경험이 어색해질 수 있다고 판단했다. 결국 **"모든 OS에서 같은 UI를 제공하는 것"보다 "서비스의 핵심 경험은 유지하되 OS별 사용 맥락에 맞는 UI를 제공하는 것"** 이 더 중요하다는 결론에 도달했고, macOS는 메뉴바 펫, Windows는 바탕화면 펫으로 피봇했다. + +구현은 다소 복잡해졌지만, **공통 도메인 로직은 유지하고 OS별 UI만 분기하는 방식**으로 복잡도를 관리하기로 했다. + +--- + +## **2. 합의되지 않은 부분** + +1. **macOS 메뉴바 아이콘으로 정보를 충분히 전달할 수 있는가** + + 메뉴바 아이콘은 크기가 작아 펫의 감정이나 움직임을 표현하는 데 한계가 있다. 아이콘 클릭 시 표시되는 패널에서 더 큰 펫과 친구 상태를 보여주는 방향을 보완책으로 검토 중이다. + +2. **Windows 오버레이의 방해 정도** + + 항상 위에 떠 있는 방식은 존재감을 높이지만 작업 방해 가능성도 있다. 숨김 기능·방해 금지 모드·위치 저장을 MVP에 포함해 실제 사용 테스트를 통해 적절한 방식을 찾을 예정이다. + +3. **하드웨어 상태를 펫 상태에 반영할지 여부** + + CPU 부하에 따른 펫 상태 변경 기능은 흥미롭지만 프라이버시와 연결될 수 있다. MVP에서는 사용자가 직접 설정한 상태를 우선하고 자동 추론 기능은 후순위로 두기로 했다. + +4. 실패 기준에서 OS별 구현 차이로 인해 개발 비용이 예상보다 크게 증가할 경우를 추가할 지 + + 결국 맥이나 윈도우 플랫폼을 종료할 거냐라는 의문에 도달할까 봐 합의하지 못 했고, 나중에 합의하기로 결정했다. + + +--- + +## **3. 가장 의심스러운 가정 1가지** + +> **"사용자가 데스크톱 위에 항상 떠 있는 펫을 불편하지 않게 느낄 것이다"** +> + +macOS에서는 제어바에 존재하여 다른 제어바 요소와 비슷하게 동작함을 사용자가 예상할 수 있지만, Window에서는 윈도우 오버레이를 클릭하여 어떻게 동작할 건지 사용자가 예상할 수 있겠는가? + +이 가정이 이 서비스 전체의 핵심 전제다. 아이디어 자체는 매력적이지만, 실제 사용 환경에서는 화면을 가리거나 집중을 방해하는 요소로 느껴질 수 있다. 특히 개발자·학습자처럼 화면 공간 활용도가 높은 사용자에게는 작은 펫조차 방해가 될 수 있다. + +**따라서 MVP로 사용자를 얼마나 방해하는지를 고려해야 한다.** + +--- + +이 가정(예측)이 틀린다면 서비스의 자체를 재설계해야 하므로, MVP에서 가장 먼저 검증해야 할 항목이다. \ No newline at end of file From ba1493f9c68ec4992c816a4ddefd839c42d59a69 Mon Sep 17 00:00:00 2001 From: ujeong Date: Thu, 4 Jun 2026 17:50:04 +0900 Subject: [PATCH 002/137] =?UTF-8?q?docs:=201=EC=A3=BC=EC=B0=A8=20=EB=B3=B4?= =?UTF-8?q?=EA=B3=A0=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- report/week1.md | 294 ++++++++++++++++-------------------------------- 1 file changed, 96 insertions(+), 198 deletions(-) diff --git a/report/week1.md b/report/week1.md index 51745490..0afea482 100644 --- a/report/week1.md +++ b/report/week1.md @@ -1,10 +1,7 @@ -## **미니멀 SNS 서비스** +## **나만의 키보드 찾기 서비스** -> **서비스명 후보: 캐디 -팀명: Ellipsis -팀원: 아오, 구름, 조디악, 투핸더 -작성일: 2026년 5월 28일 -발표 예정일: 2026년 5월 29일** +> **서비스명 후보: 키버디 +작성일: 2026년 6월 2일** > --- @@ -16,8 +13,8 @@ | **항목** | **내용** | | --- | --- | | **팀 이름** | ellipsis | -| **팀 멤버** | 구름, 아오, 조디악, 투핸더 | -| **한 줄 소개** | 사용자의 데스크톱 위에 작은 펫을 띄워, 나와 친구의 상태를 귀엽고 가볍게 공유하는 미니멀 SNS 서비스를 만듭니다. | +| **팀 멤버** | 아오, 구름, 조디악, 투핸더 | +| **한 줄 소개** | 내가 원하고, fit한 키보드를 찾는 과정을 간소화해 줍니다. | --- @@ -25,170 +22,102 @@ | **구분** | **내용** | | --- | --- | -| **누가** | PC로 장시간 작업하거나 학습하는 사용자, 가까운 친구·동료와 부담 없이 연결되어 있고 싶은 사용자 | -| **어떤 상황에서** | 개발, 공부, 업무 등으로 오랜 시간 PC 앞에 머무르며, 상대방의 상태를 가볍게 확인하거나 나의 현재 상태를 자연스럽게 공유하고 싶은 상황에서 | -| **어떤 불편을 겪는가** | 기존 메신저·SNS는 상대 상태 확인을 위해 별도 앱을 열어야 하고, 단순히 "작업 중인지" 정도만 알고 싶은 상황에서도 채팅을 보내는 것은 부담스럽다. 또한 macOS와 Windows는 사용자가 자연스럽게 받아들이는 데스크톱 경험이 달라, 하나의 UI를 모든 OS에 동일하게 적용하면 각 플랫폼의 사용 맥락과 어긋날 수 있다. | +| **누가** | 키보드를 사용하는 10·20·30대 남녀 또는 키보드 애호가 | +| **어떤 상황에서** | 노트북·태블릿·스마트폰 중심 시대에서 키보드가 필수품이 아닌 선택형 제품이 되면서, 같은 값이면 더 좋은 제품을 사기 위해 종류·디자인·기능·가격을 꼼꼼히 비교하며 구매하려는 상황에서 | +| **어떤 불편을 겪는가** | 내게 꼭 맞는 키보드를 고르기 위해 알아야 할 사전 지식과 구매 과정이 지나치게 복잡하고 어렵다. 조용한 사무용 키보드 하나를 찾으려 해도 유무선 여부, 갈축·적축 같은 생소한 용어, 천차만별인 가격대, 다양한 디자인, 브랜드, 키캡 커스터마이징까지 고려해야 한다. 정보 탐색(검색·유튜브)에 많은 시간을 소비하게 된다. | --- ## **3. 솔루션** -> **macOS에서는 메뉴바에 사는 펫, Windows에서는 바탕화면에 사는 펫을 통해 나와 친구의 상태를 귀엽고 부담 없이 공유합니다.** +> **사용자가 자연어로 원하는 키보드를 설명하거나 단계적 선택지를 따라가면, 의도에 부합하는 키보드를 추천하여 구매 과정을 간소화합니다.** > -OS별 사용 맥락에 맞춰 macOS에서는 메뉴바에 작은 펫 아이콘을 표시하고, Windows에서는 바탕화면 위에 작은 펫을 띄웁니다. 사용자는 이 펫을 통해 자신의 상태를 표현하고, 친구의 상태를 확인하며, 간단한 리액션으로 부담 없이 상호작용할 수 있습니다. - -**프로젝트 설계 배경** - -최근 "셋로그"처럼 편집 과정을 제거하고 일상 공유에만 집중한 미니멀 SNS 서비스가 활발히 사용되고 있다. 이는 *적은 스트레스로 콘텐츠를 업로드하고 싶은* 사용자 니즈가 실재함을 보여준다. 이 현상을 토대로 **"미니멀 SNS 서비스를 제작하면 사용자를 이끌어낼 수 있다"** 는 가설을 검증하고자 한다. +**대전제: `간소화`** + +사용자는 두 가지 방식 중 하나로 자신에게 맞는 키보드를 찾을 수 있다. + +1. **`자연어 설명`** : 사용자가 말로써 키보드를 설명하면, 그 의도에 최대한 부합하는 키보드를 추천한다. + 1. Q. "사무실에서 사용할 조용한 키보드를 원해요. 가격대는 5만원이 넘지 않으면 좋겠어요." + 2. A. ~~한 키보드를 소개합니다. + 1. 제품명 / 특징 요약 / 가격 리스트를 제공 + 2. 사용자가 원했던 키보드의 특징을 태그 형식으로 나열. `조용함` , `사무용` … + 3. LLM이 판단하기 어려우면, 자동으로 선택지를 제공한다. +2. `선택지 제공` : 키워드를 제공하여 단계적으로 키보드를 좁혀나간다. 상관없음도 제공한다. + 1. **어떤 용도로 사용하시나요? (What)** + 1. 사무용 + 2. 게임용 + 2. **휴대하시나요?** + 1. 놓고 쓸 것입니다 + 2. 가지고 다닐래요 + 3. **타건 소리가 어땠으면 좋겠나요?** + 1. 조용해야 해요(매우 낮음) + 2. 어느정도 소리가 났으면 좋겠어요(낮은) + 3. (보통) + 4. (조금 큼) + 5. (시끄러움) + 4. **키감은 어떤 스타일을 선호하시나요?** + 1. 또각또각, 서걱서걱, 보글보글 + 5. **키압(타건 무게)은 어떤 걸 선호하시나요?** + 1. 가벼울 수 있어요 (35~45g) + 2. 보편적이에요 (45~55g) + 3. 묵직할 수 있어요(60g 이상) + 4. 잘 모르겠어요 + 6. **키보드 연결 방식은 어떻게 할까요?** + 1. 유선 + 2. 무선 + 1. usb 동글 + 2. 블루투스 + 3. 유/무선 + 7. **키보드 크기는 어떤 게 좋으신가요?** + 1. 숫자 패드가 있는 일반적인 키보드 (커요) + 2. 숫자 패드가 있지만 더 작아요 (살짝 큼) + 3. 숫자 패드가 없어요 (보통이에요) + 4. 숫자 패드가 없으면서 더 작아요 (살짝 작아요) + 5. 숫자 패드도 없고, f1~f12키도 없어요 (제일 작아요) + 8. **예산은 어느 정도로 생각하시나요?** + 1. 최소 ~ 최대 바 형식 선택 + 9. **키보드 각인은 어떻게 할까요?** + 1. 한국어, 영어가 모두 필요해요 + 2. 영어만 적혀있길 바라요 + 3. 한국어만 적혀있길 바라요 + 10. **백라이트가 필요하신가요? (키보드에 불이 들어와요)** + 1. 화려한 RGB도 포함되길 바라요 + 2. 은은한 단색만 있으면 돼요 + 3. 없어도 돼요 --- ## **4. 기술/플랫폼 선택 + 근거** -**선택 기술: Tauri v2** - -| **선택 이유** | **상세 설명** | -| --- | --- | -| **크로스 플랫폼 + OS별 UI 분기** | 단일 코드베이스로 Windows·macOS를 모두 지원하면서, 공통 도메인 로직은 공유하고 OS별 UI만 분기하여 구현 가능 | -| **macOS 메뉴바 앱 구현** | Tauri System Tray를 활용해 macOS 메뉴바에 작은 펫 아이콘을 표시하고, 클릭 시 상태·친구 정보 패널 제공 (RunCat 등 메뉴바 유틸리티 경험과 일치) | -| **Windows 바탕화면 펫 구현** | 투명하고 프레임 없는 창, 항상 위에 떠 있는 창 등 창 제어 기능으로 Windows 바탕화면 위 작은 펫 구현 가능 | -| **경량성** | Rust 기반 백엔드와 OS WebView 활용으로 Electron 대비 메모리·CPU 사용량이 적어, 장시간 백그라운드 실행에 적합 | -| **생산성 + 네이티브** | 프론트엔드는 React/TypeScript로 빠르게 구현하고, OS 밀착 기능은 Rust로 처리 | - -**예상 기술 스택** - -| **레이어** | **기술** | -| --- | --- | -| Desktop Client | Tauri v2 | -| Frontend | React, TypeScript, Vite | -| Local State | Zustand 또는 Jotai | -| Local Storage | Tauri Store Plugin | -| Window State | Tauri Window State Plugin | -| macOS UI | Tauri System Tray, Menu Bar Panel | -| Windows UI | Transparent Window, Frameless Window, Tray | -| Realtime | WebSocket | -| Backend | Spring Boot | -| Database | PostgreSQL | -| Presence Cache | Redis | -| Animation | PNG Sprite | - ---- - -## **5. 고려했지만 선택하지 않은 기술** - -| **기술** | **장점** | **선택하지 않은 이유** | -| --- | --- | --- | -| **Electron** | 투명 창·트레이 등 기능이 성숙, 구현 사례 풍부 | 장시간 백그라운드 실행 앱 특성상 앱 크기·메모리 사용량이 부담 | -| **Flutter Desktop** | UI·애니메이션 강점, Windows/macOS 데스크톱 지원 | macOS 메뉴바 앱·Windows 투명 오버레이·클릭 처리 등 OS별 세부 동작 구현 시 별도 패키지 의존도가 높아짐 | -| **OS별 네이티브 개발** | 각 플랫폼에 최적화된 완성도 높은 경험 | MVP 단계에서 두 플랫폼 별도 개발 시 비용 과대, 공통 로직 중복 구현 필요 | -| **모든 OS에서 동일한 바탕화면 펫 UI** | 제품 컨셉 설명이 쉽고, 두 OS에서 같은 화면 제공 가능 | macOS에서는 메뉴바 유틸리티 경험이 더 자연스럽고, 바탕화면에 표시되는 펫이 오히려 작업 흐름을 방해할 수 있다고 판단하여 OS별 UI로 피봇 | - ---- +제미나이 캔버스 -## **6. MVP 범위** - -## **✅ 만들 것** - -**공통 기능** - -- 내 펫 정보 관리 -- 내 상태 설정 (온라인 / 작업 중 / 쉬는 중 / 자리 비움 / 방해 금지) -- 친구 목록 확인 -- 친구 초대 및 수락 -- 친구 상태 확인 -- 친구에게 간단한 체팅 혹은 반응 보내기 - - 이모지 송신 - - 채팅 히스토리는 없어야 함 -- WebSocket 기반 실시간 상태 동기화 -- 로컬 설정 저장 - -**macOS MVP** - -- 상단 제어바에 작은 펫 아이콘 표시 -- 내 상태에 따라 메뉴바 펫 아이콘 변경 -- 메뉴바 아이콘 클릭 시 작은 정보 패널 표시 -- 패널에서 내 상태 확인·변경 / 친구 상태 확인 / 리액션 전송 -- 설정 화면 열기 - -**Windows MVP** - -- 윈도우 오버레이로 작은 펫 표시 -- 펫 드래그 이동 및 위치 저장·재실행 시 복원 -- 펫 클릭 시 간단한 리액션 -- 친구 상태에 따라 펫 모습 변경 -- 트레이 메뉴(하단제어바) (설정 열기 / 펫 숨기기 / 종료) - -## **❌ MVP에서 만들지 않는 기능** - -- 로그인 -- LLM 기반 AI 대화 -- 복잡한 채팅 기능 -- 정교한 펫 커스터마이징, 펫 만들기 -- 공식 OS 위젯 -- 활성 앱 이름·창 제목 분석(로컬에서만) & 정확한 CPU/RAM 수치 표시 - - 소프트웨어 자원/하드웨어 자원 구분 -- per-pixel click-through 같은 고급 클릭 통과 기능 -- App Store 배포(불가능할 수도 있음) -- 스트레칭 알림 -- 모바일/웨어러블 연동? - -> **MVP에서는 "OS별로 자연스러운 위치에 펫이 존재하고, 이를 통해 상태 공유와 가벼운 상호작용이 가능한가" 를 검증하는 데 집중합니다.** -> +- 웹 퍼블리싱이 필요없어서 MVP를 검증하지 않아도 된다. +- 간편한 구현이 가능하다. --- -## **7. 검증 방법 초안** - -## **성공 기준** - -- 펫을 방해 요소가 아닌 **귀여운 동반자**로 느끼는가 -- 친구 펫/상태를 통한 리액션이 채팅보다 **부담 없는 소통 방식**으로 느껴지는가 -- OS별로 다른 UI를 제공하는 것이 사용자에게 어색하지 않고 자연스럽게 받아들여지는가 -- 장시간 PC 작업 중 스트레칭 제안이 유용하거나 긍정적으로 느껴지는가 -- macOS와 Windows 각각에서 핵심 기능이 안정적으로 동작하는가 - -## **실패 기준** - -- 펫이 화면에서 **방해된다고 느끼는** 사용자가 다수인 경우 -- 사용자가 친구 상태 공유 기능의 **필요성을 느끼지 못하는** 경우 -- 리액션 기능이 **너무 가볍거나 의미 없다고** 느껴지는 경우 -- Tauri에서 메뉴바 패널 또는 투명 창 구현이 불안정할 경우 - -## **검증 방식** - -- macOS / Windows 각각에서 프로토타입 사용 테스트 -- 사용자 인터뷰 및 짧은 설문 - - 실제로 스트레칭 하셨나요? 이 기능이 유용하다고 느끼셨나요? - -> 정량적인 부분은 의견을 받거나 회고를 통해서 평가하기, 서버 사용량이나 로그를 따서 정량적인 활성도 확인하기, 구글 애널리틱스, 파이어베이스 이벤트 등등 -> - -**핵심 질문** - -- macOS 사용자는 메뉴바에 있는 작은 펫으로도 충분히 매력을 느끼는가? -- Windows 사용자는 바탕화면 위의 펫을 방해 요소가 아닌 동반자로 느끼는가? -- 친구 상태를 텍스트가 아닌 펫으로 표현하는 방식이 직관적인가? -- 채팅 없이 리액션만으로도 의미 있는 가벼운 소통이 가능한가? +## **5. 구현하지 않을 것** -**핵심 지표** - -| **지표** | **설명** | +| **기능** | **비고** | | --- | --- | -| 앱 재실행률 | 자발적으로 앱을 다시 켜는지 여부 | -| 사용자별 macOS 메뉴바 아이콘 클릭 횟수 | 메뉴바 펫에 대한 관심도 | -| Windows 펫 위치 이동 여부 | 사용자가 펫을 자기 방식으로 배치하는지 | -| 상태 변경 사용 횟수 | 상태 기능을 실제로 활용하는지 | -| 친구 초대/수락 수 | 소셜 기능 진입률 | -| 리액션 전송 횟수 | 핵심 상호작용 활성화 정도 | -| 강제 종료(의도적인 종료)·삭제 여부 | 추적 가능 여부는 미확정이나 추적 희망 | +| 키캡 커스터마이징 | MVP 범위 외 | +| 타건음 제공 | MVP 범위 외 | --- -## **8. 발표 자료 링크** +## **6. 가설 및 인터뷰** + +기획 검증을 위해 다음 가설을 세우고 인터뷰로 확인하고자 한다. -- **발표 예정일:** 2026년 5월 29일 -- **발표 자료 링크:** 미정 (추후 업데이트 예정) +1. 노트북에 별도의 키보드를 연결하여 사용하고 있다. +2. 키보드는 더 이상 필수 입력장치가 아닌, 필요에 따라 구매하는 선택형 제품이라고 인식한다. +3. 키보드 구매 시 입력이라는 목적성보다 취향을 따진다. +4. 자신의 사용 목적이나 취향에 맞게 키보드 디자인을 선택하고픈 니즈가 있다. +5. 구매 시 고려할 옵션(축, 유무선, LED, 키 배열 등)이 많아 부담을 느낀 경험이 있다. +6. 키보드 선택 과정에서 정보 탐색(검색·리뷰·유튜브)에 많은 시간을 소비한다. +7. 자신의 사용 환경(사무실·집·게임 등)에 맞는 키보드를 명확히 추천받고 싶어 한다. +8. 구매 과정이 복잡하여 추천 서비스 또는 가이드의 필요성을 느낀다. (간단한 설명 / 추천 희망) --- @@ -196,65 +125,34 @@ OS별 사용 맥락에 맞춰 macOS에서는 메뉴바에 작은 펫 아이콘 ## **1. 가장 어려웠던 의사결정 + 어떻게 풀었는지** -**의사결정 ①: 데스크톱 앱 구현 기술 스택 선택** - -이 서비스는 일반적인 웹 서비스가 아니라, 사용자의 바탕화면 위에 작은 펫이 항상 존재해야 하는 서비스다. 그렇기에, 데스크톱 특화 기능 구현 가능성과 하드웨어 성능 부담이 적은 기술을 선택해야 했다. - -초기에는 Electron, Tauri, Flutter Desktop, OS별 네이티브 개발 네 가지 옵션을 함께 검토했다. - -- **Electron:** 기능과 생태계가 안정적이나, 장시간 켜두는 앱 특성상 메모리·앱 크기 부담 -- **Flutter Desktop:** UI·애니메이션 강점이 있으나, 오버레이 창 제어에서 추가 구현 부담 예상 -- **OS별 네이티브:** 완성도 높지만 두 플랫폼 개발 비용이 MVP 규모에 비해 과도 - -결국 **크로스 플랫폼 지원 + 경량 실행 + 웹 프론트엔드 생산성 + Rust를 통한 네이티브 접근성**을 모두 고려해 Tauri를 선택했다. 단, Tauri에서도 OS별 창 제어 제약이 존재할 수 있으므로 초기 개발 단계에서 **투명 창과 항상 위 창의 안정성을 먼저 검증**하기로 했다. +**의사결정: 피봇 여부 (마라톤 회의)** ---- - -**의사결정 ②: 모든 OS에서 동일한 UI vs. OS별 다른 UX 제공** ← *기획 과정에서 변경된 핵심 결정* - -**macOS와 Windows 사용자가 자연스럽게 받아들이는 데스크톱 경험이 다르다**는 점이 문제로 떠올랐다. +기존에는 단순히 "셋로그"라는 앱을 따라 해 보고 싶었던 것이었고, 문제를 제기한 게 아니라 문제를 끼워 맞추는 식이어서 진행할 때마다 괴리를 느꼈다. 억지로 만들어낸 문제에 대해 MVP 검증을 수행하는 건 의미 없고 배울 게 없다고 판단했다. -- **macOS:** RunCat처럼 제어바에 상주하여 표시하는 경험에 익숙하다. 바탕화면 위에 펫을 계속 띄우는 방식은 귀엽기보다 작업 흐름을 방해하는 요소가 될 수 있다. -- **Windows:** 작업표시줄(하단 메뉴바)에서 표시할 수 있는 범위가 적다. - -이 차이를 무시하고 동일한 UI를 적용하면 한 쪽 OS에서 사용자 경험이 어색해질 수 있다고 판단했다. 결국 **"모든 OS에서 같은 UI를 제공하는 것"보다 "서비스의 핵심 경험은 유지하되 OS별 사용 맥락에 맞는 UI를 제공하는 것"** 이 더 중요하다는 결론에 도달했고, macOS는 메뉴바 펫, Windows는 바탕화면 펫으로 피봇했다. - -구현은 다소 복잡해졌지만, **공통 도메인 로직은 유지하고 OS별 UI만 분기하는 방식**으로 복잡도를 관리하기로 했다. +정말 우리가 느낀 불편함을 해결하는 서비스를 만들어 문제를 해결하고, 그 과정에서 더 많은 배움을 얻고자 했다. 결국 실제 겪은 불편함으로 문제를 인식하고, 이를 해결하기 위한 서비스를 구상하는 방향으로 피봇했다. --- ## **2. 합의되지 않은 부분** -1. **macOS 메뉴바 아이콘으로 정보를 충분히 전달할 수 있는가** - - 메뉴바 아이콘은 크기가 작아 펫의 감정이나 움직임을 표현하는 데 한계가 있다. 아이콘 클릭 시 표시되는 패널에서 더 큰 펫과 친구 상태를 보여주는 방향을 보완책으로 검토 중이다. - -2. **Windows 오버레이의 방해 정도** - - 항상 위에 떠 있는 방식은 존재감을 높이지만 작업 방해 가능성도 있다. 숨김 기능·방해 금지 모드·위치 저장을 MVP에 포함해 실제 사용 테스트를 통해 적절한 방식을 찾을 예정이다. - -3. **하드웨어 상태를 펫 상태에 반영할지 여부** - - CPU 부하에 따른 펫 상태 변경 기능은 흥미롭지만 프라이버시와 연결될 수 있다. MVP에서는 사용자가 직접 설정한 상태를 우선하고 자동 추론 기능은 후순위로 두기로 했다. - -4. 실패 기준에서 OS별 구현 차이로 인해 개발 비용이 예상보다 크게 증가할 경우를 추가할 지 - - 결국 맥이나 윈도우 플랫폼을 종료할 거냐라는 의문에 도달할까 봐 합의하지 못 했고, 나중에 합의하기로 결정했다. - +1. 결과를 어떻게 보여줄 것인가? + 1. 키보드를 몇개까지 보여줄것인가? + 2. 키보드에 대한 정보를 얼마나 알려줘야할까? + 3. 키보드와 관련된 옵션을 소개시켜야할까? +2. 자연어 입력의 경우, 사용자의 입력을 챗봇이 이해하지 못하면 사용자에게 다시 되물어야 하는가? + 1. 자연어 입력과 선택지를 제공하여 사용자의 선택을 돕는 서비스인데, 챗봇이 선택지로 다시 질문을 해야 할까? 아니면 되물어야 할까? 만약, 선택지 외에 다른 옵션을 사용자가 원하면 어떻게 해야 할까? +3. 설명을 편하게 하는 것 말고는, 다나와보다 사용성이 편리한가? + 1. 다나와도 우리 서비스처럼 많은 키보드 카테고리를 제공하는데, 우리 서비스와 다나와를 비교했을 때 우리 서비스가 더 유리하다고 할 수 있을까? +4. 주문 플로우가 필요한가? (쿠팡 링크를 제공할 것인가?) + 1. 보통 이러한 서비스는 마지막에 결제 정보까지 제공하는데 이걸 제공해야할까? --- -## **3. 가장 의심스러운 가정 1가지** +## **3. 가장 의심스러운 가정** -> **"사용자가 데스크톱 위에 항상 떠 있는 펫을 불편하지 않게 느낄 것이다"** +> 키보드의 지식이 많은 사람들이 이 서비스를 필요로 하는가? > -macOS에서는 제어바에 존재하여 다른 제어바 요소와 비슷하게 동작함을 사용자가 예상할 수 있지만, Window에서는 윈도우 오버레이를 클릭하여 어떻게 동작할 건지 사용자가 예상할 수 있겠는가? - -이 가정이 이 서비스 전체의 핵심 전제다. 아이디어 자체는 매력적이지만, 실제 사용 환경에서는 화면을 가리거나 집중을 방해하는 요소로 느껴질 수 있다. 특히 개발자·학습자처럼 화면 공간 활용도가 높은 사용자에게는 작은 펫조차 방해가 될 수 있다. - -**따라서 MVP로 사용자를 얼마나 방해하는지를 고려해야 한다.** - ---- +우리가 만든 서비스가 키보드 지식이 없어도 본인에게 맞는 키보드를 선택할 수 있도록 도와주는 서비스인데, 키보드 지식이 많은 사람도 서비스 대상으로 봐야할까? 오히려 라이트층에 집중해야 하지 않을까? -이 가정(예측)이 틀린다면 서비스의 자체를 재설계해야 하므로, MVP에서 가장 먼저 검증해야 할 항목이다. \ No newline at end of file +현재는 키보드를 모르는 누구나 봐도 이해하기 쉽도록 질문지를 구성하였기 때문에 오히려 키보드에 대한 지식이 많은 코어층은 우리 서비스가 불필요할 수 있다. \ No newline at end of file From 8b4a7d7670615ba91658a82a7036e8b0aafbd930 Mon Sep 17 00:00:00 2001 From: ujeong Date: Thu, 4 Jun 2026 17:56:29 +0900 Subject: [PATCH 003/137] =?UTF-8?q?docs:=202=EC=A3=BC=EC=B0=A8=20=EB=B3=B4?= =?UTF-8?q?=EA=B3=A0=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- report/week2.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 report/week2.md diff --git a/report/week2.md b/report/week2.md new file mode 100644 index 00000000..89a17d24 --- /dev/null +++ b/report/week2.md @@ -0,0 +1,139 @@ +# Week 2 보고서 — ellipsis + +## 이번 주 한 일 (계획 vs 실제) + +**계획** + +1. 기존 아이템(상태 공유 서비스, 플러피) 기획 +2. 워크숍과 인터뷰를 통해 가설 검증 +3. 가설 기반 사용자 문제 인식 여부 조사 +4. 문제 해결을 위한 프로젝트 MVP 구현 + +**실제** + +1. 기존 아이템(상태 공유 서비스, 플러피) 기획 및 진행 +2. 워크숍 / 인터뷰 진행 결과, 핵심 가설 검증 실패 +3. 해당 결과 기반 발표 진행 및 크루 / 코치로부터 부정적 피드백 확인 +4. 깨진 가설 및 의문에 대한 단순 기능 개선의 한계 인지 및 프로젝트 방향 전환 결정 +5. 논의 결과, 키보드 추천 도우미 서비스 ‘키버디’로 아이디어 피벗 진행 +6. 간소화된 워크숍 / 인터뷰 진행을 통한 신규 가설 검증 및 사용자 문제 인식 확인 +7. 설문조사 기반 기능 설정 및 Gemini Canvas로 UI 프로토타입 구현 +8. 키보드 카테고리 대상 약 100개 제품 데이터 크롤링 및 수집 +9. 수집 데이터 및 프로토타입 기반 작동 가능한 웹 페이지 구현 +10. 추천 결과 정확도 개선 필요성 인지 및 최종 발표 전까지 지속적 고도화 계획 수립 + +--- + +## A. 인터뷰 전 (워크숍 산출물) + +### HMW + +- HMW 1: 어떻게 하면 사용자가 복잡한 키보드 용어를 몰라도 **편하게 키보드를 고를 수 있게** 도와줄 수 있을까? +- HMW 2: 어떻게 하면 사용자가 검색/리뷰 탐색에 쓰는 시간을 줄이고 **원하는 키보드를 빠르게** 찾을 수 있을까? +- HMW 3: 어떻게 하면 “축, 배열, 유무선” 같은 기술적인 언어를 사용하지 않고 원하는 키보드를 찾을 수 있을까? +- **검증할 HMW**: + - HMW 1: 어떻게 하면 사용자가 복잡한 키보드 용어를 몰라도 **편하게 키보드를 고를 수 있게** 도와줄 수 있을까? + +### 핵심 가설 + +| # | 가설 | 지표 (누가/얼마나/무엇을) | 측정 방법 | +| --- | --- | --- | --- | +| 1 | 사용자는 키보드 구매 과정에서 높은 진입장벽과 선택 피로를 느낀다. | 응답자 중 과반이 옵션이 많아 어렵거나 부담스럽고, 정보 탐색 중 피로감 때문에 구매를 미룬 경험이 있다고 응답하면 가설을 지지한 것으로 본다. | 설문조사를 통해 키보드 지식 수준, 옵션에 대한 부담감, 정보 탐색 중 피로로 인한 구매 미루기 경험 여부를 질문한다. | +| 2 | 키보드는 ‘선택형 취향 제품’으로 인식된다. | 응답자 중 70% 이상이 키보드를 필수 입력장치가 아닌 필요에 따라 선택해서 사는 제품이라 인식하고, 과반이 구매 시 기능보다 타건감·소음·디자인 등 취향 요소를 더 중요하게 고려한다고 응답하면 가설을 지지한 것으로 본다. | 설문조사를 통해 키보드에 대한 인식(필수품 vs 선택형)과 구매 시 가장 중요하게 고려하는 요소(기능/가격 vs 취향 요소)를 질문한다. | +| 3 | 사용자는 ‘간단한 선택 → 맞춤 추천’ 형태의 가이드를 원한다. | 응답자 중 과반이 자신의 사용 환경과 취향에 맞는 키보드 추천을 받고 싶고, 키보드를 추천해주는 서비스를 사용해보고 싶다고 응답하면 가설을 지지한 것으로 본다. | 설문조사를 통해 맞춤형 추천에 대한 니즈와 질문 기반 추천 서비스 사용 의향을 질문한다. | + +### 인터뷰 질문지 + +- 귀하의 연령대는 어떻게 되십니까? +- **노트북 사용 시, 별도의 키보드를 연결하여 사용하고 계신가요?** +- 키보드를 필수 입력장치 뿐만 아닌, **필요에 따라 구매하는 선택형 제품**이라고 생각하시나요? +- 키보드를 구매할 때, 단순한 입력 기능보다, **취향(타건감, 무게, 키배열)을 더 중요**하게 고려하시나요? +- 키보드 구매 시, 축 종류, 연결 방식, LED, 키 배열 등 다양한 옵션으로 인해 부담을 느낀 적이 있으신가요? +- 키보드를 선택하는 과정에서 **검색, 리뷰, 유튜브 등을 참고하며 많은 시간을 사용**하시나요? +- **자신의 사용 환경(사무실, 집, 게임 등) 및 취향에 맞는 키보드를 명확하게 추천**받고 싶으신가요? +- 키보드 추천 서비스나 가이드가 있다면 사용하실 의향이 있나요? + +### 인터뷰 대상자 + +우아한테크코스 크루 및 리뷰어 불특정 다수 28명 + +--- + +## B. 인터뷰 실행 + +본 프로젝트는 촉박한 시일 내 아이디어 피봇을 통해 이전 사이클을 빠르게 진행하였기에, 불가피하게 인터뷰 과정을 Google Form을 활용하여 대체하였습니다. + +--- + +## C. 인터뷰 후 분석 + +가설 검증을 위해 구글 폼을 활용한 우아한테크코스 내부 설문조사(인터뷰)를 실시했습니다. + +6월 4일 오후 2시까지 `28개`의 설문 응답을 받았습니다. + +| **구분** | **비율** | +| --- | --- | +| **부분 지지** | **41 ~ 60%** | +| **지지** | **61 ~ 80%** | +| **강력 지지** | **81 ~ 100%** | + +1 | **노트북에 별도 키보드를 연결해 사용한다** | `부분 지지` | **59% (17/29명) “네”** + +2 | **키보드를 선택형 제품으로 인식한다** | `강력 지지` | **83% (24/29명) “네”** + +3 | **구매 시 취향을 더 중요하게 고려한다** | `강력 지지` | **90% (26/29명) “네”** + +4 | **다양한 옵션으로 인해 부담을 느낀다** | `지지` | **69% (20/29명) “어렵다 / 너무 어렵다” 응답** + +5 | **정보 탐색에 많은 시간을 소비한다** | `지지` | **66% (19/29명) “오래 걸렸어요”** + +6 | **사용 환경에 맞는 추천을 받고 싶다** | `강력 지지` | **86% (25/29명) “네”** + +7 | **추천 서비스가 있다면 이용하겠다** | `지지` | **72% (21/29명) “네”** + +### 공통 문제 1개 + +- 29명 중 20명이 “축, 배열, 유무선, LED 등 다양한 옵션 때문에 어렵다 / 너무 어렵다”고 응답했고, 19명이 “키보드를 고르기 위해 정보를 찾는 데 오래 걸렸다”고 응답했다. +- 가장 강하게 표현된 부분: “키보드는 기능보다 취향(타건감, 소음, 디자인 등)을 더 중요하게 고려하는 선택형 제품이며, 자신의 사용 환경에 맞는 키보드를 명확하게 추천받고 싶다”는 응답이 가장 강하게 표현되었다. (취향 고려 90%, 선택형 인식 83%, 추천 니즈 86%) +- 일부 “키보드에 관심이 없다”거나 추천 서비스 이용 의사가 낮은 응답자는 핵심 타겟과 다른 집단일 수 있으므로, 해석 시 주 타겟(관심, 구매 의향이 있는 사용자)의 응답 패턴을 중심으로 분석했다. + +따라서 현재 사용자가 원하는 키보드 탐색 과정이 지나치게 복잡해, 피로를 느끼고 결국 선택을 미루는 문제가 있다고 판단했습니다. + +### Impact × Effort + +| | Effort 낮음 | Effort 높음 | +| --- | --- | --- | +| Impact 높음 | 키보드 데이터 추가 확보 | 추천 결과의 품질을 어떻게 높일 것인가 | +| Impact 낮음 | MVP 배포 및 피드백 수집 | 제공할 데이터의 신뢰성을 어떻게 높일 것인가? | + +### 다음 주 액션 (일부만 개선) + +- 다음 주에 시도할 1가지: 추천 결과의 품질 고도화 +- 어떻게 효과를 확인할 것인가: 사용자 반응 수집 + +--- + +## D. 회고 + +### 깨진 가설 + +- 깨진 가설은 없습니다. + +### 인터뷰에서 못 잡은 것 + +키보드를 구매하지 않은, 노트북 내장 키보드만 사용하는 층에서 `귀하께서는 내장키보드를 선택하셨습니다. 그 이유는 무엇인가요?` 라는 항목을 두고 + +- 비싸서 +- 필요성을 못 느껴서 +- 노트북 내장 키보드가 더 편해서 +- 어려워서 +- 기타 (자유 서술) + +라는 검증도 해보았으면 어땠을까 합니다. + +위 문항 중 `필요성을 못 느껴서`, `노트북 내장 키보드가 더 편해서` 응답을 제외한 나머지 응답은 우리 서비스의 잠재적 대상으로 볼 수 있었을 것 같습니다. + +### 다음 인터뷰를 위한 액션 + +- 대면 인터뷰 진행 +- MVP 기능 구현 및 고도화 \ No newline at end of file From d00f33051cc00cf54a9ebc657e87fdbc7bf7ce53 Mon Sep 17 00:00:00 2001 From: ujeong Date: Thu, 4 Jun 2026 18:01:54 +0900 Subject: [PATCH 004/137] =?UTF-8?q?chore:=20=ED=94=84=EB=A1=9C=ED=86=A0?= =?UTF-8?q?=ED=83=80=EC=9E=85,=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC,=20=EC=88=98=EC=A7=91=ED=95=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B2=A8=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- impl/crawl.py | 280 +++++++ impl/danawa_crawler.py | 201 +++++ impl/keyboardfinder_jsx.tsx | 479 ++++++++++++ impl/output/keyboards.csv | 101 +++ impl/output/keyboards.json | 1402 +++++++++++++++++++++++++++++++++++ 5 files changed, 2463 insertions(+) create mode 100644 impl/crawl.py create mode 100644 impl/danawa_crawler.py create mode 100644 impl/keyboardfinder_jsx.tsx create mode 100644 impl/output/keyboards.csv create mode 100644 impl/output/keyboards.json diff --git a/impl/crawl.py b/impl/crawl.py new file mode 100644 index 00000000..42bec866 --- /dev/null +++ b/impl/crawl.py @@ -0,0 +1,280 @@ +"""다나와 키보드 검색 결과 목록 페이지 크롤러. + +인기순(기본 정렬) 검색 결과를 1페이지부터 순회하며 최대 100개 키보드 제품의 +기본정보(제품명/브랜드/가격/이미지)와 스펙(스위치/연결/배열/키압)을 목록에서만 추출해 +JSON과 CSV로 저장한다. 상세 페이지는 순회하지 않으며, 목록에서 확인 불가한 스펙은 빈 값으로 둔다. +""" + +import csv +import json +import re +import time +from pathlib import Path + +import requests +from bs4 import BeautifulSoup + +SEARCH_URL = "https://search.danawa.com/dsearch.php" +KEYWORD = "키보드" +TARGET = 100 +MAX_PAGES = 40 # 안전장치(무한 루프 방지) +DELAY_SEC = 1.5 # 페이지 간 요청 간격(1-2초) +OUTPUT_DIR = Path(__file__).resolve().parent / "output" + +# 완전한 레코드 판정에 필요한 필드(모두 값이 있어야 함) +# engraving/backlight는 명시 없을 때 "정보없음"/"없음"으로 채워지므로 항상 비어있지 않음 +REQUIRED_FIELDS = ("product_name", "brand", "price", "image_url", + "switch_type", "connection", "layout", "key_force", + "weight_g", "wireless_type", "engraving", "backlight") + + +def is_complete(item: dict) -> bool: + return all(item.get(f) not in (None, "") for f in REQUIRED_FIELDS) + +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" + ), + "Referer": "https://www.danawa.com/", + "Accept-Language": "ko-KR,ko;q=0.9", +} + +# 스펙 토큰 분류용 패턴 +CONNECTION_TOKENS = ("유선+무선", "유선", "무선", "블루투스", "2.4GHz", "동글") +SWITCH_MECH_TOKENS = ("기계식", "멤브레인", "무접점", "광축", "펜타그래프", "정전용량") +SWITCH_AXIS = re.compile(r"(적축|청축|갈축|황축|흑축|백축|은축|저소음\s*\w*축|\w+축)") +LAYOUT_TOKENS = ("풀배열", "텐키리스", "미니배열", "미니") +LAYOUT_KEYCOUNT = re.compile(r"^\d{2,3}키$") +# "키압 : 43g" / "키압: 43g" 형태에서 값 추출 +KEYFORCE = re.compile(r"키압\s*[::]?\s*([0-9]+\s*g(?:f)?|구분압|균등압|\S+)", re.IGNORECASE) +# 무게: "1020g" 또는 "1.02kg" 같은 단독 토큰(키압/배터리mAh와 구분) +WEIGHT = re.compile(r"^([0-9]+(?:\.[0-9]+)?)\s*(kg|g)$", re.IGNORECASE) +# 무선연결 타입 후보 +WIRELESS_KEYS = ("전용동글", "동글", "리시버", "블루투스", "2.4GHz", "RF") +# 각인: "한/영 정각", "영문 정각", "한/영 음각" 등(정각/음각/각인/무각 포함 토큰) +ENGRAVING_TOKENS = ("정각", "음각", "각인", "무각") +# 백라이트: "RGB 백라이트", "단색 백라이트", "LED" 등 +BACKLIGHT_TOKENS = ("백라이트", "LED") + +# spec 항목 구분자: " / "(앞뒤 공백 있는 슬래시)만 분리해 "한/영", "S/W매크로" 보존 +SPEC_SPLIT = re.compile(r"\s+/\s+") + + +def parse_spec(spec_text: str) -> dict: + """전체 spec 문자열에서 스위치/연결/배열/키압/무게/무선타입을 추출한다.""" + tokens = [t.strip() for t in SPEC_SPLIT.split(spec_text) if t.strip()] + connection = "" + layout = "" + key_force = "" + weight_g = None + mech = "" + axis = "" + wireless = [] + engraving = "" + backlight = "" + + for tok in tokens: + if tok == "키보드": + continue + if not connection: + for c in CONNECTION_TOKENS: + if tok == c or tok.startswith(c): + connection = tok + break + if not layout: + if tok in LAYOUT_TOKENS or LAYOUT_KEYCOUNT.match(tok): + layout = tok + if not mech: + for m in SWITCH_MECH_TOKENS: + if m in tok: + mech = m + break + if not axis: + am = SWITCH_AXIS.search(tok) + if am: + axis = am.group(1) + if not key_force: + km = KEYFORCE.search(tok) + if km: + key_force = km.group(1).strip() + if weight_g is None: + wm = WEIGHT.match(tok) + if wm: + val = float(wm.group(1)) + grams = val * 1000 if wm.group(2).lower() == "kg" else val + # 키보드 무게는 보통 100g 이상 -> 키압(수십 g) 오인 방지 + if grams >= 100: + weight_g = int(round(grams)) + # 무선연결 타입은 여러 개 누적 + for wk in WIRELESS_KEYS: + if wk in tok and tok not in wireless: + wireless.append(tok) + break + if not engraving and any(e in tok for e in ENGRAVING_TOKENS): + engraving = tok + if not backlight and any(b in tok for b in BACKLIGHT_TOKENS): + backlight = tok + + switch = " ".join(p for p in (mech, axis) if p).strip() + + # 멤브레인/펜타그래프는 키압을 제공하지 않으므로 0g으로 설정 + if not key_force and mech in ("멤브레인", "펜타그래프"): + key_force = "0g" + + # 무선 타입: 토큰이 있으면 그대로, 유선 전용이면 "유선", 그 외(무선인데 미파악)는 빈 값 + if wireless: + wireless_type = ", ".join(wireless) + elif connection == "유선": + wireless_type = "유선" + else: + wireless_type = "" + + # 각인/백라이트: 명시 없으면 각각 "정보없음"/"없음"으로 채움(사용자 결정) + engraving = engraving or "정보없음" + backlight = backlight or "없음" + + return { + "switch_type": switch, + "connection": connection, + "layout": layout, + "key_force": key_force, + "weight_g": weight_g, + "wireless_type": wireless_type, + "engraving": engraving, + "backlight": backlight, + } + + +def parse_price(price_text: str): + digits = re.sub(r"[^\d]", "", price_text or "") + return int(digits) if digits else None + + +def extract_brand(name: str) -> str: + return name.split()[0] if name else "" + + +def parse_item(li) -> dict | None: + name_el = li.select_one(".prod_name a") + if not name_el: + return None + name = name_el.get_text(strip=True) + if not name: + return None + + price_el = li.select_one(".price_sect strong") + price = parse_price(price_el.get_text(strip=True) if price_el else "") + + img_el = li.select_one(".thumb_image img") + image_url = "" + if img_el: + # lazyload: 실제 이미지는 data-src/data-original 에 있고 src 는 noImg placeholder + image_url = (img_el.get("data-src") or img_el.get("data-original") + or img_el.get("src") or "") + if "noImg" in image_url or "noData" in image_url: + image_url = "" # placeholder -> 결측 처리(완전성 필터가 제외) + if image_url.startswith("//"): + image_url = "https:" + image_url + + # 전체 스펙(키압/치수/무게 포함)은 div.spec-box--full 안에 있음. 없으면 짧은 spec_list로 폴백. + spec_el = li.select_one("div.spec-box--full") or li.select_one(".spec_list") + empty = {"switch_type": "", "connection": "", "layout": "", "key_force": "", + "weight_g": None, "wireless_type": "", + "engraving": "정보없음", "backlight": "없음"} + spec = parse_spec(spec_el.get_text(" ", strip=True)) if spec_el else empty + + return { + "product_name": name, + "brand": extract_brand(name), + "price": price, + "image_url": image_url, + "switch_type": spec["switch_type"], + "connection": spec["connection"], + "layout": spec["layout"], + "key_force": spec["key_force"], + "weight_g": spec["weight_g"], + "wireless_type": spec["wireless_type"], + "engraving": spec["engraving"], + "backlight": spec["backlight"], + } + + +def fetch_page(page: int) -> list: + params = {"k1": KEYWORD, "module": "goods", "act": "dispMain", "page": str(page)} + try: + r = requests.get(SEARCH_URL, params=params, headers=HEADERS, timeout=20) + r.raise_for_status() + except requests.RequestException as e: + print(f" [page {page}] 요청 실패: {e} -> 다음 페이지로 진행") + return [] + soup = BeautifulSoup(r.text, "lxml") + results = [] + for li in soup.select("li.prod_item"): + # 광고/연관상품(prod_main_info 없는 항목) 제외 + if not li.select_one(".prod_main_info"): + continue + try: + item = parse_item(li) + except Exception as e: # 한 항목 파싱 실패가 전체를 멈추지 않게 + print(f" [page {page}] 항목 파싱 실패: {e}") + continue + if item: + results.append(item) + return results + + +def main(): + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + collected = [] # 완전한 레코드만 보관 + seen = set() + skipped_incomplete = 0 + page = 1 + while len(collected) < TARGET and page <= MAX_PAGES: + items = fetch_page(page) + if not items: + # 빈 페이지 = 마지막 페이지 도달로 간주하고 중단(무한 루프 방지) + print(f"page {page}: 0개 -> 마지막 페이지로 간주, 중단") + break + added = 0 + for it in items: + key = it["product_name"] + if key in seen: + continue + seen.add(key) + if not is_complete(it): + skipped_incomplete += 1 + continue + collected.append(it) + added += 1 + if len(collected) >= TARGET: + break + print(f"page {page}: +{added} 완전 (누적 {len(collected)}, 불완전 누적제외 {skipped_incomplete})") + if len(collected) >= TARGET: + break + page += 1 + time.sleep(DELAY_SEC) + + collected = collected[:TARGET] + + json_path = OUTPUT_DIR / "keyboards.json" + csv_path = OUTPUT_DIR / "keyboards.csv" + fields = ["product_name", "brand", "price", "image_url", + "switch_type", "connection", "layout", "key_force", + "weight_g", "wireless_type", "engraving", "backlight"] + + with json_path.open("w", encoding="utf-8") as f: + json.dump(collected, f, ensure_ascii=False, indent=2) + + with csv_path.open("w", encoding="utf-8-sig", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(collected) + + print(f"\n총 {len(collected)}개 수집 완료") + print(f"JSON: {json_path}") + print(f"CSV : {csv_path}") + + +if __name__ == "__main__": + main() diff --git a/impl/danawa_crawler.py b/impl/danawa_crawler.py new file mode 100644 index 00000000..30fe3cd4 --- /dev/null +++ b/impl/danawa_crawler.py @@ -0,0 +1,201 @@ +"""다나와 키보드 검색 결과 1회성 크롤러. + +검색 목록 페이지(상세 미순회)에서 인기순으로 약 100개 제품의 +기본정보(제품명/브랜드/가격/이미지)와 스펙(스위치/연결/배열/키압)을 +추출해 JSON, CSV 로 저장한다. + +robots.txt(https://search.danawa.com/robots.txt)는 /dsearch.php 를 막지 +않으나 Crawl-delay: 10 을 명시하므로 기본 요청 간격을 10초로 둔다. +개인/학습용 전제이며, 상업/배포 시 약관을 재검토할 것. +""" +from __future__ import annotations + +import csv +import json +import re +import sys +import time +from dataclasses import asdict, dataclass, field +from pathlib import Path + +import requests +from bs4 import BeautifulSoup + +# ---- 설정 --------------------------------------------------------------- +SEARCH_URL = "https://search.danawa.com/dsearch.php" +KEYWORD = "키보드" +SORT = "saveDESC" # 인기순(많이 찾는 순) +PER_PAGE = 40 +TARGET_COUNT = 100 +REQUEST_DELAY_SEC = 10.0 # robots.txt Crawl-delay 준수 +TIMEOUT_SEC = 20 +MAX_PAGES = 10 # 안전장치(끝 페이지 도달 시 그 전에 중단됨) +USER_AGENT = ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" +) +OUT_DIR = Path(__file__).resolve().parent / "output" + +# 스펙 토큰 매칭용 키워드 (다나와 목록 스펙은 '/' 구분 평문) +LAYOUT_KEYWORDS = ["풀배열", "텐키리스", "미니배열", "미니", "일체형", "84키", "87키", "104키"] +CONNECTION_KEYWORDS = ["유선+무선", "유선/무선", "유선", "무선", "블루투스", "전용동글", "전용 동글", "USB"] +SWITCH_TYPE_KEYWORDS = ["기계식", "멤브레인", "무접점", "펜타그래프", "플런저", "광축"] + + +@dataclass +class Product: + name: str + brand: str + price: str + image_url: str + switch: str + connection: str + layout: str + key_force: str + product_url: str = "" + raw_spec: str = field(default="") + + +def fetch_list_page(session: requests.Session, page: int) -> str: + params = { + "k1": KEYWORD, + "module": "goods", + "act": "dispMain", + "sort": SORT, + "page": str(page), + "limit": str(PER_PAGE), + } + resp = session.get(SEARCH_URL, params=params, timeout=TIMEOUT_SEC) + resp.raise_for_status() + return resp.text + + +def parse_spec(spec_text: str) -> dict: + """슬래시 구분 스펙 평문에서 스위치/연결/배열/키압 추출. 결측은 빈 문자열.""" + tokens = [t.strip() for t in spec_text.split("/") if t.strip()] + + def first_match(keywords: list[str]) -> str: + for tok in tokens: + for kw in keywords: + if kw in tok: + return tok + return "" + + layout = first_match(LAYOUT_KEYWORDS) + connection = first_match(CONNECTION_KEYWORDS) + + # 스위치: 타입 토큰 + 축 토큰을 합쳐 표현 + switch_type = first_match(SWITCH_TYPE_KEYWORDS) + axis = next((t for t in tokens if "축" in t), "") + switch = " ".join(p for p in [switch_type, axis] if p).strip() + + # 키압: '키압'/'gf' 명시 토큰만 채택(무게 'g' 토큰 오인 방지). 목록엔 드묾. + key_force = "" + for tok in tokens: + if "압" in tok or re.search(r"\d+\s*gf\b", tok): + key_force = tok + break + + return {"switch": switch, "connection": connection, "layout": layout, "key_force": key_force} + + +def parse_product(li) -> Product | None: + name_el = li.select_one(".prod_name a") + if not name_el: + return None + name = name_el.get_text(strip=True) + product_url = name_el.get("href", "") + + price_el = li.select_one(".price_sect strong") + price = price_el.get_text(strip=True) if price_el else "" + + img_el = li.select_one(".thumb_image img") + image_url = "" + if img_el: + image_url = img_el.get("data-original") or img_el.get("src") or "" + if image_url.startswith("//"): + image_url = "https:" + image_url + + spec_el = li.select_one(".spec_list") + raw_spec = spec_el.get_text("/", strip=True) if spec_el else "" + spec = parse_spec(raw_spec) + + # 브랜드: 제품명 첫 토큰(목록에 별도 브랜드 필드가 없어 근사 추출) + brand = name.split(" ", 1)[0] if name else "" + + return Product( + name=name, + brand=brand, + price=price, + image_url=image_url, + product_url=product_url, + raw_spec=raw_spec, + **spec, + ) + + +def crawl() -> list[Product]: + session = requests.Session() + session.headers.update({"User-Agent": USER_AGENT, "Accept-Language": "ko-KR,ko;q=0.9"}) + + products: list[Product] = [] + for page in range(1, MAX_PAGES + 1): + if page > 1: + time.sleep(REQUEST_DELAY_SEC) + print(f"[page {page}] 요청 중...", flush=True) + html = fetch_list_page(session, page) + soup = BeautifulSoup(html, "lxml") + items = [ + li + for li in soup.select("ul.product_list > li.prod_item") + if li.get("id", "").startswith("productItem") + ] + if not items: + print(f"[page {page}] 상품 없음 -> 끝 페이지로 판단, 중단", flush=True) + break + + page_count = 0 + for li in items: + p = parse_product(li) + if p: + products.append(p) + page_count += 1 + if len(products) >= TARGET_COUNT: + break + print(f"[page {page}] {page_count}개 수집 (누적 {len(products)})", flush=True) + if len(products) >= TARGET_COUNT: + break + + return products[:TARGET_COUNT] + + +def save(products: list[Product]) -> None: + OUT_DIR.mkdir(parents=True, exist_ok=True) + rows = [asdict(p) for p in products] + + json_path = OUT_DIR / "keyboards.json" + json_path.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8") + + csv_path = OUT_DIR / "keyboards.csv" + fields = ["name", "brand", "price", "image_url", "switch", "connection", "layout", "key_force", "product_url", "raw_spec"] + with csv_path.open("w", newline="", encoding="utf-8-sig") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + + print(f"\n저장 완료: {len(products)}개") + print(f" - {json_path}") + print(f" - {csv_path}") + + +def main() -> int: + products = crawl() + if not products: + print("수집된 제품이 없습니다.", file=sys.stderr) + return 1 + save(products) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/impl/keyboardfinder_jsx.tsx b/impl/keyboardfinder_jsx.tsx new file mode 100644 index 00000000..cdda0c9a --- /dev/null +++ b/impl/keyboardfinder_jsx.tsx @@ -0,0 +1,479 @@ +import React, { useState, useEffect } from 'react'; +import { + Search, + Keyboard, + MessageSquare, + ChevronRight, + ChevronLeft, + SlidersHorizontal, + Check, + RefreshCw, + Copy, + CheckCircle2, + Filter, + ArrowUpDown +} from 'lucide-react'; + +// --- [모의 데이터] 검색 결과에 표시될 키보드 목록 --- +const mockKeyboards = [ + { + id: 1, + name: "한성컴퓨터 GK898B 무접점", + brand: "한성컴퓨터", + description: "조용한 사무실에서 쓰기 좋은 보글보글한 키감의 끝판왕", + tags: ["무접점", "보글보글", "무선", "사무용"], + price: "169,000원", + imageColor: "bg-blue-100" + }, + { + id: 2, + name: "로지텍 MX Keys S", + brand: "로지텍", + description: "펜타그래프의 정석, 완벽한 무선 사무용 키보드", + tags: ["펜타그래프", "저소음", "블루투스", "풀배열"], + price: "159,000원", + imageColor: "bg-slate-200" + }, + { + id: 3, + name: "키크론 K3 PRO", + brand: "키크론", + description: "가볍게 들고 다니는 로우프로파일 기계식 키보드", + tags: ["휴대용", "기계식", "적축", "텐키리스"], + price: "144,000원", + imageColor: "bg-gray-800" + }, + { + id: 4, + name: "레오폴드 FC900R BT", + brand: "레오폴드", + description: "클래식한 디자인과 정갈한 또각또각 타건감", + tags: ["갈축", "또각또각", "유무선", "풀배열"], + price: "175,000원", + imageColor: "bg-orange-100" + } +]; + +// --- [질문 데이터] 단계별 선택지 --- +const questions = [ + { id: 'usage', title: '어떤 용도로 사용하시나요?', options: ['사무용', '게임용', '상관없음'] }, + { id: 'portability', title: '주로 어디서 사용하시나요?', options: ['책상에 놓고 쓸 거예요', '자주 가지고 다닐래요', '상관없음'] }, + { id: 'sound', title: '타건 소리는 어느 정도가 좋나요?', options: ['조용해야 해요 (매우 낮음)', '조금 소리가 났으면 해요 (낮음)', '적당한 소리 (보통)', '경쾌한 소리 (조금 큼)', '타건감 위주 (시끄러워도 됨)'] }, + { id: 'feel', title: '어떤 느낌의 키감을 선호하시나요?', options: ['또각또각 (걸림이 있는 느낌)', '서걱서걱 (부드럽게 들어가는 느낌)', '보글보글 (독특한 무접점 느낌)', '잘 모르겠어요'] }, + { id: 'weight', title: '키를 누를 때의 무게감은요?', options: ['가볍게 눌렸으면 좋겠어요 (35~45g)', '보편적인게 좋아요 (45~55g)', '묵직한게 좋아요 (60g 이상)', '잘 모르겠어요'] }, + { id: 'connection', title: '어떤 연결 방식을 원하시나요?', options: ['유선', '무선 USB 동글', '블루투스', '유/무선 모두', '상관없음'] }, + { id: 'size', title: '원하시는 키보드 크기가 있나요?', options: ['숫자 패드가 있는 일반 키보드 (풀배열)', '숫자 패드가 있지만 콤팩트함 (1800배열)', '숫자 패드가 없음 (텐키리스)', '숫자 패드도, 일부 특수키도 없음 (75%/65%)', 'F1~F12키도 없는 미니 (60%)'] }, + { id: 'budget', title: '예산은 어느 정도로 생각하시나요?', type: 'range', options: [] }, + { id: 'language', title: '키보드 각인은 어떻게 할까요?', options: ['한국어, 영어가 모두 필요해요', '영어만 적혀있길 바라요', '한국어만 적혀있길 바라요', '상관없음'] }, + { id: 'backlight', title: '백라이트(조명)가 필요하신가요?', options: ['화려한 RGB가 좋아요', '은은한 단색 조명이 좋아요', '없어도 돼요 (배터리 절약)'] }, +]; + +export default function App() { + const [view, setView] = useState('home'); // home, step, results + const [loading, setLoading] = useState(false); + + // --- HOME VIEW --- + const HomeView = () => { + const [query, setQuery] = useState(''); + const templates = [ + "조용한 사무실에서 눈치보지 않고 사용할 도각도각 소리가 나는 키보드 추천해줘", + "게임할 때 반응속도가 빠르고 화려한 RGB 조명이 있는 텐키리스 키보드 찾아줘", + "아이패드랑 같이 들고 다닐 작고 가벼운 블루투스 키보드 필요해" + ]; + + const handleSubmit = () => { + if(!query) return; + setLoading(true); + setTimeout(() => { + setLoading(false); + setView('results'); + }, 1500); + }; + + return ( +
+
+

나만의 키보드 찾기

+

어떤 키보드를 찾으시나요? 자유롭게 말해주세요.

+
+ + {/* 채팅창 섹션 */} +
+ +
+ +
+
+ + {/* 템플릿 제공 섹션 */} +
+

이런 식으로 질문해 보세요:

+
+ {templates.map((txt, idx) => ( + + ))} +
+
+ + {/* 단계별 선택 작게 배치 */} +
+

질문에 답하며 하나씩 찾고 싶다면?

+ +
+
+ ); + }; + + // --- STEP BY STEP VIEW --- + const StepByStepView = () => { + const [step, setStep] = useState(0); + const [answers, setAnswers] = useState({}); + const [minBudget, setMinBudget] = useState(0); + const [maxBudget, setMaxBudget] = useState(1000000); + + const currentQ = questions[step]; + const isLastStep = step === questions.length - 1; + + const handleSelect = (option) => { + setAnswers({ ...answers, [currentQ.id]: option }); + if (!isLastStep) { + setTimeout(() => setStep(step + 1), 200); + } + }; + + const handleComplete = () => { + setLoading(true); + setTimeout(() => { + setLoading(false); + setView('results'); + }, 1500); + }; + + return ( +
+ + + {/* Progress Bar */} +
+
+
+ +
+

{currentQ.title}

+ +
+ {currentQ.type === 'range' ? ( +
+
+
+ 최소 금액 + {minBudget.toLocaleString()}원 +
+ ~ +
+ 최대 금액 + + {maxBudget >= 1000000 ? '1,000,000원+' : `${maxBudget.toLocaleString()}원`} + +
+
+ +
+ {/* Background Track */} +
+ + {/* Active Track */} +
+ + {/* Min Slider */} + { + const val = Number(e.target.value); + setMinBudget(Math.min(val, maxBudget - 10000)); + }} + className="absolute w-full left-0 right-0 appearance-none bg-transparent pointer-events-none z-20 [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:w-6 [&::-webkit-slider-thumb]:h-6 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-blue-600 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:cursor-pointer" + /> + {/* Max Slider */} + { + const val = Number(e.target.value); + setMaxBudget(Math.max(val, minBudget + 10000)); + }} + className="absolute w-full left-0 right-0 appearance-none bg-transparent pointer-events-none z-30 [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:w-6 [&::-webkit-slider-thumb]:h-6 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-blue-600 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:cursor-pointer" + /> +
+
+ 0원 + 100만원+ +
+
+ ) : ( + currentQ.options.map((opt, idx) => { + const isSelected = answers[currentQ.id] === opt; + return ( + + ) + }) + )} +
+ +
+ + + {(currentQ.type === 'range' || isLastStep) && ( + + )} +
+
+
+ ); + }; + + // --- RESULT VIEW --- + const ResultView = () => { + const [activeFilter, setActiveFilter] = useState('전체'); + const [sortOrder, setSortOrder] = useState('default'); + const [copiedId, setCopiedId] = useState(null); + + // 동적 필터 옵션 생성 (제조사 + 모든 태그 중복 제거) + const generateFilters = () => { + const allTags = new Set(); + mockKeyboards.forEach(k => { + allTags.add(k.brand); + k.tags.forEach(t => allTags.add(t)); + }); + return ['전체', ...Array.from(allTags)]; + }; + const filterOptions = generateFilters(); + + // 가격 문자열 -> 숫자 변환 함수 + const getPriceNumber = (priceStr) => parseInt(priceStr.replace(/[^0-9]/g, ''), 10); + + // 1. 필터링 + let processedKeyboards = activeFilter === '전체' + ? [...mockKeyboards] + : mockKeyboards.filter(k => k.tags.includes(activeFilter) || k.brand === activeFilter); + + // 2. 정렬 + if (sortOrder === 'priceAsc') { + processedKeyboards.sort((a, b) => getPriceNumber(a.price) - getPriceNumber(b.price)); + } else if (sortOrder === 'priceDesc') { + processedKeyboards.sort((a, b) => getPriceNumber(b.price) - getPriceNumber(a.price)); + } + + // 클립보드 복사 함수 + const handleCopy = (text, id) => { + const textArea = document.createElement("textarea"); + textArea.value = text; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand('copy'); + setCopiedId(id); + setTimeout(() => setCopiedId(null), 2000); // 2초 후 초기화 + } catch (err) { + console.error('복사 실패', err); + } + document.body.removeChild(textArea); + }; + + return ( +
+
+ +

[{processedKeyboards.length}개의 제품 찾음]

+
+ +
+
+
+

+ 총 {processedKeyboards.length}개의 상품을 찾았어요 +

+

입력하신 조건에 가장 잘 맞는 추천 목록입니다.

+
+ + {/* 정렬 드롭다운 */} +
+ + +
+
+ + {/* 필터 영역 */} +
+
+ +
+ {filterOptions.map(f => ( + + ))} +
+ + {/* 제품 리스트 */} +
+ {processedKeyboards.length === 0 ? ( +
+ 해당 조건에 맞는 제품이 없습니다. +
+ ) : ( + processedKeyboards.map((item) => ( +
+ +
+ +
+ +
+
+

{item.name}

+ +
+

+ {item.description} +

+ +
+ + {item.brand} + + {item.tags.map((tag, idx) => ( + + #{tag} + + ))} +
+ +
+ {item.price} +
+
+ +
+ )) + )} +
+ +
+ +
+
+
+ ); + }; + + return ( +
+ {loading && ( +
+
+ +
+

취향을 분석하고 있어요...

+
+ )} + + {view === 'home' && } + {view === 'step' && } + {view === 'results' && } +
+ ); +} \ No newline at end of file diff --git a/impl/output/keyboards.csv b/impl/output/keyboards.csv new file mode 100644 index 00000000..bba29ab4 --- /dev/null +++ b/impl/output/keyboards.csv @@ -0,0 +1,101 @@ +product_name,brand,price,image_url,switch_type,connection,layout,key_force,weight_g,wireless_type,engraving,backlight +AULA F108 PRO 유무선 기계식 치즈 화이트 한글,AULA,99000,https://img.danuri.io/catalog-image/476/026/094/a2377078683d4985b1880e8ea1f7a7a4.jpg,기계식,유선+무선,풀배열,43g,1020,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +FL-ESPORTS NX108 유무선 기계식 크림 말차,FL-ESPORTS,77000,https://img.danuri.io/catalog-image/377/122/097/c7cdfec0c9dd483994d08f9b8d4f1a07.jpg,기계식,유선+무선,풀배열,43g,1180,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +AULA F87 Pro 기계식 다크 올리비아 한글,AULA,42800,https://img.danuri.io/catalog-image/976/466/063/0b2065d34e0d4c87b1f55c8111f2ec38.jpg,기계식,유선,텐키리스,43g,916,유선,한/영 정각,RGB 백라이트 +로지텍 KEYS-TO-GO 2 (정품),로지텍,89000,https://img.danuri.io/catalog-image/382/164/062/e4b4351b046d4fd1b46eec78855b5235.jpg,펜타그래프,무선,미니,0g,222,블루투스,한/영 정각,없음 +AULA F87 Pro 유무선 기계식 인디고 블랙 한글,AULA,67000,https://img.danuri.io/catalog-image/527/437/061/389ff5db08d545118e0f487bdd0fae70.jpg,기계식,유선+무선,텐키리스,35g,916,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +AULA F108 PRO 유무선 기계식 오로라 블랙 한글,AULA,89000,https://img.danuri.io/catalog-image/440/026/094/e048dc0fdeb8441194f62db164479dcd.jpg,기계식,유선+무선,풀배열,43g,1020,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 K560 축교환 레인보우 무빙 LED 기계식 블랙,앱코,30300,https://img.danuri.io/catalog-image/190/363/013/e2bb0e5726a0479dad2163b6a77bfce8.jpg,기계식,유선,풀배열,45g,840,유선,정보없음,레인보우 백라이트 +AULA F108 기계식 라이트 그레이,AULA,59000,https://img.danuri.io/catalog-image/050/423/108/849d4b0d6a88421299ec7bd316e78dc2.jpg,기계식,유선,풀배열,43g,1027,유선,한/영 정각,RGB 백라이트 +로지텍 MX Keys S (정품),로지텍,143420,https://img.danuri.io/catalog-image/697/437/020/303bda60ac8242179bd4834dd3a5746a.jpg,펜타그래프,무선,풀배열,0g,810,블루투스,정보없음,단색 백라이트 +한성컴퓨터 TFG Magnetox 2XF 저소음,한성컴퓨터,119000,https://img.danuri.io/catalog-image/051/622/122/0c7628f3d9ff448ba99b54ed36f1f31e.jpg,무접점 자석축,유선,풀배열,35g,1233,유선,한/영 정각,RGB 백라이트 +Razer Huntsman V3 Pro TKL 8K KR,Razer,328560,https://img.danuri.io/catalog-image/388/699/100/444dd1538aa640789efe1f612b65874d.jpg,무접점 광축,유선,텐키리스,40g,932,유선,한/영 정각,RGB 백라이트 +앱코 ACH105 오피스워커 유무선 기계식 앨리스블루,앱코,79000,https://img.danuri.io/catalog-image/760/690/103/d3572b09d95441fd9c842a43994f870b.jpeg,기계식,유선+무선,풀배열,47g,1424,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 K561 교체축 유무선 블루투스 기계식 블랙,앱코,29900,https://img.danuri.io/catalog-image/812/708/036/c644fb6ed51047419cba178591fd3ccc.jpg,기계식,유선+무선,풀배열,45g,867,"전용동글(리시버), 블루투스",한/영 정각,없음 +한성컴퓨터 GK698 OfficeMaster 유무선,한성컴퓨터,35910,https://img.danuri.io/catalog-image/519/752/072/0b4579d061394d40a9789926ea639c61.jpg,펜타그래프,유선+무선,풀배열,0g,768,"전용동글(리시버), 블루투스",한/영 정각,없음 +앱코 MK98 AI 코파일럿,앱코,9900,https://img.danuri.io/catalog-image/278/465/069/9efa48aa105140b988d0ebd1b447657f.jpg,펜타그래프,무선,98키,50g,500,전용동글(리시버),레이저각인 키캡,없음 +AULA S102Pro 유무선,AULA,29800,https://img.danuri.io/catalog-image/247/222/106/77dcaa2d3f6941999776b20d08204273.jpg,멤브레인,유선+무선,풀배열,0g,820,"전용동글(리시버), 블루투스",레이저각인 키캡,RGB 백라이트 +앱코 HACKER K640 축교환 게이밍 기계식 블랙,앱코,42000,https://img.danuri.io/catalog-image/084/709/004/1b79a2c70e9f4d3bac552f83034a2c28.jpg,기계식,유선,풀배열,50g,1050,유선,한/영 정각,레인보우 백라이트 +AULA F87 Pro 기계식 라이트 올리비아 한글,AULA,48000,https://img.danuri.io/catalog-image/021/467/063/4fdde6443e6b45cda77338373fdb677d.jpg,기계식,유선,텐키리스,43g,916,유선,한/영 정각,RGB 백라이트 +앱코 MK108 저소음 멤브레인,앱코,18900,https://img.danuri.io/catalog-image/917/346/038/4ecaad29f6954a60bb59e1aeacf08abd.jpg,멤브레인,유선,풀배열,0g,711,유선,한/영 정각,레인보우 백라이트 +COX CS108 유무선 기계식,COX,62900,https://img.danuri.io/catalog-image/014/978/096/0c8d2aab928e441eb67c89df21acd1f0.jpg,기계식,유선+무선,풀배열,43g,1200,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +AULA F75 MAX 유무선 기계식 올리비아 화이트,AULA,74000,https://img.danuri.io/catalog-image/637/268/090/331151b08c7c4eccb3918e9616fc347b.jpg,기계식,유선+무선,미니,46g,1023,"블루투스, 전용동글(리시버)",한/영 정각,RGB 백라이트 +AULA F99 유무선 기계식 올리비아 화이트 한글,AULA,82000,https://img.danuri.io/catalog-image/972/934/058/e5e8c9cc28044a19aad2de21e3bbdc37.jpg,기계식,유선+무선,99키,34g,1183,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +AULA F87 Pro 유무선 기계식 올리비아 화이트 한글,AULA,67000,https://img.danuri.io/catalog-image/344/826/059/ddd11564553343b680c520305f40e32c.jpg,기계식,유선+무선,텐키리스,40g,916,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 K580 교체축 게이밍 기계식 스카이,앱코,29300,https://img.danuri.io/catalog-image/424/533/068/498e54238a674c77856b6d416498602b.jpg,기계식,유선,풀배열,50g,500,유선,한/영 정각,레인보우 백라이트 +로지텍 ERGO K860 (정품),로지텍,189050,https://img.danuri.io/catalog-image/911/479/013/79da1db6ee514fe4a35209d52cd90c0d.jpg,펜타그래프,무선,풀배열,0g,1160,"전용동글(리시버), 블루투스",정보없음,없음 +앱코 A87K 3모드 퀵스압 유무선 기계식,앱코,59000,https://img.danuri.io/catalog-image/770/850/095/d3b4d2636bfb422da059958e834d146a.jpg,기계식,유선+무선,텐키리스,45g,960,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 AK87 슬림 프로 3모드 로우 프로파일 유무선 기계식,앱코,75000,https://img.danuri.io/catalog-image/418/888/094/20e1ad71e1e74e1a8c2a2738b18aa4d3.jpg,기계식,유선+무선,텐키리스,43g,817,"전용동글(리시버), 블루투스",영문 정각,RGB 백라이트 +MSI FORGE K200 WIRELESS,MSI,25410,https://img.danuri.io/catalog-image/203/206/088/2e5ddc263d8745d3b7619bcabca8bee8.jpg,펜타그래프,무선,풀배열,0g,445,전용동글(리시버),레이저각인 키캡,없음 +주연테크 긱스타 GKG87 유무선 기계식 블랙,주연테크,52110,https://img.danuri.io/catalog-image/848/566/094/26f0494ef17f43f4aebe9f8a12bb8ea0.jpg,기계식,유선+무선,텐키리스,45g,952,"블루투스, 전용동글(리시버)",한/영 정각,RGB 백라이트 +앱코 AN06F TKL PBT 게이밍 기계식,앱코,16900,https://img.danuri.io/catalog-image/130/006/019/06c4316b5301481996789b2338684b37.jpg,기계식,유선,텐키리스,60g,650,유선,한/영 정각,RGB 백라이트 +CORSAIR K70 RGB TKL OPX 광적축 게이밍 기계식키보드,CORSAIR,199000,https://img.danuri.io/catalog-image/421/795/016/8787e71493e547dab11f5dfb732dc806.jpg,무접점 광축,유선,텐키리스,45g,930,유선,한/영 정각,RGB 백라이트 +EVGA Z20 RGB 광축 게이밍키보드한글,EVGA,60450,https://img.danuri.io/catalog-image/578/811/014/a36e791c54744af5a6ed7ca9a128e858.jpg,무접점 광축,유선,풀배열,40g,1130,유선,한/영 정각,RGB 백라이트 +CHERRY XTRFY K37 유무선,CHERRY,48560,https://img.danuri.io/catalog-image/029/618/122/7d7489146e87475a8e6dea458cbb8771.jpeg,멤브레인,유선+무선,98키,0g,750,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +발키리 VK-99 PRO LVBU 유무선 기계식,발키리,98000,https://img.danuri.io/catalog-image/179/978/072/e0aae728d961433ab0c78659cd4d6c05.jpg,기계식,유선+무선,99키,43g,1165,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +FL-ESPORTS OG104 유무선 핫스왑 풀윤활 기계식 그레이 한글,FL-ESPORTS,159000,https://img.danuri.io/catalog-image/029/708/036/90a602fcf20e4c558f1e54208a379263.jpg,기계식,유선+무선,풀배열,45g,1370,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +AULA F99 유무선 기계식 인디고 블랙 한글,AULA,112400,https://img.danuri.io/catalog-image/412/145/060/bb8f91aceb7948f6bd4bdc5c7541ed4e.jpg,기계식,유선+무선,99키,40g,1183,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 LKN84BT 8K 로우 프로파일 슬림 유무선 무접점,앱코,149000,https://img.danuri.io/catalog-image/582/098/078/9dd7a9cea60a46af9ba405010060db71.jpg,무접점,유선+무선,미니,35g,791,"블루투스, 전용동글(리시버)",한/영 정각,없음 +AULA F108 PRO 유무선 기계식 해외구매,AULA,54180,https://img.danuri.io/catalog-image/351/331/076/4482f824c6ea4d7da188f837e2e9eca7.jpg,기계식,유선+무선,풀배열,45g,930,"전용동글(리시버), 블루투스",정보없음,RGB 백라이트 +로지텍 mx keys mini (정품),로지텍,112500,https://img.danuri.io/catalog-image/342/151/016/98a253888dd247c58814d6d03145c1b4.jpg,펜타그래프,무선,미니,0g,506,블루투스,한/영 정각,단색 백라이트 +CORSAIR 갤리온 100 SD 스트림덱 LCD 기계식,CORSAIR,491000,https://img.danuri.io/catalog-image/577/947/104/7e14d17430c04823bae6fe5606f2bd61.jpg,기계식,유선,텐키리스,45g,1390,유선,영문 정각,RGB 백라이트 +한성컴퓨터 TFG Magnetox 2XL 저소음,한성컴퓨터,99610,https://img.danuri.io/catalog-image/077/622/122/92f2dce9b5f8444b83a3dde5351146c5.jpg,무접점 자석축,유선,텐키리스,35g,1042,유선,한/영 정각,RGB 백라이트 +FL-ESPORTS X80 유무선 기계식,FL-ESPORTS,59800,https://img.danuri.io/catalog-image/556/364/096/00e65b1d7a3c4ca6a0e8b75be03baa26.jpg,기계식,유선+무선,텐키리스,40g,910,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +CORSAIR K70 RGB PRO,CORSAIR,199910,https://img.danuri.io/catalog-image/009/388/016/83d3a55d3c38462eb41e2b77cfc0a478.jpg,기계식,유선,풀배열,45g,1150,유선,한/영 정각,RGB 백라이트 +한성컴퓨터 GK868B PRO 동그리 8K 유무선 무접점,한성컴퓨터,125100,https://img.danuri.io/catalog-image/479/607/047/e6c7feae6178443fbbb3342fcc3f785e.jpg,무접점,유선+무선,미니,35g,630,"전용동글(리시버), 블루투스",한/영 정각,없음 +COX CSK88 VIA 실리콘 사운드 뎀퍼 유무선 기계식 그린,COX,35750,https://img.danuri.io/catalog-image/367/426/091/8be61b66b56841cb90ccea8a5b86c89e.jpg,기계식,유선+무선,텐키리스,40g,957,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 AF108PRO ABKO x SOAI 콜라보 3모드 기계식 코랄블루,앱코,85510,https://img.danuri.io/catalog-image/511/953/101/54365924b6fd44178394b293df7b9ea7.jpeg,기계식,유선+무선,풀배열,46g,1029,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +COX COS500 무선,COX,28000,https://img.danuri.io/catalog-image/551/319/077/610c59225d1d44ada752a5822251cae2.jpg,멤브레인,무선,텐키리스,0g,286,"전용동글(리시버), 블루투스",한/영 정각,없음 +앱코 K561 교체축 유무선 블루투스 기계식 화이트,앱코,29900,https://img.danuri.io/catalog-image/887/708/036/856053bfa47e4092a955cd066c7f15cd.jpg,기계식,유선+무선,풀배열,45g,867,"전용동글(리시버), 블루투스",한/영 정각,없음 +앱코 AF108PRO ABKO x SOAI 콜라보 3모드 기계식 와인그레이,앱코,87000,https://img.danuri.io/catalog-image/320/633/122/f5c875b3e188473fa32dc82a9d62ddd1.jpeg,기계식,유선+무선,풀배열,46g,1029,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 K562 교체축 3모드 계산기 유무선 기계식,앱코,26900,https://img.danuri.io/catalog-image/845/642/090/87c46bb45b694003935a6ffe8449a79e.jpg,기계식,유선+무선,96키,50g,862,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +COX CQK81 VIA 엣지 LED & 노브 3MODE 유무선 기계식,COX,33000,https://img.danuri.io/catalog-image/840/437/090/1d3cc05026b643699b08c5aaa8bb9aa0.jpg,기계식,유선+무선,미니,45g,800,"전용동글(리시버), 블루투스",영문 정각,RGB 백라이트 +한성컴퓨터 GK787SE OfficeMaster 8K 기계식 뽀송,한성컴퓨터,77000,https://img.danuri.io/catalog-image/633/269/030/73e59c5e54864f088a513cc898151a0b.jpg,기계식,유선,풀배열,38g,1500,유선,한/영 정각,없음 +COX COS600 무선,COX,26900,https://img.danuri.io/catalog-image/033/886/073/63a5d0bea58241e99c3135db1abeea06.jpg,펜타그래프,무선,풀배열,60g,615,"전용동글(리시버), 블루투스",레이저각인 키캡,없음 +CORSAIR 뱅가드 96 MLX 게이밍 무선 기계식,CORSAIR,257470,https://img.danuri.io/catalog-image/871/483/109/b6fd5765220648d980fe576f914e027a.jpg,기계식,유선+무선,96키,45g,1167,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +CORSAIR K70 MAX RGB MGX 게이밍 기계식,CORSAIR,279000,https://img.danuri.io/catalog-image/863/546/027/060ac8bd44f94904813ddaba9af064fd.jpg,무접점 자석축,유선,풀배열,45g,1390,유선,한/영 정각,RGB 백라이트 +앱코 AM61 자석축 래피드 트리거,앱코,38400,https://img.danuri.io/catalog-image/238/235/060/9c056e44f8bb4831856463e4997c0391.jpg,무접점 자석축,유선,미니,50g,550,유선,한/영 정각,RGB 백라이트 +앱코 NTP84,앱코,32550,https://img.danuri.io/catalog-image/015/692/108/a801688a9e54479fadd08651273a8cc1.jpg,멤브레인,유선+무선,미니,45g,540,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +Teamwolf RAVEN 68 HE,Teamwolf,81000,https://img.danuri.io/catalog-image/868/341/074/1658353830774a0dbc43f05a6f381f48.jpg,무접점 자석축,유선,미니,30g,680,유선,한/영 정각,RGB 백라이트 +CHERRY XTRFY MX 3.1 RGB MX2A 블랙,CHERRY,149460,https://img.danuri.io/catalog-image/739/220/045/03f6632f313644868cb7d396c0f04c3c.jpg,기계식,유선,풀배열,45g,1120,유선,레이저각인 키캡,RGB 백라이트 +AULA F65 유무선 기계식 스카이 블랙,AULA,67000,https://img.danuri.io/catalog-image/126/325/073/a821a4851637435a82fedecac35d4a27.jpg,기계식,유선+무선,미니,43g,804,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +로지텍 G413 SE (정품),로지텍,75050,https://img.danuri.io/catalog-image/631/396/016/9e13839e84f04030ad58047dfd467b7e.jpg,기계식,유선,풀배열,50g,780,유선,정보없음,단색 백라이트 +COX CM105KDW 3모드 조약돌,COX,27900,https://img.danuri.io/catalog-image/848/721/101/ee9695fb4a2046f4a94a71e01a63c190.jpg,멤브레인,유선+무선,풀배열,45g,810,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +COX CMK87R 자석축 래피드 트리거,COX,59000,https://img.danuri.io/catalog-image/551/505/071/df8b69ded87e4d27a0c90ba7bda804e1.jpg,무접점 자석축,유선,텐키리스,30g,935,유선,한/영 정각,RGB 백라이트 +앱코 AK108 3모드 OTEMU 특주축 유무선 기계식 그레이,앱코,69000,https://img.danuri.io/catalog-image/446/015/093/2e1f2b855a21429e996de4e5d1f63f71.jpg,기계식,유선+무선,풀배열,40g,1117,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +FL-ESPORTS NX108 유무선 기계식 크림 핑크,FL-ESPORTS,77000,https://img.danuri.io/catalog-image/413/122/097/fa3fd9f16ef240b682f4252ea14de8a6.jpg,기계식,유선+무선,풀배열,43g,1180,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 AKN10BT 반알만한 무접점,앱코,250000,https://img.danuri.io/catalog-image/004/191/092/317a3c44705c4c74b172b4defd9098f8.jpg,무접점,유선+무선,풀배열,45g,1200,"전용동글(리시버), 블루투스",한/영 정각,없음 +CORSAIR K100 AIR WIRELESS RGB Ultra Low Profile 게이밍 기계식키보드,CORSAIR,378880,https://img.danuri.io/catalog-image/703/932/017/f60535d22068499098328a922f3324e1.jpg,기계식,유선+무선,풀배열,65g,780,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 AO98 레트로 타임리스 3모드 기계식,앱코,59000,https://img.danuri.io/catalog-image/484/702/097/554cf3858e17411dbda4acf29ec9ffc8.jpg,기계식,유선+무선,98키,45g,1090,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +COX CK01 TKL PBT SL 기계식키보드,COX,29900,https://img.danuri.io/catalog-image/348/185/015/084823b166fc4190816483d972831a89.jpg,기계식,유선,텐키리스,60g,700,유선,한/영 정각,없음 +AULA F65 유무선 기계식 올리비아 화이트,AULA,67000,https://img.danuri.io/catalog-image/682/696/073/b0d4d75acc0c4c42a62e6c2d8f7a5620.jpg,기계식,유선+무선,미니,43g,804,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +CHERRY XTRFY MX 8.3 TKL 8K 유무선 기계식,CHERRY,371070,https://img.danuri.io/catalog-image/100/699/100/8b35eb77f81647e6bda88649ade94b57.jpg,기계식,유선+무선,텐키리스,45g,1250,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 AN02 블랙 RGB BAR 축교환 기계식키보드,앱코,34900,https://img.danuri.io/catalog-image/640/650/014/3f849288b9264d679ffeac758f14736a.jpg,기계식,유선,풀배열,50g,830,유선,한/영 정각,레인보우 백라이트 +앱코 K640T SLIM 축교환 무빙LED 텐키리스 기계식 화이트,앱코,28900,https://img.danuri.io/catalog-image/614/282/015/0841a71c5c8a4b349fccb257cf53d714.jpg,기계식,유선,텐키리스,45g,572,유선,정보없음,단색 백라이트 +앱코 AS104 오피스네비게이터 유무선 기계식 블루레몬,앱코,99000,https://img.danuri.io/catalog-image/615/903/071/01c2c2020c844e66a8bf376813fa5ae0.jpg,기계식,유선+무선,풀배열,45g,1163,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 AK87 3모드 OTEMU 특주축 유무선 기계식 다크 그라데이션,앱코,62000,https://img.danuri.io/catalog-image/639/828/074/a693e440bfee49f2b3a49174707c4c80.jpg,기계식,유선+무선,텐키리스,40g,1024,"전용동글(리시버), 블루투스",정보없음,RGB 백라이트 +앱코 K669 카일 광축 게이밍,앱코,66130,https://img.danuri.io/catalog-image/820/690/019/71eac0988c0f4b1ca1abe027312fcf30.jpg,무접점 광축,유선,풀배열,55g,1260,유선,한/영 정각,레인보우 백라이트 +CHERRY XTRFY K5 PRO TMR,CHERRY,149000,https://img.danuri.io/catalog-image/232/007/102/9d7f2dcba0ad4b188902bc1cdc89452c.jpg,무접점 자석축,유선,미니,38g,568,유선,레이저각인 키캡,RGB 백라이트 +COX CFL87+21 옴니키(원솔루션) 3모드 유무선 기계식,COX,55000,https://img.danuri.io/catalog-image/255/969/092/6afb9b6744c94f6787db7641bd1aaba1.jpg,기계식,유선+무선,텐키리스,45g,944,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +CORSAIR 뱅가드 96 MLX 게이밍 기계식,CORSAIR,212000,https://img.danuri.io/catalog-image/298/245/098/0ff89fcecf484ba78d0cf383f0aed5f6.jpg,기계식,유선,96키,40g,994,유선,한/영 정각,RGB 백라이트 +앱코 K570 축교환 레인보우 무빙 LED 기계식키보드,앱코,27900,https://img.danuri.io/catalog-image/087/471/014/ceb091f4070c4bb791bda651774b4673.jpg,기계식,유선,풀배열,45g,744,유선,한/영 정각,레인보우 백라이트 +앱코 K660S V2 카일 광축 V2 게이밍 화이트,앱코,66000,https://img.danuri.io/catalog-image/583/991/075/e5c3625766b54c5a90fa4f71892ed18c.jpg,무접점 광축,유선,풀배열,40g,1240,유선,한/영 정각,레인보우 백라이트 +앱코 TOS300 매크로덱,앱코,29900,https://img.danuri.io/catalog-image/701/080/094/e61c7dfa2bbf4e7e8b8bcd7748f405d4.jpg,펜타그래프,유선,99키,0g,776,유선,한/영 정각,단색 백라이트 +darkFlash DK108,darkFlash,18310,https://img.danuri.io/catalog-image/443/964/092/d4423995c7da46eca2236e48818cedb1.jpg,멤브레인,유선,풀배열,0g,810,유선,한/영 정각,없음 +COX CK88 유무선 기계식 블랙로얄,COX,81340,https://img.danuri.io/catalog-image/328/810/094/ffe997ffd95845838c214bafa63d8822.jpg,기계식,유선+무선,텐키리스,45g,1410,"전용동글(리시버), 블루투스",영문 정각,레인보우 백라이트 +앱코 AK108 3모드 OTEMU 특주축 유무선 기계식 올리비아,앱코,69000,https://img.danuri.io/catalog-image/518/015/093/49f4fcbaf0214a41914e27e0d2123fd9.jpg,기계식,유선+무선,풀배열,40g,1117,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 K660S V2 카일 광축 V2 게이밍 블랙,앱코,66000,https://img.danuri.io/catalog-image/689/990/075/ac910d420aca49cba8db8e9de1148ff9.jpg,무접점 광축,유선,풀배열,40g,1240,유선,한/영 정각,레인보우 백라이트 +AULA F75 MAX 유무선 기계식 스카이 블랙,AULA,79000,https://img.danuri.io/catalog-image/462/556/079/1f6edd1dae3747b0b90873b701e0ba79.jpg,기계식,유선+무선,미니,40g,1023,"블루투스, 전용동글(리시버)",한/영 정각,RGB 백라이트 +주연테크 긱스타 GKG87 유무선 기계식 화이트,주연테크,52110,https://img.danuri.io/catalog-image/818/566/094/748aa62883c24164b04f9992b25cd951.jpg,기계식,유선+무선,텐키리스,42g,952,"블루투스, 전용동글(리시버)",한/영 정각,RGB 백라이트 +앱코 ACH105 오피스워커 유무선 기계식 라벤더,앱코,79000,https://img.danuri.io/catalog-image/739/690/103/69615bf8686e4bdeae99ecd5a9231737.jpeg,기계식,유선+무선,풀배열,47g,1424,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +앱코 AN02 핑크 RGB BAR 축교환 기계식키보드,앱코,39900,https://img.danuri.io/catalog-image/802/650/014/de58628b5ac449459a15223b2f18aa7a.jpg,기계식,유선,풀배열,50g,830,유선,한/영 정각,단색 백라이트 +ATK RS6 Air,ATK,89000,https://img.danuri.io/catalog-image/179/663/122/069946b8135e42c8bff3760493ec1ffe.jpeg,무접점 자석축,유선,미니,35g,710,유선,영문 정각,RGB 백라이트 +앱코 MK108W 무선,앱코,29000,https://img.danuri.io/catalog-image/670/223/072/456a150d78e74a879b11c416cf70ecdd.jpg,멤브레인,무선,풀배열,0g,715,"전용동글(리시버), 블루투스",레이저각인 키캡,레인보우 백라이트 +COX CK01 Navy 교체축 사이드 RGB 게이밍 기계식키보드,COX,27900,https://img.danuri.io/catalog-image/044/224/013/0774175657bb482e824e1bcaf5ee79d6.jpg,기계식,유선,풀배열,45g,828,유선,정보없음,단색 백라이트 +앱코 K480 교체축 게이밍 기계식 젠틀맨,앱코,27900,https://img.danuri.io/catalog-image/518/706/077/4c8184930f5d4a91b0740c824237cc7f.jpg,기계식,유선,텐키리스,45g,604,유선,한/영 정각,RGB 백라이트 +COX CK01 TKL,COX,29900,https://img.danuri.io/catalog-image/436/440/014/63fb8085a1a4429eb51b5021b30bc8a3.jpg,기계식,유선,텐키리스,45g,700,유선,한/영 정각,단색 백라이트 +앱코 AK10 오피스 마스터 슬림 무선,앱코,15900,https://img.danuri.io/catalog-image/034/111/079/24fe69d0b70d4891a744e73a0e6441fc.jpg,멤브레인,무선,풀배열,0g,495,"전용동글(리시버), 블루투스",한/영 정각,없음 +앱코 AK87 3모드 OTEMU 특주축 유무선 기계식 그레이,앱코,49000,https://img.danuri.io/catalog-image/564/819/074/c7f17f48810b49fb825f97853f3c2dc6.jpg,기계식,유선+무선,텐키리스,40g,1024,"전용동글(리시버), 블루투스",한/영 정각,RGB 백라이트 +Createkeebs LUMINKEY Magger 68 HE 프로페셔널,Createkeebs,179000,https://img.danuri.io/catalog-image/805/004/072/62ccb746f7054627a113b6183b9ce761.jpg,무접점 자석축,유선,미니,36g,1200,유선,영문 정각,RGB 백라이트 +CORSAIR K65 RGB PLUS 유무선 애플 에디션,CORSAIR,220570,https://img.danuri.io/catalog-image/445/542/101/00f4876f0aa44b5b87c17faacef7f682.jpg,기계식,유선+무선,미니,45g,922,"전용동글(리시버), 블루투스",영문 정각,RGB 백라이트 +앱코 LKN99 BT 로우 프로파일 슬림 무접점,앱코,159000,https://img.danuri.io/catalog-image/142/519/094/20e3e7cb6a924d6cab75c3743c8d397a.jpg,무접점,유선+무선,99키,50g,1443,"블루투스, 전용동글(리시버)",한/영 정각,없음 diff --git a/impl/output/keyboards.json b/impl/output/keyboards.json new file mode 100644 index 00000000..95223d1d --- /dev/null +++ b/impl/output/keyboards.json @@ -0,0 +1,1402 @@ +[ + { + "product_name": "AULA F108 PRO 유무선 기계식 치즈 화이트 한글", + "brand": "AULA", + "price": 99000, + "image_url": "https://img.danuri.io/catalog-image/476/026/094/a2377078683d4985b1880e8ea1f7a7a4.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "43g", + "weight_g": 1020, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "FL-ESPORTS NX108 유무선 기계식 크림 말차", + "brand": "FL-ESPORTS", + "price": 77000, + "image_url": "https://img.danuri.io/catalog-image/377/122/097/c7cdfec0c9dd483994d08f9b8d4f1a07.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "43g", + "weight_g": 1180, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "AULA F87 Pro 기계식 다크 올리비아 한글", + "brand": "AULA", + "price": 42800, + "image_url": "https://img.danuri.io/catalog-image/976/466/063/0b2065d34e0d4c87b1f55c8111f2ec38.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "텐키리스", + "key_force": "43g", + "weight_g": 916, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "로지텍 KEYS-TO-GO 2 (정품)", + "brand": "로지텍", + "price": 89000, + "image_url": "https://img.danuri.io/catalog-image/382/164/062/e4b4351b046d4fd1b46eec78855b5235.jpg", + "switch_type": "펜타그래프", + "connection": "무선", + "layout": "미니", + "key_force": "0g", + "weight_g": 222, + "wireless_type": "블루투스", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "AULA F87 Pro 유무선 기계식 인디고 블랙 한글", + "brand": "AULA", + "price": 67000, + "image_url": "https://img.danuri.io/catalog-image/527/437/061/389ff5db08d545118e0f487bdd0fae70.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "35g", + "weight_g": 916, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "AULA F108 PRO 유무선 기계식 오로라 블랙 한글", + "brand": "AULA", + "price": 89000, + "image_url": "https://img.danuri.io/catalog-image/440/026/094/e048dc0fdeb8441194f62db164479dcd.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "43g", + "weight_g": 1020, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 K560 축교환 레인보우 무빙 LED 기계식 블랙", + "brand": "앱코", + "price": 30300, + "image_url": "https://img.danuri.io/catalog-image/190/363/013/e2bb0e5726a0479dad2163b6a77bfce8.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 840, + "wireless_type": "유선", + "engraving": "정보없음", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "AULA F108 기계식 라이트 그레이", + "brand": "AULA", + "price": 59000, + "image_url": "https://img.danuri.io/catalog-image/050/423/108/849d4b0d6a88421299ec7bd316e78dc2.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "43g", + "weight_g": 1027, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "로지텍 MX Keys S (정품)", + "brand": "로지텍", + "price": 143420, + "image_url": "https://img.danuri.io/catalog-image/697/437/020/303bda60ac8242179bd4834dd3a5746a.jpg", + "switch_type": "펜타그래프", + "connection": "무선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 810, + "wireless_type": "블루투스", + "engraving": "정보없음", + "backlight": "단색 백라이트" + }, + { + "product_name": "한성컴퓨터 TFG Magnetox 2XF 저소음", + "brand": "한성컴퓨터", + "price": 119000, + "image_url": "https://img.danuri.io/catalog-image/051/622/122/0c7628f3d9ff448ba99b54ed36f1f31e.jpg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "풀배열", + "key_force": "35g", + "weight_g": 1233, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "Razer Huntsman V3 Pro TKL 8K KR", + "brand": "Razer", + "price": 328560, + "image_url": "https://img.danuri.io/catalog-image/388/699/100/444dd1538aa640789efe1f612b65874d.jpg", + "switch_type": "무접점 광축", + "connection": "유선", + "layout": "텐키리스", + "key_force": "40g", + "weight_g": 932, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 ACH105 오피스워커 유무선 기계식 앨리스블루", + "brand": "앱코", + "price": 79000, + "image_url": "https://img.danuri.io/catalog-image/760/690/103/d3572b09d95441fd9c842a43994f870b.jpeg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "47g", + "weight_g": 1424, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 K561 교체축 유무선 블루투스 기계식 블랙", + "brand": "앱코", + "price": 29900, + "image_url": "https://img.danuri.io/catalog-image/812/708/036/c644fb6ed51047419cba178591fd3ccc.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 867, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "한성컴퓨터 GK698 OfficeMaster 유무선", + "brand": "한성컴퓨터", + "price": 35910, + "image_url": "https://img.danuri.io/catalog-image/519/752/072/0b4579d061394d40a9789926ea639c61.jpg", + "switch_type": "펜타그래프", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 768, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "앱코 MK98 AI 코파일럿", + "brand": "앱코", + "price": 9900, + "image_url": "https://img.danuri.io/catalog-image/278/465/069/9efa48aa105140b988d0ebd1b447657f.jpg", + "switch_type": "펜타그래프", + "connection": "무선", + "layout": "98키", + "key_force": "50g", + "weight_g": 500, + "wireless_type": "전용동글(리시버)", + "engraving": "레이저각인 키캡", + "backlight": "없음" + }, + { + "product_name": "AULA S102Pro 유무선", + "brand": "AULA", + "price": 29800, + "image_url": "https://img.danuri.io/catalog-image/247/222/106/77dcaa2d3f6941999776b20d08204273.jpg", + "switch_type": "멤브레인", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 820, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "레이저각인 키캡", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 HACKER K640 축교환 게이밍 기계식 블랙", + "brand": "앱코", + "price": 42000, + "image_url": "https://img.danuri.io/catalog-image/084/709/004/1b79a2c70e9f4d3bac552f83034a2c28.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "50g", + "weight_g": 1050, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "AULA F87 Pro 기계식 라이트 올리비아 한글", + "brand": "AULA", + "price": 48000, + "image_url": "https://img.danuri.io/catalog-image/021/467/063/4fdde6443e6b45cda77338373fdb677d.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "텐키리스", + "key_force": "43g", + "weight_g": 916, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 MK108 저소음 멤브레인", + "brand": "앱코", + "price": 18900, + "image_url": "https://img.danuri.io/catalog-image/917/346/038/4ecaad29f6954a60bb59e1aeacf08abd.jpg", + "switch_type": "멤브레인", + "connection": "유선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 711, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "COX CS108 유무선 기계식", + "brand": "COX", + "price": 62900, + "image_url": "https://img.danuri.io/catalog-image/014/978/096/0c8d2aab928e441eb67c89df21acd1f0.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "43g", + "weight_g": 1200, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "AULA F75 MAX 유무선 기계식 올리비아 화이트", + "brand": "AULA", + "price": 74000, + "image_url": "https://img.danuri.io/catalog-image/637/268/090/331151b08c7c4eccb3918e9616fc347b.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "미니", + "key_force": "46g", + "weight_g": 1023, + "wireless_type": "블루투스, 전용동글(리시버)", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "AULA F99 유무선 기계식 올리비아 화이트 한글", + "brand": "AULA", + "price": 82000, + "image_url": "https://img.danuri.io/catalog-image/972/934/058/e5e8c9cc28044a19aad2de21e3bbdc37.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "99키", + "key_force": "34g", + "weight_g": 1183, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "AULA F87 Pro 유무선 기계식 올리비아 화이트 한글", + "brand": "AULA", + "price": 67000, + "image_url": "https://img.danuri.io/catalog-image/344/826/059/ddd11564553343b680c520305f40e32c.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "40g", + "weight_g": 916, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 K580 교체축 게이밍 기계식 스카이", + "brand": "앱코", + "price": 29300, + "image_url": "https://img.danuri.io/catalog-image/424/533/068/498e54238a674c77856b6d416498602b.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "50g", + "weight_g": 500, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "로지텍 ERGO K860 (정품)", + "brand": "로지텍", + "price": 189050, + "image_url": "https://img.danuri.io/catalog-image/911/479/013/79da1db6ee514fe4a35209d52cd90c0d.jpg", + "switch_type": "펜타그래프", + "connection": "무선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 1160, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "정보없음", + "backlight": "없음" + }, + { + "product_name": "앱코 A87K 3모드 퀵스압 유무선 기계식", + "brand": "앱코", + "price": 59000, + "image_url": "https://img.danuri.io/catalog-image/770/850/095/d3b4d2636bfb422da059958e834d146a.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 960, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AK87 슬림 프로 3모드 로우 프로파일 유무선 기계식", + "brand": "앱코", + "price": 75000, + "image_url": "https://img.danuri.io/catalog-image/418/888/094/20e1ad71e1e74e1a8c2a2738b18aa4d3.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "43g", + "weight_g": 817, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "영문 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "MSI FORGE K200 WIRELESS", + "brand": "MSI", + "price": 25410, + "image_url": "https://img.danuri.io/catalog-image/203/206/088/2e5ddc263d8745d3b7619bcabca8bee8.jpg", + "switch_type": "펜타그래프", + "connection": "무선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 445, + "wireless_type": "전용동글(리시버)", + "engraving": "레이저각인 키캡", + "backlight": "없음" + }, + { + "product_name": "주연테크 긱스타 GKG87 유무선 기계식 블랙", + "brand": "주연테크", + "price": 52110, + "image_url": "https://img.danuri.io/catalog-image/848/566/094/26f0494ef17f43f4aebe9f8a12bb8ea0.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 952, + "wireless_type": "블루투스, 전용동글(리시버)", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AN06F TKL PBT 게이밍 기계식", + "brand": "앱코", + "price": 16900, + "image_url": "https://img.danuri.io/catalog-image/130/006/019/06c4316b5301481996789b2338684b37.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "텐키리스", + "key_force": "60g", + "weight_g": 650, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "CORSAIR K70 RGB TKL OPX 광적축 게이밍 기계식키보드", + "brand": "CORSAIR", + "price": 199000, + "image_url": "https://img.danuri.io/catalog-image/421/795/016/8787e71493e547dab11f5dfb732dc806.jpg", + "switch_type": "무접점 광축", + "connection": "유선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 930, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "EVGA Z20 RGB 광축 게이밍키보드한글", + "brand": "EVGA", + "price": 60450, + "image_url": "https://img.danuri.io/catalog-image/578/811/014/a36e791c54744af5a6ed7ca9a128e858.jpg", + "switch_type": "무접점 광축", + "connection": "유선", + "layout": "풀배열", + "key_force": "40g", + "weight_g": 1130, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "CHERRY XTRFY K37 유무선", + "brand": "CHERRY", + "price": 48560, + "image_url": "https://img.danuri.io/catalog-image/029/618/122/7d7489146e87475a8e6dea458cbb8771.jpeg", + "switch_type": "멤브레인", + "connection": "유선+무선", + "layout": "98키", + "key_force": "0g", + "weight_g": 750, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "발키리 VK-99 PRO LVBU 유무선 기계식", + "brand": "발키리", + "price": 98000, + "image_url": "https://img.danuri.io/catalog-image/179/978/072/e0aae728d961433ab0c78659cd4d6c05.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "99키", + "key_force": "43g", + "weight_g": 1165, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "FL-ESPORTS OG104 유무선 핫스왑 풀윤활 기계식 그레이 한글", + "brand": "FL-ESPORTS", + "price": 159000, + "image_url": "https://img.danuri.io/catalog-image/029/708/036/90a602fcf20e4c558f1e54208a379263.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 1370, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "AULA F99 유무선 기계식 인디고 블랙 한글", + "brand": "AULA", + "price": 112400, + "image_url": "https://img.danuri.io/catalog-image/412/145/060/bb8f91aceb7948f6bd4bdc5c7541ed4e.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "99키", + "key_force": "40g", + "weight_g": 1183, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 LKN84BT 8K 로우 프로파일 슬림 유무선 무접점", + "brand": "앱코", + "price": 149000, + "image_url": "https://img.danuri.io/catalog-image/582/098/078/9dd7a9cea60a46af9ba405010060db71.jpg", + "switch_type": "무접점", + "connection": "유선+무선", + "layout": "미니", + "key_force": "35g", + "weight_g": 791, + "wireless_type": "블루투스, 전용동글(리시버)", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "AULA F108 PRO 유무선 기계식 해외구매", + "brand": "AULA", + "price": 54180, + "image_url": "https://img.danuri.io/catalog-image/351/331/076/4482f824c6ea4d7da188f837e2e9eca7.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 930, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "정보없음", + "backlight": "RGB 백라이트" + }, + { + "product_name": "로지텍 mx keys mini (정품)", + "brand": "로지텍", + "price": 112500, + "image_url": "https://img.danuri.io/catalog-image/342/151/016/98a253888dd247c58814d6d03145c1b4.jpg", + "switch_type": "펜타그래프", + "connection": "무선", + "layout": "미니", + "key_force": "0g", + "weight_g": 506, + "wireless_type": "블루투스", + "engraving": "한/영 정각", + "backlight": "단색 백라이트" + }, + { + "product_name": "CORSAIR 갤리온 100 SD 스트림덱 LCD 기계식", + "brand": "CORSAIR", + "price": 491000, + "image_url": "https://img.danuri.io/catalog-image/577/947/104/7e14d17430c04823bae6fe5606f2bd61.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 1390, + "wireless_type": "유선", + "engraving": "영문 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "한성컴퓨터 TFG Magnetox 2XL 저소음", + "brand": "한성컴퓨터", + "price": 99610, + "image_url": "https://img.danuri.io/catalog-image/077/622/122/92f2dce9b5f8444b83a3dde5351146c5.jpg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "텐키리스", + "key_force": "35g", + "weight_g": 1042, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "FL-ESPORTS X80 유무선 기계식", + "brand": "FL-ESPORTS", + "price": 59800, + "image_url": "https://img.danuri.io/catalog-image/556/364/096/00e65b1d7a3c4ca6a0e8b75be03baa26.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "40g", + "weight_g": 910, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "CORSAIR K70 RGB PRO", + "brand": "CORSAIR", + "price": 199910, + "image_url": "https://img.danuri.io/catalog-image/009/388/016/83d3a55d3c38462eb41e2b77cfc0a478.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 1150, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "한성컴퓨터 GK868B PRO 동그리 8K 유무선 무접점", + "brand": "한성컴퓨터", + "price": 125100, + "image_url": "https://img.danuri.io/catalog-image/479/607/047/e6c7feae6178443fbbb3342fcc3f785e.jpg", + "switch_type": "무접점", + "connection": "유선+무선", + "layout": "미니", + "key_force": "35g", + "weight_g": 630, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "COX CSK88 VIA 실리콘 사운드 뎀퍼 유무선 기계식 그린", + "brand": "COX", + "price": 35750, + "image_url": "https://img.danuri.io/catalog-image/367/426/091/8be61b66b56841cb90ccea8a5b86c89e.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "40g", + "weight_g": 957, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AF108PRO ABKO x SOAI 콜라보 3모드 기계식 코랄블루", + "brand": "앱코", + "price": 85510, + "image_url": "https://img.danuri.io/catalog-image/511/953/101/54365924b6fd44178394b293df7b9ea7.jpeg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "46g", + "weight_g": 1029, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "COX COS500 무선", + "brand": "COX", + "price": 28000, + "image_url": "https://img.danuri.io/catalog-image/551/319/077/610c59225d1d44ada752a5822251cae2.jpg", + "switch_type": "멤브레인", + "connection": "무선", + "layout": "텐키리스", + "key_force": "0g", + "weight_g": 286, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "앱코 K561 교체축 유무선 블루투스 기계식 화이트", + "brand": "앱코", + "price": 29900, + "image_url": "https://img.danuri.io/catalog-image/887/708/036/856053bfa47e4092a955cd066c7f15cd.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 867, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "앱코 AF108PRO ABKO x SOAI 콜라보 3모드 기계식 와인그레이", + "brand": "앱코", + "price": 87000, + "image_url": "https://img.danuri.io/catalog-image/320/633/122/f5c875b3e188473fa32dc82a9d62ddd1.jpeg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "46g", + "weight_g": 1029, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 K562 교체축 3모드 계산기 유무선 기계식", + "brand": "앱코", + "price": 26900, + "image_url": "https://img.danuri.io/catalog-image/845/642/090/87c46bb45b694003935a6ffe8449a79e.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "96키", + "key_force": "50g", + "weight_g": 862, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "COX CQK81 VIA 엣지 LED & 노브 3MODE 유무선 기계식", + "brand": "COX", + "price": 33000, + "image_url": "https://img.danuri.io/catalog-image/840/437/090/1d3cc05026b643699b08c5aaa8bb9aa0.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "미니", + "key_force": "45g", + "weight_g": 800, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "영문 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "한성컴퓨터 GK787SE OfficeMaster 8K 기계식 뽀송", + "brand": "한성컴퓨터", + "price": 77000, + "image_url": "https://img.danuri.io/catalog-image/633/269/030/73e59c5e54864f088a513cc898151a0b.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "38g", + "weight_g": 1500, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "COX COS600 무선", + "brand": "COX", + "price": 26900, + "image_url": "https://img.danuri.io/catalog-image/033/886/073/63a5d0bea58241e99c3135db1abeea06.jpg", + "switch_type": "펜타그래프", + "connection": "무선", + "layout": "풀배열", + "key_force": "60g", + "weight_g": 615, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "레이저각인 키캡", + "backlight": "없음" + }, + { + "product_name": "CORSAIR 뱅가드 96 MLX 게이밍 무선 기계식", + "brand": "CORSAIR", + "price": 257470, + "image_url": "https://img.danuri.io/catalog-image/871/483/109/b6fd5765220648d980fe576f914e027a.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "96키", + "key_force": "45g", + "weight_g": 1167, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "CORSAIR K70 MAX RGB MGX 게이밍 기계식", + "brand": "CORSAIR", + "price": 279000, + "image_url": "https://img.danuri.io/catalog-image/863/546/027/060ac8bd44f94904813ddaba9af064fd.jpg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 1390, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AM61 자석축 래피드 트리거", + "brand": "앱코", + "price": 38400, + "image_url": "https://img.danuri.io/catalog-image/238/235/060/9c056e44f8bb4831856463e4997c0391.jpg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "미니", + "key_force": "50g", + "weight_g": 550, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 NTP84", + "brand": "앱코", + "price": 32550, + "image_url": "https://img.danuri.io/catalog-image/015/692/108/a801688a9e54479fadd08651273a8cc1.jpg", + "switch_type": "멤브레인", + "connection": "유선+무선", + "layout": "미니", + "key_force": "45g", + "weight_g": 540, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "Teamwolf RAVEN 68 HE", + "brand": "Teamwolf", + "price": 81000, + "image_url": "https://img.danuri.io/catalog-image/868/341/074/1658353830774a0dbc43f05a6f381f48.jpg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "미니", + "key_force": "30g", + "weight_g": 680, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "CHERRY XTRFY MX 3.1 RGB MX2A 블랙", + "brand": "CHERRY", + "price": 149460, + "image_url": "https://img.danuri.io/catalog-image/739/220/045/03f6632f313644868cb7d396c0f04c3c.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 1120, + "wireless_type": "유선", + "engraving": "레이저각인 키캡", + "backlight": "RGB 백라이트" + }, + { + "product_name": "AULA F65 유무선 기계식 스카이 블랙", + "brand": "AULA", + "price": 67000, + "image_url": "https://img.danuri.io/catalog-image/126/325/073/a821a4851637435a82fedecac35d4a27.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "미니", + "key_force": "43g", + "weight_g": 804, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "로지텍 G413 SE (정품)", + "brand": "로지텍", + "price": 75050, + "image_url": "https://img.danuri.io/catalog-image/631/396/016/9e13839e84f04030ad58047dfd467b7e.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "50g", + "weight_g": 780, + "wireless_type": "유선", + "engraving": "정보없음", + "backlight": "단색 백라이트" + }, + { + "product_name": "COX CM105KDW 3모드 조약돌", + "brand": "COX", + "price": 27900, + "image_url": "https://img.danuri.io/catalog-image/848/721/101/ee9695fb4a2046f4a94a71e01a63c190.jpg", + "switch_type": "멤브레인", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 810, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "COX CMK87R 자석축 래피드 트리거", + "brand": "COX", + "price": 59000, + "image_url": "https://img.danuri.io/catalog-image/551/505/071/df8b69ded87e4d27a0c90ba7bda804e1.jpg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "텐키리스", + "key_force": "30g", + "weight_g": 935, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AK108 3모드 OTEMU 특주축 유무선 기계식 그레이", + "brand": "앱코", + "price": 69000, + "image_url": "https://img.danuri.io/catalog-image/446/015/093/2e1f2b855a21429e996de4e5d1f63f71.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "40g", + "weight_g": 1117, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "FL-ESPORTS NX108 유무선 기계식 크림 핑크", + "brand": "FL-ESPORTS", + "price": 77000, + "image_url": "https://img.danuri.io/catalog-image/413/122/097/fa3fd9f16ef240b682f4252ea14de8a6.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "43g", + "weight_g": 1180, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AKN10BT 반알만한 무접점", + "brand": "앱코", + "price": 250000, + "image_url": "https://img.danuri.io/catalog-image/004/191/092/317a3c44705c4c74b172b4defd9098f8.jpg", + "switch_type": "무접점", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 1200, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "CORSAIR K100 AIR WIRELESS RGB Ultra Low Profile 게이밍 기계식키보드", + "brand": "CORSAIR", + "price": 378880, + "image_url": "https://img.danuri.io/catalog-image/703/932/017/f60535d22068499098328a922f3324e1.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "65g", + "weight_g": 780, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AO98 레트로 타임리스 3모드 기계식", + "brand": "앱코", + "price": 59000, + "image_url": "https://img.danuri.io/catalog-image/484/702/097/554cf3858e17411dbda4acf29ec9ffc8.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "98키", + "key_force": "45g", + "weight_g": 1090, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "COX CK01 TKL PBT SL 기계식키보드", + "brand": "COX", + "price": 29900, + "image_url": "https://img.danuri.io/catalog-image/348/185/015/084823b166fc4190816483d972831a89.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "텐키리스", + "key_force": "60g", + "weight_g": 700, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "AULA F65 유무선 기계식 올리비아 화이트", + "brand": "AULA", + "price": 67000, + "image_url": "https://img.danuri.io/catalog-image/682/696/073/b0d4d75acc0c4c42a62e6c2d8f7a5620.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "미니", + "key_force": "43g", + "weight_g": 804, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "CHERRY XTRFY MX 8.3 TKL 8K 유무선 기계식", + "brand": "CHERRY", + "price": 371070, + "image_url": "https://img.danuri.io/catalog-image/100/699/100/8b35eb77f81647e6bda88649ade94b57.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 1250, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AN02 블랙 RGB BAR 축교환 기계식키보드", + "brand": "앱코", + "price": 34900, + "image_url": "https://img.danuri.io/catalog-image/640/650/014/3f849288b9264d679ffeac758f14736a.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "50g", + "weight_g": 830, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "앱코 K640T SLIM 축교환 무빙LED 텐키리스 기계식 화이트", + "brand": "앱코", + "price": 28900, + "image_url": "https://img.danuri.io/catalog-image/614/282/015/0841a71c5c8a4b349fccb257cf53d714.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 572, + "wireless_type": "유선", + "engraving": "정보없음", + "backlight": "단색 백라이트" + }, + { + "product_name": "앱코 AS104 오피스네비게이터 유무선 기계식 블루레몬", + "brand": "앱코", + "price": 99000, + "image_url": "https://img.danuri.io/catalog-image/615/903/071/01c2c2020c844e66a8bf376813fa5ae0.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 1163, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AK87 3모드 OTEMU 특주축 유무선 기계식 다크 그라데이션", + "brand": "앱코", + "price": 62000, + "image_url": "https://img.danuri.io/catalog-image/639/828/074/a693e440bfee49f2b3a49174707c4c80.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "40g", + "weight_g": 1024, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "정보없음", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 K669 카일 광축 게이밍", + "brand": "앱코", + "price": 66130, + "image_url": "https://img.danuri.io/catalog-image/820/690/019/71eac0988c0f4b1ca1abe027312fcf30.jpg", + "switch_type": "무접점 광축", + "connection": "유선", + "layout": "풀배열", + "key_force": "55g", + "weight_g": 1260, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "CHERRY XTRFY K5 PRO TMR", + "brand": "CHERRY", + "price": 149000, + "image_url": "https://img.danuri.io/catalog-image/232/007/102/9d7f2dcba0ad4b188902bc1cdc89452c.jpg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "미니", + "key_force": "38g", + "weight_g": 568, + "wireless_type": "유선", + "engraving": "레이저각인 키캡", + "backlight": "RGB 백라이트" + }, + { + "product_name": "COX CFL87+21 옴니키(원솔루션) 3모드 유무선 기계식", + "brand": "COX", + "price": 55000, + "image_url": "https://img.danuri.io/catalog-image/255/969/092/6afb9b6744c94f6787db7641bd1aaba1.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 944, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "CORSAIR 뱅가드 96 MLX 게이밍 기계식", + "brand": "CORSAIR", + "price": 212000, + "image_url": "https://img.danuri.io/catalog-image/298/245/098/0ff89fcecf484ba78d0cf383f0aed5f6.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "96키", + "key_force": "40g", + "weight_g": 994, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 K570 축교환 레인보우 무빙 LED 기계식키보드", + "brand": "앱코", + "price": 27900, + "image_url": "https://img.danuri.io/catalog-image/087/471/014/ceb091f4070c4bb791bda651774b4673.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 744, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "앱코 K660S V2 카일 광축 V2 게이밍 화이트", + "brand": "앱코", + "price": 66000, + "image_url": "https://img.danuri.io/catalog-image/583/991/075/e5c3625766b54c5a90fa4f71892ed18c.jpg", + "switch_type": "무접점 광축", + "connection": "유선", + "layout": "풀배열", + "key_force": "40g", + "weight_g": 1240, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "앱코 TOS300 매크로덱", + "brand": "앱코", + "price": 29900, + "image_url": "https://img.danuri.io/catalog-image/701/080/094/e61c7dfa2bbf4e7e8b8bcd7748f405d4.jpg", + "switch_type": "펜타그래프", + "connection": "유선", + "layout": "99키", + "key_force": "0g", + "weight_g": 776, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "단색 백라이트" + }, + { + "product_name": "darkFlash DK108", + "brand": "darkFlash", + "price": 18310, + "image_url": "https://img.danuri.io/catalog-image/443/964/092/d4423995c7da46eca2236e48818cedb1.jpg", + "switch_type": "멤브레인", + "connection": "유선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 810, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "COX CK88 유무선 기계식 블랙로얄", + "brand": "COX", + "price": 81340, + "image_url": "https://img.danuri.io/catalog-image/328/810/094/ffe997ffd95845838c214bafa63d8822.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 1410, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "영문 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "앱코 AK108 3모드 OTEMU 특주축 유무선 기계식 올리비아", + "brand": "앱코", + "price": 69000, + "image_url": "https://img.danuri.io/catalog-image/518/015/093/49f4fcbaf0214a41914e27e0d2123fd9.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "40g", + "weight_g": 1117, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 K660S V2 카일 광축 V2 게이밍 블랙", + "brand": "앱코", + "price": 66000, + "image_url": "https://img.danuri.io/catalog-image/689/990/075/ac910d420aca49cba8db8e9de1148ff9.jpg", + "switch_type": "무접점 광축", + "connection": "유선", + "layout": "풀배열", + "key_force": "40g", + "weight_g": 1240, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "AULA F75 MAX 유무선 기계식 스카이 블랙", + "brand": "AULA", + "price": 79000, + "image_url": "https://img.danuri.io/catalog-image/462/556/079/1f6edd1dae3747b0b90873b701e0ba79.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "미니", + "key_force": "40g", + "weight_g": 1023, + "wireless_type": "블루투스, 전용동글(리시버)", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "주연테크 긱스타 GKG87 유무선 기계식 화이트", + "brand": "주연테크", + "price": 52110, + "image_url": "https://img.danuri.io/catalog-image/818/566/094/748aa62883c24164b04f9992b25cd951.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "42g", + "weight_g": 952, + "wireless_type": "블루투스, 전용동글(리시버)", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 ACH105 오피스워커 유무선 기계식 라벤더", + "brand": "앱코", + "price": 79000, + "image_url": "https://img.danuri.io/catalog-image/739/690/103/69615bf8686e4bdeae99ecd5a9231737.jpeg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "풀배열", + "key_force": "47g", + "weight_g": 1424, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 AN02 핑크 RGB BAR 축교환 기계식키보드", + "brand": "앱코", + "price": 39900, + "image_url": "https://img.danuri.io/catalog-image/802/650/014/de58628b5ac449459a15223b2f18aa7a.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "50g", + "weight_g": 830, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "단색 백라이트" + }, + { + "product_name": "ATK RS6 Air", + "brand": "ATK", + "price": 89000, + "image_url": "https://img.danuri.io/catalog-image/179/663/122/069946b8135e42c8bff3760493ec1ffe.jpeg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "미니", + "key_force": "35g", + "weight_g": 710, + "wireless_type": "유선", + "engraving": "영문 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 MK108W 무선", + "brand": "앱코", + "price": 29000, + "image_url": "https://img.danuri.io/catalog-image/670/223/072/456a150d78e74a879b11c416cf70ecdd.jpg", + "switch_type": "멤브레인", + "connection": "무선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 715, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "레이저각인 키캡", + "backlight": "레인보우 백라이트" + }, + { + "product_name": "COX CK01 Navy 교체축 사이드 RGB 게이밍 기계식키보드", + "brand": "COX", + "price": 27900, + "image_url": "https://img.danuri.io/catalog-image/044/224/013/0774175657bb482e824e1bcaf5ee79d6.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "풀배열", + "key_force": "45g", + "weight_g": 828, + "wireless_type": "유선", + "engraving": "정보없음", + "backlight": "단색 백라이트" + }, + { + "product_name": "앱코 K480 교체축 게이밍 기계식 젠틀맨", + "brand": "앱코", + "price": 27900, + "image_url": "https://img.danuri.io/catalog-image/518/706/077/4c8184930f5d4a91b0740c824237cc7f.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 604, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "COX CK01 TKL", + "brand": "COX", + "price": 29900, + "image_url": "https://img.danuri.io/catalog-image/436/440/014/63fb8085a1a4429eb51b5021b30bc8a3.jpg", + "switch_type": "기계식", + "connection": "유선", + "layout": "텐키리스", + "key_force": "45g", + "weight_g": 700, + "wireless_type": "유선", + "engraving": "한/영 정각", + "backlight": "단색 백라이트" + }, + { + "product_name": "앱코 AK10 오피스 마스터 슬림 무선", + "brand": "앱코", + "price": 15900, + "image_url": "https://img.danuri.io/catalog-image/034/111/079/24fe69d0b70d4891a744e73a0e6441fc.jpg", + "switch_type": "멤브레인", + "connection": "무선", + "layout": "풀배열", + "key_force": "0g", + "weight_g": 495, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "없음" + }, + { + "product_name": "앱코 AK87 3모드 OTEMU 특주축 유무선 기계식 그레이", + "brand": "앱코", + "price": 49000, + "image_url": "https://img.danuri.io/catalog-image/564/819/074/c7f17f48810b49fb825f97853f3c2dc6.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "텐키리스", + "key_force": "40g", + "weight_g": 1024, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "한/영 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "Createkeebs LUMINKEY Magger 68 HE 프로페셔널", + "brand": "Createkeebs", + "price": 179000, + "image_url": "https://img.danuri.io/catalog-image/805/004/072/62ccb746f7054627a113b6183b9ce761.jpg", + "switch_type": "무접점 자석축", + "connection": "유선", + "layout": "미니", + "key_force": "36g", + "weight_g": 1200, + "wireless_type": "유선", + "engraving": "영문 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "CORSAIR K65 RGB PLUS 유무선 애플 에디션", + "brand": "CORSAIR", + "price": 220570, + "image_url": "https://img.danuri.io/catalog-image/445/542/101/00f4876f0aa44b5b87c17faacef7f682.jpg", + "switch_type": "기계식", + "connection": "유선+무선", + "layout": "미니", + "key_force": "45g", + "weight_g": 922, + "wireless_type": "전용동글(리시버), 블루투스", + "engraving": "영문 정각", + "backlight": "RGB 백라이트" + }, + { + "product_name": "앱코 LKN99 BT 로우 프로파일 슬림 무접점", + "brand": "앱코", + "price": 159000, + "image_url": "https://img.danuri.io/catalog-image/142/519/094/20e3e7cb6a924d6cab75c3743c8d397a.jpg", + "switch_type": "무접점", + "connection": "유선+무선", + "layout": "99키", + "key_force": "50g", + "weight_g": 1443, + "wireless_type": "블루투스, 전용동글(리시버)", + "engraving": "한/영 정각", + "backlight": "없음" + } +] \ No newline at end of file From fd9af018cb2a9f9036454f594eb348440e83d624 Mon Sep 17 00:00:00 2001 From: ujeong Date: Tue, 9 Jun 2026 14:01:41 +0900 Subject: [PATCH 005/137] =?UTF-8?q?feat(keybuddy):=20=ED=82=A4=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EC=B2=9C=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=EB=A5=BC=20impl/keybuddy=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다나와 크롤러 데이터 기반 키보드 추천 웹앱(Vite+React+TS) 이관. node_modules/dist/.env.local 등 빌드 산출물과 실제 키 파일은 제외하고 .env.local.example만 포함. README 경로를 impl 기준으로 갱신. --- impl/keybuddy/README.md | 44 + impl/keybuddy/frontend/.env.local.example | 7 + impl/keybuddy/frontend/.gitignore | 6 + impl/keybuddy/frontend/index.html | 12 + impl/keybuddy/frontend/package-lock.json | 2515 +++++++++++++++++ impl/keybuddy/frontend/package.json | 26 + impl/keybuddy/frontend/src/App.tsx | 544 ++++ .../keybuddy/frontend/src/data/keyboards.json | 1402 +++++++++ impl/keybuddy/frontend/src/index.css | 23 + impl/keybuddy/frontend/src/lib/recommend.ts | 119 + impl/keybuddy/frontend/src/main.tsx | 10 + impl/keybuddy/frontend/src/types.ts | 31 + impl/keybuddy/frontend/src/vite-env.d.ts | 10 + impl/keybuddy/frontend/tsconfig.json | 21 + impl/keybuddy/frontend/vite.config.ts | 7 + 15 files changed, 4777 insertions(+) create mode 100644 impl/keybuddy/README.md create mode 100644 impl/keybuddy/frontend/.env.local.example create mode 100644 impl/keybuddy/frontend/.gitignore create mode 100644 impl/keybuddy/frontend/index.html create mode 100644 impl/keybuddy/frontend/package-lock.json create mode 100644 impl/keybuddy/frontend/package.json create mode 100644 impl/keybuddy/frontend/src/App.tsx create mode 100644 impl/keybuddy/frontend/src/data/keyboards.json create mode 100644 impl/keybuddy/frontend/src/index.css create mode 100644 impl/keybuddy/frontend/src/lib/recommend.ts create mode 100644 impl/keybuddy/frontend/src/main.tsx create mode 100644 impl/keybuddy/frontend/src/types.ts create mode 100644 impl/keybuddy/frontend/src/vite-env.d.ts create mode 100644 impl/keybuddy/frontend/tsconfig.json create mode 100644 impl/keybuddy/frontend/vite.config.ts diff --git a/impl/keybuddy/README.md b/impl/keybuddy/README.md new file mode 100644 index 00000000..0f1492d9 --- /dev/null +++ b/impl/keybuddy/README.md @@ -0,0 +1,44 @@ +# keybuddy + +다나와 크롤러(`output/keyboards.json`, 100개)를 입력으로, 자연어/단계별 질문에 맞춰 +키보드를 추천해 주는 웹 서비스. 추천은 Claude API(LLM)로 수행합니다. + +## 구조 + +``` +keybuddy/ + frontend/ Vite + React + TS + Tailwind (단일 프론트, 서버 없음) + src/ + App.tsx 화면(홈 / 단계별 질문 / 결과) + lib/recommend.ts Claude 호출 + 카탈로그 프롬프트 캐싱 + 구조화 출력 + data/keyboards.json 크롤링 데이터 사본(추천 후보) + types.ts +``` + +별도 백엔드 없이 프론트에서 Claude를 직접 호출합니다(`dangerouslyAllowBrowser`). +LLM은 `data/keyboards.json`의 인덱스만 골라 반환하고, 가격/이미지 등 실제 값은 +프론트가 카탈로그에서 채웁니다(환각 방지). 제품별 추천 사유/태그는 LLM이 생성합니다. + +## 실행 + +```bash +cd impl/keybuddy/frontend +cp .env.local.example .env.local # VITE_ANTHROPIC_API_KEY 채우기 +npm install +npm run dev +``` + +## 키 보호 주의 + +`.env.local`은 git 커밋만 막아줄 뿐, `VITE_` 변수는 빌드 번들에 인라인되어 +브라우저에서 노출됩니다. **내 컴퓨터에서 나만 쓰는 로컬 용도로만** 사용하세요. +공개 배포가 필요하면 키를 쥐는 작은 프록시(서버)로 전환해야 합니다. + +## 데이터 갱신 + +상위 크롤러(`impl/crawl.py`)를 다시 돌린 뒤 결과를 복사합니다. (아래는 `impl/` 기준) + +```bash +python3 crawl.py +cp output/keyboards.json keybuddy/frontend/src/data/keyboards.json +``` diff --git a/impl/keybuddy/frontend/.env.local.example b/impl/keybuddy/frontend/.env.local.example new file mode 100644 index 00000000..c1bffe38 --- /dev/null +++ b/impl/keybuddy/frontend/.env.local.example @@ -0,0 +1,7 @@ +# 이 파일을 .env.local 로 복사한 뒤 키를 채우세요. (.env.local 은 git에 올라가지 않습니다) +# 주의: VITE_ 변수는 빌드 시 클라이언트 번들에 인라인됩니다. +# 내 컴퓨터에서만 쓰는 로컬 용도로만 사용하고, 공개 배포는 하지 마세요. +VITE_ANTHROPIC_API_KEY=sk-ant-... + +# 선택: 모델 변경(기본 claude-sonnet-4-6). 더 높은 품질이 필요하면 아래 주석 해제. +# VITE_ANTHROPIC_MODEL=claude-opus-4-8 diff --git a/impl/keybuddy/frontend/.gitignore b/impl/keybuddy/frontend/.gitignore new file mode 100644 index 00000000..19632070 --- /dev/null +++ b/impl/keybuddy/frontend/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.env.local +.env.*.local +*.log +.DS_Store diff --git a/impl/keybuddy/frontend/index.html b/impl/keybuddy/frontend/index.html new file mode 100644 index 00000000..4944400b --- /dev/null +++ b/impl/keybuddy/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + keybuddy - 나만의 키보드 찾기 + + +
+ + + diff --git a/impl/keybuddy/frontend/package-lock.json b/impl/keybuddy/frontend/package-lock.json new file mode 100644 index 00000000..7912ea00 --- /dev/null +++ b/impl/keybuddy/frontend/package-lock.json @@ -0,0 +1,2515 @@ +{ + "name": "keybuddy", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "keybuddy", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.69.0", + "lucide-react": "^0.460.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.69.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.69.0.tgz", + "integrity": "sha512-L92d2q47BSq+7slUqHBL1d2DwloulZotYGCTDt9AYRtPmYF+iK6rnwq9JaZwPPJgk+LenbcbQ/nj6gfaDFsl9w==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.30", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.30.tgz", + "integrity": "sha512-3ek6mwJL5/VBewBcY4S66cqlCtK3qi4WIq37Z0m/NHw1hjhI7274Mx1qz/+ggSzyBCOEf7eHjBN6INjPAWYfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.367", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.367.tgz", + "integrity": "sha512-4Mk/mrynCNQ+atY40D3UpmhLWB6AHMbYMlIrPhHcMF6x0L7O0b052FCAsxw1LlaR++UFuNg3D/A6XCuGDa0guQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.22.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.2.tgz", + "integrity": "sha512-0rxICaFZ7NQho/sHely2bvOPRP0Eu2B0NZ9zM54YvRvWMn7jfz3DmnOZDR9LlXDdDcqntAVc6Hfy4gr/tdH/Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.460.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz", + "integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/impl/keybuddy/frontend/package.json b/impl/keybuddy/frontend/package.json new file mode 100644 index 00000000..b51169c2 --- /dev/null +++ b/impl/keybuddy/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "keybuddy", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.69.0", + "lucide-react": "^0.460.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/impl/keybuddy/frontend/src/App.tsx b/impl/keybuddy/frontend/src/App.tsx new file mode 100644 index 00000000..72d309b4 --- /dev/null +++ b/impl/keybuddy/frontend/src/App.tsx @@ -0,0 +1,544 @@ +import { useState } from 'react'; +import { + Search, + Keyboard, + ChevronLeft, + SlidersHorizontal, + Check, + RefreshCw, + Copy, + CheckCircle2, + Filter, + ArrowUpDown, +} from 'lucide-react'; +import { recommend } from './lib/recommend'; +import type { Recommendation, RecommendInput, RecommendResult } from './types'; + +// --- [질문 데이터] 단계별 선택지 --- +const questions = [ + { id: '용도', title: '어떤 용도로 사용하시나요?', options: ['사무용', '게임용', '상관없음'] }, + { + id: '휴대성', + title: '주로 어디서 사용하시나요?', + options: ['책상에 놓고 쓸 거예요', '자주 가지고 다닐래요', '상관없음'], + }, + { + id: '소리', + title: '타건 소리는 어느 정도가 좋나요?', + options: [ + '조용해야 해요 (매우 낮음)', + '조금 소리가 났으면 해요 (낮음)', + '적당한 소리 (보통)', + '경쾌한 소리 (조금 큼)', + '타건감 위주 (시끄러워도 됨)', + ], + }, + { + id: '키감', + title: '어떤 느낌의 키감을 선호하시나요?', + options: [ + '또각또각 (걸림이 있는 느낌)', + '서걱서걱 (부드럽게 들어가는 느낌)', + '보글보글 (독특한 무접점 느낌)', + '잘 모르겠어요', + ], + }, + { + id: '키압', + title: '키를 누를 때의 무게감은요?', + options: [ + '가볍게 눌렸으면 좋겠어요 (35~45g)', + '보편적인게 좋아요 (45~55g)', + '묵직한게 좋아요 (60g 이상)', + '잘 모르겠어요', + ], + }, + { + id: '연결방식', + title: '어떤 연결 방식을 원하시나요?', + options: ['유선', '무선 USB 동글', '블루투스', '유/무선 모두', '상관없음'], + }, + { + id: '크기', + title: '원하시는 키보드 크기가 있나요?', + options: [ + '숫자 패드가 있는 일반 키보드 (풀배열)', + '숫자 패드가 있지만 콤팩트함 (1800배열)', + '숫자 패드가 없음 (텐키리스)', + '숫자 패드도, 일부 특수키도 없음 (75%/65%)', + 'F1~F12키도 없는 미니 (60%)', + ], + }, + { id: '예산', title: '예산은 어느 정도로 생각하시나요?', type: 'range', options: [] as string[] }, + { + id: '각인', + title: '키보드 각인은 어떻게 할까요?', + options: [ + '한국어, 영어가 모두 필요해요', + '영어만 적혀있길 바라요', + '한국어만 적혀있길 바라요', + '상관없음', + ], + }, + { + id: '백라이트', + title: '백라이트(조명)가 필요하신가요?', + options: ['화려한 RGB가 좋아요', '은은한 단색 조명이 좋아요', '없어도 돼요 (배터리 절약)'], + }, +]; + +// 이미지 lazyload 깨짐 대비: 실패 시 키보드 아이콘으로 대체 +function KeyboardImage({ src, alt }: { src: string; alt: string }) { + const [failed, setFailed] = useState(false); + if (failed || !src) { + return ( +
+ +
+ ); + } + return ( + {alt} setFailed(true)} + className="w-full h-full object-contain bg-white" + /> + ); +} + +export default function App() { + const [view, setView] = useState<'home' | 'step' | 'results'>('home'); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const runRecommend = async (input: RecommendInput) => { + setLoading(true); + setError(null); + try { + const res = await recommend(input); + setResult(res); + setView('results'); + } catch (e) { + setError(e instanceof Error ? e.message : '추천 중 오류가 발생했습니다.'); + } finally { + setLoading(false); + } + }; + + // --- HOME VIEW --- + const HomeView = () => { + const [query, setQuery] = useState(''); + const templates = [ + '조용한 사무실에서 눈치보지 않고 사용할 도각도각 소리가 나는 키보드 추천해줘', + '게임할 때 반응속도가 빠르고 화려한 RGB 조명이 있는 텐키리스 키보드 찾아줘', + '아이패드랑 같이 들고 다닐 작고 가벼운 블루투스 키보드 필요해', + ]; + + const handleSubmit = () => { + if (!query) return; + runRecommend({ mode: 'freeform', query }); + }; + + return ( +
+
+

나만의 키보드 찾기

+

어떤 키보드를 찾으시나요? 자유롭게 말해주세요.

+
+ + {error && ( +
+ {error} +
+ )} + + {/* 채팅창 섹션 */} +
+