[FEAT] recommendation controller - #41
Merged
Merged
Conversation
…mmendation-output-validations API 구현
Contributor
Walkthrough추천 모듈에 검증 계약과 Changes추천 검증 및 API
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant RecommendationController
participant SimulationApiClient
participant CandidateFilterService
participant RecommendationEngineService
participant RecommendationValidator
Client->>RecommendationController: POST /recommendations
RecommendationController->>SimulationApiClient: 환경 fixture와 호환성 규칙 조회
SimulationApiClient-->>RecommendationController: 후보와 규칙 반환
RecommendationController->>CandidateFilterService: 후보 필터 결과 계산
CandidateFilterService-->>RecommendationController: CandidateFilterResult 반환
RecommendationController->>RecommendationEngineService: 프로필과 필터 결과로 추천 생성
RecommendationEngineService-->>RecommendationController: Recommendation 반환
Client->>RecommendationController: POST /recommendation-output-validations
RecommendationController->>SimulationApiClient: 전체 후보 조회
SimulationApiClient-->>RecommendationController: 전체 후보 목록 반환
RecommendationController->>RecommendationValidator: Recommendation과 후보 목록 검증
RecommendationValidator-->>RecommendationController: ValidationOutcome 반환
RecommendationController-->>Client: RecommendationValidationResponse 반환
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
Contributor
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@backend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/controller/RecommendationController.java`:
- Around line 52-62: Update RecommendationController.recommend so it does not
trust or pass request.candidateFilterResult() directly to
recommendationEngineService. Recompute the candidate filter result on the server
using the request’s environmentId and sessionContext, or validate a
server-issued opaque/signed result bound to both environment and session before
invoking the recommendation engine.
In
`@backend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/RecommendationValidator.java`:
- Around line 53-54: Update the recommendation validation flow so
validateRecommendedCandidate also receives excludedCandidateIds, and make it
report a validation issue at /recommendation/recommendedCandidateId when the
recommended candidate is excluded. Preserve the existing alternative-candidate
validation behavior and issue handling.
- Around line 106-111: Update the alternative-candidate validation flow around
alternativeCandidateIds and validateAlternativeCandidate to track IDs already
encountered. When a later entry repeats an earlier ID, return a validation issue
for that entry using its array-index path instead of validating it as a new
candidate; preserve the existing validation behavior for unique IDs.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1f1583cf-1e44-447f-96ab-41a752f8ae0b
📒 Files selected for processing (8)
backend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/RecommendationValidator.javabackend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/ValidationIssue.javabackend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/ValidationOutcome.javabackend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/controller/RecommendationController.javabackend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/controller/dto/CandidateFilterRequest.javabackend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/controller/dto/RecommendationRequest.javabackend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/controller/dto/RecommendationValidationRequest.javabackend/src/main/java/com/kiobridge/kiobridge/modules/recommendation/controller/dto/RecommendationValidationResponse.java
…mmendation-output-validations API 구현
cha-hyunwoo
added a commit
that referenced
this pull request
Aug 10, 2026
* chore: 백엔드 배포 기반 구성 * chore: aws 자동 배포 구성 (#5) * [FEAT] contracts 패키지 공통 파일 (#7) * chore: 설정 파일 및 패키지명 수정 * feat: contracts Simulation API 제출 계약 * feat: contracts 공통 상태 enum * chore: pr issue template 등록 * chore: gitignore 추가 * chore: coderabbit 설정 * feat: Target/Action/UserDecision/ExecutionPlan 레코드 * [FEAT] modules executionplan (#8) * feat: 응답 dto * feat: 세션 생성/제출/검증/실행/Evidence 호출 * feat: 전체 파이프라인 오케스트레이션 * fix: Simulation API 호출 timeout 설정 및 제출 전 승인-계획 불변식 검증 추가 * refactor: SimulationApiClient를 contracts로 이동 (#9) * [FEAT] Evidence, SimulationValidation 타입 정의 추가 (#11) * feat: Evidence, SimulationValidation 타입 정의 * feat: RunResult 등 run 관련 타입 추가, Evidence resolvedSimulationTrace 필드 보완 * [FEAT] SimulationApiClient Input Contract 조회·검증 기능 추가 (#13) * refactor: Jackson 3 JsonNode 타입으로 통일 * feat: Input Contract 조회 및 검증 기능 추가 * [FEAT] Evidence 조회 및 화면용 요약 서비스 구현 (#14) * feat: evidence 조회 API, 화면용 요약 서비스 추가 * fix: evidence.result() null 처리 및 결제 계획 시 FAIL 처리 반영 * [FEAT] Canonical Profile 정규화 API 구현 (#17) * feat: Canonical Profile 정규화 API 구현 * [FIX] Profile providerId를 서버 설정값으로 고정 * [FEAT] executionplan 내부 api 구현 및 리팩토링 & ParticipantSubmission 누락 필드 추가 (#18) * feat: 내부 api dto * feat: Service 세션 생성, 일괄처리 부분 분리 * feat: ParticipantSubmission 누락 필드 추가 * fix: ParticipantSubmission field null check + buildExecutionPlan (stub) 예외처리 추가 * [FEAT] 프론트엔드 온보딩·승인·실행 화면 추가 (#15) * feat: 온보딩·프로필·승인·실행 화면 추가 (Vite + React) 비어 있던 frontend/ 에 키오스크 접근성 앱을 넣는다. 프로필 저장부터 QR 연결, 매핑 결과 승인, 실행 진행, 장바구니 확인까지 전 화면이 동작한다. - 백엔드 연결 지점은 src/api/client.ts 의 KioBridgeApi 4개 메서드 한 곳 - 붙기 전까지는 mock 구현으로 돌고, setScenario() 로 예외 상태를 재현한다 - 프로필은 의미값(텍스트)만 담는다. 상품 ID·화면 좌표를 저장하지 않는다 - 결제 UI 없음. 종료 상태는 cart_ready("장바구니에 담았어요") 하나뿐 - 아이콘 Phosphor(MIT), 사진 출처는 src/assets/ATTRIBUTIONS.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: 프론트 타입과 백엔드 contracts 의 관계를 바로잡는다 "백엔드 contracts 와 이름을 맞춰 놨다"고 적었는데 사실이 아니다. 겹치는 타입 이름이 하나도 없다. contracts 는 주최측이 정한 Simulation Kit 규격이고, domain/types.ts 는 앱과 우리 서버 사이 규격이라 층이 다르다. 그대로 두면 백엔드 담당이 1:1 대응을 기대하고 붙이다 헤맨다. 실제로 이어지는 지점만 표로 적고, 아직 아무도 안 만든 것(앱용 REST 엔드포인트)과 ID 번역이 서버 몫이라는 점을 밝힌다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: low_confidence 승인도 서버가 다시 검증하게 한다 client.ts 는 "프론트 가드만 믿지 않는다"고 적어 두고 정작 low_confidence 만 빼놓고 있었다. 화면에서는 사용자가 카드를 짚어야 승인 버튼이 열리는데, 그 사실이 approve() 인자에 실리지 않아 서버가 확인할 방법이 없었다. changed 가 acknowledgedDiff 로 받는 보장을 low_confidence 도 받게 맞춘다. 확신이 낮을수록 사용자가 직접 짚었다는 사실이 더 중요하다. - ApproveInput 에 confirmedLowConfidence 추가 (필드마다 무엇을 뜻하는지 주석) - approve() 에 CONFIRMATION_REQUIRED 분기 추가 - OrderLowConfidence 호출부에서 confirmedLowConfidence: true 전달 CodeRabbit 리뷰 반영 (PR #15). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 프로필 카드에서 고르는 곳과 지우는 곳을 분리 카드 전체가 role="radio" 이고 그 안에 삭제 버튼이 들어 있었다. 삭제 버튼에 포커스를 두고 Enter 를 눌러도 카드만 선택되고 지워지지 않았다. 부모 div 의 onKeyDown 이 그 Enter 를 가로채 preventDefault() 하는 바람에 버튼의 기본 동작(클릭)이 막혔기 때문이다. 키보드만 쓰는 사람에게는 프로필을 지울 방법이 아예 없었다. - 선택은 시각적으로 숨긴 진짜 input[type=radio] 가 맡는다. 화살표 키 이동과 그룹당 탭 한 번(로빙 tabindex)을 브라우저가 처리한다. - 삭제 버튼은 label 바깥 형제로 옮겼다. - 목록 컨테이너는 role="radiogroup" 대신 fieldset + 숨긴 legend 로 바꿨다. - 포커스 표시는 라디오의 focus 를 받아 카드에 3px outline 으로 그린다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(qr): 스캐너의 중첩 타이머를 정리하지 않아 언마운트 뒤 페어링이 나가던 문제 QrScannerModal 의 useEffect 는 바깥 2.5초 타이머만 해제하고 그 안에서 예약한 800ms 타이머는 그대로 두었다. 스캐너가 떠 있는 동안에도 하단 탭은 눌린다. 스캔이 끝난 뒤 800ms 안에 다른 탭으로 옮기면 안쪽 타이머만 살아남아 언마운트된 컴포넌트에서 onDetected 가 돌고 api.claimPairing 이 호출된다. 화면에는 아무 표시도 안 되는데 키오스크에는 사용자가 요청한 적 없는 연결 요청이 남는다. 두 타이머 ID 를 모두 붙잡아 cleanup 에서 함께 해제한다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 프로필 삭제 버튼의 터치 영역을 44x44 로 넓힘 높이는 44 였지만 폭이 30 이었다. 앱 전체를 훑어 44px 미만인 조작 요소는 이것 하나뿐이었다. 밑줄은 글자에만 걸리므로 minWidth 를 44 로 올려도 보이는 크기는 그대로고 누를 수 있는 영역만 넓어진다. 카드 크기(321x168)도 그대로다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 기본 팔레트가 WCAG AA 에 미달하던 대비를 전부 올림 고대비를 별도 옵션으로 켜고 끄게 만드는 대신 기본값을 고쳤다. 찾아 들어가야 보이는 설정은 대비가 필요한 사람이 못 찾을 수 있고, 지금 값은 옵션을 켜기 전 상태에서 이미 기준 미달이었다. 측정값 (흰 배경 / SURFACE 위): - TEXT_2 #8A8A8E 3.44 / 3.21 → #6B6B70 5.30 / 4.95 - TEXT_3 #C4C4C8 1.74 / 1.62 → 글자에서 전부 제거 - FAIL #D92D20 FAIL_BG 위 4.35 → #CE2A1F 4.75 고친 곳 - 읽어야 하는 글자에 쓰이던 TEXT_3 12곳을 TEXT_2 로 옮김. 하단 탭의 안 눌린 항목(내 프로필·계정)이 1.74:1 이라 특히 나빴다. - 고른 프로필 카드: 태그·메모·삭제가 반투명 흰색이라 3.31~3.35 였다. 태그는 바탕을 어둡게 눌러 6.6, 나머지는 흰색으로 올려 5.08. - QR 스캔 안내문 42% 흰색(3.95) → 62%(7.8). 조명을 조절하라는 문장은 화면이 어두울 때 읽는 문장이다. - 안 고른 동그라미 테두리와 변경 확인 체크박스 테두리가 각각 1.62 / 1.29 라 컨트롤 경계 기준(3:1)에 못 미쳤다. TEXT_2 / WARN 으로 바꿈. - 비활성 버튼 글자는 WCAG 예외지만 1.62 는 무엇을 기다리는지 못 읽는다. 초록 알약이 회색으로 바뀌는 것만으로 신호는 충분해서 TEXT_2 로 올림. WARN_BORDER 는 쓸 곳이 없어져서 뺐다. 시나리오 스위치를 돌려 가며 전 화면을 다시 쟀다. 환영·전화번호·인증·이름·프로필 목록·프로필 만들기·QR(스캔/연결중/연결됨/실패/만료) ·확인(정확/애매/없음/변경/불확실)·실행(진행/담김/중단)·계정·접근성·개인정보 전부 미달 0건. * fix(a11y): QR 화면 그릇이 넘칠 때 아래 버튼에 닿을 수 없던 문제 QrScreen 의 가운데 그릇이 overflow-hidden 이었다. 그 안의 패널들(연결중·연결됨·실패·만료)은 아래 버튼을 mt-auto 로 바닥에 붙인다. 내용이 그릇보다 커지면 그 버튼이 잘려 나가는데, hidden 이라 스크롤도 안 생긴다. 포커스를 옮기면 브라우저가 알아서 스크롤해 주므로 키보드로는 닿지만, 손가락으로는 닿을 방법이 아예 없다. 앱이 주는 큰 글씨(1.18배)까지는 넘치지 않는다. 1.4배까지도 안 넘친다. 사용자가 브라우저·OS 글씨 크기를 더 키우면 1.6배 근처부터 넘친다(측정 102px, 1.8배에서 195px). overflow-y-auto 로 바꾸면 넘칠 때만 스크롤이 생기고 안 넘치면 지금과 똑같이 보인다. 1 / 1.18 / 1.6 / 1.8 / 2.2 배에서 잘리는 그릇 0건, 못 닿는 조작 요소 0건. 프로필 목록·계정·개인정보 화면도 1.8 배에서 함께 확인했다. * chore(review): 안 쓰는 shadcn 원본 48개를 CodeRabbit 리뷰 대상에서 제외 frontend/src/app/components/ui/** 는 Figma Make 재출력용으로만 남겨 둔 파일들이라 어느 화면도 import 하지 않는다. 번들에도 들어가지 않는다. 리뷰가 반복해서 여기를 지적하는데, 고쳐도 사용자에게 도달하는 코드는 바뀌지 않아 실제 결함을 찾는 데 방해가 된다. 다시 쓰기 시작하면 이 줄을 지운다. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * [FEAT] 장소별 질문 목록을 시뮬레이션 킷 fixture 축에 맞춤 (#20) * feat(profile): 장소별 질문을 시뮬레이션 킷 fixture 축에 맞춤 앱이 묻는 조건과 심사 환경이 실제로 가르는 축이 서로 달랐다. 드라이브스루·버거 세트·사이드 변경처럼 fixture 에 대응이 없는 질문은 어르신이 골라도 결과가 달라지지 않는, 물어만 보고 버리는 질문이었다. 반대로 fixture 에 있는데 앱에 없던 축이 셋 있었다. - 병원 '접근성 지원' — 우리가 점수 축 20% 로 넣어 놓고 물어보는 화면이 없었다 - 관공서 '인증 방식' — PUBLIC-004 를 AUTH_METHOD_UNAVAILABLE 로 빼는 조건인데 앱에서 입력할 방법이 없었다 - 음식점 '컵' — 확인 카드 필수 5개 중 하나인데 '추가 옵션' 다중선택에 묻혀 있었다 메뉴 이름은 목록에서 고르게 하지 않고 자유 입력(menuName)으로 둔다. 사용자가 부르는 이름과 오늘 화면의 이름을 맞추는 건 매핑의 일이다. 알레르기만 fixture 축에 맞추지 않았다. option-group 이 아니라 사람에 대한 절대 조건이라, 오늘 이 가게에 걸리는 게 땅콩뿐이라고 나머지를 지우면 새우 알레르기가 있는 사람이 그 사실을 말할 방법이 사라진다. mock.ts 의 옵션 라벨도 같은 이름으로 맞췄다. tsc --noEmit · build 통과. 세 장소 모두 화면에서 확인. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(frontend): 안 쓰는 shadcn 컴포넌트 48개와 딸려 온 의존성 53개 제거 frontend/src/app/components/ui/ 의 48개 파일은 어느 화면도 import 하지 않는다. components/figma/ImageWithFallback.tsx 도 마찬가지고, styles/globals.css 는 0바이트 빈 파일이었다. 참조 0건을 확인하고 지웠다. 지우기 전에 react 와 react-dom 을 dependencies 에 명시했다. 지금까지 두 패키지가 선언 없이 @mui/material·radix 의 전이 의존으로만 딸려 오고 있었다. ui/ 를 걷어내면 React 자체가 사라질 뻔했다. tw-animate-css 는 남긴다. JS import 가 아니라 styles/tailwind.css 의 @import 라 코드 검색에는 안 걸리지만 실제로 쓰인다. dependencies 55개 → 4개 (react, react-dom, lucide-react, tw-animate-css) npm ci 92 packages / 6s JS 번들 223.12 kB → 223.12 kB (변화 없음 — 원래 번들에 없던 코드다) CSS 번들 86.03 kB → 13.58 kB (Tailwind 가 그 48개까지 훑고 있었다) tsc --noEmit · build 통과. 개인 저장소와 번들 해시 동일. 실제 화면에서 폰 프레임·Tailwind 유틸리티·QR 화면 overflow-y-auto· 1/1.18/1.6/2.2배 확대를 모두 확인했다. .coderabbit.yaml 의 components/ui 제외 필터는 이제 가리킬 대상이 없어졌지만, 다음 PR 에서 별도로 정리한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * CodeRabbit Generated Unit Tests: Add Generated Unit Tests for PR Changes (#22) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Delete frontend directory * Create yena.txt * Delete .coderabbit.yaml * [FEAT] chicken-store SessionContext 정규화 API 구현 (#25) * [FEAT] recommendation contracts 정의 (#21) * feat: recommendation관련 contracts 정의 (Recommendation, ExcludedCandidate, Candidate, CompatibilityRule, CompatibilityRuleSet) * fix: ParticipantSubmission에 누락된 inputContractVersion 필드 추가, TODO: 담당2 Recommendation 타입으로 교체 * feat: PublicFixture 계약 정의 및 fixture·compatibility-rules 조회 메서드 추가 * chore: 운영 프론트 cors 설정 추가 (#27) * [REFACTOR] SessionContext 공통 기반 타입과 치킨 구현 분리 (#29) * refactor: SessionContext 공통 기반 타입과 치킨 구현 분리 * style: SessionContext 리팩터링 코드 정리 * [FEAT] Evidence.recommendation 타입 교체 (Object → Recommendation) (#32) * feat: Evidence.recommendation 타입을 Recommendation으로 교체 * fix: recommendationReasons 첫 번째 비공백 값만 요약에 사용 * feat: Canonical Input 통합 검증 API 구현 (#34) * [FEAT] contracts lost fields 업데이트 (#35) * feat: ExecutionPlan filed 업데이트 * feat: UserDecision filed 업데이트 * fix: approved/actions 불변식 강제 + confirmedAt 형식 검증: * [FEAT] recommendation ruleevaluator interface (#36) * feat: RuleResult * feat: Rule EvaluationResult record * feat: RuleEvaluator 인터페이스 * feat: 구현체 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation 필터링 오케스트레이션 구현 (#37) * chore: 파일삭제 * feat: 필터링 후보 결과 레코드 * feat: 후보 필터링 서비스 * fix: CandidateFilterResult 리스트 불변 복사 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation ruleevaluator (#38) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: Kit compare() 사용 * feat: asList(), stringOf() 공통 유틸 * feat: RuleEvaluator 수정 * test: CandidateFilter + RuleEvaluator 테스트 코드 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation engine 초기 구현 (#39) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: RecommendationEngineService STEP5~7 추천 로직 초기 구현 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 프론트엔드 앱 추가 및 백엔드 연동 계층 (#30) * feat: 프론트엔드 앱 추가 및 백엔드 연동 계층 dev 에서 frontend 가 통째로 지워져 있어(44301ac) 다시 올린다. 개인 저장소(Yena07/kiobridge-app)에서 리뷰 112건을 반영하며 만든 것이고 테스트 84개가 붙어 있다. [무엇이 들어 있나] src/app/App.tsx 화면 전부 (온보딩·프로필·QR·확인·실행) src/api/client.ts 앱이 쓰는 계약 + 목 구현 src/api/backend.ts 팀 백엔드 연동 계층 src/api/mock.ts 프로필로 매핑 응답을 만드는 로직 src/domain/ 계약 타입 · 장소별 질문 목록 api/bff/ 백엔드 호출을 서버가 대신 보내는 프록시 [백엔드 붙이는 법] client.ts 마지막 줄만 바꾸면 된다. export const api = createApi(createTeamBackend()); createTeamBackend 는 팀 백엔드가 실제로 구현한 경로에 맞춰 두었다. 명세서와 다른 부분이 있어서 그대로 옮기면 안 붙는다. 명세서 실제 구현 POST /api/v1/sessions POST /internal/simulation/session submission -> validate -> execute POST /internal/simulation/submit-and-run candidate-filters / recommendations 아직 없음 추천 계열이 생기면 filterCandidates·recommend 둘만 채우면 된다. [CORS 는 해결해 두었다] api/bff 가 백엔드로 대신 보내므로 브라우저는 같은 출처로만 요청한다. 백엔드에서 allowed-origin 을 맞출 필요가 없고, 미리보기 배포마다 주소가 바뀌는 문제도 없다. Vercel 환경변수 KIOBRIDGE_API_BASE 만 넣으면 된다. 열린 프록시가 되지 않게 통과 경로를 명시하고 클라이언트 헤더는 넘기지 않는다. [.coderabbit.yaml 복구] 루트 설정이 지워져 있어 되살렸다. 팀 저장소이므로 profile 은 chill 로 두고, base_branches 를 dev·main 으로, path_instructions 를 frontend/** 로 맞췄다. 실격 조건(결제 UI 부재·승인 전 실행 경로 없음·상품 ID 미보유·병원 의료 판단 금지·관공서 자격 판단 금지)과 접근성 기준이 들어 있다. [프론트가 지키고 있는 것] - 결제 관련 문자열 0건. src 전체를 훑는 테스트로 잠가 두었다. - 승인 전 실행계획 생성 0건. approve 호출은 버튼 핸들러 안에만 있다. - 상품 ID 미보유. 후보 표식은 c1·c2·c3 형태만. - 선택 불가능 후보 추천 0건. 알레르기·품절·이용 불가는 후보에서 제거한다. - 신뢰도 낮으면 사용자가 직접 짚어야 승인된다. - 실제 개인정보 미수집. 이름 대신 호칭, 전화번호는 시연용 고정값. npm ci · npx tsc --noEmit · npm test 84/84 · npm run build 통과. 번들 해시가 개인 저장소 빌드와 동일하다. * fix: 코드 리뷰 14건 반영 승인 경로 - client.ts: 중복 승인 검사와 확정 사이의 await 를 없앤다. 동시에 들어온 승인 두 건이 모두 검사를 통과해 둘 다 담기던 문제. backend.ts 와 같은 방식으로 첫 await 전에 표시하고 실패하면 되돌린다. - backend.ts: clarification 이 아닌데 candidateId 가 오면 거절한다. client.ts 는 이미 막고 있었고 이쪽만 열려 있었다. 응답 문구 - mock.ts: 담을 후보가 없는 이유(장소 미정·지원하지 않는 장소·조건 제외)를 시나리오 스위치가 not_found 를 골랐을 때도 구분한다. 병원 프로필에도 "메뉴가 바뀌었을 수 있어요" 라고 답하던 문제. - mock.ts: 메뉴 이름 뒤 조사를 헬퍼로 붙인다. 사용자가 적은 이름에 받침이 없으면 "커피이 없어요" 가 됐다. 접근성 - App.tsx·tokens.ts: 회전 표시를 SVG SMIL 에서 CSS 애니메이션으로 바꾼다. SMIL 은 CSS 규칙의 대상이 아니라 prefers-reduced-motion 이 멈추지 못했다. - App.tsx: QR 탭에 다시 들어갈 때 연결이 살아 있으면 스캐너를 열지 않고 연결 상태를 보여 준다. 같은 상태를 두고 두 화면이 다른 말을 했다. 정리 - tsconfig.json: compilerOptions.types 가 두 번 적혀 뒤엣것이 앞엣것을 덮었다. - App.tsx: 렌더 본문에서 ref 를 변형하던 것을 useEffect 로 옮긴다. - App.tsx: options 선언을 참조보다 위로 옮긴다. - catalog.tsx: DETAIL_OPTIONS·PLACE_ICONS 키를 PlaceType 으로 좁힌다. - ATTRIBUTIONS.md: 코드블록 언어 표시와 제목 뒤 빈 줄. 테스트 - 결제 문자열 스캔 범위를 src 에서 리포지터리 루트로 넓힌다. index.html· vite.config.ts·api/bff 가 검사에서 빠져 있었다. 제외 목록은 .gitignore 를 읽는다. - 동시 승인 회귀 테스트를 client·backend 양쪽에 추가. - '조건 얘기를 한다' 테스트가 이름과 다른 것을 단언하던 것을 바로잡는다. 84 → 89 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: 팀 레포 PR 코드 리뷰 반영 확인 화면이 사실이 아닌 말을 하던 것 - mock.ts: 시나리오 스위치가 '변경' 인데 실제로는 다 맞을 때, 멀쩡한 행 하나에 matched:false 와 "오늘은 제공되지 않아요" 를 붙이던 시연용 경로를 없앤다. 실제로 제공되는 옵션을 안 된다고 말한 것이고, 심사 중 스위치를 돌리면 그대로 보였다. 스위치는 결과 종류를 고를 뿐 없는 불일치를 만들지 못한다. - backend.ts: 이름 없는 후보를 다 걸러내고도 clarification 을 내보내면, 화면은 "비슷한 메뉴가 여러 개예요" 라면서 고를 것을 하나도 못 보여 준다. 승인은 후보 선택을 요구하므로 사용자가 그 화면에서 빠져나갈 수 없다. 연동 계층 - createSession 이 claimCode 를 버리고 있었다. QR 로 어떤 키오스크를 찍었는지와 무관하게 세션이 열린다는 뜻이다. 함께 보낸다. - getEvidence 를 POST 로 부르고 있었다. 명세는 GET 이다. - getEvidence 가 cart·abort 를 옮기지 않아 "장바구니에 담았어요" 화면에 개수도 금액도 뜨지 않았다. 중단 사유도 마찬가지. BFF - 경로만 검사하고 메서드는 무엇이든 통과시켰다. GET·POST 만 연다. 테스트 - 저장소 루트를 .git 위치로 찾는다. '..' 개수를 고정하면 개인 레포와 팀 레포 중 한쪽이 반드시 틀린다. 팀 레포에서는 frontend 까지만 올라가 그 바깥을 검사하지 못했고, 개인 레포에서 한 단계 더 올리면 Downloads 전체를 훑는다. - 스캔이 실제로 훑었는지를 먼저 잠근다. 빈 배열은 늘 통과하므로, 기준점이 틀어지면 이 파일의 모든 보증이 조용히 사라진다. - .md 도 훑는다. 실제로 연동 문서에 금지어를 그대로 적어 둔 적이 있다. - 응답 전체에 서버 후보 ID 가 섞이지 않는지 결과 종류마다 검사한다. - planId 에 일련번호를 붙인다. 밀리초만 쓰면 지연 0 인 테스트에서 겹친다. 문서·기타 - ATTRIBUTIONS.md 를 빌드가 dist 로 복사한다. CC BY-SA 는 저작자 표시를 저작물과 함께 배포할 것을 요구하는데, 문서 자신은 "배포물에도 포함된다" 라고 적혀 있으면서 실제로는 번들에 없었다. - BACKEND_INTEGRATION.md: 금지 문자열을 그대로 적어 둔 줄을 고치고, 질문 번호를 정리하고(③ 중복), claimCode·evidence 경로를 질문에 추가하고, 후보 식별자를 서버가 들고 있어 달라는 요청을 적는다. - README.md 코드블록 언어 표시. 90 → 92 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FEAT] recommendation controller (#41) * feat: ValidationIssue 검증 결과 항목 타입 추가 * feat: ValidationOutcome 검증 결과 타입 추가 * feat: RecommendationValidator STEP5 출력 자체 검증 로직 구현 * feat: candidate-filters 요청 DTO 추가 * feat: recommendations 요청 DTO 추가 * feat: recommendation-output-validations 요청·응답 DTO 추가 * feat: RecommendationController candidate-filters·recommendations·recommendation-output-validations API 구현 * feat: RecommendationController candidate-filters·recommendations·recommendation-output-validations API 구현 * fix: RecommendationValidator가 제외된 추천 후보·중복 대안 후보를 검증하지 못하던 문제 수정 * [FEAT] executionplan buildexecutionplan (#42) * feat: 시그니처 교체하고 실제 조립 로직 구현 * feat: 그룹별 실제 option id 결정 * feat: 컨트롤러에서 environmentId/sessionContext까지 서비스로 전달하도록 수정 * feat: Object 객체 확정 객체로 교체 * test: executionplan + ChickenSToreOptionResolver test code * feat: sessionId<->environmentId 매핑 처리 * test: test 갱신 * fix: ChickenStoreOptionReslover 버그 수정 + 예외 처리 구현 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] orchestrator구현 (#43) * feat: environmentId 재조회 및 ParticipantSubmission 조립 * feat: POST /internal/orchestrator/approve 신설 * feat: 오케스트레이터 레코드 * test: 오케스트레이터 test --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * fix: CandidateFilterService 의 품절 여부 체크 추가 (#44) Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FIX] 통합 테스트 중 버그 수정 (#45) * fix: CandidateFilterService 의 품절 여부 체크 추가 * fix: Target, UserDecision에 @JsonInclude(JsonInclude.Include.NON_NULL) 추가 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 백엔드 신규 경로 연동 및 입력 정규화 적용 (#46) * feat: 프론트엔드 앱 추가 및 백엔드 연동 계층 dev 에서 frontend 가 통째로 지워져 있어(44301ac) 다시 올린다. 개인 저장소(Yena07/kiobridge-app)에서 리뷰 112건을 반영하며 만든 것이고 테스트 84개가 붙어 있다. [무엇이 들어 있나] src/app/App.tsx 화면 전부 (온보딩·프로필·QR·확인·실행) src/api/client.ts 앱이 쓰는 계약 + 목 구현 src/api/backend.ts 팀 백엔드 연동 계층 src/api/mock.ts 프로필로 매핑 응답을 만드는 로직 src/domain/ 계약 타입 · 장소별 질문 목록 api/bff/ 백엔드 호출을 서버가 대신 보내는 프록시 [백엔드 붙이는 법] client.ts 마지막 줄만 바꾸면 된다. export const api = createApi(createTeamBackend()); createTeamBackend 는 팀 백엔드가 실제로 구현한 경로에 맞춰 두었다. 명세서와 다른 부분이 있어서 그대로 옮기면 안 붙는다. 명세서 실제 구현 POST /api/v1/sessions POST /internal/simulation/session submission -> validate -> execute POST /internal/simulation/submit-and-run candidate-filters / recommendations 아직 없음 추천 계열이 생기면 filterCandidates·recommend 둘만 채우면 된다. [CORS 는 해결해 두었다] api/bff 가 백엔드로 대신 보내므로 브라우저는 같은 출처로만 요청한다. 백엔드에서 allowed-origin 을 맞출 필요가 없고, 미리보기 배포마다 주소가 바뀌는 문제도 없다. Vercel 환경변수 KIOBRIDGE_API_BASE 만 넣으면 된다. 열린 프록시가 되지 않게 통과 경로를 명시하고 클라이언트 헤더는 넘기지 않는다. [.coderabbit.yaml 복구] 루트 설정이 지워져 있어 되살렸다. 팀 저장소이므로 profile 은 chill 로 두고, base_branches 를 dev·main 으로, path_instructions 를 frontend/** 로 맞췄다. 실격 조건(결제 UI 부재·승인 전 실행 경로 없음·상품 ID 미보유·병원 의료 판단 금지·관공서 자격 판단 금지)과 접근성 기준이 들어 있다. [프론트가 지키고 있는 것] - 결제 관련 문자열 0건. src 전체를 훑는 테스트로 잠가 두었다. - 승인 전 실행계획 생성 0건. approve 호출은 버튼 핸들러 안에만 있다. - 상품 ID 미보유. 후보 표식은 c1·c2·c3 형태만. - 선택 불가능 후보 추천 0건. 알레르기·품절·이용 불가는 후보에서 제거한다. - 신뢰도 낮으면 사용자가 직접 짚어야 승인된다. - 실제 개인정보 미수집. 이름 대신 호칭, 전화번호는 시연용 고정값. npm ci · npx tsc --noEmit · npm test 84/84 · npm run build 통과. 번들 해시가 개인 저장소 빌드와 동일하다. * fix: 코드 리뷰 14건 반영 승인 경로 - client.ts: 중복 승인 검사와 확정 사이의 await 를 없앤다. 동시에 들어온 승인 두 건이 모두 검사를 통과해 둘 다 담기던 문제. backend.ts 와 같은 방식으로 첫 await 전에 표시하고 실패하면 되돌린다. - backend.ts: clarification 이 아닌데 candidateId 가 오면 거절한다. client.ts 는 이미 막고 있었고 이쪽만 열려 있었다. 응답 문구 - mock.ts: 담을 후보가 없는 이유(장소 미정·지원하지 않는 장소·조건 제외)를 시나리오 스위치가 not_found 를 골랐을 때도 구분한다. 병원 프로필에도 "메뉴가 바뀌었을 수 있어요" 라고 답하던 문제. - mock.ts: 메뉴 이름 뒤 조사를 헬퍼로 붙인다. 사용자가 적은 이름에 받침이 없으면 "커피이 없어요" 가 됐다. 접근성 - App.tsx·tokens.ts: 회전 표시를 SVG SMIL 에서 CSS 애니메이션으로 바꾼다. SMIL 은 CSS 규칙의 대상이 아니라 prefers-reduced-motion 이 멈추지 못했다. - App.tsx: QR 탭에 다시 들어갈 때 연결이 살아 있으면 스캐너를 열지 않고 연결 상태를 보여 준다. 같은 상태를 두고 두 화면이 다른 말을 했다. 정리 - tsconfig.json: compilerOptions.types 가 두 번 적혀 뒤엣것이 앞엣것을 덮었다. - App.tsx: 렌더 본문에서 ref 를 변형하던 것을 useEffect 로 옮긴다. - App.tsx: options 선언을 참조보다 위로 옮긴다. - catalog.tsx: DETAIL_OPTIONS·PLACE_ICONS 키를 PlaceType 으로 좁힌다. - ATTRIBUTIONS.md: 코드블록 언어 표시와 제목 뒤 빈 줄. 테스트 - 결제 문자열 스캔 범위를 src 에서 리포지터리 루트로 넓힌다. index.html· vite.config.ts·api/bff 가 검사에서 빠져 있었다. 제외 목록은 .gitignore 를 읽는다. - 동시 승인 회귀 테스트를 client·backend 양쪽에 추가. - '조건 얘기를 한다' 테스트가 이름과 다른 것을 단언하던 것을 바로잡는다. 84 → 89 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: 팀 레포 PR 코드 리뷰 반영 확인 화면이 사실이 아닌 말을 하던 것 - mock.ts: 시나리오 스위치가 '변경' 인데 실제로는 다 맞을 때, 멀쩡한 행 하나에 matched:false 와 "오늘은 제공되지 않아요" 를 붙이던 시연용 경로를 없앤다. 실제로 제공되는 옵션을 안 된다고 말한 것이고, 심사 중 스위치를 돌리면 그대로 보였다. 스위치는 결과 종류를 고를 뿐 없는 불일치를 만들지 못한다. - backend.ts: 이름 없는 후보를 다 걸러내고도 clarification 을 내보내면, 화면은 "비슷한 메뉴가 여러 개예요" 라면서 고를 것을 하나도 못 보여 준다. 승인은 후보 선택을 요구하므로 사용자가 그 화면에서 빠져나갈 수 없다. 연동 계층 - createSession 이 claimCode 를 버리고 있었다. QR 로 어떤 키오스크를 찍었는지와 무관하게 세션이 열린다는 뜻이다. 함께 보낸다. - getEvidence 를 POST 로 부르고 있었다. 명세는 GET 이다. - getEvidence 가 cart·abort 를 옮기지 않아 "장바구니에 담았어요" 화면에 개수도 금액도 뜨지 않았다. 중단 사유도 마찬가지. BFF - 경로만 검사하고 메서드는 무엇이든 통과시켰다. GET·POST 만 연다. 테스트 - 저장소 루트를 .git 위치로 찾는다. '..' 개수를 고정하면 개인 레포와 팀 레포 중 한쪽이 반드시 틀린다. 팀 레포에서는 frontend 까지만 올라가 그 바깥을 검사하지 못했고, 개인 레포에서 한 단계 더 올리면 Downloads 전체를 훑는다. - 스캔이 실제로 훑었는지를 먼저 잠근다. 빈 배열은 늘 통과하므로, 기준점이 틀어지면 이 파일의 모든 보증이 조용히 사라진다. - .md 도 훑는다. 실제로 연동 문서에 금지어를 그대로 적어 둔 적이 있다. - 응답 전체에 서버 후보 ID 가 섞이지 않는지 결과 종류마다 검사한다. - planId 에 일련번호를 붙인다. 밀리초만 쓰면 지연 0 인 테스트에서 겹친다. 문서·기타 - ATTRIBUTIONS.md 를 빌드가 dist 로 복사한다. CC BY-SA 는 저작자 표시를 저작물과 함께 배포할 것을 요구하는데, 문서 자신은 "배포물에도 포함된다" 라고 적혀 있으면서 실제로는 번들에 없었다. - BACKEND_INTEGRATION.md: 금지 문자열을 그대로 적어 둔 줄을 고치고, 질문 번호를 정리하고(③ 중복), claimCode·evidence 경로를 질문에 추가하고, 후보 식별자를 서버가 들고 있어 달라는 요청을 적는다. - README.md 코드블록 언어 표시. 90 → 92 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 팀 백엔드 실제 응답 모양에 맞춰 연동 계층을 다시 짠다 dev 의 ExecutionPlanController.ExecuteResult.Evidence 를 직접 읽고 맞췄다. submit-and-run 응답에 증거가 같이 오므로 따로 조회하지 않는다. valid 가 실행 여부를 알려 주므로 validate 가 서버 판단을 그대로 읽는다. reviewSnapshot 에서 장바구니 개수.금액을 만들고, 없으면 비워 둔다. 99 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: 실제로 띄워서 붙여 본 결과를 적는다 킷.백엔드.프론트 셋을 로컬에 올리고 요청을 넣어 본 결과다. 되는 것: 세션 생성. claimCode 를 보내도 400 이 나지 않음. 막히는 것: buildExecutionPlan 미구현, State enum 이 닭강정집 전용, 추천 계열 HTTP 경로 없음. 셋 다 백엔드 쪽이고 프론트가 손댈 수 없다. 급한 순서 표와 로컬에서 셋 다 띄우는 방법을 함께 넣는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: 문서 안에서 서로 어긋나던 두 곳을 맞춘다 붙이는 방법이 createHttpBackend 라고 되어 있었다. 팀 백엔드에는 createTeamBackend 를 써야 한다. 인터페이스 표의 getEvidence 도 앞쪽 설명과 두 말을 하고 있었다. 명세서 경로와 팀 백엔드 실제를 두 칸으로 나눠 적는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(readme): 낡은 곳 넷을 고친다 - 백엔드 붙이는 예시가 createHttpBackend 였다. createTeamBackend 로 고치고, 추천 경로가 없어 지금 바꾸면 확인 화면이 비어 버린다는 것도 적는다. - 결제 문자열 스캔 범위가 src 가 아니라 저장소 전체다. - '백엔드가 아직 없어' 는 더 이상 사실이 아니다. - 구조 표에 api/bff 가 빠져 있었다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: BFF 함수가 배포에서 잡히지 않던 두 가지를 고친다 배포본을 찔러 보니 경로마다 답이 달랐다. /api/bff/x 500 /api/bff/internal/simulation/session 404 catch-all 파일명(api/bff/[...path].ts)을 걷어내고 평범한 api/bff.ts 하나로 바꾼다. 뒤쪽 경로는 vercel.json rewrite 가 ?p= 로 넘긴다. 핸들러도 Node 방식(req, res)으로 바꾼다. 웹 방식은 req.url 이 절대 주소가 아니라 new URL 에서 터졌다 - 500 의 정체다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 팀 백엔드의 새 경로 셋에 붙인다 orchestrator/approve 안내 문서대로 셋 다 반영했다. 1) BFF 허용경로 internal/orchestrator/approve 와 api/v1/recommendation-output-validations 를 연다. 실제로 쓰는 순서대로 목록을 다시 정렬한다. 2) 한글 선택지 → enum 변환기 (src/api/canonical.ts) 화면은 '매운맛' 을 들고 있고 계약은 SpicyLevel.HOT 이다. 둘을 잇는 곳을 한 파일에 모은다. 값은 백엔드 enum 파일에서 그대로 옮겼다. · 안 고른 축은 NO_PREFERENCE, 모르는 값은 UNKNOWN 으로 구분한다. 뭉뚱그리면 새 선택지가 생겼을 때 서버가 '아무거나 괜찮대요' 로 읽는다. · 모르는 알레르기도 버리지 않고 UNKNOWN 으로 보낸다. 조용히 버리면 그 사람의 알레르기가 서버에 전달되지 않는다. · 이름·전화번호가 들어갈 자리를 만들지 않는다. displayName 은 선택이라 아예 안 보낸다. 테스트로 잠갔다. 3) 승인 경로 교체 submit 이 orchestrator/approve 로 다섯 조각을 갖춰 보낸다. environmentId 는 보내지 않는다 — 서버가 sessionId 로 다시 조회한다. 사용자가 고른 후보가 있으면 그것이 1순위로 바뀌어 나간다. 곁들여 - filterCandidates·recommend 를 실제 경로에 붙인다. 이름·가격은 추천이 아니라 candidate-filters 의 eligibleCandidates 에서 온다. 이게 없으면 화면에 상품 ID 밖에 보여 줄 게 없고 그건 실격 조건이다. - recommend 에 survivingCandidateIds 를 보내지 않는다. 서버가 다시 계산한다 — 클라이언트가 보낸 필터 결과를 믿으면 알레르기 필터를 우회할 수 있다. 그 판단이 옳아서 따라간다. - createSession 이 돌려주는 environmentId 를 받아 쓴다. 예전에는 chicken-store 로 고정이라 병원에 붙어도 닭강정을 봤다. - client.ts 에 getProfile 을 연다. 백엔드에 프로필 저장소가 없어서 매 호출마다 내용을 함께 보내야 한다. 아직 전환하지 않는다. matchedOptions 가 없어 확인 카드가 조건별 판단을 못 채우고, 운영 배포에 컨트롤러가 아직 없다. 그 둘이 풀리면 client.ts 마지막 줄 하나만 바꾸면 된다. 111 → 119 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 로컬에서 dev 백엔드로 붙여 보고 찾은 것들을 고친다 운영 배포(main)에 컨트롤러가 없어서 dev 를 로컬에서 돌려 붙였다. 네 경로 전부 200 이고 확인 화면이 실제 백엔드 데이터로 그려진다. 로컬 연동 길을 연다 - npm run dev:team — VITE_BACKEND=team 으로 팀 백엔드를 쓴다. 기본(npm run dev)은 그대로 목이라 배포본은 바뀌지 않는다. - vite 개발 서버가 /api/bff 를 KIOBRIDGE_API_BASE 로 넘긴다. 배포본의 BFF 함수가 하는 일을 개발 서버가 대신하는 것이라 CORS 가 없다. - 그 프록시가 Origin·Referer·Cookie 를 떼고 보낸다. 안 떼면 백엔드 CORS 필터가 403 을 준다. 배포본 BFF 도 클라이언트 헤더를 넘기지 않는다. 붙여 보고 고친 것 - 제외 사유로 explanation 대신 reasonText 를 쓴다. explanation 은 "ruleId=CHICKEN_ALLERGEN_HARD_CONSTRAINT, sourceValue=[PEANUT]" 같은 규칙 추적 문자열이다. 그대로 두면 어르신 화면에 그게 나간다. - available:false 후보를 후보 목록과 추천 양쪽에서 뺀다. 서버가 품절 닭강정을 eligibleCandidates 에 남기고 대안으로도 올려 보낸다. 심사 필수 기준이 '선택 불가능 후보 추천 0건' 이라 여기서도 막는다. 뺀 이유는 사용자에게 말해 준다 — 조용히 사라지면 "왜 없지?" 가 된다. - 같은 후보의 제외 사유가 후보 필터와 추천 양쪽에서 와서 두 번 보였다. 후보 id 로 합친다. - userDecision 에 note: null 을 보내지 않는다. 킷 스키마가 막는다. matchedOptions 를 만든다 - candidate-filters 응답에 후보별 attributes(spicyLevel·boneType)와 supportedOptions(SERVICE_TYPE·CUP)가 함께 온다. 사용자가 고른 값과 같은 어휘라 그대로 비교할 수 있다. 이름 문자열로 짐작하는 게 아니다. - 확인 카드의 "형태 순살 → 고르신 메뉴와 달라요" 가 이 결과다. - 서버가 matchedOptions 를 주면 그때 이 계산은 걷어낸다. 문서에 붙여 본 결과와 백엔드 쪽 문제 넷을 적는다 (승인 스키마 오류 · 품절 후보 추천 · explanation 문구 · boneType 미반영) 122 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 실서버에 붙어 있는지 눈으로 보이게 한다 화면만 봐서는 목인지 실서버인지 알 수가 없다. 둘 다 그럴듯한 답을 돌려주기 때문이다. 붙여 놓고도 "이게 진짜 도는 건가" 를 확인할 방법이 없다는 지적을 받았다. 실제로 나간 요청을 그대로 띄운다. ● 실서버에 붙어 있습니다 주고받은 요청 3건 · 성공 3건 200 /api/v1/recommendations 12ms 200 /api/v1/candidate-filters 14ms 200 /internal/simulation/session 12ms 이게 증거가 되는 이유는 목이 이 줄을 만들 수 없기 때문이다. 목은 브라우저 안에서 답을 만들어 내서 네트워크 요청이 아예 없다. - src/api/연동기록.ts — 오간 요청을 최근 12건만 들고 있는 작은 저장소. - createTeamBackend 의 보내기 가 경로·상태·걸린 시간을 남긴다. 실패도 남긴다. 성공만 보이면 조용한 실패를 못 본다. - App.tsx 의 연동표시 가 그것을 그린다. 배포본에는 들어가지 않는다. 팀백엔드모드는 빌드할 때 정해지는 상수라 기본 빌드에서는 이 컴포넌트를 부르는 자리가 통째로 빠진다. '실서버에 붙어 있습니다' · 'KIOBRIDGE_API_BASE' 가 dist 에 없는 것을 확인했다. 심사용 배포본이나 시연 영상에 개발용 창이 나올 일이 없다. 122 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 표준형을 직접 만들지 않고 서버 정규화 경로를 거친다 추천이 정규화된 값을 못 받고 있다는 지적을 받았다. 맞는 지적이었다. CanonicalProfile.SessionContext 를 프론트가 만들어 바로 추천에 넣고 있었다. 정규화를 거치면 세 가지가 달라진다. - providerId 를 짐작하지 않는다. 서버가 kiobridge.team-id 에서 채운다. 전에는 설정 파일을 보고 "WHATTHEBUG" 를 적어 두고 있었다. - 킷 스키마로 검증한 값만 쓴다. 잘못된 값이면 매핑 단계에서 멈춘다. 전에는 아무도 확인하지 않아서 승인 직전에야 터졌다. - reconfirmationFields 를 받는다. 어떤 항목을 다시 물어야 하는지 서버가 알려 준다. collectionMetadata 는 source WEB_FORM . confidence 1.0 . confirmedByUser true 로 보낸다. 사용자가 화면에서 직접 눌러 고른 값이라 그게 사실이다. 낮춰 적으면 서버가 RECONFIRMATION_REQUIRED 를 주는데, 이미 고른 것을 또 묻게 된다. 승인은 매핑 때 받아 둔 정규화 결과를 다시 쓴다. 승인 때 새로 만들면 확인 화면에서 본 조건과 실제 제출되는 조건이 갈라질 수 있다. 테스트 목이 호출 순서에 기대고 있었다. 정규화 두 개가 앞에 끼자 전부 어긋났다. 경로를 보고 답하도록 바꾼다 - 앞으로 호출이 늘어도 안 깨진다. 로컬 dev 로 확인했다. 여섯 경로 전부 200 이고 장바구니까지 간다. profile-normalizations . session-context-normalizations candidate-filters . recommendations . orchestrator/approve . simulation/session 122 -> 124 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 추천 전에 통합 검증(recommendationReady)을 확인한다 canonical-inputs/validate 를 건너뛰고 있다는 지적을 받았다. 맞는 지적이고, 형식 문제가 아니라 안전 문제였다. 개별 정규화는 각자 반쪽만 본다. 프로필 따로, 세션 맥락 따로. 합쳐야 보이는 게 있고 알레르기가 그렇다. 실제로 찔러 보니 이렇게 나온다. 정상 입력 status=VALID recommendationReady=true 알레르기 UNKNOWN status=RECONFIRMATION_REQUIRED recommendationReady=false HARD_CONSTRAINT_UNKNOWN "allergenIds 가 UNKNOWN 입니다. 임의로 추론하지 말고 재확인하거나 안전한 대체경로를 사용하세요." 이건 우리 코드에 실제로 있는 경로다. 변환기가 모르는 알레르기를 UNKNOWN 으로 보낸다 - 조용히 버리는 것보다 낫다고 판단해서다. 그런데 그 다음에 이 문을 안 거치고 바로 추천으로 갔다. 화면에 새 알레르기 선택지가 하나만 늘어도, 앱이 모르는 알레르기를 가진 분에게 그대로 음식을 추천하게 된다. recommendationReady 가 false 면 후보 필터도 추천도 부르지 않는다. 사용자에게는 서버 문구 대신 할 수 있는 일로 바꿔 말한다 - 프로필에서 다시 고르거나 직원에게 도움을 청하라고. 테스트로 잠갔다. false 일 때 실제 호출이 하나도 나가지 않아야 통과한다. 로컬 dev 로 확인했다. 일곱 경로 전부 200 이고 장바구니까지 간다. 124 -> 126 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: PR #46 코드 리뷰 14건 반영 사용자에게 사실이 아닌 말을 하던 것 - UNKNOWN 축을 '안 맞음' 으로 단정하고 있었다. 사용자가 고른 선택지를 우리가 enum 으로 못 옮긴 것인데 "오늘은 이 조합이 없어요" 가 나갔다. NO_PREFERENCE 와 함께 판단에서 뺀다. - 품절 안내의 조사가 '은' 으로 고정이었다. 받침 없는 이름에서 틀린다. 받침을 보고 은/는 을 고른다. - FAIL 을 계속 진행 중으로 두고 있었다. 화면이 90초를 기다리다 일반 오류로 끝난다. 끝난 일은 끝났다고 말하되, 안전 중단과 문구를 나눈다. 안전 중단만 직원 초기화를 안내한다. 승인이 서버 검증에 걸릴 수 있던 것 - 고른 후보를 1순위로 덮으면서 대안에서 빼지 않았다. 두 필드가 겹치면 RecommendationValidator 가 ALTERNATIVE_DUPLICATES_RECOMMENDED 로 막는다. 고른 것을 대안에서 빼고 원래 1순위를 대안으로 내린다. 지우기 약속을 못 지키던 것 - createTeamBackend 이 들고 있던 네 Map 을 비울 경로가 없었다. '이 기기에서 정보 지우기' 를 눌러도 정규화된 프로필과 세션 맥락이 메모리에 남았다. forgetSession 을 구현하고 createApi.forgetAll 이 환경 Map 도 비우게 한다. 낡은 값을 쓰던 것 - 정규화 캐시 키가 프로필 id 뿐이었다. 프로필을 고치거나 다른 키오스크에 붙으면 첫 결과를 계속 썼다. 환경과 입력 내용을 키에 넣는다. collectedAt 은 부를 때마다 달라져 키에서 뺀다 - 넣으면 캐시가 한 번도 안 맞는다. 그 밖에 - BFF 본문 읽기에 1MB 상한. 인증 없이 열린 함수라 상한이 없으면 큰 본문 하나로 메모리가 계속 는다. 넘으면 413. - consent.personalization 을 opts 로 받는다. 기본값은 true 로 둔다 - 사용자가 조건을 저장하고 이 프로필로 주문하기를 누른 행동이 그 주문에 한한 동의라고 본다. 앱이 실제로 개인화하는데 false 로 적는 것도 사실과 다르다. - 개발용 연결 표시를 접을 수 있게 하고 폭을 화면에 맞춘다. 200% 확대에서 휴대폰 틀의 아래 버튼을 덮어 터치를 가로챌 수 있었다. 문서 - 인터페이스 표가 filterCandidates.recommend 를 '경로 없음' 으로 적고 있었다. 같은 문서 앞부분과 코드는 이미 붙었다고 말한다. - matchedOptions 절이 낡았다. 프론트가 이미 채우고 있으니, 서버가 직접 주면 이 계산을 걷어내겠다는 요청으로 바꾼다. - '네 경로 전부 200' 이 실제로는 일곱 요청이다. - 언어 없는 코드펜스 14개에 text 를 붙인다. - README 의 목 구동 사유를 지금 상태로 맞추고, 형태.컵이 순위에 반영되지 않는다는 제한을 적는다. 로컬 dev 로 확인했다. 1순위가 아닌 후보를 골라도 승인이 통과하고 장바구니까지 간다. 126 -> 131 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: BFF 본문 상한을 바이트로 센다 모음.length 는 UTF-16 코드 단위라 한글 100만 자 = 약 3MB 가 통과했다. 상한이 의도의 셋 배였다. 청크의 Buffer 길이를 누적해서 바이트로 센다. 마지막에 Buffer.concat 으로 한 번에 문자열을 만들어 조각 경계에서 한글이 잘릴 여지도 없앤다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FEAT] ExecuteResult evidence(JsonNode) → Evidence 타입 파싱 연결 (#48) * feat: JsonNode evidence를 Evidence 타입으로 파싱해 오케스트레이터 응답에 연결 * fix: evidence 필수 필드 검증 및 NullNode 체크 추가, 요약 메시지 개선 * feat: 회원 및 프로필 영속성 모델 추가 (#50) * [FEAT] 회원가입 및 로그인 API 구현 (#52) * feat: 회원가입 및 로그인 API 구현 * fix: BCrypt 입력 제한 및 DB 예외 처리 보완 * test: BCrypt 72바이트 경계 검증 보완 * feat: 사용자 주문 프로필 저장 및 조회 API 구현 (#54) * chore: 운영 백엔드 PostgreSQL 네트워크 연결 (#55) * fix: executionplanservice null 체크 버그 수정 (#57) Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FIX] recommendation PASS, SKIP 점수 수정 (#59) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: RecommendationEngineService STEP5~7 추천 로직 초기 구현 * fix: STEP2 파트 passesByCandidateId 추가 전달 * fix: SKIPPED, PASS 점수 오류 수정 * fix: WARN 없는 항목의 confidence 임계값 크로스 테스트 픽스처 보정 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 아이디·비밀번호 회원가입·로그인 연동 및 주문표 서버 동기화 (#58) modules/member 의 네 경로에 프론트를 붙였습니다. 로그인은 끝까지 선택입니다 - 로그인하지 않아도 추천부터 최종 확인까지 전 과정이 그대로 동작합니다. 전화번호.인증번호 화면을 흐름째 걷어내고 사용자가 직접 지은 아이디.비밀번호로 바꿨습니다. 실명.전화번호를 받는 칸이 없습니다. (#56 의 프론트 지적이 이것입니다) '프로필' 을 '주문표' 로 바꿨습니다. 화면 말만 바뀌고 백엔드 profile 필드명은 그대로입니다. 함께 들어가는 것 - 승인 전에 거절할 수 있고, 거절도 서버에 기록합니다 - npm run check:backend - 앱이 부르는 경로를 앱이 부르는 순서로 확인 - 배포 빌드가 목으로 나가던 것을 고쳤습니다(build:team) - #48 의 summary.status 를 앱 말투로 옮겨 중단 화면 제목에 씁니다 백엔드에 부탁드린 것은 frontend/docs/BACKEND_INTEGRATION.md 에 있습니다 - 토큰 발급, 주문표 삭제 경로, place 완화, summary.status 를 코드로. 코드래빗 리뷰 24건(17 + 증분 7) 반영. 미해결 0건. frontend/ 만 바뀝니다. tsc 0, 195 tests, build ok. --------- Co-authored-by: ParkSeYoung128 <tpdudtpdud128@gmail.com> Co-authored-by: kjp0411 <98109773+kjp0411@users.noreply.github.com> Co-authored-by: Yena07 <164753655+Yena07@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Gganii <rkdms5991@naver.com> Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com>
cha-hyunwoo
added a commit
that referenced
this pull request
Aug 10, 2026
* [RELEASE] KioBridge P0 MVP 및 회원 영속성 배포 (#56) * chore: 백엔드 배포 기반 구성 * chore: aws 자동 배포 구성 (#5) * [FEAT] contracts 패키지 공통 파일 (#7) * chore: 설정 파일 및 패키지명 수정 * feat: contracts Simulation API 제출 계약 * feat: contracts 공통 상태 enum * chore: pr issue template 등록 * chore: gitignore 추가 * chore: coderabbit 설정 * feat: Target/Action/UserDecision/ExecutionPlan 레코드 * [FEAT] modules executionplan (#8) * feat: 응답 dto * feat: 세션 생성/제출/검증/실행/Evidence 호출 * feat: 전체 파이프라인 오케스트레이션 * fix: Simulation API 호출 timeout 설정 및 제출 전 승인-계획 불변식 검증 추가 * refactor: SimulationApiClient를 contracts로 이동 (#9) * [FEAT] Evidence, SimulationValidation 타입 정의 추가 (#11) * feat: Evidence, SimulationValidation 타입 정의 * feat: RunResult 등 run 관련 타입 추가, Evidence resolvedSimulationTrace 필드 보완 * [FEAT] SimulationApiClient Input Contract 조회·검증 기능 추가 (#13) * refactor: Jackson 3 JsonNode 타입으로 통일 * feat: Input Contract 조회 및 검증 기능 추가 * [FEAT] Evidence 조회 및 화면용 요약 서비스 구현 (#14) * feat: evidence 조회 API, 화면용 요약 서비스 추가 * fix: evidence.result() null 처리 및 결제 계획 시 FAIL 처리 반영 * [FEAT] Canonical Profile 정규화 API 구현 (#17) * feat: Canonical Profile 정규화 API 구현 * [FIX] Profile providerId를 서버 설정값으로 고정 * [FEAT] executionplan 내부 api 구현 및 리팩토링 & ParticipantSubmission 누락 필드 추가 (#18) * feat: 내부 api dto * feat: Service 세션 생성, 일괄처리 부분 분리 * feat: ParticipantSubmission 누락 필드 추가 * fix: ParticipantSubmission field null check + buildExecutionPlan (stub) 예외처리 추가 * [FEAT] 프론트엔드 온보딩·승인·실행 화면 추가 (#15) * feat: 온보딩·프로필·승인·실행 화면 추가 (Vite + React) 비어 있던 frontend/ 에 키오스크 접근성 앱을 넣는다. 프로필 저장부터 QR 연결, 매핑 결과 승인, 실행 진행, 장바구니 확인까지 전 화면이 동작한다. - 백엔드 연결 지점은 src/api/client.ts 의 KioBridgeApi 4개 메서드 한 곳 - 붙기 전까지는 mock 구현으로 돌고, setScenario() 로 예외 상태를 재현한다 - 프로필은 의미값(텍스트)만 담는다. 상품 ID·화면 좌표를 저장하지 않는다 - 결제 UI 없음. 종료 상태는 cart_ready("장바구니에 담았어요") 하나뿐 - 아이콘 Phosphor(MIT), 사진 출처는 src/assets/ATTRIBUTIONS.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: 프론트 타입과 백엔드 contracts 의 관계를 바로잡는다 "백엔드 contracts 와 이름을 맞춰 놨다"고 적었는데 사실이 아니다. 겹치는 타입 이름이 하나도 없다. contracts 는 주최측이 정한 Simulation Kit 규격이고, domain/types.ts 는 앱과 우리 서버 사이 규격이라 층이 다르다. 그대로 두면 백엔드 담당이 1:1 대응을 기대하고 붙이다 헤맨다. 실제로 이어지는 지점만 표로 적고, 아직 아무도 안 만든 것(앱용 REST 엔드포인트)과 ID 번역이 서버 몫이라는 점을 밝힌다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: low_confidence 승인도 서버가 다시 검증하게 한다 client.ts 는 "프론트 가드만 믿지 않는다"고 적어 두고 정작 low_confidence 만 빼놓고 있었다. 화면에서는 사용자가 카드를 짚어야 승인 버튼이 열리는데, 그 사실이 approve() 인자에 실리지 않아 서버가 확인할 방법이 없었다. changed 가 acknowledgedDiff 로 받는 보장을 low_confidence 도 받게 맞춘다. 확신이 낮을수록 사용자가 직접 짚었다는 사실이 더 중요하다. - ApproveInput 에 confirmedLowConfidence 추가 (필드마다 무엇을 뜻하는지 주석) - approve() 에 CONFIRMATION_REQUIRED 분기 추가 - OrderLowConfidence 호출부에서 confirmedLowConfidence: true 전달 CodeRabbit 리뷰 반영 (PR #15). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 프로필 카드에서 고르는 곳과 지우는 곳을 분리 카드 전체가 role="radio" 이고 그 안에 삭제 버튼이 들어 있었다. 삭제 버튼에 포커스를 두고 Enter 를 눌러도 카드만 선택되고 지워지지 않았다. 부모 div 의 onKeyDown 이 그 Enter 를 가로채 preventDefault() 하는 바람에 버튼의 기본 동작(클릭)이 막혔기 때문이다. 키보드만 쓰는 사람에게는 프로필을 지울 방법이 아예 없었다. - 선택은 시각적으로 숨긴 진짜 input[type=radio] 가 맡는다. 화살표 키 이동과 그룹당 탭 한 번(로빙 tabindex)을 브라우저가 처리한다. - 삭제 버튼은 label 바깥 형제로 옮겼다. - 목록 컨테이너는 role="radiogroup" 대신 fieldset + 숨긴 legend 로 바꿨다. - 포커스 표시는 라디오의 focus 를 받아 카드에 3px outline 으로 그린다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(qr): 스캐너의 중첩 타이머를 정리하지 않아 언마운트 뒤 페어링이 나가던 문제 QrScannerModal 의 useEffect 는 바깥 2.5초 타이머만 해제하고 그 안에서 예약한 800ms 타이머는 그대로 두었다. 스캐너가 떠 있는 동안에도 하단 탭은 눌린다. 스캔이 끝난 뒤 800ms 안에 다른 탭으로 옮기면 안쪽 타이머만 살아남아 언마운트된 컴포넌트에서 onDetected 가 돌고 api.claimPairing 이 호출된다. 화면에는 아무 표시도 안 되는데 키오스크에는 사용자가 요청한 적 없는 연결 요청이 남는다. 두 타이머 ID 를 모두 붙잡아 cleanup 에서 함께 해제한다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 프로필 삭제 버튼의 터치 영역을 44x44 로 넓힘 높이는 44 였지만 폭이 30 이었다. 앱 전체를 훑어 44px 미만인 조작 요소는 이것 하나뿐이었다. 밑줄은 글자에만 걸리므로 minWidth 를 44 로 올려도 보이는 크기는 그대로고 누를 수 있는 영역만 넓어진다. 카드 크기(321x168)도 그대로다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 기본 팔레트가 WCAG AA 에 미달하던 대비를 전부 올림 고대비를 별도 옵션으로 켜고 끄게 만드는 대신 기본값을 고쳤다. 찾아 들어가야 보이는 설정은 대비가 필요한 사람이 못 찾을 수 있고, 지금 값은 옵션을 켜기 전 상태에서 이미 기준 미달이었다. 측정값 (흰 배경 / SURFACE 위): - TEXT_2 #8A8A8E 3.44 / 3.21 → #6B6B70 5.30 / 4.95 - TEXT_3 #C4C4C8 1.74 / 1.62 → 글자에서 전부 제거 - FAIL #D92D20 FAIL_BG 위 4.35 → #CE2A1F 4.75 고친 곳 - 읽어야 하는 글자에 쓰이던 TEXT_3 12곳을 TEXT_2 로 옮김. 하단 탭의 안 눌린 항목(내 프로필·계정)이 1.74:1 이라 특히 나빴다. - 고른 프로필 카드: 태그·메모·삭제가 반투명 흰색이라 3.31~3.35 였다. 태그는 바탕을 어둡게 눌러 6.6, 나머지는 흰색으로 올려 5.08. - QR 스캔 안내문 42% 흰색(3.95) → 62%(7.8). 조명을 조절하라는 문장은 화면이 어두울 때 읽는 문장이다. - 안 고른 동그라미 테두리와 변경 확인 체크박스 테두리가 각각 1.62 / 1.29 라 컨트롤 경계 기준(3:1)에 못 미쳤다. TEXT_2 / WARN 으로 바꿈. - 비활성 버튼 글자는 WCAG 예외지만 1.62 는 무엇을 기다리는지 못 읽는다. 초록 알약이 회색으로 바뀌는 것만으로 신호는 충분해서 TEXT_2 로 올림. WARN_BORDER 는 쓸 곳이 없어져서 뺐다. 시나리오 스위치를 돌려 가며 전 화면을 다시 쟀다. 환영·전화번호·인증·이름·프로필 목록·프로필 만들기·QR(스캔/연결중/연결됨/실패/만료) ·확인(정확/애매/없음/변경/불확실)·실행(진행/담김/중단)·계정·접근성·개인정보 전부 미달 0건. * fix(a11y): QR 화면 그릇이 넘칠 때 아래 버튼에 닿을 수 없던 문제 QrScreen 의 가운데 그릇이 overflow-hidden 이었다. 그 안의 패널들(연결중·연결됨·실패·만료)은 아래 버튼을 mt-auto 로 바닥에 붙인다. 내용이 그릇보다 커지면 그 버튼이 잘려 나가는데, hidden 이라 스크롤도 안 생긴다. 포커스를 옮기면 브라우저가 알아서 스크롤해 주므로 키보드로는 닿지만, 손가락으로는 닿을 방법이 아예 없다. 앱이 주는 큰 글씨(1.18배)까지는 넘치지 않는다. 1.4배까지도 안 넘친다. 사용자가 브라우저·OS 글씨 크기를 더 키우면 1.6배 근처부터 넘친다(측정 102px, 1.8배에서 195px). overflow-y-auto 로 바꾸면 넘칠 때만 스크롤이 생기고 안 넘치면 지금과 똑같이 보인다. 1 / 1.18 / 1.6 / 1.8 / 2.2 배에서 잘리는 그릇 0건, 못 닿는 조작 요소 0건. 프로필 목록·계정·개인정보 화면도 1.8 배에서 함께 확인했다. * chore(review): 안 쓰는 shadcn 원본 48개를 CodeRabbit 리뷰 대상에서 제외 frontend/src/app/components/ui/** 는 Figma Make 재출력용으로만 남겨 둔 파일들이라 어느 화면도 import 하지 않는다. 번들에도 들어가지 않는다. 리뷰가 반복해서 여기를 지적하는데, 고쳐도 사용자에게 도달하는 코드는 바뀌지 않아 실제 결함을 찾는 데 방해가 된다. 다시 쓰기 시작하면 이 줄을 지운다. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * [FEAT] 장소별 질문 목록을 시뮬레이션 킷 fixture 축에 맞춤 (#20) * feat(profile): 장소별 질문을 시뮬레이션 킷 fixture 축에 맞춤 앱이 묻는 조건과 심사 환경이 실제로 가르는 축이 서로 달랐다. 드라이브스루·버거 세트·사이드 변경처럼 fixture 에 대응이 없는 질문은 어르신이 골라도 결과가 달라지지 않는, 물어만 보고 버리는 질문이었다. 반대로 fixture 에 있는데 앱에 없던 축이 셋 있었다. - 병원 '접근성 지원' — 우리가 점수 축 20% 로 넣어 놓고 물어보는 화면이 없었다 - 관공서 '인증 방식' — PUBLIC-004 를 AUTH_METHOD_UNAVAILABLE 로 빼는 조건인데 앱에서 입력할 방법이 없었다 - 음식점 '컵' — 확인 카드 필수 5개 중 하나인데 '추가 옵션' 다중선택에 묻혀 있었다 메뉴 이름은 목록에서 고르게 하지 않고 자유 입력(menuName)으로 둔다. 사용자가 부르는 이름과 오늘 화면의 이름을 맞추는 건 매핑의 일이다. 알레르기만 fixture 축에 맞추지 않았다. option-group 이 아니라 사람에 대한 절대 조건이라, 오늘 이 가게에 걸리는 게 땅콩뿐이라고 나머지를 지우면 새우 알레르기가 있는 사람이 그 사실을 말할 방법이 사라진다. mock.ts 의 옵션 라벨도 같은 이름으로 맞췄다. tsc --noEmit · build 통과. 세 장소 모두 화면에서 확인. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(frontend): 안 쓰는 shadcn 컴포넌트 48개와 딸려 온 의존성 53개 제거 frontend/src/app/components/ui/ 의 48개 파일은 어느 화면도 import 하지 않는다. components/figma/ImageWithFallback.tsx 도 마찬가지고, styles/globals.css 는 0바이트 빈 파일이었다. 참조 0건을 확인하고 지웠다. 지우기 전에 react 와 react-dom 을 dependencies 에 명시했다. 지금까지 두 패키지가 선언 없이 @mui/material·radix 의 전이 의존으로만 딸려 오고 있었다. ui/ 를 걷어내면 React 자체가 사라질 뻔했다. tw-animate-css 는 남긴다. JS import 가 아니라 styles/tailwind.css 의 @import 라 코드 검색에는 안 걸리지만 실제로 쓰인다. dependencies 55개 → 4개 (react, react-dom, lucide-react, tw-animate-css) npm ci 92 packages / 6s JS 번들 223.12 kB → 223.12 kB (변화 없음 — 원래 번들에 없던 코드다) CSS 번들 86.03 kB → 13.58 kB (Tailwind 가 그 48개까지 훑고 있었다) tsc --noEmit · build 통과. 개인 저장소와 번들 해시 동일. 실제 화면에서 폰 프레임·Tailwind 유틸리티·QR 화면 overflow-y-auto· 1/1.18/1.6/2.2배 확대를 모두 확인했다. .coderabbit.yaml 의 components/ui 제외 필터는 이제 가리킬 대상이 없어졌지만, 다음 PR 에서 별도로 정리한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * CodeRabbit Generated Unit Tests: Add Generated Unit Tests for PR Changes (#22) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Delete frontend directory * Create yena.txt * Delete .coderabbit.yaml * [FEAT] chicken-store SessionContext 정규화 API 구현 (#25) * [FEAT] recommendation contracts 정의 (#21) * feat: recommendation관련 contracts 정의 (Recommendation, ExcludedCandidate, Candidate, CompatibilityRule, CompatibilityRuleSet) * fix: ParticipantSubmission에 누락된 inputContractVersion 필드 추가, TODO: 담당2 Recommendation 타입으로 교체 * feat: PublicFixture 계약 정의 및 fixture·compatibility-rules 조회 메서드 추가 * chore: 운영 프론트 cors 설정 추가 (#27) * [REFACTOR] SessionContext 공통 기반 타입과 치킨 구현 분리 (#29) * refactor: SessionContext 공통 기반 타입과 치킨 구현 분리 * style: SessionContext 리팩터링 코드 정리 * [FEAT] Evidence.recommendation 타입 교체 (Object → Recommendation) (#32) * feat: Evidence.recommendation 타입을 Recommendation으로 교체 * fix: recommendationReasons 첫 번째 비공백 값만 요약에 사용 * feat: Canonical Input 통합 검증 API 구현 (#34) * [FEAT] contracts lost fields 업데이트 (#35) * feat: ExecutionPlan filed 업데이트 * feat: UserDecision filed 업데이트 * fix: approved/actions 불변식 강제 + confirmedAt 형식 검증: * [FEAT] recommendation ruleevaluator interface (#36) * feat: RuleResult * feat: Rule EvaluationResult record * feat: RuleEvaluator 인터페이스 * feat: 구현체 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation 필터링 오케스트레이션 구현 (#37) * chore: 파일삭제 * feat: 필터링 후보 결과 레코드 * feat: 후보 필터링 서비스 * fix: CandidateFilterResult 리스트 불변 복사 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation ruleevaluator (#38) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: Kit compare() 사용 * feat: asList(), stringOf() 공통 유틸 * feat: RuleEvaluator 수정 * test: CandidateFilter + RuleEvaluator 테스트 코드 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation engine 초기 구현 (#39) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: RecommendationEngineService STEP5~7 추천 로직 초기 구현 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 프론트엔드 앱 추가 및 백엔드 연동 계층 (#30) * feat: 프론트엔드 앱 추가 및 백엔드 연동 계층 dev 에서 frontend 가 통째로 지워져 있어(44301ac) 다시 올린다. 개인 저장소(Yena07/kiobridge-app)에서 리뷰 112건을 반영하며 만든 것이고 테스트 84개가 붙어 있다. [무엇이 들어 있나] src/app/App.tsx 화면 전부 (온보딩·프로필·QR·확인·실행) src/api/client.ts 앱이 쓰는 계약 + 목 구현 src/api/backend.ts 팀 백엔드 연동 계층 src/api/mock.ts 프로필로 매핑 응답을 만드는 로직 src/domain/ 계약 타입 · 장소별 질문 목록 api/bff/ 백엔드 호출을 서버가 대신 보내는 프록시 [백엔드 붙이는 법] client.ts 마지막 줄만 바꾸면 된다. export const api = createApi(createTeamBackend()); createTeamBackend 는 팀 백엔드가 실제로 구현한 경로에 맞춰 두었다. 명세서와 다른 부분이 있어서 그대로 옮기면 안 붙는다. 명세서 실제 구현 POST /api/v1/sessions POST /internal/simulation/session submission -> validate -> execute POST /internal/simulation/submit-and-run candidate-filters / recommendations 아직 없음 추천 계열이 생기면 filterCandidates·recommend 둘만 채우면 된다. [CORS 는 해결해 두었다] api/bff 가 백엔드로 대신 보내므로 브라우저는 같은 출처로만 요청한다. 백엔드에서 allowed-origin 을 맞출 필요가 없고, 미리보기 배포마다 주소가 바뀌는 문제도 없다. Vercel 환경변수 KIOBRIDGE_API_BASE 만 넣으면 된다. 열린 프록시가 되지 않게 통과 경로를 명시하고 클라이언트 헤더는 넘기지 않는다. [.coderabbit.yaml 복구] 루트 설정이 지워져 있어 되살렸다. 팀 저장소이므로 profile 은 chill 로 두고, base_branches 를 dev·main 으로, path_instructions 를 frontend/** 로 맞췄다. 실격 조건(결제 UI 부재·승인 전 실행 경로 없음·상품 ID 미보유·병원 의료 판단 금지·관공서 자격 판단 금지)과 접근성 기준이 들어 있다. [프론트가 지키고 있는 것] - 결제 관련 문자열 0건. src 전체를 훑는 테스트로 잠가 두었다. - 승인 전 실행계획 생성 0건. approve 호출은 버튼 핸들러 안에만 있다. - 상품 ID 미보유. 후보 표식은 c1·c2·c3 형태만. - 선택 불가능 후보 추천 0건. 알레르기·품절·이용 불가는 후보에서 제거한다. - 신뢰도 낮으면 사용자가 직접 짚어야 승인된다. - 실제 개인정보 미수집. 이름 대신 호칭, 전화번호는 시연용 고정값. npm ci · npx tsc --noEmit · npm test 84/84 · npm run build 통과. 번들 해시가 개인 저장소 빌드와 동일하다. * fix: 코드 리뷰 14건 반영 승인 경로 - client.ts: 중복 승인 검사와 확정 사이의 await 를 없앤다. 동시에 들어온 승인 두 건이 모두 검사를 통과해 둘 다 담기던 문제. backend.ts 와 같은 방식으로 첫 await 전에 표시하고 실패하면 되돌린다. - backend.ts: clarification 이 아닌데 candidateId 가 오면 거절한다. client.ts 는 이미 막고 있었고 이쪽만 열려 있었다. 응답 문구 - mock.ts: 담을 후보가 없는 이유(장소 미정·지원하지 않는 장소·조건 제외)를 시나리오 스위치가 not_found 를 골랐을 때도 구분한다. 병원 프로필에도 "메뉴가 바뀌었을 수 있어요" 라고 답하던 문제. - mock.ts: 메뉴 이름 뒤 조사를 헬퍼로 붙인다. 사용자가 적은 이름에 받침이 없으면 "커피이 없어요" 가 됐다. 접근성 - App.tsx·tokens.ts: 회전 표시를 SVG SMIL 에서 CSS 애니메이션으로 바꾼다. SMIL 은 CSS 규칙의 대상이 아니라 prefers-reduced-motion 이 멈추지 못했다. - App.tsx: QR 탭에 다시 들어갈 때 연결이 살아 있으면 스캐너를 열지 않고 연결 상태를 보여 준다. 같은 상태를 두고 두 화면이 다른 말을 했다. 정리 - tsconfig.json: compilerOptions.types 가 두 번 적혀 뒤엣것이 앞엣것을 덮었다. - App.tsx: 렌더 본문에서 ref 를 변형하던 것을 useEffect 로 옮긴다. - App.tsx: options 선언을 참조보다 위로 옮긴다. - catalog.tsx: DETAIL_OPTIONS·PLACE_ICONS 키를 PlaceType 으로 좁힌다. - ATTRIBUTIONS.md: 코드블록 언어 표시와 제목 뒤 빈 줄. 테스트 - 결제 문자열 스캔 범위를 src 에서 리포지터리 루트로 넓힌다. index.html· vite.config.ts·api/bff 가 검사에서 빠져 있었다. 제외 목록은 .gitignore 를 읽는다. - 동시 승인 회귀 테스트를 client·backend 양쪽에 추가. - '조건 얘기를 한다' 테스트가 이름과 다른 것을 단언하던 것을 바로잡는다. 84 → 89 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: 팀 레포 PR 코드 리뷰 반영 확인 화면이 사실이 아닌 말을 하던 것 - mock.ts: 시나리오 스위치가 '변경' 인데 실제로는 다 맞을 때, 멀쩡한 행 하나에 matched:false 와 "오늘은 제공되지 않아요" 를 붙이던 시연용 경로를 없앤다. 실제로 제공되는 옵션을 안 된다고 말한 것이고, 심사 중 스위치를 돌리면 그대로 보였다. 스위치는 결과 종류를 고를 뿐 없는 불일치를 만들지 못한다. - backend.ts: 이름 없는 후보를 다 걸러내고도 clarification 을 내보내면, 화면은 "비슷한 메뉴가 여러 개예요" 라면서 고를 것을 하나도 못 보여 준다. 승인은 후보 선택을 요구하므로 사용자가 그 화면에서 빠져나갈 수 없다. 연동 계층 - createSession 이 claimCode 를 버리고 있었다. QR 로 어떤 키오스크를 찍었는지와 무관하게 세션이 열린다는 뜻이다. 함께 보낸다. - getEvidence 를 POST 로 부르고 있었다. 명세는 GET 이다. - getEvidence 가 cart·abort 를 옮기지 않아 "장바구니에 담았어요" 화면에 개수도 금액도 뜨지 않았다. 중단 사유도 마찬가지. BFF - 경로만 검사하고 메서드는 무엇이든 통과시켰다. GET·POST 만 연다. 테스트 - 저장소 루트를 .git 위치로 찾는다. '..' 개수를 고정하면 개인 레포와 팀 레포 중 한쪽이 반드시 틀린다. 팀 레포에서는 frontend 까지만 올라가 그 바깥을 검사하지 못했고, 개인 레포에서 한 단계 더 올리면 Downloads 전체를 훑는다. - 스캔이 실제로 훑었는지를 먼저 잠근다. 빈 배열은 늘 통과하므로, 기준점이 틀어지면 이 파일의 모든 보증이 조용히 사라진다. - .md 도 훑는다. 실제로 연동 문서에 금지어를 그대로 적어 둔 적이 있다. - 응답 전체에 서버 후보 ID 가 섞이지 않는지 결과 종류마다 검사한다. - planId 에 일련번호를 붙인다. 밀리초만 쓰면 지연 0 인 테스트에서 겹친다. 문서·기타 - ATTRIBUTIONS.md 를 빌드가 dist 로 복사한다. CC BY-SA 는 저작자 표시를 저작물과 함께 배포할 것을 요구하는데, 문서 자신은 "배포물에도 포함된다" 라고 적혀 있으면서 실제로는 번들에 없었다. - BACKEND_INTEGRATION.md: 금지 문자열을 그대로 적어 둔 줄을 고치고, 질문 번호를 정리하고(③ 중복), claimCode·evidence 경로를 질문에 추가하고, 후보 식별자를 서버가 들고 있어 달라는 요청을 적는다. - README.md 코드블록 언어 표시. 90 → 92 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FEAT] recommendation controller (#41) * feat: ValidationIssue 검증 결과 항목 타입 추가 * feat: ValidationOutcome 검증 결과 타입 추가 * feat: RecommendationValidator STEP5 출력 자체 검증 로직 구현 * feat: candidate-filters 요청 DTO 추가 * feat: recommendations 요청 DTO 추가 * feat: recommendation-output-validations 요청·응답 DTO 추가 * feat: RecommendationController candidate-filters·recommendations·recommendation-output-validations API 구현 * feat: RecommendationController candidate-filters·recommendations·recommendation-output-validations API 구현 * fix: RecommendationValidator가 제외된 추천 후보·중복 대안 후보를 검증하지 못하던 문제 수정 * [FEAT] executionplan buildexecutionplan (#42) * feat: 시그니처 교체하고 실제 조립 로직 구현 * feat: 그룹별 실제 option id 결정 * feat: 컨트롤러에서 environmentId/sessionContext까지 서비스로 전달하도록 수정 * feat: Object 객체 확정 객체로 교체 * test: executionplan + ChickenSToreOptionResolver test code * feat: sessionId<->environmentId 매핑 처리 * test: test 갱신 * fix: ChickenStoreOptionReslover 버그 수정 + 예외 처리 구현 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] orchestrator구현 (#43) * feat: environmentId 재조회 및 ParticipantSubmission 조립 * feat: POST /internal/orchestrator/approve 신설 * feat: 오케스트레이터 레코드 * test: 오케스트레이터 test --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * fix: CandidateFilterService 의 품절 여부 체크 추가 (#44) Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FIX] 통합 테스트 중 버그 수정 (#45) * fix: CandidateFilterService 의 품절 여부 체크 추가 * fix: Target, UserDecision에 @JsonInclude(JsonInclude.Include.NON_NULL) 추가 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 백엔드 신규 경로 연동 및 입력 정규화 적용 (#46) * feat: 프론트엔드 앱 추가 및 백엔드 연동 계층 dev 에서 frontend 가 통째로 지워져 있어(44301ac) 다시 올린다. 개인 저장소(Yena07/kiobridge-app)에서 리뷰 112건을 반영하며 만든 것이고 테스트 84개가 붙어 있다. [무엇이 들어 있나] src/app/App.tsx 화면 전부 (온보딩·프로필·QR·확인·실행) src/api/client.ts 앱이 쓰는 계약 + 목 구현 src/api/backend.ts 팀 백엔드 연동 계층 src/api/mock.ts 프로필로 매핑 응답을 만드는 로직 src/domain/ 계약 타입 · 장소별 질문 목록 api/bff/ 백엔드 호출을 서버가 대신 보내는 프록시 [백엔드 붙이는 법] client.ts 마지막 줄만 바꾸면 된다. export const api = createApi(createTeamBackend()); createTeamBackend 는 팀 백엔드가 실제로 구현한 경로에 맞춰 두었다. 명세서와 다른 부분이 있어서 그대로 옮기면 안 붙는다. 명세서 실제 구현 POST /api/v1/sessions POST /internal/simulation/session submission -> validate -> execute POST /internal/simulation/submit-and-run candidate-filters / recommendations 아직 없음 추천 계열이 생기면 filterCandidates·recommend 둘만 채우면 된다. [CORS 는 해결해 두었다] api/bff 가 백엔드로 대신 보내므로 브라우저는 같은 출처로만 요청한다. 백엔드에서 allowed-origin 을 맞출 필요가 없고, 미리보기 배포마다 주소가 바뀌는 문제도 없다. Vercel 환경변수 KIOBRIDGE_API_BASE 만 넣으면 된다. 열린 프록시가 되지 않게 통과 경로를 명시하고 클라이언트 헤더는 넘기지 않는다. [.coderabbit.yaml 복구] 루트 설정이 지워져 있어 되살렸다. 팀 저장소이므로 profile 은 chill 로 두고, base_branches 를 dev·main 으로, path_instructions 를 frontend/** 로 맞췄다. 실격 조건(결제 UI 부재·승인 전 실행 경로 없음·상품 ID 미보유·병원 의료 판단 금지·관공서 자격 판단 금지)과 접근성 기준이 들어 있다. [프론트가 지키고 있는 것] - 결제 관련 문자열 0건. src 전체를 훑는 테스트로 잠가 두었다. - 승인 전 실행계획 생성 0건. approve 호출은 버튼 핸들러 안에만 있다. - 상품 ID 미보유. 후보 표식은 c1·c2·c3 형태만. - 선택 불가능 후보 추천 0건. 알레르기·품절·이용 불가는 후보에서 제거한다. - 신뢰도 낮으면 사용자가 직접 짚어야 승인된다. - 실제 개인정보 미수집. 이름 대신 호칭, 전화번호는 시연용 고정값. npm ci · npx tsc --noEmit · npm test 84/84 · npm run build 통과. 번들 해시가 개인 저장소 빌드와 동일하다. * fix: 코드 리뷰 14건 반영 승인 경로 - client.ts: 중복 승인 검사와 확정 사이의 await 를 없앤다. 동시에 들어온 승인 두 건이 모두 검사를 통과해 둘 다 담기던 문제. backend.ts 와 같은 방식으로 첫 await 전에 표시하고 실패하면 되돌린다. - backend.ts: clarification 이 아닌데 candidateId 가 오면 거절한다. client.ts 는 이미 막고 있었고 이쪽만 열려 있었다. 응답 문구 - mock.ts: 담을 후보가 없는 이유(장소 미정·지원하지 않는 장소·조건 제외)를 시나리오 스위치가 not_found 를 골랐을 때도 구분한다. 병원 프로필에도 "메뉴가 바뀌었을 수 있어요" 라고 답하던 문제. - mock.ts: 메뉴 이름 뒤 조사를 헬퍼로 붙인다. 사용자가 적은 이름에 받침이 없으면 "커피이 없어요" 가 됐다. 접근성 - App.tsx·tokens.ts: 회전 표시를 SVG SMIL 에서 CSS 애니메이션으로 바꾼다. SMIL 은 CSS 규칙의 대상이 아니라 prefers-reduced-motion 이 멈추지 못했다. - App.tsx: QR 탭에 다시 들어갈 때 연결이 살아 있으면 스캐너를 열지 않고 연결 상태를 보여 준다. 같은 상태를 두고 두 화면이 다른 말을 했다. 정리 - tsconfig.json: compilerOptions.types 가 두 번 적혀 뒤엣것이 앞엣것을 덮었다. - App.tsx: 렌더 본문에서 ref 를 변형하던 것을 useEffect 로 옮긴다. - App.tsx: options 선언을 참조보다 위로 옮긴다. - catalog.tsx: DETAIL_OPTIONS·PLACE_ICONS 키를 PlaceType 으로 좁힌다. - ATTRIBUTIONS.md: 코드블록 언어 표시와 제목 뒤 빈 줄. 테스트 - 결제 문자열 스캔 범위를 src 에서 리포지터리 루트로 넓힌다. index.html· vite.config.ts·api/bff 가 검사에서 빠져 있었다. 제외 목록은 .gitignore 를 읽는다. - 동시 승인 회귀 테스트를 client·backend 양쪽에 추가. - '조건 얘기를 한다' 테스트가 이름과 다른 것을 단언하던 것을 바로잡는다. 84 → 89 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: 팀 레포 PR 코드 리뷰 반영 확인 화면이 사실이 아닌 말을 하던 것 - mock.ts: 시나리오 스위치가 '변경' 인데 실제로는 다 맞을 때, 멀쩡한 행 하나에 matched:false 와 "오늘은 제공되지 않아요" 를 붙이던 시연용 경로를 없앤다. 실제로 제공되는 옵션을 안 된다고 말한 것이고, 심사 중 스위치를 돌리면 그대로 보였다. 스위치는 결과 종류를 고를 뿐 없는 불일치를 만들지 못한다. - backend.ts: 이름 없는 후보를 다 걸러내고도 clarification 을 내보내면, 화면은 "비슷한 메뉴가 여러 개예요" 라면서 고를 것을 하나도 못 보여 준다. 승인은 후보 선택을 요구하므로 사용자가 그 화면에서 빠져나갈 수 없다. 연동 계층 - createSession 이 claimCode 를 버리고 있었다. QR 로 어떤 키오스크를 찍었는지와 무관하게 세션이 열린다는 뜻이다. 함께 보낸다. - getEvidence 를 POST 로 부르고 있었다. 명세는 GET 이다. - getEvidence 가 cart·abort 를 옮기지 않아 "장바구니에 담았어요" 화면에 개수도 금액도 뜨지 않았다. 중단 사유도 마찬가지. BFF - 경로만 검사하고 메서드는 무엇이든 통과시켰다. GET·POST 만 연다. 테스트 - 저장소 루트를 .git 위치로 찾는다. '..' 개수를 고정하면 개인 레포와 팀 레포 중 한쪽이 반드시 틀린다. 팀 레포에서는 frontend 까지만 올라가 그 바깥을 검사하지 못했고, 개인 레포에서 한 단계 더 올리면 Downloads 전체를 훑는다. - 스캔이 실제로 훑었는지를 먼저 잠근다. 빈 배열은 늘 통과하므로, 기준점이 틀어지면 이 파일의 모든 보증이 조용히 사라진다. - .md 도 훑는다. 실제로 연동 문서에 금지어를 그대로 적어 둔 적이 있다. - 응답 전체에 서버 후보 ID 가 섞이지 않는지 결과 종류마다 검사한다. - planId 에 일련번호를 붙인다. 밀리초만 쓰면 지연 0 인 테스트에서 겹친다. 문서·기타 - ATTRIBUTIONS.md 를 빌드가 dist 로 복사한다. CC BY-SA 는 저작자 표시를 저작물과 함께 배포할 것을 요구하는데, 문서 자신은 "배포물에도 포함된다" 라고 적혀 있으면서 실제로는 번들에 없었다. - BACKEND_INTEGRATION.md: 금지 문자열을 그대로 적어 둔 줄을 고치고, 질문 번호를 정리하고(③ 중복), claimCode·evidence 경로를 질문에 추가하고, 후보 식별자를 서버가 들고 있어 달라는 요청을 적는다. - README.md 코드블록 언어 표시. 90 → 92 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 팀 백엔드 실제 응답 모양에 맞춰 연동 계층을 다시 짠다 dev 의 ExecutionPlanController.ExecuteResult.Evidence 를 직접 읽고 맞췄다. submit-and-run 응답에 증거가 같이 오므로 따로 조회하지 않는다. valid 가 실행 여부를 알려 주므로 validate 가 서버 판단을 그대로 읽는다. reviewSnapshot 에서 장바구니 개수.금액을 만들고, 없으면 비워 둔다. 99 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: 실제로 띄워서 붙여 본 결과를 적는다 킷.백엔드.프론트 셋을 로컬에 올리고 요청을 넣어 본 결과다. 되는 것: 세션 생성. claimCode 를 보내도 400 이 나지 않음. 막히는 것: buildExecutionPlan 미구현, State enum 이 닭강정집 전용, 추천 계열 HTTP 경로 없음. 셋 다 백엔드 쪽이고 프론트가 손댈 수 없다. 급한 순서 표와 로컬에서 셋 다 띄우는 방법을 함께 넣는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: 문서 안에서 서로 어긋나던 두 곳을 맞춘다 붙이는 방법이 createHttpBackend 라고 되어 있었다. 팀 백엔드에는 createTeamBackend 를 써야 한다. 인터페이스 표의 getEvidence 도 앞쪽 설명과 두 말을 하고 있었다. 명세서 경로와 팀 백엔드 실제를 두 칸으로 나눠 적는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(readme): 낡은 곳 넷을 고친다 - 백엔드 붙이는 예시가 createHttpBackend 였다. createTeamBackend 로 고치고, 추천 경로가 없어 지금 바꾸면 확인 화면이 비어 버린다는 것도 적는다. - 결제 문자열 스캔 범위가 src 가 아니라 저장소 전체다. - '백엔드가 아직 없어' 는 더 이상 사실이 아니다. - 구조 표에 api/bff 가 빠져 있었다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: BFF 함수가 배포에서 잡히지 않던 두 가지를 고친다 배포본을 찔러 보니 경로마다 답이 달랐다. /api/bff/x 500 /api/bff/internal/simulation/session 404 catch-all 파일명(api/bff/[...path].ts)을 걷어내고 평범한 api/bff.ts 하나로 바꾼다. 뒤쪽 경로는 vercel.json rewrite 가 ?p= 로 넘긴다. 핸들러도 Node 방식(req, res)으로 바꾼다. 웹 방식은 req.url 이 절대 주소가 아니라 new URL 에서 터졌다 - 500 의 정체다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 팀 백엔드의 새 경로 셋에 붙인다 orchestrator/approve 안내 문서대로 셋 다 반영했다. 1) BFF 허용경로 internal/orchestrator/approve 와 api/v1/recommendation-output-validations 를 연다. 실제로 쓰는 순서대로 목록을 다시 정렬한다. 2) 한글 선택지 → enum 변환기 (src/api/canonical.ts) 화면은 '매운맛' 을 들고 있고 계약은 SpicyLevel.HOT 이다. 둘을 잇는 곳을 한 파일에 모은다. 값은 백엔드 enum 파일에서 그대로 옮겼다. · 안 고른 축은 NO_PREFERENCE, 모르는 값은 UNKNOWN 으로 구분한다. 뭉뚱그리면 새 선택지가 생겼을 때 서버가 '아무거나 괜찮대요' 로 읽는다. · 모르는 알레르기도 버리지 않고 UNKNOWN 으로 보낸다. 조용히 버리면 그 사람의 알레르기가 서버에 전달되지 않는다. · 이름·전화번호가 들어갈 자리를 만들지 않는다. displayName 은 선택이라 아예 안 보낸다. 테스트로 잠갔다. 3) 승인 경로 교체 submit 이 orchestrator/approve 로 다섯 조각을 갖춰 보낸다. environmentId 는 보내지 않는다 — 서버가 sessionId 로 다시 조회한다. 사용자가 고른 후보가 있으면 그것이 1순위로 바뀌어 나간다. 곁들여 - filterCandidates·recommend 를 실제 경로에 붙인다. 이름·가격은 추천이 아니라 candidate-filters 의 eligibleCandidates 에서 온다. 이게 없으면 화면에 상품 ID 밖에 보여 줄 게 없고 그건 실격 조건이다. - recommend 에 survivingCandidateIds 를 보내지 않는다. 서버가 다시 계산한다 — 클라이언트가 보낸 필터 결과를 믿으면 알레르기 필터를 우회할 수 있다. 그 판단이 옳아서 따라간다. - createSession 이 돌려주는 environmentId 를 받아 쓴다. 예전에는 chicken-store 로 고정이라 병원에 붙어도 닭강정을 봤다. - client.ts 에 getProfile 을 연다. 백엔드에 프로필 저장소가 없어서 매 호출마다 내용을 함께 보내야 한다. 아직 전환하지 않는다. matchedOptions 가 없어 확인 카드가 조건별 판단을 못 채우고, 운영 배포에 컨트롤러가 아직 없다. 그 둘이 풀리면 client.ts 마지막 줄 하나만 바꾸면 된다. 111 → 119 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 로컬에서 dev 백엔드로 붙여 보고 찾은 것들을 고친다 운영 배포(main)에 컨트롤러가 없어서 dev 를 로컬에서 돌려 붙였다. 네 경로 전부 200 이고 확인 화면이 실제 백엔드 데이터로 그려진다. 로컬 연동 길을 연다 - npm run dev:team — VITE_BACKEND=team 으로 팀 백엔드를 쓴다. 기본(npm run dev)은 그대로 목이라 배포본은 바뀌지 않는다. - vite 개발 서버가 /api/bff 를 KIOBRIDGE_API_BASE 로 넘긴다. 배포본의 BFF 함수가 하는 일을 개발 서버가 대신하는 것이라 CORS 가 없다. - 그 프록시가 Origin·Referer·Cookie 를 떼고 보낸다. 안 떼면 백엔드 CORS 필터가 403 을 준다. 배포본 BFF 도 클라이언트 헤더를 넘기지 않는다. 붙여 보고 고친 것 - 제외 사유로 explanation 대신 reasonText 를 쓴다. explanation 은 "ruleId=CHICKEN_ALLERGEN_HARD_CONSTRAINT, sourceValue=[PEANUT]" 같은 규칙 추적 문자열이다. 그대로 두면 어르신 화면에 그게 나간다. - available:false 후보를 후보 목록과 추천 양쪽에서 뺀다. 서버가 품절 닭강정을 eligibleCandidates 에 남기고 대안으로도 올려 보낸다. 심사 필수 기준이 '선택 불가능 후보 추천 0건' 이라 여기서도 막는다. 뺀 이유는 사용자에게 말해 준다 — 조용히 사라지면 "왜 없지?" 가 된다. - 같은 후보의 제외 사유가 후보 필터와 추천 양쪽에서 와서 두 번 보였다. 후보 id 로 합친다. - userDecision 에 note: null 을 보내지 않는다. 킷 스키마가 막는다. matchedOptions 를 만든다 - candidate-filters 응답에 후보별 attributes(spicyLevel·boneType)와 supportedOptions(SERVICE_TYPE·CUP)가 함께 온다. 사용자가 고른 값과 같은 어휘라 그대로 비교할 수 있다. 이름 문자열로 짐작하는 게 아니다. - 확인 카드의 "형태 순살 → 고르신 메뉴와 달라요" 가 이 결과다. - 서버가 matchedOptions 를 주면 그때 이 계산은 걷어낸다. 문서에 붙여 본 결과와 백엔드 쪽 문제 넷을 적는다 (승인 스키마 오류 · 품절 후보 추천 · explanation 문구 · boneType 미반영) 122 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 실서버에 붙어 있는지 눈으로 보이게 한다 화면만 봐서는 목인지 실서버인지 알 수가 없다. 둘 다 그럴듯한 답을 돌려주기 때문이다. 붙여 놓고도 "이게 진짜 도는 건가" 를 확인할 방법이 없다는 지적을 받았다. 실제로 나간 요청을 그대로 띄운다. ● 실서버에 붙어 있습니다 주고받은 요청 3건 · 성공 3건 200 /api/v1/recommendations 12ms 200 /api/v1/candidate-filters 14ms 200 /internal/simulation/session 12ms 이게 증거가 되는 이유는 목이 이 줄을 만들 수 없기 때문이다. 목은 브라우저 안에서 답을 만들어 내서 네트워크 요청이 아예 없다. - src/api/연동기록.ts — 오간 요청을 최근 12건만 들고 있는 작은 저장소. - createTeamBackend 의 보내기 가 경로·상태·걸린 시간을 남긴다. 실패도 남긴다. 성공만 보이면 조용한 실패를 못 본다. - App.tsx 의 연동표시 가 그것을 그린다. 배포본에는 들어가지 않는다. 팀백엔드모드는 빌드할 때 정해지는 상수라 기본 빌드에서는 이 컴포넌트를 부르는 자리가 통째로 빠진다. '실서버에 붙어 있습니다' · 'KIOBRIDGE_API_BASE' 가 dist 에 없는 것을 확인했다. 심사용 배포본이나 시연 영상에 개발용 창이 나올 일이 없다. 122 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 표준형을 직접 만들지 않고 서버 정규화 경로를 거친다 추천이 정규화된 값을 못 받고 있다는 지적을 받았다. 맞는 지적이었다. CanonicalProfile.SessionContext 를 프론트가 만들어 바로 추천에 넣고 있었다. 정규화를 거치면 세 가지가 달라진다. - providerId 를 짐작하지 않는다. 서버가 kiobridge.team-id 에서 채운다. 전에는 설정 파일을 보고 "WHATTHEBUG" 를 적어 두고 있었다. - 킷 스키마로 검증한 값만 쓴다. 잘못된 값이면 매핑 단계에서 멈춘다. 전에는 아무도 확인하지 않아서 승인 직전에야 터졌다. - reconfirmationFields 를 받는다. 어떤 항목을 다시 물어야 하는지 서버가 알려 준다. collectionMetadata 는 source WEB_FORM . confidence 1.0 . confirmedByUser true 로 보낸다. 사용자가 화면에서 직접 눌러 고른 값이라 그게 사실이다. 낮춰 적으면 서버가 RECONFIRMATION_REQUIRED 를 주는데, 이미 고른 것을 또 묻게 된다. 승인은 매핑 때 받아 둔 정규화 결과를 다시 쓴다. 승인 때 새로 만들면 확인 화면에서 본 조건과 실제 제출되는 조건이 갈라질 수 있다. 테스트 목이 호출 순서에 기대고 있었다. 정규화 두 개가 앞에 끼자 전부 어긋났다. 경로를 보고 답하도록 바꾼다 - 앞으로 호출이 늘어도 안 깨진다. 로컬 dev 로 확인했다. 여섯 경로 전부 200 이고 장바구니까지 간다. profile-normalizations . session-context-normalizations candidate-filters . recommendations . orchestrator/approve . simulation/session 122 -> 124 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 추천 전에 통합 검증(recommendationReady)을 확인한다 canonical-inputs/validate 를 건너뛰고 있다는 지적을 받았다. 맞는 지적이고, 형식 문제가 아니라 안전 문제였다. 개별 정규화는 각자 반쪽만 본다. 프로필 따로, 세션 맥락 따로. 합쳐야 보이는 게 있고 알레르기가 그렇다. 실제로 찔러 보니 이렇게 나온다. 정상 입력 status=VALID recommendationReady=true 알레르기 UNKNOWN status=RECONFIRMATION_REQUIRED recommendationReady=false HARD_CONSTRAINT_UNKNOWN "allergenIds 가 UNKNOWN 입니다. 임의로 추론하지 말고 재확인하거나 안전한 대체경로를 사용하세요." 이건 우리 코드에 실제로 있는 경로다. 변환기가 모르는 알레르기를 UNKNOWN 으로 보낸다 - 조용히 버리는 것보다 낫다고 판단해서다. 그런데 그 다음에 이 문을 안 거치고 바로 추천으로 갔다. 화면에 새 알레르기 선택지가 하나만 늘어도, 앱이 모르는 알레르기를 가진 분에게 그대로 음식을 추천하게 된다. recommendationReady 가 false 면 후보 필터도 추천도 부르지 않는다. 사용자에게는 서버 문구 대신 할 수 있는 일로 바꿔 말한다 - 프로필에서 다시 고르거나 직원에게 도움을 청하라고. 테스트로 잠갔다. false 일 때 실제 호출이 하나도 나가지 않아야 통과한다. 로컬 dev 로 확인했다. 일곱 경로 전부 200 이고 장바구니까지 간다. 124 -> 126 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: PR #46 코드 리뷰 14건 반영 사용자에게 사실이 아닌 말을 하던 것 - UNKNOWN 축을 '안 맞음' 으로 단정하고 있었다. 사용자가 고른 선택지를 우리가 enum 으로 못 옮긴 것인데 "오늘은 이 조합이 없어요" 가 나갔다. NO_PREFERENCE 와 함께 판단에서 뺀다. - 품절 안내의 조사가 '은' 으로 고정이었다. 받침 없는 이름에서 틀린다. 받침을 보고 은/는 을 고른다. - FAIL 을 계속 진행 중으로 두고 있었다. 화면이 90초를 기다리다 일반 오류로 끝난다. 끝난 일은 끝났다고 말하되, 안전 중단과 문구를 나눈다. 안전 중단만 직원 초기화를 안내한다. 승인이 서버 검증에 걸릴 수 있던 것 - 고른 후보를 1순위로 덮으면서 대안에서 빼지 않았다. 두 필드가 겹치면 RecommendationValidator 가 ALTERNATIVE_DUPLICATES_RECOMMENDED 로 막는다. 고른 것을 대안에서 빼고 원래 1순위를 대안으로 내린다. 지우기 약속을 못 지키던 것 - createTeamBackend 이 들고 있던 네 Map 을 비울 경로가 없었다. '이 기기에서 정보 지우기' 를 눌러도 정규화된 프로필과 세션 맥락이 메모리에 남았다. forgetSession 을 구현하고 createApi.forgetAll 이 환경 Map 도 비우게 한다. 낡은 값을 쓰던 것 - 정규화 캐시 키가 프로필 id 뿐이었다. 프로필을 고치거나 다른 키오스크에 붙으면 첫 결과를 계속 썼다. 환경과 입력 내용을 키에 넣는다. collectedAt 은 부를 때마다 달라져 키에서 뺀다 - 넣으면 캐시가 한 번도 안 맞는다. 그 밖에 - BFF 본문 읽기에 1MB 상한. 인증 없이 열린 함수라 상한이 없으면 큰 본문 하나로 메모리가 계속 는다. 넘으면 413. - consent.personalization 을 opts 로 받는다. 기본값은 true 로 둔다 - 사용자가 조건을 저장하고 이 프로필로 주문하기를 누른 행동이 그 주문에 한한 동의라고 본다. 앱이 실제로 개인화하는데 false 로 적는 것도 사실과 다르다. - 개발용 연결 표시를 접을 수 있게 하고 폭을 화면에 맞춘다. 200% 확대에서 휴대폰 틀의 아래 버튼을 덮어 터치를 가로챌 수 있었다. 문서 - 인터페이스 표가 filterCandidates.recommend 를 '경로 없음' 으로 적고 있었다. 같은 문서 앞부분과 코드는 이미 붙었다고 말한다. - matchedOptions 절이 낡았다. 프론트가 이미 채우고 있으니, 서버가 직접 주면 이 계산을 걷어내겠다는 요청으로 바꾼다. - '네 경로 전부 200' 이 실제로는 일곱 요청이다. - 언어 없는 코드펜스 14개에 text 를 붙인다. - README 의 목 구동 사유를 지금 상태로 맞추고, 형태.컵이 순위에 반영되지 않는다는 제한을 적는다. 로컬 dev 로 확인했다. 1순위가 아닌 후보를 골라도 승인이 통과하고 장바구니까지 간다. 126 -> 131 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: BFF 본문 상한을 바이트로 센다 모음.length 는 UTF-16 코드 단위라 한글 100만 자 = 약 3MB 가 통과했다. 상한이 의도의 셋 배였다. 청크의 Buffer 길이를 누적해서 바이트로 센다. 마지막에 Buffer.concat 으로 한 번에 문자열을 만들어 조각 경계에서 한글이 잘릴 여지도 없앤다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FEAT] ExecuteResult evidence(JsonNode) → Evidence 타입 파싱 연결 (#48) * feat: JsonNode evidence를 Evidence 타입으로 파싱해 오케스트레이터 응답에 연결 * fix: evidence 필수 필드 검증 및 NullNode 체크 추가, 요약 메시지 개선 * feat: 회원 및 프로필 영속성 모델 추가 (#50) * [FEAT] 회원가입 및 로그인 API 구현 (#52) * feat: 회원가입 및 로그인 API 구현 * fix: BCrypt 입력 제한 및 DB 예외 처리 보완 * test: BCrypt 72바이트 경계 검증 보완 * feat: 사용자 주문 프로필 저장 및 조회 API 구현 (#54) * chore: 운영 백엔드 PostgreSQL 네트워크 연결 (#55) * fix: executionplanservice null 체크 버그 수정 (#57) Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FIX] recommendation PASS, SKIP 점수 수정 (#59) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: RecommendationEngineService STEP5~7 추천 로직 초기 구현 * fix: STEP2 파트 passesByCandidateId 추가 전달 * fix: SKIPPED, PASS 점수 오류 수정 * fix: WARN 없는 항목의 confidence 임계값 크로스 테스트 픽스처 보정 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 아이디·비밀번호 회원가입·로그인 연동 및 주문표 서버 동기화 (#58) modules/member 의 네 경로에 프론트를 붙였습니다. 로그인은 끝까지 선택입니다 - 로그인하지 않아도 추천부터 최종 확인까지 전 과정이 그대로 동작합니다. 전화번호.인증번호 화면을 흐름째 걷어내고 사용자가 직접 지은 아이디.비밀번호로 바꿨습니다. 실명.전화번호를 받는 칸이 없습니다. (#56 의 프론트 지적이 이것입니다) '프로필' 을 '주문표' 로 바꿨습니다. 화면 말만 바뀌고 백엔드 profile 필드명은 그대로입니다. 함께 들어가는 것 - 승인 전에 거절할 수 있고, 거절도 서버에 기록합니다 - npm run check:backend - 앱이 부르는 경로를 앱이 부르는 순서로 확인 - 배포 빌드가 목으로 나가던 것을 고쳤습니다(build:team) - #48 의 summary.status 를 앱 말투로 옮겨 중단 화면 제목에 씁니다 백엔드에 부탁드린 것은 frontend/docs/BACKEND_INTEGRATION.md 에 있습니다 - 토큰 발급, 주문표 삭제 경로, place 완화, summary.status 를 코드로. 코드래빗 리뷰 24건(17 + 증분 7) 반영. 미해결 0건. frontend/ 만 바뀝니다. tsc 0, 195 tests, build ok. --------- Co-authored-by: ParkSeYoung128 <tpdudtpdud128@gmail.com> Co-authored-by: kjp0411 <98109773+kjp0411@users.noreply.github.com> Co-authored-by: Yena07 <164753655+Yena07@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Gganii <rkdms5991@naver.com> Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * fix: Spring Boot 4 Flyway 자동 마이그레이션 활성화 --------- Co-authored-by: ParkSeYoung128 <tpdudtpdud128@gmail.com> Co-authored-by: kjp0411 <98109773+kjp0411@users.noreply.github.com> Co-authored-by: Yena07 <164753655+Yena07@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Gganii <rkdms5991@naver.com> Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com>
cha-hyunwoo
added a commit
that referenced
this pull request
Aug 10, 2026
* chore: 백엔드 배포 기반 구성 * chore: aws 자동 배포 구성 (#5) * [FEAT] contracts 패키지 공통 파일 (#7) * chore: 설정 파일 및 패키지명 수정 * feat: contracts Simulation API 제출 계약 * feat: contracts 공통 상태 enum * chore: pr issue template 등록 * chore: gitignore 추가 * chore: coderabbit 설정 * feat: Target/Action/UserDecision/ExecutionPlan 레코드 * [FEAT] modules executionplan (#8) * feat: 응답 dto * feat: 세션 생성/제출/검증/실행/Evidence 호출 * feat: 전체 파이프라인 오케스트레이션 * fix: Simulation API 호출 timeout 설정 및 제출 전 승인-계획 불변식 검증 추가 * refactor: SimulationApiClient를 contracts로 이동 (#9) * [FEAT] Evidence, SimulationValidation 타입 정의 추가 (#11) * feat: Evidence, SimulationValidation 타입 정의 * feat: RunResult 등 run 관련 타입 추가, Evidence resolvedSimulationTrace 필드 보완 * [FEAT] SimulationApiClient Input Contract 조회·검증 기능 추가 (#13) * refactor: Jackson 3 JsonNode 타입으로 통일 * feat: Input Contract 조회 및 검증 기능 추가 * [FEAT] Evidence 조회 및 화면용 요약 서비스 구현 (#14) * feat: evidence 조회 API, 화면용 요약 서비스 추가 * fix: evidence.result() null 처리 및 결제 계획 시 FAIL 처리 반영 * [FEAT] Canonical Profile 정규화 API 구현 (#17) * feat: Canonical Profile 정규화 API 구현 * [FIX] Profile providerId를 서버 설정값으로 고정 * [FEAT] executionplan 내부 api 구현 및 리팩토링 & ParticipantSubmission 누락 필드 추가 (#18) * feat: 내부 api dto * feat: Service 세션 생성, 일괄처리 부분 분리 * feat: ParticipantSubmission 누락 필드 추가 * fix: ParticipantSubmission field null check + buildExecutionPlan (stub) 예외처리 추가 * [FEAT] 프론트엔드 온보딩·승인·실행 화면 추가 (#15) * feat: 온보딩·프로필·승인·실행 화면 추가 (Vite + React) 비어 있던 frontend/ 에 키오스크 접근성 앱을 넣는다. 프로필 저장부터 QR 연결, 매핑 결과 승인, 실행 진행, 장바구니 확인까지 전 화면이 동작한다. - 백엔드 연결 지점은 src/api/client.ts 의 KioBridgeApi 4개 메서드 한 곳 - 붙기 전까지는 mock 구현으로 돌고, setScenario() 로 예외 상태를 재현한다 - 프로필은 의미값(텍스트)만 담는다. 상품 ID·화면 좌표를 저장하지 않는다 - 결제 UI 없음. 종료 상태는 cart_ready("장바구니에 담았어요") 하나뿐 - 아이콘 Phosphor(MIT), 사진 출처는 src/assets/ATTRIBUTIONS.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: 프론트 타입과 백엔드 contracts 의 관계를 바로잡는다 "백엔드 contracts 와 이름을 맞춰 놨다"고 적었는데 사실이 아니다. 겹치는 타입 이름이 하나도 없다. contracts 는 주최측이 정한 Simulation Kit 규격이고, domain/types.ts 는 앱과 우리 서버 사이 규격이라 층이 다르다. 그대로 두면 백엔드 담당이 1:1 대응을 기대하고 붙이다 헤맨다. 실제로 이어지는 지점만 표로 적고, 아직 아무도 안 만든 것(앱용 REST 엔드포인트)과 ID 번역이 서버 몫이라는 점을 밝힌다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: low_confidence 승인도 서버가 다시 검증하게 한다 client.ts 는 "프론트 가드만 믿지 않는다"고 적어 두고 정작 low_confidence 만 빼놓고 있었다. 화면에서는 사용자가 카드를 짚어야 승인 버튼이 열리는데, 그 사실이 approve() 인자에 실리지 않아 서버가 확인할 방법이 없었다. changed 가 acknowledgedDiff 로 받는 보장을 low_confidence 도 받게 맞춘다. 확신이 낮을수록 사용자가 직접 짚었다는 사실이 더 중요하다. - ApproveInput 에 confirmedLowConfidence 추가 (필드마다 무엇을 뜻하는지 주석) - approve() 에 CONFIRMATION_REQUIRED 분기 추가 - OrderLowConfidence 호출부에서 confirmedLowConfidence: true 전달 CodeRabbit 리뷰 반영 (PR #15). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 프로필 카드에서 고르는 곳과 지우는 곳을 분리 카드 전체가 role="radio" 이고 그 안에 삭제 버튼이 들어 있었다. 삭제 버튼에 포커스를 두고 Enter 를 눌러도 카드만 선택되고 지워지지 않았다. 부모 div 의 onKeyDown 이 그 Enter 를 가로채 preventDefault() 하는 바람에 버튼의 기본 동작(클릭)이 막혔기 때문이다. 키보드만 쓰는 사람에게는 프로필을 지울 방법이 아예 없었다. - 선택은 시각적으로 숨긴 진짜 input[type=radio] 가 맡는다. 화살표 키 이동과 그룹당 탭 한 번(로빙 tabindex)을 브라우저가 처리한다. - 삭제 버튼은 label 바깥 형제로 옮겼다. - 목록 컨테이너는 role="radiogroup" 대신 fieldset + 숨긴 legend 로 바꿨다. - 포커스 표시는 라디오의 focus 를 받아 카드에 3px outline 으로 그린다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(qr): 스캐너의 중첩 타이머를 정리하지 않아 언마운트 뒤 페어링이 나가던 문제 QrScannerModal 의 useEffect 는 바깥 2.5초 타이머만 해제하고 그 안에서 예약한 800ms 타이머는 그대로 두었다. 스캐너가 떠 있는 동안에도 하단 탭은 눌린다. 스캔이 끝난 뒤 800ms 안에 다른 탭으로 옮기면 안쪽 타이머만 살아남아 언마운트된 컴포넌트에서 onDetected 가 돌고 api.claimPairing 이 호출된다. 화면에는 아무 표시도 안 되는데 키오스크에는 사용자가 요청한 적 없는 연결 요청이 남는다. 두 타이머 ID 를 모두 붙잡아 cleanup 에서 함께 해제한다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 프로필 삭제 버튼의 터치 영역을 44x44 로 넓힘 높이는 44 였지만 폭이 30 이었다. 앱 전체를 훑어 44px 미만인 조작 요소는 이것 하나뿐이었다. 밑줄은 글자에만 걸리므로 minWidth 를 44 로 올려도 보이는 크기는 그대로고 누를 수 있는 영역만 넓어진다. 카드 크기(321x168)도 그대로다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 기본 팔레트가 WCAG AA 에 미달하던 대비를 전부 올림 고대비를 별도 옵션으로 켜고 끄게 만드는 대신 기본값을 고쳤다. 찾아 들어가야 보이는 설정은 대비가 필요한 사람이 못 찾을 수 있고, 지금 값은 옵션을 켜기 전 상태에서 이미 기준 미달이었다. 측정값 (흰 배경 / SURFACE 위): - TEXT_2 #8A8A8E 3.44 / 3.21 → #6B6B70 5.30 / 4.95 - TEXT_3 #C4C4C8 1.74 / 1.62 → 글자에서 전부 제거 - FAIL #D92D20 FAIL_BG 위 4.35 → #CE2A1F 4.75 고친 곳 - 읽어야 하는 글자에 쓰이던 TEXT_3 12곳을 TEXT_2 로 옮김. 하단 탭의 안 눌린 항목(내 프로필·계정)이 1.74:1 이라 특히 나빴다. - 고른 프로필 카드: 태그·메모·삭제가 반투명 흰색이라 3.31~3.35 였다. 태그는 바탕을 어둡게 눌러 6.6, 나머지는 흰색으로 올려 5.08. - QR 스캔 안내문 42% 흰색(3.95) → 62%(7.8). 조명을 조절하라는 문장은 화면이 어두울 때 읽는 문장이다. - 안 고른 동그라미 테두리와 변경 확인 체크박스 테두리가 각각 1.62 / 1.29 라 컨트롤 경계 기준(3:1)에 못 미쳤다. TEXT_2 / WARN 으로 바꿈. - 비활성 버튼 글자는 WCAG 예외지만 1.62 는 무엇을 기다리는지 못 읽는다. 초록 알약이 회색으로 바뀌는 것만으로 신호는 충분해서 TEXT_2 로 올림. WARN_BORDER 는 쓸 곳이 없어져서 뺐다. 시나리오 스위치를 돌려 가며 전 화면을 다시 쟀다. 환영·전화번호·인증·이름·프로필 목록·프로필 만들기·QR(스캔/연결중/연결됨/실패/만료) ·확인(정확/애매/없음/변경/불확실)·실행(진행/담김/중단)·계정·접근성·개인정보 전부 미달 0건. * fix(a11y): QR 화면 그릇이 넘칠 때 아래 버튼에 닿을 수 없던 문제 QrScreen 의 가운데 그릇이 overflow-hidden 이었다. 그 안의 패널들(연결중·연결됨·실패·만료)은 아래 버튼을 mt-auto 로 바닥에 붙인다. 내용이 그릇보다 커지면 그 버튼이 잘려 나가는데, hidden 이라 스크롤도 안 생긴다. 포커스를 옮기면 브라우저가 알아서 스크롤해 주므로 키보드로는 닿지만, 손가락으로는 닿을 방법이 아예 없다. 앱이 주는 큰 글씨(1.18배)까지는 넘치지 않는다. 1.4배까지도 안 넘친다. 사용자가 브라우저·OS 글씨 크기를 더 키우면 1.6배 근처부터 넘친다(측정 102px, 1.8배에서 195px). overflow-y-auto 로 바꾸면 넘칠 때만 스크롤이 생기고 안 넘치면 지금과 똑같이 보인다. 1 / 1.18 / 1.6 / 1.8 / 2.2 배에서 잘리는 그릇 0건, 못 닿는 조작 요소 0건. 프로필 목록·계정·개인정보 화면도 1.8 배에서 함께 확인했다. * chore(review): 안 쓰는 shadcn 원본 48개를 CodeRabbit 리뷰 대상에서 제외 frontend/src/app/components/ui/** 는 Figma Make 재출력용으로만 남겨 둔 파일들이라 어느 화면도 import 하지 않는다. 번들에도 들어가지 않는다. 리뷰가 반복해서 여기를 지적하는데, 고쳐도 사용자에게 도달하는 코드는 바뀌지 않아 실제 결함을 찾는 데 방해가 된다. 다시 쓰기 시작하면 이 줄을 지운다. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * [FEAT] 장소별 질문 목록을 시뮬레이션 킷 fixture 축에 맞춤 (#20) * feat(profile): 장소별 질문을 시뮬레이션 킷 fixture 축에 맞춤 앱이 묻는 조건과 심사 환경이 실제로 가르는 축이 서로 달랐다. 드라이브스루·버거 세트·사이드 변경처럼 fixture 에 대응이 없는 질문은 어르신이 골라도 결과가 달라지지 않는, 물어만 보고 버리는 질문이었다. 반대로 fixture 에 있는데 앱에 없던 축이 셋 있었다. - 병원 '접근성 지원' — 우리가 점수 축 20% 로 넣어 놓고 물어보는 화면이 없었다 - 관공서 '인증 방식' — PUBLIC-004 를 AUTH_METHOD_UNAVAILABLE 로 빼는 조건인데 앱에서 입력할 방법이 없었다 - 음식점 '컵' — 확인 카드 필수 5개 중 하나인데 '추가 옵션' 다중선택에 묻혀 있었다 메뉴 이름은 목록에서 고르게 하지 않고 자유 입력(menuName)으로 둔다. 사용자가 부르는 이름과 오늘 화면의 이름을 맞추는 건 매핑의 일이다. 알레르기만 fixture 축에 맞추지 않았다. option-group 이 아니라 사람에 대한 절대 조건이라, 오늘 이 가게에 걸리는 게 땅콩뿐이라고 나머지를 지우면 새우 알레르기가 있는 사람이 그 사실을 말할 방법이 사라진다. mock.ts 의 옵션 라벨도 같은 이름으로 맞췄다. tsc --noEmit · build 통과. 세 장소 모두 화면에서 확인. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(frontend): 안 쓰는 shadcn 컴포넌트 48개와 딸려 온 의존성 53개 제거 frontend/src/app/components/ui/ 의 48개 파일은 어느 화면도 import 하지 않는다. components/figma/ImageWithFallback.tsx 도 마찬가지고, styles/globals.css 는 0바이트 빈 파일이었다. 참조 0건을 확인하고 지웠다. 지우기 전에 react 와 react-dom 을 dependencies 에 명시했다. 지금까지 두 패키지가 선언 없이 @mui/material·radix 의 전이 의존으로만 딸려 오고 있었다. ui/ 를 걷어내면 React 자체가 사라질 뻔했다. tw-animate-css 는 남긴다. JS import 가 아니라 styles/tailwind.css 의 @import 라 코드 검색에는 안 걸리지만 실제로 쓰인다. dependencies 55개 → 4개 (react, react-dom, lucide-react, tw-animate-css) npm ci 92 packages / 6s JS 번들 223.12 kB → 223.12 kB (변화 없음 — 원래 번들에 없던 코드다) CSS 번들 86.03 kB → 13.58 kB (Tailwind 가 그 48개까지 훑고 있었다) tsc --noEmit · build 통과. 개인 저장소와 번들 해시 동일. 실제 화면에서 폰 프레임·Tailwind 유틸리티·QR 화면 overflow-y-auto· 1/1.18/1.6/2.2배 확대를 모두 확인했다. .coderabbit.yaml 의 components/ui 제외 필터는 이제 가리킬 대상이 없어졌지만, 다음 PR 에서 별도로 정리한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * CodeRabbit Generated Unit Tests: Add Generated Unit Tests for PR Changes (#22) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Delete frontend directory * Create yena.txt * Delete .coderabbit.yaml * [FEAT] chicken-store SessionContext 정규화 API 구현 (#25) * [FEAT] recommendation contracts 정의 (#21) * feat: recommendation관련 contracts 정의 (Recommendation, ExcludedCandidate, Candidate, CompatibilityRule, CompatibilityRuleSet) * fix: ParticipantSubmission에 누락된 inputContractVersion 필드 추가, TODO: 담당2 Recommendation 타입으로 교체 * feat: PublicFixture 계약 정의 및 fixture·compatibility-rules 조회 메서드 추가 * chore: 운영 프론트 cors 설정 추가 (#27) * [REFACTOR] SessionContext 공통 기반 타입과 치킨 구현 분리 (#29) * refactor: SessionContext 공통 기반 타입과 치킨 구현 분리 * style: SessionContext 리팩터링 코드 정리 * [FEAT] Evidence.recommendation 타입 교체 (Object → Recommendation) (#32) * feat: Evidence.recommendation 타입을 Recommendation으로 교체 * fix: recommendationReasons 첫 번째 비공백 값만 요약에 사용 * feat: Canonical Input 통합 검증 API 구현 (#34) * [FEAT] contracts lost fields 업데이트 (#35) * feat: ExecutionPlan filed 업데이트 * feat: UserDecision filed 업데이트 * fix: approved/actions 불변식 강제 + confirmedAt 형식 검증: * [FEAT] recommendation ruleevaluator interface (#36) * feat: RuleResult * feat: Rule EvaluationResult record * feat: RuleEvaluator 인터페이스 * feat: 구현체 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation 필터링 오케스트레이션 구현 (#37) * chore: 파일삭제 * feat: 필터링 후보 결과 레코드 * feat: 후보 필터링 서비스 * fix: CandidateFilterResult 리스트 불변 복사 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation ruleevaluator (#38) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: Kit compare() 사용 * feat: asList(), stringOf() 공통 유틸 * feat: RuleEvaluator 수정 * test: CandidateFilter + RuleEvaluator 테스트 코드 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] recommendation engine 초기 구현 (#39) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: RecommendationEngineService STEP5~7 추천 로직 초기 구현 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 프론트엔드 앱 추가 및 백엔드 연동 계층 (#30) * feat: 프론트엔드 앱 추가 및 백엔드 연동 계층 dev 에서 frontend 가 통째로 지워져 있어(44301ac) 다시 올린다. 개인 저장소(Yena07/kiobridge-app)에서 리뷰 112건을 반영하며 만든 것이고 테스트 84개가 붙어 있다. [무엇이 들어 있나] src/app/App.tsx 화면 전부 (온보딩·프로필·QR·확인·실행) src/api/client.ts 앱이 쓰는 계약 + 목 구현 src/api/backend.ts 팀 백엔드 연동 계층 src/api/mock.ts 프로필로 매핑 응답을 만드는 로직 src/domain/ 계약 타입 · 장소별 질문 목록 api/bff/ 백엔드 호출을 서버가 대신 보내는 프록시 [백엔드 붙이는 법] client.ts 마지막 줄만 바꾸면 된다. export const api = createApi(createTeamBackend()); createTeamBackend 는 팀 백엔드가 실제로 구현한 경로에 맞춰 두었다. 명세서와 다른 부분이 있어서 그대로 옮기면 안 붙는다. 명세서 실제 구현 POST /api/v1/sessions POST /internal/simulation/session submission -> validate -> execute POST /internal/simulation/submit-and-run candidate-filters / recommendations 아직 없음 추천 계열이 생기면 filterCandidates·recommend 둘만 채우면 된다. [CORS 는 해결해 두었다] api/bff 가 백엔드로 대신 보내므로 브라우저는 같은 출처로만 요청한다. 백엔드에서 allowed-origin 을 맞출 필요가 없고, 미리보기 배포마다 주소가 바뀌는 문제도 없다. Vercel 환경변수 KIOBRIDGE_API_BASE 만 넣으면 된다. 열린 프록시가 되지 않게 통과 경로를 명시하고 클라이언트 헤더는 넘기지 않는다. [.coderabbit.yaml 복구] 루트 설정이 지워져 있어 되살렸다. 팀 저장소이므로 profile 은 chill 로 두고, base_branches 를 dev·main 으로, path_instructions 를 frontend/** 로 맞췄다. 실격 조건(결제 UI 부재·승인 전 실행 경로 없음·상품 ID 미보유·병원 의료 판단 금지·관공서 자격 판단 금지)과 접근성 기준이 들어 있다. [프론트가 지키고 있는 것] - 결제 관련 문자열 0건. src 전체를 훑는 테스트로 잠가 두었다. - 승인 전 실행계획 생성 0건. approve 호출은 버튼 핸들러 안에만 있다. - 상품 ID 미보유. 후보 표식은 c1·c2·c3 형태만. - 선택 불가능 후보 추천 0건. 알레르기·품절·이용 불가는 후보에서 제거한다. - 신뢰도 낮으면 사용자가 직접 짚어야 승인된다. - 실제 개인정보 미수집. 이름 대신 호칭, 전화번호는 시연용 고정값. npm ci · npx tsc --noEmit · npm test 84/84 · npm run build 통과. 번들 해시가 개인 저장소 빌드와 동일하다. * fix: 코드 리뷰 14건 반영 승인 경로 - client.ts: 중복 승인 검사와 확정 사이의 await 를 없앤다. 동시에 들어온 승인 두 건이 모두 검사를 통과해 둘 다 담기던 문제. backend.ts 와 같은 방식으로 첫 await 전에 표시하고 실패하면 되돌린다. - backend.ts: clarification 이 아닌데 candidateId 가 오면 거절한다. client.ts 는 이미 막고 있었고 이쪽만 열려 있었다. 응답 문구 - mock.ts: 담을 후보가 없는 이유(장소 미정·지원하지 않는 장소·조건 제외)를 시나리오 스위치가 not_found 를 골랐을 때도 구분한다. 병원 프로필에도 "메뉴가 바뀌었을 수 있어요" 라고 답하던 문제. - mock.ts: 메뉴 이름 뒤 조사를 헬퍼로 붙인다. 사용자가 적은 이름에 받침이 없으면 "커피이 없어요" 가 됐다. 접근성 - App.tsx·tokens.ts: 회전 표시를 SVG SMIL 에서 CSS 애니메이션으로 바꾼다. SMIL 은 CSS 규칙의 대상이 아니라 prefers-reduced-motion 이 멈추지 못했다. - App.tsx: QR 탭에 다시 들어갈 때 연결이 살아 있으면 스캐너를 열지 않고 연결 상태를 보여 준다. 같은 상태를 두고 두 화면이 다른 말을 했다. 정리 - tsconfig.json: compilerOptions.types 가 두 번 적혀 뒤엣것이 앞엣것을 덮었다. - App.tsx: 렌더 본문에서 ref 를 변형하던 것을 useEffect 로 옮긴다. - App.tsx: options 선언을 참조보다 위로 옮긴다. - catalog.tsx: DETAIL_OPTIONS·PLACE_ICONS 키를 PlaceType 으로 좁힌다. - ATTRIBUTIONS.md: 코드블록 언어 표시와 제목 뒤 빈 줄. 테스트 - 결제 문자열 스캔 범위를 src 에서 리포지터리 루트로 넓힌다. index.html· vite.config.ts·api/bff 가 검사에서 빠져 있었다. 제외 목록은 .gitignore 를 읽는다. - 동시 승인 회귀 테스트를 client·backend 양쪽에 추가. - '조건 얘기를 한다' 테스트가 이름과 다른 것을 단언하던 것을 바로잡는다. 84 → 89 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: 팀 레포 PR 코드 리뷰 반영 확인 화면이 사실이 아닌 말을 하던 것 - mock.ts: 시나리오 스위치가 '변경' 인데 실제로는 다 맞을 때, 멀쩡한 행 하나에 matched:false 와 "오늘은 제공되지 않아요" 를 붙이던 시연용 경로를 없앤다. 실제로 제공되는 옵션을 안 된다고 말한 것이고, 심사 중 스위치를 돌리면 그대로 보였다. 스위치는 결과 종류를 고를 뿐 없는 불일치를 만들지 못한다. - backend.ts: 이름 없는 후보를 다 걸러내고도 clarification 을 내보내면, 화면은 "비슷한 메뉴가 여러 개예요" 라면서 고를 것을 하나도 못 보여 준다. 승인은 후보 선택을 요구하므로 사용자가 그 화면에서 빠져나갈 수 없다. 연동 계층 - createSession 이 claimCode 를 버리고 있었다. QR 로 어떤 키오스크를 찍었는지와 무관하게 세션이 열린다는 뜻이다. 함께 보낸다. - getEvidence 를 POST 로 부르고 있었다. 명세는 GET 이다. - getEvidence 가 cart·abort 를 옮기지 않아 "장바구니에 담았어요" 화면에 개수도 금액도 뜨지 않았다. 중단 사유도 마찬가지. BFF - 경로만 검사하고 메서드는 무엇이든 통과시켰다. GET·POST 만 연다. 테스트 - 저장소 루트를 .git 위치로 찾는다. '..' 개수를 고정하면 개인 레포와 팀 레포 중 한쪽이 반드시 틀린다. 팀 레포에서는 frontend 까지만 올라가 그 바깥을 검사하지 못했고, 개인 레포에서 한 단계 더 올리면 Downloads 전체를 훑는다. - 스캔이 실제로 훑었는지를 먼저 잠근다. 빈 배열은 늘 통과하므로, 기준점이 틀어지면 이 파일의 모든 보증이 조용히 사라진다. - .md 도 훑는다. 실제로 연동 문서에 금지어를 그대로 적어 둔 적이 있다. - 응답 전체에 서버 후보 ID 가 섞이지 않는지 결과 종류마다 검사한다. - planId 에 일련번호를 붙인다. 밀리초만 쓰면 지연 0 인 테스트에서 겹친다. 문서·기타 - ATTRIBUTIONS.md 를 빌드가 dist 로 복사한다. CC BY-SA 는 저작자 표시를 저작물과 함께 배포할 것을 요구하는데, 문서 자신은 "배포물에도 포함된다" 라고 적혀 있으면서 실제로는 번들에 없었다. - BACKEND_INTEGRATION.md: 금지 문자열을 그대로 적어 둔 줄을 고치고, 질문 번호를 정리하고(③ 중복), claimCode·evidence 경로를 질문에 추가하고, 후보 식별자를 서버가 들고 있어 달라는 요청을 적는다. - README.md 코드블록 언어 표시. 90 → 92 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FEAT] recommendation controller (#41) * feat: ValidationIssue 검증 결과 항목 타입 추가 * feat: ValidationOutcome 검증 결과 타입 추가 * feat: RecommendationValidator STEP5 출력 자체 검증 로직 구현 * feat: candidate-filters 요청 DTO 추가 * feat: recommendations 요청 DTO 추가 * feat: recommendation-output-validations 요청·응답 DTO 추가 * feat: RecommendationController candidate-filters·recommendations·recommendation-output-validations API 구현 * feat: RecommendationController candidate-filters·recommendations·recommendation-output-validations API 구현 * fix: RecommendationValidator가 제외된 추천 후보·중복 대안 후보를 검증하지 못하던 문제 수정 * [FEAT] executionplan buildexecutionplan (#42) * feat: 시그니처 교체하고 실제 조립 로직 구현 * feat: 그룹별 실제 option id 결정 * feat: 컨트롤러에서 environmentId/sessionContext까지 서비스로 전달하도록 수정 * feat: Object 객체 확정 객체로 교체 * test: executionplan + ChickenSToreOptionResolver test code * feat: sessionId<->environmentId 매핑 처리 * test: test 갱신 * fix: ChickenStoreOptionReslover 버그 수정 + 예외 처리 구현 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] orchestrator구현 (#43) * feat: environmentId 재조회 및 ParticipantSubmission 조립 * feat: POST /internal/orchestrator/approve 신설 * feat: 오케스트레이터 레코드 * test: 오케스트레이터 test --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * fix: CandidateFilterService 의 품절 여부 체크 추가 (#44) Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FIX] 통합 테스트 중 버그 수정 (#45) * fix: CandidateFilterService 의 품절 여부 체크 추가 * fix: Target, UserDecision에 @JsonInclude(JsonInclude.Include.NON_NULL) 추가 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 백엔드 신규 경로 연동 및 입력 정규화 적용 (#46) * feat: 프론트엔드 앱 추가 및 백엔드 연동 계층 dev 에서 frontend 가 통째로 지워져 있어(44301ac) 다시 올린다. 개인 저장소(Yena07/kiobridge-app)에서 리뷰 112건을 반영하며 만든 것이고 테스트 84개가 붙어 있다. [무엇이 들어 있나] src/app/App.tsx 화면 전부 (온보딩·프로필·QR·확인·실행) src/api/client.ts 앱이 쓰는 계약 + 목 구현 src/api/backend.ts 팀 백엔드 연동 계층 src/api/mock.ts 프로필로 매핑 응답을 만드는 로직 src/domain/ 계약 타입 · 장소별 질문 목록 api/bff/ 백엔드 호출을 서버가 대신 보내는 프록시 [백엔드 붙이는 법] client.ts 마지막 줄만 바꾸면 된다. export const api = createApi(createTeamBackend()); createTeamBackend 는 팀 백엔드가 실제로 구현한 경로에 맞춰 두었다. 명세서와 다른 부분이 있어서 그대로 옮기면 안 붙는다. 명세서 실제 구현 POST /api/v1/sessions POST /internal/simulation/session submission -> validate -> execute POST /internal/simulation/submit-and-run candidate-filters / recommendations 아직 없음 추천 계열이 생기면 filterCandidates·recommend 둘만 채우면 된다. [CORS 는 해결해 두었다] api/bff 가 백엔드로 대신 보내므로 브라우저는 같은 출처로만 요청한다. 백엔드에서 allowed-origin 을 맞출 필요가 없고, 미리보기 배포마다 주소가 바뀌는 문제도 없다. Vercel 환경변수 KIOBRIDGE_API_BASE 만 넣으면 된다. 열린 프록시가 되지 않게 통과 경로를 명시하고 클라이언트 헤더는 넘기지 않는다. [.coderabbit.yaml 복구] 루트 설정이 지워져 있어 되살렸다. 팀 저장소이므로 profile 은 chill 로 두고, base_branches 를 dev·main 으로, path_instructions 를 frontend/** 로 맞췄다. 실격 조건(결제 UI 부재·승인 전 실행 경로 없음·상품 ID 미보유·병원 의료 판단 금지·관공서 자격 판단 금지)과 접근성 기준이 들어 있다. [프론트가 지키고 있는 것] - 결제 관련 문자열 0건. src 전체를 훑는 테스트로 잠가 두었다. - 승인 전 실행계획 생성 0건. approve 호출은 버튼 핸들러 안에만 있다. - 상품 ID 미보유. 후보 표식은 c1·c2·c3 형태만. - 선택 불가능 후보 추천 0건. 알레르기·품절·이용 불가는 후보에서 제거한다. - 신뢰도 낮으면 사용자가 직접 짚어야 승인된다. - 실제 개인정보 미수집. 이름 대신 호칭, 전화번호는 시연용 고정값. npm ci · npx tsc --noEmit · npm test 84/84 · npm run build 통과. 번들 해시가 개인 저장소 빌드와 동일하다. * fix: 코드 리뷰 14건 반영 승인 경로 - client.ts: 중복 승인 검사와 확정 사이의 await 를 없앤다. 동시에 들어온 승인 두 건이 모두 검사를 통과해 둘 다 담기던 문제. backend.ts 와 같은 방식으로 첫 await 전에 표시하고 실패하면 되돌린다. - backend.ts: clarification 이 아닌데 candidateId 가 오면 거절한다. client.ts 는 이미 막고 있었고 이쪽만 열려 있었다. 응답 문구 - mock.ts: 담을 후보가 없는 이유(장소 미정·지원하지 않는 장소·조건 제외)를 시나리오 스위치가 not_found 를 골랐을 때도 구분한다. 병원 프로필에도 "메뉴가 바뀌었을 수 있어요" 라고 답하던 문제. - mock.ts: 메뉴 이름 뒤 조사를 헬퍼로 붙인다. 사용자가 적은 이름에 받침이 없으면 "커피이 없어요" 가 됐다. 접근성 - App.tsx·tokens.ts: 회전 표시를 SVG SMIL 에서 CSS 애니메이션으로 바꾼다. SMIL 은 CSS 규칙의 대상이 아니라 prefers-reduced-motion 이 멈추지 못했다. - App.tsx: QR 탭에 다시 들어갈 때 연결이 살아 있으면 스캐너를 열지 않고 연결 상태를 보여 준다. 같은 상태를 두고 두 화면이 다른 말을 했다. 정리 - tsconfig.json: compilerOptions.types 가 두 번 적혀 뒤엣것이 앞엣것을 덮었다. - App.tsx: 렌더 본문에서 ref 를 변형하던 것을 useEffect 로 옮긴다. - App.tsx: options 선언을 참조보다 위로 옮긴다. - catalog.tsx: DETAIL_OPTIONS·PLACE_ICONS 키를 PlaceType 으로 좁힌다. - ATTRIBUTIONS.md: 코드블록 언어 표시와 제목 뒤 빈 줄. 테스트 - 결제 문자열 스캔 범위를 src 에서 리포지터리 루트로 넓힌다. index.html· vite.config.ts·api/bff 가 검사에서 빠져 있었다. 제외 목록은 .gitignore 를 읽는다. - 동시 승인 회귀 테스트를 client·backend 양쪽에 추가. - '조건 얘기를 한다' 테스트가 이름과 다른 것을 단언하던 것을 바로잡는다. 84 → 89 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: 팀 레포 PR 코드 리뷰 반영 확인 화면이 사실이 아닌 말을 하던 것 - mock.ts: 시나리오 스위치가 '변경' 인데 실제로는 다 맞을 때, 멀쩡한 행 하나에 matched:false 와 "오늘은 제공되지 않아요" 를 붙이던 시연용 경로를 없앤다. 실제로 제공되는 옵션을 안 된다고 말한 것이고, 심사 중 스위치를 돌리면 그대로 보였다. 스위치는 결과 종류를 고를 뿐 없는 불일치를 만들지 못한다. - backend.ts: 이름 없는 후보를 다 걸러내고도 clarification 을 내보내면, 화면은 "비슷한 메뉴가 여러 개예요" 라면서 고를 것을 하나도 못 보여 준다. 승인은 후보 선택을 요구하므로 사용자가 그 화면에서 빠져나갈 수 없다. 연동 계층 - createSession 이 claimCode 를 버리고 있었다. QR 로 어떤 키오스크를 찍었는지와 무관하게 세션이 열린다는 뜻이다. 함께 보낸다. - getEvidence 를 POST 로 부르고 있었다. 명세는 GET 이다. - getEvidence 가 cart·abort 를 옮기지 않아 "장바구니에 담았어요" 화면에 개수도 금액도 뜨지 않았다. 중단 사유도 마찬가지. BFF - 경로만 검사하고 메서드는 무엇이든 통과시켰다. GET·POST 만 연다. 테스트 - 저장소 루트를 .git 위치로 찾는다. '..' 개수를 고정하면 개인 레포와 팀 레포 중 한쪽이 반드시 틀린다. 팀 레포에서는 frontend 까지만 올라가 그 바깥을 검사하지 못했고, 개인 레포에서 한 단계 더 올리면 Downloads 전체를 훑는다. - 스캔이 실제로 훑었는지를 먼저 잠근다. 빈 배열은 늘 통과하므로, 기준점이 틀어지면 이 파일의 모든 보증이 조용히 사라진다. - .md 도 훑는다. 실제로 연동 문서에 금지어를 그대로 적어 둔 적이 있다. - 응답 전체에 서버 후보 ID 가 섞이지 않는지 결과 종류마다 검사한다. - planId 에 일련번호를 붙인다. 밀리초만 쓰면 지연 0 인 테스트에서 겹친다. 문서·기타 - ATTRIBUTIONS.md 를 빌드가 dist 로 복사한다. CC BY-SA 는 저작자 표시를 저작물과 함께 배포할 것을 요구하는데, 문서 자신은 "배포물에도 포함된다" 라고 적혀 있으면서 실제로는 번들에 없었다. - BACKEND_INTEGRATION.md: 금지 문자열을 그대로 적어 둔 줄을 고치고, 질문 번호를 정리하고(③ 중복), claimCode·evidence 경로를 질문에 추가하고, 후보 식별자를 서버가 들고 있어 달라는 요청을 적는다. - README.md 코드블록 언어 표시. 90 → 92 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 팀 백엔드 실제 응답 모양에 맞춰 연동 계층을 다시 짠다 dev 의 ExecutionPlanController.ExecuteResult.Evidence 를 직접 읽고 맞췄다. submit-and-run 응답에 증거가 같이 오므로 따로 조회하지 않는다. valid 가 실행 여부를 알려 주므로 validate 가 서버 판단을 그대로 읽는다. reviewSnapshot 에서 장바구니 개수.금액을 만들고, 없으면 비워 둔다. 99 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: 실제로 띄워서 붙여 본 결과를 적는다 킷.백엔드.프론트 셋을 로컬에 올리고 요청을 넣어 본 결과다. 되는 것: 세션 생성. claimCode 를 보내도 400 이 나지 않음. 막히는 것: buildExecutionPlan 미구현, State enum 이 닭강정집 전용, 추천 계열 HTTP 경로 없음. 셋 다 백엔드 쪽이고 프론트가 손댈 수 없다. 급한 순서 표와 로컬에서 셋 다 띄우는 방법을 함께 넣는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: 문서 안에서 서로 어긋나던 두 곳을 맞춘다 붙이는 방법이 createHttpBackend 라고 되어 있었다. 팀 백엔드에는 createTeamBackend 를 써야 한다. 인터페이스 표의 getEvidence 도 앞쪽 설명과 두 말을 하고 있었다. 명세서 경로와 팀 백엔드 실제를 두 칸으로 나눠 적는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(readme): 낡은 곳 넷을 고친다 - 백엔드 붙이는 예시가 createHttpBackend 였다. createTeamBackend 로 고치고, 추천 경로가 없어 지금 바꾸면 확인 화면이 비어 버린다는 것도 적는다. - 결제 문자열 스캔 범위가 src 가 아니라 저장소 전체다. - '백엔드가 아직 없어' 는 더 이상 사실이 아니다. - 구조 표에 api/bff 가 빠져 있었다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: BFF 함수가 배포에서 잡히지 않던 두 가지를 고친다 배포본을 찔러 보니 경로마다 답이 달랐다. /api/bff/x 500 /api/bff/internal/simulation/session 404 catch-all 파일명(api/bff/[...path].ts)을 걷어내고 평범한 api/bff.ts 하나로 바꾼다. 뒤쪽 경로는 vercel.json rewrite 가 ?p= 로 넘긴다. 핸들러도 Node 방식(req, res)으로 바꾼다. 웹 방식은 req.url 이 절대 주소가 아니라 new URL 에서 터졌다 - 500 의 정체다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 팀 백엔드의 새 경로 셋에 붙인다 orchestrator/approve 안내 문서대로 셋 다 반영했다. 1) BFF 허용경로 internal/orchestrator/approve 와 api/v1/recommendation-output-validations 를 연다. 실제로 쓰는 순서대로 목록을 다시 정렬한다. 2) 한글 선택지 → enum 변환기 (src/api/canonical.ts) 화면은 '매운맛' 을 들고 있고 계약은 SpicyLevel.HOT 이다. 둘을 잇는 곳을 한 파일에 모은다. 값은 백엔드 enum 파일에서 그대로 옮겼다. · 안 고른 축은 NO_PREFERENCE, 모르는 값은 UNKNOWN 으로 구분한다. 뭉뚱그리면 새 선택지가 생겼을 때 서버가 '아무거나 괜찮대요' 로 읽는다. · 모르는 알레르기도 버리지 않고 UNKNOWN 으로 보낸다. 조용히 버리면 그 사람의 알레르기가 서버에 전달되지 않는다. · 이름·전화번호가 들어갈 자리를 만들지 않는다. displayName 은 선택이라 아예 안 보낸다. 테스트로 잠갔다. 3) 승인 경로 교체 submit 이 orchestrator/approve 로 다섯 조각을 갖춰 보낸다. environmentId 는 보내지 않는다 — 서버가 sessionId 로 다시 조회한다. 사용자가 고른 후보가 있으면 그것이 1순위로 바뀌어 나간다. 곁들여 - filterCandidates·recommend 를 실제 경로에 붙인다. 이름·가격은 추천이 아니라 candidate-filters 의 eligibleCandidates 에서 온다. 이게 없으면 화면에 상품 ID 밖에 보여 줄 게 없고 그건 실격 조건이다. - recommend 에 survivingCandidateIds 를 보내지 않는다. 서버가 다시 계산한다 — 클라이언트가 보낸 필터 결과를 믿으면 알레르기 필터를 우회할 수 있다. 그 판단이 옳아서 따라간다. - createSession 이 돌려주는 environmentId 를 받아 쓴다. 예전에는 chicken-store 로 고정이라 병원에 붙어도 닭강정을 봤다. - client.ts 에 getProfile 을 연다. 백엔드에 프로필 저장소가 없어서 매 호출마다 내용을 함께 보내야 한다. 아직 전환하지 않는다. matchedOptions 가 없어 확인 카드가 조건별 판단을 못 채우고, 운영 배포에 컨트롤러가 아직 없다. 그 둘이 풀리면 client.ts 마지막 줄 하나만 바꾸면 된다. 111 → 119 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 로컬에서 dev 백엔드로 붙여 보고 찾은 것들을 고친다 운영 배포(main)에 컨트롤러가 없어서 dev 를 로컬에서 돌려 붙였다. 네 경로 전부 200 이고 확인 화면이 실제 백엔드 데이터로 그려진다. 로컬 연동 길을 연다 - npm run dev:team — VITE_BACKEND=team 으로 팀 백엔드를 쓴다. 기본(npm run dev)은 그대로 목이라 배포본은 바뀌지 않는다. - vite 개발 서버가 /api/bff 를 KIOBRIDGE_API_BASE 로 넘긴다. 배포본의 BFF 함수가 하는 일을 개발 서버가 대신하는 것이라 CORS 가 없다. - 그 프록시가 Origin·Referer·Cookie 를 떼고 보낸다. 안 떼면 백엔드 CORS 필터가 403 을 준다. 배포본 BFF 도 클라이언트 헤더를 넘기지 않는다. 붙여 보고 고친 것 - 제외 사유로 explanation 대신 reasonText 를 쓴다. explanation 은 "ruleId=CHICKEN_ALLERGEN_HARD_CONSTRAINT, sourceValue=[PEANUT]" 같은 규칙 추적 문자열이다. 그대로 두면 어르신 화면에 그게 나간다. - available:false 후보를 후보 목록과 추천 양쪽에서 뺀다. 서버가 품절 닭강정을 eligibleCandidates 에 남기고 대안으로도 올려 보낸다. 심사 필수 기준이 '선택 불가능 후보 추천 0건' 이라 여기서도 막는다. 뺀 이유는 사용자에게 말해 준다 — 조용히 사라지면 "왜 없지?" 가 된다. - 같은 후보의 제외 사유가 후보 필터와 추천 양쪽에서 와서 두 번 보였다. 후보 id 로 합친다. - userDecision 에 note: null 을 보내지 않는다. 킷 스키마가 막는다. matchedOptions 를 만든다 - candidate-filters 응답에 후보별 attributes(spicyLevel·boneType)와 supportedOptions(SERVICE_TYPE·CUP)가 함께 온다. 사용자가 고른 값과 같은 어휘라 그대로 비교할 수 있다. 이름 문자열로 짐작하는 게 아니다. - 확인 카드의 "형태 순살 → 고르신 메뉴와 달라요" 가 이 결과다. - 서버가 matchedOptions 를 주면 그때 이 계산은 걷어낸다. 문서에 붙여 본 결과와 백엔드 쪽 문제 넷을 적는다 (승인 스키마 오류 · 품절 후보 추천 · explanation 문구 · boneType 미반영) 122 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 실서버에 붙어 있는지 눈으로 보이게 한다 화면만 봐서는 목인지 실서버인지 알 수가 없다. 둘 다 그럴듯한 답을 돌려주기 때문이다. 붙여 놓고도 "이게 진짜 도는 건가" 를 확인할 방법이 없다는 지적을 받았다. 실제로 나간 요청을 그대로 띄운다. ● 실서버에 붙어 있습니다 주고받은 요청 3건 · 성공 3건 200 /api/v1/recommendations 12ms 200 /api/v1/candidate-filters 14ms 200 /internal/simulation/session 12ms 이게 증거가 되는 이유는 목이 이 줄을 만들 수 없기 때문이다. 목은 브라우저 안에서 답을 만들어 내서 네트워크 요청이 아예 없다. - src/api/연동기록.ts — 오간 요청을 최근 12건만 들고 있는 작은 저장소. - createTeamBackend 의 보내기 가 경로·상태·걸린 시간을 남긴다. 실패도 남긴다. 성공만 보이면 조용한 실패를 못 본다. - App.tsx 의 연동표시 가 그것을 그린다. 배포본에는 들어가지 않는다. 팀백엔드모드는 빌드할 때 정해지는 상수라 기본 빌드에서는 이 컴포넌트를 부르는 자리가 통째로 빠진다. '실서버에 붙어 있습니다' · 'KIOBRIDGE_API_BASE' 가 dist 에 없는 것을 확인했다. 심사용 배포본이나 시연 영상에 개발용 창이 나올 일이 없다. 122 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 표준형을 직접 만들지 않고 서버 정규화 경로를 거친다 추천이 정규화된 값을 못 받고 있다는 지적을 받았다. 맞는 지적이었다. CanonicalProfile.SessionContext 를 프론트가 만들어 바로 추천에 넣고 있었다. 정규화를 거치면 세 가지가 달라진다. - providerId 를 짐작하지 않는다. 서버가 kiobridge.team-id 에서 채운다. 전에는 설정 파일을 보고 "WHATTHEBUG" 를 적어 두고 있었다. - 킷 스키마로 검증한 값만 쓴다. 잘못된 값이면 매핑 단계에서 멈춘다. 전에는 아무도 확인하지 않아서 승인 직전에야 터졌다. - reconfirmationFields 를 받는다. 어떤 항목을 다시 물어야 하는지 서버가 알려 준다. collectionMetadata 는 source WEB_FORM . confidence 1.0 . confirmedByUser true 로 보낸다. 사용자가 화면에서 직접 눌러 고른 값이라 그게 사실이다. 낮춰 적으면 서버가 RECONFIRMATION_REQUIRED 를 주는데, 이미 고른 것을 또 묻게 된다. 승인은 매핑 때 받아 둔 정규화 결과를 다시 쓴다. 승인 때 새로 만들면 확인 화면에서 본 조건과 실제 제출되는 조건이 갈라질 수 있다. 테스트 목이 호출 순서에 기대고 있었다. 정규화 두 개가 앞에 끼자 전부 어긋났다. 경로를 보고 답하도록 바꾼다 - 앞으로 호출이 늘어도 안 깨진다. 로컬 dev 로 확인했다. 여섯 경로 전부 200 이고 장바구니까지 간다. profile-normalizations . session-context-normalizations candidate-filters . recommendations . orchestrator/approve . simulation/session 122 -> 124 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: 추천 전에 통합 검증(recommendationReady)을 확인한다 canonical-inputs/validate 를 건너뛰고 있다는 지적을 받았다. 맞는 지적이고, 형식 문제가 아니라 안전 문제였다. 개별 정규화는 각자 반쪽만 본다. 프로필 따로, 세션 맥락 따로. 합쳐야 보이는 게 있고 알레르기가 그렇다. 실제로 찔러 보니 이렇게 나온다. 정상 입력 status=VALID recommendationReady=true 알레르기 UNKNOWN status=RECONFIRMATION_REQUIRED recommendationReady=false HARD_CONSTRAINT_UNKNOWN "allergenIds 가 UNKNOWN 입니다. 임의로 추론하지 말고 재확인하거나 안전한 대체경로를 사용하세요." 이건 우리 코드에 실제로 있는 경로다. 변환기가 모르는 알레르기를 UNKNOWN 으로 보낸다 - 조용히 버리는 것보다 낫다고 판단해서다. 그런데 그 다음에 이 문을 안 거치고 바로 추천으로 갔다. 화면에 새 알레르기 선택지가 하나만 늘어도, 앱이 모르는 알레르기를 가진 분에게 그대로 음식을 추천하게 된다. recommendationReady 가 false 면 후보 필터도 추천도 부르지 않는다. 사용자에게는 서버 문구 대신 할 수 있는 일로 바꿔 말한다 - 프로필에서 다시 고르거나 직원에게 도움을 청하라고. 테스트로 잠갔다. false 일 때 실제 호출이 하나도 나가지 않아야 통과한다. 로컬 dev 로 확인했다. 일곱 경로 전부 200 이고 장바구니까지 간다. 124 -> 126 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: PR #46 코드 리뷰 14건 반영 사용자에게 사실이 아닌 말을 하던 것 - UNKNOWN 축을 '안 맞음' 으로 단정하고 있었다. 사용자가 고른 선택지를 우리가 enum 으로 못 옮긴 것인데 "오늘은 이 조합이 없어요" 가 나갔다. NO_PREFERENCE 와 함께 판단에서 뺀다. - 품절 안내의 조사가 '은' 으로 고정이었다. 받침 없는 이름에서 틀린다. 받침을 보고 은/는 을 고른다. - FAIL 을 계속 진행 중으로 두고 있었다. 화면이 90초를 기다리다 일반 오류로 끝난다. 끝난 일은 끝났다고 말하되, 안전 중단과 문구를 나눈다. 안전 중단만 직원 초기화를 안내한다. 승인이 서버 검증에 걸릴 수 있던 것 - 고른 후보를 1순위로 덮으면서 대안에서 빼지 않았다. 두 필드가 겹치면 RecommendationValidator 가 ALTERNATIVE_DUPLICATES_RECOMMENDED 로 막는다. 고른 것을 대안에서 빼고 원래 1순위를 대안으로 내린다. 지우기 약속을 못 지키던 것 - createTeamBackend 이 들고 있던 네 Map 을 비울 경로가 없었다. '이 기기에서 정보 지우기' 를 눌러도 정규화된 프로필과 세션 맥락이 메모리에 남았다. forgetSession 을 구현하고 createApi.forgetAll 이 환경 Map 도 비우게 한다. 낡은 값을 쓰던 것 - 정규화 캐시 키가 프로필 id 뿐이었다. 프로필을 고치거나 다른 키오스크에 붙으면 첫 결과를 계속 썼다. 환경과 입력 내용을 키에 넣는다. collectedAt 은 부를 때마다 달라져 키에서 뺀다 - 넣으면 캐시가 한 번도 안 맞는다. 그 밖에 - BFF 본문 읽기에 1MB 상한. 인증 없이 열린 함수라 상한이 없으면 큰 본문 하나로 메모리가 계속 는다. 넘으면 413. - consent.personalization 을 opts 로 받는다. 기본값은 true 로 둔다 - 사용자가 조건을 저장하고 이 프로필로 주문하기를 누른 행동이 그 주문에 한한 동의라고 본다. 앱이 실제로 개인화하는데 false 로 적는 것도 사실과 다르다. - 개발용 연결 표시를 접을 수 있게 하고 폭을 화면에 맞춘다. 200% 확대에서 휴대폰 틀의 아래 버튼을 덮어 터치를 가로챌 수 있었다. 문서 - 인터페이스 표가 filterCandidates.recommend 를 '경로 없음' 으로 적고 있었다. 같은 문서 앞부분과 코드는 이미 붙었다고 말한다. - matchedOptions 절이 낡았다. 프론트가 이미 채우고 있으니, 서버가 직접 주면 이 계산을 걷어내겠다는 요청으로 바꾼다. - '네 경로 전부 200' 이 실제로는 일곱 요청이다. - 언어 없는 코드펜스 14개에 text 를 붙인다. - README 의 목 구동 사유를 지금 상태로 맞추고, 형태.컵이 순위에 반영되지 않는다는 제한을 적는다. 로컬 dev 로 확인했다. 1순위가 아닌 후보를 골라도 승인이 통과하고 장바구니까지 간다. 126 -> 131 tests, tsc 0, build ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: BFF 본문 상한을 바이트로 센다 모음.length 는 UTF-16 코드 단위라 한글 100만 자 = 약 3MB 가 통과했다. 상한이 의도의 셋 배였다. 청크의 Buffer 길이를 누적해서 바이트로 센다. 마지막에 Buffer.concat 으로 한 번에 문자열을 만들어 조각 경계에서 한글이 잘릴 여지도 없앤다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FEAT] ExecuteResult evidence(JsonNode) → Evidence 타입 파싱 연결 (#48) * feat: JsonNode evidence를 Evidence 타입으로 파싱해 오케스트레이터 응답에 연결 * fix: evidence 필수 필드 검증 및 NullNode 체크 추가, 요약 메시지 개선 * feat: 회원 및 프로필 영속성 모델 추가 (#50) * [FEAT] 회원가입 및 로그인 API 구현 (#52) * feat: 회원가입 및 로그인 API 구현 * fix: BCrypt 입력 제한 및 DB 예외 처리 보완 * test: BCrypt 72바이트 경계 검증 보완 * feat: 사용자 주문 프로필 저장 및 조회 API 구현 (#54) * chore: 운영 백엔드 PostgreSQL 네트워크 연결 (#55) * fix: executionplanservice null 체크 버그 수정 (#57) Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FIX] recommendation PASS, SKIP 점수 수정 (#59) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: RecommendationEngineService STEP5~7 추천 로직 초기 구현 * fix: STEP2 파트 passesByCandidateId 추가 전달 * fix: SKIPPED, PASS 점수 오류 수정 * fix: WARN 없는 항목의 confidence 임계값 크로스 테스트 픽스처 보정 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 아이디·비밀번호 회원가입·로그인 연동 및 주문표 서버 동기화 (#58) modules/member 의 네 경로에 프론트를 붙였습니다. 로그인은 끝까지 선택입니다 - 로그인하지 않아도 추천부터 최종 확인까지 전 과정이 그대로 동작합니다. 전화번호.인증번호 화면을 흐름째 걷어내고 사용자가 직접 지은 아이디.비밀번호로 바꿨습니다. 실명.전화번호를 받는 칸이 없습니다. (#56 의 프론트 지적이 이것입니다) '프로필' 을 '주문표' 로 바꿨습니다. 화면 말만 바뀌고 백엔드 profile 필드명은 그대로입니다. 함께 들어가는 것 - 승인 전에 거절할 수 있고, 거절도 서버에 기록합니다 - npm run check:backend - 앱이 부르는 경로를 앱이 부르는 순서로 확인 - 배포 빌드가 목으로 나가던 것을 고쳤습니다(build:team) - #48 의 summary.status 를 앱 말투로 옮겨 중단 화면 제목에 씁니다 백엔드에 부탁드린 것은 frontend/docs/BACKEND_INTEGRATION.md 에 있습니다 - 토큰 발급, 주문표 삭제 경로, place 완화, summary.status 를 코드로. 코드래빗 리뷰 24건(17 + 증분 7) 반영. 미해결 0건. frontend/ 만 바뀝니다. tsc 0, 195 tests, build ok. * [FIX] Spring Boot 4 Flyway 자동 마이그레이션 활성화 (#60) * [RELEASE] KioBridge P0 MVP 및 회원 영속성 배포 (#56) * chore: 백엔드 배포 기반 구성 * chore: aws 자동 배포 구성 (#5) * [FEAT] contracts 패키지 공통 파일 (#7) * chore: 설정 파일 및 패키지명 수정 * feat: contracts Simulation API 제출 계약 * feat: contracts 공통 상태 enum * chore: pr issue template 등록 * chore: gitignore 추가 * chore: coderabbit 설정 * feat: Target/Action/UserDecision/ExecutionPlan 레코드 * [FEAT] modules executionplan (#8) * feat: 응답 dto * feat: 세션 생성/제출/검증/실행/Evidence 호출 * feat: 전체 파이프라인 오케스트레이션 * fix: Simulation API 호출 timeout 설정 및 제출 전 승인-계획 불변식 검증 추가 * refactor: SimulationApiClient를 contracts로 이동 (#9) * [FEAT] Evidence, SimulationValidation 타입 정의 추가 (#11) * feat: Evidence, SimulationValidation 타입 정의 * feat: RunResult 등 run 관련 타입 추가, Evidence resolvedSimulationTrace 필드 보완 * [FEAT] SimulationApiClient Input Contract 조회·검증 기능 추가 (#13) * refactor: Jackson 3 JsonNode 타입으로 통일 * feat: Input Contract 조회 및 검증 기능 추가 * [FEAT] Evidence 조회 및 화면용 요약 서비스 구현 (#14) * feat: evidence 조회 API, 화면용 요약 서비스 추가 * fix: evidence.result() null 처리 및 결제 계획 시 FAIL 처리 반영 * [FEAT] Canonical Profile 정규화 API 구현 (#17) * feat: Canonical Profile 정규화 API 구현 * [FIX] Profile providerId를 서버 설정값으로 고정 * [FEAT] executionplan 내부 api 구현 및 리팩토링 & ParticipantSubmission 누락 필드 추가 (#18) * feat: 내부 api dto * feat: Service 세션 생성, 일괄처리 부분 분리 * feat: ParticipantSubmission 누락 필드 추가 * fix: ParticipantSubmission field null check + buildExecutionPlan (stub) 예외처리 추가 * [FEAT] 프론트엔드 온보딩·승인·실행 화면 추가 (#15) * feat: 온보딩·프로필·승인·실행 화면 추가 (Vite + React) 비어 있던 frontend/ 에 키오스크 접근성 앱을 넣는다. 프로필 저장부터 QR 연결, 매핑 결과 승인, 실행 진행, 장바구니 확인까지 전 화면이 동작한다. - 백엔드 연결 지점은 src/api/client.ts 의 KioBridgeApi 4개 메서드 한 곳 - 붙기 전까지는 mock 구현으로 돌고, setScenario() 로 예외 상태를 재현한다 - 프로필은 의미값(텍스트)만 담는다. 상품 ID·화면 좌표를 저장하지 않는다 - 결제 UI 없음. 종료 상태는 cart_ready("장바구니에 담았어요") 하나뿐 - 아이콘 Phosphor(MIT), 사진 출처는 src/assets/ATTRIBUTIONS.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: 프론트 타입과 백엔드 contracts 의 관계를 바로잡는다 "백엔드 contracts 와 이름을 맞춰 놨다"고 적었는데 사실이 아니다. 겹치는 타입 이름이 하나도 없다. contracts 는 주최측이 정한 Simulation Kit 규격이고, domain/types.ts 는 앱과 우리 서버 사이 규격이라 층이 다르다. 그대로 두면 백엔드 담당이 1:1 대응을 기대하고 붙이다 헤맨다. 실제로 이어지는 지점만 표로 적고, 아직 아무도 안 만든 것(앱용 REST 엔드포인트)과 ID 번역이 서버 몫이라는 점을 밝힌다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: low_confidence 승인도 서버가 다시 검증하게 한다 client.ts 는 "프론트 가드만 믿지 않는다"고 적어 두고 정작 low_confidence 만 빼놓고 있었다. 화면에서는 사용자가 카드를 짚어야 승인 버튼이 열리는데, 그 사실이 approve() 인자에 실리지 않아 서버가 확인할 방법이 없었다. changed 가 acknowledgedDiff 로 받는 보장을 low_confidence 도 받게 맞춘다. 확신이 낮을수록 사용자가 직접 짚었다는 사실이 더 중요하다. - ApproveInput 에 confirmedLowConfidence 추가 (필드마다 무엇을 뜻하는지 주석) - approve() 에 CONFIRMATION_REQUIRED 분기 추가 - OrderLowConfidence 호출부에서 confirmedLowConfidence: true 전달 CodeRabbit 리뷰 반영 (PR #15). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 프로필 카드에서 고르는 곳과 지우는 곳을 분리 카드 전체가 role="radio" 이고 그 안에 삭제 버튼이 들어 있었다. 삭제 버튼에 포커스를 두고 Enter 를 눌러도 카드만 선택되고 지워지지 않았다. 부모 div 의 onKeyDown 이 그 Enter 를 가로채 preventDefault() 하는 바람에 버튼의 기본 동작(클릭)이 막혔기 때문이다. 키보드만 쓰는 사람에게는 프로필을 지울 방법이 아예 없었다. - 선택은 시각적으로 숨긴 진짜 input[type=radio] 가 맡는다. 화살표 키 이동과 그룹당 탭 한 번(로빙 tabindex)을 브라우저가 처리한다. - 삭제 버튼은 label 바깥 형제로 옮겼다. - 목록 컨테이너는 role="radiogroup" 대신 fieldset + 숨긴 legend 로 바꿨다. - 포커스 표시는 라디오의 focus 를 받아 카드에 3px outline 으로 그린다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(qr): 스캐너의 중첩 타이머를 정리하지 않아 언마운트 뒤 페어링이 나가던 문제 QrScannerModal 의 useEffect 는 바깥 2.5초 타이머만 해제하고 그 안에서 예약한 800ms 타이머는 그대로 두었다. 스캐너가 떠 있는 동안에도 하단 탭은 눌린다. 스캔이 끝난 뒤 800ms 안에 다른 탭으로 옮기면 안쪽 타이머만 살아남아 언마운트된 컴포넌트에서 onDetected 가 돌고 api.claimPairing 이 호출된다. 화면에는 아무 표시도 안 되는데 키오스크에는 사용자가 요청한 적 없는 연결 요청이 남는다. 두 타이머 ID 를 모두 붙잡아 cleanup 에서 함께 해제한다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 프로필 삭제 버튼의 터치 영역을 44x44 로 넓힘 높이는 44 였지만 폭이 30 이었다. 앱 전체를 훑어 44px 미만인 조작 요소는 이것 하나뿐이었다. 밑줄은 글자에만 걸리므로 minWidth 를 44 로 올려도 보이는 크기는 그대로고 누를 수 있는 영역만 넓어진다. 카드 크기(321x168)도 그대로다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(a11y): 기본 팔레트가 WCAG AA 에 미달하던 대비를 전부 올림 고대비를 별도 옵션으로 켜고 끄게 만드는 대신 기본값을 고쳤다. 찾아 들어가야 보이는 설정은 대비가 필요한 사람이 못 찾을 수 있고, 지금 값은 옵션을 켜기 전 상태에서 이미 기준 미달이었다. 측정값 (흰 배경 / SURFACE 위): - TEXT_2 #8A8A8E 3.44 / 3.21 → #6B6B70 5.30 / 4.95 - TEXT_3 #C4C4C8 1.74 / 1.62 → 글자에서 전부 제거 - FAIL #D92D20 FAIL_BG 위 4.35 → #CE2A1F 4.75 고친 곳 - 읽어야 하는 글자에 쓰이던 TEXT_3 12곳을 TEXT_2 로 옮김. 하단 탭의 안 눌린 항목(내 프로필·계정)이 1.74:1 이라 특히 나빴다. - 고른 프로필 카드: 태그·메모·삭제가 반투명 흰색이라 3.31~3.35 였다. 태그는 바탕을 어둡게 눌러 6.6, 나머지는 흰색으로 올려 5.08. - QR 스캔 안내문 42% 흰색(3.95) → 62%(7.8). 조명을 조절하라는 문장은 화면이 어두울 때 읽는 문장이다. - 안 고른 동그라미 테두리와 변경 확인 체크박스 테두리가 각각 1.62 / 1.29 라 컨트롤 경계 기준(3:1)에 못 미쳤다. TEXT_2 / WARN 으로 바꿈. - 비활성 버튼 글자는 WCAG 예외지만 1.62 는 무엇을 기다리는지 못 읽는다. 초록 알약이 회색으로 바뀌는 것만으로 신호는 충분해서 TEXT_2 로 올림. WARN_BORDER 는 쓸 곳이 없어져서 뺐다. 시나리오 스위치를 돌려 가며 전 화면을 다시 쟀다. 환영·전화번호·인증·이름·프로필 목록·프로필 만들기·QR(스캔/연결중/연결됨/실패/만료) ·확인(정확/애매/없음/변경/불확실)·실행(진행/담김/중단)·계정·접근성·개인정보 전부 미달 0건. * fix(a11y): QR 화면 그릇이 넘칠 때 아래 버튼에 닿을 수 없던 문제 QrScreen 의 가운데 그릇이 overflow-hidden 이었다. 그 안의 패널들(연결중·연결됨·실패·만료)은 아래 버튼을 mt-auto 로 바닥에 붙인다. 내용이 그릇보다 커지면 그 버튼이 잘려 나가는데, hidden 이라 스크롤도 안 생긴다. 포커스를 옮기면 브라우저가 알아서 스크롤해 주므로 키보드로는 닿지만, 손가락으로는 닿을 방법이 아예 없다. 앱이 주는 큰 글씨(1.18배)까지는 넘치지 않는다. 1.4배까지도 안 넘친다. 사용자가 브라우저·OS 글씨 크기를 더 키우면 1.6배 근처부터 넘친다(측정 102px, 1.8배에서 195px). overflow-y-auto 로 바꾸면 넘칠 때만 스크롤이 생기고 안 넘치면 지금과 똑같이 보인다. 1 / 1.18 / 1.6 / 1.8 / 2.2 배에서 잘리는 그릇 0건, 못 닿는 조작 요소 0건. 프로필 목록·계정·개인정보 화면도 1.8 배에서 함께 확인했다. * chore(review): 안 쓰는 shadcn 원본 48개를 CodeRabbit 리뷰 대상에서 제외 frontend/src/app/components/ui/** 는 Figma Make 재출력용으로만 남겨 둔 파일들이라 어느 화면도 import 하지 않는다. 번들에도 들어가지 않는다. 리뷰가 반복해서 여기를 지적하는데, 고쳐도 사용자에게 도달하는 코드는 바뀌지 않아 실제 결함을 찾는 데 방해가 된다. 다시 쓰기 시작하면 이 줄을 지운다. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * [FEAT] 장소별 질문 목록을 시뮬레이션 킷 fixture 축에 맞춤 (#20) * feat(profile): 장소별 질문을 시뮬레이션 킷 fixture 축에 맞춤 앱이 묻는 조건과 심사 환경이 실제로 가르는 축이 서로 달랐다. 드라이브스루·버거 세트·사이드 변경처럼 fixture 에 대응이 없는 질문은 어르신이 골라도 결과가 달라지지 않는, 물어만 보고 버리는 질문이었다. 반대로 fixture 에 있는데 앱에 없던 축이 셋 있었다. - 병원 '접근성 지원' — 우리가 점수 축 20% 로 넣어 놓고 물어보는 화면이 없었다 - 관공서 '인증 방식' — PUBLIC-004 를 AUTH_METHOD_UNAVAILABLE 로 빼는 조건인데 앱에서 입력할 방법이 없었다 - 음식점 '컵' — 확인 카드 필수 5개 중 하나인데 '추가 옵션' 다중선택에 묻혀 있었다 메뉴 이름은 목록에서 고르게 하지 않고 자유 입력(menuName)으로 둔다. 사용자가 부르는 이름과 오늘 화면의 이름을 맞추는 건 매핑의 일이다. 알레르기만 fixture 축에 맞추지 않았다. option-group 이 아니라 사람에 대한 절대 조건이라, 오늘 이 가게에 걸리는 게 땅콩뿐이라고 나머지를 지우면 새우 알레르기가 있는 사람이 그 사실을 말할 방법이 사라진다. mock.ts 의 옵션 라벨도 같은 이름으로 맞췄다. tsc --noEmit · build 통과. 세 장소 모두 화면에서 확인. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(frontend): 안 쓰는 shadcn 컴포넌트 48개와 딸려 온 의존성 53개 제거 frontend/src/app/components/ui/ 의 48개 파일은 어느 화면도 import 하지 않는다. components/figma/ImageWithFallback.tsx 도 마찬가지고, styles/globals.css 는 0바이트 빈 파일이었다. 참조 0건을 확인하고 지웠다. 지우기 전에 react 와 react-dom 을 dependencies 에 명시했다. 지금까지 두 패키지가 선언 없이 @mui/material·radix 의 전이 의존으로만 딸려 오고 있었다. ui/ 를 걷어내면 React 자체가 사라질 뻔했다. tw-animate-css 는 남긴다. JS import 가 아니라 styles/tailwind.css 의 @import 라 코드 검색에는 안 걸리지만 실제로 쓰인다. dependencies 55개 → 4개 (react, react-dom, lucide-react, tw-animate-css) npm ci 92 packages / 6s JS 번들 223.12 kB → 223.12 kB (변화 없음 — 원래 번들에 없던 코드다) CSS 번들 86.03 kB → 13.58 kB (Tailwind 가 그 48개까지 훑고 있었다) tsc --noEmit · build 통과. 개인 저장소와 번들 해시 동일. 실제 화면에서 폰 프레임·Tailwind 유틸리티·QR 화면 overflow-y-auto· 1/1.18/1.6/2.2배 확대를 모두 확인했다. .coderabbit.yaml 의 components/ui 제외 필터는 이제 가리킬 대상이 없어졌지만, 다음 PR 에서 별도로 정리한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * CodeRabbit Generated Unit Tests: Add Generated Unit Tests for PR Changes (#22) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Delete frontend directory * Create yena.txt * Delete .coderabbit.yaml * [FEAT] chicken-store SessionContext 정규화 API 구현 (#25) * [FEAT] recommendation contracts 정의 (#21) * feat: recommendation관련 contracts 정의 (Recommendation, ExcludedCandidate, Candidate, CompatibilityRule, CompatibilityRuleSet) * fix: ParticipantSubmission에 누락된 i…
Yena07
added a commit
that referenced
this pull request
Aug 13, 2026
* [FEAT] 말로 주문표를 채우고, 화면과 읽어주기를 다듬습니다 개인 레포에서 리뷰를 마치고 머지한 둘을 한 번에 올립니다(#39 · #41). ## 말로 주문표를 채웁니다 만들기 화면 맨 위에 '말로 채우기' 를 뒀습니다. "매운 닭강정 포장으로 두 개" 라고 말하면 아래 칸이 채워집니다. **모델이 값을 만들지 않습니다. 우리가 아는 값 중에서 고르기만 합니다.** 화면은 "당신이 고른 조건을 이렇게 썼습니다" 라고 말하는데, 말하지 않은 조건이 섞이면 그 말이 거짓이 됩니다. "치킨 먹고 싶어요" 아무 축도 안 고름 "혼자 먹을 거예요" 수량 안 고름 (둘이 하나를 나눌 수도 있다) "적당히 매콤하게" 맵기 안 고름 (순한맛일 수도 있다) "순한맛으로 할까 매운맛으로 할까" 맵기 안 고름 (아직 고르는 중이다) 알레르기는 말로 안 받습니다. 틀리면 사람이 다치는 값이라 눈으로 보고 고릅니다. 들은 말은 **어느 칸에도 저장하지 않습니다.** 자유 발화라 이름·전화번호가 섞일 수 있는데, 메뉴 이름 칸에는 메모와 달리 개인정보 검사가 없습니다. 확인 화면에 보여 주기만 하고 사라집니다. 소리는 기기 안에서 처리될 때만 듣습니다. processLocally 를 켜고, **그 스위치가 있는 브라우저에서만 단추를 내밉니다** — 없으면 소리가 어디로 가는지 우리가 알 수 없고, 개인정보 화면은 "이 기기 밖으로 나가지 않아요" 라고 말하고 있습니다. ## 화면에서 설명을 걷어냈습니다 도움 설정 화면의 두 문단, 가입 흐름의 두 줄을 뺐습니다. 동의 문구는 "개인정보 수집 동의서" 한 줄로 줄였습니다. 접근성 화면에서 가격 한도와 못 드시는 것을 뺐습니다. 계약에서 자리가 다릅니다 — 이 화면의 것들은 accessibility·interaction 이고 저 둘은 hardConstraints 입니다. ## 읽어주기 화면을 넘기면 즉시 멈춥니다. 예전에는 새 화면의 읽기가 시작될 때에야 끊겨서, 그 사이 지난 화면을 계속 읽었고 새 화면에 읽을 것이 없으면 아예 안 끊겼습니다. 읽을 값어치가 없는 것은 안 읽습니다(data-소리생략) — 앱 이름, 개발용 연동 막대, 화면마다 똑같은 하단 탭. 전 kio. bridge. 게스트로 이용 중. ... QR 찍기. 내 주문표. 계정 후 게스트로 이용 중. ... ## 이유가 무엇에 대한 얘기인지 밝힙니다 전 제외: 지금은 품절이라 제외됐어요 후 제외: 닭강정 — 지금은 품절이라 제외됐어요 서버 문장은 그대로 두고 앞뒤에 우리가 아는 사실만 덧댑니다. 축은 서버 원문에서 봅니다 — 이름을 먼저 붙이면 "종이컵 세트" 같은 이름 때문에 없는 축이 걸립니다. ## 영어 화면에 우리말이 없습니다 로고의 aria-label("키오브릿지")과 언어 항목("한국어")을 표에 넣었습니다. 주문표 카드의 사용자 입력은 그대로 둡니다 — data-원문 이 지킵니다. backend/ 는 건드리지 않았습니다. tsc 0 · 테스트 316 통과 · 빌드 성공(--mode team) * [FIX] 리뷰 반영 — 문맥 없는 말로 안 고르고, 입력 방식을 실제로 쓴 것에서 받습니다 네 건 모두 유효했습니다. ## 문맥 없는 별칭이 축을 골랐습니다 "I am here" 가 먹고 가기를, "regular chicken" 이 일반컵을 골랐습니다. 우리말도 같아서 "일반적으로" 가 일반컵에 걸렸습니다. **사용자가 말하지 않은 조건이 섞이는 자리**라 그냥 둘 수 없었습니다. 혼자서는 다른 뜻이 되는 별칭을 뺐습니다 — here · paper · regular · 일반 · 종이. 축을 같이 말한 것만 받습니다("regular cup", "종이컵"). ## "적당히 매콤하게" 를 못 들었다고 했습니다 값을 못 고르면 전부 '못 들었어요' 로 갔습니다. 맵기를 말하기는 한 사람에게 못 들었다고 하면, 다시 또박또박 말해 보다가 같은 답을 받습니다. 축 얘기를 했는지 따로 보고 모호한축으로 넣습니다. 화면은 "말씀은 들었는데 어느 쪽인지 못 골랐어요" 라고 말합니다. ## preferredInput 을 voiceGuide 에서 끌어 왔습니다 voiceGuide 는 **읽어 주는** 설정이고 preferredInput 은 **넣는** 방식입니다. 둘은 자주 어긋납니다. 말로 채웠는데 읽어 주기를 껐다 → TOUCH 로 나갔다 손으로 골랐는데 읽어 주기를 켰다 → VOICE 로 나갔다 키오스크는 이 값을 보고 안내 방식을 정하므로 틀리면 그쪽이 잘못 준비합니다. api/inputsource.ts 를 두고 '말로 채운 적이 있나' 를 따로 담습니다. 확인 화면에서 실제로 채웠을 때만 켜지고, 손으로 몇 칸 고쳐도 그대로입니다 — 몇 칸 고쳤다고 "말로 넣는 사람" 이 아니게 되지는 않습니다. 새로고침을 넘어 남고(session.ts의 voiceUsed), 로그아웃과 기기 지우기에서 비웁니다. 들은 말 자체는 여전히 어디에도 안 담습니다. 남는 것은 무엇으로 채웠나 하는 사실 하나뿐입니다. ## 문서가 제품보다 느슨했습니다 명세에 "새우는 빼 주세요 는 받는다" 고 적혀 있었는데, 실제로 만든 것은 말로는 아예 안 받는 쪽입니다. 문서가 느슨한 채로 남으면 나중에 그것을 보고 만드는 사람이 느슨한 쪽으로 되돌립니다. 제품에 맞췄습니다. 확인 화면 예시도 지금 화면 문구로 바꾸고, 조사를 자리표시자로 붙이지 않는 이유를 적었습니다. tsc 0 · 테스트 325 통과(음성 21개) · 빌드 성공(--mode team) * [FIX] 리뷰 반영 — 지원하지 않는 이용 방식도 "못 골랐어요" 로 알립니다 ## "배달로 할게요" 를 못 들었다고 했습니다 이용 방식을 말했는데 우리 목록(먹고 가기·포장하기)에 없는 값입니다. 지금은 못들은축으로 가서 화면이 "못 들었어요" 라고 했습니다. 말한 사람은 다시 또박또박 말해 보다가 같은 답을 받습니다. 값으로 등록할 수는 없습니다 — 없는 것을 고를 수는 없습니다. 축을 말한 것으로 보고 "말씀은 들었는데 어느 쪽인지 못 골랐어요" 라고 답합니다. 영어(deliver· delivery)도 같이 넣었습니다. ## 문서 코드 블록에 언어를 붙였습니다 확인 화면 예시 블록입니다. tsc 0 · 테스트 327 통과(음성 23개) · 빌드 성공(--mode team)
Yena07
added a commit
that referenced
this pull request
Aug 14, 2026
* [FEAT] GlobalExceptionHandler 구체화 (#87) * feat: GlobalExceptionHandler 구체화 * fix: NPE->REQUIRED_FIELD_MISSING + 내부 업스트림 오류 400 반환 문제 해결 * fix: NPE 메시지 노출 + OPTION_GROUP_EMPTY 테스트 누락 수정 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * fix: @Valid 오류 응답을 공통 형식으로 반환 (#91) * feat: pgvector 마이그레이션 분리 및 EmbeddingService 구현, OpenAI 임베딩 연동 테스트 완료 (#92) * feat: pgvector 마이그레이션 분리 및 EmbeddingService 구현, OpenAI 임베딩 연동 테스트 완료 * fix: 마이그레이션 버전 충돌 방지를 위해 V2 → V3로 변경 * fix: OpenAI/vector 설정을 vector 프로필로 격리, 폴더명 db/postgresql로 변경, EmbeddingServiceTest 조건부 실행으로 분리 * fix: RestClient 타임아웃 설정, spicy_level CHECK 제약 추가, EmbeddingServiceTest를 external 태그로 기본 테스트에서 분리 * fix: ivfflat 인덱스를 hnsw로 교체 (빈 테이블 기준 학습 문제 방지) * [FEAT] 서버 주문표 삭제와 FE↔BE 계약 점검 넷을 반영합니다 (#94) * [FEAT] 서버 주문표 삭제와 FE↔BE 계약 점검 넷을 반영합니다 개인 레포에서 리뷰를 마치고 머지한 것 셋을 frontend/ 로 옮깁니다. 백엔드는 건드리지 않았습니다. ## ① 서버에 올라간 주문표도 지웁니다 (팀 #79 연결) DELETE /api/v1/users/{userId}/profiles/{profileId} 를 붙였습니다. '이 기기에서 정보 지우기' 가 이제 서버까지 닿습니다. 배포본에서만 나던 구멍도 같이 막았습니다. api/bff.ts 가 GET·POST 만 넘겨 주고 있어서 DELETE 는 운영에서 405 가 났습니다 - 개발 서버의 프록시는 다 넘겨 줘서 로컬에서는 안 보였습니다. 경로별 메서드 허용 목록을 두고, 메서드 검사를 경로 파싱 뒤로 옮겼습니다. src/api/bff-guard.test.ts 를 새로 넣었습니다. api/bff.ts 는 서버 함수라 브라우저 테스트 환경에서 import 할 수 없어서, 파일을 글로 읽고 정규식을 뽑아 검사합니다. DELETE 가 프로필 상세 경로 하나에만 열려 있는지, 기본 메서드 목록에 안 섞였는지를 못 박습니다. ## ② 재확인 분기가 죽어 있었습니다 RECONFIRMATION_REQUIRED 검사가 INVALID 검사 아래에 있어서 한 번도 닿지 못했습니다. 백엔드는 reconfirmationFields 가 비지 않을 때만 RECONFIRMATION_REQUIRED 를 내고 그 필드는 contractValidation.errors 에서 만드는데, 킷이 HARD_CONSTRAINT_UNKNOWN 을 error 로 넣기 때문에 그 경우 valid 가 반드시 false 입니다. 그래서 늘 INVALID 쪽이 먼저 던졌고, 알레르기를 모르는 분에게 돌아갈 길 없는 오류와 킷 원문이 그대로 나갔습니다. 순서를 뒤집었습니다. ## ③ 중단된 실행의 진행 표시가 거짓이었습니다 state 가 aborted 인데 다섯 단계가 전부 '됨' 으로 그려졌습니다. reachedStep 까지만 done 으로, 그 칸은 failed 로 그립니다. ## ④ 못 맞춘 조건과 후보별 규칙 판정 unmetConditions 를 버리고 있었습니다. 담기는 담는데 한 축이 어긋난 경우가 여기 오는데, 화면까지 오지 않아서 사용자는 자기 조건이 다 반영된 줄 알고 승인했습니다. warningsByCandidateId · passesByCandidateId 도 받아 씁니다. 예전에는 이걸 버리고 attributes·supportedOptions 로 같은 판단을 다시 했는데, 같은 판단을 두 곳에서 하면 언젠가 갈라집니다. 그리고 다시 하는 쪽은 알 수 없는 것이 있습니다 - WARN 이 없다는 사실만으로는 '일치한다' 를 뜻하지 않습니다. SKIPPED 면 '비교한 적이 없다' 일 뿐입니다. 응답에 판정 필드가 아예 없을 때만 예전 방식으로 물러납니다. 필드는 왔는데 그 후보 항목만 없는 것은 '비교한 축이 하나도 없다' 는 뜻이라 빈 것이 맞는 답입니다. ## ⑤ 장소 가드 카페 주문표로도 주문 버튼이 눌렸습니다. 백엔드가 다루는 것은 닭강정집뿐이라 serviceType 이 UNKNOWN 이 되고 나머지 축은 전부 NO_PREFERENCE 가 되는데, 킷 스키마가 UNKNOWN 을 허용하는 enum 이라 검증은 통과하고 닭강정이 추천됐습니다. 게다가 확인 카드가 텅 빈 채로 승인 화면이 떴습니다 - 무엇을 담는지 못 보고 승인하게 됩니다. 막고, 왜 막는지 화면에 적습니다. ## 확인 frontend/ 에서 tsc 0 · 테스트 243개 통과 · vite build ok * [FIX] 리뷰 반영 — 지운 주문표가 되살아나던 것과 안내 문구 지적 다섯 중 넷을 고쳤습니다. 하나(BFF 인증)는 프론트에서 못 고치는 것이라 이슈로만 남깁니다. ## ① 개별 삭제가 서버까지 안 갔습니다 목록에서 주문표 하나를 지우면 이 기기에서만 지워졌습니다. 화면은 "지우면 되돌릴 수 없어요" 라고 말해 놓고, 다음에 로그인하면 그 주문표가 그대로 돌아왔습니다. 서버에서온것 으로 거르지 않습니다 - 그 집합은 서버에서 '불러온' 것만 담아서, 이 기기에서 만들어 올린 주문표는 거기 없습니다. 걸러 내면 정작 지워야 할 것을 빠뜨립니다. 없는 것을 지워도 204 라 다 보냅니다. ## ② 정보 지우기가 서버에만 있는 주문표를 놓쳤습니다 화면에 있는 id 만 지우고 있었습니다. 로그인 직후 목록을 불러오는 중에 정보 지우기를 누르면 서버에만 있던 주문표는 아직 sheets 에 없고, 계정세대가 올라가 늦게 도착한 목록도 버려집니다. 그 주문표에는 DELETE 가 한 번도 안 갑니다. 먼저 listSheets 로 받아 합칩니다. 목록을 못 받으면 아는 것만 지우되 **다 지웠다고 말하지 않습니다** - 화면이 "서버에 올라간 주문표도 함께 지워요" 라고 약속했으므로, 못 지킨 것은 못 지켰다고 말해야 합니다. ## ③ 고른 적 없는 장소를 얘기하고 있었습니다 장소를 안 고른 주문표에도 "이 장소는 아직 키오스크와 연결되지 않았어요" 가 떴습니다. '이 장소' 가 무엇인지 알 길이 없고, 무엇을 하면 주문할 수 있는지도 안 알려 줍니다. 둘을 나눠 말합니다. ## ④ 재확인 판정만 오면 우리가 다시 계산했습니다 서버판정있음 을 warnings·passes 둘로만 정하고 있었습니다. 재확인 판정만 실려 오면 그 검사가 거짓이 되어 확인표() 로 물러났고, 서버가 "비교하지 못했다" 고 한 축을 우리가 맞다고 말하게 됩니다. 셋 중 하나라도 오면 서버가 판정한 것으로 봅니다. 회귀 시험을 넣고, 고치기 전 코드로 돌려 정말 잡히는지 봤습니다. → expected [ { label: '이용 방식', …(2) }, …(3) ] to deeply equal [] ## 안 고친 것 — BFF 가 DELETE 를 호출자에게 못 묶습니다 맞는 지적이지만 프론트에서 고칠 수 없습니다. 백엔드가 토큰을 주지 않아서(응답이 { userId, loginId } 뿐) BFF 가 세션에서 사용자를 끌어낼 방법이 없습니다. DELETE 만의 문제도 아닙니다 - GET/POST 도 경로의 userId 를 그대로 믿고 있어서 같은 노출입니다. #83 에 올려 두었습니다. 토큰이 생기면 BFF 에서 경로의 userId 와 맞춰 보는 것은 그때 한 줄입니다. 확인: frontend 에서 tsc 0 · 테스트 244개 통과 · vite build ok * docs: reconfirmationsByCandidateId 를 왜 안 읽는지 적어 둔다 리뷰 답변에서 적어 두겠다고 한 것이다. 이 필드는 allergenIds 에 UNKNOWN 이 섞였을 때만 채워지는데, 그 경우는 그보다 앞인 정규화에서 이미 막힌다(RECONFIRMATION_REQUIRED -> RECONFIRM_REQUIRED). filterCandidates 는 정규화() 를 먼저 부르므로 후보 필터까지 도달하지 못한다. 선언만 보고 '안 쓰네' 하면 닿지도 않는 길을 화면에 이으려다 아무도 지나가지 않는 코드를 만들게 된다. 다음 사람이 같은 결론에 도달하지 않도록 이유를 타입 옆에 남긴다. tsc 0 · 테스트 244개 통과 * [FIX] 목록을 못 받은 사실이 삭제 결과에 덮이지 않게 한다 확인창 자리가 하나뿐이라 나중 것이 앞의 것을 덮고 있었습니다. 1. listSheets 실패 -> 아는 것만 지우기 시작 + "목록을 못 받았어요" 창 2. 그중 하나라도 삭제 실패 -> "이 기기에서는 지웠어요" 창이 1번을 덮음 3. 거기서 '다시 시도' -> 못 지운 것만 다시 지움. 목록은 다시 안 받음 그러면 처음 조회에서 빠진 서버 주문표는 영영 안 지워지고, 사용자는 목록을 못 받았다는 사실조차 못 보고 지나갑니다. 목록을 못 받았다는 사실을 삭제 쪽으로 넘겨 하나의 창에서 함께 말합니다. 목록못봄 이면 지우지 못한 것이 없어도 알린다 무엇이 올라가 있었는지조차 모르는 상태라 다 지켰다고 말할 수 없다 목록못봄 이면 다시 시도가 목록부터 다시 받는다 서버주문표모두지우기 로 돌아간다 문구도 세 갈래로 나눴습니다. 목록 못 받음 + 다 지움 "남은 것이 있는지 알 수 없어요" 목록 못 받음 + 일부 실패 "목록도 못 받았고 지우지 못한 것도 N개 있어요" 목록 받음 + 일부 실패 기존 문구 그대로 확인: frontend 에서 tsc 0 · 테스트 244개 통과 · vite build ok * fix: 알레르기 제외 사유를 사용자 문구로 변환 (#95) * [FIX] 프로필 API 세션 인증 및 소유권 검증 (#97) * feat: 프로필 API 세션 인증 및 소유권 검증 추가 * test: 세션 인증 리뷰 사항 반영 * [FEAT] 새로고침을 넘겨 이어 쓰고, 계약에 있던 자리 넷을 화면에서 묻습니다 (#96) * [FEAT] 새로고침을 넘겨 이어 쓰고, 계약에 있던 자리 넷을 화면에서 묻습니다 개인 레포에서 코드래빗 리뷰를 마치고 머지한 넷을 frontend/ 로 옮깁니다. 백엔드는 건드리지 않았습니다. Yena07#29 새로고침 이어쓰기 · 도움 설정 화면 · 가격 한도 · 필드 감사 · 소리 안내 Yena07#30 안내 언어 Yena07#33 알레르기 Yena07#31 개인정보 동의 ## 계약에 있는데 화면이 안 묻던 자리 넷 여태 값을 박아 보내고 있었습니다. 로컬 백엔드로 하나씩 확인했습니다. hardConstraints.maxPriceKrw 늘 null -> 가격 한도를 묻는다 interaction.preferredInput 늘 TOUCH -> 소리 안내를 켜면 VOICE interaction.language 늘 ko-KR -> 안내 언어를 고른다 consent.personalization 늘 true -> 동의를 받는다 가격 한도는 서버의 가격 점수를 살립니다. 같은 주문표로 재 봤습니다. 한도 없음 priceScore 0.0 confidence 0.5 대안 2개 한도 5,800원 priceScore 0.0259 confidence 0.8 대안 0개 ## 새로고침해도 이번 이용이 이어집니다 sessionStorage 에 적습니다. localStorage 가 아닌 이유는 개인정보 화면이 "이번 이용이 끝나면 남지 않아요" 라고 약속하고 있어서입니다 — sessionStorage 는 창을 닫으면 사라져 그 약속을 그대로 지킵니다. 연결(pairingId)은 안 담습니다. 디스크에 적어 두고 되살리면 QR 한 번이 새로고침을 넘어 계속 쓰이는 실행 권한이 됩니다(P0-2). 승인 화면도 살아 있는 연결 위에서만 뜻이 있어 되살리지 않습니다(P0-4). ## 개인정보 동의 없이는 앱에 들어갈 수 없습니다 들어오는 문 셋(첫 화면.로그인.가입) 모두에서 막습니다. 게스트도 같이 막습니다 - 로그인한 사람에게만 물으면 정작 가장 많이 쓰일 길에서는 안 묻는 셈이 됩니다. 가드는 한 곳에 둡니다. 화면마다 버튼만 잠그면 샜습니다 - '자세히' 로 개인정보 안내를 열고 뒤로 가면 앱 안으로 들어가졌습니다. ## 알레르기는 가입 때 한 번만 묻습니다 주문표마다 물으면 새 주문표를 만들 때마다 다시 골라야 하고, 한 번 빠뜨리면 그 주문표로 주문할 때 안 걸러집니다. 빠뜨려도 되는 값이 아닙니다. 서버로 나갈 때 주문표 쪽과 합집합을 만듭니다 - 덮지 않으므로 어느 쪽을 빠뜨려도 안전한 방향으로만 틀립니다. ## 서버가 주는데 안 읽던 값들 sourceValue/candidateValue "오늘은 이 조합이 없어요" -> "이 메뉴는 매운맛이에요" severity BLOCK 이면 후보에서 뺀다 scoreBreakdown 숫자 말고 축 이름만 ("이걸 보고 골랐어요") ## 확인 frontend/ 에서 tsc 0 · 테스트 300개 통과 · vite build ok * [FIX] 리뷰 반영 — 복원 자리를 하나로 모으고, 로그아웃이 한도까지 비웁니다 세션 복원이 두 줄로 갈라져 있었습니다(병합 흔적). 지금은 둘 다 돌아서 문제가 없지만, 새 값이 생겼을 때 한쪽에만 넣는 날이 옵니다. 그날 빠지는 것이 알레르기면 걸러졌어야 할 후보가 새로고침 뒤에 그냥 올라옵니다. 한 줄로 모았습니다. 로그아웃이 가격 한도를 안 비우고 있었습니다. 한도는 화면에 늘 보이는 값이 아니라 후보를 조용히 잘라 내는 값입니다. 남겨 두면 다음 사람은 앞사람의 지갑 사정으로 걸러진 목록을 보고 이유를 알 방법이 없습니다. 동의·알레르기와 같은 판단으로 비웁니다. 도움 설정은 그대로 둡니다 — 켜져 있는 게 화면에 바로 보여서 모르고 쓰게 되는 종류가 아닙니다. 호칭 칸에 길이 제한을 뒀습니다. 실제 이름인지는 코드가 가려낼 수 없지만 ('김씨' 는 호칭이자 성씨입니다) 이 칸이 호칭 말고 다른 것을 담지 못하게는 할 수 있습니다. 안내 문구도 "아니어도 괜찮아요" 에서 "실제 이름 말고" 로 바꿨습니다. 그 밖에 - profileInput 을 정규화에서 한 번만 만듭니다. contextInput 과 같은 규칙입니다. - 테스트 픽스처의 상품 ID·좌표 모양을 중립 이름으로 바꿨습니다. 걸러 내는 쪽은 흰 목록이라 칸 이름과 무관하고, 금지된 값의 모양을 소스에 두지 않는 게 낫습니다. - 읽기() 주석이 "한 칸이라도 이상하면 통째로 버린다" 고 했는데 실제로는 주문표만 그렇습니다. 나머지는 그 칸만 안전한 기본값으로 되돌립니다. 실제 동작대로 고쳤습니다. - 칸 열한 개짜리 테스트 제목이 "열 개" 였습니다. tsc 0 · 테스트 300 통과 · 빌드 성공 * [FEAT] 영어 안내와 소리 안내를 넣고, 저장한 주문표를 고칠 수 있게 합니다 (#98) * [FEAT] 영어 안내와 소리 안내를 넣고, 저장한 주문표를 고칠 수 있게 합니다 개인 레포에서 리뷰를 마치고 머지한 넷을 한 번에 올립니다(#34 · #35 · #36 · #38). ## 화면을 영어로 옮깁니다 안내 언어를 English 로 두면 화면 글이 실제로 영어가 됩니다. 예전에는 고를 수는 있는데 화면은 한국어였습니다 — 키오스크에 전하기만 하는 값이었습니다. 문장마다 t() 를 두르지 않고, 그린 뒤에 한 번에 옮깁니다(src/i18n/apply.ts). 화면에 나가는 우리말이 600줄 가까이 되고 App.tsx 한 파일에 흩어져 있어서, 손으로 두르면 빠뜨리는 자리가 생기고 그 자리는 화면을 열어 보기 전까지 아무도 모릅니다. 반쯤 영어인 화면은 한국어 화면보다 나쁩니다. 안 옮기는 것을 정해 뒀습니다. - 사용자가 적은 값(주문표 이름·메모·호칭). data-원문 으로 표시해 건드리지 않습니다. 표에 있는 문구를 사용자가 그대로 적을 수 있습니다 — 주문표 이름을 '포장하기' 로 지으면 눈으로는 '포장하기' 를 보고 귀로는 'Take out' 을 듣게 됐었습니다. - 서버가 준 문장(추천 이유·제외 사유). 그대로 인용한다는 원칙을 지킵니다. - 메뉴 이름. 키오스크 화면의 글자와 같아야 합니다. - 저장값. selections 는 계속 우리말이고 canonical.ts 가 그것을 enum 으로 옮깁니다. 언어 목록을 넷에서 둘로 줄였습니다. 옮길 준비가 된 언어만 올립니다 — 고를 수는 있는데 화면은 한국어인 언어를 두는 것이 가장 나쁩니다. ## 소리 안내가 화면에 있는 것을 다 읽습니다 제목 한 줄만 읽고 있었습니다. 정작 알아야 할 것 — 왜 이 메뉴를 골랐는지, 무엇이 왜 빠졌는지, 연결이 어디까지 갔는지 — 은 안 읽었습니다. ① 화면이 바뀌면 앞의 말을 끊고 처음부터 다 읽는다 ② 같은 화면에 뭔가 새로 뜨면 새로 뜬 줄만 뒤에 붙여 읽는다 ② 가 핵심입니다. 제외 사유는 화면을 바꾸지 않고 나중에 도착합니다. 전체를 다시 읽으면 방금 들은 말을 또 듣고, 앞의 말을 끊으면 문장 하나를 통째로 잃습니다. 겹 아래(inert)는 안 읽고, 적는 동안 바뀌는 글과 1초마다 바뀌는 남은 시간은 바뀔 때 안 읽습니다(data-소리조용). 목소리 언어도 화면 언어를 따라갑니다. ## 가격 한도를 주문표 만들 때 직접 적습니다 주문 직전에 셋 중 하나를 고르게 했습니다. 그때는 이미 키오스크 앞이라 조건을 되짚는 자리가 아니고, 1,951원처럼 자기 사정에 맞는 값이 있는 사람에게 셋은 남의 금액입니다. 맵기·형태를 고르는 자리에서 같이 적습니다. 담기는 곳은 그대로 이번 이용입니다 — 주문표에 넣으면 지난주 한도가 오늘 주문에서 말없이 후보를 자릅니다. ## 저장한 주문표를 고칠 수 있습니다 지우고 처음부터 다시 적는 것뿐이었습니다. 맵기만 바꾸려 해도 이름·장소·메모를 전부 다시 적는 일입니다. #96 리뷰에서도 나왔던 자리입니다. 카드의 '고치기' 로 열고, 저장하면 같은 id 로 그 자리에 덮습니다. 목에 등록해 둔 사본도 같이 갈아 줍니다 — 안 갈면 조건은 고쳤는데 추천이 옛 조건으로 돕니다. ## 그 밖에 - 첫 화면에서 동의 안내가 소개 문장을 18px 덮던 것을 고쳤습니다. - 메뉴 이름·메모 라벨을 칸에 묶었습니다(label htmlFor). 글자를 눌러도 커서가 갑니다. - 폼 참고 자료를 docs/PROFILE_FORM_REFERENCE.md 에 남겼습니다. backend/ 는 건드리지 않았습니다. tsc 0 · 테스트 300 통과 · 빌드 성공(--mode team) * [FIX] 낡은 작업 트리를 옮겼습니다. 개인 master 기준으로 다시 맞춥니다 이 PR 을 처음 만들 때 개인 레포의 origin/master 가 아니라 그때 체크아웃돼 있던 작업 트리(feat/edit-sheet)를 복사했습니다. 그 브랜치는 #34 직후에서 갈라져 나온 것이라 **#35 와 #37 이 들어 있지 않았습니다.** 그래서 이미 고친 것들이 되돌아가 있었습니다. 코드래빗이 정확히 그 자리를 짚었습니다. 세션 복원 줄이 둘 → 하나로 (#96 · #37 에서 고친 것) 픽스처의 SKU-1 · x · y → 중립 이름으로 테스트 제목 "열 개" → "열한 개" 읽기() 주석 → 실제 동작대로 budget.ts 한도후보 주석 → 직접 적는 방식 설명으로 첫 화면 사진 minHeight 160 → 동의 안내가 소개 문장을 덮던 것(#35) apply.ts 되돌리기 원문 → '지금 지우는 값' 으로(#35) 개인 origin/master(1909692) 에서 다시 옮겼고, 옮긴 뒤 두 트리가 파일 단위로 완전히 같은지 확인했습니다. 복원줄 1 · SKU 0 · 제목 "열한 개뿐이다" · minHeight 160 있음 · apply.ts 쓴것 7곳 tsc 0 · 테스트 300 통과 · 빌드 성공(--mode team) * [FIX] 리뷰 반영 — 단위를 소리로 알리고, 남던 우리말 조각과 새는 참조를 고칩니다 낡은 트리를 되돌린 뒤에도 남은 다섯 건입니다. ## 가격 한도의 단위가 소리로 안 들렸습니다 칸 오른쪽의 "원" 은 장식이라 aria-hidden 입니다. 라벨은 "가격 한도 (선택)", placeholder 는 "예: 8000" 이라, 소리로만 듣는 사람은 이 칸이 원 단위인지 만 원 단위인지 알 수 없었습니다. 잘못 적으면 후보가 통째로 걸러지는 값입니다. 라벨에 sr-only 로 덧붙였습니다. 화면에는 1x1 로 숨고 소리로는 "가격 한도 (선택) 원 단위" 로 들립니다. 눈에 보이는 "원" 은 그대로 둡니다. ## 한 줄 안에 우리말 조각이 남았습니다 "키오스크가 한 일 3가지 · 2가지 실패 접기" 에서 앞은 옮겨지고 뒤 둘은 우리말로 남았습니다. "가지 실패" 는 표에 없었고 "접기" 도 없었습니다(“보기”만 있었습니다). 숫자가 끼는 쪽은 tf() 로 통째로 옮기고, "접기" 는 표에 넣었습니다. ## 되돌릴 자리를 적어 둔 Map 이 사라진 노드를 붙잡았습니다 열쇠가 DOM 노드인데 강한 참조라, React 가 걷어 낸 노드도 회수되지 않았습니다. 영어로바꾸기 는 화면이 다시 그려질 때마다 불리므로, 영어를 켜 둔 채 화면을 옮겨 다닐수록 쌓였습니다. 비우는 것은 한국어로 되돌릴 때뿐이라 그때까지 계속 늘었습니다. 바꾸기 전에 isConnected 로 걸러 냅니다. 매번 훑어도 쌉니다 — 지금 화면에 있는 만큼만 들어 있기 때문입니다. ## 메모에 쉼표가 있으면 그 뒤가 번역됐습니다 원문으로 지킬 말을 모으는 단위와 지키는 단위가 달랐습니다. 모으는 쪽은 요소 하나의 글자를 통째로 담는데, 라벨을 옮기는 쪽은 ", " 로 쪼개어 조각마다 표를 봅니다. 메모를 "얼음 적게, 포장하기" 로 적으면 통째로는 안 걸리고 '포장하기' 조각만 따로 걸려서, **사용자가 적은 말의 절반이 Take out 이 됐습니다.** 토막도 같이 지킵니다. ## vi-VN 회귀 검사 zh-CN 만 막고 있었습니다. 둘 다 목록에서 뺐으므로 둘 다 못 돌아와야 합니다. ## 확인 메모 "얼음 적게, 포장하기" · 영어 화면 보임 얼음 적게, 포장하기 들림 닭강정, Restaurant, Hot, 얼음 적게, 포장하기 ← 조각이 안 옮겨짐 가격 한도 라벨 보임 Price limit (optional) 단위 장식 KRW 들림 Price limit (optional) in won 숨김 1x1 · position absolute tsc 0 · 테스트 300 통과 · 빌드 성공(--mode team) * [FIX] recommendation v.5.1.6 수정사항 반영 (#88) * feat: Chickenstore 환경의 CompatibilityRule errorcode 사용자 친화 메시지 * feat: CandidateFilterResult requiresReconfirmation 필드 map으로 변경 * feat: Map 변경에 따른 서비스 수정 * feat: RuleEvaluator: 1단계 값 추출 로직 * feat: RecommendationEngineService STEP5~7 추천 로직 초기 구현 * fix: STEP2 파트 passesByCandidateId 추가 전달 * fix: SKIPPED, PASS 점수 오류 수정 * fix: WARN 없는 항목의 confidence 임계값 크로스 테스트 픽스처 보정 * fix: 뼈 타입 추천 점수에 추가 * fix: STAFF_ASSISTANCE_REASON 멘트 추가 * fix: 병합 오류 수정 * fix: v5.1.6 RC5 변경에 맞추어 뼈타입.컵옵션 추가 * feat: 혼잡 시간대(요일+시간대) 기반 포장 메뉴 가산점 추가 --------- Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> * [FEAT] 말로 주문표를 채우고, 화면과 읽어주기를 다듬습니다 (#99) * [FEAT] 말로 주문표를 채우고, 화면과 읽어주기를 다듬습니다 개인 레포에서 리뷰를 마치고 머지한 둘을 한 번에 올립니다(#39 · #41). ## 말로 주문표를 채웁니다 만들기 화면 맨 위에 '말로 채우기' 를 뒀습니다. "매운 닭강정 포장으로 두 개" 라고 말하면 아래 칸이 채워집니다. **모델이 값을 만들지 않습니다. 우리가 아는 값 중에서 고르기만 합니다.** 화면은 "당신이 고른 조건을 이렇게 썼습니다" 라고 말하는데, 말하지 않은 조건이 섞이면 그 말이 거짓이 됩니다. "치킨 먹고 싶어요" 아무 축도 안 고름 "혼자 먹을 거예요" 수량 안 고름 (둘이 하나를 나눌 수도 있다) "적당히 매콤하게" 맵기 안 고름 (순한맛일 수도 있다) "순한맛으로 할까 매운맛으로 할까" 맵기 안 고름 (아직 고르는 중이다) 알레르기는 말로 안 받습니다. 틀리면 사람이 다치는 값이라 눈으로 보고 고릅니다. 들은 말은 **어느 칸에도 저장하지 않습니다.** 자유 발화라 이름·전화번호가 섞일 수 있는데, 메뉴 이름 칸에는 메모와 달리 개인정보 검사가 없습니다. 확인 화면에 보여 주기만 하고 사라집니다. 소리는 기기 안에서 처리될 때만 듣습니다. processLocally 를 켜고, **그 스위치가 있는 브라우저에서만 단추를 내밉니다** — 없으면 소리가 어디로 가는지 우리가 알 수 없고, 개인정보 화면은 "이 기기 밖으로 나가지 않아요" 라고 말하고 있습니다. ## 화면에서 설명을 걷어냈습니다 도움 설정 화면의 두 문단, 가입 흐름의 두 줄을 뺐습니다. 동의 문구는 "개인정보 수집 동의서" 한 줄로 줄였습니다. 접근성 화면에서 가격 한도와 못 드시는 것을 뺐습니다. 계약에서 자리가 다릅니다 — 이 화면의 것들은 accessibility·interaction 이고 저 둘은 hardConstraints 입니다. ## 읽어주기 화면을 넘기면 즉시 멈춥니다. 예전에는 새 화면의 읽기가 시작될 때에야 끊겨서, 그 사이 지난 화면을 계속 읽었고 새 화면에 읽을 것이 없으면 아예 안 끊겼습니다. 읽을 값어치가 없는 것은 안 읽습니다(data-소리생략) — 앱 이름, 개발용 연동 막대, 화면마다 똑같은 하단 탭. 전 kio. bridge. 게스트로 이용 중. ... QR 찍기. 내 주문표. 계정 후 게스트로 이용 중. ... ## 이유가 무엇에 대한 얘기인지 밝힙니다 전 제외: 지금은 품절이라 제외됐어요 후 제외: 닭강정 — 지금은 품절이라 제외됐어요 서버 문장은 그대로 두고 앞뒤에 우리가 아는 사실만 덧댑니다. 축은 서버 원문에서 봅니다 — 이름을 먼저 붙이면 "종이컵 세트" 같은 이름 때문에 없는 축이 걸립니다. ## 영어 화면에 우리말이 없습니다 로고의 aria-label("키오브릿지")과 언어 항목("한국어")을 표에 넣었습니다. 주문표 카드의 사용자 입력은 그대로 둡니다 — data-원문 이 지킵니다. backend/ 는 건드리지 않았습니다. tsc 0 · 테스트 316 통과 · 빌드 성공(--mode team) * [FIX] 리뷰 반영 — 문맥 없는 말로 안 고르고, 입력 방식을 실제로 쓴 것에서 받습니다 네 건 모두 유효했습니다. ## 문맥 없는 별칭이 축을 골랐습니다 "I am here" 가 먹고 가기를, "regular chicken" 이 일반컵을 골랐습니다. 우리말도 같아서 "일반적으로" 가 일반컵에 걸렸습니다. **사용자가 말하지 않은 조건이 섞이는 자리**라 그냥 둘 수 없었습니다. 혼자서는 다른 뜻이 되는 별칭을 뺐습니다 — here · paper · regular · 일반 · 종이. 축을 같이 말한 것만 받습니다("regular cup", "종이컵"). ## "적당히 매콤하게" 를 못 들었다고 했습니다 값을 못 고르면 전부 '못 들었어요' 로 갔습니다. 맵기를 말하기는 한 사람에게 못 들었다고 하면, 다시 또박또박 말해 보다가 같은 답을 받습니다. 축 얘기를 했는지 따로 보고 모호한축으로 넣습니다. 화면은 "말씀은 들었는데 어느 쪽인지 못 골랐어요" 라고 말합니다. ## preferredInput 을 voiceGuide 에서 끌어 왔습니다 voiceGuide 는 **읽어 주는** 설정이고 preferredInput 은 **넣는** 방식입니다. 둘은 자주 어긋납니다. 말로 채웠는데 읽어 주기를 껐다 → TOUCH 로 나갔다 손으로 골랐는데 읽어 주기를 켰다 → VOICE 로 나갔다 키오스크는 이 값을 보고 안내 방식을 정하므로 틀리면 그쪽이 잘못 준비합니다. api/inputsource.ts 를 두고 '말로 채운 적이 있나' 를 따로 담습니다. 확인 화면에서 실제로 채웠을 때만 켜지고, 손으로 몇 칸 고쳐도 그대로입니다 — 몇 칸 고쳤다고 "말로 넣는 사람" 이 아니게 되지는 않습니다. 새로고침을 넘어 남고(session.ts의 voiceUsed), 로그아웃과 기기 지우기에서 비웁니다. 들은 말 자체는 여전히 어디에도 안 담습니다. 남는 것은 무엇으로 채웠나 하는 사실 하나뿐입니다. ## 문서가 제품보다 느슨했습니다 명세에 "새우는 빼 주세요 는 받는다" 고 적혀 있었는데, 실제로 만든 것은 말로는 아예 안 받는 쪽입니다. 문서가 느슨한 채로 남으면 나중에 그것을 보고 만드는 사람이 느슨한 쪽으로 되돌립니다. 제품에 맞췄습니다. 확인 화면 예시도 지금 화면 문구로 바꾸고, 조사를 자리표시자로 붙이지 않는 이유를 적었습니다. tsc 0 · 테스트 325 통과(음성 21개) · 빌드 성공(--mode team) * [FIX] 리뷰 반영 — 지원하지 않는 이용 방식도 "못 골랐어요" 로 알립니다 ## "배달로 할게요" 를 못 들었다고 했습니다 이용 방식을 말했는데 우리 목록(먹고 가기·포장하기)에 없는 값입니다. 지금은 못들은축으로 가서 화면이 "못 들었어요" 라고 했습니다. 말한 사람은 다시 또박또박 말해 보다가 같은 답을 받습니다. 값으로 등록할 수는 없습니다 — 없는 것을 고를 수는 없습니다. 축을 말한 것으로 보고 "말씀은 들었는데 어느 쪽인지 못 골랐어요" 라고 답합니다. 영어(deliver· delivery)도 같이 넣었습니다. ## 문서 코드 블록에 언어를 붙였습니다 확인 화면 예시 블록입니다. tsc 0 · 테스트 327 통과(음성 23개) · 빌드 성공(--mode team) * [FEAT] 로그인 토큰을 붙이고, 한 개 값 한도를 뜻대로 부르고 합계를 알려 줍니다 (#100) * [FEAT] 로그인 토큰을 붙이고, 한 개 값 한도를 뜻대로 부르고 합계를 알려 줍니다 개인 레포에서 리뷰를 마치고 머지한 셋을 한 번에 올립니다(#42 · #43 · #44). ## 로그인 토큰 (팀 요청) 백엔드가 로그인·회원가입 응답에 accessToken 을 주기 시작했고, 주문표 조회·저장· 삭제가 Authorization: Bearer 를 요구합니다. 토큰은 api/token.ts 에만 담고 **sessionStorage 에는 안 남깁니다.** 값 하나로 남의 주문표를 읽고 지울 수 있는데, 적어 두면 개발자 도구를 열 수 있는 사람이 그대로 읽습니다. 비밀번호를 안 남기는 것과 같은 판단입니다. Account 객체에도 안 싣습니다. 그건 session.ts 가 적어 두는 값이라, 넣으면 토큰이 따라 나갑니다. BFF 는 헤더를 통째로 넘기지 않습니다. Authorization 한 칸만 고르고 Bearer 모양인지 보고 길이도 자릅니다 — 헤더 하나로 백엔드에 큰 값을 밀어 넣는 길을 막습니다. 허용경로 주석의 "백엔드가 토큰을 발급하지 않는다" 도 고쳤습니다. **알아 두실 것**: 토큰을 안 적어 두므로 새로고침하면 사라집니다. 계정은 남아서 화면은 로그인한 것처럼 보이는데 주문표 요청은 401 을 받습니다. "다시 로그인해 주세요" 를 띄우는 자리는 아직 없습니다. ## 첫 화면에서 사진을 뺐습니다 사진 칸만 position: relative 라 나중에 그려졌고, 가운데 칸이 marginTop -12 로 겹치는 12px 만큼 손 그림 윗부분이 그라데이션 띠에 덮여 **손가락이 잘려** 보였습니다. 사진을 빼면서 끌어올리던 것도 같이 없앴습니다. ## 가격 한도를 뜻 그대로 부릅니다 8,000원을 적고 8,000원짜리를 두 개 담았는데 안 걸러진다는 얘기가 있었습니다. 킷 규칙(CHICKEN_PRICE_LIMIT)이 후보의 단가에 MAX 를 겁니다 — quantity 는 그 비교에 안 들어갑니다. 서버가 틀린 것이 아니라 화면이 그 값을 잘못 부르고 있었습니다. 가격 한도 (선택) → 한 개 값 한도 (선택) {금액}보다 비싼… → 한 개에 {금액}보다 비싼… ## 합계가 넘으면 확인 화면에서 알려 줍니다 한 개 값은 한도 안이지만, 2개면 16,000원이에요. **막지는 않습니다.** 서버는 통과시키므로 여기서 막으면 서버는 되는데 앱만 안 되는 상태가 되고, 사용자는 왜 막혔는지 알 수 없습니다. 확인 카드로 오던 값이 priceText("8,000원") 라는 글자뿐이라 수량과 곱할 수 없었습니다. display 에 숫자 price 를 더했습니다 — 글자에서 숫자를 도로 뽑으면 통화 표기가 바뀌는 날 조용히 틀립니다. backend/ 는 건드리지 않았습니다. tsc 0 · 테스트 332 통과 · 빌드 성공(--mode team) * [FIX] 리뷰 반영 — 401 을 안내하고, 합계 알림을 모든 승인 화면에 답니다 ## 401 을 받으면 토큰을 버리고 안내합니다 토큰을 메모리에만 두므로 새로고침 한 번이면 사라지는데 계정은 남습니다. 화면은 로그인한 것처럼 보이는데 요청마다 401 이 오고, 버려 두지 않으면 안 되는 토큰을 계속 붙여 보냅니다. 401 에서 토큰을 비우고 "로그인이 풀렸어요. 다시 로그인해 주세요" 로 안내합니다. 사용자 잘못이 아니라 우리가 안 적어 두기로 한 결과라, 무엇을 하면 되는지 말해 줍니다. 회귀 테스트를 붙였습니다. ## 합계 알림이 한 화면에만 있었습니다 승인 버튼이 있는 화면은 셋입니다 — exact · changed · low_confidence. 셋 다 MappedItem 을 그리고 셋 다 담깁니다. 한 곳에만 달아서 나머지 둘에서는 합계가 한도를 넘어도 아무 말 없이 담겼습니다. 단가를 result 와 상관없이 읽고, 세 화면 모두에 같은 안내를 답니다. ## price 를 쓸 수 있는 값일 때만 싣습니다 NaN·Infinity 가 들어오면 곱한 결과도 그것들이라 "NaN원이에요" 같은 문장이 화면에 나갑니다. 음수도 값이 될 수 없습니다. 없는 것으로 두면 화면이 안내를 안 띄웁니다. ## 영어 문구가 경계값을 빠뜨렸습니다 단가가 한도와 같아도 이 안내가 뜹니다(합계만 넘으면 뜹니다). "under" 는 미만이라 그 경우를 빠뜨립니다 — "within" 으로 고쳤습니다. ## 안 고친 것 — 후보 ID display 가 실제 후보 ID 를 열쇠로 쓴다는 지적입니다. **이번 변경이 만든 것이 아니고**, 화면으로 나가는 값은 이미 c1·c2 로 바꿔 보냅니다(backend.ts:407). display 는 backend.ts 안에서만 쓰는 조립용 표입니다. 지적이 요구하는 해결(서버가 세션 범위의 불투명 토큰을 발급)은 백엔드 계약 변경이라 이 PR 에서 할 수 없습니다. 프론트에서 열쇠만 바꿔 가리는 것은 지적한 분 말대로 해결이 아닙니다. tsc 0 · 테스트 333 통과 · 빌드 성공(--mode team) * [FIX] 버린 점수 셋을 마저 보여 주고, 토큰 만료를 미리 봅니다 팀원이 origin/dev 를 대조해 알려 준 두 가지입니다. **서버가 매긴 점수 여섯 중 셋만 화면에 나왔습니다.** 점수축 표에 이용 방식· 맵기·가격만 적혀 있어서 형태·컵·혼잡 시간대가 조용히 버려졌습니다. 추천 순위에는 이미 반영된 값인데 "이걸 보고 골랐어요" 목록에서만 빠져서, 뼈 없는 것과 종이컵을 고른 사람은 자기가 고른 조건이 계산에 안 들어간 줄 압니다. 셋을 마저 적었습니다. 혼잡 시간대는 사용자가 고른 값이 아니라 가게 사정이라 그렇게 이름 붙였습니다. **토큰 만료(expiresAt)를 안 보고 있었습니다.** 지난 토큰을 붙여 보내면 서버가 401 로 답하고, 사용자는 한 번 실패한 뒤에야 다시 로그인하라는 말을 듣습니다. 서버가 주는 만료 시각을 토큰과 함께 담아 두고, 보내기 전에 지났으면 없는 것으로 봅니다. 만료를 못 읽으면(형식이 다르거나 안 주면) 검사를 건너뜁니다 — 잘못 읽은 시각으로 멀쩡한 토큰을 버리는 것보다 낫습니다. 토큰은 여전히 Account 에 안 넣습니다. Account 는 session.ts 가 sessionStorage 에 적는 값이고, 토큰은 적어 두는 순간 개발자 도구로 읽힙니다(token.ts). 336개 테스트 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 토큰을 요청당 한 번만 읽어 `Bearer null` 을 막습니다 리뷰 지적입니다. 제가 만든 문제였습니다. 만료를 보게 하면서 `읽기()` 가 값을 돌려주기만 하는 함수가 아니게 됐습니다 — 지난 토큰을 만나면 지웁니다. 그런데 부르는 쪽은 예전 그대로 조건에서 한 번, 헤더에서 한 번, 두 번 불렀습니다. 두 호출 사이에 만료가 걸리면 조건은 통과하고 헤더에는 `Bearer null` 이 실려 나갑니다. 서버가 그걸 어떻게 읽을지는 알 수 없습니다. 값을 한 번 받아 두고 조건과 헤더가 같은 것을 보게 했습니다. BFF 도 같은 모양이라 함께 고쳤습니다. 거긴 요청 헤더만 보는 순수 함수라 지금은 버그가 아니지만, `!` 로 단언하고 있어서 나중에 상태를 보게 되는 날 어긋나도 타입 검사가 안 잡습니다. 337개 테스트 통과 (만료 직전 값이 헤더까지 그대로 가는지 확인하는 것 하나 추가). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [TEST] 테스트가 실제 로그인 계약을 확인하게 합니다 (#103) 팀원 지적입니다. 계정 테스트가 서버는 `{userId, loginId}` 만 준다고 가정하고 쓰여 있었습니다. 실제 백엔드는 `accessToken` 을 함께 주고 주문표 경로는 그 토큰을 요구합니다. 예전 모양으로만 확인하면 **CI 는 초록인데 실제 연동은 깨집니다.** 토큰 붙이는 코드는 #100 에서 들어갔지만, 그게 계속 그대로인지 지켜보는 것이 없었습니다. 다섯 가지를 확인합니다. - 로그인 응답의 accessToken 을 붙든다 - 가입 응답의 accessToken 도 붙든다 - 주문표 요청에 `Authorization: Bearer` 가 붙는다 - 로그인 전에는 안 붙인다 (빈 값을 Bearer 뒤에 붙이면 서버가 어떻게 읽을지 모른다) - 401 을 받으면 토큰을 버린다 (안 버리면 다음 요청도 같은 401 을 받는다) 화면으로는 계정만 간다는 것도 함께 확인합니다. 토큰이 Account 에 섞이면 session.ts 가 sessionStorage 에 적고, 개발자 도구를 열 수 있는 사람이 그대로 읽습니다(token.ts). 342개 테스트 통과 · 타입 통과. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FIX] 시나리오에 쓰는 것만 남기고, 영어와 소리 안내를 손봅니다 (#101) * [FIX] 시나리오에 쓰는 것만 남기고, 이름 없이도 주문표를 저장합니다 개인 레포 #46 에 있던 것을 팀으로 옮깁니다. 이제 팀 레포에만 올리기로 해서, 개인에만 남아 있으면 대회에 나가는 코드에는 없는 것이 됩니다. **가게 이름을 키트에서 읽습니다.** 화면에 적어 둔 이름과 manifest.json 의 displayName 이 달랐습니다. 사용자가 보는 이름과 실제로 조작하는 키오스크가 다르면 "이 가게가 맞나" 를 확인할 방법이 없어집니다. 키트 값(닭강정 가게)으로 맞췄습니다. **고를 수 있는 장소를 음식점 하나로 줄였습니다.** 카페·관공서·병원을 고르면 뒤에 붙는 것이 없어서, 고르고 나서야 아무 일도 안 일어난다는 걸 알게 됩니다. 고를 수 없는 것을 안 보여 주는 편이 낫습니다. PlaceType 자체는 그대로 둡니다 — 목데이터와 테스트가 쓰고 있고, 타입을 줄이면 이번 시나리오 밖의 것까지 지웁니다. **'그림 안내' 와 '소리 대신 화면' 을 뺐습니다.** 이번 키오스크가 둘 다 안 합니다. 켤 수 있게 두면 켠 사람은 뭔가 달라질 거라고 믿는데 화면은 그대로입니다. 키트가 요구하는 일곱 항목은 payload 에 그대로 실립니다 — 화면에서만 안 물어봅니다. **메뉴 이름 없이도 저장됩니다.** 이름은 나중에 알아볼 표시일 뿐인데 없으면 저장이 막혀서, 말로 채운 사람은 다 골라 놓고도 못 넘어갔습니다. 타입 통과 · 336개 테스트 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 영어로 바꾸면 추천 이유도 영어로 나옵니다 영어로 바꿔도 이 줄만 우리말로 남았습니다. "순살 닭강정 — 선호하신 맵기와 맞는 메뉴라 (고르신 값: 매운맛)" 처럼요. 영어를 쓰는 사람은 왜 이게 담겼는지 못 읽으면서 승인 버튼을 눌러야 했습니다. 화면 글은 DOM 을 훑으며 통째로 옮기는데(i18n/apply.ts), 이 줄만은 매번 메뉴 이름과 고른 값이 달라서 표의 열쇠와 같아지는 일이 없습니다. **완성문을 되쪼개는 대신 조각을 그대로 들고 옵니다.** 되쪼개면 메뉴 이름에 구분자가 들어 있는 날 조용히 틀립니다. 세 조각은 성격이 다 다릅니다. 메뉴 안 옮깁니다. 가게가 붙인 이름이라 옮기면 영어를 쓰는 사람이 키오스크 화면에서 그 이름을 못 찾습니다 — 눌러야 하는 버튼에 적힌 글자는 우리말입니다. 문장 서버가 만든 말. 본 적 있는 것만 옮깁니다. 고른값 우리가 아는 말이라 그냥 옮깁니다. **en.ts 에 적어 둔 원칙을 하나 바꿨습니다.** "서버가 준 문장은 넣지 않는다 — 그대로 인용한다" 였는데, 인용을 지키느라 영어를 쓰는 사람이 못 읽는 줄을 남긴 셈이었습니다. 인용을 버리지 않는 선에서 넣습니다: 본 적 있는 문장만 옮기고, 모르는 문장은 서버가 준 그대로 우리말로 나갑니다. 지어낸 영어보다 낫고, 무엇이 안 옮겨졌는지 화면에서 바로 보입니다. 서버가 영어로 줄 수 있게 되면 이 칸은 지웁니다. 조각 없이 만들어진 이유(목데이터·예전 코드)는 완성문을 그대로 씁니다 — 오늘과 같게 보입니다. 343개 테스트 통과 (영어일 때 이 줄에 우리말이 안 남는지 보는 것 6개 추가). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 첫 화면에서 소리 안내를 켤 수 있습니다 소리 안내 스위치가 도움 설정 화면에만 있었습니다. 그런데 그 화면까지 가려면 첫 화면을 읽고 눌러야 합니다 — **읽어 줘야 읽을 수 있는 사람은 켜러 갈 수가 없었습니다.** 이 스위치가 필요한 사람이 정확히 이 스위치에 못 닿았습니다. 첫 화면에 답니다. 동의 문구보다 위입니다 — 아래 두면 동의문을 먼저 읽어야 하는데 그게 바로 못 읽는 그 글입니다. 도움 설정 화면과 같은 ToggleRow 를 씁니다. role="switch" 와 aria-checked 가 함께 따라오고, 두 자리의 생김새와 누르는 크기가 같습니다. 소리를 못 내는 기기에서는 안 보입니다(소리를낼수있나). 켜도 아무 일이 없는 스위치를 두면 켠 사람은 켜졌다고 믿습니다. 브라우저에서 확인했습니다. 켜는 즉시 첫 화면을 읽고("키오스크 앞에서 헤매지 않도록…"), 앱 이름은 건너뜁니다. 다시 누르면 꺼집니다. 343개 테스트 통과 · 타입 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 어느 가게에 연결됐는지 한 문장으로 말합니다 "연결되었습니다" 아래에 가게 이름을 따로 떼어 뒀습니다. 소리로 듣는 사람은 "연결되었습니다" 만 듣고 무엇에 연결됐는지는 못 들은 채 넘어갑니다. 한 문장으로 합칩니다 — "닭강정 가게 키오스크에 연결되었습니다". 이름은 키트가 준 값입니다(manifest.json 의 displayName). 옮기지 않습니다 — 키오스크 화면에 적힌 글자와 같아야 맞는 가게인지 확인할 수 있습니다. '오늘의 메뉴와 맞춰보는 중' 이 영어 표에 없어서 영어로 바꿔도 우리말로 남았습니다. 넣었습니다. 343개 테스트 통과 · 타입 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 화면에서 뺀 도움 항목이 서버로 나가지 않게 하고, 게스트도 바로 만들게 합니다 리뷰 지적입니다. 제가 만든 문제였습니다. '그림 안내' 와 '소리 대신 화면' 을 화면에서만 뺐습니다. 그런데 그 전에 켜 둔 사람의 저장소에는 true 가 남아 있고, 새로고침하면 그대로 되살아나 계약 payload 에 실려 나갑니다. **화면 어디에도 없는 값이라 끌 방법이 없습니다.** 사용자가 고르지도 않은 조건을 우리가 대신 말하는 셈입니다. 되살릴 때 그 두 칸을 false 로 둡니다. 계약의 일곱 칸은 그대로 나갑니다 — 안 묻는 칸이 false 인 것이지 칸이 사라지는 게 아닙니다. 다시 묻게 되면 목록에서 빼면 됩니다. **게스트도 첫 화면에서 바로 주문표를 만들게 합니다.** 예전에는 빈 목록으로 보냈습니다. "저장된 주문표가 없어요 / 새 주문표를 추가해보세요" 만 있는 화면인데, 여기서 무엇을 해야 하는지 알아채고 단추를 한 번 더 누르는 일이 이 앱을 쓸 사람에게 쉽지 않습니다. 로그인하는 쪽은 이미 곧바로 물어봅니다 — 가입 없이 들어온 사람만 빈 목록 앞에 세워 둘 이유가 없습니다. 뒤로 가면 목록으로 나갑니다. 343개 테스트 통과 (되살릴 때 두 칸이 꺼지고 아직 묻는 칸은 살아나는지 확인하는 것 하나 추가) · 타입 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 제외 사유에 규칙 식별자가 뜨지 않게 합니다 리뷰 지적입니다. 제외 사유를 explanation 에서 읽고 있었습니다. 그 자리는 규칙 추적용이라 "ruleId=..., sourceValue=[PEANUT]" 같은 값이 들어옵니다. 사람이 읽는 문장은 reasonText 에 따로 오고, 같은 파일의 사유문장() 이 이미 그 자리를 알고 있었는데 여기만 explanation 을 읽었습니다. 다만 두 자리를 다 쓰는 응답이 있습니다 — 어떤 경로는 explanation 에 사람이 읽는 문장을 담아 줍니다(테스트 fixture 가 그렇습니다). reasonText 를 먼저 보고 없을 때만 물러납니다. 규칙 추적 문자열로 보이면 안 씁니다. 둘 다 비면 그 줄을 아예 안 만듭니다. 빈 문장에 메뉴 이름만 붙이면 "순살 닭강정 — " 이 되고, 이유를 말한다면서 아무 이유도 안 적는 셈입니다. 344개 테스트 통과 · 타입 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 메뉴 이름에도 개인정보 검사를 겁니다 * [FIX] 메뉴 이름에도 개인정보 검사를 걸고, 이유에 고른 값을 짐작해 붙이지 않습니다 * [FIX] 도움 항목을 뺀 근거를 킷에서 확인한 사실로 고쳐 적습니다 * [FIX] 공백뿐인 reasonText 때문에 제외 이유가 사라지지 않게 합니다 리뷰 지적입니다. 바로 앞 커밋에서 제가 만든 것입니다. `(reasonText || explanation).trim()` 순서로 골랐습니다. 공백뿐인 reasonText 는 truthy 라 explanation 을 아예 안 보고, 그 뒤 trim 으로 빈 문자열이 됩니다. 사람이 읽을 수 있는 사유가 explanation 에 있는데도 제외 이유가 통째로 사라집니다. 각각 다듬고 나서 고릅니다. 346개 테스트 통과 · 타입 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 꼭 골라야 하는 것이 비면 미리 알리고, 메뉴 이름 검사의 한계를 잠가 둡니다 * [FIX] 백엔드가 실제로 보내는 제외 사유가 영어로도 나옵니다 * [FIX] 뺀 메뉴의 이름을 문장에서 분리해 영어로도 이유가 나오게 합니다 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FIX] 말하기가 조용히 실패하지 않게 합니다 (#102) * [FIX] 말하기가 조용히 실패하지 않게 합니다 말하기 단추를 눌러도 아무 일이 안 일어났습니다. 제가 만든 문제입니다. #39 에서 "음성이 기기 밖으로 안 나간다" 는 잘못된 주장을 고치면서 `processLocally = true` 를 켰습니다. 그런데 그걸 켜면 **기기 안 음성 모델이 깔려 있어야** 듣기가 시작됩니다. 코드는 모델이 있는지 안 보고, 없을 때 받아 두지도 않았습니다. 단추는 보였습니다 — `processLocally` 라는 속성이 있기만 하면 보여 주게 해 놨으니까요. 속성이 있다는 것과 모델이 있다는 것은 다른 얘기였습니다. 그래서 누르면 오류가 나고, 그 오류가 `"안됨"` 하나로 뭉뚱그려져 화면에는 "지금은 말로 채울 수 없어요" 만 떴습니다. 무엇을 하면 되는지 알 수 없었습니다. **누르기 전에 모델을 봅니다.** `available()` 로 묻고, 받아야 하면 그 자리에서 받습니다 — 단추를 누른 자리라 받을 수 있습니다. 브라우저는 사람이 누르지 않으면 받기를 시작하지 않아서, 나중에 조용히 받아 두려 하면 그때는 못 받습니다. **받는 동안을 화면에 둡니다.** 처음 한 번만 지나가는 자리인데, 없으면 누르고 한참 아무 일도 안 일어나는 것처럼 보입니다. **모델이 없어서 못 하는 것과 아예 안 되는 것을 갈라 놓습니다.** 할 일이 달라서 같은 말로 뭉뚱그리면 안 됩니다. `available` 을 안 주는 브라우저에서는 막지 않습니다. 물어볼 방법이 없는데 미리 막으면 되는 브라우저에서도 단추가 죽습니다 — 안 되면 start() 가 오류로 알려 주고, 그 오류를 이유별로 옮깁니다. 337개 테스트 통과 · 타입 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 화면을 떠나거나 장소를 바꾸면 준비 중이던 듣기도 버립니다 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * [FEAT] 비로그인 주문표를 묻고 남기고, 말로 채우기를 한 칸씩 묻습니다 (#104) * [FIX] 확인 카드에서 사진을 뺍니다 메뉴 사진을 확인 카드와 후보 줄에서 뺍니다. 사진은 alt="" aria-hidden 이라 소리로 듣는 사람에게는 처음부터 없는 것이었고, 눈으로 보는 사람에게도 확인에 보태는 것이 없었습니다. 확인 카드가 갖춰야 하는 다섯 가지(상품명·포장·매운맛·종이컵·가격)는 다 글자입니다. 서버 후보에는 사진 필드가 아예 없어서(KitCandidate) 실서버에서는 늘 빈 자리였고, 자리만 잡고 있었습니다. 337개 테스트 통과 · 타입 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FEAT] 비로그인이면 주문표를 남길지 묻고, 기본은 안 남깁니다 로그인하지 않고 만든 주문표가 묻지도 않고 이 기기에 남았습니다. 가입도 로그인도 없이 들어온 사람은 자기 정보가 어디에 남는지 확인할 방법이 없습니다. 묻지 않고 남기면 안 됩니다. 만들고 나면 '저장할까요?' 를 묻습니다. **기본값은 안 남기는 쪽입니다** — 두 단추 중 무엇을 크게 두느냐가 곧 기본값이라 '이번만 쓰기' 를 대표 단추로 뒀습니다. 되돌릴 수 있는 쪽을 기본으로 둔다는 뜻이기도 합니다. 안 남긴 것은 다시 만들면 되지만, 남긴 것은 남았다는 사실 자체를 모를 수 있습니다. '이번만 쓰기' 로 만든 주문표는 화면 목록에는 그대로 있어서 만들고 바로 주문하는 길은 똑같이 동작합니다. 다만 새로고침을 넘기지 않고(session.ts), 서버에도 안 올립니다 — 이 기기에도 안 남기겠다고 한 것을 서버에 두면 그 선택이 거짓이 됩니다. 로그인한 사람에게는 안 묻습니다. 자기 계정에 저장하는 것이 이미 뜻이 통하고, 지우고 싶으면 목록에서 지우면 됩니다. 고치는 중일 때도 안 묻습니다 — 이미 목록에 있는 것을 고치는 길이라, 여기서 '이번만 쓰기' 를 고르면 있던 주문표가 조용히 사라지는 것처럼 보입니다. 브라우저에서 양쪽 다 확인했습니다. '이번만 쓰기' 는 목록에 뜨고 저장소는 비어 있고 새로고침하면 사라집니다. '이 기기에 저장하기' 는 저장소에 남습니다. 340개 테스트 통과 · 타입 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FEAT] 말로 채우기를 한 칸씩 묻는 방식으로 바꿉니다 예전에는 한 번에 다 말하게 했습니다("포장이고 매운맛으로 두 개"). 짧은 문장으로 여러 축을 한꺼번에 말하는 건 익숙한 사람에게나 쉬운 일입니다. 무엇을 말해야 하는지 모르는 채로 마이크가 켜지면 대부분 아무 말도 못 합니다. 한 칸씩 묻습니다. 답할 것이 하나뿐이라 말이 짧아집니다. 답하는 길도 둘로 엽니다. 보기 이름을 그대로 "매운맛" 예 / 아니오 "매운맛 — 이것으로 할까요?" → "네" 둘째 길이 필요한 이유 — 물음표로 끝나는 말을 들으면 사람은 네/아니오로 답합니다. 이름을 다시 말하게 하면 물어 놓고 안 듣는 셈이 됩니다. 보기가 셋 이상일 때 '아니오' 로는 아무것도 안 고릅니다. 아니라는 말만으로는 무엇을 고를지 알 수 없고, 짐작해서 넣으면 안 고른 것이 골라집니다. 그때는 다음 칸으로 넘어갑니다. '네' 는 한 글자라 다른 말에 묻어 들어갑니다("보통맛이네"). 낱말로 있을 때만 봅니다. '아니오' 를 먼저 봅니다 — "네 아니오" 처럼 둘 다 들어가면 고쳐 말한 것입니다. 손으로 고르는 길과 건너뛰기를 늘 함께 둡니다. 말이 안 되는 자리에서 갇히면 안 됩니다. 고른 값만 넣습니다. 들은 말은 어디에도 저장하지 않습니다 — 목록에 있는 값만 들어갑니다. 브라우저에서 확인했습니다. 다섯 칸을 차례로 묻고, 고르면 다음 칸으로 넘어가고, 고른 값이 아래 세부 옵션에 그대로 반영됩니다. 확인 중에 조사가 틀린 것을 찾았습니다("순한맛로") — 받침에 따라 달라지는 자리라 조사가 필요 없는 문장으로 바꿨습니다. 340개 테스트 통과 · 타입 통과 · 팀 모드 빌드 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [FIX] 애매한 답을 예로 읽지 않고, 고쳐도 이번만 쓰기를 지킵니다 * [FIX] 손으로 고르거나 건너뛰면 듣던 것을 끊습니다 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: ParkSeYoung128 <tpdudtpdud128@gmail.com> Co-authored-by: parkseyoung <parkseyoung@users.noreply.github.com> Co-authored-by: cha-hyunwoo <ckgusdn10@gmail.com> Co-authored-by: kjp0411 <98109773+kjp0411@users.noreply.github.com> Co-authored-by: Gganii <rkdms5991@naver.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📌 작업 내용 및 특이사항
RecommendationValidator (STEP5 출력 자체 검증):
ValidationIssue(code/message/path),ValidationOutcome(valid/issues) 타입 추가 + recommendedCandidateId·alternativeCandidateIds·excludedCandidates·recommendationReasons·confidence 정합성 검증 로직 구현RecommendationController:
/api/v1/candidate-filters,/api/v1/recommendations,/api/v1/recommendation-output-validations3개 API 구현 (요청/응답 DTO 4개 포함)candidateFilterResult를 클라이언트가 보낸 그대로 믿고, environmentId도 안 쓰여서 다른 환경 결과를 재사용하는 것도 막을 방법이 없었다.→ 서버가 environmentId+sessionContext로 직접 다시 계산하도록 고침 fix: recommendations API가 클라이언트의 candidateFilterResult를 신뢰하지 않고 서버에서 재계산하도록 수정
fix: RecommendationValidator가 제외된 추천 후보·중복 대안 후보를 검증하지 못하던 문제 수정
📚 기타
Summary by CodeRabbit