# Data Repository API v1

## 1. Overview

Data Repository API v1은 Data Repository의 저장 데이터를 외부 클라이언트가 읽기 전용으로 탐색하고 파일을 다운로드할 수 있도록 제공하는 HTTP API입니다.

공식 Public API 경계:

```text
/api/v1/**
```

현재 v1은 **Read-only**입니다.

| Method | Endpoint | Description |
|---|---|---|
| GET | `/api/v1/directory` | 디렉터리 정보 조회 |
| GET | `/api/v1/directory/contents` | 디렉터리의 하위 디렉터리와 항목 조회 |
| GET | `/api/v1/assets/{id}` | 항목 상세 및 파일 목록 조회 |
| GET | `/api/v1/assets/{id}/files/{file_id}/download` | 파일 다운로드 |

## 2. Base URL

Data Repository API v1은 현재 **두 가지 접근 경로를 병행**합니다.

### Internal / Tailscale

```text
http://100.89.135.42:18432
```

현재 운영자가 직접 사용하는 내부 접근 경로입니다. Tailscale 네트워크에 접근 가능한 클라이언트만 사용할 수 있습니다.

### Production Domain / HTTPS

정식 도메인과 Synology Reverse Proxy가 구성되면 다음 형태의 주소를 함께 제공합니다.

```text
https://<production-domain>
```

도메인 도입 이후에도 API endpoint 자체는 동일하며 Base URL만 달라집니다.

```text
${BASE_URL}/api/v1/...
```

### Client Base URL Variable

문서의 모든 예제는 `BASE_URL` 변수를 기준으로 사용합니다.

현재 Tailscale 사용:

```bash
BASE_URL="http://100.89.135.42:18432"
```

향후 Production Domain 사용:

```bash
BASE_URL="https://<production-domain>"
```

따라서 클라이언트는 API 경로를 코드에 절대 URL로 반복해서 하드코딩하지 않고 Base URL을 환경/설정으로 분리하는 것을 권장합니다.

예:

```text
${BASE_URL}/api/v1/directory
${BASE_URL}/api/v1/directory/contents
${BASE_URL}/api/v1/assets/{id}
${BASE_URL}/api/v1/assets/{id}/files/{file_id}/download
```

> 도메인 도입 전에는 Tailscale Base URL이 실제 운영 가능한 주소입니다. 도메인과 HTTPS가 도입된 뒤에는 두 접근 경로를 병행할 수 있으며, 외부 공유용 클라이언트에는 Production Domain 사용을 권장합니다.

## 3. Versioning

현재 API version:

```text
/api/v1
```

클라이언트는 `/api/v1/**`만 공식 API 계약으로 사용해야 합니다.

## 4. Authentication and Access Control

현재 API v1에는 애플리케이션 레벨의 API Key, Bearer Token, HTTP Basic 등의 별도 인증이 구현되어 있지 않습니다.

현재 Tailscale 접근 경로는 Tailscale 네트워크 자체의 접근 통제를 전제로 합니다.

```text
http://100.89.135.42:18432
```

향후 Production Domain / HTTPS 접근 경로가 추가되더라도 **HTTPS 자체는 사용자 인증을 제공하지 않습니다.**

따라서 공용 인터넷에서 접근 가능한 형태로 제공할 경우에는 API authentication 또는 Reverse Proxy 수준의 접근 제어 정책을 별도로 구성해야 합니다.

클라이언트 관점에서 인증 방식이 추가되지 않는 동안에는 Base URL만 선택하면 API 계약 자체는 동일합니다.

## 5. Content Type

JSON 응답:

```http
Content-Type: application/json; charset=utf-8
```

파일 다운로드 endpoint의 최초 응답은 redirect입니다.

## 6. Directory Path Semantics

Directory API의 `path` query parameter는 **Data Root인 `data` 아래의 상대 경로**입니다.

예를 들어 전체 경로가:

```text
data/core/SMS
```

라면 API 요청에는:

```text
core/SMS
```

를 사용합니다.

올바른 예:

```text
?path=core/SMS
?path=개인자료/사진/2026
```

잘못된 예:

```text
?path=data
?path=data/core/SMS
```

`data` prefix를 포함하면 `400 invalid_path`가 반환됩니다.

### Root Directory

Root는 `path`를 생략하거나 빈 문자열로 요청합니다.

```http
GET /api/v1/directory
GET /api/v1/directory?path=
```

현재 root 응답 예:

```json
{
  "directory": {
    "id": 1,
    "name": "data",
    "path": "data",
    "relative_path": "",
    "root": true
  }
}
```

### Unicode Path

한글 등 Unicode path를 지원합니다.

예:

```text
개인자료/사진/2026
```

클라이언트는 query parameter를 정상적으로 URL encoding해야 합니다.

## 7. Timestamps

시간 값은 ISO 8601 문자열이며 timezone offset을 포함합니다.

예:

```text
2026-08-07T11:21:56.997+09:00
```

---

# 8. Directory API

## 8.1 Get Directory

### Request

```http
GET /api/v1/directory
GET /api/v1/directory?path={relative_path}
```

| Parameter | Type | Required | Description |
|---|---|---:|---|
| `path` | string | No | `data` 기준 상대 경로. 생략/빈 값이면 root |

### Example

```bash
curl -sS --get \
  --data-urlencode "path=core/SMS" \
  "$BASE_URL/api/v1/directory"
```

### Response — 200 OK

```json
{
  "directory": {
    "id": 3,
    "name": "SMS",
    "path": "data/core/SMS",
    "relative_path": "core/SMS",
    "root": false
  }
}
```

### Directory Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Directory identifier |
| `name` | string | Directory name |
| `path` | string | 전체 canonical path |
| `relative_path` | string | `data` 아래 상대 경로 |
| `root` | boolean | Data Root 여부 |

## 8.2 Get Directory Contents

디렉터리 자체 정보, 직속 하위 디렉터리, 살아 있는(non-trash) 항목을 함께 반환합니다.

### Request

```http
GET /api/v1/directory/contents
GET /api/v1/directory/contents?path={relative_path}
```

### Example

```bash
curl -sS --get \
  --data-urlencode "path=core/SMS" \
  "$BASE_URL/api/v1/directory/contents"
```

### Response — 200 OK

```json
{
  "directory": {
    "id": 3,
    "name": "SMS",
    "path": "data/core/SMS",
    "relative_path": "core/SMS",
    "root": false
  },
  "directories": [],
  "assets": [
    {
      "id": 13,
      "name": "정렬 테스트",
      "category": "테스트",
      "archived": false,
      "file_count": 0,
      "total_bytes": 0,
      "updated_at": "2026-08-07T11:21:56.997+09:00"
    }
  ]
}
```

### Child Directory Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Directory identifier |
| `name` | string | Directory name |
| `path` | string | 전체 canonical path |
| `relative_path` | string | `data` 아래 상대 경로 |

### Asset Summary Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Asset identifier |
| `name` | string | Asset name |
| `category` | string | Category name |
| `archived` | boolean | Archive 상태 |
| `file_count` | integer | 살아 있는 file 수 |
| `total_bytes` | integer | 살아 있는 file 총 byte 수 |
| `updated_at` | string | ISO 8601 timestamp |

현재 구현의 ordering:

- `directories`: `name` 오름차순
- `assets`: `updated_at` 내림차순

문자열 정렬은 DB collation의 영향을 받을 수 있습니다.

`assets`에는 `kept` 상태의 항목만 포함됩니다. 휴지통 항목은 노출되지 않습니다.

---

# 9. Asset API

## 9.1 Get Asset

### Request

```http
GET /api/v1/assets/{id}
```

| Parameter | Type | Description |
|---|---|---|
| `id` | integer | Asset identifier |

### Example

```bash
curl -sS \
  "$BASE_URL/api/v1/assets/14"
```

### Response — 200 OK

```json
{
  "asset": {
    "id": 14,
    "name": "테스트 자료",
    "archived": false,
    "category": {
      "id": 4,
      "name": "테스트"
    },
    "directory": {
      "id": 3,
      "name": "SMS",
      "path": "data/core/SMS",
      "relative_path": "core/SMS"
    },
    "file_count": 1,
    "total_bytes": 1462848,
    "created_at": "2026-08-06T11:10:00.430+09:00",
    "updated_at": "2026-08-06T11:10:00.430+09:00"
  },
  "files": [
    {
      "id": 5,
      "filename": "ChatGPT Installer.exe",
      "content_type": "application/x-msdownload;format=pe32",
      "byte_size": 1462848,
      "created_at": "2026-08-06T11:10:00.463+09:00"
    }
  ]
}
```

> 위 ID/이름/파일명/크기는 runtime 검증 당시의 예시 데이터이며 고정 계약이 아닙니다.

### Asset Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Asset identifier |
| `name` | string | Asset name |
| `archived` | boolean | Archive 상태 |
| `category.id` | integer | Category identifier |
| `category.name` | string | Category name |
| `directory.id` | integer | Directory identifier |
| `directory.name` | string | Directory name |
| `directory.path` | string | 전체 canonical path |
| `directory.relative_path` | string | 상대 path |
| `file_count` | integer | 살아 있는 file 수 |
| `total_bytes` | integer | 살아 있는 file 총 byte 수 |
| `created_at` | string | ISO 8601 timestamp |
| `updated_at` | string | ISO 8601 timestamp |

### File Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | 공개 API의 file identifier (`AssetFile` ID) |
| `filename` | string | 원본 file name |
| `content_type` | string | MIME/content type |
| `byte_size` | integer | File size in bytes |
| `created_at` | string | ISO 8601 timestamp |

다운로드 endpoint의 `{file_id}`에는 `files[].id`를 사용합니다.

클라이언트는 Active Storage Blob ID나 storage key를 추측하거나 사용해서는 안 됩니다.

---

# 10. File Download API

## 10.1 Download File

### Request

```http
GET /api/v1/assets/{id}/files/{file_id}/download
```

| Parameter | Type | Description |
|---|---|---|
| `id` | integer | Asset identifier |
| `file_id` | integer | Asset 상세의 `files[].id` |

### Example

```bash
curl -fL \
  "$BASE_URL/api/v1/assets/14/files/5/download" \
  -o downloaded-file
```

### Redirect Contract

최초 응답은 production runtime 기준:

```http
HTTP/1.1 302 Found
Location: http://.../rails/active_storage/...
```

클라이언트는 redirect를 따라가야 합니다. `curl`에서는 `-L`을 사용합니다.

Runtime 검증에서는 예상 `1462848` bytes 파일이 최종 다운로드 후 실제로 `1462848` bytes와 일치했습니다.

### Internal URL Warning

다음은 내부 구현 경로입니다.

```text
/rails/active_storage/**
```

클라이언트는 redirect URL을 저장하거나 조립해서는 안 됩니다.

항상 공식 endpoint:

```text
/api/v1/assets/{id}/files/{file_id}/download
```

에서 다운로드를 시작합니다.

---

# 11. Error Contract

공통 JSON error envelope:

```json
{
  "error": {
    "code": "...",
    "message": "..."
  }
}
```

## 11.1 Invalid Path — 400 Bad Request

예:

```http
GET /api/v1/directory?path=data
```

응답:

```json
{
  "error": {
    "code": "invalid_path",
    "message": "data는 생략하고 data 아래의 상대 경로만 입력해주세요"
  }
}
```

클라이언트는 사람이 읽는 `message`보다 `error.code`를 분기 기준으로 사용하는 것이 권장됩니다.

## 11.2 Not Found — 404 Not Found

존재하지 않는 정상 형식의 Directory, Asset, File은 동일한 오류를 반환합니다.

```json
{
  "error": {
    "code": "not_found",
    "message": "요청한 리소스를 찾을 수 없습니다."
  }
}
```

Runtime에서 다음을 모두 검증했습니다.

- 존재하지 않는 Directory
- 존재하지 않는 Asset
- 존재하는 Asset 아래의 존재하지 않는 File

---

# 12. HTTP Status Summary

| Status | Meaning |
|---:|---|
| `200 OK` | Directory / Contents / Asset 조회 성공 |
| `302 Found` | File download endpoint redirect |
| `400 Bad Request` | 잘못된 directory path |
| `404 Not Found` | Directory / Asset / File 없음 |

그 외 서버 오류는 일반 HTTP 오류로 취급하며 현재 v1의 안정된 application error contract로 별도 정의되어 있지 않습니다.

---

# 13. Recommended Client Flow

```text
1. GET /api/v1/directory/contents
        ↓
2. directories[].relative_path 로 하위 탐색
        ↓
3. assets[].id 확보
        ↓
4. GET /api/v1/assets/{id}
        ↓
5. files[].id 확보
        ↓
6. GET /api/v1/assets/{id}/files/{file_id}/download
        ↓
7. HTTP redirect를 따라 file 수신
```

Directory 탐색에는 응답의 `relative_path`를 사용하는 것이 가장 안전합니다.

---

# 14. curl Quick Start

먼저 사용할 접근 경로를 선택합니다.

Tailscale:

```bash
BASE_URL="http://100.89.135.42:18432"
```

Production Domain / HTTPS 도입 후:

```bash
BASE_URL="https://<production-domain>"
```

Root contents:

```bash
curl -sS "$BASE_URL/api/v1/directory/contents"
```

Nested contents:

```bash
curl -sS --get \
  --data-urlencode "path=core/SMS" \
  "$BASE_URL/api/v1/directory/contents"
```

Asset:

```bash
curl -sS "$BASE_URL/api/v1/assets/14"
```

Download:

```bash
curl -fL \
  "$BASE_URL/api/v1/assets/14/files/5/download" \
  -o output.bin
```

---

# 15. Public API Boundary

## Supported Public Contract

```text
/api/v1/**
```

현재 공식 endpoint:

```text
GET /api/v1/directory
GET /api/v1/directory/contents
GET /api/v1/assets/{id}
GET /api/v1/assets/{id}/files/{file_id}/download
```

## Not Public API

```text
/directories/**
/assets/**
/categories/**
/rails/**
```

특히:

```text
/rails/active_storage/**
```

는 Rails/Active Storage 내부 구현입니다.

클라이언트는 내부 route, signed blob URL, storage key, Rails controller 이름 등에 의존해서는 안 됩니다.

---

# 16. Read-only Contract

API v1에는 mutation endpoint가 없습니다.

지원하지 않는 Public API 기능:

```text
POST
PATCH
PUT
DELETE
upload
restore
purge
trash mutation
```

웹 UI에 mutation route가 존재하더라도 API v1 Public Contract가 아닙니다.

---

# 17. Compatibility Guidance

클라이언트 권장 원칙:

1. `/api/v1`만 공식 API namespace로 사용합니다.
2. 미래에 새 JSON field가 추가될 수 있으므로 알 수 없는 field를 허용합니다.
3. breaking change는 새로운 API version에서 처리하는 것을 원칙으로 합니다.
4. `id`는 opaque identifier처럼 취급하고 의미나 연속성을 추론하지 않습니다.
5. File download redirect URL을 저장하지 않습니다.
6. Directory 탐색에는 `relative_path`를 사용합니다.
7. Timestamp는 timezone-aware ISO 8601로 파싱합니다.
8. Error handling은 `message`보다 `error.code`를 우선합니다.

---

# 18. Runtime Validation Record

이 문서는 Rails route/controller 구현과 Synology NAS production runtime의 실제 HTTP 호출을 함께 검증하여 작성되었습니다.

검증 완료:

```text
GET Directory root                         200
GET Directory empty path                   200
GET Directory relative path                200
GET Directory Unicode path                 200
GET Directory Contents root                200
GET Directory Contents nested              200
GET Asset                                  200
GET File Download                          302
Follow File Download                       success
Invalid "data" prefix                      400 invalid_path
Nonexistent Directory                      404 not_found
Nonexistent Asset                          404 not_found
Nonexistent File                           404 not_found
JSON Content-Type                          verified
ISO 8601 timezone-aware timestamps         verified
```

---

# 19. Current Deployment Note

현재 검증 환경:

```text
Environment: Production
Host: Synology NAS

Available now:
  Tailscale Base URL:
  http://100.89.135.42:18432

Planned additional access:
  Production Domain / HTTPS:
  https://<production-domain>
```

현재는 Tailscale Base URL을 사용합니다.

정식 도메인과 Synology Reverse Proxy가 적용되면 Production Domain / HTTPS Base URL을 추가하고, 기존 Tailscale 경로는 운영자용 내부 접근 경로로 병행할 수 있습니다.

API namespace와 endpoint 계약은 어느 Base URL을 사용하더라도 동일합니다.

```text
${BASE_URL}/api/v1/**
```

---

# 20. Security Note

현재 API에는 별도 application-level authentication이 없습니다.

### Tailscale Access

현재 Tailscale Base URL은 Tailscale 네트워크 접근 통제를 전제로 사용합니다.

```text
http://100.89.135.42:18432
```

### Production Domain / HTTPS

향후 정식 도메인과 Reverse Proxy를 통해 다음 접근 경로를 추가할 수 있습니다.

```text
https://<production-domain>
```

HTTPS는 전송 구간을 암호화하지만 API 사용자를 인증하지는 않습니다.

따라서 Production Domain이 공용 인터넷에서 접근 가능해질 경우에는 다음 중 적절한 보호 계층을 추가해야 합니다.

- API authentication
- Reverse Proxy authentication/access policy
- IP/network allow-list
- TLS/HTTPS

인증 방식이 추가되면 본 문서의 Authentication section 및 request example을 함께 갱신해야 합니다.

### Client Configuration Recommendation

클라이언트는 주소를 코드에 직접 반복해서 작성하지 말고 Base URL을 환경 설정으로 분리하는 것을 권장합니다.

```text
BASE_URL=http://100.89.135.42:18432
```

또는:

```text
BASE_URL=https://<production-domain>
```

API 경로는 동일하게 유지합니다.

```text
${BASE_URL}/api/v1/...
```
