Control 설정 모달 입력 보존 개선
This commit is contained in:
@@ -113,10 +113,13 @@ WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과
|
||||
- 팬 자동 제어: 팬 시작 온도, 최대 온도, 즉시 최대 PWM 온도, 자동 상승/하강 PWM step
|
||||
- 보안 정책: 자동 로그인 유지 기간, remember 쿠키 Secure/SameSite, CSRF 길이, remember 토큰 길이, User-Agent/IP 접두사 검증, 로그인 실패 잠금, 재부팅 허용 여부, 재부팅 확인 문구, 명령 timeout, WiFi 조작 허용 여부
|
||||
- 알림 정책: 배터리 긴급/위험/경고/복구 기준, 배터리 낮음/복구 알림 쿨다운, 시스템 유의사항 알림 쿨다운
|
||||
- 화면/진단 표시: 프로세스 후보 수, 사용자 서비스 로그 줄 수, 사용자 서비스 캐시 시간, 팬 이상 이력 수, HA 알림 이력 수
|
||||
- 저전압/스로틀링: 최근 판정창, 복구 중 유지 시간, 반복 발생 기준 횟수
|
||||
- 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이, RPM 감시 시작 PWM, alert 유지 중 반복 기록, 반복 기록 간격, 하강 변화 원인 후보 표시 여부
|
||||
- 배터리 예측: 단기/장기 학습 범위, 후보 필터 비율, 부하 보정 강도, 전압 하한, 용량 모델 가중치, 상태/잔여시간 차트 표본 수
|
||||
|
||||
설정 모달이 열려 있는 동안 1초 상태 갱신은 폼 DOM을 다시 만들지 않습니다. 입력 중인 값은 저장, 기본값 초기화, 닫기 전까지 유지되며, 백그라운드 snapshot은 내부 설정 payload만 갱신합니다. 단순 boolean 설정은 `켜기`/`끄기` 세그먼트로 표시합니다.
|
||||
|
||||
## 갱신 주기
|
||||
|
||||
- WebSocket 상태 갱신: 1초마다
|
||||
|
||||
@@ -179,6 +179,11 @@ function setting_definitions(): array
|
||||
'notify.battery_low_cooldown_seconds' => ['group' => 'notify', 'label' => '배터리 낮음 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_BATTERY_LOW_COOLDOWN, 'min' => 1, 'max' => 86400, 'step' => 1, 'unit' => '초'],
|
||||
'notify.battery_clear_cooldown_seconds' => ['group' => 'notify', 'label' => '배터리 복구 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_BATTERY_CLEAR_COOLDOWN, 'min' => 1, 'max' => 86400, 'step' => 1, 'unit' => '초'],
|
||||
'notify.system_notice_cooldown_seconds' => ['group' => 'notify', 'label' => '시스템 유의사항 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_SYSTEM_NOTICE_COOLDOWN, 'min' => 10, 'max' => 86400, 'step' => 10, 'unit' => '초'],
|
||||
'display.process_limit' => ['group' => 'display', 'label' => '프로세스 후보 표시 수', 'type' => 'number', 'default' => 6, 'min' => 1, 'max' => 30, 'step' => 1, 'unit' => '개'],
|
||||
'display.custom_service_log_lines' => ['group' => 'display', 'label' => '사용자 서비스 로그 줄 수', 'type' => 'number', 'default' => 12, 'min' => 1, 'max' => 50, 'step' => 1, 'unit' => '줄'],
|
||||
'display.custom_service_cache_seconds' => ['group' => 'display', 'label' => '사용자 서비스 캐시 시간', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 60, 'step' => 1, 'unit' => '초'],
|
||||
'display.fan_notice_history_limit' => ['group' => 'display', 'label' => '팬 이상 이력 표시 수', 'type' => 'number', 'default' => 100, 'min' => 10, 'max' => 500, 'step' => 10, 'unit' => '개'],
|
||||
'display.ha_notify_history_limit' => ['group' => 'display', 'label' => 'HA 알림 이력 표시 수', 'type' => 'number', 'default' => 20, 'min' => 5, 'max' => 100, 'step' => 5, 'unit' => '개'],
|
||||
'battery.trend_hours' => ['group' => 'battery', 'label' => '단기 배터리 학습 범위', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 48, 'step' => 1, 'unit' => '시간'],
|
||||
'battery.profile_days' => ['group' => 'battery', 'label' => '장기 배터리 학습 범위', 'type' => 'number', 'default' => 45, 'min' => 3, 'max' => 90, 'step' => 1, 'unit' => '일'],
|
||||
'battery.load_adjust_recent' => ['group' => 'battery', 'label' => '최근 추세 부하 보정 강도', 'type' => 'number', 'default' => 0.45, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
|
||||
@@ -261,6 +266,7 @@ function settings_payload(): array
|
||||
$groups = [
|
||||
'security' => '보안 정책',
|
||||
'notify' => '알림 정책',
|
||||
'display' => '화면/진단 표시',
|
||||
'fan' => '팬 자동 제어',
|
||||
'battery' => '배터리 예측',
|
||||
'event' => '저전압/스로틀링',
|
||||
|
||||
+6
-5
@@ -3058,7 +3058,8 @@ function systemd_service_logs(string $unit, int $limit = 12): array
|
||||
function custom_systemd_services(): array
|
||||
{
|
||||
$cachePath = '/tmp/control-custom-services.json';
|
||||
if (is_file($cachePath) && filemtime($cachePath) >= time() - 5) {
|
||||
$cacheSeconds = (int)setting_value('display.custom_service_cache_seconds');
|
||||
if (is_file($cachePath) && filemtime($cachePath) >= time() - $cacheSeconds) {
|
||||
$cached = json_decode((string)@file_get_contents($cachePath), true);
|
||||
if (is_array($cached)) {
|
||||
return $cached;
|
||||
@@ -3104,7 +3105,7 @@ function custom_systemd_services(): array
|
||||
|
||||
foreach ($names as $name) {
|
||||
$record = $records[$name] ?? [];
|
||||
$logs = systemd_service_logs($name, 12);
|
||||
$logs = systemd_service_logs($name, (int)setting_value('display.custom_service_log_lines'));
|
||||
|
||||
$rows[] = [
|
||||
'name' => $name,
|
||||
@@ -3259,7 +3260,7 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
], $battery['remaining']['seconds'] ?? null);
|
||||
$history = array_slice($history, -(int)setting_value('battery.history_sync_limit'));
|
||||
|
||||
$processes = process_resource_data(6);
|
||||
$processes = process_resource_data((int)setting_value('display.process_limit'));
|
||||
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes);
|
||||
|
||||
$shouldLogNotice = !empty($fanSpike['alert_started'])
|
||||
@@ -3297,8 +3298,8 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
'processes' => $processes,
|
||||
'custom_services' => custom_systemd_services(),
|
||||
'fan_spike' => $fanSpike,
|
||||
'fan_spike_history' => fan_spike_history(100),
|
||||
'ha_notify_history' => ha_notify_log_rows(20),
|
||||
'fan_spike_history' => fan_spike_history((int)setting_value('display.fan_notice_history_limit')),
|
||||
'ha_notify_history' => ha_notify_log_rows((int)setting_value('display.ha_notify_history_limit')),
|
||||
'settings' => settings_payload(),
|
||||
];
|
||||
|
||||
|
||||
+41
-20
@@ -86,6 +86,7 @@
|
||||
batteryTooltipText: '',
|
||||
settingsPayload: null,
|
||||
settingsOpen: false,
|
||||
settingsDirty: false,
|
||||
};
|
||||
|
||||
const storageKeys = {
|
||||
@@ -521,9 +522,7 @@
|
||||
cleanup(true);
|
||||
};
|
||||
const onCancel = () => cleanup(mode === 'confirm' ? false : null);
|
||||
const onLayerClick = event => {
|
||||
if (event.target === layer) onCancel();
|
||||
};
|
||||
const onLayerClick = () => {};
|
||||
const onKeydown = event => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
@@ -606,8 +605,13 @@
|
||||
return json.data;
|
||||
}
|
||||
|
||||
function renderSettings(payload = state.settingsPayload) {
|
||||
function settingDomId(key) {
|
||||
return `setting-${String(key).replace(/[^A-Za-z0-9_-]/g, '-')}`;
|
||||
}
|
||||
|
||||
function renderSettings(payload = state.settingsPayload, options = {}) {
|
||||
if (!els.settingsBody || !payload) return;
|
||||
if (state.settingsOpen && state.settingsDirty && !options.force) return;
|
||||
state.settingsPayload = payload;
|
||||
const groups = payload.groups || {};
|
||||
const byGroup = {};
|
||||
@@ -618,21 +622,23 @@
|
||||
});
|
||||
const fieldHtml = item => {
|
||||
const key = escapeHtml(item.key);
|
||||
const id = escapeHtml(settingDomId(item.key));
|
||||
const value = item.value ?? item.default ?? '';
|
||||
const type = item.type || 'number';
|
||||
if (type === 'boolean') {
|
||||
const checked = value === true || value === 1 || value === '1' || value === 'true';
|
||||
return `
|
||||
<span class="settings-toggle">
|
||||
<input data-setting-key="${key}" type="checkbox" value="1" ${checked ? 'checked' : ''}>
|
||||
<span>${checked ? '켜기' : '끄기'}</span>
|
||||
<input id="${id}" name="${key}" data-setting-key="${key}" type="checkbox" value="1" autocomplete="off" ${checked ? 'checked' : ''}>
|
||||
<span class="settings-toggle-on">켜기</span>
|
||||
<span class="settings-toggle-off">끄기</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
if (type === 'select') {
|
||||
const options = item.options || {};
|
||||
return `
|
||||
<select data-setting-key="${key}" class="settings-select">
|
||||
<select id="${id}" name="${key}" data-setting-key="${key}" class="settings-select" autocomplete="off">
|
||||
${Object.entries(options).map(([optionValue, label]) => `
|
||||
<option value="${escapeHtml(optionValue)}" ${String(optionValue) === String(value) ? 'selected' : ''}>${escapeHtml(label)}</option>
|
||||
`).join('')}
|
||||
@@ -642,19 +648,25 @@
|
||||
if (type === 'text') {
|
||||
return `
|
||||
<input
|
||||
id="${id}"
|
||||
name="${key}"
|
||||
data-setting-key="${key}"
|
||||
type="text"
|
||||
maxlength="${escapeHtml(item.max_length ?? 255)}"
|
||||
autocomplete="off"
|
||||
value="${escapeHtml(value)}">
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<input
|
||||
id="${id}"
|
||||
name="${key}"
|
||||
data-setting-key="${key}"
|
||||
type="number"
|
||||
min="${escapeHtml(item.min ?? '')}"
|
||||
max="${escapeHtml(item.max ?? '')}"
|
||||
step="${escapeHtml(item.step ?? '1')}"
|
||||
autocomplete="off"
|
||||
value="${escapeHtml(value)}">
|
||||
`;
|
||||
};
|
||||
@@ -675,7 +687,7 @@
|
||||
<h4>${escapeHtml(groups[group] || group)}</h4>
|
||||
<div class="settings-grid">
|
||||
${items.map(item => `
|
||||
<label class="settings-field">
|
||||
<div class="settings-field">
|
||||
<span class="settings-label">
|
||||
<span>${escapeHtml(item.label || item.key)}</span>
|
||||
<span>${escapeHtml(item.unit || '')}</span>
|
||||
@@ -684,17 +696,27 @@
|
||||
${fieldHtml(item)}
|
||||
<span class="settings-default">${t('settingsDefault')} ${escapeHtml(defaultValueText(item))}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</section>
|
||||
`).join('');
|
||||
els.settingsBody.querySelectorAll('[data-setting-key]').forEach(input => {
|
||||
input.addEventListener('input', () => {
|
||||
state.settingsDirty = true;
|
||||
});
|
||||
input.addEventListener('change', () => {
|
||||
state.settingsDirty = true;
|
||||
});
|
||||
});
|
||||
els.settingsBody.querySelectorAll('.settings-toggle input').forEach(input => {
|
||||
input.addEventListener('change', () => {
|
||||
const label = input.closest('.settings-toggle')?.querySelector('span');
|
||||
if (label) label.textContent = input.checked ? '켜기' : '끄기';
|
||||
const toggle = input.closest('.settings-toggle');
|
||||
if (toggle) toggle.dataset.checked = input.checked ? '1' : '0';
|
||||
});
|
||||
input.closest('.settings-toggle')?.addEventListener('click', () => {
|
||||
const toggle = input.closest('.settings-toggle');
|
||||
toggle?.setAttribute('data-checked', input.checked ? '1' : '0');
|
||||
toggle?.addEventListener('click', () => {
|
||||
input.checked = !input.checked;
|
||||
input.dispatchEvent(new Event('change'));
|
||||
});
|
||||
@@ -716,6 +738,7 @@
|
||||
return;
|
||||
}
|
||||
state.settingsOpen = true;
|
||||
state.settingsDirty = false;
|
||||
els.settingsDialog.dataset.open = '1';
|
||||
els.settingsDialog.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
@@ -723,6 +746,7 @@
|
||||
function closeSettings() {
|
||||
if (!els.settingsDialog) return;
|
||||
state.settingsOpen = false;
|
||||
state.settingsDirty = false;
|
||||
delete els.settingsDialog.dataset.open;
|
||||
els.settingsDialog.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
@@ -742,8 +766,9 @@
|
||||
const data = await api('settings_save', {
|
||||
settings: JSON.stringify(currentSettingsFormValues()),
|
||||
});
|
||||
state.settingsDirty = false;
|
||||
render(data);
|
||||
renderSettings(data.settings);
|
||||
renderSettings(data.settings, { force: true });
|
||||
notice(t('settingsSaved'), 'success');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -757,8 +782,9 @@
|
||||
}
|
||||
try {
|
||||
const data = await api('settings_reset', {});
|
||||
state.settingsDirty = false;
|
||||
render(data);
|
||||
renderSettings(data.settings);
|
||||
renderSettings(data.settings, { force: true });
|
||||
notice(t('settingsResetDone'), 'success');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -1809,7 +1835,7 @@
|
||||
function render(data) {
|
||||
if (data.settings) {
|
||||
state.settingsPayload = data.settings;
|
||||
if (state.settingsOpen) {
|
||||
if (!state.settingsOpen) {
|
||||
renderSettings(data.settings);
|
||||
}
|
||||
}
|
||||
@@ -2062,11 +2088,6 @@
|
||||
els.settingsClose?.addEventListener('click', closeSettings);
|
||||
els.settingsSave?.addEventListener('click', saveSettings);
|
||||
els.settingsReset?.addEventListener('click', resetSettings);
|
||||
els.settingsDialog?.addEventListener('click', event => {
|
||||
if (event.target === els.settingsDialog) {
|
||||
closeSettings();
|
||||
}
|
||||
});
|
||||
els.translateBtn?.addEventListener('click', () => {
|
||||
applyLang(state.lang === 'ko' ? 'en' : 'ko');
|
||||
refreshStatus();
|
||||
|
||||
+2
-2
@@ -135,7 +135,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
.notice{position:fixed;right:20px;bottom:20px;min-width:220px;max-width:420px;padding:14px 16px;border-radius:14px;font-weight:500;background:var(--table-head);color:var(--text);border:1px solid var(--line);box-shadow:var(--shadow);z-index:99}.notice:empty{display:none}.notice[data-type=success]{border-color:rgba(53,196,107,.5)}.notice[data-type=error]{border-color:rgba(255,95,87,.5)}
|
||||
.status-value.interactive{cursor:help;text-decoration:underline;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:4px}.runtime-tooltip{position:fixed;left:0;top:0;z-index:110;display:none;width:min(520px,calc(100vw - 28px));max-height:calc(100vh - 28px);overflow:hidden;padding:14px 16px;border:1px solid rgba(84,101,128,.78);border-radius:16px;background:var(--card);color:var(--text);box-shadow:0 18px 48px rgba(0,0,0,.38);font-size:13px;line-height:1.55;white-space:pre-line;word-break:keep-all;overflow-wrap:anywhere;pointer-events:none}.runtime-tooltip[data-open="1"]{display:block}.runtime-tooltip::before{content:"";position:absolute;left:18px;top:-7px;width:12px;height:12px;border-left:1px solid rgba(84,101,128,.78);border-top:1px solid rgba(84,101,128,.78);background:var(--card);transform:rotate(45deg)}html[data-theme="light"] .runtime-tooltip{background:#f8fbff;border-color:#aebed2;color:#172033;box-shadow:0 18px 44px rgba(28,43,64,.18)}html[data-theme="light"] .runtime-tooltip::before{background:#f8fbff;border-color:#aebed2}
|
||||
.dialog-layer{position:fixed;inset:0;display:none;align-items:center;justify-content:center;padding:22px;background:rgba(4,7,12,.68);backdrop-filter:blur(8px);z-index:120}.dialog-layer[data-open="1"]{display:flex}.dialog-box{width:min(430px,100%);border:1px solid rgba(84,101,128,.78);border-radius:20px;background:var(--card);box-shadow:0 24px 70px rgba(0,0,0,.42);padding:20px}.dialog-box h3{margin:0 0 8px;font-size:19px;font-weight:500}.dialog-message{margin:0 0 16px;color:var(--sub);line-height:1.5;white-space:pre-wrap}.dialog-input{width:100%;height:48px;margin-bottom:14px;border-radius:14px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 14px;outline:none}.dialog-input:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.dialog-actions{display:flex;justify-content:flex-end;gap:10px}.dialog-actions .btn{min-width:84px}.dialog-actions .btn.red{background:#c62828}
|
||||
.settings-layer{align-items:stretch}.settings-box{width:min(980px,100%);max-height:calc(100vh - 44px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;padding:0;overflow:hidden}.settings-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:18px 20px;border-bottom:1px solid var(--line)}.settings-sub{margin:0;color:var(--sub);font-size:13px}.settings-body{overflow:auto;padding:16px 20px;display:grid;gap:14px}.settings-group{border:1px solid var(--line);border-radius:16px;background:var(--card2);padding:14px}.settings-group h4{margin:0 0 12px;font-size:15px}.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.settings-field{display:grid;gap:7px;min-width:0}.settings-label{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--sub);font-size:13px}.settings-label span:last-child{white-space:nowrap;color:var(--sub)}.settings-control{display:flex;align-items:center;gap:9px}.settings-control input,.settings-select{width:100%;height:42px;border-radius:12px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 12px;outline:none}.settings-control input:focus,.settings-select:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.settings-toggle{display:flex;align-items:center;width:100%;height:42px;border:1px solid var(--line);border-radius:12px;background:var(--input);padding:4px;cursor:pointer}.settings-toggle input{position:absolute;opacity:0;pointer-events:none}.settings-toggle span{display:grid;place-items:center;width:72px;height:32px;border-radius:10px;background:#334155;color:#cbd5e1;font-weight:700;font-size:12px}.settings-toggle input:checked+span{background:var(--blue);color:#fff}.settings-default{font-size:12px;color:var(--sub);white-space:nowrap}.settings-actions{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--line)}
|
||||
.settings-layer{align-items:stretch}.settings-box{width:min(980px,100%);max-height:calc(100vh - 44px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;padding:0;overflow:hidden}.settings-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:18px 20px;border-bottom:1px solid var(--line)}.settings-sub{margin:0;color:var(--sub);font-size:13px}.settings-body{overflow:auto;padding:16px 20px;display:grid;gap:14px}.settings-group{border:1px solid var(--line);border-radius:16px;background:var(--card2);padding:14px}.settings-group h4{margin:0 0 12px;font-size:15px}.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.settings-field{display:grid;gap:7px;min-width:0}.settings-label{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--sub);font-size:13px}.settings-label span:last-child{white-space:nowrap;color:var(--sub)}.settings-control{display:flex;align-items:center;gap:9px}.settings-control input,.settings-select{width:100%;height:42px;border-radius:12px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 12px;outline:none}.settings-control input:focus,.settings-select:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.settings-toggle{display:grid;grid-template-columns:1fr 1fr;align-items:center;width:100%;height:42px;border:1px solid var(--line);border-radius:12px;background:var(--input);padding:4px;cursor:pointer;gap:4px}.settings-toggle input{position:absolute;opacity:0;pointer-events:none}.settings-toggle span{display:grid;place-items:center;min-width:0;height:32px;border-radius:10px;color:var(--sub);font-weight:700;font-size:12px;transition:background .12s ease,color .12s ease}.settings-toggle[data-checked="1"] .settings-toggle-on,.settings-toggle[data-checked="0"] .settings-toggle-off{background:var(--blue);color:#fff}.settings-toggle[data-checked="0"] .settings-toggle-off{background:#64748b}.settings-default{font-size:12px;color:var(--sub);white-space:nowrap}.settings-actions{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--line)}
|
||||
@media(max-width:1320px){.chart-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:1100px){.layout{grid-template-columns:1fr}.chart-grid,.resource-grid{grid-template-columns:1fr}.topbar{align-items:flex-start;flex-direction:column}.topbar-right{width:100%}.topbar-right .btn{flex:1}.stat-grid{grid-template-columns:1fr}.resource-head{align-items:flex-start;flex-direction:column}.baseline-pill{text-align:left;white-space:normal}}
|
||||
@media(max-width:720px){.settings-grid{grid-template-columns:1fr}.settings-head{align-items:stretch;flex-direction:column}.settings-actions{display:grid;grid-template-columns:1fr 1fr}.settings-box{max-height:calc(100vh - 24px)}}
|
||||
@@ -364,7 +364,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
<div class="dialog-box" role="dialog" aria-modal="true" aria-labelledby="customDialogTitle">
|
||||
<h3 id="customDialogTitle">확인</h3>
|
||||
<p id="customDialogMessage" class="dialog-message"></p>
|
||||
<input id="customDialogInput" class="dialog-input" type="text" autocomplete="off" hidden>
|
||||
<input id="customDialogInput" name="custom_dialog_input" class="dialog-input" type="text" autocomplete="off" hidden>
|
||||
<div class="dialog-actions">
|
||||
<button id="customDialogCancel" class="btn secondary" type="button">취소</button>
|
||||
<button id="customDialogOk" class="btn" type="button">확인</button>
|
||||
|
||||
Reference in New Issue
Block a user