update 22

This commit is contained in:
2026-01-07 01:41:17 +09:00
parent 66e8e21302
commit 057a1bad41
86 changed files with 779 additions and 194 deletions

Binary file not shown.

View File

@@ -1 +1 @@
import"./index-09HB4Lmg.js";import{_ as e,a as s,h as r,G as n,i as o,t as c,n as d}from"./index-BB0X_WMV.js";const l={__name:"Badge",props:{text:String,variant:{type:String,default:"default"}},setup(a){return(t,i)=>(r(),s("span",{class:d(["badge",`badge-${a.variant}`])},[n(t.$slots,"default",{},()=>[o(c(a.text),1)])],2))}},g=e(l,[["__scopeId","data-v-b7bd2350"]]);export{g as B};
import"./index--fsvNaiQ.js";import{_ as e,a as s,h as r,G as n,i as o,t as c,n as d}from"./index-DFoOAXeQ.js";const l={__name:"Badge",props:{text:String,variant:{type:String,default:"default"}},setup(a){return(t,i)=>(r(),s("span",{class:d(["badge",`badge-${a.variant}`])},[n(t.$slots,"default",{},()=>[o(c(a.text),1)])],2))}},g=e(l,[["__scopeId","data-v-b7bd2350"]]);export{g as B};

View File

@@ -1 +1 @@
import"./index-09HB4Lmg.js";import{_ as d,r,a,e as c,G as u,n as f,h as o}from"./index-BB0X_WMV.js";const m=["type","disabled"],b={key:0,class:"spinner"},y={__name:"Button",props:{type:{type:String,default:"button"},variant:{type:String,default:"primary"},size:{type:String,default:"md"},disabled:Boolean,loading:Boolean},emits:["click"],setup(e,{expose:l}){const n=r(null);return l({focus:()=>{var t;(t=n.value)==null||t.focus()}}),(t,s)=>(o(),a("button",{ref_key:"buttonRef",ref:n,type:e.type,class:f(["btn",`btn-${e.variant}`,{"btn-sm":e.size==="sm","btn-lg":e.size==="lg"}]),disabled:e.disabled||e.loading,onClick:s[0]||(s[0]=i=>t.$emit("click",i))},[e.loading?(o(),a("span",b)):c("",!0),u(t.$slots,"default",{},void 0)],10,m))}},B=d(y,[["__scopeId","data-v-c92354e1"]]);export{B};
import"./index--fsvNaiQ.js";import{_ as d,r,a,e as c,G as u,n as f,h as o}from"./index-DFoOAXeQ.js";const m=["type","disabled"],b={key:0,class:"spinner"},y={__name:"Button",props:{type:{type:String,default:"button"},variant:{type:String,default:"primary"},size:{type:String,default:"md"},disabled:Boolean,loading:Boolean},emits:["click"],setup(e,{expose:l}){const n=r(null);return l({focus:()=>{var t;(t=n.value)==null||t.focus()}}),(t,s)=>(o(),a("button",{ref_key:"buttonRef",ref:n,type:e.type,class:f(["btn",`btn-${e.variant}`,{"btn-sm":e.size==="sm","btn-lg":e.size==="lg"}]),disabled:e.disabled||e.loading,onClick:s[0]||(s[0]=i=>t.$emit("click",i))},[e.loading?(o(),a("span",b)):c("",!0),u(t.$slots,"default",{},void 0)],10,m))}},B=d(y,[["__scopeId","data-v-c92354e1"]]);export{B};

View File

@@ -1 +1 @@
import{_ as C,r as g,o as x,a as l,b as s,l as w,s as I,t as d,F as B,g as T,h as o,d as R,w as m,f as L,u as _}from"./index-BB0X_WMV.js";import{a as A,C as F}from"./index-09HB4Lmg.js";import{C as O,a as W,L as N,B as V,p as E,b as M,c as $,d as P,e as z}from"./chartjs-plugin-datalabels.esm-DiDzp_cw.js";const U={class:"daily-stats"},Y={class:"page-header"},Z={class:"filter-section"},j={key:0,class:"loading"},q={key:1,class:"no-data"},G={key:2,class:"server-charts"},H={class:"chart-header"},J={class:"chart-subtitle"},K={class:"chart-wrapper"},Q={class:"chart-container"},X={__name:"DailyStats",setup(tt){O.register(W,N,V,E,M,$,P);const c=g(!1),r=g([]),n=g(f());function f(){return new Date().toISOString().split("T")[0]}function b(){const t=new Date(n.value);t.setDate(t.getDate()-1),n.value=t.toISOString().split("T")[0],i()}function y(){const t=new Date(n.value);t.setDate(t.getDate()+1),n.value=t.toISOString().split("T")[0],i()}const S={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"top"},tooltip:{mode:"index",intersect:!1},datalabels:{display:t=>{if(t.datasetIndex!==2)return!1;const e=t.chart.data.datasets,a=t.dataIndex;return e.reduce((u,p)=>u+(p.data[a]||0),0)>0},anchor:"end",align:"end",offset:0,font:{size:9},color:"#666",formatter:(t,e)=>{const a=e.chart.data.datasets,v=e.dataIndex;return a.reduce((u,p)=>u+(p.data[v]||0),0)}}},scales:{x:{stacked:!0,ticks:{maxRotation:0,autoSkip:!0,maxTicksLimit:24,callback:function(t,e){const a=this.getLabelForValue(t);return a&&a.endsWith(":00")?a:""}},grid:{display:!1}},y:{stacked:!0,beginAtZero:!0}},barPercentage:.8,categoryPercentage:.9},D=t=>({labels:t.timeStats.map(a=>a.time),datasets:[{label:"CRITICAL",data:t.timeStats.map(a=>a.critical),backgroundColor:"#9b59b6",borderWidth:0},{label:"ERROR",data:t.timeStats.map(a=>a.error),backgroundColor:"#e74c3c",borderWidth:0},{label:"WARN",data:t.timeStats.map(a=>a.warn),backgroundColor:"#f39c12",borderWidth:0}]}),i=async()=>{c.value=!0;try{r.value=await A.getTimeStatsByServer(n.value,15)}catch(t){console.error("Failed to load stats:",t),r.value=[]}finally{c.value=!1}},h=t=>{const e=new Date(t);return`${e.getFullYear()}${e.getMonth()+1}${e.getDate()}`},k=t=>t.timeStats.reduce((e,a)=>e+a.total,0);return x(()=>{i()}),(t,e)=>(o(),l("div",U,[s("div",Y,[e[1]||(e[1]=s("h2",null,"일별 에러현황",-1)),s("div",Z,[s("button",{class:"nav-btn",onClick:b},"◀ 이전"),w(s("input",{type:"date","onUpdate:modelValue":e[0]||(e[0]=a=>n.value=a),onChange:i},null,544),[[I,n.value]]),s("button",{class:"nav-btn",onClick:y},"다음 ▶")])]),c.value?(o(),l("div",j,[...e[2]||(e[2]=[s("p",null,"로딩중...",-1)])])):r.value.length===0?(o(),l("div",q,[s("p",null,d(h(n.value))+"에 분석된 에러 데이터가 없습니다.",1)])):(o(),l("div",G,[(o(!0),l(B,null,T(r.value,a=>(o(),R(_(F),{key:a.serverId,class:"server-chart-card"},{header:m(()=>[s("div",H,[s("h3",null,"🖥️ "+d(a.serverName),1),s("span",J,d(h(n.value))+" 15분 단위 에러 ("+d(k(a))+"건)",1)])]),default:m(()=>[s("div",K,[s("div",Q,[L(_(z),{data:D(a),options:S},null,8,["data"])])])]),_:2},1024))),128))]))]))}},nt=C(X,[["__scopeId","data-v-b7241be8"]]);export{nt as default};
import{_ as C,r as g,o as x,a as l,b as s,l as w,s as I,t as d,F as B,g as T,h as o,d as R,w as m,f as L,u as _}from"./index-DFoOAXeQ.js";import{a as A,C as F}from"./index--fsvNaiQ.js";import{C as O,a as W,L as N,B as V,p as E,b as M,c as $,d as P,e as z}from"./chartjs-plugin-datalabels.esm-B8xtR40N.js";const U={class:"daily-stats"},Y={class:"page-header"},Z={class:"filter-section"},j={key:0,class:"loading"},q={key:1,class:"no-data"},G={key:2,class:"server-charts"},H={class:"chart-header"},J={class:"chart-subtitle"},K={class:"chart-wrapper"},Q={class:"chart-container"},X={__name:"DailyStats",setup(tt){O.register(W,N,V,E,M,$,P);const c=g(!1),r=g([]),n=g(f());function f(){return new Date().toISOString().split("T")[0]}function b(){const t=new Date(n.value);t.setDate(t.getDate()-1),n.value=t.toISOString().split("T")[0],i()}function y(){const t=new Date(n.value);t.setDate(t.getDate()+1),n.value=t.toISOString().split("T")[0],i()}const S={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"top"},tooltip:{mode:"index",intersect:!1},datalabels:{display:t=>{if(t.datasetIndex!==2)return!1;const e=t.chart.data.datasets,a=t.dataIndex;return e.reduce((u,p)=>u+(p.data[a]||0),0)>0},anchor:"end",align:"end",offset:0,font:{size:9},color:"#666",formatter:(t,e)=>{const a=e.chart.data.datasets,v=e.dataIndex;return a.reduce((u,p)=>u+(p.data[v]||0),0)}}},scales:{x:{stacked:!0,ticks:{maxRotation:0,autoSkip:!0,maxTicksLimit:24,callback:function(t,e){const a=this.getLabelForValue(t);return a&&a.endsWith(":00")?a:""}},grid:{display:!1}},y:{stacked:!0,beginAtZero:!0}},barPercentage:.8,categoryPercentage:.9},D=t=>({labels:t.timeStats.map(a=>a.time),datasets:[{label:"CRITICAL",data:t.timeStats.map(a=>a.critical),backgroundColor:"#9b59b6",borderWidth:0},{label:"ERROR",data:t.timeStats.map(a=>a.error),backgroundColor:"#e74c3c",borderWidth:0},{label:"WARN",data:t.timeStats.map(a=>a.warn),backgroundColor:"#f39c12",borderWidth:0}]}),i=async()=>{c.value=!0;try{r.value=await A.getTimeStatsByServer(n.value,15)}catch(t){console.error("Failed to load stats:",t),r.value=[]}finally{c.value=!1}},h=t=>{const e=new Date(t);return`${e.getFullYear()}${e.getMonth()+1}${e.getDate()}`},k=t=>t.timeStats.reduce((e,a)=>e+a.total,0);return x(()=>{i()}),(t,e)=>(o(),l("div",U,[s("div",Y,[e[1]||(e[1]=s("h2",null,"일별 에러현황",-1)),s("div",Z,[s("button",{class:"nav-btn",onClick:b},"◀ 이전"),w(s("input",{type:"date","onUpdate:modelValue":e[0]||(e[0]=a=>n.value=a),onChange:i},null,544),[[I,n.value]]),s("button",{class:"nav-btn",onClick:y},"다음 ▶")])]),c.value?(o(),l("div",j,[...e[2]||(e[2]=[s("p",null,"로딩중...",-1)])])):r.value.length===0?(o(),l("div",q,[s("p",null,d(h(n.value))+"에 분석된 에러 데이터가 없습니다.",1)])):(o(),l("div",G,[(o(!0),l(B,null,T(r.value,a=>(o(),R(_(F),{key:a.serverId,class:"server-chart-card"},{header:m(()=>[s("div",H,[s("h3",null,"🖥️ "+d(a.serverName),1),s("span",J,d(h(n.value))+" 15분 단위 에러 ("+d(k(a))+"건)",1)])]),default:m(()=>[s("div",K,[s("div",Q,[L(_(z),{data:D(a),options:S},null,8,["data"])])])]),_:2},1024))),128))]))]))}},nt=C(X,[["__scopeId","data-v-b7241be8"]]);export{nt as default};

View File

@@ -1 +0,0 @@
import"./index-09HB4Lmg.js";import{_ as h,a as e,h as a,b as o,e as u,F as d,g as i,j as f,t as c,G as y,i as g}from"./index-BB0X_WMV.js";const b={class:"data-table-wrapper"},p={class:"data-table"},$={key:0,class:"actions-col"},_={key:0},D=["colspan"],S={key:1},T=["colspan"],B=["onClick"],N={key:0,class:"actions-col"},V={__name:"DataTable",props:{columns:{type:Array,required:!0},data:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},emptyText:{type:String,default:"데이터가 없습니다."}},emits:["row-click"],setup(n){const m=(t,r)=>t==null?"-":r.type==="date"&&t?new Date(t).toLocaleString("ko-KR"):r.type==="boolean"?t?"Y":"N":t;return(t,r)=>(a(),e("div",b,[o("table",p,[o("thead",null,[o("tr",null,[(a(!0),e(d,null,i(n.columns,s=>(a(),e("th",{key:s.key,style:f({width:s.width})},c(s.label),5))),128)),t.$slots.actions?(a(),e("th",$,"작업")):u("",!0)])]),o("tbody",null,[n.loading?(a(),e("tr",_,[o("td",{colspan:n.columns.length+(t.$slots.actions?1:0),class:"loading-cell"}," 로딩 중... ",8,D)])):!n.data||n.data.length===0?(a(),e("tr",S,[o("td",{colspan:n.columns.length+(t.$slots.actions?1:0),class:"empty-cell"},c(n.emptyText),9,T)])):(a(!0),e(d,{key:2},i(n.data,(s,k)=>(a(),e("tr",{key:s.id||k,onClick:l=>t.$emit("row-click",s)},[(a(!0),e(d,null,i(n.columns,l=>(a(),e("td",{key:l.key},[y(t.$slots,l.key,{row:s,value:s[l.key]},()=>[g(c(m(s[l.key],l)),1)])]))),128)),t.$slots.actions?(a(),e("td",N,[y(t.$slots,"actions",{row:s},void 0)])):u("",!0)],8,B))),128))])])]))}},A=h(V,[["__scopeId","data-v-db5e24a9"]]);export{A as D};

View File

@@ -1 +1 @@
import"./index-09HB4Lmg.js";import{_ as i,c as s,a as t,h as l,e as o,i as c,t as n,F as m,g as y}from"./index-BB0X_WMV.js";const h={class:"form-group"},v=["for"],b={key:0,class:"required"},g=["id","type","value","placeholder","disabled","readonly"],f=["id","value","placeholder","disabled","readonly","rows"],k=["id","value","disabled"],V={key:0,value:""},S=["value"],x={key:4,class:"error-text"},I={key:5,class:"hint-text"},B={__name:"FormInput",props:{modelValue:{type:[String,Number],default:""},label:String,type:{type:String,default:"text"},placeholder:String,required:Boolean,disabled:Boolean,readonly:Boolean,error:String,hint:String,rows:{type:Number,default:3},options:{type:Array,default:()=>[]}},emits:["update:modelValue"],setup(e){const r=s(()=>`input-${Math.random().toString(36).slice(2,9)}`);return(u,d)=>(l(),t("div",h,[e.label?(l(),t("label",{key:0,for:r.value},[c(n(e.label)+" ",1),e.required?(l(),t("span",b,"*")):o("",!0)],8,v)):o("",!0),e.type!=="textarea"&&e.type!=="select"?(l(),t("input",{key:1,id:r.value,type:e.type,value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,readonly:e.readonly,class:"form-input",onInput:d[0]||(d[0]=a=>u.$emit("update:modelValue",a.target.value))},null,40,g)):e.type==="textarea"?(l(),t("textarea",{key:2,id:r.value,value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,readonly:e.readonly,rows:e.rows,class:"form-input",onInput:d[1]||(d[1]=a=>u.$emit("update:modelValue",a.target.value))},null,40,f)):e.type==="select"?(l(),t("select",{key:3,id:r.value,value:e.modelValue,disabled:e.disabled,class:"form-input",onChange:d[2]||(d[2]=a=>u.$emit("update:modelValue",a.target.value))},[e.placeholder?(l(),t("option",V,n(e.placeholder),1)):o("",!0),(l(!0),t(m,null,y(e.options,a=>(l(),t("option",{key:a.value,value:a.value},n(a.label),9,S))),128))],40,k)):o("",!0),e.error?(l(),t("span",x,n(e.error),1)):o("",!0),e.hint?(l(),t("span",I,n(e.hint),1)):o("",!0)]))}},N=i(B,[["__scopeId","data-v-45f49038"]]);export{N as F};
import"./index--fsvNaiQ.js";import{_ as i,c as s,a as t,h as l,e as o,i as c,t as n,F as m,g as y}from"./index-DFoOAXeQ.js";const h={class:"form-group"},v=["for"],b={key:0,class:"required"},g=["id","type","value","placeholder","disabled","readonly"],f=["id","value","placeholder","disabled","readonly","rows"],k=["id","value","disabled"],V={key:0,value:""},S=["value"],x={key:4,class:"error-text"},I={key:5,class:"hint-text"},B={__name:"FormInput",props:{modelValue:{type:[String,Number],default:""},label:String,type:{type:String,default:"text"},placeholder:String,required:Boolean,disabled:Boolean,readonly:Boolean,error:String,hint:String,rows:{type:Number,default:3},options:{type:Array,default:()=>[]}},emits:["update:modelValue"],setup(e){const r=s(()=>`input-${Math.random().toString(36).slice(2,9)}`);return(u,d)=>(l(),t("div",h,[e.label?(l(),t("label",{key:0,for:r.value},[c(n(e.label)+" ",1),e.required?(l(),t("span",b,"*")):o("",!0)],8,v)):o("",!0),e.type!=="textarea"&&e.type!=="select"?(l(),t("input",{key:1,id:r.value,type:e.type,value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,readonly:e.readonly,class:"form-input",onInput:d[0]||(d[0]=a=>u.$emit("update:modelValue",a.target.value))},null,40,g)):e.type==="textarea"?(l(),t("textarea",{key:2,id:r.value,value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,readonly:e.readonly,rows:e.rows,class:"form-input",onInput:d[1]||(d[1]=a=>u.$emit("update:modelValue",a.target.value))},null,40,f)):e.type==="select"?(l(),t("select",{key:3,id:r.value,value:e.modelValue,disabled:e.disabled,class:"form-input",onChange:d[2]||(d[2]=a=>u.$emit("update:modelValue",a.target.value))},[e.placeholder?(l(),t("option",V,n(e.placeholder),1)):o("",!0),(l(!0),t(m,null,y(e.options,a=>(l(),t("option",{key:a.value,value:a.value},n(a.label),9,S))),128))],40,k)):o("",!0),e.error?(l(),t("span",x,n(e.error),1)):o("",!0),e.hint?(l(),t("span",I,n(e.hint),1)):o("",!0)]))}},N=i(B,[["__scopeId","data-v-45f49038"]]);export{N as F};

View File

@@ -1 +1 @@
import{_ as r,d as m,a as d,e as i,b as e,t as u,G as c,j as f,m as p,T as h,h as s}from"./index-BB0X_WMV.js";import"./index-09HB4Lmg.js";const y={class:"modal-header"},_={class:"modal-body"},k={key:0,class:"modal-footer"},v={__name:"Modal",props:{modelValue:{type:Boolean,default:!1},title:{type:String,default:""},width:{type:String,default:"500px"}},emits:["update:modelValue","close"],setup(t,{emit:n}){const a=n,l=()=>{a("update:modelValue",!1),a("close")};return(o,V)=>(s(),m(h,{to:"body"},[t.modelValue?(s(),d("div",{key:0,class:"modal-overlay",onClick:p(l,["self"])},[e("div",{class:"modal",style:f({width:t.width})},[e("div",y,[e("h3",null,u(t.title),1),e("button",{class:"close-btn",onClick:l},"×")]),e("div",_,[c(o.$slots,"default",{},void 0)]),o.$slots.footer?(s(),d("div",k,[c(o.$slots,"footer",{},void 0)])):i("",!0)],4)])):i("",!0)]))}},S=r(v,[["__scopeId","data-v-90993dd3"]]);export{S as M};
import{_ as r,d as m,a as d,e as i,b as e,t as u,G as c,j as f,m as p,T as h,h as s}from"./index-DFoOAXeQ.js";import"./index--fsvNaiQ.js";const y={class:"modal-header"},_={class:"modal-body"},k={key:0,class:"modal-footer"},v={__name:"Modal",props:{modelValue:{type:Boolean,default:!1},title:{type:String,default:""},width:{type:String,default:"500px"}},emits:["update:modelValue","close"],setup(t,{emit:n}){const a=n,l=()=>{a("update:modelValue",!1),a("close")};return(o,V)=>(s(),m(h,{to:"body"},[t.modelValue?(s(),d("div",{key:0,class:"modal-overlay",onClick:p(l,["self"])},[e("div",{class:"modal",style:f({width:t.width})},[e("div",y,[e("h3",null,u(t.title),1),e("button",{class:"close-btn",onClick:l},"×")]),e("div",_,[c(o.$slots,"default",{},void 0)]),o.$slots.footer?(s(),d("div",k,[c(o.$slots,"footer",{},void 0)])):i("",!0)],4)])):i("",!0)]))}},S=r(v,[["__scopeId","data-v-90993dd3"]]);export{S as M};

View File

@@ -1 +1 @@
import{_ as M,r as g,o as w,a as l,b as s,l as x,s as R,t as c,F as $,g as B,h as o,d as D,w as v,f as I,u as _}from"./index-BB0X_WMV.js";import{a as N,C as F}from"./index-09HB4Lmg.js";import{C as A,a as L,L as E,B as T,p as V,b as Y,c as O,d as z,e as U}from"./chartjs-plugin-datalabels.esm-DiDzp_cw.js";const W={class:"monthly-stats"},Z={class:"page-header"},j={class:"filter-section"},q={key:0,class:"loading"},G={key:1,class:"no-data"},H={key:2,class:"server-charts"},J={class:"chart-header"},K={class:"chart-subtitle"},P={class:"chart-container"},Q={__name:"MonthlyStats",setup(X){A.register(L,E,T,V,Y,O,z);const i=g(!1),r=g([]),n=g(f());function f(){const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}function b(){const[a,e]=n.value.split("-").map(Number),t=new Date(a,e-2,1);n.value=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`,d()}function y(){const[a,e]=n.value.split("-").map(Number),t=new Date(a,e,1);n.value=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`,d()}const S={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"top"},tooltip:{mode:"index",intersect:!1},datalabels:{display:a=>{if(a.datasetIndex!==2)return!1;const e=a.chart.data.datasets,t=a.dataIndex;return e.reduce((p,h)=>p+(h.data[t]||0),0)>0},anchor:"end",align:"end",offset:2,font:{size:10,weight:"bold"},color:"#666",formatter:(a,e)=>{const t=e.chart.data.datasets,u=e.dataIndex;return t.reduce((p,h)=>p+(h.data[u]||0),0)}}},scales:{x:{stacked:!0},y:{stacked:!0,beginAtZero:!0}}},C=a=>({labels:a.dailyStats.map(t=>`${new Date(t.date).getDate()}`),datasets:[{label:"CRITICAL",data:a.dailyStats.map(t=>t.critical),backgroundColor:"#9b59b6",borderRadius:2},{label:"ERROR",data:a.dailyStats.map(t=>t.error),backgroundColor:"#e74c3c",borderRadius:2},{label:"WARN",data:a.dailyStats.map(t=>t.warn),backgroundColor:"#f39c12",borderRadius:2}]}),d=async()=>{i.value=!0;try{const[a,e]=n.value.split("-").map(Number);r.value=await N.getMonthlyStatsByServer(a,e)}catch(a){console.error("Failed to load stats:",a),r.value=[]}finally{i.value=!1}},m=a=>{const[e,t]=a.split("-");return`${e}${parseInt(t)}`},k=a=>a.dailyStats.reduce((e,t)=>e+t.total,0);return w(()=>{d()}),(a,e)=>(o(),l("div",W,[s("div",Z,[e[1]||(e[1]=s("h2",null,"월별 에러현황",-1)),s("div",j,[s("button",{class:"nav-btn",onClick:b},"◀ 이전"),x(s("input",{type:"month","onUpdate:modelValue":e[0]||(e[0]=t=>n.value=t),onChange:d},null,544),[[R,n.value]]),s("button",{class:"nav-btn",onClick:y},"다음 ▶")])]),i.value?(o(),l("div",q,[...e[2]||(e[2]=[s("p",null,"로딩중...",-1)])])):r.value.length===0?(o(),l("div",G,[s("p",null,c(m(n.value))+"에 분석된 에러 데이터가 없습니다.",1)])):(o(),l("div",H,[(o(!0),l($,null,B(r.value,t=>(o(),D(_(F),{key:t.serverId,class:"server-chart-card"},{header:v(()=>[s("div",J,[s("h3",null,"🖥️ "+c(t.serverName),1),s("span",K,c(m(n.value))+" 일별 에러 ("+c(k(t))+"건)",1)])]),default:v(()=>[s("div",P,[I(_(U),{data:C(t),options:S},null,8,["data"])])]),_:2},1024))),128))]))]))}},st=M(Q,[["__scopeId","data-v-7c1c78fe"]]);export{st as default};
import{_ as M,r as g,o as w,a as l,b as s,l as x,s as R,t as c,F as $,g as B,h as o,d as D,w as v,f as I,u as _}from"./index-DFoOAXeQ.js";import{a as N,C as F}from"./index--fsvNaiQ.js";import{C as A,a as L,L as E,B as T,p as V,b as Y,c as O,d as z,e as U}from"./chartjs-plugin-datalabels.esm-B8xtR40N.js";const W={class:"monthly-stats"},Z={class:"page-header"},j={class:"filter-section"},q={key:0,class:"loading"},G={key:1,class:"no-data"},H={key:2,class:"server-charts"},J={class:"chart-header"},K={class:"chart-subtitle"},P={class:"chart-container"},Q={__name:"MonthlyStats",setup(X){A.register(L,E,T,V,Y,O,z);const i=g(!1),r=g([]),n=g(f());function f(){const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}function b(){const[a,e]=n.value.split("-").map(Number),t=new Date(a,e-2,1);n.value=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`,d()}function y(){const[a,e]=n.value.split("-").map(Number),t=new Date(a,e,1);n.value=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`,d()}const S={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"top"},tooltip:{mode:"index",intersect:!1},datalabels:{display:a=>{if(a.datasetIndex!==2)return!1;const e=a.chart.data.datasets,t=a.dataIndex;return e.reduce((p,h)=>p+(h.data[t]||0),0)>0},anchor:"end",align:"end",offset:2,font:{size:10,weight:"bold"},color:"#666",formatter:(a,e)=>{const t=e.chart.data.datasets,u=e.dataIndex;return t.reduce((p,h)=>p+(h.data[u]||0),0)}}},scales:{x:{stacked:!0},y:{stacked:!0,beginAtZero:!0}}},C=a=>({labels:a.dailyStats.map(t=>`${new Date(t.date).getDate()}`),datasets:[{label:"CRITICAL",data:a.dailyStats.map(t=>t.critical),backgroundColor:"#9b59b6",borderRadius:2},{label:"ERROR",data:a.dailyStats.map(t=>t.error),backgroundColor:"#e74c3c",borderRadius:2},{label:"WARN",data:a.dailyStats.map(t=>t.warn),backgroundColor:"#f39c12",borderRadius:2}]}),d=async()=>{i.value=!0;try{const[a,e]=n.value.split("-").map(Number);r.value=await N.getMonthlyStatsByServer(a,e)}catch(a){console.error("Failed to load stats:",a),r.value=[]}finally{i.value=!1}},m=a=>{const[e,t]=a.split("-");return`${e}${parseInt(t)}`},k=a=>a.dailyStats.reduce((e,t)=>e+t.total,0);return w(()=>{d()}),(a,e)=>(o(),l("div",W,[s("div",Z,[e[1]||(e[1]=s("h2",null,"월별 에러현황",-1)),s("div",j,[s("button",{class:"nav-btn",onClick:b},"◀ 이전"),x(s("input",{type:"month","onUpdate:modelValue":e[0]||(e[0]=t=>n.value=t),onChange:d},null,544),[[R,n.value]]),s("button",{class:"nav-btn",onClick:y},"다음 ▶")])]),i.value?(o(),l("div",q,[...e[2]||(e[2]=[s("p",null,"로딩중...",-1)])])):r.value.length===0?(o(),l("div",G,[s("p",null,c(m(n.value))+"에 분석된 에러 데이터가 없습니다.",1)])):(o(),l("div",H,[(o(!0),l($,null,B(r.value,t=>(o(),D(_(F),{key:t.serverId,class:"server-chart-card"},{header:v(()=>[s("div",J,[s("h3",null,"🖥️ "+c(t.serverName),1),s("span",K,c(m(n.value))+" 일별 에러 ("+c(k(t))+"건)",1)])]),default:v(()=>[s("div",P,[I(_(U),{data:C(t),options:S},null,8,["data"])])]),_:2},1024))),128))]))]))}},st=M(Q,[["__scopeId","data-v-7c1c78fe"]]);export{st as default};

View File

@@ -1 +0,0 @@
.card-header-content[data-v-fc6fccb5]{display:flex;justify-content:space-between;align-items:center}.card-header-content h3[data-v-fc6fccb5]{margin:0}.action-buttons[data-v-fc6fccb5]{display:flex;gap:4px}.regex-code[data-v-fc6fccb5]{font-family:monospace;background:#f1f3f4;padding:2px 6px;border-radius:3px;font-size:12px}.form-group[data-v-fc6fccb5]{margin-bottom:16px}.form-group label[data-v-fc6fccb5]{display:flex;align-items:center;gap:8px;cursor:pointer}.test-section[data-v-fc6fccb5]{display:flex;flex-direction:column;gap:16px}.test-pattern label[data-v-fc6fccb5]{display:block;font-weight:500;margin-bottom:6px}.regex-display[data-v-fc6fccb5]{display:block;font-family:monospace;background:#f8f9fa;padding:12px;border-radius:4px;font-size:13px;word-break:break-all}.test-result[data-v-fc6fccb5]{padding:16px;border-radius:8px;margin-top:8px}.test-result.success[data-v-fc6fccb5]{background:#d4edda;border:1px solid #c3e6cb}.test-result.fail[data-v-fc6fccb5]{background:#f8d7da;border:1px solid #f5c6cb}.test-result h4[data-v-fc6fccb5]{margin:0 0 12px}.test-result p[data-v-fc6fccb5]{margin:0}.match-info[data-v-fc6fccb5]{margin-top:8px}.match-info label[data-v-fc6fccb5]{font-weight:500;margin-right:8px}.match-info code[data-v-fc6fccb5]{background:#0000001a;padding:2px 6px;border-radius:3px}.error-msg[data-v-fc6fccb5]{color:#721c24}

View File

@@ -0,0 +1 @@
.page-header[data-v-821062ae]{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header h2[data-v-821062ae]{margin:0}.loading[data-v-821062ae],.empty-state[data-v-821062ae]{text-align:center;padding:60px;background:#fff;border-radius:8px;color:#666}.empty-state p[data-v-821062ae]{margin-bottom:16px}.pattern-grid[data-v-821062ae]{display:grid;grid-template-columns:repeat(auto-fill,minmax(400px,1fr));gap:20px}.pattern-card[data-v-821062ae]{transition:box-shadow .2s,transform .2s}.pattern-card[data-v-821062ae]:hover{box-shadow:0 4px 12px #00000026;transform:translateY(-2px)}.pattern-header[data-v-821062ae]{display:flex;justify-content:space-between;align-items:center}.pattern-title[data-v-821062ae]{display:flex;align-items:center;gap:10px}.pattern-title h4[data-v-821062ae]{margin:0;font-size:16px;font-weight:600}.pattern-body[data-v-821062ae]{padding:4px 0}.pattern-info[data-v-821062ae]{margin-bottom:12px}.pattern-info label[data-v-821062ae]{display:block;font-size:12px;color:#666;margin-bottom:4px}.regex-box[data-v-821062ae]{display:block;font-family:Consolas,Monaco,monospace;background:#f1f3f5;padding:10px 12px;border-radius:6px;font-size:13px;word-break:break-all;line-height:1.4;border-left:3px solid #3498db}.regex-box.exclude[data-v-821062ae]{border-left-color:#e67e22;background:#fef5e7}.pattern-meta[data-v-821062ae]{display:flex;flex-wrap:wrap;gap:12px;padding-top:8px;border-top:1px solid #eee;font-size:13px}.meta-item[data-v-821062ae]{display:flex;align-items:center;gap:4px}.meta-label[data-v-821062ae]{color:#888}.meta-value[data-v-821062ae]{font-weight:500;color:#333}.meta-item.description[data-v-821062ae]{flex-basis:100%;color:#666;font-style:italic}.pattern-actions[data-v-821062ae]{display:flex;gap:8px;padding-top:12px;border-top:1px solid #eee;margin-top:12px}.action-btn[data-v-821062ae]{flex:1;display:flex;align-items:center;justify-content:center;gap:4px;padding:10px 12px;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;transition:all .2s}.action-btn.test[data-v-821062ae]{background:#e8f4fd;color:#2980b9}.action-btn.test[data-v-821062ae]:hover{background:#d4e9f7}.action-btn.edit[data-v-821062ae]{background:#fef3e2;color:#d68910}.action-btn.edit[data-v-821062ae]:hover{background:#fce8c9}.action-btn.delete[data-v-821062ae]{background:#fdeaea;color:#c0392b}.action-btn.delete[data-v-821062ae]:hover{background:#f9d6d6}.form-group[data-v-821062ae]{margin-bottom:16px}.form-group label[data-v-821062ae]{display:flex;align-items:center;gap:8px;cursor:pointer}.test-section[data-v-821062ae]{display:flex;flex-direction:column;gap:16px}.test-pattern label[data-v-821062ae]{display:block;font-weight:500;margin-bottom:6px}.regex-display[data-v-821062ae]{display:block;font-family:monospace;background:#f8f9fa;padding:12px;border-radius:4px;font-size:13px;word-break:break-all}.test-result[data-v-821062ae]{padding:16px;border-radius:8px;margin-top:8px}.test-result.success[data-v-821062ae]{background:#d4edda;border:1px solid #c3e6cb}.test-result.fail[data-v-821062ae]{background:#f8d7da;border:1px solid #f5c6cb}.test-result h4[data-v-821062ae]{margin:0 0 12px}.test-result p[data-v-821062ae]{margin:0}.match-info[data-v-821062ae]{margin-top:8px}.match-info label[data-v-821062ae]{font-weight:500;margin-right:8px}.match-info code[data-v-821062ae]{background:#0000001a;padding:2px 6px;border-radius:3px}.error-msg[data-v-821062ae]{color:#721c24}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.card-header-content[data-v-6f59a208]{display:flex;justify-content:space-between;align-items:center}.card-header-content h3[data-v-6f59a208]{margin:0}.action-buttons[data-v-6f59a208]{display:flex;gap:4px}.form-group[data-v-6f59a208]{margin-bottom:16px}.form-group label[data-v-6f59a208]{display:flex;align-items:center;gap:8px;cursor:pointer}.log-path-section h4[data-v-6f59a208]{margin:0 0 12px;font-size:14px;color:#333}.log-path-form[data-v-6f59a208]{padding:16px;background:#f8f9fa;border-radius:8px;margin-bottom:20px}.log-path-inputs[data-v-6f59a208]{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:12px}.log-path-list table[data-v-6f59a208]{width:100%;border-collapse:collapse}.log-path-list th[data-v-6f59a208],.log-path-list td[data-v-6f59a208]{padding:10px 12px;text-align:left;border-bottom:1px solid #eee}.log-path-list th[data-v-6f59a208]{background:#f8f9fa;font-weight:600;font-size:13px}.empty-text[data-v-6f59a208]{color:#6c757d;text-align:center;padding:20px}.warning-text[data-v-6f59a208]{color:#e74c3c;font-size:14px}.test-result[data-v-6f59a208]{padding:16px;border-radius:8px}.test-result.success[data-v-6f59a208]{background:#d4edda;border:1px solid #c3e6cb}.test-result.fail[data-v-6f59a208]{background:#f8d7da;border:1px solid #f5c6cb}.test-result p[data-v-6f59a208]{margin:0}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{_ as S,r as m,o as F,a as p,f as a,w as i,u as n,h as v,m as B,b as t,i as c}from"./index-BB0X_WMV.js";import{C as V,b}from"./index-09HB4Lmg.js";import{F as u}from"./FormInput-BwPgkOAn.js";import{B as y}from"./Button-CoXLXkk7.js";const k={class:"settings"},w={key:0,class:"loading"},C={class:"setting-section"},U={class:"setting-section"},h={class:"setting-section"},M={class:"setting-section"},z={class:"form-actions"},A={__name:"Settings",setup(N){const r=m(!1),d=m(!1),l=m({}),f={"server.port":"8080","export.path":"./exports","retention.days":"90","scan.timeout":"30","scan.maxFileSize":"100"},g=async()=>{r.value=!0;try{const o=await b.getAllAsMap();l.value={...f,...o}}catch(o){console.error("Failed to load settings:",o),l.value={...f}}finally{r.value=!1}},x=async()=>{d.value=!0;try{for(const[o,e]of Object.entries(l.value))await b.save({key:o,value:String(e)});alert("설정이 저장되었습니다.")}catch(o){console.error("Failed to save settings:",o),alert("설정 저장에 실패했습니다.")}finally{d.value=!1}};return F(()=>{g()}),(o,e)=>(v(),p("div",k,[a(n(V),null,{header:i(()=>[...e[5]||(e[5]=[t("div",{class:"card-header-content"},[t("h3",null,"설정")],-1)])]),default:i(()=>[r.value?(v(),p("div",w,"로딩중...")):(v(),p("form",{key:1,onSubmit:B(x,["prevent"]),class:"settings-form"},[t("div",C,[e[6]||(e[6]=t("h4",null,"일반 설정",-1)),a(n(u),{modelValue:l.value["server.port"],"onUpdate:modelValue":e[0]||(e[0]=s=>l.value["server.port"]=s),label:"서버 포트",type:"number",hint:"애플리케이션이 실행될 포트 번호 (기본: 8080)"},null,8,["modelValue"])]),t("div",U,[e[7]||(e[7]=t("h4",null,"내보내기 설정",-1)),a(n(u),{modelValue:l.value["export.path"],"onUpdate:modelValue":e[1]||(e[1]=s=>l.value["export.path"]=s),label:"내보내기 경로",placeholder:"예: C:\\LogHunter\\exports",hint:"리포트 파일이 저장될 기본 경로"},null,8,["modelValue"])]),t("div",h,[e[8]||(e[8]=t("h4",null,"데이터 관리",-1)),a(n(u),{modelValue:l.value["retention.days"],"onUpdate:modelValue":e[2]||(e[2]=s=>l.value["retention.days"]=s),label:"로그 보관 기간 (일)",type:"number",hint:"에러 로그 데이터 보관 기간 (0 = 무제한)"},null,8,["modelValue"])]),t("div",M,[e[9]||(e[9]=t("h4",null,"스캔 설정",-1)),a(n(u),{modelValue:l.value["scan.timeout"],"onUpdate:modelValue":e[3]||(e[3]=s=>l.value["scan.timeout"]=s),label:"스캔 타임아웃 (초)",type:"number",hint:"SFTP 연결 및 파일 다운로드 타임아웃"},null,8,["modelValue"]),a(n(u),{modelValue:l.value["scan.maxFileSize"],"onUpdate:modelValue":e[4]||(e[4]=s=>l.value["scan.maxFileSize"]=s),label:"최대 파일 크기 (MB)",type:"number",hint:"분석할 로그 파일의 최대 크기"},null,8,["modelValue"])]),t("div",z,[a(n(y),{onClick:g,variant:"secondary"},{default:i(()=>[...e[10]||(e[10]=[c("초기화",-1)])]),_:1}),a(n(y),{type:"submit",loading:d.value},{default:i(()=>[...e[11]||(e[11]=[c("저장",-1)])]),_:1},8,["loading"])])],32))]),_:1}),a(n(V),{class:"app-info"},{header:i(()=>[...e[12]||(e[12]=[t("h3",null,"애플리케이션 정보",-1)])]),default:i(()=>[e[13]||(e[13]=t("div",{class:"info-list"},[t("div",{class:"info-item"},[t("span",{class:"label"},"버전"),t("span",{class:"value"},"1.0.0")]),t("div",{class:"info-item"},[t("span",{class:"label"},"프레임워크"),t("span",{class:"value"},"Spring Boot 3.2 + Vue 3")]),t("div",{class:"info-item"},[t("span",{class:"label"},"데이터베이스"),t("span",{class:"value"},"SQLite (./data/loghunter.db)")])],-1))]),_:1})]))}},j=S(A,[["__scopeId","data-v-fdca948e"]]);export{j as default};
import{_ as S,r as m,o as F,a as p,f as a,w as i,u as n,h as v,m as B,b as t,i as c}from"./index-DFoOAXeQ.js";import{C as V,b}from"./index--fsvNaiQ.js";import{F as u}from"./FormInput-BsW78DWl.js";import{B as y}from"./Button-Ddldcbk7.js";const k={class:"settings"},w={key:0,class:"loading"},C={class:"setting-section"},U={class:"setting-section"},h={class:"setting-section"},M={class:"setting-section"},z={class:"form-actions"},A={__name:"Settings",setup(N){const r=m(!1),d=m(!1),l=m({}),f={"server.port":"8080","export.path":"./exports","retention.days":"90","scan.timeout":"30","scan.maxFileSize":"100"},g=async()=>{r.value=!0;try{const o=await b.getAllAsMap();l.value={...f,...o}}catch(o){console.error("Failed to load settings:",o),l.value={...f}}finally{r.value=!1}},x=async()=>{d.value=!0;try{for(const[o,e]of Object.entries(l.value))await b.save({key:o,value:String(e)});alert("설정이 저장되었습니다.")}catch(o){console.error("Failed to save settings:",o),alert("설정 저장에 실패했습니다.")}finally{d.value=!1}};return F(()=>{g()}),(o,e)=>(v(),p("div",k,[a(n(V),null,{header:i(()=>[...e[5]||(e[5]=[t("div",{class:"card-header-content"},[t("h3",null,"설정")],-1)])]),default:i(()=>[r.value?(v(),p("div",w,"로딩중...")):(v(),p("form",{key:1,onSubmit:B(x,["prevent"]),class:"settings-form"},[t("div",C,[e[6]||(e[6]=t("h4",null,"일반 설정",-1)),a(n(u),{modelValue:l.value["server.port"],"onUpdate:modelValue":e[0]||(e[0]=s=>l.value["server.port"]=s),label:"서버 포트",type:"number",hint:"애플리케이션이 실행될 포트 번호 (기본: 8080)"},null,8,["modelValue"])]),t("div",U,[e[7]||(e[7]=t("h4",null,"내보내기 설정",-1)),a(n(u),{modelValue:l.value["export.path"],"onUpdate:modelValue":e[1]||(e[1]=s=>l.value["export.path"]=s),label:"내보내기 경로",placeholder:"예: C:\\LogHunter\\exports",hint:"리포트 파일이 저장될 기본 경로"},null,8,["modelValue"])]),t("div",h,[e[8]||(e[8]=t("h4",null,"데이터 관리",-1)),a(n(u),{modelValue:l.value["retention.days"],"onUpdate:modelValue":e[2]||(e[2]=s=>l.value["retention.days"]=s),label:"로그 보관 기간 (일)",type:"number",hint:"에러 로그 데이터 보관 기간 (0 = 무제한)"},null,8,["modelValue"])]),t("div",M,[e[9]||(e[9]=t("h4",null,"스캔 설정",-1)),a(n(u),{modelValue:l.value["scan.timeout"],"onUpdate:modelValue":e[3]||(e[3]=s=>l.value["scan.timeout"]=s),label:"스캔 타임아웃 (초)",type:"number",hint:"SFTP 연결 및 파일 다운로드 타임아웃"},null,8,["modelValue"]),a(n(u),{modelValue:l.value["scan.maxFileSize"],"onUpdate:modelValue":e[4]||(e[4]=s=>l.value["scan.maxFileSize"]=s),label:"최대 파일 크기 (MB)",type:"number",hint:"분석할 로그 파일의 최대 크기"},null,8,["modelValue"])]),t("div",z,[a(n(y),{onClick:g,variant:"secondary"},{default:i(()=>[...e[10]||(e[10]=[c("초기화",-1)])]),_:1}),a(n(y),{type:"submit",loading:d.value},{default:i(()=>[...e[11]||(e[11]=[c("저장",-1)])]),_:1},8,["loading"])])],32))]),_:1}),a(n(V),{class:"app-info"},{header:i(()=>[...e[12]||(e[12]=[t("h3",null,"애플리케이션 정보",-1)])]),default:i(()=>[e[13]||(e[13]=t("div",{class:"info-list"},[t("div",{class:"info-item"},[t("span",{class:"label"},"버전"),t("span",{class:"value"},"1.0.0")]),t("div",{class:"info-item"},[t("span",{class:"label"},"프레임워크"),t("span",{class:"value"},"Spring Boot 3.2 + Vue 3")]),t("div",{class:"info-item"},[t("span",{class:"label"},"데이터베이스"),t("span",{class:"value"},"SQLite (./data/loghunter.db)")])],-1))]),_:1})]))}},j=S(A,[["__scopeId","data-v-fdca948e"]]);export{j as default};

View File

@@ -1,4 +1,4 @@
var Yn=Object.defineProperty;var Un=(i,t,e)=>t in i?Yn(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var w=(i,t,e)=>Un(i,typeof t!="symbol"?t+"":t,e);import{x as Us,y as Xs,z as oi,A as Xn,r as Kn,o as qn,B as Gn,C as Zn,D as ri,E as Ks,p as Qn}from"./index-BB0X_WMV.js";/*!
var Yn=Object.defineProperty;var Un=(i,t,e)=>t in i?Yn(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var w=(i,t,e)=>Un(i,typeof t!="symbol"?t+"":t,e);import{x as Us,y as Xs,z as oi,A as Xn,r as Kn,o as qn,B as Gn,C as Zn,D as ri,E as Ks,p as Qn}from"./index-DFoOAXeQ.js";/*!
* @kurkle/color v0.3.4
* https://github.com/kurkle/color#readme
* (c) 2024 Jukka Kurkela

View File

@@ -18,7 +18,7 @@
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
</style>
<script type="module" crossorigin src="/assets/index-BB0X_WMV.js"></script>
<script type="module" crossorigin src="/assets/index-DFoOAXeQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BJ4-9mFZ.css">
</head>
<body>

Binary file not shown.

View File

@@ -1,39 +1,71 @@
<template>
<div class="pattern-manage">
<Card>
<template #header>
<div class="card-header-content">
<h3>패턴 관리</h3>
<Button @click="openAddModal">+ 패턴 추가</Button>
</div>
</template>
<div class="page-header">
<h2>패턴 관리</h2>
<Button @click="openAddModal">+ 패턴 추가</Button>
</div>
<DataTable
:columns="columns"
:data="patterns"
:loading="loading"
empty-text="등록된 패턴이 없습니다."
>
<template #severity="{ value }">
<Badge :variant="getSeverityVariant(value)">{{ value }}</Badge>
</template>
<template #regex="{ value }">
<code class="regex-code">{{ truncate(value, 50) }}</code>
</template>
<template #active="{ value }">
<Badge :variant="value ? 'success' : 'default'">
{{ value ? '활성' : '비활성' }}
</Badge>
</template>
<template #actions="{ row }">
<div class="action-buttons">
<Button size="sm" variant="secondary" @click="openTestModal(row)">테스트</Button>
<Button size="sm" @click="openEditModal(row)">수정</Button>
<Button size="sm" variant="danger" @click="confirmDelete(row)">삭제</Button>
<div v-if="loading" class="loading">
<p>로딩중...</p>
</div>
<div v-else-if="patterns.length === 0" class="empty-state">
<p>등록된 패턴이 없습니다.</p>
<Button @click="openAddModal"> 패턴 추가하기</Button>
</div>
<!-- 패턴 카드 그리드 -->
<div v-else class="pattern-grid">
<Card v-for="pattern in patterns" :key="pattern.id" class="pattern-card">
<template #header>
<div class="pattern-header">
<div class="pattern-title">
<Badge :variant="getSeverityVariant(pattern.severity)" size="sm">
{{ pattern.severity }}
</Badge>
<h4>{{ pattern.name }}</h4>
</div>
<Badge :variant="pattern.active ? 'success' : 'default'" size="sm">
{{ pattern.active ? '활성' : '비활성' }}
</Badge>
</div>
</template>
</DataTable>
</Card>
<div class="pattern-body">
<div class="pattern-info">
<label>정규식</label>
<code class="regex-box">{{ pattern.regex }}</code>
</div>
<div v-if="pattern.excludeRegex" class="pattern-info">
<label>제외 정규식</label>
<code class="regex-box exclude">{{ pattern.excludeRegex }}</code>
</div>
<div class="pattern-meta">
<span class="meta-item">
<span class="meta-label">컨텍스트</span>
<span class="meta-value">{{ pattern.contextLines }}</span>
</span>
<span v-if="pattern.description" class="meta-item description">
{{ pattern.description }}
</span>
</div>
</div>
<div class="pattern-actions">
<button class="action-btn test" @click="openTestModal(pattern)" title="테스트">
🧪 테스트
</button>
<button class="action-btn edit" @click="openEditModal(pattern)" title="수정">
수정
</button>
<button class="action-btn delete" @click="confirmDelete(pattern)" title="삭제">
🗑 삭제
</button>
</div>
</Card>
</div>
<!-- 패턴 추가/수정 모달 -->
<Modal v-model="showPatternModal" :title="isEdit ? '패턴 수정' : '패턴 추가'" width="600px">
@@ -154,17 +186,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { DataTable, Modal, FormInput, Button, Badge, Card } from '@/components'
import { Modal, FormInput, Button, Badge, Card } from '@/components'
import { patternApi } from '@/api'
const columns = [
{ key: 'name', label: '패턴명', width: '150px' },
{ key: 'regex', label: '정규식' },
{ key: 'severity', label: '심각도', width: '100px' },
{ key: 'contextLines', label: '컨텍스트', width: '90px' },
{ key: 'active', label: '상태', width: '80px' }
]
const severityOptions = [
{ value: 'CRITICAL', label: 'CRITICAL' },
{ value: 'ERROR', label: 'ERROR' },
@@ -323,40 +347,184 @@ const getSeverityVariant = (severity) => {
return map[severity] || 'default'
}
const truncate = (str, len) => {
if (!str) return ''
return str.length > len ? str.substring(0, len) + '...' : str
}
onMounted(() => {
loadPatterns()
})
</script>
<style scoped>
.card-header-content {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-header h2 {
margin: 0;
}
.loading,
.empty-state {
text-align: center;
padding: 60px;
background: white;
border-radius: 8px;
color: #666;
}
.empty-state p {
margin-bottom: 16px;
}
/* 패턴 그리드 */
.pattern-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.pattern-card {
transition: box-shadow 0.2s, transform 0.2s;
}
.pattern-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
.pattern-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header-content h3 {
margin: 0;
.pattern-title {
display: flex;
align-items: center;
gap: 10px;
}
.action-buttons {
.pattern-title h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.pattern-body {
padding: 4px 0;
}
.pattern-info {
margin-bottom: 12px;
}
.pattern-info label {
display: block;
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.regex-box {
display: block;
font-family: 'Consolas', 'Monaco', monospace;
background: #f1f3f5;
padding: 10px 12px;
border-radius: 6px;
font-size: 13px;
word-break: break-all;
line-height: 1.4;
border-left: 3px solid #3498db;
}
.regex-box.exclude {
border-left-color: #e67e22;
background: #fef5e7;
}
.pattern-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding-top: 8px;
border-top: 1px solid #eee;
font-size: 13px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.regex-code {
font-family: monospace;
background: #f1f3f4;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
.meta-label {
color: #888;
}
.meta-value {
font-weight: 500;
color: #333;
}
.meta-item.description {
flex-basis: 100%;
color: #666;
font-style: italic;
}
/* 액션 버튼 */
.pattern-actions {
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #eee;
margin-top: 12px;
}
.action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 10px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.action-btn.test {
background: #e8f4fd;
color: #2980b9;
}
.action-btn.test:hover {
background: #d4e9f7;
}
.action-btn.edit {
background: #fef3e2;
color: #d68910;
}
.action-btn.edit:hover {
background: #fce8c9;
}
.action-btn.delete {
background: #fdeaea;
color: #c0392b;
}
.action-btn.delete:hover {
background: #f9d6d6;
}
/* 폼 */
.form-group {
margin-bottom: 16px;
}
@@ -368,6 +536,7 @@ onMounted(() => {
cursor: pointer;
}
/* 테스트 섹션 */
.test-section {
display: flex;
flex-direction: column;

View File

@@ -1,40 +1,105 @@
<template>
<div class="server-manage">
<Card>
<template #header>
<div class="card-header-content">
<h3>서버 관리</h3>
<Button @click="openAddModal">+ 서버 추가</Button>
</div>
</template>
<div class="page-header">
<h2>서버 관리</h2>
<Button @click="openAddModal">+ 서버 추가</Button>
</div>
<DataTable
:columns="columns"
:data="servers"
:loading="loading"
empty-text="등록된 서버가 없습니다."
>
<template #active="{ value }">
<Badge :variant="value ? 'success' : 'default'">
{{ value ? '활성' : '비활성' }}
</Badge>
</template>
<template #authType="{ value }">
{{ value === 'PASSWORD' ? '비밀번호' : '키 파일' }}
</template>
<template #lastScanAt="{ value }">
{{ value ? formatDate(value) : '-' }}
</template>
<template #actions="{ row }">
<div class="action-buttons">
<Button size="sm" variant="success" @click="testConnection(row)" :loading="testingId === row.id">테스트</Button>
<Button size="sm" variant="secondary" @click="openLogPathModal(row)">경로</Button>
<Button size="sm" @click="openEditModal(row)">수정</Button>
<Button size="sm" variant="danger" @click="confirmDelete(row)">삭제</Button>
<div v-if="loading" class="loading">
<p>로딩중...</p>
</div>
<div v-else-if="servers.length === 0" class="empty-state">
<p>등록된 서버가 없습니다.</p>
<Button @click="openAddModal"> 서버 추가하기</Button>
</div>
<!-- 서버 카드 그리드 -->
<div v-else class="server-grid">
<Card v-for="server in servers" :key="server.id" class="server-card">
<template #header>
<div class="server-header">
<div class="server-title">
<Badge :variant="server.active ? 'success' : 'default'" size="sm">
{{ server.active ? '활성' : '비활성' }}
</Badge>
<h4>{{ server.name }}</h4>
</div>
</div>
</template>
</DataTable>
</Card>
<div class="server-body">
<div class="server-info-grid">
<div class="info-item">
<span class="info-icon">🌐</span>
<div class="info-content">
<span class="info-label">호스트</span>
<span class="info-value">{{ server.host }}:{{ server.port }}</span>
</div>
</div>
<div class="info-item">
<span class="info-icon">👤</span>
<div class="info-content">
<span class="info-label">사용자</span>
<span class="info-value">{{ server.username }}</span>
</div>
</div>
<div class="info-item">
<span class="info-icon">🔑</span>
<div class="info-content">
<span class="info-label">인증방식</span>
<span class="info-value">{{ server.authType === 'PASSWORD' ? '비밀번호' : '키 파일' }}</span>
</div>
</div>
<div class="info-item">
<span class="info-icon">📅</span>
<div class="info-content">
<span class="info-label">마지막 분석</span>
<span class="info-value">{{ server.lastScanAt ? formatDate(server.lastScanAt) : '-' }}</span>
</div>
</div>
</div>
</div>
<!-- 진행 상황 -->
<div v-if="progressMap[server.id]" class="progress-section">
<div class="progress-header">
<span class="status-text">{{ getStatusText(progressMap[server.id]) }}</span>
<Badge :variant="progressMap[server.id].status === 'RUNNING' ? 'warn' : (progressMap[server.id].status === 'SUCCESS' ? 'success' : 'error')">
{{ progressMap[server.id].status }}
</Badge>
</div>
<div class="progress-bar-container">
<div
class="progress-bar"
:style="{ width: getProgressPercent(progressMap[server.id]) + '%' }"
></div>
</div>
<div class="progress-details">
<span>파일: {{ progressMap[server.id].scannedFiles }} / {{ progressMap[server.id].totalFiles }}</span>
<span>에러: {{ progressMap[server.id].errorsFound }}</span>
</div>
</div>
<div class="server-actions">
<button class="action-btn scan" @click="scanServer(server)" :disabled="!server.active || scanningId === server.id">
{{ scanningId === server.id ? '⏳ 분석중...' : '▶️ 분석 실행' }}
</button>
<button class="action-btn test" @click="testConnection(server)" :disabled="testingId === server.id">
{{ testingId === server.id ? '⏳' : '🔌' }}
</button>
<button class="action-btn path" @click="openLogPathModal(server)">
📁
</button>
<button class="action-btn edit" @click="openEditModal(server)">
</button>
<button class="action-btn delete" @click="confirmDelete(server)">
🗑
</button>
</div>
</Card>
</div>
<!-- 서버 추가/수정 모달 -->
<Modal v-model="showServerModal" :title="isEdit ? '서버 수정' : '서버 추가'" width="500px">
@@ -106,10 +171,10 @@
</Modal>
<!-- 로그 경로 관리 모달 -->
<Modal v-model="showLogPathModal" :title="`로그 경로 관리 - ${selectedServer?.name || ''}`" width="700px">
<Modal v-model="showLogPathModal" :title="`로그 경로 관리 - ${selectedServer?.name || ''}`" width="750px">
<div class="log-path-section">
<div class="log-path-form">
<h4>경로 추가</h4>
<h4> 경로 추가</h4>
<div class="log-path-inputs">
<FormInput
v-model="logPathForm.path"
@@ -119,7 +184,7 @@
<FormInput
v-model="logPathForm.filePattern"
label="파일 패턴"
placeholder="예: *.log, catalina.*.log"
placeholder="예: *.log"
/>
<FormInput
v-model="logPathForm.description"
@@ -128,39 +193,36 @@
/>
</div>
<Button size="sm" @click="addLogPath" :disabled="!logPathForm.path || !logPathForm.filePattern">
추가
경로 추가
</Button>
</div>
<div class="log-path-list">
<h4>등록된 경로</h4>
<table v-if="logPaths.length > 0">
<thead>
<tr>
<th>경로</th>
<th>파일 패턴</th>
<th>설명</th>
<th>활성</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="lp in logPaths" :key="lp.id">
<td>{{ lp.path }}</td>
<td>{{ lp.filePattern }}</td>
<td>{{ lp.description || '-' }}</td>
<td>
<Badge :variant="lp.active ? 'success' : 'default'">
{{ lp.active ? 'Y' : 'N' }}
<h4>📂 등록된 경로 ({{ logPaths.length }})</h4>
<div v-if="logPaths.length === 0" class="empty-paths">
등록된 경로가 없습니다.
</div>
<div v-else class="path-cards">
<div v-for="lp in logPaths" :key="lp.id" class="path-card">
<div class="path-info">
<div class="path-main">
<code class="path-value">{{ lp.path }}</code>
<span class="path-pattern">{{ lp.filePattern }}</span>
</div>
<div class="path-meta">
<span v-if="lp.description" class="path-desc">{{ lp.description }}</span>
<Badge :variant="lp.active ? 'success' : 'default'" size="sm">
{{ lp.active ? '활성' : '비활성' }}
</Badge>
</td>
<td>
<Button size="sm" variant="danger" @click="deleteLogPath(lp.id)">삭제</Button>
</td>
</tr>
</tbody>
</table>
<p v-else class="empty-text">등록된 경로가 없습니다.</p>
</div>
</div>
<button class="path-delete" @click="deleteLogPath(lp.id)" title="삭제">
🗑
</button>
</div>
</div>
</div>
</div>
<template #footer>
@@ -171,7 +233,7 @@
<!-- 삭제 확인 모달 -->
<Modal v-model="showDeleteModal" title="서버 삭제" width="400px">
<p>정말로 <strong>{{ deleteTarget?.name }}</strong> 서버를 삭제하시겠습니까?</p>
<p class="warning-text">관련된 모든 로그 경로와 에러 이력도 함께 삭제됩니다.</p>
<p class="warning-text"> 관련된 모든 로그 경로와 에러 이력도 함께 삭제됩니다.</p>
<template #footer>
<Button variant="secondary" @click="showDeleteModal = false">취소</Button>
<Button variant="danger" @click="deleteServer" :loading="deleting">삭제</Button>
@@ -197,17 +259,8 @@
<script setup>
import { ref, onMounted } from 'vue'
import { DataTable, Modal, FormInput, Button, Badge, Card } from '@/components'
import { serverApi, logPathApi } from '@/api'
const columns = [
{ key: 'name', label: '서버명', width: '150px' },
{ key: 'host', label: '호스트' },
{ key: 'port', label: '포트', width: '80px' },
{ key: 'authType', label: '인증방식', width: '100px' },
{ key: 'active', label: '상태', width: '80px' },
{ key: 'lastScanAt', label: '마지막 분석', width: '150px' }
]
import { Modal, FormInput, Button, Badge, Card } from '@/components'
import { serverApi, logPathApi, scanApi } from '@/api'
const authTypeOptions = [
{ value: 'PASSWORD', label: '비밀번호' },
@@ -250,6 +303,10 @@ const logPathForm = ref({
const showDeleteModal = ref(false)
const deleteTarget = ref(null)
// Scan
const scanningId = ref(null)
const progressMap = ref({})
// Test Connection
const testingId = ref(null)
const showTestResultModal = ref(false)
@@ -348,6 +405,68 @@ const deleteServer = async () => {
}
}
// Scan Server
const scanServer = (server) => {
scanningId.value = server.id
progressMap.value[server.id] = {
status: 'RUNNING',
currentPath: '',
currentFile: '',
totalFiles: 0,
scannedFiles: 0,
errorsFound: 0
}
scanApi.startWithProgress(
server.id,
(progress) => {
progressMap.value[server.id] = progress
},
(result) => {
scanningId.value = null
if (result.success) {
progressMap.value[server.id] = {
...progressMap.value[server.id],
status: 'SUCCESS',
message: `완료: ${result.filesScanned}개 파일, ${result.errorsFound}개 에러`
}
} else {
progressMap.value[server.id] = {
...progressMap.value[server.id],
status: 'FAILED',
message: result.error
}
}
loadServers()
// 5초 후 진행상황 제거
setTimeout(() => {
delete progressMap.value[server.id]
}, 5000)
},
(error) => {
scanningId.value = null
progressMap.value[server.id] = {
...progressMap.value[server.id],
status: 'FAILED',
message: error
}
}
)
}
const getStatusText = (progress) => {
if (progress.status === 'SUCCESS') return progress.message || '완료'
if (progress.status === 'FAILED') return progress.message || '실패'
if (progress.currentFile) return `분석중: ${progress.currentFile}`
if (progress.currentPath) return `경로: ${progress.currentPath}`
return '준비중...'
}
const getProgressPercent = (progress) => {
if (progress.totalFiles === 0) return 0
return Math.round((progress.scannedFiles / progress.totalFiles) * 100)
}
// Test Connection
const testConnection = async (server) => {
testingId.value = server.id
@@ -415,21 +534,236 @@ onMounted(() => {
</script>
<style scoped>
.card-header-content {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-header h2 {
margin: 0;
}
.loading,
.empty-state {
text-align: center;
padding: 60px;
background: white;
border-radius: 8px;
color: #666;
}
.empty-state p {
margin-bottom: 16px;
}
/* 서버 그리드 */
.server-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 20px;
}
.server-card {
transition: box-shadow 0.2s, transform 0.2s;
}
.server-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header-content h3 {
margin: 0;
}
.action-buttons {
.server-title {
display: flex;
gap: 4px;
align-items: center;
gap: 10px;
}
.server-title h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.server-body {
padding: 4px 0;
}
/* 진행 상황 */
.progress-section {
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
margin-top: 12px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.status-text {
font-size: 13px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 250px;
}
.progress-bar-container {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-bar {
height: 100%;
background: #3498db;
transition: width 0.3s;
}
.progress-details {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #666;
}
.server-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.info-item {
display: flex;
align-items: flex-start;
gap: 8px;
}
.info-icon {
font-size: 16px;
line-height: 1;
margin-top: 2px;
}
.info-content {
display: flex;
flex-direction: column;
}
.info-label {
font-size: 11px;
color: #888;
text-transform: uppercase;
}
.info-value {
font-size: 14px;
font-weight: 500;
color: #333;
}
/* 액션 버튼 */
.server-actions {
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #eee;
margin-top: 12px;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 10px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.action-btn.scan {
flex: 1.5;
background: #3498db;
color: white;
}
.action-btn.scan:hover:not(:disabled) {
background: #2980b9;
}
.action-btn.scan:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-btn.test {
flex: 0;
padding: 10px 14px;
background: #e8f8f0;
color: #27ae60;
}
.action-btn.test:hover:not(:disabled) {
background: #d4f0e3;
}
.action-btn.test:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.action-btn.path {
flex: 0;
padding: 10px 14px;
background: #e8f4fd;
color: #2980b9;
}
.action-btn.path:hover {
background: #d4e9f7;
}
.action-btn.edit {
flex: 0;
padding: 10px 14px;
background: #fef3e2;
color: #d68910;
}
.action-btn.edit:hover {
background: #fce8c9;
}
.action-btn.delete {
flex: 0;
padding: 10px 14px;
background: #fdeaea;
color: #c0392b;
}
.action-btn.delete:hover {
background: #f9d6d6;
}
/* 폼 */
.form-group {
margin-bottom: 16px;
}
@@ -441,6 +775,7 @@ onMounted(() => {
cursor: pointer;
}
/* 로그 경로 섹션 */
.log-path-section h4 {
margin: 0 0 12px 0;
font-size: 14px;
@@ -456,33 +791,90 @@ onMounted(() => {
.log-path-inputs {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-columns: 1.5fr 1fr 1fr;
gap: 12px;
margin-bottom: 12px;
}
.log-path-list table {
width: 100%;
border-collapse: collapse;
.log-path-list {
padding: 16px;
background: white;
border: 1px solid #eee;
border-radius: 8px;
}
.log-path-list th,
.log-path-list td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.log-path-list th {
background: #f8f9fa;
font-weight: 600;
font-size: 13px;
}
.empty-text {
color: #6c757d;
.empty-paths {
text-align: center;
padding: 20px;
color: #888;
}
.path-cards {
display: flex;
flex-direction: column;
gap: 10px;
}
.path-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 6px;
border-left: 3px solid #3498db;
}
.path-info {
flex: 1;
}
.path-main {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
}
.path-value {
font-family: monospace;
font-size: 13px;
background: #e9ecef;
padding: 4px 8px;
border-radius: 4px;
}
.path-pattern {
font-size: 12px;
color: #666;
background: #fff3cd;
padding: 2px 8px;
border-radius: 4px;
}
.path-meta {
display: flex;
align-items: center;
gap: 8px;
}
.path-desc {
font-size: 12px;
color: #888;
}
.path-delete {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 8px;
border-radius: 4px;
transition: background 0.2s;
}
.path-delete:hover {
background: #fdeaea;
}
.warning-text {

View File

@@ -1,5 +1,6 @@
package research.loghunter.service;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -7,6 +8,10 @@ import org.springframework.transaction.annotation.Transactional;
import research.loghunter.entity.*;
import research.loghunter.repository.*;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -32,6 +37,8 @@ public class ScanService {
private final ScannedFileRepository scannedFileRepository;
private final SftpService sftpService;
private final LogParserService logParserService;
private final EntityManager entityManager;
private final DataSource dataSource;
// 진행 상황 저장 (serverId -> ScanProgress)
private final ConcurrentHashMap<Long, ScanProgress> progressMap = new ConcurrentHashMap<>();
@@ -241,6 +248,8 @@ public class ScanService {
return new ScanResult(server.getId(), server.getName(), false, 0, 0, e.getMessage());
} finally {
progressMap.remove(server.getId());
// DB 최적화 (삭제된 데이터 공간 회수)
vacuumDatabase();
}
}
@@ -400,6 +409,23 @@ public class ScanService {
}
}
/**
* SQLite VACUUM 실행 (DB 최적화)
* 삭제된 데이터가 차지하는 공간을 회수함
*/
private void vacuumDatabase() {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
log.info("Running VACUUM to optimize database...");
long startTime = System.currentTimeMillis();
stmt.execute("VACUUM");
long elapsed = System.currentTimeMillis() - startTime;
log.info("VACUUM completed in {}ms", elapsed);
} catch (Exception e) {
log.warn("Failed to vacuum database: {}", e.getMessage());
}
}
// DTOs
public static class ScanProgress {
private Long serverId;

View File

@@ -1 +1 @@
import"./index-09HB4Lmg.js";import{_ as e,a as s,h as r,G as n,i as o,t as c,n as d}from"./index-BB0X_WMV.js";const l={__name:"Badge",props:{text:String,variant:{type:String,default:"default"}},setup(a){return(t,i)=>(r(),s("span",{class:d(["badge",`badge-${a.variant}`])},[n(t.$slots,"default",{},()=>[o(c(a.text),1)])],2))}},g=e(l,[["__scopeId","data-v-b7bd2350"]]);export{g as B};
import"./index--fsvNaiQ.js";import{_ as e,a as s,h as r,G as n,i as o,t as c,n as d}from"./index-DFoOAXeQ.js";const l={__name:"Badge",props:{text:String,variant:{type:String,default:"default"}},setup(a){return(t,i)=>(r(),s("span",{class:d(["badge",`badge-${a.variant}`])},[n(t.$slots,"default",{},()=>[o(c(a.text),1)])],2))}},g=e(l,[["__scopeId","data-v-b7bd2350"]]);export{g as B};

View File

@@ -1 +1 @@
import"./index-09HB4Lmg.js";import{_ as d,r,a,e as c,G as u,n as f,h as o}from"./index-BB0X_WMV.js";const m=["type","disabled"],b={key:0,class:"spinner"},y={__name:"Button",props:{type:{type:String,default:"button"},variant:{type:String,default:"primary"},size:{type:String,default:"md"},disabled:Boolean,loading:Boolean},emits:["click"],setup(e,{expose:l}){const n=r(null);return l({focus:()=>{var t;(t=n.value)==null||t.focus()}}),(t,s)=>(o(),a("button",{ref_key:"buttonRef",ref:n,type:e.type,class:f(["btn",`btn-${e.variant}`,{"btn-sm":e.size==="sm","btn-lg":e.size==="lg"}]),disabled:e.disabled||e.loading,onClick:s[0]||(s[0]=i=>t.$emit("click",i))},[e.loading?(o(),a("span",b)):c("",!0),u(t.$slots,"default",{},void 0)],10,m))}},B=d(y,[["__scopeId","data-v-c92354e1"]]);export{B};
import"./index--fsvNaiQ.js";import{_ as d,r,a,e as c,G as u,n as f,h as o}from"./index-DFoOAXeQ.js";const m=["type","disabled"],b={key:0,class:"spinner"},y={__name:"Button",props:{type:{type:String,default:"button"},variant:{type:String,default:"primary"},size:{type:String,default:"md"},disabled:Boolean,loading:Boolean},emits:["click"],setup(e,{expose:l}){const n=r(null);return l({focus:()=>{var t;(t=n.value)==null||t.focus()}}),(t,s)=>(o(),a("button",{ref_key:"buttonRef",ref:n,type:e.type,class:f(["btn",`btn-${e.variant}`,{"btn-sm":e.size==="sm","btn-lg":e.size==="lg"}]),disabled:e.disabled||e.loading,onClick:s[0]||(s[0]=i=>t.$emit("click",i))},[e.loading?(o(),a("span",b)):c("",!0),u(t.$slots,"default",{},void 0)],10,m))}},B=d(y,[["__scopeId","data-v-c92354e1"]]);export{B};

View File

@@ -1 +1 @@
import{_ as C,r as g,o as x,a as l,b as s,l as w,s as I,t as d,F as B,g as T,h as o,d as R,w as m,f as L,u as _}from"./index-BB0X_WMV.js";import{a as A,C as F}from"./index-09HB4Lmg.js";import{C as O,a as W,L as N,B as V,p as E,b as M,c as $,d as P,e as z}from"./chartjs-plugin-datalabels.esm-DiDzp_cw.js";const U={class:"daily-stats"},Y={class:"page-header"},Z={class:"filter-section"},j={key:0,class:"loading"},q={key:1,class:"no-data"},G={key:2,class:"server-charts"},H={class:"chart-header"},J={class:"chart-subtitle"},K={class:"chart-wrapper"},Q={class:"chart-container"},X={__name:"DailyStats",setup(tt){O.register(W,N,V,E,M,$,P);const c=g(!1),r=g([]),n=g(f());function f(){return new Date().toISOString().split("T")[0]}function b(){const t=new Date(n.value);t.setDate(t.getDate()-1),n.value=t.toISOString().split("T")[0],i()}function y(){const t=new Date(n.value);t.setDate(t.getDate()+1),n.value=t.toISOString().split("T")[0],i()}const S={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"top"},tooltip:{mode:"index",intersect:!1},datalabels:{display:t=>{if(t.datasetIndex!==2)return!1;const e=t.chart.data.datasets,a=t.dataIndex;return e.reduce((u,p)=>u+(p.data[a]||0),0)>0},anchor:"end",align:"end",offset:0,font:{size:9},color:"#666",formatter:(t,e)=>{const a=e.chart.data.datasets,v=e.dataIndex;return a.reduce((u,p)=>u+(p.data[v]||0),0)}}},scales:{x:{stacked:!0,ticks:{maxRotation:0,autoSkip:!0,maxTicksLimit:24,callback:function(t,e){const a=this.getLabelForValue(t);return a&&a.endsWith(":00")?a:""}},grid:{display:!1}},y:{stacked:!0,beginAtZero:!0}},barPercentage:.8,categoryPercentage:.9},D=t=>({labels:t.timeStats.map(a=>a.time),datasets:[{label:"CRITICAL",data:t.timeStats.map(a=>a.critical),backgroundColor:"#9b59b6",borderWidth:0},{label:"ERROR",data:t.timeStats.map(a=>a.error),backgroundColor:"#e74c3c",borderWidth:0},{label:"WARN",data:t.timeStats.map(a=>a.warn),backgroundColor:"#f39c12",borderWidth:0}]}),i=async()=>{c.value=!0;try{r.value=await A.getTimeStatsByServer(n.value,15)}catch(t){console.error("Failed to load stats:",t),r.value=[]}finally{c.value=!1}},h=t=>{const e=new Date(t);return`${e.getFullYear()}${e.getMonth()+1}${e.getDate()}`},k=t=>t.timeStats.reduce((e,a)=>e+a.total,0);return x(()=>{i()}),(t,e)=>(o(),l("div",U,[s("div",Y,[e[1]||(e[1]=s("h2",null,"일별 에러현황",-1)),s("div",Z,[s("button",{class:"nav-btn",onClick:b},"◀ 이전"),w(s("input",{type:"date","onUpdate:modelValue":e[0]||(e[0]=a=>n.value=a),onChange:i},null,544),[[I,n.value]]),s("button",{class:"nav-btn",onClick:y},"다음 ▶")])]),c.value?(o(),l("div",j,[...e[2]||(e[2]=[s("p",null,"로딩중...",-1)])])):r.value.length===0?(o(),l("div",q,[s("p",null,d(h(n.value))+"에 분석된 에러 데이터가 없습니다.",1)])):(o(),l("div",G,[(o(!0),l(B,null,T(r.value,a=>(o(),R(_(F),{key:a.serverId,class:"server-chart-card"},{header:m(()=>[s("div",H,[s("h3",null,"🖥️ "+d(a.serverName),1),s("span",J,d(h(n.value))+" 15분 단위 에러 ("+d(k(a))+"건)",1)])]),default:m(()=>[s("div",K,[s("div",Q,[L(_(z),{data:D(a),options:S},null,8,["data"])])])]),_:2},1024))),128))]))]))}},nt=C(X,[["__scopeId","data-v-b7241be8"]]);export{nt as default};
import{_ as C,r as g,o as x,a as l,b as s,l as w,s as I,t as d,F as B,g as T,h as o,d as R,w as m,f as L,u as _}from"./index-DFoOAXeQ.js";import{a as A,C as F}from"./index--fsvNaiQ.js";import{C as O,a as W,L as N,B as V,p as E,b as M,c as $,d as P,e as z}from"./chartjs-plugin-datalabels.esm-B8xtR40N.js";const U={class:"daily-stats"},Y={class:"page-header"},Z={class:"filter-section"},j={key:0,class:"loading"},q={key:1,class:"no-data"},G={key:2,class:"server-charts"},H={class:"chart-header"},J={class:"chart-subtitle"},K={class:"chart-wrapper"},Q={class:"chart-container"},X={__name:"DailyStats",setup(tt){O.register(W,N,V,E,M,$,P);const c=g(!1),r=g([]),n=g(f());function f(){return new Date().toISOString().split("T")[0]}function b(){const t=new Date(n.value);t.setDate(t.getDate()-1),n.value=t.toISOString().split("T")[0],i()}function y(){const t=new Date(n.value);t.setDate(t.getDate()+1),n.value=t.toISOString().split("T")[0],i()}const S={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"top"},tooltip:{mode:"index",intersect:!1},datalabels:{display:t=>{if(t.datasetIndex!==2)return!1;const e=t.chart.data.datasets,a=t.dataIndex;return e.reduce((u,p)=>u+(p.data[a]||0),0)>0},anchor:"end",align:"end",offset:0,font:{size:9},color:"#666",formatter:(t,e)=>{const a=e.chart.data.datasets,v=e.dataIndex;return a.reduce((u,p)=>u+(p.data[v]||0),0)}}},scales:{x:{stacked:!0,ticks:{maxRotation:0,autoSkip:!0,maxTicksLimit:24,callback:function(t,e){const a=this.getLabelForValue(t);return a&&a.endsWith(":00")?a:""}},grid:{display:!1}},y:{stacked:!0,beginAtZero:!0}},barPercentage:.8,categoryPercentage:.9},D=t=>({labels:t.timeStats.map(a=>a.time),datasets:[{label:"CRITICAL",data:t.timeStats.map(a=>a.critical),backgroundColor:"#9b59b6",borderWidth:0},{label:"ERROR",data:t.timeStats.map(a=>a.error),backgroundColor:"#e74c3c",borderWidth:0},{label:"WARN",data:t.timeStats.map(a=>a.warn),backgroundColor:"#f39c12",borderWidth:0}]}),i=async()=>{c.value=!0;try{r.value=await A.getTimeStatsByServer(n.value,15)}catch(t){console.error("Failed to load stats:",t),r.value=[]}finally{c.value=!1}},h=t=>{const e=new Date(t);return`${e.getFullYear()}${e.getMonth()+1}${e.getDate()}`},k=t=>t.timeStats.reduce((e,a)=>e+a.total,0);return x(()=>{i()}),(t,e)=>(o(),l("div",U,[s("div",Y,[e[1]||(e[1]=s("h2",null,"일별 에러현황",-1)),s("div",Z,[s("button",{class:"nav-btn",onClick:b},"◀ 이전"),w(s("input",{type:"date","onUpdate:modelValue":e[0]||(e[0]=a=>n.value=a),onChange:i},null,544),[[I,n.value]]),s("button",{class:"nav-btn",onClick:y},"다음 ▶")])]),c.value?(o(),l("div",j,[...e[2]||(e[2]=[s("p",null,"로딩중...",-1)])])):r.value.length===0?(o(),l("div",q,[s("p",null,d(h(n.value))+"에 분석된 에러 데이터가 없습니다.",1)])):(o(),l("div",G,[(o(!0),l(B,null,T(r.value,a=>(o(),R(_(F),{key:a.serverId,class:"server-chart-card"},{header:m(()=>[s("div",H,[s("h3",null,"🖥️ "+d(a.serverName),1),s("span",J,d(h(n.value))+" 15분 단위 에러 ("+d(k(a))+"건)",1)])]),default:m(()=>[s("div",K,[s("div",Q,[L(_(z),{data:D(a),options:S},null,8,["data"])])])]),_:2},1024))),128))]))]))}},nt=C(X,[["__scopeId","data-v-b7241be8"]]);export{nt as default};

View File

@@ -1 +0,0 @@
import"./index-09HB4Lmg.js";import{_ as h,a as e,h as a,b as o,e as u,F as d,g as i,j as f,t as c,G as y,i as g}from"./index-BB0X_WMV.js";const b={class:"data-table-wrapper"},p={class:"data-table"},$={key:0,class:"actions-col"},_={key:0},D=["colspan"],S={key:1},T=["colspan"],B=["onClick"],N={key:0,class:"actions-col"},V={__name:"DataTable",props:{columns:{type:Array,required:!0},data:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},emptyText:{type:String,default:"데이터가 없습니다."}},emits:["row-click"],setup(n){const m=(t,r)=>t==null?"-":r.type==="date"&&t?new Date(t).toLocaleString("ko-KR"):r.type==="boolean"?t?"Y":"N":t;return(t,r)=>(a(),e("div",b,[o("table",p,[o("thead",null,[o("tr",null,[(a(!0),e(d,null,i(n.columns,s=>(a(),e("th",{key:s.key,style:f({width:s.width})},c(s.label),5))),128)),t.$slots.actions?(a(),e("th",$,"작업")):u("",!0)])]),o("tbody",null,[n.loading?(a(),e("tr",_,[o("td",{colspan:n.columns.length+(t.$slots.actions?1:0),class:"loading-cell"}," 로딩 중... ",8,D)])):!n.data||n.data.length===0?(a(),e("tr",S,[o("td",{colspan:n.columns.length+(t.$slots.actions?1:0),class:"empty-cell"},c(n.emptyText),9,T)])):(a(!0),e(d,{key:2},i(n.data,(s,k)=>(a(),e("tr",{key:s.id||k,onClick:l=>t.$emit("row-click",s)},[(a(!0),e(d,null,i(n.columns,l=>(a(),e("td",{key:l.key},[y(t.$slots,l.key,{row:s,value:s[l.key]},()=>[g(c(m(s[l.key],l)),1)])]))),128)),t.$slots.actions?(a(),e("td",N,[y(t.$slots,"actions",{row:s},void 0)])):u("",!0)],8,B))),128))])])]))}},A=h(V,[["__scopeId","data-v-db5e24a9"]]);export{A as D};

View File

@@ -1 +1 @@
import"./index-09HB4Lmg.js";import{_ as i,c as s,a as t,h as l,e as o,i as c,t as n,F as m,g as y}from"./index-BB0X_WMV.js";const h={class:"form-group"},v=["for"],b={key:0,class:"required"},g=["id","type","value","placeholder","disabled","readonly"],f=["id","value","placeholder","disabled","readonly","rows"],k=["id","value","disabled"],V={key:0,value:""},S=["value"],x={key:4,class:"error-text"},I={key:5,class:"hint-text"},B={__name:"FormInput",props:{modelValue:{type:[String,Number],default:""},label:String,type:{type:String,default:"text"},placeholder:String,required:Boolean,disabled:Boolean,readonly:Boolean,error:String,hint:String,rows:{type:Number,default:3},options:{type:Array,default:()=>[]}},emits:["update:modelValue"],setup(e){const r=s(()=>`input-${Math.random().toString(36).slice(2,9)}`);return(u,d)=>(l(),t("div",h,[e.label?(l(),t("label",{key:0,for:r.value},[c(n(e.label)+" ",1),e.required?(l(),t("span",b,"*")):o("",!0)],8,v)):o("",!0),e.type!=="textarea"&&e.type!=="select"?(l(),t("input",{key:1,id:r.value,type:e.type,value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,readonly:e.readonly,class:"form-input",onInput:d[0]||(d[0]=a=>u.$emit("update:modelValue",a.target.value))},null,40,g)):e.type==="textarea"?(l(),t("textarea",{key:2,id:r.value,value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,readonly:e.readonly,rows:e.rows,class:"form-input",onInput:d[1]||(d[1]=a=>u.$emit("update:modelValue",a.target.value))},null,40,f)):e.type==="select"?(l(),t("select",{key:3,id:r.value,value:e.modelValue,disabled:e.disabled,class:"form-input",onChange:d[2]||(d[2]=a=>u.$emit("update:modelValue",a.target.value))},[e.placeholder?(l(),t("option",V,n(e.placeholder),1)):o("",!0),(l(!0),t(m,null,y(e.options,a=>(l(),t("option",{key:a.value,value:a.value},n(a.label),9,S))),128))],40,k)):o("",!0),e.error?(l(),t("span",x,n(e.error),1)):o("",!0),e.hint?(l(),t("span",I,n(e.hint),1)):o("",!0)]))}},N=i(B,[["__scopeId","data-v-45f49038"]]);export{N as F};
import"./index--fsvNaiQ.js";import{_ as i,c as s,a as t,h as l,e as o,i as c,t as n,F as m,g as y}from"./index-DFoOAXeQ.js";const h={class:"form-group"},v=["for"],b={key:0,class:"required"},g=["id","type","value","placeholder","disabled","readonly"],f=["id","value","placeholder","disabled","readonly","rows"],k=["id","value","disabled"],V={key:0,value:""},S=["value"],x={key:4,class:"error-text"},I={key:5,class:"hint-text"},B={__name:"FormInput",props:{modelValue:{type:[String,Number],default:""},label:String,type:{type:String,default:"text"},placeholder:String,required:Boolean,disabled:Boolean,readonly:Boolean,error:String,hint:String,rows:{type:Number,default:3},options:{type:Array,default:()=>[]}},emits:["update:modelValue"],setup(e){const r=s(()=>`input-${Math.random().toString(36).slice(2,9)}`);return(u,d)=>(l(),t("div",h,[e.label?(l(),t("label",{key:0,for:r.value},[c(n(e.label)+" ",1),e.required?(l(),t("span",b,"*")):o("",!0)],8,v)):o("",!0),e.type!=="textarea"&&e.type!=="select"?(l(),t("input",{key:1,id:r.value,type:e.type,value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,readonly:e.readonly,class:"form-input",onInput:d[0]||(d[0]=a=>u.$emit("update:modelValue",a.target.value))},null,40,g)):e.type==="textarea"?(l(),t("textarea",{key:2,id:r.value,value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,readonly:e.readonly,rows:e.rows,class:"form-input",onInput:d[1]||(d[1]=a=>u.$emit("update:modelValue",a.target.value))},null,40,f)):e.type==="select"?(l(),t("select",{key:3,id:r.value,value:e.modelValue,disabled:e.disabled,class:"form-input",onChange:d[2]||(d[2]=a=>u.$emit("update:modelValue",a.target.value))},[e.placeholder?(l(),t("option",V,n(e.placeholder),1)):o("",!0),(l(!0),t(m,null,y(e.options,a=>(l(),t("option",{key:a.value,value:a.value},n(a.label),9,S))),128))],40,k)):o("",!0),e.error?(l(),t("span",x,n(e.error),1)):o("",!0),e.hint?(l(),t("span",I,n(e.hint),1)):o("",!0)]))}},N=i(B,[["__scopeId","data-v-45f49038"]]);export{N as F};

View File

@@ -1 +1 @@
import{_ as r,d as m,a as d,e as i,b as e,t as u,G as c,j as f,m as p,T as h,h as s}from"./index-BB0X_WMV.js";import"./index-09HB4Lmg.js";const y={class:"modal-header"},_={class:"modal-body"},k={key:0,class:"modal-footer"},v={__name:"Modal",props:{modelValue:{type:Boolean,default:!1},title:{type:String,default:""},width:{type:String,default:"500px"}},emits:["update:modelValue","close"],setup(t,{emit:n}){const a=n,l=()=>{a("update:modelValue",!1),a("close")};return(o,V)=>(s(),m(h,{to:"body"},[t.modelValue?(s(),d("div",{key:0,class:"modal-overlay",onClick:p(l,["self"])},[e("div",{class:"modal",style:f({width:t.width})},[e("div",y,[e("h3",null,u(t.title),1),e("button",{class:"close-btn",onClick:l},"×")]),e("div",_,[c(o.$slots,"default",{},void 0)]),o.$slots.footer?(s(),d("div",k,[c(o.$slots,"footer",{},void 0)])):i("",!0)],4)])):i("",!0)]))}},S=r(v,[["__scopeId","data-v-90993dd3"]]);export{S as M};
import{_ as r,d as m,a as d,e as i,b as e,t as u,G as c,j as f,m as p,T as h,h as s}from"./index-DFoOAXeQ.js";import"./index--fsvNaiQ.js";const y={class:"modal-header"},_={class:"modal-body"},k={key:0,class:"modal-footer"},v={__name:"Modal",props:{modelValue:{type:Boolean,default:!1},title:{type:String,default:""},width:{type:String,default:"500px"}},emits:["update:modelValue","close"],setup(t,{emit:n}){const a=n,l=()=>{a("update:modelValue",!1),a("close")};return(o,V)=>(s(),m(h,{to:"body"},[t.modelValue?(s(),d("div",{key:0,class:"modal-overlay",onClick:p(l,["self"])},[e("div",{class:"modal",style:f({width:t.width})},[e("div",y,[e("h3",null,u(t.title),1),e("button",{class:"close-btn",onClick:l},"×")]),e("div",_,[c(o.$slots,"default",{},void 0)]),o.$slots.footer?(s(),d("div",k,[c(o.$slots,"footer",{},void 0)])):i("",!0)],4)])):i("",!0)]))}},S=r(v,[["__scopeId","data-v-90993dd3"]]);export{S as M};

View File

@@ -1 +1 @@
import{_ as M,r as g,o as w,a as l,b as s,l as x,s as R,t as c,F as $,g as B,h as o,d as D,w as v,f as I,u as _}from"./index-BB0X_WMV.js";import{a as N,C as F}from"./index-09HB4Lmg.js";import{C as A,a as L,L as E,B as T,p as V,b as Y,c as O,d as z,e as U}from"./chartjs-plugin-datalabels.esm-DiDzp_cw.js";const W={class:"monthly-stats"},Z={class:"page-header"},j={class:"filter-section"},q={key:0,class:"loading"},G={key:1,class:"no-data"},H={key:2,class:"server-charts"},J={class:"chart-header"},K={class:"chart-subtitle"},P={class:"chart-container"},Q={__name:"MonthlyStats",setup(X){A.register(L,E,T,V,Y,O,z);const i=g(!1),r=g([]),n=g(f());function f(){const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}function b(){const[a,e]=n.value.split("-").map(Number),t=new Date(a,e-2,1);n.value=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`,d()}function y(){const[a,e]=n.value.split("-").map(Number),t=new Date(a,e,1);n.value=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`,d()}const S={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"top"},tooltip:{mode:"index",intersect:!1},datalabels:{display:a=>{if(a.datasetIndex!==2)return!1;const e=a.chart.data.datasets,t=a.dataIndex;return e.reduce((p,h)=>p+(h.data[t]||0),0)>0},anchor:"end",align:"end",offset:2,font:{size:10,weight:"bold"},color:"#666",formatter:(a,e)=>{const t=e.chart.data.datasets,u=e.dataIndex;return t.reduce((p,h)=>p+(h.data[u]||0),0)}}},scales:{x:{stacked:!0},y:{stacked:!0,beginAtZero:!0}}},C=a=>({labels:a.dailyStats.map(t=>`${new Date(t.date).getDate()}`),datasets:[{label:"CRITICAL",data:a.dailyStats.map(t=>t.critical),backgroundColor:"#9b59b6",borderRadius:2},{label:"ERROR",data:a.dailyStats.map(t=>t.error),backgroundColor:"#e74c3c",borderRadius:2},{label:"WARN",data:a.dailyStats.map(t=>t.warn),backgroundColor:"#f39c12",borderRadius:2}]}),d=async()=>{i.value=!0;try{const[a,e]=n.value.split("-").map(Number);r.value=await N.getMonthlyStatsByServer(a,e)}catch(a){console.error("Failed to load stats:",a),r.value=[]}finally{i.value=!1}},m=a=>{const[e,t]=a.split("-");return`${e}${parseInt(t)}`},k=a=>a.dailyStats.reduce((e,t)=>e+t.total,0);return w(()=>{d()}),(a,e)=>(o(),l("div",W,[s("div",Z,[e[1]||(e[1]=s("h2",null,"월별 에러현황",-1)),s("div",j,[s("button",{class:"nav-btn",onClick:b},"◀ 이전"),x(s("input",{type:"month","onUpdate:modelValue":e[0]||(e[0]=t=>n.value=t),onChange:d},null,544),[[R,n.value]]),s("button",{class:"nav-btn",onClick:y},"다음 ▶")])]),i.value?(o(),l("div",q,[...e[2]||(e[2]=[s("p",null,"로딩중...",-1)])])):r.value.length===0?(o(),l("div",G,[s("p",null,c(m(n.value))+"에 분석된 에러 데이터가 없습니다.",1)])):(o(),l("div",H,[(o(!0),l($,null,B(r.value,t=>(o(),D(_(F),{key:t.serverId,class:"server-chart-card"},{header:v(()=>[s("div",J,[s("h3",null,"🖥️ "+c(t.serverName),1),s("span",K,c(m(n.value))+" 일별 에러 ("+c(k(t))+"건)",1)])]),default:v(()=>[s("div",P,[I(_(U),{data:C(t),options:S},null,8,["data"])])]),_:2},1024))),128))]))]))}},st=M(Q,[["__scopeId","data-v-7c1c78fe"]]);export{st as default};
import{_ as M,r as g,o as w,a as l,b as s,l as x,s as R,t as c,F as $,g as B,h as o,d as D,w as v,f as I,u as _}from"./index-DFoOAXeQ.js";import{a as N,C as F}from"./index--fsvNaiQ.js";import{C as A,a as L,L as E,B as T,p as V,b as Y,c as O,d as z,e as U}from"./chartjs-plugin-datalabels.esm-B8xtR40N.js";const W={class:"monthly-stats"},Z={class:"page-header"},j={class:"filter-section"},q={key:0,class:"loading"},G={key:1,class:"no-data"},H={key:2,class:"server-charts"},J={class:"chart-header"},K={class:"chart-subtitle"},P={class:"chart-container"},Q={__name:"MonthlyStats",setup(X){A.register(L,E,T,V,Y,O,z);const i=g(!1),r=g([]),n=g(f());function f(){const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}function b(){const[a,e]=n.value.split("-").map(Number),t=new Date(a,e-2,1);n.value=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`,d()}function y(){const[a,e]=n.value.split("-").map(Number),t=new Date(a,e,1);n.value=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`,d()}const S={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"top"},tooltip:{mode:"index",intersect:!1},datalabels:{display:a=>{if(a.datasetIndex!==2)return!1;const e=a.chart.data.datasets,t=a.dataIndex;return e.reduce((p,h)=>p+(h.data[t]||0),0)>0},anchor:"end",align:"end",offset:2,font:{size:10,weight:"bold"},color:"#666",formatter:(a,e)=>{const t=e.chart.data.datasets,u=e.dataIndex;return t.reduce((p,h)=>p+(h.data[u]||0),0)}}},scales:{x:{stacked:!0},y:{stacked:!0,beginAtZero:!0}}},C=a=>({labels:a.dailyStats.map(t=>`${new Date(t.date).getDate()}`),datasets:[{label:"CRITICAL",data:a.dailyStats.map(t=>t.critical),backgroundColor:"#9b59b6",borderRadius:2},{label:"ERROR",data:a.dailyStats.map(t=>t.error),backgroundColor:"#e74c3c",borderRadius:2},{label:"WARN",data:a.dailyStats.map(t=>t.warn),backgroundColor:"#f39c12",borderRadius:2}]}),d=async()=>{i.value=!0;try{const[a,e]=n.value.split("-").map(Number);r.value=await N.getMonthlyStatsByServer(a,e)}catch(a){console.error("Failed to load stats:",a),r.value=[]}finally{i.value=!1}},m=a=>{const[e,t]=a.split("-");return`${e}${parseInt(t)}`},k=a=>a.dailyStats.reduce((e,t)=>e+t.total,0);return w(()=>{d()}),(a,e)=>(o(),l("div",W,[s("div",Z,[e[1]||(e[1]=s("h2",null,"월별 에러현황",-1)),s("div",j,[s("button",{class:"nav-btn",onClick:b},"◀ 이전"),x(s("input",{type:"month","onUpdate:modelValue":e[0]||(e[0]=t=>n.value=t),onChange:d},null,544),[[R,n.value]]),s("button",{class:"nav-btn",onClick:y},"다음 ▶")])]),i.value?(o(),l("div",q,[...e[2]||(e[2]=[s("p",null,"로딩중...",-1)])])):r.value.length===0?(o(),l("div",G,[s("p",null,c(m(n.value))+"에 분석된 에러 데이터가 없습니다.",1)])):(o(),l("div",H,[(o(!0),l($,null,B(r.value,t=>(o(),D(_(F),{key:t.serverId,class:"server-chart-card"},{header:v(()=>[s("div",J,[s("h3",null,"🖥️ "+c(t.serverName),1),s("span",K,c(m(n.value))+" 일별 에러 ("+c(k(t))+"건)",1)])]),default:v(()=>[s("div",P,[I(_(U),{data:C(t),options:S},null,8,["data"])])]),_:2},1024))),128))]))]))}},st=M(Q,[["__scopeId","data-v-7c1c78fe"]]);export{st as default};

View File

@@ -1 +0,0 @@
.card-header-content[data-v-fc6fccb5]{display:flex;justify-content:space-between;align-items:center}.card-header-content h3[data-v-fc6fccb5]{margin:0}.action-buttons[data-v-fc6fccb5]{display:flex;gap:4px}.regex-code[data-v-fc6fccb5]{font-family:monospace;background:#f1f3f4;padding:2px 6px;border-radius:3px;font-size:12px}.form-group[data-v-fc6fccb5]{margin-bottom:16px}.form-group label[data-v-fc6fccb5]{display:flex;align-items:center;gap:8px;cursor:pointer}.test-section[data-v-fc6fccb5]{display:flex;flex-direction:column;gap:16px}.test-pattern label[data-v-fc6fccb5]{display:block;font-weight:500;margin-bottom:6px}.regex-display[data-v-fc6fccb5]{display:block;font-family:monospace;background:#f8f9fa;padding:12px;border-radius:4px;font-size:13px;word-break:break-all}.test-result[data-v-fc6fccb5]{padding:16px;border-radius:8px;margin-top:8px}.test-result.success[data-v-fc6fccb5]{background:#d4edda;border:1px solid #c3e6cb}.test-result.fail[data-v-fc6fccb5]{background:#f8d7da;border:1px solid #f5c6cb}.test-result h4[data-v-fc6fccb5]{margin:0 0 12px}.test-result p[data-v-fc6fccb5]{margin:0}.match-info[data-v-fc6fccb5]{margin-top:8px}.match-info label[data-v-fc6fccb5]{font-weight:500;margin-right:8px}.match-info code[data-v-fc6fccb5]{background:#0000001a;padding:2px 6px;border-radius:3px}.error-msg[data-v-fc6fccb5]{color:#721c24}

View File

@@ -0,0 +1 @@
.page-header[data-v-821062ae]{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header h2[data-v-821062ae]{margin:0}.loading[data-v-821062ae],.empty-state[data-v-821062ae]{text-align:center;padding:60px;background:#fff;border-radius:8px;color:#666}.empty-state p[data-v-821062ae]{margin-bottom:16px}.pattern-grid[data-v-821062ae]{display:grid;grid-template-columns:repeat(auto-fill,minmax(400px,1fr));gap:20px}.pattern-card[data-v-821062ae]{transition:box-shadow .2s,transform .2s}.pattern-card[data-v-821062ae]:hover{box-shadow:0 4px 12px #00000026;transform:translateY(-2px)}.pattern-header[data-v-821062ae]{display:flex;justify-content:space-between;align-items:center}.pattern-title[data-v-821062ae]{display:flex;align-items:center;gap:10px}.pattern-title h4[data-v-821062ae]{margin:0;font-size:16px;font-weight:600}.pattern-body[data-v-821062ae]{padding:4px 0}.pattern-info[data-v-821062ae]{margin-bottom:12px}.pattern-info label[data-v-821062ae]{display:block;font-size:12px;color:#666;margin-bottom:4px}.regex-box[data-v-821062ae]{display:block;font-family:Consolas,Monaco,monospace;background:#f1f3f5;padding:10px 12px;border-radius:6px;font-size:13px;word-break:break-all;line-height:1.4;border-left:3px solid #3498db}.regex-box.exclude[data-v-821062ae]{border-left-color:#e67e22;background:#fef5e7}.pattern-meta[data-v-821062ae]{display:flex;flex-wrap:wrap;gap:12px;padding-top:8px;border-top:1px solid #eee;font-size:13px}.meta-item[data-v-821062ae]{display:flex;align-items:center;gap:4px}.meta-label[data-v-821062ae]{color:#888}.meta-value[data-v-821062ae]{font-weight:500;color:#333}.meta-item.description[data-v-821062ae]{flex-basis:100%;color:#666;font-style:italic}.pattern-actions[data-v-821062ae]{display:flex;gap:8px;padding-top:12px;border-top:1px solid #eee;margin-top:12px}.action-btn[data-v-821062ae]{flex:1;display:flex;align-items:center;justify-content:center;gap:4px;padding:10px 12px;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;transition:all .2s}.action-btn.test[data-v-821062ae]{background:#e8f4fd;color:#2980b9}.action-btn.test[data-v-821062ae]:hover{background:#d4e9f7}.action-btn.edit[data-v-821062ae]{background:#fef3e2;color:#d68910}.action-btn.edit[data-v-821062ae]:hover{background:#fce8c9}.action-btn.delete[data-v-821062ae]{background:#fdeaea;color:#c0392b}.action-btn.delete[data-v-821062ae]:hover{background:#f9d6d6}.form-group[data-v-821062ae]{margin-bottom:16px}.form-group label[data-v-821062ae]{display:flex;align-items:center;gap:8px;cursor:pointer}.test-section[data-v-821062ae]{display:flex;flex-direction:column;gap:16px}.test-pattern label[data-v-821062ae]{display:block;font-weight:500;margin-bottom:6px}.regex-display[data-v-821062ae]{display:block;font-family:monospace;background:#f8f9fa;padding:12px;border-radius:4px;font-size:13px;word-break:break-all}.test-result[data-v-821062ae]{padding:16px;border-radius:8px;margin-top:8px}.test-result.success[data-v-821062ae]{background:#d4edda;border:1px solid #c3e6cb}.test-result.fail[data-v-821062ae]{background:#f8d7da;border:1px solid #f5c6cb}.test-result h4[data-v-821062ae]{margin:0 0 12px}.test-result p[data-v-821062ae]{margin:0}.match-info[data-v-821062ae]{margin-top:8px}.match-info label[data-v-821062ae]{font-weight:500;margin-right:8px}.match-info code[data-v-821062ae]{background:#0000001a;padding:2px 6px;border-radius:3px}.error-msg[data-v-821062ae]{color:#721c24}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.card-header-content[data-v-6f59a208]{display:flex;justify-content:space-between;align-items:center}.card-header-content h3[data-v-6f59a208]{margin:0}.action-buttons[data-v-6f59a208]{display:flex;gap:4px}.form-group[data-v-6f59a208]{margin-bottom:16px}.form-group label[data-v-6f59a208]{display:flex;align-items:center;gap:8px;cursor:pointer}.log-path-section h4[data-v-6f59a208]{margin:0 0 12px;font-size:14px;color:#333}.log-path-form[data-v-6f59a208]{padding:16px;background:#f8f9fa;border-radius:8px;margin-bottom:20px}.log-path-inputs[data-v-6f59a208]{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:12px}.log-path-list table[data-v-6f59a208]{width:100%;border-collapse:collapse}.log-path-list th[data-v-6f59a208],.log-path-list td[data-v-6f59a208]{padding:10px 12px;text-align:left;border-bottom:1px solid #eee}.log-path-list th[data-v-6f59a208]{background:#f8f9fa;font-weight:600;font-size:13px}.empty-text[data-v-6f59a208]{color:#6c757d;text-align:center;padding:20px}.warning-text[data-v-6f59a208]{color:#e74c3c;font-size:14px}.test-result[data-v-6f59a208]{padding:16px;border-radius:8px}.test-result.success[data-v-6f59a208]{background:#d4edda;border:1px solid #c3e6cb}.test-result.fail[data-v-6f59a208]{background:#f8d7da;border:1px solid #f5c6cb}.test-result p[data-v-6f59a208]{margin:0}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{_ as S,r as m,o as F,a as p,f as a,w as i,u as n,h as v,m as B,b as t,i as c}from"./index-BB0X_WMV.js";import{C as V,b}from"./index-09HB4Lmg.js";import{F as u}from"./FormInput-BwPgkOAn.js";import{B as y}from"./Button-CoXLXkk7.js";const k={class:"settings"},w={key:0,class:"loading"},C={class:"setting-section"},U={class:"setting-section"},h={class:"setting-section"},M={class:"setting-section"},z={class:"form-actions"},A={__name:"Settings",setup(N){const r=m(!1),d=m(!1),l=m({}),f={"server.port":"8080","export.path":"./exports","retention.days":"90","scan.timeout":"30","scan.maxFileSize":"100"},g=async()=>{r.value=!0;try{const o=await b.getAllAsMap();l.value={...f,...o}}catch(o){console.error("Failed to load settings:",o),l.value={...f}}finally{r.value=!1}},x=async()=>{d.value=!0;try{for(const[o,e]of Object.entries(l.value))await b.save({key:o,value:String(e)});alert("설정이 저장되었습니다.")}catch(o){console.error("Failed to save settings:",o),alert("설정 저장에 실패했습니다.")}finally{d.value=!1}};return F(()=>{g()}),(o,e)=>(v(),p("div",k,[a(n(V),null,{header:i(()=>[...e[5]||(e[5]=[t("div",{class:"card-header-content"},[t("h3",null,"설정")],-1)])]),default:i(()=>[r.value?(v(),p("div",w,"로딩중...")):(v(),p("form",{key:1,onSubmit:B(x,["prevent"]),class:"settings-form"},[t("div",C,[e[6]||(e[6]=t("h4",null,"일반 설정",-1)),a(n(u),{modelValue:l.value["server.port"],"onUpdate:modelValue":e[0]||(e[0]=s=>l.value["server.port"]=s),label:"서버 포트",type:"number",hint:"애플리케이션이 실행될 포트 번호 (기본: 8080)"},null,8,["modelValue"])]),t("div",U,[e[7]||(e[7]=t("h4",null,"내보내기 설정",-1)),a(n(u),{modelValue:l.value["export.path"],"onUpdate:modelValue":e[1]||(e[1]=s=>l.value["export.path"]=s),label:"내보내기 경로",placeholder:"예: C:\\LogHunter\\exports",hint:"리포트 파일이 저장될 기본 경로"},null,8,["modelValue"])]),t("div",h,[e[8]||(e[8]=t("h4",null,"데이터 관리",-1)),a(n(u),{modelValue:l.value["retention.days"],"onUpdate:modelValue":e[2]||(e[2]=s=>l.value["retention.days"]=s),label:"로그 보관 기간 (일)",type:"number",hint:"에러 로그 데이터 보관 기간 (0 = 무제한)"},null,8,["modelValue"])]),t("div",M,[e[9]||(e[9]=t("h4",null,"스캔 설정",-1)),a(n(u),{modelValue:l.value["scan.timeout"],"onUpdate:modelValue":e[3]||(e[3]=s=>l.value["scan.timeout"]=s),label:"스캔 타임아웃 (초)",type:"number",hint:"SFTP 연결 및 파일 다운로드 타임아웃"},null,8,["modelValue"]),a(n(u),{modelValue:l.value["scan.maxFileSize"],"onUpdate:modelValue":e[4]||(e[4]=s=>l.value["scan.maxFileSize"]=s),label:"최대 파일 크기 (MB)",type:"number",hint:"분석할 로그 파일의 최대 크기"},null,8,["modelValue"])]),t("div",z,[a(n(y),{onClick:g,variant:"secondary"},{default:i(()=>[...e[10]||(e[10]=[c("초기화",-1)])]),_:1}),a(n(y),{type:"submit",loading:d.value},{default:i(()=>[...e[11]||(e[11]=[c("저장",-1)])]),_:1},8,["loading"])])],32))]),_:1}),a(n(V),{class:"app-info"},{header:i(()=>[...e[12]||(e[12]=[t("h3",null,"애플리케이션 정보",-1)])]),default:i(()=>[e[13]||(e[13]=t("div",{class:"info-list"},[t("div",{class:"info-item"},[t("span",{class:"label"},"버전"),t("span",{class:"value"},"1.0.0")]),t("div",{class:"info-item"},[t("span",{class:"label"},"프레임워크"),t("span",{class:"value"},"Spring Boot 3.2 + Vue 3")]),t("div",{class:"info-item"},[t("span",{class:"label"},"데이터베이스"),t("span",{class:"value"},"SQLite (./data/loghunter.db)")])],-1))]),_:1})]))}},j=S(A,[["__scopeId","data-v-fdca948e"]]);export{j as default};
import{_ as S,r as m,o as F,a as p,f as a,w as i,u as n,h as v,m as B,b as t,i as c}from"./index-DFoOAXeQ.js";import{C as V,b}from"./index--fsvNaiQ.js";import{F as u}from"./FormInput-BsW78DWl.js";import{B as y}from"./Button-Ddldcbk7.js";const k={class:"settings"},w={key:0,class:"loading"},C={class:"setting-section"},U={class:"setting-section"},h={class:"setting-section"},M={class:"setting-section"},z={class:"form-actions"},A={__name:"Settings",setup(N){const r=m(!1),d=m(!1),l=m({}),f={"server.port":"8080","export.path":"./exports","retention.days":"90","scan.timeout":"30","scan.maxFileSize":"100"},g=async()=>{r.value=!0;try{const o=await b.getAllAsMap();l.value={...f,...o}}catch(o){console.error("Failed to load settings:",o),l.value={...f}}finally{r.value=!1}},x=async()=>{d.value=!0;try{for(const[o,e]of Object.entries(l.value))await b.save({key:o,value:String(e)});alert("설정이 저장되었습니다.")}catch(o){console.error("Failed to save settings:",o),alert("설정 저장에 실패했습니다.")}finally{d.value=!1}};return F(()=>{g()}),(o,e)=>(v(),p("div",k,[a(n(V),null,{header:i(()=>[...e[5]||(e[5]=[t("div",{class:"card-header-content"},[t("h3",null,"설정")],-1)])]),default:i(()=>[r.value?(v(),p("div",w,"로딩중...")):(v(),p("form",{key:1,onSubmit:B(x,["prevent"]),class:"settings-form"},[t("div",C,[e[6]||(e[6]=t("h4",null,"일반 설정",-1)),a(n(u),{modelValue:l.value["server.port"],"onUpdate:modelValue":e[0]||(e[0]=s=>l.value["server.port"]=s),label:"서버 포트",type:"number",hint:"애플리케이션이 실행될 포트 번호 (기본: 8080)"},null,8,["modelValue"])]),t("div",U,[e[7]||(e[7]=t("h4",null,"내보내기 설정",-1)),a(n(u),{modelValue:l.value["export.path"],"onUpdate:modelValue":e[1]||(e[1]=s=>l.value["export.path"]=s),label:"내보내기 경로",placeholder:"예: C:\\LogHunter\\exports",hint:"리포트 파일이 저장될 기본 경로"},null,8,["modelValue"])]),t("div",h,[e[8]||(e[8]=t("h4",null,"데이터 관리",-1)),a(n(u),{modelValue:l.value["retention.days"],"onUpdate:modelValue":e[2]||(e[2]=s=>l.value["retention.days"]=s),label:"로그 보관 기간 (일)",type:"number",hint:"에러 로그 데이터 보관 기간 (0 = 무제한)"},null,8,["modelValue"])]),t("div",M,[e[9]||(e[9]=t("h4",null,"스캔 설정",-1)),a(n(u),{modelValue:l.value["scan.timeout"],"onUpdate:modelValue":e[3]||(e[3]=s=>l.value["scan.timeout"]=s),label:"스캔 타임아웃 (초)",type:"number",hint:"SFTP 연결 및 파일 다운로드 타임아웃"},null,8,["modelValue"]),a(n(u),{modelValue:l.value["scan.maxFileSize"],"onUpdate:modelValue":e[4]||(e[4]=s=>l.value["scan.maxFileSize"]=s),label:"최대 파일 크기 (MB)",type:"number",hint:"분석할 로그 파일의 최대 크기"},null,8,["modelValue"])]),t("div",z,[a(n(y),{onClick:g,variant:"secondary"},{default:i(()=>[...e[10]||(e[10]=[c("초기화",-1)])]),_:1}),a(n(y),{type:"submit",loading:d.value},{default:i(()=>[...e[11]||(e[11]=[c("저장",-1)])]),_:1},8,["loading"])])],32))]),_:1}),a(n(V),{class:"app-info"},{header:i(()=>[...e[12]||(e[12]=[t("h3",null,"애플리케이션 정보",-1)])]),default:i(()=>[e[13]||(e[13]=t("div",{class:"info-list"},[t("div",{class:"info-item"},[t("span",{class:"label"},"버전"),t("span",{class:"value"},"1.0.0")]),t("div",{class:"info-item"},[t("span",{class:"label"},"프레임워크"),t("span",{class:"value"},"Spring Boot 3.2 + Vue 3")]),t("div",{class:"info-item"},[t("span",{class:"label"},"데이터베이스"),t("span",{class:"value"},"SQLite (./data/loghunter.db)")])],-1))]),_:1})]))}},j=S(A,[["__scopeId","data-v-fdca948e"]]);export{j as default};

View File

@@ -1,4 +1,4 @@
var Yn=Object.defineProperty;var Un=(i,t,e)=>t in i?Yn(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var w=(i,t,e)=>Un(i,typeof t!="symbol"?t+"":t,e);import{x as Us,y as Xs,z as oi,A as Xn,r as Kn,o as qn,B as Gn,C as Zn,D as ri,E as Ks,p as Qn}from"./index-BB0X_WMV.js";/*!
var Yn=Object.defineProperty;var Un=(i,t,e)=>t in i?Yn(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var w=(i,t,e)=>Un(i,typeof t!="symbol"?t+"":t,e);import{x as Us,y as Xs,z as oi,A as Xn,r as Kn,o as qn,B as Gn,C as Zn,D as ri,E as Ks,p as Qn}from"./index-DFoOAXeQ.js";/*!
* @kurkle/color v0.3.4
* https://github.com/kurkle/color#readme
* (c) 2024 Jukka Kurkela

View File

@@ -18,7 +18,7 @@
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
</style>
<script type="module" crossorigin src="/assets/index-BB0X_WMV.js"></script>
<script type="module" crossorigin src="/assets/index-DFoOAXeQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BJ4-9mFZ.css">
</head>
<body>