894 lines
82 KiB
HTML
894 lines
82 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>AJIN Transfer 목업</title>
|
||
<link rel="icon" href="logo.png">
|
||
<style>
|
||
:root{
|
||
--font:"Pretendard Variable",Pretendard,-apple-system,BlinkMacSystemFont,"Segoe UI","Malgun Gothic","Apple SD Gothic Neo",sans-serif;
|
||
--bg:#f4f5f7;--surface:#fff;--surface-2:#f6f7f9;--border:#e3e6ec;
|
||
--text:#141821;--muted:#667085;
|
||
--accent:#3552d6;--accent-hover:#2b44b5;--accent-soft:#e9edfd;--accent-ink:#2b44b5;
|
||
--ok:#12805c;--ok-soft:#e1f5ec;--warn:#a85a05;--warn-soft:#fff2da;--danger:#c0362c;--danger-soft:#fdeceb;
|
||
--shadow:0 1px 2px rgba(16,24,40,.05),0 6px 20px rgba(16,24,40,.06);
|
||
--radius:14px;
|
||
}
|
||
@media (prefers-color-scheme:dark){
|
||
:root:not([data-theme=light]){
|
||
--bg:#0e1117;--surface:#161a22;--surface-2:#1c212b;--border:#2a303c;
|
||
--text:#eceff4;--muted:#98a2b3;
|
||
--accent:#4d69ee;--accent-hover:#6079f2;--accent-soft:#1d2546;--accent-ink:#9db0ff;
|
||
--ok:#4cc79a;--ok-soft:#12302a;--warn:#f0b35a;--warn-soft:#38290f;--danger:#ea6a5f;--danger-soft:#3a1816;
|
||
--shadow:0 1px 2px rgba(0,0,0,.4),0 6px 20px rgba(0,0,0,.35);
|
||
}
|
||
}
|
||
:root[data-theme=dark]{
|
||
--bg:#0e1117;--surface:#161a22;--surface-2:#1c212b;--border:#2a303c;
|
||
--text:#eceff4;--muted:#98a2b3;
|
||
--accent:#4d69ee;--accent-hover:#6079f2;--accent-soft:#1d2546;--accent-ink:#9db0ff;
|
||
--ok:#4cc79a;--ok-soft:#12302a;--warn:#f0b35a;--warn-soft:#38290f;--danger:#ea6a5f;--danger-soft:#3a1816;
|
||
--shadow:0 1px 2px rgba(0,0,0,.4),0 6px 20px rgba(0,0,0,.35);
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{margin:0}
|
||
body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||
button,input,select{font:inherit;color:inherit}
|
||
h1,h2,h3,p{margin:0}
|
||
.muted{color:var(--muted)}
|
||
.ic{flex:none;vertical-align:middle}
|
||
|
||
/* ---- 목업 컨트롤 (실제 화면 아님) ---- */
|
||
.mock-bar{position:sticky;top:0;z-index:60;background:#0d1017;color:#c3cad8;font-size:12px;display:flex;gap:6px 16px;flex-wrap:wrap;align-items:center;padding:8px 14px}
|
||
.mock-bar b{color:#fff;font-weight:700}
|
||
.mock-bar .grp{display:flex;align-items:center;gap:6px}
|
||
.mock-bar .seg{display:flex;gap:2px;background:#1a1f2d;border-radius:8px;padding:2px}
|
||
.mock-bar .seg button{background:transparent;border:0;color:#9aa3b8;padding:4px 10px;border-radius:6px;cursor:pointer;font-size:12px}
|
||
.mock-bar .seg button.on{background:#3b4670;color:#fff}
|
||
|
||
/* ---- 공통 ---- */
|
||
.view{display:none}
|
||
.view.on{display:flex;flex-direction:column;height:calc(100dvh - var(--bar,44px))}
|
||
.hdr{flex:none;background:var(--surface);border-bottom:1px solid var(--border)}
|
||
.hdr .in{max-width:940px;margin:0 auto;padding:12px 20px;display:flex;align-items:center;gap:10px}
|
||
.brand{display:flex;align-items:center;gap:12px;font-weight:800;font-size:23px;letter-spacing:-.015em}
|
||
.brand img{width:38px;height:38px;display:block}
|
||
.badge-admin{font-size:11px;font-weight:700;background:var(--accent-soft);color:var(--accent-ink);padding:2px 8px;border-radius:99px}
|
||
.spacer{flex:1}
|
||
.scroll{flex:1;min-height:0;overflow:auto}
|
||
.wrap{max-width:940px;margin:0 auto;padding:20px;width:100%}
|
||
.foot{flex:none;text-align:center;color:var(--muted);font-size:12px;padding:10px 16px}
|
||
|
||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--border);background:var(--surface);padding:8px 14px;border-radius:10px;cursor:pointer;font-weight:600;font-size:14px;transition:background .15s,border-color .15s;white-space:nowrap}
|
||
.btn:hover{background:var(--surface-2)}
|
||
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
||
.btn.primary:hover{background:var(--accent-hover)}
|
||
.btn.danger{background:var(--danger);border-color:var(--danger);color:#fff}
|
||
.btn.ghost{border-color:transparent;background:transparent}
|
||
.btn.ghost:hover{background:var(--surface-2)}
|
||
.btn.sm{padding:6px 10px;font-size:13px;border-radius:8px}
|
||
.btn.lg{padding:12px 24px;font-size:16px;border-radius:12px}
|
||
.btn.ic-only{padding:7px}
|
||
.btn:disabled{opacity:.45;cursor:not-allowed}
|
||
.btn:focus-visible,.chip:focus-visible,input:focus-visible,select:focus-visible,.drop:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
||
|
||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow)}
|
||
.tag{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;padding:2px 8px;border-radius:99px;background:var(--accent-soft);color:var(--accent-ink)}
|
||
.tag.opt{background:var(--surface-2);color:var(--muted)}
|
||
.tag.ok{background:var(--ok-soft);color:var(--ok)}
|
||
.tag.warn{background:var(--warn-soft);color:var(--warn)}
|
||
.tag.bad{background:var(--danger-soft);color:var(--danger)}
|
||
|
||
.tile{--c:var(--muted);width:52px;height:52px;border-radius:12px;display:grid;place-items:center;font-size:11px;font-weight:800;flex:none;color:var(--c);background:color-mix(in srgb,var(--c) 14%,transparent);letter-spacing:.02em}
|
||
button.tile{position:relative;border:0;padding:0;font-family:inherit;cursor:pointer;transition:box-shadow .15s,transform .15s}
|
||
button.tile:hover{box-shadow:0 0 0 2px var(--accent);transform:translateY(-1px)}
|
||
button.tile:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
||
.tile.th{position:relative;padding:0;background:var(--surface-2);border:1px solid var(--border)}
|
||
.tile.th img{width:100%;height:100%;object-fit:cover;border-radius:11px;display:block}
|
||
.tile.th .ext{position:absolute;left:3px;bottom:3px;background:rgba(15,18,28,.74);color:#fff;font-size:9px;font-weight:800;padding:1px 5px;border-radius:5px;letter-spacing:.02em}
|
||
button.tile .eye{position:absolute;right:-6px;bottom:-6px;width:21px;height:21px;border-radius:50%;background:var(--surface);border:1px solid var(--border);color:var(--accent-ink);display:grid;place-items:center;box-shadow:0 1px 3px rgba(16,24,40,.15)}
|
||
.t-img{--c:#3b82f6}.t-pdf{--c:#ef4444}.t-xls{--c:#16a34a}.t-doc{--c:#6366f1}.t-zip{--c:#b7791f}.t-txt{--c:#64748b}
|
||
|
||
.bar{height:6px;border-radius:99px;background:var(--surface-2);overflow:hidden}
|
||
.bar>i{display:block;height:100%;width:0;background:var(--accent);border-radius:99px;transition:width .15s linear}
|
||
.bar.ok>i{background:var(--ok)}
|
||
|
||
input[type=text],input[type=password],input[type=search]{width:100%;border:1px solid var(--border);background:var(--surface);border-radius:10px;padding:9px 12px;font-size:14px}
|
||
input::placeholder{color:var(--muted)}
|
||
|
||
/* ---- 업로드 ---- */
|
||
#view-upload .wrap{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px;padding-bottom:6px}
|
||
.ctl-card{flex:none;padding:12px 14px}
|
||
.ctl-top{display:flex;align-items:center;gap:12px}
|
||
.ctl-top .sum{flex:1;min-width:0;color:var(--muted);font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.ctl-top .warn{color:var(--warn);font-size:13px;font-weight:600;white-space:nowrap}
|
||
.btn.up{padding:9px 22px;font-size:15px}
|
||
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
||
.fl{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||
.ctl-toggle{display:none;align-items:center;justify-content:space-between;width:100%;margin-top:12px;padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--surface-2);cursor:pointer;font-weight:600;font-size:14px}
|
||
.ctl{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(0,1fr);gap:12px 24px;margin-top:12px;padding-top:12px;border-top:1px solid var(--border);align-items:start}
|
||
.lbl{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700;color:var(--muted);margin-bottom:8px;white-space:nowrap}
|
||
.fl .lbl{margin:0}
|
||
.chips{display:flex;flex-wrap:wrap;gap:8px}
|
||
.chip{border:1px solid var(--border);background:var(--surface);border-radius:9px;padding:6px 12px;cursor:pointer;font-weight:600;font-size:13.5px;transition:background .15s}
|
||
.chip:hover{background:var(--surface-2)}
|
||
.chip[aria-checked=true]{background:var(--accent);border-color:var(--accent);color:#fff}
|
||
.sel{position:relative;display:inline-block}
|
||
.sel select{appearance:none;-webkit-appearance:none;border:1px dashed var(--border);background:var(--surface);border-radius:9px;padding:6px 30px 6px 12px;cursor:pointer;font-weight:600;font-size:13.5px}
|
||
.sel select.active{background:var(--accent);border:1px solid var(--accent);color:#fff}
|
||
.sel select.active option{color:var(--text);background:var(--surface)}
|
||
.sel::after{content:"";position:absolute;right:13px;top:50%;width:6px;height:6px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:translateY(-70%) rotate(45deg);pointer-events:none;color:var(--muted)}
|
||
.sel:has(select.active)::after{color:#fff}
|
||
.pw{display:flex;gap:8px}
|
||
.pwi{position:relative;flex:1;min-width:0}
|
||
.pwi input{padding-right:34px}
|
||
.clr{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:26px;height:26px;border:0;border-radius:7px;background:transparent;color:var(--muted);cursor:pointer;display:grid;place-items:center}
|
||
.clr:hover{background:var(--surface-2);color:var(--text)}
|
||
.clr:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
||
.chk{display:flex;align-items:flex-start;gap:9px;margin-top:8px;font-size:13px;cursor:pointer}
|
||
.chk input{margin-top:3px;width:16px;height:16px;accent-color:var(--accent)}
|
||
.chk.off{opacity:.5;cursor:not-allowed}
|
||
.ctl-foot{display:flex;align-items:center;gap:14px;margin-top:18px;padding-top:16px;border-top:1px solid var(--border)}
|
||
.ctl-foot .sum{color:var(--muted);font-size:14px;flex:1}
|
||
.ctl-foot .warn{color:var(--warn);font-size:13px;font-weight:600}
|
||
|
||
.list-card{flex:1;min-height:220px;display:flex;flex-direction:column}
|
||
#view-upload .wrap{overflow:auto}
|
||
.list-h{flex:none;display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-bottom:1px solid var(--border);font-weight:700}
|
||
.list-h span{color:var(--muted);font-weight:500;font-size:14px}
|
||
.list-b{flex:1;min-height:0;overflow:auto;padding:6px 8px}
|
||
.row{display:flex;align-items:center;gap:14px;padding:12px 10px}
|
||
.row+.row{border-top:1px solid var(--border)}
|
||
.rm{flex:1;min-width:0}
|
||
.rn{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.rs{color:var(--muted);font-size:13px}
|
||
.cmt{width:100%;margin-top:6px;border:1px solid transparent;background:var(--surface-2);border-radius:8px;padding:6px 10px;font-size:13px}
|
||
.cmt:disabled{background:transparent;padding-left:0}
|
||
.row .bar{margin-top:8px}
|
||
.acts{display:flex;gap:6px;flex:none}
|
||
.drop{flex:1;margin:12px;border:2px dashed var(--border);border-radius:16px;display:grid;place-items:center;text-align:center;color:var(--muted);cursor:pointer;padding:20px;transition:border-color .15s,background .15s}
|
||
.drop:hover{border-color:var(--accent);background:var(--accent-soft)}
|
||
.drop .big{display:grid;place-items:center;width:64px;height:64px;border-radius:18px;background:var(--accent-soft);color:var(--accent-ink);margin:0 auto 14px}
|
||
.drop strong{display:block;color:var(--text);font-size:17px;margin-bottom:4px}
|
||
|
||
.drop-strip{flex:none;display:none;align-items:center;justify-content:center;gap:10px;margin:4px 12px 12px;padding:12px;border:2px dashed var(--border);border-radius:12px;color:var(--muted);font-size:14px;font-weight:600;cursor:pointer;transition:border-color .15s,background .15s}
|
||
.drop-strip:hover,.drop-strip.over{border-color:var(--accent);background:var(--accent-soft);color:var(--accent-ink)}
|
||
.drop-overlay{position:fixed;inset:0;z-index:150;display:none;align-items:center;justify-content:center;background:rgba(40,60,160,.28);backdrop-filter:blur(2px);pointer-events:none}
|
||
.drop-overlay.on{display:flex}
|
||
.drop-overlay .box{display:grid;gap:6px;justify-items:center;border:3px dashed var(--accent);border-radius:24px;background:var(--surface);padding:36px 56px;text-align:center;box-shadow:var(--shadow);color:var(--muted)}
|
||
.drop-overlay .box strong{font-size:20px;color:var(--text)}
|
||
.drop-overlay .big{display:grid;place-items:center;width:68px;height:68px;border-radius:20px;background:var(--accent-soft);color:var(--accent-ink);margin-bottom:6px}
|
||
@media (hover:none){.drop-strip{display:none!important}}
|
||
.prog-card{flex:none;padding:20px}
|
||
.prog-top{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:12px}
|
||
.prog-top .pct{font-size:30px;font-weight:800;letter-spacing:-.02em}
|
||
.prog-card .bar{height:10px}
|
||
.done-card{flex:none;padding:20px}
|
||
.done-top{display:flex;gap:14px;align-items:center}
|
||
.okc{width:44px;height:44px;border-radius:50%;background:var(--ok-soft);color:var(--ok);display:grid;place-items:center;flex:none}
|
||
.done-top h2{font-size:19px}
|
||
.share{display:flex;gap:8px;margin-top:16px}
|
||
.share input{flex:1;font-family:ui-monospace,Consolas,monospace;font-size:13.5px;background:var(--surface-2)}
|
||
.kv{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:16px 0 0}
|
||
.kv div{background:var(--surface-2);border-radius:10px;padding:10px 12px}
|
||
.kv dt{font-size:12px;color:var(--muted);font-weight:600}
|
||
.kv dd{margin:2px 0 0;font-weight:600;font-size:14px;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
||
.kv code{font-family:ui-monospace,Consolas,monospace}
|
||
.done-acts{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px}
|
||
|
||
/* ---- 다운로드 ---- */
|
||
.dl-head{padding:20px;display:flex;gap:16px;align-items:center;flex-wrap:wrap}
|
||
.dl-head h1{font-size:20px;letter-spacing:-.01em}
|
||
.dl-head .meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
|
||
.dl-head .grow{flex:1;min-width:220px}
|
||
.notice{margin-top:14px;display:flex;gap:10px;align-items:center;padding:10px 14px;border-radius:12px;background:var(--ok-soft);color:var(--ok);font-size:14px;font-weight:600}
|
||
.dl-list{margin-top:14px}
|
||
.center-card{max-width:420px;margin:48px auto;padding:28px;text-align:center}
|
||
.center-card .big{width:56px;height:56px;border-radius:16px;margin:0 auto 14px;display:grid;place-items:center;background:var(--accent-soft);color:var(--accent-ink)}
|
||
.center-card h2{font-size:19px;margin-bottom:6px}
|
||
.err{color:var(--danger);font-size:13px;font-weight:600;margin-top:8px;min-height:20px}
|
||
|
||
/* ---- 관리자 ---- */
|
||
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
|
||
.stat{padding:14px 16px}
|
||
.stat .k{font-size:12px;color:var(--muted);font-weight:600}
|
||
.stat .v{font-size:22px;font-weight:800;letter-spacing:-.02em;margin-top:2px}
|
||
.tools{display:flex;gap:10px;margin:16px 0 12px;flex-wrap:wrap}
|
||
.tools .s{position:relative;flex:1;min-width:200px}
|
||
.tools .s .ic{position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--muted)}
|
||
.tools .s input{padding-left:34px}
|
||
.tools select{border:1px solid var(--border);background:var(--surface);border-radius:10px;padding:9px 12px;font-size:14px}
|
||
table{width:100%;border-collapse:collapse;font-size:14px}
|
||
th{text-align:left;font-size:12px;color:var(--muted);font-weight:700;padding:10px 14px;border-bottom:1px solid var(--border);white-space:nowrap}
|
||
td{padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:middle;white-space:nowrap}
|
||
tr.b{cursor:pointer}
|
||
tr.b:hover td{background:var(--surface-2)}
|
||
tr.frow td{background:var(--surface-2)}
|
||
.mono{font-family:ui-monospace,Consolas,monospace;font-size:13px}
|
||
td.r,th.r{text-align:right}
|
||
.tw{overflow-x:auto}
|
||
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:16px}
|
||
.tabs button{background:none;border:0;padding:10px 14px;font-weight:700;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;font-size:14px}
|
||
.tabs button.on{color:var(--accent-ink);border-color:var(--accent)}
|
||
.set{max-width:720px}
|
||
.set .card{padding:20px;margin-bottom:16px}
|
||
.set h2{font-size:16px;margin-bottom:14px}
|
||
.fld{margin-bottom:16px}
|
||
.fld:last-child{margin-bottom:0}
|
||
.fld .help{font-size:12.5px;color:var(--muted);margin-top:6px}
|
||
.logo-row{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
|
||
.logo-prev{width:76px;height:76px;border:1px dashed var(--border);border-radius:16px;display:grid;place-items:center;background:var(--surface-2);flex:none}
|
||
.logo-prev img{width:52px;height:52px;object-fit:contain}
|
||
.pv-hdr{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--border);border-radius:12px;background:var(--surface);font-weight:800;font-size:23px;letter-spacing:-.015em}
|
||
.pv-hdr img{width:38px;height:38px;object-fit:contain}
|
||
.row2{display:grid;grid-template-columns:1fr 140px;gap:12px}
|
||
.chev{display:inline-block;transition:transform .15s}
|
||
tr.b.open .chev{transform:rotate(90deg)}
|
||
|
||
/* ---- 모달 / 토스트 ---- */
|
||
.modal-bg{position:fixed;inset:0;background:rgba(8,10,16,.6);display:none;align-items:center;justify-content:center;z-index:100;padding:16px}
|
||
.modal-bg.on{display:flex}
|
||
.modal{background:var(--surface);border-radius:16px;max-width:540px;width:100%;max-height:90dvh;display:flex;flex-direction:column;box-shadow:0 24px 60px rgba(0,0,0,.35);overflow:hidden}
|
||
.modal.wide{max-width:880px}
|
||
.mh{flex:none;display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--border)}
|
||
.mh h3{flex:1;font-size:16px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.mb{flex:1;min-height:0;overflow:auto;padding:18px}
|
||
.mf{flex:none;padding:12px 18px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end;align-items:center}
|
||
.pv{display:grid;place-items:center;min-height:320px;background:var(--surface-2);border-radius:12px;padding:12px}
|
||
.pv svg{max-width:100%;height:auto;border-radius:8px}
|
||
.paper{background:#fff;color:#222;width:min(420px,100%);aspect-ratio:210/297;border-radius:4px;box-shadow:0 4px 18px rgba(0,0,0,.2);padding:36px 32px;font-size:11px}
|
||
.paper h4{margin:0 0 14px;font-size:16px}
|
||
.paper i{display:block;height:7px;background:#e6e8ec;border-radius:3px;margin-bottom:9px}
|
||
.pv:has(pre.txt){place-items:start stretch}
|
||
pre.txt{margin:0;width:100%;white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;font-size:13px;line-height:1.6}
|
||
.qr{display:grid;place-items:center;padding:6px}
|
||
.qr canvas{width:220px;height:220px;image-rendering:pixelated;background:#fff;border-radius:8px;padding:10px}
|
||
.my-item{display:flex;gap:12px;align-items:center;padding:12px 0}
|
||
.my-item+.my-item{border-top:1px solid var(--border)}
|
||
.note{font-size:12.5px;color:var(--muted);display:flex;gap:6px}
|
||
.toast{position:fixed;left:50%;bottom:28px;transform:translate(-50%,20px);background:#1b2030;color:#fff;padding:10px 18px;border-radius:99px;font-size:14px;font-weight:600;opacity:0;pointer-events:none;transition:.2s;z-index:200}
|
||
.toast.on{opacity:1;transform:translate(-50%,0)}
|
||
|
||
@media (max-width:760px){
|
||
.ctl{grid-template-columns:1fr;gap:18px}
|
||
.kv{grid-template-columns:1fr}
|
||
.stats{grid-template-columns:repeat(2,1fr)}
|
||
}
|
||
@media (max-width:640px){
|
||
.hdr .in{padding:10px 14px}
|
||
.wrap{padding:14px}
|
||
.btn .hide-m{display:none}
|
||
.ctl-top{flex-wrap:wrap}
|
||
.ctl-top .sum{display:none}
|
||
.ctl-top .btn.up{flex:1}
|
||
.ctl-top .warn{flex-basis:100%;order:3}
|
||
.ctl-toggle{display:flex}
|
||
.ctl.collapsed{display:none}
|
||
.row{gap:10px}
|
||
.acts .btn .hide-m{display:none}
|
||
.dl-all{width:100%}
|
||
.dl-all .btn{flex:1}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="mock-bar" id="bar">
|
||
<b>목업</b>
|
||
<div class="grp">화면
|
||
<div class="seg" id="seg-view">
|
||
<button data-v="upload" class="on">업로드</button><button data-v="download">다운로드</button><button data-v="admin">관리자</button>
|
||
</div>
|
||
</div>
|
||
<div class="grp" id="grp-up">상태
|
||
<div class="seg" id="seg-up">
|
||
<button data-s="empty">빈 화면</button><button data-s="files" class="on">파일 선택</button><button data-s="uploading">업로드 중</button><button data-s="done">완료</button>
|
||
</div>
|
||
</div>
|
||
<div class="grp" id="grp-dl" style="display:none">상태
|
||
<div class="seg" id="seg-dl">
|
||
<button data-s="list" class="on">목록</button><button data-s="password">비밀번호 요구</button><button data-s="internal">사내 인증</button><button data-s="onetime">1회 다운로드</button><button data-s="expired">만료됨</button>
|
||
</div>
|
||
</div>
|
||
<div class="grp">테마
|
||
<div class="seg" id="seg-theme">
|
||
<button data-t="auto" class="on">자동</button><button data-t="light">라이트</button><button data-t="dark">다크</button>
|
||
</div>
|
||
</div>
|
||
<span style="color:#7f8aa3">창 너비를 줄이면 모바일 배치를 볼 수 있습니다 · 비밀번호 화면 예시 입력: ajin</span>
|
||
</div>
|
||
|
||
<!-- ================= 업로드 ================= -->
|
||
<section class="view on" id="view-upload">
|
||
<header class="hdr"><div class="in">
|
||
<div class="brand"><img src="logo.png" alt=""><span class="bn">AJIN Transfer</span></div>
|
||
<div class="spacer"></div>
|
||
<button class="btn ghost" id="btn-my"><i data-i="history"></i><span class="hide-m">내 업로드</span></button>
|
||
<button class="btn" id="btn-new"><i data-i="plus"></i><span class="hide-m">새 업로드</span></button>
|
||
</div></header>
|
||
|
||
<div class="wrap">
|
||
<!-- 상단 고정 영역: 설정 + 업로드 버튼 -->
|
||
<div class="card ctl-card" id="ctl-card">
|
||
<div class="ctl-top">
|
||
<button class="btn" id="btn-add" title="파일을 화면 어디에든 끌어다 놓거나 Ctrl+V로 붙여넣을 수도 있어요"><i data-i="plus"></i>파일 추가</button>
|
||
<div class="sum" id="sum"></div>
|
||
<span class="warn" id="warn"></span>
|
||
<button class="btn primary up" id="btn-up"><i data-i="upload"></i>업로드</button>
|
||
</div>
|
||
<button class="ctl-toggle" id="ctl-toggle" aria-expanded="false"><span id="ctl-sum">설정</span><i data-i="chevron"></i></button>
|
||
<div class="ctl collapsed" id="ctl">
|
||
<div class="fl">
|
||
<span class="lbl" id="ret-lbl">보관 기간</span>
|
||
<div class="chips" role="radiogroup" aria-labelledby="ret-lbl" id="chips"></div>
|
||
</div>
|
||
<div>
|
||
<label class="sr" for="pw">비밀번호</label>
|
||
<div class="pw">
|
||
<div class="pwi"><input type="text" id="pw" placeholder="비밀번호 (필수)" autocomplete="off"><button class="clr" id="btn-clear" title="비밀번호 지우기" aria-label="비밀번호 지우기" style="display:none"><i data-i="x" data-s="15"></i></button></div>
|
||
<button class="btn ic-only" id="btn-gen" title="무작위 비밀번호 생성" aria-label="무작위 비밀번호 생성"><i data-i="refresh"></i></button>
|
||
</div>
|
||
<label class="chk" id="chk-wrap"><input type="checkbox" id="chk" checked><span>인증된 사용자는 비밀번호 없이 다운로드</span></label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 업로드 중 -->
|
||
<div class="card prog-card" id="prog-card" style="display:none">
|
||
<div class="prog-top"><span class="pct" id="pct">0%</span><span class="muted" id="pbytes"></span></div>
|
||
<div class="bar"><i id="pbar"></i></div>
|
||
</div>
|
||
|
||
<!-- 완료 -->
|
||
<div class="card done-card" id="done-card" style="display:none"></div>
|
||
|
||
<!-- 파일 목록 (이 영역만 스크롤) -->
|
||
<div class="card list-card" id="list-card">
|
||
<div class="list-h" id="list-h"></div>
|
||
<div class="list-b" id="list-b"></div>
|
||
<div class="drop-strip" id="drop-strip" tabindex="0" role="button"><i data-i="upload"></i>파일을 여기로 끌어다 놓거나 클릭해서 추가</div>
|
||
</div>
|
||
<input type="file" id="finput" multiple hidden>
|
||
</div>
|
||
<footer class="foot" data-copy></footer>
|
||
</section>
|
||
|
||
<!-- ================= 다운로드 ================= -->
|
||
<section class="view" id="view-download">
|
||
<header class="hdr"><div class="in">
|
||
<div class="brand"><img src="logo.png" alt=""><span class="bn">AJIN Transfer</span></div>
|
||
<div class="spacer"></div>
|
||
<button class="btn"><i data-i="plus"></i><span class="hide-m">새 업로드</span></button>
|
||
</div></header>
|
||
<div class="scroll"><div class="wrap" id="dl-body"></div></div>
|
||
<footer class="foot" data-copy></footer>
|
||
</section>
|
||
|
||
<!-- ================= 관리자 ================= -->
|
||
<section class="view" id="view-admin">
|
||
<header class="hdr"><div class="in">
|
||
<div class="brand"><img src="logo.png" alt=""><span class="bn">AJIN Transfer</span><span class="badge-admin">관리자</span></div>
|
||
<div class="spacer"></div>
|
||
<button class="btn" id="btn-refresh"><i data-i="refresh"></i><span class="hide-m">새로고침</span></button>
|
||
</div></header>
|
||
<div class="scroll"><div class="wrap" id="ad-body"></div></div>
|
||
<footer class="foot" data-copy></footer>
|
||
</section>
|
||
|
||
<!-- 모달 -->
|
||
<div class="modal-bg" id="m-qr"><div class="modal" role="dialog" aria-modal="true" aria-label="QR 코드">
|
||
<div class="mh"><h3>QR 코드</h3><button class="btn ghost ic-only" data-close aria-label="닫기"><i data-i="x"></i></button></div>
|
||
<div class="mb"><div class="qr"><canvas id="qr" width="29" height="29"></canvas></div><p class="muted" style="text-align:center;margin-top:12px;font-size:13px">휴대폰 카메라로 스캔하면 다운로드 페이지가 열립니다 (목업은 샘플 패턴)</p></div>
|
||
</div></div>
|
||
|
||
<div class="modal-bg" id="m-prev"><div class="modal wide" role="dialog" aria-modal="true" aria-label="미리보기">
|
||
<div class="mh">
|
||
<h3 id="pv-name"></h3><span class="muted" id="pv-idx" style="font-size:13px"></span>
|
||
<button class="btn sm ic-only" id="pv-prev" aria-label="이전"><i data-i="left"></i></button>
|
||
<button class="btn sm ic-only" id="pv-next" aria-label="다음"><i data-i="right"></i></button>
|
||
<button class="btn sm" id="pv-dl"><i data-i="download"></i><span class="hide-m">다운로드</span></button>
|
||
<button class="btn ghost ic-only" data-close aria-label="닫기"><i data-i="x"></i></button>
|
||
</div>
|
||
<div class="mb"><div class="pv" id="pv-body"></div></div>
|
||
</div></div>
|
||
|
||
<div class="modal-bg" id="m-my"><div class="modal" role="dialog" aria-modal="true" aria-label="내 업로드">
|
||
<div class="mh"><h3>내 업로드</h3><button class="btn ghost ic-only" data-close aria-label="닫기"><i data-i="x"></i></button></div>
|
||
<div class="mb" id="my-body"></div>
|
||
<div class="mf" style="justify-content:flex-start"><div class="note"><i data-i="info" data-s="15"></i><span>이 브라우저에서 올린 항목만 표시됩니다. 다른 PC에서는 보이지 않고, 브라우저 데이터를 지우면 사라집니다. 비밀번호는 저장하지 않습니다.</span></div></div>
|
||
</div></div>
|
||
|
||
<div class="modal-bg" id="m-confirm"><div class="modal" role="alertdialog" aria-modal="true" aria-label="삭제 확인">
|
||
<div class="mh"><h3 id="cf-title">삭제할까요?</h3></div>
|
||
<div class="mb" id="cf-body"></div>
|
||
<div class="mf"><button class="btn" data-close>취소</button><button class="btn danger" id="cf-ok"><i data-i="trash"></i>삭제</button></div>
|
||
</div></div>
|
||
|
||
<div class="drop-overlay" id="drop-overlay" aria-hidden="true"><div class="box"><div class="big"><i data-i="upload" data-s="34"></i></div><strong>여기에 놓아 파일 추가</strong><span>놓으면 목록에 추가됩니다</span></div></div>
|
||
<div class="toast" id="toast" role="status"></div>
|
||
|
||
<script>
|
||
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
|
||
const ICONS={
|
||
upload:'<path d="M12 3v12"/><path d="m17 8-5-5-5 5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>',
|
||
download:'<path d="M12 15V3"/><path d="m7 10 5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>',
|
||
plus:'<path d="M5 12h14"/><path d="M12 5v14"/>',
|
||
x:'<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||
check:'<path d="M20 6 9 17l-5-5"/>',
|
||
copy:'<rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
||
eye:'<path d="M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0"/><circle cx="12" cy="12" r="3"/>',
|
||
link:'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
||
lock:'<rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
||
trash:'<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>',
|
||
search:'<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
||
mail:'<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>',
|
||
qr:'<rect width="5" height="5" x="3" y="3" rx="1"/><rect width="5" height="5" x="16" y="3" rx="1"/><rect width="5" height="5" x="3" y="16" rx="1"/><path d="M21 16h-3a2 2 0 0 0-2 2v3"/><path d="M21 21v.01"/><path d="M12 7v3a2 2 0 0 1-2 2H7"/><path d="M3 12h.01"/><path d="M12 3h.01"/><path d="M12 16v.01"/><path d="M16 12h1"/><path d="M21 12v.01"/><path d="M12 21v-1"/>',
|
||
history:'<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
||
refresh:'<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
|
||
chevron:'<path d="m6 9 6 6 6-6"/>',
|
||
left:'<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
|
||
right:'<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
|
||
clock:'<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
|
||
info:'<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
|
||
shield:'<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/>',
|
||
alert:'<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
||
archive:'<rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/>'
|
||
};
|
||
function ic(n,s=18){return `<svg class="ic" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[n]||''}</svg>`}
|
||
function hydrate(root=document){root.querySelectorAll('i[data-i]').forEach(e=>{e.outerHTML=ic(e.dataset.i,+e.dataset.s||18)})}
|
||
|
||
const LOGO='data:image/png;base64,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';
|
||
const NOW=new Date();
|
||
const pad=n=>String(n).padStart(2,'0');
|
||
const fmtDate=d=>`${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||
const addH=h=>new Date(NOW.getTime()+h*3600e3);
|
||
function fmtSize(b){if(b<1024)return b+' B';const u=['KB','MB','GB','TB'];let i=-1;do{b/=1024;i++}while(b>=1024&&i<3);return (b>=100?b.toFixed(0):b.toFixed(1))+' '+u[i]}
|
||
function remain(d){const h=(d-NOW)/3600e3;if(h<1)return Math.max(1,Math.round(h*60))+'분 남음';if(h<48)return Math.round(h)+'시간 남음';return Math.round(h/24)+'일 남음'}
|
||
function toast(m){const t=$('#toast');t.textContent=m;t.classList.add('on');clearTimeout(toast.t);toast.t=setTimeout(()=>t.classList.remove('on'),1800)}
|
||
async function copy(text,msg){try{await navigator.clipboard.writeText(text)}catch(e){}toast(msg||'복사했습니다')}
|
||
/* 관리자 설정에서 바꾸는 값 (실제 구현에서는 서버의 settings.json) */
|
||
const BRAND={name:'AJIN Transfer',logo:LOGO,owner:'아진산업(주) 전장선행개발팀',from:2026};
|
||
function applyBrand(){
|
||
$$('.brand img').forEach(i=>i.src=BRAND.logo);
|
||
$$('.brand .bn').forEach(s=>s.textContent=BRAND.name);
|
||
document.title=BRAND.name+' 목업';
|
||
$('link[rel=icon]').href=BRAND.logo;
|
||
const yy=new Date().getFullYear();
|
||
$$('[data-copy]').forEach(e=>e.textContent=`© ${BRAND.from}${yy>BRAND.from?'–'+yy:''} ${BRAND.owner}`);
|
||
}
|
||
|
||
const SAMPLE=[
|
||
{name:'설계검토_회의록.pdf',size:1.8*1048576,ext:'PDF',cls:'t-pdf',prev:'pdf',cmt:'11/4 회의 결과'},
|
||
{name:'시험결과_v3.xlsx',size:4.2*1048576,ext:'XLSX',cls:'t-xls',cmt:''},
|
||
{name:'시제품_사진_01.jpg',size:3.6*1048576,ext:'JPG',cls:'t-img',prev:'image',cmt:'외관 정면'},
|
||
{name:'CAN_로그_1105.zip',size:120*1048576,ext:'ZIP',cls:'t-zip',cmt:''},
|
||
{name:'README.txt',size:2150,ext:'TXT',cls:'t-txt',prev:'text',cmt:''}
|
||
];
|
||
/* 썸네일: 실제 구현에서는 업로드하는 브라우저가 작은 이미지를 만들어 파일과 함께 서버에 올림 */
|
||
const svgURI=s=>'data:image/svg+xml;utf8,'+encodeURIComponent(s);
|
||
SAMPLE[0].thumb=svgURI('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52"><rect width="52" height="52" fill="#e9ecf2"/><rect x="11" y="5" width="30" height="42" rx="2" fill="#fff" stroke="#cfd4de"/><rect x="15" y="10" width="14" height="3.5" rx="1.7" fill="#9aa3b5"/><rect x="15" y="17" width="22" height="2" fill="#d6dae3"/><rect x="15" y="22" width="22" height="2" fill="#d6dae3"/><rect x="15" y="27" width="17" height="2" fill="#d6dae3"/><rect x="15" y="33" width="22" height="2" fill="#d6dae3"/><rect x="15" y="38" width="12" height="2" fill="#d6dae3"/></svg>');
|
||
SAMPLE[2].thumb=svgURI('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52"><defs><linearGradient id="a" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#8fb3ff"/><stop offset="1" stop-color="#2b3f9e"/></linearGradient></defs><rect width="52" height="52" fill="url(#a)"/><rect x="8" y="17" width="36" height="22" rx="3" fill="#10162e" opacity=".85"/><rect x="11" y="21" width="12" height="8" rx="1.5" fill="#fcb813"/><circle cx="33" cy="25" r="4" fill="#4d69ee"/><rect x="11" y="32" width="22" height="2" rx="1" fill="#46507f"/></svg>');
|
||
SAMPLE[4].thumb=svgURI('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52"><rect width="52" height="52" fill="#fbfbfd"/><g fill="#8b93a7"><rect x="6" y="8" width="26" height="2.4" rx="1"/><rect x="6" y="14" width="38" height="2" rx="1" opacity=".7"/><rect x="6" y="19" width="30" height="2" rx="1" opacity=".7"/><rect x="6" y="24" width="36" height="2" rx="1" opacity=".7"/><rect x="6" y="29" width="22" height="2" rx="1" opacity=".7"/><rect x="6" y="34" width="34" height="2" rx="1" opacity=".7"/><rect x="6" y="39" width="18" height="2" rx="1" opacity=".7"/></g></svg>');
|
||
function tileHTML(f,pvAttr){
|
||
const inner=f.thumb?`<img src="${f.thumb}" alt=""><span class="ext">${f.ext}</span>`:f.ext;
|
||
const c=`tile ${f.cls}${f.thumb?' th':''}`;
|
||
if(pvAttr)return `<button class="${c}" ${pvAttr} title="미리보기" aria-label="${f.name} 미리보기">${inner}<span class="eye">${ic('eye',12)}</span></button>`;
|
||
return `<div class="${c}">${inner}</div>`;
|
||
}
|
||
async function makeThumb(file,k){
|
||
try{
|
||
const s=104,c=document.createElement('canvas');c.width=c.height=s;const g=c.getContext('2d');
|
||
if(k.prev==='image'){
|
||
const b=await createImageBitmap(file),r=Math.max(s/b.width,s/b.height),w=b.width*r,h=b.height*r;
|
||
g.drawImage(b,(s-w)/2,(s-h)/2,w,h);return c.toDataURL('image/webp',.75);
|
||
}
|
||
if(k.prev==='text'){
|
||
const t=await file.slice(0,800).text();g.fillStyle='#fbfbfd';g.fillRect(0,0,s,s);g.fillStyle='#5b647a';g.font='11px monospace';
|
||
t.split(/\r?\n/).slice(0,8).forEach((l,i)=>g.fillText(l.slice(0,15),8,18+i*12));return c.toDataURL('image/webp',.8);
|
||
}
|
||
}catch(e){}
|
||
return null;
|
||
}
|
||
const QUICK=[['one-time','1회 다운로드'],['86400','1일'],['259200','3일'],['432000','5일'],['604800','7일']];
|
||
const MORE=[['3600','1시간'],['21600','6시간'],['1209600','2주'],['2419200','4주'],['4838400','8주']];
|
||
const retLabel=k=>[...QUICK,...MORE].find(x=>x[0]===k)[1];
|
||
const SHARE='https://transfer.example.com/3f9a1c7be2d4';
|
||
const ACCEPT_BYPASS=true;
|
||
|
||
/* ---------------- 업로드 ---------------- */
|
||
const U={state:'files',files:[],ret:'604800',pw:'',bypass:true,timer:null};
|
||
function genPw(){const c='ABCDEFGHJKLMNPQRSTUVWXYZ23456789';let s='';for(let i=0;i<9;i++){s+=c[Math.floor(Math.random()*c.length)];if(i===2||i===5)s+='-'}return s}
|
||
const needPw=()=>U.ret!=='one-time';
|
||
function expiryDate(){return U.ret==='one-time'?null:new Date(NOW.getTime()+ +U.ret*1000)}
|
||
function newFiles(){return SAMPLE.map(f=>({...f,p:0,c:f.cmt}))}
|
||
|
||
function setUpload(s,animate){
|
||
clearInterval(U.timer);
|
||
U.state=s;
|
||
if(s==='empty')U.files=[];
|
||
else if(!U.files.length)U.files=newFiles();
|
||
if(s!=='empty'&&needPw()&&!U.pw)U.pw=genPw();
|
||
if(s==='files')U.files.forEach(f=>f.p=0);
|
||
if(s==='uploading'){
|
||
if(animate){U.files.forEach(f=>f.p=0);U.timer=setInterval(tick,110)}
|
||
else U.files.forEach((f,i)=>f.p=[100,72,41,14,0][i]??0);
|
||
}
|
||
if(s==='done')U.files.forEach(f=>f.p=100);
|
||
$$('#seg-up button').forEach(b=>b.classList.toggle('on',b.dataset.s===s));
|
||
renderUpload();
|
||
}
|
||
function tick(){
|
||
const sp=[9,6,4,2.2,12];
|
||
U.files.forEach((f,i)=>{f.p=Math.min(100,f.p+(sp[i%5])*(0.6+Math.random()*0.8))});
|
||
renderUpload();
|
||
if(U.files.every(f=>f.p>=100)){clearInterval(U.timer);setTimeout(()=>setUpload('done'),350)}
|
||
}
|
||
function totals(){const t=U.files.reduce((a,f)=>a+f.size,0);const d=U.files.reduce((a,f)=>a+f.size*f.p/100,0);return{t,d,pct:t?Math.min(100,Math.round(d/t*100)):0}}
|
||
|
||
function renderChips(){
|
||
const box=$('#chips');
|
||
const isMore=MORE.some(m=>m[0]===U.ret);
|
||
box.innerHTML=QUICK.map(([k,l])=>`<button class="chip" role="radio" aria-checked="${U.ret===k}" data-k="${k}">${l}</button>`).join('')+
|
||
`<span class="sel"><select id="more" aria-label="그 외 보관 기간" class="${isMore?'active':''}"><option value="" ${isMore?'':'selected'}>그 외</option>${MORE.map(([k,l])=>`<option value="${k}" ${U.ret===k?'selected':''}>${l}</option>`).join('')}</select></span>`;
|
||
box.querySelectorAll('.chip').forEach(b=>b.onclick=()=>{U.ret=b.dataset.k;afterSettings()});
|
||
$('#more').onchange=e=>{if(e.target.value){U.ret=e.target.value;afterSettings()}};
|
||
}
|
||
function afterSettings(){renderChips();syncControls();}
|
||
function syncControls(){
|
||
const np=needPw();
|
||
$('#pw').placeholder=np?'비밀번호 (필수)':'비밀번호 (선택)';
|
||
const hasPw=U.pw.trim().length>0;
|
||
$('#btn-clear').style.display=U.pw.length>0?'':'none';
|
||
$('#chk').disabled=!hasPw;$('#chk-wrap').classList.toggle('off',!hasPw);
|
||
$('#chk').checked=U.bypass&&hasPw;
|
||
const n=U.files.length,{t}=totals();
|
||
$('#sum').textContent=n?`파일 ${n}개 · ${fmtSize(t)} · ${retLabel(U.ret)}`:'파일을 추가해 주세요';
|
||
const blockPw=np&&!hasPw;
|
||
$('#warn').textContent=n&&blockPw?'비밀번호를 입력하세요':'';
|
||
$('#btn-up').disabled=!n||blockPw;
|
||
$('#ctl-sum').textContent=`${retLabel(U.ret)} · ${hasPw?'비밀번호 설정됨':np?'비밀번호 필요':'비밀번호 없음'}`;
|
||
}
|
||
|
||
function rowHTML(f,i,mode){
|
||
const editing=mode==='files';
|
||
const showBar=mode==='uploading'||mode==='done';
|
||
return `<div class="row">${tileHTML(f,f.prev?`data-pvu="${i}"`:'')}
|
||
<div class="rm"><div class="rn" title="${f.name}">${f.name}</div>
|
||
<div class="rs">${fmtSize(f.size)}${showBar?` · ${Math.round(f.p)}%`:''}</div>
|
||
${showBar?`<div class="bar ${f.p>=100?'ok':''}"><i style="width:${f.p}%"></i></div>`:''}
|
||
${editing||f.c?`<input class="cmt" type="text" data-i="${i}" placeholder="코멘트 (선택)" value="${f.c||''}" ${editing?'':'disabled'}>`:''}
|
||
</div>
|
||
<div class="acts">${editing?`<button class="btn ghost ic-only" data-rm="${i}" aria-label="${f.name} 삭제">${ic('x')}</button>`:f.p>=100?`<span class="tag ok">${ic('check',13)}완료</span>`:''}</div></div>`;
|
||
}
|
||
function renderUpload(){
|
||
const s=U.state;
|
||
const showCtl=s==='empty'||s==='files';
|
||
$('#ctl-card').style.display=showCtl?'':'none';
|
||
$('#prog-card').style.display=s==='uploading'?'':'none';
|
||
$('#done-card').style.display=s==='done'?'':'none';
|
||
$('#btn-add').disabled=false;
|
||
$('#drop-strip').style.display=(s==='files'&&U.files.length)?'flex':'none';
|
||
if(showCtl){
|
||
renderChips();
|
||
if($('#pw').value!==U.pw)$('#pw').value=U.pw;
|
||
syncControls();
|
||
}
|
||
if(s==='uploading'){
|
||
const {t,d,pct}=totals();
|
||
$('#pct').textContent=pct+'%';$('#pbytes').textContent=`${fmtSize(d)} / ${fmtSize(t)}`;$('#pbar').style.width=pct+'%';
|
||
}
|
||
if(s==='done')renderDone();
|
||
const {t}=totals();
|
||
$('#list-h').innerHTML=U.files.length?`파일 <span>${U.files.length}개 · ${fmtSize(t)}</span>`:'파일';
|
||
const lb=$('#list-b');
|
||
if(!U.files.length){
|
||
lb.innerHTML=`<div class="drop" tabindex="0" role="button" id="drop"><div><div class="big">${ic('upload',30)}</div><strong>여기에 파일을 끌어다 놓으세요</strong>또는 클릭해서 파일을 선택합니다</div></div>`;
|
||
lb.style.display='flex';lb.style.padding='0';
|
||
$('#drop').onclick=addFile;$('#drop').onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();addFile()}};
|
||
}else{
|
||
lb.style.display='';lb.style.padding='';
|
||
lb.innerHTML=U.files.map((f,i)=>rowHTML(f,i,s)).join('');
|
||
lb.querySelectorAll('[data-rm]').forEach(b=>b.onclick=()=>{U.files.splice(+b.dataset.rm,1);if(!U.files.length)setUpload('empty');else renderUpload()});
|
||
lb.querySelectorAll('.cmt').forEach(inp=>inp.oninput=()=>{U.files[+inp.dataset.i].c=inp.value});
|
||
lb.querySelectorAll('[data-pvu]').forEach(b=>b.onclick=()=>openPreview(+b.dataset.pvu,U.files.map((f,i)=>({f,i})).filter(x=>x.f.prev),true));
|
||
}
|
||
}
|
||
function renderDone(){
|
||
const exp=expiryDate();
|
||
const hasPw=U.pw.trim().length>0;
|
||
$('#done-card').innerHTML=`
|
||
<div class="done-top"><div class="okc">${ic('check',24)}</div><div><h2>업로드가 완료되었습니다</h2><p class="muted">받는 사람은 만료 전까지 이 링크로 파일을 내려받을 수 있습니다.</p></div></div>
|
||
<div class="share"><input type="text" readonly value="${SHARE}" aria-label="다운로드 링크"><button class="btn" id="d-copy">${ic('copy')}링크 복사</button></div>
|
||
<dl class="kv">
|
||
<div><dt>비밀번호</dt><dd>${hasPw?`<code>${U.pw}</code>`:'<span class="muted">없음</span>'}</dd></div>
|
||
<div><dt>만료</dt><dd>${exp?`${fmtDate(exp)} <span class="muted" style="font-weight:500">(${remain(exp)})</span>`:'1회 다운로드 후 삭제'}</dd></div>
|
||
<div><dt>비밀번호 면제</dt><dd>${hasPw&&U.bypass?'인증된 사용자':'<span class="muted">없음</span>'}</dd></div>
|
||
</dl>
|
||
<div class="done-acts">
|
||
<button class="btn primary" id="d-both">${ic('copy')}링크와 비밀번호 복사</button>
|
||
<button class="btn" id="d-qr">${ic('qr')}QR 코드</button>
|
||
<button class="btn" id="d-mail">${ic('mail')}이메일</button>
|
||
</div>`;
|
||
$('#d-copy').onclick=()=>copy(SHARE,'링크를 복사했습니다');
|
||
$('#d-both').onclick=()=>{
|
||
const lines=[`${BRAND.name} 파일 공유`,`다운로드 링크: ${SHARE}`];
|
||
if(hasPw)lines.push(`비밀번호: ${U.pw}`);
|
||
lines.push(`만료: ${exp?fmtDate(exp):'1회 다운로드 후 삭제'}`);
|
||
copy(lines.join('\n'),hasPw?'링크와 비밀번호를 복사했습니다':'링크를 복사했습니다');
|
||
};
|
||
$('#d-qr').onclick=()=>{drawQR();openModal('m-qr')};
|
||
$('#d-mail').onclick=()=>toast('메일 앱이 열립니다 (본문에 비밀번호는 넣지 않음)');
|
||
}
|
||
const editable=()=>U.state==='empty'||U.state==='files';
|
||
function addFile(){if(editable())$('#finput').click()}
|
||
function kind(name,type){
|
||
const e=(name.split('.').pop()||'').toLowerCase();
|
||
const lab=(e.length>0&&e.length<=4?e:'file').toUpperCase();
|
||
if(/^(jpe?g|png|gif|webp|svg|bmp|avif)$/.test(e)||/^image\//.test(type))return{ext:lab,cls:'t-img',prev:'image'};
|
||
if(e==='pdf')return{ext:'PDF',cls:'t-pdf',prev:'pdf'};
|
||
if(/^(xlsx?|csv)$/.test(e))return{ext:lab,cls:'t-xls'};
|
||
if(/^(docx?|pptx?|hwp|hwpx)$/.test(e))return{ext:lab,cls:'t-doc'};
|
||
if(/^(zip|7z|tar|gz|rar)$/.test(e))return{ext:lab,cls:'t-zip'};
|
||
if(/^(txt|md|log|json|xml|csv|ini|conf)$/.test(e))return{ext:lab,cls:'t-txt',prev:'text'};
|
||
return{ext:lab,cls:'t-txt'};
|
||
}
|
||
function addFiles(list){
|
||
if(!editable()){toast('업로드 중에는 파일을 추가할 수 없습니다');return}
|
||
const arr=[...list];if(!arr.length)return;
|
||
const added=arr.map(f=>{const it={name:f.name,size:f.size,...kind(f.name,f.type),file:f,p:0,c:''};U.files.push(it);return it});
|
||
added.forEach(it=>makeThumb(it.file,it).then(t=>{if(t&&U.files.includes(it)){it.thumb=t;renderUpload()}}));
|
||
if(needPw()&&!U.pw)U.pw=genPw();
|
||
setUpload('files');
|
||
toast(`파일 ${arr.length}개를 추가했습니다`);
|
||
}
|
||
$('#finput').onchange=e=>{addFiles(e.target.files);e.target.value=''};
|
||
$('#drop-strip').onclick=addFile;
|
||
$('#drop-strip').onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();addFile()}};
|
||
/* 화면 어디에 놓아도 추가 + 끌고 들어오면 오버레이 */
|
||
let dragN=0;const hasFiles=e=>e.dataTransfer&&[...(e.dataTransfer.types||[])].includes('Files');
|
||
addEventListener('dragenter',e=>{if(!hasFiles(e))return;e.preventDefault();dragN++;if(editable()&&$('#view-upload').classList.contains('on'))$('#drop-overlay').classList.add('on')});
|
||
addEventListener('dragover',e=>{if(hasFiles(e))e.preventDefault()});
|
||
addEventListener('dragleave',e=>{if(!hasFiles(e))return;dragN=Math.max(0,dragN-1);if(!dragN)$('#drop-overlay').classList.remove('on')});
|
||
addEventListener('drop',e=>{if(!hasFiles(e))return;e.preventDefault();dragN=0;$('#drop-overlay').classList.remove('on');if($('#view-upload').classList.contains('on'))addFiles(e.dataTransfer.files)});
|
||
/* Ctrl+V 붙여넣기 */
|
||
addEventListener('paste',e=>{
|
||
if(!$('#view-upload').classList.contains('on')||!editable())return;
|
||
const fs=[...(e.clipboardData?e.clipboardData.files:[])];if(!fs.length)return;
|
||
e.preventDefault();
|
||
const t=new Date(),stamp=`${pad(t.getHours())}${pad(t.getMinutes())}${pad(t.getSeconds())}`;
|
||
addFiles(fs.map((f,i)=>/^image\./.test(f.name)?new File([f],`붙여넣기_${stamp}${fs.length>1?'_'+(i+1):''}.${(f.type.split('/')[1]||'png')}`,{type:f.type}):f));
|
||
});
|
||
$('#btn-add').onclick=addFile;
|
||
$('#pw').oninput=e=>{U.pw=e.target.value;syncControls()};
|
||
$('#btn-clear').onclick=()=>{U.pw='';$('#pw').value='';syncControls();$('#pw').focus()};
|
||
$('#btn-gen').onclick=()=>{U.pw=genPw();$('#pw').value=U.pw;syncControls()};
|
||
$('#chk').onchange=e=>{U.bypass=e.target.checked};
|
||
$('#btn-up').onclick=()=>setUpload('uploading',true);
|
||
$('#btn-new').onclick=()=>{if(U.state==='uploading'&&!confirm('업로드가 진행 중입니다. 새로 시작하면 진행 중인 업로드가 중단됩니다. 계속할까요?'))return;U.pw='';U.ret='604800';U.bypass=true;U.files=[];setUpload('empty')};
|
||
$('#ctl-toggle').onclick=()=>{const c=$('#ctl'),open=c.classList.toggle('collapsed')===false;$('#ctl-toggle').setAttribute('aria-expanded',open)};
|
||
|
||
/* ---------------- 내 업로드 ---------------- */
|
||
function renderMy(){
|
||
const items=[
|
||
{sid:'3f9a1c7be2d4',names:'설계검토_회의록.pdf 외 4개',size:131.7e6,at:addH(-1),exp:addH(24*7-1),st:'ok'},
|
||
{sid:'a81c20f9d7e3',names:'시험성적서_최종.pdf',size:2.4e6,at:addH(-30),exp:addH(24*2),st:'ok'},
|
||
{sid:'71be09c4a2f8',names:'펌웨어_v2.4.bin 외 1개',size:18e6,at:addH(-72),exp:null,st:'gone',note:'1회 다운로드 완료'},
|
||
{sid:'0d5e7a13b9c6',names:'회의자료_10월.pptx',size:9.1e6,at:addH(-24*9),exp:addH(-24*2),st:'expired'}
|
||
];
|
||
$('#my-body').innerHTML=items.map(it=>{
|
||
const tag=it.st==='ok'?`<span class="tag ok">${remain(it.exp)}</span>`:it.st==='expired'?'<span class="tag bad">만료됨</span>':`<span class="tag warn">${it.note}</span>`;
|
||
const live=it.st==='ok';
|
||
return `<div class="my-item"><div class="rm"><div class="rn">${it.names}</div>
|
||
<div class="rs">${fmtDate(it.at)} · ${fmtSize(it.size)} · <span class="mono">${it.sid}</span></div></div>
|
||
${tag}
|
||
<div class="acts">${live?`<button class="btn sm ic-only" title="링크 복사" aria-label="링크 복사" data-cp="${it.sid}">${ic('copy',16)}</button>`:''}
|
||
<button class="btn sm ic-only ghost" title="목록에서 제거" aria-label="목록에서 제거" data-del>${ic('x',16)}</button></div></div>`}).join('');
|
||
$$('#my-body [data-cp]').forEach(b=>b.onclick=()=>copy('https://transfer.example.com/'+b.dataset.cp,'링크를 복사했습니다'));
|
||
$$('#my-body [data-del]').forEach(b=>b.onclick=()=>{b.closest('.my-item').remove();toast('목록에서 제거했습니다')});
|
||
}
|
||
$('#btn-my').onclick=()=>{renderMy();openModal('m-my')};
|
||
|
||
/* ---------------- 다운로드 ---------------- */
|
||
const D={state:'list'};
|
||
const DL_EXP=addH(24*7-1);
|
||
function dlRow(f,i,one){
|
||
const canPrev=f.prev&&!one;
|
||
return `<div class="row">${tileHTML(one?{...f,thumb:null}:f,canPrev?`data-pv="${i}"`:'')}
|
||
<div class="rm"><div class="rn" title="${f.name}">${f.name}</div><div class="rs">${fmtSize(f.size)}${f.cmt?` · ${f.cmt}`:''}</div></div>
|
||
<div class="acts">${canPrev?`<button class="btn sm" data-pv="${i}">${ic('eye',16)}<span class="hide-m">미리보기</span></button>`:''}
|
||
<button class="btn sm primary" data-dl="${i}">${ic('download',16)}<span class="hide-m">다운로드</span></button>
|
||
<button class="btn sm ic-only" data-cp="${i}" title="링크 복사" aria-label="링크 복사">${ic('copy',16)}</button></div></div>`;
|
||
}
|
||
function renderDL(){
|
||
const b=$('#dl-body'),s=D.state;
|
||
if(s==='password'){
|
||
b.innerHTML=`<div class="card center-card"><div class="big">${ic('lock',26)}</div><h2>비밀번호가 필요합니다</h2><p class="muted" style="margin-bottom:18px">보낸 사람에게 받은 비밀번호를 입력하세요.</p>
|
||
<form id="pwf"><input type="password" id="dpw" placeholder="비밀번호" autofocus autocomplete="off"><div class="err" id="derr"></div>
|
||
<button class="btn primary lg" style="width:100%;margin-top:6px" type="submit">확인</button></form></div>`;
|
||
$('#pwf').onsubmit=e=>{e.preventDefault();if($('#dpw').value==='ajin'){D.state='list';syncDlBar();renderDL()}else{$('#derr').textContent='비밀번호가 올바르지 않습니다'}};
|
||
return;
|
||
}
|
||
if(s==='expired'){
|
||
b.innerHTML=`<div class="card center-card"><div class="big" style="background:var(--danger-soft);color:var(--danger)">${ic('alert',26)}</div><h2>링크를 찾을 수 없습니다</h2><p class="muted" style="margin-bottom:18px">만료되었거나 삭제된 링크입니다. 보낸 사람에게 다시 요청하세요.</p><button class="btn primary">${ic('plus')}새 업로드</button></div>`;
|
||
return;
|
||
}
|
||
const one=s==='onetime';
|
||
const tot=SAMPLE.reduce((a,f)=>a+f.size,0);
|
||
const exp=one?`<span class="tag warn">${ic('alert',13)}1회 다운로드 후 삭제됩니다</span>`:`<span class="tag ${(DL_EXP-NOW)/36e5<24?'warn':''}">${ic('clock',13)}${fmtDate(DL_EXP)} 만료 · ${remain(DL_EXP)}</span>`;
|
||
b.innerHTML=`<div class="card dl-head"><div class="grow"><h1>공유된 파일 ${SAMPLE.length}개</h1>
|
||
<div class="meta"><span class="tag opt">${fmtSize(tot)}</span>${exp}${one?'':`<span class="tag opt">${ic('lock',13)}비밀번호 보호</span>`}</div></div>
|
||
<div class="fl dl-all" role="group" aria-labelledby="all-lbl"><span class="lbl" id="all-lbl">전체 받기</span><button class="btn" data-all="zip">${ic('download',16)}.zip</button><button class="btn" data-all="tar.gz">${ic('download',16)}.tar.gz</button></div></div>
|
||
${s==='internal'?`<div class="notice">${ic('shield',18)}사내 인증이 확인되어 비밀번호 없이 열렸습니다.</div>`:''}
|
||
<div class="card dl-list"><div class="list-b" style="overflow:visible">${SAMPLE.map((f,i)=>dlRow(f,i,one)).join('')}</div></div>`;
|
||
b.querySelectorAll('[data-pv]').forEach(x=>x.onclick=()=>openPreview(+x.dataset.pv));
|
||
b.querySelectorAll('[data-dl]').forEach(x=>x.onclick=()=>toast('다운로드를 시작합니다 (같은 탭에서 저장)'));
|
||
b.querySelectorAll('[data-cp]').forEach(x=>x.onclick=()=>copy(`${SHARE.replace(/\/[^/]*$/,'')}/files/…`,'파일 링크를 복사했습니다'));
|
||
b.querySelectorAll('[data-all]').forEach(x=>x.onclick=()=>toast(`${x.dataset.all} 전체 다운로드를 시작합니다`));
|
||
}
|
||
function syncDlBar(){$$('#seg-dl button').forEach(b=>b.classList.toggle('on',b.dataset.s===D.state))}
|
||
|
||
/* ---------------- 미리보기 ---------------- */
|
||
let PV={list:[],i:0};
|
||
function renderReal(f,body){
|
||
const url=f._url||(f._url=URL.createObjectURL(f.file));
|
||
if(f.prev==='image')body.innerHTML=`<img src="${url}" alt="${f.name.replace(/"/g,'"')}" style="max-width:100%;max-height:68vh;border-radius:8px">`;
|
||
else if(f.prev==='pdf')body.innerHTML=`<iframe src="${url}" title="${f.name.replace(/"/g,'"')}" style="width:100%;height:68vh;border:0;border-radius:8px;background:#fff"></iframe>`;
|
||
else{body.innerHTML='<pre class="txt"></pre>';const cur=f;f.file.slice(0,200000).text().then(t=>{if(PV.list[PV.i]&&PV.list[PV.i].f===cur)body.firstChild.textContent=t})}
|
||
}
|
||
function openPreview(idx,list,local){
|
||
PV.local=!!local;
|
||
PV.list=list||SAMPLE.map((f,i)=>({f,i})).filter(x=>x.f.prev);
|
||
PV.i=Math.max(0,PV.list.findIndex(x=>x.i===idx));
|
||
renderPreview();openModal('m-prev');
|
||
}
|
||
function renderPreview(){
|
||
const {f}=PV.list[PV.i];
|
||
$('#pv-name').textContent=f.name;$('#pv-idx').textContent=`${PV.i+1} / ${PV.list.length}`;
|
||
$('#pv-prev').style.visibility=PV.i>0?'':'hidden';$('#pv-next').style.visibility=PV.i<PV.list.length-1?'':'hidden';
|
||
const body=$('#pv-body');
|
||
$('#pv-dl').style.display=PV.local?'none':'';
|
||
if(f.file)return renderReal(f,body);
|
||
if(f.prev==='image')body.innerHTML=`<svg viewBox="0 0 800 480" role="img" aria-label="이미지 미리보기 샘플"><defs><linearGradient id="g1" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#8fb3ff"/><stop offset="1" stop-color="#2b3f9e"/></linearGradient></defs><rect width="800" height="480" fill="url(#g1)"/><rect x="170" y="150" width="460" height="230" rx="18" fill="#10162e" opacity=".85"/><rect x="200" y="180" width="140" height="80" rx="8" fill="#fcb813"/><circle cx="470" cy="220" r="34" fill="#4d69ee"/><rect x="540" y="196" width="60" height="48" rx="6" fill="#8fb3ff"/><rect x="200" y="290" width="380" height="14" rx="7" fill="#46507f"/><rect x="200" y="318" width="260" height="14" rx="7" fill="#46507f"/></svg>`;
|
||
else if(f.prev==='pdf')body.innerHTML=`<div class="paper"><h4>설계검토 회의록</h4>${'<i></i><i></i><i style="width:72%"></i><div style="height:10px"></div>'.repeat(4)}</div>`;
|
||
else body.innerHTML=`<pre class="txt">AJIN Transfer 샘플 README
|
||
|
||
- 시험 일자: 2026-11-04
|
||
- 장비: 시제품 A-2
|
||
- 비고: 로그 파일은 CAN_로그_1105.zip 참고
|
||
|
||
문의: 전장선행개발팀</pre>`;
|
||
}
|
||
$('#pv-prev').onclick=()=>{if(PV.i>0){PV.i--;renderPreview()}};
|
||
$('#pv-next').onclick=()=>{if(PV.i<PV.list.length-1){PV.i++;renderPreview()}};
|
||
$('#pv-dl').onclick=()=>toast('다운로드를 시작합니다');
|
||
|
||
/* ---------------- 관리자 ---------------- */
|
||
let BK=[
|
||
{sid:'3f9a1c7be2d4',lock:true,created:addH(-1),last:addH(-0.5),exp:addH(24*7-1),files:SAMPLE.map((f,i)=>({...f,i}))},
|
||
{sid:'a81c20f9d7e3',lock:true,created:addH(-30),last:null,exp:addH(24*2-6),files:[{...SAMPLE[0],name:'시험성적서_최종.pdf'}]},
|
||
{sid:'71be09c4a2f8',lock:false,created:addH(-5),last:null,exp:null,one:true,files:[{...SAMPLE[3],name:'펌웨어_v2.4.bin',ext:'BIN',size:17*1048576},{...SAMPLE[4],name:'릴리즈노트.txt',size:4200}]},
|
||
{sid:'c92e4d18a0b7',lock:true,created:addH(-20),last:addH(-3),exp:addH(5),files:[{...SAMPLE[2],name:'라인_점검_사진.jpg'},{...SAMPLE[2],name:'라인_점검_사진2.jpg'}]}
|
||
];
|
||
let AD={q:'',sort:'new',open:null,tab:'files'};
|
||
function adminTabs(){return `<div class="tabs" role="tablist"><button role="tab" data-tab="files" class="${AD.tab==='files'?'on':''}">파일 관리</button><button role="tab" data-tab="settings" class="${AD.tab==='settings'?'on':''}">설정</button></div>`}
|
||
function bindTabs(){$$('#ad-body [data-tab]').forEach(b=>b.onclick=()=>{AD.tab=b.dataset.tab;renderAdmin()})}
|
||
let DRAFT=null;
|
||
function renderSettings(){
|
||
if(!DRAFT)DRAFT={...BRAND};
|
||
const yy=new Date().getFullYear();
|
||
$('#ad-body').innerHTML=adminTabs()+`<div class="set">
|
||
<div class="card"><h2>브랜드</h2>
|
||
<div class="fld"><label class="lbl" for="s-name">서비스 이름</label><input type="text" id="s-name" value="${DRAFT.name}" maxlength="40"><div class="help">상단 로고 옆, 브라우저 탭 제목, 메일 제목에 표시됩니다.</div></div>
|
||
<div class="fld"><div class="lbl">로고</div>
|
||
<div class="logo-row"><div class="logo-prev"><img id="s-logo" src="${DRAFT.logo}" alt="현재 로고"></div>
|
||
<div><div style="display:flex;gap:8px;flex-wrap:wrap"><button class="btn" id="s-up">${ic('upload',16)}이미지 선택</button><button class="btn ghost" id="s-reset">기본 로고로 되돌리기</button></div>
|
||
<div class="help">PNG · SVG · WebP, 최대 1MB. 정사각형을 권장합니다. 브라우저 탭 아이콘으로도 사용됩니다.</div></div></div>
|
||
<input type="file" id="s-file" accept="image/png,image/svg+xml,image/webp" hidden></div>
|
||
<div class="fld"><div class="lbl">하단 저작권 표기</div>
|
||
<div class="row2"><input type="text" id="s-owner" value="${DRAFT.owner}" aria-label="소유자 문구" maxlength="60"><input type="text" id="s-from" value="${DRAFT.from}" aria-label="시작 연도" inputmode="numeric"></div>
|
||
<div class="help">왼쪽은 소유자 문구, 오른쪽은 시작 연도입니다. 현재 연도가 더 크면 자동으로 범위로 표시됩니다.</div></div>
|
||
</div>
|
||
<div class="card"><h2>미리보기</h2>
|
||
<div class="pv-hdr"><img id="p-logo" src="${DRAFT.logo}" alt=""><span id="p-name">${DRAFT.name}</span></div>
|
||
<p class="muted" id="p-foot" style="margin-top:12px;font-size:12.5px"></p></div>
|
||
<div style="display:flex;gap:8px"><button class="btn primary" id="s-save">${ic('check',16)}저장</button><button class="btn" id="s-cancel">되돌리기</button></div>
|
||
<p class="note" style="margin-top:14px">${ic('info',15)}<span>설정은 서버 데이터 폴더(/data)에 저장되어 컨테이너를 다시 만들어도 유지됩니다.</span></p></div>`;
|
||
bindTabs();
|
||
const foot=()=>{const f=parseInt(DRAFT.from)||yy;$('#p-foot').textContent=`© ${f}${yy>f?'–'+yy:''} ${DRAFT.owner}`;$('#p-name').textContent=DRAFT.name||'(이름 없음)'};
|
||
foot();
|
||
$('#s-name').oninput=e=>{DRAFT.name=e.target.value;foot()};
|
||
$('#s-owner').oninput=e=>{DRAFT.owner=e.target.value;foot()};
|
||
$('#s-from').oninput=e=>{DRAFT.from=e.target.value;foot()};
|
||
$('#s-up').onclick=()=>$('#s-file').click();
|
||
$('#s-file').onchange=e=>{const f=e.target.files[0];if(!f)return;if(f.size>1048576){toast('1MB 이하의 이미지만 사용할 수 있습니다');return}
|
||
const r=new FileReader();r.onload=()=>{DRAFT.logo=r.result;$('#s-logo').src=r.result;$('#p-logo').src=r.result};r.readAsDataURL(f)};
|
||
$('#s-reset').onclick=()=>{DRAFT.logo=LOGO;$('#s-logo').src=LOGO;$('#p-logo').src=LOGO};
|
||
$('#s-cancel').onclick=()=>{DRAFT=null;renderSettings();toast('저장된 설정으로 되돌렸습니다')};
|
||
$('#s-save').onclick=()=>{if(!DRAFT.name.trim()){toast('서비스 이름을 입력하세요');return}Object.assign(BRAND,DRAFT,{from:parseInt(DRAFT.from)||yy});DRAFT=null;applyBrand();renderSettings();toast('저장했습니다 · 모든 화면에 바로 적용됩니다')};
|
||
}
|
||
const bSize=b=>b.files.reduce((a,f)=>a+f.size,0);
|
||
function renderAdmin(){
|
||
if(AD.tab==='settings')return renderSettings();
|
||
const total=BK.reduce((a,b)=>a+bSize(b),0),nf=BK.reduce((a,b)=>a+b.files.length,0);
|
||
const soon=BK.filter(b=>b.exp&&(b.exp-NOW)/36e5<24).length;
|
||
let list=BK.filter(b=>!AD.q||b.sid.includes(AD.q)||b.files.some(f=>f.name.toLowerCase().includes(AD.q.toLowerCase())));
|
||
const key={new:(a,b)=>b.created-a.created,size:(a,b)=>bSize(b)-bSize(a),exp:(a,b)=>(a.exp||9e15)-(b.exp||9e15)}[AD.sort];
|
||
list=list.slice().sort(key);
|
||
const expCell=b=>b.one?'<span class="tag warn">1회 다운로드</span>':`${fmtDate(b.exp)} <span class="tag ${(b.exp-NOW)/36e5<24?'warn':'opt'}">${remain(b.exp)}</span>`;
|
||
let rows='';
|
||
list.forEach(b=>{
|
||
const open=AD.open===b.sid;
|
||
rows+=`<tr class="b ${open?'open':''}" data-b="${b.sid}"><td><span class="chev">${ic('right',14)}</span> <span class="mono">${b.sid}</span> ${b.lock?`<span title="비밀번호 보호">${ic('lock',14)}</span>`:''}</td><td>${b.files.length}개</td><td class="r">${fmtSize(bSize(b))}</td><td>${fmtDate(b.created)}</td><td>${b.last?fmtDate(b.last):'<span class="muted">-</span>'}</td><td>${expCell(b)}</td>
|
||
<td class="r" style="white-space:nowrap"><button class="btn sm" data-bz="${b.sid}">${ic('download',15)}<span class="hide-m">zip</span></button> <button class="btn sm ic-only" data-bd="${b.sid}" title="bucket 삭제" aria-label="bucket 삭제">${ic('trash',15)}</button></td></tr>`;
|
||
if(open)b.files.forEach((f,fi)=>{rows+=`<tr class="frow"><td colspan="2" style="padding-left:40px"><div style="display:flex;gap:10px;align-items:center"><div class="tile ${f.cls}" style="width:32px;height:32px;font-size:9px;border-radius:8px">${f.ext}</div><div><div class="rn" style="font-weight:600">${f.name}</div>${f.cmt?`<div class="rs">${f.cmt}</div>`:''}</div></div></td><td class="r">${fmtSize(f.size)}</td><td colspan="3"></td>
|
||
<td class="r" style="white-space:nowrap">${f.prev?`<button class="btn sm ic-only" data-fp="${b.sid}|${fi}" title="미리보기" aria-label="미리보기">${ic('eye',15)}</button> `:''}<button class="btn sm ic-only" data-fd="${b.sid}|${fi}" title="다운로드" aria-label="다운로드">${ic('download',15)}</button> <button class="btn sm ic-only" data-fx="${b.sid}|${fi}" title="삭제" aria-label="삭제">${ic('trash',15)}</button></td></tr>`});
|
||
});
|
||
$('#ad-body').innerHTML=adminTabs()+`
|
||
<div class="stats"><div class="card stat"><div class="k">전체 사용 용량</div><div class="v">${fmtSize(total)}</div></div><div class="card stat"><div class="k">bucket</div><div class="v">${BK.length}개</div></div><div class="card stat"><div class="k">파일</div><div class="v">${nf}개</div></div><div class="card stat"><div class="k">24시간 내 만료</div><div class="v">${soon}개</div></div></div>
|
||
<div class="tools"><div class="s">${ic('search',16)}<input type="search" id="aq" placeholder="SID 또는 파일 이름 검색" value="${AD.q}"></div>
|
||
<select id="as" aria-label="정렬"><option value="new" ${AD.sort==='new'?'selected':''}>최신순</option><option value="size" ${AD.sort==='size'?'selected':''}>용량 큰 순</option><option value="exp" ${AD.sort==='exp'?'selected':''}>만료 임박 순</option></select></div>
|
||
<div class="card tw"><table><thead><tr><th>SID</th><th>파일</th><th class="r">크기</th><th>생성</th><th>마지막 다운로드</th><th>만료</th><th></th></tr></thead><tbody>${rows||'<tr><td colspan="7" class="muted" style="text-align:center;padding:28px">결과가 없습니다</td></tr>'}</tbody></table></div>`;
|
||
const body=$('#ad-body');
|
||
bindTabs();
|
||
$('#aq').oninput=e=>{AD.q=e.target.value;const p=e.target.selectionStart;renderAdmin();const n=$('#aq');n.focus();n.setSelectionRange(p,p)};
|
||
$('#as').onchange=e=>{AD.sort=e.target.value;renderAdmin()};
|
||
body.querySelectorAll('tr.b').forEach(tr=>tr.onclick=e=>{if(e.target.closest('button'))return;AD.open=AD.open===tr.dataset.b?null:tr.dataset.b;renderAdmin()});
|
||
body.querySelectorAll('[data-bz]').forEach(x=>x.onclick=()=>toast('관리자 다운로드: 1회 다운로드 파일도 소모되지 않습니다'));
|
||
body.querySelectorAll('[data-fd]').forEach(x=>x.onclick=()=>toast('60초 유효 서명 주소로 다운로드합니다'));
|
||
body.querySelectorAll('[data-fp]').forEach(x=>x.onclick=()=>{const [sid,fi]=x.dataset.fp.split('|');const b=BK.find(z=>z.sid===sid);const list=b.files.map((f,i)=>({f,i})).filter(z=>z.f.prev);openPreview(+fi,list)});
|
||
body.querySelectorAll('[data-bd]').forEach(x=>x.onclick=()=>{const b=BK.find(z=>z.sid===x.dataset.bd);confirmDel('bucket을 삭제할까요?',`<p><span class="mono">${b.sid}</span>의 파일 ${b.files.length}개(${fmtSize(bSize(b))})가 서버에서 즉시 삭제됩니다.</p><p class="muted" style="margin-top:8px">삭제하면 복구할 수 없고, 이 링크로는 더 이상 받을 수 없습니다.</p>`,()=>{BK=BK.filter(z=>z.sid!==b.sid);renderAdmin();toast('삭제했습니다')})});
|
||
body.querySelectorAll('[data-fx]').forEach(x=>x.onclick=()=>{const [sid,fi]=x.dataset.fx.split('|');const b=BK.find(z=>z.sid===sid),f=b.files[+fi];confirmDel('파일을 삭제할까요?',`<p><strong>${f.name}</strong> (${fmtSize(f.size)})</p><p style="margin-top:8px">받는 사람 화면에서도 즉시 사라집니다. 같은 링크의 나머지 파일은 그대로 받을 수 있습니다.</p><p class="muted" style="margin-top:8px">삭제하면 복구할 수 없습니다.</p>`,()=>{b.files.splice(+fi,1);if(!b.files.length){BK=BK.filter(z=>z!==b);AD.open=null}renderAdmin();toast('삭제했습니다')})});
|
||
}
|
||
$('#btn-refresh').onclick=()=>{renderAdmin();toast('새로고침했습니다')};
|
||
let cfCb=null;
|
||
function confirmDel(title,html,cb){$('#cf-title').textContent=title;$('#cf-body').innerHTML=html;cfCb=cb;openModal('m-confirm')}
|
||
$('#cf-ok').onclick=()=>{closeModals();cfCb&&cfCb()};
|
||
|
||
/* ---------------- 공통 ---------------- */
|
||
function openModal(id){$('#'+id).classList.add('on')}
|
||
function closeModals(){$$('.modal-bg.on').forEach(m=>m.classList.remove('on'))}
|
||
$$('.modal-bg').forEach(m=>m.addEventListener('mousedown',e=>{if(e.target===m)closeModals()}));
|
||
$$('[data-close]').forEach(b=>b.onclick=closeModals);
|
||
document.addEventListener('keydown',e=>{
|
||
if(e.key==='Escape')closeModals();
|
||
if($('#m-prev').classList.contains('on')){if(e.key==='ArrowLeft')$('#pv-prev').click();if(e.key==='ArrowRight')$('#pv-next').click()}
|
||
});
|
||
function drawQR(){
|
||
const c=$('#qr'),g=c.getContext('2d'),n=29;g.fillStyle='#fff';g.fillRect(0,0,n,n);g.fillStyle='#111';
|
||
let seed=7;const rnd=()=>(seed=(seed*16807)%2147483647)/2147483647;
|
||
for(let y=0;y<n;y++)for(let x=0;x<n;x++){
|
||
const fin=(x<8&&y<8)||(x>n-9&&y<8)||(x<8&&y>n-9);
|
||
if(!fin&&rnd()>.5)g.fillRect(x,y,1,1);
|
||
}
|
||
[[0,0],[n-7,0],[0,n-7]].forEach(([ox,oy])=>{g.fillStyle='#111';g.fillRect(ox,oy,7,7);g.fillStyle='#fff';g.fillRect(ox+1,oy+1,5,5);g.fillStyle='#111';g.fillRect(ox+2,oy+2,3,3)});
|
||
}
|
||
|
||
/* 목업 컨트롤 */
|
||
function showView(v){
|
||
$$('.view').forEach(s=>s.classList.toggle('on',s.id==='view-'+v));
|
||
$$('#seg-view button').forEach(b=>b.classList.toggle('on',b.dataset.v===v));
|
||
$('#grp-up').style.display=v==='upload'?'':'none';
|
||
$('#grp-dl').style.display=v==='download'?'':'none';
|
||
if(v==='download')renderDL();
|
||
if(v==='admin')renderAdmin();
|
||
if(v==='upload')renderUpload();
|
||
}
|
||
$$('#seg-view button').forEach(b=>b.onclick=()=>showView(b.dataset.v));
|
||
$$('#seg-up button').forEach(b=>b.onclick=()=>setUpload(b.dataset.s,false));
|
||
$$('#seg-dl button').forEach(b=>b.onclick=()=>{D.state=b.dataset.s;syncDlBar();renderDL()});
|
||
$$('#seg-theme button').forEach(b=>b.onclick=()=>{
|
||
const t=b.dataset.t;if(t==='auto')document.documentElement.removeAttribute('data-theme');else document.documentElement.dataset.theme=t;
|
||
$$('#seg-theme button').forEach(x=>x.classList.toggle('on',x===b));
|
||
});
|
||
function fitBar(){document.documentElement.style.setProperty('--bar',$('#bar').offsetHeight+'px')}
|
||
addEventListener('resize',fitBar);
|
||
|
||
hydrate();
|
||
applyBrand();
|
||
fitBar();
|
||
setUpload('files',false);
|
||
</script>
|
||
</body>
|
||
</html>
|