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>
BIN
certification/01_소프트웨어_저작권_등록_신청서.docx
Normal file
BIN
certification/02_소프트웨어사업자_신고서.docx
Normal file
BIN
certification/03_조달청_나라장터_물품_등록_신청서.docx
Normal file
642
certification/generate_docx.py
Normal 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에서 편집 가능합니다.")
|
||||
667
certification/generate_each_pdf.py
Normal 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)")
|
||||
450
certification/source/01_core_ssh_agentless.py
Normal 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])
|
||||
455
certification/source/02_core_license_engine.py
Normal 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")
|
||||
501
certification/source/03_router_sr_management.py
Normal 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,
|
||||
}
|
||||
|
||||
90
certification/source/04_core_ai_classifier.py
Normal 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,
|
||||
}
|
||||
200
certification/source/05_frontend_dashboard.js
Normal 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;
|
||||
31
certification/source/README.md
Normal 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개 명령어로 인프라 제어
|
||||
|
Before Width: | Height: | Size: 896 B |
@ -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>
|
||||
@ -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>
|
||||
@ -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>');
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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"> </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"> </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"> </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;"> 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> <a href="#"><img src="image/prev_btn01.gif" border="0" align="absmiddle"></a> 1 2 <b>3</b> 4 5 6 7 8 9 10 <a href="#"><img src="image/next_btn02.gif" border="0" align="absmiddle"></a> <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> + 패밀리 사이트 +</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"> </td></tr>
|
||||
</table>
|
||||
<!-- /카피라이터 -->
|
||||
|
||||
<!--카피라이터링크-->
|
||||
<map name="mail_img">
|
||||
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
|
||||
</map>
|
||||
<!--/카피라이터링크-->
|
||||
</body>
|
||||
</html>
|
||||
@ -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"> </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"> </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"> </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;"> 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> <a href="#"><img src="image/prev_btn01.gif" border="0" align="absmiddle"></a> 1 2 <b>3</b> 4 5 6 7 8 9 10 <a href="#"><img src="image/next_btn02.gif" border="0" align="absmiddle"></a> <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> + 패밀리 사이트 +</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"> </td></tr>
|
||||
</table>
|
||||
<!-- /카피라이터 -->
|
||||
|
||||
<!--카피라이터링크-->
|
||||
<map name="mail_img">
|
||||
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
|
||||
</map>
|
||||
<!--/카피라이터링크-->
|
||||
</body>
|
||||
</html>
|
||||
@ -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"> </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"> </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"> </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;"> 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> <a href="#"><img src="image/prev_btn01.gif" border="0" align="absmiddle"></a> 1 2 <b>3</b> 4 5 6 7 8 9 10 <a href="#"><img src="image/next_btn02.gif" border="0" align="absmiddle"></a> <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> + 패밀리 사이트 +</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"> </td></tr>
|
||||
</table>
|
||||
<!-- /카피라이터 -->
|
||||
|
||||
<!--카피라이터링크-->
|
||||
<map name="mail_img">
|
||||
<area shape="rect" coords="270,1,367,15" href="mailto:asaweb@asaweb.com">
|
||||
</map>
|
||||
<!--/카피라이터링크-->
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 493 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 497 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 932 B |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 137 B |
|
Before Width: | Height: | Size: 676 B |
|
Before Width: | Height: | Size: 140 B |
|
Before Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 138 B |
|
Before Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 682 B |
|
Before Width: | Height: | Size: 79 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 304 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 743 B |
|
Before Width: | Height: | Size: 609 B |
|
Before Width: | Height: | Size: 748 B |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 598 B |
|
Before Width: | Height: | Size: 598 B |
|
Before Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 466 B |
|
Before Width: | Height: | Size: 809 B |
|
Before Width: | Height: | Size: 823 B |
|
Before Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 367 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 873 B |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 605 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 558 B |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 556 B |
|
Before Width: | Height: | Size: 868 B |
|
Before Width: | Height: | Size: 596 B |
|
Before Width: | Height: | Size: 455 B |
|
Before Width: | Height: | Size: 455 B |
|
Before Width: | Height: | Size: 352 B |
|
Before Width: | Height: | Size: 415 B |
|
Before Width: | Height: | Size: 556 B |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 654 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 283 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 303 B |