- StylePack에 name 필드를 추가하고 스타일별 이름을 정했습니다(bestfriend = "베스트프랜드 짹", commentator = "해설가 짹").
- 각 페르소나 아키타입에 이름을 명시하고, AI 정체성 규칙을 "자기 지칭은 페르소나 이름을 쓰되, AI임을 직접 물으면 부정하지 않는다"로 갱신했습니다. 밋밋한 "AI 캐릭터" 단독 지칭을 지양합니다.
- 정중한 해설위원 톤의 "commentator" 스타일 팩을 추가하고 STYLE_PACKS에 등록했습니다(bestfriend/commentator 2종).
- 팀 페르소나에서 말투 요소를 제거했습니다: 반말 샘플 문장을 걷어내고, 1인칭("나"/"저")을 팀 블록이 아니라 스타일 팩이 정하도록 옮겼습니다. 팀 페르소나는 이제 팀색(연고·정서·라이벌·금기·어휘)만 담습니다.
- 공통 프롬프트의 역할 분리를 명확히 했습니다: 말투·캐릭터·1인칭은 스타일이, 팀색은 팀 페르소나가 정하며 팀 페르소나는 말투를 지정하지 않습니다.
- 대화 스타일(3부 기본 스타일 + 짹 아키타입)을 chatStyles.ts의 스타일 팩으로 분리하고, 현재 "익명 야구방 상주러" 스타일을 "bestfriend" id로 등록했습니다.
- COMMON 시스템 프롬프트는 1부 안전·데이터, 2부 앱 능력만 고정으로 남기고 3부 자리에 {{styleBaseStyle}} 플레이스홀더를 두어 스타일 팩에서 주입하도록 했습니다.
- ChatConfig에 stylePack 필드(기본 bestfriend)를 추가해 config/chat.stylePack으로 코드 배포 없이 스타일을 교체할 수 있게 했습니다.
- 조립 결과는 기존과 본문 동일(3부 헤더 주석 1줄 외)로 동작 변화 없이 구조만 모듈화했습니다.
- 공통 프롬프트를 1부 안전·데이터 정책 / 2부 앱 능력 정책(확정 데이터 vs 라이브 데이터 구분) / 3부 기본 대화 스타일의 고정 3층으로 재구성했습니다.
- 캐릭터·말투·팀색을 교체 가능한 4층(짹 페르소나 팩 + 팀 페르소나)으로 분리하고, 친절한 상담형 AI 톤 대신 "익명 야구방 상주러" 반응성 톤으로 전환했습니다.
- 두산(OB)에 실제 팀 페르소나 팩을 추가하고 DEFAULT_TEAM_PERSONAS에 등록해, generic placeholder 대신 팀색이 살아나도록 했습니다.
- 말버릇 "짹" 사용을 희소화하고, 사용자가 주장한 결과는 대화 전제로 수용하되 공식 수치는 도구 확인 없이 단정하지 않도록 했습니다.
- 빌드 결과물(lib)과 에이전트 작업 상태(.omc, .omx)를 제외하여 저장소의 청결도를 유지합니다.
- 스크립트 실행 시 생성되는 임시 데이터 폴더(tmp)를 추가하여 불필요한 파일의 버전 관리를 방지했습니다.
- 프로젝트 개발 환경에서 발생하는 에디터 및 OS 관련 부산물을 일괄 정리하여 관리 효율성을 높였습니다.
- 로컬 전용 작업물인 짹 말투 검수 로그/하니스(chat-review) 폴더를 버전 관리에서 제외했습니다.
- 시스템 프롬프트에서 시사적 데이터(경기 결과·일정·통계 등)를 제거하고, 정체성 정보만 남겨 모델의 '도구 우선 사용' 원칙을 강화했습니다.
- 지식 수준별 가이드를 별도 모듈로 분리하여 시스템이 직접 주입하도록 개선함으로써 교차 유저 캐시 히트율을 최적화했습니다.
- 사용자의 감정적 반응에 우선순위를 두도록 대화 지침을 보완하여 AI의 공감 능력을 향상하고 불필요한 정보 제공을 방지했습니다.
특정 uid로 실제 도구·모델 루프를 돌려 호출된 도구·토큰·속도·캐시 히트를
보고하는 /debug/chatToolsSheet, /debug/chatProbe(Sheet) 엔드포인트를 추가한다
(요청별 model 오버라이드로 A/B 가능). prod에 같은 검증을 돌리는
scripts/chat-tools-sheet.ts도 추가한다.
무인증 + 임의 uid의 개인 데이터를 노출하는 검수용 임시 엔드포인트다.
기본 모델을 gemini-3.1-flash-lite로, 전 모델에 공평한 24초 호출 예산과
2048 토큰 출력 한도로 맞추고, Gemini 3 모델은 thinkingLevel을 쓴다.
개인화 값(닉네임·지식수준)을 공통 프롬프트에서 빼서 모든 유저 공통의
정적 prefix로 만들어 Vertex 교차-유저 implicit caching 히트율을 높인다.
순위(get_team_rank_snapshot)·출석(get_my_attendance_this_month)·예측 복기
(get_prediction_breakdown_date) 도구를 추가하고, 상대 날짜(dayOffset)와
리그 전체 로스터 조회를 지원한다.
모델이 호출한 도구(이름·인자)를 전송 응답과 메시지 이력에 함께 실어
클라이언트가 나중에 활용할 수 있게 한다.
- 특정 경기에서 활약한 선수(WPA 상위)를 조회하는 `get_game_standouts` 도구를 추가하고, 시스템 프롬프트에 과거 기록 검증 로직을 강화했습니다.
- Gemini 3.0 이상 모델의 Thinking 설정을 지원하도록 Provider 계층을 개선하여 모델 세대별 최적의 추론 환경을 구성했습니다.
- 도구 우선 사용 원칙을 구체화하여 AI가 경기 데이터의 사실 관계를 스스로 확인하고 답변하도록 환각 방지 로직을 보완했습니다.
- KBO 경기 라인업(타순) 및 선수 기록을 실시간으로 조회하는 AI 도구(Function Calling) 체계를 구현했습니다.
- Anthropic 및 Google Gemini 모델의 Tool-loop를 지원하도록 Provider 계층을 개선하여, 시점 의존적인 데이터를 모델이 필요할 때만 스스로 조회하도록 최적화했습니다.
- 시스템 프롬프트에 '도구 우선 사용' 원칙을 추가하여 환각(Hallucination) 현상을 방지하고, 미발표 정보나 경기 미진행 시 정확한 사실 확인 로직을 강화했습니다.
- AI 챗봇 '짹'의 페르소나 정의 및 시스템 프롬프트 상수를 추가했습니다.
- 채팅 이력, 쿼터 관리, 요청 멱등성 보장을 위한 Firestore 데이터 모델을 구현했습니다.
- 위기 상황 대응(자해·위해 예고 등) 및 입력/출력 필터링 파이프라인을 구축했습니다.
- Gemini 및 Anthropic 모델을 지원하는 AI Provider 추상화 계층을 마련했습니다.
- KBO 공식 홈페이지에서 연도별 팀 순위 및 상대 전적 데이터를 가져오는 크롤링 스크립트를 구현했습니다.
- ASP.NET의 ViewState 및 EventValidation 필드를 처리하여 동적인 연도 전환 요청을 지원합니다.
- 데이터를 가독성 높은 콘솔 테이블 형태로 출력하거나 JSON 형식으로 변환하는 기능을 제공합니다.
fetchRankFromKbo awaited each year's getOrFetch sequentially even though
years use independent cache keys. Map to Promise.all so multi-year lookups
run concurrently; order is preserved.
getScoreboard loaded the full user doc just to use 5 fields. Add
getUserForScoreboard using getAll with a fieldMask so only displayName,
photoUrl, tierPoints, favoriteTeamCode, and rankSnapshot are fetched,
cutting per-request bandwidth. The short-lived me-rank cache (meRankCache,
30s) already covers the rank-count side of R5.
The 02:00 cron prefix-deleted all game_detail__ docs, but those already
carry a response-derived dynamic TTL (7d for finished games, 30s live,
etc.). Bulk-wiping them forces a cold-miss stampede of external KBO
detail re-fetches right after the cron. Drop the deletion and let each
key expire on its own TTL. rank__ and schedule_day__ still get the
safety-net invalidation.
W5: getMe synced photoUrl on every read whenever the token URL string
differed, but providers (Google) rotate query params (=sN-c sizing) for
the same image, causing write churn on a read path. Compare only the URL
before '?' so the write fires only on a genuine photo change.
R6: createMe/updateMe re-read the user with getUser right after writing.
Synthesize the response instead — updateMe from the already-read doc plus
the applied patch; createMe from the written inputs (createdAt approximated
as now, since the stored value uses serverTimestamp and later reads reflect
it). Removes one Firestore read per onboarding/profile edit.
dailyArchive wrote users/{uid} twice per cron run: once for rankSnapshot
and once for the judgment. Extract the read-only computeRankSnapshot, then
pass the pre-judgment snapshot into judgeDay so applyDailyJudgmentTx merges
it into the same patch as the judgment write. The snapshot is computed from
current (pre-judgment) tierPoints before the transaction, preserving the
"rank BEFORE judgment" semantics; on the idempotent guard-skip path no
write occurs (and stale re-snapshotting is avoided). snapshotRankForUser is
kept as a thin wrapper for its existing callers/tests.
waitForCache, the schedule month-poll loop, and getOrFetchDynamic all
polled Firestore every fixed 500ms (up to ~50 reads) while waiting for a
lock holder to populate the cache. Replace the fixed interval with a
shared backoffDelayMs helper (300ms base, doubling, 5s cap) bounded by the
same ~25s timeout, cutting per-waiter read amplification by roughly 5x.
Timeout/fallback semantics are unchanged.
During dailyArchive, reconcileDayVotes may call processGameEndWithGame
per unjudged game, each invalidating every voter's stats cache. Since
dailyArchive already invalidates each archived user once at the end of
its per-user loop, the per-game fan-out is redundant. Add an opt-in
skipInvalidate flag and use it from reconcile. The live onGameCompleted
trigger path keeps invalidating as before. Stale data is impossible:
getStats also self-invalidates on the daily forDate rollover.
dailyArchive wrote the voteHistory doc twice per active user per day:
once data-only before judgeDay, then again with judgment fields inside
judgeDay. Drop the pre-write so judgeDay is the single writer in the
common path. To keep data safety: judgeDay now persists the doc before
userVotes is removed; on the idempotent guard-skip path judgeDay restores
data only if the doc is missing (crash-after-tx recovery); and if judgeDay
throws, dailyArchive still saves data-only before removal, preserving the
prior failure-path behavior. judgment/points/streak results unchanged.
judgeDay and hasMissedGameDayBetween re-ran the same listByDate query for
every user in an archive run (U× amplification for the archived date plus
its look-back window). Introduce a memoizing GameDayCache and thread one
instance through the whole runDailyArchive pass so each distinct date is
read from Firestore once. Off-day detection is naturally memoized via the
cached query results. Standalone callers (statsService) keep their prior
behavior via a per-call default cache. No change to judgment results.
- 출석 독려 푸시 알림 기능과 관련 스케줄러(attendanceReminder)를 삭제했습니다.(클라이언트로 이관)
- 유저 데이터에서 알림 슬롯 필드를 제거하고, 출석 시 수행하던 슬롯 계산 및 저장 로직을 제거했습니다.
- 주간 보너스 확인 로직을 리팩토링하고, getMonth 함수 내 비동기 데이터 조회를 병렬화하여 성능을 개선했습니다.
- KBO 경기 상세 정보에 선발 및 예상 라인업 정보를 포함하고 CLI에서 이를 확인할 수 있는 출력 기능을 추가했습니다.
- 경기가 시작되기 전 KBO API가 빈 데이터를 반환하는 상황(code 200)을 예외 처리하고, 경기 상태에 따라 null을 반환하도록 개선했습니다.
- 종료된 경기는 박스스코어에서 선발 명단을 추출하고, 시작 전 경기는 라인업 분석 API를 호출하는 이원화된 데이터 수집 로직을 구현했습니다.
- 예상 라인업이 확정으로 전환되는 시점을 빠르게 반영하기 위해 라인업 출처에 따라 캐시 TTL을 동적으로 조절하는 기능을 도입했습니다.
- 라인업 파싱 및 경기 시작 전후의 데이터 처리 로직에 대한 검증을 위해 서비스 레이어 테스트를 업데이트했습니다.
- 출석 체크 시 데일리 포인트와 주간/월간 보너스를 지급하며, 멱등성 및 클럭 오차 처리를 포함한 로직을 구현했습니다.
- 포인트 이력 관리를 위한 `pointLedger` 컬렉션과 잔액 추적 리포지토리를 추가했습니다.
- 사용자별 알림 슬롯에 맞춰 FCM 출석 독려 푸시를 발송하는 스케줄러와 설정 API를 도입했습니다.
- RTDB의 희소 배열 응답을 7일 기준 배열로 정규화하여 통계 데이터의 일관성을 강화했습니다.
- 출석 시스템 전반에 대한 단위 및 통합 테스트를 추가하여 안정성을 확보했습니다.
- KBO 경기 취소 시 '우천취소' 등 구체적인 사유(cancelReason)를 파싱하여 저장하는 로직을 추가했습니다.
- 기존의 `syncYesterdayFromLive` 함수를 `forceSyncDay`로 이름을 변경하고, 어제 날짜뿐만 아니라 특정 날짜를 강제로 동기화할 수 있도록 기능을 일반화했습니다.
- Firestore 업데이트 전 기존 데이터와 취소 사유를 비교하는 로직을 추가하여 불필요한 쓰기 비용을 최적화했습니다.
- 다양한 취소 상황과 노트 필드 처리 방식에 대한 회귀 테스트 케이스를 추가하여 신뢰성을 확보했습니다.
- KBO 일정 파싱 로직의 신뢰성을 확보하고 무승부 경기 처리와 관련된 회귀 오류를 방지하기 위해 통합 테스트를 추가했습니다.
- 무승부 상황에서 점수가 정상적으로 추출되어 경기 상태가 'completed'로 올바르게 표시되는지 검증하는 케이스를 포함했습니다.
- 우천 취소, 예정된 경기, 일반적인 승패 경기 등 다양한 시나리오에서 점수와 상태값이 설계된 대로 파싱되는지 확인합니다.
- KBO 경기 결과 파싱 시 무승부(`draw`) 상황의 점수도 추출할 수 있도록 정규표현식을 개선하여 경기 상태가 'scheduled'로 오분류되는 문제를 해결했습니다.
- 승리 팀 코드(`winningTeamCode`)가 없는 무승부 경기에서도 결과 처리가 정상적으로 수행되도록 서비스 레이어의 예외 처리를 수정했습니다.
- 무승부 시 어느 팀에 투표했더라도 모두 정답(`result: true`)으로 처리되도록 투표 결과 정산 및 자가 치유(reconcile) 로직을 업데이트했습니다.
- KBO API 요청 매개변수에 `TeamCode` enum을 적용하여 타입 안정성을 강화하고 서비스 레이어의 불필요한 매핑 로직을 제거했습니다.
- `TEAM_CODES` 상수를 내부 데이터 정규화용 어댑터로 한정하여 외부 API 의존성을 줄였습니다.
- 핸들러 단계에서 유효한 팀 코드인지 검증하는 로직을 추가하여 잘못된 요청에 대한 에러 처리를 강화했습니다.
- `getPlayerStats` 함수의 인자 전달 방식을 객체 형태로 리팩토링하여 가독성과 유지보수성을 높였습니다.
- 클라이언트 응답 전용 `UserProfile` 인터페이스를 도입하여 내부 도메인 모델과의 의존성을 분리하고 데이터 보안을 강화했습니다.
- 스트릭, 티어, 티켓 등 지연 보정이 필요하거나 통계 성격이 강한 필드를 응답에서 제외하고 `/stats` API로 데이터 서빙 경로를 일원화했습니다.
- 특히 DB의 스트릭 값은 지연 보정(lazy correction) 전의 상태일 수 있어, 사용자에게 잘못된 정보가 노출되는 것을 방지했습니다.
- KBO 팀 순위 데이터를 단순 문자열 파싱에서 숫자형 및 구조화된 타입(`WinLoss`, `WinLossDraw`)으로 개선하여 데이터의 신뢰도와 활용도를 높였습니다.
- `getMe`, `createMe`, `updateMe` 등 사용자 관련 서비스 로직이 정제된 프로필 형식을 반환하도록 수정했습니다.
- 사용자의 마지막 참여일 이후 실제 경기가 있었음에도 참여하지 않은 경우를 감지하여 스트릭을 0으로 초기화하는 지연 보정(lazy correction) 로직을 도입했습니다.
- KBO 휴장일(월요일, 전체 우천 취소 등)을 자동으로 식별하여 실제 경기가 열린 날에만 결석 판정이 내려지도록 `hasMissedGameDayBetween` 검증 기능을 구현했습니다.
- 실시간 라이브 데이터를 기반으로 전일 경기 결과를 확정하는 기능을 추가하여 월간 일정 갱신 과정에서 발생할 수 있는 데이터 누락을 보완했습니다.
- 일일 아카이브 및 경기 동기화 로직을 수동으로 트리거할 수 있는 디버그 엔드포인트를 추가하고, 통계 데이터에 산출 기준일(forDate)을 포함하여 캐시 무효화 정밀도를 개선했습니다.
- Naver Sports의 highLight 엔드포인트를 통해 득점 발생 타석의 상세 데이터를 추출하는 기능을 구현했습니다.
- 단순 이닝별 점수 보강을 넘어 타석 결과, 주자 이동, 아웃 카운트 및 베이스 상태 변화를 포함한 정밀한 정보를 제공합니다.
- KBO와 네이버 간의 경기 ID 체계 차이를 자동으로 처리하며, 병렬 요청 구조로 전체 조회 성능을 최적화했습니다.
- 득점 주자의 출루 위치와 타점 발생 상황을 구조화된 데이터로 파싱하여 상세 조회 기능을 고도화했습니다.
- KBO 게임센터의 스코어보드, 박스스코어, 키플레이어 데이터를 통합 조회하는 기능을 구현했습니다.
- 경기 상태(종료, 진행 중, 시작 전)에 따라 TTL을 30초에서 7일까지 유연하게 적용하는 동적 캐싱 시스템을 도입했습니다.
- KBO 공식 데이터에서 이닝별 득점이 누락되는 경우를 대비해 네이버 스포츠 API를 통한 데이터 보완 로직을 추가했습니다.
- Firestore의 중첩 배열 저장 제한을 회피하기 위해 테이블 데이터를 객체 배열 구조로 파싱하도록 설계했습니다.
- CLI 상세 조회 명령어와 REST API 엔드포인트를 추가하고, HTML 엔티티 디코딩 및 관련 단위 테스트를 보완했습니다.
- 전체 및 팀별 스코어보드 기능을 구현하고 순위 변동 시각화를 위한 스냅샷 시스템을 도입했습니다.
- `dailyArchive` 시점에 `rankSnapshot`을 기록하여 이전 대비 순위 변화량을 정확히 산출합니다.
- 상위권 데이터를 RTDB에 사전 계산하여 저장하고 `MemCache`를 적용해 조회 성능을 최적화했습니다.
- Firestore 복합 색인과 Count Aggregation으로 동점자 처리 및 상위 퍼센타일 산출 로직을 구현했습니다.
- 주간 결과 타입을 `DailyJudgment`로 전환하여 판정 세부 상태가 통계 응답에 포함되도록 수정했습니다.
- 전체 및 팀별 스코어보드 기능을 구현하고, 순위 변동(delta)을 시각화하기 위한 스냅샷 시스템을 도입했습니다.
- `dailyArchive` 과정에서 사용자의 현재 순위를 `rankSnapshot`으로 기록하여, 다음 판정 시점의 순위 변화량을 정확히 계산합니다.
- 상위권 데이터는 RTDB에 사전 계산(precompute)하여 저장함으로써 대규모 조회 요청에 대응하고, `MemCache`를 통해 API 응답 성능을 최적화했습니다.
- Firestore 복합 색인과 Count Aggregation을 활용하여 동점자 처리가 포함된 랭킹 및 상위 퍼센타일 산출 로직을 구현했습니다.
- 관련 서비스 레이어, 저장소 함수, API 핸들러 및 단위 테스트 코드를 추가했습니다.
- `dailyArchive` 과정에 `judgeDay`를 통합하여 경기 결과에 따른 일간 판정, 스트릭 업데이트, 티어 포인트 가산을 자동화했습니다.
- 주간 모든 경기를 성공적으로 예측했을 때 '주간 마스터 티켓'을 지급하는 `judgeWeekIfNeeded` 로직을 구현했습니다.
- Firestore 트랜잭션을 사용하여 판정 결과 반영의 원자성을 확보하고, 중복 처리를 방지하는 멱등성 가드를 적용했습니다.
- `statsService`가 실시간 계산 대신 사용자 문서에 저장된 스트릭과 티어 정보를 활용하도록 고도화하고 관련 유틸리티와 테스트 코드를 추가했습니다.
- KBO 리그의 경기 일정 특성을 반영하여 주간 통계 산출 기준을 기존 ISO 주차 방식에서 화요일~월요일 주기로 변경했습니다.
- `tuesdayOf` 유틸리티를 추가하여 특정 날짜가 속한 주의 시작일(화요일)을 기준으로 주간 데이터를 판정하고 집계하도록 개선했습니다.
- 서비스 내의 모든 주요 함수에 JSDoc 주석을 추가하여 각 로직의 역할과 매개변수에 대한 상세한 설명을 보완했습니다.
- 코드 포맷팅을 정리하고 불필요한 ISO 주차 계산 로직을 제거하여 통계 산출 파이프라인을 최적화했습니다.