🔥 Issue 개요
백엔드에 개인 액세스 토큰(PAT) API 가 추가된다(Dvely_springboot PR #306, Issue #304).
사용자의 Claude Code·Codex 가 Qeploy 를 MCP 도구로 호출해 배포 상태·로그·실패 원인을 직접 읽고, 배포까지 실행할 수 있게 하는 기능이다. 그러려면 헤드리스 클라이언트가 쓸 자격증명이 필요한데, 현재 브라우저 JWT 는 수명이 1시간이라 쓸 수 없다. 그래서 장수명 토큰을 따로 둔다.
FE 가 없으면 이 기능 전체가 시작되지 않는다. 토큰을 발급받을 창구가 웹 UI 뿐이기 때문이다.
🎯 목표
⚠️ 이 화면의 특수 사정 — 평문은 단 한 번이다
서버가 해시만 저장한다. 복호화할 일이 없고 비교만 하므로 해시로 충분하고, DB 를 잃어도 동작하는 토큰이 함께 새지 않는다. 대신 재조회 경로가 존재하지 않는다.
그래서 발급 응답의 token 을 사용자가 놓치면 폐기하고 새로 발급하는 것 외에 방법이 없다. UI 가 이걸 감당해야 한다.
- 발급 성공 화면은 모달이나 전용 단계로 두고, 실수로 이탈하지 않게 한다. 토스트로 띄우고 3초 뒤 사라지게 하면 안 된다.
- 복사 버튼을 두고, 복사했는지 확인한 뒤에 닫히게 한다(체크박스나 "복사했습니다" 확인).
- "이 값은 다시 볼 수 없습니다" 를 눈에 띄게 적는다.
- 목록으로 돌아간 뒤 평문을 다시 렌더링하지 않는다. 상태에 남겨두지 말 것.
✅ 작업 범위
포함 범위
- PAT 목록 / 발급 / 폐기 UI
- 발급 시 스코프(READ·WRITE) 선택, 이름·만료일 입력
- 발급 결과 1회 노출 처리(위 특수 사정)
- 만료된 토큰을 목록에서 구분 표시
- 연동 안내 문구(아래 "연동 안내에 넣을 내용")
제외 범위 (지금 하지 말 것)
- 토큰 수정. 발급 후 스코프·만료를 바꾸는 API 는 없다. 바꾸려면 폐기 후 재발급이다.
- 스코프를 READ·WRITE 외로 늘리는 것. 현재 2종뿐이다.
- 사용량·호출 이력 표시. 백엔드가
lastUsedAt 만 제공하고 그것도 1시간 스로틀이라 실시간이 아니다.
📋 API 계약
전부 Authorization: Bearer {accessToken}(기존 JWT) 필요. 응답은 공통 봉투({status, code, message, data}) 안에 담긴다.
1) 목록 조회
GET /api/v1/api-tokens
→ 200 [
{
"apiTokenId": 1,
"tokenPrefix": "qp_a1b2c3d4",
"scope": "READ",
"label": "내 노트북 Claude Code",
"expiresAt": "2026-12-07T10:00:00",
"lastUsedAt": "2026-09-08T09:00:00",
"createdAt": "2026-09-08T10:00:00"
}
]
본인 것만 반환된다(소유자는 토큰에서만 결정되며 요청 파라미터가 없다).
lastUsedAt 은 1시간 스로틀로 갱신된다. 매 요청 UPDATE 를 인증 경로에 얹지 않기 위해서다. 방금 쓴 토큰이 "1시간 전 사용" 으로 보일 수 있으니 "실시간 활동" 처럼 표현하지 말 것.
expiresAt 이 과거면 만료된 것이다. 서버가 목록에서 걸러주지 않으므로 FE 가 구분해 표시한다.
2) 발급
POST /api/v1/api-tokens
{ "scope": "READ", "label": "내 노트북 Claude Code", "expiresInDays": 90 }
→ 201 {
"token": "qp_a1b2c3d4e5f6...", // ★ 이 응답에서만 노출된다
"info": { ...위 목록과 동일한 객체 1개... }
}
scope 필수. READ | WRITE
label 선택, 최대 64자
expiresInDays 선택. 생략 시 90일, 범위 1~365. 벗어나면 400
3) 폐기
DELETE /api/v1/api-tokens/{apiTokenId}
→ 204
→ 404 (발급한 적 없는 ID)
즉시 무효화된다.
🔐 스코프를 어떻게 설명할 것인가
서버가 HTTP 메서드로 강제한다. READ 토큰은 GET 만 되고 변경 메서드(POST·PUT·PATCH·DELETE)는 403 이다. 엔드포인트별로 거는 방식이면 새 엔드포인트가 누군가 애노테이션을 기억한 날에야 보호되지만, 메서드 기준은 작성한 날부터 덮인다.
화면 문구 제안:
| 스코프 |
문구 |
READ |
조회만 — 배포 상태·로그·환경변수 목록을 읽습니다. 배포하거나 설정을 바꿀 수 없습니다. |
WRITE |
조회 + 변경 — 배포 실행, 환경변수 수정, 도메인 연결까지 할 수 있습니다. |
기본 선택은 READ 로 둔다. 에이전트가 실수하면 결과가 틀린 답이 아니라 진짜 배포다. 사용자가 WRITE 를 고를 때는 그 차이를 읽고 고르게 해야 한다.
🔌 연동 안내에 넣을 내용
발급 화면 하단이나 별도 안내 탭에 넣어주면 좋겠다. 사용자가 토큰만 받고 어디에 쓰는지 모르면 기능이 안 쓰인다.
# Claude Code
claude mcp add qeploy -- npx -y @qeploy/mcp
# Codex
codex mcp add qeploy -- npx -y @qeploy/mcp
환경변수 2개가 필요하다.
| 변수 |
값 |
QEPLOY_TOKEN |
방금 발급한 토큰 |
QEPLOY_API_URL |
Qeploy API 주소 |
쓰기 도구는 기본으로 꺼져 있다. 켜려면 QEPLOY_ENABLE_WRITES=true 가 추가로 필요하고, 그때도 WRITE 스코프 토큰이어야 한다. 이 둘은 별개의 층이라 안내에서 함께 설명해야 혼란이 없다.
npm 패키지는 아직 미배포다. 공개 시점은 백엔드 쪽에서 별도 공지한다. 안내 UI 는 만들어두되 링크는 배포 후 연결한다.
🚨 에러 처리
| 상황 |
응답 |
FE 처리 |
expiresInDays 범위 밖 |
400 |
입력 검증으로 미리 막기 |
scope 누락 |
400 |
필수 선택 |
| 없는 토큰 폐기 |
404 |
목록 새로고침 |
발급된 PAT 자체를 쓰다 나는 401/403 은 FE 관심사가 아니다(에이전트 쪽에서 난다). 다만 안내 문구에 "토큰이 만료되면 새로 발급해야 한다" 는 넣어주면 좋다.
📎 참고
- 백엔드 PR:
Dvely_springboot #306
- 백엔드 Issue:
Dvely_springboot #304
- API 계약 상세:
.notion/api.md §17
- 설계 배경:
docs/qeploy-mcp-cli-design.md
🔥 Issue 개요
백엔드에 개인 액세스 토큰(PAT) API 가 추가된다(
Dvely_springbootPR #306, Issue #304).사용자의 Claude Code·Codex 가 Qeploy 를 MCP 도구로 호출해 배포 상태·로그·실패 원인을 직접 읽고, 배포까지 실행할 수 있게 하는 기능이다. 그러려면 헤드리스 클라이언트가 쓸 자격증명이 필요한데, 현재 브라우저 JWT 는 수명이 1시간이라 쓸 수 없다. 그래서 장수명 토큰을 따로 둔다.
FE 가 없으면 이 기능 전체가 시작되지 않는다. 토큰을 발급받을 창구가 웹 UI 뿐이기 때문이다.
🎯 목표
qp_a1b2c3d4)만 보이고 평문은 어디에도 없다서버가 해시만 저장한다. 복호화할 일이 없고 비교만 하므로 해시로 충분하고, DB 를 잃어도 동작하는 토큰이 함께 새지 않는다. 대신 재조회 경로가 존재하지 않는다.
그래서 발급 응답의
token을 사용자가 놓치면 폐기하고 새로 발급하는 것 외에 방법이 없다. UI 가 이걸 감당해야 한다.✅ 작업 범위
포함 범위
제외 범위 (지금 하지 말 것)
lastUsedAt만 제공하고 그것도 1시간 스로틀이라 실시간이 아니다.📋 API 계약
전부
Authorization: Bearer {accessToken}(기존 JWT) 필요. 응답은 공통 봉투({status, code, message, data}) 안에 담긴다.1) 목록 조회
본인 것만 반환된다(소유자는 토큰에서만 결정되며 요청 파라미터가 없다).
lastUsedAt은 1시간 스로틀로 갱신된다. 매 요청 UPDATE 를 인증 경로에 얹지 않기 위해서다. 방금 쓴 토큰이 "1시간 전 사용" 으로 보일 수 있으니 "실시간 활동" 처럼 표현하지 말 것.expiresAt이 과거면 만료된 것이다. 서버가 목록에서 걸러주지 않으므로 FE 가 구분해 표시한다.2) 발급
scope필수.READ|WRITElabel선택, 최대 64자expiresInDays선택. 생략 시 90일, 범위 1~365. 벗어나면 4003) 폐기
즉시 무효화된다.
🔐 스코프를 어떻게 설명할 것인가
서버가 HTTP 메서드로 강제한다.
READ토큰은 GET 만 되고 변경 메서드(POST·PUT·PATCH·DELETE)는 403 이다. 엔드포인트별로 거는 방식이면 새 엔드포인트가 누군가 애노테이션을 기억한 날에야 보호되지만, 메서드 기준은 작성한 날부터 덮인다.화면 문구 제안:
READWRITE기본 선택은
READ로 둔다. 에이전트가 실수하면 결과가 틀린 답이 아니라 진짜 배포다. 사용자가WRITE를 고를 때는 그 차이를 읽고 고르게 해야 한다.🔌 연동 안내에 넣을 내용
발급 화면 하단이나 별도 안내 탭에 넣어주면 좋겠다. 사용자가 토큰만 받고 어디에 쓰는지 모르면 기능이 안 쓰인다.
환경변수 2개가 필요하다.
QEPLOY_TOKENQEPLOY_API_URL쓰기 도구는 기본으로 꺼져 있다. 켜려면
QEPLOY_ENABLE_WRITES=true가 추가로 필요하고, 그때도WRITE스코프 토큰이어야 한다. 이 둘은 별개의 층이라 안내에서 함께 설명해야 혼란이 없다.🚨 에러 처리
expiresInDays범위 밖scope누락발급된 PAT 자체를 쓰다 나는 401/403 은 FE 관심사가 아니다(에이전트 쪽에서 난다). 다만 안내 문구에 "토큰이 만료되면 새로 발급해야 한다" 는 넣어주면 좋다.
📎 참고
Dvely_springboot#306Dvely_springboot#304.notion/api.md§17docs/qeploy-mcp-cli-design.md