fix : 승인 UI를 서버 상태로 구동하고 프리뷰·활동 이력 표시 문제 수정 - #31
Merged
Conversation
프리뷰 세션 폴링이 status=PROVISIONING일 때만 돌아서, 세션이 아직 없는(204) 동안에는 폴링이 꺼져 있었다. CODE 스텝이 세션을 만드는 시점은 이 쿼리의 첫 조회보다 늦을 수 있어 PROVISIONING → ACTIVE 전이를 통째로 놓쳤고, 작업이 끝날 때까지 "프리뷰 없음" 화면이 남았다. - Agent 태스크가 도는 동안에는 세션이 없어도 폴링을 유지한다 - AgentConversationPanel이 작업 진행 여부를 부모로 올린다 - access 쿼리 retry를 0으로 — 재시도도 토큰을 회전시켜 직전 응답의 주소를 무효화하므로 404가 났다 - deriveAgentPreviewUrl → resolvePreviewFrameUrl, 태스크 응답이 아니라 열람 권한 발급 응답을 받는다는 것을 이름과 주석에 명시 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
승인 버튼을 띄울지 여부를 메시지 본문에서 유추하고 있었다.
본문에 '승인 후 실행'이 있으면 버튼을 붙이고 '[숫자]'를 approvalId로
읽었는데, 모델이 그 문장을 지어내면 존재하지 않는 승인 버튼이 그대로
떴다. dev에서 실제로 만들어진 적 없는 approval 54가 화면에 노출됐다.
승인 대기 여부·유형·입력 명세는 서버만 안다. 태스크의 pendingApprovalId로
승인 상세를 받아 그 응답으로 화면을 그리도록 바꿨다.
- 메시지 단위 승인 버튼을 걷어내고 패널 하단에 AgentApprovalCard 하나를
둔다. 서버 응답이 PENDING일 때만 뜬다
- 승인 유형별 제목·설명을 붙여 무엇을 승인하는지 버튼 옆에서 읽히게 했다
- input 명세가 있으면 defaultValue를 채운 입력 필드를 그리고, 승인 시
{ [input.field]: 값 }을 본문에 싣는다. 비우면 서버가 기본값을 쓴다
- pattern·maxLength·required를 입력 단계에서 검증한다
- approvalTypeSchema에 REPOSITORY_BINDING 추가 — 없어서 해당 승인을
조회하면 zod parse가 throw했다
- 승인 실패 시 낙관적으로 감춘 카드를 되돌린다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
채팅 메시지 스키마의 needsApproval / pendingApprovalId 는 서버 응답
(MessageResponse)에 존재한 적이 없는 필드다. 승인 UI를 승인 API로
옮기면서 읽는 곳도 사라져 죽은 상태만 남았다.
- chat.type.ts 메시지 스키마에서 두 필드 제거
- createLocalMessage 옵션과 세션 병합 로직에서 제거
- 승인 결정 뮤테이션에서 messageId 인자 제거 — 승인 카드는 특정 메시지에
매달려 있지 않아 되돌릴 메시지가 없다. 로컬 메시지 ID가 -1부터
감소하므로 sentinel 로 넘기던 -1 이 실제 메시지와 충돌할 수 있었다
승인 대기 여부는 이제 GET /agent/tasks/{taskId} 의 pendingApprovalId
한 곳에서만 온다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
승인을 결정하면 서버 상태가 바뀌는데 화면은 옛 값을 그대로 들고 있었다. 저장소가 연결됐는데도 상단에 안 뜨고, 무엇을 승인했는지 채팅에도 남지 않아 새로고침해야 보였다. - 결정 후 대화 메시지 / 저장소 설정 / 프로젝트 상세를 무효화한다 - 결정 성공 시 로컬 임시 메시지를 덧붙이는 대신 오버레이를 비운다. 게이트 안내와 결과는 서버에 기록되므로, 임시 메시지를 남기면 곧 도착할 서버 메시지와 겹친다. 기존 메시지는 남아 있어 화면이 비지는 않는다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
promptMode가 confirm 시 어느 URL로 갈지만 가르고, 제목·설명·버튼 문구는 두 모드가 같은 것을 썼다. 실제 상황은 "이미 준 권한의 인증이 만료됨"인데 화면은 "권한이 없으니 허용해달라"고 말해서, 이미 권한을 준 사용자가 왜 또 요구하는지 알 수 없었다. - install / reauthorize 문구를 분리한다 - 재인증·설치 성공 후 사용자 정보를 다시 읽어 저장된 만료 플래그를 갱신한다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
재인증 모달이 githubAppTokenExpired 로 떴다. 그 값은 액세스 토큰의 신선도만 나타내는데 서버는 GitHub 호출 경로마다 리프레시 토큰으로 자동 재발급한다. 액세스 토큰 수명이 8시간이라 멀쩡히 권한 있는 사용자에게 8시간마다 모달이 떴다. - 모달 조건을 githubAppReauthorizationRequired 로 바꾼다. 자동 재발급이 불가능할 때(리프레시 토큰 없음/만료)만 true 다 - githubAppTokenExpired 는 진단용으로 스키마에 남기고 용도를 주석에 적었다 - resolvePendingApprovalId 에서 "매칭 실패 시 프로젝트 안 아무 PENDING 승인이나 집어오는" 폴백을 제거한다. 스캐폴딩 승인이 WAITING_APPROVAL 로 남아 쌓이는 구조라 다른 대화의 승인 카드가 뜰 수 있었다 주의 : githubAppReauthorizationRequired 는 백엔드 신규 필드다. 이 필드가 없는 서버에서는 재인증 모달이 뜨지 않으므로 함께 배포해야 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
summary를 흰 배경 둥근 박스로 그려서 바로 아래 입력 필드와 생김새가 같았다. REPOSITORY_BINDING은 summary가 "[저장소 연결] my-repo"라 입력 기본값과 같은 값이라, 같은 값이 든 입력창이 두 개 있는 것처럼 보였다. - summary를 채워진 박스 대신 좌측 선 + 흐린 글씨로 바꿔 입력 필드와 구분한다 - summary가 입력 기본값을 되풀이할 뿐이면 아예 감춘다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
백엔드 #133 으로 POST /projects 응답이 { projectId, name, status } 로
줄었다. FE는 원래 projectId 만 쓰고 있었다.
- 생성 응답 스키마에서 taskId / taskStatus / approvalIds 제거.
approvalIds 는 읽는 코드가 처음부터 없었다
- 프로젝트 단위 taskId 저장소(taskIdByProject)를 걷어낸다. 세 곳에서 쓰기만
하고 readProjectTaskId 호출부가 없는 쓰기 전용 죽은 코드였다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
백엔드 #133 이 방치 승인을 TTL(기본 7일)로 CANCELLED 처리한다. 그 상태에서 승인/거절을 누르면 409 가 나는데, onError 가 낙관적으로 감췄던 카드를 그대로 되돌려 이미 닫힌 승인이 다시 떴다. 실패 시 승인 상세를 무효화해 상태를 다시 읽는다. 아직 PENDING 이면 카드가 돌아오고, 이미 닫혔으면 사라진다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
배포가 이 값을 publish 디렉터리 힌트로 읽던 폴백이 있었다(백엔드 #134로 제거). 이름과 예시가 콘텐츠 템플릿을 가리키는데 읽는 쪽은 프레임워크를 기대해서, 프레임워크 이름을 넣으면 빈 산출물이 조용히 배포됐다. FE 는 지금 항상 null 을 보내 안 깨지지만, 나중에 템플릿을 실어 보낼 때 같은 함정에 빠지지 않도록 요청 스키마 주석에 적어둔다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
활동 유형 enum 에 'PROJECT_CREATED' 하나만 있었다. APPROVAL_APPROVED 나 CHANGE_MERGED 가 섞이면 배열 전체가 zod 파싱에 실패하고, getProjectDetailBundle 의 Promise.allSettled 가 그 실패를 빈 배열로 삼켜서 표가 통째로 비었다. 콘솔에도 아무것도 안 남아 원인이 안 보였다. 활동이 하나도 없는 새 프로젝트만 PROJECT_CREATED 한 줄이 보이고, 정작 반영 이력이 쌓인 프로젝트일수록 빈 표가 되는 상태였다. - 활동 유형을 열린 문자열로 받는다. 화면은 이 값을 배지로 보여줄 뿐 분기하지 않으므로 닫힌 enum 이 얻는 게 없고, 서버가 유형을 늘릴 때마다 표가 통째로 죽는 대가만 있었다 - 곁다리 조회가 rejected 면 console.warn 을 남긴다. 조용히 삼키는 구조라 다음에 같은 일이 생겨도 흔적은 보이게 한다 - 이력이 없을 때 "표시할 이력이 없습니다."를 표시한다. 머리글만 남은 빈 표와 조회 실패가 구분되지 않았다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
표가 살아나니 드러난 것들이다. - CHANGE_* 메시지에 에이전트 요약 마크다운이 통째로 들어와 한 행이 화면 절반을 차지했다. 두 줄로 자르고 전문은 title 로 남긴다. 서버에서도 줄이기로 했으나 표 셀은 긴 내용에 견뎌야 한다 - occurredAt 을 ISO 문자열 그대로 보여주고 있었다. 날짜·시각으로 표시한다 - 활동 유형 주석을 정정한다. 고정 목록이 아니라 접두사 + 상태 enum 조합이라 네 enum 중 어디에 값이 늘어도 새 유형이 생긴다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
백엔드가 message 절단을 철회하고 표현 방식을 FE 판단에 맡겼다. 서버에서 자르면 정보가 되돌릴 수 없게 사라지는데, 실제로 필요한 건 "목록에선 짧게, 필요하면 펼쳐 보기"라 표현 계층에서 푸는 게 맞다. - 기본은 두 줄로 접고, 길 때만 "자세히" 버튼으로 전문을 펼친다 - 전문을 title 속성에 넣던 것을 걷어낸다. 758자 30줄짜리 마크다운에는 네이티브 툴팁이 쓸모가 없었다 CHANGE_* 만 30줄을 넘고 APPROVAL_* 은 1줄이라, 짧은 메시지에는 버튼을 붙이지 않는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
인라인 접기/펼치기는 어포던스가 약하고 펼칠 때 행 높이가 튀어서, 행 클릭 → 상세 모달로 바꿨다. - 목록은 한 줄로 자른다. table-fixed 로 두어야 내용 칸이 남은 폭만 차지하고 truncate 가 먹는다 - 행 전체가 클릭 가능하고, 키보드용으로 상태 칸에 chevron 버튼을 둔다 - 모달은 유형 배지 + 시각을 머리에 두고 본문은 스크롤한다. 마크다운은 렌더링하지 않고 원문 그대로 둔다 - formatActivityTime 을 lib/projectActivity 로 옮겨 표와 모달이 같이 쓴다 백엔드 #135 로 overview.recentChanges 가 빠져서 스키마에서도 지웠다. 읽는 코드는 원래 없었고, 스키마 형태(z.array(z.string()))가 실제 응답 (객체 배열)과 어긋나 있어 쓰기 시작하면 깨질 자리였다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
승인 복원을 taskId 로 하고 있었다. taskId 는 메시지 생성 응답에만 실려 오고 FE 메모리(taskIdByConversation)에만 남아서 새로고침하면 사라진다. 서버의 chat_messages 에도 task_id 컬럼이 없어 목록 조회로는 되찾을 수 없다. 결과적으로 승인은 PENDING 이고 게이트 안내도 채팅에 남아 있는데 카드만 없어져, 사용자가 결정할 방법이 화면에서 사라졌다. 승인 목록으로 복원하도록 바꿨다. conversationId·taskId·input 이 모두 들어 있어 taskId 없이도 카드를 그릴 수 있다. 반드시 conversationId 로 거른다. 프로젝트 단위로 찾으면 conversationId 가 null 인 고아 스캐폴딩 승인이 잡혀서, 앞서 폴백을 제거해 없앤 오표시가 그대로 재현된다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
게이트웨이는 항상 절대 URL을 주므로 resolvePreviewFrameUrl 의 상대 경로 분기는 프리뷰에 대해 실행되지 않는다. iframe 이 열리는 근거는 그 분기가 아니라 QEPLOY_PREVIEW_GATEWAY_BASE_URL 이 페이지와 같은 오리진이라는 것 하나다. 게이트웨이를 별도 도메인으로 분리하면 CSP frame-ancestors 에 막혀 조용히 빈 화면이 되고, cross-origin iframe 이라 FE 는 감지할 수 없다. "상대 경로라 안전하다"로 잘못 기억되지 않도록 적어둔다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
작업이 끝나면 FE 가 task.summary 로 어시스턴트 말풍선을 직접 만들어 붙이고 있었다. 서버도 같은 사건을 chat_messages 에 적는데 문구가 달라 (요약 전문 vs 짧은 안내) 본문 병합에 걸리지 않아 두 벌로 보였다. 새로고침하면 서버 것만 남아 하나로 줄던 게 그 증거다. 서술은 서버 메시지 하나로 통일한다. 결정 경로는 앞서 같은 방식으로 정리했고(6f56bc3) 전송 경로만 남아 있었다. - 어시스턴트 로컬 메시지를 만들지 않는다. 사용자 메시지는 오버레이에 남겨 둔다 — 서버 목록이 도착하면 본문이 같아 병합된다 - 대신 FAILED / CANCELLED 는 알림으로 알린다. 실패 시 서버가 채팅에 남기는지 확인되지 않아, 스켈레톤만 사라지고 아무 말이 없는 상황을 막는다. 말풍선이 아니라 알림이라 서버 메시지와 겹치지 않는다 FE 는 SSE 나 이벤트 엔드포인트를 쓰지 않으므로 agent_run_events 와는 무관하다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
백엔드 #139 로 취소와 리스 소진까지 chat_messages 에 남게 됐다. 모든 종료 상태가 채팅에 흔적을 남기므로, 임시로 두었던 FAILED / CANCELLED 알림을 제거한다. 그 알림은 리스 소진 하나 때문에 유지하던 것이었는데, 빌드 실패마다 모달이 뜨고 거기 담긴 task.error 가 채팅의 복구 안내보다 정보가 적어서 정보가 적은 쪽이 더 방해되는 방식으로 뜨고 있었다. 태스크 상태를 문구로 옮기던 formatAgentTaskReply 도 함께 없앤다. 이 함수가 서버 서술과 겹치는 말풍선을 만들던 원인이었고, 이제 쓰는 곳이 없다. 주의 : 취소·리스 소진 문구는 백엔드 #139 에 있다. 배포 순서는 백엔드 main 이 먼저다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
✅ 병합 가능 — 백엔드 운영 반영 완료
이 브랜치는 백엔드 신규 필드 둘에 의존해서 백엔드 main 배포를 기다리고 있었습니다. 2026-08-17 백엔드 PR #140 병합·운영 배포로 해소됐습니다.
githubAppReauthorizationRequired(BE #132)main병합은 곧바로 프로덕션 배포로 이어집니다(deploy-ec2.yml, push 트리거).무엇을 고쳤나
1. 승인 UI를 채팅 본문 파싱 대신 승인 API로 구동
승인 버튼을 띄울지를 메시지 본문에서 유추하고 있었습니다.
content.includes('승인 후 실행')이면 버튼을 붙이고/\[(\d+)\]/로 뽑은 숫자를 approvalId로 썼습니다. 모델이 그 문장을 지어내면 존재하지 않는 승인 버튼이 그대로 떴고, dev에서 실제로 만들어진 적 없는 approval 54가 노출됐습니다.AgentApprovalCard하나를 둡니다. 서버 응답이PENDING일 때만 렌더합니다input명세가 있으면defaultValue를 채운 입력 필드를 그리고, 승인 시{ [input.field]: 값 }을 본문에 싣습니다. 비우면 서버가 기본값을 씁니다pattern·maxLength·required를 입력 단계에서 검증합니다approvalTypeSchema에REPOSITORY_BINDING을 추가했습니다 — 없어서 해당 승인을 조회하면 zod parse가 throw했습니다2. Agent 빌드 중 프리뷰 준비 화면이 뜨지 않던 문제
프리뷰 세션 폴링이
status === 'PROVISIONING'일 때만 돌아서, 세션이 아직 없는(204) 동안에는 폴링이 꺼져 있었습니다. CODE 스텝이 세션을 만드는 시점이 첫 조회보다 늦으면PROVISIONING → ACTIVE전이를 통째로 놓치고, 작업이 끝날 때까지 "프리뷰 없음" 화면이 남았습니다.retry를 0으로 — 재시도가 토큰을 회전시켜 직전 응답의 주소를 무효화해 404가 났습니다3. 최근 활동 표가 통째로 비어 보이던 문제
활동 유형 enum에
'PROJECT_CREATED'하나만 있어서,APPROVAL_APPROVED나CHANGE_MERGED가 섞이면 배열 전체가 파싱에 실패했습니다.Promise.allSettled가 그 실패를 빈 배열로 삼켜 콘솔에도 흔적이 없었습니다. 활동이 없는 새 프로젝트만 한 줄 보이고, 이력이 쌓인 프로젝트일수록 빈 표가 되는 상태였습니다.접두사 + 상태 enum으로 조합해 만들어 고정 목록이 아닙니다console.warn을 남깁니다CHANGE_*는 30줄 넘는 마크다운이 옵니다)occurredAt을 ISO 원문 대신 날짜·시각으로 표시합니다4. 라이브 중 어시스턴트 답변이 두 벌로 보이던 문제
작업이 끝나면 FE가
task.summary로 말풍선을 직접 만들어 붙였습니다. 서버도 같은 사건을chat_messages에 적는데 문구가 달라 병합에 안 걸리고 두 벌이 됐습니다(새로고침하면 하나로 줄었습니다).서술은 서버 메시지 하나로 통일했습니다. 태스크 상태를 문구로 옮기던
formatAgentTaskReply도 제거했습니다.5. GitHub 재인증 모달
githubAppTokenExpired로 모달을 띄우고 있었는데, 그 값은 액세스 토큰의 신선도만 나타내고 서버는 호출 경로마다 리프레시 토큰으로 자동 재발급합니다. 액세스 토큰 수명이 8시간이라 멀쩡히 권한 있는 사용자에게 하루 세 번꼴로 모달이 떴습니다.githubAppReauthorizationRequired로 교체했습니다6. 그 밖의 수정
taskId에 의존하고 있었습니다. 승인 목록의conversationId로 복원합니다summary가 입력창처럼 보이던 문제검증
dev(백엔드 develop 배포분)에서 실측했습니다.
input기본값 포함)미검증 1건: 프리뷰 폴링 레이스는 빌드가 짧은 케이스로 재현해야 확정됩니다. 지금까지 두 번 통과했지만 둘 다 빌드가 길어 구 코드로도 넘어갔을 수 있습니다.
bun run typecheck/lint/build통과.🤖 Generated with Claude Code