202 lines
10 KiB
PHP
202 lines
10 KiB
PHP
@extends('admin.layouts.app')
|
|
|
|
@section('title', $mode === 'create' ? '메일 템플릿 생성' : '메일 템플릿 수정')
|
|
@section('page_title', $mode === 'create' ? '메일 템플릿 생성' : '메일 템플릿 수정')
|
|
@section('page_desc', '스킨을 선택하고 제목/본문을 작성한 뒤, 발송 화면에서 바로 적용할 수 있습니다.')
|
|
|
|
@push('head')
|
|
<style>
|
|
.grid{display:grid; grid-template-columns: 1fr 1fr; gap:16px;}
|
|
@media (max-width: 1100px){ .grid{grid-template-columns:1fr;} }
|
|
|
|
.btn{padding:8px 12px;font-size:13px;border-radius:12px;line-height:1.1;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;
|
|
border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.06);color:inherit;cursor:pointer;}
|
|
.btn:hover{background:rgba(255,255,255,.10);}
|
|
.btn--primary{background:rgba(59,130,246,.88);border-color:rgba(59,130,246,.95);color:#fff;}
|
|
.btn--ghost{background:transparent;}
|
|
|
|
.mono{padding:4px 8px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.10);
|
|
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;}
|
|
|
|
.previewBox{border:1px solid rgba(255,255,255,.10); background:rgba(0,0,0,.10); border-radius:14px; overflow:hidden;}
|
|
.previewHead{display:flex; justify-content:space-between; align-items:center; padding:10px 12px; border-bottom:1px solid rgba(255,255,255,.08);}
|
|
.previewBody{padding:12px;}
|
|
.emailFrame{background:#fff; color:#111; border-radius:12px; padding:14px;}
|
|
.emailFrame.dark{background:#0b1220; color:#e5e7eb;}
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
<form method="POST" action="{{ $mode === 'create' ? route('admin.mail.templates.store') : route('admin.mail.templates.update', ['id'=>$tpl->id]) }}">
|
|
@csrf
|
|
@if($mode !== 'create')
|
|
@method('PUT')
|
|
@endif
|
|
|
|
<div class="grid">
|
|
<div class="a-card" style="padding:16px;">
|
|
@if($mode === 'create')
|
|
<div style="margin-bottom:12px;">
|
|
<div class="a-muted" style="margin-bottom:6px;">Code (unique)</div>
|
|
<input class="a-input" name="code" value="{{ old('code') }}" placeholder="ex) event_seol_2026">
|
|
<div class="a-muted" style="margin-top:6px;">영문/숫자/대시/언더바 3~60</div>
|
|
</div>
|
|
@else
|
|
<div style="margin-bottom:12px;">
|
|
<div class="a-muted" style="margin-bottom:6px;">Code</div>
|
|
<div><span class="mono">{{ $tpl->code }}</span></div>
|
|
</div>
|
|
@endif
|
|
|
|
<div style="margin-bottom:12px;">
|
|
<div class="a-muted" style="margin-bottom:6px;">제목(관리용)</div>
|
|
<input class="a-input" name="title" id="titleText" value="{{ old('title', $tpl->title ?? '') }}">
|
|
</div>
|
|
|
|
<div style="margin-bottom:12px;">
|
|
<div class="a-muted" style="margin-bottom:6px;">메일 제목(Subject)</div>
|
|
<input class="a-input" name="subject" id="subjectText" value="{{ old('subject', $tpl->subject_tpl ?? '') }}" placeholder="예) [PIN FOR YOU] 설맞이 혜택 안내">
|
|
</div>
|
|
|
|
<div style="margin-bottom:12px;">
|
|
<div class="a-muted" style="margin-bottom:6px;">스킨</div>
|
|
<select class="a-input" name="skin_key" id="skinKey">
|
|
@foreach(($skins ?? []) as $sk)
|
|
<option value="{{ $sk['key'] }}" @selected(old('skin_key', $tpl->skin_key ?? 'clean') === $sk['key'])>
|
|
{{ $sk['label'] }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
<div class="a-muted" style="font-size:12px; margin-top:6px;">발송 시 선택된 스킨으로 렌더링됩니다.</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom:12px;">
|
|
<div class="a-muted" style="margin-bottom:6px;">본문</div>
|
|
<textarea class="a-input" name="body" id="bodyText" rows="10">{{ old('body', $tpl->body_tpl ?? '') }}</textarea>
|
|
|
|
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:10px;">
|
|
<button type="button" class="btn btn--ghost" data-insert="{_text_02_}">{_text_02_}</button>
|
|
<button type="button" class="btn btn--ghost" data-insert="{_text_03_}">{_text_03_}</button>
|
|
<button type="button" class="btn btn--ghost" data-insert="{_text_04_}">{_text_04_}</button>
|
|
<span class="a-muted" style="font-size:12px;">토큰 빠른 삽입</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom:12px;">
|
|
<div class="a-muted" style="margin-bottom:6px;">설명</div>
|
|
<input class="a-input" name="description" value="{{ old('description', $tpl->description ?? '') }}">
|
|
</div>
|
|
|
|
<div style="margin-bottom:16px;">
|
|
<label class="a-pill">
|
|
<input type="checkbox" name="is_active" value="1" @checked((int)old('is_active', $tpl->is_active ?? 1) === 1)>
|
|
활성
|
|
</label>
|
|
</div>
|
|
|
|
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
|
<button class="btn btn--primary" type="submit">저장</button>
|
|
<a class="btn" href="{{ route('admin.mail.templates.index') }}">목록</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="a-card" style="padding:16px;">
|
|
<div class="previewBox">
|
|
<div class="previewHead">
|
|
<div class="a-muted">미리보기</div>
|
|
<div class="a-muted" style="font-size:12px;">스킨 + 현재 입력값</div>
|
|
</div>
|
|
<div class="previewBody">
|
|
<iframe id="tplPreviewIframe"
|
|
style="width:100%; height:720px; border:0; border-radius:12px; background:#fff;"></iframe>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="a-muted" style="font-size:12px; margin-top:10px; line-height:1.6;">
|
|
* 1차 버전은 SMTP “접수 성공” 기준으로 완료 처리합니다.<br>
|
|
* 안정화 후 SES 이벤트(Delivery/Bounce) 연동해 정확도 올리면 됩니다.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
@push('scripts')
|
|
<script>
|
|
(() => {
|
|
const subjectText = document.getElementById('subjectText');
|
|
const bodyText = document.getElementById('bodyText');
|
|
const skinKeyEl = document.getElementById('skinKey');
|
|
const pvSubject = document.getElementById('pvSubject');
|
|
const pvBody = document.getElementById('pvBody');
|
|
const frame = document.getElementById('previewFrame');
|
|
const PREVIEW_URL = @json(\Illuminate\Support\Facades\Route::has('admin.mail.preview') ? route('admin.mail.preview') : '');
|
|
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
const iframe = document.getElementById('tplPreviewIframe');
|
|
|
|
let tmr = null;
|
|
async function renderPreview(){
|
|
if(!PREVIEW_URL || !iframe) return;
|
|
|
|
const fd = new FormData();
|
|
fd.append('skin_key', skinKeyEl?.value || 'clean');
|
|
fd.append('subject', subjectText?.value || '');
|
|
fd.append('body', bodyText?.value || '');
|
|
|
|
try{
|
|
const res = await fetch(PREVIEW_URL, {
|
|
method: 'POST',
|
|
headers: csrf ? {'X-CSRF-TOKEN': csrf} : {},
|
|
body: fd,
|
|
});
|
|
iframe.srcdoc = await res.text();
|
|
}catch(e){
|
|
iframe.srcdoc = `<div style="padding:16px;font-family:system-ui">preview fail: ${String(e)}</div>`;
|
|
}
|
|
}
|
|
|
|
function debounce(){
|
|
clearTimeout(tmr);
|
|
tmr = setTimeout(renderPreview, 250);
|
|
}
|
|
|
|
[subjectText, bodyText].forEach(el => el?.addEventListener('input', debounce));
|
|
skinKeyEl?.addEventListener('change', debounce);
|
|
renderPreview();
|
|
|
|
function escapeHtml(s){
|
|
return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
|
}
|
|
|
|
function refreshPreview(){
|
|
const sub = (subjectText?.value || '').trim() || '(제목)';
|
|
const body = (bodyText?.value || '').trim();
|
|
if(pvSubject) pvSubject.textContent = sub;
|
|
if(pvBody) pvBody.innerHTML = body ? escapeHtml(body).replace(/\n/g,'<br>') : '<span style="opacity:.7;">(본문)</span>';
|
|
|
|
const k = (skinKeyEl?.value || 'clean');
|
|
frame?.classList.toggle('dark', k === 'dark');
|
|
}
|
|
|
|
[subjectText, bodyText].forEach(el => el?.addEventListener('input', refreshPreview));
|
|
skinKeyEl?.addEventListener('change', refreshPreview);
|
|
refreshPreview();
|
|
|
|
// token insert
|
|
document.querySelectorAll('[data-insert]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const token = btn.getAttribute('data-insert') || '';
|
|
const ta = bodyText;
|
|
if(!ta) return;
|
|
const start = ta.selectionStart ?? ta.value.length;
|
|
const end = ta.selectionEnd ?? ta.value.length;
|
|
ta.value = ta.value.slice(0,start) + token + ta.value.slice(end);
|
|
ta.focus();
|
|
ta.selectionStart = ta.selectionEnd = start + token.length;
|
|
refreshPreview();
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
@endpush
|
|
@endsection
|