[FEAT] 아이디·비밀번호 회원가입·로그인 연동 및 주문표 서버 동기화 - #58
Conversation
개인 레포에서 코드래빗 리뷰 두 번(#7 · #8)을 받고 머지한 것을 그대로 옮긴다. 프론트만 바뀐다 — backend/ 는 건드리지 않았다. 무엇이 생겼나 modules/member 의 네 경로에 붙였다(src/api/account.ts). POST /api/v1/auth/signup · login GET·POST /api/v1/users/{userId}/profiles 로그인은 끝까지 선택이다. 심사 항목이 "로그인하지 않아도 추천부터 최종 확인까지 전 과정이 동작하는지" 라, 이 파일의 어떤 것도 주 흐름을 막지 않는다. 로그인해서 얻는 것은 하나뿐이다 - 저장해 둔 주문표를 다음에도 불러오는 것. 실명.전화번호는 받지 않는다. 아이디는 사용자가 지어낸 문자열이다. '프로필' 을 '주문표' 로 바꿨다. 사람을 가리키는 말로 읽혔는데 실제로는 저장해 둔 주문 하나다(닭강정.매운맛.순살.종이컵.1개). 백엔드는 계속 profile 이라 부르고 그 이름은 계약이라 그대로 둔다 - 화면 말만 바꿨다. 알고 써야 하는 것 셋 (docs/BACKEND_INTEGRATION.md 에 적었다) · 토큰이 없다. 응답이 { userId, loginId } 뿐이라 주문표 요청이 userId 를 경로에 넣어 나간다. 인증이 아니라 식별이다. userId 는 1부터 올라가는 숫자라 숫자만 바꾸면 남의 주문표를 읽고 쓴다. BFF 를 닫아도 해결되지 않는다 - 백엔드가 열려 있는 한 직접 부를 수 있다. 막는 자리는 서버다. 세션 토큰을 발급해 주시면 부르기 한 곳에 헤더를 붙이면 끝이다. · 주문표 삭제 경로가 없다. 화면에서 지워도 서버에는 남는다. 그래서 '이 기기에서 정보 지우기' 문구를 지운 척하지 않게 고쳐 두었다. · place 가 @notblank 라 장소 없는 주문표는 400 이다. 그 400 에는 code 도 message 도 없어서(@Valid 실패는 GlobalExceptionHandler 를 안 탄다) 이유를 말해 줄 수 없다. 올리기 전에 프론트가 거르고 화면이 먼저 알린다. 그 사이 개인 레포에서 머지한 것도 함께 온다 · 승인 전에 거절할 수 있다. 거절도 서버에 기록한다(approved:false). · npm run check:backend - 앱이 부르는 경로를 앱이 부르는 순서로 불러 보고 200 인지만이 아니라 후보에 이름이 있는지.품절이 섞였는지. 실제로 담기는지까지 본다. · 시연용 시나리오 패널을 기본으로 감춘다(?demo=1 로만 보인다). · #48 의 summary 를 결과 화면에 한 줄로 붙인다. README 세 줄은 팀 레포 기준으로 고쳤다 - 백엔드가 여기서는 저장소 루트에 있고(../backend), 시나리오 패널 설명이 낡아 있었다. tsc 0, 182 tests, build ok
|
Warning Review limit reached
Next review available in: 21 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: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Walkthrough
Changes주문표 기반 애플리케이션 전환
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant App
participant AccountApi
participant BFF
participant TeamBackend
App->>AccountApi: 회원가입 또는 로그인 요청
AccountApi->>BFF: 계정 요청 전달
BFF->>TeamBackend: 허용된 인증 요청 전달
TeamBackend-->>BFF: 계정 응답 반환
BFF-->>AccountApi: 정규화된 응답 반환
AccountApi-->>App: Account 반환
App->>AccountApi: OrderSheet 저장 요청
AccountApi->>BFF: OrderSheet DTO 전달
BFF->>TeamBackend: OrderSheet 저장
TeamBackend-->>App: 저장 결과 반환
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/api/backend.ts (1)
205-221: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift키오스크 상품 ID를 브라우저에 보관하지 마세요.
rec와후보Map은 서버의 실제candidateId를 브라우저 메모리에 저장합니다. 화면에c1표식만 노출해도 앱은 상품 ID를 보관하므로 실격 조건을 위반합니다. 서버가 페어링 세션에 실제 후보를 보관하고, 프론트에는 세션 한정 표식만 반환하도록 계약을 변경하세요.
frontend/src/api/backend.ts#L205-L221:rec전체 대신 상품 ID가 제거된 화면용 응답만 세션에 보관하세요.frontend/src/api/backend.ts#L643-L652: 실제 후보 ID와 추천을 브라우저 Map에 저장하지 마세요. 서버 세션에서 표식을 실제 후보로 해석하세요.As per path instructions, "키오스크 상품 ID 나 화면 좌표를 앱이 들고 있거나 저장하면 안 됩니다."
🤖 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 205 - 221, Update requestMapping in frontend/src/api/backend.ts:205-221 to store only a display-safe response in the browser session, removing actual candidate IDs from rec while preserving the information needed for rendering. Update the logic at frontend/src/api/backend.ts:643-652 so browser-side Maps do not retain actual candidate IDs or recommendation data; resolve session-scoped display markers to real candidates through the server pairing session instead.Source: Path instructions
🧹 Nitpick comments (2)
frontend/src/api/account.test.ts (1)
404-422: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value가짜 타이머 정리를
afterEach로 옮기면 누수를 막습니다.
vi.useRealTimers()가 테스트 본문 안에 있습니다.advanceTimersByTimeAsync가 던지면 이 줄이 실행되지 않습니다. 그러면 가짜 타이머가 다음 테스트로 넘어갑니다. 이 파일의 다른 테스트는setAccountMockDelay(0)로도setTimeout을 씁니다.제안 수정
-const 원래fetch = globalThis.fetch; -afterEach(() => { globalThis.fetch = 원래fetch; }); +const 원래fetch = globalThis.fetch; +afterEach(() => { + globalThis.fetch = 원래fetch; + vi.useRealTimers(); +});그리고 테스트 본문의
vi.useRealTimers();는 지웁니다.🤖 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/account.test.ts` around lines 404 - 422, Move fake-timer cleanup to this test file’s afterEach hook so vi.useRealTimers() always runs even when advanceTimersByTimeAsync or the awaited login rejects; remove the vi.useRealTimers() call from the “응답이 오지 않으면 끊고 사용자에게 말한다” test body while preserving its timeout assertions.frontend/src/api/account.ts (1)
354-359: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win실서버
signup·login에도 입력 검사를 두면 목과 동작이 같아집니다.
saveSheet는못올리는이유를 이 계층에서 다시 검사합니다.signup과login은 검사하지 않습니다. 목 구현(Line 211-216)은 "한쪽만 막으면 구현을 바꿀 때 샌다"라고 적고 두 검사를 모두 부릅니다.지금 상태에서 화면 가드를 우회한 값이 실서버로 가면, 사용자는 code 없는 400 에서 온 일반 문구만 받습니다. 목에서는 구체 문구를 받습니다. 같은 입력에 다른 안내가 나갑니다.
제안 수정
+ const 계정입력검사 = (loginId: string, password: string): void => { + const 아이디문제 = 아이디검사(loginId); + if (아이디문제) throw new KioBridgeError("INVALID_REQUEST", 아이디문제, true); + const 비번문제 = 비밀번호검사(password); + if (비번문제) throw new KioBridgeError("INVALID_REQUEST", 비번문제, true); + }; + return { - signup: (loginId, password) => - 부르기<Account>("POST", "/api/v1/auth/signup", { loginId: loginId.trim(), password }), + async signup(loginId, password) { + 계정입력검사(loginId, password); + return 부르기<Account>("POST", "/api/v1/auth/signup", { loginId: loginId.trim(), password }); + },
login은 기존 계정의 비밀번호 규칙이 바뀔 수 있으므로 아이디 검사만 두는 편이 안전합니다.이 지적은 경로 지침을 따릅니다: "승인 조건(후보 선택·변경 확인·저확신 확인)을 프론트 가드뿐 아니라 이 계층에서도 다시 검사하는지 봐 주세요."
🤖 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/account.ts` around lines 354 - 359, 실서버 API 래퍼의 signup과 login에도 목 구현과 동일한 입력 검사를 적용하세요. signup은 아이디와 비밀번호 검사를 모두 수행하고, login은 비밀번호 규칙 변경을 허용하도록 아이디 검사만 수행하며, 기존 목 구현과 동일한 검증 헬퍼를 재사용해 구체적인 오류 안내를 유지하세요.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/api/bff.ts`:
- Around line 80-82: Introduce an environment-controlled feature flag for the
unauthenticated users/{id}/profiles route in the route-matching configuration,
defaulting to disabled unless explicitly enabled. Apply the flag only to the
/^api\/v1\/users\/\d+\/profiles$/ pattern and leave the auth signup/login route
and existing profile flow unchanged when enabled.
In `@frontend/docs/BACKEND_INTEGRATION.md`:
- Around line 363-369: 문서의 삭제 안내를 실제 UI 문자열에 맞게 수정하고, 개인정보 화면과 확인창의 문구를 각각 정확히
반영하세요. api.forgetAll()은 로컬 상태와 세션만 정리한다고 명시하고, 계정 초기화 및 프로필 삭제는 App.tsx 확인창 핸들러의
별도 책임으로 구분하세요. 서버 프로필 삭제 경로가 없으면 주문표가 서버에 남는다는 안내는 유지하세요.
In `@frontend/README.md`:
- Around line 41-45: Update the frontend README command block to explicitly
label three separate terminals, keep the backend command rooted at the
repository root, and run dev:team from frontend so each command uses the correct
working directory. Also clarify that the commands at lines 50 and 56, including
node tools/check-backend.mjs, must be run from frontend.
In `@frontend/src/api/account.ts`:
- Around line 361-364: Update frontend/src/api/account.ts lines 361-364 in
listSheets to validate that the response is an array and filter out non-object
elements before mapping with 주문표읽기, throwing BAD_RESPONSE for an invalid
top-level response; also update frontend/src/api/account.ts lines 330-340 to
attach rejection handling to res.text() like res.json(), converting failures to
BAD_RESPONSE.
- Around line 330-340: Wrap the res.text() call in the account request flow with
KioBridgeError handling so stream failures are converted instead of propagating
raw TypeError instances. Update the logic after the !res.ok branch to preserve
the empty-body undefined result while mapping text-read failures to the existing
error code/message contract, reusing the surrounding KioBridgeError and 문구
symbols.
In `@frontend/src/api/backend.ts`:
- Around line 382-390: Track each rejected pairingId in a separate
deletion-target set within reject, rather than relying only on active sessions.
In frontend/src/api/backend.ts lines 382-390, record the pairingId after the
rejection flow; in lines 779-785, have forgetAll invoke the server deletion API
for these recorded targets and propagate success only when deletion succeeds,
treating a missing deletion API as incomplete. Ensure forgetSession does not
falsely report server deletion when it only clears the browser Map.
- Around line 643-652: 매핑 상태를 주문표 ID가 아닌 페어링 세션별로 분리하세요.
frontend/src/api/backend.ts의 후보, 추천, 마지막키 및 정규화됨 상태에 사용할 pairingId 또는 서버
sessionId를 filterCandidates와 recommend에 전달하고, 모든 조회·저장이 동일한 세션 키를 사용하도록 수정하세요.
같은 sheetId를 서로 다른 조건과 환경으로 두 세션에서 매핑한 뒤 각 세션이 자신의 recommendation과
sessionContext를 승인하는 회귀 테스트를 추가하세요.
In `@frontend/src/api/mock.ts`:
- Around line 92-94: Update 메뉴이름 so an empty menuName does not fall back to
MOCK_MENU_NAME; instead, the mock response flow must return a not_found or
input-validation error requiring a menu name. Trace the relevant clarification
and not_found handling around registerSheet so no unsaved fixed menu value is
presented as user input, and add a test covering an OrderSheet with an empty
menuName.
In `@frontend/src/app/App.tsx`:
- Around line 1795-1797: Update the logged-in 안내 문구 in the `body` ternary so it
states that only order sheets with a selected place are uploaded to the server;
preserve the guest message and the existing behavior of `주문표저장` and `못올리는이유`.
- Around line 1019-1026: Update the saved-sheet radio `aria-label` and the
corresponding delete-button `aria-label` near the related sheet controls so
entries sharing the same `menuName` and `place` include an additional
distinguishing attribute, such as the sheet tag or memo, in the spoken label.
Ensure both selection and destructive deletion controls remain uniquely
identifiable without relying on color.
- Around line 535-549: Update the privacy navigation flow around the signup
screen and the onPrivacy handler so opening PrivacyScreen preserves the
originating screen; make its back action return to signup with the entered ID
and password intact instead of always calling setScreen("saved") and
setTab("account").
- Around line 2931-2944: Update the logout and device-information deletion flows
to clear the pending confirmation by calling set확인대기(null) alongside the
account-generation update. Use the existing logout and information-clear
handlers, ensuring stale retry dialogs cannot remain after the active account
changes.
- Around line 475-493: Update the signup `보내기` handler to pass `loginId.trim()`
to `account.signup` instead of the raw value, matching the validation performed
by `아이디검사`. Apply the same normalization in `LoginScreen`’s `보내기` handler so
both signup and login transmit the trimmed identifier while preserving the
existing validation and submission flow.
- Around line 3235-3246: Update the shared createApi().forgetAll()
implementation to clear the local sheets Map with sheets.clear(), matching
mockApi.forgetAll(). Ensure the common deletion path removes locally cached
order sheets in addition to clearing session, expiration, and environment state;
do not rely only on the caller’s setSheets([]).
In `@frontend/src/domain/types.ts`:
- Around line 14-24: OrderSheet and the 주문표복사/saveSheet persistence flow must
not accept or store arbitrary memo text. Remove memo from the OrderSheet and
server synchronization contract, or restrict it to an explicit predefined
non-identifying value set, ensuring names, phone numbers, addresses, resident
numbers, and patient numbers cannot be entered or persisted.
In `@frontend/tools/check-backend.mjs`:
- Around line 114-117: Update the logging branch around cf and
eligibleCandidates so the candidate count uses the same null-safe fallback as
the first-candidate lookup. Preserve the existing message and candidate display
while ensuring missing eligibleCandidates does not throw and allows subsequent
recommendation, approval, and cause guidance output to run.
- Around line 22-37: Update the fetch call inside 확인 to use an AbortSignal
timeout, ensuring requests are cancelled after a finite interval such as 15
seconds. Keep the existing catch handling so timed-out requests increment 실패 and
report the connection failure instead of leaving the script pending.
---
Outside diff comments:
In `@frontend/src/api/backend.ts`:
- Around line 205-221: Update requestMapping in
frontend/src/api/backend.ts:205-221 to store only a display-safe response in the
browser session, removing actual candidate IDs from rec while preserving the
information needed for rendering. Update the logic at
frontend/src/api/backend.ts:643-652 so browser-side Maps do not retain actual
candidate IDs or recommendation data; resolve session-scoped display markers to
real candidates through the server pairing session instead.
---
Nitpick comments:
In `@frontend/src/api/account.test.ts`:
- Around line 404-422: Move fake-timer cleanup to this test file’s afterEach
hook so vi.useRealTimers() always runs even when advanceTimersByTimeAsync or the
awaited login rejects; remove the vi.useRealTimers() call from the “응답이 오지 않으면
끊고 사용자에게 말한다” test body while preserving its timeout assertions.
In `@frontend/src/api/account.ts`:
- Around line 354-359: 실서버 API 래퍼의 signup과 login에도 목 구현과 동일한 입력 검사를 적용하세요.
signup은 아이디와 비밀번호 검사를 모두 수행하고, login은 비밀번호 규칙 변경을 허용하도록 아이디 검사만 수행하며, 기존 목 구현과
동일한 검증 헬퍼를 재사용해 구체적인 오류 안내를 유지하세요.
🪄 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: 717dd20d-2fbf-4b34-8a95-f01ff7fddc8b
📒 Files selected for processing (18)
frontend/README.mdfrontend/api/bff.tsfrontend/docs/BACKEND_INTEGRATION.mdfrontend/package.jsonfrontend/src/api/account.test.tsfrontend/src/api/account.tsfrontend/src/api/backend.test.tsfrontend/src/api/backend.tsfrontend/src/api/canonical.test.tsfrontend/src/api/canonical.tsfrontend/src/api/client.test.tsfrontend/src/api/client.tsfrontend/src/api/mock.test.tsfrontend/src/api/mock.tsfrontend/src/app/App.tsxfrontend/src/domain/catalog.tsxfrontend/src/domain/types.tsfrontend/tools/check-backend.mjs
지적 17건을 하나씩 코드와 대조했다. 13건을 고치고 4건은 답변으로 남겼다.
가장 심각한 것 — 화면이 약속한 삭제가 실제로 안 일어났다
· createApi().forgetAll() 이 client.ts 의 주문표 사본을 안 지웠다. 목은
지우고 있었고 이 경로만 빠져 있었다. 이름.고른 조건.메모가 그대로 남는다.
· reject 가 세션을 지워서, 거절까지 갔던 사람은 forgetAll 이 붙인 구현의
캐시(정규화된 주문표 - 고른 알레르기.맵기 전부)를 못 찾았다. 지우는 대신
rejected 로 표시한다. 다시 승인할 수 없는 성질은 그대로다.
계정이 바뀌어도 재시도가 옛 계정으로 나갔다
주문표올리기 는 실패한 순간에만 세대를 봤다. 확인창의 '다시 시도' 는
그 검사 없이 불린다. A 로 실패 -> 확인창을 둔 채 로그아웃 -> B 로 로그인
-> '다시 시도' 를 누르면 A 의 userId 로 요청이 나간다. B 가 쓰는 기기에서
A 의 계정에 데이터가 올라간다. 재시도가 처음의 세대를 들고 다니게 하고,
로그아웃할 때 떠 있는 확인창도 닫는다.
가입 중에 개인정보 안내를 열면 적은 것이 사라졌다
그 화면의 뒤로가기는 늘 홈으로 간다. 가입 화면으로 돌아올 길이 없어서
아이디와 비밀번호가 둘 다 초기화됐다. 무엇을 저장하는지 확인하고 가입하려는
사람이 가장 먼저 밟는 길인데, 확인하면 처음부터 다시 적어야 했다.
안내를 가입 화면 안에서 펼친다. 내용은 한 곳(개인정보항목)에서 온다.
말과 실제가 달랐던 것 넷
· "로그인하면 서버에도 올라가요" - 장소를 안 고른 주문표는 안 올라간다.
조건을 문장에 넣었다.
· 지우는 방법 - 서버에 남는 것이 주문표만이 아니다. 승인.거절 기록도 남는다.
· 목이 이름 없는 주문표에 "닭강정" 을 채워서 "저장하신 '닭강정'이 오늘의
메뉴에 없어요" 라고 말했다. 저장한 적 없는 이름이다. 모르면 안 쓴다.
· 문서가 인용한 UI 문구가 실제 문자열과 달랐다. forgetAll 과 화면 핸들러의
책임도 표로 나눴다.
안 지켜지던 약속 하나
메모는 자유 입력이라 전화번호를 적을 수 있다. 칸 옆 안내만으로는 안 지켜진다.
모양으로 걸러 저장을 막는다(전화번호.주민등록번호). 지우지는 않는다 -
앱이 말없이 고치면 화면에 보이는 것과 저장되는 것이 달라진다.
이름은 이 방법으로 못 가려낸다. 완벽한 검사가 아니라는 걸 주석에 적었다.
나머지
· BFF 의 주문표 경로를 환경 변수로 끌 수 있게 했다
(KIOBRIDGE_DISABLE_PROFILE_SYNC=1). "백엔드가 열려 있으니 같다" 는 판단은
주소를 알아야 하고 CORS 를 넘어야 한다는 두 마찰을 빠뜨렸다. 로그인은
선택이라 닫아도 주 흐름은 돈다.
· listSheets 가 배열인지 확인한다. res.text() 실패도 KioBridgeError 로 싼다.
· 같은 이름.장소의 주문표를 소리로 구분할 수 없었다. MOCK_SHEETS 에 실제로
"닭강정/음식점" 이 둘 있다. 라벨에 고른 조건을 함께 읽는다.
· check-backend 에 20초 상한. eligibleCandidates 가 없을 때 죽던 것도 고쳤다.
· README 의 명령 블록에 터미널 셋과 작업 디렉터리를 밝혔다.
함께: #48 summary.status 를 쓴다
status 는 개발자 말투가 아니라 이미 한국어 문장이었다. 내 주석이 틀렸다.
다만 '~되었습니다' 체라 마지막 화면에서만 문체가 바뀐다. 네 문장을 앱 말투로
옮겨 중단 화면 제목에 쓴다 - stopType 만으로는 서버가 '실행할 수 없습니다'
로 분류한 경우를 구분할 수 없다. 모르는 값이면 우리 문구로 물러난다.
문장 대신 코드로 달라고 docs 에 적었다.
로컬에서 승인까지 확인했다. 옛 빌드에서는 summary 가 아예 안 왔다.
고치지 않은 넷과 이유는 PR 스레드에 달았다.
tsc 0, 185 -> 193 tests, build ok
키를 주문표 id 로 두는 근거가 코드에 없었다. 여러 연결을 동시에 다루게 되면 이 전제가 깨진다는 것도 함께 적는다 - 그때 무엇을 바꿔야 하는지까지.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/README.md (1)
113-115: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win서버 동기화 조건을 문서에 명시하세요.
로그인 상태만으로 모든 주문표가 서버에 올라가지는 않습니다.
place가 없는 주문표는 이 기기에만 저장됩니다. Line 114에 이 조건을 추가하세요.수정 예시
-- 주문표와 로그인 상태는 메모리에만 있습니다. `localStorage` 를 쓰지 않아 새로고침하면 사라집니다. 로그인한 사람의 주문표만 서버에 올라가고, 다시 로그인하면 돌아옵니다. +- 주문표와 로그인 상태는 메모리에만 있습니다. `localStorage` 를 쓰지 않아 새로고침하면 사라집니다. 로그인한 사람의 주문표 중 장소를 정해 둔 주문표만 서버에 올라가고, 다시 로그인하면 돌아옵니다.🤖 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/README.md` around lines 113 - 115, Update the README statement about server-synced order lists to specify that synchronization requires both an authenticated user and a place; order lists without place remain stored only on the device.Source: Path instructions
🧹 Nitpick comments (2)
frontend/src/api/backend.test.ts (1)
287-297: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win거절 가드가 원인인지 오류 코드로 확인하세요.
rejects.toThrow()는 어떤 예외든 통과합니다.sheetId불일치나 만료 검사가 먼저 걸려도 이 테스트는 초록으로 남습니다.backend.ts의s.rejected가드가 동작했는지 구분되지 않습니다.
KioBridgeError의code를 단언하면 가드가 정확히 잠깁니다.💚 제안 수정
await expect( api.approve({ pairingId: "s1", sheetId: "p1", mappingResult: "exact" }), - ).rejects.toThrow(); + ).rejects.toMatchObject({ code: "MAPPING_REQUIRED" });🤖 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.test.ts` around lines 287 - 297, Update the rejection test around approve so it asserts the thrown KioBridgeError has the specific error code produced by the s.rejected guard in backend.ts, rather than only checking that some exception is thrown. Keep the existing rejected-session setup and verify the code identifies rejection as the cause.frontend/src/api/backend.ts (1)
421-432: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win빈 세션 ID 폴백을 방어적으로 처리하세요.
ids가 비면forgetSession("")을 호출합니다. 현재 팀 백엔드 구현은 세션 ID를 검증하지 않아서 동작합니다. 다른Backend구현이 세션 ID를 검증하거나 네트워크를 부르면 여기서 예외가 던져집니다. 그러면 로컬 정리는 이미 끝났는데 화면은 '정보 지우기' 실패로 표시합니다.정리 실패가 사용자 화면을 막지 않도록 개별 호출을 감싸는 것이 안전합니다.
♻️ 제안 수정
if (backend.forgetSession) { - await Promise.all((ids.length > 0 ? ids : [""]).map((id) => backend.forgetSession!(id))); + await Promise.all( + (ids.length > 0 ? ids : [""]).map((id) => + backend.forgetSession!(id).catch(() => { + // 지우기는 되돌릴 수 없다. 로컬은 이미 비웠으므로 여기서 멈추지 않는다. + }), + ), + ); }🤖 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 421 - 432, Update forgetAll so each backend.forgetSession call is individually guarded against errors, including the empty-session fallback call, allowing local cleanup to complete without propagating backend failures to the UI. Preserve the existing session ID iteration and cleanup behavior.
🤖 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/docs/BACKEND_INTEGRATION.md`:
- Line 384: Update the fenced code block at the reported documentation section
to specify the text language, changing the opening fence to use text while
preserving its contents unchanged.
In `@frontend/src/api/account.ts`:
- Around line 123-126: 개인정보 차단 정책이 전화번호·주민등록번호에만 한정되지 않도록 확장하세요.
frontend/src/api/account.ts 123-126의 개인정보같은메모와 저장 경로에서 이름·전화번호·주소·주민번호·환자번호를
검사하고, frontend/src/app/App.tsx 929-987에서도 동일 정책으로 경고와 저장 차단을 적용하세요.
frontend/src/api/account.test.ts 96-108에는 각 사례와 차단 시 fetch 미호출 검증을 추가하고,
frontend/docs/BACKEND_INTEGRATION.md 356-359 및 frontend/README.md 107-109는 실제 구현
범위와 일치하도록 개인정보 미수집 정책을 수정하세요. memo는 자유 입력 필드로 유지하세요.
In `@frontend/src/api/backend.test.ts`:
- Around line 270-285: Update the test around claimPairing, reject, and
forgetAll to assert that forgetSession was called with the rejected pairing ID,
such as "s1", rather than only checking that it was called. Preserve the
existing setup and verify the specific ID remains available for cleanup after
rejection.
---
Outside diff comments:
In `@frontend/README.md`:
- Around line 113-115: Update the README statement about server-synced order
lists to specify that synchronization requires both an authenticated user and a
place; order lists without place remain stored only on the device.
---
Nitpick comments:
In `@frontend/src/api/backend.test.ts`:
- Around line 287-297: Update the rejection test around approve so it asserts
the thrown KioBridgeError has the specific error code produced by the s.rejected
guard in backend.ts, rather than only checking that some exception is thrown.
Keep the existing rejected-session setup and verify the code identifies
rejection as the cause.
In `@frontend/src/api/backend.ts`:
- Around line 421-432: Update forgetAll so each backend.forgetSession call is
individually guarded against errors, including the empty-session fallback call,
allowing local cleanup to complete without propagating backend failures to the
UI. Preserve the existing session ID iteration and cleanup behavior.
🪄 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: 86c2fb99-5401-492e-b122-c46266c12bd8
📒 Files selected for processing (12)
frontend/.claude/launch.jsonfrontend/README.mdfrontend/api/bff.tsfrontend/docs/BACKEND_INTEGRATION.mdfrontend/src/api/account.test.tsfrontend/src/api/account.tsfrontend/src/api/backend.test.tsfrontend/src/api/backend.tsfrontend/src/api/mock.test.tsfrontend/src/api/mock.tsfrontend/src/app/App.tsxfrontend/tools/check-backend.mjs
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/tools/check-backend.mjs
- frontend/src/api/mock.ts
- frontend/src/api/mock.test.ts
테스트가 의도한 회귀를 잠그지 못했다
forgetAll 은 세션이 하나도 없어도 forgetSession("") 을 한 번 부른다.
그래서 toHaveBeenCalled() 는 reject 가 다시 세션을 지우도록 되돌아가도
통과한다. 거절한 그 페어링(s1)으로 불렸는지를 본다. 날카로운 지적이었다.
문서가 구현보다 많이 약속했다
"실명.전화번호.주소는 받지도 저장하지도 않습니다" 라고 적어 두었는데,
메모는 자유 입력이라 그 원칙에 뚫린 구멍이다. 앞 커밋에서 전화번호와
주민등록번호만 막아 놓고 문서는 그대로 단정하고 있었다.
검사 범위를 넓히고(주소 - 행정구역 + 번지.호), 무엇을 못 막는지를 같은
무게로 적었다. 이름과 환자번호는 정해진 모양이 없어 정규식으로 못 거른다.
보장하는 것은 이 검사가 아니라 "받는 칸이 하나도 없다" 는 사실이고,
메모는 거기서 예외다 - 그렇게 적었다(account.ts.docs.README 세 곳).
주소는 행정구역 이름만으로 보지 않는다. "강남역 앞에서 받을게요" 는 주문에
필요한 말이지 사는 곳이 아니다. 번지.호수까지 있어야 막는다.
이름이 통과하는 것을 확인하는 테스트도 넣었다 - 통과하는 게 맞고,
그 사실이 코드에 남아 있어야 다음 사람이 이 검사를 과신하지 않는다.
이름 없는 주문표로는 후보를 만들지 않는다
앞 커밋에서 지어낸 이름은 지웠지만 후보는 그대로 골랐다. 이름을 모르는 채로
고르면 화면이 "저장하신 것과 비슷한 메뉴예요" 라고 말하게 된다 - 무엇과
비슷한지 이 함수도 모르는 채로. not_found 로 답하고 무엇을 하면 되는지 말한다.
markdownlint - 코드 블록에 언어를 붙였다.
tsc 0, 193 -> 195 tests, build ok
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 (1)
frontend/src/app/App.tsx (1)
929-956: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift자유 입력
memo를 저장·전송하면서 합성 데이터 보장을 문서화하는 것이 공통 원인입니다. 이름과 환자번호는 현재 검사되지 않으므로 코드와 문서의 보장을 함께 수정해야 합니다.
frontend/src/app/App.tsx#L929-L956: 개인정보가 들어갈 수 있는 자유 입력을 저장 경로에서 제거하거나 강제 검증을 추가하세요.frontend/src/app/App.tsx#L983-L987: 검증되지 않은memo를OrderSheet로 전달하지 않도록 하세요.frontend/docs/BACKEND_INTEGRATION.md#L356-L365: 구현이 보장하지 않는 “전부 합성 데이터” 문구를 제거하세요.🤖 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/app/App.tsx` around lines 929 - 956, Update frontend/src/app/App.tsx lines 929-956 around 개인정보같은메모 and the memo textarea to prevent unverified free-text personal information from being saved or transmitted, using removal or mandatory validation that covers names and patient numbers as well as existing patterns. At frontend/src/app/App.tsx lines 983-987, ensure unvalidated memo is not passed to OrderSheet. Update frontend/docs/BACKEND_INTEGRATION.md lines 356-365 to remove the claim that all data is synthetic unless the implementation enforces that guarantee.Sources: Path instructions, Learnings
🤖 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/app/App.tsx`:
- Around line 929-956: Update frontend/src/app/App.tsx lines 929-956 around
개인정보같은메모 and the memo textarea to prevent unverified free-text personal
information from being saved or transmitted, using removal or mandatory
validation that covers names and patient numbers as well as existing patterns.
At frontend/src/app/App.tsx lines 983-987, ensure unvalidated memo is not passed
to OrderSheet. Update frontend/docs/BACKEND_INTEGRATION.md lines 356-365 to
remove the claim that all data is synthetic unless the implementation enforces
that guarantee.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: bd9e707b-5cb8-4725-9de4-b54cfc909754
📒 Files selected for processing (8)
frontend/README.mdfrontend/docs/BACKEND_INTEGRATION.mdfrontend/src/api/account.test.tsfrontend/src/api/account.tsfrontend/src/api/backend.test.tsfrontend/src/api/mock.test.tsfrontend/src/api/mock.tsfrontend/src/app/App.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/src/api/account.test.ts
- frontend/README.md
- frontend/src/api/account.ts
팀원이 찾아 주었다. package.json 에 build:team 이 없고 vercel.json 이 npm run build 를 쓰고 있었다. 목이냐 실서버냐를 정하는 것은 빌드 모드 하나인데 (vite.config.ts 의 define 이 mode === "team" 일 때만 VITE_BACKEND 를 채운다), 배포 빌드에는 그 모드가 안 붙어 있었다. 연동을 다 해 놓고 배포본만 목이었다. 번들을 열어서 확인했다. npm run build internal/orchestrator/approve 없음 (목) npm run build:team internal/orchestrator/approve 있음 (실서버) 배포 순서에 조건이 하나 붙는다 지금 https://api.hyunwoocha.site 는 /actuator/health 만 200 이고 컨트롤러 경로는 404 다(main 이 dev 보다 뒤처져 있다). 직접 불러서 확인했다. 이 상태에서 프론트를 먼저 다시 배포하면 배포본이 아무것도 못 한다 - 목으로 돌 때보다 나빠진다. 백엔드를 먼저 배포하고, 그 다음에 프론트를 다시 배포해야 한다. README 의 '알려진 제한' 에 그 순서와 확인 방법을 적었다. tsc 0, 195 tests, build ok
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/README.md (1)
113-116: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win실서버와 로컬 저장 경로에 개인정보 검사를 적용하세요.
클라이언트의 목·실서버 API는
못올리는이유를 호출하지만,SaveProfileRequest.memo에는@Size만 있고UserProfileService는 메모를 그대로 저장합니다. 따라서 직접 API 요청으로 전화번호·주민등록번호·주소 형식의 메모를 저장할 수 있습니다. 백엔드 저장 경계에 동일한 검증기를 추가하고, 목·실서버 및 로그인·비로그인 경로의 거부 테스트를 작성하세요.App.tsx의주문표저장도addSheet(p)전에 검증해야 합니다.🤖 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/README.md` around lines 113 - 116, 실서버와 로컬 저장 경계 모두에 메모 개인정보 검증을 적용하세요. SaveProfileRequest의 memo 검증을 기존 못올리는이유와 동일한 규칙으로 확장하고 UserProfileService가 검증되지 않은 메모를 저장하지 않도록 하며, App.tsx의 주문표저장에서도 addSheet(p) 전에 검사하세요. 목·실서버 및 로그인·비로그인 API 경로에서 전화번호·주민등록번호·주소 형식 메모가 거부되는 테스트를 추가하세요.
🤖 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/README.md`:
- Line 119: Update the README deployment verification command to pass the URL to
the npm script using the `--` argument separator, changing the `npm run
check:backend` invocation while preserving the existing URL and deployment-order
guidance.
- Line 122: 운영 배포 전에 LoginResponse와 UserProfileController의 주문표 API에 토큰 발급·검증 및
소유자 검사를 추가하세요. BFF가 Authorization: Bearer 헤더를 백엔드로 전달하고, GET/POST
/api/v1/users/{userId}/profiles에서 인증 사용자와 경로의 userId가 일치하는지 검증하도록 수정하세요. 다른 사용자의
조회·저장이 거부되는 교차 사용자 테스트를 작성하고, 완료 전까지 build:team 운영 배포를 차단하세요.
In `@frontend/vercel.json`:
- Line 4: Update the Vercel buildCommand configuration to use npm run build
instead of npm run build:team until the backend is deployed and required
endpoint smoke tests are enforced as a deployment gate.
---
Outside diff comments:
In `@frontend/README.md`:
- Around line 113-116: 실서버와 로컬 저장 경계 모두에 메모 개인정보 검증을 적용하세요. SaveProfileRequest의
memo 검증을 기존 못올리는이유와 동일한 규칙으로 확장하고 UserProfileService가 검증되지 않은 메모를 저장하지 않도록 하며,
App.tsx의 주문표저장에서도 addSheet(p) 전에 검사하세요. 목·실서버 및 로그인·비로그인 API 경로에서 전화번호·주민등록번호·주소
형식 메모가 거부되는 테스트를 추가하세요.
🪄 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: 325817b7-8db1-4eff-b75e-1ea0cf8628f5
📒 Files selected for processing (3)
frontend/README.mdfrontend/package.jsonfrontend/vercel.json
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/package.json
npm 10 에서는 -- 없이도 인수가 넘어간다(둘 다 돌려서 확인했다). 다만 -- 가 이식성 있는 형태이고, 옛 npm 에서는 없으면 스크립트가 인수를 못 받는다.
watTHEBUG/kioBridge#58 이 dev(b11ea32)에 머지된 것을 이 저장소 루트로 가져온다. 앱 코드.테스트.문서는 팀 레포와 같다. 다른 것은 다섯 곳뿐이고 전부 구조가 달라서 같게 두면 틀려지는 자리다 - .gitattributes/.gitignore/.vercelignore 의 backend 사본 규칙, README 의 터미널 3 과 구조 표. 배포 빌드가 목으로 나가던 것도 함께 온다(build:team). 이 저장소가 Vercel 에 연결돼 있으므로 시연 주소가 바뀌는 건 여기다. 다만 백엔드를 먼저 배포하고 그 다음에 프론트를 다시 배포해야 한다 - 지금 api.hyunwoocha.site 는 컨트롤러가 404 다. tsc 0, 195 tests, build ok
* 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>
* [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>
* 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…
무엇을
modules/member의 네 경로에 프론트를 붙였습니다. 개인 레포에서 코드래빗 리뷰를 두 번(#7 · #8) 받고 머지한 것을 그대로 옮겼습니다.frontend/18개 파일만 바뀝니다.backend/는 건드리지 않았습니다.로그인은 끝까지 선택입니다
심사 항목이 "로그인하지 않아도 추천부터 최종 확인까지 전 과정이 동작하는지" 라, 이 변경의 어떤 것도 주 흐름을 막지 않습니다. 로그인해서 얻는 것은 하나뿐입니다 — 저장해 둔 주문표를 다음에 열었을 때도 불러오는 것.
실명·전화번호는 받지 않습니다. 아이디는 사용자가 지어낸 문자열입니다.
'프로필' 을 '주문표' 로 바꿨습니다 (화면 말만)
사람을 가리키는 말로 읽혔는데 실제로는 저장해 둔 주문 하나입니다(닭강정 · 매운맛 · 순살 · 종이컵 · 1개). 백엔드는 계속
profile이라 부르고, 그 이름은 계약이라 그대로 뒀습니다 — 요청·응답 필드명은 하나도 안 바뀝니다.백엔드 쪽에 부탁드릴 것 셋
붙이면서 확인한 것 중 프론트에서 고칠 수 없는 것입니다.
frontend/docs/BACKEND_INTEGRATION.md에 같은 무게로 적어 두었습니다.{ userId, loginId }뿐userId를 경로에 넣어 나갑니다. 인증이 아니라 식별입니다.GET·POST /api/v1/users/{userId}/profiles에 인증 검사 없음userId가 1부터 올라가는 숫자라, 숫자만 바꾸면 남의 주문표를 읽고 씁니다POST /api/v1/auth/login②가 가장 급합니다. BFF(
frontend/api/bff.ts)를 닫아도 해결되지 않습니다 — 백엔드가 열려 있는 한 누구든 직접 부를 수 있고, 닫으면 우리 화면만 못 쓰게 됩니다. 막는 자리는 서버입니다.세션 토큰(또는
Authorization헤더)을 발급해 주시고, 주문표 경로에서 토큰의 주인과{userId}가 같은지 확인해 주세요. 발급해 주시면 프론트는src/api/account.ts의부르기한 곳에 헤더를 붙이면 끝입니다. ①이 풀리면 ②도 같이 풀립니다.그때까지 열어 두는 기준도 문서에 적었습니다 — 이 경로로 오가는 것은 전부 합성 데이터입니다(
dataClassification은 항상SYNTHETIC_PROFILE, 테스트로 잠갔습니다). 실제 개인정보를 받는 칸이 하나라도 생기면 그 순간 이 경로는 닫아야 합니다.그 외 두 가지
DELETE /api/v1/users/{userId}/profiles/{profileId}가 생기면 바로 부르겠습니다.place가@NotBlank라 장소 없는 주문표는 400 입니다. 그 400 에code도message도 없어서(@Valid실패는GlobalExceptionHandler를 안 탑니다) 사용자에게 이유를 말해 줄 수 없습니다. 지금은 프론트가 올리기 전에 거르고 화면이 먼저 알립니다.place를 선택 항목으로 완화해 주시거나, 어렵다면MethodArgumentNotValidException핸들러를 추가해 주세요 — 후자는 다른 모든@Valid경로에도 도움이 됩니다.그 사이 개인 레포에서 머지한 것도 함께 옵니다
approved:false→ExecutionPlan.empty()라 키오스크를 건드리지 않습니다). 거절한 세션으로는 다시 승인할 수 없습니다.npm run check:backend— 앱이 부르는 경로를 앱이 부르는 순서로 불러 보고, 200 인지만이 아니라 후보에 이름이 있는지 · 품절이 섞였는지 · 실제로 담기는지까지 봅니다.?demo=1로만 보입니다).summary를 결과 화면에 한 줄로 붙였습니다({ valid, summary, raw }와 옛 모양 둘 다 받습니다).확인
tsc 0· 182 tests 통과 ·build okREADME.md세 줄은 팀 레포 기준으로 고쳤습니다 — 백엔드가 여기서는 저장소 루트에 있고(../backend), 시나리오 패널 설명이 낡아 있었습니다.Summary by CodeRabbit
새 기능
개선
문서