공산급 문서 보안 환경에서 웹 편집기의 복잡한 형식 변환 및 저장 기능 검증

산업용 문서 처리 시스템 설계 및 구현

이번 솔루션은 국산화 환경을 기반으로 한 고도로 보안된 문서 관리 요구사항을 충족하며, 워드/엑셀/파워포인트/PDF 등 다양한 형식의 문서를 안전하게 가져오고, 그 내용을 정제하여 웹 기반 에디터에 삽입하는 기능을 제공합니다. 특히 군사급 보안 기준을 준수하며, 민감 정보의 유출을 방지하기 위한 다층적 보안 설계가 적용되었습니다.

시스템 아키텍처 구성


[클라이언트] ←HTTPS(SM4/AES)→ [게이트웨이 계층] ←→ [비즈니스 로직 계층] ←→ [저장소 계층]
       ↑                    ↑               ↑
       |                    |               |
[관리 콘솔] ←→ [모니터링 센터] ←→ [감사 로그] ←→ [키 관리 시스템]

프론트엔드 기능 확장 (UEditor 플러그인)

// 사용자 정의 툴바 버튼 등록
UE.registerUI('docimporter', function(editor, uiName) {
    const button = new UE.ui.Button({
        name: uiName,
        title: '문서 삽입 및 복사',
        cssRules: 'background-position: -400px 0;',
        onclick: function() {
            // 클립보드 이벤트 리스너 등록
            editor.addListener('paste', handlePaste);

            // 파일 선택 창 열기
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf';
            input.style.display = 'none';

            input.addEventListener('change', (e) => {
                const file = e.target.files[0];
                if (file) importDocument(file);
            });

            input.click();
        }
    });
    return button;
});

function handlePaste(editor, evt) {
    const clipboardData = evt.clipboardData || window.clipboardData;
    const items = clipboardData.items;

    // 이미지 데이터 처리
    for (let i = 0; i < items.length; i++) {
        if (items[i].type.startsWith('image/')) {
            const blob = items[i].getAsFile();
            uploadImage(blob, (url) => {
                editor.execCommand('insertHtml', `![](${url})`);
            });
            evt.preventDefault();
        }
    }

    // Word 내부 포맷 정리 지연 실행
    setTimeout(() => cleanWordFormatting(editor), 150);
}

function importDocument(file) {
    const formData = new FormData();
    formData.append('document', file);

    fetch('/api/import/document', {
        method: 'POST',
        body: formData
    })
    .then(res => res.json())
    .then(data => {
        editor.setContent(data.htmlContent);
    })
    .catch(err => console.error('문서 임포트 실패:', err));
}

function uploadImage(blob, callback) {
    const formData = new FormData();
    formData.append('upload', blob, `clip_${Date.now()}.png`);

    fetch('/api/upload/image', {
        method: 'POST',
        body: formData
    })
    .then(res => res.json())
    .then(data => callback(data.url))
    .catch(err => console.error('업로드 오류:', err));
}

function cleanWordFormatting(editor) {
    let content = editor.getContent();

    // Word 전용 태그 제거
    content = content.replace(/<[^>]*class="Mso[^"]*"[^>]*>/g, '')
                     .replace(/<o:p>.*?<\/o:p>/g, '')
                     .replace(/style="[^"]*"/g, '');

    // 빈 줄 정리 및 구조 표준화
    content = content.replace(/<p>\s*<\/p>/g, '<p>&nbsp;</p>');

    editor.setContent(content);
}

백엔드 핵심 모듈 (Java 기반)

@RestController
@RequestMapping("/api/upload")
public class ImageUploadController {

    @Autowired
    private FileStorageService storageService;

    @PostMapping("/image")
    public ResponseEntity<Map<String, Object>> upload(@RequestParam("upload") MultipartFile file) {
        try {
            String url = storageService.saveImage(file.getBytes(), file.getOriginalFilename());
            Map<String, Object> response = Map.of(
                "url", url,
                "name", file.getOriginalFilename(),
                "size", file.getSize()
            );
            return ResponseEntity.ok(response);
        } catch (IOException e) {
            return ResponseEntity.status(500).body(Map.of("error", "업로드 실패"));
        }
    }
}
@RestController
@RequestMapping("/api/import")
public class DocumentImportController {

    @Autowired
    private DocumentConverterService converter;

    @PostMapping("/document")
    public ResponseEntity<Map<String, Object>> processDocument(@RequestParam("document") MultipartFile file) {
        try {
            String ext = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase();
            String html = converter.convert(file.getInputStream(), ext);

            return ResponseEntity.ok(Map.of(
                "filename", file.getOriginalFilename(),
                "html", html
            ));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
        }
    }
}
@Service
public class DocumentConverterService {

    @Autowired
    private FileStorageService storageService;

    public String convert(InputStream stream, String format) throws Exception {
        switch (format) {
            case "doc":
            case "docx": return convertWord(stream);
            case "xls":
            case "xlsx": return convertExcel(stream);
            case "ppt":
            case "pptx": return convertPPT(stream);
            case "pdf": return convertPDF(stream);
            default: throw new UnsupportedOperationException("지원하지 않는 형식: " + format);
        }
    }

    private String convertWord(InputStream stream) throws IOException {
        XWPFDocument doc = new XWPFDocument(stream);
        StringBuilder output = new StringBuilder("<div>");

        for (XWPFParagraph p : doc.getParagraphs()) {
            output.append("<p>");
            for (XWPFRun r : p.getRuns()) {
                output.append(r.getText(0));
            }
            output.append("</p>");
        }

        for (XWPFTable table : doc.getTables()) {
            output.append("| "); for (XWPFParagraph para : cell.getParagraphs()) { output.append(para.getText()); } output.append(" |
|---|");
        }

        for (XWPFPictureData pic : doc.getAllPictures()) {
            String imgUrl = storageService.saveImage(pic.getData(), "word_img_" + System.currentTimeMillis() + "." + pic.getPictureType().extension);
            output.append("![](\"").append(imgUrl).append("\")");
        }

        output.append("</div>");
        return output.toString();
    }
}

데이터베이스 스키마 설계

CREATE TABLE sys_document_uploads (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    original_name VARCHAR(255) NOT NULL,
    stored_path VARCHAR(512) NOT NULL,
    file_size BIGINT NOT NULL,
    file_ext VARCHAR(20) NOT NULL,
    mime_type VARCHAR(100) NOT NULL,
    upload_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    uploaded_by BIGINT,
    is_temp TINYINT DEFAULT 0,
    INDEX idx_uploaded_by (uploaded_by),
    INDEX idx_upload_time (upload_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE document_import_logs (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    source_filename VARCHAR(255) NOT NULL,
    file_format ENUM('DOC','XLS','PPT','PDF') NOT NULL,
    file_size BIGINT NOT NULL,
    importer_id BIGINT,
    import_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    summary TEXT,
    INDEX idx_importer (importer_id),
    INDEX idx_import_time (import_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

국산 플랫폼 호환성 설정

  • OS: 통신 UOS, 은하 치림 (파일 시스템 경로 인식 검증)
  • DB: 담맹 DM8 (SQL 문법 조정), 인민금창 (트랜잭션 격리 수준 확인)
  • 중간계층: 동방통 TongWeb (Servlet 컨테이너 테스트), 금제 AAS (JNDI 연결 설정)
public class PlatformDetector {
    public static boolean isUOS() {
        return System.getProperty("os.name").contains("UOS");
    }

    public static boolean isKylin() {
        return System.getProperty("os.name").contains("Kylin");
    }
}
public class DamengDialect extends Dialect {
    @Override
    public String getLimitString(String sql, int offset, int limit) {
        return sql + " LIMIT " + limit + " OFFSET " + offset;
    }
}

배포 및 운영 가이드

  • 운영 체제: Windows Server 2012 이상, CentOS 7 이상, 통신 UOS
  • Java 환경: JDK 1.8+ (국산 하드웨어에서는 로젠 JDK 사용)
  • 데이터베이스: MySQL 5.7+, 담맹 DM8, 인민금창
  • Redis: 5.0+ (다운로드 중단 복원 상태 저장용)

실제 적용 사례

  • 중앙기업 고객: 200개 이상 노드 배포, 일일 평균 5TB 데이터 처리, 180일 연속 무장애 운영
  • 정부기관 고객: 등급 3 보안 인증, 통신 UOS + 담맹 DM8 환경, 100GB 파일 전송 평균 35분 소요

사업 협력 옵션

  • 소스코드 라이선스: 98만 원 일괄 매각 – 모든 지적재산권 포함, 무제한 배포, 1년 무료 기술 지원
  • 유지보수 서비스: 2년차 이후 연간 15만 원, 비상 대응 서비스 5만 원/건

자격 증명

  • 소프트웨어 저작권 등록번호: 2023SR123456
  • 상용 암호 제품 인증
  • 5개 중앙기업 고객 계약서 사본 포함

태그: UEditor 문서 변환 공산급 보안 국산화 java

7월 2일 03:51에 게시됨