uVersion
한국어
다운로드 →

Wiki

REST API

uVersion 서버 HTTP 엔드포인트 레퍼런스: 인증, 리포지토리, 파일, 잠금, 댓글, 관심 목록, 프로덕션 보드, 빌드, 관리.

uVersion 서버는 JSON HTTP API를 노출합니다. 호출은 JWT(JSON Web Token), 즉 로그인 시 서버가 건네주고 그 뒤 각 요청에서 되돌려 보내는 세션 토큰으로 인증됩니다. 이 페이지는 데스크톱 클라이언트, 에디터 플러그인, 그리고 uversion CLI가 사용하는 엔드포인트를 문서화합니다. 이를 직접 호출하여 uVersion을 자체 내부 도구(자체 대시보드, 감사 스크립트, 웹훅 등)에 통합할 수 있습니다.

이 페이지는 API 전체를 다루지 않습니다. 여기서 설명하지 않은 여러 라우트 계열이 존재합니다. 목록은 다루지 않은 영역 섹션에 있습니다.

규약

기본 URL

문서화된 모든 경로는 당신 인스턴스의 URL에 상대적입니다. 예제는 https://uversion.mygamestudio.com을 사용합니다. 당신의 것으로 바꾸세요.

필수 헤더

헤더
AuthorizationBearer <jwt>. /api/auth/*/api/server-info(그리고 /health)를 제외한 모든 /api/* 라우트에서
Content-TypeJSON 본문을 가진 POST/PUT에는 application/json. 바이너리 업로드(빌드 파일)에는 application/octet-stream.
Acceptapplication/json 권장(서버는 기본적으로 JSON을 반환합니다)

응답 형식은 두 가지이며, 어느 것을 읽는지 알아야 합니다

서버의 응답 형식은 하나가 아니라 둘이며, 이를 혼동하는 것은 통합을 시작하는 사람에게 가장 값비싼 실수입니다.

1. 인증된 라우트(/api/auth/*를 제외한 모든 /api/*)는 봉투로 응답합니다:

{
  "success": true,
  "data": { /* payload */ }
}

오류의 경우, 같은 라우트는 다음과 같이 응답합니다:

{
  "success": false,
  "error": "No write permission on this repository"
}

success 필드는 항상 존재합니다. data는 성공 시, error는 실패 시 존재하며, 둘이 함께 있는 경우는 결코 없습니다.

2. 인증 라우트(/api/auth/login, /refresh, /validate, /logout, /register, /change-password)는 이 봉투를 사용하지 않습니다. successdata도 없는 벌거벗은 객체를 반환합니다:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": { "id": 12, "username": "alice", ... },
  "must_change_password": false
}

그리고 그 오류는 success가 없는 단일 필드 객체입니다:

{ "error": "Invalid credentials" }

실제적 결과: /api/auth/login에서 토큰은 .token에서 읽고 .data.token에서는 읽지 않습니다. .data.token을 조회하는 스크립트는 눈에 보이는 오류 없이 null을 받습니다.

끝으로, GET /api/files/{repo_id}/content는 그 어느 것도 반환하지 않습니다. 파일의 원시 바이너리 내용을, 있는 그대로 반환합니다.

페이지네이션

공통 페이지네이션 규약은 없습니다. 각 라우트는 자기만의 것이 있거나, 아예 없습니다. limittotalhas_more도 전제하지 마세요.

라우트허용되는 파라미터응답의 형태
GET /api/files/{repo_id}/history limit(기본 50), offset(기본 0), path 커밋의 평면 배열. total 없음, has_more 없음: 배열이 limit보다 적은 요소를 담을 때 끝에 도달한 것입니다.
GET /api/repositories include_inactive만(그리고 슈퍼 관리자에게만 존중됩니다) 평면 배열. limitoffset도 읽지 않습니다.
GET /api/locks/{repo_id}/status 없음 리포지토리의 모든 잠금의 평면 배열.
GET /api/files/{repo_id}/snapshot commit_hash, 필수 파일의 평면 배열.
GET /api/admin/audit pageper_page, limit/offset가 아님, 그리고 필터(user_id, action, entity_type, from, to) 슈퍼 관리자 전용. 공통 규약의 부재를 잘 보여줍니다: 페이지 번호로 페이지를 나누는 유일한 라우트입니다.

여기 나열되지 않은 모든 라우트는, 그 결과 전체를 한 번의 호출로 반환한다고 간주하세요.

속도 제한

일반적인 속도 제한은 없습니다: Unreal 프로젝트는 수천 개의 파일을 가지며, 일괄 작업(잠금 획득, 해제)이 끊임없이 스로틀을 유발할 것이기 때문입니다. 따라서 API를 대량으로 호출할 수 있습니다.

제한되는 라우트는 /api/auth/login 하나뿐입니다: 사용자 이름당 15분마다 5회의 실패한 시도. 성공한 로그인은 집계되지 않습니다.

토큰 취소

각 세션 토큰은 tv 필드(token version)를 지니며, 이는 데이터베이스의 users.token_version 열을 반영합니다. 서버는 각 요청에서 둘을 비교합니다. POST /api/auth/logout 호출, 관리자에 의한 비밀번호 재설정, 또는 계정 비활성화는 이 값을 증가시키며, 그로 인해 해당 사용자의 기존 모든 토큰이 즉시, 그의 모든 머신에서 무효화됩니다.

Curl

터미널에서 API를 호출하려면. .token에 주목하세요: /api/auth/login의 응답은 벌거벗은 객체이며, 통과할 .data가 없습니다.

TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"secret"}' | jq -r '.token')

# 반면, 인증된 라우트는 이 봉투를 확실히 사용합니다:
curl -s -H "Authorization: Bearer $TOKEN" \
  https://uversion.mygamestudio.com/api/repositories | jq '.data'

인증

참고: 이 섹션의 어떤 라우트도 {"success", "data"} 봉투를 사용하지 않습니다. 벌거벗은 객체를 반환하며, 그 오류는 {"error": "..."} 형태입니다.

POST /api/auth/login

사용자를 인증하고 30일간 유효한 세션 토큰을 반환합니다.

Body:

{
  "username": "alice",
  "password": "secret"
}

Response 200:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": 12,
    "username": "alice",
    "email": "alice@mygamestudio.com",
    "role": "lead"
  },
  "must_change_password": false
}

어떠한 expires_at 필드도 없습니다: 유효 기간은 토큰 자체에서 읽거나, 서버 구성에서 유추됩니다(기본 30일).

must_change_password를 무시하지 마세요. 이 필드는 계정이 아직 임시 비밀번호로 동작할 때 true입니다: 설치 프로그램이 초기 관리자 계정을 위해 생성한 것, 또는 관리자가 재설정 시 방금 설정한 것입니다. 이를 보지 않는 클라이언트는 사용자를 그 임시 비밀번호에 무기한 방치합니다. 기대되는 동작은, 다른 어떤 작업보다 먼저 즉시 POST /api/auth/change-password로 리디렉션하는 것입니다.

오류:

  • 401: 잘못된 자격 증명 또는 비활성화된 계정
  • 429: 이 사용자 이름에 대해 15분 창 내에서 5회의 실패한 시도에 도달

서버는 계정이 존재하지 않을 때 더미 해시에 대해서도, 항상 비밀번호의 Argon2id 검증을 실행합니다. 따라서 응답은 두 경우 모두 같은 시간이 걸리며, 이는 요청 시간을 재어 계정의 존재를 추측하는 것을 막습니다.

POST /api/auth/refresh

비밀번호를 다시 거치지 않고 세션 토큰을 갱신합니다.

헤더: Authorization: Bearer <현재_토큰>. 본문 없음.

Response 200: /login과 정확히 같은 형태 (token, user, must_change_password), 새로운 토큰과 함께.

오류:

  • 401: 무효하거나 만료된 토큰, 비활성화된 계정, 또는 오래된 token_version

POST /api/auth/logout

현재 사용자의 모든 토큰을, 그의 모든 머신에서, users.token_version을 증가시켜 무효화합니다. 어디서나 다시 로그인해야 합니다: 데스크톱 클라이언트, 에디터 플러그인, CLI 포함.

Response 200: { "logged_out": true }.

POST /api/auth/validate

토큰이 아직 유효한지 확인하고 해당하는 사용자를 반환합니다. 검사는 is_activetoken_version에도 미치므로, 취소된 토큰은 아직 만료되지 않았더라도 거부됩니다.

본문에 주의: 이것은 객체가 아니라 벌거벗은 JSON 문자열, 즉 큰따옴표로 둘러싸인 토큰입니다.

curl -X POST https://uversion.mygamestudio.com/api/auth/validate \
  -H "Content-Type: application/json" \
  -d '"eyJhbGciOiJIUzI1NiIs..."'

Response 200: 벌거벗은 사용자 객체.

{
  "id": 12,
  "username": "alice",
  "email": "alice@mygamestudio.com",
  "role": "lead"
}

validexpires_atrefreshed_token도 없습니다: 유효성은 HTTP 코드(200 또는 401)에서 읽고, 갱신은 /api/auth/refresh를 통해 이루어지며, 결코 이 라우트를 통해서는 아닙니다.

POST /api/auth/register

이 라우트는 기본적으로 거부합니다. 공개 등록은, 운영자가 서버 구성에서 명시적으로 활성화하지 않는 한 비활성화되어 있습니다. 그렇지 않으면 응답은 403{"error": "Open registration is disabled; contact your administrator"}입니다.

정상 운영에서, 계정은 관리를 통해 생성됩니다: POST /api/admin/users, 또는 관리 패널의 사용자 탭. /register에 의존하는 통합을 구축하지 마세요.

POST /api/auth/change-password

현재 사용자의 비밀번호를 변경합니다. token_version을 증가시켜, 방금 그 호출에 사용된 것을 포함하여 이전의 모든 토큰을 무효화합니다.

리포지토리

GET /api/repositories

권한 테이블로 필터링된, 현재 사용자가 접근할 수 있는 리포지토리를 나열합니다. 리포지토리에 대해 어떤 권한 규칙도 없는 사용자는 그것을 보지 못합니다. adminlead 역할은 모든 리포지토리를 봅니다.

쿼리 파라미터: 단 하나, include_inactive(불리언, 기본 false)이며, 슈퍼 관리자에게만 존중됩니다. limitoffset도 없습니다: 라우트는 전체 목록을 반환합니다.

Response 200:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "hero-rpg",
      "description": "Main RPG project",
      "storage_path": "/var/lib/uversion/data/hero-rpg",
      "is_active": true,
      "created_at": "2026-01-15T10:00:00Z",
      "updated_at": "2026-05-15T08:30:00Z"
    },
    {
      "id": 2,
      "name": "shared-assets",
      "description": "Shared asset library",
      "storage_path": "/var/lib/uversion/data/shared-assets",
      "is_active": true,
      "created_at": "2026-02-01T09:00:00Z",
      "updated_at": "2026-05-02T11:12:00Z"
    }
  ]
}

반환되는 필드는 이것들뿐입니다. 특히 owner도, current_revision도, file_count도, size_bytes도, last_commit_at도 없습니다: 리포지토리는 API의 의미에서 소유자를 갖지 않으며, 용량 데이터는 관리 통계 라우트로 얻습니다.

GET /api/repositories/{repo_id}

리포지토리의 상세. 목록과 같은 객체 형태, 같은 필드.

오류:

  • 403: 이 리포지토리에 접근 불가
  • 404: 리포지토리 없음

POST /api/repositories

리포지토리를 생성합니다. 슈퍼 관리자 전용, 즉 admin 역할과 그것뿐입니다. 이것은 기능(capability)이 아닙니다: 검사는 역할에 직접 이루어지므로, project_admin이나 lead403을 받습니다. 제품에 create_repos 기능은 존재하지 않습니다.

Body:

{
  "name": "new-project",
  "description": "선택적 설명"
}

검증:

  • name: 1~255자. 서버는 어떤 문자 집합 제약도 적용하지 않습니다. 디스크상의 저장 경로는 이름을 정규화하여 유도됩니다.
  • description: 선택 사항.

고유성은 서버 규모에서 이름 하나만에 적용됩니다. 소유자 개념이 없으므로, 「소유자별」 고유성도 없습니다.

오류:

  • 400: 빈 이름 또는 255자 초과
  • 403: 호출자가 admin 역할을 갖지 않음
  • 409: 이미 이 이름의 리포지토리가 있음

파일

POST /api/files/{repo_id}/upload-chunks

조각의 목록이 아니라 파일 하나 전체를 전송합니다. 라우트 이름은 오해를 부릅니다: 파일을 블록(chunks)으로 나누는 것은 서버이지, 당신이 아닙니다. 호출자가 이 분할을 스스로 해야 할 일은 결코 없습니다.

SHA-256 해시로 인식되는 동일한 블록은 중복 제거됩니다: 이미 서버에 존재하는 블록은, 어느 파일이나 어느 리포지토리에서 왔든 두 번째로 저장되지 않습니다. 이것이 가장자리만 변경된 큰 바이너리 파일이 디스크 공간을 거의 들이지 않는 이유입니다.

Body: base64로 인코딩된 완전한 파일을 담은, 단일 필드 객체.

{
  "data": "<base64로 인코딩된 파일 전체>"
}

그 밖의 어떤 형태, 특히 chunks 배열은 서버에 의해 거부됩니다.

Response 200:

{
  "success": true,
  "data": {
    "chunks": [
      { "hash": "abc123...", "offset": 0,       "size": 1048576, "compressed_size": 423152 },
      { "hash": "def456...", "offset": 1048576, "size": 2097152, "compressed_size": 891204 }
    ],
    "chunks_stored": 1,
    "chunks_deduplicated": 1
  }
}

chunks 배열은 있는 그대로, 완전한 객체를 포함하여, 뒤따르는 POST /commit에서 재사용합니다. chunks_stored는 실제로 디스크에 쓰인 블록을, chunks_deduplicated는 이미 존재하던 것을 셉니다.

권한: 리포지토리에 대한 쓰기가 요구되며, 그렇지 않으면 403.

제한: 요청 본문은 1 GB로 제한됩니다(security.max_body_size_files로 조정 가능). 이 제한보다 큰 파일은 이 라우트를 통과할 수 없습니다.

POST /api/files/{repo_id}/commit

파일과 그 블록의 목록으로부터 원자적 커밋을 생성합니다. 모든 파일이 통과하거나, 아무것도 통과하지 않습니다.

action에 허용되는 값은 add, modify, delete 셋뿐입니다

added가 아니고, modified도 아니고, deleted도 아닙니다.

이것은 형식상의 세부가 아닙니다. 서버는 말 그대로 action == "delete"를 테스트하고 나머지 전부를 추가나 수정으로 취급합니다. 따라서 "deleted"를 보내도 아무것도 삭제되지 않습니다: 파일은 빈 chunks 배열과 함께 쓰기 브랜치로 가고, 서버는 빈 내용의 리비전을 기록합니다. 어떤 오류도 발생하지 않습니다. 파일은 남아 있고, 그 최신 버전은 빈 것으로 덮어써지며, 그 손실은 다른 누군가의 다음 sync에서야 보입니다.

Body:

{
  "message": "Updated main level + hero pose pass",
  "commit_hash": "선택 사항: 여러 배치를 하나의 커밋으로 묶기 위함",
  "files": [
    {
      "path": "Content/Maps/MainLevel.umap",
      "action": "modify",
      "chunks": [
        { "hash": "abc123...", "offset": 0,       "size": 1048576, "compressed_size": 423152 },
        { "hash": "def456...", "offset": 1048576, "size": 2097152, "compressed_size": 891204 }
      ]
    },
    {
      "path": "Content/Characters/NewVillain.uasset",
      "action": "add",
      "chunks": [
        { "hash": "fed789...", "offset": 0, "size": 524288, "compressed_size": 201004 }
      ]
    },
    {
      "path": "Content/OldAsset.uasset",
      "action": "delete",
      "chunks": []
    }
  ]
}

chunks는 선택 사항이 아닙니다, delete에서도. 필드는 반드시 존재해야 합니다: 그것을 생략하면 요청 전체의 역직렬화가 실패합니다. 삭제에는, 빈 배열을 보내세요.

배열에는 문자열이 아니라 객체가 들어 있습니다: /upload-chunks가 반환한 {hash, offset, size, compressed_size} 항목을 수정하지 말고 재사용하세요. 각 hash는 64자의 16진 다이제스트여야 하며, 그렇지 않으면 커밋 전체가 400으로 거부됩니다.

루트 레벨의 commit_hash 필드는 선택 사항입니다. 여러 연속 호출을 하나의 동일한 커밋이 담당하게 하는 데 쓰이며, 이는 데스크톱 클라이언트가 큰 업로드를 배치로 나눌 때 하는 것입니다. 생략하면, 서버가 하나를 계산합니다.

Response 200:

{
  "success": true,
  "data": {
    "commit_hash": "7f3a9b1c2d3e4f...",
    "files_committed": 3,
    "revisions": [
      { "path": "Content/Maps/MainLevel.umap",             "revision_number": 12 },
      { "path": "Content/Characters/NewVillain.uasset",    "revision_number": 1  },
      { "path": "Content/OldAsset.uasset",                 "revision_number": 8  }
    ]
  }
}

반환되는 필드는 이것들뿐입니다. files_changed도, bytes_uploaded도, bytes_deduped도, revision도 없습니다. revision_number는 리포지토리 버전 번호가 아니라 파일별 카운터라는 점에 유의하세요: 커밋을 식별하는 것은 commit_hash이며, 상태를 참조하기 위해 보존해야 하는 것은 그것입니다.

오류:

  • 400: 잘못된 본문, 무효한 블록 다이제스트(기대: 64개의 16진 문자), chunks 필드 누락
  • 403: 경로 중 하나에 쓰기 권한 없음
  • 409: 수정된 파일에 다른 사람이 보유한 잠금, 또는 같은 파일에 대한 동시 커밋

GET /api/files/{repo_id}/snapshot

특정 커밋 시점의 리포지토리 상태를 반환합니다: 존재하는 파일의 목록을, 그 리비전 번호와 크기와 함께. 데스크톱 클라이언트가 클론과 강제 동기화를 위해 사용합니다.

쿼리 파라미터:

  • commit_hash: 필수. 기준점 역할을 하는 커밋의 해시입니다. 이 파라미터가 없으면 요청은 거부되며, 「최신 상태」라는 기본값은 존재하지 않습니다.

어떠한 revision 파라미터도 없습니다. 스냅샷은 커밋 해시로 요청하며, 결코 번호로는 요청하지 않습니다. 알 수 없는 커밋은 404를 반환합니다.

Response 200: 감싸는 객체가 없는, 평면 배열.

{
  "success": true,
  "data": [
    {
      "path": "Content/Maps/MainLevel.umap",
      "revision_number": 12,
      "file_size": 84934656
    },
    {
      "path": "Content/Characters/Hero.uasset",
      "revision_number": 3,
      "file_size": 5242880
    }
  ]
}

응답에는 블록 목록이 포함되지 않습니다. 내용을 가져오려면, 파일을 서버 측에서 재조립하는 GET /api/files/{repo_id}/content를 통하세요.

GET /api/files/{repo_id}/content

특정 리비전에서 파일의 내용을 다운로드합니다(서버가 청크를 재조립합니다). 클론과 sync가 사용합니다.

쿼리 파라미터:

  • path: 파일 경로(repo 루트에 상대적)
  • revision(선택): 리비전 번호. 기본: 최신.

Response 200: 파일의 바이너리 내용.

GET /api/files/{repo_id}/history

리포지토리의 커밋 이력, 최신에서 가장 오래된 것으로.

쿼리 파라미터:

  • limit: 커밋의 수, 기본 50
  • offset: 기본 0
  • path(선택): 이 파일을 건드린 커밋만 유지합니다

읽히는 파라미터는 이 셋뿐입니다. authorsince도 없습니다: 알 수 없는 파라미터는 조용히 무시되어, 그럴듯하지만 필터링되지 않은 응답을 냅니다. 작성자나 날짜로의 필터링은 호출자 측에서 하세요.

Response 200: 커밋의 평면 배열.

{
  "success": true,
  "data": [
    {
      "commit_hash": "7f3a9b1c...",
      "message": "Fixed lighting in main level",
      "author": "alice",
      "created_at": "2026-05-15T08:30:00Z",
      "files": [
        {
          "path": "Content/Maps/MainLevel.umap",
          "revision_number": 12,
          "file_size": 84934656
        }
      ]
    }
  ]
}

total도, has_more도, limitoffset의 재확인도 없습니다. 이력 전체를 훑으려면, limit보다 적은 요소를 받을 때까지 offset을 증가시키세요.

각 커밋은 건드린 파일의 목록을 직접 지닙니다. 리비전이 삭제인 항목은 그렇게 표시되며, 다운로드할 내용이 없습니다.

증분 sync

데스크톱 클라이언트가 효율적으로 동기화하기 위해 사용하는 엔드포인트:

  • GET /api/files/{repo_id}/sync: 어떤 리비전 이후 변경된 파일, 델타 sync를 위함.
  • GET /api/files/{repo_id}/deletions: 서버 측에서 삭제된 파일, 삭제를 로컬로 전파하기 위함.
  • GET /api/files/{repo_id}/list: repo의 파일 목록.
  • POST /api/files/{repo_id}/checkout: 잠금을 획득하고 편집을 준비합니다.

잠금

잠금은 결코 만료되지 않습니다

잠금은 명시적으로 해제될 때까지 유지됩니다: checkin에 의해, revert에 의해, 또는 관리자의 강제 잠금 해제에 의해. 어떤 자동 만료도 없습니다, 한 시간 뒤에도, 한 달 뒤에도.

expires_at 필드는, 데이터베이스의 해당 열이 빈 값을 받지 않는다는 이유만으로 존재합니다. 서버는 거기에 백 년짜리 센티넬 값을 씁니다: 오늘 취한 잠금은 2126년경의 만기를 표시합니다. 이 필드 위에 아무것도 구축하지 말고, 이 날짜를 사용자에게 보여주지 마세요.

따라서 heartbeat는 아무것도 연장하지 않습니다. 감시 신호이며, 그 유일한 목적은 어떤 잠금이 아직 활발히 사용되는지를 관리자에게 보여주는 것입니다.

POST /api/locks/{repo_id}/acquire

경로 목록에 대해 잠금을 획득합니다. 획득은 독립적입니다: acquired 목록에는 성공이, failed 목록에는 실패가 그 이유와 함께 담깁니다.

Body:

{
  "paths": [
    "Content/Maps/MainLevel.umap",
    "Content/Characters/Hero.uasset"
  ]
}

Response 200:

{
  "success": true,
  "data": {
    "acquired": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "file_id": 12345,
        "file_path": "Content/Maps/MainLevel.umap",
        "user_id": 12,
        "username": "alice",
        "acquired_at": "2026-05-15T14:30:00Z",
        "expires_at": "2126-05-15T14:30:00Z"
      }
    ],
    "failed": [
      {
        "path": "Content/Characters/Hero.uasset",
        "reason": "File is locked",
        "locked_by": "bob"
      }
    ]
  }
}

이 예의 2126년 만기는 오타가 아닙니다: 위에서 설명한 센티넬 값입니다. 잠금은 영구적입니다.

실패 이유. reason 필드는 표시되기 위한 영어 문장이며, 안정적인 코드가 아닙니다. 이 문자열을 비교하는 로직을 작성하지 말고, 그 접두사를 「파싱」하지 마세요: 버전마다 재표현될 수 있습니다. 현재 생성되는 값은 다음과 같습니다:

reason의미locked_by
File is locked다른 누군가가 이미 잠금을 보유그 사람의 이름
No write permission이 경로에 쓰기 권한 없음null
Failed to create lock잠금 설정이 데이터베이스에서 실패null
Database error이 경로에서 데이터베이스 오류null

무효한 경로(절대 경로, ..를 포함, 비어 있음, 4096자 초과, 또는 널 바이트를 지님)는 failed에 항목을 만들지 않습니다: 요청 전체를 실패시킵니다.

POST /api/locks/{repo_id}/release

당신이 보유한 잠금을 해제합니다. Body:

{
  "paths": ["Content/Maps/MainLevel.umap"]
}

이 라우트에 force 필드는 없습니다. 하나 추가해도 아무 효과가 없습니다: 서버는 모르는 필드를 무시하고, 요청은 성공하며, 남의 잠금은 그대로 남습니다. 그것을 믿는 통합자는 파일을 해제했다고 생각하지만 그렇지 않습니다.

다른 사람의 잠금을 제거하는 유일한 라우트는 POST /api/admin/locks/{lock_id}/force-release입니다. 해당 리포지토리의 관리에 유보되며, 그 작업은 감사 로그에 기록됩니다. 잠금 식별자를 받으며, 그것은 GET /api/locks/{repo_id}/status로 얻습니다.

POST /api/locks/{repo_id}/heartbeat

당신 잠금의 활동 타임스탬프를 갱신합니다. 이것은 아무것도 연장하지 않습니다, 아무것도 만료되지 않기 때문입니다: 감시 신호이며, 관리자가 아직 사용 중인 잠금과 잊힌 잠금을 구별할 수 있게 합니다.

Body: 감싸는 객체가 없는, 경로의 JSON 배열을 직접.

["Content/Maps/MainLevel.umap", "Content/Characters/Hero.uasset"]

무효한 경로는, 오래된 추적 잔여물이 다른 것을 막지 않도록, 전체 배치를 실패시키는 대신 개별적으로 무시됩니다.

GET /api/locks/{repo_id}/status

리포지토리의 모든 잠금을, 파일 이름과 그것을 보유한 사람의 이름과 함께 나열합니다.

쿼리 파라미터: 없음. user 필터도 limitoffset도 없습니다. 라우트는 리포지토리 잠금 전체를 반환하며, 호출자 측에서 필터링합니다.

Response 200: 감싸는 객체도 total도 없는, 평면 배열.

{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "file_id": 12345,
      "file_path": "Content/Maps/MainLevel.umap",
      "user_id": 12,
      "username": "alice",
      "acquired_at": "2026-05-15T14:30:00Z",
      "expires_at": "2126-05-15T14:30:00Z"
    }
  ]
}

id 필드는, POST /api/admin/locks/{lock_id}/force-release에 전달할 것입니다.

댓글 & 리뷰

GET /api/comments/{repo_id}/comments?commit_hash=<hash>

커밋의 댓글을, 스레드로(부모 → 자식) 나열합니다.

Response 200:

{
  "success": true,
  "data": [
    {
      "id": 42,
      "commit_hash": "7f3a9b1c...",
      "file_path": "Content/Maps/MainLevel.umap",
      "author_id": 12,
      "author_username": "alice",
      "body": "LGTM, ship it",
      "parent_id": null,
      "created_at": "2026-05-15T15:00:00Z",
      "replies": [
        {
          "id": 43,
          "author_username": "bob",
          "body": "Thanks!",
          "parent_id": 42,
          "created_at": "2026-05-15T15:05:00Z"
        }
      ]
    }
  ]
}

POST /api/comments/{repo_id}/comments

댓글을 생성합니다. file_path는 선택 사항(커밋 댓글 vs. 파일 댓글). 기존 스레드에 답하려면 parent_id.

Body:

{
  "commit_hash": "7f3a9b1c...",
  "file_path": "Content/Maps/MainLevel.umap",
  "body": "LGTM, ship it",
  "parent_id": null
}

POST /api/comments/{repo_id}/reviews

커밋에 리뷰를 제출합니다. approve_changes 기능이 필요합니다.

Body:

{
  "commit_hash": "7f3a9b1c...",
  "status": "approved",
  "comment": "Lighting looks great, approving"
}

허용되는 status: approved, changes_requested, pending.

관심 목록

GET /api/watchlist/{repo_id}/watchlist

이 리포지토리에서 현재 사용자의 관심 패턴을 나열합니다.

Response 200:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "user_id": 12,
      "pattern": "Content/Characters/Hero/**",
      "notify_on": ["commit", "lock"],
      "created_at": "2026-04-10T10:00:00Z"
    }
  ]
}

POST /api/watchlist/{repo_id}/watchlist

관심 패턴을 추가합니다. Body:

{
  "pattern": "Content/Characters/Hero/**",
  "notify_on": ["commit", "lock"]
}

가능한 이벤트: commit(커밋이 일치하는 파일을 수정), lock(잠금이 획득됨), review(리뷰가 게시됨).

DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}

관심 패턴을 제거합니다. { "success": true }를 반환합니다.

프로덕션 보드(작업)

modification-requests API는 제거되었습니다: 프로덕션 보드가 그것을 흡수합니다 (요청이 카드가 되며, origin='request'). 엔드포인트는 /api/tasks/{repo_id} 아래에 중첩되어 있습니다.

라우트설명
GET /api/tasks/{repo_id}/board완전한 보드: 열 + 카드
POST /api/tasks/{repo_id}/tasks카드를 생성합니다
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}카드를 업데이트 / 삭제합니다
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees카드에 사용자를 배정합니다
GET /api/tasks/{repo_id}/assignable-users배정 가능한 사용자 목록
POST /api/tasks/{repo_id}/columns보드의 열을 구성합니다

함께 제공되는 하위 라우트: 카드 댓글, 애셋 및 커밋으로의 링크, 그리고 첨부 (표지 이미지 포함). 열의 구성은 manage_board 기능(admin / lead)을 요구합니다.

빌드

POST /api/builds/{repo_id}/upload?hash=<sha256>

빌드 파일을 <storage_path>/builds/<hash>로 업로드합니다. 서버는 쿼리 파라미터로 주어진 SHA-256을 받은 내용에 대해 검증합니다. 해시가 이미 서버 측에 존재하면, 다시 복사하지 않고 즉시 200을 반환합니다(빌드 스토리지 측 중복 제거).

헤더: Content-Type: application/octet-stream.

Body: 원시 바이너리(JSON 래핑 없음).

제한: 파일당 8 GB.

오류:

  • 400: hash 쿼리 파라미터 누락 또는 잘못됨, 또는 계산된 SHA-256 != 제공된 hash
  • 413: 파일 > 8 GB

POST /api/builds/{repo_id}/publish

모든 파일을 업로드한 후 빌드의 manifest를 등록합니다. publish_builds 기능이 필요합니다.

Body:

{
  "version": "0.1.5-nightly",
  "config": "Development",
  "platform": "Win64",
  "executable_path": "HeroRPG/Binaries/Win64/HeroRPG.exe",
  "release_notes": "Nightly build of main branch, 2026-05-15",
  "files": [
    { "path": "HeroRPG.exe",                "hash": "abc123...", "size_bytes": 12345678 },
    { "path": "HeroRPG/Content/Paks/pak0.pak", "hash": "def456...", "size_bytes": 2147483648 }
  ]
}

GET /api/builds/{repo_id}

이 프로젝트의 게시된 빌드를 나열합니다.

빌드에 대한 접근은 프로젝트별로 부여됩니다. download_builds 기능만으로는 충분하지 않습니다: 서버 전체에 미치므로, 계정이 단순한 구경꾼이 아님을 말할 뿐입니다. 거기에 더해 리포지토리에 대한 접근이나, 이 프로젝트의 빌드에 대한 명시적 허가 중 하나가 필요하며, 이는 그 관리자가 /api/admin/repositories/{repo_id}/build-access를 통해 부여합니다. 프로젝트 관리자는 자신이 관리하는 것에 당연히 접근하고, 슈퍼 관리자는 전부에 접근합니다.

Response 200:

{
  "success": true,
  "data": [
    {
      "id": 5,
      "version": "0.1.5-nightly",
      "config": "Development",
      "platform": "Win64",
      "published_by": "ci-nightly",
      "published_at": "2026-05-15T03:00:00Z",
      "size_bytes": 2159829326,
      "file_count": 142,
      "release_notes": "Nightly build of main branch, 2026-05-15"
    }
  ]
}

GET /api/builds/{repo_id}/{build_id}/file

빌드의 파일을 다운로드합니다. download_builds 기능이 필요합니다. 빌드의 manifest는 GET /api/builds/{repo_id}/{build_id}/manifest를 통해 이용할 수 있습니다.

관리

이 라우트를 실제로 지키는 것

예상과 달리, /api/admin/* 라우트는 각각 하나의 기능으로 지켜지지 않습니다. 거의 모두 역할에 관한 네 가지 검사 중 하나를 통과합니다. capability는 역할에 결부된 이름 있는 권리이며, 중요한 점은 그것이 서버 전체에서 유효하다는 것입니다. 리포지토리에 대한 참조를 전혀 지니지 않기 때문입니다. 따라서 누군가를 하나의 프로젝트에 가두는 데 결코 쓸 수 없습니다.

검사통과 대상무엇을 위한 것인가
슈퍼 관리자 admin 역할, 그리고 그것뿐 서버 전체에 해당하는 모든 것: 계정, 그룹, 감사 로그, 전역 통계, 라이선스, 서버 업데이트.
리포지토리의 관리자 admin, 또는 이 특정 리포지토리를 관리하는 project_admin 프로젝트에 고유한 모든 것: 권한, 검증 규칙, 웹훅, 빌드 접근, 강제 잠금 해제, 가비지 컬렉션.
서버 전체 기능 이름 있는 기능을 보유한 모든 역할 몇몇 횡단 라우트. 주의: 기능은 모든 리포지토리에서 유효하며, 그것이 그 본성입니다. 하나도 보유하지 않은 project_admin은, 대신 자신이 관리하는 리포지토리에서만 허용됩니다.
입장만 admin 또는 project_admin 들여보내지만, 아무것도 인가하지 않습니다. 그것을 사용하는 라우트는 그 뒤, 자신의 결과를 호출자가 관리하는 리포지토리로 스스로 제한해야 합니다.

달리 말하면: manage_usersmanage_permissions 기능은 서류상으로만 존재합니다. 설치 시 데이터베이스에 분명히 생성되지만, 어떤 코드 줄도 그것을 조회하지 않습니다. 역할에 부여해도 전혀 달라지지 않습니다. manage_users를 받았다는 이유로 admin이 아닌 계정이 사용자를 관리할 수 있다고 가정하는 통합을 작성하지 마세요: 403을 받습니다.

라우트실제 검사설명
GET /api/admin/users입장만, 그 후 필터링project_admin은 자기 범위의 계정만 봅니다
POST /api/admin/users입장만계정을 생성합니다
PUT/DELETE /api/admin/users/{id}슈퍼 관리자계정을 업데이트하거나 삭제합니다
POST /api/admin/users/{id}/reset-password슈퍼 관리자비밀번호를 재설정하고 계정의 모든 토큰을 취소합니다
GET/POST /api/admin/groups슈퍼 관리자그룹은 전역이며, 프로젝트별로 위임할 수 없습니다
GET/POST /api/admin/permissions/{repo_id}이 리포지토리의 관리자경로 패턴별 권한 규칙, 그룹 또는 사용자에게 부여
GET/POST /api/admin/repositories/{repo_id}/rules이 리포지토리의 관리자전송 전에 적용되는 검증 규칙
GET/POST /api/admin/repositories/{repo_id}/webhooks이 리포지토리의 관리자Discord, Slack, Teams, 또는 범용 웹훅
GET /api/admin/locks입장만, 그 후 필터링잠금, 관리하는 리포지토리로 제한
POST /api/admin/locks/{lock_id}/force-release이 리포지토리의 관리자다른 사람의 잠금을 제거합니다. 감사 로그에 기록
GET /api/admin/audit슈퍼 관리자감사 로그, 필터 가능하고 페이지로 나뉨
GET /api/admin/stats슈퍼 관리자스토리지 용량, 중복 제거율, 증가
GET /api/admin/licence슈퍼 관리자라이선스 상태와 소비된 좌석
GET /api/admin/repositories/{repo_id}/gc/preview이 리포지토리의 관리자아무것도 삭제하지 않고 가비지 컬렉션을 시뮬레이션합니다
POST /api/admin/repositories/{repo_id}/gc이 리포지토리의 관리자가비지 컬렉션을 시작합니다

역할, 그 서열, 그리고 각자가 할 수 있는 것의 완전한 상세는 역할과 권한에 할애된 페이지에 있습니다.

이 페이지에서 다루지 않은 API 영역

다음 라우트 계열은 존재하고, 서버에 의해 마운트되어 제공되지만, 여기서는 설명하지 않습니다. 통합에서 그것들이 필요하면, 오늘 가장 신뢰할 수 있는 것은 데스크톱 클라이언트가 하는 호출을 관찰하거나, 우리에게 연락하는 것입니다.

접두사무엇을 다루는가
/api/advisor/*Project Health: Unreal 프로젝트 감사, 그 소견, 기둥별 점수, 그리고 무시할 항목의 분류.
/api/distribution/*프로젝트 간 배포: 소스 리포지토리와 대상 리포지토리 사이의 링크, 한 프로젝트에서 다른 프로젝트로의 파일 게시, 이력.
/api/binaries/*커밋에 결부된, UnrealGameSync 모델에 따른 사전 컴파일된 에디터 바이너리.
/api/watchlist/*위에서 설명한 관심 패턴을 넘어: 알림과 받은 편지함.
/api/profile/*현재 사용자의 프로필.
/api/admin/repositories/{repo_id}/admins누가 리포지토리를 관리하는가: 프로젝트 관리자의 지명과 해임.
/api/admin/repositories/{repo_id}/build-access누가 이 프로젝트의 빌드를 다운로드할 수 있는가, 프로젝트별로 부여.
/api/admin/licence라이선스 상태와 소비된 좌석.
/api/admin/server/*관리 패널에서의 서버 업데이트.

상태 확인

GET /health

인증되지 않은 엔드포인트로, 서버가 데이터베이스에 접근할 수 있으면 200 OK를 반환합니다. 로드 밸런서나 모니터링의 상태 확인에 사용됩니다(Prometheus blackbox, Datadog synthetic 등).

Response 200: OK(text/plain).

Response 503: 데이터베이스에 도달할 수 없는 경우.

GET /api/server-info

서버 메타데이터(버전 등)를 반환하는 공개, 인증되지 않은 엔드포인트. /health와 마찬가지로, 인증 미들웨어로 보호되지 않습니다.

오류 형식

오류의 경우, 인증된 라우트는 { "success": false, "error": "..." }를, /api/auth/* 라우트는 { "error": "..." }를 반환하며, 두 경우 모두 적절한 HTTP 코드를 동반합니다.

error 필드는 코드가 아니라 인간을 향한 메시지입니다. 안정적인 오류 코드의 카탈로그는 본문에도 헤더에도 존재하지 않습니다. 따라서 그 내용 위에 로직을 구축하지 말고, 그 접두사를 분석하지 마세요: 이 문장들은 버전마다 재표현되며, 조용히 깨지는 문자열 테스트는 아예 테스트가 없는 것보다 나쁩니다.

동작을 분기할 근거로 삼을 수 있는 것은 HTTP 코드뿐입니다. 아래 표가 그 읽는 법을 줍니다.

HTTP의미언제
400Bad request무효한 파라미터, 잘못된 JSON, 본문이 너무 큼(하드한 413 제한 이전)
401Not authenticated토큰 부재, 만료, 무효한 서명, 또는 token_version 불일치
403Permission denied기능 부재, 또는 path / repo에 대한 권한 없음
404Resource not foundrepo / 파일 / commit / user가 없거나 접근 불가
409Conflict잠금이 이미 보유됨, 동시 commit, unique 제약 위반
413Payload too large본문이 제한을 초과(/files에서 1 GB, /builds/upload에서 8 GB)
429Too many requests/auth/login에서만(사용자별 속도 제한)
500Internal server errorDB 오류, IO 오류. 서버 측에 로깅됨, 버그를 보고하면 timestamp를 첨부하세요.
503Service unavailable데이터베이스 도달 불가(health check) 또는 마이그레이션 진행 중