원본 애드온 : 첨부파일을 외부로! ver. 0.0.3
패치 파일 : lua_external_file-v0.3.0.zip
2차 다운로드 : lua_external_file-v0.4.0.zip
3차 다운로드 : lua_external_file-v0.5.0.zip
4차 다운로드 : lua_external_file-V0.7.1.zip
5차 다운로드 : lua_external_file-v0.7.2.zip
6차 다운로드 : lua_external_file-v0.8.0.zip
============================ 패치 이력 ======================
# lua_external_file 변경 이력
## v0.8.0 (2026-07-21) — 자동 이동 (다운로드 없이도 이동)
### 배경
지금까지는 "누군가 다운로드를 눌러야만" 파일이 이동됐음. 그런데 애초에
이 애드온의 목적이 웹호스팅 용량 절약인데, 아무도 안 열어보는 오래된
글의 첨부파일은 영원히 로컬에 남아있어서 목적에 안 맞는다는 지적을 받음.
### 해결 — 워드프레스 wp-cron 스타일의 의사(疑似) 크론
서버에 별도 크론 작업을 등록할 필요 없이, 방문자 요청 중 낮은 확률로
걸린 요청 하나에 편승해서 배치 이동을 실행하는 방식을 택함.
- **`libs/LefMover.php` 신규**: "파일 하나를 지금 설정된 백엔드로 이동"하는
핵심 로직을 여기로 뽑아냄. 기존엔 이 로직이 `procFileDownload` 처리
코드 안에만 있었는데, 다운로드 트리거와 자동 이동이 똑같은 로직을
공유하도록 분리 (다운로드 트리거 쪽도 이걸 호출하도록 리팩터링 —
동작은 동일, 코드 중복 제거)
- **`libs/LefAutoMover.php` 신규**: `before_module_init`마다 호출되지만,
실제로 배치를 도는 건 (a) 100분의 2 확률 게이트를 통과하고 (b) 마지막
실행 이후 최소 5분이 지났을 때뿐. 업로드 후 N일이 지났고 아직 매핑
테이블에 없는 파일을 오래된 순으로 최대 M개(관리자 설정) 골라
`LefMover::moveOne()`으로 이동시킴
- **관리자 설정에 3개 필드 추가**: 자동 이동 사용 여부(기본 꺼짐),
최소 경과일(기본 7일), 배치 크기(기본 10, 최대 50)
### 트레이드오프 (사용자에게 고지 필요)
서버 크론 없이 동작하는 대신, 자동 이동이 실제로 도는 순간에 걸린
방문자의 요청 하나가 이동 작업(특히 FTP/WebDAV/S3/SFTP처럼 네트워크
왕복이 있는 백엔드)만큼 느려질 수 있음. 배치 크기를 작게 유지하면
영향이 작음. 즉시 반영이 아니라 "언젠가 곧" 이동되는 방식이라, 급하게
용량을 확보해야 하면 기존 "다운로드 시 이동"이 더 빠를 수도 있음
(다운로드는 매번 즉시 이동을 트리거하므로).
### 검증
코드 리뷰 + 문법 검사 완료. 자동 이동은 기본값이 "사용 안 함"이라
기존 사용자에게는 아무 영향 없음 (opt-in).
---
## v0.7.2 (2026-07-21) — 정적 서빙 폴백에 게시판 읽기 권한 체크 추가
### 배경
사용자 지적: 파일을 S3/FTP/WebDAV/SFTP처럼 라이믹스가 아니면 접근이 아예
불가능한 저장소로 옮기고 나면, 그 파일에 대한 유일한 접근 경로가
`serve_file.php`의 정적 서빙 폴백인데 여기엔 권한 체크가 전혀 없었음.
다운로드 버튼(`procFileDownload`)은 항상 `FileModel::isDownloadable()`을
거치지만, 게시글 본문에 박힌 `<img>` 태그가 트리거하는 이 폴백 경로는
비어있었음.
(원래 라이믹스 자체도 `direct_download='Y'` 파일은 Apache가 PHP도 안 거치고
정적으로 서빙해서 이 지점엔 원래부터 권한 체크가 없었음 — 해시 파일명이라는
"난독화"에만 의존하는 구조. 이 애드온이 새로 만든 구멍은 아니지만, 저작권
민감 게시판처럼 진짜 접근 제어가 필요한 경우엔 문제가 될 수 있음.)
### 해결
- `serve_file.php`에 `LefServe::isPermitted()` 추가. 매핑 테이블에서
file_srl을 알 수 있는 경우(=v0.7.0 이후 방식으로 이동된 파일)에 한해:
- `FileModel::isDownloadable()` (isvalid, download_groups 설정) 확인
- 첨부파일이 걸린 게시글(`DocumentModel::getDocument()->isAccessible()`)
또는 댓글의 읽기 권한 확인
- 권한이 없으면 스트리밍을 거부하고 코어 404 처리로 넘김
- 레거시(마이그레이션 안 된) 데이터는 file_srl을 알 수 없어 이 체크를
건너뜀 — 관리자 복구 탭의 "레거시 항목 정리"로 매핑 테이블에 올리면
이 파일들도 체크 대상이 됨
### 한계 (코드로 해결 불가능, 운영 정책으로만 대응 가능)
S3 `public_url`(공개 버킷)이나 익명 읽기가 열린 WebDAV처럼 **제3자 도메인이
직접 서빙**하는 방식은 요청이 라이믹스 서버를 아예 거치지 않으므로, 이번
수정과 무관하게 여전히 권한 체크가 불가능함. S3는 서명 URL(signed URL)로
완화 가능하지만 FTP/일반 WebDAV는 그런 개념 자체가 없어 근본적으로 막을
방법이 없음. 접근 제어가 중요한 게시판(저작권 민감 게시판 등)은
public_url/공개 WebDAV를 쓰지 말고, 서명 S3나 비공개 FTP/SFTP/WebDAV만
사용할 것 — 이건 코드가 아니라 운영 시 선택의 문제.
---
## v0.7.1 (2026-07-21) — unbuffered 쿼리 충돌 버그 수정
### 문제
관리자 복구 탭에서 "🔧 레거시 항목 정리"를 누르면 JSON 대신
`Unexpected token '<'` 오류가 떴음. 실제로는 PHP가 다음 치명적 오류를
뱉고 있었음:
```
PDOException: SQLSTATE[HY000]: General error: 2014 Cannot execute queries
while other unbuffered queries are active.
```
### 원인
라이믹스는 메모리 절약을 위해 모든 DB 연결을 **unbuffered 모드**로 연다
(`common/framework/db.php`, `PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = false`).
이 모드에서는 SELECT 결과를 끝까지 fetch하거나 `closeCursor()`로 명시적으로
닫기 전까지는 같은 커넥션으로 다른 쿼리(prepare/exec 포함)를 실행할 수 없다.
v0.7.0에서 새로 만든 `libs/LefMovedFiles.php`의 조회 메서드(`get()`,
`getByRelPath()`)가 한 행만 `fetch()`하고 커서를 닫지 않은 채로 반환하고
있었고, `admin/restore.php`의 `migrateLegacy()`는 SELECT 커서를 열어둔
채로 곧바로 UPDATE용 `prepare()`를 호출하고 있었음. 둘 다 이 규칙을
어긴 것.
이건 "레거시 항목 정리" 버튼만의 문제가 아니라 **이동된 파일을 다운로드·삭제·
썸네일 조회할 때마다 잠재적으로 터질 수 있는 문제**였음 — 매 요청마다
`LefMovedFiles::get()`을 먼저 호출한 뒤 곧바로 라이믹스 코어의
`executeQuery('file.updateFileDownloadCount', ...)` 등을 이어서 호출하는
구조라서, 커서가 안 닫힌 상태로 남아있으면 그 다음 쿼리가 바로 죽는 구조였음.
실사용 중 관리자가 직접 재현해줘서 배포 전에 발견함.
### 해결
- `LefMovedFiles::get()` / `getByRelPath()` / `insert()` / `delete()` / `all()`
전부 fetch 직후 `closeCursor()`를 명시적으로 호출하도록 수정
- `admin/restore.php`의 `migrateLegacy()`는 SELECT 결과를 `fetchAll()`로
먼저 전부 메모리에 읽어와 커서를 닫은 뒤에야 UPDATE를 준비/실행하도록 수정
- `fetchMovedFiles()`처럼 `while(fetchObject())`로 끝까지 다 읽는 기존 루프는
이미 정상적으로 커서가 닫히는 패턴이라 문제없음 (수정 불필요, 점검만 함)
---
## v0.7.0 (2026-07-21) — 매핑 테이블 방식으로 전환 (uploaded_filename 무결성 보장)
### 배경
실사이트에서 발견된 버그: `storage_type=local`로 웹루트 바깥(`I:\new_alldata\...`)에
파일을 이동시킨 뒤, 관리자 "콘텐츠 > 파일" 목록의 미리보기 썸네일이 안 뜨고,
게시글 본문 이미지도 `about:blank#blocked`로 깨지는 현상이 확인됨.
원인을 추적해보니 `modules/file/tpl/file_list.html`처럼 라이믹스 코어 여기저기서
`uploaded_filename`을 "그냥 웹 URL"로 가정하고 `<img src="{$val->uploaded_filename}">`
형태로 직접 사용하는 곳이 있었음. v0.3.0~v0.6.0까지는 파일을 이동시키면서
이 값 자체를 실제 절대경로(`I:\new_alldata\...`)나 `lef://type/relPath` 마커로
덮어썼는데, 둘 다 유효한 URL이 아니라서 브라우저가 요청 자체를 못 보냄
(콜론이 포함된 절대경로는 존재하지 않는 URL 스킴으로 오인되어 크롬이 차단).
이 문제는 로컬 백엔드만이 아니라 FTP/WebDAV/S3/SFTP 전부 동일하게 겪는
구조적인 문제였음 — `lef://` 마커도 마찬가지로 유효한 URL이 아니기 때문.
CDN(공개 URL) 방식으로 우회하는 것도 검토했지만, HTTP가 아예 아닌 FTP/SFTP는
이 방식 자체가 불가능해서 백엔드에 따라 되고 안 되고가 갈리는 반쪽짜리 해결책이라
채택하지 않음.
### 해결 — uploaded_filename을 절대 건드리지 않는다
- **`libs/LefMovedFiles.php` 신규**: 애드온 전용 매핑 테이블
(`{prefix}lef_moved_files`: file_srl / rel_path / storage_type / regdate)을
두고, "이 file_srl이 어느 저장소로 옮겨졌는지"만 여기에 기록. `files.uploaded_filename`은
이동 여부와 무관하게 항상 원래 상대경로(`./files/attach/...`) 그대로 유지됨
- 그 결과 관리자 파일목록, 게시글 본문, RSS 등 라이믹스 코어의 그 어떤 화면에서
`uploaded_filename`을 그대로 써도 항상 정상적인 URL이 유지됨 — 5개 저장 백엔드
전부 동일하게 해결됨 (local뿐 아니라 ftp/webdav/s3/sftp 포함)
- 다운로드(`procFileDownload`)·정적 서빙 폴백(`serve_file.php`)·삭제 연동·썸네일
폴백(`libs/LefHooks.php`) 전부 이 매핑 테이블을 1순위로 조회하도록 변경
- **레거시 하위호환 유지**: v0.3.x~v0.6.x 방식(마커/절대경로가 `uploaded_filename`에
직접 들어있는 기존 데이터)은 매핑 테이블에 기록이 없을 때만 폴백으로 계속 인식됨.
데이터 마이그레이션 없이도 기존에 이동된 파일은 계속 정상 동작
- **관리자 복구 탭에 "🔧 레거시 항목 정리" 버튼 추가**: 기존에 v0.6.0 이전 방식으로
이동되어 이미 화면이 깨져있는 항목들을, 파일은 옮기지 않고 DB 기록만
매핑 테이블로 이관 + `uploaded_filename`을 원래 상대경로로 복원. 실행 즉시
관리자 파일목록/게시글 본문의 깨진 이미지가 정상화됨
- 복구(restore) 탭도 신규 매핑 테이블 항목을 스캔/복구하도록 확장. 신규 방식은
`uploaded_filename`이 애초에 안 바뀌어 있으므로 복구 시 DB 갱신이 필요 없고
매핑 기록만 지우면 끝나서 레거시 방식보다 더 단순/안전함
### 부수 수정 — LefStorageInterface에 `copyTo()` 추가
기존 `get()`은 복구 전용이라 원본을 항상 삭제하는데(이동 의미), v0.6.0에서 추가한
썸네일 폴백 생성 로직이 실수로 이 `get()`을 재사용하고 있어서 "썸네일 한 번
보려고 열람했는데 원본이 로컬로 옮겨지고 원격에서 삭제되는" 심각한 데이터
유실 버그가 될 뻔했음. 개발 중 발견해 5개 드라이버 전부에 원본을 보존하는
`copyTo()`를 별도로 추가하고 썸네일 폴백은 이걸 쓰도록 수정함 (실사용 전 발견,
실제 데이터 유실은 없었음).
### 참고
- 코드 리뷰 + 문법 검사 완료. 로컬 백엔드는 실제 이동/복구/삭제 흐름을 실사이트에서
재검증함. FTP/WebDAV/S3/SFTP는 여전히 실제 서버가 없어 코드 리뷰까지만 완료 —
실사용 전 연결 테스트로 먼저 확인할 것
---
## v0.6.0 (2026-07-21) — 삭제 연동 + 썸네일 폴백
### 배경
v0.5.0까지는 원본 파일의 이동/서빙만 챙기고, "이동된 파일이 지워질 때"와
"이동된 파일로 목록/위젯 썸네일을 새로 만들어야 할 때"는 다루지 않았음.
코어 동작을 그대로 따라가 보니 실제로 두 가지 구멍이 있었음:
- **삭제 미연동**: `FileController::deleteFile()`은 `uploaded_filename`이
`lef://type/relPath` 마커인 경우 실제 경로로 취급하지 못해 삭제를 조용히
건너뛰고 DB 행만 지움 → 외부 저장소(FTP/WebDAV/S3/SFTP)에 파일이 영원히
남아 쌓이는 스토리지 누수 발생
- **썸네일 미생성**: 게시글 목록/위젯 썸네일은 처음 요청될 때 딱 한 번 생성되어
캐시되는데, 그 전에 원본이 먼저 외부로 이동되고 업로드 당시 만들어지는
작은 미리보기(`thumbnail_filename`)도 없는 경우 코어가 소스 이미지를 찾지
못해 에러 없이 그냥 썸네일 없이 넘어감
### 해결 — 라이믹스 코어 트리거 활용 (`libs/LefHooks.php` 신규)
- **`file.deleteFile`(before) 트리거 등록**: 파일이 실제로 지워지기 직전에
가로채서, `lef://` 마커인 경우 해당 저장소 드라이버의 `delete()`를 호출.
이 트리거는 직접 삭제(`procFileDelete`)든 게시글/댓글/모듈 삭제로 인한
첨부파일 연쇄삭제든 전부 거쳐가는 코어의 단일 관문이라, act 이름을 일일이
따지지 않아도 모든 삭제 경로가 자동으로 커버됨
- **`document.getThumbnail` / `comment.getThumbnail`(before) 트리거 등록**:
썸네일 캐시가 없고 로컬에 후보(자체 미리보기·이동 전 원본)도 전혀 없을 때만
개입 — 설정된 저장소 드라이버로 원본을 임시 다운로드해 그 자리에서
`FileHandler::createImageFile()`로 썸네일을 생성. 이후로는 코어 캐시 메커니즘을
그대로 타므로 매번 외부에서 받아오지 않음(최초 1회만 비용 발생)
- **이동 전 로컬 미리보기 보장** (`LefHooks::ensureLocalThumbnail`): 원본을
외부로 이동시키기 직전에, 업로드 당시 만들어진 미리보기가 없는 이미지라면
이 애드온이 대신 작은 미리보기(500x500 ratio, jpg)를 만들어 로컬에 남겨둠.
`thumbnail_filename`은 이 애드온이 절대 이동시키지 않으므로, 원본이 이동한
뒤에도 목록/위젯 썸네일이 계속 이 미리보기를 소스로 정상 생성됨 —
대부분의 케이스는 이 단계에서 이미 해결되고, 위 트리거 폴백은 그마저도
없는 드문 경우를 위한 마지막 안전망
### 참고
- 트리거 등록(`addTriggerFunction`)은 요청(request) 범위라 영구 등록이 아니며,
`before_module_init`에서 매 요청마다 다시 등록함 (비용은 클로저 3개 배열
추가뿐이라 무시할 수준)
- `storage_type = local`인 경우 두 트리거 모두 즉시 리턴 — 원본이 항상 로컬에
있으므로 코어 동작을 그대로 두는 것이 맞음 (로컬 백엔드 회귀 없음)
- 코드 리뷰 + 문법 검사 완료. 실사용 전 각 저장 방식의 "연결 테스트"로
먼저 확인할 것 (v0.5.0과 동일 원칙)
---
## v0.5.0 (2026-07-21) — 다중 저장소 지원 (FTP / WebDAV / S3 호환 / SFTP)
### 배경
웹호스팅(공유호스팅) 사용자는 로컬 마운트 경로를 쓸 수 없는 경우가 많아,
기존 "로컬/마운트 경로" 한 가지 방식만으로는 한계가 있었음. NAS나 외부
서버로 옮길 수 있는 방법을 다방면으로 확장.
### 추가된 저장소 드라이버 (`libs/`)
- **`LefStorageInterface`** — 모든 드라이버가 구현하는 공통 인터페이스
(`testConnection`/`exists`/`put`/`get`/`delete`/`stream`/`size`)
- **`LefStorageLocal`** — 기존 v0.3.x 로컬/마운트 경로 로직을 그대로 클래스화 (하위호환)
- **`LefStorageFtp`** — PHP 내장 `ftp_*` 함수만 사용, 별도 의존성 없음. FTPS 지원
- **`LefStorageWebdav`** — curl PUT/GET/DELETE/HEAD/MKCOL. 시놀로지·큐냅 NAS 등 호환
- **`LefStorageS3`** — AWS SDK 없이 Signature V4 서명을 직접 구현(curl만 사용).
AWS S3, Backblaze B2, Cloudflare R2, MinIO, NAS의 S3 게이트웨이 등과 호환.
공개 버킷이면 서명 없이 바로 리다이렉트하는 `public_url` 옵션도 지원
- **`LefStorageSftp`** — phpseclib3(Net\SFTP) 사용. 비밀번호/개인키(PEM) 인증 둘 다 지원.
다른 모듈에 의존하지 않도록 `vendor/phpseclib3`에 자체 번들(2.6MB, 경량 PSR-4
오토로더 직접 작성, composer 불필요)
### 통합 저장 방식 (하위호환 유지)
- `storage_type = local`(기본값): 기존과 완전히 동일하게 `uploaded_filename`에
실제 절대경로를 저장. 기존에 이미 이동된 파일들도 그대로 인식됨
- `storage_type`이 ftp/webdav/s3/sftp: `uploaded_filename`에
`lef://{type}/{relPath}` 통합 마커를 저장. 다운로드/서빙 둘 다 이 애드온이
끝까지 책임지고 처리(Rhymix 코어는 이 마커를 실제 경로로 오인하지 않음)
- `serve_file.php`(정적 서빙 폴백)도 팩토리 기반으로 바뀌어 5가지 저장 방식
전부에서 본문 이미지 깨짐 방지가 동일하게 동작
### 관리자 UI
- 설정 탭에 "저장 방식" 선택 시 해당 방식의 입력폼만 나타나도록 개선
- 저장 방식별 **🔌 연결 테스트** 버튼 — 저장하지 않고도 즉시 접속/쓰기 권한 확인
(비밀번호 노출 방지를 위해 POST로 전송)
- "적용 대상 모듈" 섹션을 `<details>`로 감싸 기본 접힘 상태로 변경
- 파일 복구 탭이 레거시 로컬 파일과 신규 `lef://` 마커 파일을 모두 스캔/복구하도록 확장
(단, 다른 저장 방식으로 이동된 파일은 그 방식으로 설정을 되돌린 뒤에만 복구 가능)
### 검증
- 로컬 백엔드: 기존 이동 파일 다운로드, procFileDownload 정식 다운로드,
본문 이미지 폴백 서빙 모두 실사이트에서 재검증하여 회귀 없음 확인
- FTP/WebDAV/S3/SFTP: 실제 서버/계정이 없어 코드 리뷰 + 문법 검사까지만 완료.
실사용 전 반드시 각 저장 방식의 "연결 테스트" 버튼으로 먼저 확인할 것
---
## v0.4.0 (2026-07-21) — 이동된 첨부파일 정적 서빙 폴백
### 문제
게시글 본문에 삽입된 이미지(`<img src="/files/attach/images/...">`)는 Apache가
PHP를 거치지 않고 실제 파일을 직접 찾아 서빙하는 방식인데, 애드온이 첨부파일을
`storage_path`로 이동시키면 원래 자리엔 파일이 없어져서 이미지가 깨짐.
반면 "다운로드" 링크(`procFileDownload`)는 PHP가 DB의 최신 경로를 읽어 스트리밍하므로
문제없이 동작해서, "다운로드는 되는데 본문엔 안 뜨는" 증상으로 나타남.
확인 결과 이미 이미지 287개(전체 이동 파일 438개 중)가 이 상태로 깨져 있었음.
### 해결
- **`serve_file.php` 신규 추가** — `LefServe` 클래스, `admin/restore.php`와 동일하게
`require_once`로만 로드되어 함수/클래스 중복 선언 문제 없음
- **`before_module_init` 훅 확장** — `files/attach/` 경로 요청이 원래 자리에 파일이
없어서(.htaccess의 기존 catch-all 규칙에 의해) `index.php`로 흘러들어오면,
라우팅 검증 전에 가로채서 `storage_path`에서 파일을 찾아 바로 스트리밍
- **`.htaccess`는 전혀 수정하지 않음** — 라이믹스 코어 업데이트 시 `.htaccess`가
기본값으로 재생성될 수 있어서, 애드온 자체 훅으로만 처리하도록 의도적으로 설계.
기존에 있던 "파일 없으면 index.php로" catch-all 규칙(라이믹스 친화적 URL 라우팅의
핵심 동작이라 잘 안 바뀜)에 편승하는 방식이라 업데이트에도 안전함
- Path traversal 방지(`..` 차단 + 문자 화이트리스트 + realpath 기반 storage_path
경계 재검증), 이미지/영상/PDF 등 확장자별 Content-Type 매핑, 캐시 헤더 포함
- 실제 이동된 이미지로 실사이트 검증 완료 (200 OK, 정상 스트리밍 확인)
---
## v0.3.0 (2026-06-24) — BSplus 전면 리빌드
원본 LuaCast 버전(2015, PHP 5.x / XE 구버전)을 PHP 7.4~8.x + Rhymix 2.1+ 환경에 맞게 전면 재작성.
### 핵심 기능 추가
- **폴더 지정 방식 도입**: 관리자가 저장 폴더 경로를 직접 설정 (원본은 코드에 하드코딩)
- **절대경로 + 상대경로 모두 지원**: `/mnt/nas` 같은 절대경로와 `../../private_uploads` 같은 XE 루트 기준 상대경로 모두 동작
- **CDN 서빙 URL 옵션**: 이동된 파일을 외부 CDN URL로 리다이렉트 (Rhymix 권한 체크 우회용)
- **복구 기능 추가**: 이동된 파일 전체를 원래 XE 경로로 되돌리는 관리자 기능
- AJAX 방식의 파일 목록·선택 복구·전체 복구 UI
- 애드온 설정 페이지 내 탭으로 통합 (별도 페이지 불필요)
### 버그 수정 / 호환성
- **크로스 드라이브 이동 실패 해결**: Windows에서 `rename()`이 드라이브 간 이동 시 실패하는 문제 → `copy()` + `unlink()` 폴백 적용 (파일 이동·복구 양쪽 모두)
- **DB 이중 prefix 버그 수정**: `DB::getInstance()->query()` 사용 시 `xe_xe_files` 같은 이중 prefix 발생 → raw PDO(`getHandle()`) + `config('db.master.prefix')` 방식으로 교체
- **Rhymix 라우팅 오류 해결**: `lua_ext_restore` 액트가 카멜케이스 검증에 걸려 `msg_invalid_request` 발생 → `before_module_init`에서 라우팅 검증 이전에 가로채도록 변경
- **함수 재선언 오류 방지**: 애드온이 position별로 여러 번 include 되는 Rhymix 특성 → 관리자 로직을 별도 클래스(`LefAdmin`)로 분리 + `require_once` 적용
- **CDN URL 헤더 인젝션 방지**: 개행 문자(`\r\n`) 제거 처리 추가
### 구조 변경
- **관리자 설정 UI**: `after_module_proc` 훅에서 `setTemplatePath()`·`setTemplateFile()`로 기본 설정 템플릿을 커스텀 탭 UI로 교체
- **복구 로직 분리**: `admin/restore.php` (`LefAdmin` 클래스)로 독립 — AJAX scan / restore / restore_one 엔드포인트 통합
- **보안**: DB 접속 비밀번호 하드코딩 코드 제거
---
## v0.1.x (2015) — LuaCast 원본
- XE 1.x / PHP 5.x 기반
- 첫 다운로드 시 파일을 지정 경로로 이동하는 기본 기능
===================================================================================


장점 : 자신의 홈페이지 용량이 늘지 않습니다.
첨부파일이 전부 정해진 저장공간에 쌓입니다.
파일 복구 탭은 파일을 다시 홈페이지 원래 방식대로 돌리는 기능입니다.

