Repository navigation
Conversation
* 🔧 chore: 환경별 스토리지 설정 처리 개선 * 🔧 chore: MSW를 전부 켜거나 전부 끄는 옵션 추가 인증만 mock이고 일부 요청만 실제 서버로 보내면, mock이 발급한 토큰을 서버가 검증하지 못해 401이 난다. 그래서 부분 활성화를 막고 VITE_ENABLE_MSW=false 로 한 번에 끄는 길만 남긴다. 원본 커밋(2c1e26e)에는 인증 응답 파싱·로그인 상태 동기화 수정이 함께 있었으나, 그 부분은 dev 가 자체적으로 고쳐 최종 결과가 동일해졌다. 낡은 구현을 되살리지 않기 위해 의도적으로 제외했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix: 밴드 목록 mock 응답을 실제 계약에 맞춤 밴드 목록 mock이 실제 응답과 모양이 달라 목록 테스트가 실제 계약을 검증하지 못했다. 원본 커밋(a3a2fde)에는 밴드·프로필 스키마와 profile-api 파싱 수정이 함께 있었으나, dev 가 #254(GET /users 응답에 대표 세션 추가)에서 같은 파일들을 다시 썼다. 그쪽이 더 새로운 계약이므로 의도적으로 제외하고 mock·테스트 두 파일만 남긴다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat: 자연어 밴드 데이터 조회 UI 추가 * 🔧 chore: 프론트 의존성 빌드 스크립트 허용 * ✨ feat: 구조화 LLM 호출과 provider fallback 추가 * ✨ feat: Text-to-SQL 생성과 AST 검증 추가 * ✨ feat: 자연어 밴드 데이터 조회 API 추가 * 📝 docs: 자연어 밴드 조회 API 명세 추가 * ✨ feat: LLM key와 provider 순환 제어 추가 * ✨ feat: 자연어 질문 사용자별 호출 제한 추가 * 🔧 chore: LLM 순환과 호출 제한 환경 변수 추가 * 🔧 chore: 개발 환경변수 동기화 검증 제거 * 🧪 test: 추가 자연어 조회 평가기와 재현 도구 * 🐛 fix: 수정 선호 장르 테이블명과 추가 스키마 계약 검사 * 🐛 fix: 추가 팀장 조회 관계와 밴드 범위 회귀 검사 * 🔧 chore: 변경 실험 결과 보관을 저장소 외부 경로로 제한 * ✨ feat: 추가 질문별 관계 맥락과 오류별 SQL 복구 지시 * ✨ feat: 추가 자유 SQL 집계 검증과 정형 결과 전달 정책 * ✨ feat: 추가 자연어 조회 전체 결과와 제한 안내 화면 * 🧪 test: 추가 다중 세션 평가 데이터와 별도 평가 문항 1인 1행 데이터에서는 중복 집계·중복 명단이 정답으로 채점돼 이전 50/50이 실제로는 42/50이었다. 겸업 멤버의 세션 행을 추가하고 수정에 사용하지 않는 holdout 20문항을 구조 수정 전에 고정했다. * 🐛 fix: SQL 재생성에서 고친 규칙을 다시 어기는 문제 수정 검증기가 첫 의미 위반에서 멈추고 재생성에는 직전 실패만 전달해, 관계를 고치면 DISTINCT를 빠뜨리고 DISTINCT를 고치면 관계를 다시 빠뜨렸다. 보안·구문 검사는 즉시 거부를 유지하고 의미 규칙 위반은 모두 모아 알리며, 재생성에는 모든 시도의 위반과 직전 SQL을 함께 준다. * ✨ feat: 추가 참여·팀 편성의 사람 단위 실행 겸업 멤버는 같은 일정·팀에 세션 행이 여럿이라 모델이 DISTINCT를 빠뜨리면 인원과 명단이 중복됐다. 집계 단위를 질문 정규식으로 추측해 거부하는 대신, 검증을 통과한 SQL의 참여·팀 편성 테이블을 서버가 (일정, 멤버)·(팀, 멤버)당 한 행인 서브쿼리로 바꿔 실행한다. 결과 조건 표시와 평가기는 서브쿼리 별칭을 원래 테이블로 해석한다. * ⚡ perf: 사람 단위 실행을 인덱스로 찾는 대표 행 조건으로 변경 GROUP BY 서브쿼리는 다른 밴드 행까지 참여 테이블 전체를 매번 집계했다. 같은 (일정, 멤버)에서 id가 가장 작은 행만 남기는 NOT EXISTS 조건으로 바꿔 기존 (일정, 멤버, 세션) 인덱스를 사용한다. 이전 실행 SQL 96건의 결과는 두 구현이 같다. 서브쿼리 별칭 해석은 더 이상 필요 없어 되돌렸다. * 🐛 fix: 수정 대표 행 조회의 서버 별칭 충돌 * 🧪 test: 추가 대표 관계의 평가 컬럼 대응 검증 * 🐛 fix: 수정 대표 행 변환의 외부 조인 결과 누락 * 🐛 fix: 수정 상위 조회의 정수 LIMIT 파라미터 검증 * ✨ feat: 추가 밴드 데이터 기반 아티스트 이름 확인 * ✨ feat: 추가 자연어 조회 응답 종류와 사람이 읽는 결과 문구 요약은 행을 다시 나열하지 않고 결론 한 줄만 만든다. 조건은 최상위 AND마다 칩 하나로 나누고 enum·UTC 시각 대신 한글 값·한국 시간·지금·기간으로 보여준다. 응답에 kind와 결과 제목을 넣고, 재생성까지 SQL을 만들지 못한 경우는 503 대신 질문을 바꿔 달라는 REPHRASE 응답으로 돌려준다. 아티스트 확인은 누르면 바로 보낼 질문을 담은 후보 배열로 주고, 추천 질문은 모델 없이 고정 SQL을 같은 검증기로 확인해 실행한다. * ✨ feat: 추가 물어보기 전체 화면 시트와 결과 유형별 화면 홈에는 입력 한 줄과 짧은 추천 칩만 두고 답은 하단 입력창이 고정된 전체 화면 시트에서 보여준다. 집계는 큰 숫자, 한 건은 카드, 이름 목록은 칩, 여러 열은 순위와 열 이름을 붙인 세로 목록으로 그린다. 조건 칩, 단계별 로딩, 이어서 물어보기, 답할 수 없음·질문 변경·이름 확인의 다음 행동, 실패 시 질문 되돌리기와 다시 시도를 추가했다. * 🐛 fix: 수정 물어보기 결과에서 같은 내용을 두 번 보여주던 문구 실제 화면에서 0건·한 건·상위 N개는 요약이 결과를 다시 읽고, 이름 확인은 후보를 문장과 칩에 두 번 보여줬다. 이 경우 헤드라인은 결과 제목만 쓰고, 이름 확인 문장은 후보 개수만 말한다. 다시 시도 버튼의 테두리 대비도 높였다. * 🐛 fix: 수정 이름 확인 문구가 곡 검색처럼 읽히던 표현 아티스트 이름 후보는 외부 곡 검색이 아니라 현재 밴드의 곡 목록에서 찾는다. '눌러서 바로 찾기', '찾는 이름이 없으면'처럼 검색 서비스로 읽히던 문구를 '밴드 곡 목록에 있는 이름'이 드러나게 바꿨다. * ✨ feat: 추가 추천 질문별 이어서 물어볼 질문 이어서 물어보기에 다른 추천 질문을 보여주면 방금 질문과 맞지 않았다. 추천 질문마다 그 답에 이어지는 자유 질문을 정의해 추천 질문 목록 API로 내려준다. 화면은 이 질문을 question으로 보내 모델이 처리한다. 합성 평가 DB에서 실제 모델로 답을 확인한 질문만 넣었다. * ✨ feat: 추가 홈 입력과 대화형 물어보기 화면 홈 검색바에서 바로 입력하고, 보내거나 추천 칩을 누르면 대화 화면이 아래에서 올라와 화면 전체를 덮는다. 질문은 말풍선으로, 답은 입력 중 표시 뒤에 결론·조건과 결과·이어서 물어보기 순서로 나타나며 이어서 물은 질문은 아래에 쌓인다. 추천 질문처럼 응답이 빨라도 답하는 중 표시를 최소 0.7초 보여준다. 뜻이 모호하던 '접기'·'수정' 글자 링크는 닫기 아이콘과 실패 시 다시 시도로 바꾸고, 10건을 넘는 결과는 처음 10건만 보여준 뒤 펼친다. PC에서도 뒤 화면이 비치지 않게 바탕을 화면 전체 폭으로 깔았다. * 🐛 fix: 추천 질문을 질문 횟수 제한에서 제외 추천 질문은 모델을 호출하지 않아 비용이 들지 않는데도 사용자당 5분 10회 제한에 함께 세져, 추천 칩과 이어서 물어보기를 몇 번 누르면 자유 질문을 못 하게 됐다. presetId가 있는 요청은 서비스와 같은 기준으로 고정 SQL 경로이므로 제한 가드에서 건너뛴다. * 🐛 fix: 출력 토큰 한도로 잘린 LLM 응답을 provider 장애와 구분 응답이 maxOutputTokens에서 잘리면 JSON 파싱 실패로 처리돼 재시도 없이 다음 provider로 넘어가고, 로그에는 provider 전환만 남아 토큰 부족이 장애처럼 보였다. Gemini의 finishReason MAX_TOKENS와 OpenAI 호환 finish_reason length를 별도 오류로 구분하고, 같은 provider에 한도를 두 배(최대 4096)로 늘려 한 번만 다시 부른다. 그래도 잘리면 출력 토큰 한도 부족으로 로그를 남기고 다음 호출 대상으로 넘긴다. * 💄 style: 포커스 링 색을 primary로 변경 다크 테마에서 --color-ring이 secondary(#1a1971)를 가리켜 버튼 포커스 표시가 배경에 묻혔다. * 💄 style: 물어보기 화면을 기존 화면 디자인 패턴에 맞춤 - 말풍선·결과 카드 바탕을 bg-surface-3로 바꿔 그라데이션 배경에서 구분되게 함 - 홈 카드 테두리, 추천 칩 구분선, 결과 행 구분선을 없애고 간격으로 나눔 - 머리·바닥을 다른 전체 화면 모달처럼 header-glow/footer-glow로 나눔 - 입력창은 공용 Input 기본값을 쓰고, 질문 칩은 일정 필터 칩 모양으로 통일 - 다시 시도 버튼을 기존 outline 버튼 색 조합으로 맞춤 * 🐛 fix: 페이지를 오갈 때 스크롤바 폭만큼 화면이 밀리던 문제 스크롤바가 자리를 차지하는 PC 환경에서 긴 페이지(밴드 홈)와 짧은 페이지(아카이브)를 오가면 가운데 정렬된 화면 전체가 좌우로 밀렸다. html에 scrollbar-gutter: stable을 두어 스크롤바 자리를 항상 비워 둔다. * ✨ feat: 추가 물어보기 헤더 진입점과 이어서 보기 바 - 대화 상태를 앱 레이아웃의 Provider로 올려 화면을 닫고 밴드 안 다른 페이지로 옮겨도 대화가 남게 함 - 밴드 홈·아카이브·라이브러리 헤더에 물어보기 아이콘 추가 - 대화 화면을 닫으면 하단 탭바 위에 이어서 보기 바를 띄우고, X로 대화를 끝낼 수 있게 함 - 대화 화면 머리에 새 대화 버튼 추가, 경로가 바뀌면 화면만 내리고 대화는 유지 - 밴드 홈 카드에서 묻는 질문은 항상 새 대화로 시작 - 대화가 밴드를 바꿀 수 있어 질문 API 훅이 bandId를 호출마다 받도록 변경 * 💄 style: 물어보기 화면 머리·바닥 바를 화면 폭에 맞춤 바가 앱 폭(648) 컨테이너 안에 있어 PC에서 화면 가운데에만 떠 보였다. 바는 화면 폭을 다 채우고 안의 내용만 앱 폭으로 가운데 둔다. * 🐛 fix: 모달을 열면 오른쪽에 스크롤바 폭만큼 띠가 생기던 문제 스크롤을 잠글 때 Radix가 body에 스크롤바 폭만큼 margin-right를 주는데, scrollbar-gutter: stable로 비워 둔 자리까지 남아 두 번 보정됐다. 잠긴 동안에는 자리를 비우지 않게 한다. * ✨ feat: 물어보기 후속 질문에 직전 턴의 검증된 SQL을 맥락으로 전달 "그중에"처럼 앞 결과를 가리키는 질문은 조건 칩만으로 WHERE 절을 복원하지 못한다. 답마다 원래 SQL·실행 파라미터·결과 건수를 context로 돌려주고, 다음 요청의 history(최대 3턴)로 받는다. 클라이언트가 보낸 SQL은 실행 SQL과 같은 검증을 다시 통과해야 하며 파싱 후 다시 만든 문장만 프롬프트에 넣어 주석에 숨긴 지시문을 지운다. 결과 행과 답 문장은 보내지 않는다. 짧은 후속 질문은 현재 질문으로 인원 단위가 정해지지 않을 때만 바로 앞 질문과 이어 붙여 판별한다. 사람 단위 변환은 한 일정 안의 중복만 없애므로 여러 일정에 걸친 인원 검사는 유지한다. 대화 턴 상한(5)을 DTO에서 막고 turnIndex를 로그에 남긴다. * ✨ feat: 물어보기 대화에서 직전 턴을 함께 보내고 질문 5개에서 새 대화를 권함 이어서 묻는 질문에만 직전 답의 조회 구조(최대 3턴)와 몇 번째 질문인지를 붙인다. 첫 질문의 요청 본문은 그대로다. 질문이 5개 쌓이면 입력창 대신 새 대화 안내를 보여주고 이어서 묻기 버튼을 숨긴다. * 🧪 test: 직전 턴을 고정한 후속 질문 평가 문항 추가 조건 이어받기 4문항, 생략된 대상 이어받기 2문항, 이력을 무시해야 하는 대조 1문항이다. 직전 턴은 정답 SQL로 고정해 첫 질문 정답 여부와 무관하게 지시어 해석만 보고, 제품과 같은 재검증을 거쳐 이력으로 넣는다. * 📝 docs: 평가 README에 후속 질문 평가 세트 설명 추가 * 🐛 fix: LEFT JOIN·서브쿼리·출력 별칭으로 밴드 범위와 컬럼 허용 목록을 우회하던 문제 범위 루트를 쿼리 전체에서 모으던 방식을 SELECT마다 따로 검사하도록 바꾼다. 범위 루트는 각 SELECT의 WHERE 최상위 AND 조건에서만 인정하고, 서브쿼리는 바깥에서 이미 범위 안인 별칭에만 기댈 수 있다. LEFT JOIN은 오른쪽, RIGHT JOIN은 왼쪽만 ON 조건으로 범위 안에 넣는다. - LEFT JOIN bands ON ... AND b.id = $1 은 매칭되지 않은 행의 b.deleted_at이 NULL이라 다른 밴드 행이 남았다. - NOT EXISTS 안의 b.id = $1 이 바깥 테이블의 범위로 인정됐다. - SELECT 목록의 맨 이름은 PostgreSQL이 실제 컬럼으로 해석하는데, 같은 이름의 출력 별칭이 있으면 허용 목록 검사를 건너뛰었다. 이제 같은 SELECT의 ORDER BY 항목일 때만 출력 별칭으로 인정한다. 10/11 평가의 모델 출력 276건을 새 검증기로 다시 검사해 통과·거부 결과가 모두 같음을 확인했다. * 🐛 fix: 빈 presetId로 물어보기 질문 횟수 제한을 건너뛰던 문제 Guard는 DTO 변환 전 원본 body를 보므로 빈 문자열·공백 presetId도 추천 질문으로 보고 제한을 건너뛰었다. DTO는 이를 undefined로 바꿔 자유 질문으로 모델을 호출한다. 앞뒤 공백을 지운 값이 실제 추천 질문 ID일 때만 제한에서 뺀다. * 🐛 fix: 물어보기 요청 하나의 모델 호출 시간을 9초로 제한하고 감사 로그의 연락처를 가림 재생성 3회와 provider 재시도, 출력 잘림 재시도가 곱해져 한 요청이 2분을 넘길 수 있었다. 화면 요청은 10초에 끊기므로 SQL 생성 전체에 9초 마감을 두고, LlmService는 마감을 넘기면 재시도와 다음 호출을 멈추고 남은 시간만큼만 기다린다. 10/11 평가에서 가장 오래 걸린 생성은 4.7초였다. 감사 로그는 userId·bandId와 함께 남으므로 질문의 이메일·전화번호를 가리고, DB 오류는 바인딩 값이 섞일 수 있는 원문 대신 오류 이름과 코드만 남긴다. * 🐛 fix: LLM 재시도 0 설정이 무시되고 거절 응답이 TypeError로 기록되던 문제 LLM_MAX_RETRIES=0이 양수 파서에서 거부돼 기본값 2로 돌아갔다. OpenAI 호환 provider는 거절·도구 호출 응답의 content가 null이라 null.trim()에서 실패했다. 순환 토글을 끄면 첫 provider·첫 key만 쓰는 현재 동작에 맞게 설정 주석도 고친다. * 🔧 chore: 로컬·테스트가 아니면 스토리지 설정을 요구하고 개발 secret 검사를 되살림 NODE_ENV가 없거나 staging이면 AWS 설정 없이 부팅되던 것을 운영처럼 막는다. 개발 환경변수 동기화는 필수 키 검사 없이도 객체 형태, NODE_ENV=development, 키 이름 형식을 확인해 다른 환경 secret이 개발 파일에 써지지 않게 한다. * 🐛 fix: 닫은 대화의 실패한 질문이 입력창에 다시 채워지고 다른 계정에 대화가 남던 문제 늦게 실패한 질문은 그 턴이 지금 대화에 있고 입력창이 비어 있을 때만 되돌린다. 계정이 바뀌면 대화 Provider를 새로 만든다. 시트를 열면 입력창 대신 시트 자체에 포커스를 둬 키보드를 올리지 않으면서 Tab이 시트 밖으로 나가지 않게 하고, 닫을 때 포커스가 body로 가면 헤더의 물어보기 버튼으로 보낸다. * 🐛 fix: 서버 결과 행 상한을 화면 schema에 고정해 두던 문제 maxRows를 50으로 고정해 서버 상한이 바뀌면 모든 표 응답이 파싱 실패로 보였다. 값은 양의 정수로만 보고 행 수가 maxRows 이하인지만 확인한다. * ✨ feat: 자연어 조회의 모델 SQL을 컬럼 권한과 밴드 RLS가 걸린 전용 DB 역할로 실행 보안 경계가 앱 레벨 AST 검증기 하나뿐이라 검증기에 구멍이 생기면 앱 계정 권한 그대로 모든 테이블·컬럼이 열렸다. 모델 SQL을 실행할 때만 읽기 전용 트랜잭션 안에서 SET LOCAL ROLE assistant_reader로 바꾸고 set_config('app.band_id', ..., true)를 건다. - assistant_reader는 카탈로그 허용 컬럼에만 SELECT 권한이 있다. users.email 같은 컬럼은 권한 오류가 난다. - 18개 테이블에 assistant_reader 전용 RLS 정책을 둬 app.band_id 밴드의 행만 보인다. 설정이 없으면 아무 행도 보이지 않는다. 테이블 소유자인 앱 계정의 다른 조회에는 영향이 없다. - 앱 계정이 테이블 소유자가 아니면 RLS를 켜는 순간 앱 조회가 막히므로 그 경우 마이그레이션을 실패시킨다. - 사람 단위 변환의 SELECT *는 카탈로그 컬럼 목록으로 바꿨다. 컬럼 권한과 카탈로그가 어긋나지 않게 카탈로그 테스트에서 마이그레이션의 GRANT와 대조한다. 평가 DB에서 10/11 평가의 실행 SQL 231건을 전용 역할과 앱 계정으로 각각 실행해 결과가 모두 같음을 확인했다(1건은 ORDER BY가 없어 행 순서만 다름). 리뷰의 LEFT JOIN 우회는 자기 밴드 행만, NOT EXISTS 우회는 0행을 돌려준다. DB 실행 p95는 4.6ms에서 13.4ms가 됐다. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* 🔧 chore: 환경별 스토리지 설정 처리 개선 * 🔧 chore: MSW를 전부 켜거나 전부 끄는 옵션 추가 인증만 mock이고 일부 요청만 실제 서버로 보내면, mock이 발급한 토큰을 서버가 검증하지 못해 401이 난다. 그래서 부분 활성화를 막고 VITE_ENABLE_MSW=false 로 한 번에 끄는 길만 남긴다. 원본 커밋(2c1e26e)에는 인증 응답 파싱·로그인 상태 동기화 수정이 함께 있었으나, 그 부분은 dev 가 자체적으로 고쳐 최종 결과가 동일해졌다. 낡은 구현을 되살리지 않기 위해 의도적으로 제외했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix: 밴드 목록 mock 응답을 실제 계약에 맞춤 밴드 목록 mock이 실제 응답과 모양이 달라 목록 테스트가 실제 계약을 검증하지 못했다. 원본 커밋(a3a2fde)에는 밴드·프로필 스키마와 profile-api 파싱 수정이 함께 있었으나, dev 가 #254(GET /users 응답에 대표 세션 추가)에서 같은 파일들을 다시 썼다. 그쪽이 더 새로운 계약이므로 의도적으로 제외하고 mock·테스트 두 파일만 남긴다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat: 자연어 밴드 데이터 조회 UI 추가 * 🔧 chore: 프론트 의존성 빌드 스크립트 허용 * ✨ feat: 구조화 LLM 호출과 provider fallback 추가 * ✨ feat: Text-to-SQL 생성과 AST 검증 추가 * ✨ feat: 자연어 밴드 데이터 조회 API 추가 * 📝 docs: 자연어 밴드 조회 API 명세 추가 * ✨ feat: LLM key와 provider 순환 제어 추가 * ✨ feat: 자연어 질문 사용자별 호출 제한 추가 * 🔧 chore: LLM 순환과 호출 제한 환경 변수 추가 * 🔧 chore: 개발 환경변수 동기화 검증 제거 * 🧪 test: 추가 자연어 조회 평가기와 재현 도구 * 🐛 fix: 수정 선호 장르 테이블명과 추가 스키마 계약 검사 * 🐛 fix: 추가 팀장 조회 관계와 밴드 범위 회귀 검사 * 🔧 chore: 변경 실험 결과 보관을 저장소 외부 경로로 제한 * ✨ feat: 추가 질문별 관계 맥락과 오류별 SQL 복구 지시 * ✨ feat: 추가 자유 SQL 집계 검증과 정형 결과 전달 정책 * ✨ feat: 추가 자연어 조회 전체 결과와 제한 안내 화면 * 🧪 test: 추가 다중 세션 평가 데이터와 별도 평가 문항 1인 1행 데이터에서는 중복 집계·중복 명단이 정답으로 채점돼 이전 50/50이 실제로는 42/50이었다. 겸업 멤버의 세션 행을 추가하고 수정에 사용하지 않는 holdout 20문항을 구조 수정 전에 고정했다. * 🐛 fix: SQL 재생성에서 고친 규칙을 다시 어기는 문제 수정 검증기가 첫 의미 위반에서 멈추고 재생성에는 직전 실패만 전달해, 관계를 고치면 DISTINCT를 빠뜨리고 DISTINCT를 고치면 관계를 다시 빠뜨렸다. 보안·구문 검사는 즉시 거부를 유지하고 의미 규칙 위반은 모두 모아 알리며, 재생성에는 모든 시도의 위반과 직전 SQL을 함께 준다. * ✨ feat: 추가 참여·팀 편성의 사람 단위 실행 겸업 멤버는 같은 일정·팀에 세션 행이 여럿이라 모델이 DISTINCT를 빠뜨리면 인원과 명단이 중복됐다. 집계 단위를 질문 정규식으로 추측해 거부하는 대신, 검증을 통과한 SQL의 참여·팀 편성 테이블을 서버가 (일정, 멤버)·(팀, 멤버)당 한 행인 서브쿼리로 바꿔 실행한다. 결과 조건 표시와 평가기는 서브쿼리 별칭을 원래 테이블로 해석한다. * ⚡ perf: 사람 단위 실행을 인덱스로 찾는 대표 행 조건으로 변경 GROUP BY 서브쿼리는 다른 밴드 행까지 참여 테이블 전체를 매번 집계했다. 같은 (일정, 멤버)에서 id가 가장 작은 행만 남기는 NOT EXISTS 조건으로 바꿔 기존 (일정, 멤버, 세션) 인덱스를 사용한다. 이전 실행 SQL 96건의 결과는 두 구현이 같다. 서브쿼리 별칭 해석은 더 이상 필요 없어 되돌렸다. * 🐛 fix: 수정 대표 행 조회의 서버 별칭 충돌 * 🧪 test: 추가 대표 관계의 평가 컬럼 대응 검증 * 🐛 fix: 수정 대표 행 변환의 외부 조인 결과 누락 * 🐛 fix: 수정 상위 조회의 정수 LIMIT 파라미터 검증 * ✨ feat: 추가 밴드 데이터 기반 아티스트 이름 확인 * ✨ feat: 추가 자연어 조회 응답 종류와 사람이 읽는 결과 문구 요약은 행을 다시 나열하지 않고 결론 한 줄만 만든다. 조건은 최상위 AND마다 칩 하나로 나누고 enum·UTC 시각 대신 한글 값·한국 시간·지금·기간으로 보여준다. 응답에 kind와 결과 제목을 넣고, 재생성까지 SQL을 만들지 못한 경우는 503 대신 질문을 바꿔 달라는 REPHRASE 응답으로 돌려준다. 아티스트 확인은 누르면 바로 보낼 질문을 담은 후보 배열로 주고, 추천 질문은 모델 없이 고정 SQL을 같은 검증기로 확인해 실행한다. * ✨ feat: 추가 물어보기 전체 화면 시트와 결과 유형별 화면 홈에는 입력 한 줄과 짧은 추천 칩만 두고 답은 하단 입력창이 고정된 전체 화면 시트에서 보여준다. 집계는 큰 숫자, 한 건은 카드, 이름 목록은 칩, 여러 열은 순위와 열 이름을 붙인 세로 목록으로 그린다. 조건 칩, 단계별 로딩, 이어서 물어보기, 답할 수 없음·질문 변경·이름 확인의 다음 행동, 실패 시 질문 되돌리기와 다시 시도를 추가했다. * 🐛 fix: 수정 물어보기 결과에서 같은 내용을 두 번 보여주던 문구 실제 화면에서 0건·한 건·상위 N개는 요약이 결과를 다시 읽고, 이름 확인은 후보를 문장과 칩에 두 번 보여줬다. 이 경우 헤드라인은 결과 제목만 쓰고, 이름 확인 문장은 후보 개수만 말한다. 다시 시도 버튼의 테두리 대비도 높였다. * 🐛 fix: 수정 이름 확인 문구가 곡 검색처럼 읽히던 표현 아티스트 이름 후보는 외부 곡 검색이 아니라 현재 밴드의 곡 목록에서 찾는다. '눌러서 바로 찾기', '찾는 이름이 없으면'처럼 검색 서비스로 읽히던 문구를 '밴드 곡 목록에 있는 이름'이 드러나게 바꿨다. * ✨ feat: 추가 추천 질문별 이어서 물어볼 질문 이어서 물어보기에 다른 추천 질문을 보여주면 방금 질문과 맞지 않았다. 추천 질문마다 그 답에 이어지는 자유 질문을 정의해 추천 질문 목록 API로 내려준다. 화면은 이 질문을 question으로 보내 모델이 처리한다. 합성 평가 DB에서 실제 모델로 답을 확인한 질문만 넣었다. * ✨ feat: 추가 홈 입력과 대화형 물어보기 화면 홈 검색바에서 바로 입력하고, 보내거나 추천 칩을 누르면 대화 화면이 아래에서 올라와 화면 전체를 덮는다. 질문은 말풍선으로, 답은 입력 중 표시 뒤에 결론·조건과 결과·이어서 물어보기 순서로 나타나며 이어서 물은 질문은 아래에 쌓인다. 추천 질문처럼 응답이 빨라도 답하는 중 표시를 최소 0.7초 보여준다. 뜻이 모호하던 '접기'·'수정' 글자 링크는 닫기 아이콘과 실패 시 다시 시도로 바꾸고, 10건을 넘는 결과는 처음 10건만 보여준 뒤 펼친다. PC에서도 뒤 화면이 비치지 않게 바탕을 화면 전체 폭으로 깔았다. * 🐛 fix: 추천 질문을 질문 횟수 제한에서 제외 추천 질문은 모델을 호출하지 않아 비용이 들지 않는데도 사용자당 5분 10회 제한에 함께 세져, 추천 칩과 이어서 물어보기를 몇 번 누르면 자유 질문을 못 하게 됐다. presetId가 있는 요청은 서비스와 같은 기준으로 고정 SQL 경로이므로 제한 가드에서 건너뛴다. * 🐛 fix: 출력 토큰 한도로 잘린 LLM 응답을 provider 장애와 구분 응답이 maxOutputTokens에서 잘리면 JSON 파싱 실패로 처리돼 재시도 없이 다음 provider로 넘어가고, 로그에는 provider 전환만 남아 토큰 부족이 장애처럼 보였다. Gemini의 finishReason MAX_TOKENS와 OpenAI 호환 finish_reason length를 별도 오류로 구분하고, 같은 provider에 한도를 두 배(최대 4096)로 늘려 한 번만 다시 부른다. 그래도 잘리면 출력 토큰 한도 부족으로 로그를 남기고 다음 호출 대상으로 넘긴다. * 💄 style: 포커스 링 색을 primary로 변경 다크 테마에서 --color-ring이 secondary(#1a1971)를 가리켜 버튼 포커스 표시가 배경에 묻혔다. * 💄 style: 물어보기 화면을 기존 화면 디자인 패턴에 맞춤 - 말풍선·결과 카드 바탕을 bg-surface-3로 바꿔 그라데이션 배경에서 구분되게 함 - 홈 카드 테두리, 추천 칩 구분선, 결과 행 구분선을 없애고 간격으로 나눔 - 머리·바닥을 다른 전체 화면 모달처럼 header-glow/footer-glow로 나눔 - 입력창은 공용 Input 기본값을 쓰고, 질문 칩은 일정 필터 칩 모양으로 통일 - 다시 시도 버튼을 기존 outline 버튼 색 조합으로 맞춤 * 🐛 fix: 페이지를 오갈 때 스크롤바 폭만큼 화면이 밀리던 문제 스크롤바가 자리를 차지하는 PC 환경에서 긴 페이지(밴드 홈)와 짧은 페이지(아카이브)를 오가면 가운데 정렬된 화면 전체가 좌우로 밀렸다. html에 scrollbar-gutter: stable을 두어 스크롤바 자리를 항상 비워 둔다. * ✨ feat: 추가 물어보기 헤더 진입점과 이어서 보기 바 - 대화 상태를 앱 레이아웃의 Provider로 올려 화면을 닫고 밴드 안 다른 페이지로 옮겨도 대화가 남게 함 - 밴드 홈·아카이브·라이브러리 헤더에 물어보기 아이콘 추가 - 대화 화면을 닫으면 하단 탭바 위에 이어서 보기 바를 띄우고, X로 대화를 끝낼 수 있게 함 - 대화 화면 머리에 새 대화 버튼 추가, 경로가 바뀌면 화면만 내리고 대화는 유지 - 밴드 홈 카드에서 묻는 질문은 항상 새 대화로 시작 - 대화가 밴드를 바꿀 수 있어 질문 API 훅이 bandId를 호출마다 받도록 변경 * ✨ feat: 추가 자연어 조회 품질 측정과 실패 신호 검증과 실행이 성공하면 outcome 'success' 하나로 집계돼, 질문과 어긋난 답을 구분할 수 없었다. 50문항 코퍼스는 정답지가 있어 채점되지만 실사용 질문은 정답지가 없고, 지금은 흘러가면 사라진다. 성공을 두 가지로 나눴다. - exec_ok: 검증을 통과해 실행까지 끝났는지. 서버가 결정론으로 안다 - answered: 질문에 실제로 답했는지. 결정론으로는 알 수 없어 NULL로 둔다 틀렸다는 것을 코드가 직접 알 수는 없으므로 '의심할 자리'만 결정론으로 모아 assistant_query_logs에 남긴다. 사람·모델 판정이 붙을 때 이 행을 입력으로 쓰고 answered를 채운다. 판정을 지금 넣지 않은 이유는, 판정기를 검증할 정답 라벨이 먼저 필요하고 그 라벨을 만들 실사용 로그가 아직 없기 때문이다. 보안 위반과 의미 위반을 다른 클래스로 나눴다. 둘 다 InvalidSqlQueryError여서 재생성 횟수 하나에 합쳐졌고, 허용 목록 밖을 건드린 질문이 있었는지 나중에 알 수 없었다. 재생성 정책은 바꾸지 않고 분류만 나눈다. 재질문은 저장하지 않고 분석 질의로 계산한다. 쓰기 시점에 앞 턴을 다시 읽어야 하고, 유사도 기준을 고치면 이미 저장한 행이 낡는다. session_id·turn_index· question만 남겨 기준을 바꿔도 과거 데이터를 다시 해석할 수 있게 했다. 측정이 제품을 막지 않게 했다. 기록 실패는 warn으로만 남고 조회 결과를 그대로 돌려준다. ASSISTANT_QUERY_LOG_ENABLED=false로 끄면 stdout 로그만 남는다. 기존 CloudWatch 질의가 깨지지 않게 stdout 로그는 그대로 두고 필드만 늘렸다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style: 물어보기 화면 머리·바닥 바를 화면 폭에 맞춤 바가 앱 폭(648) 컨테이너 안에 있어 PC에서 화면 가운데에만 떠 보였다. 바는 화면 폭을 다 채우고 안의 내용만 앱 폭으로 가운데 둔다. * 🐛 fix: 모달을 열면 오른쪽에 스크롤바 폭만큼 띠가 생기던 문제 스크롤을 잠글 때 Radix가 body에 스크롤바 폭만큼 margin-right를 주는데, scrollbar-gutter: stable로 비워 둔 자리까지 남아 두 번 보정됐다. 잠긴 동안에는 자리를 비우지 않게 한다. * 🐛 fix: 합쳐진 검증 오류와 생성 장애에서 측정 기록이 빠지던 문제 리뷰에서 세 가지를 짚어 주셨고 모두 실제 누락이었다. 1. 정책 위반이 의미 위반과 합쳐지면 분류가 사라졌다. COLUMN_NOT_ALLOWED와 SELECT_STAR_BLOCKED는 check() 안에서 던져져 violations 배열에 들어가고, 둘 이상이면 combineViolations가 평범한 InvalidSqlQueryError로 합친다. 그 순간 instanceof 검사를 빠져나가 policy_violations가 빈 배열로 남았다. 클래스 대신 오류가 policyCodes를 직접 들고 다니게 해서, 합쳐도 분류가 보존되도록 했다. 재생성 정책은 그대로다. 2. 모델 호출·이름 확인 장애가 측정에서 빠졌다. 두 경로는 재생성으로 풀리지 않아 그대로 다시 던져졌고, 실패율 분모에 들어가지 않았으며 그때까지 쓴 토큰도 사라졌다. SqlGenerationFailedError로 감싸 토큰과 시도 횟수를 들고 나오게 한 뒤, INFRA로 기록하고 원인 예외를 그대로 다시 던진다. 사용자 응답과 상태 코드는 바뀌지 않는다. 3. 측정 필드가 조회를 막았다. turnIndex가 100을 넘으면 400이 나고 재시도도 계속 실패했다. 측정 필드 하나 때문에 조회가 실패하면 측정을 끄게 된다 — 이 PR이 세운 원칙과 어긋났다. @max를 빼고 기록할 때 상한으로 깎는다. 사용자에게 보이는 대화 상한 안내는 화면이 담당하므로 이 범위에 넣지 않았다. 그리고 고치면서 발견한 것 둘을 함께 정리했다. - 이름 확인 장애 테스트가 아티스트 바인딩이 잡히지 않는 SQL을 써서 실행 단계에서 실패하고 있었다. 통과했지만 의도한 경로가 아니었다. - generation_failed와 execution_failed가 둘 다 INFRA로 들어가 테이블에서 구분되지 않았다. outcome 컬럼을 추가해 단계를 가를 수 있게 했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat: 물어보기 후속 질문에 직전 턴의 검증된 SQL을 맥락으로 전달 "그중에"처럼 앞 결과를 가리키는 질문은 조건 칩만으로 WHERE 절을 복원하지 못한다. 답마다 원래 SQL·실행 파라미터·결과 건수를 context로 돌려주고, 다음 요청의 history(최대 3턴)로 받는다. 클라이언트가 보낸 SQL은 실행 SQL과 같은 검증을 다시 통과해야 하며 파싱 후 다시 만든 문장만 프롬프트에 넣어 주석에 숨긴 지시문을 지운다. 결과 행과 답 문장은 보내지 않는다. 짧은 후속 질문은 현재 질문으로 인원 단위가 정해지지 않을 때만 바로 앞 질문과 이어 붙여 판별한다. 사람 단위 변환은 한 일정 안의 중복만 없애므로 여러 일정에 걸친 인원 검사는 유지한다. 대화 턴 상한(5)을 DTO에서 막고 turnIndex를 로그에 남긴다. * ✨ feat: 물어보기 대화에서 직전 턴을 함께 보내고 질문 5개에서 새 대화를 권함 이어서 묻는 질문에만 직전 답의 조회 구조(최대 3턴)와 몇 번째 질문인지를 붙인다. 첫 질문의 요청 본문은 그대로다. 질문이 5개 쌓이면 입력창 대신 새 대화 안내를 보여주고 이어서 묻기 버튼을 숨긴다. * 🧪 test: 직전 턴을 고정한 후속 질문 평가 문항 추가 조건 이어받기 4문항, 생략된 대상 이어받기 2문항, 이력을 무시해야 하는 대조 1문항이다. 직전 턴은 정답 SQL로 고정해 첫 질문 정답 여부와 무관하게 지시어 해석만 보고, 제품과 같은 재검증을 거쳐 이력으로 넣는다. * 📝 docs: 평가 README에 후속 질문 평가 세트 설명 추가 * 🐛 fix: LEFT JOIN·서브쿼리·출력 별칭으로 밴드 범위와 컬럼 허용 목록을 우회하던 문제 범위 루트를 쿼리 전체에서 모으던 방식을 SELECT마다 따로 검사하도록 바꾼다. 범위 루트는 각 SELECT의 WHERE 최상위 AND 조건에서만 인정하고, 서브쿼리는 바깥에서 이미 범위 안인 별칭에만 기댈 수 있다. LEFT JOIN은 오른쪽, RIGHT JOIN은 왼쪽만 ON 조건으로 범위 안에 넣는다. - LEFT JOIN bands ON ... AND b.id = $1 은 매칭되지 않은 행의 b.deleted_at이 NULL이라 다른 밴드 행이 남았다. - NOT EXISTS 안의 b.id = $1 이 바깥 테이블의 범위로 인정됐다. - SELECT 목록의 맨 이름은 PostgreSQL이 실제 컬럼으로 해석하는데, 같은 이름의 출력 별칭이 있으면 허용 목록 검사를 건너뛰었다. 이제 같은 SELECT의 ORDER BY 항목일 때만 출력 별칭으로 인정한다. 10/11 평가의 모델 출력 276건을 새 검증기로 다시 검사해 통과·거부 결과가 모두 같음을 확인했다. * 🐛 fix: 빈 presetId로 물어보기 질문 횟수 제한을 건너뛰던 문제 Guard는 DTO 변환 전 원본 body를 보므로 빈 문자열·공백 presetId도 추천 질문으로 보고 제한을 건너뛰었다. DTO는 이를 undefined로 바꿔 자유 질문으로 모델을 호출한다. 앞뒤 공백을 지운 값이 실제 추천 질문 ID일 때만 제한에서 뺀다. * 🐛 fix: 물어보기 요청 하나의 모델 호출 시간을 9초로 제한하고 감사 로그의 연락처를 가림 재생성 3회와 provider 재시도, 출력 잘림 재시도가 곱해져 한 요청이 2분을 넘길 수 있었다. 화면 요청은 10초에 끊기므로 SQL 생성 전체에 9초 마감을 두고, LlmService는 마감을 넘기면 재시도와 다음 호출을 멈추고 남은 시간만큼만 기다린다. 10/11 평가에서 가장 오래 걸린 생성은 4.7초였다. 감사 로그는 userId·bandId와 함께 남으므로 질문의 이메일·전화번호를 가리고, DB 오류는 바인딩 값이 섞일 수 있는 원문 대신 오류 이름과 코드만 남긴다. * 🐛 fix: LLM 재시도 0 설정이 무시되고 거절 응답이 TypeError로 기록되던 문제 LLM_MAX_RETRIES=0이 양수 파서에서 거부돼 기본값 2로 돌아갔다. OpenAI 호환 provider는 거절·도구 호출 응답의 content가 null이라 null.trim()에서 실패했다. 순환 토글을 끄면 첫 provider·첫 key만 쓰는 현재 동작에 맞게 설정 주석도 고친다. * 🔧 chore: 로컬·테스트가 아니면 스토리지 설정을 요구하고 개발 secret 검사를 되살림 NODE_ENV가 없거나 staging이면 AWS 설정 없이 부팅되던 것을 운영처럼 막는다. 개발 환경변수 동기화는 필수 키 검사 없이도 객체 형태, NODE_ENV=development, 키 이름 형식을 확인해 다른 환경 secret이 개발 파일에 써지지 않게 한다. * 🐛 fix: 닫은 대화의 실패한 질문이 입력창에 다시 채워지고 다른 계정에 대화가 남던 문제 늦게 실패한 질문은 그 턴이 지금 대화에 있고 입력창이 비어 있을 때만 되돌린다. 계정이 바뀌면 대화 Provider를 새로 만든다. 시트를 열면 입력창 대신 시트 자체에 포커스를 둬 키보드를 올리지 않으면서 Tab이 시트 밖으로 나가지 않게 하고, 닫을 때 포커스가 body로 가면 헤더의 물어보기 버튼으로 보낸다. * 🐛 fix: 서버 결과 행 상한을 화면 schema에 고정해 두던 문제 maxRows를 50으로 고정해 서버 상한이 바뀌면 모든 표 응답이 파싱 실패로 보였다. 값은 양의 정수로만 보고 행 수가 maxRows 이하인지만 확인한다. * ✨ feat: 자연어 조회의 모델 SQL을 컬럼 권한과 밴드 RLS가 걸린 전용 DB 역할로 실행 보안 경계가 앱 레벨 AST 검증기 하나뿐이라 검증기에 구멍이 생기면 앱 계정 권한 그대로 모든 테이블·컬럼이 열렸다. 모델 SQL을 실행할 때만 읽기 전용 트랜잭션 안에서 SET LOCAL ROLE assistant_reader로 바꾸고 set_config('app.band_id', ..., true)를 건다. - assistant_reader는 카탈로그 허용 컬럼에만 SELECT 권한이 있다. users.email 같은 컬럼은 권한 오류가 난다. - 18개 테이블에 assistant_reader 전용 RLS 정책을 둬 app.band_id 밴드의 행만 보인다. 설정이 없으면 아무 행도 보이지 않는다. 테이블 소유자인 앱 계정의 다른 조회에는 영향이 없다. - 앱 계정이 테이블 소유자가 아니면 RLS를 켜는 순간 앱 조회가 막히므로 그 경우 마이그레이션을 실패시킨다. - 사람 단위 변환의 SELECT *는 카탈로그 컬럼 목록으로 바꿨다. 컬럼 권한과 카탈로그가 어긋나지 않게 카탈로그 테스트에서 마이그레이션의 GRANT와 대조한다. 평가 DB에서 10/11 평가의 실행 SQL 231건을 전용 역할과 앱 계정으로 각각 실행해 결과가 모두 같음을 확인했다(1건은 ORDER BY가 없어 행 순서만 다름). 리뷰의 LEFT JOIN 우회는 자기 밴드 행만, NOT EXISTS 우회는 0행을 돌려준다. DB 실행 p95는 4.6ms에서 13.4ms가 됐다. --------- Co-authored-by: njm1250 <njm1250@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
github-actions
Bot
requested review from
DevJunz,
JichanPark12 and
pcwadarong
October 17, 2026 06:19
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
포함된 PR