fix(core,extensions): 설치 완료 스크롤 + 미인증 화면 테마 버튼 무반응 수정

Modern PHP User Group 2026-09 정기모임 설치 리뷰에서 접수된 제보 2건.

설치 완료·실패·중단·필수파일 안내는 페이지 최상단에 뜨는데, 진행 로그를 보느라
화면이 아래에 머물러 있으면 안내가 눈에 들어오지 않았다. 네 경로 모두 DOM 변경이
끝난 뒤 결과 섹션으로 부드럽게 이동시킨다(모션 최소화 설정 시 즉시 이동).

미인증 3화면의 테마 버튼은 setTheme 을 params.theme 으로 불렀는데 핸들러는
action.target 만 읽는다. 엔진에 상호 폴백이 없어 클릭이 콘솔 경고 한 줄만 남기고
아무 일도 하지 않았다. 로그인 이후 화면은 핸들러를 거치지 않는 ThemeToggle 을
쓰기 때문에 정상이었고, 그래서 이 세 화면에서만 나타났다.

같은 계약 불일치가 편집기 액션 레시피(admin 9·basic 3)와 두 템플릿 문서에도 있어
함께 고쳤다. 편집기로 만든 액션은 생성 즉시 no-op 이 되는데 오류가 남지 않는다.
정적 검사 레지스트리가 오히려 틀린 계약(params.theme 필수)을 강제하고 있어 올바른
형태를 막고 있었으므로 두 미러를 함께 정정했다.

테마를 고치는 과정에서 별개 결함이 드러났다. GDPR 스토리지 인터셉터가 기능 쿠키
미동의 상태에서 필수 목록 밖 저장을 부팅마다 파기하는데, 화면 테마가 그 목록에서
빠져 있었다. 저장소의 setItem 호출을 기계 도출해 대조한 결과 같은 이유로 사라지던
항목이 13건 더 있었다 — 비회원 주문 조회, 결제창 복귀 기록, 본인인증 복귀 기록,
관리자 화면 상태, 자산 주소 형식 캐시. 전량 필수로 분류하고, 동의 안내 문구가
"다크모드는 기능 쿠키" 라고 말하던 부분을 사실에 맞게 정정했다(기설치본의 저장된
문구는 업그레이드 스텝이 정정하며, 운영자가 고친 문구는 건드리지 않는다).

재발 방지는 허용목록을 직접 import 하고 모집단을 디렉토리 순회로 도출하는 커버리지
테스트가 맡는다 — 손으로 열거하지 않으므로 새 저장 키가 등재를 빠뜨리면 붉어진다.
This commit is contained in:
HeuJung
2026-09-03 18:42:30 +09:00
parent 92923a5011
commit 8cf829f953
47 changed files with 1424 additions and 282 deletions
+11
View File
@@ -2323,6 +2323,17 @@ hr {
box-shadow: var(--shadow-md);
}
/*
* 결과/안내 섹션으로 스크롤할 때 sticky 헤더에 가려지지 않도록 여백을 확보한다.
* 헤더 실높이는 데스크톱 70px / 모바일 60px (+ 하단 테두리 1px) 이므로 여유를 포함해 90px.
* 전역 `html { scroll-behavior: smooth }` 는 두지 않는다 — 로그 영역 등 내부 스크롤에
* 부작용을 준다. 스크롤 동작은 JS(scrollResultIntoView)가 개별 호출로 지정한다.
*/
.result-section,
#env-setup-section {
scroll-margin-top: 90px;
}
/* 성공 배경 */
.result-section:has(.result-icon-success) {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+37
View File
@@ -3842,6 +3842,31 @@
}
/**
* 결과/안내 섹션을 뷰포트로 부드럽게 스크롤합니다.
*
* 완료/실패/중단 안내는 페이지 최상단에 있는데, 설치 진행 중에는 사용자가 로그를 보느라
* 화면이 하단에 머물러 있는 경우가 많다. 그대로 두면 안내가 표시되어도 눈에 들어오지
* 않는다. 이미 최상단이면 스크롤은 no-op 이라 새로고침 복원 경로에서도 무해하다.
*
* 헤더 바가 sticky 이므로 섹션 상단이 가려지지 않도록 CSS 의 scroll-margin-top 이
* 여백을 확보한다 (installer.css 의 .result-section, #env-setup-section).
*
* @param {HTMLElement|null} sectionEl 스크롤 대상 섹션
*/
function scrollResultIntoView(sectionEl) {
if (!sectionEl) return;
// 모션 최소화를 선호하는 사용자는 즉시 이동
const reduceMotion = window.matchMedia
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
sectionEl.scrollIntoView({
behavior: reduceMotion ? 'auto' : 'smooth',
block: 'start',
});
}
/**
* 완료 섹션 표시
*/
@@ -3924,6 +3949,9 @@
} catch (_) {
// fetch 자체가 실패해도 무시 — runtime.php 가 보존되어 다음 부팅 시 앱 정상 동작
}
// 완료 안내로 스크롤 (타이틀 숨김·카드 접기로 문서 높이가 바뀐 뒤에 호출)
scrollResultIntoView(completionSection);
}
/**
@@ -4066,6 +4094,9 @@
failureSection.style.opacity = '1';
}, 100);
}
// 실패 안내로 스크롤 (카드 body 를 펼친 뒤에 호출)
scrollResultIntoView(failureSection);
}
/**
@@ -4399,6 +4430,9 @@
setTaskStatus(nextTask.id, 'aborted', nextTask.target || null);
}
}
// 중단 안내로 스크롤 (로그 렌더링으로 문서 높이가 늘어난 뒤에 호출)
scrollResultIntoView(abortedSection);
}
/**
@@ -5213,6 +5247,9 @@
if (statusEl) {
statusEl.classList.add('hidden');
}
// 안내 섹션으로 스크롤 (목록·명령어를 채운 뒤에 호출)
scrollResultIntoView(section);
}
/**