add pipelines
This commit is contained in:
@@ -1,24 +1,4 @@
|
||||
# Cloud# — 데이터베이스 개념적 설계서
|
||||
|
||||
> 본 문서는 Cloud# 서비스의 핵심 엔티티에 대한 개념적 DB 설계를 정의한다.
|
||||
> 물리적 DDL이 아닌 **엔티티의 역할, 컬럼 의미, 설계 의도, 제약 조건**을 중심으로 기술한다.
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
|
||||
1. [엔티티 목록](#1. 엔티티 목록)
|
||||
2. [ER 다이어그램](#2-er-다이어그램)
|
||||
3. [엔티티 상세 설계](#3-엔티티-상세-설계)
|
||||
- [3.1 User](#31-user)
|
||||
- [3.2 Folder](#32-folder)
|
||||
- [3.3 FileItem](#33-fileitem)
|
||||
- [3.4 UploadSession](#34-uploadsession)
|
||||
4. [관계 정의](#4-관계-정의)
|
||||
5. [제약 조건 요약](#5-제약-조건-요약)
|
||||
6. [설계 결정 사항 (Decision Log)](#6-설계-결정-사항-decision-log)
|
||||
|
||||
---
|
||||
# Cloud# — 데이터베이스 설계서
|
||||
|
||||
## 1. 엔티티 목록
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
|
||||
![[file_download_flow.svg]]
|
||||
## 단계별 상세
|
||||
|
||||
### 1. 사용자 인증 / 공유 링크 접근
|
||||
|
||||
요청의 진입점으로, 두 가지 접근 경로를 처리한다.
|
||||
|
||||
- **인증 사용자**: `Authorization` 헤더의 JWT Bearer 토큰을 파싱 및 검증
|
||||
- **공유 링크**: URL 쿼리 파라미터의 `share_token`을 파싱
|
||||
|
||||
두 경로 모두 이후 단계에서 동일한 인터페이스로 처리될 수 있도록 내부적으로 정규화한다.
|
||||
|
||||
---
|
||||
|
||||
### 2. 파일 권한 / 공유 상태 검증
|
||||
|
||||
DB를 조회하여 요청자가 해당 파일에 접근할 수 있는지 확인한다.
|
||||
|
||||
- 인증 사용자: 파일의 소유자 또는 공유 대상 여부 확인
|
||||
- 공유 링크: 링크의 활성화 여부, 만료 여부, 비밀번호 보호 여부 확인
|
||||
- 권한이 없으면 즉시 **`403 Forbidden`** 반환 (이후 단계로 진행하지 않음)
|
||||
|
||||
##### Risk
|
||||
권한이 없는 경우 `403`, 파일이 없는 경우 `404`를 분리 반환한다. 이 차이를 이용하면 공격자가 파일 ID를 열거하여 존재하는 파일 목록을 추론할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
### 3. 다운로드 토큰 발급
|
||||
|
||||
실제 스트리밍 요청에 사용할 단명(short-lived) 토큰을 생성한다.
|
||||
|
||||
- TTL은 **약 60초** 권장 (재사용·탈취 위험 최소화)
|
||||
- 토큰에는 파일 ID, 요청자 식별자, 만료 시각을 포함
|
||||
- HMAC 서명 또는 JWT로 위변조 방지
|
||||
|
||||
```
|
||||
# Redis 기반 원자적 무효화
|
||||
result = REDIS.SET(f"dl_token:{token}", "used", NX=True, EX=60)
|
||||
# NX: key가 없을 때만 SET → 원자적 보장
|
||||
# result가 None이면 이미 사용된 토큰 → 401
|
||||
|
||||
```
|
||||
|
||||
> API 서버와 스트리밍 서버를 분리 운영할 때 특히 유용하다.
|
||||
> 클라이언트는 이 토큰을 받아 스트리밍 엔드포인트에 직접 요청한다.
|
||||
|
||||
---
|
||||
|
||||
### 4. 다운로드 토큰으로 파일 스트림 요청
|
||||
|
||||
클라이언트가 발급받은 토큰으로 스트리밍 엔드포인트에 요청한다.
|
||||
|
||||
```
|
||||
GET /files/stream?token={download_token}
|
||||
Range: bytes=0-1048575 (선택, Range 요청 시)
|
||||
```
|
||||
|
||||
- 토큰이 만료되었거나 유효하지 않으면 **`401 Unauthorized`** 반환
|
||||
- 토큰은 1회 사용 후 무효화하는 것을 권장 (재생 공격 방어)
|
||||
|
||||
#### Risk
|
||||
- "토큰은 1회 사용 후 무효화하는 것을 권장"이라고만 기술되어 있으나, 동시에 같은 토큰으로 2개 이상의 요청이 들어오는 경우의 원자적 무효화 전략이 없다.
|
||||
- 토큰을 1회 사용 후 무효화하면, 대용량 파일의 Range 기반 이어받기(resume)가 불가능해진다. 네트워크 끊김 후 클라이언트가 `Range: bytes=10485760-`으로 재요청하면, 이미 무효화된 토큰이므로 `401`이 반환된다.
|
||||
-
|
||||
---
|
||||
|
||||
### 5. storage_key → 로컬 FS 경로 resolve
|
||||
|
||||
`storage_key`를 실제 파일 시스템 경로로 변환한다.
|
||||
|
||||
- 키-경로 맵(DB 또는 설정 파일)을 조회하여 절대 경로 획득
|
||||
- **Path traversal 방어 필수**: `realpath()` 등으로 경로를 정규화하고, 허용된 루트 디렉터리(`/data/uploads/` 등) 내에 있는지 반드시 검증
|
||||
- 파일이 존재하지 않으면 **`404 Not Found`** 반환
|
||||
|
||||
---
|
||||
|
||||
### 6. 파일 스트림 응답 (Range 지원 권장)
|
||||
|
||||
파일을 청크 단위로 스트리밍하여 응답한다.
|
||||
|
||||
**응답 헤더:**
|
||||
|
||||
|헤더|값|
|
||||
|---|---|
|
||||
|`Content-Type`|파일 MIME 타입|
|
||||
|`Content-Disposition`|`attachment; filename="파일명"`|
|
||||
|`Accept-Ranges`|`bytes`|
|
||||
|`Content-Length`|파일 크기 (전체 또는 청크)|
|
||||
|
||||
**Range 지원 시 동작:**
|
||||
|
||||
- `Range` 헤더가 있으면 **`206 Partial Content`** 로 응답
|
||||
- `Range` 헤더가 없으면 **`200 OK`** 로 전체 파일 응답
|
||||
- Range를 지원하면 대용량 파일 resume, 미디어 스트리밍, 멀티파트 다운로드 모두 대응 가능
|
||||
|
||||
|
||||
#### Risk
|
||||
- 응답 헤더에 `Content-Type: 파일 MIME 타입`이라고만 되어 있고, 이 값을 **어디서 가져오는지** 명시되어 있지 않다. 업로드 시 클라이언트가 보낸 값을 DB에 저장하고 그대로 사용하는 경우, 공격자가 `text/html`이나 `image/svg+xml`로 위장한 악성 파일을 업로드할 수 있다.
|
||||
- 로컬 FS에서 직접 스트리밍하므로 API/스트리밍 서버의 egress 대역폭이 병목이 된다. 동시 다운로드 수 제한, 사용자별 대역폭 제한, 전체 서버 대역폭 보호 정책이 없다.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 7. 로그 기록 / 카운트 증가
|
||||
|
||||
다운로드 완료 후 감사(Audit) 로그를 기록하고, 공유 링크의 경우 카운트를 증가시킨다.
|
||||
|
||||
- **로그 항목**: 파일 ID, 요청자, IP, User-Agent, 다운로드 시각, 파일 크기
|
||||
- **카운트 증가**: 공유 링크에만 적용 (`download_count++`)
|
||||
- **타이밍 주의**: 스트림이 **완전히 전송된 이후**에 카운트를 올린다. 요청 시점에 올리면 중단된 다운로드도 집계된다.
|
||||
|
||||
---
|
||||
|
||||
## 에러 응답 정리
|
||||
|
||||
|상황|HTTP 상태 코드|
|
||||
|---|---|
|
||||
|인증 실패 / 토큰 만료|`401 Unauthorized`|
|
||||
|권한 없음|`403 Forbidden`|
|
||||
|파일 없음|`404 Not Found`|
|
||||
|Range 범위 초과|`416 Range Not Satisfiable`|
|
||||
|정상 전체 응답|`200 OK`|
|
||||
|정상 부분 응답 (Range)|`206 Partial Content`|
|
||||
|
||||
---
|
||||
|
||||
## 보안 체크리스트
|
||||
|
||||
- [ ] JWT / share_token 서명 검증
|
||||
- [ ] 파일 접근 권한 DB 조회 (캐시 사용 시 TTL 주의)
|
||||
- [ ] 다운로드 토큰 단명 발급 (TTL ≤ 60s)
|
||||
- [ ] 토큰 1회 사용 후 무효화
|
||||
- [ ] Path traversal 방어 (`realpath` + 루트 경로 검증)
|
||||
- [ ] `Content-Disposition: attachment` 헤더 명시
|
||||
- [ ] 다운로드 완료 후 감사 로그 기록
|
||||
127
설계/img/download_flowchart_part1.svg
Normal file
127
설계/img/download_flowchart_part1.svg
Normal file
@@ -0,0 +1,127 @@
|
||||
<svg width="100%" viewBox="0 0 680 980" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<mask id="imagine-text-gaps-ke4rf7" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="980" fill="white"/><rect x="289.46826171875" y="31.31909942626953" width="101.06349182128906" height="21.36180305480957" fill="black" rx="2"/><rect x="344.0000305175781" y="72.52478790283203" width="76.78408813476562" height="18.95044231414795" fill="black" rx="2"/><rect x="276.8463134765625" y="103.31910705566406" width="126.30742645263672" height="21.36180305480957" fill="black" rx="2"/><rect x="254.30764770507812" y="124.5247802734375" width="171.38482666015625" height="18.95044231414795" fill="black" rx="2"/><rect x="309.39215087890625" y="210.5247802734375" width="61.215736389160156" height="18.95044231414795" fill="black" rx="2"/><rect x="453.7825622558594" y="199.9431915283203" width="24.4349365234375" height="18.95044231414795" fill="black" rx="2"/><rect x="527.9691162109375" y="211.319091796875" width="124.06185150146484" height="21.36180305480957" fill="black" rx="2"/><rect x="343.5177307128906" y="266.5247802734375" width="28.428754806518555" height="18.95044231414795" fill="black" rx="2"/><rect x="267.53619384765625" y="303.319091796875" width="144.9276580810547" height="21.36180305480957" fill="black" rx="2"/><rect x="246.4066619873047" y="324.5248107910156" width="187.18678283691406" height="18.95044231414795" fill="black" rx="2"/><rect x="309.39215087890625" y="410.5247802734375" width="61.215736389160156" height="18.95044231414795" fill="black" rx="2"/><rect x="453.7825622558594" y="399.9432067871094" width="24.4349365234375" height="18.95044231414795" fill="black" rx="2"/><rect x="536.8534545898438" y="411.319091796875" width="106.29312896728516" height="21.36180305480957" fill="black" rx="2"/><rect x="343.5177307128906" y="466.5248107910156" width="28.428754806518555" height="18.95044231414795" fill="black" rx="2"/><rect x="248.38096618652344" y="503.31915283203125" width="183.23817443847656" height="21.36180305480957" fill="black" rx="2"/><rect x="236.07171630859375" y="524.5248413085938" width="207.85665893554688" height="18.95044231414795" fill="black" rx="2"/><rect x="298.3526306152344" y="610.5247802734375" width="83.29476928710938" height="18.95044231414795" fill="black" rx="2"/><rect x="453.7825622558594" y="599.9432373046875" width="24.4349365234375" height="18.95044231414795" fill="black" rx="2"/><rect x="536.8534545898438" y="611.319091796875" width="106.29312896728516" height="21.36180305480957" fill="black" rx="2"/><rect x="343.5177307128906" y="666.5248413085938" width="28.428754806518555" height="18.95044231414795" fill="black" rx="2"/><rect x="267.3025817871094" y="703.319091796875" width="145.39486694335938" height="21.36180305480957" fill="black" rx="2"/><rect x="241.73464965820312" y="724.5247802734375" width="196.5308074951172" height="18.95044231414795" fill="black" rx="2"/><rect x="283.032958984375" y="799.3191528320312" width="114.07769012451172" height="21.36180305480957" fill="black" rx="2"/><rect x="222.00669860839844" y="820.5248413085938" width="236.26307678222656" height="18.95044231414795" fill="black" rx="2"/><rect x="267.04638671875" y="897.3191528320312" width="145.9072723388672" height="21.36180305480957" fill="black" rx="2"/><rect x="56.000003814697266" y="937.5247802734375" width="54.99894714355469" height="18.95044231414795" fill="black" rx="2"/><rect x="156" y="937.5247802734375" width="54.99894714355469" height="18.95044231414795" fill="black" rx="2"/><rect x="256" y="937.5247802734375" width="54.75307083129883" height="18.95044231414795" fill="black" rx="2"/><rect x="356" y="937.5247802734375" width="54.99894714355469" height="18.95044231414795" fill="black" rx="2"/></mask></defs>
|
||||
|
||||
<!-- START -->
|
||||
<g onclick="sendPrompt('파일 다운로드 요청 시작 조건은?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="240" y="20" width="200" height="44" rx="22" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="42" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">클라이언트 요청</text>
|
||||
</g>
|
||||
|
||||
<!-- Step1 label -->
|
||||
<line x1="340" y1="64" x2="340" y2="94" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="348" y="82" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">다운로드 시도</text>
|
||||
|
||||
<!-- 1단계: 인증 -->
|
||||
<g onclick="sendPrompt('인증 방식 차이: JWT vs share_token')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="190" y="94" width="300" height="56" rx="8" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="114" text-anchor="middle" dominant-baseline="central" style="fill:rgb(60, 52, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">1단계 — 사용자 인증</text>
|
||||
<text x="340" y="134" text-anchor="middle" dominant-baseline="central" style="fill:rgb(83, 74, 183);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">JWT Bearer 또는 share_token</text>
|
||||
</g>
|
||||
|
||||
<!-- 인증 분기 다이아몬드 -->
|
||||
<line x1="340" y1="150" x2="340" y2="190" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<polygon points="340,190 400,222 340,254 280,222" fill="none" stroke="var(--color-text-secondary)" stroke-width="0.5" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="220" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">토큰 유효?</text>
|
||||
|
||||
<!-- No → 401 -->
|
||||
<line x1="400" y1="222" x2="530" y2="222" marker-end="url(#arrow)" mask="url(#imagine-text-gaps-ke4rf7)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="466" y="214" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">No</text>
|
||||
<g onclick="sendPrompt('401 Unauthorized 처리 시 클라이언트 동작은?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="530" y="200" width="120" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="590" y="222" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">401 Unauthorized</text>
|
||||
</g>
|
||||
|
||||
<!-- Yes → 2단계 -->
|
||||
<line x1="340" y1="254" x2="340" y2="294" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="348" y="276" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Yes</text>
|
||||
|
||||
<!-- 2단계: 파일 권한 -->
|
||||
<g onclick="sendPrompt('파일 권한 검증에서 확인하는 항목은?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="185" y="294" width="310" height="56" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="314" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">2단계 — 파일 권한 검증</text>
|
||||
<text x="340" y="334" text-anchor="middle" dominant-baseline="central" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">소유자·공유 링크·만료·비밀번호 확인</text>
|
||||
</g>
|
||||
|
||||
<!-- 권한 분기 -->
|
||||
<line x1="340" y1="350" x2="340" y2="390" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<polygon points="340,390 400,422 340,454 280,422" fill="none" stroke="var(--color-text-secondary)" stroke-width="0.5" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="420" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">접근 허용?</text>
|
||||
|
||||
<!-- No → 404 -->
|
||||
<line x1="400" y1="422" x2="530" y2="422" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="466" y="414" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">No</text>
|
||||
<g onclick="sendPrompt('권한 없음과 파일 없음을 모두 404로 처리하는 이유는?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="530" y="400" width="120" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="590" y="422" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">404 Not Found</text>
|
||||
</g>
|
||||
|
||||
<!-- Yes → 3단계 -->
|
||||
<line x1="340" y1="454" x2="340" y2="494" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="348" y="476" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Yes</text>
|
||||
|
||||
<!-- 3단계: 메타데이터 검증 -->
|
||||
<g onclick="sendPrompt('다운로드 가능 상태를 사전 검증하는 이유는?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="165" y="494" width="350" height="56" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="514" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">3단계 — 메타데이터 사전 검증</text>
|
||||
<text x="340" y="534" text-anchor="middle" dominant-baseline="central" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">storage_key · 삭제/손상/격리 상태 확인</text>
|
||||
</g>
|
||||
|
||||
<!-- 메타 분기 -->
|
||||
<line x1="340" y1="550" x2="340" y2="590" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<polygon points="340,590 400,622 340,654 280,622" fill="none" stroke="var(--color-text-secondary)" stroke-width="0.5" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="620" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">다운로드 가능?</text>
|
||||
|
||||
<!-- No → 404 -->
|
||||
<line x1="400" y1="622" x2="530" y2="622" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="466" y="614" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">No</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="530" y="600" width="120" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="590" y="622" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">404 Not Found</text>
|
||||
</g>
|
||||
|
||||
<!-- Yes → 4단계 -->
|
||||
<line x1="340" y1="654" x2="340" y2="694" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="348" y="676" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Yes</text>
|
||||
|
||||
<!-- 4단계: 세션 발급 -->
|
||||
<g onclick="sendPrompt('다운로드 세션 토큰 TTL을 5분으로 설정하는 이유는?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="165" y="694" width="350" height="56" rx="8" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="714" text-anchor="middle" dominant-baseline="central" style="fill:rgb(60, 52, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">4단계 — 세션 토큰 발급</text>
|
||||
<text x="340" y="734" text-anchor="middle" dominant-baseline="central" style="fill:rgb(83, 74, 183);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">HMAC/JWT · TTL 5분 · fileId 바인딩</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="750" x2="340" y2="790" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- 응답 -->
|
||||
<g onclick="sendPrompt('downloadUrl 응답 구조는?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="185" y="790" width="310" height="56" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="810" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">클라이언트에 응답</text>
|
||||
<text x="340" y="830" text-anchor="middle" dominant-baseline="central" style="fill:rgb(95, 94, 90);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">downloadUrl + sessionToken + expiresAt</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="846" x2="340" y2="886" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- END 표시 (파트2로 이어짐) -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="240" y="886" width="200" height="44" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="908" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">→ 파트 2: 스트리밍 단계</text>
|
||||
</g>
|
||||
|
||||
<!-- 범례 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"><rect x="40" y="940" width="14" height="14" rx="3" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/></g>
|
||||
<text x="60" y="947" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">인증·세션</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"><rect x="140" y="940" width="14" height="14" rx="3" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/></g>
|
||||
<text x="160" y="947" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">권한·검증</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"><rect x="240" y="940" width="14" height="14" rx="3" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/></g>
|
||||
<text x="260" y="947" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">에러 응답</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"><rect x="340" y="940" width="14" height="14" rx="3" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/></g>
|
||||
<text x="360" y="947" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">시작·종료</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
125
설계/img/download_flowchart_part2.svg
Normal file
125
설계/img/download_flowchart_part2.svg
Normal file
@@ -0,0 +1,125 @@
|
||||
<svg width="100%" viewBox="0 0 680 980" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<mask id="imagine-text-gaps-6i3lz8" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="980" fill="white"/><rect x="217.8470916748047" y="31.31909942626953" width="244.305908203125" height="21.36180305480957" fill="black" rx="2"/><rect x="267.6529846191406" y="113.31909942626953" width="144.69406127929688" height="21.36180305480957" fill="black" rx="2"/><rect x="261.0594177246094" y="134.5247802734375" width="157.88119506835938" height="18.95044231414795" fill="black" rx="2"/><rect x="309.39215087890625" y="220.52479553222656" width="61.215736389160156" height="18.95044231414795" fill="black" rx="2"/><rect x="453.7825622558594" y="209.94320678710938" width="24.4349365234375" height="18.95044231414795" fill="black" rx="2"/><rect x="527.9691162109375" y="221.31910705566406" width="124.06185150146484" height="21.36180305480957" fill="black" rx="2"/><rect x="343.5177307128906" y="276.5247802734375" width="28.428754806518555" height="18.95044231414795" fill="black" rx="2"/><rect x="233.79222106933594" y="313.3191223144531" width="212.4156494140625" height="21.36180305480957" fill="black" rx="2"/><rect x="225.024658203125" y="334.5248107910156" width="229.9507598876953" height="18.95044231414795" fill="black" rx="2"/><rect x="284.6229553222656" y="420.5247802734375" width="110.754150390625" height="18.95044231414795" fill="black" rx="2"/><rect x="453.7825622558594" y="409.9432067871094" width="24.4349365234375" height="18.95044231414795" fill="black" rx="2"/><rect x="538.439697265625" y="421.319091796875" width="103.12068176269531" height="21.36180305480957" fill="black" rx="2"/><rect x="343.5177307128906" y="476.5248107910156" width="28.428754806518555" height="18.95044231414795" fill="black" rx="2"/><rect x="261.4211120605469" y="513.319091796875" width="157.2958984375" height="21.36180305480957" fill="black" rx="2"/><rect x="183.7753143310547" y="534.5247802734375" width="312.73223876953125" height="18.95044231414795" fill="black" rx="2"/><rect x="290.4968566894531" y="620.5247802734375" width="99.00629425048828" height="18.95044231414795" fill="black" rx="2"/><rect x="451.54449462890625" y="609.9431762695312" width="28.428754806518555" height="18.95044231414795" fill="black" rx="2"/><rect x="521.8464965820312" y="621.319091796875" width="136.3070526123047" height="21.36180305480957" fill="black" rx="2"/><rect x="344.0000305175781" y="676.5247802734375" width="24.4349365234375" height="18.95044231414795" fill="black" rx="2"/><rect x="276.9480285644531" y="715.319091796875" width="126.10397338867188" height="21.36180305480957" fill="black" rx="2"/><rect x="243.03074645996094" y="769.319091796875" width="194.0831756591797" height="21.36180305480957" fill="black" rx="2"/><rect x="226.4827880859375" y="790.5247802734375" width="227.03453063964844" height="18.95044231414795" fill="black" rx="2"/><rect x="295.9073486328125" y="867.3191528320312" width="88.18531036376953" height="21.36180305480957" fill="black" rx="2"/><rect x="56.000003814697266" y="927.5247802734375" width="79.91132354736328" height="18.95044231414795" fill="black" rx="2"/><rect x="196" y="927.5247802734375" width="77.07798767089844" height="18.95044231414795" fill="black" rx="2"/><rect x="336.0000305175781" y="927.5247802734375" width="54.75307083129883" height="18.95044231414795" fill="black" rx="2"/><rect x="446.0000305175781" y="927.5247802734375" width="79.91129302978516" height="18.95044231414795" fill="black" rx="2"/></mask></defs>
|
||||
|
||||
<!-- START -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="185" y="20" width="310" height="44" rx="22" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="42" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">GET /files/stream?sessionToken=…</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="64" x2="340" y2="104" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- 5단계: 토큰 검증 -->
|
||||
<g onclick="sendPrompt('세션 토큰 TTL 내 Range 요청을 허용하는 이유는?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="175" y="104" width="330" height="56" rx="8" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="124" text-anchor="middle" dominant-baseline="central" style="fill:rgb(60, 52, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">5단계 — 세션 토큰 검증</text>
|
||||
<text x="340" y="144" text-anchor="middle" dominant-baseline="central" style="fill:rgb(83, 74, 183);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">만료·위변조·fileId 불일치 확인</text>
|
||||
</g>
|
||||
|
||||
<!-- 토큰 분기 -->
|
||||
<line x1="340" y1="160" x2="340" y2="200" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<polygon points="340,200 400,232 340,264 280,232" fill="none" stroke="var(--color-text-secondary)" stroke-width="0.5" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="230" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">토큰 유효?</text>
|
||||
|
||||
<!-- No → 401 -->
|
||||
<line x1="400" y1="232" x2="530" y2="232" marker-end="url(#arrow)" mask="url(#imagine-text-gaps-6i3lz8)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="466" y="224" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">No</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="530" y="210" width="120" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="590" y="232" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">401 Unauthorized</text>
|
||||
</g>
|
||||
|
||||
<!-- Yes → 6단계 -->
|
||||
<line x1="340" y1="264" x2="340" y2="304" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="348" y="286" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Yes</text>
|
||||
|
||||
<!-- 6단계: storage_key resolve -->
|
||||
<g onclick="sendPrompt('Path traversal 공격을 방어하는 방법은?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="155" y="304" width="370" height="56" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="324" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">6단계 — 경로 resolve 및 최종 검증</text>
|
||||
<text x="340" y="344" text-anchor="middle" dominant-baseline="central" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">realpath() · 루트 경로 검증 · 파일 open 확인</text>
|
||||
</g>
|
||||
|
||||
<!-- 경로 분기 -->
|
||||
<line x1="340" y1="360" x2="340" y2="400" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<polygon points="340,400 400,432 340,464 280,432" fill="none" stroke="var(--color-text-secondary)" stroke-width="0.5" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="430" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">파일 존재·접근 가능?</text>
|
||||
|
||||
<!-- No → 404 -->
|
||||
<line x1="400" y1="432" x2="530" y2="432" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="466" y="424" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">No</text>
|
||||
<g onclick="sendPrompt('메타데이터는 있는데 실제 파일이 없을 때 어떻게 처리하나요?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="530" y="410" width="120" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="590" y="432" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">404 + 경고 로그</text>
|
||||
</g>
|
||||
|
||||
<!-- Yes → 7단계 -->
|
||||
<line x1="340" y1="464" x2="340" y2="504" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="348" y="486" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Yes</text>
|
||||
|
||||
<!-- 7단계: 스트림 응답 -->
|
||||
<g onclick="sendPrompt('Range 요청 처리 규칙과 응답 헤더는?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="155" y="504" width="370" height="56" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="524" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">7단계 — 파일 스트림 응답</text>
|
||||
<text x="340" y="544" text-anchor="middle" dominant-baseline="central" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">200 OK / 206 Partial · Content-Disposition: attachment</text>
|
||||
</g>
|
||||
|
||||
<!-- Range 분기 -->
|
||||
<line x1="340" y1="560" x2="340" y2="600" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<polygon points="340,600 400,632 340,664 280,632" fill="none" stroke="var(--color-text-secondary)" stroke-width="0.5" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="630" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Range 헤더 있음?</text>
|
||||
|
||||
<!-- Yes → 206 -->
|
||||
<line x1="400" y1="632" x2="530" y2="632" marker-end="url(#arrow)" mask="url(#imagine-text-gaps-6i3lz8)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="466" y="624" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Yes</text>
|
||||
<g onclick="sendPrompt('206 Partial Content 응답 조건은?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="530" y="610" width="120" height="44" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="590" y="632" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">206 Partial Content</text>
|
||||
</g>
|
||||
|
||||
<!-- No → 200 -->
|
||||
<line x1="340" y1="664" x2="340" y2="704" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="348" y="686" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">No</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="240" y="704" width="200" height="44" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="726" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">200 OK (전체 파일)</text>
|
||||
</g>
|
||||
|
||||
<!-- 206과 200 합류 -->
|
||||
<line x1="590" y1="654" x2="590" y2="760" stroke="var(--color-text-secondary)" stroke-width="0.8" fill="none" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<path d="M590 760 L340 760" fill="none" stroke="var(--color-text-secondary)" stroke-width="0.8" marker-end="url(#arrow)" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<line x1="340" y1="748" x2="340" y2="760" stroke="var(--color-text-secondary)" stroke-width="0.8" fill="none" style="fill:none;stroke:rgb(61, 61, 58);color:rgb(0, 0, 0);stroke-width:0.8px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- 8단계: 로그 -->
|
||||
<g onclick="sendPrompt('download_attempt_count와 completed_count를 분리하는 이유는?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="155" y="760" width="370" height="56" rx="8" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="780" text-anchor="middle" dominant-baseline="central" style="fill:rgb(60, 52, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">8단계 — 로그 기록 / 카운트 증가</text>
|
||||
<text x="340" y="800" text-anchor="middle" dominant-baseline="central" style="fill:rgb(83, 74, 183);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">attempt / completed 분리 집계 · 감사 로그</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="816" x2="340" y2="856" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- END -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="240" y="856" width="200" height="44" rx="22" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="878" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">다운로드 완료</text>
|
||||
</g>
|
||||
|
||||
<!-- 범례 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"><rect x="40" y="930" width="14" height="14" rx="3" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/></g>
|
||||
<text x="60" y="937" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">인증·세션·로그</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"><rect x="180" y="930" width="14" height="14" rx="3" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/></g>
|
||||
<text x="200" y="937" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">검증·스트리밍</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"><rect x="320" y="930" width="14" height="14" rx="3" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/></g>
|
||||
<text x="340" y="937" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">에러 응답</text>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"><rect x="430" y="930" width="14" height="14" rx="3" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/></g>
|
||||
<text x="450" y="937" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">시작·종료·상태</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -1,121 +0,0 @@
|
||||
<svg width="100%" viewBox="0 0 680 820" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<mask id="imagine-text-gaps-hhsrrm" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="820" fill="white"/><rect x="249.55258178710938" y="41.31908416748047" width="180.89492797851562" height="21.361833572387695" fill="black" rx="2"/><rect x="249.9481964111328" y="60.5247688293457" width="180.10369873046875" height="18.950468063354492" fill="black" rx="2"/><rect x="254.57876586914062" y="125.31908416748047" width="170.84254455566406" height="21.361833572387695" fill="black" rx="2"/><rect x="259.8648986816406" y="144.52476501464844" width="160.2702178955078" height="18.950468063354492" fill="black" rx="2"/><rect x="557.7877807617188" y="129.319091796875" width="104.42449188232422" height="21.361833572387695" fill="black" rx="2"/><rect x="274.4046936035156" y="209.319091796875" width="131.1906509399414" height="21.361833572387695" fill="black" rx="2"/><rect x="256.2780456542969" y="228.5247802734375" width="167.44403076171875" height="18.950468063354492" fill="black" rx="2"/><rect x="253.23367309570312" y="293.319091796875" width="173.53273010253906" height="21.361833572387695" fill="black" rx="2"/><rect x="259.1829528808594" y="312.5247802734375" width="161.63414001464844" height="18.950468063354492" fill="black" rx="2"/><rect x="567.0753173828125" y="297.319091796875" width="85.84944152832031" height="21.361833572387695" fill="black" rx="2"/><rect x="216.82582092285156" y="377.319091796875" width="246.3626251220703" height="21.361833572387695" fill="black" rx="2"/><rect x="242.3297882080078" y="396.5247802734375" width="195.34051513671875" height="18.950468063354492" fill="black" rx="2"/><rect x="556.8533935546875" y="381.319091796875" width="106.29329681396484" height="21.361833572387695" fill="black" rx="2"/><rect x="280.7759704589844" y="461.319091796875" width="118.59143829345703" height="21.361833572387695" fill="black" rx="2"/><rect x="245.60020446777344" y="480.5247802734375" width="188.7996826171875" height="18.950468063354492" fill="black" rx="2"/><rect x="262.80377197265625" y="545.319091796875" width="154.53204345703125" height="21.361833572387695" fill="black" rx="2"/><rect x="238.98402404785156" y="564.5247802734375" width="202.0320587158203" height="18.950468063354492" fill="black" rx="2"/><rect x="48.80827713012695" y="37.31908416748047" width="72.38346862792969" height="21.361833572387695" fill="black" rx="2"/><rect x="44.41883850097656" y="121.319091796875" width="80.76659393310547" height="21.361833572387695" fill="black" rx="2"/><rect x="35.018280029296875" y="285.319091796875" width="99.97607421875" height="21.361833572387695" fill="black" rx="2"/><rect x="35.32346725463867" y="373.319091796875" width="99.38409423828125" height="21.361833572387695" fill="black" rx="2"/><rect x="29.020008087158203" y="457.319091796875" width="111.994140625" height="21.361833572387695" fill="black" rx="2"/><rect x="37.825260162353516" y="541.319091796875" width="93.89728546142578" height="21.361833572387695" fill="black" rx="2"/><rect x="306.45703125" y="640.5247802734375" width="67.37143325805664" height="18.950468063354492" fill="black" rx="2"/><rect x="356.4570007324219" y="658.5247802734375" width="67.37143325805664" height="18.950468063354492" fill="black" rx="2"/></mask></defs>
|
||||
|
||||
<!-- Step 1 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="30" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="52" text-anchor="middle" dominant-baseline="central" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">1. 사용자 인증 / 공유 링크 접근</text>
|
||||
<text x="340" y="70" text-anchor="middle" dominant-baseline="central" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">JWT 토큰 또는 share_token 파싱</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="86" x2="340" y2="114" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="114" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="136" text-anchor="middle" dominant-baseline="central" style="fill:rgb(60, 52, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">2. 파일 권한 / 공유 상태 검증</text>
|
||||
<text x="340" y="154" text-anchor="middle" dominant-baseline="central" style="fill:rgb(83, 74, 183);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">DB 조회 → 접근 허용 여부 확인</text>
|
||||
</g>
|
||||
|
||||
<!-- 403 분기 -->
|
||||
<line x1="480" y1="142" x2="560" y2="142" marker-end="url(#arrow)" stroke="var(--color-text-danger)" stroke-width="1" mask="url(#imagine-text-gaps-hhsrrm)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="560" y="118" width="100" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="610" y="140" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">403 Forbidden</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="170" x2="340" y2="198" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="198" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="220" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">3. 다운로드 토큰 발급</text>
|
||||
<text x="340" y="238" text-anchor="middle" dominant-baseline="central" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">단기 서명 토큰 생성 (TTL ~60s)</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="254" x2="340" y2="282" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 4 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="282" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="304" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">4. 토큰으로 파일 스트림 요청</text>
|
||||
<text x="340" y="322" text-anchor="middle" dominant-baseline="central" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">GET /stream?token=… 호출</text>
|
||||
</g>
|
||||
|
||||
<!-- Token 검증 분기 -->
|
||||
<line x1="480" y1="310" x2="560" y2="310" marker-end="url(#arrow)" stroke="var(--color-text-danger)" stroke-width="1" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="560" y="286" width="100" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="610" y="308" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">401 Expired</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="338" x2="340" y2="366" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 5 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="366" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(250, 238, 218);stroke:rgb(133, 79, 11);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="388" text-anchor="middle" dominant-baseline="central" style="fill:rgb(99, 56, 6);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">5. storage_key → 로컬 FS 경로 resolve</text>
|
||||
<text x="340" y="406" text-anchor="middle" dominant-baseline="central" style="fill:rgb(133, 79, 11);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">키-경로 맵 조회 / path traversal 방어</text>
|
||||
</g>
|
||||
|
||||
<!-- Not found 분기 -->
|
||||
<line x1="480" y1="394" x2="560" y2="394" marker-end="url(#arrow)" stroke="var(--color-text-danger)" stroke-width="1" mask="url(#imagine-text-gaps-hhsrrm)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="560" y="370" width="100" height="44" rx="8" stroke-width="0.5" style="fill:rgb(252, 235, 235);stroke:rgb(163, 45, 45);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="610" y="392" text-anchor="middle" dominant-baseline="central" style="fill:rgb(121, 31, 31);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">404 Not Found</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="422" x2="340" y2="450" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 6 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="450" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(234, 243, 222);stroke:rgb(59, 109, 17);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="472" text-anchor="middle" dominant-baseline="central" style="fill:rgb(39, 80, 10);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">6. 파일 스트림 응답</text>
|
||||
<text x="340" y="490" text-anchor="middle" dominant-baseline="central" style="fill:rgb(59, 109, 17);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">206 Partial Content (Range 지원)</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="506" x2="340" y2="534" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Step 7 -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="200" y="534" width="280" height="56" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="556" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">7. 로그 기록 / 카운트 증가</text>
|
||||
<text x="340" y="574" text-anchor="middle" dominant-baseline="central" style="fill:rgb(95, 94, 90);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">공유 링크인 경우 download_count++</text>
|
||||
</g>
|
||||
|
||||
<!-- Actor labels -->
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="30" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="48" text-anchor="middle" dominant-baseline="central" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">클라이언트</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="114" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(238, 237, 254);stroke:rgb(83, 74, 183);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="132" text-anchor="middle" dominant-baseline="central" style="fill:rgb(60, 52, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Auth / ACL</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="246" width="110" height="100" rx="8" stroke-width="0.5" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="296" text-anchor="middle" dominant-baseline="central" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Token Service</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="366" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(250, 238, 218);stroke:rgb(133, 79, 11);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="384" text-anchor="middle" dominant-baseline="central" style="fill:rgb(99, 56, 6);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Storage Layer</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="450" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(234, 243, 222);stroke:rgb(59, 109, 17);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="468" text-anchor="middle" dominant-baseline="central" style="fill:rgb(39, 80, 10);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Stream Handler</text>
|
||||
</g>
|
||||
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||
<rect x="30" y="534" width="110" height="36" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="85" y="552" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Audit Logger</text>
|
||||
</g>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="200" y="630" width="280" height="60" rx="8" fill="none" stroke="var(--color-border-tertiary)" stroke-width="0.5" stroke-dasharray="4 3" style="fill:none;stroke:rgba(31, 30, 29, 0.15);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-dasharray:4px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="340" y="650" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">정상 흐름 →</text>
|
||||
<line x1="280" y1="668" x2="330" y2="668" stroke="var(--color-text-danger)" stroke-width="1" marker-end="url(#arrow)" style="fill:rgb(0, 0, 0);stroke:rgb(127, 44, 40);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<text x="390" y="668" text-anchor="middle" dominant-baseline="central" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">에러 분기 →</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 30 KiB |
175
설계/img/file_download_sequence_chart.svg
Normal file
175
설계/img/file_download_sequence_chart.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 47 KiB |
284
설계/pipeline/다운로드 파이프라인.md
Normal file
284
설계/pipeline/다운로드 파이프라인.md
Normal file
@@ -0,0 +1,284 @@
|
||||
## 개요
|
||||
|
||||
이 문서는 **인증 사용자**와 **공유 링크 사용자** 모두를 지원하는 파일 다운로드 시스템의 전체 흐름을 정의한다. 핵심 구조는 다음과 같다:
|
||||
|
||||
> **권한 검증 → 다운로드 가능 상태 사전 검증 → 다운로드 세션 발급 → 최종 경로 검증 및 스트리밍**
|
||||
|
||||
---
|
||||
|
||||
## 전체 흐름 (8단계)
|
||||
![[download_flowchart_part1.svg]]
|
||||
|
||||
![[download_flowchart_part2.svg]]
|
||||
---
|
||||
|
||||
## 단계별 상세
|
||||
|
||||
### 1단계: 사용자 인증 / 공유 링크 접근
|
||||
|
||||
**목적:** 요청자의 신원을 확인하고 내부 컨텍스트로 정규화한다.
|
||||
|
||||
| 접근 경로 | 인증 방식 | 정규화 결과 |
|
||||
|-----------|-----------|-------------|
|
||||
| 인증 사용자 | `Authorization` 헤더의 JWT Bearer 토큰 | `subjectType=user`, `subjectId={userId}` |
|
||||
| 공유 링크 사용자 | 쿼리 파라미터 또는 별도 엔드포인트의 `share_token` | `subjectType=share_link`, `subjectId={shareLinkId}` |
|
||||
|
||||
**에러 처리:**
|
||||
|
||||
| 상황 | 응답 |
|
||||
|------|------|
|
||||
| 인증 토큰 유효하지 않음 / 만료 | `401 Unauthorized` |
|
||||
| 공유 토큰 유효하지 않음 | `404 Not Found` |
|
||||
|
||||
---
|
||||
|
||||
### 2단계: 파일 권한 / 공유 상태 검증
|
||||
|
||||
**목적:** DB 조회를 통해 요청자가 해당 파일에 접근할 수 있는지 확인한다.
|
||||
|
||||
**검증 항목:**
|
||||
|
||||
| 접근 유형 | 검증 내용 |
|
||||
| --------- | ---------------------------------------- |
|
||||
| 인증 사용자 | 파일 소유자 여부, 공유 대상 여부, 조직/프로젝트/폴더 단위 접근 권한 |
|
||||
| 공유 링크 사용자 | 링크 활성화 여부, 만료 여부, 비밀번호 검증, 다운로드 허용 여부 |
|
||||
|
||||
**핵심 원칙:** 파일 존재 여부 노출을 최소화하기 위해 아래 경우는 **모두 `404 Not Found`** 로 처리한다.
|
||||
|
||||
- 파일이 실제로 존재하지 않음
|
||||
- 요청자에게 접근 권한이 없음
|
||||
- 공유 링크가 파일에 매핑되지 않음
|
||||
- 비활성화된 공유 링크로 접근 시도
|
||||
|
||||
> 외부 클라이언트 관점에서 **"접근 불가"와 "존재하지 않음"을 구분할 수 없다.**
|
||||
|
||||
---
|
||||
|
||||
### 3단계: 파일 메타데이터 및 다운로드 가능 상태 검증
|
||||
|
||||
**목적:** 다운로드가 불가능한 파일에 대해 세션을 발급하지 않기 위한 **사전 검증**이다.
|
||||
|
||||
**검증 항목:**
|
||||
|
||||
- 파일 메타데이터 존재 여부
|
||||
- `storage_key` 존재 여부
|
||||
- 삭제 / 손상 / 비활성 / 격리 상태 여부
|
||||
- 다운로드 정책상 차단 대상 여부
|
||||
- (가능 시) 실제 스토리지 객체 존재 여부
|
||||
|
||||
**실패 시:** `404 Not Found`
|
||||
|
||||
> ⚠️ 이 단계는 사전 검증이다. 세션 발급 후 실제 스트리밍 시점까지 파일 상태가 변할 수 있으므로, **최종 검증은 6단계에서 다시 수행**한다.
|
||||
|
||||
---
|
||||
|
||||
### 4단계: 다운로드 세션 토큰 발급
|
||||
|
||||
**목적:** 실제 파일 스트리밍에 사용할 세션 토큰을 발급한다.
|
||||
|
||||
**세션 토큰 속성:**
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| TTL | 약 5분 |
|
||||
| 포함 정보 | 파일 ID, 요청자 식별자(`subjectType`, `subjectId`), 세션 ID, 만료 시각 |
|
||||
| 보호 방식 | HMAC 또는 JWT 서명, 필요 시 Redis/DB에 메타데이터 저장 |
|
||||
|
||||
**세션이 지원하는 요청 유형:**
|
||||
|
||||
- 브라우저의 동일 다운로드 재요청
|
||||
- `Range` 기반 이어받기(resume)
|
||||
- 네트워크 끊김 후 재시도
|
||||
- 일부 브라우저/다운로드 매니저의 병렬 Range 요청
|
||||
|
||||
**응답 예시:**
|
||||
|
||||
```json
|
||||
{
|
||||
"downloadUrl": "/files/stream?sessionToken={download_session_token}",
|
||||
"expiresAt": "2026-03-17T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**리스크 및 대응:**
|
||||
|
||||
| 리스크 | 대응 |
|
||||
|--------|------|
|
||||
| TTL이 너무 길면 URL 탈취 악용 가능 | TTL 5분 내외로 제한 |
|
||||
| TTL이 너무 짧으면 대용량 파일 재요청 실패 | 세션 TTL 내 반복 요청 허용 |
|
||||
| 토큰 유출 | 파일 ID + 요청자 식별자에 바인딩, 선택적 IP/UA 바인딩 |
|
||||
|
||||
**추가 보안:**
|
||||
|
||||
- 다운로드 세션은 **발급 시점 권한**을 기준으로 TTL 동안 유효
|
||||
- Access log 마스킹
|
||||
- `Cache-Control: private, no-store`
|
||||
|
||||
---
|
||||
|
||||
### 5단계: 세션 토큰으로 파일 스트림 요청
|
||||
|
||||
**클라이언트 요청 예시:**
|
||||
|
||||
```http
|
||||
GET /files/stream?sessionToken={download_session_token}
|
||||
Range: bytes=0-1048575
|
||||
```
|
||||
|
||||
**처리 규칙:**
|
||||
|
||||
| 상황 | 처리 |
|
||||
|------|------|
|
||||
| 세션 토큰 만료 / 위변조 | `401 Unauthorized` |
|
||||
| 세션이 가리키는 파일과 요청 대상 불일치 | `404 Not Found` |
|
||||
| 세션 TTL 내 반복 GET / Range / resume | ✅ 허용 |
|
||||
| Multi-range 요청 | ❌ 미지원 (정책에 따라 `416` 또는 전체 응답) |
|
||||
|
||||
**설계 원칙:**
|
||||
|
||||
> 세션 자체는 짧게 유지하되, 세션이 살아있는 동안은 브라우저 다운로드 모델을 수용한다.
|
||||
|
||||
**"1회용 토큰 + 즉시 무효화" 방식을 사용하지 않는 이유:**
|
||||
|
||||
브라우저는 다운로드 중 동일 URL로 재요청, Range resume, 병렬 요청, 프록시 중복 요청 등을 발생시킬 수 있어 충돌한다.
|
||||
|
||||
---
|
||||
|
||||
### 6단계: `storage_key` → 실제 파일 경로 resolve 및 최종 검증
|
||||
|
||||
**목적:** 스트림을 열기 직전에 실제 파일 경로를 확정하고 최종 검증을 수행한다.
|
||||
|
||||
**처리 절차:**
|
||||
|
||||
1. 키-경로 맵(DB 또는 설정) 조회 → 절대 경로 획득
|
||||
2. `realpath()` 등으로 경로 정규화
|
||||
3. 정규화된 경로가 허용된 루트 디렉터리 내부인지 검증 (예: `/data/uploads/`)
|
||||
4. 실제 파일 존재 여부 확인
|
||||
5. 파일 open 가능 여부 확인
|
||||
|
||||
**보안 요구사항:**
|
||||
|
||||
- Path traversal 방어 필수 (`../`, 심볼릭 링크 우회, 상대 경로 등 차단)
|
||||
- 허용 루트 외부로 벗어나면 즉시 실패 처리
|
||||
|
||||
**에러 처리:**
|
||||
|
||||
| 상황 | 응답 | 내부 처리 |
|
||||
|------|------|-----------|
|
||||
| 실제 파일 미존재 | `404 Not Found` | - |
|
||||
| 메타데이터 존재하나 스토리지 파일 누락 | `404 Not Found` | 경고 로그 / 장애 알림 |
|
||||
|
||||
---
|
||||
|
||||
### 7단계: 파일 스트림 응답
|
||||
|
||||
**기본 응답 헤더:**
|
||||
|
||||
| 헤더 | 값 |
|
||||
|------|-----|
|
||||
| `Content-Type` | 서버가 결정한 안전한 MIME 타입 |
|
||||
| `Content-Disposition` | `attachment; filename="..."; filename*=UTF-8''...` |
|
||||
| `Accept-Ranges` | `bytes` |
|
||||
| `Content-Length` | 전체 또는 부분 응답 크기 |
|
||||
| `ETag` | 파일 버전 식별자 |
|
||||
| `Last-Modified` | 파일 최종 수정 시각 (선택) |
|
||||
|
||||
**Range 처리:**
|
||||
|
||||
| 조건 | 응답 |
|
||||
|------|------|
|
||||
| `Range` 헤더 없음 | `200 OK` (전체 파일) |
|
||||
| `Range` 헤더 있음 | `206 Partial Content` (부분 응답) |
|
||||
| 범위 초과 | `416 Range Not Satisfiable` |
|
||||
| `If-Range` 일치 | 부분 응답 |
|
||||
| `If-Range` 불일치 | 전체 파일 재전송 또는 정책에 따라 처리 |
|
||||
|
||||
**MIME 타입 처리 원칙:**
|
||||
|
||||
1. 서버 측 MIME 판별 사용 (클라이언트 업로드 값 불신)
|
||||
2. 저장된 메타데이터와 확장자는 보조적으로 사용
|
||||
3. 판별 불확실 시 `application/octet-stream`
|
||||
4. XSS 가능 타입이라도 `Content-Disposition: attachment`로 강제
|
||||
|
||||
**파일명 처리 원칙:**
|
||||
|
||||
- 제거/이스케이프 대상: `"`, `\r`, `\n`, `\0`, `/`, `\`
|
||||
- 최대 길이 제한 적용
|
||||
- 비 ASCII 문자는 `filename*`에 RFC 5987 방식으로 인코딩
|
||||
|
||||
```http
|
||||
Content-Disposition: attachment; filename="safe_name.ext"; filename*=UTF-8''original%20name.ext
|
||||
```
|
||||
|
||||
**성능 / 운영 보호 정책:**
|
||||
|
||||
- 사용자별 동시 다운로드 수 제한
|
||||
- 공유 링크별 rate limit
|
||||
- 전체 서버 동시 스트림 수 제한
|
||||
- 대용량 파일 전송 속도 제한 (필요 시)
|
||||
- `sendfile` / `X-Accel-Redirect` / `X-Sendfile` 활용 검토
|
||||
|
||||
### 8단계: 로그 기록 / 카운트 증가
|
||||
|
||||
**카운트 증가 정책:**
|
||||
|
||||
- **공유 링크 다운로드에만 적용**
|
||||
- `download_count`는 **세션 단위 전체 파일 다운로드 완료 시점**에만 증가
|
||||
- 운영 분석을 위해 시도 수와 완료 수를 분리 집계 가능
|
||||
|
||||
| 카운트 항목 | 증가 시점 | 용도 |
|
||||
|------------|-----------|------|
|
||||
| `download_attempt_count` | 스트림 요청 시작 시점 | 시도 횟수 추적 |
|
||||
| `download_completed_count` | 전체 파일 전송 완료 시점 | 실제 완료 횟수 추적 |
|
||||
|
||||
> 분리 집계를 통해 네트워크 중단, 사용자 취소, 실패율 등을 더 정확히 분석할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## 에러 응답 정리
|
||||
|
||||
| 상황 | HTTP 상태 코드 |
|
||||
|------|----------------|
|
||||
| 인증 실패 / 인증 토큰 만료 | `401 Unauthorized` |
|
||||
| 다운로드 세션 토큰 만료 / 위변조 | `401 Unauthorized` |
|
||||
| 파일 없음 또는 권한 없음 | `404 Not Found` |
|
||||
| Range 범위 초과 | `416 Range Not Satisfiable` |
|
||||
| 정상 전체 응답 | `200 OK` |
|
||||
| 정상 부분 응답 (Range) | `206 Partial Content` |
|
||||
|
||||
---
|
||||
|
||||
## 보안 체크리스트
|
||||
|
||||
| # | 항목 | 관련 단계 |
|
||||
|---|------|-----------|
|
||||
| 1 | JWT / share token 서명 검증 | 1단계 |
|
||||
| 2 | 파일 접근 권한 및 공유 상태 검증 | 2단계 |
|
||||
| 3 | 권한 없음과 파일 없음은 외부에 동일하게 `404` 처리 | 2단계 |
|
||||
| 4 | 파일 메타데이터 및 다운로드 가능 상태 사전 검증 | 3단계 |
|
||||
| 5 | 다운로드 세션 토큰 단명 발급 (권장 TTL: 약 5분) | 4단계 |
|
||||
| 6 | 세션 토큰은 파일 ID 및 요청자 식별자에 바인딩 | 4단계 |
|
||||
| 7 | 세션 TTL 내 GET / Range / resume / 병렬 요청 허용 | 5단계 |
|
||||
| 8 | Path traversal 방어 (`realpath` + 루트 경로 검증) | 6단계 |
|
||||
| 9 | 실제 스트리밍 직전 파일 존재 및 open 가능 여부 재검증 | 6단계 |
|
||||
| 10 | `Content-Disposition: attachment` 강제 | 7단계 |
|
||||
| 11 | 파일명 sanitize 및 `filename*` 지원 | 7단계 |
|
||||
| 12 | MIME 타입은 서버 기준으로 안전하게 결정 | 7단계 |
|
||||
| 13 | `ETag` / `If-Range` 지원 | 7단계 |
|
||||
| 14 | 다운로드 로그 및 감사 로그 기록 | 8단계 |
|
||||
| 15 | 동시 다운로드 수 / rate limit / 대역폭 보호 정책 적용 | 7~8단계 |
|
||||
|
||||
---
|
||||
|
||||
## 설계 요약
|
||||
|
||||
이 설계는 다음 **6가지 목표**를 동시에 만족한다.
|
||||
|
||||
| 목표 | 달성 방법 |
|
||||
|------|-----------|
|
||||
| **파일 존재 여부 노출 최소화** | 권한 없음 / 파일 없음 모두 `404` 통일 처리 |
|
||||
| **권한 검증 강화** | 인증 사용자·공유 링크 모두 다단계 검증 |
|
||||
| **불필요한 세션 발급 방지** | 세션 발급 전 다운로드 가능 상태 사전 검증 |
|
||||
| **브라우저 호환성 확보** | 세션 TTL 내 GET / Range / resume / 병렬 요청 허용 |
|
||||
| **재생 공격 위험 완화** | 단명 세션 토큰 + 파일·요청자 바인딩 |
|
||||
| **운영 환경 자원 보호** | 동시 다운로드 제한, rate limit, 대역폭 제어 |
|
||||
194
설계/pipeline/업로드 파이프라인.md
Normal file
194
설계/pipeline/업로드 파이프라인.md
Normal file
@@ -0,0 +1,194 @@
|
||||
|
||||
## 1. 업로드 준비 단계
|
||||
|
||||
### 1-1. 클라이언트가 업로드 대상 폴더를 선택한다
|
||||
|
||||
클라이언트는 업로드 요청 시 아래 정보를 서버에 전달한다.
|
||||
|
||||
|항목|설명|
|
||||
|---|---|
|
||||
|`targetFolderId`|업로드 대상 폴더 식별자|
|
||||
|원본 파일명|사용자가 선택한 파일의 이름|
|
||||
|파일 크기|바이트 단위 파일 크기|
|
||||
|클라이언트 MIME 타입|브라우저가 판단한 MIME 타입 (신뢰 금지)|
|
||||
|체크섬 정보|(선택) 무결성 검증용 해시값|
|
||||
|
||||
### 1-2. 서버가 사용자 인증 및 권한을 검증한다
|
||||
|
||||
- 로그인 여부 확인
|
||||
- 대상 폴더에 대한 쓰기 권한 확인
|
||||
- 타 사용자 폴더 접근 차단
|
||||
|
||||
### 1-3. 서버가 업로드 사전 검증을 수행한다
|
||||
|
||||
- 파일명 규칙 검증
|
||||
- 허용 최대 크기 검증
|
||||
- 확장자 / 업로드 정책 검증
|
||||
- 사용자 quota 사전 검사
|
||||
- 동일 폴더 내 중복 이름 정책 검사
|
||||
|
||||
### 1-4. 서버가 업로드 대상 폴더를 검증한다
|
||||
|
||||
- 폴더 존재 여부 확인
|
||||
- 삭제 상태 여부 확인
|
||||
- 업로드 가능 상태 여부 확인
|
||||
|
||||
---
|
||||
|
||||
## 2. 업로드 세션 생성 단계
|
||||
|
||||
### 2-1. 서버가 `UploadSession`을 생성한다
|
||||
|
||||
|항목|내용|
|
||||
|---|---|
|
||||
|상태|`CREATED`|
|
||||
|업로드 대상 폴더 ID|`targetFolderId` 저장|
|
||||
|파일 정보|원본 파일명 / 예상 크기 저장|
|
||||
|임시 저장 경로|임시 저장 경로 또는 임시 키 발급|
|
||||
|tus 매핑|tus 업로드 식별자 매핑 준비|
|
||||
|
||||
### 2-2. 서버가 클라이언트에 tus 업로드 정보를 반환한다
|
||||
|
||||
- 업로드 URL
|
||||
- 업로드 세션 ID
|
||||
- 재개에 필요한 메타데이터
|
||||
|
||||
---
|
||||
|
||||
## 3. 파일 전송 단계
|
||||
|
||||
### 3-1. 클라이언트가 tus 프로토콜로 청크 업로드를 시작한다
|
||||
|
||||
- 청크 단위 전송
|
||||
- 오프셋 검증
|
||||
- 중단 시 재개 가능
|
||||
- 서버는 진행 상태를 `UploadSession.received_size` 등에 반영
|
||||
|
||||
---
|
||||
|
||||
## 4. 업로드 완료 확정 단계
|
||||
|
||||
### 4-1. 서버가 업로드 완료 이벤트 또는 콜백을 수신한다
|
||||
|
||||
- tus 훅 또는 완료 엔드포인트 기반 처리
|
||||
- 동일 세션에 대한 중복 완료 요청 방지 필요 (idempotency)
|
||||
|
||||
### 4-2. 서버가 임시 업로드 파일을 최종 검증한다
|
||||
|
||||
- 실제 파일 크기 검증
|
||||
- MIME 타입 서버 측 추정 (클라이언트 값 신뢰 금지)
|
||||
- 손상 여부 또는 체크섬 검증 (선택)
|
||||
- quota 최종 재검사
|
||||
- 위험 파일 정책 검사 (필요 시)
|
||||
|
||||
### 4-3. 서버가 최종 저장 경로를 결정하고 파일을 이동한다
|
||||
|
||||
- `storage_key` 생성
|
||||
- 임시 경로 → 최종 object 경로로 **원자적 이동**
|
||||
- 사용자 표시 경로와 실제 디스크 경로는 분리
|
||||
|
||||
### 4-4. 서버가 `FileItem` 메타데이터를 생성한다
|
||||
|
||||
|필드|설명|
|
||||
|---|---|
|
||||
|`display_name`|사용자에게 표시되는 파일명|
|
||||
|`folder_id`|소속 폴더 ID|
|
||||
|`mime_type`|서버 측 추정 MIME 타입|
|
||||
|`size_bytes`|실제 파일 크기|
|
||||
|`storage_key`|내부 저장 경로 식별자|
|
||||
|`preview_status`|초기값 `PENDING`|
|
||||
|사용자 사용량|`storage_used_bytes` 갱신 (필요 시)|
|
||||
|
||||
### 4-5. 서버가 `UploadSession` 상태를 완료로 변경한다
|
||||
|
||||
|항목|내용|
|
||||
|---|---|
|
||||
|상태|`COMPLETED`|
|
||||
|완료 시각|기록|
|
||||
|`file_id`|최종 생성된 `FileItem`과 연결|
|
||||
|
||||
---
|
||||
|
||||
## 5. 후처리 단계
|
||||
|
||||
### 5-1. 서버가 후처리 작업을 비동기로 등록한다
|
||||
|
||||
|작업|유형|
|
||||
|---|---|
|
||||
|이미지 썸네일 생성|동기 또는 큐 기반|
|
||||
|텍스트 / PDF 미리보기 캐시 생성|큐 기반|
|
||||
|바이러스 스캔|후속 비동기|
|
||||
|검색 인덱싱|후속 비동기|
|
||||
|감사 로그 기록|동기 또는 큐 기반|
|
||||
|
||||
### 5-2. 서버가 클라이언트에 업로드 완료 응답을 반환한다
|
||||
|
||||
- 생성된 `FileItem` 정보
|
||||
- 목록 갱신에 필요한 최소 메타데이터
|
||||
- 프론트엔드는 현재 폴더 목록을 갱신
|
||||
|
||||
---
|
||||
|
||||
## 6. 보강 포인트
|
||||
|
||||
### A. 상태 전이
|
||||
|
||||
`UploadSession`의 상태는 아래와 같이 전이한다.
|
||||
|
||||
```
|
||||
CREATED → UPLOADING → COMPLETED
|
||||
↘ FAILED
|
||||
↘ ABORTED
|
||||
↘ EXPIRED (선택)
|
||||
```
|
||||
|
||||
|상태|설명|
|
||||
|---|---|
|
||||
|`CREATED`|세션 생성 완료, 아직 전송 시작 전|
|
||||
|`UPLOADING`|청크 전송 진행 중|
|
||||
|`COMPLETED`|모든 검증 및 저장 완료|
|
||||
|`FAILED`|완료 후 검증 실패 또는 저장 오류|
|
||||
|`ABORTED`|클라이언트 또는 서버 측 명시적 중단|
|
||||
|`EXPIRED`|세션 TTL 만료 (선택 구현)|
|
||||
|
||||
---
|
||||
|
||||
### B. 실패 처리
|
||||
|
||||
|시나리오|처리 방식|
|
||||
|---|---|
|
||||
|업로드 중 네트워크 끊김|세션 유지, tus 재개 허용|
|
||||
|완료 후 검증 실패|`UploadSession = FAILED`, 임시 파일 삭제|
|
||||
|quota 초과|최종 확정 거부, 파일 반영 금지|
|
||||
|파일 이동 성공 후 DB 저장 실패|고아 파일 정리 배치 또는 보상 로직 필요|
|
||||
|완료 콜백 중복 호출|idempotency 보장 필요|
|
||||
|
||||
---
|
||||
|
||||
### C. 트랜잭션 경계
|
||||
|
||||
아래 순서는 원자성 보장이 중요하며, 메타데이터 반영은 하나의 트랜잭션으로 묶는 것을 권장한다. 파일 이동은 트랜잭션 외부에서 수행되므로 전후 보상 로직이 필요하다.
|
||||
|
||||
```
|
||||
1. 임시 파일 검증
|
||||
2. 최종 파일 이동 ← 트랜잭션 외부 (파일시스템 작업)
|
||||
──────────────────────────── 트랜잭션 시작
|
||||
3. FileItem 생성
|
||||
4. 사용자 사용량 갱신
|
||||
5. UploadSession 완료 처리
|
||||
──────────────────────────── 트랜잭션 커밋
|
||||
```
|
||||
|
||||
> **주의:** 파일 이동 성공 후 트랜잭션 실패 시 고아 파일이 발생할 수 있으므로, 정기 정리 배치 또는 보상 트랜잭션을 설계해야 한다.
|
||||
|
||||
---
|
||||
|
||||
### D. 보안 포인트
|
||||
|
||||
|항목|내용|
|
||||
|---|---|
|
||||
|클라이언트 MIME 신뢰 금지|서버에서 직접 MIME 타입을 추정하여 사용|
|
||||
|사용자 입력 파일명 검증|특수문자, 예약어, 인코딩 우회 등 검사|
|
||||
|Path Traversal 방지|`../` 등 경로 탈출 시도 차단|
|
||||
|내부 실제 경로 외부 노출 금지|`storage_key`와 응답 경로 분리|
|
||||
|최종 저장 경로 격리|사용자 표시 폴더 구조와 실제 디스크 경로 분리|
|
||||
Reference in New Issue
Block a user