모듈 명 : 첨부파일 외부 저장소 이동
소개: 첨부파일을 S3 호환 오브젝트 스토리지 또는 FTP로 자동 이동시켜 서버 용량을 절감하는 모듈
설치경로 : ./modules/bsplus_external_storage
라이믹스 버전 : 2.1 ~ 2.1.35
PHP 버전 : 7.4 ~ 8.x
1차 다운로드 : bsplus_external_storage-v1.0.0.zip
2차 다운로드 : bsplus_external_storage-v1.2.0.zip
3차 다운로드 : bsplus_external_storage-v1.2.1.zip
================== 기능 추가 미리보기 - 1.2.1 버전 ====================
S3 & FTP 모두 완료 정상 작동합니다.

============ 미리보기=============
홈페이지 드라이브 최적화 결과 : 홈페이지 드라이브 업로드 파일 옮기기 최적화 시도 테스트 현황



============================ 패치리스트 =================================
# bsplus_external_storage 패치리스트
## v1.2.1 (2026-07-23)
### 중요 수정 (실제 R2 연동 테스트로 발견)
- **설정 저장이 조용히 막히던 버그**: 우선순위에 넣은 저장소(예: 3순위 FTP)의 세부 정보(호스트 등)를 아직 안 채웠으면 저장 자체를 막고 있었음 — 실제로는 업로드 시점에 그 저장소를 자동으로 건너뛰도록 이미 설계되어 있어서 저장을 막을 이유가 없었음. 이제 항상 저장되고, 정보가 부족한 저장소가 있으면 참고 메시지만 표시.
- **S3로 이동된 파일이 게시글 본문에서 깨지던 버그**: 다운로드 버튼 경로에는 S3 signed URL 리다이렉트를 넣었지만, 게시글 본문 `<img>` 등 정적 서빙 폴백 경로(`triggerCheckServeFile`)에는 그 로직이 빠져있었음. 문서 HTML은 파일이 이동된 뒤에도 최초 저장 당시의 로컬 URL을 그대로 갖고 있어서(자동으로 재작성되지 않음) 이 경로가 반드시 필요했음. `moduleHandler.init` 폴백에도 동일한 signed/공개 URL 리다이렉트 로직 추가.
- **S3 "공개 URL" 옵션이 실제로는 동작하지 않던 문제**: 관리자 화면에 "공개 URL" 선택지와 "공개 URL 베이스" 입력칸이 있었지만 `S3Driver`가 이 설정을 아예 읽지 않고 항상 signed URL만 생성했음. 이제 `s3_use_public_url`/`s3_public_url_base` 설정을 실제로 반영.
## v1.2.0 (2026-07-22)
### 신규 기능: 로컬 + S3 + FTP 동시 사용 (우선순위 체인)
- **"저장소 종류" 단일 선택(라디오) → 1/2/3순위 드롭다운 3개로 전면 개편**. 로컬/S3/FTP를 전부 동시에 설정해두고 순서만 지정할 수 있음 (v1.0.0의 "다중 저장소 미지원" 제한이 해소됨). 기본 추천 순서는 로컬 > S3 > FTP.
- 업로드 시 1순위부터 순서대로 시도: **로컬은 여유공간을 미리 확인**해서 부족하면 시도 자체를 건너뛰고 다음 순위로, **S3/FTP는 용량 개념이 없으므로 실제로 업로드를 시도해보고 실패하면 다음 순위로** 자동 전환 (`attemptChainUpload()` 공통 로직).
- 각 파일은 실제로 성공한 저장소 종류를 그대로 매핑에 기록하므로(활성 설정이 아니라 시도 결과 기준), 로컬 성공/S3 성공 파일이 섞여 있어도 각자 자기 위치로 정확히 추적됨 — v1.0.1에서 고친 "활성 설정 기준 드라이버 생성" 버그의 근본 해결과 같은 원칙.
- "연결 테스트" 버튼이 섹션별(로컬/S3/FTP)로 독립 동작하도록 변경 — 순위와 무관하게 각 저장소를 개별적으로 검증 가능.
- 예전 단일 `storage_type` 설정은 하위호환으로 계속 인식(첫 로그인 시 1순위로 자동 승격, 나머지는 추천 순서로 채워짐).
### 알려진 제한 사항 (갱신)
- 로컬 여러 경로(v1.1.0)와 이 다중 저장소 체인(v1.2.0)은 함께 사용 가능 — 예: "1순위=로컬(드라이브 2개 등록) > 2순위=S3".
- S3/FTP를 낮은 순위로 둔 경우, 그 단계에 도달하는 매 업로드마다 실제 API 호출이 발생함(사전 용량 체크가 없어 시도 자체가 검증 수단이기 때문) — 아주 드물게 대량 업로드 상황에서는 지연 요인이 될 수 있음.
## v1.1.0 (2026-07-22)
### 신규 기능: 로컬/마운트 다중 경로 (용량 부족 자동 대응)
- 로컬/마운트 저장소를 **여러 개(우선순위 순) 등록** 가능하게 변경. 관리자 설정 화면에서 경로를 한 줄에 하나씩 입력 (기존 단일 `local_path` 필드는 하위호환으로 계속 인식됨).
- 파일 이동 시 1순위 경로부터 `disk_free_space()`로 여유 공간(파일크기 + 100MB 여유분) 확인 → 부족하면 자동으로 다음 순위 경로 사용. 모든 경로가 꽉 차 있으면 이동을 시도하지 않고 로컬 원본을 그대로 둔 채 `failed` 상태로 기록(사전 감지 — 무의미한 시도/실패 로그를 줄임).
- 매핑 테이블에 `storage_root` 컬럼 추가 — 각 파일이 **실제로 어느 경로에 저장됐는지** 파일 단위로 기록. 삭제/다운로드/이미지 서빙 시 항상 이 값을 사용하므로, 나중에 경로 우선순위를 바꾸거나 경로를 추가/제거해도 기존 파일 위치 추적이 깨지지 않음.
- 기존에 단일 경로로 이미 이동해둔 파일들은 모듈 업데이트 시 자동으로 `storage_root`가 채워짐(하위호환 마이그레이션).
- 관리자 설정 화면에 **저장소 현황(용량 게이지)** 추가 — 등록된 각 경로의 사용률을 막대 그래프로 표시 (70% 이상 주황, 90% 이상 빨강), 여유 공간/전체 용량도 함께 표시.
- "연결 테스트"가 여러 경로를 한 번에 검사하도록 확장.
- 매핑 목록 화면에 "저장 위치" 컬럼 추가.
- **경로 줄 앞에 `#`을 붙이면 즉시 일시중지**(예: `#D:\nas_mount\attach`) — 자동 용량 감지를 기다리지 않고 관리자가 지금 당장 다음 순위 경로로 강제 전환하고 싶을 때 사용. 이미 그 경로에 있는 기존 파일은 storage_root로 추적되므로 전혀 영향받지 않음. 저장소 현황에도 "(일시중지됨)" 표시.
- **여유 공간 확보량(GB) 설정 추가** — 하드코딩됐던 100MB 여유분을 관리자가 직접 설정 가능한 값(기본 5GB)으로 변경. 백업 등 다른 용도로 남겨둘 공간을 확보하기 위한 목적 (이 값 밑으로 여유공간이 줄면 다음 순위 경로로 자동 전환).
### 알려진 제한 사항 (추가)
- S3/FTP는 용량 초과를 사전에 알 방법이 마땅치 않아 업로드 실패 시점에만 감지됨(사후 감지). 실패가 쌓였을 때 관리자에게 능동적으로 알림을 띄우는 기능은 아직 없음 — 매핑 목록에서 `failed`/`delete_failed` 건수를 수동으로 확인해야 함. (다음 버전 후보)
## v1.0.1 (2026-07-22)
### 중요 수정
- **저장소 종류를 바꾸면 기존 파일이 깨지던 버그 수정**: 파일 삭제/다운로드/게시글 이미지 서빙 트리거들이 "그 파일이 실제로 이동된 저장소"가 아니라 "현재 관리자 화면에 활성화된 저장소"를 기준으로 드라이버를 만들고 있었음. 예를 들어 로컬로 이동해둔 파일이 있는 상태에서 테스트 목적으로 S3로 활성 저장소를 바꾸면, 그 로컬 파일들의 삭제/다운로드/이미지 표시가 (엉뚱하게 S3 API로 시도되어) 실패하는 문제가 있었음. `StorageDriverFactory::create()`에 저장소 종류를 명시적으로 넘기도록 수정하고, 각 트리거는 매핑 테이블에 기록된 파일별 `storage_type`을 사용하도록 고침. 이제 활성 저장소를 자유롭게 바꿔가며 테스트해도 기존에 이동해둔 파일들은 영향받지 않음.
## v1.0.0 (2026-07-22)
첫 출시. 애드온 `lua_external_file`(첨부파일 외부로)을 참고만 하고 모듈로 완전히 새로 설계.
### 핵심 기능
- 저장소 4종 지원: 사용 안 함(기본, 서버 로컬 그대로) / 로컬·마운트 경로(NAS 등) / S3 호환 오브젝트 스토리지(SigV4 직접 구현, SDK 의존성 없음) / FTP(PHP 내장 함수만)
- 업로드 직후 자동 이동 (`file.insertFile` 트리거), 다운로드 시점 이동 없음 — 네트워크 지연 방지
- 파일 삭제 시 원격 저장소에서도 자동 삭제 (`file.deleteFile` 트리거), 실패 시 `delete_failed` 상태로 남아 관리자가 재시도 가능
- 썸네일은 항상 로컬 보관, 원본만 원격 이동 — 게시판/목록 조회는 원격 저장소를 거치지 않음
- 관리자 화면: 저장소 설정 + 연결 테스트(AJAX) / 매핑 목록 조회·검색(상태·저장소별) / 레거시(모듈 설치 이전 파일) 일괄 마이그레이션
### 보안
- 첨부파일 정적 서빙 폴백(`moduleHandler.init` 트리거)과 다운로드 버튼(`file.downloadFile` 트리거) 양쪽에 `FileModel::isDownloadable()` + 게시글/댓글 읽기 권한(`isAccessible()`) 체크 적용 — 원격 이동된 파일이 권한 체크를 우회해서 노출되는 문제 방지
- S3는 signed URL 기본, 공개 URL(public bucket) 선택 시 관리자 화면에 경고 문구 표시
- 경로 순회(`../`) 방지용 정규식 검증 (정적 서빙 폴백 경로)
### 이번 버전에서 발견/수정된 버그
- 저장소 설정 저장 시 `error_return_url`에 실존하지 않는 함수(`getRequestUriByToken`)를 써서 저장 후 404 나던 문제 → `getRequestUriByServerEnviroment()`로 수정
- 매핑 목록 하단 페이지네이션에서 `PageHandler::printNavigation()` 호출(존재하지 않는 메서드) → 직접 페이지 링크 렌더링으로 교체
- 매핑 목록 페이지 번호 반복에 `<!--@foreach(range(...) as $i)-->` 사용 시 템플릿 컴파일러가 공백에서 표현식을 잘라먹어 ParseError 발생 → `<!--@for(...)-->` 방식으로 교체
- module.xml의 관리자 액션에 `class="admin"` 속성을 잘못 사용해 "클래스를 찾을 수 없습니다" 에러 발생 → 해당 속성 제거(액션명에 "Admin"이 들어가면 이름만으로 자동 라우팅됨)
- 다운로드 버튼(`procFileDownload` → `procFileOutput`)이 로컬 파일 존재 여부만 검사해서, 원격으로 이미 이동된 파일은 다운로드가 실패하던 문제 → `file.downloadFile`(before) 트리거를 추가해 다운로드 버튼 경로에서도 원격 스트리밍/리다이렉트 처리
- 관리자 화면에 "설정" 탭만 사이트맵에 연결되고 "매핑 목록/레거시 정리" 탭은 어디에도 링크되지 않아 접근 불가능했던 문제 → 두 화면 상단에 상호 이동 링크 추가
- FTP 확장 모듈이 없는 호스팅 환경 대비 `function_exists('ftp_connect')` 가드 추가 (없으면 친절한 안내 메시지)
### 이번 사용자 환경 한정 기능 (범용 배포 시 제거 검토 대상)
- `procBsplus_external_storageAdminImportLegacyAddon`: 구버전 애드온(`lua_external_file`)이 남긴 이동 기록(`xe_lef_moved_files`)을 파일 재전송 없이 DB 기록만 그대로 새 매핑 테이블로 복사하는 일회성 기능. 그 애드온을 써본 적 없는 사이트에서는 해당 테이블 자체가 없어 자동으로 비활성 상태(버튼 노출 안 됨)이므로 다른 사용자 배포 시에도 부작용은 없음. 다만 이 모듈 고유 기능이 아니라 "그 사람만을 위한" 마이그레이션 도우미이므로, 필요 없어지면 `procBsplus_external_storageAdminImportLegacyAddon` 메서드, 관련 쿼리 XML, model의 `hasLegacyAddonTable`/`getLegacyAddonMapList`, list.html의 안내 박스를 통째로 제거해도 무방.
### 알려진 제한 사항
- S3 signed URL 다운로드는 원본 파일명(Content-Disposition) 강제를 지원하지 않음 — 브라우저가 URL의 키 이름으로 저장할 수 있음(FTP/로컬 경로는 원본 파일명으로 강제 다운로드됨)
- 다중 저장소 프로파일 미지원 (사이트 전체 단일 활성 저장소만 가능 — 설계상 의도된 단순화)
- WebDAV/SFTP 미지원 (설계 범위 제외)
- 언어 파일이 한국어(`ko.php`)만 있음 — 해외 배포 고려 시 `en.php` 등 추가 필요
### 라이선스
- GPLv2 (Rhymix 코어 및 다른 BSplus 모듈과 동일한 라이선스) — `LICENSE` 파일 참고
### 지원 환경
- **PHP**: 7.4 이상 (Rhymix 공식 최소 요구사항과 동일, `common/constants.php`의 `__XE_MIN_PHP_VERSION__` 기준). 8.2 이상 권장(Rhymix 공식 매뉴얼 권장 사항). 상한선은 없음 — 8.0 전용/폐기 예정 문법을 쓰지 않아 정적 검사 기준 7.4~8.x(최신) 전 구간 호환.
- **Rhymix**: 2.0.0(2020-12-18 출시) 이상 권장. `Rhymix\Framework\Storage`/`MIME` 네임스페이스 클래스와 `moduleHandler.init`/`file.downloadFile`/`file.insertFile`/`file.deleteFile` 트리거 훅포인트에 의존하는데, 이들은 2.0.0 이후 버전엔 확실히 존재함(라이믹스 module.xml/info.xml에는 "최소 코어버전" 선언 필드 자체가 없어 공식 문서로 더 정확한 하한을 확정할 수는 없었음). 실제 설치·테스트는 2.1.35에서 진행.



s3 테스트 연결 성공