Mublo Framework v1.5.0 → v1.5.1 패치
=====================================

이 패치는 v1.5.0 설치본 전용입니다. 다른 버전에는 적용하지 마세요.
(현재 버전은 관리자 → 시스템 관리에서 확인할 수 있습니다)

이번 패치는 무엇을 하나
-----------------------
Cloudflare 같은 프록시 뒤에서 모든 요청이 같은 IP(프록시 엣지 주소)로
기록되던 것을 고칩니다. 로그인 시도 제한·요청 제한·도배 제한·감사 로그가
모두 이 값을 쓰기 때문에 기록만의 문제가 아니었습니다. 보안 다운로드
링크를 IP에 묶는 보호도 같은 이유로 무력했습니다.

블록 HTML 편집기(관리자)에는 회원 에디터에서 v1.5.0 에 고쳐진 이미지
링크 문제가 그대로 남아 있었는데 이번에 함께 맞췄고, 인라인 서식 교체와
접근성, 업로드 취소, 저장 시 요소 보존도 고쳤습니다.

파일 삭제는 없습니다. 데이터베이스 마이그레이션도 없습니다.
composer 의존성 변경도 없습니다.

적용 방법
---------
1. 사이트 파일을 백업합니다.

2. 이 zip 안의 파일들을 설치 루트에 그대로 덮어씁니다 (디렉토리 구조 유지).

3. 삭제할 파일은 없습니다.

4. 실행할 마이그레이션이 없습니다.

5. composer 의존성 변경이 없으므로 vendor/ 는 건드리지 않아도 됩니다.

6. .env 는 덮어쓰지 않습니다. 이 zip 의 .env.example 은 참고용이며,
   TRUSTED_PROXIES 설명이 갱신되어 있습니다. 아래 "운영자가 바로 마주치는
   변화" 의 첫 항목을 읽고 필요하면 .env 를 직접 손보세요.

7. 동작 확인 —
   - 관리자 → 시스템 관리에서 버전이 1.5.1 로 보이는지
   - 로그인 기록(관리자 → 회원)에 남는 IP가 프록시 주소가 아니라
     실제 접속 주소인지
   - 에러 로그에 [TRUSTED_PROXY] 경고가 찍히지 않는지
   - 관리자 블록 편집에서 이미지에 링크가 걸리는지, 이미 색이 있는
     글자의 색을 다시 골랐을 때 실제로 바뀌는지

   브라우저가 예전 편집기 파일을 캐시하고 있을 수 있습니다. 편집기
   동작이 그대로라면 강력 새로고침(Ctrl+F5)으로 확인하세요.

운영자가 바로 마주치는 변화
---------------------------
- [반드시 확인] 신뢰 프록시 기본값이 'cloudflare' 가 됩니다.
  종전 기본값은 "불신"(빈 값)이었습니다. Cloudflare 뒤에 두는 설치가
  흔한데 불신이 기본이면 기록되는 IP가 전부 엣지 주소가 되므로 기본을
  바꿨습니다. Cloudflare 공개 대역은 코어가 상수로 들고 있으므로
  (Request::CLOUDFLARE_RANGES) 설정에 CIDR 22줄을 옮겨 적을 필요가
  없습니다 — 한 줄만 빠져도 그 대역만 조용히 틀리고 알아낼 방법이
  없었습니다.

  * Cloudflare 를 쓴다          → 손댈 것 없습니다.
  * 다른 프록시(nginx·HAProxy·
    로드밸런서) 뒤에 있다        → 그 IP/CIDR 를 직접 나열하세요.
                                   예: TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12
  * 프록시가 아예 없다          → TRUSTED_PROXIES=none 으로 끕니다.
                                   (그냥 두어도 REMOTE_ADDR 이 Cloudflare
                                   대역에 들지 않으므로 영향은 없습니다)

  빈 값과 '설정 안 함' 을 더는 구별할 수 없으므로 "프록시 없음" 은
  none 으로 명시합니다. '*'(모든 프록시 신뢰)는 IP 스푸핑을 허용하므로
  프로덕션에서는 여전히 지양합니다.

  헤더를 무조건 믿지는 않습니다. 직전 홉이 신뢰 목록에 있을 때만 전달
  헤더를 읽습니다. 서버에 직접 닿을 수 있는 사람이 헤더를 위조해 차단·
  요청 제한을 우회하지 못하게 하기 위해서입니다.

- [확인해 두면 좋은 것] IP 기반으로 차단·예외를 걸어 둔 것이 있다면
  이제 기록·판정에 쓰이는 값이 실제 사용자 IP로 바뀝니다. 그동안
  프록시 주소를 기준으로 잡아 둔 규칙이 있다면 의미가 달라집니다.

- 전달 헤더(CF-Connecting-IP·X-Forwarded-For)가 왔는데 직전 홉이 신뢰
  목록에 없으면 error_log 에 [TRUSTED_PROXY] 경고를 한 번 남깁니다.
  이 문제가 오래 드러나지 않은 이유가 아무 신호도 없어서였습니다.

- 보안 다운로드 링크가 다시 발급받은 사람에게만 열립니다.
  이 링크는 발급 시점의 IP에 묶어 두는데, 프록시 뒤에서는 모든 사용자의
  IP가 같아 묶은 것이 아무 구실도 못 했습니다 — 링크가 밖으로 새면
  누구나 열 수 있었습니다. 발급과 검증이 같은 판정을 쓰도록 맞췄습니다.

  적용 직후, 이전 IP 기준으로 발급되어 아직 살아 있는 링크는 검증에서
  떨어질 수 있습니다. 다시 내려받으면 됩니다.

- 요청 로그와 오류 로그의 IP도 같은 판정을 씁니다. 모든 요청이 같은 IP로
  남으면 오류가 한 사용자에게만 나는지 모두에게 나는지 알 수 없었습니다.

- 블록 HTML 편집기(관리자)에서 이미지에 링크를 걸면 이미지가 링크가
  됩니다. 회원 에디터에서 v1.5.0 에 고친 문제인데, 블록 HTML 편집기는
  별도 아티팩트라 그 수정이 전파되지 않았습니다. 링크 제거, 캡션을 붙일
  때 링크가 끊기던 것, 이미지 모달의 링크·대체 텍스트·캡션 칸도 함께
  맞췄습니다.

- 블록 HTML 편집기에서 글자색·배경색·글자크기를 다시 고르면 교체됩니다.
  종전에는 새 서식이 기존 것을 바깥에서 덧씌워, 이미 색이 있는 글자는
  다시 칠해도 화면이 바뀌지 않았고(CSS 상 안쪽이 이깁니다) 고를 때마다
  중첩이 쌓였으며, 캐럿만 둔 채 색을 바꾸면 제로폭 문자만 든 빈 span 이
  저장본에까지 남았습니다.

- 저장할 때 기존 요소가 사라지거나 합쳐지던 것을 고쳤습니다.
  클래스·ID·접근성 속성이 있는 span, 빈 아이콘·앵커·CSS 장식 요소는
  보존하고, 편집기가 만든 서식(색·크기·굵게·기울임·밑줄·취소선·글꼴)만
  지정된 요소에 한해 빈 껍데기 제거와 인접 병합을 계속합니다.

- 업로드 중 모달을 닫으면(ESC·배경 클릭) 그 업로드는 취소됩니다.
  종전에는 몇 초 뒤 업로드가 끝나면 취소한 이미지가 본문에 들어왔고,
  그 사이 다른 이미지를 더블클릭해 교체 모드에 들어가 있으면 취소했던
  업로드가 그 무관한 이미지를 덮어썼습니다.

- 블록 HTML 편집기의 링크 주소에도 javascript: 같은 실행 가능한 스킴을
  입력 단계에서 거부합니다. 관리자만 쓰는 화면이지만, 속성으로 주소를
  직접 넣는 경로는 저장 시 필터를 지나갑니다.

- 신규 설치의 AI 모델 목록이 현행으로 갱신됩니다.
  이미 돌고 있는 설치본에는 적용되지 않습니다 — config/ai.php 는 설치할
  때 한 번 만들어진 뒤 코어가 덮어쓰지 않는 운영자 관리 파일입니다.
  새 모델을 쓰려면 그 파일의 목록에 직접 추가하세요. 반대로 쓰던 모델을
  목록에서 지우지는 마세요. 그 모델로 저장된 도메인은 AI 기능이 폴백
  없이 멈춥니다(DomainAiConfigService::runtimeConfig).

호환성 주의 (커스텀 확장·스킨 개발자)
-------------------------------------
- 클라이언트 IP를 직접 읽고 있다면
  $_SERVER['REMOTE_ADDR'] 을 직접 읽는 코드는 프록시 판정을 거치지
  않으므로 프록시 엣지 주소를 그대로 쓰게 됩니다. Request 인스턴스가
  있으면 $request->getClientIp(), 없으면 새로 생긴 정적 진입점
  Request::clientIpFromServer($_SERVER) 를 쓰세요. 코어 쪽은 이번에
  세 자리(요청 로그·오류 처리기·보호된 파일 서비스)를 이 하나로 모았고,
  새 자리가 생기면 테스트(ClientIpSingleSourceTest)가 잡습니다.

- 새로 생긴 확장 지점
    Mublo\Core\Http\Request::CLOUDFLARE_RANGES (상수)
    Mublo\Core\Http\Request::clientIpFromServer(array $server): string
  setTrustedProxies() 가 'cloudflare'(대역으로 펼침)와 'none'(불신)을
  문자열로 받습니다. 기존 메서드의 시그니처가 바뀐 것은 없습니다.

- 편집기 파일을 복사해 고쳐 쓰는 경우
  이번 변경은 public/assets/lib/editor/mublo-editor/MubloEditor.js 와
  public/assets/js/admin/block-html-editor/BlockHtmlEditorBase.js 안에
  들어 있습니다. 덮어쓰면 그 수정이 사라집니다. 새로 추가된 CSS 규칙은
  없습니다 (MubloEditor.css 는 이번 패치에 들어 있지 않습니다).
  editor/{에디터}/config.local.php 로 설정만 덮어쓰고 있다면 영향이
  없습니다.

- 패키지 버전
    변경 없음 (코어 전용 패치)

포함 내역
---------
- 변경/추가 파일 19개 (v1.5.0 → v1.5.1 전체 차이, CHANGELOG.md 제외)
  운영에 쓰이는 파일은 8개이고, 나머지는 테스트와 CI·정적분석 설정입니다.
  (tests/ 아래 파일들은 덮어써도 사이트 동작에 영향이 없습니다)
- 삭제 대상 파일 없음
- 데이터베이스 마이그레이션 없음
- composer 의존성 변경 없음 → vendor/ 갱신 불필요
- 상세 변경 내역: CHANGELOG.md 의 [1.5.1] 섹션
  https://github.com/wwiz-mublo/mublo/blob/v1.5.1/CHANGELOG.md
- 전체 코드 변경 비교:
  https://github.com/wwiz-mublo/mublo/compare/v1.5.0...v1.5.1

v1.5.0 미만에서 올라오는 경우에는 먼저 v1.5.0 까지의 업그레이드 절차를
완료한 뒤 이 패치를 적용하세요. 또는 릴리스 페이지의 전체 배포본
MubloFramework-v1.5.1.zip 으로 파일을 교체하는 방법도 있습니다.
