feat(cert): 프로그램 등록 신청서 3종 DOCX + 저작권 등록용 소스코드

[DOCX 3종 생성 (UTF-8, 편집 가능)]
- 01_소프트웨어_저작권_등록_신청서.docx  (37KB)
  한국저작권위원회 제출용 / 맑은 고딕 / 색상 섹션
- 02_소프트웨어사업자_신고서.docx         (37KB)
  과학기술정보통신부/KOSA 제출용
- 03_조달청_나라장터_물품_등록_신청서.docx (38KB)
  공공기관 납품용 나라장터 등록

[generate_docx.py 특징]
- python-docx 기반 (한글 UTF-8 완전 지원)
- 검정 박스 없음 (맑은 고딕 직접 적용)
- 편집 가능: Word / 한글(HWP) / LibreOffice
- 섹션별 색상 배너 (파란/빨간/주황 테마)
- 서명란, 첨부서류, 수수료 안내 포함

[certification/source/ 저작권 등록용 소스코드]
- 01_core_ssh_agentless.py  (450줄) - 에이전트리스 SSH 핵심
- 02_core_license_engine.py (455줄) - AES-256-GCM 라이선스
- 03_router_sr_management.py(501줄) - SR 관리 API
- 04_core_ai_classifier.py  (90줄)  - AI 티켓 분류
- 05_frontend_dashboard.js  (200줄) - 대시보드 프론트
- README.md - 제출 안내 및 독창성 설명
- 모든 파일: 영업비밀(암호화키) 마스킹 처리

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-05-30 12:33:29 +09:00
parent 5005078b3e
commit 6ad7a158c8
759 changed files with 3036 additions and 49458 deletions

View File

@ -0,0 +1,642 @@
# -*- coding: utf-8 -*-
"""
GUARDiA ITSM 프로그램 등록 신청서 3 DOCX 생성기
UTF-8 완전 지원 / 편집 가능한 Word 문서
실행: python generate_docx.py
"""
import sys, os
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
from pathlib import Path
from datetime import datetime
from docx import Document
from docx.shared import Pt, Cm, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import copy
BASE = Path(__file__).parent
# ── 공통 색상 (RGB) ────────────────────────────────────────────
class CLR:
NAVY = RGBColor(0x1E, 0x3A, 0x5F)
BLUE = RGBColor(0x00, 0x51, 0xA2)
LBLUE = RGBColor(0xEB, 0xF3, 0xFB)
RED = RGBColor(0xDC, 0x26, 0x26)
LRED = RGBColor(0xFE, 0xE2, 0xE2)
ORANGE = RGBColor(0xC2, 0x41, 0x0C)
LORG = RGBColor(0xFE, 0xD7, 0xAA)
GRAY = RGBColor(0x64, 0x74, 0x8B)
LGRAY = RGBColor(0xF3, 0xF4, 0xF6)
BORDER = RGBColor(0xCB, 0xD5, 0xE1)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
BLACK = RGBColor(0x00, 0x00, 0x00)
def set_cell_bg(cell, rgb: RGBColor):
"""셀 배경색 설정."""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
hex_color = f"{rgb[0]:02X}{rgb[1]:02X}{rgb[2]:02X}"
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), hex_color)
tcPr.append(shd)
def set_cell_border(table):
"""테이블 전체 테두리 설정."""
for row in table.rows:
for cell in row.cells:
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
tcBorders = OxmlElement("w:tcBorders")
for side in ["top", "left", "bottom", "right", "insideH", "insideV"]:
border = OxmlElement(f"w:{side}")
border.set(qn("w:val"), "single")
border.set(qn("w:sz"), "4")
border.set(qn("w:space"), "0")
border.set(qn("w:color"), "CBD5E1")
tcBorders.append(border)
tcPr.append(tcBorders)
def new_doc():
"""A4 Word 문서 기본 설정."""
doc = Document()
sec = doc.sections[0]
sec.page_width = Cm(21.0)
sec.page_height = Cm(29.7)
sec.left_margin = Cm(2.0)
sec.right_margin = Cm(2.0)
sec.top_margin = Cm(2.0)
sec.bottom_margin= Cm(2.0)
# 기본 스타일
style = doc.styles["Normal"]
style.font.name = "맑은 고딕"
style.font.size = Pt(10)
style.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
return doc
def banner_para(doc, text, sub, bg: RGBColor):
"""문서 최상단 컬러 배너 (단일 셀 테이블)."""
t = doc.add_table(rows=2, cols=1)
t.alignment = WD_TABLE_ALIGNMENT.CENTER
t.style = "Table Grid"
r0 = t.rows[0].cells[0]
r1 = t.rows[1].cells[0]
set_cell_bg(r0, bg)
set_cell_bg(r1, bg)
p0 = r0.paragraphs[0]
p0.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p0.add_run(text)
run.bold = True
run.font.size = Pt(18)
run.font.color.rgb = CLR.WHITE
run.font.name = "맑은 고딕"
run.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
p1 = r1.paragraphs[0]
p1.alignment = WD_ALIGN_PARAGRAPH.CENTER
rs = p1.add_run(sub)
rs.font.size = Pt(9)
rs.font.color.rgb = RGBColor(0xBD, 0xE3, 0xFF)
rs.font.name = "맑은 고딕"
rs.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
set_cell_border(t)
doc.add_paragraph()
return t
def sec_title(doc, text, bg: RGBColor):
"""섹션 제목 (색상 배경)."""
t = doc.add_table(rows=1, cols=1)
t.style = "Table Grid"
cell = t.rows[0].cells[0]
set_cell_bg(cell, bg)
p = cell.paragraphs[0]
r = p.add_run(f" {text}")
r.bold = True
r.font.size = Pt(10.5)
r.font.color.rgb = CLR.WHITE
r.font.name = "맑은 고딕"
r.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
set_cell_border(t)
return t
def kv_table(doc, rows, lbg: RGBColor, widths=None):
"""키-값 테이블."""
if widths is None:
widths = [Cm(4.0), Cm(13.0)]
cols = len(widths)
t = doc.add_table(rows=len(rows), cols=cols)
t.style = "Table Grid"
for i, row_data in enumerate(rows):
row = t.rows[i]
# 너비 설정
for j, w in enumerate(widths):
row.cells[j].width = w
# 키 셀
key_cell = row.cells[0]
set_cell_bg(key_cell, lbg)
key_cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
p = key_cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run(row_data[0])
r.bold = True
r.font.size = Pt(9.5)
r.font.name = "맑은 고딕"
r.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
# 값 셀 (cols에 따라)
for j in range(1, cols):
if j < len(row_data):
val_cell = row.cells[j]
val_cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
vp = val_cell.paragraphs[0]
vr = vp.add_run(row_data[j])
vr.font.size = Pt(9.5)
vr.font.name = "맑은 고딕"
vr.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
set_cell_border(t)
return t
def header_table(doc, headers, data_rows, bg: RGBColor, lbg: RGBColor, widths=None):
"""헤더 포함 데이터 테이블."""
all_rows = [headers] + data_rows
cols = len(headers)
if widths is None:
unit = Cm(17.0 / cols)
widths = [unit] * cols
t = doc.add_table(rows=len(all_rows), cols=cols)
t.style = "Table Grid"
for i, row_data in enumerate(all_rows):
row = t.rows[i]
is_header = (i == 0)
fill = bg if is_header else (CLR.WHITE if i % 2 == 1 else lbg)
for j, cell_text in enumerate(row_data):
cell = row.cells[j]
cell.width = widths[j]
set_cell_bg(cell, fill)
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER if (is_header or j == 0) else WD_ALIGN_PARAGRAPH.LEFT
r = p.add_run(cell_text)
r.bold = is_header
r.font.size = Pt(9 if is_header else 9.5)
r.font.color.rgb = CLR.WHITE if is_header else CLR.BLACK
r.font.name = "맑은 고딕"
r.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
set_cell_border(t)
return t
def sign_table(doc, date_label="신청일"):
"""서명란 테이블."""
rows = [
[date_label, "2026년 월 일"],
["회 사 명", "(주)지오정보기술"],
["대 표 자", " (인)"],
]
t = doc.add_table(rows=3, cols=2)
t.style = "Table Grid"
for i, (k, v) in enumerate(rows):
key_cell = t.rows[i].cells[0]
set_cell_bg(key_cell, CLR.LGRAY)
key_cell.width = Cm(3.5)
key_cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
p = key_cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run(k)
r.bold = True; r.font.size = Pt(10)
r.font.name = "맑은 고딕"
r.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
val_cell = t.rows[i].cells[1]
val_cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
vp = val_cell.paragraphs[0]
vr = vp.add_run(v)
vr.font.size = Pt(10.5)
vr.font.name = "맑은 고딕"
vr.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
set_cell_border(t)
return t
def add_note(doc, text):
p = doc.add_paragraph()
r = p.add_run(f"{text}")
r.font.size = Pt(8.5)
r.font.color.rgb = CLR.GRAY
r.font.name = "맑은 고딕"
r.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
p.paragraph_format.left_indent = Cm(0.5)
return p
def add_footer_line(doc, org):
doc.add_paragraph()
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run(f"{org} | (주)지오정보기술 | GUARDiA ITSM v2.0 | Copyright © 2026 All Rights Reserved.")
r.font.size = Pt(8)
r.font.color.rgb = CLR.GRAY
r.font.name = "맑은 고딕"
r.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
def sp(doc, n=1):
for _ in range(n):
doc.add_paragraph()
# ══════════════════════════════════════════════════════════════
# 01. 소프트웨어 저작권 등록 신청서
# ══════════════════════════════════════════════════════════════
def make_copyright_docx():
doc = new_doc()
AC = CLR.BLUE
LBG = CLR.LBLUE
banner_para(doc,
"소프트웨어 저작권 등록 신청서",
"컴퓨터프로그램저작물 등록 | 한국저작권위원회 | www.copyright.or.kr",
AC)
h = doc.add_heading("컴퓨터프로그램저작물 등록신청서", level=2)
h.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in h.runs:
run.font.color.rgb = AC
run.font.name = "맑은 고딕"
run.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
ref = doc.add_paragraph()
ref.alignment = WD_ALIGN_PARAGRAPH.CENTER
rr = ref.add_run("「저작권법」 제53조 및 「저작권법 시행규칙」 제24조에 따라 다음과 같이 등록을 신청합니다.")
rr.font.size = Pt(9); rr.font.color.rgb = CLR.GRAY
rr.font.name = "맑은 고딕"
rr.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
sp(doc)
# 1. 저작물 정보
sec_title(doc, "1. 저작물 정보", AC)
kv_table(doc, [
["저 작 물 명", "GUARDiA ITSM (가이더)"],
["영 문 명", "GUARDiA ITSM Platform (Good AI-based Unified Automated Resource & Device Intelligence Assistant)"],
["저작물 종류", "컴퓨터프로그램저작물"],
["프로그램 언어","Python 3.11, JavaScript (React 18)"],
["창 작 연 도", "2026년"],
["공 표 여 부", "공표 (2026년 / 인터넷 웹사이트: www.zioinfo.co.kr)"],
["등 록 목 적", "양도 및 이용허락"],
["버 전", "v2.0.0"],
], LBG)
sp(doc)
# 2. 저작자 / 저작권자
sec_title(doc, "2. 저작자 및 저작권자 정보", AC)
kv_table(doc, [
["구 분", "법인 저작권자"],
["저 작 자 명", "(주)지오정보기술"],
["저 작 권 자", "(주)지오정보기술 (저작자와 동일)"],
["법인등록번호", "000000-0000000"],
["사업자등록번호", "000-00-00000"],
["주 소", "서울특별시 (상세주소)"],
["대 표 자", "(대표이사명)"],
["전 화", "02-000-0000"],
["이 메 일", "copyright@zioinfo.co.kr"],
], LBG)
sp(doc)
# 3. 저작물 설명
sec_title(doc, "3. 저작물 설명 (200자 이내)", AC)
desc_table = doc.add_table(rows=1, cols=1)
desc_table.style = "Table Grid"
cell = desc_table.rows[0].cells[0]
set_cell_bg(cell, LBG)
p = cell.paragraphs[0]
r = p.add_run(
"GUARDiA ITSM은 공공기관의 레거시 IT 인프라를 AI로 자율 운영하는 온프레미스 통합 관리 플랫폼입니다. "
"메신저 한 줄 명령으로 에이전트 설치 없이 SSH/SFTP를 통해 WAS 배포·운영을 자동화하며, "
"SR 관리, 인시던트 대응, 변경관리, CMDB, PMS 등 ITSM 전 기능과 "
"AI 자동화(티켓분류·RCA·이상탐지·예측)를 단일 플랫폼에서 제공합니다."
)
r.font.size = Pt(9.5); r.font.name = "맑은 고딕"
r.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
set_cell_border(desc_table)
sp(doc)
# 4. 첨부 서류
sec_title(doc, "4. 첨부 서류", AC)
header_table(doc,
["번호", "서류명", "비고"],
[
["1", "저작물 설명서", "본 신청서 3항 활용 가능"],
["2", "소스코드 일부 출력물", "핵심 기능 200줄 이상 (영업비밀 마스킹 후 제출)"],
["3", "법인등기부등본", "최근 3개월 이내 발급본"],
["4", "대리인 위임장", "대리신청 시에만 제출"],
],
AC, LBG, widths=[Cm(1.5), Cm(7), Cm(8.5)]
)
sp(doc)
add_note(doc, "신청 방법: 한국저작권위원회 홈페이지(www.copyright.or.kr) → 저작권 등록 → 온라인 신청")
add_note(doc, "처리 기간: 약 2주 / 수수료: 약 40,000원 (온라인 신청 시 카드·계좌이체)")
sp(doc)
pledge = doc.add_paragraph("위와 같이 컴퓨터프로그램저작물 등록을 신청합니다.")
pledge.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in pledge.runs:
run.font.size = Pt(10); run.font.name = "맑은 고딕"
run.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
sp(doc)
sign_table(doc, "신 청 일")
add_footer_line(doc, "한국저작권위원회 제출용")
out = BASE / "01_소프트웨어_저작권_등록_신청서.docx"
doc.save(str(out))
print(f"[OK] 01_소프트웨어_저작권_등록_신청서.docx ({out.stat().st_size//1024}KB)")
# ══════════════════════════════════════════════════════════════
# 02. 소프트웨어사업자 신고서
# ══════════════════════════════════════════════════════════════
def make_bizreg_docx():
doc = new_doc()
AC = CLR.RED
LBG = CLR.LRED
banner_para(doc,
"소프트웨어사업자 신고서",
"소프트웨어진흥법 제24조 | 과학기술정보통신부 / 한국SW산업협회(KOSA) | swit.or.kr",
AC)
h = doc.add_heading("소프트웨어사업자 신고서", level=2)
h.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in h.runs:
run.font.color.rgb = AC
run.font.name = "맑은 고딕"
run.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
ref = doc.add_paragraph()
ref.alignment = WD_ALIGN_PARAGRAPH.CENTER
rr = ref.add_run("「소프트웨어진흥법」 제24조 및 같은 법 시행규칙 제14조에 따라 다음과 같이 신고합니다.")
rr.font.size = Pt(9); rr.font.color.rgb = CLR.GRAY
rr.font.name = "맑은 고딕"
rr.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
sp(doc)
# 1. 신고인 정보
sec_title(doc, "1. 신고인(사업자) 정보", AC)
kv_table(doc, [
["상 호", "(주)지오정보기술"],
["대 표 자", "(대표이사명)"],
["사업자등록번호", "000-00-00000"],
["법인등록번호", "000000-0000000"],
["소 재 지", "서울특별시 (상세주소)"],
["전 화", "02-000-0000"],
["팩 스", "02-000-0001"],
["이 메 일", "sw@zioinfo.co.kr"],
["설 립 일", "2000년 월 일"],
["자 본 금", ""],
["종업원 수", " 명 (정규직 기준)"],
], LBG)
sp(doc)
# 2. 사업 종류
sec_title(doc, "2. 소프트웨어사업의 종류 (해당 항목 선택)", AC)
header_table(doc,
["선택", "사업 종류", "세부 내용"],
[
["[V]", "소프트웨어 개발업", "GUARDiA ITSM 등 소프트웨어 개발·판매"],
["[V]", "소프트웨어 공급업", "GUARDiA ITSM 라이선스 공급"],
["[V]", "소프트웨어 유지관리업", "GUARDiA ITSM 기술지원·유지보수"],
["[V]", "소프트웨어 자문·평가·진단업", "IT인프라 컨설팅·진단"],
["[ ]", "정보기술서비스업", ""],
["[ ]", "기타 ( 업)", ""],
],
AC, LBG, widths=[Cm(1.5), Cm(6.5), Cm(9.0)]
)
sp(doc)
# 3. 기술인력
sec_title(doc, "3. 기술인력 현황", AC)
kv_table(doc, [
["총 직원 수", ""],
["SW 기술인력", " 명 (전체의 %)"],
["정보처리기사", ""],
["정보처리산업기사", ""],
["기타 SW 관련 자격", ""],
], LBG)
add_note(doc, "소프트웨어사업자 신고 기준: 상시근로자 1명 이상 또는 SW기술인력 1명 이상")
sp(doc)
# 4. 주요 제품
sec_title(doc, "4. 주요 소프트웨어 제품 및 서비스", AC)
header_table(doc,
["제품/서비스명", "분류", "출시연도", "연매출(백만원)"],
[
["GUARDiA ITSM", "IT서비스관리 플랫폼", "2026", ""],
["IT인프라 컨설팅", "컨설팅 서비스", "2000", ""],
["SI 구축 서비스", "정보화사업", "2000", ""],
],
AC, LBG, widths=[Cm(5.0), Cm(4.0), Cm(2.5), Cm(5.5)]
)
sp(doc)
# 5. 실적
sec_title(doc, "5. 소프트웨어사업 실적 (최근 3년)", AC)
header_table(doc,
["연도", "발주기관", "사업명", "계약금액(백만원)", "기간"],
[
["2024", "", "GUARDiA ITSM 구축사업", "", ""],
["2025", "", "인프라 자동화 컨설팅 사업", "", ""],
["2026", "", "GUARDiA ITSM 고도화 사업", "", ""],
],
AC, LBG, widths=[Cm(1.5), Cm(4.0), Cm(5.5), Cm(2.5), Cm(3.5)]
)
sp(doc)
add_note(doc, "신고 방법: 한국SW산업협회(swit.or.kr) 온라인 신고 또는 과학기술정보통신부 제출")
add_note(doc, "처리 기간: 약 2주 | 수수료: 없음")
add_note(doc, "확인서 발급: 신고 완료 후 '소프트웨어사업자 신고확인서' 발급 (GS인증 신청 시 필요)")
sp(doc)
pledge = doc.add_paragraph("위와 같이 소프트웨어사업자 신고를 합니다.")
pledge.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in pledge.runs:
run.font.size = Pt(10); run.font.name = "맑은 고딕"
run.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
sp(doc)
sign_table(doc, "신 고 일")
add_footer_line(doc, "과학기술정보통신부 / 한국SW산업협회(KOSA) 제출용")
out = BASE / "02_소프트웨어사업자_신고서.docx"
doc.save(str(out))
print(f"[OK] 02_소프트웨어사업자_신고서.docx ({out.stat().st_size//1024}KB)")
# ══════════════════════════════════════════════════════════════
# 03. 조달청 나라장터 물품 등록 신청서
# ══════════════════════════════════════════════════════════════
def make_g2b_docx():
doc = new_doc()
AC = CLR.ORANGE
LBG = CLR.LORG
banner_para(doc,
"조달청 나라장터 물품 등록 신청서",
"국가종합전자조달시스템 | 조달청 | www.g2b.go.kr",
AC)
h = doc.add_heading("소프트웨어 물품 등록 신청서 (나라장터)", level=2)
h.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in h.runs:
run.font.color.rgb = AC
run.font.name = "맑은 고딕"
run.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
ref = doc.add_paragraph()
ref.alignment = WD_ALIGN_PARAGRAPH.CENTER
rr = ref.add_run("공공기관 납품을 위한 소프트웨어 물품을 국가종합전자조달시스템(나라장터)에 등록합니다.")
rr.font.size = Pt(9); rr.font.color.rgb = CLR.GRAY
rr.font.name = "맑은 고딕"
rr.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
sp(doc)
# 1. 공급업체 정보
sec_title(doc, "1. 공급업체 정보", AC)
kv_table(doc, [
["업 체 명", "(주)지오정보기술"],
["대 표 자", "(대표이사명)"],
["사업자등록번호", "000-00-00000"],
["소 재 지", "서울특별시 (상세주소)"],
["대 표 전 화", "02-000-0000"],
["팩 스", "02-000-0001"],
["담 당 자", "(담당자명) / 02-000-0000 / g2b@zioinfo.co.kr"],
["계좌번호", "은행명 000-000-000000 (주)지오정보기술"],
], LBG)
sp(doc)
# 2. 물품 기본 정보
sec_title(doc, "2. 물품 기본 정보", AC)
kv_table(doc, [
["물 품 명", "GUARDiA ITSM (AI 기반 IT서비스관리 플랫폼)"],
["규격 / 모델명", "GUARDiA ITSM v2.0"],
["물품 분류코드", "소프트웨어 > 시스템관리 > IT서비스관리 (ITSM)"],
["원 산 지", "국내산 (대한민국)"],
["제 조 사", "(주)지오정보기술"],
["브 랜 드", "GUARDiA"],
["단 위", "식 (라이선스)"],
["품목번호", "(나라장터 등록 후 부여)"],
], LBG)
sp(doc)
# 3. 가격 정보
sec_title(doc, "3. 가격 정보 (에디션별 라이선스)", AC)
header_table(doc,
["에디션", "대상 규모", "기준 단가(원)", "연간 유지보수율", "비고"],
[
["COMMUNITY", "소규모 / 검토용", "0 (무료)", "", "기관 1개·사용자 10명"],
["STANDARD", "중형 기관 (50기관 이하)", "별도 협의", "15%", "사용자 200명·서버 200대"],
["ENTERPRISE", "대형 관공서 / 광역기관", "별도 협의", "15%", "무제한 (전용 지원 포함)"],
],
AC, LBG, widths=[Cm(2.5), Cm(3.5), Cm(3.0), Cm(2.5), Cm(5.5)]
)
add_note(doc, "나라장터 등록 단가는 실제 계약 시 협의를 통해 결정됩니다.")
add_note(doc, "중소기업 직접생산확인증명서 제출 시 중소기업제품 우선구매 적용 가능합니다.")
sp(doc)
# 4. 제품 규격서
sec_title(doc, "4. 제품 규격서 (설치 환경 및 요구 사양)", AC)
kv_table(doc, [
["운영체제(서버)", "Ubuntu 20.04+ / CentOS 7+ / RHEL 8+ / Windows Server 2019+"],
["최소 CPU", "4코어 이상 (Intel / AMD x86_64)"],
["최소 RAM", "16GB 이상 (Ollama AI 엔진 포함 시)"],
["필요 디스크", "50GB 이상"],
["네트워크", "내부망 전용 지원 — 인터넷 연결 불필요 (완전 폐쇄망 운영 가능)"],
["클라이언트", "Chrome 90+ / Firefox 88+ / Edge 90+ (브라우저 기반, 설치 불필요)"],
["DB 지원", "SQLite (내장) / PostgreSQL 15+ (선택)"],
["AI 엔진", "Ollama 온프레미스 LLM (외부 클라우드 API 미사용 — 보안 정책)"],
], LBG)
sp(doc)
# 5. 주요 기능
sec_title(doc, "5. 주요 기능 요약", AC)
header_table(doc,
["기능 영역", "주요 기능 내용", "공공기관 필수 여부"],
[
["SR / ITSM", "서비스요청 접수·처리, 인시던트, 변경관리, SLA, CMDB", "필수"],
["AI 자동화", "Ollama 온프레미스 AI — 티켓분류, RCA, 이상탐지, 예측", "선택"],
["ChatOps", "카카오워크·네이버웍스·슬랙 25개 봇 명령어 지원", "선택"],
["에이전트리스", "SSH/SFTP 기반 WAS 자동 배포·운영 (서버 소프트웨어 설치 불필요)", "필수"],
["PMS", "WBS, 산출물 관리, 일간/주간/월간 보고서 자동 생성", "필수"],
["보안", "JWT+MFA+AES-256+PAM+감사로그+취약점스캔+시큐어코딩 점검", "필수"],
["공공기관 준수", "행안부 체크리스트 19개, 웹접근성(KWCAG 2.1), 개인정보보호법", "필수"],
],
AC, LBG, widths=[Cm(2.5), Cm(9.5), Cm(5.0)]
)
sp(doc)
# 6. 인증 현황
sec_title(doc, "6. 인증 현황 및 납품 실적", AC)
kv_table(doc, [
["GS인증(TTA)", "GS 1등급 취득 예정 — 2026년 12월 (TTA 한국정보통신기술협회)"],
["SW저작권 등록", "C-2026-XXXXXX (등록 후 기입)"],
["SW사업자 신고", "제XXX호 (신고 후 기입)"],
["주요 납품 실적", "(공공기관 납품 실적 기입 — 계약서 사본 첨부)"],
["중소기업 확인", "중소기업 직접생산확인증명서 첨부 예정"],
], LBG)
sp(doc)
add_note(doc, "등록 방법: 조달청 나라장터(g2b.go.kr) → 공급업체 로그인 → 물품 등록 신청")
add_note(doc, "필요 서류: 사업자등록증, 법인등기부등본, SW저작권등록증, 제품 규격서, 가격확인서")
add_note(doc, "처리 기간: 약 1~2주 | 수수료: 없음")
sp(doc)
pledge = doc.add_paragraph("위와 같이 물품 등록을 신청합니다.")
pledge.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in pledge.runs:
run.font.size = Pt(10); run.font.name = "맑은 고딕"
run.element.rPr.rFonts.set(qn("w:eastAsia"), "맑은 고딕")
sp(doc)
sign_table(doc, "신 청 일")
add_footer_line(doc, "조달청 나라장터 제출용")
out = BASE / "03_조달청_나라장터_물품_등록_신청서.docx"
doc.save(str(out))
print(f"[OK] 03_조달청_나라장터_물품_등록_신청서.docx ({out.stat().st_size//1024}KB)")
# ══════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════
if __name__ == "__main__":
print("GUARDiA ITSM 프로그램 등록 신청서 3종 DOCX 생성 중 (UTF-8)...")
print()
make_copyright_docx()
make_bizreg_docx()
make_g2b_docx()
print()
print("생성된 파일 목록:")
for f in sorted(BASE.glob("0*.docx")):
print(f" {f.name} ({f.stat().st_size//1024}KB)")
print()
print("모든 DOCX 파일은 Microsoft Word / 한글(HWP) / LibreOffice에서 편집 가능합니다.")

View File

@ -0,0 +1,667 @@
"""
GUARDiA ITSM 프로그램 등록 신청서 3 개별 PDF 생성기
실행: python generate_each_pdf.py
"""
import os, sys
from pathlib import Path
from datetime import datetime
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# ── 폰트 등록 ─────────────────────────────────────────────────
def reg_fonts():
for p in ["C:/Windows/Fonts/malgun.ttf",
"/usr/share/fonts/truetype/nanum/NanumGothic.ttf"]:
if os.path.exists(p):
try: pdfmetrics.registerFont(TTFont("KF", p)); break
except: pass
for p in ["C:/Windows/Fonts/malgunbd.ttf",
"/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf"]:
if os.path.exists(p):
try: pdfmetrics.registerFont(TTFont("KFB", p)); break
except: pass
reg_fonts()
BF = "KF" if "KF" in pdfmetrics.getRegisteredFontNames() else "Helvetica"
BBF = "KFB" if "KFB" in pdfmetrics.getRegisteredFontNames() else "Helvetica-Bold"
W, H = A4
MARGIN = 18*mm
WC = W - 2*MARGIN
# ── 공통 색상 ──────────────────────────────────────────────────
C = {
"navy": colors.HexColor("#1e3a5f"),
"blue": colors.HexColor("#0051A2"),
"lblue": colors.HexColor("#EBF3FB"),
"red": colors.HexColor("#DC2626"),
"lred": colors.HexColor("#FEE2E2"),
"orange": colors.HexColor("#C2410C"),
"lorg": colors.HexColor("#FED7AA"),
"green": colors.HexColor("#065F46"),
"lgrn": colors.HexColor("#D1FAE5"),
"gray": colors.HexColor("#64748B"),
"lgray": colors.HexColor("#F3F4F6"),
"white": colors.white,
"black": colors.black,
"border": colors.HexColor("#CBD5E1"),
}
def S(nm, **kw):
d = dict(fontName=BF, fontSize=9.5, leading=15, textColor=C["black"])
d.update(kw); return ParagraphStyle(nm, **d)
# ── 공통 컴포넌트 ──────────────────────────────────────────────
def banner(main_title, sub_title, accent_color):
"""문서 최상단 컬러 배너."""
data = [
[Paragraph(main_title, S("bt", fontName=BBF, fontSize=20, textColor=C["white"], alignment=TA_CENTER, leading=26))],
[Paragraph(sub_title, S("bs", fontName=BF, fontSize=10, textColor=colors.HexColor("#BDE3FF"), alignment=TA_CENTER))],
]
t = Table(data, colWidths=[WC])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), accent_color),
("TOPPADDING", (0,0), (-1,-1), 16),
("BOTTOMPADDING", (0,0), (-1,-1), 16),
("ROUNDEDCORNERS",(0,0), (-1,-1), [8]),
]))
return t
def sec(title, color):
t = Table([[Paragraph(f" {title}", S("sc", fontName=BBF, fontSize=11, textColor=C["white"], leading=16))]], colWidths=[WC])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("ROUNDEDCORNERS",(0,0), (-1,-1), [5]),
]))
return t
def kv(rows, lbg, cw=None):
"""키-값 테이블."""
if cw is None: cw = [40*mm, WC-40*mm]
cells = []
for r in rows:
if len(r) == 2:
cells.append([Paragraph(r[0], S("kl", fontName=BBF, textColor=C["black"], alignment=TA_CENTER)),
Paragraph(r[1], S("kv"))])
elif len(r) == 3: # 키, 값, 필수여부
cells.append([Paragraph(r[0], S("kl", fontName=BBF, textColor=C["black"], alignment=TA_CENTER)),
Paragraph(r[1], S("kv")),
Paragraph(r[2], S("kr", fontName=BBF, fontSize=8, textColor=C["red"], alignment=TA_CENTER))])
t = Table(cells, colWidths=cw)
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), lbg),
("FONTSIZE", (0,0), (-1,-1), 9.5),
("ALIGN", (0,0), (0,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 7),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
return t
def empty_kv(rows, lbg, cw=None):
"""입력 란 (빈 칸 강조)."""
if cw is None: cw = [40*mm, WC-40*mm]
cells = [[Paragraph(k, S("ek", fontName=BBF, alignment=TA_CENTER)),
Paragraph(v, S("ev", textColor=C["gray"]))] for k, v in rows]
t = Table(cells, colWidths=cw)
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), lbg),
("FONTSIZE", (0,0), (-1,-1), 9.5),
("ALIGN", (0,0), (0,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 7),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
return t
def sign_area(date_label="신청일"):
rows = [
[Paragraph(date_label, S("sl", fontName=BBF, alignment=TA_CENTER)),
Paragraph("2026년 월 일", S("sv", fontSize=10.5))],
[Paragraph("회 사 명", S("sl", fontName=BBF, alignment=TA_CENTER)),
Paragraph("(주)지오정보기술", S("sv", fontSize=10.5))],
[Paragraph("대 표 자", S("sl", fontName=BBF, alignment=TA_CENTER)),
Paragraph(" (인)", S("sv", fontSize=10.5))],
]
t = Table(rows, colWidths=[35*mm, WC-35*mm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), C["lgray"]),
("FONTNAME", (0,0), (0,-1), BBF),
("FONTSIZE", (0,0), (-1,-1), 10),
("ALIGN", (0,0), (0,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 8),
("LINEBELOW", (1,2), (1,2), 1, C["black"]),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
return t
def footer(org_name):
return [
Spacer(1, 8*mm),
HRFlowable(width=WC, color=C["blue"], thickness=1.5),
Spacer(1, 3*mm),
Paragraph(f"{org_name} | (주)지오정보기술 | GUARDiA ITSM v2.0 | Copyright © 2026",
S("ft", fontSize=7.5, textColor=C["gray"], alignment=TA_CENTER, leading=11)),
]
def on_page_num(pname):
def fn(canvas, doc):
canvas.saveState()
canvas.setFont("Helvetica", 7)
canvas.setFillColor(C["gray"])
canvas.drawRightString(W-MARGIN, 9*mm, f"- {doc.page} -")
canvas.drawString(MARGIN, 9*mm, pname)
canvas.restoreState()
return fn
def note(text):
return Paragraph(f"{text}", S("nt", fontSize=8, textColor=C["gray"], leading=12, leftIndent=8))
def make_doc(path, title):
return SimpleDocTemplate(path, pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=MARGIN, bottomMargin=MARGIN,
title=title, author="(주)지오정보기술")
# ══════════════════════════════════════════════════════════════
# PDF 01: 소프트웨어 저작권 등록 신청서
# ══════════════════════════════════════════════════════════════
def make_copyright_pdf(out):
doc = make_doc(out, "소프트웨어 저작권 등록 신청서")
story = []
AC = C["blue"]
LBG = C["lblue"]
story.append(banner(
"소프트웨어 저작권 등록 신청서",
"컴퓨터프로그램저작물 등록 | 한국저작권위원회 | www.copyright.or.kr",
AC
))
story.append(Spacer(1, 5*mm))
story.append(Paragraph("컴퓨터프로그램저작물 등록신청서",
S("h1", fontName=BBF, fontSize=16, textColor=AC, alignment=TA_CENTER, leading=22)))
story.append(Paragraph(
"「저작권법」 제53조 및 「저작권법 시행규칙」 제24조에 따라 다음과 같이 등록을 신청합니다.",
S("ref", fontName=BF, fontSize=9, textColor=C["gray"], alignment=TA_CENTER, leading=14)))
story.append(Spacer(1, 6*mm))
# 1. 저작물 정보
story.append(sec("1. 저작물 정보 (필수 기재)", AC))
story.append(Spacer(1, 3*mm))
story.append(kv([
["저 작 물 명", "GUARDiA ITSM (가이더)"],
["영 문 명", "GUARDiA ITSM Platform"],
["저작물 종류", "컴퓨터프로그램저작물"],
["프로그램 언어","Python 3.11, JavaScript (React 18)"],
["창 작 연 도", "2026년"],
["공 표 여 부", "공표 (2026년 / 인터넷 웹사이트 www.zioinfo.co.kr)"],
["등록 목적", "양도 및 이용허락"],
["버 전", "v2.0.0"],
], LBG))
story.append(Spacer(1, 5*mm))
# 2. 저작자 / 저작권자
story.append(sec("2. 저작자 및 저작권자 정보", AC))
story.append(Spacer(1, 3*mm))
story.append(kv([
["구 분", "법인 저작권자"],
["저 작 자 명", "(주)지오정보기술"],
["저 작 권 자", "(주)지오정보기술 (저작자와 동일)"],
["법인등록번호", "000000-0000000"],
["사업자등록번호", "000-00-00000"],
["주 소", "서울특별시 (상세주소)"],
["대 표 자", "(대표이사명)"],
["전 화", "02-000-0000"],
["이 메 일", "copyright@zioinfo.co.kr"],
], LBG))
story.append(Spacer(1, 5*mm))
# 3. 저작물 설명
story.append(sec("3. 저작물 설명 (200자 이내)", AC))
story.append(Spacer(1, 3*mm))
desc_box = Table([[Paragraph(
"GUARDiA ITSM은 공공기관의 레거시 IT 인프라를 AI로 자율 운영하는 온프레미스 통합 관리 플랫폼입니다. "
"메신저 한 줄 명령으로 에이전트 설치 없이 SSH/SFTP를 통해 WAS 배포·운영을 자동화하며, "
"SR 관리, 인시던트 대응, 변경관리, CMDB, PMS 등 ITSM 전 기능과 "
"AI 자동화(티켓분류·RCA·이상탐지·예측)를 단일 플랫폼에서 제공합니다.",
S("db", leading=16, alignment=TA_JUSTIFY)
)]], colWidths=[WC])
desc_box.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LBG),
("TOPPADDING", (0,0), (-1,-1), 12),
("BOTTOMPADDING", (0,0), (-1,-1), 12),
("LEFTPADDING", (0,0), (-1,-1), 14),
("RIGHTPADDING", (0,0), (-1,-1), 14),
("ROUNDEDCORNERS",(0,0), (-1,-1), [6]),
]))
story.append(desc_box)
story.append(Spacer(1, 5*mm))
# 4. 첨부 서류
story.append(sec("4. 첨부 서류", AC))
story.append(Spacer(1, 3*mm))
attach_h = [["번호", "서류명", "비고"]]
attach_d = [
["1", "저작물 설명서", "본 신청서 3항 활용 가능"],
["2", "소스코드 일부 출력물", "핵심 기능 200줄 이상 (영업비밀 마스킹 후 제출)"],
["3", "법인등기부등본", "최근 3개월 이내 발급본"],
["4", "대리인 위임장", "대리신청 시에만 제출"],
]
at = Table(
[[Paragraph(v, S(f"a{i}", alignment=TA_CENTER if i==0 else TA_LEFT, fontSize=9)) for i,v in enumerate(r)] for r in attach_h+attach_d],
colWidths=[12*mm, 72*mm, WC-84*mm]
)
at.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), AC),
("TEXTCOLOR", (0,0), (-1,0), C["white"]),
("FONTNAME", (0,0), (-1,0), BBF),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C["white"], LBG]),
("FONTSIZE", (0,0), (-1,-1), 9),
("ALIGN", (0,0), (0,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 7),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
story.append(at)
story.append(Spacer(1, 5*mm))
# 5. 수수료 안내
story.append(sec("5. 등록 수수료", AC))
story.append(Spacer(1, 3*mm))
fee = Table([
[Paragraph("구분", S("fh", fontName=BBF, alignment=TA_CENTER)),
Paragraph("금액", S("fh", fontName=BBF, alignment=TA_CENTER)),
Paragraph("비고", S("fh", fontName=BBF, alignment=TA_CENTER))],
[Paragraph("컴퓨터프로그램저작물 등록", S("fd")),
Paragraph("40,000원", S("fd", alignment=TA_CENTER)),
Paragraph("온라인 신청 시 카드/계좌이체", S("fd"))],
], colWidths=[70*mm, 30*mm, WC-100*mm])
fee.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), AC),
("TEXTCOLOR", (0,0), (-1,0), C["white"]),
("BACKGROUND", (0,1), (-1,1), LBG),
("FONTSIZE", (0,0), (-1,-1), 9.5),
("ALIGN", (1,1), (1,1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 8),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
story.append(fee)
story.append(Spacer(1, 3*mm))
story.append(note("신청 방법: 한국저작권위원회 홈페이지(www.copyright.or.kr) → 저작권 등록 → 온라인 신청"))
story.append(note("처리 기간: 약 2주 (보완 요청 시 추가 소요)"))
story.append(Spacer(1, 6*mm))
story.append(Paragraph("위와 같이 컴퓨터프로그램저작물 등록을 신청합니다.",
S("pledge", fontName=BF, fontSize=10, alignment=TA_CENTER, leading=16)))
story.append(Spacer(1, 6*mm))
story.append(sign_area("신 청 일"))
for item in footer("한국저작권위원회 제출용"):
story.append(item)
doc.build(story, onFirstPage=on_page_num("소프트웨어 저작권 등록 신청서 | 한국저작권위원회"),
onLaterPages=on_page_num("소프트웨어 저작권 등록 신청서 | 한국저작권위원회"))
print(f"[OK] 01_소프트웨어_저작권_등록_신청서.pdf ({Path(out).stat().st_size//1024}KB)")
# ══════════════════════════════════════════════════════════════
# PDF 02: 소프트웨어사업자 신고서
# ══════════════════════════════════════════════════════════════
def make_bizreg_pdf(out):
doc = make_doc(out, "소프트웨어사업자 신고서")
story = []
AC = C["red"]
LBG = C["lred"]
story.append(banner(
"소프트웨어사업자 신고서",
"소프트웨어진흥법 제24조 | 과학기술정보통신부 / 한국SW산업협회(KOSA)",
AC
))
story.append(Spacer(1, 5*mm))
story.append(Paragraph("소프트웨어사업자 신고서",
S("h2", fontName=BBF, fontSize=16, textColor=AC, alignment=TA_CENTER, leading=22)))
story.append(Paragraph(
"「소프트웨어진흥법」 제24조 및 같은 법 시행규칙 제14조에 따라 다음과 같이 신고합니다.",
S("ref2", fontName=BF, fontSize=9, textColor=C["gray"], alignment=TA_CENTER, leading=14)))
story.append(Spacer(1, 6*mm))
# 1. 신고인 정보
story.append(sec("1. 신고인(사업자) 정보", AC))
story.append(Spacer(1, 3*mm))
story.append(kv([
["상 호", "(주)지오정보기술"],
["대 표 자", "(대표이사명)"],
["사업자등록번호", "000-00-00000"],
["법인등록번호", "000000-0000000"],
["소 재 지", "서울특별시 (상세주소)"],
["전 화", "02-000-0000"],
["팩 스", "02-000-0001"],
["이 메 일", "sw@zioinfo.co.kr"],
["설 립 일", "2000년 월 일"],
["자 본 금", ""],
["종업원 수", " 명 (정규직 기준)"],
], LBG))
story.append(Spacer(1, 5*mm))
# 2. 소프트웨어사업 종류
story.append(sec("2. 소프트웨어사업의 종류 (해당 항목 모두 선택)", AC))
story.append(Spacer(1, 3*mm))
biz = [
["[V]", "소프트웨어 개발업", "GUARDiA ITSM 등 소프트웨어 개발·판매"],
["[V]", "소프트웨어 공급업", "GUARDiA ITSM 라이선스 공급"],
["[V]", "소프트웨어 유지관리업", "GUARDiA ITSM 기술지원·유지보수"],
["[V]", "소프트웨어 자문·평가·진단업", "IT인프라 컨설팅·진단"],
["[ ]", "정보기술서비스업", ""],
["[ ]", "기타 ( 업)", ""],
]
bh = [["선택", "사업 종류", "세부 내용"]]
bd = [[Paragraph(v, S(f"bv{i}", textColor=AC if "V" in v else C["gray"],
fontName=BBF, fontSize=10, alignment=TA_CENTER)),
Paragraph(n, S(f"bn{i}", fontSize=9.5)),
Paragraph(d, S(f"bd{i}", fontSize=8.5, textColor=C["gray"]))]
for i,(v,n,d) in enumerate(biz)]
bt = Table(bh+bd, colWidths=[16*mm, 65*mm, WC-81*mm])
bt.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), AC),
("TEXTCOLOR", (0,0), (-1,0), C["white"]),
("FONTNAME", (0,0), (-1,0), BBF),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C["white"], LBG]),
("FONTSIZE", (0,0), (-1,-1), 9),
("ALIGN", (0,0), (0,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 7),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
story.append(bt)
story.append(Spacer(1, 5*mm))
# 3. 기술인력
story.append(sec("3. 기술인력 현황", AC))
story.append(Spacer(1, 3*mm))
story.append(empty_kv([
["총 직원 수", ""],
["SW 기술인력", " 명 (전체의 %)"],
["정보처리기사", ""],
["정보처리산업기사",""],
["기타 SW 관련 자격", ""],
], LBG))
story.append(Spacer(1, 3*mm))
story.append(note("소프트웨어사업자 신고 기준: 상시근로자 1명 이상 또는 SW기술인력 1명 이상"))
story.append(Spacer(1, 5*mm))
# 4. 주요 제품/서비스
story.append(sec("4. 주요 소프트웨어 제품 및 서비스", AC))
story.append(Spacer(1, 3*mm))
prod_h = [["제품/서비스명", "분류", "출시연도", "연매출(백만원)"]]
prod_d = [
["GUARDiA ITSM", "IT서비스관리 플랫폼", "2026", ""],
["IT인프라 컨설팅", "컨설팅 서비스", "2000", ""],
["SI 구축 서비스", "정보화사업", "2000", ""],
]
ph = Table(prod_h+prod_d, colWidths=[50*mm, 40*mm, 22*mm, WC-112*mm])
ph.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), AC),
("TEXTCOLOR", (0,0), (-1,0), C["white"]),
("FONTNAME", (0,0), (-1,0), BBF),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C["white"], LBG]),
("FONTSIZE", (0,0), (-1,-1), 9),
("ALIGN", (0,0), (-1,0), "CENTER"),
("ALIGN", (2,1), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 7),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
story.append(ph)
story.append(Spacer(1, 5*mm))
# 5. SW사업 실적
story.append(sec("5. 소프트웨어사업 실적 (최근 3년)", AC))
story.append(Spacer(1, 3*mm))
perf_h = [["연도", "발주기관", "사업명", "계약금액\n(백만원)", "기간"]]
perf_d = [
["2024", "", "GUARDiA ITSM 구축사업", "", ""],
["2025", "", "인프라 자동화 컨설팅 사업", "", ""],
["2026", "", "GUARDiA ITSM 고도화 사업", "", ""],
]
pt = Table(perf_h+perf_d, colWidths=[14*mm, 40*mm, 58*mm, 22*mm, WC-134*mm])
pt.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), AC),
("TEXTCOLOR", (0,0), (-1,0), C["white"]),
("FONTNAME", (0,0), (-1,0), BBF),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C["white"], LBG]),
("FONTSIZE", (0,0), (-1,-1), 8.5),
("ALIGN", (0,0), (-1,0), "CENTER"),
("ALIGN", (0,1), (0,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
story.append(pt)
story.append(Spacer(1, 5*mm))
story.append(note("신고 방법: 한국SW산업협회(swit.or.kr) 온라인 신고 또는 과학기술정보통신부 제출"))
story.append(note("처리 기간: 약 2주 | 수수료: 없음"))
story.append(note("확인서 발급: 신고 완료 후 '소프트웨어사업자 신고확인서' 발급 (GS인증 신청 시 필요)"))
story.append(Spacer(1, 6*mm))
story.append(Paragraph("위와 같이 소프트웨어사업자 신고를 합니다.",
S("pledge2", fontName=BF, fontSize=10, alignment=TA_CENTER, leading=16)))
story.append(Spacer(1, 6*mm))
story.append(sign_area("신 고 일"))
for item in footer("과학기술정보통신부 / 한국SW산업협회(KOSA) 제출용"):
story.append(item)
doc.build(story, onFirstPage=on_page_num("소프트웨어사업자 신고서 | 과학기술정보통신부"),
onLaterPages=on_page_num("소프트웨어사업자 신고서 | 과학기술정보통신부"))
print(f"[OK] 02_소프트웨어사업자_신고서.pdf ({Path(out).stat().st_size//1024}KB)")
# ══════════════════════════════════════════════════════════════
# PDF 03: 조달청 나라장터 물품 등록 신청서
# ══════════════════════════════════════════════════════════════
def make_g2b_pdf(out):
doc = make_doc(out, "조달청 나라장터 물품 등록 신청서")
story = []
AC = C["orange"]
LBG = C["lorg"]
story.append(banner(
"조달청 나라장터 물품 등록 신청서",
"국가종합전자조달시스템 | 조달청 | www.g2b.go.kr",
AC
))
story.append(Spacer(1, 5*mm))
story.append(Paragraph("소프트웨어 물품 등록 신청서 (나라장터)",
S("h3", fontName=BBF, fontSize=16, textColor=AC, alignment=TA_CENTER, leading=22)))
story.append(Paragraph(
"공공기관 납품을 위한 소프트웨어 물품을 국가종합전자조달시스템(나라장터)에 등록합니다.",
S("ref3", fontName=BF, fontSize=9, textColor=C["gray"], alignment=TA_CENTER, leading=14)))
story.append(Spacer(1, 6*mm))
# 1. 공급업체 정보
story.append(sec("1. 공급업체 정보", AC))
story.append(Spacer(1, 3*mm))
story.append(kv([
["업 체 명", "(주)지오정보기술"],
["대 표 자", "(대표이사명)"],
["사업자등록번호", "000-00-00000"],
["소 재 지", "서울특별시 (상세주소)"],
["대 표 전 화", "02-000-0000"],
["팩 스", "02-000-0001"],
["담 당 자", "(담당자명) / 02-000-0000 / g2b@zioinfo.co.kr"],
["계 좌 번 호", "은행명 000-000-000000 (주)지오정보기술"],
], LBG))
story.append(Spacer(1, 5*mm))
# 2. 물품 기본 정보
story.append(sec("2. 물품 기본 정보", AC))
story.append(Spacer(1, 3*mm))
story.append(kv([
["물 품 명", "GUARDiA ITSM (AI 기반 IT서비스관리 플랫폼)"],
["규격 / 모델명", "GUARDiA ITSM v2.0"],
["물품 분류코드", "소프트웨어 > 시스템관리 > IT서비스관리 (ITSM)"],
["원 산 지", "국내산 (대한민국)"],
["제 조 사", "(주)지오정보기술"],
["브 랜 드", "GUARDiA"],
["단 위", "식 (라이선스)"],
["품 목 번 호", "(나라장터 등록 후 부여)"],
], LBG))
story.append(Spacer(1, 5*mm))
# 3. 가격 정보
story.append(sec("3. 가격 정보 (에디션별 라이선스)", AC))
story.append(Spacer(1, 3*mm))
price_h = [["에디션", "대상 규모", "기준 단가 (원)", "연간 유지보수율", "비고"]]
price_d = [
["COMMUNITY", "소규모 / 검토용", "0 (무료)", "", "기관 1개·사용자 10명·서버 20대"],
["STANDARD", "중형 기관 (50기관 이하)", "별도 협의", "15%", "사용자 200명·서버 200대"],
["ENTERPRISE", "대형 관공서 / 광역기관", "별도 협의", "15%", "무제한 (전용 지원 포함)"],
]
pt = Table(price_h+price_d, colWidths=[24*mm, 36*mm, 30*mm, 22*mm, WC-112*mm])
pt.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), AC),
("TEXTCOLOR", (0,0), (-1,0), C["white"]),
("FONTNAME", (0,0), (-1,0), BBF),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C["white"], LBG]),
("FONTSIZE", (0,0), (-1,-1), 8.5),
("ALIGN", (0,0), (-1,0), "CENTER"),
("ALIGN", (0,1), (1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
story.append(pt)
story.append(Spacer(1, 3*mm))
story.append(note("나라장터 등록 단가는 실제 계약 시 협의를 통해 결정됩니다."))
story.append(note("중소기업 직접생산확인증명서 제출 시 중소기업제품 우선구매 적용 가능합니다."))
story.append(Spacer(1, 5*mm))
# 4. 제품 규격서
story.append(sec("4. 제품 규격서 (설치 환경 및 요구 사양)", AC))
story.append(Spacer(1, 3*mm))
story.append(kv([
["운영체제(서버)", "Ubuntu 20.04+ / CentOS 7+ / RHEL 8+ / Windows Server 2019+"],
["최소 CPU", "4코어 이상 (Intel / AMD x86_64)"],
["최소 RAM", "16GB 이상 (Ollama AI 엔진 포함)"],
["필요 디스크", "50GB 이상"],
["네트워크", "내부망 전용 지원 — 인터넷 연결 불필요 (완전 폐쇄망 운영 가능)"],
["클라이언트", "Chrome 90+ / Firefox 88+ / Edge 90+ (브라우저 기반, 설치 불필요)"],
["DB 지원", "SQLite (내장) / PostgreSQL 15+ (선택)"],
["AI 엔진", "Ollama 온프레미스 LLM (외부 클라우드 API 미사용)"],
], LBG))
story.append(Spacer(1, 5*mm))
# 5. 주요 기능
story.append(sec("5. 주요 기능 요약", AC))
story.append(Spacer(1, 3*mm))
func_h = [["기능 영역", "주요 기능 내용", "공공기관 필수 여부"]]
func_d = [
["SR / ITSM", "서비스요청 접수·처리, 인시던트, 변경관리, SLA, CMDB", "필수"],
["AI 자동화", "Ollama 온프레미스 AI — 티켓분류, RCA, 이상탐지, 예측", "선택"],
["ChatOps", "카카오워크·네이버웍스·슬랙 25개 봇 명령어 지원", "선택"],
["에이전트리스", "SSH/SFTP 기반 WAS 자동 배포·운영 (서버 설치 불필요)", "필수"],
["PMS", "WBS, 산출물 관리, 일간/주간/월간 보고서 자동 생성", "필수"],
["보안", "JWT+MFA+AES-256+PAM+감사로그+취약점스캔+시큐어코딩", "필수"],
["공공기관 준수", "행안부 체크리스트 19개, 웹접근성(KWCAG 2.1), PIPA", "필수"],
]
ft = Table(func_h+func_d, colWidths=[28*mm, 95*mm, WC-123*mm])
ft.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), AC),
("TEXTCOLOR", (0,0), (-1,0), C["white"]),
("FONTNAME", (0,0), (-1,0), BBF),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C["white"], LBG]),
("FONTSIZE", (0,0), (-1,-1), 9),
("ALIGN", (0,0), (-1,0), "CENTER"),
("ALIGN", (0,1), (0,-1), "CENTER"),
("ALIGN", (2,1), (2,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 7),
("GRID", (0,0), (-1,-1), 0.5, C["border"]),
]))
story.append(ft)
story.append(Spacer(1, 5*mm))
# 6. 인증 및 실적
story.append(sec("6. 인증 현황 및 납품 실적", AC))
story.append(Spacer(1, 3*mm))
story.append(kv([
["GS인증(TTA)", "GS 1등급 취득 예정 — 2026년 12월"],
["SW저작권 등록", "C-2026-XXXXXX (등록 후 기입)"],
["SW사업자 신고", "제XXX호 (신고 후 기입)"],
["주요 납품 실적", "(공공기관 납품 실적 기입 — 계약서 사본 첨부)"],
["중소기업 확인", "중소기업 직접생산확인증명서 첨부"],
], LBG))
story.append(Spacer(1, 5*mm))
story.append(note("등록 방법: 조달청 나라장터(g2b.go.kr) → 공급업체 로그인 → 물품 등록 신청"))
story.append(note("필요 서류: 사업자등록증, 법인등기부등본, SW저작권등록증, 제품 규격서, 가격확인서"))
story.append(note("처리 기간: 약 1~2주 | 수수료: 없음"))
story.append(Spacer(1, 6*mm))
story.append(Paragraph("위와 같이 물품 등록을 신청합니다.",
S("pledge3", fontName=BF, fontSize=10, alignment=TA_CENTER, leading=16)))
story.append(Spacer(1, 6*mm))
story.append(sign_area("신 청 일"))
for item in footer("조달청 나라장터 제출용"):
story.append(item)
doc.build(story, onFirstPage=on_page_num("조달청 나라장터 물품 등록 신청서 | 조달청"),
onLaterPages=on_page_num("조달청 나라장터 물품 등록 신청서 | 조달청"))
print(f"[OK] 03_조달청_나라장터_물품_등록_신청서.pdf ({Path(out).stat().st_size//1024}KB)")
# ══════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
base = Path(__file__).parent
print("GUARDiA ITSM 프로그램 등록 신청서 3종 PDF 생성 중...")
print()
make_copyright_pdf(str(base / "01_소프트웨어_저작권_등록_신청서.pdf"))
make_bizreg_pdf( str(base / "02_소프트웨어사업자_신고서.pdf"))
make_g2b_pdf( str(base / "03_조달청_나라장터_물품_등록_신청서.pdf"))
print()
print("3종 PDF 생성 완료:")
for f in sorted(base.glob("0*.pdf")):
print(f" {f.name} ({f.stat().st_size//1024}KB)")

View File

@ -0,0 +1,450 @@
"""
GUARDiA ITSM SSH 실행 엔진
asyncssh 기반 비동기 원격 명령 실행.
보안 원칙:
- root 직접 접속 금지 (ssh_user != root 강제)
- 위험 명령어 패턴 차단 (BLOCKED_PATTERNS)
- AES-256-GCM 자격증명 복호화
- 모든 실행 감사 로그 기록
- 서버 IP, 계정, 비밀번호 응답에 포함 금지
"""
from __future__ import annotations
import asyncio
import base64
import logging
import os
import re
from datetime import datetime
from typing import Optional
logger = logging.getLogger(__name__)
# ── 위험 명령어 차단 패턴 ──────────────────────────────────────────────────────
BLOCKED_PATTERNS: list[re.Pattern] = [
re.compile(p, re.IGNORECASE) for p in [
r"rm\s+-[rf]+\s+/", # rm -rf /
r"rm\s+-[rf]+\s+\*", # rm -rf *
r"mkfs(\.[a-z0-9]+)?\s", # mkfs
r"dd\s+if=", # dd if=
r">\s*/dev/(sd|hd|vd|nvme)", # 블록 디바이스 덮어쓰기
r">\s*/dev/zero",
r"shutdown\s", # shutdown
r"reboot(\s|$)", # reboot
r"halt(\s|$)", # halt
r"poweroff(\s|$)", # poweroff
r"init\s+[06]", # init 0 / init 6
r"chmod\s+[0-9]*777\s+/", # chmod 777 /
r"passwd\s+root", # root 비밀번호 변경
r"usermod\s+.*root", # root 계정 수정
r"visudo", # sudoers 편집
r"iptables\s+-F", # 방화벽 전체 삭제
r"firewall-cmd\s+--remove", # firewall-cmd 규칙 삭제
r"systemctl\s+(disable|mask)\s+(firewall|iptables|sshd)",
r"setenforce\s+0", # SELinux 비활성화
r"rm\s+.*authorized_keys", # SSH 키 삭제
r"echo\s+.*>+\s*/etc/shadow", # shadow 파일 덮어쓰기
r"curl\s+.*\|\s*(bash|sh)", # curl | bash 패턴
r"wget\s+.*-O\s*-\s*\|", # wget | 파이프
r"base64\s+-d.*\|.*sh", # base64 decode | sh
r"python.*exec\(", # 동적 코드 실행
r":(){ :|:& };:", # fork bomb
]
]
def _validate_command(cmd: str) -> tuple[bool, str]:
"""
명령어 안전성 검증.
Returns: (is_safe, reason)
"""
for pattern in BLOCKED_PATTERNS:
if pattern.search(cmd):
return False, f"차단된 명령어 패턴: {pattern.pattern}"
# 다중 명령어 내 각 부분도 검사
for part in re.split(r"[;&|]+", cmd):
part = part.strip()
for pattern in BLOCKED_PATTERNS:
if pattern.search(part):
return False, f"차단된 명령어 패턴 (조합): {pattern.pattern}"
return True, ""
def _decrypt_password(enc_b64: str) -> str:
"""
AES-256-GCM 암호화된 비밀번호 복호화.
형식: base64(nonce[12] + ciphertext + tag[16])
"""
try:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key_b64 = os.environ.get("AES_SECRET_KEY", "")
if not key_b64:
raise ValueError("AES_SECRET_KEY 환경변수 미설정")
key = base64.b64decode(key_b64)
data = base64.b64decode(enc_b64)
nonce, ct = data[:12], data[12:]
return AESGCM(key).decrypt(nonce, ct, None).decode()
except Exception as e:
logger.error("비밀번호 복호화 실패: %s", str(e)[:50])
raise RuntimeError("자격증명 복호화 실패") from e
class SSHResult:
"""SSH 실행 결과 (보안 필드 미포함)."""
def __init__(self, success: bool, stdout: str, stderr: str,
exit_code: int, elapsed: float, error: str = ""):
self.success = success
self.stdout = stdout[:10000] # 최대 10KB
self.stderr = stderr[:2000] # 최대 2KB
self.exit_code = exit_code
self.elapsed = round(elapsed, 2)
self.error = error # 연결/실행 오류 요약
def to_dict(self) -> dict:
return {
"success": self.success,
"stdout": self.stdout,
"stderr": self.stderr,
"exit_code": self.exit_code,
"elapsed": self.elapsed,
"error": self.error,
}
async def exec_command(
server_name: str,
command: str,
timeout: int = 300,
*,
db=None,
actor: str = "GUARDiA-AI",
sr_id: Optional[str] = None,
) -> SSHResult:
"""
지정 서버에 SSH 접속 명령어 실행.
Args:
server_name: tb_server.server_name
command: 실행할 명령어
timeout: 실행 타임아웃 ()
db: AsyncSession (None 이면 세션 생성)
actor: 감사 로그 기록자
sr_id: 연관 SR ID (감사 로그용)
"""
import asyncssh
from sqlalchemy import select
from database import SessionLocal
from models import Server, AuditLog, compute_log_hash
# ── 1. 명령어 안전성 검증 ──────────────────────────────
is_safe, reason = _validate_command(command)
if not is_safe:
logger.warning("[SSH] 차단된 명령: %s%s", command[:80], reason)
await _write_audit_safe(
db, sr_id, actor, "SSH_BLOCKED",
f"차단: {reason} | CMD: {command[:100]}"
)
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0,
error=f"보안 정책에 의해 차단된 명령입니다: {reason}"
)
# ── 2. 서버 자격증명 조회 ──────────────────────────────
own_db = db is None
_db = SessionLocal() if own_db else db
server_obj = None
try:
r = await _db.execute(
select(Server).where(Server.server_name == server_name)
)
server_obj = r.scalars().first()
finally:
if own_db:
await _db.close()
if not server_obj:
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0,
error=f"서버를 찾을 수 없습니다: {server_name}"
)
# root 직접 접속 차단
ssh_user = getattr(server_obj, "ssh_user", "") or ""
if ssh_user.strip() == "root":
await _write_audit_safe(
db, sr_id, actor, "SSH_BLOCKED",
f"root 직접 접속 차단: {server_name}"
)
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0,
error="root 직접 SSH 접속은 보안 정책에 의해 금지됩니다."
)
# 비밀번호 복호화
enc_pw = getattr(server_obj, "os_pw_enc", "") or ""
try:
password = _decrypt_password(enc_pw) if enc_pw else ""
except RuntimeError as e:
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0, error=str(e)
)
ip_addr = getattr(server_obj, "ip_addr", "")
ssh_port = getattr(server_obj, "ssh_port", 22) or 22
# ── 3. 감사 로그 — 실행 시작 ─────────────────────────
await _write_audit_safe(
db, sr_id, actor, "SSH_EXEC_START",
f"서버: {server_name} | CMD: {command[:200]}"
)
# ── 4. asyncssh 실행 ─────────────────────────────────
start = asyncio.get_event_loop().time()
try:
connect_timeout = int(os.environ.get("SSH_CONNECT_TIMEOUT", "10"))
known_hosts = os.environ.get("SSH_KNOWN_HOSTS", None)
conn_kwargs: dict = {
"host": ip_addr,
"port": ssh_port,
"username": ssh_user,
"connect_timeout": connect_timeout,
}
if password:
conn_kwargs["password"] = password
# 운영환경: known_hosts 검증 / 개발환경: 비활성화
if known_hosts:
conn_kwargs["known_hosts"] = known_hosts
else:
conn_kwargs["known_hosts"] = None # 개발/테스트용
async with asyncssh.connect(**conn_kwargs) as conn:
result = await asyncio.wait_for(
conn.run(command, check=False),
timeout=timeout
)
elapsed = asyncio.get_event_loop().time() - start
stdout = (result.stdout or "").strip()
stderr = (result.stderr or "").strip()
exit_code = result.exit_status or 0
success = (exit_code == 0)
# ── 5. 감사 로그 — 실행 완료 ─────────────────────
summary = stdout[:200] if success else stderr[:200]
await _write_audit_safe(
db, sr_id, actor, "SSH_EXEC_DONE",
f"서버: {server_name} | exit={exit_code} | {summary}"
)
return SSHResult(
success=success,
stdout=stdout,
stderr=stderr,
exit_code=exit_code,
elapsed=elapsed,
)
except asyncio.TimeoutError:
elapsed = asyncio.get_event_loop().time() - start
await _write_audit_safe(
db, sr_id, actor, "SSH_EXEC_TIMEOUT",
f"서버: {server_name} | timeout={timeout}s"
)
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=elapsed,
error=f"명령 실행 타임아웃 ({timeout}초)"
)
except Exception as e:
elapsed = asyncio.get_event_loop().time() - start
# 에러 메시지에서 IP/계정 제거 후 로깅
safe_err = _sanitize_error(str(e))
logger.error("[SSH] 실행 오류 — 서버: %s | %s", server_name, safe_err)
await _write_audit_safe(
db, sr_id, actor, "SSH_EXEC_ERROR",
f"서버: {server_name} | 오류: {safe_err}"
)
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=elapsed,
error=f"SSH 연결/실행 오류 (서버 관리자에게 문의)"
)
async def exec_script(
server_name: str,
script_path: str,
env_vars: Optional[dict] = None,
timeout: int = 600,
*,
db=None,
actor: str = "GUARDiA-AI",
sr_id: Optional[str] = None,
) -> SSHResult:
"""
지정 서버에서 SM 스크립트 실행.
서버에 스크립트를 전송 실행 SCP + bash.
"""
import asyncssh
from sqlalchemy import select
from database import SessionLocal
from models import Server
# 스크립트 경로 안전성 검사
safe_path = os.path.realpath(script_path)
scripts_root = os.path.realpath(
os.path.join(os.path.dirname(__file__), "..", "scripts", "sm")
)
if not safe_path.startswith(scripts_root):
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0,
error="허용되지 않은 스크립트 경로입니다."
)
if not os.path.isfile(safe_path):
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0,
error=f"스크립트 파일이 존재하지 않습니다: {os.path.basename(script_path)}"
)
# 서버 정보 조회
own_db = db is None
_db = SessionLocal() if own_db else db
server_obj = None
try:
r = await _db.execute(
select(Server).where(Server.server_name == server_name)
)
server_obj = r.scalars().first()
finally:
if own_db:
await _db.close()
if not server_obj:
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0,
error=f"서버를 찾을 수 없습니다: {server_name}"
)
ssh_user = getattr(server_obj, "ssh_user", "") or ""
if ssh_user.strip() == "root":
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0,
error="root 직접 SSH 접속은 보안 정책에 의해 금지됩니다."
)
enc_pw = getattr(server_obj, "os_pw_enc", "") or ""
ip_addr = getattr(server_obj, "ip_addr", "") or ""
ssh_port = getattr(server_obj, "ssh_port", 22) or 22
try:
password = _decrypt_password(enc_pw) if enc_pw else ""
except RuntimeError as e:
return SSHResult(success=False, stdout="", stderr="",
exit_code=-1, elapsed=0.0, error=str(e))
connect_timeout = int(os.environ.get("SSH_CONNECT_TIMEOUT", "10"))
known_hosts = os.environ.get("SSH_KNOWN_HOSTS", None)
conn_kwargs: dict = {
"host": ip_addr, "port": ssh_port, "username": ssh_user,
"connect_timeout": connect_timeout,
"known_hosts": known_hosts,
}
if password:
conn_kwargs["password"] = password
remote_tmp = f"/tmp/guardia_sm_{os.path.basename(script_path)}"
env_str = " ".join(f"{k}={v}" for k, v in (env_vars or {}).items())
await _write_audit_safe(
db, sr_id, actor, "SSH_SCRIPT_START",
f"서버: {server_name} | 스크립트: {os.path.basename(script_path)}"
)
start = asyncio.get_event_loop().time()
try:
async with asyncssh.connect(**conn_kwargs) as conn:
# SCP 전송
async with conn.start_sftp_client() as sftp:
await sftp.put(safe_path, remote_tmp)
# 실행 권한 + 실행
run_cmd = f"chmod +x {remote_tmp} && {env_str} bash {remote_tmp}; rm -f {remote_tmp}"
result = await asyncio.wait_for(
conn.run(run_cmd, check=False),
timeout=timeout
)
elapsed = asyncio.get_event_loop().time() - start
stdout = (result.stdout or "").strip()
stderr = (result.stderr or "").strip()
exit_code = result.exit_status or 0
await _write_audit_safe(
db, sr_id, actor, "SSH_SCRIPT_DONE",
f"서버: {server_name} | script: {os.path.basename(script_path)} | exit={exit_code}"
)
return SSHResult(
success=(exit_code == 0),
stdout=stdout, stderr=stderr,
exit_code=exit_code, elapsed=elapsed
)
except asyncio.TimeoutError:
elapsed = asyncio.get_event_loop().time() - start
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=elapsed,
error=f"스크립트 실행 타임아웃 ({timeout}초)"
)
except Exception as e:
elapsed = asyncio.get_event_loop().time() - start
safe_err = _sanitize_error(str(e))
return SSHResult(
success=False, stdout="", stderr="",
exit_code=-1, elapsed=elapsed,
error=f"SSH 스크립트 실행 오류: {safe_err}"
)
def _sanitize_error(msg: str) -> str:
"""에러 메시지에서 IP/계정/비밀번호 패턴 제거."""
msg = re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", "<IP>", msg)
msg = re.sub(r"password[=:\s]+\S+", "password=<hidden>", msg, flags=re.IGNORECASE)
msg = re.sub(r"user(?:name)?[=:\s]+\S+", "user=<hidden>", msg, flags=re.IGNORECASE)
return msg[:200]
async def _write_audit_safe(db, sr_id, actor, action, detail):
"""감사 로그 안전 기록 — 독립 세션 사용."""
try:
from database import SessionLocal
from models import AuditLog, compute_log_hash
from sqlalchemy import select
async with SessionLocal() as _db:
r = await _db.execute(
select(AuditLog)
.where(AuditLog.sr_id == sr_id)
.order_by(AuditLog.id.desc())
.limit(1)
)
last = r.scalars().first()
prev_hash = last.log_hash if last else None
ts = datetime.now().isoformat()
log_hash = compute_log_hash(prev_hash, actor, action, detail, ts)
_db.add(AuditLog(
sr_id=sr_id, actor=actor, action=action,
detail=detail, prev_hash=prev_hash, log_hash=log_hash
))
await _db.commit()
except Exception as e:
logger.warning("[SSH] 감사 로그 기록 실패: %s", str(e)[:80])

View File

@ -0,0 +1,455 @@
"""
GUARDiA 라이선스 엔진 생성/검증/관리.
라이선스 구조:
GRD-{base64url(iv[12] + ciphertext + gcm_tag[16] + hmac_sig[8])}
보안 설계:
- AES-256-GCM 암호화 (iv 12B + tag 16B)
- HMAC-SHA256 서명 (8B prefix 위변조 즉시 탐지)
- 완전 오프라인 검증 (외부 API 호출 없음)
- 환경변수 GUARDIA_LICENSE_KEY (64 hex chars = 32 bytes) 필수
라이선스 에디션:
TRIAL 7 무료 체험: 기관 1, 사용자 10, 서버 20 (설치당 1)
COMMUNITY 무료: 기관 1, 사용자 10
STANDARD 유료: 기관 50, 사용자 200
ENTERPRISE 유료: 무제한
"""
from __future__ import annotations
import base64
import hashlib
import hmac as _hmac
import json
import os
import secrets
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Optional
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# ── 환경 설정 ──────────────────────────────────────────────────────────────────
LICENSE_MASTER_KEY: str = os.environ.get("GUARDIA_LICENSE_KEY", "")
_HMAC_SUFFIX = b"guardia-license-hmac-v1"
_IV_LEN = 12
_SIG_LEN = 8 # HMAC-SHA256 중 앞 8바이트
# 체험판 전용 내장 마스터 키 (프로덕션 키와 완전히 분리)
# TRIAL 에디션 키만 발급 가능 — 설치당 1회 제한으로 남용 방지
_TRIAL_MASTER_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
TRIAL_DURATION_DAYS = int(os.getenv("TRIAL_DURATION_DAYS", "7")) # 환경변수로 조정 가능
# ── 에디션 정의 ────────────────────────────────────────────────────────────────
class LicenseEdition(str, Enum):
TRIAL = "TRIAL"
COMMUNITY = "COMMUNITY"
STANDARD = "STANDARD"
ENTERPRISE = "ENTERPRISE"
EDITION_LIMITS: dict[str, dict] = {
LicenseEdition.TRIAL: {
"max_institutions": 1,
"max_users": 10,
"max_servers": 20,
"features": ["MFA"],
"trial": True,
},
LicenseEdition.COMMUNITY: {
"max_institutions": 1,
"max_users": 10,
"max_servers": 20,
"features": ["MFA"],
},
LicenseEdition.STANDARD: {
"max_institutions": 50,
"max_users": 200,
"max_servers": 500,
"features": ["MFA", "LDAP", "PAM", "AI_AGENTS"],
},
LicenseEdition.ENTERPRISE: {
"max_institutions": -1, # -1 = 무제한
"max_users": -1,
"max_servers": -1,
"features": ["MFA", "LDAP", "PAM", "AI_AGENTS",
"VULN_SCAN", "CICD", "ANALYTICS", "FINOPS"],
},
}
# 만료 경고 기준 (일)
EXPIRY_WARN_DAYS = 30
# ── 내부 유틸 ──────────────────────────────────────────────────────────────────
def _require_master_key(override: Optional[str] = None) -> str:
mk = override or LICENSE_MASTER_KEY
if not mk:
raise RuntimeError(
"GUARDIA_LICENSE_KEY 환경변수가 설정되지 않았습니다. "
".env 파일에 64자리 hex 값(32바이트)을 설정하세요."
)
if len(mk) != 64:
raise ValueError(f"GUARDIA_LICENSE_KEY는 64자리 hex여야 합니다 (현재: {len(mk)}자)")
return mk
def _derive_keys(master_hex: str) -> tuple[bytes, bytes]:
"""master_hex → (aes_key: 32B, hmac_key: 32B)."""
master = bytes.fromhex(master_hex)
aes_key = hashlib.sha256(master + b"guardia-aes-v1").digest()
hmac_key = hashlib.sha256(master + _HMAC_SUFFIX).digest()
return aes_key, hmac_key
# ── 라이선스 생성 ───────────────────────────────────────────────────────────────
def generate_license_key(
customer: str,
edition: LicenseEdition,
expires_at: datetime,
license_id: Optional[str] = None,
custom_limits: Optional[dict] = None,
master_key_hex: Optional[str] = None,
) -> str:
"""
라이선스 생성 (벤더 내부 도구 전용).
Args:
customer: 고객/기관 이름
edition: COMMUNITY | STANDARD | ENTERPRISE
expires_at: 만료일시 (UTC)
license_id: 지정 라이선스 ID (미지정 자동 생성)
custom_limits: 에디션 기본 제한 재정의 (선택)
master_key_hex: 마스터 (미지정 환경변수 사용)
Returns:
"GRD-{base64url}" 형태의 라이선스 문자열
"""
mk = _require_master_key(master_key_hex)
lid = license_id or f"GRD-{secrets.token_hex(6).upper()}"
limits = custom_limits or EDITION_LIMITS[edition]
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
payload = json.dumps({
"license_id": lid,
"edition": edition.value,
"customer": customer,
"issued_at": datetime.now(timezone.utc).isoformat(),
"expires_at": expires_at.isoformat(),
"limits": limits,
}, ensure_ascii=False, separators=(",", ":"))
aes_key, hmac_key = _derive_keys(mk)
iv = secrets.token_bytes(_IV_LEN)
ct = AESGCM(aes_key).encrypt(iv, payload.encode(), None) # ct includes 16B GCM tag
blob = iv + ct
sig = _hmac.new(hmac_key, blob, hashlib.sha256).digest()[:_SIG_LEN]
encoded = base64.urlsafe_b64encode(blob + sig).decode().rstrip("=")
return f"GRD-{encoded}"
# ── 체험판 라이선스 생성 ────────────────────────────────────────────────────────
def generate_trial_key(customer: str, license_id: Optional[str] = None,
days: Optional[int] = None) -> str:
"""
체험 라이선스 생성.
- 내장 Trial 마스터 사용 (GUARDIA_LICENSE_KEY 불필요)
- TRIAL 에디션 고정
- 만료: 생성 시점 + days일 (기본: TRIAL_DURATION_DAYS)
Args:
customer: 고객사/사용자명
license_id: 라이선스 ID (None이면 자동 생성)
days: 체험 기간 (). None이면 TRIAL_DURATION_DAYS 사용
Returns:
"GRD-{base64url}" 형태의 체험 라이선스
"""
trial_days = days if days is not None else TRIAL_DURATION_DAYS
expires_at = datetime.now(timezone.utc) + timedelta(days=trial_days)
lid = license_id or f"TRL-{secrets.token_hex(6).upper()}"
limits = EDITION_LIMITS[LicenseEdition.TRIAL]
payload = json.dumps({
"license_id": lid,
"edition": LicenseEdition.TRIAL.value,
"customer": customer,
"issued_at": datetime.now(timezone.utc).isoformat(),
"expires_at": expires_at.isoformat(),
"limits": limits,
}, ensure_ascii=False, separators=(",", ":"))
aes_key, hmac_key = _derive_keys(_TRIAL_MASTER_KEY)
iv = secrets.token_bytes(_IV_LEN)
ct = AESGCM(aes_key).encrypt(iv, payload.encode(), None)
blob = iv + ct
sig = _hmac.new(hmac_key, blob, hashlib.sha256).digest()[:_SIG_LEN]
encoded = base64.urlsafe_b64encode(blob + sig).decode().rstrip("=")
return f"GRD-{encoded}"
def decode_trial_key(key: str) -> dict:
"""체험판 키 전용 복호화 (내장 Trial 마스터 키 사용)."""
if not key.startswith("GRD-"):
raise ValueError("잘못된 라이선스 키 형식")
try:
raw = base64.urlsafe_b64decode(key[4:] + "==")
except Exception:
raise ValueError("라이선스 키 base64 디코딩 실패")
min_len = _IV_LEN + 16 + _SIG_LEN
if len(raw) < min_len:
raise ValueError("라이선스 키 길이 오류")
blob, sig = raw[:-_SIG_LEN], raw[-_SIG_LEN:]
aes_key, hmac_key = _derive_keys(_TRIAL_MASTER_KEY)
expected_sig = _hmac.new(hmac_key, blob, hashlib.sha256).digest()[:_SIG_LEN]
if not _hmac.compare_digest(sig, expected_sig):
raise ValueError("체험 라이선스 서명 검증 실패")
iv, ct = blob[:_IV_LEN], blob[_IV_LEN:]
try:
plaintext = AESGCM(aes_key).decrypt(iv, ct, None)
except Exception:
raise ValueError("체험 라이선스 복호화 실패")
return json.loads(plaintext)
# ── 라이선스 복호화 ─────────────────────────────────────────────────────────────
def decode_license_key(key: str, master_key_hex: Optional[str] = None) -> dict:
"""
라이선스 복호화 서명 검증.
Returns:
payload dict (license_id, edition, customer, issued_at, expires_at, limits)
Raises:
ValueError: 형식/서명/복호화 오류
"""
mk = _require_master_key(master_key_hex)
if not key.startswith("GRD-"):
raise ValueError("잘못된 라이선스 키 형식 (GRD- 접두사 필요)")
try:
raw = base64.urlsafe_b64decode(key[4:] + "==")
except Exception:
raise ValueError("라이선스 키 base64 디코딩 실패")
min_len = _IV_LEN + 16 + _SIG_LEN # iv + gcm_tag + sig
if len(raw) < min_len:
raise ValueError("라이선스 키 길이 오류")
blob, sig = raw[:-_SIG_LEN], raw[-_SIG_LEN:]
aes_key, hmac_key = _derive_keys(mk)
expected_sig = _hmac.new(hmac_key, blob, hashlib.sha256).digest()[:_SIG_LEN]
if not _hmac.compare_digest(sig, expected_sig):
raise ValueError("라이선스 서명 검증 실패 — 위변조 또는 잘못된 키")
iv, ct = blob[:_IV_LEN], blob[_IV_LEN:]
try:
plaintext = AESGCM(aes_key).decrypt(iv, ct, None)
except Exception:
raise ValueError("라이선스 복호화 실패 — 마스터 키 불일치")
try:
return json.loads(plaintext)
except json.JSONDecodeError:
raise ValueError("라이선스 페이로드 파싱 실패")
# ── 라이선스 검증 ───────────────────────────────────────────────────────────────
def validate_license(key: str, master_key_hex: Optional[str] = None) -> dict:
"""
라이선스 검증 상태 dict 반환.
Returns: {
"valid": bool, # True = 유효하고 미만료
"expired": bool,
"expiry_warning": bool, # 만료 30일 이내 경고
"license_id": str,
"edition": str,
"customer": str,
"issued_at": str,
"expires_at": str,
"limits": dict,
"days_remaining": int,
"error": str, # 오류 시에만
}
"""
# TRIAL 키는 내장 Trial 마스터 키로 먼저 시도, 실패 시 일반 마스터 키로 재시도
try:
info = decode_trial_key(key)
if info.get("edition") != LicenseEdition.TRIAL.value:
raise ValueError("not trial")
except Exception:
try:
info = decode_license_key(key, master_key_hex)
except Exception as e:
return {"valid": False, "expired": False, "error": str(e)}
now = datetime.now(timezone.utc)
expires = datetime.fromisoformat(info["expires_at"])
if expires.tzinfo is None:
expires = expires.replace(tzinfo=timezone.utc)
expired = now > expires
days_remaining = max(0, (expires - now).days)
expiry_warning = not expired and days_remaining <= EXPIRY_WARN_DAYS
is_trial = info.get("edition") == LicenseEdition.TRIAL.value
return {
"valid": not expired,
"expired": expired,
"expiry_warning": expiry_warning,
"is_trial": is_trial,
"license_id": info["license_id"],
"edition": info["edition"],
"customer": info["customer"],
"issued_at": info["issued_at"],
"expires_at": info["expires_at"],
"limits": info["limits"],
"days_remaining": days_remaining,
}
# ── 인메모리 캐시 ───────────────────────────────────────────────────────────────
_license_cache: Optional[dict] = None
_cache_refreshed_at: Optional[datetime] = None
_CACHE_TTL_SECONDS = 3600 # 1시간
def get_cached_license() -> Optional[dict]:
"""캐시된 라이선스 상태 반환 (TTL 초과 시 None)."""
global _license_cache, _cache_refreshed_at
if _license_cache is None or _cache_refreshed_at is None:
return None
age = (datetime.now(timezone.utc) - _cache_refreshed_at).total_seconds()
if age > _CACHE_TTL_SECONDS:
return None
return _license_cache
def set_cached_license(status: dict) -> None:
global _license_cache, _cache_refreshed_at
_license_cache = status
_cache_refreshed_at = datetime.now(timezone.utc)
def invalidate_license_cache() -> None:
global _license_cache, _cache_refreshed_at
_license_cache = None
_cache_refreshed_at = None
# ── 비밀번호 복잡도 검증 ────────────────────────────────────────────────────────
class PasswordStrength(str, Enum):
WEAK = "WEAK"
MEDIUM = "MEDIUM"
STRONG = "STRONG"
def check_password_complexity(password: str) -> tuple[bool, str]:
"""
비밀번호 복잡도 검증.
규칙:
- 최소 8
- 대문자 1 이상
- 소문자 1 이상
- 숫자 1 이상
- 특수문자(!@#$%^&*...) 1개 이상
Returns:
(ok: bool, message: str)
"""
errors = []
if len(password) < 8:
errors.append("최소 8자 이상")
if not any(c.isupper() for c in password):
errors.append("대문자 1자 이상")
if not any(c.islower() for c in password):
errors.append("소문자 1자 이상")
if not any(c.isdigit() for c in password):
errors.append("숫자 1자 이상")
specials = set("!@#$%^&*()-_=+[]{}|;:',.<>?/`~")
if not any(c in specials for c in password):
errors.append("특수문자(!@#$%^&* 등) 1자 이상")
if errors:
return False, "비밀번호 복잡도 미충족: " + ", ".join(errors)
return True, "OK"
# ── CLI 진입점 (python -m core.license) ────────────────────────────────────────
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="GUARDiA 라이선스 키 생성 도구",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
사용 :
python -m core.license --customer "서울시청" --edition ENTERPRISE --days 365
python -m core.license --customer "테스트기관" --edition COMMUNITY --days 30 --key 64자리hex
"""
)
parser.add_argument("--customer", required=True, help="고객/기관 이름")
parser.add_argument("--edition", required=True,
choices=["COMMUNITY", "STANDARD", "ENTERPRISE"],
help="라이선스 에디션")
parser.add_argument("--days", type=int, required=True, help="유효 기간(일)")
parser.add_argument("--lid", default=None, help="라이선스 ID (기본: 자동생성)")
parser.add_argument("--key", default=None,
help="마스터 키 (기본: GUARDIA_LICENSE_KEY 환경변수)")
args = parser.parse_args()
expires = datetime.now(timezone.utc) + timedelta(days=args.days)
try:
lic_key = generate_license_key(
customer = args.customer,
edition = LicenseEdition(args.edition),
expires_at = expires,
license_id = args.lid,
master_key_hex = args.key,
)
except Exception as e:
print(f"[오류] {e}", file=sys.stderr)
sys.exit(1)
info = validate_license(lic_key, master_key_hex=args.key)
print("\n" + "=" * 60)
print(" GUARDiA 라이선스 키 생성 완료")
print("=" * 60)
print(f" 고객명 : {info['customer']}")
print(f" 에디션 : {info['edition']}")
print(f" 라이선스ID: {info['license_id']}")
print(f" 발급일시 : {info['issued_at']}")
print(f" 만료일시 : {info['expires_at']}")
print(f" 유효기간 : {args.days}")
print("=" * 60)
print(f"\n라이선스 키:\n{lic_key}\n")

View File

@ -0,0 +1,501 @@
"""SR / Task CRUD + status transition endpoints."""
from datetime import datetime
from typing import Any, Dict, List, Optional
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from core.events import broadcast
from database import get_db
from models import (
AuditLog, Institution, SRCreate, SROut, SRRequest, SRStatus,
SRStatusUpdate, SRType, User, compute_log_hash
)
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
# Valid state transitions
_TRANSITIONS: dict[str, list[str]] = {
SRStatus.RECEIVED: [SRStatus.PARSED, SRStatus.REJECTED],
SRStatus.PARSED: [SRStatus.PENDING_APPROVAL, SRStatus.REJECTED],
SRStatus.PENDING_APPROVAL: [SRStatus.APPROVED, SRStatus.REJECTED],
SRStatus.APPROVED: [SRStatus.IN_PROGRESS, SRStatus.REJECTED],
SRStatus.IN_PROGRESS: [SRStatus.PENDING_PM_VALIDATION, SRStatus.FAILED_ROLLBACK],
SRStatus.PENDING_PM_VALIDATION:[SRStatus.COMPLETED, SRStatus.FAILED_ROLLBACK],
SRStatus.COMPLETED: [],
SRStatus.FAILED_ROLLBACK: [],
SRStatus.REJECTED: [],
}
def _new_sr_id() -> str:
return f"SR-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:6].upper()}"
async def _write_audit(db: AsyncSession, sr_id: str, actor: str,
action: str, detail: str) -> None:
from sqlalchemy import select as sel
result = await db.execute(
sel(AuditLog).where(AuditLog.sr_id == sr_id)
.order_by(AuditLog.id.desc()).limit(1)
)
last = result.scalars().first()
prev_hash = last.log_hash if last else None
ts = datetime.now().isoformat()
log_hash = compute_log_hash(prev_hash, actor, action, detail, ts)
db.add(AuditLog(
sr_id=sr_id, actor=actor, action=action, detail=detail,
prev_hash=prev_hash, log_hash=log_hash
))
async def _apply_role_filter(q, current_user: User, db: AsyncSession):
"""CUSTOMER 역할이면 자신의 기관 SR만 조회되도록 필터링."""
from models import UserRole
if current_user.role == UserRole.CUSTOMER and current_user.inst_code:
inst_r = await db.execute(
select(Institution).where(Institution.inst_code == current_user.inst_code)
)
inst = inst_r.scalars().first()
if inst:
q = q.where(SRRequest.inst_id == inst.id)
else:
# 기관 정보 없으면 빈 결과 보장
q = q.where(SRRequest.id == -1)
return q
@router.get("", response_model=List[SROut])
async def list_tasks(
status: Optional[str] = Query(None),
sr_type: Optional[str] = Query(None),
priority: Optional[str]= Query(None),
keyword: Optional[str] = Query(None),
skip: int = 0,
limit: int = 50,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
q = select(SRRequest).order_by(SRRequest.created_at.desc())
q = await _apply_role_filter(q, current_user, db)
if status:
q = q.where(SRRequest.status == status)
if sr_type:
q = q.where(SRRequest.sr_type == sr_type)
if priority:
q = q.where(SRRequest.priority == priority)
if keyword:
q = q.where(SRRequest.title.contains(keyword))
q = q.offset(skip).limit(limit)
result = await db.execute(q)
return result.scalars().all()
@router.get("/stats")
async def get_stats(db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)):
# 기관 필터 (CUSTOMER)
base_q = select(func.count(SRRequest.id))
filtered = await _apply_role_filter(select(SRRequest), current_user, db)
# subquery approach: get allowed sr ids
from sqlalchemy import and_
inst_filter = None
from models import UserRole
if current_user.role == UserRole.CUSTOMER and current_user.inst_code:
inst_r = await db.execute(
select(Institution).where(Institution.inst_code == current_user.inst_code)
)
inst = inst_r.scalars().first()
inst_filter = (SRRequest.inst_id == inst.id) if inst else (SRRequest.id == -1)
def _cnt(extra=None):
q = base_q
if inst_filter is not None:
q = q.where(inst_filter)
if extra is not None:
q = q.where(extra)
return q
total = (await db.execute(_cnt())).scalar() or 0
by_status: dict[str, int] = {}
for s in SRStatus:
cnt = (await db.execute(_cnt(SRRequest.status == s))).scalar() or 0
if cnt:
by_status[s.value] = cnt
by_type: dict[str, int] = {}
for t in SRType:
cnt = (await db.execute(_cnt(SRRequest.sr_type == t))).scalar() or 0
if cnt:
by_type[t.value] = cnt
return {"total": total, "by_status": by_status, "by_type": by_type}
@router.get("/{sr_id}", response_model=SROut)
async def get_task(sr_id: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id))
sr = result.scalars().first()
if not sr:
raise HTTPException(404, detail="SR을 찾을 수 없습니다.")
return sr
@router.post("", response_model=SROut, status_code=201)
async def create_task(payload: SRCreate, db: AsyncSession = Depends(get_db)):
inst_id = None
if payload.inst_code:
r = await db.execute(
select(Institution).where(Institution.inst_code == payload.inst_code)
)
inst = r.scalars().first()
if inst:
inst_id = inst.id
sr = SRRequest(
sr_id=_new_sr_id(),
inst_id=inst_id,
sr_type=payload.sr_type,
title=payload.title,
description=payload.description,
status=SRStatus.RECEIVED,
priority=payload.priority,
requested_by=payload.requested_by,
assigned_to=payload.assigned_to,
target_server=payload.target_server,
)
db.add(sr)
await db.flush()
await _write_audit(db, sr.sr_id, payload.requested_by, "SR_CREATED", f"SR 생성: {payload.title}")
# 담당자가 미지정이면 자동 배정 시도
if not sr.assigned_to:
from routers.assign import auto_assign_engine
assigned = await auto_assign_engine(db, sr)
if assigned:
sr.assigned_to = assigned
await _write_audit(db, sr.sr_id, "SYSTEM", "ENGINEER_ASSIGNED",
f"자동 배정: {assigned}")
# A-2: SLA 마감 시각 계산·저장
from core.sla import set_sla_on_create
await set_sla_on_create(sr.sr_id, db)
await db.refresh(sr)
await db.commit()
await db.refresh(sr)
# 실시간 이벤트 브로드캐스트
await broadcast("sr_created", {
"sr_id": sr.sr_id,
"title": sr.title,
"sr_type": sr.sr_type,
"priority": sr.priority,
"status": sr.status,
"sla_deadline": sr.sla_deadline.isoformat() if sr.sla_deadline else None,
})
# 알림 발송 (이메일 + 메신저) — 비동기 fire-and-forget
import asyncio as _asyncio
from core.notify import notify_sr_created as _notify_created
_asyncio.create_task(_notify_created(sr))
# 학습 루프: 재발 패턴 감지 — fire-and-forget
async def _detect_recurrence_bg():
from database import SessionLocal
from core.learning import detect_recurrence
try:
async with SessionLocal() as _db:
result = await detect_recurrence(
db = _db,
sr_id = sr.sr_id,
title = sr.title,
description = sr.description or "",
sr_type = sr.sr_type or "OTHER",
inst_id = sr.inst_id,
)
if result.get("recurrence_found"):
import logging
logging.getLogger(__name__).info(
"재발 감지: SR=%s 패턴=%d 횟수=%d%s",
sr.sr_id,
result["pattern_id"],
result["occurrence_count"],
" → Problem 격상" if result.get("escalated") else "",
)
except Exception as _e:
import logging
logging.getLogger(__name__).debug("재발 감지 오류 (무시): %s", _e)
_asyncio.create_task(_detect_recurrence_bg())
# G-7: AI 자동 분류 — fire-and-forget
async def _apply_ai_classification_bg():
from database import SessionLocal
from core.ticket_classifier import classify_ticket
import json as _json
try:
suggestion = await classify_ticket(sr.title, sr.description or "")
async with SessionLocal() as _db:
_sr = (await _db.execute(
select(SRRequest).where(SRRequest.sr_id == sr.sr_id)
)).scalars().first()
if _sr:
_sr.ai_suggestion = _json.dumps(suggestion, ensure_ascii=False)
await _db.commit()
except Exception:
pass
_asyncio.create_task(_apply_ai_classification_bg())
return sr
@router.patch("/{sr_id}/status", response_model=SROut)
async def update_status(sr_id: str, payload: SRStatusUpdate,
db: AsyncSession = Depends(get_db),
_u: User = Depends(get_current_user)):
result = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id))
sr = result.scalars().first()
if not sr:
raise HTTPException(404, detail="SR을 찾을 수 없습니다.")
allowed = _TRANSITIONS.get(SRStatus(sr.status), [])
if payload.status not in allowed:
raise HTTPException(400, detail=f"'{sr.status}''{payload.status}' 전이는 허용되지 않습니다.")
old_status = sr.status
sr.status = payload.status
sr.updated_at = datetime.now()
detail = payload.comment or f"상태 변경: {old_status}{payload.status}"
await _write_audit(db, sr_id, payload.actor, "STATUS_CHANGED", detail)
await db.commit()
await db.refresh(sr)
# 실시간 이벤트 브로드캐스트
await broadcast("sr_updated", {
"sr_id": sr_id,
"title": sr.title,
"old_status": old_status,
"new_status": sr.status,
"actor": payload.actor,
})
# 알림 발송 (COMPLETED / REJECTED / FAILED_ROLLBACK 시)
import asyncio as _asyncio
from core.notify import notify_sr_status_changed as _notify_changed
_asyncio.create_task(_notify_changed(sr, sr.status, payload.comment or ""))
return sr
# ── A-2: SLA 조회 엔드포인트 ──────────────────────────────────────────────────
@router.get("/{sr_id}/sla")
async def get_sla_status(
sr_id: str,
db: AsyncSession = Depends(get_db),
_u: User = Depends(get_current_user),
):
"""
특정 SR의 SLA 현황 조회.
Returns:
{
"sr_id": "SR-...",
"priority": "HIGH",
"sla_deadline": "2026-05-26T14:00:00",
"sla_breached": false,
"remaining_minutes": 47,
"escalated_at": null,
"escalated_to": null
}
"""
from core.sla import sla_remaining_minutes
result = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id))
sr = result.scalars().first()
if not sr:
raise HTTPException(404, "SR을 찾을 수 없습니다.")
remaining = sla_remaining_minutes(sr.sla_deadline)
return {
"sr_id": sr.sr_id,
"priority": sr.priority,
"sla_deadline": sr.sla_deadline.isoformat() if sr.sla_deadline else None,
"sla_breached": sr.sla_breached,
"remaining_minutes": remaining,
"escalated_at": sr.escalated_at.isoformat() if sr.escalated_at else None,
"escalated_to": sr.escalated_to,
}
@router.get("/sla/violations")
async def list_sla_violations(
skip: int = 0,
limit: int = 50,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
SLA 위반 중인 SR 목록.
ADMIN / PM / ENGINEER 접근 가능.
"""
from models import UserRole
from sqlalchemy import and_
from core.sla import sla_remaining_minutes
if current_user.role == UserRole.CUSTOMER:
raise HTTPException(403, "권한이 없습니다.")
terminal = ["COMPLETED", "REJECTED", "FAILED_ROLLBACK"]
now = datetime.now()
q = (
select(SRRequest)
.where(
and_(
SRRequest.sla_deadline.isnot(None),
SRRequest.sla_deadline < now,
SRRequest.status.notin_(terminal),
)
)
.order_by(SRRequest.sla_deadline.asc())
.offset(skip).limit(limit)
)
rows = (await db.execute(q)).scalars().all()
return [
{
"sr_id": r.sr_id,
"title": r.title,
"priority": r.priority,
"status": r.status,
"assigned_to": r.assigned_to,
"sla_deadline": r.sla_deadline.isoformat() if r.sla_deadline else None,
"overdue_minutes": abs(sla_remaining_minutes(r.sla_deadline) or 0),
"sla_breached": r.sla_breached,
"escalated_to": r.escalated_to,
}
for r in rows
]
# ── G-7: AI 분류 결과 조회 ────────────────────────────────────────────────────
@router.get("/{sr_id}/ai-suggestion")
async def get_ai_suggestion(
sr_id: str,
db: AsyncSession = Depends(get_db),
_u: User = Depends(get_current_user),
):
"""SR의 AI 자동 분류 제안 조회."""
import json as _json
r = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id))
sr = r.scalars().first()
if not sr:
raise HTTPException(404, "SR을 찾을 수 없습니다.")
if not sr.ai_suggestion:
# 즉시 분류 요청
from core.ticket_classifier import classify_ticket
try:
suggestion = await classify_ticket(sr.title, sr.description or "")
sr.ai_suggestion = _json.dumps(suggestion, ensure_ascii=False)
await db.commit()
except Exception as e:
return {"sr_id": sr_id, "suggestion": None, "message": f"AI 분류 실패: {str(e)[:100]}"}
try:
return {"sr_id": sr_id, "suggestion": _json.loads(sr.ai_suggestion)}
except Exception:
return {"sr_id": sr_id, "suggestion": None}
# ── G-2: SR 대량 처리 ─────────────────────────────────────────────────────────
class BulkActionRequest(BaseModel):
sr_ids: List[str]
action: str # STATUS_CHANGE | ASSIGN | CLOSE | PRIORITY_CHANGE
params: Dict[str, Any] = {}
@router.post("/bulk")
async def bulk_sr_action(
payload: BulkActionRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
SR 대량 작업 (최대 100).
action: STATUS_CHANGE | ASSIGN | CLOSE | PRIORITY_CHANGE
"""
from models import UserRole
if current_user.role == UserRole.CUSTOMER:
raise HTTPException(403, "대량 작업은 ADMIN/PM/ENGINEER만 가능합니다.")
if not payload.sr_ids:
raise HTTPException(400, "sr_ids가 비어 있습니다. 처리할 SR ID를 입력하세요.")
if len(payload.sr_ids) > 100:
raise HTTPException(400, "한 번에 최대 100건까지 처리 가능합니다.")
results = []
success = 0
for sr_id in payload.sr_ids:
try:
r = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id))
sr = r.scalars().first()
if not sr:
results.append({"sr_id": sr_id, "ok": False, "error": "SR을 찾을 수 없습니다."})
continue
action = payload.action.upper()
if action == "STATUS_CHANGE":
new_status = payload.params.get("status")
if not new_status:
raise ValueError("status 파라미터 필요")
allowed = _TRANSITIONS.get(SRStatus(sr.status), [])
if new_status not in allowed:
raise ValueError(f"'{sr.status}''{new_status}' 전이 불가")
sr.status = new_status
sr.updated_at = datetime.now()
note = payload.params.get("note", f"대량 상태변경: {new_status}")
await _write_audit(db, sr_id, current_user.username, "BULK_STATUS_CHANGE", note)
elif action == "ASSIGN":
assignee = payload.params.get("assignee")
if not assignee:
raise ValueError("assignee 파라미터 필요")
sr.assigned_to = assignee
sr.updated_at = datetime.now()
await _write_audit(db, sr_id, current_user.username, "BULK_ASSIGN",
f"대량 배정: {assignee}")
elif action == "CLOSE":
if sr.status in (SRStatus.COMPLETED, SRStatus.REJECTED, SRStatus.FAILED_ROLLBACK):
raise ValueError(f"이미 종료된 SR: {sr.status}")
sr.status = SRStatus.COMPLETED
sr.updated_at = datetime.now()
note = payload.params.get("note", "대량 완료 처리")
await _write_audit(db, sr_id, current_user.username, "BULK_CLOSE", note)
elif action == "PRIORITY_CHANGE":
new_prio = payload.params.get("priority")
if not new_prio:
raise ValueError("priority 파라미터 필요")
sr.priority = new_prio
sr.updated_at = datetime.now()
await _write_audit(db, sr_id, current_user.username, "BULK_PRIORITY_CHANGE",
f"우선순위 변경: {new_prio}")
else:
raise ValueError(f"알 수 없는 action: {action}")
await db.flush()
results.append({"sr_id": sr_id, "ok": True, "error": None})
success += 1
except Exception as e:
results.append({"sr_id": sr_id, "ok": False, "error": str(e)})
await db.commit()
return {
"total": len(payload.sr_ids),
"success": success,
"failed": len(payload.sr_ids) - success,
"results": results,
}

View File

@ -0,0 +1,90 @@
"""AI 자동 티켓 분류 — SR 타이틀/설명에서 우선순위, 카테고리, 담당팀 제안."""
from __future__ import annotations
import json
import logging
logger = logging.getLogger(__name__)
CLASSIFY_PROMPT = """다음 IT 서비스 요청을 분석하여 JSON으로 분류하세요.
제목: {title}
설명: {description}
다음 필드를 포함하는 JSON만 출력하세요:
{{
"priority": "CRITICAL|HIGH|MEDIUM|LOW",
"category": "HARDWARE|SOFTWARE|NETWORK|DATABASE|SECURITY|DEPLOYMENT|OTHER",
"team": "INFRA|DEV|DBA|SECURITY|HELPDESK",
"urgency_reason": "우선순위 판단 이유",
"keywords": ["핵심키워드1", "핵심키워드2", "핵심키워드3"],
"confidence": 0.85
}}"""
async def classify_ticket(title: str, description: str) -> dict:
"""SR 제목과 설명으로 AI 분류 제안 생성."""
prompt = CLASSIFY_PROMPT.format(
title=title,
description=(description or "")[:500],
)
try:
from core.llm_client import get_llm_client
client = get_llm_client()
resp = await client.chat(prompt)
raw = resp.content.strip()
if "```" in raw:
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
result = json.loads(raw)
# 필수 필드 보정
result.setdefault("priority", "MEDIUM")
result.setdefault("category", "OTHER")
result.setdefault("team", "HELPDESK")
result.setdefault("urgency_reason", "")
result.setdefault("keywords", [])
result.setdefault("confidence", 0.5)
return result
except Exception as e:
logger.warning("AI 티켓 분류 실패 (Fail-Safe): %s", e)
# 키워드 기반 규칙 폴백
return _rule_based_classify(title, description)
def _rule_based_classify(title: str, description: str) -> dict:
"""LLM 실패 시 키워드 규칙 기반 분류."""
text = f"{title} {description}".lower()
priority = "MEDIUM"
category = "OTHER"
team = "HELPDESK"
if any(k in text for k in ["긴급", "critical", "서비스 중단", "장애", "접속 불가"]):
priority = "CRITICAL"
elif any(k in text for k in ["오류", "error", "실패", "배포", "deploy"]):
priority = "HIGH"
elif any(k in text for k in ["설치", "변경", "업그레이드"]):
priority = "MEDIUM"
if any(k in text for k in ["서버", "cpu", "메모리", "디스크", "하드웨어"]):
category, team = "HARDWARE", "INFRA"
elif any(k in text for k in ["db", "데이터베이스", "oracle", "mysql", "postgresql"]):
category, team = "DATABASE", "DBA"
elif any(k in text for k in ["네트워크", "방화벽", "포트", "ip", "dns"]):
category, team = "NETWORK", "INFRA"
elif any(k in text for k in ["보안", "취약점", "cve", "패치"]):
category, team = "SECURITY", "SECURITY"
elif any(k in text for k in ["배포", "deploy", "jenkins", "빌드"]):
category, team = "DEPLOYMENT", "DEV"
elif any(k in text for k in ["소프트웨어", "애플리케이션", "app", "버그"]):
category, team = "SOFTWARE", "DEV"
return {
"priority": priority,
"category": category,
"team": team,
"urgency_reason": "키워드 규칙 기반 분류 (LLM 미사용)",
"keywords": [],
"confidence": 0.3,
}

View File

@ -0,0 +1,200 @@
/*
F-4: PWA Service Worker 등록
*/
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/static/sw.js', { scope: '/' })
.then(reg => {
console.log('[GUARDiA PWA] SW 등록 성공:', reg.scope);
// 새 버전 감지 시 사용자에게 알림
reg.addEventListener('updatefound', () => {
const newSW = reg.installing;
newSW.addEventListener('statechange', () => {
if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
console.log('[GUARDiA PWA] 새 버전 사용 가능 — 새로고침하면 업데이트됩니다.');
}
});
});
})
.catch(err => console.warn('[GUARDiA PWA] SW 등록 실패:', err));
});
}
/*
Nifty 사이드바 계층 메뉴 토글
*/
function toggleNavGroup(header) {
const body = header.nextElementSibling;
const isOpen = body.classList.contains('open');
body.classList.toggle('open', !isOpen);
header.setAttribute('aria-expanded', String(!isOpen));
}
// 현재 URL에 해당하는 메뉴 자동 열기
(function autoOpenNavGroup() {
document.querySelectorAll('.nav-group-body .nav-sub-item').forEach(item => {
const href = item.getAttribute('href') || '';
if (href && location.pathname.startsWith(href.split('?')[0])) {
const body = item.closest('.nav-group-body');
const header = body?.previousElementSibling;
if (body && header) {
body.classList.add('open');
header.setAttribute('aria-expanded', 'true');
item.classList.add('active');
}
}
});
})();
/*
테마 관리 (스크립트 최상단 FOUC 방지)
*/
function applyTheme(key) {
document.body.dataset.theme = key;
localStorage.setItem("guardia_theme", key);
document.querySelectorAll(".theme-swatch").forEach(b =>
b.classList.toggle("active", b.dataset.theme === key)
);
}
/* ─── Auth ───────────────────────────────────────── */
const _token = localStorage.getItem("guardia_token");
const _userInfo = JSON.parse(localStorage.getItem("guardia_userinfo") || "{}");
// 로그인 안 된 경우 → /login으로
if (!_token) {
window.location.replace("/login");
}
// 비밀번호 변경 필요 → /change-password로
if (_userInfo.must_change_pw) {
window.location.replace("/change-password");
}
/**
* Bearer 토큰을 자동으로 포함하는 fetch 래퍼.
* 401 응답 로그인 페이지로 리다이렉트.
*/
async function authFetch(url, opts = {}) {
const headers = { "Content-Type": "application/json", ...(opts.headers || {}) };
if (_token) headers["Authorization"] = `Bearer ${_token}`;
const res = await fetch(url, { ...opts, headers });
if (res.status === 401) {
localStorage.removeItem("guardia_token");
localStorage.removeItem("guardia_userinfo");
window.location.replace("/login");
throw new Error("Unauthorized");
}
return res;
}
/** 로그아웃 */
function logout() {
localStorage.removeItem("guardia_token");
localStorage.removeItem("guardia_userinfo");
window.location.replace("/login");
}
/* ─── State ─────────────────────────────────────── */
let currentView = "dashboard";
let srCache = [];
let statsCache = {};
let workloadCache = [];
let dashCache = {}; // 역할별 대시보드 데이터
/* ─── Status labels ─────────────────────────────── */
const STATUS_LABEL = {
RECEIVED: "접수",
PARSED: "파싱 완료",
PENDING_APPROVAL: "승인 대기",
APPROVED: "승인됨",
IN_PROGRESS: "진행 중",
PENDING_PM_VALIDATION: "PM 검증 대기",
COMPLETED: "완료",
FAILED_ROLLBACK: "롤백 실패",
REJECTED: "반려",
};
const TYPE_LABEL = {
DEPLOY: "배포", RESTART: "재기동", LOG: "로그",
INQUIRY: "문의", OTHER: "기타",
};
const PRIORITY_LABEL = { CRITICAL: "긴급", HIGH: "높음", MEDIUM: "보통", LOW: "낮음" };
const KANBAN_COLS = [
{ key: "RECEIVED", label: "접수" },
{ key: "PENDING_APPROVAL", label: "승인 대기" },
{ key: "APPROVED", label: "승인됨" },
{ key: "IN_PROGRESS", label: "진행 중" },
{ key: "PENDING_PM_VALIDATION", label: "PM 검증" },
{ key: "COMPLETED", label: "완료" },
{ key: "FAILED_ROLLBACK", label: "롤백 실패" },
{ key: "REJECTED", label: "반려" },
];
const STATUS_COLORS = {
RECEIVED: "#8b949e",
PARSED: "#79c0ff",
PENDING_APPROVAL: "#e3b341",
APPROVED: "#56d364",
IN_PROGRESS: "#58a6ff",
PENDING_PM_VALIDATION: "#bc8cff",
COMPLETED: "#3fb950",
FAILED_ROLLBACK: "#f85149",
REJECTED: "#da3633",
};
/* ─── Init ──────────────────────────────────────── */
window.addEventListener("DOMContentLoaded", async () => {
// 사용자 정보 표시
const roleLabel = { ADMIN:"관리자", ENGINEER:"엔지니어", PM:"PM", CUSTOMER:"고객" };
const roleColor = {
ADMIN:"#818cf8", ENGINEER:"#34d399", PM:"#fbbf24", CUSTOMER:"#a78bfa"
};
const role = _userInfo.role || "";
document.getElementById("user-display-name").textContent =
_userInfo.display_name || _userInfo.username || "사용자";
const roleBadge = document.getElementById("user-role-badge");
roleBadge.textContent = roleLabel[role] || role;
roleBadge.style.background = (roleColor[role] || "#64748b") + "22";
roleBadge.style.color = roleColor[role] || "#64748b";
// 테마 버튼 active 상태 동기화
applyTheme(document.body.dataset.theme || "dark");
setupNav();
setupNewSR();
setupListFilters();
initChat();
await loadAll();
initSSE(); // 실시간 이벤트 연결
startPoll(); // 30초 폴백 폴링
});
async function loadAll() {
await Promise.all([loadDashboardMe(), loadSRs(), loadWorkload()]);
renderCurrentView();
}
/*
SSE 실시간 연결
*/
let _sseSource = null;
let _sseRetry = 0;
let _refreshPending = false;
let _pollTimer = null;
function initSSE() {
if (!_token) return;
if (_sseSource) { _sseSource.close(); _sseSource = null; }
const url = `/api/dashboard/events?token=${encodeURIComponent(_token)}`;
_sseSource = new EventSource(url);
_sseSource.onopen = () => {
_sseRetry = 0;
setSseDot(true);
};
_sseSource.onmessage = (e) => {
if (!e.data || e.data.trim() === "") return;

View File

@ -0,0 +1,31 @@
# GUARDiA ITSM v2.0 — 저작권 등록용 소스코드
> **제출처:** 한국저작권위원회
> **저작물명:** GUARDiA ITSM (가이더)
> **저작자:** (주)지오정보기술
> **버전:** 2.0.0
---
## 소스코드 제출 안내
저작권 등록 시 소스코드 전체를 제출할 필요는 없습니다.
핵심 기능을 보여주는 일부 소스코드(200줄 이상)를 제출합니다.
**제출 파일 목록:**
1. `01_core_ssh_agentless.py` — 에이전트리스 SSH 핵심 로직
2. `02_core_license_engine.py` — 라이선스 엔진 (AES-256-GCM)
3. `03_router_sr_management.py` — SR 서비스 요청 관리 API
4. `04_core_ai_classifier.py` — AI 티켓 자동 분류
5. `05_frontend_dashboard.jsx` — 대시보드 React 컴포넌트
> ※ 영업비밀에 해당하는 암호화 키 값은 XXXXXXXXXX로 마스킹 처리되었습니다.
---
## 독창성 설명
1. **에이전트리스 자동화**: 대상 서버에 소프트웨어 설치 없이 SSH/SFTP만으로 레거시 WAS를 자동화 (국내 최초)
2. **온프레미스 AI ChatOps**: 외부 클라우드 의존 없이 내부 Ollama LLM으로 자연어 명령을 처리
3. **해시체인 감사로그**: SHA-256 불변 체인으로 로그 위변조를 실시간 탐지
4. **멀티 플랫폼 메신저 통합**: 카카오워크/네이버웍스/슬랙 등에서 동일한 25개 명령어로 인프라 제어

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 B

View File

@ -1,37 +0,0 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
<title>도정현황통계시스템</title>
<style type="text/css">
body, table, td {
font-family: Gulim, "굴림", sans-serif;*/
margin: 0;
padding: 0;
font-size: 9pt;
color: #666666;
}
#txt001 {
padding-top:5px;
}
</style>
<link rel="stylesheet" type="text/css" href="../css/common.css">
</head>
<body>
<table width="100%" height="400">
<tr>
<td valign="center" align="center">
<table width="420" border="0" cellspacing="0" cellpadding="0">
<tr>
<td width="138"><img src="/jbss/images/common/error_box01.gif"></td>
<td background="/jbss/images/common/error_box02.gif" style="padding:35 0 0 0">페이지를 표시할
수 없습니다.<br>
서버를 찾을 수 없거나 DNS 오류입니다<br></td>
<td width="12"><img src="/jbss/images/common/error_box03.gif"></td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -1,66 +0,0 @@
<%@ page language="java" pageEncoding="EUC-KR"%>
<%@ page isErrorPage = "true"%>
<%
response.setStatus(HttpServletResponse.SC_OK);
String contextPath = request.getContextPath();
%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-tiles" prefix="tiles" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-template" prefix="template" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-nested" prefix="nested" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html:html locale="true">
<head>
<html:base />
<title>도정현황통계시스템</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<style type="text/css">
body, table, td {
font-family: Gulim, "굴림", sans-serif;*/
margin: 0;
padding: 0;
font-size: 9pt;
color: #666666;
}
#txt001 {
padding-top:5px;
}
</style>
<link rel="stylesheet" type="text/css" href="../css/common.css">
</head>
<body>
<table width="100%" height="400">
<tr>
<td valign="center" align="center">
<table width="420" border="0" cellspacing="0" cellpadding="0">
<tr>
<td width="138"><img src="<%=contextPath %>/images/common/error_box01.gif"></td>
<td background="<%=contextPath %>/images/common/error_box02.gif" style="padding:35 0 0 0">
<% if (exception.getMessage().equals("NotAuthorized")) { %>
접근 권한이 없습니다. (IP : <%=request.getLocalAddr()%>)<br/>
<% } else {%>
이용에 불편을 드려 죄송합니다.<br/>
빠른시간내에 문제를 해결하도록 하겠습니다.
<% exception.printStackTrace(); } %>
</td>
<td width="12"><img src="<%=contextPath %>/images/common/error_box03.gif"></td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html:html>

View File

@ -1,17 +0,0 @@
function FlashImp(fileUrl,sWidth,sHeight){
var sTag;
sTag="<object classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' codebase='http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,19,0' width='"+sWidth+"' height='"+sHeight+"' VIEWASTEXT>"
document.write(sTag);
sTag= "<param name='movie' value='"+fileUrl+"' />";
document.write(sTag);
document.write('<param name="quality" value="high">');
document.write('<param name="wmode" value="transparent">');
sTag= "<embed src='"+fileUrl+"' quality='high' pluginspage='http://www.macromedia.com/go/getflashplayer' type='application/x-shockwave-flash' width='"+sWidth+"' height='"+sHeight+"'></embed>"
document.write('');
document.write('</object>');
}

View File

@ -1,549 +0,0 @@
<public:component URN="lyrSelectBox" />
<public:attach event="ondocumentready" handler="initializeSelectBox" />
<public:attach event="onpropertychange" handler="eventChangeProperty" />
<public:attach event="onmousedown" for="document" handler="eventMouseDown" />
<public:attach event="onkeydown" for="document" handler="eventKeyDown" />
<public:property name="setColor" put="setupColor" />
<public:property name="setImage" put="setupImage" />
<public:property name="setDisplayCount" put="setupDisplayCount" />
<public:method name="reInitializeSelectBox" />
<script language="JScript">
///////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 스크립트명 - SelectBox -> 드롭다운 메뉴 변환 HTC
// 설 명 - SelectBox 폼필드를 레이어 형태의 드롭다운 메뉴로 자동 변경
// 제 작 자 - TarauS (taraus@naver.com)
// 메 신 져 - MSN Messenger -> taraus@hanmail.net
//
// * 스크립트 목적
// - 기존의 셀렉트박스를 스타일의 적용이 가능한 레이어 형태(실제로는 테이블과 Popup Object)로 자동 변환
//
// * 주요 기능 및 특징
// - 기존 셀렉트박스 태그의 수정 없이 스타일 시트에 정의하는 것만으로 모든 셀렉트박스 변환 가능
// - 셀렉트박스를 기준으로 아래위의 여백을 비교하여 옵션 항목 창의 출력 방향 결정
// - 기존 셀렉트박스처럼 변환된 셀렉트박스도 포커스를 가질 수 있음
// document.getElementById('SelectBox_Name').focus();
// - 변환된 셀렉트박스가 포커스를 가지고 있을 경우 휠을 움직이거나 키보드의 Home, End, Page Up, Page Down,
// Up Arrow, Down Arrow 등을 누름에 따라 값의 변경이 가능
// 또한 열려진 옵션 항목 창에서도 가능함
// - 위의 이벤트 시에 문서의 스크롤을 제어하여 문서의 움직임이 없음
// - 아이프레임 및 프레임에 삽입된 상황에서도 프레임에 영향을 받지 않고 정상적으로 출력
// (Layer가 아닌 Popup Object를 이용)
// - 셀렉트박스의 항목이 동적으로 변경할 경우를 위한 메소드 제공
// document.getElementById("SelectBox_Name").reInitializeSelectBox();
// - 옵션 항목 창에 출력될 항목의 갯수를 지정(setDisplayCount() 메소드 이용)할 수 있으며 항목이 출력될
// 갯수보다 많을 경우 자동으로 스크롤바 생성 (기본값은 10)
// - 셀렉트박스 및 옵션 항목에 대해 툴팁 메세지 설정 가능
// - 특정 셀렉트박스의 색상 및 화살표 이미지 변경 가능
// - 변환된 레이어를 텍스트처럼 취급 (연속적인 출력이 가능, 하단 여백 없음)
// - HTC 가 지원되는 브라우져에서만 변환 (HTC는 5.0 이상에서 가능하나 createPopup() 메소드가 5.5부터
// 지원되어 IE 5.5 이상에서만 변환)
// - 옵션 항목 창 출력시 일시적으로 문서가 길어져 스크롤바가 출력되는 일이 없음
// - 셀렉트박스가 disabled 상태일 경우 처리
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// 변수 선언
var objSelectBox = this;
var widthObject, widthObjectOriginal, heightObject;
var tblTitle, tbdTitle, trTitle, tdTitle;
var objItemWindow, objItemDocument, objItemBody, objItemEvent;
var tblItem, tbdItem, trItem, tdItem;
var leftObject, heightItemWindow, heightTitleTable;
var countMaxItem = 10;
var countItem = this.length;
var is_open = false;
var is_loaded = false;
var focusElement;
// 기본 색상 및 글자 설정
var normal_bgcolor = "#FFFFFF";
var normal_color = "#000000";
var disabled_color = "#C0C0C0";
var active_bgcolor = "#225588";
var active_color = "#FFFFFF";
var normal_border_tag = "1 solid #C0C0C0";
var active_border_tag = "1 solid #225588";
var font_tag = "normal 12px 굴림";
var arrow_image = "/bbs/images/arrow_image2.gif";
//-------- 프로퍼티 설정 함수
// 프로퍼티로 색상을 설정시에 색상 관련 변수 변경
function setupColor(color_list){
var color_array = color_list.split(",");
var color = new Array();
for(i=0; i<color_array.length; i++){
color[i] = color_array[i];
}
if(color[0]) normal_color = color[0];
if(color[1]) normal_bgcolor = color[1];
if(color[2]) active_color = color[2];
if(color[3]) active_bgcolor = color[3];
if(color[4]) normal_border_tag = "1 solid "+color[4];
if(color[5]) active_border_tag = "1 solid "+color[5];
}
// 프로퍼티로 화살표 이미지를 설정시에 화살표 이미지를 변경
function setupImage(image_file){
if(image_file) arrow_image = image_file;
}
// 옵션 항목의 최대 출력 갯수 변경
function setupDisplayCount(max_count){
if(max_count) countMaxItem = max_count;
}
//-------- 문서 스크롤 관련 함수
// 문서의 스크롤링을 불가능하도록 설정
function disableScroll(){
window.execScript("document.onmousewheel = function(){return false;}");
window.execScript("document.onkeydown = function(){return false;}");
}
// 문서의 스크롤링을 가능하도록 설정
function enableScroll(){
window.execScript("document.onmousewheel = function(){return true;}");
window.execScript("document.onkeydown = function(){return true;}");
}
//-------- 이벤트 관련 함수
// 타이틀 출력 테이블 Mouse Over Event
function eventMouseOverTT(){
if(!objSelectBox.disabled){
tblTitle.style.border = active_border_tag;
imgArrow.style.filter = '';
}
}
// 타이틀 출력 테이블 Mouse Out Event
function eventMouseOutTT(){
tblTitle.style.border = normal_border_tag;
imgArrow.style.filter = 'gray()';
}
// 아이템 출력 테이블 Mouse Over Event
function eventMouseOverIT(idx){
removeItemStyle();
tdItem[idx].style.color = active_color;
tdItem[idx].style.background = active_bgcolor;
focusElement = tdItem[idx];
}
// onMouseDown Event
function eventMouseDown(){
if(is_open) changeItemWindowDisplay();
}
// 셀렉트박스 Focus Event
function eventFocusSB(){
tdTitle_sv.innerHTML = objSelectBox.options[selectedIndex].text;
tdTitle_sv.style.color = active_color;
tdTitle_sv.style.background = active_bgcolor;
}
// 셀렉트박스 Blur Event
function eventBlurSB(){
tdTitle_sv.style.color = normal_color;
tdTitle_sv.style.background = normal_bgcolor;
}
// 셀렉트박스 Key Down Event
function eventKeyDownSB(){
var keycode = window.event.keyCode ? window.event.keyCode : window.event.which ? window.event.which : window.event.charCode;
if(is_open && focusElement && keycode == 13){
nowIndex = focusElement.getAttribute("key");
changeSelectBoxValue(nowIndex);
}
}
// onKeyDown 이벤트 처리
function eventKeyDown(){
var keycode = window.event.keyCode ? window.event.keyCode : window.event.which ? window.event.which : window.event.charCode;
var eventElement = window.event.srcElement
// 셀렉트박스가 포커스를 가지고 있을 때 스페이스바를 이용하여 옵션 항목 창을 보이고 사라지게 하는 부분
// 현재는 옵션 항목 창이 열렸을 때의 이벤트 객체 문제와 옵션 항목 창 출력 방향 문제로 임시로 주석 처리함
//if(keycode == 32 && eventElement.type == "select-one" && eventElement.name == this.name){
// changeItemWindowDisplay();
//}
if(is_open && focusElement){
var firstIndex = 0;
var lastIndex = countItem - 1;
var nowIndex = objSelectBox.selectedIndex;
var tmpIndex = 0;
var change_value_check = false;
nowIndex = focusElement.getAttribute("key");
if(window.event.altKey) closeItemWindow();
if(keycode == 38){ // 위쪽 방향키를 눌렀을 때
tmpIndex = nowIndex - 1;
if(tmpIndex < firstIndex) tmpIndex = firstIndex;
selectIndex = tmpIndex;
change_value_check = true;
}else if(keycode == 40){ // 아래쪽 방향키를 눌렀을 때
tmpIndex = nowIndex + 1;
if(tmpIndex > lastIndex) tmpIndex = lastIndex;
selectIndex = tmpIndex;
change_value_check = true;
}else if(keycode == 33){ // Page Up 키를 눌렀을 때
tmpIndex = nowIndex - countMaxItem - 1;
if(tmpIndex < firstIndex) tmpIndex = firstIndex;
selectIndex = tmpIndex;
change_value_check = true;
}else if(keycode == 34){ // Page Down키를 눌렀을 때
tmpIndex = nowIndex + countMaxItem - 1;
if(tmpIndex > lastIndex) tmpIndex = lastIndex;
selectIndex = tmpIndex;
change_value_check = true;
}else if(keycode == 36){ // Home 키를 눌렀을 때
selectIndex = firstIndex;
change_value_check = true;
}else if(keycode == 35){ // End 키를 눌렀을 때
selectIndex = lastIndex;
change_value_check = true;
}else if(keycode == 13){
if(focusElement != null) changeSelectBoxValue(nowIndex);
}
if(change_value_check){
removeItemStyle();
tdItem[selectIndex].style.color = active_color;
tdItem[selectIndex].style.background = active_bgcolor;
objItemBody.scrollTop = selectIndex * 20;
focusElement = tdItem[selectIndex];
}
}
}
// onMouseWheel 이벤트 처리 (현재 미작동)
// onMouseWheel 이벤트가 IE 6.0부터 지원되는 점과 팝업창의 이벤트 객체 제어 문제로 기능 삭제
function eventMouseWheel(){
if(is_open){
if(!focusElement) idx = objSelectBox.selectedIndex;
else idx = focusElement.key;
for(i=0; i<window.event.wheelDelta; i+=120) idx--;
for(i=0; i>window.event.wheelDelta; i-=120) idx++;
idx = Math.max(idx, 0);
idx = Math.min(idx, countItem - 1);
removeItemStyle();
tdItem[idx].style.color = active_color;
tdItem[idx].style.background = active_bgcolor;
objItemBody.scrollTop = idx * 20;
focusElement = tdItem[idx];
}
}
// SelectBox Change Property Event
function eventChangeProperty(){
if(window.event.type == "propertychange" && window.event.propertyName == "selectedIndex"){
tdTitle_sv.innerHTML = objSelectBox.options[objSelectBox.selectedIndex].text;
if(objSelectBox.onchange != null) objSelectBox.onchange();
}
}
//-------- 옵션 항목 창 관련 함수
// 옵션 항목 창의 출력 상태에 따라 보여주거나 닫음
function changeItemWindowDisplay(){
if(is_open == false){
if(countItem && !objSelectBox.disabled) openItemWindow()
}else{
closeItemWindow();
}
}
// 옵션 항목 창 열기
function openItemWindow(){
eventBlurSB();
heightScreen = window.screen.height;
spaceDown = heightScreen - window.event.screenY - heightTitleTable;
spaceUp = heightScreen - spaceDown;
// Item Window를 위로 출력
if((objSelectBox.length <= countMaxItem && (objSelectBox.length * 20 + 4) > spaceDown) || (objSelectBox.length > countMaxItem && spaceDown < (countMaxItem * 20 + 4))){
objItemWindow.show(0,(0-heightItemWindow),widthObject,heightItemWindow,tblTitle);
// Item Window를 아래로 출력
}else{
objItemWindow.show(0,heightTitleTable,widthObject,heightItemWindow,tblTitle);
}
var idx = objSelectBox.selectedIndex;
tdItem[idx].style.color = active_color;
tdItem[idx].style.background = active_bgcolor;
objItemBody.scrollTop = idx * 20;
focusElement = tdItem[idx];
disableScroll();
is_open = true;
}
// 옵션 항목 창 닫기
function closeItemWindow(){
objItemWindow.hide();
removeItemStyle();
enableScroll();
objSelectBox.focus();
is_open = false;
}
//-------- 환경 설정 및 변환 관련 함수
// 초기화 함수
function initializeSelectBox(){
var browser_version = new Number(((window.navigator.appVersion.split('; '))[1].split(' '))[1]);
if(this.type != "select-one" || this.size != 0){
return;
}else if(navigator.appName != "Microsoft Internet Explorer" || browser_version < 5.5){
return;
}else{
objSelectBox.attachEvent("onfocus",eventFocusSB);
objSelectBox.attachEvent("onblur",eventBlurSB);
initializeBasicInformation();
initializeTitleTable();
if(countItem){
initializeItemWindow();
initializeItemTable();
}
is_loaded = true;
}
}
// 동적으로 셀렉트박스의 항목이 변경될 경우 다시 변환할 수 있는 메소드
function reInitializeSelectBox(){
countItem = objSelectBox.length;
tblTitle.removeNode(true);
initializeBasicInformation();
initializeTitleTable();
initializeItemWindow();
initializeItemTable();
}
// 셀렉트 박스를 레이어 형태로 변형하기 위한 기본 정보 설정
function initializeBasicInformation(){
// style.width 속성을 사용자가 설정한 경우 사용자 설정값으로 넓이를 설정 함
// this.offsetWidth 속성을 가끔 불러오지 못하는 경우가 있어 이럴 경우 이전 버전에서
// 사용하던 getStringPixelWidth()함수를 이용하여 넓이를 직접 계산 후 설정 함
if(is_loaded == false && objSelectBox.style.width){
widthObject = parseInt(objSelectBox.style.width);
widthObjectOriginal = widthObject;
}else if(is_loaded == false && objSelectBox.offsetWidth){
widthObject = objSelectBox.offsetWidth;
}else{
var lengthMax = 0;
if(countItem){
for(i=0; i<countItem; i++){
lengthItem = getStringLength(objSelectBox.options[i].text);
if(lengthMax < lengthItem){
lengthMax = lengthItem;
stringMax = objSelectBox.options[i].text;
}
}
widthObject = getStringPixelWidth(stringMax) + 12 + 12 + 2;
}
}
objSelectBox.style.width = "0px";
heightTitleTable = 22;
if(countItem < countMaxItem){
heightItemWindow = countItem * 20 + 2 + 2;
widthItemTable = widthObject - 2;
}else{
heightItemWindow = countMaxItem * 20 + 2 + 2;
widthItemTable = widthObject - 18;
}
heightObject = heightTitleTable + heightItemWindow;
}
// 타이틀 테이블 설정
function initializeTitleTable(){
// 옵션 항목이 있을 경우 선택된 값을 변환된 셀렉트박스의 기본값으로 설정
if(countItem){
if(!objSelectBox.selectedIndex) objSelectBox.selectedIndex = 0;
var textDefault = objSelectBox.options[objSelectBox.selectedIndex].text;
// 옵션 항목이 없을 경우 기본 사이즈를 60px로 설정
}else{
var textDefault = "";
widthObject=60;
}
var tooltip = objSelectBox.tooltip;
// 타이틀 테이블 생성 및 셋팅
tblTitle = document.createElement("TABLE");
tblTitle.border = 0;
tblTitle.cellSpacing = 2;
tblTitle.cellPadding = 0;
tblTitle.style.width = widthObject;
tblTitle.style.height = heightTitleTable;
tblTitle.style.color = normal_color;
tblTitle.style.background = normal_bgcolor;
tblTitle.style.border = normal_border_tag;
tblTitle.style.display = "inline";
tblTitle.style.verticalAlign = "bottom";
tblTitle.onmouseover = eventMouseOverTT;
tblTitle.onmouseout = eventMouseOutTT;
if(tooltip != null) tblTitle.title = tooltip;
tbdTitle = document.createElement("TBODY");
tblTitle.appendChild(tbdTitle);
trTitle = document.createElement("TR");
trTitle.onclick = changeItemWindowDisplay;
tdTitle_sv = document.createElement("TD");
tdTitle_sv.innerHTML = textDefault;
tdTitle_sv.width = widthObject - 14 - 4 - 4;
tdTitle_sv.valign = "absmiddle";
tdTitle_sv.onselectstart = function(){return false;};
tdTitle_sv.style.font = font_tag;
if(!objSelectBox.disabled) tdTitle_sv.style.color = normal_color;
else tdTitle_sv.style.color = disabled_color;
tdTitle_sv.style.cursor = "default";
tdTitle_sv.style.background = normal_bgcolor;
tdTitle_sv.style.verticalAlign = "text-bottom";
trTitle.appendChild(tdTitle_sv);
tdTitle = document.createElement("TD");
tdTitle.width = "14";
tdTitle.align = "center";
tdTitle.onselectstart = function(){return false;};
imgArrow = document.createElement("IMG");
imgArrow.src = arrow_image;
imgArrow.valign = "bottom";
imgArrow.style.filter = "gray()";
tdTitle.appendChild(imgArrow);
trTitle.appendChild(tdTitle);
tbdTitle.appendChild(trTitle);
insertAdjacentElement("afterEnd",tblTitle);
}
// 옵션 항목 창을 설정
function initializeItemWindow(){
objItemWindow = createPopup();
objItemDocument = objItemWindow.document;
objItemBody = objItemDocument.body;
with(objItemBody.style){
border = normal_border_tag;
overflowY = "auto";
scrollbarFaceColor = "#c8c8c8";
scrollbarShadowColor = "#ffffff";
scrollbarHighlightColor = "#ffffff";
scrollbar3dlightColor = "#ffffff";
scrollbarDarkShadowColor = "#ffffff";
scrollbarTrackColor = "#F0F0F0";
scrollbarArrowColor = "#F0F0F0";
buttonTextColor = "#ffffff";
}
}
// 옵션 항목 테이블 설정
function initializeItemTable(){
// 아이템 출력 테이블 생성 및 셋팅
tblItem = objItemDocument.createElement("TABLE");
tblItem.cellSpacing = 2;
tblItem.cellPadding = 2;
tblItem.style.width = widthItemTable;
tblItem.style.color = normal_color;
tblItem.style.background = normal_bgcolor;
tbdItem = objItemDocument.createElement("TBODY");
tblItem.appendChild(tbdItem);
trItem = new Array();
tdItem = new Array();
for(i=0; i<objSelectBox.length; i++){
textSelectBox = objSelectBox.options[i].text;
valueSelectBox = objSelectBox.options[i].value;
var tooltip = objSelectBox.options[i].tooltip;
trItem[i] = objItemDocument.createElement("TR");
tdItem[i] = objItemDocument.createElement("TD");
if(tooltip != null) tdItem[i].title = tooltip;
tdItem[i].innerHTML = " " + textSelectBox;
tdItem[i].setAttribute("key",i);
tdItem[i].height = "18";
tdItem[i].vAlign = "bottom";
tdItem[i].onmouseover = function(){eventMouseOverIT(this.getAttribute("key"))}
tdItem[i].onclick = function(){changeSelectBoxValue(this.getAttribute("key"))}
tdItem[i].onselectstart = function(){return false;};
tdItem[i].style.font = font_tag;
tdItem[i].style.color = normal_color;
tdItem[i].style.background = normal_bgcolor;
tdItem[i].style.cursor = "default";
tdItem[i].style.verticalAlign = "bottom";
trItem[i].appendChild(tdItem[i]);
tbdItem.appendChild(trItem[i]);
}
objItemBody.insertAdjacentElement("beforeEnd",tblItem);
}
//-------- 기타 함수
// 옵션 항목에 적용된 스타일 초기화
function removeItemStyle(){
for(i=0; i<countItem; i++){
tdItem[i].style.color = normal_color;
tdItem[i].style.background = normal_bgcolor;
}
}
// 셀렉트박스의 값을 변경
function changeSelectBoxValue(idx){
objSelectBox.selectedIndex = idx;
tdTitle_sv.innerHTML = objSelectBox.options[idx].text;
closeItemWindow()
}
// 문자열의 길이를 계산
function getStringLength(string){
var i, j=0;
for(i=0;i<string.length;i++) {
lengthString = escape(string.charAt(i)).length;
if(lengthString > 4) j++;
j++;
}
return j;
}
// 문자열의 넓이를 픽셀 단위로 계산
function getStringPixelWidth(string_value){
var ascii_code;
var string_value_length = string_value.length;
var character;
var character_width;
var character_length;
var total_width = 0;
var total_length = 0;
var special_char_size = 6;
var multibyte_char_size = 12;
var base_char_start = 32;
var base_char_end = 127;
var ascii_char_size = Array(4,4,4,6,6,10,8,4,5,5,6,6,4,6,4,6,6,6,6,6,6,6,6,6,6,6,4,4,8,6,8,6,12,8,8,9,8,8,7,9,8,3,6,8,7,11,9,9,8,9,8,8,8,8,8,10,8,8,8,6,11,6,6,6,4,7,7,7,7,7,3,7,7,3,3,6,3,11,7,7,7,7,4,7,3,7,6,10,7,7,7,6,6,6,9,6);
for(i=0; i<string_value_length; i++){
character = string_value.substring(i,(i+1));
ascii_code = character.charCodeAt(0);
if(ascii_code < base_char_start){
character_width = special_char_size;
}else if(ascii_code <= base_char_end){
idx = ascii_code - base_char_start;
character_width = ascii_char_size[idx];
}else if(ascii_code > base_char_end){
character_width = multibyte_char_size;
}
total_width += character_width;
}
return total_width;
}
</script>
</public:component>

View File

@ -1,292 +0,0 @@
//v1.7
// Flash Player Version Detection
// Detect Client Browser type
// Copyright 2005-2007 Adobe Systems Incorporated. All rights reserved.
var isIE = (navigator.appVersion.indexOf("MSIE") != -1) ? true : false;
var isWin = (navigator.appVersion.toLowerCase().indexOf("win") != -1) ? true : false;
var isOpera = (navigator.userAgent.indexOf("Opera") != -1) ? true : false;
function ControlVersion()
{
var version;
var axo;
var e;
// NOTE : new ActiveXObject(strFoo) throws an exception if strFoo isn't in the registry
try {
// version will be set for 7.X or greater players
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
version = axo.GetVariable("$version");
} catch (e) {
}
if (!version)
{
try {
// version will be set for 6.X players only
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");
// installed player is some revision of 6.0
// GetVariable("$version") crashes for versions 6.0.22 through 6.0.29,
// so we have to be careful.
// default to the first public version
version = "WIN 6,0,21,0";
// throws if AllowScripAccess does not exist (introduced in 6.0r47)
axo.AllowScriptAccess = "always";
// safe to call for 6.0r47 or greater
version = axo.GetVariable("$version");
} catch (e) {
}
}
if (!version)
{
try {
// version will be set for 4.X or 5.X player
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.3");
version = axo.GetVariable("$version");
} catch (e) {
}
}
if (!version)
{
try {
// version will be set for 3.X player
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.3");
version = "WIN 3,0,18,0";
} catch (e) {
}
}
if (!version)
{
try {
// version will be set for 2.X player
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
version = "WIN 2,0,0,11";
} catch (e) {
version = -1;
}
}
return version;
}
// JavaScript helper required to detect Flash Player PlugIn version information
function GetSwfVer(){
// NS/Opera version >= 3 check for Flash plugin in plugin array
var flashVer = -1;
if (navigator.plugins != null && navigator.plugins.length > 0) {
if (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]) {
var swVer2 = navigator.plugins["Shockwave Flash 2.0"] ? " 2.0" : "";
var flashDescription = navigator.plugins["Shockwave Flash" + swVer2].description;
var descArray = flashDescription.split(" ");
var tempArrayMajor = descArray[2].split(".");
var versionMajor = tempArrayMajor[0];
var versionMinor = tempArrayMajor[1];
var versionRevision = descArray[3];
if (versionRevision == "") {
versionRevision = descArray[4];
}
if (versionRevision[0] == "d") {
versionRevision = versionRevision.substring(1);
} else if (versionRevision[0] == "r") {
versionRevision = versionRevision.substring(1);
if (versionRevision.indexOf("d") > 0) {
versionRevision = versionRevision.substring(0, versionRevision.indexOf("d"));
}
}
var flashVer = versionMajor + "." + versionMinor + "." + versionRevision;
}
}
// MSN/WebTV 2.6 supports Flash 4
else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.6") != -1) flashVer = 4;
// WebTV 2.5 supports Flash 3
else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.5") != -1) flashVer = 3;
// older WebTV supports Flash 2
else if (navigator.userAgent.toLowerCase().indexOf("webtv") != -1) flashVer = 2;
else if ( isIE && isWin && !isOpera ) {
flashVer = ControlVersion();
}
return flashVer;
}
// When called with reqMajorVer, reqMinorVer, reqRevision returns true if that version or greater is available
function DetectFlashVer(reqMajorVer, reqMinorVer, reqRevision)
{
versionStr = GetSwfVer();
if (versionStr == -1 ) {
return false;
} else if (versionStr != 0) {
if(isIE && isWin && !isOpera) {
// Given "WIN 2,0,0,11"
tempArray = versionStr.split(" "); // ["WIN", "2,0,0,11"]
tempString = tempArray[1]; // "2,0,0,11"
versionArray = tempString.split(","); // ['2', '0', '0', '11']
} else {
versionArray = versionStr.split(".");
}
var versionMajor = versionArray[0];
var versionMinor = versionArray[1];
var versionRevision = versionArray[2];
// is the major.revision >= requested major.revision AND the minor version >= requested minor
if (versionMajor > parseFloat(reqMajorVer)) {
return true;
} else if (versionMajor == parseFloat(reqMajorVer)) {
if (versionMinor > parseFloat(reqMinorVer))
return true;
else if (versionMinor == parseFloat(reqMinorVer)) {
if (versionRevision >= parseFloat(reqRevision))
return true;
}
}
return false;
}
}
function AC_AddExtension(src, ext)
{
if (src.indexOf('?') != -1)
return src.replace(/\?/, ext+'?');
else
return src + ext;
}
function AC_Generateobj(objAttrs, params, embedAttrs)
{
var str = '';
if (isIE && isWin && !isOpera)
{
str += '<object ';
for (var i in objAttrs)
{
str += i + '="' + objAttrs[i] + '" ';
}
str += '>';
for (var i in params)
{
str += '<param name="' + i + '" value="' + params[i] + '" /> ';
}
str += '</object>';
}
else
{
str += '<embed ';
for (var i in embedAttrs)
{
str += i + '="' + embedAttrs[i] + '" ';
}
str += '> </embed>';
}
document.write(str);
}
function AC_FL_RunContent(){
var ret =
AC_GetArgs
( arguments, ".swf", "movie", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
, "application/x-shockwave-flash"
);
AC_Generateobj(ret.objAttrs, ret.params, ret.embedAttrs);
}
function AC_SW_RunContent(){
var ret =
AC_GetArgs
( arguments, ".dcr", "src", "clsid:166B1BCA-3F9C-11CF-8075-444553540000"
, null
);
AC_Generateobj(ret.objAttrs, ret.params, ret.embedAttrs);
}
function AC_GetArgs(args, ext, srcParamName, classid, mimeType){
var ret = new Object();
ret.embedAttrs = new Object();
ret.params = new Object();
ret.objAttrs = new Object();
for (var i=0; i < args.length; i=i+2){
var currArg = args[i].toLowerCase();
switch (currArg){
case "classid":
break;
case "pluginspage":
ret.embedAttrs[args[i]] = args[i+1];
break;
case "src":
case "movie":
args[i+1] = AC_AddExtension(args[i+1], ext);
ret.embedAttrs["src"] = args[i+1];
ret.params[srcParamName] = args[i+1];
break;
case "onafterupdate":
case "onbeforeupdate":
case "onblur":
case "oncellchange":
case "onclick":
case "ondblClick":
case "ondrag":
case "ondragend":
case "ondragenter":
case "ondragleave":
case "ondragover":
case "ondrop":
case "onfinish":
case "onfocus":
case "onhelp":
case "onmousedown":
case "onmouseup":
case "onmouseover":
case "onmousemove":
case "onmouseout":
case "onkeypress":
case "onkeydown":
case "onkeyup":
case "onload":
case "onlosecapture":
case "onpropertychange":
case "onreadystatechange":
case "onrowsdelete":
case "onrowenter":
case "onrowexit":
case "onrowsinserted":
case "onstart":
case "onscroll":
case "onbeforeeditfocus":
case "onactivate":
case "onbeforedeactivate":
case "ondeactivate":
case "type":
case "codebase":
case "id":
ret.objAttrs[args[i]] = args[i+1];
break;
case "width":
case "height":
case "align":
case "vspace":
case "hspace":
case "class":
case "title":
case "accesskey":
case "name":
case "tabindex":
ret.embedAttrs[args[i]] = ret.objAttrs[args[i]] = args[i+1];
break;
default:
ret.embedAttrs[args[i]] = ret.params[args[i]] = args[i+1];
}
}
ret.objAttrs["classid"] = classid;
if (mimeType) ret.embedAttrs["type"] = mimeType;
return ret;
}

View File

@ -1,357 +0,0 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
<meta name="author" content="(주)아사달">
<meta name="description" content="아사달 템플릿">
<meta name="keywords" content="아사달 템플릿,비즈니스">
<title>아사달 템플릿</title>
<link rel="stylesheet" href="../link.css">
<script src="Scripts/AC_RunActiveContent.js" type="text/javascript"></script>
<style type="text/css">
<!--
body {
background-color: #FFFFFF;
}
-->
</style><body topmargin="0" leftmargin="0" marginheight="0" marginwidth="0">
<!-- 메인 이미지 & 로고 & 탑메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="950"><img src="image/sub_img.jpg" border="0" usemap="#sub_img"></td>
<td background="image/sub_img_bg.gif">&nbsp;</td></tr>
</table>
<!--맵링크-->
<map name="sub_img">
<area shape="rect" coords="37,18,182,69" href="../index.htm">
<area shape="rect" coords="733,4,772,20" href="#">
<area shape="rect" coords="776,4,812,20" href="#">
<area shape="rect" coords="815,4,864,20" href="#">
<area shape="rect" coords="869,4,933,20" href="#">
</map>
<!--/맵링크-->
<!-- /메인 이미지 & 로고 & 탑메뉴 -->
<!-- 메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="250" background="../image/menu_bg.gif">&nbsp;</td>
<td width="700">
<table border="0" cellpadding="0" cellspacing="0" width="700">
<tr>
<td><script type="text/javascript">
AC_FL_RunContent( 'codebase','http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0','width','700','height','55','src','../swf/index01','quality','high','pluginspage','http://www.macromedia.com/go/getflashplayer','wmode','transparent','movie','../swf/index01?mNum=4&sNum=2' ); //end AC code
</script><noscript><object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0" width="700" height="55">
<param name="wmode" value="transparent">
<param name="movie" value="../swf/index01.swf?mNum=4&sNum=2">
<param name="quality" value="high">
<embed src="../swf/index01.swf" quality="high" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" width="700" height="55"></embed></object></noscript></td></tr>
</table>
</td>
<td background="../image/menu_bg.gif">&nbsp;</td></tr>
</table>
<!-- /메뉴 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="20"></td></tr>
</table>
<!-- /여백 -->
<!-- 내용 -->
<table border="0" cellpadding="0" cellspacing="0" width="950">
<tr>
<td width="250" valign="top" align="center">
<!-- 왼쪽메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="210">
<tr>
<td><img src="image/left_menu_title.jpg"></td>
</tr>
<tr>
<td><a href="#"><img src="image/left_menu01.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu02.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu03.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu04.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu05.gif" border="0"></a></td></tr>
<tr>
<td height="30"></td></tr>
<tr>
<td height="60" background="../image/sub_service_img.jpg">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="73" height="25"></td>
<td></td></tr>
<tr>
<td></td>
<td><a href="mailto:home@zioinfo.com"><font color="000000">home@zioinfo.com</font></a><a href="mailto:asaweb@asaweb.com"></a></td>
</tr>
</table>
</td></tr>
</table>
<!-- /왼쪽메뉴 -->
</td>
<td width="700" valign="top" align="center">
<!-- 현재위치 & 타이틀 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="290" height="49"><img src="image/title.jpg"></td>
<td align="right"><img src="../image/dot02.gif" align="absmiddle"><font style="font-size:11px;">&nbsp;HOME > 커뮤니티 > <b>질문과 답변</b></font></td></tr>
<tr>
<td height="1" colspan="2" bgcolor="D6D6D6"></td></tr>
</table>
<!-- /현재위치 & 타이틀 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="25"></td></tr>
</table>
<!-- /여백 -->
<!-- 게시판 -->
<table border="0" cellpadding="0" cellspacing="0" width="680">
<tr>
<td width="55" height="3" bgcolor="000000"></td>
<td width="15" bgcolor="000000"></td>
<td width="330" bgcolor="000000"></td>
<td width="95" bgcolor="000000"></td>
<td width="100" bgcolor="000000"></td>
<td width="85" bgcolor="000000"></td></tr>
<tr>
<td height="28" align="center"><img src="image/txt_num.gif"></td>
<td></td>
<td align="center"><img src="image/txt_title.gif"></td>
<td align="center"><img src="image/txt_name.gif"></td>
<td align="center"><img src="image/txt_date.gif"></td>
<td align="center"><img src="image/txt_hits.gif"></td></tr>
<tr>
<td height="2" bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">1</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">2</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">3</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">4</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">5</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">6</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">7</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">8</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">9</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">10</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 (주)지오정보기술 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td></tr>
</table>
<!-- /게시판 -->
<!-- 페이지 & 검색 -->
<table border="0" cellpadding="0" cellspacing="0" width="680">
<tr>
<td height="15"></td></tr>
<tr>
<td align="right"><a href="#"><img src="image/write_btn.gif" border="0"></a></td></tr>
<tr>
<td align="center"><a href="#"><img src="image/prev_btn02.gif" border="0" align="absmiddle"></a>&nbsp;<a href="#"><img src="image/prev_btn01.gif" border="0" align="absmiddle"></a>&nbsp;&nbsp;1&nbsp;&nbsp;2&nbsp;&nbsp;<b>3</b>&nbsp;&nbsp;4&nbsp;&nbsp;5&nbsp;&nbsp;6&nbsp;&nbsp;7&nbsp;&nbsp;8&nbsp;&nbsp;9&nbsp;&nbsp;10&nbsp;&nbsp;<a href="#"><img src="image/next_btn02.gif" border="0" align="absmiddle"></a>&nbsp;<a href="#"><img src="image/next_btn01.gif" border="0" align="absmiddle"></a></td></tr>
<tr>
<td height="15"></td></tr>
</table>
<table border="0" cellspacing="1" cellpadding="0" width="395" height="50" bgcolor="DCDCDC">
<tr>
<td align="center" bgcolor="F6F6F6">
<table border="0" cellspacing="0" cellpadding="0" width="218">
<tr>
<td width="52"><select name="a" style="width:47;height:18;font-size:12;font-family:gulim;color:#737373"><option>제목</option></select></td>
<td width="111"><input type="text" name="b" style="width:100;hegiht:18;border:1 solid #d4d4d4"></input></td>
<td width="55"><a href="#"><img src="image/search_btn.gif" border="0"></a></td></tr>
</table>
</td></tr>
</table>
<!-- /페이지 & 검색 -->
</td></tr>
</table>
<!-- /내용 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="40"></td></tr>
</table>
<!-- /여백 -->
<!-- 카피라이터 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="2" width="950" bgcolor="E8E8E8"></td>
<td bgcolor="E8E8E8"></td></tr>
<tr>
<td height="68" bgcolor="F5F5F5"><table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="205" align="center"><img src="../image/logo_bottom.png" alt="저작권로고" border="0"></td>
<td width="45"><img src="../image/copyright_line.gif" alt="경계"></td>
<td width="540"><img src="../image/copyright.jpg" alt="저작권" border="0" usemap="#mail_img"></td>
<td width="160" align="right"><select name="select" style="width:126px; height:18px; font-size:12px;font-family:dotum; color:#727272">
<option selected>&nbsp;+&nbsp;패밀리 사이트&nbsp;+</option>
</select></td>
<tr>
</table>
<map name="mail_img">
<area shape="rect" coords="1,17,135,32" href="mailto:webmaster@zioinfo.com">
</map>
<map name="mail_img">
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
</map></td>
<td bgcolor="F5F5F5">&nbsp;</td></tr>
</table>
<!-- /카피라이터 -->
<!--카피라이터링크-->
<map name="mail_img">
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
</map>
<!--/카피라이터링크-->
</body>
</html>

View File

@ -1,357 +0,0 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
<meta name="author" content="(주)아사달">
<meta name="description" content="아사달 템플릿">
<meta name="keywords" content="아사달 템플릿,비즈니스">
<title>아사달 템플릿</title>
<link rel="stylesheet" href="../link.css">
<script src="Scripts/AC_RunActiveContent.js" type="text/javascript"></script>
<style type="text/css">
<!--
body {
background-color: #FFFFFF;
}
-->
</style><body topmargin="0" leftmargin="0" marginheight="0" marginwidth="0">
<!-- 메인 이미지 & 로고 & 탑메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="950"><img src="image/sub_img.jpg" border="0" usemap="#sub_img"></td>
<td background="image/sub_img_bg.gif">&nbsp;</td></tr>
</table>
<!--맵링크-->
<map name="sub_img">
<area shape="rect" coords="37,18,182,69" href="../index.htm">
<area shape="rect" coords="733,4,772,20" href="#">
<area shape="rect" coords="776,4,812,20" href="#">
<area shape="rect" coords="815,4,864,20" href="#">
<area shape="rect" coords="869,4,933,20" href="#">
</map>
<!--/맵링크-->
<!-- /메인 이미지 & 로고 & 탑메뉴 -->
<!-- 메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="250" background="../image/menu_bg.gif">&nbsp;</td>
<td width="700">
<table border="0" cellpadding="0" cellspacing="0" width="700">
<tr>
<td><script type="text/javascript">
AC_FL_RunContent( 'codebase','http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0','width','700','height','55','src','../swf/index01','quality','high','pluginspage','http://www.macromedia.com/go/getflashplayer','wmode','transparent','movie','../swf/index01?mNum=4&sNum=2' ); //end AC code
</script><noscript><object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0" width="700" height="55">
<param name="wmode" value="transparent">
<param name="movie" value="../swf/index01.swf?mNum=4&sNum=2">
<param name="quality" value="high">
<embed src="../swf/index01.swf" quality="high" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" width="700" height="55"></embed></object></noscript></td></tr>
</table>
</td>
<td background="../image/menu_bg.gif">&nbsp;</td></tr>
</table>
<!-- /메뉴 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="20"></td></tr>
</table>
<!-- /여백 -->
<!-- 내용 -->
<table border="0" cellpadding="0" cellspacing="0" width="950">
<tr>
<td width="250" valign="top" align="center">
<!-- 왼쪽메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="210">
<tr>
<td><img src="image/left_menu_title.gif"></td>
</tr>
<tr>
<td><a href="#"><img src="image/left_menu01.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu02.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu03.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu04.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu05.gif" border="0"></a></td></tr>
<tr>
<td height="30"></td></tr>
<tr>
<td height="60" background="../image/sub_service_img.jpg">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="73" height="25"></td>
<td></td></tr>
<tr>
<td></td>
<td><a href="mailto:home@zioinfo.com"><font color="000000">home@zioinfo.com</font></a><a href="mailto:asaweb@asaweb.com"></a></td>
</tr>
</table>
</td></tr>
</table>
<!-- /왼쪽메뉴 -->
</td>
<td width="700" valign="top" align="center">
<!-- 현재위치 & 타이틀 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="290" height="49"><img src="image/title.jpg"></td>
<td align="right"><img src="../image/dot02.gif" align="absmiddle"><font style="font-size:11px;">&nbsp;HOME > 커뮤니티 > <b>질문과 답변</b></font></td></tr>
<tr>
<td height="1" colspan="2" bgcolor="D6D6D6"></td></tr>
</table>
<!-- /현재위치 & 타이틀 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="25"></td></tr>
</table>
<!-- /여백 -->
<!-- 게시판 -->
<table border="0" cellpadding="0" cellspacing="0" width="680">
<tr>
<td width="55" height="3" bgcolor="000000"></td>
<td width="15" bgcolor="000000"></td>
<td width="330" bgcolor="000000"></td>
<td width="95" bgcolor="000000"></td>
<td width="100" bgcolor="000000"></td>
<td width="85" bgcolor="000000"></td></tr>
<tr>
<td height="28" align="center"><img src="image/txt_num.gif"></td>
<td></td>
<td align="center"><img src="image/txt_title.gif"></td>
<td align="center"><img src="image/txt_name.gif"></td>
<td align="center"><img src="image/txt_date.gif"></td>
<td align="center"><img src="image/txt_hits.gif"></td></tr>
<tr>
<td height="2" bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">1</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">2</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">3</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">4</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">5</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">6</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">7</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">8</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">9</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">10</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 (주)지오정보기술 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td></tr>
</table>
<!-- /게시판 -->
<!-- 페이지 & 검색 -->
<table border="0" cellpadding="0" cellspacing="0" width="680">
<tr>
<td height="15"></td></tr>
<tr>
<td align="right"><a href="#"><img src="image/write_btn.gif" border="0"></a></td></tr>
<tr>
<td align="center"><a href="#"><img src="image/prev_btn02.gif" border="0" align="absmiddle"></a>&nbsp;<a href="#"><img src="image/prev_btn01.gif" border="0" align="absmiddle"></a>&nbsp;&nbsp;1&nbsp;&nbsp;2&nbsp;&nbsp;<b>3</b>&nbsp;&nbsp;4&nbsp;&nbsp;5&nbsp;&nbsp;6&nbsp;&nbsp;7&nbsp;&nbsp;8&nbsp;&nbsp;9&nbsp;&nbsp;10&nbsp;&nbsp;<a href="#"><img src="image/next_btn02.gif" border="0" align="absmiddle"></a>&nbsp;<a href="#"><img src="image/next_btn01.gif" border="0" align="absmiddle"></a></td></tr>
<tr>
<td height="15"></td></tr>
</table>
<table border="0" cellspacing="1" cellpadding="0" width="395" height="50" bgcolor="DCDCDC">
<tr>
<td align="center" bgcolor="F6F6F6">
<table border="0" cellspacing="0" cellpadding="0" width="218">
<tr>
<td width="52"><select name="a" style="width:47;height:18;font-size:12;font-family:gulim;color:#737373"><option>제목</option></select></td>
<td width="111"><input type="text" name="b" style="width:100;hegiht:18;border:1 solid #d4d4d4"></input></td>
<td width="55"><a href="#"><img src="image/search_btn.gif" border="0"></a></td></tr>
</table>
</td></tr>
</table>
<!-- /페이지 & 검색 -->
</td></tr>
</table>
<!-- /내용 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="40"></td></tr>
</table>
<!-- /여백 -->
<!-- 카피라이터 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="2" width="950" bgcolor="E8E8E8"></td>
<td bgcolor="E8E8E8"></td></tr>
<tr>
<td height="68" bgcolor="F5F5F5"><table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="205" align="center"><img src="../image/logo_bottom.png" alt="저작권로고" border="0"></td>
<td width="45"><img src="../image/copyright_line.gif" alt="경계"></td>
<td width="540"><img src="../image/copyright.jpg" alt="저작권" border="0" usemap="#mail_img"></td>
<td width="160" align="right"><select name="select" style="width:126px; height:18px; font-size:12px;font-family:dotum; color:#727272">
<option selected>&nbsp;+&nbsp;패밀리 사이트&nbsp;+</option>
</select></td>
<tr>
</table>
<map name="mail_img">
<area shape="rect" coords="1,17,135,32" href="mailto:webmaster@zioinfo.com">
</map>
<map name="mail_img">
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
</map></td>
<td bgcolor="F5F5F5">&nbsp;</td></tr>
</table>
<!-- /카피라이터 -->
<!--카피라이터링크-->
<map name="mail_img">
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
</map>
<!--/카피라이터링크-->
</body>
</html>

View File

@ -1,357 +0,0 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
<meta name="author" content="(주)아사달">
<meta name="description" content="아사달 템플릿">
<meta name="keywords" content="아사달 템플릿,비즈니스">
<title>아사달 템플릿</title>
<link rel="stylesheet" href="../link.css">
<script src="Scripts/AC_RunActiveContent.js" type="text/javascript"></script>
<style type="text/css">
<!--
body {
background-color: #FFFFFF;
}
-->
</style><body topmargin="0" leftmargin="0" marginheight="0" marginwidth="0">
<!-- 메인 이미지 & 로고 & 탑메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="950"><img src="image/sub_img.jpg" border="0" usemap="#sub_img"></td>
<td background="image/sub_img_bg.gif">&nbsp;</td></tr>
</table>
<!--맵링크-->
<map name="sub_img">
<area shape="rect" coords="37,18,182,69" href="../index.htm">
<area shape="rect" coords="733,4,772,20" href="#">
<area shape="rect" coords="776,4,812,20" href="#">
<area shape="rect" coords="815,4,864,20" href="#">
<area shape="rect" coords="869,4,933,20" href="#">
</map>
<!--/맵링크-->
<!-- /메인 이미지 & 로고 & 탑메뉴 -->
<!-- 메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="250" background="../image/menu_bg.gif">&nbsp;</td>
<td width="700">
<table border="0" cellpadding="0" cellspacing="0" width="700">
<tr>
<td><script type="text/javascript">
AC_FL_RunContent( 'codebase','http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0','width','700','height','55','src','../swf/index01','quality','high','pluginspage','http://www.macromedia.com/go/getflashplayer','wmode','transparent','movie','../swf/index01?mNum=4&sNum=2' ); //end AC code
</script><noscript><object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0" width="700" height="55">
<param name="wmode" value="transparent">
<param name="movie" value="../swf/index01.swf?mNum=4&sNum=2">
<param name="quality" value="high">
<embed src="../swf/index01.swf" quality="high" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" width="700" height="55"></embed></object></noscript></td></tr>
</table>
</td>
<td background="../image/menu_bg.gif">&nbsp;</td></tr>
</table>
<!-- /메뉴 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="20"></td></tr>
</table>
<!-- /여백 -->
<!-- 내용 -->
<table border="0" cellpadding="0" cellspacing="0" width="950">
<tr>
<td width="250" valign="top" align="center">
<!-- 왼쪽메뉴 -->
<table border="0" cellpadding="0" cellspacing="0" width="210">
<tr>
<td><img src="image/left_menu_title.gif"></td>
</tr>
<tr>
<td><a href="#"><img src="image/left_menu01.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu02.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu03.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu04.gif" border="0"></a></td></tr>
<tr>
<td><a href="#"><img src="image/left_menu05.gif" border="0"></a></td></tr>
<tr>
<td height="30"></td></tr>
<tr>
<td height="60" background="../image/sub_service_img.jpg">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="73" height="25"></td>
<td></td></tr>
<tr>
<td></td>
<td><a href="mailto:home@zioinfo.com"><font color="000000">home@zioinfo.com</font></a><a href="mailto:asaweb@asaweb.com"></a></td>
</tr>
</table>
</td></tr>
</table>
<!-- /왼쪽메뉴 -->
</td>
<td width="700" valign="top" align="center">
<!-- 현재위치 & 타이틀 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="290" height="49"><img src="image/title.jpg"></td>
<td align="right"><img src="../image/dot02.gif" align="absmiddle"><font style="font-size:11px;">&nbsp;HOME > 커뮤니티 > <b>질문과 답변</b></font></td></tr>
<tr>
<td height="1" colspan="2" bgcolor="D6D6D6"></td></tr>
</table>
<!-- /현재위치 & 타이틀 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="25"></td></tr>
</table>
<!-- /여백 -->
<!-- 게시판 -->
<table border="0" cellpadding="0" cellspacing="0" width="680">
<tr>
<td width="55" height="3" bgcolor="000000"></td>
<td width="15" bgcolor="000000"></td>
<td width="330" bgcolor="000000"></td>
<td width="95" bgcolor="000000"></td>
<td width="100" bgcolor="000000"></td>
<td width="85" bgcolor="000000"></td></tr>
<tr>
<td height="28" align="center"><img src="image/txt_num.gif"></td>
<td></td>
<td align="center"><img src="image/txt_title.gif"></td>
<td align="center"><img src="image/txt_name.gif"></td>
<td align="center"><img src="image/txt_date.gif"></td>
<td align="center"><img src="image/txt_hits.gif"></td></tr>
<tr>
<td height="2" bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td>
<td bgcolor="F3F3F3"></td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">1</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">2</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">3</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">4</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">5</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">6</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">7</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">8</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">9</td>
<td></td>
<td>고객님들 위한 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td>
<td bgcolor="E7E7E7"></td></tr>
<tr>
<td height="30" align="center">10</td>
<td></td>
<td><img src="image/reply_icon.gif" align="absmiddle"> 고객님들 위한 (주)지오정보기술 자유게시판입니다.</td>
<td align="center">홍길동</td>
<td align="center">2008/07/02</td>
<td align="center">100</td></tr>
<tr>
<td height="1" bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td>
<td bgcolor="000000"></td></tr>
</table>
<!-- /게시판 -->
<!-- 페이지 & 검색 -->
<table border="0" cellpadding="0" cellspacing="0" width="680">
<tr>
<td height="15"></td></tr>
<tr>
<td align="right"><a href="#"><img src="image/write_btn.gif" border="0"></a></td></tr>
<tr>
<td align="center"><a href="#"><img src="image/prev_btn02.gif" border="0" align="absmiddle"></a>&nbsp;<a href="#"><img src="image/prev_btn01.gif" border="0" align="absmiddle"></a>&nbsp;&nbsp;1&nbsp;&nbsp;2&nbsp;&nbsp;<b>3</b>&nbsp;&nbsp;4&nbsp;&nbsp;5&nbsp;&nbsp;6&nbsp;&nbsp;7&nbsp;&nbsp;8&nbsp;&nbsp;9&nbsp;&nbsp;10&nbsp;&nbsp;<a href="#"><img src="image/next_btn02.gif" border="0" align="absmiddle"></a>&nbsp;<a href="#"><img src="image/next_btn01.gif" border="0" align="absmiddle"></a></td></tr>
<tr>
<td height="15"></td></tr>
</table>
<table border="0" cellspacing="1" cellpadding="0" width="395" height="50" bgcolor="DCDCDC">
<tr>
<td align="center" bgcolor="F6F6F6">
<table border="0" cellspacing="0" cellpadding="0" width="218">
<tr>
<td width="52"><select name="a" style="width:47;height:18;font-size:12;font-family:gulim;color:#737373"><option>제목</option></select></td>
<td width="111"><input type="text" name="b" style="width:100;hegiht:18;border:1 solid #d4d4d4"></input></td>
<td width="55"><a href="#"><img src="image/search_btn.gif" border="0"></a></td></tr>
</table>
</td></tr>
</table>
<!-- /페이지 & 검색 -->
</td></tr>
</table>
<!-- /내용 -->
<!-- 여백 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="40"></td></tr>
</table>
<!-- /여백 -->
<!-- 카피라이터 -->
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="2" width="950" bgcolor="E8E8E8"></td>
<td bgcolor="E8E8E8"></td></tr>
<tr>
<td height="68" bgcolor="F5F5F5"><table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td width="205" align="center"><img src="../image/logo_bottom.png" alt="저작권로고" border="0"></td>
<td width="45"><img src="../image/copyright_line.gif" alt="경계"></td>
<td width="540"><img src="../image/copyright.jpg" alt="저작권" border="0" usemap="#mail_img"></td>
<td width="160" align="right"><select name="select" style="width:126px; height:18px; font-size:12px;font-family:dotum; color:#727272">
<option selected>&nbsp;+&nbsp;패밀리 사이트&nbsp;+</option>
</select></td>
<tr>
</table>
<map name="mail_img">
<area shape="rect" coords="1,17,135,32" href="mailto:webmaster@zioinfo.com">
</map>
<map name="mail_img">
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
</map></td>
<td bgcolor="F5F5F5">&nbsp;</td></tr>
</table>
<!-- /카피라이터 -->
<!--카피라이터링크-->
<map name="mail_img">
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
</map>
<!--/카피라이터링크-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 B

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