한국어 Windows 에서 whoami 가 CP949 로 출력해 계정명에 한글이 있으면 invalid UTF-8 이 응답 배열에 실리고 json_encode 가 false 를 반환한다. echo false 는 빈 문자열이라 HTTP 200 + 빈 본문이 나가는데 예외도 로그도 남지 않아, 설치 마법사 2단계가 진행 불가 상태로 멈춘다. 2026-07 수정(7.0.2)은 로그 축 4곳만 막았다. 출처에서 정규화하고(1차), 응답 경계를 단일 헬퍼로 닫고(2차), 실패 시 문제 필드 경로를 로그에 남기도록(3차) 세 층으로 처리해 다른 경로가 열린 채 남지 않게 했다. 정규화는 복원 가능한 코드페이지 출력을 먼저 되살리므로, 종전 mb_scrub 단독 처리와 달리 한글이 U+FFFD 로 훼손되지 않는다. 같은 원인으로 한국어 Windows 에서 관리자 환경설정의 시스템 정보가 500 이 되던 것도 함께 막았다.
236 lines
11 KiB
PHP
236 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Step 4: 확장 기능 선택
|
|
*
|
|
* 설치할 템플릿, 모듈, 플러그인을 선택합니다.
|
|
* 관리자 템플릿은 최소 1개 필수이며, 나머지는 선택 사항입니다.
|
|
*/
|
|
?>
|
|
|
|
<div class="installer-container installer-container-wide">
|
|
<h1 class="installer-title"><?= htmlspecialchars(lang('step_4_extension_selection')) ?></h1>
|
|
|
|
<p class="installer-description">
|
|
<?= htmlspecialchars(lang('extension_selection_description')) ?>
|
|
</p>
|
|
|
|
<!-- 로딩 표시 -->
|
|
<div id="extensions-loading" class="extension-loading">
|
|
<div class="spinner"></div>
|
|
<p><?= htmlspecialchars(lang('loading_extensions')) ?></p>
|
|
</div>
|
|
|
|
<!-- 에러 표시 -->
|
|
<div id="extensions-error" class="alert alert-error hidden">
|
|
<p id="extensions-error-message"></p>
|
|
<button type="button" onclick="loadExtensions()" class="btn btn-sm btn-secondary">
|
|
<?= htmlspecialchars(lang('retry')) ?>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 확장 선택 폼 -->
|
|
<form id="extension-form" class="installer-form hidden">
|
|
<!-- 마스터 일괄 토글 (모든 다중 선택 섹션) -->
|
|
<div class="bulk-toggle-master">
|
|
<label class="bulk-toggle-label">
|
|
<input type="checkbox" id="bulk-toggle-all" class="bulk-toggle-checkbox" />
|
|
<span><?= htmlspecialchars(lang('bulk_select_all_optional')) ?></span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- 관리자 템플릿 (필수) -->
|
|
<div class="requirement-card">
|
|
<div class="requirement-card-header">
|
|
<h3 class="requirement-card-title">
|
|
<i class="fas fa-shield-alt"></i>
|
|
<?= htmlspecialchars(lang('admin_templates')) ?>
|
|
<span class="badge badge-required"><?= htmlspecialchars(lang('required')) ?></span>
|
|
</h3>
|
|
</div>
|
|
<div class="requirement-card-body">
|
|
<p class="card-description"><?= htmlspecialchars(lang('admin_templates_description')) ?></p>
|
|
<div id="admin-templates-list" class="extension-grid">
|
|
<!-- JavaScript로 동적 렌더링 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 사용자 템플릿 (선택) -->
|
|
<div class="requirement-card">
|
|
<div class="requirement-card-header">
|
|
<h3 class="requirement-card-title">
|
|
<i class="fas fa-palette"></i>
|
|
<?= htmlspecialchars(lang('user_templates')) ?>
|
|
<span class="badge badge-optional"><?= htmlspecialchars(lang('optional')) ?></span>
|
|
</h3>
|
|
<label class="bulk-toggle-section">
|
|
<input type="checkbox" class="bulk-toggle-checkbox" data-bulk-target="user_templates" />
|
|
<span><?= htmlspecialchars(lang('bulk_select_section')) ?></span>
|
|
</label>
|
|
</div>
|
|
<div class="requirement-card-body">
|
|
<p class="card-description"><?= htmlspecialchars(lang('user_templates_description')) ?></p>
|
|
<div id="user-templates-list" class="extension-grid">
|
|
<!-- JavaScript로 동적 렌더링 -->
|
|
</div>
|
|
<div id="user-templates-empty" class="extension-empty hidden">
|
|
<?= htmlspecialchars(lang('no_user_templates')) ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모듈 (선택) -->
|
|
<div class="requirement-card">
|
|
<div class="requirement-card-header">
|
|
<h3 class="requirement-card-title">
|
|
<i class="fas fa-cube"></i>
|
|
<?= htmlspecialchars(lang('modules')) ?>
|
|
<span class="badge badge-optional"><?= htmlspecialchars(lang('optional')) ?></span>
|
|
</h3>
|
|
<label class="bulk-toggle-section">
|
|
<input type="checkbox" class="bulk-toggle-checkbox" data-bulk-target="modules" />
|
|
<span><?= htmlspecialchars(lang('bulk_select_section')) ?></span>
|
|
</label>
|
|
</div>
|
|
<div class="requirement-card-body">
|
|
<p class="card-description"><?= htmlspecialchars(lang('modules_description')) ?></p>
|
|
<div id="modules-list" class="extension-grid">
|
|
<!-- JavaScript로 동적 렌더링 -->
|
|
</div>
|
|
<div id="modules-empty" class="extension-empty hidden">
|
|
<?= htmlspecialchars(lang('no_modules')) ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 플러그인 (선택) -->
|
|
<div class="requirement-card">
|
|
<div class="requirement-card-header">
|
|
<h3 class="requirement-card-title">
|
|
<i class="fas fa-plug"></i>
|
|
<?= htmlspecialchars(lang('plugins')) ?>
|
|
<span class="badge badge-optional"><?= htmlspecialchars(lang('optional')) ?></span>
|
|
</h3>
|
|
<label class="bulk-toggle-section">
|
|
<input type="checkbox" class="bulk-toggle-checkbox" data-bulk-target="plugins" />
|
|
<span><?= htmlspecialchars(lang('bulk_select_section')) ?></span>
|
|
</label>
|
|
</div>
|
|
<div class="requirement-card-body">
|
|
<p class="card-description"><?= htmlspecialchars(lang('plugins_description')) ?></p>
|
|
<div id="plugins-list" class="extension-grid">
|
|
<!-- JavaScript로 동적 렌더링 -->
|
|
</div>
|
|
<div id="plugins-empty" class="extension-empty hidden">
|
|
<?= htmlspecialchars(lang('no_plugins')) ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 언어팩 (선택) -->
|
|
<div class="requirement-card">
|
|
<div class="requirement-card-header">
|
|
<h3 class="requirement-card-title">
|
|
<i class="fas fa-language"></i>
|
|
<?= htmlspecialchars(lang('language_packs')) ?>
|
|
<span class="badge badge-optional"><?= htmlspecialchars(lang('optional')) ?></span>
|
|
</h3>
|
|
<label class="bulk-toggle-section">
|
|
<input type="checkbox" class="bulk-toggle-checkbox" data-bulk-target="language_packs" />
|
|
<span><?= htmlspecialchars(lang('bulk_select_section')) ?></span>
|
|
</label>
|
|
</div>
|
|
<div class="requirement-card-body">
|
|
<p class="card-description"><?= htmlspecialchars(lang('language_packs_description')) ?></p>
|
|
<div id="language-packs-locale-groups">
|
|
<!-- JavaScript로 locale 별 서브헤딩 + 카드 동적 렌더링 -->
|
|
</div>
|
|
<div id="language-packs-empty" class="extension-empty hidden">
|
|
<?= htmlspecialchars(lang('no_language_packs')) ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 선택 요약 -->
|
|
<div class="requirement-card selection-summary">
|
|
<div class="requirement-card-header">
|
|
<h3 class="requirement-card-title">
|
|
<i class="fas fa-clipboard-check"></i>
|
|
<?= htmlspecialchars(lang('selection_summary')) ?>
|
|
</h3>
|
|
</div>
|
|
<div class="requirement-card-body">
|
|
<div class="summary-grid">
|
|
<div class="summary-item">
|
|
<span class="summary-label"><?= htmlspecialchars(lang('admin_templates')) ?></span>
|
|
<span class="summary-count" id="summary-admin-templates">0</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label"><?= htmlspecialchars(lang('user_templates')) ?></span>
|
|
<span class="summary-count" id="summary-user-templates">0</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label"><?= htmlspecialchars(lang('modules')) ?></span>
|
|
<span class="summary-count" id="summary-modules">0</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label"><?= htmlspecialchars(lang('plugins')) ?></span>
|
|
<span class="summary-count" id="summary-plugins">0</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label"><?= htmlspecialchars(lang('language_packs')) ?></span>
|
|
<span class="summary-count" id="summary-language-packs">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 의존성 경고 (누락 의존성 존재 시 표시) -->
|
|
<div id="dependency-warning" class="alert alert-warning hidden">
|
|
<h4 class="alert-title">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<?= htmlspecialchars(lang('dependency_warning_title')) ?>
|
|
</h4>
|
|
<p><?= htmlspecialchars(lang('dependency_warning_description')) ?></p>
|
|
<ul id="dependency-warning-list" class="dependency-warning-list"></ul>
|
|
<button type="button" onclick="autoSelectMissingDependencies()" class="btn btn-sm btn-primary">
|
|
<?= htmlspecialchars(lang('dependency_auto_select')) ?>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 네비게이션 -->
|
|
<div class="btn-group btn-group-spread">
|
|
<button type="button" onclick="goToStep(3)" class="btn btn-secondary">
|
|
<?= htmlspecialchars(lang('previous')) ?>
|
|
</button>
|
|
|
|
<button type="submit" id="submit-btn" class="btn btn-primary" disabled>
|
|
<?= htmlspecialchars(lang('next')) ?>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
// Step 4 번역 키 전달
|
|
window.EXTENSION_LABELS = {
|
|
version: <?= installer_json_encode(lang('version')) ?>,
|
|
select: <?= installer_json_encode(lang('select')) ?>,
|
|
selected: <?= installer_json_encode(lang('selected')) ?>,
|
|
dependencies: <?= installer_json_encode(lang('dependencies')) ?>,
|
|
admin_template_required: <?= installer_json_encode(lang('admin_template_required')) ?>,
|
|
saving: <?= installer_json_encode(lang('saving')) ?>,
|
|
save_failed: <?= installer_json_encode(lang('save_failed')) ?>,
|
|
next: <?= installer_json_encode(lang('next')) ?>,
|
|
extension_load_failed: <?= installer_json_encode(lang('extension_load_failed')) ?>,
|
|
dep_auto_badge_label: <?= installer_json_encode(lang('dep_auto_badge_label')) ?>,
|
|
dep_lock_message: <?= installer_json_encode(lang('dep_lock_message')) ?>,
|
|
dep_version_required: <?= installer_json_encode(lang('dep_version_required')) ?>,
|
|
dep_version_available: <?= installer_json_encode(lang('dep_version_available')) ?>,
|
|
language_pack_disabled_by_extension: <?= installer_json_encode(lang('language_pack_disabled_by_extension')) ?>,
|
|
language_pack_scope_core: <?= installer_json_encode(lang('language_pack_scope_core')) ?>,
|
|
language_pack_scope_module: <?= installer_json_encode(lang('language_pack_scope_module')) ?>,
|
|
language_pack_scope_plugin: <?= installer_json_encode(lang('language_pack_scope_plugin')) ?>,
|
|
language_pack_scope_template: <?= installer_json_encode(lang('language_pack_scope_template')) ?>
|
|
};
|
|
</script>
|