Files

573 lines
81 KiB
HTML
Raw Permalink Normal View History

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>관리자 콘솔 — AI Webmaker</title>
<link rel="stylesheet" href="/admin.css">
</head>
<body>
<div class="adm">
<header class="adm-hdr">
<div class="brand"><span class="logo">⚙</span><h1>관리자 콘솔</h1><span class="muted" id="ver"></span><span class="muted" id="who"></span></div>
<nav class="tabs">
<button type="button" class="active" data-tab="sites">사이트 관리</button>
<button type="button" data-tab="errors">로그 보기</button>
<button type="button" data-tab="billing">회원 · 청구 <span id="apBadge" class="badge-n hidden" title="가입 승인 대기"></span></button>
<button type="button" data-tab="deploy">배포 서버</button>
<button type="button" data-tab="feedback">사용자 문의 <span id="fbBadge" class="badge-n hidden"></span></button>
<button type="button" data-tab="recovery" title="배포·도메인 구매·메일 계정·인증서·데이터 파일 중 정상 완료되지 않은 작업 (v1.6.31)">운영 복구 <span id="rcBadge" class="badge-n hidden"></span></button>
<button type="button" data-tab="audit" title="설정 변경·계정·접속 정보·비용 작업 기록 (v1.6.31)">작업 기록</button>
<button type="button" data-tab="templates">템플릿</button>
<button type="button" data-tab="mail">메일 시스템</button>
<button type="button" data-tab="search">검색 등록 <span id="srBadge" class="badge-n hidden"></span></button>
<button type="button" data-tab="i18n">언어</button>
<button type="button" data-tab="theme">화면 설정</button>
<button type="button" data-tab="ai" title="LLM 연결(서버 로그인·OAuth 토큰·API 키)·사용할 모델 선택·연결 테스트·AI 제공자 — 여기서 켠 모델만 등급·내부 호출에 지정할 수 있습니다">AI 연결</button>
<button type="button" data-tab="settings">설정</button>
<button type="button" data-tab="system" title="설정 파일(data/config.json)·연동 서비스(Gitea·Arimail)·AI 엔진 상태 — 초기 설정 페이지와 같은 화면">시스템 설정</button>
</nav>
<div class="spacer"></div>
<span class="muted" id="reloadInfo"></span><button type="button" id="btnReload" class="ghost" title="지금 보고 있는 탭을 벗어나지 않고 화면의 모든 데이터를 다시 불러옵니다">새로고침</button>
<span class="lang-switch" style="display:none"><select id="langSelect" title="Language" style="width:auto;margin:0"></select></span>
<button type="button" id="btnLogout" class="ghost">로그아웃</button>
</header>
<div class="ver-bar hidden" id="verBar"></div>
<div class="kpi" id="kpi"></div>
<section class="card tab" id="tab-sites">
<h2>모든 사이트 <span class="muted" id="siteCount"></span>
<span class="spacer"></span>
<span class="tabs" style="display:inline-flex"><button type="button" class="active" data-filter="all">전체</button><button type="button" data-filter="pending">배포 대기중</button><button type="button" data-filter="deployed">배포 완료</button><button type="button" data-filter="working">작업 중</button></span>
</h2>
<div id="deployStatus" class="deploy-status hidden"></div>
<div class="table-wrap"><table class="adm-t" id="sitesTable"><thead><tr><th>배포 상태</th><th>마지막 변경</th><th>사이트</th><th>소유자</th><th>도메인</th><th>배포 위치</th><th>방식</th><th>단계</th><th>작업 상태</th><th>명령</th><th>오류</th><th>토큰</th><th>비용(USD)</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<p class="muted">배포 위치: 사이트마다 배포할 서버를 지정합니다(★ 는 기본 배포 서버). 사용자 화면에는 서버 이름이 보이지 않으며, 사용자의 "최종 확정(발행)"과 재배포는 여기서 지정한 서버로 갑니다. <span id="statsCollectLine"></span> <button type="button" class="ghost" id="btnStatsCollect">접속 통계 지금 수집</button></p>
</section>
<div id="deployDialog" class="dlg hidden"><div class="dlg-box">
<h2 id="ddTitle">첫 배포 정보 입력</h2>
<div class="muted" id="ddSite"></div>
<div class="field-label">배포 서버</div><select id="ddTarget"></select>
<div class="field-label">연결할 도메인 (선택, 쉼표로 여러 개 — 예: www.example.com, example.com)</div><input id="ddDomains" placeholder="도메인 없이 배포하려면 비워 두세요">
<label class="radio"><input type="checkbox" id="ddSsl" checked> Let's Encrypt SSL 자동 발급 (DNS 가 배포 서버를 가리키는 도메인만)</label>
<p class="muted" id="ddNote">배포 후에는 같은 서버로의 재배포가 정보 입력 없이 바로 진행됩니다.</p>
<div class="row"><button type="button" id="ddGo" class="primary">이 정보로 배포</button><button type="button" id="ddCancel" class="ghost">취소</button><span id="ddMsg" class="muted"></span></div>
</div></div>
<section class="card tab hidden" id="tab-detail">
<h2><span id="dTitle"></span></h2>
<div class="row">
<a id="dOpen" class="btn ghost" target="_blank" rel="noopener">사이트 열기</a>
<a id="dReport" class="btn ghost" target="_blank" rel="noopener">검증 리포트</a>
<button type="button" id="dCancel" class="ghost warn hidden">실행 중 작업 중단</button>
<button type="button" id="dDelete" class="ghost danger">사이트 삭제</button>
<span class="spacer"></span>
<button type="button" id="dBack" class="ghost">← 목록</button>
</div>
<div class="deploy-box">
<div class="field-label">배포</div>
<div class="row">
<span class="muted">배포 서버</span><select id="dTarget" style="width:auto;margin:0"></select>
<span class="muted">도메인</span><input id="dDomains" placeholder="www.example.com, example.com" style="width:260px;margin:0">
<button type="button" id="dDeploy" class="primary">이 서버로 지금 배포</button>
<button type="button" id="dAccess" class="ghost">접속 확인</button>
<button type="button" id="dSsl" class="ghost">SSL 발급/갱신</button>
<button type="button" id="dRollback" class="ghost warn" title="배포 서버에 보관된 직전 배포본으로 운영 주소를 되돌립니다 (v1.6.31)">이전 배포본으로</button>
</div>
<div id="dDeployResult" class="deploy-result hidden"></div>
<div class="table-wrap"><table class="adm-t" id="depTable"><thead><tr><th>일시</th><th>서버</th><th>결과</th><th>URL</th><th>내용</th><th>처리자</th></tr></thead><tbody></tbody></table></div>
</div>
<div class="grid">
<div>
<div class="field-label">명령 이력 (클릭하면 상세 로그)</div>
<div class="table-wrap"><table class="adm-t" id="cmdTable"><thead><tr><th>#</th><th>종류</th><th>모델</th><th>상태</th><th>시작</th><th>토큰</th><th>비용</th><th>오류</th></tr></thead><tbody></tbody></table></div>
<div class="field-label">버전</div>
<div class="table-wrap"><table class="adm-t" id="verTable"><thead><tr><th>#</th><th>종류</th><th>시각</th><th>내용</th></tr></thead><tbody></tbody></table></div>
<div class="field-label">기획 상태 / Brief</div>
<pre class="json" id="dPlan"></pre>
</div>
<div>
<div class="field-label">상세 로그 <span class="muted" id="dLogHint"></span></div>
<pre id="dLog" class="log"></pre>
</div>
</div>
</section>
<!-- v1.3.2: "오류 로그" → "로그 보기". 종류(오류·경고·중단·배포·완료·상세)·기간·회사(회원)·사이트·검색어로 찾아본다 -->
<section class="card tab hidden" id="tab-errors">
<h2>로그 보기 <span class="muted" id="lgSummary"></span> <span class="muted">(사용자에게는 보이지 않는 상세 원인 포함)</span></h2>
<div class="lg-filters">
<div class="lg-kinds" id="lgKinds"></div>
<div class="row">
<select id="lgRange" style="width:auto;margin:0"><option value="1">오늘</option><option value="7" selected>최근 7일</option><option value="30">최근 30일</option><option value="90">최근 90일</option><option value="all">전체</option><option value="custom">직접 지정</option></select>
<input id="lgFrom" type="date" class="hidden" style="width:auto;margin:0"><input id="lgTo" type="date" class="hidden" style="width:auto;margin:0">
<select id="lgOwner" style="width:auto;margin:0"><option value="">모든 회사(회원)</option></select>
<select id="lgSite" style="width:auto;margin:0;max-width:260px"><option value="">모든 사이트</option></select>
<input id="lgQ" placeholder="내용 검색 (예: 시간 초과, 403)" style="flex:1;min-width:180px;margin:0">
<button type="button" id="lgReset" class="ghost">조건 초기화</button>
</div>
</div>
<div class="table-wrap lg-wrap"><table class="adm-t" id="errTable"><thead><tr><th>시각</th><th>종류</th><th>회사(회원)</th><th>사이트</th><th>작업</th><th>내용</th></tr></thead><tbody></tbody></table></div>
<p class="muted" id="lgMore"></p>
</section>
<section class="card tab hidden" id="tab-billing">
<h2>모델별 사용량 · 비용</h2>
<div class="table-wrap"><table class="adm-t" id="modelTable"><thead><tr><th>모델</th><th>호출</th><th>입력</th><th>출력</th><th>캐시 생성</th><th>캐시 읽기</th><th>토큰 합계</th><th>비용(USD)</th><th>단가(입력/출력, $/1M)</th></tr></thead><tbody></tbody></table></div>
<div class="fx-line" id="fxLine"></div>
<!-- v1.6.40 (요구 1): 선불 충전 상품 · 주문 — 사용자 화면 "캐시·충전" 에서 고르는 상품(금액 + 인센티브)과, PG 미연동 상태에서 접수된 결제 대기 주문의 입금 확인(적립)·취소 -->
<h2 style="margin-top:16px">선불 충전 상품 · 주문 <span id="ppBadge" class="badge-n hidden" title="결제 대기 주문"></span></h2>
<p class="muted">사용자 화면 "캐시 · 충전" 에 보이는 선불 충전 상품입니다. 적립 캐시 = 결제 금액 + 추가 인센티브(정률 % 또는 정액). 결제 대행(PG)이 아직 연동되지 않아 사용자의 "결제 진행" 은 <b>결제 대기 주문</b>만 만듭니다 — 입금(결제)을 확인한 뒤 아래 "입금 확인 → 적립" 을 누르면 적립 금액이 캐시 원장에 더해집니다(메모에 비밀값은 적지 마세요).</p>
<div class="grid">
<div>
<div class="field-label">상품</div>
<div class="table-wrap"><table class="adm-t" id="ppTable"><thead><tr><th>이름</th><th>결제 금액</th><th>인센티브</th><th>적립 캐시</th><th>표시</th><th>순서</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<div class="field-label" style="margin-top:8px"><span id="ppFormTitle">새 상품</span></div>
<div class="charge-form pp-form">
<input type="hidden" id="ppId">
<input id="ppName" placeholder="이름 (예: 베이직 1만원)" maxlength="40">
<input id="ppAmount" type="number" min="1000" step="1000" placeholder="결제 금액(원)">
<select id="ppBonusType"><option value="percent">정률 인센티브 (%)</option><option value="fixed">정액 인센티브 (원)</option></select>
<input id="ppBonusValue" type="number" min="0" step="1" placeholder="인센티브 값">
<input id="ppSort" type="number" step="1" placeholder="표시 순서 (작을수록 먼저)" style="width:150px">
<label class="inline" style="margin:0;display:inline-flex;align-items:center;gap:4px"><input type="checkbox" id="ppActive" checked style="width:auto;margin:0"> 사용자에게 표시</label>
<input id="ppNote" placeholder="설명 (선택, 상품 카드에 보임)" maxlength="200">
<button type="button" id="ppSave" class="primary">저장</button><button type="button" id="ppReset" class="ghost">새 상품</button>
</div>
<p class="muted" id="ppMsg"></p>
</div>
<div>
<div class="field-label">충전 주문 <select id="ppOrderFilter" style="width:auto;margin:0 0 0 6px"><option value="">모든 상태</option><option value="pending" selected>결제 대기</option><option value="paid">적립 완료</option><option value="cancelled">취소</option></select></div>
<div class="table-wrap ledger-scroll"><table class="adm-t" id="ppOrders"><thead><tr><th>일시</th><th>회원</th><th>상품</th><th>결제 금액</th><th>적립</th><th>상태</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<p class="muted" id="ppOrderMsg"></p>
</div>
</div>
<h2 style="margin-top:16px">회원별 캐시 · 사용</h2>
<p class="muted">캐시(<span class="cash-unit">원</span>)는 관리자가 설정·추가하며 작업이 끝날 때마다 사용액이 차감됩니다. 캐시가 0 이하이면 새 작업을 시작할 수 없고, 진행 중 소진되면 작업이 멈췄다가 충전 시 자동으로 이어집니다. 외부 서비스도 서비스 키로 조정할 수 있습니다.</p>
<div class="table-wrap"><table class="adm-t" id="billTable"><thead><tr><th>회원</th><th>역할</th><th>사이트</th><th>토큰</th><th>비용(USD)</th><th>사용 캐시(누적)</th><th>캐시 잔액</th><th>충전 대기</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<div id="billDetail" class="hidden">
<h3 id="billDetailTitle"></h3>
<div class="grid">
<div><div class="field-label">일자별 정산</div><div class="table-wrap"><table class="adm-t" id="dayTable"><thead><tr><th>일자</th><th>명령</th><th>적용 환율</th><th>USD</th><th>원화 정산</th><th>청구</th></tr></thead><tbody></tbody></table></div></div>
<div><div class="field-label">캐시 충전 · 조정</div>
<div class="charge-form"><input id="chargeAmount" type="number" placeholder="금액(원)"><input id="chargeMemo" placeholder="메모 (예: 계좌이체 확인, 월 기본 제공)"><button type="button" id="btnCharge" class="primary">캐시 추가</button><button type="button" id="btnSetCredit" class="ghost">이 값으로 설정</button></div>
<p class="muted">"캐시 추가"는 현재 잔액에 더합니다(음수면 차감). "이 값으로 설정"은 잔액을 입력한 값으로 맞춥니다. 캐시 소진으로 멈춘 작업은 충전 즉시 이어서 진행됩니다.</p>
<div class="field-label">캐시 원장</div><div class="table-wrap ledger-scroll"><table class="adm-t" id="chargeTable"><thead><tr><th>일시</th><th>종류</th><th>금액(<span class="cash-unit">원</span>)</th><th>잔액</th><th>메모</th><th>처리자</th></tr></thead><tbody></tbody></table></div>
</div>
</div>
</div>
</section>
<section class="card tab hidden" id="tab-deploy">
<h2>배포 서버 <span class="muted">— 하나의 서비스 서버가 여러 웹호스팅 서버를 관리합니다. 기본 배포 서버는 사용자가 "최종 확정(발행)"할 때 자동으로 사용됩니다.</span></h2>
<div class="table-wrap"><table class="adm-t" id="targetTable"><thead><tr><th>기본</th><th>이름</th><th>종류</th><th>접속</th><th>사이트 저장 경로</th><th>공개 URL(선택)</th><th>키</th><th>sudo</th><th>SSL</th><th>마지막 테스트</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<!-- v1.5.13 (요구 4): 추가·수정 폼은 화면에 늘 보이지 않고 "배포 서버 추가" 또는 행의 "수정" 을 눌렀을 때만 대화상자(tDialog)로 열린다 — 실수로 고치는 일을 막고 목록이 넓게 보이도록 -->
<div class="row" style="margin-top:12px"><button type="button" id="tNew" class="primary">배포 서버 추가</button><span id="tListMsg" class="muted"></span></div>
<div style="margin-top:14px">
<h2>등록 안내</h2>
<p class="muted">1. 원격 서버를 저장한 뒤 행의 <b>키 생성</b>을 누르면 이 서비스 서버에 전용 SSH 키가 만들어지고, 공개키가 별도 창에 보입니다. <b>복사</b> 버튼으로 복사해 배포 서버의 <code>~/.ssh/authorized_keys</code> 에 한 줄로 등록하세요(v1.5.13). 이미 쓰는 키가 있으면 수정 창에서 개인키를 붙여넣어도 됩니다.</p>
<p class="muted">2. <b>연결 테스트</b>로 접속과 저장 경로 쓰기 권한을 확인합니다.</p>
<p class="muted">3. 배포 서버의 웹서버(nginx 등)가 저장 경로를 <code>공개URL/&lt;사이트ID&gt;/</code> 로 서비스하도록 설정합니다. 예시 nginx 설정은 README 를 참고하세요.</p>
<p class="muted">4. <b>기본으로</b> 지정한 서버가 사용자의 최종 확정(발행) 시 자동 배포 대상이 됩니다. 사이트별로 다른 서버를 지정하려면 사이트 상세 화면에서 선택하세요.</p>
</div>
<!-- 배포 서버 추가·수정 대화상자 (입력칸 id 는 1.5.11 까지의 인라인 폼과 같다 — admin.js fillTargetForm/tSave 가 그대로 쓴다) -->
<dialog id="tDialog" class="wm-dialog wide"><form method="dialog" id="tForm">
<h2 id="tFormTitle">배포 서버 추가</h2>
<input type="hidden" id="tId">
<label class="radio"><input type="radio" name="tType" value="ssh" checked> 원격 서버 (SSH)</label>
<label class="radio"><input type="radio" name="tType" value="local"> 이 서비스 서버의 디렉터리 (로컬)</label>
<input id="tName" placeholder="이름 (예: 호스팅 1호기) — 필수" title="비워 두면 '이름을 입력하세요' 오류로 저장되지 않습니다">
<div class="two"><input id="tHost" placeholder="서버 주소 (IP 또는 호스트명) — SSH 서버는 필수" title="비워 두면 '서버 주소(host)를 입력하세요' 오류가 나고, 잘못 적으면 연결 테스트에서 'Could not resolve' 또는 'Connection refused' 로 실패합니다"><input id="tPort" type="number" placeholder="SSH 포트 (기본 22)" data-plain></div>
<input id="tUser" placeholder="SSH 사용자 (예: deploy) — 비우면 root" title="공개키를 등록한 계정과 다르면 'Permission denied (publickey)' 로 실패합니다">
<input id="tBaseDir" placeholder="사이트 저장 경로 (예: /var/www/sites) — 필수, / 로 시작" title="비워 두거나 / 로 시작하지 않으면 저장되지 않습니다. 접속 사용자에게 쓰기 권한이 없으면 연결 테스트가 '쓰기 권한 없음' 으로 실패하고 배포가 되지 않습니다">
<input id="tPublicUrl" placeholder="공개 URL (선택, 예: https://host.example.com) — 있으면 배포 후 공개URL/사이트ID/ 링크를 표시합니다">
<input id="tNote" placeholder="메모 (선택)">
<label class="radio"><input type="checkbox" id="tVhost" checked> 배포 시 도메인 vhost 자동 설정 (배포 서버에 nginx 필요)</label>
<label class="radio"><input type="checkbox" id="tSsl" checked> 배포 시 Let's Encrypt SSL 인증서 자동 발급 (certbot, DNS 가 이 서버를 가리키는 도메인만)</label>
<label class="radio"><input type="checkbox" id="tSudo"> 이 서버에서 sudo 명령 사용 허용 (nginx 설정 쓰기·reload, certbot 실행에 필요 — 허용하지 않으면 파일만 배포)</label>
<div class="field-label">SSH 개인키 (선택) — 비워 두고 저장한 뒤 "키 생성"으로 이 서버가 키를 만들게 할 수도 있습니다</div>
<textarea id="tKey" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY----- … 붙여넣기"></textarea>
<div class="row"><button type="button" id="tSave" class="primary">저장</button><button type="button" id="tCancel" class="ghost">취소</button><span id="tMsg" class="muted"></span></div>
</form></dialog>
<!-- v1.5.13 (요구 3): 생성된 공개키는 별도 창에 한 줄로 보이고 버튼으로 복사한다 — 화면에서 직접 긁어 복사하다 줄이 잘리거나 앞뒤가 섞이는 실수를 막는다 -->
<dialog id="tKeyDialog" class="wm-dialog wide"><form method="dialog">
<h2 id="tKeyTitle">공개키</h2>
<p class="muted" id="tKeyNote">배포 서버의 <code>~/.ssh/authorized_keys</code> 에 아래 한 줄을 그대로 추가하세요 (접속 사용자의 홈). 줄바꿈 없이 한 줄입니다.</p>
<textarea id="tKeyText" rows="4" readonly style="width:100%;font-family:ui-monospace,monospace;font-size:12px;white-space:pre;overflow-x:auto"></textarea>
<div class="row"><button type="button" id="tKeyCopy" class="primary">공개키 복사</button><button type="button" id="tKeyClose" class="ghost">닫기</button><span id="tKeyMsg" class="muted"></span></div>
</form></dialog>
</section>
<!-- v1.6.31 (요구 20): 외부 작업 복구 — 정상 완료되지 않은 배포·구매·메일 계정·인증서·데이터 파일 -->
<section class="card tab hidden" id="tab-recovery">
<h2>운영 복구 <span class="muted">— 정상 완료되지 않은 외부 작업과 손상 파일</span> <button type="button" id="rcRefresh" class="ghost">새로고침</button></h2>
<p class="muted">사이트·사용자·진행 단계·실패 원인·사용자가 할 일을 보여 주고, 다시 실행할 수 있는 작업은 여기서 재시도합니다(이미 끝난 단계는 다시 하지 않습니다). 자동으로 해결할 수 없는 것은 "관리자 확인 필요" 로 표시됩니다.</p>
<div class="table-wrap"><table class="adm-t" id="rcTable"><thead><tr><th>종류</th><th>사이트 / 사용자</th><th>상태</th><th>시각</th><th>내용</th><th>할 일</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<p class="muted" id="rcMsg"></p>
</section>
<!-- v1.6.31 (요구 21): 관리자 작업 기록 -->
<section class="card tab hidden" id="tab-audit">
<h2>작업 기록 <span class="muted">— 누가 · 언제 · 어떤 사이트/사용자에 · 무엇을 (비밀값은 기록하지 않음)</span></h2>
<div class="row"><input id="auUser" placeholder="사용자 ID" style="width:140px;margin:0"><input id="auSite" placeholder="사이트 ID" style="width:200px;margin:0"><input id="auAction" placeholder="작업 (예: deploy., auth., setting)" style="width:180px;margin:0"><input id="auQ" placeholder="검색어" style="flex:1;margin:0"><button type="button" id="auSearch" class="primary">조회</button></div>
<div class="table-wrap lg-wrap"><table class="adm-t" id="auTable"><thead><tr><th>시각</th><th>누가</th><th>작업</th><th>대상</th><th>사이트</th><th>결과</th><th>내용</th></tr></thead><tbody></tbody></table></div>
</section>
<section class="card tab hidden" id="tab-feedback">
<h2>사용자 문의 <span class="muted" id="fbSummary"></span> <select id="fbTypeFilter" style="width:auto;margin:0 0 0 8px"><option value="">모든 종류</option><option value="inquiry">문의</option><option value="bug">오류</option><option value="suggestion">제안</option><option value="request">요구</option></select> <span class="spacer"></span><button type="button" class="ghost" id="fbBacklogBtn">다음 작업 반영 목록</button></h2>
<div id="fbBacklog" class="hidden card" style="margin-bottom:10px"><div class="row"><b>다음 작업 반영 목록</b><span class="muted" id="fbBacklogInfo"></span><span class="spacer"></span><button type="button" class="ghost" id="fbBacklogCopy">마크다운 복사</button><button type="button" class="ghost" id="fbBacklogClose">닫기</button></div><pre class="log" id="fbBacklogMd" style="height:auto;max-height:360px;white-space:pre-wrap"></pre></div>
<p class="muted">사용자가 올린 문의·제안·오류·요구 사항입니다. 글이 올라오면 고급 등급 모델이 종류에 맞게 1차 분석(문의: 질문 파악과 답변 초안, 오류: 추정 원인·재현 조건, 제안·요구: 필요성·작업량)을 해 댓글로 붙이며, 분석 비용은 사용자에게 청구하지 않고 여기 표시됩니다.</p>
<div class="table-wrap"><table class="adm-t" id="fbTable"><thead><tr><th>접수</th><th>종류</th><th>제목</th><th>사용자</th><th>사이트</th><th>AI 분석</th><th>분석 비용</th><th>다음 작업</th><th>상태</th></tr></thead><tbody></tbody></table></div>
<div id="fbDetail" class="hidden" style="margin-top:12px">
<h3 id="fbdTitle"></h3>
<div class="grid">
<div><div class="field-label">내용</div><pre class="log" id="fbdBody" style="height:auto;max-height:260px;white-space:pre-wrap"></pre><div class="field-label">사용 환경 (신고 시 자동 수집)</div><pre class="log" id="fbdEnv" style="height:auto;max-height:180px;white-space:pre-wrap;font-size:11px"></pre><div class="field-label">댓글 (AI 분석 · 관리자 답변)</div><div id="fbdComments"></div></div>
<div><div class="field-label">다음 작업 반영 <span class="muted">— AI 분석을 보고 결정</span></div>
<label class="radio"><input type="checkbox" id="fbdPlanned"> 다음 수정(릴리스)에 반영</label>
<textarea id="fbdPlanNote" rows="5" placeholder="반영할 내용을 구체적으로 적어 주세요. 예) 관리 탭 수정 지시 입력창 아래에 '색상 변경', '문구 수정', '섹션 추가' 예시 버튼 3개를 두고, 누르면 입력창에 예시 문장을 채운다."></textarea>
<p class="muted" id="fbdPlanInfo"></p>
<div class="field-label">처리</div><select id="fbdStatus"><option value="new">새 글 (접수)</option><option value="reviewing">검토 중 (AI 검토)</option><option value="done">처리 완료 (관리자 답변)</option></select><p class="muted" style="margin:4px 0">답변을 적어 저장하면 자동으로 "처리 완료"가 되고, 사용자 화면의 "내 제안 · 오류 신고" 목록에 답변이 표시됩니다. 제안은 반려하지 않습니다.</p><textarea id="fbdReply" rows="4" placeholder="관리자 답변 (사용자 화면의 내 제안 목록에 표시)"></textarea><div class="row"><button type="button" id="fbdSave" class="primary">저장</button><button type="button" id="fbdDraft" class="ghost hidden">AI 답변 초안 넣기</button><button type="button" id="fbdReanalyze" class="ghost">AI 다시 분석</button><span id="fbdMsg" class="muted"></span></div></div>
</div>
</div>
</section>
<section class="card tab hidden" id="tab-i18n">
<h2>언어 (다국어) <span class="muted" id="i18nSource"></span></h2>
<p class="muted">사용자 화면 문구(기본 한국어)를 다른 언어로 보여 주는 기능입니다. ① "원본 파일 내려받기"로 한국어 문구 목록(JSON)을 받아 translation 을 채우고, lang(언어 코드)·name 을 적습니다. ② 아래에 끌어다 놓아 올립니다(JSON 여러 개 또는 ZIP 가능). ③ 켜기/끄기로 노출을 조절합니다. 서비스 버전이 올라가 문구가 바뀌면 그보다 낮은 버전에서 번역한 언어는 자동으로 숨겨지며, 새 원본 파일로 다시 번역해 올리면 됩니다.</p>
<div class="row"><a class="btn ghost" id="i18nExport" href="/api/admin/i18n/export" download>원본(한국어) 파일 내려받기</a><span class="muted" id="i18nMsg"></span></div>
<div class="dropzone" id="i18nDrop" style="margin:10px 0;padding:18px;text-align:center;border:2px dashed #475569;border-radius:10px">번역 파일(JSON)이나 ZIP 을 여기에 끌어다 놓거나 <label class="dz-pick" for="i18nFiles" style="text-decoration:underline;cursor:pointer">클릭해서 선택</label><input id="i18nFiles" type="file" multiple accept=".json,.zip" class="hidden"></div>
<div id="i18nResults"></div>
<div class="table-wrap"><table class="adm-t" id="i18nTable"><thead><tr><th>언어</th><th>표시 이름</th><th>번역 항목</th><th>번역 기준 버전</th><th>올린 날짜</th><th>상태</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<!-- v1.3.16: 서비스 화면 기본 언어 (관리자 강제 → 사용자는 접속 후 자기 선호 언어로 바꿀 수 있음) -->
<h2 style="margin-top:16px">서비스 화면 기본 언어</h2>
<p class="muted">빌더·관리자 콘솔·사이트 관리자·로그인 화면의 기본 언어입니다. "모든 사용자에게 지금 적용" 을 켜고 적용하면, 그 전에 사용자가 골라 둔 언어보다 이 언어가 우선합니다. 사용자는 접속한 뒤 화면 위 언어 선택으로 자기 선호 언어를 다시 고를 수 있고, 그 선택은 계정에 저장됩니다. 표시 중인(번역이 켜진) 언어만 고를 수 있습니다.</p>
<div class="row"><select id="uiDefLang" style="width:auto;margin:0" data-dirty-watch></select><label class="radio" style="margin:0"><input type="checkbox" id="uiDefForce"> 모든 사용자에게 지금 적용(강제)</label><button type="button" id="uiDefSave" class="primary">적용</button><span id="uiDefMsg" class="muted"></span></div>
<!-- v1.3.16: 생성 사이트 시스템 문구 (locales/site/ko.json) — 위젯·폼·오류·동의문·예약·문의·메일 알림·법적 고지 -->
<h2 style="margin-top:16px">생성 사이트 시스템 문구 <span class="muted" id="smSummary"></span></h2>
<p class="muted">사이트에 들어가는 모듈 위젯·폼·오류 메시지·동의문·쿠키·법적 고지·소유자 알림 메일의 글자입니다. 코드에는 문구 id 만 있고 원문은 <code>locales/site/ko.json</code> 에 있습니다. 다국어 사이트가 어떤 언어를 처음 쓰면 AI 가 그 언어 번역을 채우며(서비스 공용), 여기서 언어별로 고칠 수 있습니다(고친 번역이 우선).</p>
<div class="row"><select id="smLang" style="width:auto;margin:0"></select><input id="smQ" placeholder="문구·id 찾기" style="width:200px;margin:0"><label class="radio" style="margin:0"><input type="checkbox" id="smMissing"> 번역 없는 것만</label><button type="button" id="smSave" class="primary">고친 번역 저장</button><span id="smMsg" class="muted"></span></div>
<div class="table-wrap" style="max-height:420px;overflow:auto"><table class="adm-t" id="smTable"><thead><tr><th>id</th><th>묶음</th><th>원문(한국어)</th><th>번역</th><th>출처</th></tr></thead><tbody></tbody></table></div>
</section>
<!-- v1.3.10: 사이트 템플릿 — 관리자가 만든 사이트를 템플릿으로, 사용자 사용 허용, 해시태그·디렉터리 -->
<section class="card tab hidden" id="tab-templates">
<h2>사이트 템플릿 <span class="muted" id="tpSummary"></span> <span class="spacer"></span><a class="btn primary" href="/builder" target="_blank" rel="noopener">빌더에서 사이트 만들기</a></h2>
<p class="muted">관리자가 빌더로 만든 사이트를 템플릿으로 등록하고, "사용자 사용 허용" 을 켠 템플릿만 사용자 생성 탭의 "템플릿에서 시작" 에 보입니다. 디렉터리는 업종(대분류 › 세부 업종) 구조를 기본으로 쓰고, 필요하면 별도 디렉터리를 만듭니다. #해시태그로 검색할 수 있습니다.</p>
<div class="grid">
<div>
<h3>디렉터리</h3>
<div id="tpTree" class="tp-tree"></div>
<div class="row" style="margin-top:6px"><input id="tpDirName" placeholder="새 디렉터리 이름" style="flex:1;margin:0"><select id="tpDirParent" style="width:auto;margin:0"></select><button type="button" id="tpDirAdd" class="ghost">만들기</button></div>
<h3 style="margin-top:14px">템플릿 등록</h3>
<label>관리자가 만든 사이트<select id="tpSite"></select></label>
<label>템플릿 이름<input id="tpTitle"></label>
<div class="row"><select id="tpDir" style="flex:1;margin:0"></select><input id="tpTags" placeholder="#카페 #미니멀" style="flex:1;margin:0"></div>
<label>설명<textarea id="tpDesc" rows="2"></textarea></label>
<div class="row"><button type="button" id="tpRegister" class="primary">템플릿으로 등록</button><span class="muted" id="tpMsg"></span></div>
</div>
<div>
<div class="row"><input id="tpQ" placeholder="#해시태그 또는 낱말로 찾기" style="flex:1;margin:0"><span class="muted" id="tpDirNow"></span></div>
<div class="table-wrap"><table class="adm-t" id="tpTable"><thead><tr><th>템플릿</th><th>디렉터리</th><th>해시태그</th><th>사용자 사용 허용</th><th>파일</th><th>담은 날</th><th>작업</th></tr></thead><tbody></tbody></table></div>
</div>
</div>
</section>
<!-- v1.3.5: 검색사이트 등록 — 사람이 해야 하는 작업(확인 코드·DNS·검색사이트에서 확인·사이트맵 제출)을 관리자가 진행 -->
<section class="card tab hidden" id="tab-search">
<h2>검색 등록 <span class="muted" id="srSummary"></span></h2>
<p class="muted">사용자가 관리 탭에서 구글·네이버 등록을 시작한 사이트입니다. 여기에는 서비스가 대신할 수 없어 <b>사람이 해야 하는 작업</b>만 보입니다(v1.3.10). 구글은 연동 키에 서비스 계정 키를 넣으면 확인 코드 받기부터 사이트맵 제출까지 모두 자동이고, 네이버는 공개 API 가 없어 "사이트 추가·확인 코드 받기" 와 "확인 누르기" 만 사람이 합니다(코드를 넣으면 태그 넣기·재배포·운영 확인·IndexNow 색인 요청은 자동). 배포가 끝난 사이트만 진행됩니다.</p>
<label class="radio"><input type="checkbox" id="srPendingOnly" checked> 수동 작업이 남은 사이트만</label>
<label class="radio"><input type="checkbox" id="srShowAuto"> 자동 단계도 보기</label>
<div id="srList"></div>
</section>
<!-- v1.3.2: 메일 시스템 — 사이트(호스팅 도메인) 단위로 켜고 설정한다 -->
<section class="card tab hidden" id="tab-mail">
<h2>메일 시스템 <span class="muted">— 사이트(호스팅 도메인)마다 켜고 끕니다</span></h2>
<p class="muted">사이트를 골라 "메일 시스템 사용" 을 누르고 메일 도메인·메일 서버 주소 등을 입력하면, 그 도메인에 등록할 DNS 레코드가 표시됩니다. 도메인 소유자가 DNS 를 등록하고 전파가 확인되면 사용자 화면이 "사용 가능" 으로 바뀌고, 그 전에는 "메일시스템 설정중" 으로 보입니다. 메일 주소는 "아이디@메일도메인" 이며 사이트 관리자 계정과 같은 계정을 씁니다.</p>
<p class="muted" id="msBackend"></p>
<!-- v1.6.38 (요구 2): 메일 서버(Arimail) 등록부 — 웹메이커와 다른 서버에 설치된 Arimail 을 여러 대 등록하고 닉네임·IP 로 구분해 사이트마다 고른다 -->
<h3>메일 서버 (Arimail) 연동 <span class="muted">— 받는 메일·웹메일·메일 계정을 맡는 서버. 다른 서버에 설치된 Arimail 도 여러 대 등록할 수 있습니다</span></h3>
<p class="muted">Arimail 한 대는 메일 도메인 하나를 다룹니다(계정 = 아이디@메일도메인). 사이트의 메일 도메인과 같은 도메인을 다루는 서버가 자동으로 쓰이고, 아래 사이트 설정에서 서버를 직접 고를 수도 있습니다. 비밀번호는 암호화 보관되며 화면·기록에는 나오지 않습니다. "기본" 서버는 도메인이 맞는 서버가 없을 때 쓰입니다. 설정 파일(mail.*)에 등록된 서버는 "기본 서버(설정 파일)" 로 함께 보입니다.</p>
<div class="table-wrap"><table class="adm-t" id="mvTable"><thead><tr><th>닉네임</th><th>서버 IP</th><th>주소 (웹메일 / API)</th><th>메일 도메인</th><th>관리자 ID</th><th>연결 확인</th><th>사용량 API</th><th>기본</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<div class="row"><button type="button" id="mvAdd" class="ghost">메일 서버 추가</button><span id="mvMsg" class="muted"></span></div>
<!-- v1.7.3 (요구 2): 메일 사용량 — Arimail 1.0.9 사용량 API(모든 도메인 요약). 표의 [사용량] 또는 [키 발급] 뒤에 열린다 -->
<div id="mvUsage" class="card hidden" style="margin-top:8px">
<div class="row"><h3 id="mvUsageTitle" style="margin:0">메일 사용량</h3><button type="button" id="mvUsageRefresh" class="ghost small">새로 받기</button><button type="button" id="mvUsageClose" class="ghost small">닫기</button></div>
<p class="muted" id="mvUsageMsg"></p>
<div class="table-wrap"><table class="adm-t" id="mvUsageTable"><thead><tr><th>메일 도메인</th><th>계정</th><th>메일함 용량</th><th>메일 수</th><th>오늘 발송/수신</th><th>이번 주</th><th>이달</th><th>이달 발송/수신 바이트</th></tr></thead><tbody></tbody></table></div>
<p class="muted small">Arimail 사용량 API(1.0.9 이상)가 준 값입니다 — 메일함 용량은 저장된 원본(.eml) 크기 합, 발송은 발송 완료 시각·수신은 받은 시각 기준, 시간대는 Arimail 통계 시간대(기본 Asia/Seoul). 5분 캐시. 같은 값이 사용자 화면(사이트 정보 › 디스크 사용량 "메일 데이터", 메일 › 한눈에 보기)에도 보입니다.</p>
</div>
<!-- v1.6.39 (요구 4): 저장된 비밀번호로 로그인이 안 될 때 — Arimail 의 다른 관리자 계정(초기 admin 등)으로 한 번 로그인해 연동 계정 비밀번호를 다시 만든다. 여기 넣은 비밀번호는 저장하지 않는다 -->
<div id="mvReset" class="card hidden" style="margin-top:8px">
<h3 id="mvRvTitle">다른 관리자 계정으로 재설정</h3>
<p class="muted">저장된 비밀번호로는 Arimail 에 로그인되지 않습니다. Arimail 관리자 페이지(/admin)에 들어갈 수 있는 <b>다른 관리자 계정</b>(예: 초기 계정 admin)의 아이디·비밀번호를 넣으면, 그 계정으로 한 번 로그인해 연동 계정의 비밀번호를 새 무작위 값으로 바꾸고 보관합니다. 여기 넣은 비밀번호는 저장하지 않습니다. 연동 계정이 Arimail 에 없으면 관리자 역할로 새로 만듭니다.</p>
<div class="row">
<input id="mvRvLogin" placeholder="Arimail 관리자 아이디 (예: admin)" autocomplete="off" style="max-width:220px">
<input id="mvRvPw" type="password" placeholder="그 계정의 비밀번호 (1회용)" autocomplete="off" style="max-width:220px">
<button type="button" id="mvRvGo" class="primary">이 계정으로 재설정</button><button type="button" id="mvRvCancel" class="ghost">닫기</button>
</div>
<p class="msg err" id="mvRvMsg"></p>
</div>
<div id="mvForm" class="card hidden" style="margin-top:8px">
<h3 id="mvFormTitle">메일 서버 추가</h3>
<div class="grid">
<div>
<label>닉네임 <span class="muted">(서버를 고를 때 IP 와 함께 표시)</span><input id="mvNick" placeholder="예: ibit 메일 서버" autocomplete="off"></label>
<label>서버 IP <span class="muted">(표시·구분용, 선택)</span><input id="mvIp" placeholder="예: 115.68.194.192" autocomplete="off"></label>
<label>Arimail 주소 (공개 — 웹메일 주소로 안내)<input id="mvUrl" placeholder="https://webmail.example.com" autocomplete="off"></label>
<label>API 주소 <span class="muted">(선택 — 서버 간 호출용. 같은 서버면 http://127.0.0.1:8790, 비우면 공개 주소)</span><input id="mvApi" placeholder="비우면 공개 주소 사용" autocomplete="off"></label>
</div>
<div>
<label>메일 도메인 <span class="muted">(Arimail 환경 MAILBOX 의 도메인과 같아야 함)</span><input id="mvDomain" placeholder="example.com" autocomplete="off"></label>
<label>관리자 로그인 ID <span class="muted">(Arimail /admin 의 관리자 역할 계정)</span><input id="mvLogin" placeholder="admin" autocomplete="off"></label>
<label>관리자 비밀번호 <span class="muted" id="mvPwNote">(새 서버는 필수)</span><input id="mvPw" type="password" placeholder="암호화 보관 — 수정 때 비우면 유지" autocomplete="new-password"></label>
<!-- v1.7.3 (요구 2): 사용량 API 키 — 보통은 비워 두고 표의 [키 발급] 으로 받는다. Arimail 전체 설정 화면에서 직접 만든 키를 붙여 넣는 칸 -->
<label>사용량 API 키 <span class="muted" id="mvUkNote">(선택)</span><input id="mvUsageKey" type="password" placeholder="au_… — Arimail 전체 설정 › 사용량 API 카드에서 만든 키 (비우면 유지)" autocomplete="off"></label>
<p class="muted small">메일 사용량 연동(Arimail 1.0.9 이상): 키는 암호화 보관되며 메일함 용량·메일 수·발송량을 읽는 데만 씁니다(읽기 전용). 표의 <b>키 발급</b>을 누르면 저장된 관리자 계정으로 Arimail 에서 키를 받아 자동으로 보관하므로 보통 이 칸은 비워 둡니다. Arimail 쪽 "접속 허용 IP" 를 쓰면 이 웹메이커 서버의 공인 IP(같은 서버면 127.0.0.1 도)를 넣어야 합니다.</p>
<p class="muted small">수정할 때 이 칸에 값을 넣으면 <b>저장된 비밀번호가 바뀝니다</b>(저장 전에 확인 창). 틀린 값으로 바꾸면 메일 계정 관리가 멈추므로, 비밀번호를 그대로 두려면 비워 두세요. 바꾼 뒤에는 표의 "이전 비밀번호로" 로 되돌릴 수 있습니다.</p>
<div class="row"><button type="button" id="mvSave" class="primary">저장</button><button type="button" id="mvCancel" class="ghost">닫기</button></div>
<p class="muted" id="mvFormMsg"></p>
</div>
</div>
</div>
<h3 style="margin-top:14px">사이트별 메일 시스템</h3>
<div class="table-wrap"><table class="adm-t" id="msSites"><thead><tr><th>사이트</th><th>회사(회원)</th><th>연결된 도메인</th><th>메일 도메인</th><th>상태</th><th>메일 계정</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<div id="msPanel" class="card hidden" style="margin-top:12px">
<h3 id="msPanelTitle"></h3>
<div class="grid">
<div>
<label>메일 도메인 <span class="muted">(메일 주소 = 아이디@이 도메인)</span><input id="msDomain" list="msDomainList" placeholder="example.com" autocomplete="off"><datalist id="msDomainList"></datalist></label>
<!-- v1.6.38 (요구 2): 이 사이트의 받는 메일·웹메일을 맡을 메일 서버 (자동 = 메일 도메인이 같은 서버 → 기본 서버) -->
<label>메일 서버 (Arimail) <span class="muted" id="msServerNote"></span><select id="msServer"></select></label>
<label>메일 서버 주소 (MX 호스트) <span class="muted" id="msMxDefault"></span><input id="msMx" placeholder="비우면 기본값 사용 (예: mail.webmaker.ibit.kr)" autocomplete="off"></label>
<div class="two"><label>SPF include (선택)<input id="msSpf" placeholder="비우면 기본값" autocomplete="off"></label><label>DMARC (선택)<input id="msDmarc" placeholder="비우면 기본값 v=DMARC1; p=none" autocomplete="off"></label></div>
<div class="row"><button type="button" id="msApply" class="primary">메일 시스템 사용 · 설정 저장</button><button type="button" id="msCheck" class="ghost">DNS 다시 확인</button><button type="button" id="msOff" class="ghost danger">이 사이트 메일 끄기</button><button type="button" id="msClose" class="ghost">닫기</button></div>
<p class="muted" id="msMsg"></p>
</div>
<div><div class="ms-status" id="msStatus"></div></div>
</div>
<div class="table-wrap"><table class="adm-t" id="msRecords"><thead><tr><th>종류</th><th>호스트(이름)</th><th>값</th><th>우선순위</th><th>용도</th><th>확인</th></tr></thead><tbody></tbody></table></div>
</div>
<h3 style="margin-top:16px">기본값 <span class="muted">— 사이트 설정에서 비워 둔 항목에 쓰입니다</span></h3>
<div class="row"><input id="msDefMx" placeholder="기본 메일 서버 주소 (MX 호스트, 예: mail.webmaker.ibit.kr)" autocomplete="off" style="flex:1;margin:0"><input id="msDefSpf" placeholder="기본 SPF include (선택)" autocomplete="off" style="flex:1;margin:0"><input id="msDefDmarc" placeholder="기본 DMARC (선택)" autocomplete="off" style="flex:1;margin:0"><button type="button" id="msSave" class="primary">기본값 저장</button></div>
<p class="muted" id="msDefMsg"></p>
</section>
<!-- v1.3.2: 화면 설정 — 설정 탭에서 분리, 넓은 미리보기 -->
<section class="card tab hidden" id="tab-theme">
<h2>화면 설정 <span class="muted" id="thState"></span></h2>
<p class="muted">사용자 화면(빌더)·로그인·사이트 관리자 페이지의 색상과 기본 구성을 바꿉니다. 바꾸는 즉시 오른쪽 미리보기에 반영되고, "저장" 을 눌러야 사용자에게 적용됩니다(사용자는 새로고침 시 반영). 만들어지는 고객 사이트의 디자인과는 관계없습니다.</p>
<div class="th-layout">
<div class="th-controls">
<div class="row"><span class="muted">색 구성</span><select id="thPreset" style="width:auto;margin:0"></select></div>
<div class="th-grid" id="thColors"></div>
<div class="th-grid">
<label>왼쪽 작업 패널 폭 <span class="muted" id="thLeftWidthV"></span><input id="thLeftWidth" type="range" min="360" max="720" step="10"></label>
<label>글자 크기 <span class="muted" id="thFontSizeV"></span><input id="thFontSize" type="range" min="12" max="17" step="1"></label>
<label>모서리 둥글기 <span class="muted" id="thRadiusV"></span><input id="thRadius" type="range" min="0" max="18" step="1"></label>
<label>간격<select id="thDensity"><option value="compact">촘촘하게</option><option value="normal">보통</option><option value="comfortable">넉넉하게</option></select></label>
<label>작업 패널 위치<select id="thPanelSide"><option value="left">왼쪽 (미리보기 오른쪽)</option><option value="right">오른쪽 (미리보기 왼쪽)</option></select></label>
<label>위쪽 영역 스타일<select id="thHeaderStyle"><option value="light">밝게</option><option value="accent">강조색</option><option value="dark">어둡게</option></select></label>
</div>
<div class="row"><button type="button" id="thSave" class="primary">저장 (사용자에게 적용)</button><button type="button" id="thRevert" class="ghost">저장된 값으로 되돌리기</button><button type="button" id="thReset" class="ghost">기본값으로 되돌리기</button></div>
<p class="muted" id="thMsg"></p>
</div>
<div class="th-stage">
<div class="row th-views" id="thViews"><button type="button" data-view="builder" class="active">빌더 (PC)</button><button type="button" data-view="mobile">빌더 (모바일)</button><button type="button" data-view="login">로그인</button><button type="button" data-view="siteadmin">사이트 관리자</button><button type="button" data-view="all">한눈에 보기</button><span class="spacer"></span><a class="btn ghost" href="/" target="_blank" rel="noopener">실제 사용자 화면 열기</a></div>
<div class="th-frames" id="thFrames"></div>
</div>
</div>
</section>
<!-- v1.5.16 (요구 3): AI 연결 탭 — LLM 초기 연결(Claude 인증·연결 테스트·사용할 Claude 모델, AI 제공자 키·모델 선택). 설정 탭의 "모델 연결"·"AI 제공자" 를 옮긴 것이며 입력칸 id 와 API 는 그대로다 -->
<section class="card tab hidden" id="tab-ai">
<h2>AI 연결 <span class="muted">— LLM 연결(서버 로그인 · OAuth 토큰 · API 키) · 사용할 모델 선택 · 연결 테스트 · AI 제공자</span></h2>
<p class="muted">여기서 연결하고 "사용" 으로 켠 모델만 <b>설정</b> 탭의 모델 등급(기본·중급·고급)과 내부 호출 모델에 지정할 수 있습니다. 연결을 끊거나 모델을 끄면 그 모델을 쓰던 등급은 설정 탭에서 "현재 사용 불가" 로 표시되니 다른 모델로 바꿔 주세요.</p>
<div class="grid">
<div>
<h2>Claude 모델 연결 <span class="muted">— Anthropic Claude Code CLI 인증</span></h2>
<p class="muted">AI 작업(사이트 생성·수정)의 기본 엔진인 Claude Code 의 인증 방식입니다. 저장한 뒤 "연결 테스트" 로 실제 호출이 되는지 확인하세요. 초기 설정 페이지(시스템 설정 탭) 의 "AI 엔진 인증" 과 같은 값입니다.</p>
<label class="radio"><input type="radio" name="authMode" value="server-login"> 서버 로그인 사용 <span class="muted" id="serverLoginInfo"></span></label>
<label class="radio"><input type="radio" name="authMode" value="oauth-token"> OAuth 토큰 (Claude 구독) <span class="muted">— 서버 또는 PC 에서 <code>claude setup-token</code> 으로 발급</span></label>
<input id="sOauth" placeholder="OAuth 토큰 붙여넣기" autocomplete="off">
<label class="radio"><input type="radio" name="authMode" value="api-key"> API 키 (Anthropic Console, 종량제)</label>
<input id="sApiKey" placeholder="sk-ant-api03-…" autocomplete="off">
<div class="row"><button type="button" id="btnSaveAuth" class="primary">저장</button><button type="button" id="btnTestConn" class="ghost">연결 테스트</button><span id="connResult" class="muted"></span></div>
<!-- v1.5.16 (요구 3): 사용할 Claude 모델 — 여기서 켠 모델만 설정 탭의 등급·내부 호출 모델 목록에 나온다 (settings.claudeModels, 비어 있으면 전체) -->
<h2 style="margin-top:12px">사용할 Claude 모델 <span class="muted">— 체크한 모델만 등급·내부 호출에 지정할 수 있습니다</span></h2>
<div id="claudeModelRows"></div>
<div class="row"><button type="button" id="btnSaveClaudeModels" class="primary">사용할 모델 저장</button><span id="claudeModelsMsg" class="muted"></span></div>
</div>
<div>
<h2>AI 제공자 (OpenAI · Gemini · Groq · OpenRouter 등)</h2>
<p class="muted">제공자별 API 키(OpenAI 는 서버 로그인도 가능)를 저장하고 "모델 조회"로 사용 가능한 모델을 불러온 뒤, 이 서비스에서 쓸 모델을 체크해 저장합니다. 인증된(키가 있고 활성화된) 제공자의 체크한 모델만 설정 탭의 등급·내부 호출 모델 목록에 나옵니다. Claude 는 왼쪽 "Claude 모델 연결" 을 따릅니다.</p>
<div class="table-wrap"><table class="adm-t" id="provTable"><thead><tr><th>제공자</th><th>상태</th><th>키</th><th>제공 모델</th><th>마지막 조회</th><th title="설정 탭 › 모델 등급 지정 목록에 이 제공자의 모델이 들어가는지 (v1.6.38)">등급 목록</th><th></th></tr></thead><tbody></tbody></table></div>
<div id="provForm" class="hidden" style="margin-top:10px;border:1px solid var(--line);border-radius:10px;padding:10px">
<h3 id="provTitle"></h3>
<label class="radio"><input type="checkbox" id="pEnabled"> 활성화 (사용자에게 제공)</label>
<div id="pAuthRow" class="hidden">
<label class="radio"><input type="radio" name="pAuth" value="api-key"> API 키로 연결</label>
<label class="radio"><input type="radio" name="pAuth" value="server-login"> 서버 로그인 (ChatGPT 계정, OpenAI Codex CLI) <span class="muted" id="pCodexInfo"></span></label>
</div>
<input id="pKey" placeholder="API 키" autocomplete="off">
<input id="pBase" placeholder="기본 URL (OpenAI 호환 서버일 때만)" class="hidden">
<div class="row"><button type="button" id="pFetch" class="ghost">모델 조회</button><span id="pMsg" class="muted"></span></div>
<!-- v1.6.38 (요구 1): 이 제공자가 설정 탭의 등급 목록에 들어가는지와 빠진 이유 — 키 없이 저장해 놓고 "반영이 안 된다" 고 보이던 문제 -->
<p id="pUsable" class="muted"></p>
<div class="table-wrap"><table class="adm-t" id="pModels"><thead><tr><th>제공</th><th>모델</th><th>입력 $/1M</th><th>출력 $/1M</th></tr></thead><tbody></tbody></table></div>
<div class="row"><button type="button" id="pSave" class="primary">저장</button><button type="button" id="pClose" class="ghost">닫기</button></div>
</div>
</div>
</div>
</section>
<section class="card tab hidden" id="tab-settings">
<div class="grid">
<div>
<h2>모델 등급 지정 <span class="muted">— 사용자는 기본·중급·고급만 고르며, 실제 모델은 여기서만 보고 바꿉니다</span></h2>
<!-- v1.5.16 (요구 3): 고를 수 있는 모델은 AI 연결 탭에서 연결하고 "사용" 으로 켠 것뿐이다 (서버도 저장 때 같은 조건으로 검사) -->
<p class="muted">목록에는 <b>AI 연결</b> 탭에서 연결하고 "사용" 으로 켠 모델만 나옵니다. 모델이 없거나 더 필요하면 AI 연결 탭에서 Claude 인증·사용할 Claude 모델·AI 제공자를 먼저 설정하세요.</p>
<div id="tierRows"></div>
<div class="row"><span class="muted">새 사이트 기본 등급</span><select id="sDefaultTier" style="width:auto;margin:0"></select><button type="button" id="btnSaveTiers" class="primary">등급 저장</button><span id="tierMsg" class="muted"></span></div>
<!-- v1.5.16 (요구 2): "Claude 기본 모델"(Claude 모델 id 고정) 을 없애고, 등급 지정과 같은 목록(AI 연결 탭에서 켠 모델)에서 고르는 "내부 호출 모델" 로 바꿨다 -->
<h2 style="margin-top:12px">내부 호출 모델 <span class="muted">— 등급과 무관한 서비스 내부 호출(사용자 문의 AI 분석, 모델을 지정하지 않은 호출)에 사용</span></h2>
<p class="muted">사이트 생성·수정·기획 단계는 사용자가 고른 등급의 모델을 쓰고, 등급에 묶이지 않은 내부 호출만 이 모델을 씁니다. 위 등급 지정과 같은 목록(AI 연결 탭에서 켠 모델)에서 고르며, 이 모델의 제공자를 끄면 그 사이에는 Claude 기본값으로 대신합니다.</p>
<div class="row"><select id="sInternalRef" style="flex:1;margin:0" data-dirty-watch></select><button type="button" id="btnSaveInternal" class="primary">모델 변경</button><span id="internalMsg" class="muted"></span></div>
<!-- v1.4.5 사용자 첫 화면 페이지: 빌더 오른쪽 미리보기에 사이트를 고르기 전 보이는 페이지 (관리자가 /builder 에서 만든 사이트 중 선택) -->
<h2 style="margin-top:12px">사용자 첫 화면 페이지 <span class="muted">— 빌더 오른쪽 빈 창에 보이는 페이지</span></h2>
<!-- v1.4.15: "첫 화면" 의 뜻을 분명히 — 서비스 홈페이지·로그인 화면이 아니라, 로그인한 사용자의 왼쪽 작업 영역이 비어 있을 때(아직 사이트를 고르지·만들지 않음) 오른쪽 빌더 창에 보이는 페이지 -->
<p class="muted">서비스 홈페이지(로그인 화면)의 첫 화면이 <b>아닙니다</b>. 로그인한 사용자가 아직 사이트를 고르거나 만들지 않아 왼쪽 작업 영역이 비어 있을 때, 오른쪽 빌더 창(평소 "미리보기 없음" 으로 비어 있는 곳)에 보일 페이지입니다. 사용자가 사이트를 고르면 그 사이트 미리보기로 바뀝니다. 관리자 계정이 빌더(<a href="/builder" target="_blank" rel="noopener">/builder</a>)에서 만든 사이트 중에서 고르며, 배포했으면 운영 주소, 아니면 서비스의 사이트 주소로 보입니다. "기본 안내" 를 고르면 지금까지의 안내 문구가 보입니다.</p>
<div class="row"><select id="sWelcome" data-dirty-watch style="flex:1;margin:0"></select><button type="button" id="btnSaveWelcome" class="primary">첫 화면 변경</button><a class="btn ghost" id="welcomeOpen" target="_blank" rel="noopener">열어 보기</a><span id="welcomeMsg" class="muted"></span></div>
<!-- v1.4.1 캐시 표시 단위: 화면·안내에 보이는 캐시 단위 글자만 바꾼다 (1 단위 = 1원 기준, 청구·차감 계산은 그대로) -->
<h2 style="margin-top:12px">캐시 표시 단위</h2>
<p class="muted">사용자·관리자 화면에서 캐시 금액 뒤에 붙는 단위입니다. 원·환·동·골드·크레딧·유닛·D·ING 처럼 자유롭게 정할 수 있고, 금액 계산은 그대로(1 단위 = 1원)입니다. 환율·도메인 원가처럼 실제 화폐 금액은 계속 "원" 으로 보입니다.</p>
<div class="row"><input id="sCashUnit" data-dirty-watch maxlength="12" placeholder="예: 원, 크레딧, D" style="width:160px;margin:0"><button type="button" id="btnSaveCashUnit" class="primary">단위 변경</button><span id="cashUnitMsg" class="muted"></span></div>
<h2 style="margin-top:12px">신규 회원 기본 캐시</h2>
<p class="muted">회원가입 직후 자동으로 지급되는 캐시(<span class="cash-unit">원</span>)입니다. 0 이면 지급하지 않으며, 가입 후에는 회원·청구 탭에서 개별 조정합니다.</p>
<div class="row"><input id="sSignupCredit" data-dirty-watch type="number" min="0" step="100" placeholder="예: 5000" style="width:160px;margin:0"><span class="muted cash-unit">원</span><button type="button" id="btnSaveSignupCredit" class="primary">저장</button><span id="signupCreditMsg" class="muted"></span></div>
<h2 style="margin-top:12px">AI에게 맡기기 수정 횟수 한도</h2>
<p class="muted">생성 후 발행(배포) 전까지 사용자가 요청할 수 있는 수정 횟수입니다. 한도에 이르면 사이트는 "발행 대기" 상태가 되어 발행한 뒤에만 다시 수정할 수 있습니다. 0 이면 무제한.</p>
<div class="row"><input id="sMaxEdits" data-dirty-watch type="number" min="0" step="1" placeholder="예: 10" style="width:160px;margin:0"><span class="muted">회</span><button type="button" id="btnSaveMaxEdits" class="primary">저장</button><span id="maxEditsMsg" class="muted"></span></div>
<!-- v1.3.1: 외부 연동 접속 정보 (암호화 보관소 data/credentials.json) -->
<h2 style="margin-top:12px">외부 연동 접속 정보 <span class="muted">— 암호화 보관</span></h2>
<p class="muted">메일 발송(Resend), 메일 게이트웨이, Claude 인증처럼 다른 서비스에 접속할 때 쓰는 주소·설정·API 키/토큰입니다. 키는 서버의 <code>data/credentials.json</code>(권한 600)에 AES-256-GCM 으로 암호화되어 저장되고 화면에는 앞뒤 몇 글자만 보입니다. 키 파일(<code>.key</code> 등, KEY=VALUE 또는 JSON)의 서버 경로를 넣고 "키 파일에서 가져오기" 를 누르거나 키 파일을 직접 올리면 서버가 한 번 읽어 암호화 보관하므로, 이후에는 평문 키 파일을 지워도 됩니다.</p>
<div class="row"><select id="vKind" style="width:auto;margin:0"></select><input id="vId" placeholder="식별자 (기타 서비스만, 예: crm-api)" style="width:200px;margin:0"><input id="vLabel" placeholder="표시 이름 (선택)" style="flex:1;margin:0"></div>
<div class="row" style="margin-top:6px"><input id="vUrl" placeholder="주소 (https://…)" autocomplete="off" style="flex:1;margin:0"><input id="vSecret" type="password" placeholder="API 키 또는 토큰" autocomplete="new-password" style="flex:1;margin:0"></div>
<textarea id="vSettings" rows="2" placeholder="설정 (선택, 한 줄에 이름=값)" style="margin-top:6px"></textarea>
<div class="row" style="margin-top:6px"><button type="button" id="vSave" class="primary">암호화해 저장</button><input id="vFile" placeholder="또는 서버의 키 파일 경로 (예: /home/master/work/WebHoting_AI_Agent/.key)" style="flex:1;margin:0"><button type="button" id="vImport" class="ghost">키 파일에서 가져오기</button></div>
<div class="row" style="margin-top:6px"><span class="muted">또는 키 파일을 직접 올리기 (.key, .env, .json — 서버에 파일로 저장하지 않고 필요한 값만 암호화 보관)</span><input id="vUpload" type="file" accept=".key,.env,.json,.txt,text/plain,application/json" style="width:auto;margin:0"><button type="button" id="vUploadGo" class="ghost">올려서 가져오기</button></div>
<p class="muted" id="vMsg"></p>
<div class="table-wrap"><table class="adm-t" id="vTable"><thead><tr><th>이름 · 종류</th><th>주소</th><th>키</th><th>출처</th><th>연결 확인</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<h2 style="margin-top:12px">작업 서버 수집 키 <span class="muted">— 다음 작업 반영 목록 읽기 전용</span></h2>
<p class="muted">작업(개발) 서버가 <code>GET /api/service/backlog</code> (헤더 <code>X-Backlog-Key</code>) 로 "다음 작업 반영" 으로 결정한 제안·오류를 주기적으로 가져갑니다. 비워 두면 수집이 꺼집니다.</p>
<div class="row"><input id="sBacklogKey" placeholder="수집 키 (비워 두면 비활성)" autocomplete="off" style="flex:1;margin:0"><button type="button" id="btnGenBacklogKey" class="ghost">생성</button><button type="button" id="btnSaveBacklogKey" class="primary">저장</button><span id="backlogKeyMsg" class="muted"></span></div>
<h2 style="margin-top:12px">외부 서비스 캐시 연동 키</h2>
<p class="muted">다른 서비스가 <code>POST /api/service/credits/&lt;사용자ID&gt;</code> (헤더 <code>X-Service-Key</code>, 본문 <code>{type:set|add|adjust|refund, amount, memo, source}</code>) 로 캐시를 조정할 때 쓰는 키입니다. <code>GET</code> 은 잔액·원장 조회.</p>
<div class="row"><input id="sServiceKey" placeholder="서비스 키 (비워 두면 외부 연동 비활성)" autocomplete="off" style="flex:1;margin:0"><button type="button" id="btnGenServiceKey" class="ghost">생성</button><button type="button" id="btnSaveServiceKey" class="primary">저장</button><span id="serviceKeyMsg" class="muted"></span></div>
<!-- v1.5.16 (요구 3): "모델 연결"(Claude 인증·연결 테스트) 과 "AI 제공자" 는 AI 연결 탭으로 옮겼다 -->
<p class="muted">Claude 인증(서버 로그인·OAuth 토큰·API 키)·연결 테스트·사용할 Claude 모델·AI 제공자(OpenAI 등)는 <b>AI 연결</b> 탭에 있습니다.</p>
</div>
<div>
<!-- v1.3.16: 금액·비율 값마다 바로 옆에 실행 버튼 (예전에는 저장 버튼이 SSL 항목 아래에 하나뿐이라 마진을 고치고 새로고침하면 사라졌음) -->
<h2>환율 · 마진</h2>
<label class="radio"><input type="radio" name="fxMode" value="auto" data-dirty-watch> 자동 조회 (ECB/시장 중간환율 + 스프레드)</label>
<div class="inline"><span>스프레드 %</span><input id="sSpread" type="number" step="0.1" min="0" style="width:90px" data-dirty-watch></div>
<label class="radio"><input type="radio" name="fxMode" value="manual" data-dirty-watch> 직접 입력 (당일 USD 구입 가격)</label>
<div class="inline"><span>1 USD =</span><input id="sManualRate" type="number" step="0.01" min="0" style="width:120px" data-dirty-watch><span>원</span></div>
<div class="row"><button type="button" id="btnSaveFx" class="primary">환율 설정 저장</button><button type="button" id="btnRefreshFx" class="ghost">오늘 환율 갱신</button><span id="fxMsg" class="muted"></span></div>
<p class="muted" id="fxNow"></p>
<div class="inline"><span>사용자 청구 마진 %</span><input id="sMargin" type="number" step="1" min="0" max="1000" style="width:90px" data-dirty-watch><button type="button" id="btnSaveMargin" class="primary">마진 변경</button><span id="marginMsg" class="muted"></span></div>
<p class="muted">청구 금액 = 비용(USD) × 명령 실행일의 USD 구입 가격(원) × (1 + 마진). 마진은 변경한 뒤 새로 끝나는 작업부터 적용됩니다.</p>
<!-- v1.3.17: 비밀번호 규칙 (서비스 계정·사이트 관리자 계정의 새 비밀번호에 적용, 기본은 기존 규칙 6자 이상) -->
<!-- v1.5.27 (요구 6): 회원가입 규칙 — 가입 기능 ON/OFF(설정 파일 service.allowSignup, 1.5.11) 와 가입 뒤 이용 방식(settings.signupApproval) -->
<h2 style="margin-top:12px">회원가입 규칙</h2>
<p class="muted">회원가입을 끄면 로그인 화면의 회원가입 탭이 사라지고 가입 API 는 "회원가입이 닫혀 있습니다" 로 거절합니다. "관리자 승인 후 이용" 을 고르면 가입은 신청으로 접수되고, 아래 회원 · 청구 탭에서 승인해야 로그인할 수 있습니다.</p>
<label class="radio"><input type="checkbox" id="suEnabled" data-dirty-watch> 회원가입 허용 (끄면 가입 불가)</label>
<div class="inline"><span>가입 후 이용</span><select id="suApproval" style="width:auto" data-dirty-watch><option value="immediate">가입 즉시 이용</option><option value="approval">관리자 승인 후 이용</option></select><button type="button" id="btnSaveSignup" class="primary">회원가입 규칙 저장</button><span id="suMsg" class="muted"></span></div>
<h2 style="margin-top:12px">비밀번호 규칙</h2>
<p class="muted">회원가입·비밀번호 변경·관리자 재설정·사이트 관리자 계정 만들기에서 새로 정하는 비밀번호에 적용됩니다. 이미 쓰고 있는 비밀번호로 로그인하는 것은 막지 않습니다. 기본값은 지금까지의 규칙(6자 이상)입니다.</p>
<div class="inline"><span>최소 길이</span><input id="ppMin" type="number" min="4" max="64" step="1" style="width:80px" data-dirty-watch><span>최대 길이</span><input id="ppMax" type="number" min="4" max="256" step="1" style="width:80px" data-dirty-watch></div>
<label class="radio"><input type="checkbox" id="ppLetter" data-dirty-watch> 영문자 포함</label>
<label class="radio"><input type="checkbox" id="ppMixed" data-dirty-watch> 대문자·소문자 모두 포함</label>
<label class="radio"><input type="checkbox" id="ppDigit" data-dirty-watch> 숫자 포함</label>
<label class="radio"><input type="checkbox" id="ppSymbol" data-dirty-watch> 특수문자 포함</label>
<label class="radio"><input type="checkbox" id="ppNoId" data-dirty-watch> 아이디를 포함하지 않음</label>
<div class="row"><button type="button" id="btnSavePp" class="primary">비밀번호 규칙 저장</button><span id="ppMsg" class="muted"></span></div>
<!-- v1.4.0: 도메인 서비스 (Openprovider) — 값마다 저장 버튼. 접속 정보(사용자 이름·비밀번호)는 왼쪽 "외부 연동 접속 정보" 의 Openprovider 항목 -->
<h2 style="margin-top:12px">도메인 서비스</h2>
<!-- v1.6.35 (요구 1-1): 관리자는 접속 정보·플랫폼 도메인·서브 도메인 허용 여부·가격만 정한다. 도메인 검색·구매(장바구니)·이미 가진 도메인 등록(네임서버 이전/직접 DNS)·영역·레코드 관리는 사용자 "도메인" 탭으로 옮겼다 -->
<p class="muted">관리자는 여기서 <b>접속 정보</b>(왼쪽 "외부 연동 접속 정보" 에 Openprovider · Porkbun · Route 53 · Google Cloud DNS), <b>플랫폼 도메인</b>, <b>서브 도메인 등록 허용</b>, 가격 요율을 정합니다. 도메인 검색·구매(장바구니), 이미 가진 도메인 등록(네임서버 이전 또는 직접 DNS 관리), DNS 영역·레코드 관리는 v1.6.35 부터 사용자가 자기 "도메인" 탭에서 스스로 합니다. 접속 정보를 넣기 전에는 사용자 화면에 "도메인 서비스가 아직 준비되지 않았습니다" 가 보입니다.</p>
<div class="inline"><span>도메인 요율 %</span><input id="dmMargin" type="number" step="1" min="100" max="1000" style="width:90px" data-dirty-watch><button type="button" id="btnDmMargin" class="primary">요율 변경</button><span id="dmMarginMsg" class="muted"></span></div>
<p class="muted">사용자 가격 = 공급사(Openprovider) 가격 × 요율 ÷ 100 을 그날 환율로 원화로 바꿔 100원 단위로 올림. 기본 250 (2.5배). 구매·갱신 전에 사용자에게 먼저 보입니다.</p>
<!-- v1.5.36 (요구 4·5): 등록 업체(Openprovider·Porkbun)와 DNS 공급자(수동·Openprovider·Porkbun·Route 53·Google Cloud DNS)를 고른다. 접속 정보는 왼쪽 "외부 연동 접속 정보" 의 같은 이름 종류 -->
<div class="inline"><span>플랫폼 도메인</span><input id="dmPlatform" placeholder="ibit.kr" style="width:160px" data-dirty-watch><span>DNS 공급자</span><select id="dmDns" style="width:auto" data-dirty-watch></select><span>등록 업체</span><select id="dmRegistrar" style="width:auto" data-dirty-watch></select><button type="button" id="btnDmPlatform" class="primary">저장</button><span id="dmPlatformMsg" class="muted"></span></div>
<p class="muted" id="dmProvNote"></p>
<p class="muted">서브 도메인 계정은 이 도메인 아래 이름(mycompany.ibit.kr)을 씁니다. DNS 수동이면 운영자가 플랫폼 도메인 DNS 에 와일드카드 A 레코드(*.ibit.kr → 사이트 IP)를 한 번 넣어 두어야 하고, 자동이면 플랫폼 도메인이 Openprovider DNS 에 있어야 합니다.</p>
<div class="inline"><span>사이트 IP</span><input id="dmIp" placeholder="비우면 서비스 주소의 IP" style="width:180px" data-dirty-watch><button type="button" id="btnDmIp" class="primary">저장</button><span id="dmIpMsg" class="muted"></span></div>
<div class="inline"><span>판매 확장자</span><input id="dmTlds" placeholder="com, net, org, co, io, shop" style="flex:1;min-width:160px" data-dirty-watch><button type="button" id="btnDmTlds" class="primary">저장</button><span id="dmTldsMsg" class="muted"></span></div>
<div class="inline"><span>등록 금지 이름</span><input id="dmReserved" placeholder="예: shop, support" style="flex:1;min-width:160px" data-dirty-watch><button type="button" id="btnDmReserved" class="primary">저장</button><span id="dmReservedMsg" class="muted"></span></div>
<!-- v1.6.35 (요구 1-1): 서브 도메인 등록 자체를 막는 스위치. 끄면 서브 도메인 계정도 새 이름을 등록할 수 없고(이미 등록된 것은 유지), 사용자 화면에 이유가 보인다 -->
<div class="inline"><label class="radio" style="margin:0"><input type="checkbox" id="dmAllowSub" data-dirty-watch> 서브 도메인 등록 허용 (끄면 플랫폼 도메인 아래 새 이름을 등록할 수 없습니다 — 자기 도메인만 사용)</label><button type="button" id="btnDmAllowSub" class="primary">저장</button><span id="dmAllowSubMsg" class="muted"></span></div>
<div class="inline"><span>새 가입자 계정 종류</span><select id="dmDefType" style="width:auto" data-dirty-watch><option value="subdomain">서브 도메인 전용</option><option value="domain">자체 도메인</option></select><button type="button" id="btnDmDefType" class="primary">저장</button><span id="dmDefTypeMsg" class="muted"></span></div>
<label class="radio"><input type="checkbox" id="dmUserMail" data-dirty-watch> 메일(MX) 자동 설정 때 그 사이트의 메일 시스템도 켜기</label>
<div class="row"><button type="button" id="btnDmUserMail" class="primary">저장</button><span id="dmUserMailMsg" class="muted"></span></div>
<p class="muted">회원별 계정 종류는 회원·청구 탭의 회원 목록에서 바꿉니다. 이미 있던(1.4.0 이전) 회원은 자체 도메인 계정으로 유지됩니다.</p>
<!-- v1.5.36 (요구 5) DNS 영역 관리 → v1.6.35 (요구 1-1): 영역 만들기·이전된 영역 받기는 사용자 "도메인" 탭(이미 가진 도메인 등록 › 네임서버 이전)으로 옮겼다. 관리자는 현황 조회·레코드 확인·쓰지 않는 영역 정리만 한다. 회원 도메인의 영역은 force 로만 지운다. 모든 변경은 감사 기록 -->
<h3 style="margin:12px 0 4px">DNS 영역 현황 (조회·정리) <button type="button" id="dzRefresh" class="ghost">목록</button></h3>
<p class="muted">선택한 DNS 공급자에 있는 영역(hosted zone / managed zone)과 그 영역을 쓰는 회원을 봅니다. 영역 만들기·이전된 영역 받기는 v1.6.35 부터 회원이 자기 "도메인" 탭(이미 가진 도메인 등록 › 네임서버 이전)에서 직접 하므로 여기서는 하지 않습니다. 회원이 쓰지 않는 영역(회원 칸이 "-")만 정리하세요 — 회원 도메인의 영역은 확인을 거쳐야 지워집니다. 변경 내역은 아래 감사 기록에 남습니다.</p>
<div class="row"><select id="dzProvider" style="width:auto;margin:0"></select><span id="dzMsg" class="muted"></span></div>
<div class="table-wrap"><table class="adm-t" id="dzTable"><thead><tr><th>영역</th><th>네임서버</th><th>회원</th><th>레코드</th><th>작업</th></tr></thead><tbody></tbody></table></div>
<div id="dzRecBox" class="hidden" style="margin-top:8px;border:1px solid var(--line);border-radius:10px;padding:10px">
<h3 id="dzRecTitle" style="margin:0 0 6px"></h3>
<div class="table-wrap"><table class="adm-t" id="dzRecTable"><thead><tr><th>이름</th><th>종류</th><th>값</th><th>TTL</th><th>우선순위</th><th></th></tr></thead><tbody></tbody></table></div>
<div class="row" style="margin-top:6px"><input id="dzRecName" placeholder="이름 (비우면 도메인 자신)" style="width:150px;margin:0"><select id="dzRecType" style="width:auto;margin:0"><option>A</option><option>AAAA</option><option>CNAME</option><option>MX</option><option>TXT</option><option>CAA</option><option>SRV</option><option>NS</option></select><input id="dzRecValue" placeholder="값" style="flex:1;margin:0"><input id="dzRecPrio" placeholder="우선순위" type="number" style="width:90px;margin:0"><button type="button" id="dzRecAdd" class="primary">추가</button><button type="button" id="dzRecClose" class="ghost">닫기</button></div>
</div>
<details style="margin-top:6px"><summary class="muted">DNS 감사 기록 (최근 100건)</summary><div class="table-wrap"><table class="adm-t" id="dzAudit"><thead><tr><th>일시</th><th>누가</th><th>작업</th><th>공급자</th><th>영역</th><th>내용</th></tr></thead><tbody></tbody></table></div></details>
<h2 style="margin-top:12px">내 비밀번호 변경</h2>
<input id="aPwCur" type="password" placeholder="현재 비밀번호" autocomplete="current-password">
<input id="aPwNew" type="password" placeholder="새 비밀번호 (6자 이상)" data-pw-rule="placeholder" data-pw-head="새 비밀번호" autocomplete="new-password">
<div class="row"><button type="button" id="aPwSave" class="primary">변경</button><span id="aPwMsg" class="muted"></span></div>
<h2 style="margin-top:12px">메일 발송 (문의 접수 위젯)</h2>
<p class="muted">생성 사이트의 문의·예약 폼이 접수한 내용을 <code>webmaster@사이트도메인</code>(Resend 에 검증된 도메인일 때, 아니면 기본 발신 도메인)에서 사이트의 문의 수신 이메일로 보냅니다. 키는 메일 에이전트 키 파일(.key, <code>RESEND_API_KEY=…</code>)을 지정하거나 직접 입력합니다.</p>
<p class="muted">v1.3.1: 키 파일 경로를 저장하면 서버가 그 파일을 한 번 읽어 "외부 연동 접속 정보" 보관소에 암호화해 넣고, 이후 발송은 보관소의 키를 씁니다(평문 키 파일은 지워도 됨). 직접 입력한 키도 보관소에 암호화 저장됩니다.</p>
<input id="mKeyFile" placeholder="키 파일 경로 (예: /home/master/work/remember-voice/.key)">
<input id="mApiKey" placeholder="또는 Resend API 키 직접 입력 (re_…)" autocomplete="off">
<div class="two"><input id="mDomain" placeholder="기본 발신 도메인 (예: ibit.kr)"><input id="mFromName" placeholder="발신자 표시 이름 (기본 Webmaster)"></div>
<input id="mFormBase" placeholder="폼 접수 서버 주소 (생성 사이트가 호출, 기본: 이 서비스 공개 URL)">
<p class="muted" id="mStatus"></p>
<div class="row"><button type="button" id="mSave" class="primary">저장</button><input id="mTestTo" placeholder="테스트 수신 주소" style="width:220px;margin:0"><button type="button" id="mTest" class="ghost">테스트 메일 발송</button><span id="mTestMsg" class="muted"></span></div>
<h2 style="margin-top:12px">소스 저장소 (로컬 Gitea) <button type="button" id="gTest" class="ghost">연결 확인</button></h2>
<!-- v1.4.14 (요구 8): 사이트 버전은 회사 소스 저장소가 아니라 서비스 서버 안의 전용 Gitea(webmaker/sites)에 보관한다 -->
<p class="muted">사이트의 생성·수정 버전과 배포본을 보관하는 git 저장소입니다. 서비스 서버 안에 전용 Gitea(계정 webmaker, 저장소 webmaker/sites)를 두고 이 서버의 키로 푸시합니다. 아직 설치하지 않았으면 서버에서 <code>deploy/install-gitea.sh</code> 를 실행하세요(계정·저장소·키 등록·서비스 설정까지 자동).</p>
<p id="gStatus" class="muted"></p>
<textarea id="gPubKey" rows="2" readonly class="hidden" style="width:100%;font-size:11px;font-family:ui-monospace,monospace"></textarea>
<h2 style="margin-top:12px">SSL (Let's Encrypt)</h2>
<div class="row"><input id="sLeEmail" type="email" placeholder="인증서 만료 알림을 받을 이메일 (예: admin@example.com)" style="flex:1;margin:0" data-dirty-watch><button type="button" id="btnSaveLeEmail" class="primary">저장</button><span id="leMsg" class="muted"></span></div>
<p class="muted">배포 시 certbot 으로 인증서를 발급할 때 사용합니다. 비워 두면 이메일 없이 발급합니다.</p>
<!-- v1.4.14 (요구 9): 플랫폼 도메인 와일드카드 인증서 — 서브 도메인 사이트 전체의 HTTPS 를 인증서 하나로. 고객 도메인은 사이트별 인증서 그대로 -->
<h3 style="margin:12px 0 4px">플랫폼 도메인 와일드카드 인증서 <button type="button" id="sslRefresh" class="ghost">상태 확인</button></h3>
<p class="muted">서브 도메인 사이트(<span id="sslPlatformEx">이름.ibit.kr</span>)는 사이트마다 인증서를 받지 않고 <b>*.플랫폼 도메인</b> 와일드카드 인증서 하나로 HTTPS 를 적용합니다. 고객이 가진 도메인(example.com)은 지금처럼 배포 때 사이트별 인증서를 따로 발급합니다. 와일드카드는 DNS 인증(TXT 레코드)만 가능합니다 — 플랫폼 DNS 가 Openprovider(자동) 이면 서비스가 넣고, 수동이면 아래에 뜨는 TXT 를 DNS 관리 화면에 넣어 주세요(갱신 때도 같은 절차).</p>
<p id="sslStatus" class="muted"></p>
<div id="sslPending" class="hidden" style="border:1px solid var(--line);border-radius:8px;padding:8px 10px;margin:6px 0">
<b>DNS 에 넣을 TXT 레코드</b> <span class="muted" id="sslPendingNote"></span>
<div class="row" style="margin-top:6px"><span class="muted" style="min-width:48px">이름</span><input id="sslTxtName" readonly style="flex:1;margin:0;font-family:ui-monospace,monospace"><button type="button" class="ghost" data-copy="sslTxtName">복사</button></div>
<div class="row" style="margin-top:4px"><span class="muted" style="min-width:48px">종류</span><input value="TXT" readonly style="width:80px;margin:0"></div>
<div class="row" style="margin-top:4px"><span class="muted" style="min-width:48px">값</span><input id="sslTxtValue" readonly style="flex:1;margin:0;font-family:ui-monospace,monospace"><button type="button" class="ghost" data-copy="sslTxtValue">복사</button></div>
</div>
<div class="row"><button type="button" id="sslIssue" class="primary">와일드카드 인증서 발급·갱신</button><button type="button" id="sslCancel" class="ghost hidden">진행 취소</button><span id="sslMsg" class="muted"></span></div>
</div>
</div>
</section>
<!-- v1.5.7 (요구 2·7): 시스템 설정 — 설정 파일(data/config.json)·연동 서비스·AI 엔진 상태. 초기 설정 페이지(setup-server.js)와 같은 화면(public/setup.js)을 /api/admin/config 로 붙인다 -->
<section class="card tab hidden" id="tab-system">
<h2>시스템 설정 <span class="muted">— 설정 파일 · 연동 서비스(Gitea·Arimail) · AI 엔진</span></h2>
<p class="muted">서비스가 동작하는 데 필요한 환경 정보는 코드·systemd 유닛이 아니라 설정 파일 <code>data/config.json</code> 에 있습니다(1.5.7). 설치 직후에는 별도 포트의 초기 설정 페이지(기본 <code>:8795</code>, 1.5.11)나 <code>/setup</code> 에서 같은 화면을 쓰고(설치 점검표가 끝나기 전에는 첫 화면이 그리로 넘어갑니다), 운영 중에는 여기서 바꿉니다. 비밀값(토큰·비밀번호)은 암호화 보관소에 저장되며 "재시작 후 적용" 표시가 있는 값은 저장한 뒤 서비스를 재시작해야 반영됩니다. 배포 때 정해지는 값(포트·경로)은 읽기 전용입니다.</p>
<div id="sysConfig"></div>
</section>
</div>
<script src="/numfmt.js"></script><script src="/i18n.js"></script><script src="/pwrule.js"></script><!-- v1.3.10: 관리자 콘솔 다국어 / v1.3.17 비밀번호 규칙 안내 -->
<script src="/setup.js"></script><!-- v1.5.7 시스템 설정 탭 화면 -->
<script src="/admin.js"></script>
</body>
</html>