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>
This commit is contained in:
DESKTOP-TKLFCPRython 2026-05-31 10:09:17 +09:00
parent 6e02e7efe0
commit 1e98f0d04a
343 changed files with 354925 additions and 67 deletions

View File

@ -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};

File diff suppressed because one or more lines are too long

View File

@ -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};

View File

@ -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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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};

File diff suppressed because one or more lines are too long

View File

@ -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}

View File

@ -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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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}}

View File

@ -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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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}

View File

@ -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};

View File

@ -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};

View File

@ -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}}

File diff suppressed because one or more lines are too long

View File

@ -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}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@ -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
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View File

@ -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"
}
]

View File

@ -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

View File

@ -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

View File

@ -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

View 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
View 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
View 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
View 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 으로 접속하세요"

View 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)"

View 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
View 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"

View 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 실행)"

View 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** ✅ |

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'
' (주)지오정보기술 &nbsp;|&nbsp; 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
View 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 플랫폼 설치 완료')

Some files were not shown because too many files have changed in this diff Show More