Rhymix 애드온 lua_external_file을 참고만 하고 모듈로 전면 재설계 — 첨부파일을 로컬 마운트/S3 호환 스토리지/FTP로 자동 이동시켜 서버 용량을 절감
consolidate.html을 공유해주심 — 기존 네이티브 <select multiple>(Ctrl/Cmd+클릭 다중 선택)이 특히 모바일/터치 환경에서 다중 선택이 사실상 불가능했던 문제를 해결한 패치. 체크박스 그리드 + "전체 게시판" 마스터 체크박스(클릭 시 전체 선택/해제 토글) 방식으로 교체 — 폼 필드명은 그대로라 백엔드 수정 없이 그대로 적용됨.UPDATE xe_files SET thumbnail_filename = ''로 코어 컬럼을 직접 지워서 우회 — 이 방식은 실제로 원격 이전된(로컬 원본이 이미 없는) 파일에는 위험하지만, 이 리포트를 계기로 재생성 버튼의 진짜 결함을 발견함. 후보 조회 쿼리가 "이전됨 + 썸네일 있음"만 조건으로 걸었는데, 재생성에 성공해도 썸네일은 여전히 "있는" 상태라 이 조건에서 절대 빠지지 않아 매번 map_srl 오름차순 맨 앞 30건만 반복 처리하고 뒤에 있는 파일은 버튼을 아무리 눌러도 처리되지 않는 구조였음. thumb_fixed 컬럼을 추가해 재생성 성공한 항목을 표시하고 후보 조회에서 제외 — 이제 반복해서 누르면 뒤에 있는 파일들로 순서대로 넘어감.document.item.php의 getThumbnail()) 화면에 보이는 썸네일은 항상 thumbnail_filename(이 모듈이 만든 로컬 미리보기)을 게시판 설정의 크롭 타입으로 다시 리사이즈한 결과라, "원본 파일 그 자체"가 그대로 표시되는 경로가 코어에 없다는 걸 확인 — 대신 "잘라내지 않고 비율 그대로(레터박스)" 표시하는 옵션을 신규 추가. 관리자 설정에 체크박스 추가(기본 꺼짐), 신규 트리거 document.getThumbnail/comment.getThumbnail(before)로 이전된 첨부파일이 있는 글/댓글의 크롭 타입만 골라 'ratio'로 강제.files/thumbnails(코어의 표시 캐시)만 지우고 재시도해도 여전했던 이유는, 이 모듈이 만들어둔 원본 미리보기 자체가 옛날(레터박스) 방식 그대로 남아있었기 때문. ensureLocalThumbnail()은 thumbnail_filename이 이미 있으면 건드리지 않고 넘어가는 구조라 자동 복구가 안 됐음. [매핑 목록 / 레거시 정리] 탭에 "기존 썸네일 재생성 (여백 문제 수정)" 버튼 신규 추가 — 기존 미리보기 삭제 + DB 값 비움 → 원격 원본 재다운로드 → v1.9.5 새 로직으로 재생성. 한 번에 최대 30건.ensureLocalThumbnail()이 만드는 로컬 미리보기가 최종 썸네일이 아니라 라이믹스 코어가 실제 썸네일을 만들 때 "원본 대신 읽는 소스"인데, 이걸 고정 500×500 정사각형에 레터박스(여백) 모드로 만들고 있었던 것 — 원본이 정사각형이 아니면 흰 여백이 미리보기 자체에 박혀서, 코어가 나중에 "크롭"으로 다시 잘라도 그 여백까지 함께 잘려 들어감. 고정 정사각형 대신 원본 비율을 유지한 채 긴 변만 최대 1600px로 축소하도록 변경 — 어떤 모드로 다시 리사이즈되든 여백·왜곡 없이 정상 동작.FileModel::getFile()로 파일 정보를 조회해 썸네일 생성을 호출하도록 추가.map_srl 오름차순으로 가장 오래된 300건만 매번 조회하고 오프셋도 없었으며, "썸네일 없음" 조건도 SQL이 아니라 PHP에서만 걸렀던 것 — 그 결과 전체 1800건 중 처음 300건만 영원히 반복해서 들여다보는 구조였고, 301번째 이후 파일은 이 버튼으로 애초에 도달할 방법이 없었음. getBsplus_external_storageMigratedImageList.xml에 "썸네일 없음"과 "이미지 파일만(mime_type)" 조건을 SQL 단으로 옮겨, 성공적으로 고친 파일이 다음 조회부터 자동으로 빠지고 뒤 순번 파일이 자연스럽게 올라오도록 수정. 덧붙여 "목표 저장소 파일을 수동으로 지워도 매핑 목록이 안 바뀌는" 현상은 버그가 아니라 설계상 의도된 동작(원격 파일 수천 개의 실시간 존재 확인은 너무 느려서 매핑 테이블 기록만 신뢰)임을 확인해 안내.thumbnail_filename이 비어있는 이미지는 게시판 목록에 처음 표시할 때 원본을 읽어 그 자리에서 썸네일을 생성하는데, 이 모듈이 원본을 원격 이동 직후 로컬에서 지워버려서 아직 한 번도 화면에 안 뜬 이미지는 그 생성이 조용히 실패하던 것 — 코드 주석엔 "썸네일은 항상 로컬에 유지"라고 적혀 있었지만 실제로 그걸 보장하는 코드가 없었음(구버전 애드온의 방식만 주석으로 옮기고 실제 로직은 이식 안 함). 원본 이동 직전 로컬 미리보기를 만들어두는 ensureLocalThumbnail()을 구버전 애드온에서 이식해 3곳(업로드 트리거·레거시 마이그레이션·데이터 통합)에 적용. 이미 이동되어 이미 망가진 이미지를 위한 "누락된 썸네일 생성" 백필 도구도 추가. 최종 검토에서 이 수정이 처음엔 core files 테이블을 raw DB 쿼리로 직접 갱신하고 있었는데(다른 모듈의 실제 이중 접두어 버그 사례와 유사한 패턴으로 지적받음 — 조사 결과 이 코드 자체는 안전했지만) 일관성을 위해 정식 쿼리 XML 방식으로 교체.__XE__ 가드가 없었는데, 확인해보니 라이믹스 코어 루트 .htaccess가 modules/ 안의 일반 .php 파일 직접 실행까지는 막아주지 않는 구조라 그 경로를 직접 요청하면 부트스트랩 없이 실행될 수 있었음. 339개 파일 각각에 가드를 넣는 대신(업스트림 라이브러리라 건드리면 이후 버전업 시 diff가 꼬임) vendor/.htaccess로 그 디렉토리 전체의 외부 HTTP 접근을 차단(PHP 내부 require/오토로더 로딩에는 영향 없음). 이 항목 외 재검토에서 추가로 발견된 실제 결함은 없음(나머지는 관리자 액션 파일 위치 관련 휴리스틱 오탐 12건과 vendor 라이브러리 내 기존 오탐들).vendor/에 번들해 서버에 ssh2 확장 모듈이 없어도 동작하도록 구현, 계정 1개만 지원(SSH·SFTP는 보통 같은 계정을 쓰므로). 우선순위 슬롯도 7단계 → 8단계로 확장. ② "이 게시판 전체를 저 저장소로 모아달라"고 실행해도 실제로는 안 됐던 문제 — getConsolidationCandidates가 이미 매핑된 파일만 대상으로 삼고, 모듈 설치 이전부터 있던 매핑 없는 레거시 파일은 건드리지 못하던 구조적 갭이었음. 이미 매핑된 파일 처리 후 남는 처리 개수 한도 안에서 레거시 파일도 함께 이동하도록 수정.streamFile()/downloadToLocal()가 strpos($real, $realStorage) !== 0로만 경로 포함 여부를 확인하고 있어, $realStorage가 /data/storage일 때 /data/storage_evil/x 같은 형제 디렉터리도 prefix가 겹쳐 통과할 수 있는 구조였음. 현재는 $remotePath가 사용자 직접 입력이 아니라 매핑 테이블 값이라 실사용 위험은 낮지만, 향후 재사용 대비 사전 방어로 구분자 포함 비교 방식으로 교체. CLI로 정상 접근/traversal 차단 양쪽 다 직접 검증.catch(\Exception)로만 감싸여 있어 PHP의 Error/TypeError 계열은 놓치고 있던 것을 catch(\Throwable)로 방어 범위 확장. 점검 중 v1.7.2에서 고친 시나리오가 getMapByOriginalPath 쿼리의 status='migrated' 필터 덕에 애초에 그 경로로는 도달하지 않는다는 것도 재확인(그래도 "트리거가 끝까지 못 돌아 상태 갱신이 누락된" 케이스에는 여전히 유효한 방어). buildRemotePath()의 ltrim() 마스크 사용 방식도 더 정확한 방식으로 교체(현재 데이터로는 문제 된 적 없는 사전 예방적 수정).files 테이블 행을 즉시 지우지만, 원격(S3/FTP) 삭제가 실패해 delete_failed로 매핑이 남아있는 파일은 권한 체크 로직이 "레코드 없음 = 확인 불필요 = 허용"으로 잘못 판단해 삭제된 게시글의 첨부파일이 URL만 알면 계속 열람 가능했음. files 행이 없으면 항상 차단하도록 수정. 같은 점검에서 배지/안내문 카운트가 1000건을 넘으면 실제보다 적게 표시되던 부정확도 함께 수정.# 접두어로 즉시 일시중지 기능 추가.
files 테이블 join) 집계 모델 함수 getStorageUsageStats() 추가.bsplus_storage_widget — 스킨 2종(원형 도넛 게이지 / 가로 막대형)으로 사이트에 실제 설정된 저장소만 자동 나열. 모듈이 설치되지 않은 사이트에 위젯만 공유되는 경우를 대비해 is_dir() 물리 확인 후에만 모델을 호출하도록 방어.validate_query_xml 통과, 신규 PHP 파일 check_php_syntax 통과. review_module이 위젯 디렉토리에 "conf/module.xml 누락"을 오탐으로 보고했으나, 위젯은 모듈과 달리 conf/info.xml만 있으면 되는 정상 구조라 무시.rename() 실패 시 copy()+unlink() 폴백, 기존 LocalDriver::upload()와 동일 패턴)한 뒤에만 원격 원본을 삭제하도록 순서를 지켜 데이터 유실을 방지. 레거시 파일(원래부터 원 서버에 있던 파일)은 이미 목표 위치에 있으므로 자연히 처리 대상에서 제외됨.in_array() 관련 경고 5건은 전부 이 패치 이전부터 있던, 값이 배열 리터럴로 미리 초기화된 known false-positive라 이 패치와 무관함을 확인.consolidate.html을 공유해주심. 기존 네이티브 <select multiple>(Ctrl/Cmd+클릭으로 다중 선택)이 특히 모바일/터치 환경에서 다중 선택이 사실상 불가능해 불편했던 부분을 해결한 패치.<select multiple>에서 체크박스 그리드 + "전체 게시판" 마스터 체크박스(클릭 시 전체 선택/해제 토글, JS) 방식으로 교체. 폼 필드명(consolidate_module_srl[])은 그대로라 백엔드(procBsplus_external_storageAdminConsolidate) 수정 없이 적용됨.UPDATE xe_files SET thumbnail_filename = ''로 라이믹스 코어 컬럼을 지워서 우회함. 그 방식 자체는 실제로 원격 이전된(로컬 원본이 이미 없는) 파일에는 위험하지만(코어가 대체 소스를 못 찾아 썸네일이 아예 안 뜨게 될 수 있음), 이 리포트를 계기로 재생성 버튼의 진짜 결함을 발견함.getBsplus_external_storageThumbnailedImageList.xml(재생성 후보 조회)은 "이전됨 + 썸네일 있음"만 조건으로 걸었는데, 재생성에 성공해도 썸네일은 여전히 "있는" 상태이므로 이 조건에서 절대 빠지지 않음. 그 결과 map_srl 오름차순으로 매번 똑같은 맨 앞 30건만 반복 재생성하고, 그보다 뒤에 있는(아직 안 고친) 파일은 버튼을 아무리 눌러도 영원히 처리되지 않는 구조였음.bsplus_external_storage_map 테이블에 thumb_fixed 컬럼 추가(기존 설치는 moduleUpdate()에서 자동 추가). 재생성 성공 시 해당 매핑을 thumb_fixed = 'Y'로 표시(updateBsplus_external_storageMapThumbFixed.xml 신규) + 후보 조회 쿼리에 thumb_fixed != 'Y' 조건 추가. 이제 한 번 고친 파일은 후보에서 자연히 빠지고, 버튼을 반복해서 누르면 뒤에 있는 파일들로 순서대로 넘어감.thumbnail_filename(이 모듈이 만든 로컬 미리보기)을 게시판 설정의 크롭 타입(crop/fill 등)으로 다시 리사이즈한 결과라, "원본 파일 그 자체"가 그대로 표시되는 경로가 코어에 없음을 확인. 대안으로 "잘라내지 않고 비율 그대로(레터박스)" 표시하는 옵션을 추가.document.getThumbnail/comment.getThumbnail(둘 다 before) → triggerOverrideThumbnailType(): 옵션이 켜져 있고 대상 글/댓글에 이전(migrated)된 첨부파일이 있으면 크롭 타입을 'ratio'로 강제. FileHandler::createImageFile()의 ratio 모드는 잘라내지 않고 긴 변 기준으로 축소만 하므로 전체 이미지가 항상 보임. 이전되지 않은(로컬에만 있는) 파일에는 관여하지 않음.ensureLocalThumbnail()이 thumbnail_filename이 이미 있으면 그냥 넘어가는 구조라서다. 팔공산 님이 코어의 표시 캐시(files/thumbnails)만 지우고 재시도했지만 여전했던 건, 이 모듈이 만들어둔 원본 미리보기 자체가 옛날 방식 그대로였기 때문으로 확인됨.procBsplus_external_storageAdminRegenerateThumbnails + 쿼리 getBsplus_external_storageThumbnailedImageList.xml 추가. [매핑 목록 / 레거시 정리] 탭에 "기존 썸네일 재생성 (여백 문제 수정)" 버튼 신규 추가.thumbnail_filename 비움 → 원격 원본을 다시 내려받아 v1.9.5의 새 로직(원본 비율 유지)으로 재생성. 한 번에 최대 30건(원격 다운로드 재수반이라 무거움), 실패한 항목은 기존 "누락된 썸네일 생성" 버튼으로도 재시도됨.getBoardStorageBreakdown() 추가.ensureLocalThumbnail()이 만드는 로컬 미리보기는 최종 화면에 뜨는 썸네일이 아니라, 라이믹스 코어(document.item.php의 getThumbnail())가 게시판 목록 등에서 실제 썸네일을 만들 때 "원본 대신 읽는 소스"로 쓰는 파일이다. 코어는 files.thumbnail_filename이 있으면 그걸 원본처럼 취급해 그때그때 요청된 크기/모드(크롭 포함)로 다시 리사이즈함. 그런데 이 미리보기를 고정 500×500 정사각형에 'ratio'(레터박스) 모드로 만들고 있었음 — 원본이 정사각형이 아니면 흰 여백이 미리보기 자체에 이미 박제되고, 코어가 나중에 이걸 "크롭" 모드로 다시 잘라도 그 흰 여백까지 함께 잘려 들어가 버림.ensureLocalThumbnail(): 고정 500×500 대신, 원본 비율을 그대로 유지한 채 긴 변만 최대 1600px로 축소(원본이 더 작으면 확대하지 않음)해서 저장하도록 변경. 목표 박스 크기 자체가 원본과 같은 비율이라 어떤 모드(크롭/레터박스/채우기)로 다시 리사이즈되든 왜곡·여백·잘못된 크롭 없이 정상 동작. 신규 업로드/레거시 마이그레이션/백필 버튼 전부 이 함수 하나를 공유하므로 별도 수정 없이 다 함께 고쳐짐.ensureLocalThumbnail()을 업로드 트리거·레거시 마이그레이션·①번 경로 3곳에 넣었는데 ②번 경로에는 빠져 있었음 — 이 경로가 다루는 map 레코드에는 애초에 files 테이블 정보(thumbnail_filename 등)가 없어서 검토 때도 놓친 부분.procBsplus_external_storageAdminConsolidate의 ②번 경로에서, 원격 원본을 임시 파일로 내려받은 직후 FileModel::getFile()로 해당 파일의 files 레코드를 조회해 ensureLocalThumbnail()을 호출하도록 추가. 임시 파일이 아직 로컬에 있는 시점(업로드 전)에 처리하므로 별도 다운로드가 필요 없음.procBsplus_external_storageAdminFixThumbnails가 후보를 가져올 때 쓰던 쿼리가 항상 map_srl 오름차순으로 가장 오래된 상위 300건만 조회하고, 오프셋도 없고 "이미 썸네일이 있는지" 조건도 SQL이 아니라 PHP에서만 걸렀음. 그 결과 전체 이전 파일(팔공산 님 기준 약 1800개) 중 처음 300개만 영원히 반복해서 들여다보는 구조였고, 그 안에서 고칠 수 있는 건 몇 번 클릭하면 끝나거나 영구 실패로 남고 그 뒤로는 매번 같은 300개를 훑으며 스킵/실패만 반복(그래서 지연 없이 효과가 없었던 것). 301번째 이후 파일은 이 버튼으로 애초에 도달 불가능했음.queries/getBsplus_external_storageMigratedImageList.xml: "썸네일 없음"(thumbnail_filename IS NULL OR '')과 "이미지 파일만"(mime_type LIKE 'image/%') 조건을 SQL 단으로 이동. 성공적으로 고친 파일은 다음 조회부터 자동으로 결과에서 빠지므로, 매번 새로운(뒤쪽 순번의) 후보가 자연스럽게 올라와 전체 목록을 끝까지 훑을 수 있게 됨.bsplus_external_storage.admin.controller.php: SQL이 이미 이미지+미보유만 걸러주므로 PHP 쪽 중복 필터(확장자 정규식, 빈값 체크)를 제거해 정리.thumbnail_filename을 비워두고, 게시판 목록 등에 처음 표시할 때 원본을 읽어 그 자리에서 썸네일을 생성함. 이 모듈이 원본을 원격 이동 직후 로컬에서 지워버려서, 아직 한 번도 화면에 표시된 적 없는 이미지는 그 생성이 조용히 실패했음. 코드 주석엔 "썸네일은 항상 로컬에 유지한다"고 적혀 있었지만 실제로 그걸 보장하는 코드가 없었던 게 근본 원인 — 구버전 애드온(lua_external_file)의 동작 방식만 주석으로 옮기고, 그걸 실제로 만드는 코드(ensureLocalThumbnail)는 이식하지 않았음.bsplus_external_storage.class.php에 ensureLocalThumbnail() 추가 — thumbnail_filename이 비어있는 이미지는 원본을 지우기 직전에 FileHandler::createImageFile()로 500x500 이하 미리보기를 별도 생성해 로컬에 남기고(이 미리보기는 절대 이동시키지 않음) DB를 갱신. 구버전 애드온의 LefHooks::ensureLocalThumbnail()과 동일한 접근. 원본을 옮기고 지우는 3곳(업로드 트리거, 레거시 마이그레이션, 데이터 통합의 레거시 파일 처리) 전부에 적용.procBsplus_external_storageAdminFixThumbnails). 원격 원본을 임시로 내려받아 미리보기만 만들고 임시 파일은 즉시 삭제, 한 번에 최대 50건.ensureLocalThumbnail()이 처음엔 core files 테이블을 DB::getInstance()->query()로 직접 갱신했는데, review_module이 "다른 모듈(slimmer)에서 실제 이중 접두어 버그가 났던 패턴"이라고 경고. 직접 조사한 결과 slimmer 버그는 SQL에 접두어를 수동으로 붙인 뒤 또 자동 접두어가 붙어 xe_xe_...가 된 경우였고, 이 코드는 수동 접두어를 안 붙이므로 실제로는 안전했음 — 그래도 일관성을 위해 신규 쿼리 updateBsplus_external_storageFileThumbnail.xml로 교체(동작 동일).vendor/phpseclib3 파일 339개가 __XE__ 가드 없이 직접 웹 접근 가능하던 문제. 클래스 정의 파일이라 가드가 없는 게 정상이지만, 라이믹스 코어 루트 .htaccess가 modules/ 안의 일반 .php 실행 자체는 막지 않아 그 경로를 직접 요청하면 부트스트랩 없이 실행될 수 있었음.vendor/.htaccess로 디렉토리 전체의 외부 HTTP 접근을 차단(Require all denied / 구버전 Apache용 Deny from all 병기). PHP 내부의 require/오토로더 로딩에는 영향 없음.review_module(module.xml↔실제 코드 교차검증 포함 심층 검토)로 재검증 — 이 항목 외 추가 발견된 실제 결함 없음.procBsplus_external_storageAdminConsolidate가 선택값을 리다이렉트 URL에 실어 넘기고 dispBsplus_external_storageAdminConsolidate가 그대로 읽어 화면에 반영.getBsplus_external_storageConsolidationCandidates.xml, getBsplus_external_storageLegacyFileList.xml)에 module_srl in-조건 추가.procBsplus_external_storageAdminAddBoardOverride가 이미 upsert(삭제 후 삽입) 구조였어서 백엔드 변경 없이 UI만 추가.lib/SftpDriver.php 신규 작성. 순수 PHP 구현체 phpseclib3를 이 모듈 자체 vendor/에 번들(서버에 ssh2 확장 모듈이 없어도 동작) — 구버전 애드온(lua_external_file)에서 이미 검증된 동일 라이브러리를 재사용.procBsplus_external_storageAdminConsolidate가 bsplus_external_storage_map에 이미 매핑된(=한 번이라도 이 모듈이 이동시킨) 파일만 대상으로 삼고 있었음. 모듈 설치 이전부터 있던, 매핑 자체가 없는 레거시 파일은 이 화면에서 전혀 처리되지 않고 별도의 "레거시 마이그레이션" 버튼(목표 저장소를 임의로 지정할 수 없는 기능)에서만 다뤄지고 있었음.getBsplus_external_storageLegacyFileList.xml에 module_srl 선택 필터 추가.streamFile()/downloadToLocal()가 realpath() 결과가 저장소 경로 안에 있는지 strpos($real, $realStorage) !== 0로만 확인 — $realStorage가 /data/storage일 때 /data/storage_evil/x 같은 형제 디렉터리도 문자열 prefix가 겹쳐 통과하는 고전적인 우회 패턴이었음.$remotePath는 사용자가 직접 넘기는 값이 아니라 매핑 테이블에 기록된 값이라 실제 악용 가능성은 낮으나, 향후 이 드라이버가 다른 입력 경로에 재사용될 경우를 대비해 strpos($real, $realStorage . DIRECTORY_SEPARATOR) !== 0 && $real !== $realStorage 방식으로 교체.catch(\Exception $e)로만 감싸여 있었음. PHP 7+의 TypeError/Error 계열은 \Exception이 아니라 \Throwable이라 이 catch에 걸리지 않아, 드라이버 쪽 Error 발생 시 트리거가 도중에 끊기고(삭제 트리거는 delete_failed 기록도 못 남긴 채) 요청 전체가 죽을 수 있었음. catch(\Throwable $e)로 전부 교체.delete_failed 시나리오는 getMapByOriginalPath 쿼리가 status='migrated'만 조회하도록 이미 필터링돼 있어 그 경로로는 도달하지 않음을 재확인 — 이번 예외 처리 강화와 결합했을 때 여전히 유효한 방어(트리거가 끝까지 못 돌아 상태 갱신이 누락되는 "유령 매핑" 케이스).buildRemotePath()의 ltrim($str, './')는 "./" 리터럴이 아니라 문자 마스크로 동작해 과도하게 잘릴 여지가 있었음(현재 실제 경로 형식에서는 문제 된 적 없음) — substr() 기반 리터럴 접두어 제거로 교체.pickLocalStorageRoot()가 "여유 공간 확보량(GB)"을 항상 지켜서 그 밑으로는 저장하지 않음. 그런데 "데이터 이전 및 통합"으로 로컬 경로를 목표로 고르면 이 확인 없이 그냥 밀어넣고 있었음 — 반복 실행 시 백업 등을 위해 남겨두려던 예약 공간까지 채울 수 있었음.files 테이블 행을 즉시 삭제(action="delete" 쿼리 직접 확인)하지만, 원격(S3/FTP) 삭제가 실패해 delete_failed 상태로 매핑이 남아있으면 정적 서빙 폴백의 isServeFilePermitted()가 "파일 레코드를 못 찾음 → 권한 체크 대상 아님 → 허용"으로 잘못 처리하고 있었음. 결과적으로 게시글이 삭제됐어도 원격 삭제만 실패한 첨부파일은 URL을 아는 사람에게 계속 노출되는 상태였음.files 행이 없으면 이제 항상 차단하도록 수정 — 관리자가 매핑 목록에서 "삭제 재시도"로 원격 삭제를 완료하기 전까지는 아무에게도 서빙되지 않음.getLegacyFileCount())로 교체.getLegacyFileCount() 추가.s3_2_*/s3_3_* 등 새 접두어 필드 사용.<details>) 섹션 제공.downloadToLocal() 추가 (S3=인증 GET, FTP=ftp_get(), 로컬=파일 복사).bsplus_external_storage_board 추가 (관리자 화면에서 "설치" 버튼 클릭 필요했던 변경).isDownloadable(), 게시글/댓글 읽기 권한)를 완전히 우회함을 관리자 화면에 명시. 완전 공개 게시판에서만 권장.triggerCheckServeFile)에도 추가.S3Driver가 관련 설정을 아예 읽지 않고 있었음. 실제로 반영되도록 수정.attemptChainUpload()).storage_type 설정은 하위호환으로 1순위 자동 승격.local_path는 하위호환.disk_free_space()로 여유 공간 확인 → 부족하면 자동으로 다음 순위 경로 사용. 전부 꽉 차면 failed 상태로 사전 감지.storage_root 컬럼 추가 — 파일별 실제 저장 경로 기록.# 접두어로 경로를 즉시 일시중지하는 기능 추가.StorageDriverFactory::create()에 저장소 종류를 명시적으로 넘기고, 각 트리거는 파일별 기록된 storage_type을 사용하도록 수정.isDownloadable() + 게시글/댓글 읽기 권한 체크, 경로 순회 방지.printNavigation() 미존재, foreach(range()) 템플릿 컴파일 오류, class="admin" 오용, 다운로드 버튼 원격 파일 미지원, 관리자 탭 미연결, FTP 확장 미설치 가드.| PHP | 7.4 이상 (Rhymix 공식 최소 요구사항), 8.2 이상 권장. 상한선 없음. |
|---|---|
| Rhymix | 2.0.0 이상 권장. 실제 설치·테스트는 2.1.35에서 진행. |
| SFTP/SSH | 서버 ssh2 확장 모듈 불필요 — 순수 PHP 구현체 phpseclib3를 모듈 자체 vendor/에 번들. mbstring/openssl 확장이 있으면 더 빠르게 동작하지만 없어도 pure-PHP 폴백으로 정상 작동. |
| 라이선스 | GPLv2 (Rhymix 코어 및 다른 BSplus 모듈과 동일) |