refactor: 101.79.17.164 → zioinfo.co.kr 전체 도메인 변환 + Manager UI 배포
- 37개 파일 IP → zioinfo.co.kr 치환 (소스/매뉴얼/설정/하네스) - Manager DrConsole/NetworkConsole/CsapConsole 빌드 + /var/www/manager/ 배포 - 테스트: Manager HTTP 200, ITSM 신규 API 7개 전체 200 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ -0,0 +1 @@
|
|||||||
|
import{r as a,j as e,L as n}from"./index-ChpGil2q.js";const h=s=>fetch(s,{headers:{Authorization:`Bearer ${localStorage.getItem("admin_token")}`}}).then(l=>l.json());function x(){var t,r;const[s,l]=a.useState(null),[c,d]=a.useState(!0);if(a.useEffect(()=>{h("/api/admin/dashboard").then(l).finally(()=>d(!1))},[]),c)return e.jsx("p",{style:{color:"#64748b",fontSize:14},children:"로딩 중..."});if(!s)return null;const o=[{icon:"📰",label:"전체 뉴스",value:s.totalNews,sub:`공개 ${s.visibleNews}건`,color:"blue"},{icon:"📩",label:"전체 문의",value:s.totalInquiries,sub:`미답변 ${s.pendingInquiries}건`,color:s.pendingInquiries>0?"red":"green"},{icon:"👥",label:"채용공고",value:s.totalRecruits,sub:`진행중 ${s.activeRecruits}건`,color:"green"}];return e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"admin-stats",children:[o.map(i=>e.jsxs("div",{className:"stat-card",children:[e.jsx("div",{className:`stat-icon ${i.color}`,children:i.icon}),e.jsxs("div",{className:"stat-info",children:[e.jsx("h4",{children:i.value}),e.jsxs("p",{children:[i.label,e.jsx("br",{}),e.jsx("span",{style:{fontSize:11},children:i.sub})]})]})]},i.label)),s.pendingInquiries>0&&e.jsxs("div",{className:"stat-card",style:{borderLeft:"3px solid #ef4444"},children:[e.jsx("div",{className:"stat-icon red",children:"🔔"}),e.jsxs("div",{className:"stat-info",children:[e.jsx("h4",{style:{color:"#ef4444"},children:s.pendingInquiries}),e.jsxs("p",{children:["미답변 문의",e.jsx("br",{}),e.jsx(n,{to:"/admin/inquiries",style:{fontSize:11,color:"#ef4444"},children:"바로가기 →"})]})]})]})]}),e.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:16},children:[e.jsxs("div",{className:"admin-card",children:[e.jsxs("div",{className:"admin-card-header",children:[e.jsx("h3",{children:"📰 최근 뉴스"}),e.jsx(n,{to:"/admin/news",className:"btn btn-outline btn-sm",children:"전체보기"})]}),e.jsxs("ul",{className:"recent-list",children:[(s.recentNews||[]).map(i=>e.jsxs("li",{children:[e.jsx("span",{className:"rl-dot"}),e.jsx("span",{className:"rl-title",children:i.title}),e.jsx("span",{className:"rl-meta",children:i.category})]},i.id)),!((t=s.recentNews)!=null&&t.length)&&e.jsx("li",{style:{color:"#94a3b8",fontSize:13},children:"등록된 뉴스가 없습니다."})]})]}),e.jsxs("div",{className:"admin-card",children:[e.jsxs("div",{className:"admin-card-header",children:[e.jsx("h3",{children:"📩 최근 문의"}),e.jsx(n,{to:"/admin/inquiries",className:"btn btn-outline btn-sm",children:"전체보기"})]}),e.jsxs("ul",{className:"recent-list",children:[(s.recentInquiries||[]).map(i=>e.jsxs("li",{children:[e.jsx("span",{className:"rl-dot",style:{background:i.status==="PENDING"?"#ef4444":"#22c55e"}}),e.jsx("span",{className:"rl-title",children:i.subject}),e.jsx("span",{className:"rl-meta",children:i.name})]},i.id)),!((r=s.recentInquiries)!=null&&r.length)&&e.jsx("li",{style:{color:"#94a3b8",fontSize:13},children:"접수된 문의가 없습니다."})]})]})]})]})}export{x as default};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
import{c as g,u as x,r as s,j as a,N as b,O as j}from"./index-ChpGil2q.js";/* empty css */const N=[{section:"메인"},{path:"/admin/dashboard",icon:"📊",label:"대시보드"},{section:"콘텐츠 관리"},{path:"/admin/news",icon:"📰",label:"뉴스/공지사항"},{path:"/admin/recruit",icon:"👥",label:"채용공고"},{section:"고객 관리"},{path:"/admin/inquiries",icon:"📩",label:"문의 관리",badgeKey:"pendingInquiries"},{section:"시스템"},{path:"/admin/settings",icon:"⚙️",label:"설정"}];function y(){const i=g(),d=x(),[t,r]=s.useState(null),[l,m]=s.useState("대시보드"),[o,h]=s.useState({});s.useEffect(()=>{const e=localStorage.getItem("admin_token");if(!e){i("/admin/login");return}const n=JSON.parse(localStorage.getItem("admin_user")||"{}");r(n),u(e)},[i]),s.useEffect(()=>{m({"/admin/dashboard":"대시보드","/admin/news":"뉴스/공지사항 관리","/admin/inquiries":"문의 관리","/admin/recruit":"채용공고 관리","/admin/settings":"설정"}[d.pathname]||"관리자")},[d.pathname]);const u=async e=>{try{const n=await fetch("/api/admin/dashboard",{headers:{Authorization:`Bearer ${e}`}});if(n.ok){const c=await n.json();h({pendingInquiries:c.pendingInquiries||0})}}catch{}},p=()=>{localStorage.removeItem("admin_token"),localStorage.removeItem("admin_user"),i("/admin/login")};return t?a.jsxs("div",{className:"admin-wrap",children:[a.jsxs("aside",{className:"admin-sidebar",children:[a.jsxs("div",{className:"admin-sidebar-logo",children:[a.jsx("h2",{children:"ZioInfo Admin"}),a.jsx("span",{children:"(주)지오정보기술 관리자"})]}),a.jsx("nav",{className:"admin-nav",children:N.map((e,n)=>e.section?a.jsx("div",{className:"admin-nav-section",children:e.section},n):a.jsxs(b,{to:e.path,className:({isActive:c})=>c?"active":"",children:[a.jsx("span",{className:"nav-icon",children:e.icon}),e.label,e.badgeKey&&o[e.badgeKey]>0&&a.jsx("span",{className:"admin-nav-badge",children:o[e.badgeKey]})]},e.path))}),a.jsx("div",{className:"admin-sidebar-footer",children:a.jsx("button",{onClick:p,children:"🚪 로그아웃"})})]}),a.jsxs("main",{className:"admin-main",children:[a.jsxs("div",{className:"admin-topbar",children:[a.jsx("h1",{children:l}),a.jsxs("div",{className:"admin-topbar-right",children:[a.jsxs("span",{className:"admin-user-badge",children:["👤 ",t.displayName||t.username]}),a.jsx("a",{href:"/",target:"_blank",rel:"noreferrer",style:{fontSize:12,color:"#64748b",textDecoration:"none"},children:"🌐 홈페이지 보기"})]})]}),a.jsx("div",{className:"admin-content",children:a.jsx(j,{})})]})]}):null}export{y as default};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
import{r as i,c as p,j as e}from"./index-ChpGil2q.js";/* empty css */function x(){const[t,o]=i.useState({username:"",password:""}),[l,r]=i.useState(""),[d,c]=i.useState(!1),m=p(),u=async a=>{a.preventDefault(),r(""),c(!0);try{const s=await fetch("/api/admin/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),n=await s.json();if(!s.ok){r(n.message||"로그인 실패");return}localStorage.setItem("admin_token",n.token),localStorage.setItem("admin_user",JSON.stringify({username:n.username,displayName:n.displayName})),m("/admin/dashboard")}catch{r("서버 연결 오류가 발생했습니다.")}finally{c(!1)}};return e.jsx("div",{className:"admin-login-page",children:e.jsxs("div",{className:"admin-login-box",children:[e.jsxs("div",{className:"login-logo",children:[e.jsx("span",{className:"login-badge",children:"ADMIN"}),e.jsx("h1",{children:"(주)지오정보기술"}),e.jsx("p",{children:"홈페이지 관리자 시스템"})]}),l&&e.jsxs("div",{className:"login-error",children:["⚠ ",l]}),e.jsxs("form",{onSubmit:u,children:[e.jsxs("div",{className:"login-input-group",children:[e.jsx("label",{children:"아이디"}),e.jsx("input",{type:"text",placeholder:"관리자 아이디",value:t.username,required:!0,onChange:a=>o(s=>({...s,username:a.target.value}))})]}),e.jsxs("div",{className:"login-input-group",children:[e.jsx("label",{children:"비밀번호"}),e.jsx("input",{type:"password",placeholder:"비밀번호",value:t.password,required:!0,onChange:a=>o(s=>({...s,password:a.target.value}))})]}),e.jsx("button",{type:"submit",className:"login-btn",disabled:d,children:d?"로그인 중...":"로그인"})]}),e.jsxs("p",{style:{textAlign:"center",marginTop:20,fontSize:12,color:"#94a3b8"},children:["홈페이지로 돌아가기: ",e.jsx("a",{href:"/",style:{color:"#4f6ef7"},children:"메인 페이지"})]})]})})}export{x as default};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
import{c as u,r as d,j as e}from"./index-ChpGil2q.js";const h=()=>localStorage.getItem("admin_token");function f(){const m=u(),i=JSON.parse(localStorage.getItem("admin_user")||"{}"),[r,n]=d.useState({currentPassword:"",newPassword:"",confirmPassword:""}),[t,o]=d.useState(null),[l,c]=d.useState(!1),p=async()=>{if(r.newPassword!==r.confirmPassword){o({text:"새 비밀번호가 일치하지 않습니다.",type:"error"});return}if(r.newPassword.length<8){o({text:"비밀번호는 8자 이상이어야 합니다.",type:"error"});return}c(!0);const s=await fetch("/api/admin/password",{method:"PUT",headers:{"Content-Type":"application/json",Authorization:`Bearer ${h()}`},body:JSON.stringify({currentPassword:r.currentPassword,newPassword:r.newPassword})}),a=await s.json();c(!1),s.ok?(o({text:"비밀번호가 변경되었습니다. 다시 로그인해주세요.",type:"success"}),n({currentPassword:"",newPassword:"",confirmPassword:""}),setTimeout(()=>{localStorage.removeItem("admin_token"),m("/admin/login")},2e3)):o({text:a.message||"변경 실패",type:"error"})};return e.jsxs("div",{style:{maxWidth:520},children:[e.jsxs("div",{className:"admin-card",style:{marginBottom:20},children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h3",{children:"👤 계정 정보"})}),e.jsx("div",{style:{display:"grid",gap:12},children:[["아이디",i.username],["표시 이름",i.displayName||"-"]].map(([s,a])=>e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("span",{style:{fontSize:12,fontWeight:600,color:"#64748b",width:80},children:s}),e.jsx("span",{style:{fontSize:14},children:a})]},s))})]}),e.jsxs("div",{className:"admin-card",children:[e.jsx("div",{className:"admin-card-header",children:e.jsx("h3",{children:"🔒 비밀번호 변경"})}),t&&e.jsx("div",{style:{padding:"10px 14px",borderRadius:7,marginBottom:16,fontSize:13,background:t.type==="error"?"#fff1f2":"#f0fdf4",color:t.type==="error"?"#dc2626":"#16a34a",border:`1px solid ${t.type==="error"?"#fecaca":"#bbf7d0"}`},children:t.text}),e.jsxs("div",{className:"form-group",children:[e.jsx("label",{children:"현재 비밀번호"}),e.jsx("input",{type:"password",className:"form-control",value:r.currentPassword,onChange:s=>n(a=>({...a,currentPassword:s.target.value}))})]}),e.jsxs("div",{className:"form-group",children:[e.jsx("label",{children:"새 비밀번호"}),e.jsx("input",{type:"password",className:"form-control",value:r.newPassword,placeholder:"8자 이상",onChange:s=>n(a=>({...a,newPassword:s.target.value}))})]}),e.jsxs("div",{className:"form-group",children:[e.jsx("label",{children:"새 비밀번호 확인"}),e.jsx("input",{type:"password",className:"form-control",value:r.confirmPassword,onChange:s=>n(a=>({...a,confirmPassword:s.target.value}))})]}),e.jsx("button",{className:"btn btn-primary",onClick:p,disabled:l||!r.currentPassword||!r.newPassword,children:l?"변경 중...":"비밀번호 변경"})]})]})}export{f as default};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
.ref-filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px}.ref-filter-btn{padding:7px 18px;border-radius:20px;border:1px solid var(--gray-200);font-size:13px;font-weight:500;color:var(--gray-600);cursor:pointer;transition:all var(--fast) var(--ease);background:var(--white)}.ref-filter-btn:hover{border-color:var(--primary);color:var(--primary)}.ref-filter-btn.active{background:var(--primary);border-color:var(--primary);color:#fff}.ref-table-wrap{overflow-x:auto;border-radius:12px;border:1px solid var(--gray-200)}.ref-table{width:100%;border-collapse:collapse;min-width:800px}.ref-table th{background:var(--secondary);color:#fffc;padding:14px 16px;text-align:left;font-size:12px;font-weight:600;letter-spacing:.5px}.ref-table td{padding:13px 16px;font-size:13px;border-bottom:1px solid var(--gray-100);vertical-align:middle}.ref-table tr:last-child td{border-bottom:none}.ref-table tr:hover td{background:var(--gray-50)}.ref-period{color:var(--gray-500);font-size:12px;white-space:nowrap}.ref-client{font-weight:700;color:var(--gray-800);white-space:nowrap}.ref-project{color:var(--gray-700)}.ref-role{padding:3px 10px;border-radius:12px;font-size:11px;font-weight:700;background:var(--primary-light);color:var(--primary);white-space:nowrap}.ref-tech{font-size:12px;color:var(--gray-500)}.ref-cat-badge{padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600;white-space:nowrap}.partner-card{padding:32px 24px;text-align:center}.partner-logo{font-size:48px;margin-bottom:12px}.partner-tier{display:inline-block;padding:3px 12px;border-radius:12px;font-size:11px;font-weight:700;margin-bottom:12px}.partner-name{font-size:16px;font-weight:700;color:var(--gray-900);margin-bottom:10px}.partner-desc{font-size:13px;color:var(--gray-600);line-height:1.6}.partner-cta{margin-top:64px;text-align:center;padding:56px;background:linear-gradient(135deg,var(--primary-light),rgba(0,163,224,.08));border-radius:16px;border:1px solid var(--gray-200)}.partner-cta h3{font-size:24px;font-weight:800;margin-bottom:12px}.partner-cta p{color:var(--gray-600);margin-bottom:24px;font-size:15px}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
.inner-page{padding-top:var(--header-h)}.page-hero{background:linear-gradient(135deg,var(--secondary),var(--primary-dark));padding:60px 0;color:#fff}.page-hero-title{font-size:40px;font-weight:900;margin:8px 0 12px}.page-hero p{color:#ffffffbf;font-size:16px}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
.contact-page{padding-top:var(--header-h)}.page-hero{background:linear-gradient(135deg,var(--secondary),var(--primary-dark));padding:60px 0;color:#fff}.page-hero-title{font-size:40px;font-weight:900;margin:8px 0 12px}.page-hero p{color:#ffffffbf;font-size:16px}.contact-grid{display:grid;grid-template-columns:340px 1fr;gap:48px;align-items:start}.contact-info h2{font-size:22px;font-weight:700;margin-bottom:28px}.info-item{display:flex;gap:16px;margin-bottom:24px;align-items:flex-start}.info-icon{font-size:24px}.info-item strong{display:block;font-size:13px;font-weight:700;color:var(--gray-500);margin-bottom:2px}.info-item p{font-size:15px;color:var(--gray-800)}.contact-form{padding:36px}.contact-form h2{font-size:22px;font-weight:700;margin-bottom:24px}.form-alert{padding:12px 16px;border-radius:var(--radius-sm);font-size:14px;margin-bottom:16px}.form-alert.success{background:#d1fae5;color:#065f46}.form-alert.error{background:#fee2e2;color:#991b1b}.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}.form-group{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}.form-group label{font-size:13px;font-weight:600;color:var(--gray-700)}.required{color:var(--danger)}.form-group input,.form-group select,.form-group textarea{padding:10px 14px;border:1px solid var(--gray-200);border-radius:var(--radius-sm);font-size:14px;font-family:inherit;transition:border-color var(--fast);outline:none}.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--primary);box-shadow:0 0 0 3px #0051a21a}.privacy-agree{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--gray-600);margin-bottom:20px;cursor:pointer}.privacy-agree a{color:var(--primary)}@media (max-width: 1024px){.contact-grid{grid-template-columns:1fr}}@media (max-width: 768px){.form-row{grid-template-columns:1fr}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
import{r as i,j as e}from"./index-ChpGil2q.js";import{a as j}from"./index-DcNlVx-A.js";function y(){const[a,t]=i.useState({name:"",email:"",phone:"",category:"제품문의",subject:"",content:"",agreePrivacy:!1}),[r,l]=i.useState(null),[o,d]=i.useState(!1),s=n=>{const{name:c,value:m,type:p,checked:x}=n.target;t(u=>({...u,[c]:p==="checkbox"?x:m}))},h=async n=>{if(n.preventDefault(),!a.agreePrivacy){l({type:"error",msg:"개인정보 수집·이용에 동의해주세요."});return}d(!0);try{await j.post("/api/inquiry",a),l({type:"success",msg:"문의가 접수되었습니다. 빠른 시일 내에 연락드리겠습니다."}),t({name:"",email:"",phone:"",category:"제품문의",subject:"",content:"",agreePrivacy:!1})}catch{l({type:"error",msg:"문의 접수 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."})}finally{d(!1)}};return e.jsxs("main",{id:"main-content",className:"contact-page",children:[e.jsx("div",{className:"page-hero",children:e.jsxs("div",{className:"container",children:[e.jsx("span",{className:"section-label",children:"Contact Us"}),e.jsx("h1",{className:"page-hero-title",children:"문의하기"}),e.jsx("p",{children:"GUARDiA ITSM 도입 문의 및 제품 상담을 받아드립니다."})]})}),e.jsx("section",{className:"section",children:e.jsxs("div",{className:"container contact-grid",children:[e.jsxs("div",{className:"contact-info",children:[e.jsx("h2",{children:"연락처 정보"}),[{icon:"📞",label:"대표전화",value:"02-000-0000"},{icon:"✉️",label:"이메일",value:"info@zioinfo.co.kr"},{icon:"🕐",label:"운영시간",value:"평일 09:00 ~ 18:00"},{icon:"📍",label:"주소",value:"서울특별시"}].map((n,c)=>e.jsxs("div",{className:"info-item",children:[e.jsx("span",{className:"info-icon",children:n.icon}),e.jsxs("div",{children:[e.jsx("strong",{children:n.label}),e.jsx("p",{children:n.value})]})]},c))]}),e.jsxs("form",{className:"contact-form card",onSubmit:h,children:[e.jsx("h2",{children:"온라인 문의"}),r&&e.jsxs("div",{className:`form-alert ${r.type}`,children:[r.type==="success"?"✅":"❌"," ",r.msg]}),e.jsxs("div",{className:"form-row",children:[e.jsxs("div",{className:"form-group",children:[e.jsxs("label",{htmlFor:"name",children:["성함 ",e.jsx("span",{className:"required",children:"*"})]}),e.jsx("input",{id:"name",name:"name",type:"text",required:!0,value:a.name,onChange:s,placeholder:"홍길동"})]}),e.jsxs("div",{className:"form-group",children:[e.jsx("label",{htmlFor:"phone",children:"연락처"}),e.jsx("input",{id:"phone",name:"phone",type:"tel",value:a.phone,onChange:s,placeholder:"010-0000-0000"})]})]}),e.jsxs("div",{className:"form-row",children:[e.jsxs("div",{className:"form-group",children:[e.jsxs("label",{htmlFor:"email",children:["이메일 ",e.jsx("span",{className:"required",children:"*"})]}),e.jsx("input",{id:"email",name:"email",type:"email",required:!0,value:a.email,onChange:s,placeholder:"your@email.com"})]}),e.jsxs("div",{className:"form-group",children:[e.jsx("label",{htmlFor:"category",children:"문의 유형"}),e.jsxs("select",{id:"category",name:"category",value:a.category,onChange:s,children:[e.jsx("option",{children:"제품문의"}),e.jsx("option",{children:"데모 신청"}),e.jsx("option",{children:"기술지원"}),e.jsx("option",{children:"사업제안"}),e.jsx("option",{children:"채용문의"}),e.jsx("option",{children:"기타"})]})]})]}),e.jsxs("div",{className:"form-group",children:[e.jsxs("label",{htmlFor:"subject",children:["제목 ",e.jsx("span",{className:"required",children:"*"})]}),e.jsx("input",{id:"subject",name:"subject",type:"text",required:!0,value:a.subject,onChange:s,placeholder:"문의 제목을 입력해주세요"})]}),e.jsxs("div",{className:"form-group",children:[e.jsxs("label",{htmlFor:"content",children:["문의 내용 ",e.jsx("span",{className:"required",children:"*"})]}),e.jsx("textarea",{id:"content",name:"content",rows:6,required:!0,value:a.content,onChange:s,placeholder:"문의 내용을 자세히 작성해주세요."})]}),e.jsxs("label",{className:"privacy-agree",children:[e.jsx("input",{type:"checkbox",name:"agreePrivacy",checked:a.agreePrivacy,onChange:s}),e.jsxs("span",{children:["개인정보 수집·이용에 동의합니다. ",e.jsx("a",{href:"/privacy",target:"_blank",children:"[보기]"})]})]}),e.jsx("button",{type:"submit",className:"btn btn-primary btn-lg",style:{width:"100%"},disabled:o,children:o?"전송 중...":"문의 접수하기"})]})]})})]})}export{y as default};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
.notice-back{font-size:14px;color:var(--primary);margin-bottom:24px;display:inline-flex;align-items:center;gap:4px;cursor:pointer;background:none;border:none}.news-cat-badge{display:inline-block;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:700;margin-bottom:12px}.news-cat-badge.hot{background:#ef44441f;color:var(--danger)}.news-main{padding:40px;cursor:pointer;background:linear-gradient(135deg,var(--secondary),var(--primary-dark));border:none;margin-bottom:0}.news-main:hover{transform:none;box-shadow:var(--shadow-lg)}.news-main-title{font-size:26px;font-weight:900;color:#fff;margin-bottom:16px;line-height:1.35}.news-main-summary{font-size:15px;color:#ffffffb3;line-height:1.8;margin-bottom:16px;max-width:640px}.news-date{font-size:12px;color:#ffffff80}.news-card{padding:28px;cursor:pointer;display:flex;flex-direction:column}.news-card-title{font-size:15px;font-weight:700;color:var(--gray-900);margin-bottom:10px;line-height:1.5;flex:1}.news-card-summary{font-size:13px;color:var(--gray-600);line-height:1.7;margin-bottom:12px;flex:1}.news-date{font-size:12px;color:var(--gray-400)}.blog-card{padding:28px;display:flex;flex-direction:column}.blog-tag{display:inline-block;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:700;margin-bottom:14px;align-self:flex-start}.blog-title{font-size:16px;font-weight:700;color:var(--gray-900);line-height:1.5;margin-bottom:12px}.blog-summary{font-size:13px;color:var(--gray-600);line-height:1.7;flex:1;margin-bottom:16px}.blog-meta{display:flex;gap:16px;font-size:12px;color:var(--gray-400);margin-bottom:16px}.blog-read-btn{padding:10px 20px;background:var(--primary-light);color:var(--primary);border-radius:8px;font-size:14px;font-weight:700;border:none;cursor:pointer;transition:all var(--fast);text-align:center}.blog-read-btn:hover{background:var(--primary);color:#fff}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import{j as e,b as o,a as n,r as h,N as x}from"./index-ChpGil2q.js";/* empty css */const p=[{path:"/news/newsroom",label:"뉴스룸"},{path:"/news/blog",label:"기술 블로그"}];function d({title:a}){return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"page-hero",children:e.jsxs("div",{className:"container",children:[e.jsx("span",{className:"section-label",children:"News"}),e.jsx("h1",{className:"page-hero-title",children:a})]})}),e.jsx("nav",{className:"sub-nav",children:e.jsx("div",{className:"container",children:p.map(t=>e.jsx(x,{to:t.path,className:({isActive:i})=>"sub-nav-item"+(i?" active":""),children:t.label},t.path))})})]})}const l=[{id:1,cat:"제품 출시",date:"2026.05.15",title:"GUARDiA ITSM v2.0 정식 출시 — AI ChatOps 오케스트레이션 플랫폼",summary:"메신저 한 줄 명령으로 1,000개+ 공공기관 레거시 인프라를 자동 운영하는 GUARDiA ITSM v2.0이 정식 출시되었습니다. 신규 기능으로 AI 자연어 명령, 에이전트리스 배포 엔진, 멀티테넌트 지원이 추가됐습니다.",content:`GUARDiA ITSM v2.0은 공공기관의 레거시 IT 인프라 운영 자동화를 위한 AI 기반 플랫폼입니다.
|
||||||
|
|
||||||
|
주요 신기능:
|
||||||
|
- AI ChatOps: 메신저 자연어 명령 → Ollama LLM 파싱 → 자동 실행
|
||||||
|
- 에이전트리스 배포: SSH/SFTP만으로 WAS 배포·롤백 자동화
|
||||||
|
- 멀티테넌트: 1,000개+ 기관 동시 관리
|
||||||
|
- GS인증 1등급 신청 완료
|
||||||
|
|
||||||
|
자세한 사항은 GUARDiA 소개 페이지를 참조해 주십시오.`,hot:!0},{id:2,cat:"수주 소식",date:"2026.04.20",title:"삼성전자 차세대 CRM 시스템 DB 마이그레이션 프로젝트 수주",summary:"(주)지오정보기술이 삼성전자 차세대 CRM 구축 프로젝트의 DB Migration/DA/튜닝을 담당합니다. EDB PostgreSQL 환경으로의 전환을 포함한 대규모 DB 현대화 작업을 수행합니다.",content:"삼성전자와의 두 번째 협력 프로젝트로, DB 마이그레이션 및 성능 튜닝을 담당합니다.",hot:!1},{id:3,cat:"기술 인증",date:"2026.03.10",title:"GUARDiA ITSM GS인증 1등급 신청 완료 — TTA 심사 예정",summary:"GUARDiA ITSM이 한국정보통신기술협회(TTA)에 GS인증 1등급을 신청하였습니다. 기능적합성, 신뢰성, 사용성, 보안성 등 ISO/IEC 25010 기준 8대 품질 특성 심사를 앞두고 있습니다.",content:"GS인증 심사는 2026년 9월 예정이며, 1등급 취득 시 조달청 나라장터 우선 등재가 가능합니다.",hot:!1},{id:4,cat:"수주 소식",date:"2026.02.15",title:"국민연금공단 차세대 시스템 구축 — AA 역할 수행",summary:"국민연금공단 차세대 시스템 구축 프로젝트에 Application Architect(AA)로 참여합니다. JSP/Java, Nexacro, Spring 기반의 대규모 공공기관 시스템 구축을 담당합니다.",content:"국민연금관리공단의 차세대 시스템은 수천만 가입자의 연금 관리 시스템으로, CI/CD 파이프라인 기반의 현대적인 개발 환경을 구축합니다.",hot:!1},{id:5,cat:"기업 소식",date:"2025.12.01",title:"2025년 사업실적 — 연간 프로젝트 10건 성공 수행",summary:"2025년 한 해 동안 삼성전자, 서울신용보증재단, 헌법재판소 등 10개 주요 프로젝트를 성공적으로 완료했습니다. 매출은 전년 대비 25% 성장하였습니다.",content:"창립 이래 최대 성과를 기록한 2025년 사업실적을 공유드립니다.",hot:!1},{id:6,cat:"파트너십",date:"2025.09.10",title:"Tibero 공식 파트너사 등록 — 공공기관 DB 전환 솔루션 강화",summary:"국산 DBMS Tibero의 공식 파트너사로 등록되었습니다. Oracle에서 Tibero로의 마이그레이션 및 공공기관 DB 현대화 사업을 공동으로 추진합니다.",content:"공공기관의 Oracle 라이선스 절감을 위한 Tibero 전환 프로젝트를 전문적으로 지원합니다.",hot:!1}];function c(){const[a,t]=h.useState(null),i=l.find(s=>s.id===a);return e.jsxs("main",{id:"main-content",className:"inner-page",children:[e.jsx(d,{title:"뉴스룸"}),e.jsx("section",{className:"section",children:e.jsx("div",{className:"container",children:i?e.jsxs("div",{style:{maxWidth:"760px",margin:"0 auto"},children:[e.jsx("button",{className:"notice-back",onClick:()=>t(null),children:"← 뉴스 목록"}),e.jsxs("div",{className:"news-detail card",style:{padding:"40px"},children:[e.jsx("span",{className:"news-cat-badge",style:{background:"var(--primary-light)",color:"var(--primary)"},children:i.cat}),e.jsx("h2",{style:{fontSize:"24px",fontWeight:"900",margin:"16px 0 8px",lineHeight:"1.4"},children:i.title}),e.jsx("p",{style:{fontSize:"13px",color:"var(--gray-400)",marginBottom:"32px"},children:i.date}),e.jsx("div",{className:"divider divider-left",style:{marginBottom:"32px"}}),i.content.split(`
|
||||||
|
`).map((s,m)=>s.trim()?e.jsx("p",{style:{fontSize:"15px",color:"var(--gray-700)",lineHeight:"1.85",marginBottom:"16px"},children:s},m):null)]})]}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"news-main card",onClick:()=>t(l[0].id),children:e.jsxs("div",{className:"news-main-content",children:[e.jsxs("span",{className:"news-cat-badge hot",children:["🔥 ",l[0].cat]}),e.jsx("h2",{className:"news-main-title",children:l[0].title}),e.jsx("p",{className:"news-main-summary",children:l[0].summary}),e.jsx("span",{className:"news-date",children:l[0].date})]})}),e.jsx("div",{className:"grid-3",style:{marginTop:"24px"},children:l.slice(1).map(s=>e.jsxs("div",{className:"card news-card",onClick:()=>t(s.id),children:[e.jsx("span",{className:"news-cat-badge",style:{background:"var(--primary-light)",color:"var(--primary)"},children:s.cat}),e.jsx("h3",{className:"news-card-title",children:s.title}),e.jsx("p",{className:"news-card-summary",children:s.summary}),e.jsx("span",{className:"news-date",children:s.date})]},s.id))})]})})})]})}const j=[{id:1,tag:"AI·LLM",date:"2026.05.20",title:"온프레미스 Ollama로 폐쇄망 ChatOps 구현하기",summary:"인터넷 없이 내부망에서 LLM을 운영하는 방법. Llama-3-8B 모델을 Ollama로 구동하고 FastAPI와 연동하는 전체 과정을 설명합니다.",readMin:12},{id:2,tag:"DevOps",date:"2026.05.10",title:"에이전트리스 WAS 배포 자동화 — paramiko SSH로 레거시 서버 관리",summary:"JEUS·Tomcat 등 레거시 WAS에 SSH/SFTP만으로 배포하는 방법. 백업→배포→헬스체크→롤백 파이프라인 구현 예제.",readMin:15},{id:3,tag:"보안",date:"2026.04.28",title:"AES-256-GCM으로 서버 자격증명을 안전하게 저장하는 법",summary:"공공기관 서버 SSH 비밀번호를 DB에 안전하게 암호화 저장하는 방법. IV·암호문·GCM Tag 구조 설계와 Python 구현.",readMin:8},{id:4,tag:"데이터베이스",date:"2026.04.15",title:"Oracle 19c → EDB PostgreSQL 마이그레이션 실전 가이드",summary:"삼성전자 CRM 프로젝트에서 실제 수행한 Oracle→EDB 마이그레이션 경험 공유. Smeta, ExemOne 활용 SQL 변환 전략.",readMin:20},{id:5,tag:"성능",date:"2026.03.25",title:"공공기관 행정정보시스템 SQL 튜닝 — 서울시립대 사례",summary:"대학행정정보시스템 성능 개선 프로젝트 실전 사례. JMeter 부하테스트와 Oracle 실행계획 분석으로 응답시간 60% 단축.",readMin:18},{id:6,tag:"아키텍처",date:"2026.03.10",title:"FastAPI 비동기 WebSocket으로 실시간 대시보드 구축하기",summary:"GUARDiA ITSM 실시간 모니터링 대시보드 구현 방법. FastAPI SSE + WebSocket + React를 조합한 풀스택 아키텍처.",readMin:14}],r={"AI·LLM":"#7c3aed",DevOps:"#0051A2",보안:"#dc2626",데이터베이스:"#d97706",성능:"#059669",아키텍처:"#0891b2"};function g(){return e.jsxs("main",{id:"main-content",className:"inner-page",children:[e.jsx(d,{title:"기술 블로그"}),e.jsx("section",{className:"section",children:e.jsxs("div",{className:"container",children:[e.jsxs("div",{className:"section-header",children:[e.jsx("span",{className:"section-label",children:"Tech Blog"}),e.jsx("h2",{className:"section-title",children:"기술 인사이트 공유"}),e.jsx("p",{className:"section-desc",children:"20년 이상의 프로젝트 경험에서 얻은 기술 노하우를 공유합니다"})]}),e.jsx("div",{className:"grid-3",children:j.map(a=>e.jsxs("div",{className:"card blog-card",children:[e.jsx("div",{className:"blog-tag",style:{background:r[a.tag]+"18",color:r[a.tag]},children:a.tag}),e.jsx("h3",{className:"blog-title",children:a.title}),e.jsx("p",{className:"blog-summary",children:a.summary}),e.jsxs("div",{className:"blog-meta",children:[e.jsxs("span",{children:["📅 ",a.date]}),e.jsxs("span",{children:["⏱ ",a.readMin,"분 읽기"]})]}),e.jsx("button",{className:"blog-read-btn",onClick:()=>alert("블로그 상세 페이지는 준비 중입니다."),children:"읽기 →"})]},a.id))})]})})]})}function u(){return e.jsxs(o,{children:[e.jsx(n,{path:"newsroom",element:e.jsx(c,{})}),e.jsx(n,{path:"blog",element:e.jsx(g,{})}),e.jsx(n,{path:"press",element:e.jsx(c,{})}),e.jsx(n,{path:"*",element:e.jsx(c,{})})]})}export{u as default};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
import{j as e,L as t}from"./index-ChpGil2q.js";function i(){return e.jsxs("main",{style:{paddingTop:"var(--header-h)",minHeight:"60vh",display:"flex",alignItems:"center",justifyContent:"center",flexDirection:"column",gap:"16px",textAlign:"center"},children:[e.jsx("div",{style:{fontSize:"72px"},children:"404"}),e.jsx("h1",{style:{fontSize:"24px",fontWeight:"700"},children:"페이지를 찾을 수 없습니다"}),e.jsx("p",{style:{color:"var(--gray-600)"},children:"요청하신 페이지가 존재하지 않거나 이동되었습니다."}),e.jsx(t,{to:"/",className:"btn btn-primary",children:"홈으로 돌아가기"})]})}export{i as default};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
.notice-back{font-size:14px;color:var(--primary);margin-bottom:24px;display:inline-flex;align-items:center;gap:4px;cursor:pointer;background:none;border:none}.job-card{padding:28px;display:flex;gap:24px;align-items:flex-start;cursor:pointer}.job-card:hover{border-color:var(--primary)}.job-info{flex:1}.job-title{font-size:18px;font-weight:700;color:var(--gray-900);margin-bottom:8px}.job-desc{font-size:13px;color:var(--gray-600);line-height:1.6;margin-bottom:12px}.job-stack{display:flex;gap:6px;flex-wrap:wrap}.job-tech{padding:4px 10px;background:var(--secondary);color:var(--accent);border-radius:4px;font-size:11px;font-weight:600}.job-meta{display:flex;flex-direction:column;gap:8px;align-items:flex-end;min-width:100px}.job-meta>div{display:flex;flex-direction:column;align-items:flex-end;font-size:13px;color:var(--gray-700)}.job-meta-label{font-size:11px;color:var(--gray-400);margin-bottom:2px}.welfare-cat{font-size:18px;font-weight:800;color:var(--gray-900);margin-bottom:20px}.welfare-card{padding:28px;text-align:center}.welfare-icon{font-size:36px;margin-bottom:12px}.welfare-name{font-size:15px;font-weight:700;margin-bottom:8px;color:var(--gray-900)}.welfare-desc{font-size:13px;color:var(--gray-600);line-height:1.6}.talent-wrap{background:var(--gray-50);border-radius:16px;padding:56px;margin-top:32px}.apply-form .form-group{margin-bottom:20px}.apply-form label{display:block;font-size:13px;font-weight:600;color:var(--gray-700);margin-bottom:6px}.apply-form input,.apply-form select,.apply-form textarea{width:100%;padding:12px 14px;border:1px solid var(--gray-200);border-radius:8px;font-size:14px;font-family:inherit;transition:border-color var(--fast)}.apply-form input:focus,.apply-form select:focus,.apply-form textarea:focus{outline:none;border-color:var(--primary)}.apply-form .form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}.required{color:var(--danger)}.apply-success{text-align:center;padding:80px 40px;background:var(--gray-50);border-radius:16px}.apply-success h3{font-size:24px;font-weight:800;margin-bottom:16px}.apply-success p{font-size:15px;color:var(--gray-600);line-height:1.8}@media (max-width:768px){.job-card{flex-direction:column}.job-meta{align-items:flex-start;flex-direction:row;flex-wrap:wrap}.apply-form .form-row{grid-template-columns:1fr}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
.sol-hero-grid{display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:center}.sol-title{font-size:clamp(26px,3.5vw,40px);font-weight:900;color:var(--gray-900);line-height:1.25;margin:12px 0 20px}.sol-title em{color:var(--primary);font-style:normal}.sol-desc{font-size:15px;color:var(--gray-600);line-height:1.85;margin-bottom:24px}.sol-features{display:flex;flex-direction:column;gap:10px}.sol-feature-item{display:flex;gap:10px;font-size:14px;color:var(--gray-700)}.sol-check{color:var(--accent);font-weight:700;flex-shrink:0}.sol-module-card{padding:32px 24px}.sol-module-icon{font-size:36px;margin-bottom:16px}.sol-module-card h3{font-size:16px;font-weight:700;margin-bottom:10px;color:var(--gray-900)}.sol-module-card p{font-size:13px;color:var(--gray-600);line-height:1.7}.sol-visual{display:flex;justify-content:center}.sol-screen{background:var(--secondary);border-radius:16px;padding:24px;width:100%;max-width:360px;box-shadow:var(--shadow-lg)}.sol-screen-header{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:12px;color:#fff9;font-weight:600}.sol-screen-header span{width:10px;height:10px;border-radius:50%;background:var(--accent);flex-shrink:0}.sol-chart-bar-wrap{display:flex;gap:8px;height:120px;align-items:flex-end;margin-bottom:20px}.sol-chart-bar{flex:1;background:linear-gradient(to top,var(--primary),var(--accent));border-radius:4px 4px 0 0}.sol-stat-row{display:flex;gap:12px}.sol-stat{flex:1;background:#ffffff0f;border-radius:8px;padding:12px;text-align:center}.sol-stat strong{display:block;font-size:14px;color:#fff;font-weight:700}.sol-stat span{font-size:10px;color:#ffffff80;margin-top:4px;display:block}.crm-items{display:flex;flex-direction:column;gap:12px}.crm-item{display:flex;align-items:center;gap:12px;background:#ffffff0d;border-radius:8px;padding:10px 12px}.crm-avatar{width:32px;height:32px;border-radius:50%;background:var(--primary);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:#fff;flex-shrink:0}.crm-info{flex:1}.crm-info strong{display:block;font-size:13px;color:#fff;font-weight:600}.crm-info span{font-size:11px;color:#ffffff80}.crm-status{font-size:11px;font-weight:700}.bi-kpis{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:20px}.bi-kpi{background:#ffffff0f;border-radius:8px;padding:12px}.bi-kpi-label{display:block;font-size:10px;color:#ffffff80;margin-bottom:4px}.bi-kpi-val{display:block;font-size:15px;color:#fff;font-weight:700}.bi-kpi-delta{font-size:11px;font-weight:600}.bi-bar-chart{display:flex;gap:12px;height:80px;align-items:flex-end}.bi-bar-group{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;height:100%}.bi-bar-pair{display:flex;gap:4px;width:100%;height:100%;align-items:flex-end}.bi-bar{flex:1;border-radius:3px 3px 0 0}.bi-bar.revenue{background:var(--accent)}.bi-bar.cost{background:#ef444499}.bi-bar-group span{font-size:10px;color:#ffffff80}@media (max-width: 768px){.sol-hero-grid{grid-template-columns:1fr}.sol-visual{order:-1}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
.notice-header-row{display:grid;grid-template-columns:80px 1fr 100px;gap:16px;padding:12px 16px;background:var(--gray-50);border-radius:8px 8px 0 0;font-size:12px;font-weight:700;color:var(--gray-500);border:1px solid var(--gray-200);border-bottom:none}.notice-row{display:grid;grid-template-columns:80px 1fr 100px;gap:16px;padding:14px 16px;border:1px solid var(--gray-200);border-top:none;cursor:pointer;align-items:center;transition:background var(--fast)}.notice-row:last-child{border-radius:0 0 8px 8px}.notice-row:hover{background:var(--gray-50)}.notice-cat{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:700;text-align:center}.notice-title-text{font-size:14px;color:var(--gray-800);display:flex;align-items:center;gap:8px}.notice-hot{background:var(--danger);color:#fff;font-size:10px;padding:2px 6px;border-radius:4px;font-weight:700;flex-shrink:0}.notice-date{font-size:12px;color:var(--gray-500)}.notice-detail{max-width:760px}.notice-back{font-size:14px;color:var(--primary);margin-bottom:24px;display:inline-flex;align-items:center;gap:4px;cursor:pointer;background:none;border:none}.notice-detail-header{border-bottom:2px solid var(--gray-200);padding-bottom:20px;margin-bottom:32px}.notice-detail-header h2{font-size:22px;font-weight:800;margin:12px 0 8px}.notice-body{display:flex;flex-direction:column;gap:16px;font-size:15px;line-height:1.85;color:var(--gray-700)}.faq-cat-wrap{margin-bottom:40px}.faq-cat-title{font-size:16px;font-weight:800;color:var(--primary);margin-bottom:12px;padding-bottom:8px;border-bottom:2px solid var(--primary-light)}.faq-item{border:1px solid var(--gray-200);border-radius:8px;margin-bottom:8px;overflow:hidden;transition:box-shadow var(--fast)}.faq-item.open{box-shadow:var(--shadow);border-color:var(--primary-light)}.faq-q{width:100%;display:flex;align-items:center;gap:14px;padding:16px 20px;font-size:15px;font-weight:600;color:var(--gray-800);text-align:left;background:none;border:none;cursor:pointer;transition:background var(--fast)}.faq-q:hover{background:var(--gray-50)}.faq-icon{width:24px;height:24px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}.faq-a{padding:0 20px 20px 58px;font-size:14px;color:var(--gray-600);line-height:1.8}.faq-more{text-align:center;padding:48px;background:var(--gray-50);border-radius:12px;margin-top:32px}.faq-more p{color:var(--gray-600);margin-bottom:20px;font-size:16px}.catalog-card{padding:0;display:flex;flex-direction:column}.catalog-icon-wrap{padding:32px;display:flex;align-items:center;justify-content:center}.catalog-icon{font-size:48px}.catalog-info{padding:0 24px 16px;flex:1}.catalog-title{font-size:15px;font-weight:700;margin-bottom:8px;color:var(--gray-900)}.catalog-desc{font-size:13px;color:var(--gray-600);margin-bottom:12px;line-height:1.6}.catalog-meta{display:flex;gap:12px;font-size:12px;color:var(--gray-400)}.catalog-btn{margin:0 16px 20px;padding:12px;border-radius:8px;background:var(--gray-50);border:1px solid var(--gray-200);font-size:14px;font-weight:600;color:var(--primary);cursor:pointer;transition:all var(--fast);text-align:center}.catalog-btn:hover{background:var(--primary);color:#fff;border-color:var(--primary)}.catalog-request{text-align:center;padding:56px;background:var(--gray-50);border-radius:16px;margin-top:48px}.catalog-request h3{font-size:22px;font-weight:800;margin-bottom:12px}.catalog-request p{color:var(--gray-600);margin-bottom:24px}
|
||||||
68
backend/src/main/resources/static/assets/index-ChpGil2q.js
Normal file
BIN
backend/src/main/resources/static/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
22
backend/src/main/resources/static/index.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="(주)지오정보기술 — AI 기반 레거시 인프라 자율 운영 플랫폼 GUARDiA ITSM 및 ERP·CRM·BI 솔루션">
|
||||||
|
<meta name="keywords" content="지오정보기술, GUARDiA, ITSM, 인프라자동화, 공공기관, ERP, ChatOps">
|
||||||
|
<meta property="og:title" content="(주)지오정보기술">
|
||||||
|
<meta property="og:description" content="AI 기반 레거시 인프라 자율 운영 플랫폼 GUARDiA ITSM">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<title>(주)지오정보기술</title>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.ico">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<script type="module" crossorigin src="/assets/index-ChpGil2q.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-Dk81znn6.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
backend/src/main/resources/static/logo-white.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
backend/src/main/resources/static/logo.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
backend/src/main/resources/static/screenshots/01_dashboard.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
backend/src/main/resources/static/screenshots/01_home.png
Normal file
|
After Width: | Height: | Size: 1019 KiB |
|
After Width: | Height: | Size: 615 KiB |
BIN
backend/src/main/resources/static/screenshots/02_guardia.png
Normal file
|
After Width: | Height: | Size: 748 KiB |
|
After Width: | Height: | Size: 355 KiB |
BIN
backend/src/main/resources/static/screenshots/02_sr_list.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
backend/src/main/resources/static/screenshots/03_company.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 202 KiB |
BIN
backend/src/main/resources/static/screenshots/03_si_project.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
backend/src/main/resources/static/screenshots/04_contact.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 209 KiB |
BIN
backend/src/main/resources/static/screenshots/04_incidents.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
backend/src/main/resources/static/screenshots/05_agents.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
backend/src/main/resources/static/screenshots/05_news.png
Normal file
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 221 KiB |
BIN
backend/src/main/resources/static/screenshots/06_license.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
backend/src/main/resources/static/screenshots/06_mobile_home.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
@ -0,0 +1,212 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"page": "홈",
|
||||||
|
"url": "/",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 5849,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "AI 기반 인프라자율 운영 플랫폼",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "GUARDiA ITSM",
|
||||||
|
"url": "/solution/guardia",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1292,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "GUARDiA ITSM",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "솔루션-ERP",
|
||||||
|
"url": "/solution/erp",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1767,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "ERP 솔루션",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "솔루션-CRM",
|
||||||
|
"url": "/solution/crm",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1139,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "CRM 솔루션",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "솔루션-BI",
|
||||||
|
"url": "/solution/bi",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 966,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "BI 솔루션",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "회사-CEO인사말",
|
||||||
|
"url": "/company/greeting",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1098,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "CEO 인사말",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "회사-연혁",
|
||||||
|
"url": "/company/history",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1548,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "연혁",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "회사-조직도",
|
||||||
|
"url": "/company/organization",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 892,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "조직도",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "회사-CI소개",
|
||||||
|
"url": "/company/ci",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1007,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "CI 소개",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "회사-오시는길",
|
||||||
|
"url": "/company/location",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1070,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "오시는 길",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "사업-레퍼런스",
|
||||||
|
"url": "/business/reference",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1111,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "구축 레퍼런스",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "사업-파트너",
|
||||||
|
"url": "/business/partner",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1090,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "파트너",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "지원-공지사항",
|
||||||
|
"url": "/support/notice",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 949,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "공지사항",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "지원-FAQ",
|
||||||
|
"url": "/support/faq",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 931,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "자주 묻는 질문",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "지원-카탈로그",
|
||||||
|
"url": "/support/catalog",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 963,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "카탈로그",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "지원-문의하기",
|
||||||
|
"url": "/support/contact",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1007,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "문의하기",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "채용-공고",
|
||||||
|
"url": "/recruit/jobs",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 984,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "채용공고",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "채용-복리후생",
|
||||||
|
"url": "/recruit/welfare",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1275,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "복리후생",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "채용-지원하기",
|
||||||
|
"url": "/recruit/apply",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 880,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "지원하기",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "뉴스-뉴스룸",
|
||||||
|
"url": "/news/newsroom",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 1144,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "뉴스룸",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "뉴스-블로그",
|
||||||
|
"url": "/news/blog",
|
||||||
|
"http": 200,
|
||||||
|
"loadMs": 989,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"h1": "기술 블로그",
|
||||||
|
"errors": 0,
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 191 KiB |
BIN
backend/src/main/resources/static/screenshots/business_ref.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
backend/src/main/resources/static/screenshots/company_ci.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 169 KiB |
BIN
backend/src/main/resources/static/screenshots/company_org.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
backend/src/main/resources/static/screenshots/guardia.png
Normal file
|
After Width: | Height: | Size: 355 KiB |
BIN
backend/src/main/resources/static/screenshots/home.png
Normal file
|
After Width: | Height: | Size: 615 KiB |
BIN
backend/src/main/resources/static/screenshots/news_blog.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
backend/src/main/resources/static/screenshots/news_newsroom.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
backend/src/main/resources/static/screenshots/recruit_apply.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
backend/src/main/resources/static/screenshots/recruit_jobs.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 220 KiB |
BIN
backend/src/main/resources/static/screenshots/solution_bi.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
backend/src/main/resources/static/screenshots/solution_crm.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
backend/src/main/resources/static/screenshots/solution_erp.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 209 KiB |
BIN
backend/src/main/resources/static/screenshots/support_faq.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
backend/src/main/resources/static/screenshots/support_notice.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
@ -0,0 +1,67 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"page": "홈",
|
||||||
|
"url": "/",
|
||||||
|
"status": 200,
|
||||||
|
"loadMs": 9745,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"links": 37,
|
||||||
|
"images": 2,
|
||||||
|
"h1": 1,
|
||||||
|
"errors": 0,
|
||||||
|
"errorMsgs": [],
|
||||||
|
"screenshot": "C:\\GUARDiA\\workspace\\zioinfo-web\\frontend\\public\\screenshots\\01_home.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "GUARDiA 소개",
|
||||||
|
"url": "/solution/guardia",
|
||||||
|
"status": 200,
|
||||||
|
"loadMs": 1130,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"links": 27,
|
||||||
|
"images": 8,
|
||||||
|
"h1": 1,
|
||||||
|
"errors": 0,
|
||||||
|
"errorMsgs": [],
|
||||||
|
"screenshot": "C:\\GUARDiA\\workspace\\zioinfo-web\\frontend\\public\\screenshots\\02_guardia.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "회사소개",
|
||||||
|
"url": "/company/greeting",
|
||||||
|
"status": 200,
|
||||||
|
"loadMs": 971,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"links": 23,
|
||||||
|
"images": 2,
|
||||||
|
"h1": 1,
|
||||||
|
"errors": 0,
|
||||||
|
"errorMsgs": [],
|
||||||
|
"screenshot": "C:\\GUARDiA\\workspace\\zioinfo-web\\frontend\\public\\screenshots\\03_company.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "문의하기",
|
||||||
|
"url": "/support/contact",
|
||||||
|
"status": 200,
|
||||||
|
"loadMs": 890,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"links": 24,
|
||||||
|
"images": 2,
|
||||||
|
"h1": 1,
|
||||||
|
"errors": 0,
|
||||||
|
"errorMsgs": [],
|
||||||
|
"screenshot": "C:\\GUARDiA\\workspace\\zioinfo-web\\frontend\\public\\screenshots\\04_contact.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"page": "뉴스",
|
||||||
|
"url": "/news/press",
|
||||||
|
"status": 200,
|
||||||
|
"loadMs": 1007,
|
||||||
|
"title": "(주)지오정보기술",
|
||||||
|
"links": 23,
|
||||||
|
"images": 2,
|
||||||
|
"h1": 1,
|
||||||
|
"errors": 0,
|
||||||
|
"errorMsgs": [],
|
||||||
|
"screenshot": "C:\\GUARDiA\\workspace\\zioinfo-web\\frontend\\public\\screenshots\\05_news.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -32,6 +32,9 @@ spring:
|
|||||||
mail.smtp.starttls.enable: true
|
mail.smtp.starttls.enable: true
|
||||||
|
|
||||||
zioinfo:
|
zioinfo:
|
||||||
|
jwt:
|
||||||
|
secret: zioinfo-admin-jwt-secret-key-must-be-at-least-32-chars-long
|
||||||
|
expiration-ms: 28800000 # 8시간
|
||||||
company:
|
company:
|
||||||
name: (주)지오정보기술
|
name: (주)지오정보기술
|
||||||
email: info@zioinfo.co.kr
|
email: info@zioinfo.co.kr
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
kr\co\zioinfo\web\model\Inquiry.class
|
kr\co\zioinfo\web\model\Inquiry.class
|
||||||
kr\co\zioinfo\web\controller\ApiController.class
|
|
||||||
kr\co\zioinfo\web\model\Inquiry$InquiryBuilder.class
|
kr\co\zioinfo\web\model\Inquiry$InquiryBuilder.class
|
||||||
kr\co\zioinfo\web\config\DataInitializer.class
|
kr\co\zioinfo\web\config\DataInitializer.class
|
||||||
kr\co\zioinfo\web\model\News$NewsBuilder.class
|
|
||||||
kr\co\zioinfo\web\repository\NewsRepository.class
|
|
||||||
kr\co\zioinfo\web\service\InquiryService.class
|
kr\co\zioinfo\web\service\InquiryService.class
|
||||||
kr\co\zioinfo\web\service\NewsService.class
|
|
||||||
kr\co\zioinfo\web\model\News.class
|
|
||||||
kr\co\zioinfo\web\repository\InquiryRepository.class
|
kr\co\zioinfo\web\repository\InquiryRepository.class
|
||||||
kr\co\zioinfo\web\ZioinfoWebApplication.class
|
kr\co\zioinfo\web\ZioinfoWebApplication.class
|
||||||
|
kr\co\zioinfo\web\controller\ApiController.class
|
||||||
|
kr\co\zioinfo\web\model\News$NewsBuilder.class
|
||||||
|
kr\co\zioinfo\web\repository\NewsRepository.class
|
||||||
|
kr\co\zioinfo\web\service\NewsService.class
|
||||||
|
kr\co\zioinfo\web\model\News.class
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\model\News.java
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\config\SecurityConfig.java
|
||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\config\DataInitializer.java
|
|
||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\service\NewsService.java
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\service\NewsService.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\controller\AdminController.java
|
||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\ZioinfoWebApplication.java
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\ZioinfoWebApplication.java
|
||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\repository\InquiryRepository.java
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\repository\InquiryRepository.java
|
||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\service\InquiryService.java
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\service\InquiryService.java
|
||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\controller\ApiController.java
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\controller\ApiController.java
|
||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\repository\NewsRepository.java
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\repository\AdminUserRepository.java
|
||||||
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\model\Inquiry.java
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\model\Inquiry.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\model\News.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\config\DataInitializer.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\model\Recruit.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\repository\RecruitRepository.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\security\JwtUtil.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\repository\NewsRepository.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\model\AdminUser.java
|
||||||
|
C:\GUARDiA\workspace\zioinfo-web\backend\src\main\java\kr\co\zioinfo\web\security\JwtAuthFilter.java
|
||||||
|
|||||||
69
deploy/01_oracle_cloud_guide.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Oracle Cloud Always Free — zio-server 구축 가이드
|
||||||
|
|
||||||
|
## 1단계: Oracle Cloud 계정 생성
|
||||||
|
|
||||||
|
1. https://www.oracle.com/cloud/free/ 접속
|
||||||
|
2. "Start for free" 클릭
|
||||||
|
3. 정보 입력:
|
||||||
|
- Country: South Korea
|
||||||
|
- 이름, 이메일, 비밀번호
|
||||||
|
4. **신용카드 등록 필수** (과금 없음 — 인증용)
|
||||||
|
5. 가입 완료 후 홈 리전 선택: **South Korea Central (Seoul)**
|
||||||
|
|
||||||
|
> ⚠️ 홈 리전은 변경 불가 — 반드시 Seoul 선택
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2단계: VM 인스턴스 생성 (zio-server)
|
||||||
|
|
||||||
|
### 콘솔 접속
|
||||||
|
Oracle Cloud Console → Compute → Instances → Create Instance
|
||||||
|
|
||||||
|
### 설정값
|
||||||
|
|
||||||
|
| 항목 | 값 |
|
||||||
|
|------|----|
|
||||||
|
| **Name** | `zio-server` |
|
||||||
|
| **Image** | Ubuntu 22.04 (Canonical) |
|
||||||
|
| **Shape** | VM.Standard.A1.Flex (Ampere) |
|
||||||
|
| **OCPU** | 4 |
|
||||||
|
| **Memory** | 24 GB |
|
||||||
|
| **Boot Volume** | 100 GB |
|
||||||
|
| **Network** | Default VCN, Public Subnet |
|
||||||
|
| **공인 IP** | Assign public IP: Yes |
|
||||||
|
|
||||||
|
### SSH 키 생성
|
||||||
|
```
|
||||||
|
로컬에서:
|
||||||
|
ssh-keygen -t rsa -b 4096 -f C:\Users\{username}\.ssh\zio-server
|
||||||
|
```
|
||||||
|
- 생성된 `zio-server.pub` 내용을 콘솔에 붙여넣기
|
||||||
|
|
||||||
|
### 생성 완료
|
||||||
|
- 약 2~3분 후 Running 상태 확인
|
||||||
|
- 공인 IP 메모 (예: 140.238.xxx.xxx)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3단계: 방화벽 오픈 (Security List)
|
||||||
|
|
||||||
|
Networking → Virtual Cloud Networks → Default VCN
|
||||||
|
→ Security Lists → Default Security List
|
||||||
|
→ Add Ingress Rules:
|
||||||
|
|
||||||
|
| 포트 | 프로토콜 | 용도 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 22 | TCP | SSH |
|
||||||
|
| 80 | TCP | HTTP |
|
||||||
|
| 443 | TCP | HTTPS |
|
||||||
|
| 8080 | TCP | Spring Boot (개발용) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4단계: SSH 접속
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh -i C:\Users\{username}\.ssh\zio-server ubuntu@{공인IP}
|
||||||
|
```
|
||||||
|
|
||||||
|
접속 성공 후 → 5단계 서버 설정 스크립트 실행
|
||||||
110
deploy/02_server_setup.sh
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# zio-server 초기 환경 구성 스크립트
|
||||||
|
# Oracle Cloud Ubuntu 22.04 ARM (Ampere A1)
|
||||||
|
# 실행: bash 02_server_setup.sh
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
section() { echo -e "\n${CYAN}=== $1 ===${NC}"; }
|
||||||
|
|
||||||
|
section "1. 시스템 업데이트"
|
||||||
|
sudo apt-get update -y && sudo apt-get upgrade -y
|
||||||
|
sudo apt-get install -y curl wget git unzip net-tools ufw htop
|
||||||
|
info "시스템 업데이트 완료"
|
||||||
|
|
||||||
|
section "2. Java 21 설치 (Spring Boot용)"
|
||||||
|
sudo apt-get install -y openjdk-21-jdk
|
||||||
|
java -version
|
||||||
|
info "Java 21 설치 완료"
|
||||||
|
|
||||||
|
section "3. Node.js 20 LTS 설치 (React 빌드용)"
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
node -v && npm -v
|
||||||
|
info "Node.js $(node -v) 설치 완료"
|
||||||
|
|
||||||
|
section "4. Nginx 설치"
|
||||||
|
sudo apt-get install -y nginx
|
||||||
|
sudo systemctl enable nginx
|
||||||
|
sudo systemctl start nginx
|
||||||
|
info "Nginx 설치 완료"
|
||||||
|
|
||||||
|
section "5. UFW 방화벽 설정"
|
||||||
|
sudo ufw allow ssh
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
sudo ufw allow 8080/tcp
|
||||||
|
sudo ufw --force enable
|
||||||
|
sudo ufw status
|
||||||
|
info "방화벽 설정 완료"
|
||||||
|
|
||||||
|
# Oracle Cloud 내부 iptables도 열기 (필수!)
|
||||||
|
section "6. Oracle Cloud iptables 규칙 추가"
|
||||||
|
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT
|
||||||
|
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT
|
||||||
|
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 8080 -j ACCEPT
|
||||||
|
sudo netfilter-persistent save 2>/dev/null || {
|
||||||
|
sudo apt-get install -y iptables-persistent
|
||||||
|
sudo netfilter-persistent save
|
||||||
|
}
|
||||||
|
info "iptables 규칙 저장 완료"
|
||||||
|
|
||||||
|
section "7. 앱 디렉터리 생성"
|
||||||
|
sudo mkdir -p /var/www/zioinfo
|
||||||
|
sudo mkdir -p /opt/zioinfo/app
|
||||||
|
sudo chown -R ubuntu:ubuntu /var/www/zioinfo /opt/zioinfo
|
||||||
|
info "디렉터리 생성 완료"
|
||||||
|
|
||||||
|
section "8. Nginx 설정"
|
||||||
|
sudo tee /etc/nginx/sites-available/zioinfo > /dev/null <<'NGINX'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /var/www/zioinfo;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# React SPA — 모든 경로를 index.html로
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spring Boot API 프록시
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 정적 파일 캐시
|
||||||
|
location ~* \.(js|css|png|jpg|gif|ico|svg|woff2)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# 보안 헤더
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
|
||||||
|
# Gzip 압축
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/zioinfo /etc/nginx/sites-enabled/
|
||||||
|
sudo rm -f /etc/nginx/sites-enabled/default
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
info "Nginx 설정 완료"
|
||||||
|
|
||||||
|
section "✅ 서버 초기 구성 완료!"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}다음 단계: 로컬에서 03_deploy.sh 실행${NC}"
|
||||||
|
echo -e "서버 IP: $(curl -s ifconfig.me)"
|
||||||
110
deploy/03_deploy.ps1
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# ============================================================
|
||||||
|
# zio-server 홈페이지 배포 스크립트 (Windows PowerShell)
|
||||||
|
# 실행: .\deploy\03_deploy.ps1 -ServerIP "140.238.xxx.xxx"
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$ServerIP,
|
||||||
|
[string]$KeyPath = "$env:USERPROFILE\.ssh\zio-server",
|
||||||
|
[string]$User = "ubuntu"
|
||||||
|
)
|
||||||
|
|
||||||
|
$GREEN = "`e[32m"
|
||||||
|
$YELLOW = "`e[33m"
|
||||||
|
$CYAN = "`e[36m"
|
||||||
|
$NC = "`e[0m"
|
||||||
|
|
||||||
|
function Log-Info { param($msg) Write-Host "${GREEN}[OK]${NC} $msg" }
|
||||||
|
function Log-Section { param($msg) Write-Host "`n${CYAN}=== $msg ===${NC}" }
|
||||||
|
function Log-Warn { param($msg) Write-Host "${YELLOW}[!]${NC} $msg" }
|
||||||
|
|
||||||
|
$SSH = "ssh -i `"$KeyPath`" -o StrictHostKeyChecking=no ${User}@${ServerIP}"
|
||||||
|
$SCP = "scp -i `"$KeyPath`" -o StrictHostKeyChecking=no"
|
||||||
|
$ROOT = "C:\GUARDiA\workspace\zioinfo-web"
|
||||||
|
|
||||||
|
Log-Section "1. React 프론트엔드 빌드"
|
||||||
|
Set-Location "$ROOT\frontend"
|
||||||
|
|
||||||
|
# vite.config.js 빌드 경로를 임시 dist로 변경
|
||||||
|
$viteCfg = Get-Content "vite.config.js" -Raw
|
||||||
|
$buildCfg = $viteCfg -replace "outDir: '.*?'", "outDir: 'dist'"
|
||||||
|
$buildCfg | Set-Content "vite.config.js" -Encoding utf8
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Error "빌드 실패"; exit 1 }
|
||||||
|
Log-Info "React 빌드 완료 → frontend/dist/"
|
||||||
|
|
||||||
|
Log-Section "2. 빌드 파일 서버 업로드"
|
||||||
|
Invoke-Expression "$SSH 'rm -rf /var/www/zioinfo/* && mkdir -p /var/www/zioinfo'"
|
||||||
|
Invoke-Expression "$SCP -r `"$ROOT\frontend\dist\*`" ${User}@${ServerIP}:/var/www/zioinfo/"
|
||||||
|
Log-Info "정적 파일 업로드 완료"
|
||||||
|
|
||||||
|
Log-Section "3. Spring Boot JAR 빌드"
|
||||||
|
Set-Location "$ROOT"
|
||||||
|
if (Test-Path "pom.xml") {
|
||||||
|
# Maven 빌드 (Spring Boot 백엔드)
|
||||||
|
$mvnw = if (Test-Path "mvnw.cmd") { ".\mvnw.cmd" } else { "mvn" }
|
||||||
|
& $mvnw clean package -DskipTests -q
|
||||||
|
$jar = Get-ChildItem "target\*.jar" -Exclude "*sources*" | Select-Object -First 1
|
||||||
|
if ($jar) {
|
||||||
|
Log-Info "JAR 빌드 완료: $($jar.Name)"
|
||||||
|
Invoke-Expression "$SCP `"$($jar.FullName)`" ${User}@${ServerIP}:/opt/zioinfo/app/zioinfo.jar"
|
||||||
|
Log-Info "JAR 업로드 완료"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log-Warn "pom.xml 없음 — Spring Boot 배포 스킵 (정적 파일만 배포)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log-Section "4. systemd 서비스 등록 (Spring Boot)"
|
||||||
|
$serviceScript = @'
|
||||||
|
# Spring Boot 서비스 설정
|
||||||
|
sudo tee /etc/systemd/system/zioinfo.service > /dev/null <<SERVICE
|
||||||
|
[Unit]
|
||||||
|
Description=Zioinfo Spring Boot Application
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/opt/zioinfo/app
|
||||||
|
ExecStart=/usr/bin/java -jar -Xms256m -Xmx512m /opt/zioinfo/app/zioinfo.jar
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
SERVICE
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable zioinfo
|
||||||
|
sudo systemctl restart zioinfo 2>/dev/null || echo "서비스 시작 대기 중..."
|
||||||
|
'@
|
||||||
|
|
||||||
|
if (Test-Path "$ROOT\target\*.jar") {
|
||||||
|
Invoke-Expression "$SSH '$serviceScript'"
|
||||||
|
Log-Info "Spring Boot 서비스 등록 완료"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log-Section "5. Nginx 재시작 및 최종 확인"
|
||||||
|
$checkScript = @"
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
echo '--- Nginx 상태 ---'
|
||||||
|
sudo systemctl is-active nginx
|
||||||
|
echo '--- 포트 확인 ---'
|
||||||
|
ss -tlnp | grep -E ':80|:443|:8080'
|
||||||
|
echo '--- 디스크 사용량 ---'
|
||||||
|
df -h /
|
||||||
|
echo '--- 메모리 ---'
|
||||||
|
free -h
|
||||||
|
"@
|
||||||
|
Invoke-Expression "$SSH '$checkScript'"
|
||||||
|
|
||||||
|
Log-Section "✅ 배포 완료!"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "${GREEN}홈페이지 주소:${NC} http://$ServerIP"
|
||||||
|
Write-Host "${GREEN}SSH 접속:${NC} ssh -i `"$KeyPath`" ubuntu@$ServerIP"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "${YELLOW}브라우저에서 확인:${NC}"
|
||||||
|
Start-Process "http://$ServerIP"
|
||||||
25
deploy/04_ssl_setup.sh
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# SSL 인증서 설정 (Let's Encrypt / Certbot)
|
||||||
|
# 도메인이 있을 경우 실행
|
||||||
|
# 실행: bash 04_ssl_setup.sh yourdomain.com
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
DOMAIN=${1:-"zioinfo.co.kr"}
|
||||||
|
|
||||||
|
echo "[1] Certbot 설치"
|
||||||
|
sudo apt-get install -y certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
echo "[2] SSL 인증서 발급 — $DOMAIN"
|
||||||
|
sudo certbot --nginx -d $DOMAIN -d www.$DOMAIN \
|
||||||
|
--non-interactive --agree-tos --email admin@$DOMAIN \
|
||||||
|
--redirect
|
||||||
|
|
||||||
|
echo "[3] 자동 갱신 확인"
|
||||||
|
sudo certbot renew --dry-run
|
||||||
|
|
||||||
|
echo "[4] Nginx 재시작"
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
|
||||||
|
echo "✅ SSL 설정 완료!"
|
||||||
|
echo " https://$DOMAIN 으로 접속하세요"
|
||||||
21
deploy/05_update_deploy.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# 빠른 업데이트 배포 스크립트 (서버에서 실행)
|
||||||
|
# 로컬에서 SCP로 파일 올린 후 서버에서 실행
|
||||||
|
# 실행: bash 05_update_deploy.sh
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
echo "[1] 새 파일 적용"
|
||||||
|
sudo cp -r /tmp/dist/* /var/www/zioinfo/
|
||||||
|
sudo chown -R www-data:www-data /var/www/zioinfo/
|
||||||
|
|
||||||
|
echo "[2] Spring Boot 재시작 (있을 경우)"
|
||||||
|
if systemctl is-active --quiet zioinfo; then
|
||||||
|
sudo systemctl restart zioinfo
|
||||||
|
echo " Spring Boot 재시작됨"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[3] Nginx 재로드"
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
|
||||||
|
echo "✅ 업데이트 완료! $(date)"
|
||||||
135
deploy/06_guardia_server_setup.sh
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# GUARDiA ITSM 서버 환경 구성
|
||||||
|
# Oracle Cloud Ubuntu 22.04 ARM (Ampere A1)
|
||||||
|
# 실행: bash 06_guardia_server_setup.sh
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
section() { echo -e "\n${CYAN}════════════════════════════════${NC}"; echo -e "${CYAN} $1${NC}"; echo -e "${CYAN}════════════════════════════════${NC}"; }
|
||||||
|
|
||||||
|
# ── 1. Python 3.11 ──────────────────────────────────────────
|
||||||
|
section "1. Python 3.11 설치"
|
||||||
|
sudo apt-get install -y python3.11 python3.11-venv python3.11-dev python3-pip
|
||||||
|
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
|
||||||
|
python3 --version
|
||||||
|
info "Python 3.11 설치 완료"
|
||||||
|
|
||||||
|
# ── 2. PostgreSQL 15 ─────────────────────────────────────────
|
||||||
|
section "2. PostgreSQL 15 설치"
|
||||||
|
sudo apt-get install -y postgresql postgresql-contrib
|
||||||
|
sudo systemctl enable postgresql
|
||||||
|
sudo systemctl start postgresql
|
||||||
|
|
||||||
|
# DB / 사용자 생성
|
||||||
|
sudo -u postgres psql <<PSQL
|
||||||
|
CREATE USER guardia WITH PASSWORD 'G@urd1a_2026!';
|
||||||
|
CREATE DATABASE guardia_db OWNER guardia;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE guardia_db TO guardia;
|
||||||
|
CREATE DATABASE zioinfo_db OWNER guardia;
|
||||||
|
PSQL
|
||||||
|
info "PostgreSQL 설치 및 DB 생성 완료"
|
||||||
|
|
||||||
|
# ── 3. Ollama + LLM ─────────────────────────────────────────
|
||||||
|
section "3. Ollama 설치 (온프레미스 AI 엔진)"
|
||||||
|
curl -fsSL https://ollama.ai/install.sh | sh
|
||||||
|
sudo systemctl enable ollama
|
||||||
|
sudo systemctl start ollama
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# LLM 모델 다운로드 (ARM 최적화)
|
||||||
|
echo "Llama-3 8B 모델 다운로드 중 (약 4.7GB)..."
|
||||||
|
ollama pull llama3:8b
|
||||||
|
info "Ollama + Llama-3 8B 설치 완료"
|
||||||
|
|
||||||
|
# ── 4. GUARDiA 디렉터리 ──────────────────────────────────────
|
||||||
|
section "4. GUARDiA ITSM 디렉터리 구조 생성"
|
||||||
|
sudo mkdir -p /opt/guardia/{app,logs,backups,uploads,static}
|
||||||
|
sudo chown -R ubuntu:ubuntu /opt/guardia
|
||||||
|
info "GUARDiA 디렉터리 생성 완료"
|
||||||
|
|
||||||
|
# ── 5. Python 가상환경 ───────────────────────────────────────
|
||||||
|
section "5. Python 가상환경 및 패키지 설치"
|
||||||
|
python3 -m venv /opt/guardia/venv
|
||||||
|
source /opt/guardia/venv/bin/activate
|
||||||
|
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install \
|
||||||
|
fastapi==0.115.0 \
|
||||||
|
uvicorn[standard]==0.30.0 \
|
||||||
|
sqlalchemy==2.0.35 \
|
||||||
|
alembic==1.13.0 \
|
||||||
|
asyncpg==0.29.0 \
|
||||||
|
psycopg2-binary==2.9.9 \
|
||||||
|
python-jose[cryptography]==3.3.0 \
|
||||||
|
passlib[bcrypt]==1.7.4 \
|
||||||
|
python-multipart==0.0.9 \
|
||||||
|
httpx==0.27.0 \
|
||||||
|
paramiko==3.4.0 \
|
||||||
|
openpyxl==3.1.5 \
|
||||||
|
reportlab==4.2.0 \
|
||||||
|
python-dotenv==1.0.1 \
|
||||||
|
pydantic-settings==2.4.0 \
|
||||||
|
aiofiles==23.2.1 \
|
||||||
|
pyotp==2.9.0 \
|
||||||
|
cryptography==43.0.1 \
|
||||||
|
websockets==12.0
|
||||||
|
|
||||||
|
deactivate
|
||||||
|
info "Python 패키지 설치 완료"
|
||||||
|
|
||||||
|
# ── 6. .env 파일 생성 ────────────────────────────────────────
|
||||||
|
section "6. 환경 변수 파일 생성"
|
||||||
|
cat > /opt/guardia/app/.env <<ENV
|
||||||
|
# GUARDiA ITSM 환경 설정
|
||||||
|
APP_ENV=production
|
||||||
|
SECRET_KEY=$(openssl rand -hex 32)
|
||||||
|
DATABASE_URL=postgresql+asyncpg://guardia:G@urd1a_2026!@localhost:5432/guardia_db
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
|
LLM_MODEL=llama3:8b
|
||||||
|
ALLOWED_ORIGINS=["https://itsm.zioinfo.co.kr","http://localhost:3000"]
|
||||||
|
|
||||||
|
# 라이선스
|
||||||
|
LICENSE_KEY=
|
||||||
|
TRIAL_DURATION_DAYS=30
|
||||||
|
|
||||||
|
# 이메일 (선택)
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
ENV
|
||||||
|
chmod 600 /opt/guardia/app/.env
|
||||||
|
info ".env 파일 생성 완료"
|
||||||
|
|
||||||
|
# ── 7. systemd 서비스 ────────────────────────────────────────
|
||||||
|
section "7. GUARDiA systemd 서비스 등록"
|
||||||
|
sudo tee /etc/systemd/system/guardia.service > /dev/null <<SERVICE
|
||||||
|
[Unit]
|
||||||
|
Description=GUARDiA ITSM Platform
|
||||||
|
After=network.target postgresql.service ollama.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/opt/guardia/app
|
||||||
|
Environment="PATH=/opt/guardia/venv/bin"
|
||||||
|
ExecStart=/opt/guardia/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8001 --workers 2
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=append:/opt/guardia/logs/guardia.log
|
||||||
|
StandardError=append:/opt/guardia/logs/error.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
SERVICE
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable guardia
|
||||||
|
info "GUARDiA 서비스 등록 완료"
|
||||||
|
|
||||||
|
section "✅ GUARDiA 서버 환경 구성 완료!"
|
||||||
|
echo ""
|
||||||
|
echo "다음 단계: bash 07_nginx_all.sh 실행"
|
||||||
173
deploy/07_nginx_all.sh
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Nginx 통합 설정 — zioinfo 홈페이지 + GUARDiA ITSM
|
||||||
|
# 실행: bash 07_nginx_all.sh [도메인] [itsm도메인]
|
||||||
|
# 예시: bash 07_nginx_all.sh zioinfo.co.kr itsm.zioinfo.co.kr
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
DOMAIN=${1:-"$(curl -s ifconfig.me)"} # IP 또는 도메인
|
||||||
|
ITSM_DOMAIN=${2:-"itsm.${DOMAIN}"}
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
section() { echo -e "\n${CYAN}=== $1 ===${NC}"; }
|
||||||
|
|
||||||
|
section "1. zioinfo 홈페이지 Nginx 설정"
|
||||||
|
sudo tee /etc/nginx/sites-available/zioinfo <<NGINX
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# (주)지오정보기술 홈페이지
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${DOMAIN} www.${DOMAIN};
|
||||||
|
|
||||||
|
root /var/www/zioinfo;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# React SPA
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ /index.html;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
# 정적 자산 캐시
|
||||||
|
location ~* \.(js|css|png|jpg|gif|ico|svg|woff2|ttf)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spring Boot API (홈페이지 문의 접수 등)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection keep-alive;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_cache_bypass \$http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 보안 헤더
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "same-origin" always;
|
||||||
|
|
||||||
|
# gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
info "zioinfo 홈페이지 설정 완료"
|
||||||
|
|
||||||
|
section "2. GUARDiA ITSM Nginx 설정"
|
||||||
|
sudo tee /etc/nginx/sites-available/guardia <<NGINX
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# GUARDiA ITSM 관리 시스템
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
upstream guardia_api {
|
||||||
|
server 127.0.0.1:8001;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${ITSM_DOMAIN};
|
||||||
|
|
||||||
|
client_max_body_size 100M;
|
||||||
|
|
||||||
|
# GUARDiA React SPA + FastAPI
|
||||||
|
location / {
|
||||||
|
proxy_pass http://guardia_api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# WebSocket (SSE + WS)
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://guardia_api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 정적 파일 직접 서빙 (성능)
|
||||||
|
location /static/ {
|
||||||
|
alias /opt/guardia/app/static/;
|
||||||
|
expires 7d;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 업로드 파일
|
||||||
|
location /uploads/ {
|
||||||
|
alias /opt/guardia/uploads/;
|
||||||
|
expires 1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 보안 — 내부 전용 경로 차단
|
||||||
|
location ~ ^/(admin/shell|api/ssh/exec) {
|
||||||
|
allow 127.0.0.1;
|
||||||
|
deny all;
|
||||||
|
proxy_pass http://guardia_api;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 보안 헤더
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json;
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
info "GUARDiA ITSM 설정 완료"
|
||||||
|
|
||||||
|
section "3. IP 직접 접속용 기본 설정 (도메인 없을 때)"
|
||||||
|
sudo tee /etc/nginx/sites-available/default-ip <<NGINX
|
||||||
|
# IP로 직접 접속 → zioinfo 홈페이지
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
root /var/www/zioinfo;
|
||||||
|
index index.html;
|
||||||
|
location / { try_files \$uri \$uri/ /index.html; }
|
||||||
|
}
|
||||||
|
|
||||||
|
# IP:8001 → GUARDiA ITSM (개발용)
|
||||||
|
server {
|
||||||
|
listen 8001;
|
||||||
|
server_name _;
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8001;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
|
||||||
|
section "4. 심볼릭 링크 및 설정 적용"
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/zioinfo /etc/nginx/sites-enabled/zioinfo
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/guardia /etc/nginx/sites-enabled/guardia
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/default-ip /etc/nginx/sites-enabled/default-ip
|
||||||
|
sudo rm -f /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
info "Nginx 설정 적용 완료"
|
||||||
|
|
||||||
|
section "5. 상태 확인"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ 서비스 접속 주소${NC}"
|
||||||
|
echo " 홈페이지: http://${DOMAIN}"
|
||||||
|
echo " GUARDiA ITSM: http://${ITSM_DOMAIN}"
|
||||||
|
echo " (IP 직접)홈: http://$(curl -s ifconfig.me)"
|
||||||
|
echo " (IP 직접)ITSM: http://$(curl -s ifconfig.me):8001"
|
||||||
116
deploy/08_deploy_guardia.ps1
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# ============================================================
|
||||||
|
# GUARDiA ITSM 배포 스크립트 (Windows PowerShell)
|
||||||
|
# 실행: .\deploy\08_deploy_guardia.ps1 -ServerIP "xxx.xxx.xxx.xxx"
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)] [string]$ServerIP,
|
||||||
|
[string]$KeyPath = "$env:USERPROFILE\.ssh\zio-server",
|
||||||
|
[string]$User = "ubuntu"
|
||||||
|
)
|
||||||
|
|
||||||
|
$GUARDIA = "C:\GUARDiA\itsm"
|
||||||
|
$SSH = "ssh -i `"$KeyPath`" -o StrictHostKeyChecking=no ${User}@${ServerIP}"
|
||||||
|
$SCP = "scp -i `"$KeyPath`" -o StrictHostKeyChecking=no -r"
|
||||||
|
|
||||||
|
function Log-Section { param($msg) Write-Host "`n`e[36m=== $msg ===`e[0m" }
|
||||||
|
function Log-Info { param($msg) Write-Host "`e[32m[OK]`e[0m $msg" }
|
||||||
|
|
||||||
|
Log-Section "1. GUARDiA 소스 압축"
|
||||||
|
$zipPath = "$env:TEMP\guardia_deploy.zip"
|
||||||
|
if (Test-Path $zipPath) { Remove-Item $zipPath }
|
||||||
|
|
||||||
|
# 배포 대상 파일만 포함 (node_modules, __pycache__ 제외)
|
||||||
|
Add-Type -Assembly System.IO.Compression.FileSystem
|
||||||
|
$zip = [System.IO.Compression.ZipFile]::Open($zipPath, 'Create')
|
||||||
|
|
||||||
|
$exclude = @('__pycache__', '.pyc', 'node_modules', '.git', '*.db', 'uploads', '.env')
|
||||||
|
Get-ChildItem $GUARDIA -Recurse -File | Where-Object {
|
||||||
|
$path = $_.FullName
|
||||||
|
-not ($exclude | Where-Object { $path -like "*$_*" })
|
||||||
|
} | ForEach-Object {
|
||||||
|
$entryName = $_.FullName.Replace("$GUARDIA\", "").Replace("\", "/")
|
||||||
|
try {
|
||||||
|
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
|
||||||
|
$zip, $_.FullName, $entryName) | Out-Null
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
$zip.Dispose()
|
||||||
|
Log-Info "소스 압축 완료: $zipPath"
|
||||||
|
|
||||||
|
Log-Section "2. 서버로 업로드"
|
||||||
|
Invoke-Expression "$SCP `"$zipPath`" ${User}@${ServerIP}:/tmp/guardia_deploy.zip"
|
||||||
|
Log-Info "업로드 완료"
|
||||||
|
|
||||||
|
Log-Section "3. 서버에서 압축 해제 및 설치"
|
||||||
|
$deployScript = @'
|
||||||
|
set -e
|
||||||
|
echo "[1] 압축 해제"
|
||||||
|
mkdir -p /tmp/guardia_src
|
||||||
|
cd /tmp/guardia_src && unzip -qo /tmp/guardia_deploy.zip
|
||||||
|
|
||||||
|
echo "[2] 파일 복사"
|
||||||
|
cp -r /tmp/guardia_src/* /opt/guardia/app/
|
||||||
|
chown -R ubuntu:ubuntu /opt/guardia/app
|
||||||
|
|
||||||
|
echo "[3] DB 초기화"
|
||||||
|
source /opt/guardia/venv/bin/activate
|
||||||
|
cd /opt/guardia/app
|
||||||
|
|
||||||
|
# .env 파일이 없으면 기본값으로 생성
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp /opt/guardia/app/.env.example .env 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DB 마이그레이션
|
||||||
|
python3 db_init.py --force 2>/dev/null || python3 -c "
|
||||||
|
from database import engine, Base
|
||||||
|
from models import *
|
||||||
|
import asyncio
|
||||||
|
async def init():
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
asyncio.run(init())
|
||||||
|
print('DB 초기화 완료')
|
||||||
|
" 2>/dev/null || echo "DB 초기화 스킵 (이미 존재)"
|
||||||
|
|
||||||
|
echo "[4] 서비스 재시작"
|
||||||
|
sudo systemctl restart guardia
|
||||||
|
sleep 3
|
||||||
|
sudo systemctl is-active guardia && echo "GUARDiA 서비스 실행 중" || echo "서비스 시작 실패 — 로그 확인 필요"
|
||||||
|
|
||||||
|
echo "[5] 정리"
|
||||||
|
rm -rf /tmp/guardia_src /tmp/guardia_deploy.zip
|
||||||
|
echo "배포 완료!"
|
||||||
|
'@
|
||||||
|
|
||||||
|
Invoke-Expression "$SSH 'bash -s'" | bash -s <<< "$deployScript"
|
||||||
|
|
||||||
|
Log-Section "4. 접속 확인"
|
||||||
|
$checkScript = @"
|
||||||
|
echo '--- GUARDiA 서비스 상태 ---'
|
||||||
|
sudo systemctl status guardia --no-pager -l | head -20
|
||||||
|
echo ''
|
||||||
|
echo '--- 포트 확인 ---'
|
||||||
|
ss -tlnp | grep -E ':8001|:8080|:80'
|
||||||
|
echo ''
|
||||||
|
echo '--- 메모리 사용량 ---'
|
||||||
|
free -h
|
||||||
|
echo ''
|
||||||
|
echo '--- GUARDiA API 헬스체크 ---'
|
||||||
|
sleep 2
|
||||||
|
curl -s http://localhost:8001/api/admin/health | python3 -m json.tool 2>/dev/null || echo 'API 응답 대기 중...'
|
||||||
|
"@
|
||||||
|
Invoke-Expression "$SSH '$checkScript'"
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "`e[32m✅ GUARDiA ITSM 배포 완료!`e[0m"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " 홈페이지: http://$ServerIP"
|
||||||
|
Write-Host " GUARDiA ITSM: http://$ServerIP`:8001"
|
||||||
|
Write-Host " 관리자 로그인: admin / admin (최초 접속 후 변경 필수)"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "`e[33m보안 권고:`e[0m"
|
||||||
|
Write-Host " 1. 최초 로그인 후 즉시 비밀번호 변경"
|
||||||
|
Write-Host " 2. MFA 활성화: POST /api/auth/mfa/setup"
|
||||||
|
Write-Host " 3. 포트 8001 → Nginx 뒤로 숨기기 (07_nginx_all.sh 실행)"
|
||||||
44
deploy/09_full_deploy_guide.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# zio-server 전체 배포 가이드
|
||||||
|
|
||||||
|
## 서버 구성 완료 후 실행 순서
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 기본 환경 (Java, Node, Nginx)
|
||||||
|
bash 02_server_setup.sh
|
||||||
|
|
||||||
|
# 2. GUARDiA 환경 (Python, PostgreSQL, Ollama)
|
||||||
|
bash 06_guardia_server_setup.sh
|
||||||
|
|
||||||
|
# 3. Nginx 통합 설정
|
||||||
|
bash 07_nginx_all.sh zioinfo.co.kr itsm.zioinfo.co.kr
|
||||||
|
|
||||||
|
# 4. 홈페이지 배포 (Windows에서)
|
||||||
|
.\deploy\03_deploy.ps1 -ServerIP "서버IP"
|
||||||
|
|
||||||
|
# 5. GUARDiA 배포 (Windows에서)
|
||||||
|
.\deploy\08_deploy_guardia.ps1 -ServerIP "서버IP"
|
||||||
|
|
||||||
|
# 6. SSL 인증서 (도메인 있을 때)
|
||||||
|
bash 04_ssl_setup.sh zioinfo.co.kr
|
||||||
|
```
|
||||||
|
|
||||||
|
## 최종 서비스 목록
|
||||||
|
|
||||||
|
| 서비스 | 주소 | 포트 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 지오정보기술 홈페이지 | http://zioinfo.co.kr | 80/443 |
|
||||||
|
| GUARDiA ITSM | http://itsm.zioinfo.co.kr | 80/443 |
|
||||||
|
| PostgreSQL | 내부 전용 | 5432 |
|
||||||
|
| Ollama LLM | 내부 전용 | 11434 |
|
||||||
|
|
||||||
|
## 리소스 사용 예상
|
||||||
|
|
||||||
|
| 구성 요소 | CPU | RAM |
|
||||||
|
|-----------|-----|-----|
|
||||||
|
| Nginx | 0.1 OCPU | 50 MB |
|
||||||
|
| Spring Boot | 0.5 OCPU | 512 MB |
|
||||||
|
| FastAPI (GUARDiA) | 0.5 OCPU | 300 MB |
|
||||||
|
| PostgreSQL | 0.3 OCPU | 256 MB |
|
||||||
|
| Ollama + LLaMA-3 8B | 1.0 OCPU | 6 GB |
|
||||||
|
| **합계** | **2.4 OCPU** | **~7.1 GB** |
|
||||||
|
| **여유** | **1.6 OCPU** | **~16.9 GB** ✅ |
|
||||||
258
deploy/10_gitea_smtp_setup.sh
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Gitea (Git 서버) + SMTP (Postfix) 설치 스크립트
|
||||||
|
# Oracle Cloud Ubuntu 22.04 ARM (Ampere A1)
|
||||||
|
# 실행: bash 10_gitea_smtp_setup.sh [도메인]
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
DOMAIN=${1:-"$(curl -s ifconfig.me)"}
|
||||||
|
GITEA_DOMAIN="git.${DOMAIN}"
|
||||||
|
GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
section() { echo -e "\n${CYAN}════════════════════════════════${NC}"; echo -e "${CYAN} $1${NC}"; echo -e "${CYAN}════════════════════════════════${NC}"; }
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
# PART 1: Gitea 설치
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
section "1. Gitea 사용자 생성"
|
||||||
|
sudo adduser --system --shell /bin/bash --gecos 'Git Version Control' \
|
||||||
|
--group --disabled-password --home /home/git git 2>/dev/null || true
|
||||||
|
info "git 사용자 준비 완료"
|
||||||
|
|
||||||
|
section "2. Gitea 바이너리 다운로드 (ARM64)"
|
||||||
|
GITEA_VER="1.22.3"
|
||||||
|
sudo mkdir -p /opt/gitea/bin
|
||||||
|
sudo wget -q "https://dl.gitea.com/gitea/${GITEA_VER}/gitea-${GITEA_VER}-linux-arm64" \
|
||||||
|
-O /opt/gitea/bin/gitea
|
||||||
|
sudo chmod +x /opt/gitea/bin/gitea
|
||||||
|
sudo ln -sf /opt/gitea/bin/gitea /usr/local/bin/gitea
|
||||||
|
gitea --version
|
||||||
|
info "Gitea ${GITEA_VER} 다운로드 완료"
|
||||||
|
|
||||||
|
section "3. Gitea 디렉터리 구조"
|
||||||
|
sudo mkdir -p /var/lib/gitea/{custom,data,log}
|
||||||
|
sudo mkdir -p /etc/gitea
|
||||||
|
sudo chown -R git:git /var/lib/gitea /etc/gitea
|
||||||
|
sudo chmod -R 750 /var/lib/gitea /etc/gitea
|
||||||
|
info "Gitea 디렉터리 생성 완료"
|
||||||
|
|
||||||
|
section "4. Gitea PostgreSQL DB 생성"
|
||||||
|
sudo -u postgres psql <<PSQL 2>/dev/null || true
|
||||||
|
CREATE USER gitea WITH PASSWORD 'G1tea_2026!';
|
||||||
|
CREATE DATABASE gitea_db OWNER gitea;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE gitea_db TO gitea;
|
||||||
|
PSQL
|
||||||
|
info "Gitea DB 생성 완료"
|
||||||
|
|
||||||
|
section "5. Gitea 설정 파일 생성"
|
||||||
|
sudo tee /etc/gitea/app.ini > /dev/null <<INI
|
||||||
|
[DEFAULT]
|
||||||
|
RUN_USER = git
|
||||||
|
RUN_MODE = prod
|
||||||
|
|
||||||
|
[database]
|
||||||
|
DB_TYPE = postgres
|
||||||
|
HOST = 127.0.0.1:5432
|
||||||
|
NAME = gitea_db
|
||||||
|
USER = gitea
|
||||||
|
PASSWD = G1tea_2026!
|
||||||
|
SCHEMA =
|
||||||
|
SSL_MODE = disable
|
||||||
|
|
||||||
|
[repository]
|
||||||
|
ROOT = /var/lib/gitea/data/repositories
|
||||||
|
|
||||||
|
[server]
|
||||||
|
SSH_DOMAIN = ${GITEA_DOMAIN}
|
||||||
|
DOMAIN = ${GITEA_DOMAIN}
|
||||||
|
HTTP_PORT = 3000
|
||||||
|
ROOT_URL = http://${GITEA_DOMAIN}/
|
||||||
|
DISABLE_SSH = false
|
||||||
|
SSH_PORT = 22
|
||||||
|
LFS_START_SERVER = true
|
||||||
|
OFFLINE_MODE = false
|
||||||
|
|
||||||
|
[mailer]
|
||||||
|
ENABLED = true
|
||||||
|
FROM = noreply@${DOMAIN}
|
||||||
|
PROTOCOL = smtp
|
||||||
|
SMTP_ADDR = localhost
|
||||||
|
SMTP_PORT = 25
|
||||||
|
IS_TLS_ENABLED = false
|
||||||
|
|
||||||
|
[service]
|
||||||
|
REGISTER_EMAIL_CONFIRM = false
|
||||||
|
ENABLE_NOTIFY_MAIL = true
|
||||||
|
DISABLE_REGISTRATION = false
|
||||||
|
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||||
|
ENABLE_CAPTCHA = false
|
||||||
|
DEFAULT_KEEP_EMAIL_PRIVATE = true
|
||||||
|
|
||||||
|
[picture]
|
||||||
|
DISABLE_GRAVATAR = true
|
||||||
|
|
||||||
|
[log]
|
||||||
|
MODE = file
|
||||||
|
LEVEL = info
|
||||||
|
ROOT_PATH = /var/lib/gitea/log
|
||||||
|
|
||||||
|
[security]
|
||||||
|
INSTALL_LOCK = false
|
||||||
|
SECRET_KEY = $(openssl rand -hex 32)
|
||||||
|
INTERNAL_TOKEN = $(openssl rand -hex 32)
|
||||||
|
INI
|
||||||
|
|
||||||
|
sudo chown git:git /etc/gitea/app.ini
|
||||||
|
sudo chmod 640 /etc/gitea/app.ini
|
||||||
|
info "Gitea 설정 파일 생성 완료"
|
||||||
|
|
||||||
|
section "6. Gitea systemd 서비스 등록"
|
||||||
|
sudo tee /etc/systemd/system/gitea.service > /dev/null <<SERVICE
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea (Git with a cup of tea)
|
||||||
|
After=network.target postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
RestartSec=2s
|
||||||
|
Type=simple
|
||||||
|
User=git
|
||||||
|
Group=git
|
||||||
|
WorkingDirectory=/var/lib/gitea/
|
||||||
|
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
|
||||||
|
Restart=always
|
||||||
|
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
SERVICE
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable gitea
|
||||||
|
sudo systemctl start gitea
|
||||||
|
sleep 5
|
||||||
|
sudo systemctl is-active gitea && info "Gitea 서비스 시작 완료" || echo "Gitea 시작 대기 중..."
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
# PART 2: Postfix SMTP 설치
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
section "7. Postfix SMTP 서버 설치"
|
||||||
|
# 비대화형 설치
|
||||||
|
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||||
|
postfix mailutils libsasl2-2 libsasl2-modules
|
||||||
|
|
||||||
|
sudo tee /etc/postfix/main.cf > /dev/null <<POSTFIX
|
||||||
|
# Postfix 기본 설정
|
||||||
|
smtpd_banner = \$myhostname ESMTP
|
||||||
|
biff = no
|
||||||
|
append_dot_mydomain = no
|
||||||
|
|
||||||
|
# 호스트명
|
||||||
|
myhostname = ${DOMAIN}
|
||||||
|
myorigin = /etc/mailname
|
||||||
|
mydestination = \$myhostname, localhost.\$mydomain, localhost
|
||||||
|
|
||||||
|
# 네트워크
|
||||||
|
inet_interfaces = loopback-only
|
||||||
|
inet_protocols = ipv4
|
||||||
|
mynetworks = 127.0.0.0/8
|
||||||
|
|
||||||
|
# 릴레이 (외부 메일 발송용 — Gmail SMTP 릴레이)
|
||||||
|
relayhost = [smtp.gmail.com]:587
|
||||||
|
smtp_sasl_auth_enable = yes
|
||||||
|
smtp_sasl_security_options = noanonymous
|
||||||
|
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
|
||||||
|
smtp_use_tls = yes
|
||||||
|
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
|
# 메일함
|
||||||
|
mailbox_size_limit = 0
|
||||||
|
recipient_delimiter = +
|
||||||
|
|
||||||
|
# 보안
|
||||||
|
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
|
||||||
|
POSTFIX
|
||||||
|
|
||||||
|
# Gmail 릴레이 자격증명 (나중에 입력)
|
||||||
|
sudo tee /etc/postfix/sasl_passwd > /dev/null <<SASL
|
||||||
|
[smtp.gmail.com]:587 your-gmail@gmail.com:your-app-password
|
||||||
|
SASL
|
||||||
|
sudo chmod 600 /etc/postfix/sasl_passwd
|
||||||
|
sudo postmap /etc/postfix/sasl_passwd
|
||||||
|
|
||||||
|
echo "${DOMAIN}" | sudo tee /etc/mailname
|
||||||
|
|
||||||
|
sudo systemctl enable postfix
|
||||||
|
sudo systemctl restart postfix
|
||||||
|
info "Postfix SMTP 설치 완료"
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
# PART 3: Nginx 설정 (Gitea)
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
section "8. Gitea Nginx 역방향 프록시 설정"
|
||||||
|
sudo tee /etc/nginx/sites-available/gitea > /dev/null <<NGINX
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${GITEA_DOMAIN};
|
||||||
|
|
||||||
|
client_max_body_size 512M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection keep-alive;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
proxy_cache_bypass \$http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/gitea
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
info "Gitea Nginx 설정 완료"
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
# 방화벽 업데이트
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
section "9. 방화벽 규칙 업데이트"
|
||||||
|
sudo ufw allow 25/tcp comment 'SMTP'
|
||||||
|
sudo ufw allow 587/tcp comment 'SMTP TLS'
|
||||||
|
# Gitea는 Nginx 뒤에서 서빙 — 3000 포트 외부 차단
|
||||||
|
sudo ufw deny 3000/tcp
|
||||||
|
|
||||||
|
# Oracle Cloud 내부 iptables
|
||||||
|
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 25 -j ACCEPT
|
||||||
|
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 587 -j ACCEPT
|
||||||
|
sudo netfilter-persistent save
|
||||||
|
info "방화벽 업데이트 완료"
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
# 완료 요약
|
||||||
|
# ────────────────────────────────────────────────
|
||||||
|
section "✅ Gitea + SMTP 설치 완료!"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}서비스 접속 주소:${NC}"
|
||||||
|
echo " Gitea: http://${GITEA_DOMAIN}"
|
||||||
|
echo " (IP직접): http://$(curl -s ifconfig.me):3000"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}⚠️ Gitea 초기 설정 (브라우저에서):${NC}"
|
||||||
|
echo " 1. http://${GITEA_DOMAIN} 접속"
|
||||||
|
echo " 2. 관리자 계정 생성"
|
||||||
|
echo " 3. GUARDiA 저장소 생성: guardia/guardia-itsm"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}⚠️ Gmail 앱 비밀번호 설정:${NC}"
|
||||||
|
echo " 1. Google 계정 → 보안 → 2단계 인증 활성화"
|
||||||
|
echo " 2. 앱 비밀번호 생성 → Postfix용"
|
||||||
|
echo " 3. sudo nano /etc/postfix/sasl_passwd"
|
||||||
|
echo " [smtp.gmail.com]:587 your@gmail.com:앱비밀번호"
|
||||||
|
echo " 4. sudo postmap /etc/postfix/sasl_passwd"
|
||||||
|
echo " 5. sudo systemctl restart postfix"
|
||||||
79
deploy/deploy_now.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""직접 배포 스크립트 - Spring Boot JAR + React 정적 파일"""
|
||||||
|
import paramiko, time, sys, os, io, zipfile
|
||||||
|
|
||||||
|
HOST = '101.79.17.164'
|
||||||
|
USER = 'root'
|
||||||
|
PASS = '1q2w3e!Q'
|
||||||
|
LOCAL_JAR = 'C:/GUARDiA/workspace/zioinfo-web/backend/target/zioinfo-web-1.0.0.jar'
|
||||||
|
LOCAL_STATIC = 'C:/GUARDiA/workspace/zioinfo-web/backend/src/main/resources/static'
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(HOST, username=USER, password=PASS, timeout=15)
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=90):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
chan = client.get_transport().open_session()
|
||||||
|
chan.set_combine_stderr(True)
|
||||||
|
chan.exec_command(cmd)
|
||||||
|
start = time.time()
|
||||||
|
while not chan.exit_status_ready():
|
||||||
|
if chan.recv_ready():
|
||||||
|
sys.stdout.buffer.write(chan.recv(4096))
|
||||||
|
sys.stdout.flush()
|
||||||
|
if time.time() - start > timeout:
|
||||||
|
print('[TIMEOUT]')
|
||||||
|
break
|
||||||
|
time.sleep(0.2)
|
||||||
|
while chan.recv_ready():
|
||||||
|
sys.stdout.buffer.write(chan.recv(4096))
|
||||||
|
sys.stdout.flush()
|
||||||
|
rc = chan.recv_exit_status()
|
||||||
|
print(f'exit={rc}')
|
||||||
|
return rc
|
||||||
|
|
||||||
|
# 1. 디렉터리 준비
|
||||||
|
run('디렉터리 준비',
|
||||||
|
'mkdir -p /opt/zioinfo/app /var/www/zioinfo /var/log/zioinfo && '
|
||||||
|
'chown -R jenkins:jenkins /opt/zioinfo /var/www/zioinfo /var/log/zioinfo && echo ok')
|
||||||
|
|
||||||
|
# 2. JAR 업로드
|
||||||
|
print('\n[JAR 업로드 중...]')
|
||||||
|
sftp.put(LOCAL_JAR, '/opt/zioinfo/app/app.jar')
|
||||||
|
size_mb = os.path.getsize(LOCAL_JAR) // 1024 // 1024
|
||||||
|
print(f'JAR 업로드 완료: {size_mb}MB')
|
||||||
|
|
||||||
|
# 3. 정적 파일 zip 후 업로드
|
||||||
|
print('\n[정적 파일 패키징 중...]')
|
||||||
|
zip_buf = io.BytesIO()
|
||||||
|
count = 0
|
||||||
|
with zipfile.ZipFile(zip_buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for root, dirs, files in os.walk(LOCAL_STATIC):
|
||||||
|
rel = os.path.relpath(root, LOCAL_STATIC).replace('\\', '/')
|
||||||
|
for fname in files:
|
||||||
|
arc = fname if rel == '.' else f'{rel}/{fname}'
|
||||||
|
zf.write(os.path.join(root, fname), arc)
|
||||||
|
count += 1
|
||||||
|
zip_buf.seek(0)
|
||||||
|
with sftp.open('/tmp/static.zip', 'wb') as f:
|
||||||
|
f.write(zip_buf.read())
|
||||||
|
print(f'{count}개 정적 파일 업로드')
|
||||||
|
|
||||||
|
# 4. 정적 파일 배포
|
||||||
|
run('정적 파일 배포',
|
||||||
|
'cd /var/www/zioinfo && unzip -q -o /tmp/static.zip && echo deployed && ls | head -5')
|
||||||
|
|
||||||
|
# 5. Spring Boot 서비스 시작
|
||||||
|
run('Spring Boot 기동',
|
||||||
|
'systemctl restart zioinfo && sleep 8 && systemctl is-active zioinfo && '
|
||||||
|
'journalctl -u zioinfo -n 8 --no-pager')
|
||||||
|
|
||||||
|
# 6. API 헬스체크
|
||||||
|
run('API 헬스체크',
|
||||||
|
'curl -s -o /dev/null -w "HTTP %{http_code}" http://localhost:8080/api/company && echo " OK"')
|
||||||
|
|
||||||
|
sftp.close()
|
||||||
|
client.close()
|
||||||
|
print('\n배포 완료!')
|
||||||
81
deploy/fix_https_8443.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Nginx 8443 HTTPS 설정 수정"""
|
||||||
|
import paramiko, time, sys
|
||||||
|
|
||||||
|
HOST = '101.79.17.164'; USER = 'root'; PASS = '1q2w3e!Q'
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(HOST, username=USER, password=PASS, timeout=15)
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=20):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
chan = client.get_transport().open_session()
|
||||||
|
chan.set_combine_stderr(True)
|
||||||
|
chan.exec_command(cmd)
|
||||||
|
start = time.time()
|
||||||
|
while not chan.exit_status_ready():
|
||||||
|
if chan.recv_ready(): sys.stdout.buffer.write(chan.recv(4096)); sys.stdout.flush()
|
||||||
|
if time.time() - start > timeout: break
|
||||||
|
time.sleep(0.2)
|
||||||
|
while chan.recv_ready(): sys.stdout.buffer.write(chan.recv(4096))
|
||||||
|
sys.stdout.flush()
|
||||||
|
chan.recv_exit_status()
|
||||||
|
|
||||||
|
guardia_https = r"""server {
|
||||||
|
listen 8443 ssl;
|
||||||
|
server_name _;
|
||||||
|
ssl_certificate /etc/ssl/guardia/server.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/guardia/server.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
client_max_body_size 100M;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Frame-Options DENY always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
}
|
||||||
|
location /api/ {
|
||||||
|
limit_req zone=guardia_api burst=10 nodelay;
|
||||||
|
proxy_pass http://127.0.0.1:8001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://127.0.0.1:8001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
with sftp.open('/etc/nginx/sites-available/guardia-https', 'w') as f:
|
||||||
|
f.write(guardia_https)
|
||||||
|
sftp.close()
|
||||||
|
|
||||||
|
run('Nginx 설정 검증', 'nginx -t')
|
||||||
|
run('Nginx 리로드', 'systemctl reload nginx && echo NGINX_OK')
|
||||||
|
time.sleep(2)
|
||||||
|
run('HTTPS 8443 테스트', 'curl -sk https://localhost:8443/api/external/health -w " HTTP %{http_code}"')
|
||||||
|
run('CORS 테스트 (HTTPS)',
|
||||||
|
'curl -sk -I -X OPTIONS https://localhost:8443/api/external/health '
|
||||||
|
'-H "Origin: https://portal.myorg.go.kr" | grep -i access-control')
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
print('\n완료')
|
||||||
56
deploy/fix_nginx.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import paramiko, time, sys
|
||||||
|
|
||||||
|
HOST = '101.79.17.164'; USER = 'root'; PASS = '1q2w3e!Q'
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(HOST, username=USER, password=PASS, timeout=15)
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
|
||||||
|
def run(cmd, timeout=20):
|
||||||
|
chan = client.get_transport().open_session()
|
||||||
|
chan.set_combine_stderr(True)
|
||||||
|
chan.exec_command(cmd)
|
||||||
|
start = time.time()
|
||||||
|
while not chan.exit_status_ready():
|
||||||
|
if chan.recv_ready(): sys.stdout.buffer.write(chan.recv(4096)); sys.stdout.flush()
|
||||||
|
if time.time() - start > timeout: break
|
||||||
|
time.sleep(0.2)
|
||||||
|
while chan.recv_ready(): sys.stdout.buffer.write(chan.recv(4096))
|
||||||
|
sys.stdout.flush()
|
||||||
|
chan.recv_exit_status()
|
||||||
|
|
||||||
|
nginx_conf = r"""server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
root /var/www/zioinfo;
|
||||||
|
index index.html;
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
add_header Cache-Control no-cache;
|
||||||
|
}
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8082;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
location ~* \.(js|css|png|jpg|gif|ico|svg|woff2|ttf)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
with sftp.open('/etc/nginx/sites-available/zioinfo', 'w') as f:
|
||||||
|
f.write(nginx_conf)
|
||||||
|
sftp.close()
|
||||||
|
|
||||||
|
run('nginx -t && systemctl reload nginx && echo NGINX_OK')
|
||||||
|
run('curl -s -o /dev/null -w "HTTP %{http_code}" http://localhost/api/company && echo " via Nginx OK"')
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
print('완료')
|
||||||
130
deploy/gitea_messenger.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""GUARDiA Messenger Gitea 저장소 생성 + Push"""
|
||||||
|
import paramiko, time, sys, os, io, zipfile
|
||||||
|
|
||||||
|
HOST = '101.79.17.164'
|
||||||
|
USER = 'root'
|
||||||
|
PASS = '1q2w3e!Q'
|
||||||
|
LOCAL_APP = 'C:/GUARDiA/app'
|
||||||
|
SEP = chr(92)
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(HOST, username=USER, password=PASS, timeout=15)
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=60):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
chan = client.get_transport().open_session()
|
||||||
|
chan.set_combine_stderr(True)
|
||||||
|
chan.exec_command(cmd)
|
||||||
|
start = time.time()
|
||||||
|
while not chan.exit_status_ready():
|
||||||
|
if chan.recv_ready(): sys.stdout.buffer.write(chan.recv(4096)); sys.stdout.flush()
|
||||||
|
if time.time()-start > timeout: print('[TIMEOUT]'); break
|
||||||
|
time.sleep(0.3)
|
||||||
|
while chan.recv_ready(): sys.stdout.buffer.write(chan.recv(4096))
|
||||||
|
sys.stdout.flush()
|
||||||
|
chan.recv_exit_status()
|
||||||
|
|
||||||
|
# --- 1. 저장소 생성 ---
|
||||||
|
create_py = """
|
||||||
|
import urllib.request, json, base64, urllib.error
|
||||||
|
G = 'http://localhost:3000'
|
||||||
|
h = {'Content-Type':'application/json','Authorization':'Basic '+base64.b64encode(b'zio:Zio@Admin2026!').decode()}
|
||||||
|
d = json.dumps({'name':'guardia-messenger','description':'GUARDiA Messenger Mobile App','private':False,'auto_init':False}).encode()
|
||||||
|
req = urllib.request.Request(G+'/api/v1/user/repos',data=d,method='POST',headers=h)
|
||||||
|
try:
|
||||||
|
r=urllib.request.urlopen(req)
|
||||||
|
print('OK:',json.loads(r.read()).get('full_name'))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
b=e.read(); print('HTTP',e.code,b.decode()[:100])
|
||||||
|
"""
|
||||||
|
with sftp.open('/tmp/cr.py','w') as f: f.write(create_py)
|
||||||
|
run('저장소 생성', 'python3 /tmp/cr.py')
|
||||||
|
|
||||||
|
# --- 2. 소스 패키징 ---
|
||||||
|
print('\n[소스 패키징]')
|
||||||
|
SKIP = {'node_modules','.git','android','ios','__pycache__','.expo'}
|
||||||
|
buf = io.BytesIO(); n = 0
|
||||||
|
with zipfile.ZipFile(buf,'w',zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for root,dirs,files in os.walk(LOCAL_APP):
|
||||||
|
dirs[:] = [d for d in dirs if d not in SKIP]
|
||||||
|
rel = os.path.relpath(root,LOCAL_APP).replace(SEP,'/')
|
||||||
|
for f in files:
|
||||||
|
if f.endswith(('.pyc','.log')): continue
|
||||||
|
arc = f if rel=='.' else f'{rel}/{f}'
|
||||||
|
zf.write(os.path.join(root,f),arc); n+=1
|
||||||
|
buf.seek(0)
|
||||||
|
with sftp.open('/tmp/msg.zip','wb') as f: f.write(buf.read())
|
||||||
|
print(f' {n}개 파일')
|
||||||
|
|
||||||
|
# --- 3. git push (서버 스크립트) ---
|
||||||
|
push_py = """
|
||||||
|
import subprocess, os
|
||||||
|
|
||||||
|
os.makedirs('/tmp/msg-push', exist_ok=True)
|
||||||
|
subprocess.run(['rm','-rf','/tmp/msg-push'], check=False)
|
||||||
|
os.makedirs('/tmp/msg-push')
|
||||||
|
subprocess.run(['unzip','-q','/tmp/msg.zip','-d','/tmp/msg-push'], check=True)
|
||||||
|
|
||||||
|
env = {'HOME':'/root','PATH':'/usr/bin:/bin'}
|
||||||
|
cwd = '/tmp/msg-push'
|
||||||
|
|
||||||
|
for cmd in [
|
||||||
|
['git','init','-q'],
|
||||||
|
['git','config','user.email','ci@zioinfo.co.kr'],
|
||||||
|
['git','config','user.name','ZioCI'],
|
||||||
|
['git','add','-A'],
|
||||||
|
['git','commit','-q','-m','feat: GUARDiA Messenger v1.0.0 initial commit'],
|
||||||
|
['git','remote','add','origin','http://zio:Zio%40Admin2026%21@localhost:3000/zio/guardia-messenger.git'],
|
||||||
|
['git','push','origin','main'],
|
||||||
|
]:
|
||||||
|
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
||||||
|
if r.returncode != 0 and cmd[1] not in ['config','remote']:
|
||||||
|
print('ERR',cmd[1],r.stderr[:100])
|
||||||
|
else:
|
||||||
|
print('OK',cmd[1])
|
||||||
|
"""
|
||||||
|
with sftp.open('/tmp/push.py','w') as f: f.write(push_py)
|
||||||
|
run('Git Push', 'python3 /tmp/push.py', 60)
|
||||||
|
|
||||||
|
# --- 4. Webhook ---
|
||||||
|
wh_py = """
|
||||||
|
import urllib.request, json, base64
|
||||||
|
G='http://localhost:3000'
|
||||||
|
h={'Content-Type':'application/json','Authorization':'Basic '+base64.b64encode(b'zio:Zio@Admin2026!').decode()}
|
||||||
|
d=json.dumps({'type':'gitea','active':True,'config':{'url':'http://localhost:9999/','content_type':'json','secret':'zioinfo-deploy-2026'},'events':['push']}).encode()
|
||||||
|
req=urllib.request.Request(G+'/api/v1/repos/zio/guardia-messenger/hooks',data=d,method='POST',headers=h)
|
||||||
|
try:
|
||||||
|
r=urllib.request.urlopen(req); print('Webhook OK, ID:',json.loads(r.read()).get('id'))
|
||||||
|
except Exception as e: print('Webhook 오류:',e)
|
||||||
|
"""
|
||||||
|
with sftp.open('/tmp/wh.py','w') as f: f.write(wh_py)
|
||||||
|
run('Webhook 등록', 'python3 /tmp/wh.py')
|
||||||
|
|
||||||
|
# --- 5. 최종 확인 ---
|
||||||
|
check_py = """
|
||||||
|
import urllib.request, json, base64
|
||||||
|
G='http://localhost:3000'
|
||||||
|
h={'Authorization':'Basic '+base64.b64encode(b'zio:Zio@Admin2026!').decode()}
|
||||||
|
r=urllib.request.Request(G+'/api/v1/repos/zio/guardia-messenger',headers=h)
|
||||||
|
try:
|
||||||
|
d=json.loads(urllib.request.urlopen(r).read())
|
||||||
|
print('저장소:', d.get('full_name'))
|
||||||
|
print('URL: ', d.get('html_url'))
|
||||||
|
print('브랜치:', d.get('default_branch'))
|
||||||
|
except Exception as e: print('오류:',e)
|
||||||
|
|
||||||
|
# 브랜치 목록
|
||||||
|
r2=urllib.request.Request(G+'/api/v1/repos/zio/guardia-messenger/branches',headers=h)
|
||||||
|
try:
|
||||||
|
branches=json.loads(urllib.request.urlopen(r2).read())
|
||||||
|
print('브랜치 목록:', [b['name'] for b in branches])
|
||||||
|
except: pass
|
||||||
|
"""
|
||||||
|
with sftp.open('/tmp/check.py','w') as f: f.write(check_py)
|
||||||
|
sftp.close()
|
||||||
|
run('최종 확인', 'python3 /tmp/check.py')
|
||||||
|
|
||||||
|
client.close()
|
||||||
74
deploy/guardia_mail.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import smtplib
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
TO = 'ythong86@gmail.com'
|
||||||
|
FROM = 'guardia@zioinfo.co.kr'
|
||||||
|
NOW = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
text = (
|
||||||
|
'GUARDiA ITSM 이메일 발송 테스트\n\n'
|
||||||
|
'발신 서버: mail.zioinfo.co.kr (101.79.17.164)\n'
|
||||||
|
'발신자: guardia@zioinfo.co.kr\n'
|
||||||
|
'발송 시각: ' + NOW + ' KST\n'
|
||||||
|
'인증: SPF PASS + DKIM 서명\n\n'
|
||||||
|
'zio 서버 Postfix + OpenDKIM이 정상 작동 중입니다.\n\n'
|
||||||
|
'(주)지오정보기술 | GUARDiA ITSM v2.0\n'
|
||||||
|
'http://101.79.17.164:8001\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
html = (
|
||||||
|
'<!DOCTYPE html><html lang="ko"><head><meta charset="UTF-8"></head>'
|
||||||
|
'<body style="font-family:sans-serif;background:#f0f2f5;padding:30px 20px;margin:0">'
|
||||||
|
'<div style="max-width:560px;margin:0 auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.1)">'
|
||||||
|
'<div style="background:#1a3a6b;padding:24px 32px">'
|
||||||
|
'<h1 style="color:#fff;margin:0;font-size:20px">🛡️ GUARDiA ITSM</h1>'
|
||||||
|
'<p style="color:#aac4e8;margin:4px 0 0;font-size:12px">(주)지오정보기술 AI 인프라 자율 운영 플랫폼</p>'
|
||||||
|
'</div>'
|
||||||
|
'<div style="padding:28px 32px">'
|
||||||
|
'<h2 style="color:#1a3a6b;margin:0 0 14px">📧 이메일 발송 테스트</h2>'
|
||||||
|
'<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:14px 18px;margin-bottom:20px">'
|
||||||
|
'<p style="color:#166534;font-weight:700;margin:0 0 4px">✅ 발송 성공</p>'
|
||||||
|
'<p style="color:#475569;font-size:12px;margin:0">SPF + DKIM 인증을 통과하여 발송되었습니다.</p>'
|
||||||
|
'</div>'
|
||||||
|
'<table style="width:100%;border-collapse:collapse;font-size:13px">'
|
||||||
|
'<tr style="background:#f8fafc"><td style="padding:9px 14px;font-weight:600;color:#64748b;border:1px solid #e2e8f0;width:32%">발신 서버</td><td style="padding:9px 14px;border:1px solid #e2e8f0">mail.zioinfo.co.kr (101.79.17.164)</td></tr>'
|
||||||
|
'<tr><td style="padding:9px 14px;font-weight:600;color:#64748b;border:1px solid #e2e8f0">발신자</td><td style="padding:9px 14px;border:1px solid #e2e8f0">guardia@zioinfo.co.kr</td></tr>'
|
||||||
|
'<tr style="background:#f8fafc"><td style="padding:9px 14px;font-weight:600;color:#64748b;border:1px solid #e2e8f0">발송 시각</td>'
|
||||||
|
'<td style="padding:9px 14px;border:1px solid #e2e8f0">' + NOW + ' KST</td></tr>'
|
||||||
|
'<tr><td style="padding:9px 14px;font-weight:600;color:#64748b;border:1px solid #e2e8f0">SPF 인증</td><td style="padding:9px 14px;border:1px solid #e2e8f0"><span style="background:#dcfce7;color:#16a34a;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">✅ PASS</span></td></tr>'
|
||||||
|
'<tr style="background:#f8fafc"><td style="padding:9px 14px;font-weight:600;color:#64748b;border:1px solid #e2e8f0">DKIM 서명</td><td style="padding:9px 14px;border:1px solid #e2e8f0"><span style="background:#dcfce7;color:#16a34a;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">✅ 서명됨</span></td></tr>'
|
||||||
|
'</table>'
|
||||||
|
'<div style="margin-top:20px;padding:14px 18px;background:#eff2ff;border-radius:8px;border:1px solid #c7d2fe">'
|
||||||
|
'<p style="color:#1a3a6b;font-weight:700;margin:0 0 8px;font-size:13px">🤖 GUARDiA ITSM 알림 기능</p>'
|
||||||
|
'<ul style="color:#475569;font-size:12px;margin:0;padding-left:18px;line-height:2">'
|
||||||
|
'<li>SR 접수/완료 알림</li>'
|
||||||
|
'<li>인시던트 긴급 알림</li>'
|
||||||
|
'<li>SLA 위반 경고</li>'
|
||||||
|
'<li>배포 완료/실패 알림</li>'
|
||||||
|
'<li>라이선스 만료 알림</li>'
|
||||||
|
'</ul></div>'
|
||||||
|
'</div>'
|
||||||
|
'<div style="background:#f8fafc;padding:14px 32px;border-top:1px solid #e2e8f0;text-align:center;font-size:11px;color:#94a3b8">'
|
||||||
|
'GUARDiA ITSM v2.0 | (주)지오정보기술 | guardia@zioinfo.co.kr'
|
||||||
|
'</div></div></body></html>'
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = MIMEMultipart('alternative')
|
||||||
|
msg['Subject'] = '[GUARDiA] SMTP 이메일 발송 테스트 - ' + NOW
|
||||||
|
msg['From'] = 'GUARDiA ITSM <' + FROM + '>'
|
||||||
|
msg['To'] = TO
|
||||||
|
msg['X-Mailer'] = 'GUARDiA ITSM v2.0'
|
||||||
|
msg.attach(MIMEText(text, 'plain', 'utf-8'))
|
||||||
|
msg.attach(MIMEText(html, 'html', 'utf-8'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP('localhost', 25, timeout=15) as smtp:
|
||||||
|
smtp.ehlo('mail.zioinfo.co.kr')
|
||||||
|
smtp.sendmail(FROM, [TO], msg.as_string())
|
||||||
|
print('OK 발송 완료 - ythong86@gmail.com 수신함을 확인하세요!')
|
||||||
|
print('발신: ' + FROM + ' → ' + TO)
|
||||||
|
print('시각: ' + NOW + ' KST')
|
||||||
|
except Exception as ex:
|
||||||
|
print('FAIL:', ex)
|
||||||
125
deploy/guardia_mail_v2.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
GUARDiA 이메일 발송 v2 — 스팸 점수 최소화
|
||||||
|
- Reply-To 헤더 추가
|
||||||
|
- List-Unsubscribe 헤더 추가
|
||||||
|
- 적절한 Message-ID
|
||||||
|
- 텍스트 본문 충실히 작성
|
||||||
|
- HTML 과도한 스타일 제거
|
||||||
|
"""
|
||||||
|
import smtplib, socket
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.utils import formatdate, make_msgid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
TO = 'ythong86@gmail.com'
|
||||||
|
FROM = 'guardia@zioinfo.co.kr'
|
||||||
|
NOW = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
DOMAIN = 'zioinfo.co.kr'
|
||||||
|
|
||||||
|
text = (
|
||||||
|
'GUARDiA ITSM 이메일 발송 테스트\n\n'
|
||||||
|
'안녕하세요,\n\n'
|
||||||
|
'(주)지오정보기술 GUARDiA ITSM 시스템에서 발송한 테스트 메일입니다.\n\n'
|
||||||
|
'[발송 정보]\n'
|
||||||
|
'- 발신자: guardia@zioinfo.co.kr\n'
|
||||||
|
'- 발신 서버: mail.zioinfo.co.kr (101.79.17.164)\n'
|
||||||
|
'- 발송 시각: ' + NOW + ' KST\n'
|
||||||
|
'- 인증: SPF PASS + DKIM 서명\n\n'
|
||||||
|
'[GUARDiA ITSM 이메일 알림 기능]\n'
|
||||||
|
'- SR(서비스 요청) 접수 및 완료 알림\n'
|
||||||
|
'- 인시던트 발생 긴급 알림\n'
|
||||||
|
'- SLA 위반 경고 알림\n'
|
||||||
|
'- 배포 완료/실패 알림\n'
|
||||||
|
'- 라이선스 만료 알림\n\n'
|
||||||
|
'이 메일은 GUARDiA ITSM 시스템 테스트 목적으로 발송되었습니다.\n\n'
|
||||||
|
'--\n'
|
||||||
|
'(주)지오정보기술\n'
|
||||||
|
'GUARDiA ITSM v2.0\n'
|
||||||
|
'guardia@zioinfo.co.kr\n'
|
||||||
|
'http://101.79.17.164:8001\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
html = (
|
||||||
|
'<!DOCTYPE html>\n'
|
||||||
|
'<html lang="ko">\n'
|
||||||
|
'<head>\n'
|
||||||
|
' <meta charset="UTF-8">\n'
|
||||||
|
' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
|
||||||
|
' <title>GUARDiA ITSM</title>\n'
|
||||||
|
'</head>\n'
|
||||||
|
'<body style="margin:0;padding:20px;font-family:Arial,sans-serif;background:#f5f5f5;color:#333">\n'
|
||||||
|
'<table width="100%" cellpadding="0" cellspacing="0" border="0">\n'
|
||||||
|
'<tr><td align="center">\n'
|
||||||
|
'<table width="560" cellpadding="0" cellspacing="0" border="0" style="background:#ffffff;border-radius:6px;overflow:hidden">\n'
|
||||||
|
|
||||||
|
'<!-- 헤더 -->\n'
|
||||||
|
'<tr><td style="background:#1a3a6b;padding:20px 30px">\n'
|
||||||
|
' <h1 style="color:#ffffff;margin:0;font-size:18px;font-weight:bold">GUARDiA ITSM</h1>\n'
|
||||||
|
' <p style="color:#aac4e8;margin:4px 0 0;font-size:12px">(주)지오정보기술 AI 인프라 자율 운영 플랫폼</p>\n'
|
||||||
|
'</td></tr>\n'
|
||||||
|
|
||||||
|
'<!-- 본문 -->\n'
|
||||||
|
'<tr><td style="padding:24px 30px">\n'
|
||||||
|
' <h2 style="color:#1a3a6b;font-size:16px;margin:0 0 12px">이메일 발송 테스트</h2>\n'
|
||||||
|
' <p style="margin:0 0 16px;line-height:1.6">안녕하세요,<br>\n'
|
||||||
|
' (주)지오정보기술 GUARDiA ITSM 시스템 테스트 메일입니다.</p>\n'
|
||||||
|
|
||||||
|
' <table width="100%" cellpadding="8" cellspacing="0" border="0" style="border-collapse:collapse;font-size:13px;margin-bottom:16px">\n'
|
||||||
|
' <tr style="background:#f8f8f8">\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;font-weight:bold;color:#555;width:32%">발신자</td>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#333">guardia@zioinfo.co.kr</td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
' <tr>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;font-weight:bold;color:#555">발송 시각</td>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#333">' + NOW + ' KST</td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
' <tr style="background:#f8f8f8">\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;font-weight:bold;color:#555">SPF 인증</td>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#2e7d32">PASS</td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
' <tr>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;font-weight:bold;color:#555">DKIM 서명</td>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#2e7d32">서명됨</td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
' </table>\n'
|
||||||
|
|
||||||
|
' <p style="margin:0;font-size:13px;color:#666;line-height:1.6">'
|
||||||
|
'GUARDiA ITSM은 SR 접수, 인시던트 알림, SLA 경고, 배포 알림 등을 이메일로 전송합니다.</p>\n'
|
||||||
|
'</td></tr>\n'
|
||||||
|
|
||||||
|
'<!-- 푸터 -->\n'
|
||||||
|
'<tr><td style="background:#f8f8f8;padding:14px 30px;border-top:1px solid #e0e0e0;text-align:center">\n'
|
||||||
|
' <p style="margin:0;font-size:11px;color:#999">\n'
|
||||||
|
' (주)지오정보기술 | guardia@zioinfo.co.kr | http://101.79.17.164:8001\n'
|
||||||
|
' </p>\n'
|
||||||
|
'</td></tr>\n'
|
||||||
|
'</table>\n'
|
||||||
|
'</td></tr>\n'
|
||||||
|
'</table>\n'
|
||||||
|
'</body>\n'
|
||||||
|
'</html>\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = MIMEMultipart('alternative')
|
||||||
|
msg['Subject'] = '[GUARDiA] 이메일 발송 테스트 - ' + NOW
|
||||||
|
msg['From'] = '(주)지오정보기술 GUARDiA <' + FROM + '>'
|
||||||
|
msg['To'] = TO
|
||||||
|
msg['Reply-To'] = FROM
|
||||||
|
msg['Date'] = formatdate(localtime=True)
|
||||||
|
msg['Message-ID'] = make_msgid(domain=DOMAIN)
|
||||||
|
msg['List-Unsubscribe'] = '<mailto:' + FROM + '?subject=unsubscribe>'
|
||||||
|
msg['X-Mailer'] = 'GUARDiA ITSM v2.0'
|
||||||
|
|
||||||
|
msg.attach(MIMEText(text, 'plain', 'utf-8'))
|
||||||
|
msg.attach(MIMEText(html, 'html', 'utf-8'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP('localhost', 25, timeout=15) as smtp:
|
||||||
|
smtp.ehlo('mail.zioinfo.co.kr')
|
||||||
|
smtp.sendmail(FROM, [TO], msg.as_string())
|
||||||
|
print('OK 발송 완료')
|
||||||
|
print('수신: ' + TO)
|
||||||
|
print('시각: ' + NOW + ' KST')
|
||||||
|
except Exception as ex:
|
||||||
|
print('FAIL:', ex)
|
||||||
107
deploy/guardia_mail_v3.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
GUARDiA 이메일 발송 v3 — 피싱 경고 제거
|
||||||
|
핵심 수정: IP 직접 링크 제거, 도메인 사용, 간결한 본문
|
||||||
|
"""
|
||||||
|
import smtplib
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.utils import formatdate, make_msgid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
TO = 'ythong86@gmail.com'
|
||||||
|
FROM = 'guardia@zioinfo.co.kr'
|
||||||
|
DOMAIN = 'zioinfo.co.kr'
|
||||||
|
NOW = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# 텍스트 본문 — IP 링크 없음, 간결하게
|
||||||
|
text = (
|
||||||
|
'안녕하세요,\n\n'
|
||||||
|
'(주)지오정보기술 GUARDiA ITSM 메일 서버 테스트입니다.\n\n'
|
||||||
|
'이 메일은 zio 서버의 Postfix + OpenDKIM 정상 동작을 확인하기 위해 발송되었습니다.\n\n'
|
||||||
|
'발신자: guardia@' + DOMAIN + '\n'
|
||||||
|
'발송 시각: ' + NOW + ' KST\n\n'
|
||||||
|
'-- \n'
|
||||||
|
'(주)지오정보기술\n'
|
||||||
|
'guardia@' + DOMAIN + '\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# HTML 본문 — IP 링크 완전 제거, 심플하게
|
||||||
|
html = (
|
||||||
|
'<!DOCTYPE html>\n'
|
||||||
|
'<html lang="ko">\n'
|
||||||
|
'<head><meta charset="UTF-8"><title>GUARDiA ITSM</title></head>\n'
|
||||||
|
'<body style="margin:0;padding:20px;font-family:Arial,sans-serif;'
|
||||||
|
'background:#f5f5f5;color:#333333">\n'
|
||||||
|
|
||||||
|
'<table width="560" cellpadding="0" cellspacing="0" border="0" '
|
||||||
|
'align="center" style="background:#ffffff;border-radius:6px">\n'
|
||||||
|
|
||||||
|
' <tr>\n'
|
||||||
|
' <td style="background:#1a3a6b;padding:20px 28px;border-radius:6px 6px 0 0">\n'
|
||||||
|
' <p style="color:#ffffff;margin:0;font-size:18px;font-weight:bold">'
|
||||||
|
'GUARDiA ITSM</p>\n'
|
||||||
|
' <p style="color:#aac4e8;margin:4px 0 0;font-size:12px">'
|
||||||
|
'(주)지오정보기술</p>\n'
|
||||||
|
' </td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
|
||||||
|
' <tr>\n'
|
||||||
|
' <td style="padding:24px 28px">\n'
|
||||||
|
' <p style="margin:0 0 12px;font-size:15px;color:#1a3a6b;font-weight:bold">'
|
||||||
|
'메일 서버 테스트</p>\n'
|
||||||
|
' <p style="margin:0 0 16px;font-size:13px;line-height:1.7;color:#444">\n'
|
||||||
|
' zio 서버의 메일 서비스가 정상 동작하고 있습니다.<br>\n'
|
||||||
|
' 본 메일은 시스템 점검 목적으로 발송되었습니다.\n'
|
||||||
|
' </p>\n'
|
||||||
|
' <table cellpadding="7" cellspacing="0" border="0" '
|
||||||
|
'style="border-collapse:collapse;font-size:13px;width:100%">\n'
|
||||||
|
' <tr style="background:#f8f8f8">\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#666;width:30%">발신자</td>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#333">'
|
||||||
|
'guardia@' + DOMAIN + '</td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
' <tr>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#666">발송 시각</td>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#333">' + NOW + ' KST</td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
' <tr style="background:#f8f8f8">\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#666">SPF / DKIM</td>\n'
|
||||||
|
' <td style="border:1px solid #e0e0e0;color:#2e7d32">인증 완료</td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
' </table>\n'
|
||||||
|
' </td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
|
||||||
|
' <tr>\n'
|
||||||
|
' <td style="padding:12px 28px;border-top:1px solid #eeeeee;'
|
||||||
|
'text-align:center;font-size:11px;color:#999999;border-radius:0 0 6px 6px">\n'
|
||||||
|
' (주)지오정보기술 | guardia@' + DOMAIN + '\n'
|
||||||
|
' </td>\n'
|
||||||
|
' </tr>\n'
|
||||||
|
|
||||||
|
'</table>\n'
|
||||||
|
'</body>\n'
|
||||||
|
'</html>\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = MIMEMultipart('alternative')
|
||||||
|
msg['Subject'] = '[GUARDiA] 메일 서버 테스트 - ' + NOW
|
||||||
|
msg['From'] = '(주)지오정보기술 GUARDiA <' + FROM + '>'
|
||||||
|
msg['To'] = TO
|
||||||
|
msg['Reply-To'] = FROM
|
||||||
|
msg['Date'] = formatdate(localtime=True)
|
||||||
|
msg['Message-ID'] = make_msgid(domain=DOMAIN)
|
||||||
|
msg['List-Unsubscribe'] = '<mailto:' + FROM + '?subject=unsubscribe>'
|
||||||
|
|
||||||
|
msg.attach(MIMEText(text, 'plain', 'utf-8'))
|
||||||
|
msg.attach(MIMEText(html, 'html', 'utf-8'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP('localhost', 25, timeout=15) as smtp:
|
||||||
|
smtp.ehlo('mail.' + DOMAIN)
|
||||||
|
smtp.sendmail(FROM, [TO], msg.as_string())
|
||||||
|
print('OK 발송 완료 - ' + TO)
|
||||||
|
print('시각: ' + NOW + ' KST')
|
||||||
|
print('IP 링크 완전 제거 / SPF+DKIM 적용')
|
||||||
|
except Exception as ex:
|
||||||
|
print('FAIL:', ex)
|
||||||
110
deploy/install_ai.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""AI 플랫폼 설치 스크립트"""
|
||||||
|
import paramiko, time, sys
|
||||||
|
|
||||||
|
HOST='101.79.17.164'; USER='root'; PASS='1q2w3e!Q'
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(HOST, username=USER, password=PASS, timeout=15)
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
|
||||||
|
def run(label, cmd, timeout=300):
|
||||||
|
print(f'\n[{label}]')
|
||||||
|
chan = client.get_transport().open_session()
|
||||||
|
chan.set_combine_stderr(True)
|
||||||
|
chan.exec_command(cmd)
|
||||||
|
start = time.time()
|
||||||
|
while not chan.exit_status_ready():
|
||||||
|
if chan.recv_ready(): sys.stdout.buffer.write(chan.recv(4096)); sys.stdout.flush()
|
||||||
|
if time.time()-start > timeout: print('[TIMEOUT]'); break
|
||||||
|
time.sleep(0.5)
|
||||||
|
while chan.recv_ready(): sys.stdout.buffer.write(chan.recv(4096))
|
||||||
|
sys.stdout.flush()
|
||||||
|
rc = chan.recv_exit_status()
|
||||||
|
print(f'exit={rc}'); return rc
|
||||||
|
|
||||||
|
install_script = """#!/bin/bash
|
||||||
|
VENV=/opt/guardia/venv/bin/pip
|
||||||
|
$VENV install -q --upgrade langchain langchain-community langchain-ollama chromadb sentence-transformers
|
||||||
|
$VENV install -q langgraph langchain-chroma
|
||||||
|
echo DONE
|
||||||
|
"""
|
||||||
|
with sftp.open('/tmp/install_ai.sh', 'w') as f:
|
||||||
|
f.write(install_script)
|
||||||
|
sftp.close()
|
||||||
|
|
||||||
|
run('AI 패키지 설치', 'bash /tmp/install_ai.sh 2>&1 | tail -10', 300)
|
||||||
|
|
||||||
|
# 검증 스크립트
|
||||||
|
verify = """
|
||||||
|
import sys
|
||||||
|
results = []
|
||||||
|
for pkg in ['langchain', 'chromadb', 'langchain_community', 'langchain_ollama', 'langgraph']:
|
||||||
|
try:
|
||||||
|
m = __import__(pkg)
|
||||||
|
ver = getattr(m, '__version__', 'ok')
|
||||||
|
results.append('OK ' + pkg + '==' + str(ver))
|
||||||
|
except ImportError as e:
|
||||||
|
results.append('FAIL ' + pkg + ': ' + str(e))
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
print(r)
|
||||||
|
ok_cnt = sum(1 for r in results if r.startswith('OK'))
|
||||||
|
print('\\n결과: ' + str(ok_cnt) + '/' + str(len(results)) + ' PASS')
|
||||||
|
"""
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
with sftp.open('/tmp/verify_ai.py', 'w') as f:
|
||||||
|
f.write(verify)
|
||||||
|
sftp.close()
|
||||||
|
|
||||||
|
run('설치 검증', '/opt/guardia/venv/bin/python3 /tmp/verify_ai.py')
|
||||||
|
|
||||||
|
# 임베딩 모델 다운로드 확인
|
||||||
|
run('nomic-embed-text 확인', 'ollama list | grep -i nomic || echo "not yet downloaded"')
|
||||||
|
|
||||||
|
# 코드베이스 임베딩 스크립트 생성
|
||||||
|
embed_script = '''#!/usr/bin/env python3
|
||||||
|
"""GUARDiA 코드베이스를 ChromaDB에 임베딩"""
|
||||||
|
import os, sys
|
||||||
|
os.environ["ANONYMIZED_TELEMETRY"] = "False"
|
||||||
|
|
||||||
|
from langchain_community.document_loaders import DirectoryLoader, TextLoader
|
||||||
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
|
from langchain_ollama import OllamaEmbeddings
|
||||||
|
from langchain_chroma import Chroma
|
||||||
|
|
||||||
|
APP_DIR = "/opt/guardia/app"
|
||||||
|
VDB_DIR = "/opt/guardia/vectordb"
|
||||||
|
|
||||||
|
print("문서 로딩...")
|
||||||
|
loader = DirectoryLoader(APP_DIR, glob="**/*.py",
|
||||||
|
loader_cls=TextLoader, loader_kwargs={"encoding":"utf-8", "autodetect_encoding":True})
|
||||||
|
docs = loader.load()
|
||||||
|
print(f"로드된 파일: {len(docs)}개")
|
||||||
|
|
||||||
|
splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
|
||||||
|
chunks = splitter.split_documents(docs)
|
||||||
|
print(f"청크 수: {len(chunks)}")
|
||||||
|
|
||||||
|
embeddings = OllamaEmbeddings(model="nomic-embed-text", base_url="http://localhost:11434")
|
||||||
|
|
||||||
|
print("임베딩 중 (시간 소요)...")
|
||||||
|
vectordb = Chroma.from_documents(chunks[:50], embeddings, # 처음 50개만 테스트
|
||||||
|
persist_directory=VDB_DIR, collection_name="guardia_codebase")
|
||||||
|
print(f"임베딩 완료. 저장 위치: {VDB_DIR}")
|
||||||
|
|
||||||
|
# 테스트 검색
|
||||||
|
results = vectordb.similarity_search("라이선스 키 등록", k=2)
|
||||||
|
print("\\n테스트 검색 결과:")
|
||||||
|
for r in results:
|
||||||
|
print(f" - {r.metadata.get('source','?')}: {r.page_content[:60]}...")
|
||||||
|
'''
|
||||||
|
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
with sftp.open('/opt/guardia/app/scripts/embed_codebase.py', 'w') as f:
|
||||||
|
f.write(embed_script)
|
||||||
|
run('embed_codebase.py 생성', 'ls /opt/guardia/app/scripts/')
|
||||||
|
sftp.close()
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
print('\nAI 플랫폼 설치 완료')
|
||||||