2026-11-05 10:28:10 +09:00

894 lines
82 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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,'&quot;')}" 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,'&quot;')}" 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>