dash / index.html
openfree's picture
Update index.html
2a73efa verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VDash - ํ—ˆ๊ทธ์™€ํŠธ ๋งˆ๋ฒ•ํ•™๊ต</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Noto Sans KR',sans-serif;background:#fff;color:#1e293b;overflow-x:hidden}
#login-screen{min-height:100vh;background:linear-gradient(135deg,#1a1040,#2d1b69 40%,#1e40af);display:flex;align-items:center;justify-content:center}
.lc{background:#fff;border-radius:16px;padding:48px 40px;width:400px;box-shadow:0 25px 80px rgba(0,0,0,.35);text-align:center}
.ll{width:64px;height:64px;background:linear-gradient(135deg,#f59e0b,#d97706);border-radius:16px;display:flex;align-items:center;justify-content:center;font-size:32px;margin:0 auto 16px;box-shadow:0 8px 24px rgba(245,158,11,.3)}
.lc h1{font-size:24px;font-weight:900;margin-bottom:4px}.lc .lsub{font-size:11px;color:#94a3b8;margin-bottom:6px;font-family:'JetBrains Mono',monospace;letter-spacing:1px}
.ls{font-size:13px;color:#64748b;margin-bottom:28px}.lc label{display:block;font-size:12px;font-weight:600;color:#64748b;margin-bottom:6px;text-align:left}
.lc input{width:100%;padding:11px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;font-family:inherit;outline:none;margin-bottom:20px;background:#f8fafc}.lc input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1)}
.lb2{width:100%;padding:12px;background:linear-gradient(135deg,#1e40af,#7c3aed);color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit}
.le{background:#fef2f2;color:#ef4444;padding:10px;border-radius:8px;font-size:13px;margin-bottom:20px;text-align:center;display:none}.le.show{display:block}
#dashboard{display:none}
.hd{background:linear-gradient(135deg,#1e40af,#3b82f6);padding:0 24px;height:52px;display:flex;align-items:center;justify-content:space-between}
.hd-b{display:flex;align-items:center;gap:10px}.hd-l{width:30px;height:30px;background:rgba(255,255,255,.95);border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:16px}
.hd-t{font-size:15px;font-weight:700;color:#fff}.hd-s{font-size:9px;color:rgba(255,255,255,.6);font-family:'JetBrains Mono',monospace}
.hd-r{display:flex;align-items:center;gap:14px}.hd-c{font-family:'JetBrains Mono',monospace;font-size:11px;color:rgba(255,255,255,.8)}
.hd-o{padding:3px 10px;background:rgba(255,255,255,.15);border:1px solid rgba(255,255,255,.3);border-radius:5px;color:#fff;font-size:10px;font-weight:600;cursor:pointer}
.tabs{display:flex;background:#fff;border-bottom:2px solid #e2e8f0;overflow-x:auto;scrollbar-width:none}.tabs::-webkit-scrollbar{display:none}
.tab{padding:10px 14px;font-size:11px;font-weight:500;color:#94a3b8;cursor:pointer;white-space:nowrap;border-bottom:2px solid transparent;margin-bottom:-2px;user-select:none;position:relative}
.tab:hover{color:#475569;background:#f8fafc}.tab.active{color:#3b82f6;border-bottom-color:#3b82f6;font-weight:700;background:#eff6ff}
.tab .bg{display:inline-block;min-width:16px;height:16px;background:#3b82f6;color:#fff;font-size:9px;font-weight:700;border-radius:8px;text-align:center;line-height:16px;padding:0 4px;margin-left:4px}
.tab .alert-dot{position:absolute;top:6px;right:6px;width:8px;height:8px;background:#ef4444;border-radius:50%;border:2px solid #fff}
.mgr{background:#f8fafc;border-bottom:1px solid #e2e8f0;padding:6px 20px;display:flex;align-items:center;gap:12px;font-size:11px;color:#475569;flex-wrap:wrap}
.mg{display:inline-block;padding:1px 5px;border-radius:3px;font-size:9px;font-weight:600;margin-right:2px}.mg-m{background:#eff6ff;color:#3b82f6}.mg-s{background:#f1f5f9;color:#64748b}
/* 3-column calendar layout */
.main{display:flex;height:calc(100vh - 52px - 39px - 31px);overflow:hidden}
.col-cal{width:300px;min-width:300px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;background:#f8fafc;overflow-y:auto}
.col-list{width:280px;min-width:280px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;overflow-y:auto;background:#fff}
.col-form{flex:1;display:flex;flex-direction:column;background:#fff;overflow-y:auto}
.left{width:380px;min-width:380px;border-right:2px solid #e2e8f0;display:flex;flex-direction:column;background:#f8fafc;overflow-y:auto}
.right{flex:1;display:flex;flex-direction:column;background:#fff;overflow-y:auto}
/* search */
.search-box{padding:8px 12px;border-bottom:1px solid #e2e8f0;display:flex;gap:4px}
.search-box input{flex:1;padding:5px 8px;border:1px solid #e2e8f0;border-radius:4px;font-size:10px;outline:none;font-family:inherit}.search-box input:focus{border-color:#3b82f6}
.search-box select{padding:5px;border:1px solid #e2e8f0;border-radius:4px;font-size:10px;outline:none}
/* calendar */
.ch2{display:flex;align-items:center;justify-content:space-between;padding:10px 10px 4px}.cn3{display:flex;align-items:center;gap:6px}
.cb2{width:24px;height:24px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;color:#475569;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:11px}.cb2:hover{border-color:#3b82f6;color:#3b82f6}
.cl2{font-size:13px;font-weight:700;min-width:100px;text-align:center}
.ct2{padding:2px 6px;background:#eff6ff;border:1px solid #3b82f6;border-radius:3px;color:#3b82f6;font-size:8px;font-weight:700;cursor:pointer}
.cg2{padding:0 10px}.cw2{display:grid;grid-template-columns:repeat(7,1fr);margin-bottom:2px}
.cwd2{text-align:center;font-size:8px;font-weight:600;color:#94a3b8;padding:3px 0}.cwd2:first-child{color:#ef4444}.cwd2:last-child{color:#3b82f6}
.cd2{display:grid;grid-template-columns:repeat(7,1fr);gap:1px}
.dy{aspect-ratio:1;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:5px;cursor:pointer;font-size:10px;font-weight:500;gap:1px}
.dy:hover{background:#f1f5f9}.dy.emp{cursor:default}.dy.emp:hover{background:transparent}.dy.tod{background:#eff6ff;font-weight:700;color:#3b82f6}
.dy.sel{background:#3b82f6;color:#fff!important;font-weight:700}.dy.sun{color:#ef4444}.dy.sat{color:#3b82f6}
.dy .dots{display:flex;gap:2px;height:3px}.dy .dot{width:3px;height:3px;border-radius:50%;background:#f59e0b}
/* list items */
.tl2{padding:8px 10px;flex:1;overflow-y:auto}.tlt2{font-size:9px;font-weight:700;color:#94a3b8;margin-bottom:4px;display:flex;align-items:center;gap:4px}
.tlt2 .cn4{background:#eff6ff;color:#3b82f6;font-size:8px;padding:1px 4px;border-radius:4px;font-family:'JetBrains Mono',monospace}
.tk2{background:#fff;border:1px solid #e2e8f0;border-radius:5px;padding:6px 8px;margin-bottom:3px;cursor:pointer;display:flex;align-items:flex-start;gap:6px;box-shadow:0 1px 2px rgba(0,0,0,.03)}
.tk2:hover{border-color:#3b82f6}.tk2.ac{border-color:#3b82f6;box-shadow:0 0 0 1px #3b82f6}
.tk2 .pb2{width:3px;min-height:18px;border-radius:2px;flex-shrink:0;align-self:stretch}
.ph2{background:#ef4444}.pm2{background:#f59e0b}.pl2{background:#10b981}.pp{background:#8b5cf6}.po{background:#f97316}
.tb2{flex:1;min-width:0}.tn2{font-size:10px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tm2{font-size:8px;color:#94a3b8;margin-top:1px;display:flex;gap:3px;align-items:center;flex-wrap:wrap}
.dday{font-size:7px;font-weight:700;padding:1px 3px;border-radius:3px}.dd-r{background:#fef2f2;color:#ef4444}.dd-y{background:#fffbeb;color:#d97706}.dd-g{background:#f0fdf4;color:#059669}
/* form */
.dh3{padding:10px 16px 0;display:flex;align-items:center;justify-content:space-between}.dt3{font-size:12px;font-weight:700}
.de3{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:#94a3b8;padding:20px;text-align:center}.de3 .ic{font-size:40px;opacity:.2}
.fm2{padding:10px 16px;display:flex;flex-direction:column;gap:8px}
.fr2{display:grid;grid-template-columns:1fr 1fr;gap:8px}.fg2{display:flex;flex-direction:column;gap:2px}.fg2.full{grid-column:1/-1}
.fl2{font-size:8px;font-weight:700;color:#94a3b8;letter-spacing:.3px;text-transform:uppercase}
.fi2,.fs2,.ft2{background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:6px 8px;color:#1e293b;font-size:10px;font-family:inherit;outline:none;width:100%}
.fi2:focus,.fs2:focus,.ft2:focus{border-color:#3b82f6;box-shadow:0 0 0 2px rgba(59,130,246,.1)}
.fs2{appearance:none;cursor:pointer}.ft2{resize:vertical;min-height:44px}
.sg2{display:flex;gap:3px;flex-wrap:wrap}.sg2>div{flex:1;min-width:fit-content;padding:4px 6px;border:1px solid #e2e8f0;border-radius:3px;text-align:center;font-size:9px;font-weight:600;cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:center;gap:2px}.sg2>div:hover{border-color:#94a3b8}
.s-a{background:#fef2f2;border-color:#ef4444!important;color:#ef4444}.s-b{background:#fffbeb;border-color:#f59e0b!important;color:#b45309}
.s-c{background:#ecfdf5;border-color:#10b981!important;color:#10b981}.s-d{background:rgba(139,92,246,.08);border-color:#8b5cf6!important;color:#8b5cf6}
.s-e{background:rgba(6,182,212,.08);border-color:#06b6d4!important;color:#0891b2}.s-f{background:#eff6ff;border-color:#3b82f6!important;color:#3b82f6}
.btn{padding:5px 12px;border-radius:4px;font-size:9px;font-weight:600;cursor:pointer;border:1px solid transparent;font-family:inherit;display:inline-flex;align-items:center;gap:3px}
.btn-p{background:#3b82f6;color:#fff}.btn-p:hover{background:#2563eb}.btn-p:disabled{opacity:.6}
.btn-d{background:transparent;border-color:#ef4444;color:#ef4444}.btn-d:hover{background:#fef2f2}
.btn-g{background:#fff;border-color:#e2e8f0;color:#475569}.btn-g:hover{border-color:#94a3b8}
.btn-n{background:linear-gradient(135deg,#2563eb,#7c3aed);color:#fff;border:none}.btn-n:hover{opacity:.9}
.fa2{display:flex;gap:4px;padding-top:6px;border-top:1px solid #e2e8f0;margin-top:4px}.sp2{flex:1}
.dv2{height:1px;background:#e2e8f0;margin:3px 10px}
.ov{position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:9999;display:flex;align-items:center;justify-content:center}
.dlg{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:20px;max-width:340px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.12)}
.dlg h3{font-size:14px;font-weight:700;margin-bottom:6px}.dlg p{font-size:12px;color:#475569;margin-bottom:14px}
.da2{display:flex;gap:6px;justify-content:flex-end}
.ld2{flex:1;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:12px;gap:8px}
.spn2{width:16px;height:16px;border:2px solid #e2e8f0;border-top-color:#3b82f6;border-radius:50%;animation:sp2 .6s linear infinite}
@keyframes sp2{to{transform:rotate(360deg)}}@keyframes fd2{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.fade2{animation:fd2 .2s ease-out}
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:2px}
.stabs{display:flex;gap:4px;flex-wrap:wrap}.stab{padding:5px 10px;font-size:10px;font-weight:600;color:#94a3b8;cursor:pointer;border-radius:5px;user-select:none}.stab:hover{background:#f1f5f9}.stab.active{background:#eff6ff;color:#3b82f6}
.dash{flex:1;overflow-y:auto;padding:14px 18px}
.kpi-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:6px;margin-bottom:12px}
.kpi{background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:8px;text-align:center}.kpi .kv{font-size:18px;font-weight:900;font-family:'JetBrains Mono',monospace}.kpi .kl{font-size:7px;font-weight:600;color:#94a3b8;margin-top:1px}
.st{font-size:10px;font-weight:700;color:#64748b;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}
.pipe{display:flex;gap:3px;margin-bottom:6px;padding:0 10px}.pipe-s{flex:1;text-align:center;padding:4px 3px;border-radius:4px;font-size:8px;font-weight:700}
.ttg{padding:1px 3px;border-radius:3px;font-size:7px;font-weight:700}
.ttg-gov{background:#eff6ff;color:#3b82f6}.ttg-biz{background:#fff7ed;color:#ea580c}.ttg-dev{background:#ecfdf5;color:#059669}.ttg-pr{background:#faf5ff;color:#9333ea}.ttg-mgmt{background:#f0fdfa;color:#0d9488}
.prg{-webkit-appearance:none;width:100%;height:5px;border-radius:3px;background:#e2e8f0;outline:none}.prg::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#3b82f6;cursor:pointer}
.urg{background:#fef2f2;border:1px solid #fecaca;border-radius:6px;padding:6px 10px;margin-bottom:8px}.urg-i{font-size:9px;color:#991b1b;margin-bottom:2px}
.pcard{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:5px;margin-bottom:12px}
.pcd{background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:6px 8px}.pcd-n{font-size:10px;font-weight:700;margin-bottom:1px}
.pcd-r{font-size:7px;color:#94a3b8;margin-bottom:2px;display:flex;flex-wrap:wrap;gap:2px}
.pcd-role{font-size:7px;font-weight:600;padding:1px 3px;border-radius:2px}.pcd-s{font-size:8px;color:#475569;display:flex;gap:4px}.pcd-v{font-weight:700;font-family:'JetBrains Mono',monospace}
/* drops */
.drop-input{display:flex;gap:6px;padding:10px 12px;background:#fff;border-bottom:1px solid #e2e8f0}
.drop-item{padding:6px 12px;border-bottom:1px solid #f1f5f9;display:flex;gap:8px;align-items:flex-start}.drop-item:hover{background:#f8fafc}
.drop-avatar{width:26px;height:26px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;color:#fff;font-size:9px;font-weight:700;flex-shrink:0}
.drop-body{flex:1;min-width:0}.drop-name{font-size:9px;font-weight:700}.drop-time{font-size:7px;color:#94a3b8;margin-left:4px;font-family:'JetBrains Mono',monospace}.drop-text{font-size:10px;color:#475569;margin-top:1px;line-height:1.4}
.drop-actions{display:flex;gap:2px;margin-top:3px}.drop-react{padding:1px 5px;border:1px solid #e2e8f0;border-radius:10px;font-size:10px;cursor:pointer;background:#fff;user-select:none}.drop-react:hover{background:#f1f5f9}.drop-react.active{background:#eff6ff;border-color:#3b82f6}
.drop-del{font-size:8px;color:#94a3b8;cursor:pointer;opacity:0;transition:opacity .15s;margin-left:auto}.drop-item:hover .drop-del{opacity:1}.drop-del:hover{color:#ef4444}
/* news 4-col, pocket grid */
.nc-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.pkt-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
@media(max-width:1200px){.nc-grid{grid-template-columns:repeat(3,1fr)}.pkt-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:900px){.nc-grid,.pkt-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){.nc-grid,.pkt-grid{grid-template-columns:1fr}}
.nc{background:#fff;border:1px solid #e2e8f0;border-radius:7px;padding:8px;box-shadow:0 1px 2px rgba(0,0,0,.03);display:flex;flex-direction:column;gap:3px}.nc:hover{border-color:#3b82f6}
.nc-t{font-size:10px;font-weight:700;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.nc-tags{display:flex;gap:2px;flex-wrap:wrap}.nc-tag{font-size:6px;font-weight:600;padding:1px 4px;border-radius:2px}
.nc-rel{font-size:6px;font-weight:700;padding:1px 4px;border-radius:2px;white-space:nowrap}
.r-c{background:#fef2f2;color:#ef4444}.r-h{background:#fffbeb;color:#d97706}.r-m{background:#ecfdf5;color:#059669}.r-g{background:#f1f5f9;color:#94a3b8}
.nc-s{font-size:8px;color:#64748b;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.nc-m{display:flex;gap:4px;font-size:7px;color:#94a3b8;margin-top:auto}.nc-l{color:#3b82f6;text-decoration:none;font-weight:600}.nc-l:hover{text-decoration:underline}
.nf{flex:1;overflow-y:auto;padding:10px 14px}.nf-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.nf-t{font-size:12px;font-weight:700}
.nf-r{padding:3px 8px;background:#eff6ff;border:1px solid #3b82f6;border-radius:4px;color:#3b82f6;font-size:8px;font-weight:700;cursor:pointer}
.nf-fs{display:flex;gap:3px;flex-wrap:wrap;margin-bottom:6px}.nf-f{padding:2px 6px;border-radius:3px;font-size:7px;font-weight:600;cursor:pointer;border:1px solid #e2e8f0;background:#fff;color:#64748b}.nf-f.active{background:#3b82f6;color:#fff;border-color:#3b82f6}
/* chat */
.chat-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden}.chat-msgs{flex:1;overflow-y:auto;padding:12px 16px}
.chat-msg{margin-bottom:10px;display:flex;gap:6px}.chat-msg.user{justify-content:flex-end}
.chat-msg .cb3{max-width:75%;padding:8px 12px;border-radius:10px;font-size:11px;line-height:1.6}
.chat-msg.user .cb3{background:#3b82f6;color:#fff;border-bottom-right-radius:3px}
.chat-msg.ai .cb3{background:#f1f5f9;color:#1e293b;border-bottom-left-radius:3px;font-size:11px;line-height:1.7}
.chat-input{display:flex;gap:6px;padding:10px 14px;border-top:1px solid #e2e8f0;background:#fff}
/* pocket card */
.pkt{background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:8px;box-shadow:0 1px 2px rgba(0,0,0,.03);display:flex;flex-direction:column;gap:3px}.pkt:hover{border-color:#3b82f6}
.pkt-c{font-size:9px;color:#475569;line-height:1.4;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.pkt-m{display:flex;gap:4px;font-size:7px;color:#94a3b8;align-items:center;margin-top:auto}
.pkt-l{color:#3b82f6;text-decoration:none;font-weight:600;font-size:8px;word-break:break-all;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.pkt-l:hover{text-decoration:underline}
.pkt-tag{font-size:6px;font-weight:600;padding:1px 4px;border-radius:2px;background:#eff6ff;color:#3b82f6}
.rpt{background:#f8fafc;border:1px solid #e2e8f0;border-radius:7px;padding:12px;margin-bottom:8px}.rpt-t{font-size:11px;font-weight:700;margin-bottom:6px}.rpt-s{font-size:9px;color:#475569;line-height:1.7}.rpt-s b{color:#1e293b}
/* work request */
.wr{background:#fff;border:1px solid #e2e8f0;border-radius:7px;padding:10px;margin-bottom:6px;box-shadow:0 1px 2px rgba(0,0,0,.03)}.wr:hover{border-color:#3b82f6}
.wr-h{display:flex;align-items:center;gap:4px;margin-bottom:4px;flex-wrap:wrap}.wr-from{font-size:9px;font-weight:700}.wr-arr{font-size:9px;color:#94a3b8}.wr-to{font-size:9px;font-weight:600;color:#3b82f6}
.wr-content{font-size:10px;color:#475569;line-height:1.4;padding:6px 8px;background:#f8fafc;border-radius:5px;margin-bottom:4px}
.wr-meta{display:flex;gap:6px;font-size:7px;color:#94a3b8;align-items:center;margin-bottom:4px}.wr-dl{font-weight:700;color:#ef4444}
.wr-status{font-size:7px;font-weight:700;padding:1px 5px;border-radius:3px}.ws-wait{background:#fffbeb;color:#d97706}.ws-done{background:#ecfdf5;color:#059669}.ws-urg{background:#fef2f2;color:#ef4444}
.wr-resp{margin-top:4px;border-top:1px solid #e2e8f0;padding-top:4px}
.wr-resp-item{display:flex;gap:4px;align-items:flex-start;margin-bottom:3px;padding:4px 6px;background:#f0fdf4;border-radius:4px}
.wr-resp-name{font-size:8px;font-weight:700;color:#059669;white-space:nowrap}.wr-resp-text{font-size:9px;color:#475569;flex:1}.wr-resp-time{font-size:7px;color:#94a3b8;white-space:nowrap;font-family:'JetBrains Mono',monospace}
.wr-reply{display:flex;gap:4px;margin-top:3px}.wr-badge{background:#ef4444;color:#fff;font-size:7px;font-weight:700;padding:1px 4px;border-radius:6px;margin-left:3px}
.wr-targets{display:flex;gap:3px;flex-wrap:wrap;margin-bottom:4px}.wr-tg{padding:2px 6px;border:1px solid #e2e8f0;border-radius:3px;font-size:8px;font-weight:500;cursor:pointer;user-select:none}.wr-tg.sel{background:#3b82f6;color:#fff;border-color:#3b82f6}
.wr-pri{display:flex;gap:4px;margin-bottom:4px}.wr-pri>div{padding:3px 8px;border:1px solid #e2e8f0;border-radius:3px;font-size:8px;font-weight:600;cursor:pointer}.wr-pri .sel-n{background:#eff6ff;border-color:#3b82f6;color:#3b82f6}.wr-pri .sel-u{background:#fef2f2;border-color:#ef4444;color:#ef4444}
/* dashboard search */
.dash-search{display:flex;gap:6px;margin-bottom:10px}.dash-search input{flex:1;padding:6px 10px;border:1px solid #e2e8f0;border-radius:5px;font-size:10px;outline:none;font-family:inherit}.dash-search input:focus{border-color:#3b82f6}
/* mobile */
@media(max-width:768px){
.hd{padding:0 12px;height:44px}.hd-t{font-size:13px}.hd-s{display:none}.hd-c{display:none}
.tabs{padding:0 4px}.tab{padding:8px 10px;font-size:10px}
.main{flex-direction:column;height:auto;min-height:calc(100vh - 44px - 35px - 28px)}
.col-cal{width:100%;min-width:auto;border-right:none;border-bottom:1px solid #e2e8f0;max-height:280px}
.col-list{width:100%;min-width:auto;border-right:none;border-bottom:1px solid #e2e8f0;max-height:200px}
.col-form{min-height:300px}
.left{width:100%;min-width:auto;border-right:none;max-height:50vh}
.right{min-height:300px}
.nc-grid,.pkt-grid{grid-template-columns:1fr}
.kpi-r{grid-template-columns:repeat(3,1fr)}
.pcard{grid-template-columns:repeat(2,1fr)}
.chat-msg .cb3{max-width:90%}
}
</style>
</head>
<body>
<div id="login-screen"><div class="lc"><div class="ll">๐Ÿง™</div><h1>ํ—ˆ๊ทธ์™€ํŠธ ๋งˆ๋ฒ•ํ•™๊ต</h1><div class="lsub">VIDRAFT</div><div class="ls">๋น„๋“œ๋ž˜ํ”„ํŠธ ์—…๋ฌด ๊ด€๋ฆฌ ๋Œ€์‹œ๋ณด๋“œ</div><div class="le" id="le">์ด๋ฆ„ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค</div><label>์ด๋ฆ„</label><input id="iu" type="text" placeholder="์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”" autofocus><label>๋น„๋ฐ€๋ฒˆํ˜ธ</label><input id="ip" type="password" placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ"><button class="lb2" id="lb" onclick="doLogin()">์ž…์žฅํ•˜๊ธฐ</button></div></div>
<div id="dashboard"><div id="vd-app"></div></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.9/babel.min.js"></script>
<script>
function getToken(){return localStorage.getItem('vd_token')}function setToken(t){localStorage.setItem('vd_token',t)}function getUser(){return localStorage.getItem('vd_user')||''}function setUser(u){localStorage.setItem('vd_user',u)}function clearToken(){localStorage.removeItem('vd_token');localStorage.removeItem('vd_user')}function authHeaders(){return{'Content-Type':'application/json','Authorization':'Bearer '+getToken()}}
async function doLogin(){var u=document.getElementById('iu').value.trim(),p=document.getElementById('ip').value.trim();if(!u||!p)return;document.getElementById('lb').disabled=true;document.getElementById('le').classList.remove('show');try{var r=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})});if(r.ok){var d=await r.json();setToken(d.token);setUser(d.user);showDashboard()}else document.getElementById('le').classList.add('show')}catch(e){document.getElementById('le').classList.add('show')}document.getElementById('lb').disabled=false}
document.getElementById('ip').addEventListener('keydown',function(e){if(e.key==='Enter')doLogin()});document.getElementById('iu').addEventListener('keydown',function(e){if(e.key==='Enter')document.getElementById('ip').focus()});
function doLogout(){clearToken();document.getElementById('login-screen').style.display='flex';document.getElementById('dashboard').style.display='none'}function showDashboard(){document.getElementById('login-screen').style.display='none';document.getElementById('dashboard').style.display='block';bootReactApp()}
(async function(){if(getToken()){try{var r=await fetch('/api/tasks',{headers:authHeaders()});if(r.ok){showDashboard();return}}catch(e){}clearToken()}})();
</script>
<script type="text/babel">
const {useState,useEffect,useCallback,useMemo}=React;
async function apiCall(m,u,b){const o={method:m,headers:authHeaders()};if(b)o.body=JSON.stringify(b);const r=await fetch(u,o);if(r.status===401){doLogout();return null}return r.json()}
const api={getTasks:()=>apiCall('GET','/api/tasks'),saveTask:t=>apiCall('POST','/api/tasks',t),delTask:id=>apiCall('DELETE','/api/tasks/'+id),
getDrops:()=>apiCall('GET','/api/drops'),saveDrop:d=>apiCall('POST','/api/drops',d),delDrop:id=>apiCall('DELETE','/api/drops/'+id),
getPockets:()=>apiCall('GET','/api/pockets'),savePocket:p=>apiCall('POST','/api/pockets',p),delPocket:id=>apiCall('DELETE','/api/pockets/'+id),
getReqs:()=>apiCall('GET','/api/reqs'),saveReq:r=>apiCall('POST','/api/reqs',r),delReq:id=>apiCall('DELETE','/api/reqs/'+id)};
const TABS=[{id:'dashboard',label:'์ข…ํ•ฉ ๋Œ€์‹œ๋ณด๋“œ',icon:'๐Ÿ“Š'},{id:'gov',label:'์ •๋ถ€ ์ง€์› ๊ณผ์ œ',icon:'๐Ÿ›๏ธ'},{id:'biz',label:'์ œํœด/์˜์—…/ํˆฌ์ž',icon:'๐Ÿ’ผ'},{id:'dev',label:'๊ฐœ๋ฐœ/์šด์˜ ๋ฐ ํ”„๋กœ์ ํŠธ',icon:'โš™๏ธ'},{id:'pr',label:'PR',icon:'๐Ÿ“ข'},{id:'mgmt',label:'๊ฒฝ์˜๊ด€๋ฆฌ',icon:'๐Ÿ“‹'},{id:'rnd',label:'R&D ํ—ˆ๋ธŒ',icon:'๐Ÿ”ฌ'},{id:'wizard',label:'๋งˆ๋ฒ•์‚ฌ',icon:'๐Ÿง™'}];
const CAL_TABS=['gov','biz','dev','pr','mgmt'];
const MEMBERS=['ํ™์˜์‹','๊น€ํƒœ๋ด‰','์žฅ์žฌ์›','๊น€์˜๋ฏผ','์ •์ง„๊ธฐ','์ด์ถฉํ›ˆ','๊น€๋ฏผ์‹','์‹ ์ •ํ›ˆ'];
const ADVISORS=['๊น€๊ธฐํƒœ','์ดํ˜ธ์ค€','๊น€ํƒœํ˜„','์ตœ์„ ์˜'];const ALL_M=[...MEMBERS,...ADVISORS];
const MGR={gov:{m:'ํ™์˜์‹',s:['์žฅ์žฌ์›','๊น€ํƒœ๋ด‰']},rnd:{m:'๊น€ํƒœ๋ด‰',s:['ํ™์˜์‹','์žฅ์žฌ์›']},biz:{m:'๊น€์˜๋ฏผ',s:['์ •์ง„๊ธฐ','์ด์ถฉํ›ˆ']},dev:{m:'์žฅ์žฌ์›',s:['ํ™์˜์‹','๊น€ํƒœ๋ด‰']},pr:{m:'์ด์ถฉํ›ˆ',s:['๊น€ํƒœ๋ด‰']},mgmt:{m:'๊น€๋ฏผ์‹',s:[]},monitor:{m:'์‹ ์ •ํ›ˆ',s:['์žฅ์žฌ์›','๊น€ํƒœ๋ด‰']}};
const WD=['์ผ','์›”','ํ™”','์ˆ˜','๋ชฉ','๊ธˆ','ํ† '];
const TL={gov:'์ •๋ถ€์ง€์›',biz:'์ œํœด/์˜์—…',dev:'๊ฐœ๋ฐœ/ํ”„๋กœ์ ํŠธ',pr:'PR',mgmt:'๊ฒฝ์˜๊ด€๋ฆฌ'};
const TC={gov:'ttg-gov',biz:'ttg-biz',dev:'ttg-dev',pr:'ttg-pr',mgmt:'ttg-mgmt'};
const TCLR={gov:'#3b82f6',biz:'#ea580c',dev:'#059669',pr:'#9333ea',mgmt:'#0d9488'};
const TCFG={
gov:{label:'๊ณผ์ œ',tl:'๊ณผ์ œ๋ช…',fields:[{k:'department',l:'์ฃผ๊ด€๋ถ€์„œ',t:'select',opts:['IITP','NIA','KISA','NIPA','๊ณผ๊ธฐ๋ถ€','์‚ฐ์—…๋ถ€','์ค‘๊ธฐ๋ถ€','๋ฌธ์ฒด๋ถ€','๊ตญ๋ฐฉ๋ถ€','ํ–‰์•ˆ๋ถ€','๊ธฐํƒ€']},{k:'priority',l:'์ค‘์š”๋„',t:'pills',opts:[{v:'์ƒ',c:'s-a',i:'๐Ÿ”ด'},{v:'์ค‘',c:'s-b',i:'๐ŸŸก'},{v:'ํ•˜',c:'s-c',i:'๐ŸŸข'}]},{k:'status',l:'์ง„ํ–‰',t:'pills',opts:[{v:'์กฐ์‚ฌ',c:'s-d',i:'๐Ÿ”'},{v:'์ž‘์„ฑ',c:'s-b',i:'โœ๏ธ'},{v:'์ œ์ถœ',c:'s-c',i:'๐Ÿ“ค'}]},{k:'category',l:'๋ถ„๋ฅ˜',t:'select',opts:['์‹ ๊ทœ๊ณต๋ชจ','๊ณ„์†๊ณผ์ œ','์‚ฌ์—…ํ™”','์˜ˆ๋น„์ฐฝ์—…','๊ธฐํƒ€']},{k:'budget',l:'๊ทœ๋ชจ',t:'text',ph:'5์–ต์›'},{k:'winRate',l:'์ˆ˜์ฃผํ™•๋ฅ (%)',t:'text',ph:'0~100'},{k:'link',l:'๋งํฌ',t:'text',ph:'https://...',full:1},{k:'memo',l:'๋ฉ”๋ชจ',t:'area',full:1}],meta:t=>[t.department,t.status,t.budget].filter(Boolean),bar:t=>t.priority==='์ƒ'?'ph2':t.priority==='์ค‘'?'pm2':'pl2',pipe:['์กฐ์‚ฌ','์ž‘์„ฑ','์ œ์ถœ'],pk:'status'},
biz:{label:'๋”œ',tl:'๊ฑด๋ช…',fields:[{k:'bizType',l:'๊ตฌ๋ถ„',t:'pills',opts:[{v:'์ œํœด',c:'s-f',i:'๐Ÿค'},{v:'์˜์—…',c:'s-e',i:'๐Ÿ“ˆ'},{v:'๋งˆ์ผ€ํŒ…',c:'s-b',i:'๐Ÿ“ฃ'},{v:'ํˆฌ์ž์œ ์น˜',c:'s-a',i:'๐Ÿ’ฐ'},{v:'IR',c:'s-d',i:'๐Ÿ“Š'}]},{k:'counterpart',l:'์ƒ๋Œ€๋ฐฉ',t:'text',ph:'๊ธฐ์—…๋ช…/๋‹ด๋‹น์ž'},{k:'stage',l:'๋‹จ๊ณ„',t:'pills',opts:[{v:'์ ‘์ด‰',c:'s-d',i:'๐Ÿ“ž'},{v:'๋ฏธํŒ…',c:'s-f',i:'๐Ÿค'},{v:'ํ˜‘์˜',c:'s-b',i:'๐Ÿ“‹'},{v:'๊ณ„์•ฝ',c:'s-c',i:'โœ…'}]},{k:'amount',l:'์˜ˆ์ƒ๊ธˆ์•ก',t:'text',ph:'1์–ต์›'},{k:'keyPoint',l:'ํ•ต์‹ฌ๋…ผ์˜',t:'area',full:1},{k:'followUp',l:'ํ›„์†์กฐ์น˜',t:'area',full:1},{k:'link',l:'๋งํฌ',t:'text',ph:'https://...',full:1}],meta:t=>[t.bizType,t.counterpart,t.stage].filter(Boolean),bar:t=>t.stage==='๊ณ„์•ฝ'?'pl2':t.stage==='ํ˜‘์˜'?'pm2':'pp',pipe:['์ ‘์ด‰','๋ฏธํŒ…','ํ˜‘์˜','๊ณ„์•ฝ'],pk:'stage'},
dev:{label:'ํ”„๋กœ์ ํŠธ',tl:'ํ”„๋กœ์ ํŠธ๋ช…',fields:[{k:'devType',l:'๊ตฌ๋ถ„',t:'pills',opts:[{v:'์ˆ˜์ฃผ๊ณผ์ œ',c:'s-f',i:'๐Ÿ›๏ธ'},{v:'์™ธ์ฃผํ”„๋กœ์ ํŠธ',c:'s-e',i:'๐Ÿข'},{v:'์ž์ฒด๊ฐœ๋ฐœ',c:'s-d',i:'๐Ÿ”ง'},{v:'์œ ์ง€๋ณด์ˆ˜',c:'s-b',i:'๐Ÿ”„'}]},{k:'client',l:'๋ฐœ์ฃผ์ฒ˜',t:'text',ph:'์ •๋ถ€๊ธฐ๊ด€/๊ธฐ์—…๋ช…'},{k:'contractAmt',l:'๊ณ„์•ฝ๊ธˆ์•ก',t:'text',ph:'3์–ต์›'},{k:'stage',l:'์ง„ํ–‰๋‹จ๊ณ„',t:'pills',opts:[{v:'์ฐฉ์ˆ˜',c:'s-d',i:'๐Ÿš€'},{v:'๊ฐœ๋ฐœ',c:'s-f',i:'โš™๏ธ'},{v:'์ค‘๊ฐ„์ ๊ฒ€',c:'s-b',i:'๐Ÿ“‹'},{v:'๋‚ฉํ’ˆ',c:'s-e',i:'๐Ÿ“ฆ'},{v:'์™„๋ฃŒ',c:'s-c',i:'โœ…'}]},{k:'contractStart',l:'๊ณ„์•ฝ์‹œ์ž‘',t:'text',ph:'2026-04-01'},{k:'contractEnd',l:'๊ณ„์•ฝ์ข…๋ฃŒ',t:'text',ph:'2026-12-31'},{k:'progress',l:'์ง„ํ–‰๋ฅ ',t:'range'},{k:'platform',l:'ํ”Œ๋žซํผ',t:'select',opts:['HF Spaces','Supabase','Vercel','AWS','GitHub','๊ธฐํƒ€']},{k:'link',l:'์‚ฐ์ถœ๋ฌผ',t:'text',ph:'https://...',full:1},{k:'memo',l:'๋ฉ”๋ชจ',t:'area',full:1}],meta:t=>[t.devType,t.client,t.stage,t.progress!=null?t.progress+'%':null].filter(Boolean),bar:t=>{const s=t.stage;return s==='์™„๋ฃŒ'?'pl2':s==='๋‚ฉํ’ˆ'||s==='์ค‘๊ฐ„์ ๊ฒ€'?'pm2':s==='๊ฐœ๋ฐœ'?'po':'pp'},pipe:['์ฐฉ์ˆ˜','๊ฐœ๋ฐœ','์ค‘๊ฐ„์ ๊ฒ€','๋‚ฉํ’ˆ','์™„๋ฃŒ'],pk:'stage'},
pr:{label:'ํ™๋ณด',tl:'์ œ๋ชฉ',fields:[{k:'prType',l:'์œ ํ˜•',t:'pills',opts:[{v:'๋ณด๋„์ž๋ฃŒ',c:'s-f',i:'๐Ÿ“ฐ'},{v:'๋ธ”๋กœ๊ทธ',c:'s-d',i:'โœ๏ธ'},{v:'SNS',c:'s-e',i:'๐Ÿ“ฑ'},{v:'ํ–‰์‚ฌ',c:'s-b',i:'๐ŸŽค'}]},{k:'media',l:'๋งค์ฒด',t:'text',ph:'๋งค์ฒด๋ช…'},{k:'prStatus',l:'์ƒํƒœ',t:'pills',opts:[{v:'๊ธฐํš',c:'s-d',i:'๐Ÿ’ก'},{v:'์ž‘์„ฑ',c:'s-b',i:'โœ๏ธ'},{v:'๊ฒ€ํ† ',c:'s-e',i:'๐Ÿ‘€'},{v:'๊ฒŒ์‹œ',c:'s-c',i:'โœ…'}]},{k:'reach',l:'๋„๋‹ฌ',t:'text',ph:'์กฐํšŒ์ˆ˜'},{k:'link',l:'URL',t:'text',ph:'https://...',full:1},{k:'memo',l:'๋ฉ”๋ชจ',t:'area',full:1}],meta:t=>[t.prType,t.media,t.prStatus].filter(Boolean),bar:t=>t.prStatus==='๊ฒŒ์‹œ'?'pl2':t.prStatus==='๊ฒ€ํ† '?'pm2':'pp',pipe:['๊ธฐํš','์ž‘์„ฑ','๊ฒ€ํ† ','๊ฒŒ์‹œ'],pk:'prStatus'},
mgmt:{label:'์—…๋ฌด',tl:'์—…๋ฌด๋ช…',fields:[{k:'mgmtType',l:'๋ถ„๋ฅ˜',t:'pills',opts:[{v:'ํšŒ๊ณ„/์žฌ๋ฌด',c:'s-f',i:'๐Ÿ’ณ'},{v:'๋ฒ•๋ฌด/๊ณ„์•ฝ',c:'s-d',i:'โš–๏ธ'},{v:'์ธ์‚ฌ/์ฑ„์šฉ',c:'s-e',i:'๐Ÿ‘ฅ'},{v:'๋Œ€๊ด€',c:'s-a',i:'๐Ÿ›๏ธ'},{v:'์ด๋ฌด',c:'s-b',i:'๐Ÿ“ฆ'}]},{k:'target',l:'๋Œ€์ƒ/๊ฑฐ๋ž˜์ฒ˜',t:'text',ph:'๊ธฐ๊ด€๋ช…, ๊ธฐ์—…๋ช…, ๋‹ด๋‹น์ž'},{k:'purpose',l:'๋ชฉ์ /๋‚ด์šฉ',t:'text',ph:'์ œ์•ˆ, ๋ฏผ์›, ํ˜‘์—…, ๊ณ„์•ฝ ๋“ฑ'},{k:'amount',l:'๊ธˆ์•ก',t:'text',ph:'500๋งŒ์›'},{k:'mgmtStatus',l:'์ƒํƒœ',t:'pills',opts:[{v:'์˜ˆ์ •',c:'s-d',i:'๐Ÿ“…'},{v:'์ ‘์ด‰',c:'s-f',i:'๐Ÿ“ž'},{v:'์ง„ํ–‰',c:'s-b',i:'๐Ÿ”„'},{v:'ํšŒ์‹ ๋Œ€๊ธฐ',c:'s-e',i:'โณ'},{v:'์™„๋ฃŒ',c:'s-c',i:'โœ…'}]},{k:'result',l:'๊ฒฐ๊ณผ/ํšŒ์‹ ',t:'area',full:1},{k:'link',l:'์ฒจ๋ถ€/๋งํฌ',t:'text',ph:'https://...',full:1},{k:'memo',l:'๋ฉ”๋ชจ',t:'area',full:1}],meta:t=>[t.mgmtType,t.target,t.mgmtStatus].filter(Boolean),bar:t=>t.mgmtStatus==='์™„๋ฃŒ'?'pl2':t.mgmtStatus==='์ง„ํ–‰'||t.mgmtStatus==='ํšŒ์‹ ๋Œ€๊ธฐ'?'pm2':t.mgmtStatus==='์ ‘์ด‰'?'po':'pp',pipe:['์˜ˆ์ •','์ ‘์ด‰','์ง„ํ–‰','ํšŒ์‹ ๋Œ€๊ธฐ','์™„๋ฃŒ'],pk:'mgmtStatus'}
};
function gid(){return Date.now().toString(36)+Math.random().toString(36).substr(2,6)}
function emptyItem(d,tab){const b={id:'',tab:tab||'gov',title:'',deadline:d||'',assignee:''};const c=TCFG[tab];if(c)c.fields.forEach(f=>{if(!(f.k in b))b[f.k]=f.t==='range'?0:''});return b}
function fmtD(y,m,d){return y+'-'+String(m+1).padStart(2,'0')+'-'+String(d).padStart(2,'0')}
function dim(y,m){return new Date(y,m+1,0).getDate()}function fdow(y,m){return new Date(y,m,1).getDay()}
function dday(dl){if(!dl)return null;const d=Math.ceil((new Date(dl)-new Date().setHours(0,0,0,0))/86400000);if(d<0)return{t:'D+'+(-d),c:'dd-r'};if(d===0)return{t:'TODAY',c:'dd-r'};if(d<=3)return{t:'D-'+d,c:'dd-r'};if(d<=7)return{t:'D-'+d,c:'dd-y'};return{t:'D-'+d,c:'dd-g'}}
function MgrBar({tabId}){const m=MGR[tabId];if(!m)return null;return<div className="mgr"><span style={{fontWeight:700,color:'#3b82f6',fontSize:10}}>๋‹ด๋‹น</span><span><span className="mg mg-m">์ •</span><span style={{fontWeight:600}}>{m.m}</span></span>{m.s.length>0&&<span><span className="mg mg-s">๋ถ€</span><span style={{color:'#94a3b8'}}>{m.s.join(', ')}</span></span>}</div>}
function Calendar({year,month,tasks,selectedDate,onSelect,onPrev,onNext,onToday}){
const dm_=dim(year,month),fw=fdow(year,month),now=new Date(),ts=fmtD(now.getFullYear(),now.getMonth(),now.getDate());
const bd={};tasks.forEach(t=>{if(t.deadline)(bd[t.deadline]||(bd[t.deadline]=[])).push(t)});const cells=[];
for(let i=0;i<fw;i++)cells.push(<div key={'e'+i} className="dy emp"/>);
for(let d=1;d<=dm_;d++){const ds=fmtD(year,month,d),dow=(fw+d-1)%7,c=['dy'];if(ds===ts)c.push('tod');if(ds===selectedDate)c.push('sel');if(dow===0)c.push('sun');if(dow===6)c.push('sat');const dt=bd[ds]||[];
cells.push(<div key={d} className={c.join(' ')} onClick={()=>onSelect(ds)}><span>{d}</span><div className="dots">{dt.slice(0,3).map((t,i)=><span key={i} className="dot"/>)}</div></div>)}
return<div><div className="ch2"><div className="cn3"><button className="cb2" onClick={onPrev}>โ—€</button><span className="cl2">{year}๋…„ {month+1}์›”</span><button className="cb2" onClick={onNext}>โ–ถ</button></div><button className="ct2" onClick={onToday}>์˜ค๋Š˜</button></div>
<div className="cg2"><div className="cw2">{WD.map(w=><div key={w} className="cwd2">{w}</div>)}</div><div className="cd2">{cells}</div></div></div>}
function ItemList({items,selectedId,onSelect,tabId,showTabTag,search}){
const cfg=TCFG[tabId]||TCFG.gov;
const filtered=search?items.filter(t=>(t.title||'').includes(search)||(t.assignee||'').includes(search)):items;
const sorted=[...filtered].sort((a,b)=>(a.deadline||'').localeCompare(b.deadline||''));
return<div className="tl2"><div className="tlt2">๐Ÿ“Œ {cfg.label} <span className="cn4">{sorted.length}</span></div>
{sorted.length===0&&<div style={{color:'#94a3b8',fontSize:9,textAlign:'center',padding:10}}>ํ•ญ๋ชฉ ์—†์Œ</div>}
{sorted.map(t=>{const dd=dday(t.deadline);return<div key={t.id} className={'tk2'+(t.id===selectedId?' ac':'')} onClick={()=>onSelect(t.id)}>
<div className={'pb2 '+cfg.bar(t)}/><div className="tb2"><div className="tn2">{t.title||'(์ œ๋ชฉ ์—†์Œ)'}</div>
<div className="tm2">{showTabTag&&<span className={'ttg '+(TC[t.tab]||'')}>{TL[t.tab]||t.tab}</span>}
{dd&&<span className={'dday '+dd.c}>{dd.t}</span>}
<span>{t.deadline}</span>{t.assignee&&<span>โ€ข {t.assignee}</span>}{cfg.meta(t).map((m,i)=><span key={i}>โ€ข {m}</span>)}</div></div></div>})}</div>}
function TabForm({item,isNew,onSave,onDelete,onNew,onCancel,saving,currentTab}){
const cfg=TCFG[currentTab]||TCFG.gov;const[f,setF]=useState(item||emptyItem('',currentTab));
const iid=item?item.id:(isNew?'__new__':null);useEffect(()=>{setF(item||emptyItem('',currentTab))},[iid]);
if(!item&&!isNew)return<div className="col-form"><div className="de3"><div className="ic">๐Ÿ“‹</div><div style={{fontSize:11,color:'#94a3b8'}}>ํ•ญ๋ชฉ ์„ ํƒ</div><button className="btn btn-n" onClick={onNew}>๏ผ‹ ์ƒˆ {cfg.label}</button></div></div>;
const u=(k,v)=>setF(p=>({...p,[k]:v}));const save=()=>{if(!f.title?.trim()){alert(cfg.tl+' ํ•„์ˆ˜');return}onSave({...f,tab:currentTab,id:f.id||gid()})};
const rf=(fd)=>{if(fd.t==='text')return<input className="fi2" value={f[fd.k]||''} onChange={e=>u(fd.k,e.target.value)} placeholder={fd.ph||''}/>;if(fd.t==='area')return<textarea className="ft2" value={f[fd.k]||''} onChange={e=>u(fd.k,e.target.value)} placeholder={fd.ph||''}/>;if(fd.t==='select')return<select className="fs2" value={f[fd.k]||''} onChange={e=>u(fd.k,e.target.value)}><option value="">์„ ํƒ</option>{fd.opts.map(o=><option key={o}>{o}</option>)}</select>;if(fd.t==='pills')return<div className="sg2">{fd.opts.map(o=><div key={o.v} className={f[fd.k]===o.v?o.c:''} onClick={()=>u(fd.k,o.v)}>{o.i} {o.v}</div>)}</div>;if(fd.t==='range')return<div style={{display:'flex',alignItems:'center',gap:6}}><input type="range" className="prg" min="0" max="100" value={f[fd.k]||0} onChange={e=>u(fd.k,+e.target.value)}/><span style={{fontSize:10,fontWeight:700,fontFamily:'JetBrains Mono',minWidth:28}}>{f[fd.k]||0}%</span></div>;return null};
const pairs=[];let i=0;while(i<cfg.fields.length){const fd=cfg.fields[i];if(fd.full){pairs.push([fd]);i++}else if(i+1<cfg.fields.length&&!cfg.fields[i+1].full){pairs.push([fd,cfg.fields[i+1]]);i+=2}else{pairs.push([fd]);i++}}
return<div className="col-form fade2"><div className="dh3"><div className="dt3">{isNew?'๐Ÿ“ ์ƒˆ '+cfg.label:'๐Ÿ“‹ ์ƒ์„ธ'}</div><button className="btn btn-n" onClick={onNew}>๏ผ‹ ์ƒˆ {cfg.label}</button></div>
<div className="fm2"><div className="fr2"><div className="fg2"><span className="fl2">{cfg.tl}</span><input className="fi2" value={f.title||''} onChange={e=>u('title',e.target.value)} placeholder={cfg.tl}/></div>
<div className="fg2"><span className="fl2">๋‹ด๋‹น์ž</span><select className="fs2" value={f.assignee||''} onChange={e=>u('assignee',e.target.value)}><option value="">์„ ํƒ</option>{ALL_M.map(m=><option key={m}>{m}</option>)}</select></div></div>
<div className="fr2"><div className="fg2"><span className="fl2">๋งˆ๊ฐ์ผ</span><input type="date" className="fi2" value={f.deadline||''} onChange={e=>u('deadline',e.target.value)}/></div><div className="fg2"/></div>
{pairs.map((row,ri)=>row.length===1&&row[0].full?<div key={ri} className="fg2 full"><span className="fl2">{row[0].l}</span>{rf(row[0])}</div>:<div key={ri} className="fr2">{row.map(fd=><div key={fd.k} className="fg2"><span className="fl2">{fd.l}</span>{rf(fd)}</div>)}</div>)}
<div className="fa2">{!isNew&&<button className="btn btn-d" onClick={()=>onDelete(f.id)}>๐Ÿ—‘๏ธ</button>}<div className="sp2"/><button className="btn btn-g" onClick={onCancel}>์ทจ์†Œ</button><button className="btn btn-p" onClick={save} disabled={saving}>{saving?'โณ':(isNew?'๐Ÿ’พ ๋“ฑ๋ก':'๐Ÿ’พ ์ €์žฅ')}</button></div>
</div></div>}
function PipeBar({items,config}){if(!config?.pipe)return null;const cn={};config.pipe.forEach(s=>cn[s]=0);items.forEach(t=>{const v=t[config.pk];if(v in cn)cn[v]++});const cl=['#8b5cf6','#3b82f6','#f59e0b','#10b981','#06b6d4'];return<div className="pipe">{config.pipe.map((s,i)=><div key={s} className="pipe-s" style={{background:cl[i%cl.length]+'18',color:cl[i%cl.length]}}>{s} {cn[s]}</div>)}</div>}
/* === CalTab - 3 column layout === */
function CalTab({allTasks,tabId,saving,setSaving,setAllTasks}){
const isDash=tabId==='dashboard';const cfg=TCFG[isDash?'gov':tabId]||TCFG.gov;
const items=isDash?allTasks.filter(t=>CAL_TABS.includes(t.tab||'gov')):allTasks.filter(t=>(t.tab||'gov')===tabId);
const now=new Date();const[year,setYear]=useState(now.getFullYear());const[month,setMonth]=useState(now.getMonth());
const[selDate,setSelDate]=useState(null);const[selId,setSelId]=useState(null);const[isNew,setIsNew]=useState(false);const[cfm,setCfm]=useState(null);const[newTab,setNewTab]=useState('gov');const[search,setSearch]=useState('');
const monthStartStr=fmtD(year,month,1);
const mI=items.filter(t=>t.deadline&&t.deadline>=monthStartStr);
const sel=selId?allTasks.find(t=>t.id===selId):null;
const hsd=d=>{setSelDate(d);setSelId(null);setIsNew(false);const dt=items.filter(t=>t.deadline===d);if(dt.length>0)setSelId(dt[0].id)};
const pm=()=>{if(month===0){setMonth(11);setYear(y=>y-1)}else setMonth(m=>m-1);setSelDate(null);setSelId(null);setIsNew(false)};
const nm=()=>{if(month===11){setMonth(0);setYear(y=>y+1)}else setMonth(m=>m+1);setSelDate(null);setSelId(null);setIsNew(false)};
const gt=()=>{const n=new Date();setYear(n.getFullYear());setMonth(n.getMonth());setSelDate(fmtD(n.getFullYear(),n.getMonth(),n.getDate()))};
const hs=async(data)=>{setSaving(true);const r=await api.saveTask(data);if(r)setAllTasks(r);setSelId(data.id);setIsNew(false);setSaving(false)};
const hd=async()=>{setSaving(true);const r=await api.delTask(cfm);if(r)setAllTasks(r);setSelId(null);setIsNew(false);setCfm(null);setSaving(false)};
const actualTab=isDash?(isNew?newTab:(sel?.tab||'gov')):tabId;
return<><div className="main">
<div className="col-cal">
<Calendar year={year} month={month} tasks={items} selectedDate={selDate} onSelect={hsd} onPrev={pm} onNext={nm} onToday={gt}/>
{isDash&&<div className="pipe">{CAL_TABS.map(tid=><div key={tid} className="pipe-s" style={{background:(TCLR[tid]||'#94a3b8')+'18',color:TCLR[tid]}}>{TL[tid]} {mI.filter(t=>(t.tab||'gov')===tid).length}</div>)}</div>}
{!isDash&&<PipeBar items={mI} config={cfg}/>}
<div className="search-box"><input value={search} onChange={e=>setSearch(e.target.value)} placeholder="๐Ÿ” ๊ฒ€์ƒ‰ (์ œ๋ชฉ/๋‹ด๋‹น์ž)"/></div>
</div>
<div className="col-list">
<ItemList items={mI} selectedId={selId} onSelect={id=>{setSelId(id);setIsNew(false)}} tabId={isDash?(sel?.tab||'gov'):tabId} showTabTag={isDash} search={search}/>
</div>
{isDash&&isNew?<div className="col-form fade2"><div className="dh3"><div className="dt3">๐Ÿ“ ์ƒˆ ํ•ญ๋ชฉ</div></div><div className="fm2"><div className="fg2"><span className="fl2">ํƒญ ์„ ํƒ</span><div className="sg2">{CAL_TABS.map(tid=><div key={tid} className={newTab===tid?'s-f':''} onClick={()=>setNewTab(tid)}>{TL[tid]}</div>)}</div></div></div>
<TabForm item={emptyItem(selDate||'',newTab)} isNew={true} onSave={hs} onDelete={()=>{}} onNew={()=>{}} onCancel={()=>{setSelId(null);setIsNew(false)}} saving={saving} currentTab={newTab}/></div>
:isDash&&sel?<TabForm item={sel} isNew={false} onSave={hs} onDelete={id=>setCfm(id)} onNew={()=>{setSelId(null);setIsNew(true)}} onCancel={()=>{setSelId(null);setIsNew(false)}} saving={saving} currentTab={sel.tab||'gov'}/>
:isDash?<div className="col-form"><div className="de3"><div className="ic">๐Ÿ“…</div><div style={{fontSize:11,color:'#94a3b8'}}>ํ†ตํ•ฉ ์บ˜๋ฆฐ๋”</div><button className="btn btn-n" onClick={()=>{setSelId(null);setIsNew(true)}}>๏ผ‹ ์ƒˆ ํ•ญ๋ชฉ</button></div></div>
:<TabForm item={isNew?emptyItem(selDate||'',tabId):sel} isNew={isNew} onSave={hs} onDelete={id=>setCfm(id)} onNew={()=>{setSelId(null);setIsNew(true)}} onCancel={()=>{setSelId(null);setIsNew(false)}} saving={saving} currentTab={tabId}/>}
</div>{cfm&&<div className="ov" onClick={()=>setCfm(null)}><div className="dlg fade2" onClick={e=>e.stopPropagation()}><h3>์‚ญ์ œ</h3><p>์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?</p><div className="da2"><button className="btn btn-g" onClick={()=>setCfm(null)}>์ทจ์†Œ</button><button className="btn btn-d" onClick={hd}>๐Ÿ—‘๏ธ</button></div></div></div>}</>}
/* === DailyDrop + Emoji === */
function DailyDrop({drops,setDrops}){
const[text,setText]=useState('');const[saving,setSaving]=useState(false);
const submit=async()=>{if(!text.trim())return;setSaving(true);const d={id:gid(),author:getUser(),text:text.trim(),date:new Date().toISOString(),reactions:{}};const r=await api.saveDrop(d);if(r)setDrops(r);setText('');setSaving(false)};
const del=async(id)=>{const r=await api.delDrop(id);if(r)setDrops(r)};
const react=async(drop,emoji)=>{const rx={...(drop.reactions||{})};const me=getUser();const users=rx[emoji]||[];if(users.includes(me))rx[emoji]=users.filter(u=>u!==me);else rx[emoji]=[...users,me];
const updated={...drop,reactions:rx};const r=await api.saveDrop(updated);if(r)setDrops(r)};
const today=new Date().toISOString().slice(0,10);const grouped={};drops.forEach(d=>{const day=d.date?.slice(0,10)||today;(grouped[day]||(grouped[day]=[])).push(d)});
const days=Object.keys(grouped).sort().reverse();const dayLabel=d=>{if(d===today)return '์˜ค๋Š˜';const y=new Date();y.setDate(y.getDate()-1);if(d===y.toISOString().slice(0,10))return '์–ด์ œ';return d};
const EMOJIS=['๐Ÿ‘','๐Ÿ”ฅ','๐Ÿ‘€','โค๏ธ'];
return<div style={{flex:1,display:'flex',flexDirection:'column',overflow:'hidden'}}>
<div className="drop-input"><input className="fi2" value={text} onChange={e=>setText(e.target.value)} placeholder={getUser()+'๋‹˜, ์˜ค๋Š˜ ํ•œ ์ผ์„ ํ•œ์ค„๋กœ...'} onKeyDown={e=>{if(e.key==='Enter')submit()}}/><button className="btn btn-p" onClick={submit} disabled={saving}>{saving?'โณ':'โœ๏ธ'}</button></div>
<div style={{flex:1,overflowY:'auto'}}>{days.map(day=><div key={day}><div style={{padding:'6px 12px',fontSize:8,fontWeight:700,color:'#94a3b8',background:'#f8fafc',borderBottom:'1px solid #e2e8f0'}}>{dayLabel(day)} ({grouped[day].length})</div>
{grouped[day].sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(d=><div key={d.id} className="drop-item">
<div className="drop-avatar">{(d.author||'?')[0]}</div>
<div className="drop-body"><span className="drop-name">{d.author}</span><span className="drop-time">{d.date?.slice(11,16)}</span><div className="drop-text">{d.text}</div>
<div className="drop-actions">{EMOJIS.map(e=>{const cnt=((d.reactions||{})[e]||[]).length;const me=(d.reactions||{})[e]?.includes(getUser());
return<span key={e} className={'drop-react'+(me?' active':'')} onClick={()=>react(d,e)}>{e}{cnt>0&&<span style={{fontSize:8,marginLeft:2}}>{cnt}</span>}</span>})}
{d.author===getUser()&&<span className="drop-del" onClick={()=>del(d.id)}>โœ•</span>}</div>
</div></div>)}</div>)}</div></div>}
/* === WorkRequest + Priority === */
function WorkRequest({reqs,setReqs}){
const me=getUser();const[isNew,setIsNew]=useState(false);const[targets,setTargets]=useState([]);
const[content,setContent]=useState('');const[deadline,setDeadline]=useState('');const[priority,setPriority]=useState('์ผ๋ฐ˜');const[saving,setSaving]=useState(false);
const[replyTo,setReplyTo]=useState(null);const[replyText,setReplyText]=useState('');
const submit=async()=>{if(!targets.length){alert('๋Œ€์ƒ ์„ ํƒ');return}if(!content.trim()){alert('๋‚ด์šฉ ์ž…๋ ฅ');return}
setSaving(true);const item={id:gid(),from:me,targets,content:content.trim(),deadline,priority,responses:[],createdAt:new Date().toISOString(),status:'๋Œ€๊ธฐ'};
const r=await api.saveReq(item);if(r)setReqs(r);setContent('');setDeadline('');setTargets([]);setPriority('์ผ๋ฐ˜');setIsNew(false);setSaving(false)};
const reply=async(reqId)=>{if(!replyText.trim())return;setSaving(true);const req=reqs.find(r=>r.id===reqId);if(!req)return;
const updated={...req,responses:[...(req.responses||[]),{author:me,text:replyText.trim(),date:new Date().toISOString()}]};
const responded=new Set([...(req.responses||[]).map(r=>r.author),me]);if(req.targets.every(t=>responded.has(t)))updated.status='์™„๋ฃŒ';
const r=await api.saveReq(updated);if(r)setReqs(r);setReplyText('');setReplyTo(null);setSaving(false)};
const del=async(id)=>{const r=await api.delReq(id);if(r)setReqs(r)};
const myPending=reqs.filter(r=>r.targets?.includes(me)&&!(r.responses||[]).some(rp=>rp.author===me)&&r.status==='๋Œ€๊ธฐ');
const sorted=[...reqs].sort((a,b)=>{if(a.status!==b.status)return a.status==='๋Œ€๊ธฐ'?-1:1;if(a.priority!==b.priority)return a.priority==='๊ธด๊ธ‰'?-1:1;return(b.createdAt||'').localeCompare(a.createdAt||'')});
return<div style={{flex:1,display:'flex',flexDirection:'column',overflow:'hidden'}}>
<div style={{padding:'8px 12px',display:'flex',justifyContent:'space-between',alignItems:'center',borderBottom:'1px solid #e2e8f0'}}>
<div style={{fontSize:12,fontWeight:700}}>๐Ÿค ์—…๋ฌด ํ˜‘์กฐ {myPending.length>0&&<span className="wr-badge">{myPending.length}</span>}</div>
<button className="btn btn-n" onClick={()=>setIsNew(!isNew)}>๏ผ‹ ์š”์ฒญ</button></div>
{isNew&&<div style={{padding:10,background:'#f8fafc',borderBottom:'1px solid #e2e8f0'}}>
<div className="wr-pri"><div className={priority==='์ผ๋ฐ˜'?'sel-n':''} onClick={()=>setPriority('์ผ๋ฐ˜')}>๐Ÿ“‹ ์ผ๋ฐ˜</div><div className={priority==='๊ธด๊ธ‰'?'sel-u':''} onClick={()=>setPriority('๊ธด๊ธ‰')}>๐Ÿšจ ๊ธด๊ธ‰</div></div>
<div style={{fontSize:8,fontWeight:700,color:'#94a3b8',marginBottom:3}}>๋Œ€์ƒ (๋ณต์ˆ˜)</div>
<div className="wr-targets">{ALL_M.filter(m=>m!==me).map(m=><div key={m} className={'wr-tg'+(targets.includes(m)?' sel':'')} onClick={()=>setTargets(p=>p.includes(m)?p.filter(n=>n!==m):[...p,m])}>{m}</div>)}</div>
<input className="fi2" value={content} onChange={e=>setContent(e.target.value)} placeholder="์š”์ฒญ ๋‚ด์šฉ" style={{marginBottom:4}} onKeyDown={e=>{if(e.key==='Enter')submit()}}/>
<div style={{display:'flex',gap:4,alignItems:'center'}}><span style={{fontSize:8,color:'#94a3b8'}}>๋งˆ๊ฐ</span><input type="date" className="fi2" value={deadline} onChange={e=>setDeadline(e.target.value)} style={{width:130}}/><div className="sp2"/>
<button className="btn btn-g" onClick={()=>setIsNew(false)}>์ทจ์†Œ</button><button className="btn btn-p" onClick={submit} disabled={saving}>{saving?'โณ':'๐Ÿ“ค'}</button></div></div>}
<div style={{flex:1,overflowY:'auto',padding:'6px 10px'}}>{sorted.length===0&&<div style={{textAlign:'center',color:'#94a3b8',fontSize:10,padding:20}}>์š”์ฒญ์ด ์—†์Šต๋‹ˆ๋‹ค</div>}
{sorted.map(r=>{const needReply=r.targets?.includes(me)&&!(r.responses||[]).some(rp=>rp.author===me)&&r.status==='๋Œ€๊ธฐ';
return<div key={r.id} className="wr" style={r.priority==='๊ธด๊ธ‰'?{borderLeft:'3px solid #ef4444'}:needReply?{borderLeft:'3px solid #f59e0b'}:r.status==='์™„๋ฃŒ'?{opacity:.6}:{}}>
<div className="wr-h">{r.priority==='๊ธด๊ธ‰'&&<span className="wr-status ws-urg">๐Ÿšจ ๊ธด๊ธ‰</span>}<span className="wr-from">{r.from}</span><span className="wr-arr">โ†’</span><span className="wr-to">{(r.targets||[]).join(', ')}</span>
<span className={'wr-status '+(r.status==='๋Œ€๊ธฐ'?'ws-wait':'ws-done')}>{r.status==='๋Œ€๊ธฐ'?'โณ':'โœ…'}</span></div>
<div className="wr-content">{r.content}</div>
<div className="wr-meta"><span>{r.createdAt?.slice(0,10)}</span>{r.deadline&&<span className="wr-dl">๋งˆ๊ฐ {r.deadline}</span>}{r.from===me&&<span style={{cursor:'pointer',color:'#ef4444'}} onClick={()=>del(r.id)}>์‚ญ์ œ</span>}</div>
{(r.responses||[]).length>0&&<div className="wr-resp">{r.responses.map((rp,i)=><div key={i} className="wr-resp-item"><span className="wr-resp-name">โœ… {rp.author}</span><span className="wr-resp-text">{rp.text}</span><span className="wr-resp-time">{rp.date?.slice(0,10)}</span></div>)}</div>}
{needReply&&<div className="wr-reply">{replyTo===r.id?<><input className="fi2" value={replyText} onChange={e=>setReplyText(e.target.value)} placeholder="์‘๋‹ต..." style={{flex:1}} onKeyDown={e=>{if(e.key==='Enter')reply(r.id)}}/><button className="btn btn-p" onClick={()=>reply(r.id)}>์‘๋‹ต</button></>
:<button className="btn btn-n" onClick={()=>setReplyTo(r.id)} style={{fontSize:8}}>โœ๏ธ ์‘๋‹ต</button>}</div>}
{r.targets?.filter(t=>!(r.responses||[]).some(rp=>rp.author===t)).length>0&&r.status==='๋Œ€๊ธฐ'&&<div style={{marginTop:3,fontSize:7,color:'#94a3b8'}}>โณ {r.targets.filter(t=>!(r.responses||[]).some(rp=>rp.author===t)).join(', ')}</div>}
</div>})}</div></div>}
/* === WeeklyReport === */
function WeeklyReport({tasks,drops}){const now=new Date();const ws=new Date(now);ws.setDate(now.getDate()-now.getDay()+1);const wsStr=ws.toISOString().slice(0,10);
const wd=drops.filter(d=>(d.date||'').slice(0,10)>=wsStr);const bp={};wd.forEach(d=>{(bp[d.author]||(bp[d.author]=[])).push(d)});
const ct=tasks.filter(t=>CAL_TABS.includes(t.tab||'gov'));const in7=new Date(now.getTime()+7*86400000);
const up=ct.filter(t=>t.deadline&&new Date(t.deadline)>=now&&new Date(t.deadline)<=in7).sort((a,b)=>a.deadline.localeCompare(b.deadline));
const bt={};CAL_TABS.forEach(tid=>bt[tid]=ct.filter(t=>(t.tab||'gov')===tid).length);
const wn=Math.ceil(((now-new Date(now.getFullYear(),0,1))/86400000+new Date(now.getFullYear(),0,1).getDay()+1)/7);
return<div className="dash"><div className="rpt"><div className="rpt-t">๐Ÿ“‹ ์ฃผ๊ฐ„ ๋ฆฌํฌํŠธ โ€” {now.getFullYear()}-W{String(wn).padStart(2,'0')}</div><div className="rpt-s">
<b>๐Ÿ“Š ์ „์ฒด:</b> {ct.length}๊ฑด ({CAL_TABS.map(tid=>TL[tid]+' '+bt[tid]).join(', ')})<br/><br/>
<b>๐Ÿ’ฌ ์ด๋ฒˆ ์ฃผ ({wd.length}๊ฑด):</b><br/>{Object.entries(bp).map(([n,ds])=><div key={n} style={{margin:'3px 0'}}><b>{n}</b> ({ds.length}): {ds.map(d=>d.text).join(' / ')}</div>)}
{Object.keys(bp).length===0&&'๊ธฐ๋ก ์—†์Œ'}<br/><br/>
<b>๐Ÿ“… ๋งˆ๊ฐ ({up.length}๊ฑด):</b><br/>{up.map(t=><div key={t.id} style={{margin:'2px 0'}}>{t.deadline} <span className={'ttg '+(TC[t.tab]||'')}>{TL[t.tab]}</span> {t.title} {t.assignee&&<span style={{color:'#3b82f6'}}>({t.assignee})</span>}</div>)}
{up.length===0&&'์—†์Œ'}</div></div></div>}
/* === LinkPocket Grid + Tags === */
function LinkPocket({pockets,setPockets}){
const[isNew,setIsNew]=useState(false);const[f,setF]=useState({link:'',comment:'',tags:''});
const add=async()=>{if(!f.link.trim()&&!f.comment.trim())return;const item={...f,tags:(f.tags||'').split(',').map(s=>s.trim()).filter(Boolean),id:gid(),date:new Date().toISOString(),author:getUser()};const r=await api.savePocket(item);if(r)setPockets(r);setF({link:'',comment:'',tags:''});setIsNew(false)};
const del=async(id)=>{const r=await api.delPocket(id);if(r)setPockets(r)};
const sorted=[...pockets].sort((a,b)=>(b.date||'').localeCompare(a.date||''));
return<div className="nf"><div className="nf-h"><div className="nf-t">๐Ÿ”— ๋งํฌ ์ฃผ๋จธ๋‹ˆ ({pockets.length})</div><button className="btn btn-n" onClick={()=>setIsNew(!isNew)}>๏ผ‹ ๋˜์ง€๊ธฐ</button></div>
{isNew&&<div style={{background:'#f8fafc',border:'1px solid #e2e8f0',borderRadius:6,padding:10,marginBottom:8}}>
<input className="fi2" value={f.link} onChange={e=>setF(p=>({...p,link:e.target.value}))} placeholder="URL" style={{marginBottom:4}}/>
<input className="fi2" value={f.comment} onChange={e=>setF(p=>({...p,comment:e.target.value}))} placeholder="ํ•œ์ค„ ์ฝ”๋ฉ˜ํŠธ" style={{marginBottom:4}} onKeyDown={e=>{if(e.key==='Enter')add()}}/>
<input className="fi2" value={f.tags} onChange={e=>setF(p=>({...p,tags:e.target.value}))} placeholder="ํƒœ๊ทธ (์‰ผํ‘œ ๊ตฌ๋ถ„: ๊ฒฝ์Ÿ์‚ฌ, ๊ธฐ์ˆ )" style={{marginBottom:4}}/>
<div style={{display:'flex',gap:4}}><button className="btn btn-p" onClick={add}>๋“ฑ๋ก</button><button className="btn btn-g" onClick={()=>setIsNew(false)}>์ทจ์†Œ</button></div></div>}
<div className="pkt-grid">{sorted.length===0&&<div style={{gridColumn:'1/-1',textAlign:'center',color:'#94a3b8',fontSize:10,padding:20}}>๋งํฌ๋ฅผ ๋˜์ ธ์ฃผ์„ธ์š”</div>}
{sorted.map(p=><div key={p.id} className="pkt">{p.comment&&<div className="pkt-c">{p.comment}</div>}
{p.link&&<a href={p.link} target="_blank" rel="noopener" className="pkt-l">{p.link}</a>}
{(p.tags||[]).length>0&&<div style={{display:'flex',gap:2,flexWrap:'wrap'}}>{p.tags.map((t,i)=><span key={i} className="pkt-tag">#{t}</span>)}</div>}
<div className="pkt-m"><span style={{fontWeight:600}}>{p.author}</span><span>{p.date?.slice(0,10)}</span>{p.author===getUser()&&<span style={{cursor:'pointer',color:'#ef4444'}} onClick={()=>del(p.id)}>โœ•</span>}</div></div>)}</div></div>}
/* === MagicLibrary === */
function MagicLibrary(){const CATS=['์ƒ์‚ฐ์„ฑ','LLM','์˜์ƒAI','๋ฐ์ดํ„ฐ','๋ฒค์น˜๋งˆํฌ','์—์ด์ „ํŠธ','ํŠนํ—ˆ','์ธํ”„๋ผ','์‹œ๋ฎฌ๋ ˆ์ด์…˜','๊ธฐํƒ€'];
const DEF=[{id:'b1',name:'FINAL Bench',cat:'๋ฒค์น˜๋งˆํฌ',keywords:['AGI','TICOS'],stars:5,overview:'AGI ๊ฒ€์ฆ ๋ฒค์น˜๋งˆํฌ. HF ๊ธ€๋กœ๋ฒŒ 5์œ„.',link:'https://huggingface.co/datasets',img:'',memo:''},{id:'b2',name:'ํ—ˆ๊ทธ์™€ํŠธ ๋ฐ”๋‚˜๋‚˜',cat:'์˜์ƒAI',keywords:['ํ•œ๊ตญ์–ด'],stars:5,overview:'ํ•œ๊ตญ์–ด AI ์˜์ƒ ์ƒ์„ฑ.',link:'https://ginigen.ai',img:'',memo:''},{id:'b3',name:'SiteAgent',cat:'์—์ด์ „ํŠธ',keywords:['๋ธŒ๋ผ์šฐ์ €'],stars:4,overview:'๋ธŒ๋ผ์šฐ์ € DOM AI ์—์ด์ „ํŠธ.',link:'',img:'',memo:''},{id:'b4',name:'Proto-AGI ํŠนํ—ˆ',cat:'ํŠนํ—ˆ',keywords:['์ฐฝ๋ฐœ์„ฑ','SLAI'],stars:5,overview:'8๊ฑด ํŠนํ—ˆ.',link:'',img:'',memo:''},{id:'b5',name:'AETHER',cat:'LLM',keywords:['๋ฉ”ํƒ€์ธ์ง€'],stars:4,overview:'5๋Œ€ ๊ธฐ๋‘ฅ.',link:'',img:'',memo:''}];
const[books,setBooks]=useState(()=>{try{return JSON.parse(localStorage.getItem('vd_lib'))||DEF}catch(e){return DEF}});
const[selId,setSelId]=useState(null);const[isNew,setIsNew]=useState(false);
const[f,setF]=useState({id:'',name:'',cat:'๊ธฐํƒ€',keywords:[],stars:3,overview:'',link:'',img:'',memo:''});
const saveAll=l=>{localStorage.setItem('vd_lib',JSON.stringify(l));setBooks(l)};const u=(k,v)=>setF(p=>({...p,[k]:v}));const sel=selId?books.find(b=>b.id===selId):null;
const Stars=({n,onChange})=><div style={{display:'flex',gap:1,cursor:onChange?'pointer':'default'}}>{[1,2,3,4,5].map(i=><span key={i} style={{fontSize:12,color:i<=n?'#f59e0b':'#e2e8f0'}} onClick={()=>onChange&&onChange(i)}>โ˜…</span>)}</div>;
return<div className="main"><div className="left" style={{width:280,minWidth:280}}><div style={{padding:'6px 10px',display:'flex',justifyContent:'space-between',alignItems:'center'}}><span style={{fontSize:11,fontWeight:700}}>๐Ÿ“š ๋งˆ๋ฒ•์ฑ…</span><button className="btn btn-n" onClick={()=>{setF({id:'',name:'',cat:'๊ธฐํƒ€',keywords:[],stars:3,overview:'',link:'',img:'',memo:''});setSelId(null);setIsNew(true)}}>๏ผ‹</button></div><div className="dv2"/>
<div className="tl2">{books.map(b=><div key={b.id} className={'tk2'+(b.id===selId?' ac':'')} onClick={()=>{setF({...b,keywords:b.keywords||[]});setSelId(b.id);setIsNew(false)}}><div className="tb2"><div className="tn2">{b.name}</div><div className="tm2"><span style={{fontSize:7,fontWeight:600,padding:'1px 3px',borderRadius:2,background:'#eff6ff',color:'#3b82f6'}}>{b.cat}</span><Stars n={b.stars}/></div></div></div>)}</div></div>
{isNew||sel?<div className="right fade2"><div className="dh3"><div className="dt3">{isNew?'๐Ÿ“ ๋“ฑ๋ก':'๐Ÿ“š ์ƒ์„ธ'}</div></div><div className="fm2">
<div className="fr2"><div className="fg2"><span className="fl2">ํ”„๋กœ์ ํŠธ๋ช…</span><input className="fi2" value={f.name} onChange={e=>u('name',e.target.value)}/></div><div className="fg2"><span className="fl2">๋ถ„๋ฅ˜</span><select className="fs2" value={f.cat} onChange={e=>u('cat',e.target.value)}>{CATS.map(c=><option key={c}>{c}</option>)}</select></div></div>
<div className="fr2"><div className="fg2"><span className="fl2">์ค‘์š”๋„</span><Stars n={f.stars} onChange={v=>u('stars',v)}/></div><div className="fg2"><span className="fl2">ํ‚ค์›Œ๋“œ</span><input className="fi2" value={(f.keywords||[]).join(', ')} onChange={e=>u('keywords',e.target.value.split(',').map(s=>s.trim()).filter(Boolean))}/></div></div>
<div className="fg2 full"><span className="fl2">๊ฐœ์š”</span><textarea className="ft2" value={f.overview} onChange={e=>u('overview',e.target.value)} style={{minHeight:60}}/></div>
<div className="fg2 full"><span className="fl2">๋งํฌ</span><input className="fi2" value={f.link} onChange={e=>u('link',e.target.value)}/></div>
<div className="fg2 full"><span className="fl2">์บก์ฒ˜</span><input type="file" accept="image/*" onChange={e=>{const file=e.target.files[0];if(!file)return;const r=new FileReader();r.onload=ev=>u('img',ev.target.result);r.readAsDataURL(file)}} style={{fontSize:9}}/>{f.img&&<img src={f.img} style={{maxWidth:'100%',maxHeight:180,borderRadius:5,border:'1px solid #e2e8f0',marginTop:3}} alt=""/>}</div>
<div className="fg2 full"><span className="fl2">๋ฉ”๋ชจ</span><textarea className="ft2" value={f.memo} onChange={e=>u('memo',e.target.value)}/></div>
<div className="fa2">{!isNew&&sel&&<button className="btn btn-d" onClick={()=>{saveAll(books.filter(b=>b.id!==f.id));setSelId(null);setIsNew(false)}}>๐Ÿ—‘๏ธ</button>}<div className="sp2"/><button className="btn btn-g" onClick={()=>{setSelId(null);setIsNew(false)}}>์ทจ์†Œ</button>
<button className="btn btn-p" onClick={()=>{if(!f.name.trim())return;const item={...f,id:f.id||gid()};const idx=books.findIndex(b=>b.id===item.id);if(idx>=0){const n=[...books];n[idx]=item;saveAll(n)}else saveAll([item,...books]);setSelId(item.id);setIsNew(false)}}>๐Ÿ’พ</button></div></div></div>
:<div className="right"><div className="de3"><div className="ic">๐Ÿ“š</div><div style={{fontSize:11,color:'#94a3b8'}}>์„ ํƒ ๋˜๋Š” ๋“ฑ๋ก</div></div></div>}</div>}
/* === NewsFeed === */
function NewsFeed(){const[news,setNews]=useState([]);const[ld,setLd]=useState(true);const[ft,setFt]=useState('all');const RC={ํ•ต์‹ฌ:'r-c',์ฃผ๋ชฉ:'r-h',์ฐธ๊ณ :'r-m',์ผ๋ฐ˜:'r-g'};
const ln=async(f)=>{setLd(true);const r=await fetch(f?'/api/news?refresh=true':'/api/news',{headers:authHeaders()});if(r.status===401){doLogout();return}setNews(await r.json()||[]);setLd(false)};
useEffect(()=>{ln(false)},[]);const tags=[...new Set(news.flatMap(n=>n.tags||[]))];const fil=ft==='all'?news:news.filter(n=>(n.tags||[]).includes(ft));
return<div className="nf"><div className="nf-h"><div className="nf-t">๐Ÿ“ฐ AI ๋‰ด์Šค ({news.length})</div><button className="nf-r" onClick={()=>ln(true)}>{ld?'โณ':'๐Ÿ”„'}</button></div>
<div className="nf-fs"><div className={'nf-f'+(ft==='all'?' active':'')} onClick={()=>setFt('all')}>์ „์ฒด</div>{tags.map(t=><div key={t} className={'nf-f'+(ft===t?' active':'')} onClick={()=>setFt(t)}>{t}</div>)}</div>
{ld&&news.length===0?<div className="ld2" style={{padding:30}}><div className="spn2"/>์ˆ˜์ง‘ ์ค‘...</div>
:<div className="nc-grid">{fil.map((n,i)=><div key={i} className="nc"><div style={{display:'flex',justifyContent:'space-between',gap:3}}><span className="nc-t">{n.title}</span><span className={'nc-rel '+(RC[n.relevance]||'r-g')}>{n.relevance}</span></div>
<div className="nc-tags">{(n.tags||[]).map(t=><span key={t} className="nc-tag" style={{background:(n.colors?.[t]||'#64748b')+'18',color:n.colors?.[t]||'#64748b'}}>{t}</span>)}</div>
<div className="nc-s">{n.summary}</div><div className="nc-m"><span>{n.source}</span><span>{n.date}</span><a href={n.url} target="_blank" rel="noopener" className="nc-l">๐Ÿ”—</a></div></div>)}</div>}</div>}
function RndTab({pockets,setPockets}){const[sub,setSub]=useState('news');return<div className="main" style={{flexDirection:'column'}}><div className="mgr"><div className="stabs">
<div className={'stab'+(sub==='news'?' active':'')} onClick={()=>setSub('news')}>๐Ÿ“ฐ ๋‰ด์Šค</div><div className={'stab'+(sub==='library'?' active':'')} onClick={()=>setSub('library')}>๐Ÿ“š ๋งˆ๋ฒ•์ฑ…</div>
<div className={'stab'+(sub==='pocket'?' active':'')} onClick={()=>setSub('pocket')}>๐Ÿ”— ๋งํฌ์ฃผ๋จธ๋‹ˆ</div></div></div>
{sub==='news'?<NewsFeed/>:sub==='library'?<MagicLibrary/>:<LinkPocket pockets={pockets} setPockets={setPockets}/>}</div>}
function MgmtTab({allTasks,saving,setSaving,setAllTasks}){const[sub,setSub]=useState('tasks');
return<div className="main" style={{flexDirection:'column'}}><div className="mgr"><div className="stabs">
<div className={'stab'+(sub==='tasks'?' active':'')} onClick={()=>setSub('tasks')}>๐Ÿ“‹ ์—…๋ฌด</div>
<div className={'stab'+(sub==='files'?' active':'')} onClick={()=>setSub('files')}>๐Ÿ“ ์ž๋ฃŒ์‹ค</div></div></div>
{sub==='tasks'?<CalTab key="mgmt" allTasks={allTasks} tabId="mgmt" saving={saving} setSaving={setSaving} setAllTasks={setAllTasks}/>:<FileLibrary/>}</div>}
function FileLibrary(){const[files,setFiles]=useState([]);const[ld,setLd]=useState(true);const[uploading,setUploading]=useState(false);const[isNew,setIsNew]=useState(false);
const[title,setTitle]=useState('');const[desc,setDesc]=useState('');const[selFile,setSelFile]=useState(null);
const load=async()=>{setLd(true);const r=await fetch('/api/files',{headers:authHeaders()});if(r.ok)setFiles(await r.json());setLd(false)};
useEffect(()=>{load()},[]);
const upload=async()=>{if(!selFile)return;setUploading(true);const fd=new FormData();fd.append('file',selFile);fd.append('title',title||selFile.name);fd.append('desc',desc);fd.append('author',getUser());
const r=await fetch('/api/files/upload',{method:'POST',headers:{'Authorization':'Bearer '+getToken()},body:fd});if(r.ok){await load();setTitle('');setDesc('');setSelFile(null);setIsNew(false)}setUploading(false)};
const del=async(id)=>{await fetch('/api/files/'+encodeURIComponent(id),{method:'DELETE',headers:authHeaders()});load()};
const fmtSz=b=>b>1048576?(b/1048576).toFixed(1)+'MB':b>1024?(b/1024).toFixed(0)+'KB':b+'B';
return<div className="nf"><div className="nf-h"><div className="nf-t">๐Ÿ“ ์ž๋ฃŒ์‹ค ({files.length})</div><button className="btn btn-n" onClick={()=>setIsNew(!isNew)}>๏ผ‹ ์—…๋กœ๋“œ</button></div>
{isNew&&<div style={{background:'#f8fafc',border:'1px solid #e2e8f0',borderRadius:6,padding:10,marginBottom:8}}>
<input className="fi2" value={title} onChange={e=>setTitle(e.target.value)} placeholder="์ œ๋ชฉ" style={{marginBottom:4}}/>
<input className="fi2" value={desc} onChange={e=>setDesc(e.target.value)} placeholder="ํ•œ์ค„ ์„ค๋ช…" style={{marginBottom:4}}/>
<input type="file" onChange={e=>setSelFile(e.target.files[0])} style={{fontSize:9,marginBottom:4}}/>
<div style={{display:'flex',gap:4}}><button className="btn btn-p" onClick={upload} disabled={uploading}>{uploading?'โณ':'๐Ÿ“ค'}</button><button className="btn btn-g" onClick={()=>setIsNew(false)}>์ทจ์†Œ</button></div></div>}
{files.sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(f=><div key={f.id} className="pkt"><div style={{display:'flex',justifyContent:'space-between',gap:6}}>
<div><div style={{fontSize:10,fontWeight:700}}>{f.title||f.filename}</div>{f.desc&&<div className="pkt-c">{f.desc}</div>}
<div className="pkt-m"><span style={{fontWeight:600}}>{f.author}</span><span>{f.date?.slice(0,10)}</span><span>{fmtSz(f.size||0)}</span></div></div>
<div style={{display:'flex',gap:3,flexShrink:0}}><a href={'/api/files/download/'+encodeURIComponent(f.id)+'?token='+getToken()} className="btn btn-p" style={{textDecoration:'none',fontSize:8}} target="_blank">โฌ‡</a>
{f.author===getUser()&&<button className="btn btn-d" style={{fontSize:8,padding:'3px 6px'}} onClick={()=>del(f.id)}>โœ•</button>}</div></div></div>)}</div>}
/* === Wizard MD + History + Side === */
function md2html(t){if(!t)return '';
// Code blocks first
let h=t.replace(/```(\w*)\n([\s\S]*?)```/g,'<pre style="background:#1e293b;color:#e2e8f0;padding:8px;border-radius:5px;font-size:9px;overflow-x:auto;font-family:JetBrains Mono,monospace;margin:6px 0"><code>$2</code></pre>');
h=h.replace(/`([^`]+)`/g,'<code style="background:#f1f5f9;padding:1px 3px;border-radius:2px;font-size:9px">$1</code>');
// Tables - group consecutive | lines into one table
const lines=h.split('\n');const out=[];let tbl=[];
const flushTable=()=>{if(tbl.length===0)return;
let html='<div style="overflow-x:auto;margin:8px 0"><table style="border-collapse:collapse;width:100%;font-size:10px">';
let isFirst=true;
tbl.forEach(row=>{if(/^[\s\|\-:]+$/.test(row.replace(/\|/g,'').trim()))return;
const cells=row.split('|').slice(1,-1);if(cells.length===0)return;
if(isFirst){html+='<tr>'+cells.map(c=>'<th style="background:#f1f5f9;border:1px solid #e2e8f0;padding:6px 10px;font-weight:700;font-size:10px;color:#1e293b;text-align:left;white-space:nowrap">'+c.trim()+'</th>').join('')+'</tr>';isFirst=false;}
else{html+='<tr>'+cells.map(c=>'<td style="border:1px solid #e2e8f0;padding:6px 10px;font-size:10px;color:#475569;line-height:1.5;vertical-align:top">'+c.trim()+'</td>').join('')+'</tr>';}});
html+='</table></div>';out.push(html);tbl=[]};
lines.forEach(l=>{if(l.trim().startsWith('|')&&l.trim().endsWith('|')){tbl.push(l)}else{flushTable();out.push(l)}});flushTable();
h=out.join('\n');
h=h.replace(/^### (.+)$/gm,'<div style="font-size:12px;font-weight:700;margin:10px 0 4px;color:#1e293b;border-left:3px solid #3b82f6;padding-left:8px">$1</div>');
h=h.replace(/^## (.+)$/gm,'<div style="font-size:13px;font-weight:800;margin:12px 0 6px;color:#1e293b;border-bottom:1px solid #e2e8f0;padding-bottom:4px">$1</div>');
h=h.replace(/\*\*(.+?)\*\*/g,'<b style="color:#1e293b">$1</b>');
h=h.replace(/^[\-\*] (.+)$/gm,'<div style="padding-left:12px;margin:2px 0;position:relative"><span style="position:absolute;left:0">โ€ข</span> $1</div>');
h=h.replace(/\n/g,'<br/>');return h}
function WizardTab({tasks,drops,pockets,reqs}){
const[msgs,setMsgs]=useState(()=>{try{return JSON.parse(localStorage.getItem('vd_chat'))||[{role:'ai',text:'๐Ÿง™ ๋น„๋“œ๋ž˜ํ”„ํŠธ ๋งˆ๋ฒ•์‚ฌ์ž…๋‹ˆ๋‹ค. ๋ฌด์—‡์ด๋“  ๋ฌผ์–ด๋ณด์„ธ์š”!'}]}catch(e){return[{role:'ai',text:'๐Ÿง™ ๋งˆ๋ฒ•์‚ฌ์ž…๋‹ˆ๋‹ค.'}]}});
const[input,setInput]=useState('');const[loading,setLoading]=useState(false);
useEffect(()=>{try{localStorage.setItem('vd_chat',JSON.stringify(msgs.slice(-50)))}catch(e){}},[msgs]);
const ct=tasks.filter(t=>CAL_TABS.includes(t.tab||'gov'));const now=new Date();const today=now.toISOString().slice(0,10);
const in7=new Date(now.getTime()+7*86400000);const urgent=ct.filter(t=>t.deadline&&new Date(t.deadline)>=now&&new Date(t.deadline)<=in7);
const todayDrops=drops.filter(d=>(d.date||'').slice(0,10)===today);const me=getUser();
const myPending=(reqs||[]).filter(r=>r.targets?.includes(me)&&!(r.responses||[]).some(rp=>rp.author===me)&&r.status==='๋Œ€๊ธฐ');
const ctx=()=>`[๋น„๋“œ๋ž˜ํ”„ํŠธ]\n๊ณผ์ œ ${ct.length}๊ฑด:\n${ct.slice(0,30).map(t=>`- [${TL[t.tab]||t.tab}] ${t.title} (๋งˆ๊ฐ:${t.deadline||'๋ฏธ์ •'}, ๋‹ด๋‹น:${t.assignee||''}, ์ƒํƒœ:${t.status||t.stage||t.prStatus||t.mgmtStatus||''})`).join('\n')}\n\nํ•œ์ค„ ์ตœ๊ทผ20:\n${drops.slice(-20).map(d=>`- ${d.author}: ${d.text} (${d.date?.slice(0,10)})`).join('\n')}\n\n๋งํฌ:\n${(pockets||[]).slice(-10).map(p=>`- ${p.author}: ${p.comment||''} ${p.link||''}`).join('\n')}\n\nํ˜‘์กฐ ๋Œ€๊ธฐ:\n${(reqs||[]).filter(r=>r.status==='๋Œ€๊ธฐ').slice(0,10).map(r=>`- ${r.from}โ†’${r.targets.join(',')} "${r.content}"`).join('\n')}\n\n์˜ค๋Š˜: ${today}`;
const ask=async(q)=>{if(!q.trim()||loading)return;setInput('');setMsgs(p=>[...p,{role:'user',text:q}]);setLoading(true);
try{const r=await fetch('/api/chat',{method:'POST',headers:authHeaders(),body:JSON.stringify({messages:[{role:'system',content:'๋น„๋“œ๋ž˜ํ”„ํŠธ ๋‚ด๋ถ€ ๋น„์„œ "๋งˆ๋ฒ•์‚ฌ". ํ•œ๊ตญ์–ด. ๋งˆํฌ๋‹ค์šด ํ‘œ/๋ฆฌ์ŠคํŠธ ์ ๊ทน ํ™œ์šฉ. ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ •ํ™• ๋‹ต๋ณ€.'},{role:'user',content:ctx()+'\n\n์งˆ๋ฌธ: '+q}]})});
const d=await r.json();setMsgs(p=>[...p,{role:'ai',text:d.error?'โš ๏ธ '+d.error:(d.text||'์‘๋‹ต ์—†์Œ')}])}catch(e){setMsgs(p=>[...p,{role:'ai',text:'โš ๏ธ '+e.message}])}setLoading(false)};
const QUICK=[{i:'๐Ÿ“…',l:'์ด๋ฒˆ ์ฃผ ๋งˆ๊ฐ',q:'์ด๋ฒˆ ์ฃผ ๋งˆ๊ฐ ๊ณผ์ œ๋ฅผ ํƒญ๋ณ„, ๋‹ด๋‹น์ž๋ณ„ ํ‘œ๋กœ.'},{i:'๐Ÿ“Š',l:'์ „์ฒด ํ˜„ํ™ฉ',q:'์ „์ฒด ๊ณผ์ œ ํ˜„ํ™ฉ์„ ํƒญ๋ณ„ ํ‘œ๋กœ.'},{i:'๐Ÿ‘ฅ',l:'๋‹ด๋‹น์ž๋ณ„',q:'๋‹ด๋‹น์ž๋ณ„ ๊ณผ์ œ ์ˆ˜์™€ ๋งˆ๊ฐ ์ž„๋ฐ•์„ ํ‘œ๋กœ.'},{i:'๐Ÿ’ฌ',l:'ํ™œ๋™ ์š”์•ฝ',q:'์ด๋ฒˆ ์ฃผ ์˜ค๋Š˜์˜ ํ•œ์ค„ ๊ธฐ๋ฐ˜ ํ™œ๋™ ์š”์•ฝ.'},{i:'๐Ÿค',l:'๋ฏธ์‘๋‹ต ํ˜‘์กฐ',q:'์—…๋ฌด ํ˜‘์กฐ ๋ฏธ์‘๋‹ต ๊ฑด ์ •๋ฆฌ.'},{i:'๐Ÿ“ˆ',l:'์˜์—… ํ˜„ํ™ฉ',q:'์ œํœด/์˜์—…/ํˆฌ์ž ๋”œ ๋‹จ๊ณ„๋ณ„ ํ‘œ.'},{i:'๐Ÿ›๏ธ',l:'์ •๋ถ€๊ณผ์ œ',q:'์ •๋ถ€๊ณผ์ œ ํ˜„ํ™ฉ ์ฃผ๊ด€๋ถ€์„œ๋ณ„ ํ‘œ.'},{i:'โš™๏ธ',l:'ํ”„๋กœ์ ํŠธ',q:'ํ”„๋กœ์ ํŠธ ์ง„ํ–‰๋ฅ  ์ •๋ฆฌ.'}];
return<div className="main" style={{overflow:'hidden'}}>
<div style={{width:200,minWidth:200,borderRight:'1px solid #e2e8f0',display:'flex',flexDirection:'column',background:'#f8fafc',overflow:'hidden'}}>
<div style={{padding:'8px 10px',fontSize:10,fontWeight:700,borderBottom:'1px solid #e2e8f0'}}>โšก ๋น ๋ฅธ ์งˆ๋ฌธ</div>
<div style={{flex:1,overflowY:'auto',padding:4}}>{QUICK.map((q,i)=><div key={i} onClick={()=>ask(q.q)} style={{padding:'5px 8px',fontSize:9,cursor:'pointer',borderRadius:4,marginBottom:2,color:'#475569',display:'flex',gap:4,alignItems:'center',border:'1px solid #e2e8f0',background:'#fff'}}
onMouseEnter={e=>e.currentTarget.style.borderColor='#3b82f6'} onMouseLeave={e=>e.currentTarget.style.borderColor='#e2e8f0'}><span style={{fontSize:12}}>{q.i}</span>{q.l}</div>)}</div>
<div style={{padding:6,borderTop:'1px solid #e2e8f0',fontSize:7,color:'#94a3b8'}}>
<div style={{fontWeight:700,marginBottom:2}}>๐Ÿ“Š ์‹ค์‹œ๊ฐ„</div>
<div>๊ณผ์ œ <b style={{color:'#1e293b'}}>{ct.length}</b> | ์ž„๋ฐ• <b style={{color:'#ef4444'}}>{urgent.length}</b></div>
<div>ํ™œ๋™ <b style={{color:'#3b82f6'}}>{todayDrops.length}</b> | ํ˜‘์กฐ <b style={{color:'#f59e0b'}}>{myPending.length}</b></div>
<div style={{marginTop:4,cursor:'pointer',color:'#ef4444'}} onClick={()=>{setMsgs([{role:'ai',text:'๐Ÿง™ ๋Œ€ํ™” ์ดˆ๊ธฐํ™”๋จ'}]);localStorage.removeItem('vd_chat')}}>๐Ÿ—‘๏ธ ๋Œ€ํ™” ์ดˆ๊ธฐํ™”</div>
</div></div>
<div style={{flex:1,display:'flex',flexDirection:'column',overflow:'hidden'}}>
<div className="chat-msgs" style={{flex:1,overflowY:'auto',padding:'10px 14px'}}>
{msgs.map((m,i)=><div key={i} className={'chat-msg '+m.role}>{m.role==='ai'?<div className="cb3" dangerouslySetInnerHTML={{__html:md2html(m.text)}}/>:<div className="cb3">{m.text}</div>}</div>)}
{loading&&<div className="chat-msg ai"><div className="cb3">๐Ÿง™ ์ƒ๊ฐ ์ค‘...</div></div>}</div>
<div className="chat-input"><input className="fi2" value={input} onChange={e=>setInput(e.target.value)} placeholder="๋งˆ๋ฒ•์‚ฌ์—๊ฒŒ ๋ฌผ์–ด๋ณด์„ธ์š”..." onKeyDown={e=>{if(e.key==='Enter')ask(input)}}/><button className="btn btn-n" onClick={()=>ask(input)} disabled={loading}>๐Ÿง™</button></div></div></div>}
/* === DashOverview + Search + Recent === */
function DashOverview({tasks,drops,reqs}){
const[search,setSearch]=useState('');
const[brief,setBrief]=useState(null);const[briefLoading,setBriefLoading]=useState(false);
const ct=tasks.filter(t=>CAL_TABS.includes(t.tab||'gov'));const now=new Date();const in7=new Date(now.getTime()+7*86400000);
const urgent=ct.filter(t=>t.deadline&&new Date(t.deadline)>=now&&new Date(t.deadline)<=in7).sort((a,b)=>a.deadline.localeCompare(b.deadline));
const bt={};CAL_TABS.forEach(tid=>bt[tid]=ct.filter(t=>(t.tab||'gov')===tid).length);
const byP={};ALL_M.forEach(m=>{byP[m]=ct.filter(t=>t.assignee===m).length});
const today=now.toISOString().slice(0,10);const todayDrops=drops.filter(d=>(d.date||'').slice(0,10)===today);
const me=getUser();const myPending=(reqs||[]).filter(r=>r.targets?.includes(me)&&!(r.responses||[]).some(rp=>rp.author===me)&&r.status==='๋Œ€๊ธฐ');
const getRoles=n=>{const r=[];Object.entries(MGR).forEach(([tid,v])=>{if(v.m===n)r.push({tab:tid,role:'์ •'});if(v.s.includes(n))r.push({tab:tid,role:'๋ถ€'})});return r};
const recent=ct.sort((a,b)=>(b.deadline||'').localeCompare(a.deadline||'')).slice(0,5);
const filtered=search?ct.filter(t=>(t.title||'').includes(search)||(t.assignee||'').includes(search)):null;
const loadBrief=async(force)=>{setBriefLoading(true);try{const r=await fetch('/api/briefing',{method:'POST',headers:authHeaders(),body:JSON.stringify({user:me,force})});
const d=await r.json();setBrief(d)}catch(e){setBrief({text:'โš ๏ธ ๋ธŒ๋ฆฌํ•‘ ์ƒ์„ฑ ์‹คํŒจ'})}setBriefLoading(false)};
useEffect(()=>{loadBrief(false)},[]);
return<div className="dash">
{/* ์•„์นจ ๋ธŒ๋ฆฌํ•‘ */}
<div style={{background:'linear-gradient(135deg,#eff6ff,#faf5ff)',border:'1px solid #c7d2fe',borderRadius:8,padding:12,marginBottom:12}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}>
<span style={{fontSize:12,fontWeight:700}}>๐Ÿง™ {me}๋‹˜์˜ ๋ธŒ๋ฆฌํ•‘</span>
<button className="btn btn-g" onClick={()=>loadBrief(true)} disabled={briefLoading} style={{fontSize:8}}>{briefLoading?'โณ ์ƒ์„ฑ ์ค‘...':'๐Ÿ”„ ์ƒˆ๋กœ ์ƒ์„ฑ'}</button></div>
{briefLoading&&!brief?<div style={{textAlign:'center',padding:10,color:'#94a3b8',fontSize:10}}>๐Ÿง™ AI๊ฐ€ ๋ธŒ๋ฆฌํ•‘์„ ์ค€๋น„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...</div>
:brief?<><div dangerouslySetInnerHTML={{__html:md2html(brief.text)}} style={{fontSize:11,lineHeight:1.7,color:'#334155'}}/>
{brief.org_text&&<div style={{marginTop:8,padding:10,background:'rgba(255,255,255,.8)',borderRadius:6,border:'1px solid #e2e8f0'}}>
<div style={{fontSize:10,fontWeight:700,color:'#7c3aed',marginBottom:4}}>๐Ÿ‘‘ ์กฐ์ง ๋ธŒ๋ฆฌํ•‘ (CEO ์ „์šฉ)</div>
<div dangerouslySetInnerHTML={{__html:md2html(brief.org_text)}} style={{fontSize:10,lineHeight:1.6,color:'#334155'}}/></div>}</>
:<div style={{color:'#94a3b8',fontSize:9,textAlign:'center',padding:6}}>๋ธŒ๋ฆฌํ•‘ ๋กœ๋”ฉ ์ค‘...</div>}
</div>
<div className="dash-search"><input value={search} onChange={e=>setSearch(e.target.value)} placeholder="๐Ÿ” ์ „์ฒด ๊ฒ€์ƒ‰ (๊ณผ์ œ๋ช…, ๋‹ด๋‹น์ž)"/></div>
{filtered&&<div style={{marginBottom:10}}><div className="st">๐Ÿ” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ({filtered.length}๊ฑด)</div>
{filtered.slice(0,10).map(t=>{const dd=dday(t.deadline);return<div key={t.id} className="tk2"><div className={'pb2 '+(TCFG[t.tab]?TCFG[t.tab].bar(t):'pm2')}/><div className="tb2"><div className="tn2">{t.title}</div>
<div className="tm2"><span className={'ttg '+(TC[t.tab]||'')}>{TL[t.tab]}</span>{dd&&<span className={'dday '+dd.c}>{dd.t}</span>}<span>{t.deadline}</span>{t.assignee&&<span>โ€ข {t.assignee}</span>}</div></div></div>})}</div>}
{!search&&<>{urgent.length>0&&<><div className="st">๐Ÿšจ ๋งˆ๊ฐ ์ž„๋ฐ• ({urgent.length}๊ฑด)</div><div className="urg">{urgent.slice(0,5).map(t=>{const dd=dday(t.deadline);return<div key={t.id} className="urg-i">{dd&&<span className={'dday '+dd.c} style={{marginRight:4}}>{dd.t}</span>}<span className={'ttg '+(TC[t.tab]||'')}>{TL[t.tab]}</span> {t.title} {t.assignee&&<span style={{color:'#3b82f6'}}>({t.assignee})</span>}</div>})}</div></>}
{myPending.length>0&&<><div className="st">๐Ÿค ๋‚˜ํ•œํ…Œ ์˜จ ์š”์ฒญ ({myPending.length})</div><div className="urg" style={{background:'#fffbeb',borderColor:'#fed7aa'}}>{myPending.map(r=><div key={r.id} className="urg-i" style={{color:'#92400e'}}>{r.priority==='๊ธด๊ธ‰'&&<span style={{color:'#ef4444',fontWeight:700}}>๐Ÿšจ </span>}<b>{r.from}</b> "{r.content}" {r.deadline&&<span style={{color:'#dc2626'}}>๋งˆ๊ฐ {r.deadline}</span>}</div>)}</div></>}
<div className="st">๐Ÿ“Š ํƒญ๋ณ„</div>
<div className="kpi-r"><div className="kpi"><div className="kv">{ct.length}</div><div className="kl">์ „์ฒด</div></div>{CAL_TABS.map(tid=><div key={tid} className="kpi"><div className="kv" style={{color:TCLR[tid]}}>{bt[tid]}</div><div className="kl">{TL[tid]}</div></div>)}</div>
<div className="st">๐Ÿ• ์ตœ๊ทผ ๋“ฑ๋ก</div>
{recent.map(t=>{const dd=dday(t.deadline);return<div key={t.id} className="tk2" style={{cursor:'default'}}><div className={'pb2 '+(TCFG[t.tab]?TCFG[t.tab].bar(t):'pm2')}/><div className="tb2"><div className="tn2">{t.title}</div>
<div className="tm2"><span className={'ttg '+(TC[t.tab]||'')}>{TL[t.tab]}</span>{dd&&<span className={'dday '+dd.c}>{dd.t}</span>}<span>{t.deadline}</span>{t.assignee&&<span>โ€ข {t.assignee}</span>}</div></div></div>})}
<div className="st" style={{marginTop:8}}>๐Ÿ’ฌ ์˜ค๋Š˜์˜ ํ•œ์ค„ ({todayDrops.length})</div>
{todayDrops.length>0?todayDrops.slice(0,5).map(d=><div key={d.id} className="drop-item" style={{borderBottom:'1px solid #f1f5f9',padding:'4px 0'}}><div className="drop-avatar">{(d.author||'?')[0]}</div><div className="drop-body"><span className="drop-name">{d.author}</span><span className="drop-time">{d.date?.slice(11,16)}</span><div className="drop-text">{d.text}</div></div></div>)
:<div style={{background:'#f8fafc',border:'1px solid #e2e8f0',borderRadius:5,padding:8,textAlign:'center',color:'#94a3b8',fontSize:9,marginBottom:8}}>์˜ค๋Š˜ ๊ธฐ๋ก ์—†์Œ</div>}
<div className="st" style={{marginTop:8}}>๐Ÿ‘ฅ ์ธ์› ({MEMBERS.length}+{ADVISORS.length})</div>
<div className="pcard">{[...MEMBERS,...ADVISORS].map(m=>{const roles=getRoles(m);const adv=ADVISORS.includes(m);
return<div key={m} className="pcd" style={adv?{borderLeft:'2px solid #f59e0b'}:{}}>
<div className="pcd-n">{m}{adv&&<span style={{fontSize:7,color:'#d97706',marginLeft:3}}>์ž๋ฌธ</span>}</div>
<div className="pcd-r">{roles.map((r,i)=><span key={i} className="pcd-role" style={{background:r.role==='์ •'?'#eff6ff':'#f1f5f9',color:r.role==='์ •'?'#3b82f6':'#64748b'}}>{TL[r.tab]} {r.role}</span>)}</div>
<div className="pcd-s"><span>๊ณผ์ œ</span><span className="pcd-v">{byP[m]||0}</span></div></div>})}</div></>}
</div>}
/* === AI Insights (๋ ˆ์ด๋” + ์—ฐ๊ฒฐ ๋ฐœ๊ฒฌ) === */
function AiInsights(){
const me=getUser();
const[radar,setRadar]=useState(null);const[conn,setConn]=useState(null);const[weekly,setWeekly]=useState(null);
const[rLoading,setRLoading]=useState(false);const[cLoading,setCLoading]=useState(false);const[wLoading,setWLoading]=useState(false);
const isCeo=me==='๊น€๋ฏผ์‹'||me==='๊น€ํƒœ๋ด‰';
const loadRadar=async()=>{setRLoading(true);try{const r=await fetch('/api/radar',{method:'POST',headers:authHeaders(),body:JSON.stringify({user:me})});setRadar(await r.json())}catch(e){setRadar({text:'โš ๏ธ ์˜ค๋ฅ˜'})}setRLoading(false)};
const loadConn=async()=>{setCLoading(true);try{const r=await fetch('/api/connections',{method:'POST',headers:authHeaders(),body:JSON.stringify({user:me})});setConn(await r.json())}catch(e){setConn({text:'โš ๏ธ ์˜ค๋ฅ˜'})}setCLoading(false)};
const loadWeekly=async()=>{setWLoading(true);try{const r=await fetch('/api/weekly-ai',{method:'POST',headers:authHeaders(),body:JSON.stringify({user:me})});setWeekly(await r.json())}catch(e){setWeekly({text:'โš ๏ธ ์˜ค๋ฅ˜'})}setWLoading(false)};
return<div className="dash">
<div style={{display:'grid',gridTemplateColumns:isCeo?'1fr 1fr 1fr':'1fr 1fr',gap:8,marginBottom:12}}>
<button className="btn btn-n" onClick={loadRadar} disabled={rLoading} style={{padding:'10px',fontSize:11,justifyContent:'center'}}>๐ŸŽฏ {rLoading?'๋ถ„์„ ์ค‘...':'๊ธฐํšŒ ๋ ˆ์ด๋” ์‹คํ–‰'}</button>
<button className="btn btn-n" onClick={loadConn} disabled={cLoading} style={{padding:'10px',fontSize:11,justifyContent:'center'}}>๐Ÿ”ฎ {cLoading?'๋ถ„์„ ์ค‘...':'์—ฐ๊ฒฐ ๋ฐœ๊ฒฌ ์‹คํ–‰'}</button>
{isCeo&&<button className="btn btn-n" onClick={loadWeekly} disabled={wLoading} style={{padding:'10px',fontSize:11,justifyContent:'center',background:'linear-gradient(135deg,#7c3aed,#ec4899)'}}>๐Ÿ‘‘ {wLoading?'๋ถ„์„ ์ค‘...':'์กฐ์ง ์ง€๋Šฅ ๋ฆฌํฌํŠธ'}</button>}
</div>
{radar&&<div style={{background:'#fff',border:'1px solid #bfdbfe',borderRadius:10,padding:16,marginBottom:12,boxShadow:'0 2px 8px rgba(59,130,246,.08)'}}>
<div style={{fontSize:13,fontWeight:800,color:'#1e40af',marginBottom:8,display:'flex',alignItems:'center',gap:6}}>๐ŸŽฏ ๊ธฐํšŒ ๋ ˆ์ด๋”<span style={{fontSize:8,color:'#94a3b8',fontWeight:400,marginLeft:'auto'}}>AI ๋ถ„์„ ๊ฒฐ๊ณผ</span></div>
<div dangerouslySetInnerHTML={{__html:md2html(radar.text)}} style={{fontSize:11,lineHeight:1.7,color:'#334155'}}/></div>}
{conn&&<div style={{background:'#fff',border:'1px solid #c4b5fd',borderRadius:10,padding:16,marginBottom:12,boxShadow:'0 2px 8px rgba(139,92,246,.08)'}}>
<div style={{fontSize:13,fontWeight:800,color:'#6d28d9',marginBottom:8,display:'flex',alignItems:'center',gap:6}}>๐Ÿ”ฎ ์—ฐ๊ฒฐ ๋ฐœ๊ฒฌ<span style={{fontSize:8,color:'#94a3b8',fontWeight:400,marginLeft:'auto'}}>ํฌ๋กœ์Šค ๋ถ„์„</span></div>
<div dangerouslySetInnerHTML={{__html:md2html(conn.text)}} style={{fontSize:11,lineHeight:1.7,color:'#334155'}}/></div>}
{weekly&&<div style={{background:'linear-gradient(135deg,#faf5ff,#fdf2f8)',border:'1px solid #d8b4fe',borderRadius:10,padding:16,marginBottom:12,boxShadow:'0 2px 8px rgba(168,85,247,.1)'}}>
<div style={{fontSize:13,fontWeight:800,color:'#7c3aed',marginBottom:8,display:'flex',alignItems:'center',gap:6}}>๐Ÿ‘‘ ์ฃผ๊ฐ„ ์กฐ์ง ์ง€๋Šฅ ๋ฆฌํฌํŠธ<span style={{fontSize:8,color:'#a78bfa',fontWeight:400,marginLeft:'auto'}}>CEO ์ „์šฉ</span></div>
<div dangerouslySetInnerHTML={{__html:md2html(weekly.text)}} style={{fontSize:11,lineHeight:1.7,color:'#334155'}}/></div>}
{!radar&&!conn&&!weekly&&<div style={{textAlign:'center',color:'#94a3b8',fontSize:11,padding:40}}>
<div style={{fontSize:36,marginBottom:10}}>๐ŸŽฏ๐Ÿ”ฎ</div>
AI๊ฐ€ ๋น„๋“œ๋ž˜ํ”„ํŠธ์˜ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜์—ฌ<br/>์‚ฌ์—… ๊ธฐํšŒ์™€ ์ˆจ์€ ์—ฐ๊ฒฐ์„ ์ฐพ์•„๋ƒ…๋‹ˆ๋‹ค.<br/><br/>
<b>๐ŸŽฏ ๊ธฐํšŒ ๋ ˆ์ด๋”:</b> ๋‰ด์Šค/ํŠธ๋ Œ๋“œ โ†’ ๋น„๋“œ๋ž˜ํ”„ํŠธ ๊ธฐ์ˆ  ๋งค์นญ<br/>
<b>๐Ÿ”ฎ ์—ฐ๊ฒฐ ๋ฐœ๊ฒฌ:</b> ๊ณผ์ œ+๋งํฌ+ํ™œ๋™ ํฌ๋กœ์Šค ๋ถ„์„<br/>
{isCeo&&<><b>๐Ÿ‘‘ ์กฐ์ง ์ง€๋Šฅ ๋ฆฌํฌํŠธ:</b> ํ™œ๋™ ํŒจํ„ด/๋ณ‘๋ชฉ/๊ธฐํšŒ (CEO ์ „์šฉ)<br/></>}
</div>}
</div>}
/* === App === */
function App(){
const[tab,setTab]=useState('dashboard');const[tasks,setTasks]=useState([]);const[drops,setDrops]=useState([]);const[pockets,setPockets]=useState([]);const[reqs,setReqs]=useState([]);
const[loading,setLoading]=useState(true);const[saving,setSaving]=useState(false);const[clock,setClock]=useState('');const[dashSub,setDashSub]=useState('overview');
useEffect(()=>{const tick=()=>{setClock(new Date().toLocaleString('ko-KR',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}))};tick();const iv=setInterval(tick,30000);return()=>clearInterval(iv)},[]);
useEffect(()=>{(async()=>{setLoading(true);const[t,d,p,rq]=await Promise.all([api.getTasks(),api.getDrops(),api.getPockets(),api.getReqs()]);
setTasks((t||[]).map(x=>({...x,tab:x.tab||'gov'})));setDrops(d||[]);setPockets(p||[]);setReqs(rq||[]);setLoading(false)})()},[]);
const tabCn={};CAL_TABS.forEach(tid=>{tabCn[tid]=tasks.filter(t=>(t.tab||'gov')===tid).length});const total=CAL_TABS.reduce((s,tid)=>s+tabCn[tid],0);const showCal=CAL_TABS.includes(tab);
const me=getUser();const reqBadge=(reqs||[]).filter(r=>r.targets?.includes(me)&&!(r.responses||[]).some(rp=>rp.author===me)&&r.status==='๋Œ€๊ธฐ').length;
return<div>
<div className="hd"><div className="hd-b"><div className="hd-l">๐Ÿง™</div><div><div className="hd-t">ํ—ˆ๊ทธ์™€ํŠธ ๋งˆ๋ฒ•ํ•™๊ต</div><div className="hd-s">VIDRAFT</div></div></div>
<div className="hd-r"><span style={{fontSize:10,color:'rgba(255,255,255,.9)',fontWeight:600}}>๐Ÿ‘ค {me}</span><div className="hd-c">{clock}</div><span className="hd-o" onClick={()=>doLogout()}>๋กœ๊ทธ์•„์›ƒ</span></div></div>
<div className="tabs">{TABS.map(t=>{const cnt=t.id==='dashboard'?total:(tabCn[t.id]||0);const hasAlert=t.id==='dashboard'&&reqBadge>0;
return<div key={t.id} className={'tab'+(tab===t.id?' active':'')} onClick={()=>setTab(t.id)}>{t.icon} {t.label}{cnt>0&&<span className="bg">{cnt}</span>}{hasAlert&&<span className="alert-dot"/>}</div>})}</div>
{tab!=='dashboard'&&tab!=='wizard'&&<MgrBar tabId={tab}/>}
{tab==='dashboard'&&<div className="mgr"><div className="stabs">
{[{id:'overview',l:'๐Ÿ“Š ํ˜„ํ™ฉ'},{id:'drops',l:'๐Ÿ’ฌ ํ•œ์ค„'},{id:'collab',l:'๐Ÿค ํ˜‘์กฐ'+(reqBadge>0?' ('+reqBadge+')':'')},{id:'calendar',l:'๐Ÿ“… ์บ˜๋ฆฐ๋”'},{id:'insight',l:'๐ŸŽฏ AI ์ธ์‚ฌ์ดํŠธ'},{id:'report',l:'๐Ÿ“‹ ๋ฆฌํฌํŠธ'}].map(s=>
<div key={s.id} className={'stab'+(dashSub===s.id?' active':'')} onClick={()=>setDashSub(s.id)}>{s.l}</div>)}</div></div>}
{loading?<div className="main"><div className="ld2"><div className="spn2"/>๋กœ๋”ฉ...</div></div>:
tab==='dashboard'?(
dashSub==='overview'?<div className="main"><DashOverview tasks={tasks} drops={drops} reqs={reqs}/></div>:
dashSub==='drops'?<div className="main" style={{flexDirection:'column'}}><DailyDrop drops={drops} setDrops={setDrops}/></div>:
dashSub==='collab'?<div className="main" style={{flexDirection:'column'}}><WorkRequest reqs={reqs} setReqs={setReqs}/></div>:
dashSub==='insight'?<div className="main"><AiInsights/></div>:
dashSub==='report'?<div className="main"><WeeklyReport tasks={tasks} drops={drops}/></div>:
<CalTab key="dash" allTasks={tasks} tabId="dashboard" saving={saving} setSaving={setSaving} setAllTasks={setTasks}/>):
showCal&&tab!=='mgmt'?<CalTab key={tab} allTasks={tasks} tabId={tab} saving={saving} setSaving={setSaving} setAllTasks={setTasks}/>:
tab==='mgmt'?<MgmtTab allTasks={tasks} saving={saving} setSaving={setSaving} setAllTasks={setTasks}/>:
tab==='rnd'?<RndTab pockets={pockets} setPockets={setPockets}/>:
tab==='wizard'?<WizardTab tasks={tasks} drops={drops} pockets={pockets} reqs={reqs}/>:null}
</div>}
window.bootReactApp=function(){ReactDOM.createRoot(document.getElementById('vd-app')).render(<App/>)};
</script>
</body>
</html>