- schedule_day__ 일괄 무효화 제거 — dayTtlMs가 완료된 과거 경기일에 7일 TTL을 주는데 매일 밤 지워 무의미하게 만들고 있었다. 동일한 동적 TTL을 가진 game_detail__를 이미 제외하던 논리를 그대로 적용해 자연 만료에 위임한다
- 남은 rank__ 무효화를 400건 단위로 청킹 — 단일 batch는 500 op 하드 상한에서 INVALID_ARGUMENT로 던지므로, 캐시 문서가 늘어난 어느 날 잡 전체가 조용히 죽을 수 있었다
- 삭제 쿼리에 .select() 적용해 ref만 전송(문서 본문 불필요)
- 무효화를 try 블록 안으로 이동 — 기존에는 바깥이라 실패 시 rank 갱신·syncGamesForMonth·forceSyncDay가 전부 실행되지 않았다
- dailyArchive가 날짜별 인덱스만 읽도록 전환. 백필 마커가 없는 롤아웃 기간에는 원본도 함께 읽어 인덱스에 없는 uid만 보충한다 — "인덱스가 비었을 때만 폴백"으로 두면 미러 배포 당일처럼 인덱스가 부분적으로만 찬 날짜에서 배포 전 투표자가 판정·보상·스트릭 없이 영구 유실된다(그 날짜는 다시 처리되지 않는다)
- 순위표를 run 시작 시 1회 로드해 유저별 count aggregation을 제거(O(N x M) -> 이진 탐색). listAllRankedUsers의 프로젝션에 favoriteTeamCode 추가(문서당 과금이라 read unit은 불변). 로드 실패 시 기존 aggregation으로 폴백
- user 문서를 루프에서 1회만 읽어 computeRankSnapshot과 judgeDay가 공유 — 유저당 3 read 중 1개 제거. 판정 트랜잭션 내부 tx.get은 멱등 가드와 streak read-modify-write 구동에 필수라 유지. getUser 실패 시엔 undefined로 남겨 judgeDay가 재조회하게 한다(null을 넘기면 결석 판정이 조용히 꺼진다)
- reconcileDayVotes가 날짜 경기를 1회 확보해 getGame N+1 제거. run 스코프 Set으로 같은 경기의 processGameEndWithGame 중복 재처리 차단(byUid는 정지된 스냅샷이라 앞 유저가 치유한 경기도 뒤 유저에겐 미판정으로 보인다)
- settleDailyReward에 gameCache 파라미터 추가하고 games 조회를 트랜잭션 조기 return 가드 뒤로 이동 — no_history/already_settled/not_judged 재실행은 games read 0회로 끝난다
- 통산 예측·적중 롤링 카운터 도입(user 문서). computeStats가 전수 스캔 대신 카운터 + 올해 구간 range 조회 1회를 쓴다. 미백필 유저만 1회 스캔 후 백필하며, 이력이 없으면 기준선을 세우지 않는다(오늘로 잡으면 아직 아카이브되지 않은 어제 투표가 영구 누락된다). 기준선을 lastJudgedDate로 앞당기지도 않는다 — judgeDay가 판정 트랜잭션을 voteHistory 기록보다 먼저 커밋하므로 같은 누락이 생긴다
- stats period 범위 검증(연도 2024~올해, 월 1-12, 미래 날짜 거부)과 캐시 키 정규화 추가 — 검증이 없으면 임의 period 값으로 캐시를 매번 미스시켜 전수 집계를 강제할 수 있었고, 같은 주의 7개 날짜가 7개 캐시 엔트리를 만들었다
- precomputeScoreboardCache에 scopes 파라미터 추가 — 탈퇴 경로가 11개 전체 대신 overall과 본인 팀만 재계산(영향 범위가 그 둘뿐이고 existing은 이미 읽은 값이다)
- 테스트 16건 추가: 순위표 동등성 6건, 통산 집계·기준선 7건, dailyArchive 롤아웃 5건(신규 파일)
- /userVotesByDate/{date}/{uid}/{gameId} fan-out 인덱스 추가 — RTDB에는 부분 노드 읽기가 없어서 dailyArchive가 하루치를 처리하려면 /userVotes 트리 전체(전 유저 x 전 보존 날짜)를 내려받아야 했다
- 투표를 쓰는 3곳(submitVote, changeVote, processGameEndWithGame의 판정 결과 주입)이 원자 update 하나로 원본과 미러를 함께 기록
- 삭제 경로도 동기화: deleteUserVoteGame은 양쪽에서, deleteUserVoteIndex는 유저의 날짜 목록을 먼저 읽어 해당 날짜 미러만 정리
- database.rules.json에 userVotesByDate·userVotesByDateMeta를 read/write false로 추가 — 교차 유저 데이터라 서버 전용
- 백필 스크립트 추가(npm run backfill:vote-index). dry-run 기본, --apply로 500건씩 청킹 기록, 멱등. 완료 시 /userVotesByDateMeta/backfilledAt 마커를 남겨 잡이 원본 스캔을 그만두는 근거로 쓴다
- 미러 정합성 테스트 8건 추가
- gameRepository에 날짜 키 공유 캐시(MemCache, 15초) 추가 — 날짜별 경기 목록은 유저 독립 전역 데이터라 요청 간 공유할 수 있다. GET /prediction/games가 요청마다 range 쿼리를 재발행하던 것을 인스턴스당 분당 수 회로 수렴시킨다
- games 문서 쓰기 경로에 무효화 연결: syncGamesForMonth는 여러 날짜에 걸치므로 전량 폐기, forceSyncDay는 해당 날짜만
- createGameDayCache가 공유 캐시를 경유하도록 변경 — 요청마다 새로 만드는 짧은 수명 인스턴스도 실제 쿼리를 유발하지 않는다
- predictionHandlers의 games 분기에 Cache-Control public max-age=15 추가(무인증·전 유저 공통 응답이라 CDN·브라우저 중복 제거가 가능한데 그동안 헤더가 없었다)
- MemCache.getOrFetch가 truthy 대신 히트 여부로 판정하도록 수정 — 캐시된 null이 미스로 취급돼 fetcher가 매번 재실행되던 문제(존재하지 않는 상품 조회가 요청마다 Firestore read 유발)
- MemCache에 peek·clear 추가, 단위 테스트 6건 신설
- isAlreadyExistsError를 pointService에서 middleware/errors로 이동 — 레포지토리가 서비스를 import하는 계층 역전 없이 양쪽에서 쓰기 위함. pointService는 re-export로 기존 호출부 호환 유지
- createOrder에서 getWalletTx 사전 호출과 잔액 사전 검사 제거 — applyPointChangesTx가 같은 트랜잭션에서 동일한 HttpError(409, INSUFFICIENT_BALANCE)를 던지므로 동작은 같고 wallet 문서 tx.get이 2회에서 1회로 준다
- NotificationKey에 predictionRemind·jjaekTalk 추가(마케팅은 opt-in 맵에서 제외)
- GET/POST/PATCH /user 응답에 notifications 맵과 marketingOptIn 포함 — 클라가 서버 설정을 읽을 수 있게 함
- updateMe에 marketingOptIn 처리 추가: 동의 시각은 서버 serverTimestamp로 기록, 철회 시 동의 이력 보존 (기존에는 미파싱으로 단독 PATCH가 400)
- updateNotifications가 전체 맵 재기록 대신 patch 키만 merge 기록하도록 변경(동시 토글 유실 방지)
- userService 테스트 8건 추가(허용/차단 키, boolean 검증, 동의 시각 기록·보존, 기본값)
- backend-writes.md §1.1에 새 쓰기 필드·규칙 반영
- GET /stats/seasons 추가 — 유저의 시즌별 최종 성적(seasonHistory)을 최근 시즌부터 반환
- SeasonRecordDto로 응답 형태 고정, settledAt(Timestamp)은 UTC ISO 문자열로 변환해 유출 방지
- 정산 전 빈 목록·정렬·DTO 변환 테스트 추가
- 티어 임계값을 0/100/300/700/1500에서 0/150/500/1200/2500으로 상향 — KBO 정규시즌 1개(판정 가능일 ~160일) 내 도달을 전제로 재보정
- 스트릭 보너스 상향(일별 점증)으로 빨라진 적립 속도를 반영해 MVP는 시즌 내내 성실 참여한 상위 소수만 도달하도록 조정
- config/season 문서(id/startDate/endDate)로 시즌 경계 정의 — 문서가 없으면 시즌제 비활성(기존 동작 유지)
- endDate 다음 날 dailyArchive에서 maybeSettleSeason 1회 실행: 랭킹 대상 전원의 최종 성적(tierPoints·티어·동점 동일 rank)을 users/{uid}/seasonHistory/{seasonId}에 아카이브 후 tierPoints 0 리셋·rankSnapshot 제거
- 유저별 아카이브+리셋을 같은 배치로 묶고 settledAt 마커는 전원 완료 후 기록 — 부분 실패 재실행 시 기정산 유저 점수를 collectionGroup으로 되읽어 순위 보존(멱등)
- 스트릭은 시즌과 무관하게 유지(이월 이득은 streakBonus 상한이 제한)
- seasonHistory 본인 읽기 전용 rules와 seasonId collectionGroup 인덱스 추가, 정산 시나리오 테스트 신규 작성
- 스트릭 보너스를 마일스톤 계단(+2/+5/+10/+20)에서 min(스트릭 일수, 15) 일별 점증 구조로 교체
- 적중일 기본 적립 대비 보너스 비중을 30~50%까지 높여 스트릭 유지 유인 강화 (포인트 경제 리서치 결론 반영)
- 상한 15로 시즌 이월 장기 스트릭의 이득을 초반 램프업 생략(~105pt) 수준으로 제한 — 이월은 의도적 허용
- judgmentService 테스트의 포인트 기대값을 새 공식으로 갱신
- appointAdmin을 setAdminRole로 교체 — isAdmin boolean 인자로 부여와 해제를 모두 처리
- 자기 자신의 관리자 권한 해제는 409(CANNOT_REMOVE_SELF_ADMIN)로 차단
- 비활성 유저 검사는 권한 부여 시에만 적용, 응답 형태를 {isAdmin, changed}로 변경
- 어드민 핸들러 라우트와 테스트를 새 시그니처로 갱신
- 티어 코드를 bronze~diamond에서 야구 테마 BW/PR/ST/AS/MVP로 교체 (임계값 0/100/300/700/1500 유지), 클라이언트 동기화 주석 추가
- 누적 예측 수 기반 레벨 시스템 제거 — levels.ts 삭제, /stats 응답의 currentLevel·progress 필드 제거
- 스코어보드 top/me 엔트리에 tierPoints 파생 tier 코드 포함, 배포 이전 생성 캐시는 응답 시점에 tier 보강
- 주간 마스터 티켓 잔재 주석과 문서 표의 tickets 항목 정리, 폐기된 레벨·티켓 언급 주석 정돈
- statsService 테스트를 새 티어 코드·필드 구성으로 갱신
- shipped 전환 시 운송사·송장번호(shipment) 필수 검증 및 shippedAt/deliveredAt 기록, POST /admin/order/shipment로 배송정보 정정(배송 시작 이력에 감사 정보 동기화)
- refunded 전환 시 관리자 환불 사유(2~500자) 필수 — 주문 문서·상태 이력·포인트 원장(adminReason/adminActor)에 기록하고 DTO로 노출
- 상품 옵션에 active 필드 추가(구 문서는 활성 간주) — 비활성 옵션 주문은 OPTION_UNAVAILABLE 거부, 활성·교환 가능 상품은 활성 옵션 1개 이상 필요
- OrderDto/OrderStatusHistoryEntryDto/ProductOptionDto 확장 및 관련 테스트 보강
- 수정 파일 줄바꿈 LF 정규화 및 eslint --fix 적용
- users/{uid}/addresses 서브컬렉션에 owner 전용 read/delete 및 create/update 규칙 신설
- 필드 화이트리스트(hasOnly)와 타입·길이 검증, updatedAt은 serverTimestamp 강제
- 주문 recipient는 API 인라인 전달이라 서버는 이 컬렉션을 읽지 않음(클라이언트 직접 CRUD)
- 주문 생성 시 예약(홀드) 없이 즉시 차감하고 confirmed 로 시작 — 상태 흐름을 확정 → 배송중 → 배송완료(+어드민 환불)로 간소화
- 유저 취소 엔드포인트(POST /orders/:id/cancel)와 cancelOrder 제거 — 상태 전이는 어드민 전용, 되돌림은 환불 전이 하나로 통일 (차감 txId 를 reversalOf 로 링크)
- order_reserve/order_release 원장 타입과 reserve/capture/release op, 지갑 reservedBalance 제거 — 불변식을 available = earned - spent 로 단순화
- 이전 데이터 전면 삭제에 따라 2026-07 개편 이전 원장 문서용 레거시 정규화(StoredLedgerEntry, balanceAfter 역산, refMonth/refDay 합성)도 제거
- 주문/원장/지갑 테스트를 새 스키마로 갱신하고 orderService 에뮬레이터 테스트 6건 추가 (즉시 차감·잔액 부족·권한·전이 검증·환불 반환)
- GET /admin/users/search: 닉네임 접두어(Firestore orderBy+startAt/endAt) + 이메일 정확 일치(Auth 조회) + uid 정확 일치를 한 입력으로 통합 검색, uid 기준 중복 제거
- GET /admin/users/list: 가입일 내림차순 + 문서 id 커서 페이지네이션 (주문 목록과 동일 패턴)
- AdminUserDto 추가: 날짜는 UTC ISO 변환, 옵션 필드는 키 생략, streak/티어 등 lazy 보정 필드는 기존 원칙대로 제외, active 미백필 문서는 활성으로 간주
- 비활성(탈퇴) 유저도 검색 대상에 포함해 어드민이 상태를 확인할 수 있게 함
- 에뮬레이터 회귀 테스트 5건 추가 (접두어 검색·uid 일치·비활성 표시·빈 q 400·커서 페이지네이션)
- reversalOf 미지정 시 undefined가 원장 문서 스프레드로 흘러가 Firestore가 "Cannot use undefined as a Firestore value"로 지급 전체를 실패시키던 버그 수정
- 조건부 스프레드로 값이 있을 때만 필드를 포함하도록 변경
- 회귀 테스트 추가: reversalOf 없이 지급 시 원장에 키 자체가 없음 + 명시 시 그대로 기록됨 (에뮬레이터 vitest 2건 통과)
- 상품에 재고 없는 옵션(optionLabel/options — 예: 키링 색상)을 도입하고 주문 항목에 optionId/optionName 스냅샷 저장
- 주문 생성 시 옵션 검증: 옵션 상품에 미선택이면 OPTION_REQUIRED, 무옵션 상품에 optionId가 오면 INVALID_INPUT
- 같은 상품의 색상별 항목을 지원하도록 상품 문서를 productId당 한 번만 읽게 정리
- 재고는 별도 플랫폼에서 관리하므로 서버 재고 개념 전면 제거(ProductDoc 재고 필드, availableStock, 주문 예약/차감, stock/adjust 엔드포인트, order/status restock)
- 상세 카탈로그·주문 DTO에 옵션 노출, product/upsert가 optionLabel/options 수용
- 시드/업로드 스크립트의 재고 필드 기록 제거, vitest 계약 테스트 추가
- 포인트 시스템 개편(2026-07-16) 이전 스키마 문서가 운영 Firestore에 그대로 남아 있어 포인트 내역이 실패하던 문제 수정
- 구 문서에는 txId/uid/op/available*/reserved*가 없고 balanceAfter/refMonth/refDay가 있다
- 읽기 타입을 StoredLedgerEntry로 넓히고, DTO에서 남은 값으로 복원한다
- txId는 문서 id, uid는 요청 uid, op는 type에서, 거래 전 잔액은 balanceAfter에서 역산
- refMonth + refDay를 relatedDate(YYYY-MM-DD)로 합침
- 실제 운영 문서 형태를 그대로 쓴 회귀 테스트 추가
- serverTimestamp 미해석 문서나 레거시 문서에서 toIso가 던져 GET /user가 500이 되는 경로 제거
- UserProfileDto.createdAt을 optional로 두고 toIsoOrUndefined 사용 — 값이 없으면 키를 생략한다
- 스프레드로 조립하던 기존 동작(키 생략)과 동일하게 맞춤
- Firestore에 저장된 Timestamp가 DTO를 거쳐 JSON으로 나갈 때까지 전 구간 검증
- 앱 포인트 내역 크래시(_seconds Map을 as String으로 캐스트)의 회귀 방지용
- relatedDate 같은 날짜 전용 필드의 YYYY-MM-DD 형식 유지도 함께 확인
- Firestore Timestamp가 res.json으로 그대로 나가 {_seconds,_nanoseconds}로 직렬화되던 문제 수정
- 앱의 포인트 내역(/reward/ledger)과 예측 기록(/stats/history) 크래시 원인 제거
- 클라 소비 7개 핸들러(reward/attendance/prediction/user/stats/kbo/chat)의 모든 응답에 명시적 DTO 타입 도입
- 날짜 와이어 형식을 순수 UTC ISO 8601(...Z)로 통일 — 채팅의 기존 KST(+09:00) 출력도 UTC로 전환
- DTO는 필드를 명시적으로 나열해 조립 (스프레드 덤프 제거) — 문서에 새 Timestamp 필드가 생겨도 다시 새지 않는다
- 재사용되는 주문 형태만 toOrderDto로 분리, 나머지는 응답 경계에서 직접 조립
- Firestore 저장 형식은 변경하지 않음. 출석 idempotent 재요청 경로는 저장된 Timestamp/문자열을 모두 처리
- DTO 직렬화 회귀 테스트 추가 (_seconds 부재, UTC ISO 형식, optional 키 생략, 미지 필드 차단)
- 출석·리워드 v1 설계 문서를 실제 구현 스펙 기준으로 기록
- 확정 내용 반영: 일일 +20, 스트릭 5일차 +50(주기 1회)·10일 간격 +100, 월간 보너스 폐기
- 데이터 모델을 구현대로 수정: AttendanceStateDoc 분리, 월렛 엔진, products/orders 컬렉션, PointLedgerType 확정 목록
- 초안의 열린 결정(A6)을 결정 사항 기록 표로 대체, 구현 좌표 갱신
- reward-products/ 이미지를 리사이즈(메인 1080px, 상세 너비 1440px) + WebP(품질 82)로 변환하는 공용 서비스 추가 (sharp)
- Storage 트리거 onRewardImageUploaded(us-central1)로 이후 업로드에도 자동 적용 — 다운로드 토큰을 보존해 기존 URL이 그대로 유효
- 기존 자산 일괄 최적화 스크립트(npm run optimize:reward-images) 추가, 실행 결과 38개 파일 135MB → 5.7MB
- 재처리 무한루프는 rewardImageOptimized 메타데이터 플래그로 방지
- 상품 카탈로그(products/) 신설: 공개 필드(name·pointPrice·이미지)와 재고(total/reserved/safety)를 한 문서로 관리, 목록·상세는 60초 인메모리 캐시로 서빙
- 주문 생성 시 포인트 예약(order_reserve)과 재고 예약을 단일 트랜잭션으로 원자 처리, 주문 ID를 uid_클라이언트멱등키로 고정해 더블탭·재시도 중복 생성 차단
- 주문 상태 머신(reserved→confirmed→preparing→shipped→delivered / cancelled / refunded) 전이 검증과 전이별 capture·release·refund 역거래 원장 기록
- reward HTTP 함수 신설: 지갑·원장 조회, 상품 목록·상세, 교환 자격, 주문 생성·목록·상세·취소 API
- 관리자 라우트 추가: 상품 등록·수정, 재고 조정(가용재고 음수 방지), 주문 상태 변경, 포인트 지급·회수
- Firestore 보안규칙(지갑 본인 read, 상품·주문은 API 전용)과 orders 복합 인덱스(uid+createdAt, status+createdAt) 추가
- 상품 4종 시드(seed:rewards)와 상세 이미지 업로드(upload:reward-assets) 스크립트 추가
- 지갑 문서(users/{uid}/wallet/current)와 문서 ID=멱등키 원장(pointLedger/{txId})으로 포인트 엔진 교체 — 기존 balanceAfter 최신 row 조회 방식 폐기
- 모든 포인트 변경은 pointService.applyPointChangesTx 단일 경로로 처리, available+reserved == totalEarned-totalSpent 불변식을 매 커밋 검증
- 출석 리워드 개편: 일일 20P, 연속 5일 +50P(사이클당 1회), 10일 단위 +100P — attendance/state 문서에 스트릭 상태 저장, 주간·월간 보너스 폐기
- 승부예측 일일 리워드 정산 신설: 전체 참여 50P + 성공 100P + 퍼펙트 50P, judgeDay 이후 voteHistory.rewardSettledAt 플래그와 원장 멱등키로 배치 재실행에도 중복 지급 차단
- 관리자 포인트 지급·회수(adminPointService)와 수동 재정산 디버그 라우트(/debug/settle-reward) 추가
- 소비처 없던 티켓 시스템(dailyAllKill·weeklyMaster)과 위클리마스터 판정 흐름 전체 제거 — StatsResponse.tickets 필드 삭제로 클라 응답 스키마 변경
- 종료 스코어를 "원정:홈" 위치 표기에서 팀명 순서(우리팀:상대)로 정렬 — 순서 불일치로 모델이 홈 승리(8:3)를 "3:8 패배"로 오독하고 패배 서사까지 지어내던 문제 수정
- formatLineupResult 종료 스코어에 승패 명기 추가(기존엔 승패 표기 없음)
- 원정 관점 회귀 테스트 추가. 프로브 실측: 동일 질문 5회 중 오독 2회 → 0회
- get_team_rank_snapshot에 vs 인자 추가 — 순위표의 팀간 승패표(vsRecords)에서 두 팀의 올 시즌 맞대결 승패무를 반환
- formatH2hResult 순수 함수 분리 및 단위 테스트 추가
- 오늘 상대가 아닌 임의 팀과의 상대 전적 질문을 도구 1회 호출로 처리(일정→전적 체이닝도 가능). 기존에는 vsRecords가 도구 계층에서 버려져 모델이 접근할 경로가 없었음
- 도구 호출 후 모델이 빈 응답을 반환하는 경우, 검열 문구 대신 재시도를 유도하는 `EMPTY_REPLY_NOTICE`를 표시하도록 로직을 개선했습니다.
- `chatProviderService`에 무텍스트 STOP 응답 방어 로직을 추가하여, 함수 호출 모드를 NONE으로 강제한 후 1회 재시도를 수행하도록 처리했습니다.
- 분석 서비스의 스키마 버전을 2로 올리고, `ChatOutcome`에 `empty` 타입을 추가하여 빈 응답 사례를 필터링과 구분하여 통계에 기록합니다.
- 회원 탈퇴 시 즉시 삭제 대신 `active: false` 및 `deactivatedAt`을 기록하여 30일간 데이터를 유예하는 소프트 삭제 방식을 도입했습니다.
- 경기 취소 시 투표를 삭제하는 대신 `cancelled: true`로 마킹하여 참여 흔적을 보존하고, 통계 집계에서만 제외하도록 로직을 변경했습니다.
- 유예 기간이 지난 비활성 계정을 영구적으로 파기하는 `accountPurge` 스케줄러를 추가했습니다.
- KBO 데이터 갱신 시점(02:00)을 고려하여 03:00 이전 요청 시 전일 데이터를 반환하도록 `kboTodayKst` 유틸리티를 추가했습니다.
- `kboHandlers`에서 `day` 파라미터에 "today" 값을 전달할 경우, 위 컷오프 로직이 적용된 날짜를 자동으로 계산하도록 개선했습니다.
- 신규 테스트 코드를 통해 새벽 시간대 경계값에서도 날짜가 정확하게 계산되는지 검증했습니다.
- mergeLiveIntoSchedule·fetchScheduleMonth의 formatYmd(new Date())를 todayKst().replace로 교체 — 서버 TZ 의존 제거, 정규 KST 헬퍼로 통일
- 명시적 날짜용 formatYmd와 경기 시간대 시(hour) 게이트는 그대로 유지
- TODO.md 해당 항목 종결
- gatherUserContext가 채팅 매 호출마다 오늘 라이브 일정을 fetch하던 것을 60초 인메모리 캐시로 공유 — 같은 시간창 호출이 1회 크롤을 공유
- KST 경기 시간대(14시 이후) 밖이면 라이브 merge를 건너뛰어 하루 대부분의 불필요한 외부 KBO 크롤 제거
- 동시 사용자 증가 시 외부 엔드포인트 부하·응답 레이턴시 완화
- debug/chatProbe에 messages 배열을 받는 멀티턴 경로 추가 — 전체 대화를 stateless로 리플레이해 atkgen 등 대화형 probe 지원
- 빈/누락 content를 400 대신 200으로 안전 반환 — 외부 보안 스캐너(garak)가 4xx를 치명적 에러로 보고 런 전체를 중단하는 문제 방지
- chatProbeService에 runChatProbeConversation/runMessages 추가(저장·쿼터 없는 읽기 전용 경로 유지)
- `ChatToolCallInfo` 타입에 `label` 필드를 추가하여 클라이언트가 도구 호출을 직관적인 칩 형태로 표시할 수 있도록 개선했습니다.
- `chatToolService`에 도구별 라벨 매핑 테이블(`TOOL_LABELS`)과 이를 적용하는 `withToolLabels` 함수를 구현하여, API 응답 시점에만 라벨이 동적으로 결합되도록 설계했습니다.
- 기존 저장소(Firestore) 로직에는 영향을 주지 않으면서, 메시지 조회 및 전송 결과 반환 시점에 라벨을 주입하여 표현 계층의 유연성을 확보했습니다.
- 테스트 코드를 수정하여 도구 호출 응답 및 이력 조회 시 라벨이 정확하게 포함되는지 검증했습니다.
- 모델이 화면 안내 시 [[NAV:route]] 마커를 붙이면 서버가 검증·제거 후 actions[]로 응답·이력에 실어, 클라가 해당 화면으로 이동하는 버튼을 렌더할 수 있게 함
- chatNavService(마커 파서 + APP_ROUTES 12개 라우트·라벨), NavAction/AppRoute 타입, finalizeExchange 저장·멱등 재반환·이력 매핑까지 전 경로 연결
- 위기/필터로 교체된 응답에는 액션을 붙이지 않고, 목록 밖 route는 드롭(모델 환각 방어)
- actions는 route+label만 전달(실제 경로 매핑·이동은 클라가 보유)
- 분석 이벤트에 navRoutes/navCount 추가, chatNavService 단위 테스트 추가
- 채팅 서비스의 관측성을 확보하기 위해 `chatAnalyticsService`를 도입하여 주요 이벤트(요청 결과, 토큰 사용량, 지연 시간 등)를 구조화된 로그로 기록합니다.
- 개인정보 보호를 위해 메시지 본문은 제외하고 길잇값과 메타데이터 위주로 수집하며, 비동기 로그 싱크를 통해 서비스 응답 지연을 방지했습니다.
- `sendMessage`의 각 단계(차단, 에러, 정상 응답 등)에 로깅을 통합하고, 테스트 코드를 통해 스키마 준수 및 필드 파생 로직의 정확성을 검증했습니다.
- 실시간 중계 상황을 직접 보고 있는 것처럼 말하는 페르소나의 암시를 제거하고, 라이브 스코어 제공 불가 안내를 명확히 했습니다.
- 'bestfriend'와 'commentator' 스타일 모두에서 사용자를 훈계하거나 건전한 응원을 강요하는 설교적 프롬프트를 배제했습니다.
- 사용자의 승리 주장 시 상황을 의심하거나 초를 치지 않고 자연스럽게 호응하도록 유도하며, 점수판 농담 등 스타일별 리액션 가이드를 구체화했습니다.
- 팀 홈구장 정보를 오늘 경기 장소로 단정하지 않도록 금칙을 추가하고, 관련 테스트 케이스를 보강하여 프롬프트의 일관성을 검증했습니다.
- 적대적 QA에서 사용자가 거짓 구체 스코어("어제 10대 0 대승")를 주장하면 그 수치를 사실로 받아 각색하던 문제를 보완했습니다.
- bestfriend·commentator 스타일의 "결과 주장 수용" 항목에, 승패 분위기는 받되 사용자가 댄 구체 스코어·기록 수치는 도구로 확인되지 않으면 단정·반복하지 말고 없는 경기를 각색하지 않도록 명시했습니다.
- KT·NC·SSG·LG·KIA·롯데·삼성·키움 8개 구단의 팀 페르소나 팩을 추가해 DEFAULT_TEAM_PERSONAS가 10개 구단 전체를 커버하도록 했습니다(이전에는 HH·OB만 실팩, 나머지는 generic fallback).
- 모든 팀 블록을 톤-중립으로 작성했습니다: 1인칭·반말/존댓말 샘플·말버릇 없이 팀색(연고·상징·팬 정서·팀색 어휘·라이벌·금기)만 담아, 말투는 스타일 팩이 전적으로 정하도록 했습니다.
- 10개 구단 모두 generic이 아닌 실팩을 쓰는지 검증하는 테스트를 추가했습니다.
- StylePack에 name 필드를 추가하고 스타일별 이름을 정했습니다(bestfriend = "베스트프랜드 짹", commentator = "해설가 짹").
- 각 페르소나 아키타입에 이름을 명시하고, AI 정체성 규칙을 "자기 지칭은 페르소나 이름을 쓰되, AI임을 직접 물으면 부정하지 않는다"로 갱신했습니다. 밋밋한 "AI 캐릭터" 단독 지칭을 지양합니다.