[FEAT] 서버 주문표 삭제와 FE↔BE 계약 점검 넷을 반영합니다 - #94
Conversation
개인 레포에서 리뷰를 마치고 머지한 것 셋을 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
|
Warning Review limit reached
Next review available in: 13 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughBFF가 프로필 단건 삭제를 허용합니다. Changes프로필 및 주문표 삭제
서버 규칙 판정 처리
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant App
participant AccountApi
participant BFF
participant Backend
App->>AccountApi: deleteSheet(userId, profileId)
AccountApi->>BFF: DELETE 프로필 요청
BFF->>Backend: 허용된 단건 삭제 요청
Backend-->>AccountApi: 삭제 결과
AccountApi-->>App: 완료 또는 오류 표시
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 |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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 `@frontend/api/bff.ts`:
- Around line 98-122: Update the DELETE handling around 메서드가되나 and the
forwarding flow to derive the caller’s user ID from the authenticated session,
then compare it with the user ID captured from the requested profile path before
forwarding. Reject mismatches and unauthenticated DELETE requests, while
preserving the existing route and method allowlist for authorized matching
requests.
In `@frontend/src/api/backend.ts`:
- Around line 1318-1322: 규칙판정들 저장 로직에서 reconfirmationsByCandidateId도 서버 판정 여부에
포함하고 별도 재확인 상태로 저장하세요. 이후 축맞춤()과 화면 응답이 RECONFIRM 결과를 확인표()의 로컬 추정으로 대체하지 않고 이
서버 상태를 전달하도록 수정해, 승인 전에 재확인 및 미충족 조건이 사용자에게 표시되게 하세요.
In `@frontend/src/app/App.tsx`:
- Around line 1269-1289: Update the conditional notice around showOrder, 고른것,
and 백엔드가아는장소 so place === null displays “장소 유형을 고른 뒤 주문할 수 있어요” instead of the
unsupported-place message. Keep the existing “아직 키오스크와 연결되지 않았어요” 안내 only when a
concrete place was selected and 백엔드가아는장소 returns false.
- Around line 4040-4052: 정보 삭제 흐름에서 현재 sheets만 사용하지 말고, 서버 목록 조회가 완료된 뒤
account.listSheets(userId)의 ID와 현재 sheets의 ID를 합쳐 서버 삭제 대상으로 구성하세요. 목록 조회가 실패하면
서버 삭제 완료로 처리하거나 안내하지 말고 사용자가 재시도할 수 있는 경로를 제공하며, 기존 로컬 데이터 삭제 동작은 유지하세요.
- Around line 3693-3706: Update the individual order-sheet deletion flow,
including its confirmation handler and existing deleteSheet path, so that when
the user is logged in it also calls account.deleteSheet for the removed sheet.
Reuse 서버주문표지우기 or its established error/retry behavior, while preserving local
deletion and allowing the same server-delete path for locally-only sheets.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: 902bd5c0-5bc1-4ca6-9061-1fcfce1d7e2c
📒 Files selected for processing (8)
frontend/api/bff.tsfrontend/src/api/account.test.tsfrontend/src/api/account.tsfrontend/src/api/backend.test.tsfrontend/src/api/backend.tsfrontend/src/api/bff-guard.test.tsfrontend/src/api/client.tsfrontend/src/app/App.tsx
지적 다섯 중 넷을 고쳤습니다. 하나(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
리뷰 답변에서 적어 두겠다고 한 것이다. 이 필드는 allergenIds 에 UNKNOWN 이 섞였을 때만 채워지는데, 그 경우는 그보다 앞인 정규화에서 이미 막힌다(RECONFIRMATION_REQUIRED -> RECONFIRM_REQUIRED). filterCandidates 는 정규화() 를 먼저 부르므로 후보 필터까지 도달하지 못한다. 선언만 보고 '안 쓰네' 하면 닿지도 않는 길을 화면에 이으려다 아무도 지나가지 않는 코드를 만들게 된다. 다음 사람이 같은 결론에 도달하지 않도록 이유를 타입 옆에 남긴다. tsc 0 · 테스트 244개 통과
|
@coderabbitai review |
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
frontend/src/api/backend.ts (3)
1178-1181: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win모든 재확인 항목을 전달하세요.
reconfirmationFields는 배열인데 첫 번째message만KioBridgeError에 넣습니다. 여러 조건이 동시에 재확인을 요구하면 나머지 조건이 사라집니다.모든 사용자 문구를 합치거나 구조화된 배열로 반환하세요. 사용자가 모든 조건을 확인한 뒤에만 재시도할 수 있어야 합니다.
경로 지침의 “못 맞춘 조건이 있으면 감추지 말고 사용자에게 알려야 합니다”를 적용한 지적입니다.
🤖 Prompt for 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. In `@frontend/src/api/backend.ts` around lines 1178 - 1181, Update the RECONFIRMATION_REQUIRED handling around sr.reconfirmationFields and KioBridgeError so every reconfirmation field message is preserved and presented to the user, rather than only the first entry. Combine all available messages or pass them as a structured collection, while retaining the existing fallback when no messages are provided.Source: Path instructions
1421-1428: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift승인 조건을 이 API 계층에서도 강제하세요.
submit()은 후보 허용 여부, 추천 후보 변경 확인,requiresReconfirmation, 저확신 확인을 검사하지 않고 항상approved: true를 만듭니다.confirmedAt도 이 함수가 현재 시각으로 생성하므로 실제 사용자 확인을 증명하지 않습니다.또한
recommend()이 화면에서 제거한 후보가 원본추천Map에 남을 수 있습니다.submit()은 이 원본을 다시 승인 요청에 사용합니다. 필터를 거치지 않은 후보나 이용 불가 후보가 승인 요청에 들어갈 수 있습니다.
submit()직전에 허용 후보, 후보 변경 확인, 저확신 확인, 재확인 확인을 모두 검증하세요. 필터 결과가 없으면 허용하지 마세요. 화면용으로 정제한 추천만 저장하고, 검증 실패 시/internal/orchestrator/approve를 호출하지 마세요.경로 지침의 “승인 조건(후보 선택·변경 확인·저확신 확인)을 프론트 가드뿐 아니라 이 계층에서도 다시 검사”를 적용한 지적입니다.
🤖 Prompt for 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. In `@frontend/src/api/backend.ts` around lines 1421 - 1428, Update the submit() approval flow to revalidate the selected candidate, recommendation-change confirmation, low-confidence confirmation, and requiresReconfirmation before calling /internal/orchestrator/approve; reject when no allowed candidate remains and preserve the existing confirmed-at/user-confirmation contract instead of unconditionally generating approval data. Ensure recommend() stores the filtered, screen-refined recommendation rather than the original 추천 Map, and prevent the approval request from being sent when any validation fails.Source: Path instructions
1083-1095: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win경고 판정에도 동일한 축 해석기를 사용하세요.
경고 루프는
errorCode만 조회합니다.ruleIdfallback을 사용하지 않습니다.errorCode가 없거나 새 값이면continue로 서버가 보고한 불일치가 사라집니다. 통과 루프에는 이미ruleIdfallback이 있어 두 경로의 동작도 다릅니다.
errorCode와 정규화한ruleId를 함께 처리하는 공통 해석기를 사용하세요. 해석할 수 없는 경고도 버리지 말고 안전한 미충족 또는 재확인 상태로 전달하세요.경로 지침의 “못 맞춘 조건이 있으면 감추지 말고 사용자에게 알려야 합니다”를 적용한 지적입니다.
권장 수정 방향
+const 축찾기 = (r: 규칙판정) => + 규칙축[r.errorCode ?? ""] ?? + 규칙축[ + (r.ruleId ?? "") + .replace(/^CHICKEN_/, "") + .replace(/_PREFERENCE$/, "_MISMATCH") + ] ?? null; + -const 축 = 규칙축[r.errorCode ?? ""]; +const 축 = 축찾기(r);🤖 Prompt for 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. In `@frontend/src/api/backend.ts` around lines 1083 - 1095, Update the warning-processing loop alongside the existing pass logic to resolve each result using both errorCode and the normalized ruleId fallback, preferably through a shared axis-resolution helper. Do not silently discard warnings when neither identifier resolves; preserve them as a safe unmet or recheck state so server-reported mismatches remain visible, while retaining the existing deduplication and row-generation behavior around 본축, 한줄, and 행.Source: Path instructions
🤖 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 `@frontend/src/api/backend.ts`:
- Around line 1318-1327: Update the rule-result mapping around 서버판정있음 in
frontend/src/api/backend.ts lines 1318-1327 to persist
reconfirmationsByCandidateId as candidate-level status, and propagate that
status through the screen response and approval guard so unconfirmed candidates
cannot be approved. Extend frontend/src/api/backend.test.ts lines 681-706 to
verify matchedOptions remains empty, reconfirmation status and message are
displayed, and approval is rejected when confirmation is missing.
In `@frontend/src/app/App.tsx`:
- Around line 3725-3735: Update 서버주문표모두지우기 and 서버주문표지우기 so a failed
account.listSheets lookup remains authoritative alongside individual deletion
failures. When the lookup failed, ensure the resulting retry confirmation
invokes 서버주문표모두지우기(userId, 아는것) again rather than retrying only 못지운것, and
prevent the deletion-failure confirmation from overwriting this lookup-failure
state.
---
Outside diff comments:
In `@frontend/src/api/backend.ts`:
- Around line 1178-1181: Update the RECONFIRMATION_REQUIRED handling around
sr.reconfirmationFields and KioBridgeError so every reconfirmation field message
is preserved and presented to the user, rather than only the first entry.
Combine all available messages or pass them as a structured collection, while
retaining the existing fallback when no messages are provided.
- Around line 1421-1428: Update the submit() approval flow to revalidate the
selected candidate, recommendation-change confirmation, low-confidence
confirmation, and requiresReconfirmation before calling
/internal/orchestrator/approve; reject when no allowed candidate remains and
preserve the existing confirmed-at/user-confirmation contract instead of
unconditionally generating approval data. Ensure recommend() stores the
filtered, screen-refined recommendation rather than the original 추천 Map, and
prevent the approval request from being sent when any validation fails.
- Around line 1083-1095: Update the warning-processing loop alongside the
existing pass logic to resolve each result using both errorCode and the
normalized ruleId fallback, preferably through a shared axis-resolution helper.
Do not silently discard warnings when neither identifier resolves; preserve them
as a safe unmet or recheck state so server-reported mismatches remain visible,
while retaining the existing deduplication and row-generation behavior around
본축, 한줄, and 행.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: 097fac82-f7d7-4240-b98b-c621b8374ea8
📒 Files selected for processing (3)
frontend/src/api/backend.test.tsfrontend/src/api/backend.tsfrontend/src/app/App.tsx
| 규칙판정들.set(profile.id, { | ||
| // 셋 중 하나라도 오면 '서버가 판정했다' 로 본다. 예전에는 앞의 둘만 봐서, | ||
| // 재확인 판정만 온 응답에서 우리가 다시 맞춰 보는 쪽으로 물러났다. | ||
| // 서버가 '비교하지 못했다' 고 한 축을 우리가 맞다고 말하게 되는 자리다. | ||
| 서버판정있음: Boolean( | ||
| r.warningsByCandidateId || r.passesByCandidateId || r.reconfirmationsByCandidateId, | ||
| ), | ||
| warn: r.warningsByCandidateId ?? {}, | ||
| pass: r.passesByCandidateId ?? {}, | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
재확인 판정의 저장·표시 계약을 함께 수정하세요.
서버가 재확인을 요구해도 현재 구현은 존재 여부만 기록하고 판정 내용을 버립니다. 테스트도 이 빈 결과를 정상으로 간주합니다.
frontend/src/api/backend.ts#L1318-L1327:reconfirmationsByCandidateId를 후보별 상태로 저장하고 화면 응답과 승인 가드에 전달하세요.frontend/src/api/backend.test.ts#L681-L706:matchedOptions=[]외에 재확인 상태·문구와 미확인 승인 거부를 검증하세요.
경로 지침의 “못 맞춘 조건이 있으면 감추지 말고 사용자에게 알려야 합니다”를 적용한 지적입니다.
📍 Affects 2 files
frontend/src/api/backend.ts#L1318-L1327(this comment)frontend/src/api/backend.test.ts#L681-L706
🤖 Prompt for 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.
In `@frontend/src/api/backend.ts` around lines 1318 - 1327, Update the rule-result
mapping around 서버판정있음 in frontend/src/api/backend.ts lines 1318-1327 to persist
reconfirmationsByCandidateId as candidate-level status, and propagate that
status through the screen response and approval guard so unconfirmed candidates
cannot be approved. Extend frontend/src/api/backend.test.ts lines 681-706 to
verify matchedOptions remains empty, reconfirmation status and message are
displayed, and approval is rejected when confirmation is missing.
Source: Path instructions
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
frontend/src/api/backend.ts (4)
931-933: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win중단 사유에도 금지 표현 필터를 적용하세요.
getEvidence()는e.stopReason과서버요약.stopReason을abort.message로 그대로 전달하고, 화면은 이를 출력합니다. 서버가"결제수단..."또는"주문 완료..."를 보내면 금지 문구가 노출됩니다.보여도되나를 이 경로에도 적용하세요.🤖 Prompt for 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. In `@frontend/src/api/backend.ts` around lines 931 - 933, Apply the existing 보여도되나 forbidden-expression filter to abort messages derived from e.stopReason and 서버요약.stopReason in getEvidence(), before the screen displays them. Ensure server-provided stop reasons such as payment or order-completion text are replaced or suppressed according to the established filter behavior.Source: Path instructions
498-516: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
reachedStep을STEPS인덱스로 사용하지 마세요.reachedStep은executedActions.length인 반면STEPS는 다섯 개의 의미 단계입니다. 현재 변환은 동작 수가 4개 이상이면 실제 중단 위치와 관계없이 마지막 단계만failed로 표시합니다. 서버가 단계 인덱스 또는 동작별 단계 정보를 제공하도록 계약을 맞추고, 그 값을 단계 표시로 변환하세요.🤖 Prompt for 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. In `@frontend/src/api/backend.ts` around lines 498 - 516, Update the aborted-step mapping around 멈춘칸 and the STEPS status conversion so it does not use reachedStep as a semantic STEPS index. Use a server-provided stage index or per-action stage metadata, align the backend contract and frontend conversion to that value, and preserve done/failed/waiting statuses at the actual stopped stage.
1049-1115: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win미매핑
FAIL판정을 누락하지 않도록 처리해 주세요.
축맞춤은규칙축에 없는errorCode를 조용히 건너뜁니다. 서버가 새FAIL규칙을 반환하면 사용자의 불일치 조건이 결과에서 사라집니다. 미매핑FAIL을 일반 불일치 항목으로 보존하고, 해당 입력을 검증하는 테스트를 추가해 주세요.🤖 Prompt for 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. In `@frontend/src/api/backend.ts` around lines 1049 - 1115, Update 축맞춤 to preserve FAIL entries whose errorCode is not present in 규칙축, representing them as a generic mismatch instead of skipping them; retain existing axis mapping and deduplication for known codes. Add a test covering an unmapped FAIL result and verify it appears in the returned MappedOption output.Source: Path instructions
1334-1343: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift결과 저장 키를
pairingId기준으로 분리하세요.
후보,규칙판정들,추천,마지막키가profile.id만 사용합니다. 같은 주문표로 새 연결을 시작하면 이전 비동기 응답이 새 연결의 후보와 정규화 결과를 덮을 수 있습니다.App.tsx의alive검사는 화면 반영만 막고 이 저장을 막지 않습니다. 모든 결과를 세션별로 저장하고, 오래된 응답은 버리세요.🤖 Prompt for 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. In `@frontend/src/api/backend.ts` around lines 1334 - 1343, Update the result-storage flow around 후보, 규칙판정들, 추천, and 마지막키 to key entries by the current pairingId rather than profile.id. Track the active pairing/session and ignore responses whose pairingId is no longer current before mutating any of these stores, so stale asynchronous responses cannot overwrite the new connection’s candidates or normalized results; retain the existing alive check for UI updates.Source: Path instructions
🤖 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.
Outside diff comments:
In `@frontend/src/api/backend.ts`:
- Around line 931-933: Apply the existing 보여도되나 forbidden-expression filter to
abort messages derived from e.stopReason and 서버요약.stopReason in getEvidence(),
before the screen displays them. Ensure server-provided stop reasons such as
payment or order-completion text are replaced or suppressed according to the
established filter behavior.
- Around line 498-516: Update the aborted-step mapping around 멈춘칸 and the STEPS
status conversion so it does not use reachedStep as a semantic STEPS index. Use
a server-provided stage index or per-action stage metadata, align the backend
contract and frontend conversion to that value, and preserve done/failed/waiting
statuses at the actual stopped stage.
- Around line 1049-1115: Update 축맞춤 to preserve FAIL entries whose errorCode is
not present in 규칙축, representing them as a generic mismatch instead of skipping
them; retain existing axis mapping and deduplication for known codes. Add a test
covering an unmapped FAIL result and verify it appears in the returned
MappedOption output.
- Around line 1334-1343: Update the result-storage flow around 후보, 규칙판정들, 추천,
and 마지막키 to key entries by the current pairingId rather than profile.id. Track
the active pairing/session and ignore responses whose pairingId is no longer
current before mutating any of these stores, so stale asynchronous responses
cannot overwrite the new connection’s candidates or normalized results; retain
the existing alive check for UI updates.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2bad13ed-874f-4372-a658-d91c15386441
📒 Files selected for processing (1)
frontend/src/api/backend.ts
확인창 자리가 하나뿐이라 나중 것이 앞의 것을 덮고 있었습니다.
1. listSheets 실패 -> 아는 것만 지우기 시작 + "목록을 못 받았어요" 창
2. 그중 하나라도 삭제 실패 -> "이 기기에서는 지웠어요" 창이 1번을 덮음
3. 거기서 '다시 시도' -> 못 지운 것만 다시 지움. 목록은 다시 안 받음
그러면 처음 조회에서 빠진 서버 주문표는 영영 안 지워지고, 사용자는
목록을 못 받았다는 사실조차 못 보고 지나갑니다.
목록을 못 받았다는 사실을 삭제 쪽으로 넘겨 하나의 창에서 함께 말합니다.
목록못봄 이면 지우지 못한 것이 없어도 알린다
무엇이 올라가 있었는지조차 모르는 상태라 다 지켰다고 말할 수 없다
목록못봄 이면 다시 시도가 목록부터 다시 받는다
서버주문표모두지우기 로 돌아간다
문구도 세 갈래로 나눴습니다.
목록 못 받음 + 다 지움 "남은 것이 있는지 알 수 없어요"
목록 못 받음 + 일부 실패 "목록도 못 받았고 지우지 못한 것도 N개 있어요"
목록 받음 + 일부 실패 기존 문구 그대로
확인: frontend 에서 tsc 0 · 테스트 244개 통과 · vite build ok
|
@coderabbitai review |
|
* [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>
개인 레포에서 코드래빗 리뷰를 마치고 머지한 것 셋을
frontend/로 옮깁니다. 백엔드는 건드리지 않았습니다.옮긴 것은 코드 파일 8개뿐입니다.
.gitignore·.vercelignore·README.md등도 두 레포가 다르지만, 개인 레포에는backend/사본이 루트에 있어서 생긴 차이라 안 건드렸습니다.① 서버에 올라간 주문표도 지웁니다 (팀 #79 연결)
DELETE /api/v1/users/{userId}/profiles/{profileId}를 붙였습니다. '이 기기에서 정보 지우기' 가 이제 서버까지 닿습니다.화면 문구도 둘로 나눠 적습니다 — 주문표는 지워지고, 키오스크에 보낸 승인·거절 기록은 여전히 남습니다. 뭉뚱그려 "다 지워져요" 라고 하면 그게 거짓말이 됩니다.
배포본에서만 나던 구멍
api/bff.ts가GET·POST만 넘겨 주고 있어서 DELETE 는 운영에서 405 가 났습니다. 개발 서버의 프록시는 다 넘겨 줘서 로컬에서는 안 보였습니다.경로별 메서드 허용 목록을 두고, 메서드 검사를 경로 파싱 뒤로 옮겼습니다.
src/api/bff-guard.test.ts를 새로 넣었습니다.api/bff.ts는 Vercel 서버 함수라 브라우저 테스트 환경에서 import 할 수 없어서, 파일을 글로 읽고 정규식을 뽑아 검사합니다. DELETE 가 프로필 상세 경로 하나에만 열려 있는지, 기본 메서드 목록에 안 섞였는지, userId 자리가 숫자만 받는지를 못 박습니다.② 재확인 분기가 죽어 있었습니다
RECONFIRMATION_REQUIRED검사가INVALID검사 아래에 있어서 한 번도 닿지 못했습니다.백엔드는
reconfirmationFields가 비지 않을 때만RECONFIRMATION_REQUIRED를 내고, 그 필드는contractValidation.errors에서 만듭니다. 킷이HARD_CONSTRAINT_UNKNOWN을 warning 이 아니라 error 로 넣기 때문에,RECONFIRMATION_REQUIRED이면valid는 반드시false입니다. 그래서 늘INVALID쪽이 먼저 던졌습니다.결과적으로 알레르기를 모르는 분에게 돌아갈 길 없는 오류(
recoverable: false)와 킷 원문이 그대로 나갔습니다.③ 중단된 실행의 진행 표시가 거짓이었습니다
state가aborted인데 다섯 단계가 전부 '됨' 으로 그려졌습니다. 멈춘 주문을 보고 다 된 줄 알게 됩니다.reachedStep까지만done, 그 칸은failed로 그립니다.④ 못 맞춘 조건과 후보별 규칙 판정
unmetConditions를 버리고 있었습니다. 담기는 담는데 한 축이 어긋난 경우가 여기 오는데, 화면까지 오지 않아서 사용자는 자기 조건이 다 반영된 줄 알고 승인했습니다.warningsByCandidateId·passesByCandidateId도 받아 씁니다. 예전에는 이걸 버리고attributes·supportedOptions로 같은 판단을 다시 했습니다. 같은 판단을 두 곳에서 하면 언젠가 갈라지고, 다시 하는 쪽은 알 수 없는 것이 있습니다 — WARN 이 없다는 사실만으로는 '일치한다' 를 뜻하지 않습니다. 값이 없거나,NO_PREFERENCE같은 중립값이거나, 후보가 그 항목을 아예 선언 안 했으면SKIPPED라 경고에 아무것도 안 남지만 실제로는 "비교한 적이 없다" 일 뿐입니다.응답에 판정 필드가 아예 없을 때만 예전 방식으로 물러납니다. 필드는 왔는데 그 후보 항목만 없는 것은 '비교한 축이 하나도 없다' 는 뜻이라 빈 것이 맞는 답입니다.
⑤ 장소 가드
카페 주문표로도 주문 버튼이 눌렸습니다. 백엔드가 다루는 것은 닭강정집뿐이라
serviceType이UNKNOWN이 되고 음료·온도·사이즈·시럽은 축 자체가 없어 전부NO_PREFERENCE가 되는데, 킷 스키마가UNKNOWN을 허용하는 enum 이라 검증은 통과하고 닭강정이 추천됩니다.게다가 확인표는
UNKNOWN·NO_PREFERENCE축을 건너뛰므로 확인 카드가 텅 빈 채로 승인 화면이 떴습니다 — 무엇을 담는지 못 보고 승인하게 됩니다.막을 때 이유를 말합니다. 버튼만 잠그면 왜 안 되는지 알 수 없습니다.
확인
frontend/에서 직접 돌렸습니다 —tsc --noEmit0건 · 테스트 243개 통과 ·vite buildok백엔드에 남은 것
이 PR 과는 별개로 #90(V2 마이그레이션)과 #83(주문표 소유권 확인)이 열려 있습니다. 프론트에서 고칠 수 있는 것이 아니라 이슈로만 올려 두었습니다.
Summary by CodeRabbit
새로운 기능
개선 사항