간단 소스 편집기
허용:
css/
,
page/
· 확장자:
html/css/js
페이지 추가
새로고침
파일 목록
css
css/app.js
css/app.js
css/style.css
css/style.css
page
page/account
계정 수동 생성 ❓
page/account/account_create.html
무료 계정 ❓
page/account/free_account_list.html
무료 계정 > 상세 정보 (탭형) ❓
page/account/free_account_view_tab.html
유료 계정 ❓
page/account/paid_account_list.html
유료 계정 상세 정보 ❓
page/account/paid_account_view.html
유료 계정 > 상세 정보 (탭형) ❓
page/account/paid_account_view_tab.html
page/main
대시보드 ❓
page/main/main.html
page/ops
관리자 추가 ❓
page/ops/admin_form.html
관리자 리스트 ❓
page/ops/admin_list.html
관리자 수정 ❓
page/ops/admin_modify.html
API 로그 ❓
page/ops/api_log.html
메일발송 로그 ❓
page/ops/mail_log.html
SMS 발송 로그 ❓
page/ops/sms_log.html
page/payment
page/payment/customer_pay_log.html
page/payment/customer_pay_log.html
page/payment/customer_refund_list.html
page/payment/customer_refund_list.html
도메인 SSL 연장 로그 ❓
page/payment/domain_log.html
결제 리스트 ❓
page/payment/pay_log.html
포인트 사용 로그 ❓
page/payment/point_log.html
환불 등록 ❓
page/payment/refund_form.html
환불 리스트 ❓
page/payment/refund_list.html
세금/현금영수증 ❓
page/payment/tax_doc.html
page/payment/user_pay_log.html
page/payment/user_pay_log.html
page/payment/user_refund_log.html
page/payment/user_refund_log.html
page/product
상품 등록 ❓
page/product/product_form.html
상품 리스트 ❓
page/product/product_list.html
상품 상세 ❓
page/product/product_view.html
page/settle
계정 정산 리스트 ❓
page/settle/month_list.html
page/settle/settle_log.html
page/settle/settle_log.html
page/sidebar_menu.html
page/sidebar_menu.html
page/sidebar_menu.html
page/site
FAQ 등록 ❓
page/site/faq_form.html
FAQ ❓
page/site/faq_list.html
공지사항 등록 ❓
page/site/notice_form.html
공지사항 ❓
page/site/notice_list.html
공지사항 상세 ❓
page/site/notice_view.html
페이지 추가 ❓
page/site/page_form.html
페이지 관리 ❓
page/site/page_list.html
페이지 수정 ❓
page/site/page_modify.html
팝업창 수정 ❓
page/site/popup_form.html
팝업창 ❓
page/site/popup_list.html
page/site/popup_modify.html
page/site/popup_modify.html
1:1 문의 상세 ❓
page/site/qna_form.html
1:1 문의 ❓
page/site/qna_list.html
SEO 관리 ❓
page/site/seo.html
page/stats
page/stats/account.html
page/stats/account.html
page/stats/sales.html
page/stats/sales.html
page/stats/visit.html
page/stats/visit.html
편집
page/ops/mail_log.html
<!-- /page/ops/mail_log.html --> <style> /* 검색영역 전용: 세로 1열(상하 1레코드) + 라벨/입력 좌우 분리 */ .filter-stack { display:flex; flex-direction:column; gap:.65rem; } .filter-rowline { display:flex; gap:12px; align-items:flex-start; } .filter-rowline .filter-label { width:140px; padding-top:6px; font-weight:600; color:#374151; white-space:nowrap; } .filter-rowline .filter-input { flex:1; min-width:0; text-align:left; } .chk-row { display:flex; flex-wrap:wrap; gap:.55rem .9rem; } .chk-row .form-check { margin:0; } .filter-actions { display:flex; justify-content:center; margin-top:12px; } .filter-actions .btn { min-width:180px; font-weight:700; } .table-maillog td, .table-maillog th { white-space:nowrap; } .code-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:.9rem; } .subnote { font-size:.85rem; color:#6b7280; } /* ===== 레이어 팝업(기존 tax_doc 톤) ===== */ .ui-layer-backdrop{ position:fixed; inset:0; background:rgba(0,0,0,.45); display:none; z-index:1050; } .ui-layer{ position:fixed; left:50%; top:50%; transform:translate(-50%,-50%); width:min(980px, calc(100vw - 24px)); max-height:calc(100vh - 24px); overflow:auto; display:none; z-index:1060; } .table-maildetail td, .table-maildetail th { white-space:nowrap; } </style> <div class="d-flex align-items-center justify-content-between mb-3"> <div> <h1 class="h4 mb-1">메일발송 로그 <button type="button" class="btn btn-link btn-sm p-0 ms-2 align-baseline" onclick="openPageHelp(this)" data-title="메일발송 로그"> ❓ </button> </h1> <div class="text-muted">캠페인(발송 묶음) 단위로 발송 결과와 포인트 소진 내역을 조회합니다.</div> </div> </div> <div class="card mb-3 filter-card"> <div class="card-body"> <div class="filter-stack"> <!-- 키워드 검색 --> <div class="filter-rowline"> <div class="filter-label">키워드 검색</div> <div class="filter-input"> <div class="d-flex gap-2 flex-wrap"> <select class="form-select" style="max-width:180px;" id="kwField"> <option value="account_id">계정 아이디</option> <option value="email">이메일</option> </select> <input type="text" class="form-control" placeholder="검색어를 입력하세요" id="kwText" maxlength="60" style="max-width:600px;" /> </div> </div> </div> <!-- 기간 --> <div class="filter-rowline"> <div class="filter-label">발송기간</div> <div class="filter-input"> <div class="d-flex gap-2 flex-wrap"> <input type="date" class="form-control" style="max-width:200px;" id="useFrom" /> <div class="d-flex align-items-center text-muted">~</div> <input type="date" class="form-control" style="max-width:200px;" id="useTo" /> <div class="text-muted d-flex align-items-center">* 기본값은 빈 값입니다.</div> </div> </div> </div> <!-- 처리상태 --> <div class="filter-rowline"> <div class="filter-label">처리상태</div> <div class="filter-input"> <div class="chk-row" data-group="status"> <div class="form-check"> <input class="form-check-input" type="checkbox" id="st_all" checked> <label class="form-check-label" for="st_all">전체</label> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" id="st_ok" checked> <label class="form-check-label" for="st_ok">성공</label> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" id="st_fail" checked> <label class="form-check-label" for="st_fail">실패</label> </div> </div> </div> </div> <!-- 과금 --> <div class="filter-rowline"> <div class="filter-label">과금</div> <div class="filter-input"> <div class="chk-row" data-group="billing"> <div class="form-check"> <input class="form-check-input" type="checkbox" id="bi_all" checked> <label class="form-check-label" for="bi_all">전체</label> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" id="bi_paid" checked> <label class="form-check-label" for="bi_paid">유료</label> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" id="bi_free" checked> <label class="form-check-label" for="bi_free">무료</label> </div> </div> </div> </div> <!-- 버튼 --> <div class="filter-actions"> <div class="d-flex gap-2"> <button class="btn btn-primary" type="button">검색</button> <button class="btn btn-outline-secondary" type="button">조건 초기화</button> </div> </div> </div> </div> </div> <!-- 리스트 --> <div class="card"> <div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2"> <div class="text-muted"> 검색결과 <span class="fw-semibold">24</span>건 / 총 <span class="fw-semibold">3,120</span>건 <span class="ms-2 subnote">* 1행 = 1회 발송(캠페인/묶음) 단위</span> </div> <div class="d-flex align-items-center gap-2"> <div class="text-muted small">리스팅 개수</div> <select class="form-select form-select-sm" style="width:120px;"> <option value="10">10</option> <option value="20" selected>20</option> <option value="30">30</option> <option value="50">50</option> </select> </div> </div> <div class="card-body"> <div class="table-responsive"> <table class="table table-hover align-middle mb-0 table-maillog"> <thead> <tr class="text-muted text-center"> <th style="width:70px;">번호</th> <th>계정 아이디</th> <th>발송시각</th> <th>발송결과</th> <th>발송 수</th> <th>소진 포인트</th> <th>발송유형</th> <th>발신자 이메일</th> <th style="width:90px;">상세</th> </tr> </thead> <tbody class="text-center"> <tr> <td class="fw-semibold">10</td> <td class="code-mono">academy01</td> <td>2026-02-13 10:33:21</td> <td><span class="badge text-bg-success">성공 2,398건</span> <span class="badge text-bg-danger">실패 2건</span></td> <td class="code-mono">2,398</td> <td class="text-end">155.87</td> <td><span class="badge text-bg-light">캠페인</span></td> <td class="code-mono">no-reply@academy01.com</td> <td><button class="btn btn-outline-secondary btn-sm" type="button" onclick="openMailLayer('MAIL-202602-0010')">상세</button></td> </tr> <tr> <td class="fw-semibold">9</td> <td class="code-mono">demo002</td> <td>2026-02-12 18:05:10</td> <td><span class="badge text-bg-success">성공 312건</span> <span class="badge text-bg-danger">실패 0건</span></td> <td class="code-mono">312</td> <td class="text-center"><span class="badge text-bg-secondary">무료</span></td> <td><span class="badge text-bg-light">테스트</span></td> <td class="code-mono">test@demo.com</td> <td><button class="btn btn-outline-secondary btn-sm" type="button" onclick="openMailLayer('MAIL-TEST-202602-0009')">상세</button></td> </tr> <tr> <td class="fw-semibold">8</td> <td class="code-mono">shop777</td> <td>2026-02-11 09:41:03</td> <td><span class="badge text-bg-success">성공 980건</span> <span class="badge text-bg-danger">실패 14건</span></td> <td class="code-mono">980</td> <td class="text-end">63.70</td> <td><span class="badge text-bg-light">자동</span></td> <td class="code-mono">no-reply@shop777.com</td> <td><button class="btn btn-outline-secondary btn-sm" type="button" onclick="openMailLayer('AUTO-202602-0108')">상세</button></td> </tr> <tr> <td class="fw-semibold">7</td> <td class="code-mono">demo001</td> <td>2026-02-10 22:19:44</td> <td><span class="badge text-bg-success">성공 48건</span> <span class="badge text-bg-danger">실패 1건</span></td> <td class="code-mono">48</td> <td class="text-end">3.12</td> <td><span class="badge text-bg-light">캠페인</span></td> <td class="code-mono">notice@demo001.com</td> <td><button class="btn btn-outline-secondary btn-sm" type="button" onclick="openMailLayer('MAIL-202602-0007')">상세</button></td> </tr> <tr> <td class="fw-semibold">6</td> <td class="code-mono">academy01</td> <td>2026-02-09 08:31:47</td> <td><span class="badge text-bg-success">성공 1,020건</span> <span class="badge text-bg-danger">실패 0건</span></td> <td class="code-mono">1,020</td> <td class="text-end">66.30</td> <td><span class="badge text-bg-light">자동</span></td> <td class="code-mono">no-reply@academy01.com</td> <td><button class="btn btn-outline-secondary btn-sm" type="button" onclick="openMailLayer('AUTO-202602-0106')">상세</button></td> </tr> </tbody> </table> </div> <nav class="mt-3 d-flex justify-content-center" aria-label="pagination"> <ul class="pagination pagination-sm mb-0"> <li class="page-item disabled"><a class="page-link" href="#">이전</a></li> <li class="page-item active"><a class="page-link" href="#">1</a></li> <li class="page-item"><a class="page-link" href="#">2</a></li> <li class="page-item"><a class="page-link" href="#">3</a></li> <li class="page-item"><a class="page-link" href="#">다음</a></li> </ul> </nav> </div> </div> <!-- ===== 상세 레이어(캠페인 내 실제 발송 리스트) ===== --> <div id="mailBackdrop" class="ui-layer-backdrop" onclick="closeMailLayer()"></div> <div id="mailLayer" class="ui-layer"> <div class="card"> <div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2"> <div class="fw-semibold">메일 발송 상세</div> <div class="d-flex gap-2"> <button class="btn btn-outline-secondary btn-sm" type="button" onclick="closeMailLayer()">닫기</button> </div> </div> <div class="card-body"> <!-- 1) 상단 요약: 한 줄 배치 --> <div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3"> <div class="d-flex flex-wrap gap-3 align-items-center"> <div class="text-muted small">캠페인 ID <span class="ms-1 code-mono text-dark" id="d_campaignId">-</span> /</div> <div class="text-muted small">계정 <span class="ms-1 code-mono text-dark" id="d_accountId">-</span> /</div> <div class="text-muted small">발송시각 <span class="ms-1 text-dark" id="d_sentAt">-</span> /</div> <div class="text-muted small">발신자 <span class="ms-1 code-mono text-dark" id="d_from">-</span></div> </div> <div class="text-muted small"> <span class="badge text-bg-success" id="d_okBadge">성공 0건</span> <span class="badge text-bg-danger ms-1" id="d_failBadge">실패 0건</span> <span class="ms-2">· 발송 수 <span class="fw-semibold" id="d_sendCnt">0</span></span> <span class="ms-2">· 소진 <span class="fw-semibold" id="d_point">0.00</span></span> </div> </div> <!-- 2) 수신자 이메일 검색 UI --> <div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-2"> <div class="text-muted"> 검색된 발송 <span class="fw-semibold" id="d_totalRow">0</span>건 </div> <div class="d-flex gap-2 flex-wrap align-items-center"> <input id="d_to_kw" type="text" class="form-control form-control-sm" style="width:min(320px, 70vw);" placeholder="수신자 이메일 입력 (부분검색)"> <button class="btn btn-primary btn-sm" type="button" onclick="filterDetailRows()">검색</button> <button class="btn btn-outline-secondary btn-sm" type="button" onclick="resetDetailRows()">초기화</button> </div> </div> <!-- 3) 메시지 ID 제거 + 4) 우측 끝 에러로그 버튼 --> <div class="table-responsive"> <table class="table table-hover align-middle mb-0 table-maildetail"> <thead class="text-muted text-center"> <tr> <th style="width:60px;">번호</th> <th>수신자</th> <th style="width:90px;">상태</th> <th>처리시각</th> <th>실패사유</th> <th style="width:110px;">에러로그</th> </tr> </thead> <tbody id="d_rows" class="text-center"> <!-- JS로 채움 --> </tbody> </table> </div> <!-- 5) 페이지네이션 --> <nav class="mt-3 d-flex justify-content-center" aria-label="pagination"> <ul class="pagination pagination-sm mb-0" id="d_pager"> <!-- JS로 채움(프로토타입) --> </ul> </nav> </div> </div> </div> <script> // ===== 샘플 데이터(프로토타입) ===== var MAIL_DETAIL = { "MAIL-202602-0010": { campaignId: "MAIL-202602-0010", accountId: "academy01", sentAt: "2026-02-13 10:33:21", from: "no-reply@academy01.com", ok: 2398, fail: 2, sendCnt: 2398, point: 155.87, rows: [ {no:1, to:"user01@example.com", st:"성공", at:"2026-02-13 10:33:23", reason:"-"}, {no:2, to:"user02@example.com", st:"성공", at:"2026-02-13 10:33:23", reason:"-"}, {no:3, to:"user03@example.com", st:"실패", at:"2026-02-13 10:33:24", reason:"주소오류"}, {no:4, to:"user04@example.com", st:"성공", at:"2026-02-13 10:33:24", reason:"-"}, {no:5, to:"user05@example.com", st:"실패", at:"2026-02-13 10:33:25", reason:"수신거부"}, {no:6, to:"user06@example.com", st:"성공", at:"2026-02-13 10:33:25", reason:"-"} ] }, "MAIL-TEST-202602-0009": { campaignId: "MAIL-TEST-202602-0009", accountId: "demo002", sentAt: "2026-02-12 18:05:10", from: "test@demo.com", ok: 312, fail: 0, sendCnt: 312, point: 0, rows: [ {no:1, to:"tester01@example.com", st:"성공", at:"2026-02-12 18:05:11", reason:"-"}, {no:2, to:"tester02@example.com", st:"성공", at:"2026-02-12 18:05:11", reason:"-"}, {no:3, to:"tester03@example.com", st:"성공", at:"2026-02-12 18:05:12", reason:"-"} ] }, "AUTO-202602-0108": { campaignId: "AUTO-202602-0108", accountId: "shop777", sentAt: "2026-02-11 09:41:03", from: "no-reply@shop777.com", ok: 980, fail: 14, sendCnt: 980, point: 63.70, rows: [ {no:1, to:"buyer01@example.com", st:"성공", at:"2026-02-11 09:41:05", reason:"-"}, {no:2, to:"buyer02@example.com", st:"실패", at:"2026-02-11 09:41:05", reason:"SMTP 거절"}, {no:3, to:"buyer03@example.com", st:"성공", at:"2026-02-11 09:41:06", reason:"-"}, {no:4, to:"buyer04@example.com", st:"실패", at:"2026-02-11 09:41:06", reason:"주소오류"} ] }, "MAIL-202602-0007": { campaignId: "MAIL-202602-0007", accountId: "demo001", sentAt: "2026-02-10 22:19:44", from: "notice@demo001.com", ok: 48, fail: 1, sendCnt: 48, point: 3.12, rows: [ {no:1, to:"usera@example.com", st:"성공", at:"2026-02-10 22:19:45", reason:"-"}, {no:2, to:"userb@example.com", st:"성공", at:"2026-02-10 22:19:45", reason:"-"}, {no:3, to:"userc@example.com", st:"실패", at:"2026-02-10 22:19:46", reason:"수신거부"} ] }, "AUTO-202602-0106": { campaignId: "AUTO-202602-0106", accountId: "academy01", sentAt: "2026-02-09 08:31:47", from: "no-reply@academy01.com", ok: 1020, fail: 0, sendCnt: 1020, point: 66.30, rows: [ {no:1, to:"member01@example.com", st:"성공", at:"2026-02-09 08:31:49", reason:"-"}, {no:2, to:"member02@example.com", st:"성공", at:"2026-02-09 08:31:49", reason:"-"}, {no:3, to:"member03@example.com", st:"성공", at:"2026-02-09 08:31:50", reason:"-"} ] } }; // ===== 레이어 내부 상태(검색/페이지네이션) ===== var _currentDetail = null; var _detailAllRows = []; var _detailFiltered = []; var _detailPage = 1; var _detailPageSize = 10; function el(id){ return document.getElementById(id); } function renderPager(total, page, pageSize){ var totalPages = Math.max(1, Math.ceil(total / pageSize)); if(page > totalPages) page = totalPages; var html = ''; html += '<li class="page-item ' + (page<=1?'disabled':'') + '">' + '<a class="page-link" href="#" onclick="return gotoDetailPage(' + (page-1) + ')">이전</a></li>'; // 프로토타입: 최대 5개만 var start = Math.max(1, page - 2); var end = Math.min(totalPages, start + 4); start = Math.max(1, end - 4); for(var p=start; p<=end; p++){ html += '<li class="page-item ' + (p===page?'active':'') + '">' + '<a class="page-link" href="#" onclick="return gotoDetailPage(' + p + ')">' + p + '</a></li>'; } html += '<li class="page-item ' + (page>=totalPages?'disabled':'') + '">' + '<a class="page-link" href="#" onclick="return gotoDetailPage(' + (page+1) + ')">다음</a></li>'; el('d_pager').innerHTML = html; } function renderDetailRows(rows){ var rowsEl = el('d_rows'); rowsEl.innerHTML = ''; var start = (_detailPage - 1) * _detailPageSize; var slice = (rows || []).slice(start, start + _detailPageSize); slice.forEach(function(r){ var ok = (r.st === '성공'); var stBadge = ok ? '<span class="badge text-bg-success">성공</span>' : '<span class="badge text-bg-danger">실패</span>'; var reason = (r.reason && r.reason !== '-') ? r.reason : '<span class="text-muted">-</span>'; var errBtn = ok ? '<span class="text-muted">-</span>' : '<button class="btn btn-outline-secondary btn-sm" type="button" onclick="openMailErrorLog(' + r.no + ')">에러로그</button>'; rowsEl.insertAdjacentHTML('beforeend', '<tr>' + '<td class="fw-semibold">' + r.no + '</td>' + '<td class="code-mono text-start">' + r.to + '</td>' + '<td>' + stBadge + '</td>' + '<td>' + r.at + '</td>' + '<td class="text-start">' + reason + '</td>' + '<td>' + errBtn + '</td>' + '</tr>' ); }); renderPager((rows||[]).length, _detailPage, _detailPageSize); } function gotoDetailPage(p){ if(p < 1) p = 1; _detailPage = p; renderDetailRows(_detailFiltered); return false; } function filterDetailRows(){ if(!_currentDetail) return; var kw = (el('d_to_kw').value || '').trim().toLowerCase(); _detailPage = 1; if(!kw){ _detailFiltered = _detailAllRows.slice(); renderDetailRows(_detailFiltered); return; } _detailFiltered = _detailAllRows.filter(function(r){ return (r.to || '').toLowerCase().indexOf(kw) !== -1; }); renderDetailRows(_detailFiltered); } function resetDetailRows(){ el('d_to_kw').value = ''; _detailPage = 1; _detailFiltered = _detailAllRows.slice(); renderDetailRows(_detailFiltered); } // 에러로그 버튼(프로토타입) function openMailErrorLog(no){ alert('프로토타입: 에러로그 레이어를 띄웁니다. (row #' + no + ')'); return false; } function openMailLayer(campaignId){ var d = MAIL_DETAIL[campaignId] || null; if(!d){ alert('프로토타입: 상세 데이터가 없습니다.'); return; } _currentDetail = d; el('d_campaignId').textContent = d.campaignId; el('d_accountId').textContent = d.accountId; el('d_sentAt').textContent = d.sentAt; el('d_from').textContent = d.from; el('d_okBadge').textContent = '성공 ' + (d.ok||0) + '건'; el('d_failBadge').textContent = '실패 ' + (d.fail||0) + '건'; el('d_sendCnt').textContent = (d.sendCnt||0); var pt = (d.point||0); el('d_point').textContent = (pt===0 ? '무료' : pt.toFixed(2)); el('d_totalRow').textContent = (d.rows ? d.rows.length : 0); // 검색/페이지 상태 초기화 el('d_to_kw').value = ''; _detailAllRows = (d.rows || []).slice(); _detailFiltered = _detailAllRows.slice(); _detailPage = 1; renderDetailRows(_detailFiltered); // ✅ 전역변수 사용 금지(호환성 문제 제거) el('mailBackdrop').style.display='block'; el('mailLayer').style.display='block'; document.body.style.overflow='hidden'; } function closeMailLayer(){ el('mailBackdrop').style.display='none'; el('mailLayer').style.display='none'; document.body.style.overflow=''; } document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ closeMailLayer(); } }); </script>
저장
페이지 추가
디렉토리
page (root)
page/account
page/main
page/marketing
page/ops
page/payment
page/product
page/settle
page/site
page/stats
* page/ 하위 1레벨 폴더만 선택
파일명
.html
* 영문 소문자/숫자/_/- 만, 최대 32자
비밀번호