| <!DOCTYPE html> |
| <html lang="ko"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>μ½λ μΌ μλ§€ + μ€μ AI μμ΄μ νΈ</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap'); |
| * { margin:0; padding:0; box-sizing:border-box; } |
| :root { |
| --blue: #003087; --red: #e31837; --border: #cdd5e0; |
| --ai: #6c47ff; --ai-glow: rgba(108,71,255,0.25); |
| --hi: rgba(108,71,255,0.1); |
| } |
| body { font-family:'Noto Sans KR',sans-serif; background:#e8ecf0; min-height:100vh; } |
| |
| |
| .kh { background:var(--blue); } |
| .kh-top { display:flex; align-items:center; justify-content:space-between; padding:10px 20px; border-bottom:1px solid rgba(255,255,255,0.1); } |
| .logo { display:flex; align-items:center; gap:10px; } |
| .logo-box { width:40px;height:40px;background:white;border-radius:4px;display:flex;align-items:center;justify-content:center;font-weight:900;color:var(--blue);font-size:11px; } |
| .logo-txt { color:white;font-size:22px;font-weight:700;letter-spacing:2px; } |
| .gnb { display:flex; background:white; border-bottom:3px solid var(--red); } |
| .gnb-i { padding:13px 22px;font-size:14px;font-weight:500;color:#333;cursor:pointer;border-right:1px solid #eee; } |
| .gnb-i.on { background:var(--blue);color:white;font-weight:700; } |
| |
| |
| .wrap { max-width:1100px;margin:20px auto;padding:0 15px;display:grid;grid-template-columns:240px 1fr;gap:15px; } |
| .sidebar { background:white;border:1px solid var(--border);border-radius:4px;overflow:hidden; } |
| .sb-title { background:var(--blue);color:white;padding:12px 15px;font-size:14px;font-weight:700; } |
| .sb-menu { list-style:none; } |
| .sb-menu li { border-bottom:1px solid #f0f0f0;padding:10px 15px;font-size:13px;color:#444;cursor:pointer; } |
| .sb-menu li.on { color:var(--blue);font-weight:700;background:#f0f4ff; } |
| .sb-menu li::before { content:'βΆ';font-size:8px;color:#bbb;margin-right:6px; } |
| .sb-menu li.on::before { color:var(--blue); } |
| |
| |
| .book { display:flex;flex-direction:column;gap:12px; } |
| .ptitle { background:white;border:1px solid var(--border);padding:12px 18px;border-radius:4px;display:flex;align-items:center; } |
| .ptitle h2 { font-size:16px;font-weight:700;color:var(--blue); } |
| .fs { background:white;border:1px solid var(--border);border-radius:4px;overflow:hidden; } |
| .fsh { background:#f5f7fa;border-bottom:1px solid var(--border);padding:10px 18px;font-size:13px;font-weight:700;color:#333;display:flex;align-items:center;gap:6px; } |
| .fsh::before { content:'';width:3px;height:14px;background:var(--blue);border-radius:2px; } |
| .fsb { padding:15px 18px; } |
| .fr { display:grid;grid-template-columns:110px 1fr;align-items:center;margin-bottom:11px;gap:10px; } |
| .fr:last-child { margin-bottom:0; } |
| .fl { font-size:12px;color:#555;font-weight:500; } |
| .req { color:var(--red); } |
| |
| |
| .sw { display:flex;align-items:center;gap:8px; } |
| .sb { flex:1;border:1px solid var(--border);border-radius:3px;padding:8px 12px;font-size:14px;font-family:'Noto Sans KR',sans-serif;color:#333;background:white;min-height:36px;transition:all 0.3s;cursor:pointer; } |
| .sb.filled { background:#fffbf0;border-color:#f0a500;font-weight:500; } |
| .sb.ai-on { border-color:var(--ai)!important;box-shadow:0 0 0 3px var(--ai-glow)!important;background:var(--hi)!important; } |
| .swap { width:28px;height:28px;border:1px solid var(--border);border-radius:50%;background:white;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0; } |
| |
| .dw { display:flex;gap:8px; } |
| .db { border:1px solid var(--border);border-radius:3px;padding:8px 12px;font-size:13px;font-family:'Noto Sans KR',sans-serif;color:#333;background:white;min-width:130px;min-height:36px;transition:all 0.3s; } |
| .db.filled { background:#fffbf0;border-color:#f0a500; } |
| .db.ai-on { border-color:var(--ai)!important;box-shadow:0 0 0 3px var(--ai-glow)!important;background:var(--hi)!important; } |
| |
| .chips { display:flex;gap:6px;flex-wrap:wrap; } |
| .chip { padding:5px 12px;border:1px solid var(--border);border-radius:15px;font-size:12px;cursor:pointer;transition:all 0.3s;color:#555; } |
| .chip.sel { background:var(--blue);border-color:var(--blue);color:white;font-weight:700; } |
| .chip.sel-red { background:var(--red);border-color:var(--red);color:white;font-weight:700; } |
| .chip.sel-green { background:#16a34a;border-color:#16a34a;color:white;font-weight:700; } |
| .chip.sel-orange { background:#ea580c;border-color:#ea580c;color:white;font-weight:700; } |
| .chip.ai-flash { animation:flash 0.4s ease; } |
| @keyframes flash { 0%{transform:scale(1)} 50%{transform:scale(1.12);box-shadow:0 0 14px var(--ai-glow)} 100%{transform:scale(1)} } |
| |
| .pw { display:flex;align-items:center;gap:8px; } |
| .cb { width:26px;height:26px;border:1px solid var(--border);border-radius:3px;background:#f5f5f5;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;font-weight:700; } |
| .cn { font-size:16px;font-weight:700;min-width:22px;text-align:center;transition:all 0.3s; } |
| .cn.ai-up { color:var(--ai);transform:scale(1.4); } |
| |
| .search-btn-wrap { text-align:center;padding:16px; } |
| .search-btn { background:var(--blue);color:white;border:none;padding:13px 60px;font-size:15px;font-family:'Noto Sans KR',sans-serif;font-weight:700;border-radius:3px;cursor:pointer;letter-spacing:1px;transition:all 0.3s; } |
| .search-btn:hover { background:#002070; } |
| .search-btn.ai-ready { background:var(--ai)!important;box-shadow:0 0 20px var(--ai-glow);animation:btn-pulse 1s infinite alternate; } |
| @keyframes btn-pulse { from{box-shadow:0 0 10px var(--ai-glow)} to{box-shadow:0 0 30px rgba(108,71,255,0.5)} } |
| |
| |
| #ai-panel { |
| position:fixed; bottom:20px; right:20px; width:380px; |
| background:white; border-radius:18px; |
| box-shadow:0 8px 40px rgba(0,0,0,0.18), 0 0 0 1px rgba(108,71,255,0.15); |
| z-index:9999; overflow:hidden; |
| transform:translateY(30px); opacity:0; |
| transition:all 0.4s cubic-bezier(0.16,1,0.3,1); |
| display:none; |
| } |
| #ai-panel.show { display:block; transform:translateY(0); opacity:1; } |
| |
| .ap-header { |
| background:linear-gradient(135deg, #6c47ff, #9b59b6); |
| padding:14px 18px; display:flex; align-items:center; gap:10px; |
| } |
| .ap-icon { width:34px;height:34px;background:white;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:18px; } |
| .ap-title { color:white;font-weight:700;font-size:14px; } |
| .ap-badge { margin-left:auto;background:rgba(255,255,255,0.2);color:white;font-size:10px;padding:3px 10px;border-radius:10px; } |
| .vidraft-tag { display:none;font-size:9px;color:rgba(255,255,255,0.45);text-align:center;padding:3px 0 5px;letter-spacing:0.5px; } |
| .vidraft-tag.show { display:block; } |
| |
| |
| .ap-input-area { padding:14px 16px; border-bottom:1px solid #f0f0f0; } |
| .ap-input-label { font-size:10px;color:#888;letter-spacing:1px;margin-bottom:6px; } |
| .ap-input-wrap { display:flex;gap:8px;align-items:flex-end; } |
| #user-command { |
| flex:1; border:1px solid #e0daff; border-radius:10px; padding:10px 14px; |
| font-size:13px; font-family:'Noto Sans KR',sans-serif; color:#333; |
| resize:none; min-height:60px; outline:none; |
| transition:border-color 0.2s; |
| } |
| #user-command:focus { border-color:var(--ai); box-shadow:0 0 0 3px var(--ai-glow); } |
| .run-btn { |
| background:var(--ai); color:white; border:none; |
| width:42px; height:42px; border-radius:10px; |
| font-size:18px; cursor:pointer; flex-shrink:0; |
| transition:all 0.2s; |
| } |
| .run-btn:hover { background:#5535e0; transform:scale(1.05); } |
| .run-btn:disabled { opacity:0.4; cursor:not-allowed; transform:none; } |
| |
| |
| #ap-thinking { padding:12px 16px; border-bottom:1px solid #f0f0f0; display:none; } |
| .think-label { font-size:10px;color:var(--ai);font-weight:700;margin-bottom:6px;letter-spacing:1px; } |
| .think-text { font-size:12px;color:#555;line-height:1.7;max-height:80px;overflow-y:auto; } |
| |
| |
| #ap-log { padding:12px 16px; max-height:180px; overflow-y:auto; display:none; } |
| .log-item { display:flex;gap:8px;padding:5px 0;font-size:12px;color:#555;border-bottom:1px dashed #f0f0f0;opacity:0;transform:translateX(-8px);transition:all 0.35s; } |
| .log-item.vis { opacity:1;transform:translateX(0); } |
| |
| |
| .ap-progress { padding:10px 16px 14px; border-top:1px solid #f0f0f0; } |
| .pb-bg { background:#f0f0f0;border-radius:4px;height:6px;overflow:hidden; } |
| .pb-fill { height:100%;background:linear-gradient(90deg,var(--ai),#a78bfa);border-radius:4px;width:0%;transition:width 0.5s ease; } |
| .pb-label { display:flex;justify-content:space-between;font-size:10px;color:#999;margin-top:5px; } |
| |
| |
| .ap-presets { padding:10px 16px; border-bottom:1px solid #f0f0f0; } |
| .preset-label { font-size:10px;color:#888;margin-bottom:6px; } |
| .preset-btns { display:flex;flex-wrap:wrap;gap:5px; } |
| .preset-btn { |
| font-size:11px;padding:4px 10px;border:1px solid #e0daff;border-radius:8px; |
| background:#f8f6ff;color:#6c47ff;cursor:pointer;font-family:'Noto Sans KR',sans-serif; |
| transition:all 0.2s; |
| } |
| .preset-btn:hover { background:#ede8ff; } |
| |
| |
| #ai-toggle { |
| position:fixed; bottom:20px; left:20px; z-index:9999; |
| background:linear-gradient(135deg,#6c47ff,#9b59b6); |
| color:white; border:none; padding:13px 20px; border-radius:50px; |
| font-size:14px; font-weight:700; font-family:'Noto Sans KR',sans-serif; |
| cursor:pointer; box-shadow:0 4px 20px var(--ai-glow); |
| display:flex; align-items:center; gap:8px; |
| animation:float 3s ease-in-out infinite; |
| } |
| @keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-5px)} } |
| .dot { width:8px;height:8px;background:#4ade80;border-radius:50%;animation:blink 1.2s infinite; } |
| @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} } |
| |
| |
| #success { display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:99999;align-items:center;justify-content:center;backdrop-filter:blur(4px); } |
| #success.show { display:flex; } |
| .sc { background:white;border-radius:20px;padding:36px;text-align:center;max-width:400px;width:90%;animation:popin 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden; } |
| @keyframes popin { from{transform:scale(0.7);opacity:0} to{transform:scale(1);opacity:1} } |
| .sc-bar { position:absolute;top:0;left:0;right:0;height:5px;background:repeating-linear-gradient(90deg,var(--red) 0 20px,var(--blue) 20px 40px,#f0a500 40px 60px,#22c55e 60px 80px); } |
| .sc-emoji { font-size:56px;display:block;margin-bottom:10px; } |
| .sc-title { font-size:22px;font-weight:900;color:#1a1a2e;margin-bottom:6px; } |
| .sc-sub { font-size:13px;color:#666;line-height:1.7; } |
| .sc-ticket { background:#f8f6ff;border-radius:12px;padding:15px;margin:18px 0;text-align:left;border:2px dashed #d4cafe; } |
| .tr { display:flex;justify-content:space-between;font-size:13px;padding:3px 0; } |
| .tk { color:#888; } .tv { font-weight:700;color:#333; } |
| .t-route { font-size:20px;font-weight:900;color:var(--blue);text-align:center;padding:8px 0;border-bottom:1px dashed #e0daff;margin-bottom:8px; } |
| .sc-speed { font-size:11px;color:#22c55e;font-weight:700;margin-top:6px; } |
| .sc-close { background:var(--ai);color:white;border:none;padding:11px 28px;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:'Noto Sans KR',sans-serif;margin-top:10px; } |
| .restart { position:fixed;top:10px;right:15px;background:white;border:1px solid var(--border);padding:7px 14px;border-radius:4px;font-size:12px;font-family:'Noto Sans KR',sans-serif;cursor:pointer;z-index:9999;color:#555; } |
| |
| |
| #countdown-wrap { display:none;position:fixed;inset:0;background:rgba(0,0,20,0.88);z-index:99990;align-items:center;justify-content:center;flex-direction:column;backdrop-filter:blur(6px); } |
| #countdown-wrap.show { display:flex; } |
| .cd-label { color:rgba(255,255,255,0.6);font-size:13px;letter-spacing:3px;margin-bottom:16px; } |
| .cd-num { font-size:110px;font-weight:900;color:white;line-height:1;transition:all 0.15s; } |
| .cd-num.flash { color:var(--ai);transform:scale(1.08); } |
| .cd-sub { color:rgba(255,255,255,0.4);margin-top:14px;font-size:12px; } |
| |
| |
| .typing-cursor { display:inline-block;width:2px;height:14px;background:var(--ai);margin-left:2px;animation:cursor-blink 0.6s infinite; vertical-align:middle; } |
| @keyframes cursor-blink { 0%,100%{opacity:1} 50%{opacity:0} } |
| |
| |
| .impact-card { background:linear-gradient(135deg,#1a1a2e,#16213e);border-radius:12px;padding:14px;margin:14px 0;display:flex;gap:0;overflow:hidden; } |
| .impact-item { flex:1;text-align:center;padding:8px 6px;border-right:1px solid rgba(255,255,255,0.08); } |
| .impact-item:last-child { border:none; } |
| .impact-val { font-size:22px;font-weight:900;color:#a78bfa;line-height:1; } |
| .impact-val.green { color:#4ade80; } |
| .impact-val.red { color:#f87171; } |
| .impact-lbl { font-size:9px;color:rgba(255,255,255,0.45);margin-top:4px;letter-spacing:0.5px; } |
| |
| |
| .agent-flow { padding:10px 14px 8px;border-bottom:1px solid #f0f0f0;display:none; } |
| .agent-flow.show { display:block; } |
| .af-label { font-size:9px;color:#aaa;letter-spacing:0.5px;margin-bottom:7px; } |
| .af-nodes { display:flex;align-items:center;gap:0; } |
| .af-node { flex:1;display:flex;flex-direction:column;align-items:center;position:relative; } |
| .af-node::after { content:'β';position:absolute;right:-6px;top:10px;font-size:10px;color:#ddd;z-index:1; } |
| .af-node:last-child::after { display:none; } |
| .af-dot { width:28px;height:28px;border-radius:50%;background:#f0f0f0;border:2px solid #e0e0e0;display:flex;align-items:center;justify-content:center;font-size:12px;transition:all 0.4s;position:relative; } |
| .af-dot.active { background:var(--ai);border-color:var(--ai);box-shadow:0 0 12px var(--ai-glow);animation:node-pulse 0.8s infinite alternate; } |
| .af-dot.done { background:#22c55e;border-color:#22c55e;box-shadow:none;animation:none; } |
| .af-dot.waiting { background:#f59e0b;border-color:#f59e0b;animation:node-wait 1s infinite alternate; } |
| @keyframes node-pulse { from{box-shadow:0 0 6px var(--ai-glow)} to{box-shadow:0 0 18px rgba(108,71,255,0.6)} } |
| @keyframes node-wait { from{opacity:0.6} to{opacity:1} } |
| .af-name { font-size:8px;color:#999;margin-top:4px;text-align:center;line-height:1.2; } |
| .af-node.active .af-name { color:var(--ai);font-weight:700; } |
| .af-node.done .af-name { color:#22c55e; } |
| .af-connect { width:12px;height:2px;background:#e0e0e0;flex-shrink:0;margin-top:-14px; } |
| .af-connect.lit { background:linear-gradient(90deg,#22c55e,var(--ai));animation:flow-anim 0.6s linear; } |
| @keyframes flow-anim { from{opacity:0;transform:scaleX(0)} to{opacity:1;transform:scaleX(1)} } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div class="kh"> |
| <div class="kh-top"> |
| <div class="logo"> |
| <div class="logo-box">μ½λ μΌ</div> |
| <div class="logo-txt">KORAIL</div> |
| </div> |
| </div> |
| <div class="gnb"> |
| <div class="gnb-i on">μΉμ°¨κΆ μλ§€</div> |
| <div class="gnb-i">κΈ°μ°¨μ¬ν</div> |
| <div class="gnb-i">ν μΈ/μ΄λ²€νΈ</div> |
| <div class="gnb-i">κ³ κ°μλΉμ€</div> |
| <div class="gnb-i">μ½λ μΌλ©€λ²μ</div> |
| </div> |
| </div> |
|
|
| <div class="wrap"> |
| <div class="sidebar"> |
| <div class="sb-title">μΉμ°¨κΆ μλ§€</div> |
| <ul class="sb-menu"> |
| <li class="on">μΌλ° μΉμ°¨κΆ</li> |
| <li>KTX νΉμ€</li> |
| <li>μμ μ μΉμ°¨κΆ</li> |
| <li>μ κΈ°κΆ κ΅¬λ§€</li> |
| <li>λ¨μ²΄ μλ§€</li> |
| <li>μλ§€νμΈ/μ·¨μ</li> |
| </ul> |
| </div> |
|
|
| <div class="book"> |
| <div class="ptitle"><h2>π μΌλ° μΉμ°¨κΆ μλ§€</h2></div> |
|
|
| <div class="fs"> |
| <div class="fsh">μΆλ°/λμ°© μ 보</div> |
| <div class="fsb"> |
| <div class="fr"> |
| <div class="fl">κ΅¬κ° <span class="req">*</span></div> |
| <div class="sw"> |
| <div class="sb" id="dep" data-field="μΆλ°μ">μΆλ°μ μ ν</div> |
| <div class="swap">β</div> |
| <div class="sb" id="arr" data-field="λμ°©μ">λμ°©μ μ ν</div> |
| </div> |
| </div> |
| <div class="fr"> |
| <div class="fl">μΆλ°μΌ <span class="req">*</span></div> |
| <div class="dw"> |
| <div class="db" id="dep-date" data-field="μΆλ°μΌ">μΆλ°μΌ μ ν</div> |
| <div class="db" id="dep-time" data-field="μΆλ°μκ°">μΆλ°μκ° μ ν</div> |
| </div> |
| </div> |
| <div class="fr"> |
| <div class="fl">μκ°λ</div> |
| <div class="chips" id="time-chips"> |
| <div class="chip" data-val="μ 체">μ 체</div> |
| <div class="chip" data-val="00~06μ">00~06μ</div> |
| <div class="chip" data-val="06~09μ">06~09μ</div> |
| <div class="chip" data-val="09~12μ">09~12μ</div> |
| <div class="chip" data-val="12~15μ">12~15μ</div> |
| <div class="chip" data-val="15~18μ">15~18μ</div> |
| <div class="chip" data-val="18μμ΄ν">18μμ΄ν</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="fs"> |
| <div class="fsh">μ΄μ°¨ μ’
λ₯ λ° μ’μ</div> |
| <div class="fsb"> |
| <div class="fr"> |
| <div class="fl">μ΄μ°¨ μ’
λ₯</div> |
| <div class="chips" id="train-chips"> |
| <div class="chip" data-val="KTX">KTX</div> |
| <div class="chip" data-val="SRT">SRT</div> |
| <div class="chip" data-val="ITX-μλ§μ">ITX-μλ§μ</div> |
| <div class="chip" data-val="무κΆννΈ">무κΆννΈ</div> |
| </div> |
| </div> |
| <div class="fr"> |
| <div class="fl">μ’μ λ±κΈ</div> |
| <div class="chips" id="grade-chips"> |
| <div class="chip" data-val="νΉμ€">νΉμ€</div> |
| <div class="chip" data-val="μΌλ°μ€">μΌλ°μ€</div> |
| <div class="chip" data-val="μ 체">μ 체</div> |
| </div> |
| </div> |
| <div class="fr"> |
| <div class="fl">μ’μ μμΉ</div> |
| <div class="chips" id="pos-chips"> |
| <div class="chip" data-val="μ°½μΈ‘">μ°½μΈ‘</div> |
| <div class="chip" data-val="λ΄μΈ‘">λ΄μΈ‘</div> |
| <div class="chip" data-val="μκ΄μμ">μκ΄μμ</div> |
| </div> |
| </div> |
| <div class="fr"> |
| <div class="fl">μ§νλ°©ν₯</div> |
| <div class="chips" id="dir-chips"> |
| <div class="chip" data-val="μλ°©ν₯">μλ°©ν₯</div> |
| <div class="chip" data-val="μλ°©ν₯">μλ°©ν₯</div> |
| <div class="chip" data-val="μκ΄μμ">μκ΄μμ</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="fs"> |
| <div class="fsh">μΈμ λ° ν μΈ μ 보</div> |
| <div class="fsb"> |
| <div class="fr"> |
| <div class="fl">μΈμ <span class="req">*</span></div> |
| <div class="pw"> |
| <span style="font-size:12px;color:#888">μ±μΈ</span> |
| <div class="cb" onclick="changeCount('adult',-1)">β</div> |
| <div class="cn" id="adult">1</div> |
| <div class="cb" onclick="changeCount('adult',1)">+</div> |
| <span style="font-size:12px;color:#888;margin-left:10px">μ΄λ¦°μ΄</span> |
| <div class="cb" onclick="changeCount('child',-1)">β</div> |
| <div class="cn" id="child">0</div> |
| <div class="cb" onclick="changeCount('child',1)">+</div> |
| </div> |
| </div> |
| <div class="fr"> |
| <div class="fl">ν μΈ μ’
λ₯</div> |
| <div class="chips" id="disc-chips"> |
| <div class="chip" data-val="μ½λ μΌλ©€λ²μ">μ½λ μΌλ©€λ²μ</div> |
| <div class="chip" data-val="μ νμΉ΄λ">μ νμΉ΄λ</div> |
| <div class="chip" data-val="KBμΉ΄λ">KBμΉ΄λ</div> |
| <div class="chip" data-val="λ‘―λ°μΉ΄λ">λ‘―λ°μΉ΄λ</div> |
| <div class="chip" data-val="μ²μλ
λλ¦Ό">μ²μλ
λλ¦Ό</div> |
| <div class="chip" data-val="λ€μλ
">λ€μλ
</div> |
| </div> |
| </div> |
| <div class="fr"> |
| <div class="fl">λμ λ
Έμ </div> |
| <div class="chips" id="alt-chips"> |
| <div class="chip" data-val="SRT λμκ²μ">SRT λμκ²μ</div> |
| <div class="chip" data-val="κ²½μ ν¬ν¨">κ²½μ ν¬ν¨</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="fs"> |
| <div class="search-btn-wrap"> |
| <button class="search-btn" id="search-btn">μ΄μ°¨ μ‘°ν</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="countdown-wrap"> |
| <div class="cd-label">π€ MARL μμ¨ μμ΄μ νΈ κ°λ</div> |
| <div class="cd-num" id="cd-num">3</div> |
| <div class="cd-sub">μμ΄μ νΈ νλ ₯ μμ€ν
μ΄κΈ°ν μ€...</div> |
| </div> |
|
|
| |
| <button id="ai-toggle" onclick="startCountdown()"> |
| <div class="dot"></div>π€ AI μλμλ§€ |
| </button> |
| <button class="restart" onclick="location.reload()">βΊ μ΄κΈ°ν</button> |
|
|
| |
| <div id="ai-panel"> |
| <div class="ap-header"> |
| <div class="ap-icon">π€</div> |
| <div> |
| <div class="ap-title">MARLλ―Έλ€μ¨μ΄ X λ΄: μμ¨ μ§λ₯ μν</div> |
| <div class="ap-sub">μμ¨ λΆμ β μλ μ
λ ₯ μ€ν</div> |
| </div> |
| <div class="ap-badge" id="ap-status">λκΈ°μ€</div> |
| </div> |
| <div class="vidraft-tag" id="vidraft-tag">β‘ κ°λ°: VIDRAFT.net</div> |
|
|
| |
| <div class="agent-flow" id="agent-flow"> |
| <div class="af-label">β μμ¨ μμ΄μ νΈ νλ ₯ νν©</div> |
| <div class="af-nodes"> |
| <div class="af-node" id="af-observer"> |
| <div class="af-dot" id="dot-observer">π</div> |
| <div class="af-name">κ΄μ°°μ<br>Observer</div> |
| </div> |
| <div class="af-node" id="af-planner"> |
| <div class="af-dot" id="dot-planner">π§ </div> |
| <div class="af-name">κ³νμ<br>Planner</div> |
| </div> |
| <div class="af-node" id="af-orchestrator"> |
| <div class="af-dot" id="dot-orchestrator">π―</div> |
| <div class="af-name">μ‘°μ¨μ<br>Orch.</div> |
| </div> |
| <div class="af-node" id="af-executor"> |
| <div class="af-dot" id="dot-executor">β‘</div> |
| <div class="af-name">μ€νμ<br>Executor</div> |
| </div> |
| <div class="af-node" id="af-validator"> |
| <div class="af-dot" id="dot-validator">β
</div> |
| <div class="af-name">κ²μ¦μ<br>Validator</div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="ap-presets"> |
| <div class="preset-label">β‘ λΉ λ₯Έ λͺ
λ Ή μμ</div> |
| <div class="preset-btns"> |
| <button class="preset-btn" onclick="setCmd('μΆμ 9μ12μΌ μμΈβλΆμ°, μ€μ 6~9μ, KTX, μ°½μΈ‘, μλ°©ν₯, μ νμΉ΄λ, μ±μΈ2λͺ
, SRTλ κ°μ΄ λ΄μ€')">μΆμ λΆμ°ν</button> |
| <button class="preset-btn" onclick="setCmd('μ€ 1μ28μΌ μμΈβκ΄μ£Ό, μ€ν 3~6μ, KTX, μΌλ°μ€, KBμΉ΄λ, μ±μΈ1λͺ
μ΄λ¦°μ΄1λͺ
')">μ€ κ΄μ£Όν</button> |
| <button class="preset-btn" onclick="setCmd('3μ15μΌ μμΈβκ°λ¦, KTX, μ°½μΈ‘ μλ°©ν₯, μ무 μκ°λ, μ½λ μΌλ©€λ²μ')">κ°λ¦ μ¬ν</button> |
| </div> |
| </div> |
|
|
| <div class="ap-input-area"> |
| <div class="ap-input-label">ποΈ μμ°μ΄λ‘ μλ§€ 쑰건μ λ§νμΈμ</div> |
| <div class="ap-input-wrap"> |
| <textarea id="user-command" placeholder="μ) μΆμ 9μ12μΌ μμΈβλΆμ°, μ€μ 6~9μ, KTX, μ°½μΈ‘, μ νμΉ΄λ ν μΈ, μ±μΈ 2λͺ
"></textarea> |
| <button class="run-btn" id="run-btn" onclick="runAgent()">βΆ</button> |
| </div> |
| </div> |
|
|
| <div id="ap-thinking"> |
| <div class="think-label">π§ μμ¨ μ§λ₯ λΆμμ€</div> |
| <div class="think-text" id="think-text">MARLλ΄μ΄ νμ΄μ§ ꡬ쑰λ₯Ό νμ
νκ³ μ‘μ
κ³νμ μ립νλ μ€...</div> |
| </div> |
|
|
| <div id="ap-log"></div> |
|
|
| <div class="ap-progress"> |
| <div class="pb-bg"><div class="pb-fill" id="pb-fill"></div></div> |
| <div class="pb-label"> |
| <span id="pb-label">λͺ
λ Ήμ μ
λ ₯νκ³ βΆ λ₯Ό λλ₯΄μΈμ</span> |
| <span id="pb-pct">0%</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="success"> |
| <div class="sc"> |
| <div class="sc-bar"></div> |
| <span class="sc-emoji">π</span> |
| <div class="sc-title">μλ§€ μλ£!</div> |
| <div class="sc-sub">MARL μμ¨ μμ΄μ νΈκ° <strong id="elapsed-time">?</strong>μ΄ λ§μ<br>μ’μμ μλμΌλ‘ μ μ νμ΅λλ€</div> |
| |
| <div class="impact-card"> |
| <div class="impact-item"> |
| <div class="impact-val red" id="manual-time">8λΆ</div> |
| <div class="impact-lbl">μλ μλ§€</div> |
| </div> |
| <div class="impact-item"> |
| <div class="impact-val green" id="ai-time">?μ΄</div> |
| <div class="impact-lbl">AI μ²λ¦¬</div> |
| </div> |
| <div class="impact-item"> |
| <div class="impact-val" id="speed-mult">?λ°°</div> |
| <div class="impact-lbl">μλ ν₯μ</div> |
| </div> |
| </div> |
| <div class="sc-ticket" id="ticket-content"></div> |
| <button class="sc-close" onclick="document.getElementById('success').classList.remove('show')">νμΈ</button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let panelOpen = false; |
| let startTime = 0; |
| |
| |
| function startCountdown() { |
| if (panelOpen) { togglePanel(); return; } |
| const wrap = document.getElementById('countdown-wrap'); |
| const numEl = document.getElementById('cd-num'); |
| wrap.classList.add('show'); |
| let n = 3; |
| numEl.textContent = n; |
| const iv = setInterval(() => { |
| numEl.classList.add('flash'); |
| setTimeout(() => numEl.classList.remove('flash'), 150); |
| n--; |
| if (n <= 0) { |
| clearInterval(iv); |
| wrap.classList.remove('show'); |
| togglePanel(); |
| } else { |
| numEl.textContent = n; |
| } |
| }, 800); |
| } |
| |
| |
| function togglePanel() { |
| const panel = document.getElementById('ai-panel'); |
| panelOpen = !panelOpen; |
| if (panelOpen) { |
| panel.style.display = 'block'; |
| setTimeout(() => panel.classList.add('show'), 30); |
| } else { |
| panel.classList.remove('show'); |
| setTimeout(() => panel.style.display = 'none', 400); |
| } |
| } |
| |
| function setCmd(txt) { document.getElementById('user-command').value = txt; } |
| |
| function changeCount(type, delta) { |
| const el = document.getElementById(type); |
| let val = parseInt(el.textContent) + delta; |
| if (val < 0) val = 0; |
| el.textContent = val; |
| } |
| |
| |
| function getPageSnapshot() { |
| const fields = {}; |
| document.querySelectorAll('[data-field]').forEach(el => { |
| fields[el.dataset.field] = el.textContent.trim(); |
| }); |
| const chips = {}; |
| document.querySelectorAll('[id$="-chips"]').forEach(group => { |
| const name = group.id.replace('-chips',''); |
| chips[name] = Array.from(group.querySelectorAll('.chip')).map(c => ({ |
| value: c.dataset.val, |
| selected: c.classList.contains('sel') |
| })); |
| }); |
| return { fields, chips, |
| counts: { adult: parseInt(document.getElementById('adult').textContent), |
| child: parseInt(document.getElementById('child').textContent) } |
| }; |
| } |
| |
| |
| const GROQ_API_KEY = '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'; |
| |
| async function callClaude(command, snapshot) { |
| const systemPrompt = `λΉμ μ μΉνμ΄μ§ DOMμ λΆζνμ¬ μ¬μ©μμ μμ°μ΄ λͺ
λ Ήμ λ°λΌ νΌ νλλ₯Ό μλμΌλ‘ μ±μμ£Όλ AI μμ΄μ νΈμ
λλ€. |
| |
| νμ΄μ§ ꡬ쑰: |
| - fields: ν
μ€νΈ μ
λ ₯ νλλ€ (μΆλ°μ, λμ°©μ, μΆλ°μΌ, μΆλ°μκ°) |
| - chips: μ ν λ²νΌ κ·Έλ£Ή (time=μκ°λ, train=μ΄μ°¨μ’
λ₯, grade=μ’μλ±κΈ, pos=μ’μμμΉ, dir=μ§νλ°©ν₯, disc=ν μΈμ’
λ₯, alt=λμλ
Έμ ) |
| - counts: μ±μΈ/μ΄λ¦°μ΄ μ |
| |
| νμ¬ νμ΄μ§ μν: ${JSON.stringify(snapshot, null, 2)} |
| |
| μ¬μ©μ λͺ
λ Ήμ λΆζνμ¬ λ€μ JSON νμμΌλ‘λ§ μλ΅νμΈμ. λ€λ₯Έ ν
μ€νΈ μμ΄ JSONλ§ μΆλ ₯: |
| |
| { |
| "thinking": "λͺ
λ Ή λΆζ λ° μ€ν κ³ν (νκ΅μ΄, 2-3λ¬Έμ₯)", |
| "actions": [ |
| {"type": "fill", "field": "μΆλ°μ", "value": "μμΈ", "emoji": "π", "desc": "μΆλ°μμ μμΈλ‘ μ€μ "}, |
| {"type": "fill", "field": "λμ°©μ", "value": "λΆμ°", "emoji": "π", "desc": "λμ°©μμ λΆμ°μΌλ‘ μ€μ "}, |
| {"type": "fill", "field": "μΆλ°μΌ", "value": "2025λ
09μ 12μΌ (κΈ)", "emoji": "π
", "desc": "μΆλ°μΌ μ€μ "}, |
| {"type": "chip", "group": "time", "value": "06~09μ", "emoji": "β°", "desc": "μ€μ 6~9μ μ ν"}, |
| {"type": "chip", "group": "train", "value": "KTX", "emoji": "π", "desc": "KTX μ ν"}, |
| {"type": "count", "field": "adult", "value": 2, "emoji": "π₯", "desc": "μ±μΈ 2λͺ
μ€μ "} |
| ], |
| "result": { |
| "route": "μμΈ β λΆμ°", |
| "date": "2025.09.12 (κΈ)", |
| "time": "07:15 β 09:45", |
| "train": "KTX 121", |
| "seat": "5νΈμ°¨ 12A (μ°½μΈ‘Β·μλ°©ν₯)", |
| "passengers": "μ±μΈ 2λͺ
", |
| "discount": "μ νμΉ΄λ 10% μ μ©", |
| "price": "117,800μ" |
| } |
| } |
| |
| chip group μ΄λ¦: time(μκ°λ), train(μ΄μ°¨μ’
λ₯), grade(μ’μλ±κΈ), pos(μ’μμμΉ), dir(μ§νλ°©ν₯), disc(ν μΈμ’
λ₯), alt(λμλ
Έμ ) |
| chip valueλ λ°λμ νμ΄μ§μ μλ κ°κ³Ό μ νν μΌμΉν΄μΌ ν©λλ€.`; |
| |
| const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${GROQ_API_KEY}` }, |
| body: JSON.stringify({ |
| model: 'openai/gpt-oss-120b', |
| messages: [ |
| { role: 'system', content: systemPrompt }, |
| { role: 'user', content: command } |
| ], |
| temperature: 1, max_tokens: 8192, top_p: 1, stream: false, stop: null |
| }) |
| }); |
| if (!response.ok) { const err = await response.text(); throw new Error(`Groq ${response.status}: ${err}`); } |
| const data = await response.json(); |
| const raw = data.choices[0].message.content.trim(); |
| return JSON.parse(raw.replace(/```json|```/g, '').trim()); |
| } |
| |
| |
| const AGENT_NODES = ['observer','planner','orchestrator','executor','validator']; |
| |
| function resetAgentFlow() { |
| AGENT_NODES.forEach(n => { |
| const dot = document.getElementById('dot-'+n); |
| const node = document.getElementById('af-'+n); |
| dot.classList.remove('active','done','waiting'); |
| node.classList.remove('active','done'); |
| }); |
| } |
| |
| function activateAgent(index) { |
| AGENT_NODES.forEach((n, i) => { |
| const dot = document.getElementById('dot-'+n); |
| const node = document.getElementById('af-'+n); |
| dot.classList.remove('active','done','waiting'); |
| node.classList.remove('active','done'); |
| if (i < index) { dot.classList.add('done'); node.classList.add('done'); } |
| else if (i === index) { dot.classList.add('active'); node.classList.add('active'); } |
| }); |
| } |
| |
| function completeAllAgents() { |
| AGENT_NODES.forEach(n => { |
| document.getElementById('dot-'+n).classList.remove('active','waiting'); |
| document.getElementById('dot-'+n).classList.add('done'); |
| document.getElementById('af-'+n).classList.add('done'); |
| }); |
| } |
| |
| |
| async function typeInto(el, text, speed = 55) { |
| el.textContent = ''; |
| const cursor = document.createElement('span'); |
| cursor.className = 'typing-cursor'; |
| el.appendChild(cursor); |
| for (const ch of text) { |
| cursor.before(ch); |
| await sleep(speed + Math.random() * 30); |
| } |
| cursor.remove(); |
| } |
| |
| |
| async function executeAction(action, actionIndex, total) { |
| |
| const agentPhase = Math.floor((actionIndex / total) * 3) + 1; |
| activateAgent(Math.min(agentPhase, 3)); |
| await sleep(350); |
| |
| if (action.type === 'fill') { |
| const el = document.querySelector(`[data-field="${action.field}"]`); |
| if (!el) return; |
| el.classList.add('ai-on'); |
| await sleep(200); |
| |
| await typeInto(el, action.value, 45); |
| el.classList.remove('ai-on'); |
| el.classList.add('filled'); |
| } |
| else if (action.type === 'chip') { |
| const group = document.getElementById(action.group + '-chips'); |
| if (!group) return; |
| const chip = Array.from(group.querySelectorAll('.chip')).find(c => c.dataset.val === action.value); |
| if (!chip) return; |
| chip.classList.add('ai-flash'); |
| await sleep(300); |
| group.querySelectorAll('.chip').forEach(c => c.classList.remove('sel','sel-red','sel-green','sel-orange')); |
| chip.classList.add('sel'); |
| } |
| else if (action.type === 'count') { |
| const el = document.getElementById(action.field); |
| if (!el) return; |
| el.classList.add('ai-up'); |
| await sleep(300); |
| el.textContent = action.value; |
| el.classList.remove('ai-up'); |
| } |
| } |
| |
| |
| async function runAgent() { |
| const command = document.getElementById('user-command').value.trim(); |
| if (!command) return; |
| |
| const runBtn = document.getElementById('run-btn'); |
| runBtn.disabled = true; |
| startTime = Date.now(); |
| |
| |
| document.getElementById('ap-log').innerHTML = ''; |
| document.getElementById('ap-log').style.display = 'none'; |
| document.getElementById('pb-fill').style.width = '0%'; |
| setStatus('λΆζμ€', '#f59e0b'); |
| document.getElementById('vidraft-tag').classList.add('show'); |
| |
| |
| const flowEl = document.getElementById('agent-flow'); |
| flowEl.classList.add('show'); |
| resetAgentFlow(); |
| activateAgent(0); |
| |
| |
| const thinkDiv = document.getElementById('ap-thinking'); |
| const thinkText = document.getElementById('think-text'); |
| thinkDiv.style.display = 'block'; |
| thinkText.textContent = 'MARLλ΄μ΄ νμ΄μ§ ꡬ쑰λ₯Ό νμ
νκ³ μ‘μ
κ³νμ μ립νλ μ€...'; |
| setProgress(10, 'μμ¨ μ§λ₯ λΆζμ€...'); |
| |
| let plan; |
| try { |
| activateAgent(1); |
| const snapshot = getPageSnapshot(); |
| plan = await callClaude(command, snapshot); |
| activateAgent(2); |
| } catch(e) { |
| thinkText.textContent = 'β API μ€λ₯: ' + e.message; |
| runBtn.disabled = false; |
| setStatus('μ€λ₯', '#ef4444'); |
| return; |
| } |
| |
| thinkText.textContent = plan.thinking; |
| setProgress(20, 'μ‘μ
μ€ν μ€λΉ...'); |
| await sleep(500); |
| |
| thinkDiv.style.display = 'none'; |
| const logDiv = document.getElementById('ap-log'); |
| logDiv.style.display = 'block'; |
| setStatus('μ€νμ€', '#6c47ff'); |
| activateAgent(3); |
| |
| for (let i = 0; i < plan.actions.length; i++) { |
| const action = plan.actions[i]; |
| const pct = Math.round(20 + ((i+1)/plan.actions.length)*70); |
| setProgress(pct, action.desc); |
| |
| const item = document.createElement('div'); |
| item.className = 'log-item'; |
| item.innerHTML = `<span>${action.emoji}</span><span>${action.desc}</span>`; |
| logDiv.appendChild(item); |
| logDiv.scrollTop = logDiv.scrollHeight; |
| setTimeout(() => item.classList.add('vis'), 50); |
| |
| await executeAction(action, i, plan.actions.length); |
| } |
| |
| activateAgent(4); |
| await sleep(400); |
| completeAllAgents(); |
| |
| document.getElementById('search-btn').classList.add('ai-ready'); |
| setProgress(100, 'β
μλ£!'); |
| setStatus('μλ£', '#22c55e'); |
| |
| await sleep(500); |
| const elapsed = ((Date.now() - startTime)/1000).toFixed(1); |
| showSuccess(plan.result, elapsed); |
| runBtn.disabled = false; |
| } |
| |
| |
| function showSuccess(result, elapsed) { |
| document.getElementById('elapsed-time').textContent = elapsed; |
| const manualSec = 8 * 60; |
| const mult = Math.round(manualSec / elapsed); |
| document.getElementById('ai-time').textContent = elapsed + 'μ΄'; |
| document.getElementById('speed-mult').textContent = mult + 'λ°°'; |
| |
| const t = document.getElementById('ticket-content'); |
| t.innerHTML = ` |
| <div class="t-route">${result.route}</div> |
| <div class="tr"><span class="tk">λ μ§</span><span class="tv">${result.date}</span></div> |
| <div class="tr"><span class="tk">μκ°</span><span class="tv">${result.time}</span></div> |
| <div class="tr"><span class="tk">μ΄μ°¨</span><span class="tv">${result.train}</span></div> |
| <div class="tr"><span class="tk">μ’μ</span><span class="tv">${result.seat}</span></div> |
| <div class="tr"><span class="tk">μΈμ</span><span class="tv">${result.passengers}</span></div> |
| <div class="tr"><span class="tk">ν μΈ</span><span class="tv">${result.discount}</span></div> |
| <div class="tr"><span class="tk">κ²°μ κΈμ‘</span><span class="tv" style="color:var(--red)">${result.price}</span></div> |
| `; |
| document.getElementById('success').classList.add('show'); |
| } |
| |
| function setStatus(text, color) { |
| const el = document.getElementById('ap-status'); |
| el.textContent = text; |
| el.style.background = color || 'rgba(255,255,255,0.2)'; |
| } |
| |
| function setProgress(pct, label) { |
| document.getElementById('pb-fill').style.width = pct + '%'; |
| document.getElementById('pb-pct').textContent = pct + '%'; |
| document.getElementById('pb-label').textContent = label; |
| } |
| |
| function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } |
| </script> |
|
|
| </body> |
| </html> |
|
|