docs: [guides] R2 이미지 프록시 가이드 추가 (3가지 접근 방식, 환경 설정, 트러블슈팅)
This commit is contained in:
1
INDEX.md
1
INDEX.md
@@ -240,6 +240,7 @@ DB 도메인별:
|
||||
| [erp-api-detail.md](dev/guides/erp-api-detail.md) | ERP API 상세 |
|
||||
| [item-master-guide.md](dev/guides/item-master-guide.md) | 품목기준관리 구조 |
|
||||
| [claude-code-to-slack.md](dev/guides/claude-code-to-slack.md) | Claude Code → 슬랙 붙여넣기 가이드 |
|
||||
| [r2-image-proxy-guide.md](dev/guides/r2-image-proxy-guide.md) | R2 이미지 프록시 가이드 (redirect/streaming/API 프록시, 트러블슈팅) |
|
||||
| [claude-code-btw-guide.md](dev/guides/claude-code-btw-guide.md) | Claude Code /btw 사이드 질문 기능 가이드 |
|
||||
| [tenant-email-integration-guide.md](dev/guides/tenant-email-integration-guide.md) | 테넌트 이메일 연동 (SMTP 프리셋, MNG 관리 화면, 연결 테스트) |
|
||||
| [performance-report-excel-export.md](dev/guides/performance-report-excel-export.md) | 실적신고 확정건 엑셀 Export (건기원 양식, PhpSpreadsheet, 셀 병합) |
|
||||
|
||||
215
dev/guides/r2-image-proxy-guide.md
Normal file
215
dev/guides/r2-image-proxy-guide.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# R2 이미지 프록시 가이드
|
||||
|
||||
> **작성일**: 2026-03-21
|
||||
> **상태**: 운영 중
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
SAM 프로젝트는 파일 저장소로 **Cloudflare R2** (S3 호환)를 사용한다. MNG에서 R2 이미지를 표시할 때 환경(Docker/서버)과 용도(일반 표시/Canvas 편집/미리보기)에 따라 다른 접근 방식이 필요하다.
|
||||
|
||||
### 핵심 문제
|
||||
|
||||
```
|
||||
브라우저 → R2 직접 접근: CORS 차단 (Canvas에서 사용 불가)
|
||||
Docker 내부 → api.sam.kr: DNS 해석 불가 (500 에러)
|
||||
브라우저 JS → https://nginx: Docker 내부 URL 접근 불가
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 이미지 접근 경로 3가지
|
||||
|
||||
### 2.1 일반 `<img>` 표시 — redirect 방식
|
||||
|
||||
```
|
||||
브라우저 → /files/{id}/view → MNG FileViewController → API presigned URL → 302 redirect → R2
|
||||
```
|
||||
|
||||
- **용도**: 목록/상세 화면의 이미지 표시
|
||||
- **라우트**: `GET /files/{id}/view` → `FileViewController@show`
|
||||
- **동작**: API에서 presigned URL을 받아 브라우저를 R2로 redirect
|
||||
- **장점**: 빠름 (서버에서 이미지 다운로드 안 함)
|
||||
- **제한**: Canvas에서 사용 불가 (redirect 후 cross-origin → tainted canvas)
|
||||
|
||||
### 2.2 Canvas 편집기 — streaming 프록시
|
||||
|
||||
```
|
||||
브라우저 → /files/{id}/proxy → MNG FileViewController → R2 다운로드 → 이미지 스트리밍
|
||||
```
|
||||
|
||||
- **용도**: 절곡품 전개도 Canvas 편집기 (`fabric.Image.fromURL`)
|
||||
- **라우트**: `GET /files/{id}/proxy` → `FileViewController@proxy`
|
||||
- **동작**: MNG 서버가 R2에서 이미지를 다운로드하여 같은 도메인으로 스트리밍
|
||||
- **장점**: CORS 문제 없음, `toDataURL()` 정상 동작
|
||||
- **제한**: `file_id`가 필요 (image_path만 있으면 사용 불가)
|
||||
|
||||
### 2.3 미리보기 모달 — MNG API 프록시
|
||||
|
||||
```
|
||||
브라우저 JS → /api/admin/document-templates/presigned-url-by-path → MNG API → API 서버 → R2 presigned URL 반환
|
||||
```
|
||||
|
||||
- **용도**: 문서양식 미리보기에서 섹션 이미지 (`image_path`만 있는 경우)
|
||||
- **라우트**: `POST /api/admin/document-templates/presigned-url-by-path`
|
||||
- **동작**: 브라우저 JS가 MNG API를 호출 → MNG가 API 서버에 presigned URL 요청 → URL 반환
|
||||
- **장점**: `file_id` 없이 `image_path`로 접근 가능
|
||||
- **주의**: 동기 XHR 사용 (미리보기 렌더링 시 순차 처리)
|
||||
|
||||
---
|
||||
|
||||
## 3. 환경별 설정
|
||||
|
||||
### 3.1 Docker (로컬)
|
||||
|
||||
```env
|
||||
# api/.env
|
||||
R2_ACCESS_KEY_ID=cecd4d4c...
|
||||
R2_SECRET_ACCESS_KEY=f20136ec...
|
||||
R2_BUCKET=sam
|
||||
R2_ENDPOINT=https://caf8dcb2c4ea443018ee5e7a7421db0e.r2.cloudflarestorage.com
|
||||
R2_REGION=auto
|
||||
```
|
||||
|
||||
```env
|
||||
# mng/.env (Docker 내부 통신)
|
||||
API_BASE_URL=https://api.sam.kr
|
||||
API_INTERNAL_URL=https://nginx
|
||||
```
|
||||
|
||||
### 3.2 서버 (개발/운영)
|
||||
|
||||
```env
|
||||
# api/.env — R2 설정 동일
|
||||
# mng/.env
|
||||
API_BASE_URL=https://api.dev.codebridge-x.com
|
||||
# API_INTERNAL_URL 미설정 (직접 접근)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. MNG → API 호출 시 필수 패턴
|
||||
|
||||
MNG에서 API를 호출할 때 `API_INTERNAL_URL` 분기 처리가 **필수**이다.
|
||||
|
||||
```php
|
||||
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
|
||||
$internalUrl = config('services.api.internal_url');
|
||||
|
||||
$headers = [
|
||||
'X-API-KEY' => config('services.api.key'),
|
||||
'X-TENANT-ID' => session('selected_tenant_id', 1),
|
||||
];
|
||||
|
||||
// Docker: nginx 컨테이너 경유, Host 헤더로 서버 블록 라우팅
|
||||
if ($internalUrl) {
|
||||
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
|
||||
$baseUrl = $internalUrl;
|
||||
}
|
||||
|
||||
$response = Http::baseUrl($baseUrl)
|
||||
->withoutVerifying()
|
||||
->withHeaders($headers)
|
||||
->timeout(10)
|
||||
->get('/api/v1/...');
|
||||
```
|
||||
|
||||
> 참조 구현: `FormulaApiService::resolveApiConnection()`
|
||||
|
||||
---
|
||||
|
||||
## 5. API 화이트리스트
|
||||
|
||||
MNG에서 Bearer 토큰 없이 호출하는 API는 `ApiKeyMiddleware`의 `allowWithoutAuth`에 등록 필요:
|
||||
|
||||
```
|
||||
api/v1/bending-items 절곡 기초관리
|
||||
api/v1/bending-items/* 절곡 기초관리 상세
|
||||
api/v1/guiderail-models 가이드레일 모델
|
||||
api/v1/guiderail-models/* 가이드레일 모델 상세
|
||||
api/v1/items/*/files 품목 파일
|
||||
api/v1/files/*/presigned-url 파일 presigned URL
|
||||
api/v1/files/presigned-url-by-path 경로 기반 presigned URL
|
||||
```
|
||||
|
||||
**파일 위치**: `api/app/Http/Middleware/ApiKeyMiddleware.php`
|
||||
|
||||
---
|
||||
|
||||
## 6. 트러블슈팅
|
||||
|
||||
### 이미지가 404로 나올 때
|
||||
|
||||
1. **R2 설정 확인**: API `.env`에 `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_ENDPOINT` 존재 여부
|
||||
2. **API 캐시 클리어**: `docker exec sam-api-1 php artisan config:clear`
|
||||
3. **R2 파일 존재 확인**: `Storage::disk('r2')->exists('경로')`
|
||||
|
||||
### 이미지가 401로 나올 때
|
||||
|
||||
1. **화이트리스트 확인**: `ApiKeyMiddleware`의 `allowWithoutAuth`에 해당 라우트 등록 여부
|
||||
2. **X-API-KEY 확인**: `config('services.api.key')` 값이 `api_keys` 테이블에 존재하는지
|
||||
3. **X-TENANT-ID 확인**: `session('selected_tenant_id')` 값
|
||||
|
||||
### Canvas에서 tainted canvas 에러
|
||||
|
||||
1. **프록시 사용 확인**: `/files/{id}/view`(redirect) 대신 `/files/{id}/proxy`(streaming) 사용
|
||||
2. **`data-proxy-url` 속성**: `<img>` 태그에 `data-proxy-url="{{ route('files.proxy', $fileId) }}"` 추가
|
||||
3. **JS에서 프록시 URL 우선**: `current.dataset.proxyUrl || current.src`
|
||||
|
||||
### Docker에서 api.sam.kr 연결 실패 (cURL error 7)
|
||||
|
||||
1. **`API_INTERNAL_URL` 설정**: MNG `.env`에 `API_INTERNAL_URL=https://nginx`
|
||||
2. **Host 헤더 추가**: `$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST)`
|
||||
3. **참조**: `BendingBaseController::api()`, `FileViewController`, `DocumentTemplateController`
|
||||
|
||||
### 미리보기에서 섹션 이미지 안 나올 때
|
||||
|
||||
1. **MNG API 프록시 확인**: `/api/admin/document-templates/presigned-url-by-path` 라우트 존재 여부
|
||||
2. **`image_url` 캐시**: `_previewImageUrl` 함수에서 한 번 조회 후 `section.image_url`에 캐시
|
||||
3. **브라우저 콘솔**: XHR 요청 상태 확인 (200이면 정상, 401이면 화이트리스트, 500이면 R2 설정)
|
||||
|
||||
---
|
||||
|
||||
## 7. 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `mng/app/Http/Controllers/FileViewController.php` | `show`(redirect), `proxy`(streaming) |
|
||||
| `mng/routes/web.php` | `/files/{id}/view`, `/files/{id}/proxy` |
|
||||
| `mng/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php` | `presignedUrlByPath` (미리보기용) |
|
||||
| `mng/resources/views/document-templates/partials/preview-modal.blade.php` | `_previewImageUrl` 함수 |
|
||||
| `mng/app/Http/Controllers/BendingBaseController.php` | `api()` 메서드 (internal_url 패턴) |
|
||||
| `mng/app/Http/Controllers/DocumentTemplateController.php` | `getPresignedUrlFromApi`, `getPresignedUrlByPath` |
|
||||
| `api/app/Http/Middleware/ApiKeyMiddleware.php` | `allowWithoutAuth` 화이트리스트 |
|
||||
| `api/config/filesystems.php` | R2 디스크 설정 (`disks.r2`) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 요약 다이어그램
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ Cloudflare R2 │
|
||||
│ (S3 호환 파일 저장소) │
|
||||
└──────────┬───────────────────────┘
|
||||
│ presigned URL
|
||||
┌──────────┴───────────────────────┐
|
||||
│ API 서버 (Laravel) │
|
||||
│ /api/v1/files/{id}/presigned-url │
|
||||
│ /api/v1/files/presigned-url-by-path │
|
||||
└──────────┬───────────────────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌──────────┴──────────┐ ┌──────┴──────┐ ┌──────────┴──────────┐
|
||||
│ /files/{id}/view │ │ /files/{id} │ │ /api/admin/doc-tmpl │
|
||||
│ (redirect → R2) │ │ /proxy │ │ /presigned-url-by- │
|
||||
│ │ │ (streaming) │ │ path (MNG API) │
|
||||
│ 일반 <img> 표시 │ │ Canvas 편집 │ │ 미리보기 모달 │
|
||||
└─────────────────────┘ └─────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2026-03-21
|
||||
Reference in New Issue
Block a user