35 KiB
title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, imports, delegates, accepts_delegations, contract_packet, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label
| title | source_type | status | id | kind | project | work_item | inherits | refines | overrides | depends_on | imports | delegates | accepts_delegations | contract_packet | branch | parent_branch | related_projects | governing_docs | tags | created | target_merge | status_label | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-frontend-large-object-transfer-contract | branch-note | raw | BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-030 | project-work-item | ca-skeleton-frontend-operational-contract | WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-030 |
|
|
|
|
1 | feature-frontend-large-object-transfer-contract |
|
|
|
2026-07-28 | in-progress |
branch: feature-frontend-large-object-transfer-contract
부모 (필수)
형제 branch (같은 부모, 이번 확장에서 함께 생성):
- raw/branch-notes/feature-frontend-binary-file-io-store-contract
- raw/branch-notes/feature-frontend-cache-tier-cross-tab-invalidation-contract
- raw/branch-notes/feature-frontend-multi-protocol-api-transport-contract
- raw/branch-notes/feature-frontend-realtime-subscription-lifecycle-contract
- raw/branch-notes/feature-frontend-background-execution-worker-contract
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
2 - 패킷 스키마:
contract_packet: 1 - 완료 조건: presign 만료·part 재시도·무결성·credential 경계 fixture가 통과한다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CAPABILITY-001@1 |
신규 runtime capability 6종은 FE-REG-CAPABILITY flag로 default OFF이며 활성화는 owner·gate·runbook을 동반한다 | CAP_FE_LARGE_TRANSFER 를 이 branch 가 소유하고 default OFF 로 유지한다 |
raw/project-notes/ca-skeleton-frontend-operational-contract |
DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TRANSFER-CREDENTIAL-001@1 |
presigned URL 획득은 shared client를 경유하고 실제 byte 전송은 session credential을 첨부하지 않는 transfer adapter가 수행한다 | UploadTransferPort adapter 가 credential-less transport 를 사용하도록 강제한다 |
raw/project-notes/ca-skeleton-frontend-operational-contract |
DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-RESUMABLE-TRANSFER-001@1 |
재개 가능 전송은 part size·병렬도·part 재시도 상한을 registry로 고정하고 part 상태를 BlobStorePort에 보존한다 | TRANSFER_PART_SIZE_BYTES·TRANSFER_MAX_PARALLEL_PARTS 를 소비하고 UPLOAD_PART_STATE 에 상태를 쓴다 |
raw/project-notes/ca-skeleton-frontend-operational-contract |
브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | transfer adapter 는 shared client 의 auth interceptor 체인을 재사용하지 않고 별도 transport 를 갖는다 | refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TRANSFER-CREDENTIAL-001@1 |
UNSUPPORTED_DECISION — 조사 후에도 구현 형태를 지지하는 외부 근거 없음. trade-off: 대안인 "interceptor 에 skip 플래그" 는 기본이 첨부, 예외가 미첨부 라 플래그를 빠뜨린 새 경로가 곧바로 유출이 된다. 별도 transport 는 기본이 미첨부 라 실수의 방향이 안전한 쪽이다. 비용은 timeout·retry 정책이 두 벌로 갈라지는 것 |
proposed |
| D2 | presigned URL 은 로그·telemetry·에러 객체에 남기지 않는다. Referer 경로는 브라우저 기본 정책이 이미 막으므로 "presigned URL 을 페이지 URL 에 넣지 않는다" 는 금지 규칙 하나로 대체한다 |
local |
raw/official-docs/mdn-referrer-policy.md#C1, #C2 (기본값이 cross-origin 에 path·query 미전송) |
proposed |
| D3 | MediaUrlPolicy 는 port 가 아니라 application/policies/ 의 순수 함수다 |
local |
UNSUPPORTED_DECISION — 조사 후에도 근거 없음. trade-off: URL 파생에 I-O 가 없어 port 로 만들면 test double 만 늘고 composition root 가 커진다. 비용은 서명된 URL 을 요구하는 CDN 을 만나는 순간 순수 함수 가정이 깨져 port 승격 리팩터가 필요해지는 것 |
proposed |
| D4 | 다운로드 재개는 Accept-Ranges 로 지원을 판별하고 Range + If-Range 로 수행하며, 응답이 206 이 아니면 재개가 아니라 전체 재전송으로 취급한다. 416 은 terminal |
local |
raw/official-docs/mdn-http-range-fetch-transfer.md#C1, #C2, #C3, #C4, #C5, #C6 |
proposed |
| D6 | part 재시도는 원본 Blob 을 다시 slice 해서 새 body 를 만든다. 첫 시도의 body 를 보관했다 재사용하지 않는다 |
local |
raw/official-docs/mdn-http-range-fetch-transfer.md#C10 (읽힌 스트림은 disturbed 되어 재사용 불가) |
proposed |
| D7 | 취소는 AbortController 로만 하고 AbortError 를 REQUEST_ABORTED 로 매핑한다. 응답 수신 후 body 읽기 중 취소도 같은 kind 다 |
local |
raw/official-docs/mdn-http-range-fetch-transfer.md#C7, #C8 |
proposed |
| D8 | TRANSFER_PART_SIZE_BYTES 는 자유값이 아니라 부팅 시 하한 검증 대상이다. 하한은 선택 vendor 가 정하며(FE-Q-012), 미확정 동안은 5 MiB 를 잠정 하한으로 강제한다. part size × part 수 상한이 최대 전송 크기이므로 그 값을 계약값으로 노출한다 |
refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-RESUMABLE-TRANSFER-001@1 |
raw/official-docs/aws-s3-multipart-upload-limits.md#C1(5 MiB~5 GiB), #C2(마지막 part 예외), #C3(10,000 part 상한) |
proposed |
| D10 | UPLOAD_PART_STATE 유실은 오류가 아니라 정상 경로다. 유실을 감지하면 재개 불가로 표시하고 처음부터 전송한다. 이 branch 는 persist() 를 요청하지 않는다 |
local |
raw/official-docs/mdn-storage-quotas-eviction-persistence.md#C1(origin 전량 eviction), #C4·#C5(persist 는 브라우저 재량), #C7(Safari ITP 7일) |
proposed |
deferred (이번 회차 조사 범위 밖): D11 — 스트리밍 업로드(요청 body 를 ReadableStream 으로 전달). mdn-http-range-fetch-transfer.md §적용 경계 대로 이 페이지는 duplex 옵션과 HTTP/2 요구를 다루지 않는다. 현 계약은 part 단위 Blob 전송을 전제하며, 스트리밍 업로드가 필요해지면 별도 조사가 선행되어야 한다.
선언한 예외
해당 없음.
가져온 artifact 계약
| Artifact Ref | Owner | Producer | Schema Ref |
|---|
가져온 프로젝트 계약
| Ref | Owner | 요약 | Branch 적용 |
|---|---|---|---|
FE-GATE-029@1 |
raw/project-notes/ca-skeleton-frontend-operational-contract | presign 만료·part 재시도·무결성·credential 경계 fixture 가 실패하면 merge·release 를 MUST 차단 | 이 branch 가 owner 로서 fixture 와 report 를 산출 |
FE-OC-006@1 |
raw/branch-notes/feature-api-client-response-envelope-contract | 모든 HTTP는 shared client를 MUST 통과하고 timeout·abort·response parsing을 page에서 구현하면 안 됨 | presign 획득은 shared client 경유, byte 전송은 예외로 선언 |
FE-OC-019@1 |
raw/branch-notes/feature-frontend-browser-security-boundary-contract | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | presigned URL 유출 금지 규칙의 상위 계약 |
FE-OC-027@1 |
raw/branch-notes/feature-frontend-binary-file-io-store-contract | 파일 선택·다운로드·로컬 바이너리 저장은 등록된 port를 MUST 경유하고, 원시 File/Blob handle과 object URL 수명은 adapter 경계를 MUST NOT 벗어남 |
DELEG-FE-008 로 handle 수명을 위임 |
수신한 위임
없음. 이 branch 는 DELEG-FE-008 의 delegator 다.
가져온 흐름 단계
| Stage Ref | Order | Owner | Input | Action | Output |
|---|
목표
UploadTransferPort 와 StreamingDownloadPort 를 정의하고, presigned URL 획득과 byte 전송 사이의 credential 경계를 고정한다. 이 계약이 없으면 presigned URL 로 나가는 요청에 session 헤더가 그대로 붙어 제3자 스토리지 도메인에 인증 정보가 전달된다. 재개 가능 전송의 part 상태 소유자도 함께 고정한다.
- 이슈:
- PR:
범위
포함 범위
UploadTransferPort— presign 결과 소비, part 분할·병렬·재시도, 진행 스트림, 취소StreamingDownloadPort—ReadableStream소비, range/resume, 진행 스트림- credential-less transfer transport 와 그 경계의 negative fixture
- 무결성 검증(체크섬·크기)과
TRANSFER_INTEGRITY_MISMATCH MediaUrlPolicy— CDN base·허용 transform 기반 URL 파생 (순수 함수)- §9.6 전송 진행 surface state
CAP_FE_LARGE_TRANSFERcapability 행 소유
제외 범위
- use case, domain model, business rule — port 와 adapter 계약까지만 정의한다
- backend 의 presign 발급 endpoint 설계와 서명 알고리즘
- object storage vendor 선택과 그 제약(part 최소 크기·만료) —
FE-Q-012 - Image CDN vendor 선택
File/Blobhandle 과 object URL 수명 —DELEG-FE-008로 raw/branch-notes/feature-frontend-binary-file-io-store-contract 에 위임- 업로드 대상의 도메인 검증 규칙
근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
[[docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design]] §5.1·§5.2·§9.3 |
port 분해, MediaUrlPolicy 가 port 가 아닌 이유, 전송 surface state |
| raw/official-docs/owasp-content-security-policy-cheat-sheet | browser 경계에서 credential·URL 노출을 줄이는 상위 관점 |
| raw/official-docs/mdn-http-range-fetch-transfer | D4 재개 프로토콜 · D6 part 재시도의 body 재생성 · D7 취소의 error 매핑 |
| raw/official-docs/aws-s3-multipart-upload-limits | D8 TRANSFER_PART_SIZE_BYTES 하한 검증과 part 수 상한이 전송 크기를 결정한다는 사실 |
| raw/official-docs/mdn-referrer-policy | D2 유출 경로 3개의 우선순위 재조정 (Referer 는 브라우저 기본값이 이미 방어) |
| raw/official-docs/mdn-storage-quotas-eviction-persistence | D10 UPLOAD_PART_STATE 의 실질 수명 상한 |
| raw/project-notes/ca-skeleton-frontend-operational-contract §5.3·§7.8 | API operation registry 와 auth integration 경계 |
근거 등급 경계: 2026-07-28 /branch-spec 조사로 프로토콜 사실(range 재개, body 1회 소비, abort error, referrer 기본값)과 vendor 제약의 존재(S3 part size 하한)는 공식 문서 근거를 확보했다. 그러나 다음은 여전히 미근거다. (1) FE-D029 가 정한 transport 분리 방식 자체 — 유출 방지라는 목표는 자명하나 "interceptor 재사용 금지" 라는 구현 형태를 지지하는 외부 근거는 없다(D1). (2) part size 8 MiB 와 병렬도 3 이라는 구체 값 — aws-s3-multipart-upload-limits#C1 은 5 MiB 하한만 말하고 최적값을 말하지 않으며, 애초에 vendor 가 미확정이다(FE-Q-012). (3) MediaUrlPolicy 를 port 로 만들지 않는다는 D3. 세 항목 모두 아래 표에 라벨로 표시했다.
TODO
UploadTransferPort·StreamingDownloadPort인터페이스 확정 — 등급:planned- credential-less transport 분리와 negative fixture — 등급:
planned - presign 만료 감지와 재획득 1회 경로 — 등급:
planned - part 재시도 상한(≤2)과 상태 보존 — 등급:
planned - 무결성 검증과 불일치 시 재전송 1회 — 등급:
planned - range 기반 다운로드 재개 — 등급:
planned MediaUrlPolicy순수 함수와 허용 transform 강제 — 등급:plannedAccept-Ranges판별 후에만Range를 보내는 경로 (D4) — 등급:planned- 재개 응답이
206이 아니면 전체 재전송으로 전환 (D4) — 등급:planned - part 재시도 시
Blob재slice negative fixture (D6) — 등급:planned TRANSFER_PART_SIZE_BYTES하한 + part 수 상한 부팅 검증 (D8) — 등급:planned- part state 유실 시 "재개 불가" 표면화 (D10) — 등급:
planned - 전송 payload 에 presigned URL 문자열 0건을 증명하는 관측 등록 (관심사 7 should-fix) — 등급:
planned FE-GATE-029transfer report 산출 — 등급:planned
진행 중 메모
PRESIGN_SAMPLE_UPLOAD 는 FE-REG-API 에 등록되지만 실제 byte 전송 대상 URL 은 registry 에 등록하지 않는다. registry 는 우리 backend 의 operation 장부이고, presigned URL 은 제3자 도메인의 일회용 주소이기 때문이다. 이 비대칭이 FE-OC-006("모든 HTTP 는 shared client 경유")의 유일한 선언적 예외이며, 예외라는 사실 자체를 §범위에 남긴다.
결정 사항
- 2026-07-28: transfer adapter 가 shared client interceptor 를 재사용하지 않음 / 이유: interceptor 체인에 auth 첨부가 있으면 제3자 URL 로 새어나감 / 검토한 대안: interceptor 에 skip 플래그 추가 / 근거:
FE-D029 - 2026-07-28:
MediaUrlPolicy를 port 로 만들지 않음 / 이유: URL 파생에 I-O 가 없어 port 로 만들면 test double 만 늘어남 / 검토한 대안:MediaUrlPort/ 근거: 설계문서 §5.2
결정-근거 매핑
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | 별도 transport 로 credential 분리 | 항상. 스토리지가 same-origin proxy 만 제공하면 FE-D029 재검토 |
없음 — 조사 후에도 구현 형태의 근거 없음 | UNSUPPORTED_DECISION |
두 transport 의 timeout·retry 정책이 갈라진다. 한쪽만 고쳐지는 drift 가 생김 |
| D2 | presigned URL 을 로그·telemetry 에 남기지 않고, Referer 는 "페이지 URL 에 넣지 않기" 로 대체 |
항상 | mdn-referrer-policy.md#C1(기본값), #C2(cross-origin 은 origin 만) |
official-reference(Referer 경로) + project-local default(로그 금지) |
디버깅이 어려워진다. 실패를 operation ID 만으로 추적할 수 있어야 함 |
| D3 | MediaUrlPolicy 는 순수 함수 |
항상. CDN 이 서명된 URL 을 요구하면 port 로 승격 재검토 | 없음 — 조사 후에도 근거 없음 | UNSUPPORTED_DECISION |
서명 필요 CDN 을 만나면 순수 함수 가정이 깨져 리팩터가 필요 |
| D4 | Accept-Ranges 판별 + Range/If-Range, 206 아니면 재개 아님 |
서버가 range 를 지원할 때만 재개. 미지원이면 resumeStrategy 를 끄고 처음부터 |
mdn-http-range-fetch-transfer.md#C1~#C6 |
official-reference |
If-Range validator 로 ETag 를 쓸지 Last-Modified 를 쓸지 미정. vendor 가 무엇을 주는지에 달림(FE-Q-012) |
| D6 | part 재시도는 Blob 재slice |
항상. 예외 없음 — 프로토콜 제약이다 | mdn-http-range-fetch-transfer.md#C10 |
official-reference |
원본 Blob 을 재시도 시점까지 살려 둬야 하므로 handle 수명이 DELEG-FE-008 과 엮임 |
| D7 | AbortController 단일 취소 경로 |
항상 | mdn-http-range-fetch-transfer.md#C7, #C8 |
official-reference |
이미 전송된 바이트가 서버에서 정리되는지는 vendor 책임이며 프론트가 보장하지 못함 |
| D8 | part size 하한 검증 + 최대 전송 크기 노출 | 항상. vendor 확정 시 하한을 그 값으로 교체 | aws-s3-multipart-upload-limits.md#C1, #C2, #C3 |
official-reference(하한의 존재) + project-local default(8 MiB 라는 값) |
8 MiB × 10,000 ≈ 80 GB 라는 상한이 제품 요구를 넘는지 미확인. 이 계산은 우리 도출이지 AWS 서술이 아님 |
| D10 | part state 유실은 정상 경로 | 항상. persist() 요청은 하지 않음 |
mdn-storage-quotas-eviction-persistence.md#C1, #C4, #C5, #C7 |
official-reference |
대용량 전송 중 origin eviction 이 일어나면 사용자는 진행률만 보다가 재개 불가를 통보받는다. UX 문구 미설계 |
구현 가이드
2026-07-28
/branch-spec조사(MDN 2건 + AWS S3 1건)로 프로토콜에 의존하는 detail 을 채웠다. vendor 값에 의존하는 detail(FE-Q-012)은 여전히 미확정이므로 값이 아니라 검증 규칙으로만 적었다. 근거가 원칙만 지지하는 cell 은UNSUPPORTED_IMPL_DECISION+ trade-off 로 표시했다(CLAUDE.md §15.5 R2).
1. credential 경계 — 두 transport 의 분리 형태
Trace: D1(별도 transport) ← 근거 없음(project decision) / D2(URL 미기록) ←
mdn-referrer-policy.md#C1·#C2/ 상속DEC-…-TRANSFER-CREDENTIAL-001@1
presign 획득은 shared client 를 통과한다(FE-OC-006). byte 전송은 shared client 를 쓰지 않는 별도 함수가 수행하며, 그 함수는 auth 를 붙이는 코드에 접근할 수 없어야 한다 — 정책이 아니라 도달 불가능성으로 강제한다. 첨부 금지를 런타임 조건문으로 구현하면 조건이 하나 빠지는 순간 유출이므로, transport 자체를 분리해 첨부 코드가 그 경로에 존재하지 않게 한다.
유출 경로는 세 개가 아니라 두 개다. #C1+#C2 대로 브라우저 기본 정책(strict-origin-when-cross-origin)이 cross-origin 요청에 path·query 를 이미 보내지 않으므로 Referer 는 방어된 상태다. 남은 실질 경로는 (a) 우리 로그·telemetry·에러 객체, (b) presigned URL 이 페이지 URL·history 에 들어가는 코드. fixture 밀도를 (a)·(b) 에 몰고, Referer 에 대해서는 (b) 금지 규칙 하나만 둔다.
UNSUPPORTED_IMPL_DECISION — 분리를 "별도 모듈 + 별도 fetch wrapper" 로 구현하는 것. 근거는 목표(유출 방지)만 지지하고 형태는 지지하지 않는다. trade-off: 대안(interceptor skip 플래그)은 실수의 방향이 유출 쪽이라 배제했다. 비용은 timeout·retry 정책이 두 벌이 되는 것이며, 이를 registry 값 공유로 완화한다.
2. 다운로드 재개 프로토콜
Trace: D4 ←
mdn-http-range-fetch-transfer.md#C1~#C6/ D7(취소) ←#C7·#C8
StreamingDownloadPort 는 재개를 낙관적으로 시도하지 않는다. 순서는 고정이다.
- 최초 응답의
Accept-Ranges를 기록한다. 헤더가 없거나none이면resumeStrategy를 비활성화한다(#C1) - 재개 시
Range와 함께If-Range를 보낸다.#C5가 요구하는 "원본 미변경 보장" 을 이 헤더가 담당한다 - 응답 상태를 검사한다.
206이면Content-Range로 위치를 확인하고 이어붙인다(#C2).200이면 재개가 아니라 전체 재전송이므로 받아 둔 앞부분을 버리고 처음부터 쓴다(#C4·#C6).416은 재개 위치가 리소스 밖이라는 뜻이므로 terminal 로 보고TRANSFER_INTEGRITY_MISMATCH로 매핑한다(#C3)
3번이 이 절의 핵심이다. 200 을 성공으로 처리하면 앞부분 + 전체가 이어붙어 길이가 늘어난 파일이 만들어지고, 무결성 검사가 없으면 그대로 저장된다.
취소는 AbortController 하나로 통일한다. #C7(fetch reject)과 #C8(body 읽기 중 reject)은 다른 시점의 같은 오류이므로 둘 다 REQUEST_ABORTED 로 매핑하고 구분하지 않는다.
UNSUPPORTED_IMPL_DECISION — If-Range 의 validator 로 ETag 를 우선하고 없으면 Last-Modified 를 쓰는 것. #C5·#C6 는 조건부 재개의 필요성만 말하고 validator 선택을 말하지 않는다. trade-off: ETag 가 더 정밀하지만 vendor 가 무엇을 주는지 미확정(FE-Q-012)이라 둘 다 처리하는 쪽을 택했다.
3. part 분할·재시도
Trace: D6(재slice) ←
mdn-http-range-fetch-transfer.md#C10/ D8(하한 검증) ←aws-s3-multipart-upload-limits.md#C1~#C3/ 상속DEC-…-RESUMABLE-TRANSFER-001@1
재시도는 body 를 재생성한다. #C10 대로 한 번 읽힌 스트림은 disturbed 상태가 되어 누구도 다시 읽지 못하므로, 재시도 시 원본 Blob 을 같은 오프셋으로 다시 slice() 한다. 첫 시도의 body 객체를 보관했다 재사용하는 구현은 두 번째 시도에서 빈 본문을 보내고, 서버는 그것을 성공으로 받는다.
part size 는 값이 아니라 검증 규칙으로 고정한다.
| 규칙 | 근거 |
|---|---|
TRANSFER_PART_SIZE_BYTES ≥ 하한 을 부팅 시 검사하고 위반이면 boot fail |
#C1 — 하한 미만은 마지막 part 를 제외한 전 구간에서 거부된다 |
| 마지막 part 만 하한 예외 | #C2 — "no minimum size limit on the last part" |
ceil(총크기 / partSize) ≤ part 수 상한 을 전송 시작 전 검사 |
#C3 — 상한 초과 분할은 전송 자체가 불가능 |
최대 전송 크기 = partSize × part 수 상한 을 계약값으로 노출 |
위 두 값의 우리 도출이며 AWS 서술이 아님 |
vendor 미확정 동안 하한은 5 MiB, part 수 상한은 10,000 을 잠정값으로 쓴다. 이 두 값의 출처가 S3 라는 사실을 registry 주석에 남겨, vendor 확정 시 무엇을 바꿔야 하는지가 코드에서 보이게 한다.
UNSUPPORTED_IMPL_DECISION — part size 8 MiB 와 병렬도 3 이라는 값. #C1 은 5 MiB~5 GiB 라는 허용 범위만 말하고 최적값을 말하지 않는다. trade-off: 하한(5 MiB)에 붙이면 part 수가 늘어 상한(10,000)에 빨리 닿고, 크게 잡으면 재시도 1회의 손실이 커진다. 8 MiB 는 그 사이의 임의 지점이며 측정으로 대체되어야 한다.
4. part 상태의 수명
Trace: D10 ←
mdn-storage-quotas-eviction-persistence.md#C1·#C4·#C5·#C7
UPLOAD_PART_STATE 는 사라질 수 있는 값으로 다룬다. #C1 대로 브라우저 eviction 은 origin 전량 삭제라 이 행만 보호할 방법이 없고, #C4·#C5 대로 persist() 는 허가가 브라우저 재량이다. 따라서 adapter 는 재개 전 part 상태의 존재와 정합성을 검사하고, 없으면 오류가 아니라 "재개 불가" 로 표면화한 뒤 처음부터 전송한다.
Safari ITP 의 7일 규칙(#C7)은 이 계약에서 구속 조건이 아니다 — FE-REG-STORAGE 의 UPLOAD_PART_STATE TTL 이 24시간이라 7일보다 먼저 만료된다. 구속하는 것은 시간이 아니라 저장 압박이며, 그것은 예고 없이 온다.
persist() 요청은 이 branch 가 하지 않는다 — 소유자는 raw/branch-notes/feature-frontend-binary-file-io-store-contract D7 이다. 두 branch 가 각자 요청하면 프롬프트가 중복된다.
5. 이 branch 가 남기지 않는 것 (R3)
- presign 발급 endpoint 설계·서명 알고리즘·만료 시간 정책 → backend 소유, §범위 제외
File/Blobhandle 과 object URL 수명 →DELEG-FE-008로 raw/branch-notes/feature-frontend-binary-file-io-store-contract 에 위임- storage vendor 선택 →
FE-Q-012. 이 절은 vendor 값을 쓰지 않고 검증 규칙만 고정했다 - CSP·
Referrer-Policy헤더의 실제 설정 → raw/branch-notes/feature-frontend-browser-security-boundary-contract 소유(FE-OC-019)
엣지·실패·의존
- 실패·엣지 경로
- presigned URL 만료 →
PRESIGN_EXPIRED, 재획득 1회 후 같은 위치에서 재개. 재획득도 실패하면 사용자 주도 retry - part 재시도 상한 소진 →
UPLOAD_PART_FAILED, part 상태 보존 후transfer-paused - 체크섬/크기 불일치 →
TRANSFER_INTEGRITY_MISMATCH, 해당 part 폐기 후 재전송 1회, 재실패면 terminal - 다운로드 스트림 중단 →
STREAM_INTERRUPTED,resumeStrategy: range면 받은 범위부터 재개 - 사용자 취소 →
REQUEST_ABORTED재사용, part 상태 폐기 여부를 명시 UPLOAD_PART_STATE저장 실패 →BLOB_STORE_UNAVAILABLE. 이 행은 fallback 이 없으므로 전송을 재개 불가로 표시하고 조용히 memory 로 넘어가지 않음- 재개 요청에
200응답 → 재개가 아니라 전체 재전송이다(mdn-http-range-fetch-transfer#C4·#C6). 받아 둔 앞부분을 버리고 처음부터 쓴다. 성공으로 처리하면 길이가 늘어난 파일이 만들어진다 (D4) - 재개 위치가 리소스 밖 →
416(#C3). 재시도로 회복되지 않으므로 terminal 로 보고TRANSFER_INTEGRITY_MISMATCH로 매핑한다 Accept-Ranges부재·none→ 재개 자체가 불가(#C1).resumeStrategy를 끄고 중단 시 처음부터 다시 받는다- 재시도 시 body 재사용 → disturbed 스트림이라 빈 본문이 전송된다(
#C10). 원본Blob을 재slice 한다 (D6) - part state 유실 → 오류가 아니라 재개 불가로 표면화하고 처음부터 전송 (D10)
- part size 가 vendor 하한 미만 → 마지막 part 를 제외한 전 구간이 거부된다(
aws-s3-multipart-upload-limits#C1). 부팅 시 검사해 boot fail (D8)
- presigned URL 만료 →
- 다른 계약 의존
- raw/branch-notes/feature-frontend-binary-file-io-store-contract
D1·D7— object URL·handle 수명(DELEG-FE-008)과persist()요청 소유권. D6 이 재시도까지 원본Blob을 살려 둬야 하므로 handle 수명 규칙이 이 branch 의 재시도 가능 범위를 정한다 - raw/branch-notes/feature-frontend-storage-registry-contract —
UPLOAD_PART_STATE의quotaFallback: 없음·evictionOrder: null·TTL 24h. D10 이 이 세 값에 직접 의존한다 - raw/branch-notes/feature-api-client-response-envelope-contract — shared client 의 timeout·retry·envelope. presign 획득 경로가 이를 통과하며, D1 이 전송 경로를 여기서 떼어낸다
- raw/branch-notes/feature-frontend-browser-security-boundary-contract —
Referrer-Policy·CSP 헤더의 실제 설정(FE-OC-019). D2 는 이 branch 가 기본값을 바꾸지 않는다고 전제한다 —unsafe-url로 완화되면 D2 의 전제가 깨진다(mdn-referrer-policy#C4) - raw/branch-notes/feature-frontend-error-classification-boundary-contract —
PRESIGN_EXPIRED·UPLOAD_PART_FAILED·TRANSFER_INTEGRITY_MISMATCH·STREAM_INTERRUPTED의 kind 등록과 retryable 기본값
- raw/branch-notes/feature-frontend-binary-file-io-store-contract
검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| transfer 요청에 session credential 이 붙지 않는다 | interceptor 가 전역이면 조용히 새어나감 | negative fixture — 전송 요청을 가로채 Authorization·쿠키 헤더 부재 확인 |
planned |
| presigned URL 이 telemetry·로그에 남지 않는다 | 에러 객체에 URL 이 딸려오기 쉬움 | grep + fixture — 전송 실패 시 emit 된 event payload 에 URL 문자열 0건 | planned |
| part 재시도가 2회를 넘지 않는다 | 상위 재시도와 part 재시도가 곱해질 수 있음 | 결정론 fake clock test — 총 시도 횟수 카운트 | planned |
| 만료 후 재획득으로 같은 위치에서 재개된다 | 재개 위치 계산이 vendor 별로 다름 | integration test — 만료 강제 후 이어받기 지점 확인 | needs-confirmation |
| part size 8 MiB 가 실제 환경에서 합리적이다 | 측정 없이 정한 초기 default | 벤치마크 — 네트워크 프로파일별 처리량 비교, FE-D030 revisit trigger 에 연결 |
needs-confirmation |
UPLOAD_PART_STATE 가 quota 초과 시 memory 로 넘어가지 않는다 |
fallback 이 기본 동작으로 새기 쉬움 | negative fixture — quota 초과 강제 후 재개 불가로 표시되는지 | planned |
재개 요청에 200 이 오면 처음부터 다시 쓴다 |
성공 상태 코드라 통과시키기 쉽고, 통과하면 길이가 늘어난 파일이 만들어짐 | negative fixture — Range 를 무시하고 200 + 전체 본문을 주는 서버 stub 에 대해 최종 파일 크기가 원본과 같은지 |
planned |
Accept-Ranges 부재 시 재개를 시도하지 않는다 |
판별을 건너뛰고 낙관적으로 Range 를 보내기 쉬움 |
fixture — 헤더 없는 응답 후 중단·재개 시 Range 헤더가 나가지 않는지 |
planned |
| part 재시도가 빈 본문을 보내지 않는다 | #C10 의 disturbed 스트림 문제는 첫 시도 성공 시 드러나지 않음 |
fixture — 첫 시도 실패 강제 후 두 번째 요청의 Content-Length 가 part size 와 같은지 |
planned |
| part size 하한 위반이 부팅을 막는다 | 검증을 넣지 않으면 런타임에 vendor 거부로만 드러남 | negative fixture — 하한 미만 값으로 boot 시 실패하는지 | planned |
최대 전송 크기(partSize × part 수 상한)가 제품 요구를 넘는다 |
8 MiB × 10,000 ≈ 80 GB 는 우리 도출이며 요구를 확인하지 않았음 | 제품 요구 확인 후 registry 값 재계산 | needs-confirmation |
If-Range validator 로 무엇을 써야 하는지 |
vendor 가 ETag 를 주는지 Last-Modified 를 주는지 미확정 (FE-Q-012) |
vendor 확정 후 응답 헤더 실측 | needs-confirmation |
| 대용량 전송 중 origin eviction 이 실제로 얼마나 자주 일어나는지 | #C1 은 가능성만 말하고 빈도를 말하지 않음. UX 문구 설계가 이 빈도에 달림 |
telemetry — UPLOAD_PART_STATE 유실로 재개 불가가 된 전송 비율 |
needs-confirmation |
Audit & Findings
/branch-spec2026-07-28 조사에서 발견한 상위 계약과 조사 결과의 불일치. hub 소유 항목은 정합 권고만 남긴다(hub §3.3).
| Finding ID | 대상 | 현재 서술 | 조사 결과 | 권고 | 처리 |
|---|---|---|---|---|---|
PART_SIZE_UNVALIDATED |
hub §5.4 TRANSFER_PART_SIZE_BYTES / FE-D030 |
part size·병렬도를 registry 로 "고정" 한다고만 적고 허용 범위를 말하지 않는다 | aws-s3-multipart-upload-limits#C1: part size 는 5 MiB~5 GiB 라는 vendor 하드 제약을 받고, #C3: part 수 상한 10,000 이 최대 전송 크기를 결정한다. 하한 미만 값은 마지막 part 를 제외한 전 구간에서 거부된다 |
env key 정의에 하한 검증과 part 수 상한 검사를 추가할 것. 값 자체는 vendor 확정(FE-Q-012) 전까지 잠정 |
open — FE-Q-012 선행이나, 검증 규칙은 vendor 무관하므로 먼저 넣을 수 있음 |
REFERRER_RISK_MISWEIGHTED |
이 branch 의 D2 초안 |
"telemetry·로그·Referrer 어디에도" — 세 경로를 같은 무게로 나열했다 |
mdn-referrer-policy#C1·#C2: 기본 정책 strict-origin-when-cross-origin 이 cross-origin 요청에 path·query 를 이미 보내지 않는다. 게다가 Referer 는 요청을 유발한 문서의 URL 이지 요청 대상 URL 이 아니다 |
Referer 항목을 "presigned URL 을 페이지 URL 에 넣지 않는다" 는 금지 규칙으로 좁히고, fixture 밀도를 로그·telemetry 로 옮길 것 |
resolved 2026-07-28 — D2 서술과 §구현 가이드 1 에 반영 |
NO_GROUND_TRUTH |
/branch-spec §2 ca-tmpl 대조 |
명령은 ca-tmpl registry·코드와 대조하라고 요구 | ca-tmpl 은 Gradle/Java 백엔드 전용. frontend 구현 repo 미식별 |
전 항목 planned 유지. 승격은 FE-Q-001 이후 |
open |
관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
2026-07-28
/branch-spec§8b. agent dispatch 없이 수기 판정했다(세션 제약). governing doc = raw/project-notes/ca-skeleton-frontend-operational-contract. 기준은 hub §2.2 의 10개 universal acceptance question.
| # | 관심사 | 판정 | 근거 |
|---|---|---|---|
| 1 | 문제와 실패 모드가 구체적인가 | covered-here | §목표 — 제3자 도메인으로의 credential 전달. §엣지 12개 경로 |
| 2 | 상속한 결정을 실제로 적용했는가 | covered-here | TRANSFER-CREDENTIAL-001 → D1·D2, RESUMABLE-TRANSFER-001 → D8·D10, CAPABILITY-001 → CAP_FE_LARGE_TRANSFER 소유 |
| 3 | project-wide default 와 limit | covered-here | D4(206 아니면 재개 아님), D6(재slice), D8(하한 검증 + 최대 전송 크기), D10(유실은 정상 경로) |
| 4 | 대안을 검토했는가 | covered-here | §결정 사항 — interceptor skip 플래그 / MediaUrlPort. D1·D3 는 trade-off 를 명시 |
| 5 | 금지 구현 | covered-here | §구현 가이드 — 전송 경로에 auth 첨부 코드 도달 금지, body 재사용 금지, URL 을 로그·페이지 URL 에 넣기 금지 |
| 6 | 실패 경로가 error kind 로 매핑되는가 | covered-here | PRESIGN_EXPIRED·UPLOAD_PART_FAILED·TRANSFER_INTEGRITY_MISMATCH·STREAM_INTERRUPTED·REQUEST_ABORTED·BLOB_STORE_UNAVAILABLE |
| 7 | 관측 가능한가 | should-fix | 진행률·재개·part 재시도 telemetry 는 hub §5.8 에 있으나, URL 미유출을 증명하는 관측(payload grep gate)이 fixture 로만 있고 registry event 로 등록되지 않았다 |
| 8 | 위임 경계가 명확한가 | covered-here | DELEG-FE-008, §구현 가이드 5 의 R3 목록 |
| 9 | 검증 수단이 있는가 | covered-here | §검증해야 할 주장 14행, FE-GATE-029 fixture + transfer report |
| 10 | 말할 수 있는 범위 | covered-here | 전 항목 planned — NO_GROUND_TRUTH |
판정: Covered (missing 0) · Should-fix 1건(관심사 7). Blocking 아님.
마주친 문제
없음.
묶음 (이 branch에서 파생된 자료)
Sub-branches (세부 작업)
아직 없음.
오류 기록 (이 branch 작업 중 발생)
아직 없음.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
아직 없음.
강의 (이 작업을 위해 학습한 강의)
아직 없음.
job-posting tie-ins (이 작업에서 파생된 글감)
아직 없음.
관련 일일 노트
- 아직 없음
완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목: 없음locally-verified항목: 없음prod-verified항목: 없음
- 추출하지 않을 항목: 현재 전 항목
planned