Skip to content

[4주차 크루 미션 - 최종 발표 & 회고] 누렁이 미션 제출합니다.#21

Merged
krrong merged 87 commits into
woowacourse:byunghyunkim0from
byunghyunkim0:team/nureongi
Jun 21, 2026
Merged

[4주차 크루 미션 - 최종 발표 & 회고] 누렁이 미션 제출합니다.#21
krrong merged 87 commits into
woowacourse:byunghyunkim0from
byunghyunkim0:team/nureongi

Conversation

@byunghyunkim0

Copy link
Copy Markdown

Week 4 최종 보고서 - 누렁이

4주 흐름 한 줄 요약

카메라로 위험을 감지하던 AI 안내견에서, 점자블록 기반 지하철 역내 길을 찾아주는 음성 안내 네비게이션으로 변화


A. 처음 의도 vs 결과

문제 정의의 변천

1주차 4주차
안내견의 수가 부족하다. → 시각 장애인이 안전한 보행을 보장받기 힘들다 시각장애인들이 처음 가는 또는 익숙하지 않은, 복잡한 지하철 역에서 부대시설을 찾기 어려워한다

MVP 범위의 변천

1주차 만들 것 실제 만든 것
카메라 화면 내 목표 객체(화장실, 출구, 엘리베이터 등) 탐지 후 음성으로 유도 출발지/목적지 선택 후 최적 경로 탐색
목표 탐지 시 직진/좌/우 안내 메시지 음성 출력 (정확도 70~80% 목표) 점형 블록(꺾임, 계단 등) 기준 다음 경로 음성 안내
안내 반응 속도 0.4~1초 이내 실시간 탐지 가상 맵(우아한테크코스 11층 교육장, 서현역)에서 남은 거리·점형 블록 정보 표시
현재 위치 판단 (범위 제외) 고대비/다크모드 UI 적용
음성 버튼으로 안내 메시지 재청취
VoiceOver/Talkback 호환
STT(Speech-To-Text)로 출발지 설정 가능

기술 선택 재평가

다시 한다면 같은 기술? 왜?

  • 바드: No, 각각의 Native 앱을 따로 만드는 게 좋을 것 같다. 작업 단위가 작으면 역할을 분담하기 어려워 플랫폼별로 네이티브 개발을 하면 역할 분담이 좀 더 편해질 것
    같다고 생각했다.
  • 사무엘: Yes, 시간과 상황을 고려했을 때 최선의 기술이었다. 다만 시간과 상황에 맞는 맥락이 없다면, STT, TTS, VoiceOver/TalkBack 기능을 각각 구현해줘야해서 바드처럼 Native로 각각 구현하는게 나을 것 같다.
  • 스마일: 휴대폰으로 다시 결정한다면 동일한 선택을 할 것 같다. 빠르게 MVP를 만드는 게 중요하니까. (지팡이도 고려해봄직 할 것 같다)

고려했다 안 쓴 선택지와 뺀 이유: Flutter, AI (YOLO)

이유: 3주차에 AI, 카메라 기반 객체 탐지 기능이 MVP 범위에서 통째로 빠지면서, TF-Lite 연동을 위해 선택했던 Flutter의 강점(크로스플랫폼 + TF-Lite)이
무의미해짐. 팀이 가장 익숙한 기술(KMP/CMP)로 빠르게 MVP를 만들어 사용자에게 검증받는 것이 더 중요하다고 판단해 전환함.


B. 가설 검증 종합

주차 가설 결과 (지지/반박/불명확) 받아들인 방식
1주차 시각 장애인은 택시보다 지하철을 더 많이 이용할 것이다 지지 중증 시각장애인일수록 버스를 기피하고 지하철을 선호한다는 인터뷰 근거를 확인, 지하철 중심 서비스 설계를 유지함
1주차 우리 팀의 능력으로 카메라 기반 실시간 객체 탐지 서비스가 실현 가능할 것이다 반박 2주차에 AI/카메라 기능을 MVP에서 제외하고 점자블록 기반 경로 탐색으로 방향을 전환함
2주차 지하철 부대시설(화장실, 역무실, 개찰구 등)을 찾는 데 어려움을 느낄 것이다 불명확 자주 다니는 길은 기억으로 무리 없이 찾지만 초행길에서는 헤맨다는 답변에 따라, 페르소나를 “모든 시각 장애인”에서 “익숙하지 않거나 복잡한 역을 이용하는 시각 장애인”으로 재정의함
2주차 남/녀 화장실 구분을 쉽게 하지 못할 것이다 반박 점자 표지판, 냄새·소리로 구분 가능하다는 답변에 따라 “구분”이 아니라 “화장실 자체를 찾는” 경로 안내로 문제를 재정의함
2주차 작동하지 않는 음성유도기가 있을 것이다 지지 고장·미설치 사례를 확인하고, 유도기 위치/상태 정보 제공 필요성을 향후 백로그에 반영함
2주차 역무원이 즉각적인 도움을 제공하기 어려운 상황이 있다 지지 역당 인력 부족과 공익근무요원 감소 사례를 확인, 기술적 보조 수단의 필요성을 재확인함
2주차 밝은 색의 잘 구분되는 버튼으로 앱을 구성하는 것이 좋을 것이다 반박 다크모드 선호, 고대비·2~3색 이내 일률적 색상 사용이 더 적합하다는 답변에 따라 UI 디자인 방향을 변경함
3주차 미터(m) 단위 거리 안내가 사용자에게 의미가 있을 것이다 불명확 끝까지 검증하지 못해 다음 인터뷰에서 확인할 항목으로 이연함
3주차 화면 내 미니맵, 깜빡임/애니메이션 효과가 시각장애인(저시력자 포함)에게 의미가 있을 것이다 불명확 저시력자 대상 UI 검증이 더 필요하다고 판단, 4주차 추가 인터뷰 항목으로 이연함

가장 크게 깨진 가설

무엇이었는가: 밝은 색의 잘 구분되는 버튼으로 앱을 구성하는 것이 좋을 것이다

어떻게 받아들였는가: 밝고 대비가 잘 되는 색상이 아닌, 다크모드를 선호하고, 고대비·2~3색 이내 일률적 색상 사용이 더 적합하다는 답변에 따라 UI 디자인 방향을
변경함

테마 변경 기능을 넣을 수도 있을 것 같다고 생각했음

끝까지 검증 못 한 가설 (있다면)

  • 미터(m) 단위 거리 안내가 실제로 사용자에게 의미가 있는가
  • 화면 미니맵, 현재 위치 표시 방식(색상 변경 vs 깜빡임)이 저시력자에게 의미가 있는가
  • 시각 장애인이 가장 이해하기 쉬운 안내 메시지의 형태는 무엇인가

시연 영상

서현역 시연 영상 3번 출구 -> 개찰구

byunghyunkim0 and others added 30 commits June 5, 2026 14:31
공통 컴포넌트/화면 작업 시 지켜야 할 3가지 원칙(로직 분리, 접근성,
공식 샘플 참고)과 각 원칙의 검증 방법을 RULE.md에 정리하고, AGENTS.md에서
UI 작업 시 이 문서를 먼저 읽도록 안내한다.
디자인 산출물(피그마 등)이 없는 상태에서 컬러·타이포·레이아웃을 추정하기 위해
참고한 주요 화면 스크린샷(메인/메인 선택/현재 위치/길안내/길안내 목적지)을
docs/ui 에 보관한다.
ic_volume.xml의 fillColor="@android:color/transparent" 참조 때문에
Compose Multiplatform 프리뷰 렌더링 시
"Invalid color value @android:color/transparent" 오류가 발생했다.
멀티플랫폼 리소스 파서는 안드로이드 프레임워크 리소스 참조를 해석하지
못하므로, 동일한 의미의 리터럴 ARGB 값(#00000000)으로 바꿔 플랫폼
종속성을 제거한다.
docs/ui/RULE.md, AGENTS.md, docs/ui 스크린샷을 기준으로 화면이 만들어지기
전에 재사용할 공통 UI 계층을 ui/theme(디자인 토큰)·ui/model(UI 전용 DTO)·
ui/component(컴포넌트) 세 패키지로 나눠 작성한다.

- ui/theme: 다크 배경 + 옐로우 강조색 컬러 토큰과 4단계 타이포 토큰 정의
- ui/model: PlaceUiModel/RouteNodeUiModel 등 도메인을 모르는 최소 DTO
- ui/component: CtaButton, CurrentLocationBar, NavigationTopBar,
  DirectionGuideCard, RouteMapCard, PlaceListItem, StatTile,
  SegmentedProgressIndicator, VoiceGuideButton, BrailleIcon 등

모든 컴포넌트는 상태를 내부에 갖지 않고 파라미터로 호이스팅하며, 장식
요소와 정보 요소를 구분해 접근성 시맨틱을 부여한다. 각 파일에는 ViewModel
없이 더미 UI 모델만으로 렌더링을 검증하는 private @Preview를 동봉했다.
판단 근거와 트레이드오프는 docs/log에 별도로 기록한다.
ui/theme·ui/model·ui/component 작성 과정에서 내린 16가지 판단(패키지
구조, 디자인 토큰, 상태 호이스팅, 접근성 시맨틱, 네이밍, 빌드 검증 범위,
다섯 관점 병렬 리뷰 반영 등)과 그 근거·장단점·트레이드오프를 정리해,
다음에 화면을 붙일 때 "왜 이렇게 만들었는지"를 다시 설명하지 않아도
되도록 기록한다.
feat: 누렁이 공통 UI 컴포넌트(theme/model/component) 작성
- 컴포넌트(VoiceGuideButton, StraightArrowIcon, BrailleIcon, CtaButton) 내부에 정의된 기본 크기 설정을 제거하고 호출부에서 Modifier로 지정하도록 수정
- NureongiUiFixtures 및 관련 테스트 파일(DestinationSelectionUiStateTest, GuidanceUiStateTest) 삭제
- StatTile 내 미사용 임포트 제거
- DestinationSelectionDestinationList를 DestinationList로 변경해 가독성 개선
…tion

feat: 목적지 선택 화면 추가
feat: 안내 단계 진행 및 도착 상태 구현
Redish03 and others added 21 commits June 12, 2026 17:46
[Refactor] TTS 문구 수정 및 VoiceOver/TalkBack 활성화 시 음성이 겹쳐서 나오지 않도록 수정
- NavHost의 기본 전환 애니메이션(enter, exit, popEnter, popExit) 비활성화
- 클릭 가능 범위를 컴포넌트 전체 영역으로 확대
- 변경 버튼 텍스트 스타일 수정 (SectionHeader)
- 프리뷰에 다양한 길이의 위치 이름 케이스 추가
…-overlap

refactor: 길찾기 안내 TTS와 TalkBack 음성 겹침 방지
…-search

feat: 음성으로 현재 위치 선택 기능 추가
…tation-map

feat: 서현역 지하철 맵 데이터 추가

@krrong krrong left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 누렁이들 🐕

4주동안 고생 많으셨습니다.
짧지 않은 기간동안 프로젝트를 진행하시면서 많은 것을 배우고 느꼈을거라고 생각이 드는데요.
좋았던 부분은 발전시키고, 부족한 부분은 개선해서 다음 프로젝트에 잘 적용해보셨으면 좋겠습니다.

제가 도와드린 부분은 많이 없는 것 같지만, 좋은 결과물과 시연 영상을 만들어주셔서 너무 좋았습니다.

좀 더 이야기해보고 싶은 부분에 코멘트를 달아두었으니 가볍게 확인해주시면 좋을 것 같습니다. 🙇

Comment thread reports/week-4.md

### MVP 범위의 변천

| 1주차 만들 것 | 실제 만든 것 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

첫주차를 돌아보니 새삼 처음에 만들려고 했던 것과 실제로 만든 것은 정말 많이 달라진 것 같아요.

처음에도 상세히 작성했다고 생각했겠지만, 인터뷰등을 통해 구체화해보면서 모호한 부분도 많았구나 하는 점도 느껴보셨다면 좋겠어요.

Comment thread reports/week-4.md
Comment on lines +38 to +41
**고려했다 안 쓴 선택지와 뺀 이유: Flutter, AI (YOLO)**

이유: 3주차에 AI, 카메라 기반 객체 탐지 기능이 MVP 범위에서 통째로 빠지면서, TF-Lite 연동을 위해 선택했던 Flutter의 강점(크로스플랫폼 + TF-Lite)이
무의미해짐. 팀이 가장 익숙한 기술(KMP/CMP)로 빠르게 MVP를 만들어 사용자에게 검증받는 것이 더 중요하다고 판단해 전환함.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기획이 크게 변경되어 실제로 구현하는 것도 많이 달라졌습니다.
좋은 근거로 KMP/CMP 를 접했고, 덕분에 빠르게 MVP를 만들 수 있게 된 것 같습니다.

다만 사용자의 검증도 있었다면 어땠을까 하는 아쉬움이 좀 남네요.

Comment thread reports/week-4.md
Comment on lines +70 to +72
- 미터(m) 단위 거리 안내가 실제로 사용자에게 의미가 있는가
- 화면 미니맵, 현재 위치 표시 방식(색상 변경 vs 깜빡임)이 저시력자에게 의미가 있는가
- 시각 장애인이 가장 이해하기 쉬운 안내 메시지의 형태는 무엇인가

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여전히 저에게도 궁금한 부분이네요 😄

Comment thread reports/week-4.md
Comment on lines +31 to +36
**다시 한다면 같은 기술? 왜?**

- 바드: No, 각각의 Native 앱을 따로 만드는 게 좋을 것 같다. 작업 단위가 작으면 역할을 분담하기 어려워 플랫폼별로 네이티브 개발을 하면 역할 분담이 좀 더 편해질 것
같다고 생각했다.
- 사무엘: Yes, 시간과 상황을 고려했을 때 최선의 기술이었다. 다만 시간과 상황에 맞는 맥락이 없다면, STT, TTS, VoiceOver/TalkBack 기능을 각각 구현해줘야해서 바드처럼 Native로 각각 구현하는게 나을 것 같다.
- 스마일: 휴대폰으로 다시 결정한다면 동일한 선택을 할 것 같다. 빠르게 MVP를 만드는 게 중요하니까. (지팡이도 고려해봄직 할 것 같다)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 이야기하는 기술은 CMP/KMP가 맞을까요?

작업 단위가 작으면 역할을 분담하기 어려워

작업을 분담하는데 어려움을 겪으신 것 같은데 어떤 어려움이었는지 이러한 부분들도 회고를 통해 이야기를 해보셨을지 궁금하네요.

다만 시간과 상황에 맞는 맥락이 없다면

이 부분은 맥락이 부족한 것 같아서 더 이야기해보면 좋을 것 같아요.

지팡이도 고려해봄직 할 것 같다

는 어떤 지팡이 일까요? 🤔

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 이야기하는 기술은 CMP/KMP가 맞을까요?

-> 맞습니다..! 사실 저희가 선택한 게 해당 기술만 있다고 생각하긴합니다.. 🤔

시간과 상황에 맞는 맥락이 없었다면

-> 이거는 제 개인적인 생각이였는데요, 현재 저희가 레벨 1에서 CMP를 하고 레벨 2에서 안드로이드를 해서 이미 코틀린과 컴포즈에 익숙한 크루가 많아서 2-3주(2주차에 아이디어 변경 이후 시점) 동안엔 빠르게 MVP를 만들고, 검증해야한다고 생각했습니다.
그래서 kmp, cmp로 할 것 같습니다

제 설명이 좀 부족하기는 했네요 ㅎ,,

어떤 지팡이 일까요? 🤔

-> 시각장애인들이 일반적으로 사용하는 흰지팡이인데요,
얼마전에 제이슨이 오셔서 슬쩍 "왜 지팡이는 안되나? 점자 바닥에 비콘 이런 걸 포함시켜서 지팡이를 갖다 대면 "화장실입니다"와 같이 소리가 나거나 지팡이와 아두이노 초음파센서를 달아서 앞에 물체가 있는지 확인해주는 도구 이런것들을 해줄 수 있지 않을까? 라고 하고 가셨습니다.
미처 생각해보지 못한 것들이여서 한번 고려해볼 만한 것이라고 생각했어요! 이미 최종 발표 1주 전 쯤 왔다 가셔서 지금은 빠르게 MVP 만 검증해보자는 생각으로 나중으로 미뤘는데 해볼만한 것 같습니다

@byunghyunkim0

Copy link
Copy Markdown
Author

회고 정리 (최종)

Keep (좋았던 것 · 계속할 것)

  • 실제 사용자(시각장애인) 인터뷰를 통해 인사이트 확장
    → 우리 시선이 아닌 시각장애인의 시선으로 문제를 이해할 수 있었음
  • AI를 활용해 빠르게 MVP를 만들고 검증
    → 점자블록 기반 경로 탐색이라는 작지만 검증 가능한 범위로 좁히고, 팀이 익숙한 기술(KMP/CMP)을 선택해 학습 비용 없이 구현에 집중
    → 4주 안에 동작하는 결과물 완성
  • 문제 정의 → 가설 수립 → 검증의 사이클을 따라간 것
  • 인터뷰에서 우리의 가정을 직접 질문으로 던져본 것 (유도성 질문 대신 직접 물어보기)
  • 직접 확인, 테스트해보는 태도

Problem (아쉬웠던 것)

  • 익숙한 기술(KMP/CMP)만 사용 → 새로운 기술적 도전이 없었음
    • 단, 접근성 기능(TalkBack, VoiceOver)은 새롭게 다뤄본 경험
  • 지팡이(보행 보조기구) 등 휴대폰 외 대안을 처음부터 고민하지 못함
    → "휴대폰으로 해결해야 한다"는 고정관념이 있었던 것 같음
  • 우리가 주 사용자(페르소나)가 아니라서 피드백·검증 속도가 느렸음
    → 가설 검증 때마다 외부 시각장애인을 구해야 했음
  • 눈을 감고 직접 테스트해봤지만, 결국 이것도 우리 시선의 시뮬레이션일 뿐
    → 실제 유용성에 대한 확신이 어려움
  • 인터뷰 대상자 수(3명)가 너무 적어 대표성이 부족함
  • AI가 짜준 역할/PR 단위를 그대로 따라가며 유연성이 떨어짐
    → 실제 진행 상황에 맞춰 조정하기 어려웠고 매끄럽지 않은 지점 발생
  • 발표 자료를 직접 구성하지 않아 핵심 내용이 충분히 담기지 않음
  • iOS 개발 확인이 어려웠음
  • AI 블랙박스 문제
    → AI가 작업한 내용/근거 파악이 어려움

Try (다음에 바꿀 것)

  • 실제 서비스 배포를 통해 실사용자 경험/데이터 확보
  • 하네스 엔지니어링 도입
    → 팀원 간 일정한 규칙(커밋 메시지, 코드 구조 등)을 정하고 AI가 그 규칙을 엄격히 따르도록 강제
  • 정식 인터뷰보다 가볍고 표본을 많이 모을 수 있는 방식 도입 (예: 짧은 설문)
    → 정식 인터뷰는 상대 부담이 크고 빈도가 낮음
  • 매일 작업 단위를 팀과 함께 재조정하는 체크포인트 두기
    → 미리 짜놓은 큰 계획을 그대로 따르지 않고, 그날 상황에 맞춰 유동적으로 조정
  • 발표 자료/발표는 팀이 직접 구성
  • AI를 “드라이버”가 아닌 “동업자”로 활용
    → 코드 설명·구조도를 작게 나눠 PR로 올리고 리뷰 요청하는 방식
  • 사용자가 우리일 수 있는 더 범용적인 주제로 전환 고려

팀이 다음에 가져갈 1가지

  • 매일 그날 할 일을 팀과 논의해 작업 단위를 유동적으로 재조정한다.
    → 미리 짜놓은 큰 단위의 계획을 그대로 따르지 않고, 매일 그날의 상황에 맞춰 팀이 함께 작업 단위를 다시 정하는 체크포인트를 둔다.
    → AI가 짜준 역할/PR 단위를 그대로 따라가며 단위가 커지고 유연성이 떨어졌던 문제를 직접 해결하는 액션
    → 매일 짧게라도 “오늘 할 일/단위가 맞는지” 팀과 맞춰보는 시간을 루틴으로 만드는 것이 핵심

그 밖에 오간 의견·결정

  • 점자블록 근처에서 큰 공사가 진행되면 점자블록이 끊기거나 손상되어 경로 탐색 자체가 막힐 수 있다는 리스크
  • 정확도 기준에 대한 시각 차이
    → 우리는 90%만 잘 동작해도 문제없다고 생각하지만, 실제 시각장애인 입장에서는 기준이 다를 수 있음
    → 어느 정도를 기준으로 잡아야 하는지 추가 논의 필요 (결론 미도출)
  • MVP 고도화 판단 기준에 대한 고민
    → 시각장애인 100명에게 사용하게 했을 때 어떤 기준으로 “더 고도화해야 한다 / 충분하다”를 판단할 수 있을까?

DH-Seokjin

This comment was marked as off-topic.

@krrong krrong left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 누렁이들 🐕

최종발표까지 고생많으셨습니다.
이제 마무리할 때가 온 것 같아요.
이번 미니 프로젝트 기간동안 많은 발전을 이루어내신 것 같고, 이 서비스가 확대되어서 실제로 사용되는 날이 왔으면 좋겠다는 생각이 드네요 😄

앞으로도 계속 응원하겠습니다! 🙇

@krrong krrong merged commit 153c77c into woowacourse:byunghyunkim0 Jun 21, 2026
malibinYun pushed a commit that referenced this pull request Jun 24, 2026
* docs: 1주차 보고서 작성

* docs: 1주차 보고서 수정

* docs: 2주차 보고서 작성

* chore: 프로토타입, 데이터 크롤러, 수집한 데이터 첨부

* feat(keybuddy): 키보드 추천 프론트엔드를 impl/keybuddy로 이관

다나와 크롤러 데이터 기반 키보드 추천 웹앱(Vite+React+TS) 이관.
node_modules/dist/.env.local 등 빌드 산출물과 실제 키 파일은 제외하고
.env.local.example만 포함. README 경로를 impl 기준으로 갱신.

* docs(keybuddy): 태그 파이프라인 추천 전환 Seed 명세 추가

추천 정확도 저하(제약 무시·의도 빗나감)를 고치기 위한 태그 파이프라인 전환 요구사항을 Seed로 명세.
LLM은 태그 추출만, 검색/스코어링/결과는 빌드 시 1회 생성한 정적 규칙표로 결정론 처리.
하드 제약 위반 0건 단위테스트 + 의도 골드셋으로 검증. (ooo interview/seed, fallback QA 0.92)

* feat(keybuddy): HardConstraint/SoftIntent 스키마 상수 모듈 추가

- tagSchema.ts: HARD_NUMERIC_KEYS, HARD_ENUM_KEYS, HARD_CONSTRAINT_KEYS, HARD_CONSTRAINT_ENUMS, SOFT_INTENT_VOCAB 상수 정의 및 export
- isSoftIntentTag / isHardConstraintKey / isValidHardEnumValue 런타임 검증 헬퍼 포함
- tagSchema.test.ts: 28개 단위 테스트 (키 목록, 열거형 값, 중복 없음, 헬퍼 동작 검증)
- vitest devDependency 추가, npm test 스크립트 설정

* feat(keybuddy): validateTagSchema 함수 구현 및 단위 테스트 추가

스키마 상수 기반으로 임의 객체의 태그 스키마 준수 여부를 검증하는 validateTagSchema 함수 구현.
- 최상위 알 수 없는 키, hardConstraints 미지원 키, 열거형 허용 외 값, 숫자 키 타입 오류, softIntentTags 어휘 외 값 감지
- 복수 오류 한 번에 수집하는 방식으로 구현
- 유효/무효 케이스 총 20개 단위 테스트 추가 (전체 47개 통과)

* feat(keybuddy): extractRawTags 함수 및 단위 테스트 구현

자유형 자연어를 LLM에 전달해 hardConstraints + softIntentTags 구조의
ExtractedTags를 반환하는 extractRawTags() 함수를 구현한다.

- extractRawTags.ts: AnthropicClient 주입 구조로 테스트 격리 지원,
  스키마 밖 키/값은 parseAndSanitize에서 폐기, 비정상 JSON 안전 처리
- extractRawTags.test.ts: LLM 호출을 _setClientForTest로 모킹,
  hardConstraints/softIntentTags 키 존재 확인, 스키마 외 값 폐기 등 19개 테스트

* feat(keybuddy): sanitizeTags 함수 및 단위 테스트 추가

extractRawTags의 출력(unknown)을 받아 스키마 밖 키/값을 제거하고
정제된 ExtractedTags를 반환하는 sanitizeTags(raw: unknown) 구현.

- 최상위 hardConstraints/softIntentTags 외 키 무시
- HARD_CONSTRAINT_KEYS 밖 hardConstraints 키 제거
- 열거형 키의 HARD_CONSTRAINT_ENUMS 밖 값 제거
- 숫자 키에 number 외 타입 값 제거
- SOFT_INTENT_VOCAB 밖 softIntentTags 항목 제거
- 비정상 입력(null/배열/문자열) 안전 처리
- 알 수 없는 키 제거 / 잘못된 열거형 폐기 / 유효한 값 보존 검증 27개 테스트 추가 (전체 93개 통과)

* feat(keybuddy): extractAndValidateTags 파이프라인 통합 함수 구현

- extractRawTags -> sanitizeTags -> validateTagSchema 순서 파이프라인을 실행하는
  extractAndValidateTags(input: string) 함수를 extractRawTags.ts에 추가
- validateTagSchema 실패 시 오류를 throw하도록 구현
- sanitizeTags가 스키마 밖 값을 폐기하므로 정상 조건에서 항상 유효한 출력 보장
- extractRawTags.ts의 tagSchema 임포트에 validateTagSchema 추가
- extractAndValidateTags.test.ts: LLM 모킹으로 23개 통합 단위 테스트 추가
  (정상 응답, 스키마 밖 값 폐기 후 통과, 비정상 JSON 안전 처리, 출력 구조 보장)

* feat(keybuddy): checkLayoutViolation 함수 구현 및 단위테스트 추가

- src/lib/hardFilter.ts: 레이아웃 하드 제약 위반 판정 함수 구현
  - layoutTag 없음(빈 문자열) -> false(제약 없음)
  - keyboard.layout과 layoutTag 일치 -> false(통과)
  - 불일치 -> true(위반)
- src/__tests__/hardFilter.test.ts: 17개 단위테스트
  - 6개 레이아웃 값 일치 케이스(false)
  - 8개 불일치 케이스(true)
  - 3개 제약 없음 케이스(false)
- 전체 테스트: 133개 통과 (기존 116개 + 신규 17개)

* feat(keybuddy): 소프트태그 정적 규칙표 및 완전성 검사 구현 (Sub-AC 3-1)

- softTagRules.ts: SOFT_INTENT_VOCAB 24개 태그 전체를 커버하는 정적 규칙표 생성
  - 태그별 AttributePredicate(field/op/value) 술어 매핑
  - checkRuleTableCompleteness 함수 구현
- softTagRules.test.ts: 19개 단위 테스트 추가
  - 정적 규칙표 구조 무결성 검사 (7개)
  - 현재 규칙표 완전성 검증 - 누락 0건 확인 (2개)
  - 누락 태그 감지 시나리오 (6개): 빈 규칙표/특정 태그 누락/predicates 빈 배열
  - 경계 케이스 (3개): 빈 vocab/중복 엔트리/순서 보장

전체 152개 테스트 통과 (기존 133개 + 신규 19개)

* test(keybuddy): checkSwitchViolation 구현 및 단위테스트 추가

hardFilter.ts에 checkSwitchViolation(keyboard, switchTag) 함수 추가.
일치->false, 불일치->true, 빈 문자열->false(제약 없음) 케이스 18개 테스트.

* feat(keybuddy): 단계별 선택 입력->하드 제약 순수 매핑 함수 구현 (Sub-AC 2-1-A)

guidedInputMapper 모듈을 추가하고 단위 테스트 51개 작성.
LLM 호출 없이 결정론적으로 동작하는 순수 함수로 구현:
- mapBudgetToConstraints: 예산 범위 -> price_min/price_max
- mapConnectionToConstraints: 연결방식 선택지 -> connection/wireless_type
- mapLayoutToConstraints: 크기 선택지 -> layout (매핑 불가 선택지는 하드 제약 미생성)
- mapEngravingToConstraints: 각인 선택지 -> engraving
- mapBacklightToConstraints: 백라이트 선택지 -> backlight
- mapKeyFeelToConstraints: 키감 선택지 -> switch_type
- guidedAnswersToHardConstraints: 전체 answers+budget 통합 변환
전체 테스트 221개 통과

* feat(keybuddy): checkFormFactorViolation 함수 구현 및 단위테스트 추가

폼팩터 하드 제약 위반 판정 함수(checkFormFactorViolation)를
hardFilter.ts에 추가하고, 19개 단위테스트를 작성했다.
- 일치 시 false(6케이스), 불일치 시 true(9케이스), 태그없음 시 false(4케이스)
- 전체 테스트 240개 통과 (기존 221개 + 신규 19개)

* feat(keybuddy): 키보드 데이터셋 스키마 추출 함수 구현

Sub-AC 3-2-a: extractDatasetSchema(keyboards) 함수 추가
- Keyboard[] 입력 -> Record<속성명, Set<값>> 반환
- getFieldValues / hasField / hasValue 헬퍼 함수 포함
- 단위 테스트 39개 작성 (빈 배열, 속성명 목록, 값 추출, 중복 제거, 실제 샘플 검증 등)
- 전체 279개 테스트 통과

* feat(keybuddy): checkBudgetViolation 함수 구현 및 단위테스트 추가

- hardFilter.ts에 checkBudgetViolation(keyboard, budgetTag) 추가
  - budgetTag <= 0: 제약 없음으로 간주하여 false 반환
  - keyboard.price > budgetTag: 위반(true)
  - keyboard.price <= budgetTag: 통과(false), 경계값 포함
- hardFilter.test.ts에 13개 테스트 케이스 추가
  - 가격 < 상한, 가격 = 상한(경계값), 가격 > 상한, budgetTag <= 0 시나리오 포함
- 전체 292개 테스트 통과

* feat(keybuddy): 속성명 무결성 검사 함수 구현 및 테스트 추가

- datasetSchema.ts에 findInvalidFieldNames 함수 추가
  - 규칙표 술어의 모든 field 이름 수집 후 스키마 맵과 대조
  - 스키마에 없는(무효) 속성명을 중복 없이 반환
  - PredicateRuleEntry 제네릭 인터페이스로 순환 의존 없이 SoftTagRuleEntry 호환
- datasetSchema.test.ts에 findInvalidFieldNames 단위 테스트 9건 추가
  - 모두 유효: 빈 배열 반환 검증
  - 모두 무효: 전체 반환 검증
  - 유효/무효 혼재: 무효 속성명만 정확히 탐지
  - 중복 field: Set 기반 중복 제거 보장
  - 빈 규칙표/빈 스키마 경계 케이스 처리
- 전체 테스트 301개 통과 (기존 292개 + 신규 9개)

* feat(keybuddy): 소프트 의도 태그 단계별 순수 매핑 함수 구현 (Sub-AC 2-1-B)

- guidedInputMapper에 소프트 태그 전용 상수 추가 (PURPOSE_OPTIONS, PORTABILITY_OPTIONS, SOUND_OPTIONS, KEY_FORCE_OPTIONS)
- 소프트 전용 단계 매핑 함수: mapPurposeToSoftTags, mapPortabilityToSoftTags, mapSoundToSoftTags, mapKeyForceToSoftTags
- 하드 제약 단계의 소프트 태그 보완 함수: mapKeyFeelToSoftTags, mapLayoutToSoftTags, mapConnectionToSoftTags, mapBacklightToSoftTags, mapEngravingToSoftTags
- guidedAnswersToSoftTags 통합 함수 (중복 제거 포함)
- guidedSoftTagMapper.test.ts: 소프트 태그 매핑 단위 테스트 75개 추가
- 모든 매핑 함수는 LLM 호출 없는 동기 순수 함수, 전체 376개 테스트 통과

* feat(keybuddy): checkHardConstraintViolation 디스패처 함수 구현 (Sub-AC 4-1-5)

- HardTag 타입 정의: type(판별자) + value(제약값) 구조
- checkHardConstraintViolation 함수 구현:
  - 'layout' -> checkLayoutViolation (Sub-AC 4-1-1)
  - 'switch_type' -> checkSwitchViolation (Sub-AC 4-1-2)
  - 'form_factor' -> checkFormFactorViolation (Sub-AC 4-1-3)
  - 'price_max' -> checkBudgetViolation (Sub-AC 4-1-4)
  - 알 수 없는 유형 -> false (안전한 기본값)
- 디스패처 단위테스트 추가 (6개 describe, 30개 케이스):
  라우팅 정확성, 직접 호출과의 결과 동일성, 알 수 없는 유형, 복합 시나리오

* feat(keybuddy): 속성 값 무결성 검사 함수 구현 (Sub-AC 3-2-c)

- datasetSchema.ts에 findRulesWithInvalidValues 함수 추가
  - 속성명이 스키마에 존재하는 경우에 한해 허용 값 집합 밖 값을 참조하는 규칙 반환
  - eq 술어: 스키마 값 집합에 정확히 포함되는지 검사
  - contains 술어: 스키마 값 중 하나라도 부분 문자열로 포함하는지 검사
  - lte/gte 술어: 수치 비교 임계값이므로 검사 대상에서 제외
  - 속성명이 스키마에 없으면 해당 술어는 검사 범위 아님(findInvalidFieldNames 담당)
- PredicateWithValue, RuleEntryWithValues 제네릭 인터페이스 추가
- datasetSchema.test.ts에 19개 단위 테스트 추가 (총 421개 통과)

* feat(keybuddy): filterByHardConstraints 구현 및 단위테스트 추가

keyboards 배열과 hardTags 배열을 받아 모든 하드 제약을 만족하는
키보드만 반환하는 filterByHardConstraints 함수 구현 (Sub-AC 4-2).

- hardFilter.ts: filterByHardConstraints 추가 (checkHardConstraintViolation 기반)
- hardFilter.test.ts: filterByHardConstraints 단위테스트 5개 describe 블록 추가
  - 빈 태그 배열 / layout 단일 / price_max 단일 / 복합(layout+price+switch) / violations===0 보장
- 전체 테스트 434개 통과 (기존 401개 + 신규 33개)

* feat(keybuddy): 단계 번호 기반 TagSet 디스패처 함수 구현 (Sub-AC 2-1-C)

- TagSet 인터페이스 추가 (hard: HardConstraints, soft: SoftIntentTag[])
- dispatchStepToTagSet(stepIndex, value): 0-9 단계 번호와 선택값을 받아
  하드/소프트 매핑 함수에 라우팅하고 TagSet 반환
- STEP_ID_TO_INDEX 테이블 및 dispatchStepIdToTagSet 헬퍼 추가
- 예산 단계(7)는 { min, max } range 입력, 나머지 string 입력 처리
- 타입 불일치/알 수 없는 단계 -> { hard: {}, soft: [] } 안전 반환
- LLM 호출 없이 결정론적 동작
- 10단계 전체 시나리오 단위 테스트 (stepDispatcher.test.ts) 추가
- 전체 511개 테스트 통과

* test(keybuddy): validateTagSchema 엣지 케이스 단위 테스트 추가

하드 제약 필드 누락/타입 오류/소프트 의도 필드 누락 케이스를 명시적으로 커버
- hardConstraints: null, [], string 타입 오류 검증
- price_min 숫자/문자열 검증
- softIntentTags: null, 숫자 요소, null 요소 검증
- 각 필드 누락 시 상대 필드 오류 미발생 확인

tagSchema.test.ts: 47 -> 57개 테스트 (전체 521개 통과)

* test(keybuddy): selectionOptionConverter 추가 및 validateTagSchema 통과 단위 테스트 작성

- guidedInputMapper에 selectionOptionConverter 함수 추가
  - guidedAnswersToHardConstraints + guidedAnswersToSoftTags를 ExtractedTags 인터페이스로 통합
  - 자유형 자연어 경로(extractRawTags)와 동일한 { hardConstraints, softIntentTags } 반환
  - LLM 호출 없이 결정론적으로 동작

- selectionOptionConverter.test.ts 신규 작성 (Sub-AC 2-2b)
  - 출력 구조 보장: hardConstraints(객체) + softIntentTags(배열) 항상 존재
  - validateTagSchema 통과 검증: 빈/부분/전체 입력 + 대표 시나리오 8개
  - 하드 제약 단계 단일 입력 15케이스 스키마 통과 확인
  - 소프트 전용 단계 7케이스 스키마 통과 확인
  - 예산 입력 4케이스 스키마 통과 확인
  - softIntentTags 항목이 모두 SOFT_INTENT_VOCAB에 속함 검증
  - 결정론성: 동일 입력 -> 항상 동일 출력
  - LLM 호출 없는 동기 순수 함수 확인

전체 583개 테스트 통과

* test(keybuddy): Sub-AC 2-3-a 선택 경로와 자유형 경로의 하드 필터 결과 동일성 검증

선택 변환 태그(selectionOptionConverter)와 자유형 태그(sanitizeTags 시뮬레이션)가
동일한 하드 제약을 표현할 때 filterByHardConstraints 결과가 일치함을 검증하는
24개의 단위 테스트를 작성한다.

- layout(텐키리스/미니/풀배열) 단일 제약 동일성 검증
- switch_type(기계식/무접점) 단일 제약 동일성 검증
- price_max 단일 제약 동일성 검증
- 복합 제약(layout + switch_type + price_max) 동일성 검증
- 빈 제약, 불가능한 제약(결과 0건) 경우 검증
- 소프트태그 차이가 하드 필터 결과에 영향 없음 검증
- 1800배열 선택 시 layout 하드 제약 없음 동일성 검증
- 8개 시나리오 테이블 기반 통합 검증
- hardConstraintsToHardTags 변환 헬퍼를 테스트 내부에 정의
- 모든 결과에서 violations === 0 단언 포함

* test(keybuddy): Sub-AC 2-3-b 소프트태그 스코어 패리티 단위 테스트 추가

- softScorer.ts: SOFT_TAG_RULE_TABLE 기반 결정론 스코어링 함수 구현
  - evaluatePredicate: 속성 술어 평가 (eq/contains/lte/gte)
  - scoreBySoftTags: 키보드별 점수 벡터 산출 (LLM 없이 순수 함수)
  - deriveRankOrder: 점수 벡터 -> 순위 배열 변환

- softScoreParity.test.ts: 40개 단위 테스트 추가
  - 선택 경로(guidedAnswersToSoftTags) 소프트태그와
    자유형 경로(sanitizeTags 시뮬레이션) 소프트태그를
    scoreBySoftTags에 전달했을 때 스코어 벡터가 일치하는지 검증
  - 7개 의도 시나리오: 사무용+조용함, 게이밍+RGB, 무선+미니+휴대성,
    기계식+타건감+경쾌함, 무접점+조용함, 가성비+풀배열, 복합 전체 단계
  - 결정론성, 태그 순서 무관 점수 동일, 순위 배열 일치 검증
  - 전체 647개 테스트 통과

* test(keybuddy): 검색 엔진 진입점 및 통합 테스트 추가 (Sub-AC 2-3-c)

- searchEngine.ts: ExtractedTags -> SearchOutput 결정론 검색 진입점 구현
  - hardConstraintsToHardTags: HardConstraints -> HardTag[] 변환 (export)
  - searchKeyboards: 하드 필터 + 소프트 스코어링 + 순위 정렬 파이프라인
  - 결과 0건 시 HARD_CONSTRAINT_RELAXATION_ORDER 역순 완화 폴백
  - LLM 호출 없이 순수 함수로 동작 (자유형/단계선택 공통 진입점)
- searchEngineIntegration.test.ts: 49개 통합 테스트
  - 선택 입력(selectionOptionConverter) -> searchKeyboards 동일 시그니처 검증
  - 자유형(sanitizeTags) -> searchKeyboards 동일 시그니처 검증
  - LLM 없이 동기 순수 함수 확인 (Promise 미반환)
  - 하드 제약 위반 0건 보장 (layout/switch_type/price_max)
  - matchedTags(매칭 근거) 포함 확인
  - 폴백 완화 정보 포함 확인
  - 결정론성(동일 입력 -> 동일 출력) 확인
총 696개 테스트 통과

* feat(keybuddy): detectNoResult 함수 추가 및 단위테스트 작성

하드 제약 필터 결과 유무를 신호 객체로 반환하는 detectNoResult 구현.
- 빈 배열이면 { type: 'NO_MATCH' } 반환
- 1개 이상이면 { type: 'OK' } 반환
- 단위테스트 8개 추가 (전체 704개 통과)

* test(keybuddy): 소프트 태그 점수 단조성 단위테스트 추가

AC5 - 소프트 의도 태그가 더 많이 겹치는 키보드가 더 높은 점수와 상위 랭크를 받는다.

- 0~5개 태그 매칭 픽스처로 기본 점수 단조성 검증
- deriveRankOrder 결과가 score 내림차순임을 검증
- 태그 1개 추가 시 score 정확히 1 증가 검증 (엄격 단조성)
- 게이밍/사무용 시나리오별 단조성 검증
- 동점 시 keyboardIndex 오름차순 정렬 검증
- 태그 추가/제거에 따른 단조 증가/감소 속성 검증
- 전체 741개 테스트 통과

* test(keybuddy): getRelaxationOrder 함수 구현 및 단위 테스트 추가

Sub-AC 6-2: priority 속성을 가진 하드 제약 배열을 낮은 우선순위 항목이 먼저
오도록 오름차순 정렬하는 getRelaxationOrder 함수를 searchEngine.ts에 추가하고
22개 단위 테스트로 검증. 원본 불변성, 빈/단일/동일 priority 엣지 케이스 포함.

* feat(keybuddy): relaxAndSearch 함수 구현 및 단위 테스트 추가

searchFn 결과가 0건이면 HARD_CONSTRAINT_RELAXATION_ORDER 역순 우선순위로
제약을 1개씩 제거하며 searchFn을 재호출하고, 첫 번째 비공(非空) 결과 시점에
{ results, relaxedConstraints, relaxationSteps } 구조를 반환한다.

- relaxAndSearch<T> 제네릭 함수: searchEngine.ts에 추가
- 루프 내 searchFn 호출 시 { ...currentConstraints } 복사본 전달 (mock call 기록 안전성)
- 32개 신규 단위 테스트: 즉시 반환/완화 순서/반환 구조/불변성/제네릭 검증
- 전체 테스트 799개 통과 (기존 767개 + 신규 32개)

* test(keybuddy): 하드 제약 태그 추출 정확도 골드셋 테스트 추가 (Sub-AC 1)

대표 자연어 쿼리 10개(GS-1~GS-10)에 대해 하드 제약 추출 함수가
기대 태그 셋을 반환하는지 검증하는 골드셋 단위 테스트 작성.

- 가격 범위(price_max/price_min), 스위치 타입, 폼팩터, 연결 방식,
  백라이트, 각인, 무게 등 모든 하드 제약 차원 커버
- 복합 제약(여러 하드 제약 동시) 시나리오 포함
- 제약 없는 입력에서 hardConstraints 빈 객체 보장 검증
- 전체 840개 테스트 통과 (기존 799 + 신규 41)

* feat(keybuddy): Sub-AC 7-1 - 결과 객체 매칭 소프트태그 결정론 함수 추가

- softScorer.ts에 getMatchedSoftTags(keyboard, softTags) 함수 export
  - 단일 키보드와 쿼리 소프트태그 집합을 입력받아 교집합 태그 배열 반환
  - 입력 softTags 순서를 보존하여 결과 순서도 결정론적
  - LLM 호출 없이 순수 함수로 동작
- 단위 테스트 23개 추가 (getMatchedSoftTags.test.ts)
  - 정확한 교집합 반환 (매칭/비매칭 구분, 전체/부분/전무 매칭)
  - 빈 입력 처리 (빈 softTags -> 빈 배열)
  - 결정론성: 동일 입력 10/100회 반복 -> 항상 동일 출력
  - 순서 보존: 입력 순서와 동일한 순서로 반환
  - 부수효과 없음: 호출 후 keyboard/softTags 객체 불변
  - scoreBySoftTags matchedTags 필드와 결과 일관성 검증

* test(keybuddy): 소프트 의도 태그 추출 정확도 골드셋 테스트 추가

10개 대표 자연어 쿼리에 대해 softIntentTags 추출 결과를 검증하는
goldset 테스트(Sub-AC 2) 추가.

- 사무용/조용함, 게이밍/RGB, 휴대성/무선, 가성비, 타건감/기계식,
  저소음/무접점, 멀티페어링, 풀배열, 백라이트없음, 경쾌함 각 시나리오
- 스키마 밖 태그 폐기 검증 (2개)
- SOFT_INTENT_VOCAB 내 값만 반환 보장 (5개)
- 신규 17개 테스트, 전체 857개 통과

* feat(keybuddy): evaluateConstraint 단일 하드 제약 술어 함수 구현 (Sub-AC 7-2a)

- lib/evaluateConstraint.ts: numeric_range(lte/gte/eq), enum_match, boolean_flag 타입별 제약 평가
- attributes 키 부재/타입 불일치 시 false(fail) 반환 (안전한 기본값)
- __tests__/evaluateConstraint.test.ts: 타입별 충족/위반/경계값/타입불일치/키부재 케이스 63개 추가
- 전체 테스트 857 -> 920개 (+63)

* test(keybuddy): Sub-AC 2a - 정적 규칙표 맵 구조 골드셋 검증 테스트 추가

- softTagRules.ts에 SOFT_TAG_RULE_MAP(Record 형태) export 추가
  - 배열 형태의 SOFT_TAG_RULE_TABLE을 soft-tag -> 술어 배열 맵으로 제공
  - 각 술어는 {field(attribute), op(operator), value} 구조
- softTagRuleMap.goldset.test.ts 신규 작성 (Sub-AC 2a)
  - 골드셋 10개 태그가 SOFT_TAG_RULE_MAP 키로 모두 존재하는지 검증
  - 각 값이 배열이고 최소 1개 이상의 술어를 포함하는지 검증
  - 모든 술어가 {field, op, value} 구조(개념적 {attribute, operator, value})인지 검증
  - field/op/value 각각의 타입과 허용 범위 검증
  - 골드셋 태그별 기대 매핑 내용(조용함->switch_type, 게이밍->backlight 등) 검증
  - SOFT_INTENT_VOCAB 전체 완전성 검증
- 신규 56개 테스트 추가, 전체 976개 통과

* feat(keybuddy): buildConstraintStatusMap 구현 (Sub-AC 7-2b)

- IdentifiedConstraint(Constraint & {id}) 타입 추가
- ConstraintStatusMap(Record<string,boolean>) 타입 추가
- buildConstraintStatusMap(constraints, attributes): 내부적으로
  evaluateConstraint(7-2a)를 활용해 각 제약 ID를 키로
  pass/fail 맵을 반환
- 빈 제약 목록, 단일/복수 제약, 혼합 위반/충족, 빈 속성 맵,
  중복 ID 등 27개 단위 테스트 전부 통과

* test(keybuddy): expandSoftTag 단위 테스트 추가 (Sub-AC 2b)

- softTagRules.ts에 expandSoftTag(tag, ruleMap?) 함수 추가
  - 알려진 소프트 태그 -> SOFT_TAG_RULE_MAP의 AttributePredicate[] 반환
  - 미지 태그(규칙표 밖) -> 빈 배열([]) 반환 (폴백)
  - 커스텀 ruleMap 주입 지원 (의존성 역전)

- expandSoftTag.test.ts 신규 작성 (Sub-AC 2b 커버리지)
  - Happy path: SOFT_INTENT_VOCAB 24개 전체 골드셋 기대 술어 검증
  - 미지 태그 16종 입력 시 빈 배열 반환 검증
  - 반환 배열 참조 일관성 (SOFT_TAG_RULE_MAP과 동일 참조)
  - 커스텀 ruleMap 주입 시나리오
  - 결정론성 (동일 입력 항상 동일 출력)

전체 1144개 통과 (기존 799개 + 신규 345개)

* test(keybuddy): 하드 제약 필터 LLM 미호출 단위 테스트 추가 (Sub-AC 7.3.1)

vi.fn() spy로 LLM 클라이언트를 교체한 뒤 filterByHardConstraints 등
하드 제약 필터 함수만 단독 실행하고 LLM 호출 횟수가 0임을 검증
- _setClientForTest로 mock client 주입, afterEach에서 null로 해제
- filterByHardConstraints / checkHardConstraintViolation / 개별 위반 함수 / detectNoResult 모두 포함
- 연속 10회 호출, 혼합 호출 등 다양한 시나리오에서 createSpy.toHaveBeenCalledTimes(0) 확인
- 신규 22개 테스트 모두 통과

* test(keybuddy): Sub-AC 7.3.2 소프트 태그 스코어링 LLM 미호출 검증 테스트 추가

scoreBySoftTags, deriveRankOrder, getMatchedSoftTags 함수 단독 실행 후
vi.fn() spy로 교체한 LLM 클라이언트 호출 횟수가 0임을 검증하는
21개 단위 테스트 추가.

- scoreBySoftTags: 단일/다중/전체 태그 시나리오, 게이밍/사무용 의도
- deriveRankOrder: 정상/동점/빈/단일 원소 점수 벡터
- getMatchedSoftTags: 단일 키보드, 무선, 매칭 없음
- 파이프라인(스코어링 -> 순위) 연결 검증
- 연속 10회 호출 누적 0회 검증
- spy sanity check (수동 호출 시 횟수 증가 확인)

* test(keybuddy): Sub-AC 7.3.3 결과 조합 모듈 LLM 호출 횟수 0 검증

결과 조합/매칭 근거 생성 함수(buildReason, toRecommendations, buildSummary)를
export하고 vi.fn() spy로 LLM 클라이언트 교체 후 호출 횟수가 0임을 검증하는
단위 테스트 28개 추가.

- recommend.ts: buildReason/toRecommendations/buildSummary export 추가
- resultCompositionLlmNoCall.test.ts: Sub-AC 7.3.3 테스트 파일 신규 생성
  - buildReason: 일반/폴백/다수태그 시나리오 LLM 미호출 검증
  - buildSummary: 결과 있음/없음/폴백/MAX_RESULTS 경계 LLM 미호출 검증
  - toRecommendations: 단일/다중/빈/폴백/12개 초과 슬라이스 LLM 미호출 검증
  - 전체 파이프라인(게이밍/사무용/무선 시나리오) LLM 미호출 검증
  - 연속 10회 반복 호출에서도 LLM 누적 호출 횟수 0 검증
  - spy sanity check: spy 교체 확인 및 수동 호출 시 횟수 증가 확인

* fix(keybuddy): Seed metadata.version 문자열 타입으로 수정

ooo run Seed 검증이 metadata.version을 string으로 요구하여 검증 실패가 발생.
정수 1 -> "1"로 수정해 실행 검증을 통과하도록 함.

* feat(keybuddy): filterByConstraintPredicates 술어 기반 필터 추가 (Sub-AC 3-1)

중단된 ooo run 실행의 마지막 슬라이스로, evaluateConstraint 기반 술어 필터
함수와 단위 테스트(50여건)를 추가. 함수와 테스트는 완결·통과 상태이나
현재 검색 경로(searchEngine은 filterByHardConstraints 사용)에는 아직 미배선.

* fix(keybuddy): 하드 제약 6종 enforcement 누락 수정 + 의도 골드셋

checkHardConstraintViolation 디스패처가 connection/price_min/wireless_type/
engraving/backlight/weight_max_g를 처리하지 않아(default false) 검색에서
조용히 무시되던 버그를 수정. searchEngine은 이 6종도 HardTag로 내보내므로
무선/유선·최소가격·각인·백라이트·무게 조건이 실제로는 무시되고 있었음
(우리가 고치려던 '조건 무시'의 핵심 잔존 버그).

- connection은 '유선+무선' 호환 매칭(유선/무선 요구를 모두 충족)
- wireless_type은 부분 일치, 나머지는 정확 일치 / price_min gte / weight_max_g lte
- '미구현이라 무시됨'을 단언하던 기존 테스트를 올바른 동작으로 갱신
- 무결과 완화 통합테스트(사무용 무접점 텐키리스 유선)를 connection 완화 fallback으로 보정
- AC8 의도 골드셋(intentGoldset) 추가: 하드 제약 충족 + 의도 상위랭크 + 완화 검증

* fix(keybuddy): npm run build(tsc) 통과하도록 타입 에러 정리

중단된 ooo run이 남긴 테스트 파일들의 빌드 차단 타입 에러를 해소:
- getRelaxationOrder/tagSchema/selectionOptionConverter 테스트의 잘못된 캐스팅을
  unknown 경유 또는 readonly 보존으로 수정, toBe 메시지 인자를 expect 인자로 이동
- noUnusedLocals/noUnusedParameters는 false로 완화(미사용 변수 개별 삭제 대신
  정책으로 처리; 린터 영역). 런타임/lib 코드는 타입 클린.

tsc --noEmit + vite build 통과 확인, 1248 테스트 유지.

* feat(keybuddy): 태그 추출 견고화 + 개발용 콘솔 로그

- extractRawTags: LLM 응답이 코드펜스/설명과 함께 와도 JSON 객체를 추출하도록
  parseAndSanitize 보강(빈 태그로 떨어지던 문제 수정). 추출 모델 sonnet-4-6.
- 개발 모드(import.meta.env.DEV)에서 LLM 추출 원문과 추출된 하드/소프트 태그,
  검색 결과 요약을 브라우저 콘솔에 출력(프로덕션 빌드 제외).

* docs(keybuddy): 의도->다차원 속성 프로파일 하네싱 Seed 명세 추가

소프트 의도를 차원별 강도(필수=하드/선호=소프트/상관없음) 프로파일로 펼치는
하네싱 층 요구사항을 Seed로 명세. LLM은 NL->의도 어휘+명시 제약 매핑만,
확장/검색은 기존 SOFT_TAG_RULE_TABLE 재사용한 결정론. 명시 우선, 다중 합집합,
모순시 기존 완화. (ooo interview/seed Path B, fallback QA 0.91)

* feat(keybuddy): 의도->다차원 프로파일 정적 문서 + 결정론 확장 (AC1/3/4/5)

INTENT_PROFILE_TABLE(LLM초안+사람검수 정적 문서)과 expandIntents 구현.
- 차원별 강도: 필수->requiredTags(하드 승격) / 선호->softIntentTags / 상관없음->없음
- 명시 우선: 명시된 차원은 의도 프로파일이 건너뜀
- 다중 의도 합집합, 어휘 밖 의도 폐기, 필수 태그는 소프트에서 제거
- 완전성 검사 checkIntentProfileCompleteness. 17 단위테스트.

* feat(keybuddy): 의도 확장 검색 엔진 searchWithProfile (AC5/6)

필수 승격 태그를 하드 필터로 적용(위반 0건)하고 선호 태그로 점수 랭킹.
결과 0건이면 [의도 파생 필수 태그 -> 명시 하드 키] 순서로 단계 완화
(명시 우선 원칙: 의도 파생 제약을 명시 제약보다 먼저 완화).
searchEngine 코어 미수정, 그 export+softScorer+hardFilter 조합. 11 단위테스트.

* feat(keybuddy): extractIntentInput - 의도+명시 제약 분리 추출 (AC2)

LLM이 자연어를 {의도 어휘(INTENT_VOCABULARY), 명시 하드 제약, 명시 소프트 태그}로
분리 추출. 어휘/스키마 밖 값은 폐기. 코드펜스/설명 섞여도 파싱.
parseIntentInput(순수) + extractIntentInput(LLM). 8 단위테스트(모킹 골드셋 포함).

* feat(keybuddy): recommend를 의도 하네싱으로 배선 (AC7)

자유형: extractIntentInput -> expandIntents -> searchWithProfile.
단계선택: 용도/휴대성 -> 의도 + selectionOptionConverter(명시) -> 동일 경로.
LLM은 자유형 의도/제약 추출에만, 확장/검색은 LLM 0회(카운트 검증 테스트).
dev 콘솔 로그를 의도/필수 승격/선호 태그까지 표시하도록 확장. build+1284테스트 통과.

* docs(keybuddy): 의도 하네싱 전/후 정성 비교 리포트 + 생성기 (AC8)

대표 쿼리 10개를 결정론 추출값으로 평면 추출 vs 하네싱 비교.
지표=상위5 의도 필수 위반: BEFORE 14건 -> AFTER 5건(비완화 케이스 0건,
잔여는 필수 모순 완화에서 발생-설계상 의도).
scripts/intent-harness-report.ts(vite-node) + intent-harness-before-after.md.

* docs(keybuddy): 자연어->태그 추출 플로우 문서 + README 현행화

- docs/tag-extraction-flow.md: 2단계(LLM 의도/제약 추출 -> 결정론 확장/검색)
  플로우를 실제 코드 기준으로 상세 정리(예시 추적 포함)
- README: 구버전(LLM 인덱스 직접 선택) 설명을 현재 태그 파이프라인+의도 하네싱으로
  교정, 동작 요약 + 문서 링크 추가

* fix(keybuddy): 명시 정숙 요구를 방향 인식으로 하드 강화

조용한 사무실처럼 사용자가 정숙을 직접 말하면 명시 우선 규칙이 사무용의
저소음=필수를 건너뛰어 정숙이 점수로만 약화되고 기계식이 배제되지 않던 문제 수정.

- intentProfile에 소음 축 극성(TAG_POLARITY) 추가. 명시 소프트가 의도 필수와
  같은 차원이면 방향으로 판단: 같은 방향이면 양보 대신 하드로 강화, 충돌이면 양보
- 추출 프롬프트에 소음 축 상반 태그(정숙 vs 소란) 동시 추출 금지 규칙 추가
- 방향 강화/충돌 단위 테스트 추가, 문서 갱신

* feat(keybuddy): 추천 노출 상한 12 -> 30

결과가 항상 12개로 잘리던 제한을 30개로 확대.
MAX_RESULTS 경계 테스트도 30 기준으로 갱신

* chore: CodeRabbit 자동 코드 리뷰 설정 추가

한국어(ko-KR) 리뷰, main 대상 자동 리뷰, 산출물/lock/node_modules 제외.
프론트엔드(ts/tsx)와 Python 크롤러에 경로별 리뷰 지침,
ruff/eslint/markdownlint 도구 연동 설정.

* chore: CodeRabbit 제거하고 Claude Code Action 자동 PR 리뷰로 전환

CodeRabbit(private 14일 트라이얼) 대신 GitHub Actions에서
anthropics/claude-code-action으로 PR opened/synchronize 시 자동 리뷰.
구독 OAuth 토큰 인증으로 추가 API 과금 없음.
한국어 리뷰 지침과 경로별 중점은 REVIEW.md로 분리.

* feat: keybuddy 프론트 추천 앱 추가

* feat: Supabase Edge Function 추천 API 추가

* docs: Supabase 기반 실행 및 배포 방법 정리

* "Claude PR Assistant workflow"

* "Claude Code Review workflow"

* chore: 프로토타입, 데이터 크롤러, 추가 수집

* fix: Supabase 추천 함수 호출 설정 수정

* chore: keybuddy Supabase 구조를 impl 경로로 정리

* fix: 추천 태그를 DB 속성 기반으로 수정

* chore: 추천 모델을 gpt-5.4로 변경

* fix(keybuddy): PR 리뷰 반영 - LLM 프롬프트 오타·에러 처리·데드코드 정리

- LLM 프롬프트 오타 수정: "텍키리스" -> "텐키리스" (텐키리스 레이아웃 선호가 어휘 밖 값으로 조용히 폐기되던 문제)
- LLM 호출(extractRawTags/extractIntentInput)에 try/catch 추가, 네트워크/레이트리밋/키 오류 시 빈 추출로 graceful degrade (UI 크래시 방지)
- 데드코드 checkFormFactorViolation 제거 (layout 판정과 중복, 디스패처에서 도달 불가). 관련 테스트도 정리
- 결과 인덱스 조회를 catalog.indexOf(O(n))에서 Map 사전 구축(O(1))으로 변경 (intentSearch/searchEngine)
- searchKeyboards에 legacy 주석 추가 (메인 추천 경로는 searchWithProfile)
- 완화 루프(drop)와 satisfiesHardConstraints 필드의 의미를 주석으로 명확화

API 키 브라우저 노출 항목은 사용자 요청으로 이번 PR에서 보류.

* ci(review): Claude PR 리뷰를 라인별 inline 코멘트로 전환

단일 요약 코멘트 대신 문제 코드 라인마다 개별 코멘트가 달리도록
inline 코멘트 MCP 도구를 allowedTools로 허용하고 프롬프트에 명시.
inline 앵커링을 위해 checkout fetch-depth를 0으로 설정.

* ci(review): claude-code-action OIDC 인증용 id-token 권한 추가

claude-code-action v1이 GitHub 토큰을 OIDC로 발급받는데 id-token: write
권한이 없어 OIDC 토큰 획득에 실패하며 워크플로가 종료되던 문제 해결.

* ci: Claude 리뷰 워크플로 OIDC 권한 추가

* fix: 추천 API 오류 처리 보강

* fix: 추천 API 리뷰 지적사항 반영

* docs: keybuddy 실행 환경 설정 가이드 보강

* feat(keybuddy): 결과 화면에 구매하기 버튼과 별점 피드백 추가

디자인(untitled.pen)에 맞춰 ResultView에 두 기능을 프론트 UI/상태로 구현.

- 각 제품 카드 가격 아래에 장바구니 아이콘 + '구매하기' primary 버튼 추가
- 구매하기 클릭 시 하단 중앙 공유 toast 1개를 2초간 표시(자체 구현, 외부 의존성 없음)
- 결과 리스트 아래에 단일 별점 피드백 카드 추가(별 5개, hover 미리보기, 재선택 가능)
- 별점 선택 시 양끝 라벨('아쉬워요'/'완벽해요')을 '감사합니다 (N점)'으로 교체

* docs: 프로젝트 메모리에 git 브랜치 작업 규칙 추가

ellipsis에 직접 커밋하지 않고 새 브랜치를 만들어 작업하도록 명시

* feat(keybuddy): 별점 0.5점 단위 선택/반 개 표시 지원

별 하나를 좌/우 절반 클릭 영역으로 분리(좌=0.5점, 우=1점)하고,
채움 별을 overflow-hidden 너비로 클리핑해 0.5점일 때 반 개만 표시

* fix: 추천 결과 최대 개수 30개로 복원

* fix: 추천 후보와 출력 여유 확대

* fix: 추천 요청 timeout 여유 확대

* fix: 추천 응답 안정성 보강

* docs: 스위치 정보 catalog 정의

* docs: catalog와 유사한 스위치 이름 매핑을 위한 데이터

* docs: 실제 UI에 표시할 스위치 데이터

* feat: 키보드 스위치 옵션 매칭 및 가격비교 링크 수집 추가

* docs: 변경된 크롤러로 수집한 키보드 데이터

* docs: 변경된 크롤러로 수집한 키보드 데이터

* docs: 스위치 정보 테이블에 일치하는 스위치가 없는 키보드 로그

* docs: 크롤링 데이터에 대한 설명 추가

* chore: 최신 키보드 데이터 600개 동기화

* feat: 키보드 스위치 데이터 타입 확장

* test: 데이터셋 스키마 boolean 및 null 처리 검증

* chore: 추천 함수 키보드 타입 동기화

* feat: 스위치 그래프 계산 로직 추가

* test: 스위치 그래프 계산 규칙 검증

* feat: 결과 화면 단계 그래프 UI 추가

* fix(keybuddy): 결과 화면 리마운트/타이머 누수 수정 및 별점 hover 정밀화

PR #8 리뷰 반영.

- HomeView/StepByStepView/ResultView를 App 함수 밖으로 이동하고 props로 의존성을 전달. App 리렌더(예: setLoading(false))마다 컴포넌트 타입 참조가 새로 생성돼 전체가 언마운트/리마운트되며 state·타이머가 초기화되던 문제 해결
- useAutoDismiss 훅을 신설해 toast와 "복사됨" 피드백의 auto-dismiss를 통합. ref+clearTimeout으로 직전 타이머를 정리하고, 언마운트 시 cleanup으로 타이머 누수 방지. handleCopy의 clearTimeout 누락도 함께 해결
- 별점 hover를 좌/우 분할 버튼 onMouseEnter에서 별 컨테이너 단위 onMouseMove + offsetX 판별로 변경해 빠른 포인터 이동 시 반쪽을 놓치던 문제 보완. 자식 Star는 pointer-events-none 처리
- 별점 위젯에 role="slider"와 좌우 화살표 키 핸들러를 추가해 키보드/스크린리더 접근성 보강

* feat: 검색 결과 카드 UI 및 제품 정보 연결

* fix: 추천 결과 부족분 후보로 보강

* feat: 범용 스위치 그래프 매핑 추가

* chore: document edge function versioning

* fix: address keybuddy review comments

* fix: address follow-up keybuddy reviews

* fix: PR 리뷰 피드백 반영

* fix: address remaining keybuddy reviews

* fix: address latest keybuddy review comments

* fix: address keybuddy follow-up review comments

* fix: refine keybuddy deploy and rate limit handling

* fix: harden keybuddy function deploy workflow

* fix(keybuddy): 결과 화면 메모이즈/클립보드 폴백 및 훅 참조 안정화

- filterOptions/processed를 useMemo로 감싸 별점 hover 리렌더마다 재계산 방지
- useAutoDismiss의 show를 useCallback으로 고정하고 duration을 ref로 읽어 최신값 반영
- 클립보드 미지원/실패 시 토스트로 사용자 피드백 제공
- product_name 누락 시 idx로 key 폴백
- 별점 표시값(displayed) 계산을 map 바깥으로 호이스팅

* Update impl/keybuddy/frontend/src/App.tsx

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Update impl/keybuddy/frontend/src/App.tsx

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Update impl/keybuddy/frontend/src/App.tsx

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* fix: typecheck keybuddy edge function deploy

* fix: 검색 결과 제품 태그 표시 개선

원본 스위치 이름을 우선 표시하고 연결 방식의 중복 태그를 제거한다. 백라이트 값은 백라이트: RGB, 단색, 없음 등의 일관된 형식으로 정규화한다.

* fix: 미제공 키보드 무게 null 처리

무게 타입을 nullable로 확장하고 무게 제약 및 소프트 점수 계산에서 null을 명시적으로 처리한다. 추천 카탈로그 문자열에는 nullg 대신 무게 미제공을 출력한다.

* docs: 3주차 주간 보고서 작성

* docs: 3주차 주간 보고서 수정

* fix: 검색 결과 필터 카운트 표시 개선

* feat: 추천 결과 초보자 설명 추가

* fix: 초보자 설명 리뷰 반영

* feat(keybuddy): 홈 화면을 세그먼트 토글 레이아웃으로 재디자인

Pencil 새 메인 화면(시안 A) 반영.
- 상단 '자유롭게 입력 / 단계별 선택' 세그먼트 토글로 입력 방식 전환
- 자유 입력 탭: 텍스트 입력창 + '분석하기' 버튼
- 단계별 탭: 용도/타건감/예산 칩 미리보기 + '단계별로 시작' 버튼으로 단계 진입
- 부제를 '원하는 방식으로 키보드를 찾아보세요.'로 수정, 예시 질문 섹션 유지

homeTab 상태로 토글 모드를 관리하며 기존 freeform/step 동작은 그대로 결선

* docs: keybuddy 아키텍처와 개발 명령어로 CLAUDE.md 보강

추천 엔진의 두 경로(Edge Function/OpenAI vs 결정론 의도 하네스),
데이터 파이프라인, 버전 단일소스, 테스트 규약을 정리.
기존 Git 작업 규칙은 보존.

* docs: 에이전트 지침을 AGENTS.md 단일 정본으로 통합

CLAUDE.md/AGENTS.md 내용 중복(drift)을 막기 위해 AGENTS.md를 정본으로 두고
CLAUDE.md는 심볼릭 링크로 전환. Claude Code/Codex 등 어떤 에이전트가 읽어도
동일 내용을 본다.

* docs(knowledge): 레포 내장 지식 루프 가이드와 이식용 스크립트 추가

개인 머신(~/.claude)에 묶여 있던 knowledge-loop(추출->검토->승격)를
도구·머신 비종속으로 레포에 적용할 수 있도록 docs/knowledge/ 에 정리.

- README.md: 패턴 개념, 레포에 두는 이유, 승격 워크플로, 도구별(Claude/Codex/수동) 적용법
- scripts/: KNOWLEDGE_DIR·KNOWLEDGE_LLM_CMD 로 경로·LLM 교체 가능한 추출/넛지 스크립트
- pending.md/archive.md/promoted/: 저장소 골격 템플릿
- 기존 전역 훅과의 중복 추출을 막기 위해 이 레포는 의도적으로 훅 미등록(가이드만 제공)

* fix(keybuddy): 홈 재디자인 리뷰 반영

- 템플릿 클릭 시 freeform 탭으로 전환해 입력값이 보이도록 수정
- 추천 진행 중(loading) 분석 버튼 비활성화로 중복 요청 방지
- 홈 복귀 경로 3곳에서 homeTab을 freeform으로 초기화
- 미사용 import(SlidersHorizontal, MessageSquare) 제거

* fix(keybuddy): 홈 복귀/탭 전환 시 error·rating 상태 초기화

- 홈 복귀 로직 3곳 중복을 goHome 공용 콜백으로 추출 (DRY)
- goHome에서 rating·error도 함께 초기화해 이전 별점/오류 잔존 제거
- 단계별 진입 시 setError(null) 추가로 이전 오류 배너 잔존 제거
- 탭 전환·템플릿 선택 시 setError(null) 추가

* refactor(keybuddy): 홈 화면 리뷰 반영 - 상수/헬퍼 추출

- goHome의 불필요한 useCallback 제거(메모이즈 이점 없음, 일반 const로)
- 세그먼트 토글 배열을 모듈 상수 HOME_TABS로 추출해 렌더마다 재생성 제거
- 템플릿 선택 인라인 람다를 selectTemplate 헬퍼로 추출

* docs(agents): CLAUDE.md 심볼릭 링크 주의 안내 추가

신규 진입자가 CLAUDE.md를 별도 사본으로 오해하지 않도록,
한쪽만 편집 금지·새 도구는 사본 대신 링크로 추가·클론 시
core.symlinks 주의를 첫 문단 아래에 명시한다.

* docs(agents): 심볼릭 링크 OS별 동작 차이 명시

macOS/Linux는 추가 설정 없이 동작하고, Windows는 개발자 모드/관리자
권한과 core.symlinks 설정이 모두 필요하며 없으면 한 줄짜리 텍스트 파일로
풀린다는 점을 안내에 추가한다.

* docs: keybuddy 기술 문서를 레포 루트 docs/로 일원화

- impl/keybuddy/docs/* 와 intent-harness-before-after.md 를 레포 루트 docs/ 로 이동.
  AGENTS.md가 이미 'docs/tag-extraction-flow.md' 를 참조하는데 실제 파일이 impl/keybuddy 하위에 있어 발생한 경로 불일치를 해소
- tag-extraction-flow.md / intent-harness-before-after.md 상단에 '미연결 클라이언트 설계' 상태 헤더 추가.
  실제 배포 추천은 Supabase Edge Function + OpenAI(gpt-5.4)이며 의도 하네싱 lib는 앱 진입점에 미연결임을 문서 자체에 명시(AGENTS.md '두 개의 추천 경로'와 정합)
- 이동에 따른 상호 링크/자기 경로 참조 갱신

* docs(keybuddy): README 후보 압축 개수 40개로 정정 + 루트 docs 안내 추가

- Edge Function 실제 maxCandidates(40)와 어긋난 '25개' 표기 3곳 정정
- 루트 docs/ 기술 문서로 안내하는 '더 읽을거리' 섹션 추가

* docs: 루트 README에 keybuddy 구현 현황 반영

- 기획서(가설/문제정의)는 보존하고 '9. 구현 현황' 섹션을 추가
- 입력 방식(자연어/단계별 10문항), 결과 화면, 기획 대비 추가/미구현 항목 정리
- 기술 상세는 impl/keybuddy/README.md 및 impl/keybuddy/docs/로 안내

* fix(knowledge): 지식 루프 스크립트 견고성과 문서 정확성 보완

리뷰에서 발견한 결함을 수정한다.

- knowledge-extract.sh: cwd 누락 시 기본값 "?" 대신 $PWD 로 폴백해, git 루트를 못 찾을 때 실행 위치에 "?" 디렉토리를 만들거나 헤더에 "?"가 찍히는 부작용을 막는다.
- knowledge-extract.sh: grep -c 결과(interrupts/commits)에 기본값 0 을 보장해, 드물게 빈 문자열일 때 정수 비교(-eq)가 깨지는 것을 방지한다.
- knowledge-nudge.sh: claude-json 출력을 printf 대신 jq 로 인코딩해, 저장소 경로/KNOWLEDGE_DIR 에 따옴표·역슬래시가 있어도 JSON 이 깨지지 않게 한다. 평문 분기는 그대로 두어 jq 미설치 환경에 영향 없음.
- README: 기타 도구 수동 실행 예시가 transcript 파일을 직접 파이프하도록 적혀 있어 실제 stdin 계약(transcript_path 를 담은 JSON)과 어긋났다. 그대로 따라하면 조용히 종료되므로 JSON 입력 예시로 교정한다.

* feat(keybuddy): 결정론 의도 하네스 UI 연결 및 추천 점수순 상위 3개 제한

추천 결과가 너무 많다는 피드백에 따라 경로2(결정론 의도 하네스)를 UI에
배선하고 결과를 점수순 상위 3개로 줄인다. 점수 내림차순은 searchWithProfile/
deriveRankOrder가 보장하므로 상위 3개가 곧 점수순 top 3다.

- recommend.ts: freeform은 Edge Function 태그추출(OpenAI, 키는 서버 secret만)
  -> expandIntents -> searchWithProfile, guided는 LLM/서버 0회로 동일 검색.
  카탈로그를 클라에서 직접 검색(data/keyboards.json 600개).
- Edge Function: 자연어 -> {intents, hardConstraints, softIntentTags} 태그추출
  'extract' 모드 추가. OpenAI 호출부를 callOpenAIResponses로 공통화.
  어휘 사본 vocabulary.ts 신설(클라 tagSchema/intentProfile 어휘 복제).
  레거시 추천 생성 경로는 보존(미사용).
- searchResultComposition: toRecommendations/buildSummary에 limit 인자 추가
  (기본값 30 유지로 기존 테스트 보존), 경로2 호출 시 3 전달.
- 보안: 브라우저에서 Anthropic 직접 호출하던 경로를 더 이상 쓰지 않음.
  LLM 키는 서버에만 존재.
- recommendTop3 테스트 추가(배선/개수 제한/점수 내림차순).

* docs: 구현 현황 단계별 선택 서술을 실제 동작에 맞게 수정

코드 리뷰(PR #20) 반영.

- "후보를 좁힙니다"는 단계별로 후보를 실시간 축소하는 듯한 오해를 줘
  "순서대로 답하면 조건에 맞는 키보드를 추천합니다"로 정정. 실제로는
  마지막 단계에서 runRecommend(guided)를 한 번만 호출한다.
- "각 질문에 상관없음류 선택지를 두고"는 과장. 타건 소리/크기/백라이트
  3개 질문은 상관없음류 선택지가 없으므로 "대부분의 질문에"로 정정하고
  예외 3개를 명시.

* feat: improve guided survey experience

* fix(keybuddy): PR 리뷰 반영 - 추출 정제·파싱 견고성·테스트/문서 정합

코드 리뷰(PR #21) 피드백 반영.

- recommend.ts: 클라이언트 sanitizeExtraction의 hardConstraints가 단순 캐스트라
  키별 타입·값 검증이 빠져 2중 방어가 무력했다. extractRawTags의 sanitizeTags를
  재사용해 숫자/열거형 검증을 거치도록 수정(softIntentTags도 함께).
- index.ts(Edge Function): parseExtractJson이 JSON.parse SyntaxError를 던져 500으로
  떨어지던 문제를 null 폴백으로 변경 → sanitizeExtraction이 빈 추출로 graceful degrade.
- index.ts: 추출 프롬프트에서 사용자 쿼리의 큰따옴표를 이스케이프해 프롬프트 구조
  파손 가능성 제거.
- 테스트 규약: guided 경로 LLM 미호출 불변식을 recommendLlmNoCall.test.ts로 분리
  (*LlmNoCall.test.ts 규약 준수). recommendTop3는 포인터 주석만 남김.
- vocabularyParity.test.ts 추가: Edge Function vocabulary.ts 사본과 프론트
  tagSchema/intentProfile 어휘 드리프트를 CI에서 감지.
- AGENTS.md: 결정론 하네스 UI 배선 반영. 현행 프로덕션 경로(Edge Function
  OpenAI 태그추출 + 클라 결정론 검색)와 레거시(Edge Function 전체 추천 생성,
  클라 claude 추출)를 구분하도록 아키텍처 섹션 갱신.

* fix: address guided survey review feedback

* feat(keybuddy): 구매 클릭·추천 별점 Supabase 이벤트 수집

추천 가설 검증용 데이터를 모으기 위해 두 가지 사용자 행동을 Supabase에 적재한다(실결제 없음).

- events 테이블 마이그레이션: 단일 events(id, event_type, session_id, payload jsonb, created_at) + anon insert-only RLS
- lib/session.ts: 로그인 없는 익명 세션 UUID(localStorage)
- lib/events.ts: purchase_click('구매하기' 클릭, payload product_code)·rating(추천 전체 별점) 이벤트를 PostgREST /rest/v1/events로 직접 insert. best-effort라 실패는 삼켜 UX를 막지 않음
- App.tsx: 구매하기 버튼·별점 클릭에 계측 연결(토스트/링크 동작은 그대로)
- events.test.ts: payload 형태·PostgREST 요청/실패 동작 단위검증(9 케이스)
- 문서: AGENTS.md 이벤트 수집 경로·스위트 수, README 마이그레이션 적용·수동 확인 절차

* fix: 단계별 선택 결과 완화 제거

* feat(crawler): add YouTube media enrichment

* data(catalog): sync YouTube media links

* docs: document YouTube media enrichment workflow

* fix: 이벤트 수집 리뷰 피드백 반영

* 빈 자연어 추출 결과 추천 차단

* fix: 검색 결과 자연어 조건 전체 표시 (#27)

* chore: 키보드 데이터 추가

* refactor: 스위치 이름 조회 필드를 rawSwitchName으로 변경

* fix: 비기계식 키보드 레벨미터 고정

* feat: 구매 버튼에 가격 비교 링크 연결 (#30)

Co-authored-by: first-woosun <dbdntjs620@gmail.com>

* docs: 4주차 주간 보고서 작성

---------

Co-authored-by: chohs4164 <josh100as@gmail.com>
Co-authored-by: first-woosun <dbdntjs620@gmail.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: first-woosun <105399878+first-woosun@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants