Java & iBatis 소스 코드 분석 및 시각화 도구 개발

개발 환경 구축 및 코드 생성

아래에서는 프로젝트 환경 구축 및 코드 생성을 진행하겠습니다. 요구사항에 맞춰 프로젝트 구조, 데이터베이스 스키마, 백엔드/프론트엔드 코드를 순차적으로 제공합니다.

1. 프로젝트 구조

itf/
├── backend/                        # 백엔드 프로젝트
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/viw/itf/
│   │   │   │   ├── ItfApplication.java
│   │   │   │   ├── config/        # 설정 관련 클래스
│   │   │   │   ├── controller/    # REST API 컨트롤러
│   │   │   │   ├── entity/        # JPA 엔티티
│   │   │   │   ├── repository/    # 데이터 액세스 계층
│   │   │   │   ├── service/       # 비즈니스 로직
│   │   │   │   ├── parser/        # 소스 코드 파서
│   │   │   │   ├── analyzer/      # 의존성 분석
│   │   │   │   ├── diagram/       # 다이어그램 생성
│   │   │   │   └── util/          # 유틸리티 클래스
│   │   │   └── resources/
│   │   │       ├── application.properties
│   │   │       ├── application-local.properties
│   │   │       └── schema.sql     # 데이터베이스 스키마
│   │   └── test/                  # 테스트 코드
│   └── pom.xml                    # Maven 설정
└── frontend/                      # 프론트엔드 프로젝트
    ├── public/
    ├── src/
    │   ├── assets/                # 정적 자원
    │   ├── components/            # 재사용 컴포넌트
    │   ├── pages/                 # 페이지 컴포넌트
    │   ├── services/              # API 서비스
    │   ├── store/                 # MobX 상태 관리
    │   ├── types/                 # TypeScript 타입 정의
    │   ├── utils/                 # 유틸리티 함수
    │   ├── App.tsx
    │   └── index.tsx
    ├── package.json
    └── tsconfig.json              # TypeScript 설정

2. 데이터베이스 스키마 설계

schema.sql 파일로 제공되는, MariaDB 데이터베이스에 필요한 테이블 구조입니다:

-- 프로젝트 메타데이터 테이블
CREATE TABLE project (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    java_source_path VARCHAR(500) NOT NULL,
    ibatis_source_path VARCHAR(500) NOT NULL,
    analysis_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    version VARCHAR(50)
);

-- 패키지 정보 테이블
CREATE TABLE package (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    name VARCHAR(255) NOT NULL,
    full_path VARCHAR(500) NOT NULL,
    FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE
);

-- 클래스 정보 테이블
CREATE TABLE class (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    package_id INT NOT NULL,
    name VARCHAR(255) NOT NULL,
    full_name VARCHAR(500) NOT NULL,
    file_path VARCHAR(1000) NOT NULL,
    class_type ENUM('CLASS', 'INTERFACE', 'ENUM', 'ANNOTATION') NOT NULL,
    access_modifier VARCHAR(20),
    extends_class VARCHAR(500),
    implements_interfaces TEXT,
    is_component BOOLEAN DEFAULT FALSE,
    component_type VARCHAR(100),
    FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE,
    FOREIGN KEY (package_id) REFERENCES package(id) ON DELETE CASCADE
);

-- 메소드 정보 테이블
CREATE TABLE method (
    id INT AUTO_INCREMENT PRIMARY KEY,
    class_id INT NOT NULL,
    name VARCHAR(255) NOT NULL,
    signature VARCHAR(1000) NOT NULL,
    return_type VARCHAR(255),
    parameters TEXT,
    access_modifier VARCHAR(20),
    is_static BOOLEAN DEFAULT FALSE,
    start_line INT,
    end_line INT,
    FOREIGN KEY (class_id) REFERENCES class(id) ON DELETE CASCADE
);

-- SQL 쿼리 정보 테이블
CREATE TABLE sql_query (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    namespace VARCHAR(255) NOT NULL,
    query_id VARCHAR(255) NOT NULL,
    query_type ENUM('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'PROCEDURE', 'OTHER') NOT NULL,
    query_text TEXT NOT NULL,
    file_path VARCHAR(1000) NOT NULL,
    FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE
);

-- 의존성 정보 테이블
CREATE TABLE dependency (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    source_type ENUM('CLASS', 'METHOD', 'SQL') NOT NULL,
    source_id INT NOT NULL,
    target_type ENUM('CLASS', 'METHOD', 'SQL') NOT NULL,
    target_id INT NOT NULL,
    dependency_type VARCHAR(50) NOT NULL,  -- CALLS, EXTENDS, IMPLEMENTS, USES, SQL_CALL
    line_number INT,
    FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE
);

-- 다이어그램 메타데이터 테이블
CREATE TABLE diagram (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    name VARCHAR(255) NOT NULL,
    diagram_type ENUM('SEQUENCE', 'CLASS', 'COMPONENT', 'PACKAGE', 'ACTIVITY', 'FLOWCHART', 'MINDMAP', 'UML', 'CONTAINER') NOT NULL,
    description TEXT,
    creation_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_modified DATETIME DEFAULT CURRENT_TIMESTAMP,
    author VARCHAR(100),
    layout_data TEXT,
    FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE
);

-- 다이어그램 요소 테이블
CREATE TABLE diagram_element (
    id INT AUTO_INCREMENT PRIMARY KEY,
    diagram_id INT NOT NULL,
    element_type VARCHAR(50) NOT NULL, -- CLASS, METHOD, PACKAGE, etc.
    element_id INT NOT NULL,  -- Reference to actual element
    x_position INT,
    y_position INT,
    width INT,
    height INT,
    custom_properties TEXT,  -- JSON format for additional properties
    FOREIGN KEY (diagram_id) REFERENCES diagram(id) ON DELETE CASCADE
);

-- 다이어그램 관계 테이블
CREATE TABLE diagram_relationship (
    id INT AUTO_INCREMENT PRIMARY KEY,
    diagram_id INT NOT NULL,
    source_element_id INT NOT NULL,
    target_element_id INT NOT NULL,
    relationship_type VARCHAR(50) NOT NULL,  -- ASSOCIATION, INHERITANCE, etc.
    label VARCHAR(255),
    control_points TEXT,  -- JSON format for line control points
    custom_properties TEXT,  -- JSON format for additional properties
    FOREIGN KEY (diagram_id) REFERENCES diagram(id) ON DELETE CASCADE,
    FOREIGN KEY (source_element_id) REFERENCES diagram_element(id) ON DELETE CASCADE,
    FOREIGN KEY (target_element_id) REFERENCES diagram_element(id) ON DELETE CASCADE
);

-- 인덱스 생성
CREATE INDEX idx_class_project ON class(project_id);
CREATE INDEX idx_method_class ON method(class_id);
CREATE INDEX idx_sql_project ON sql_query(project_id);
CREATE INDEX idx_dependency_project ON dependency(project_id);
CREATE INDEX idx_dependency_source ON dependency(source_type, source_id);
CREATE INDEX idx_dependency_target ON dependency(target_type, target_id);
CREATE INDEX idx_diagram_project ON diagram(project_id);
CREATE INDEX idx_diagram_element_diagram ON diagram_element(diagram_id);
CREATE INDEX idx_diagram_relationship_diagram ON diagram_relationship(diagram_id);

3. 백엔드 구현

3.1 Maven 프로젝트 설정 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cohttp://m.viw.itf</groupId>
    <artifactId>com_viw_itf</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>itf</name>
    <description>Source code analyzer and diagram generator</description>
    
    <properties>
        <java.version>11</java.version>
        <javaparser.version>3.24.0</javaparser.version>
        <structurizr.version>1.16.1</structurizr.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- Database -->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Development Tools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Source Code Analysis -->
        <dependency>
            <groupId>com.github.javaparser</groupId>
            <artifactId>javaparser-symbol-solver-core</artifactId>
            <version>${javaparser.version}</version>
        </dependency>
        
        <!-- Structurizr for Diagrams -->
        <dependency>
            <groupId>com.structurizr</groupId>
            <artifactId>structurizr-core</artifactId>
            <version>${structurizr.version}</version>
        </dependency>
        <dependency>
            <groupId>com.structurizr</groupId>
            <artifactId>structurizr-client</artifactId>
            <version>${structurizr.version}</version>
        </dependency>
        <dependency>
            <groupId>com.structurizr</groupId>
            <artifactId>structurizr-plantuml</artifactId>
            <version>${structurizr.version}</version>
        </dependency>
        <dependency>
            <groupId>com.structurizr</groupId>
            <artifactId>structurizr-mermaid</artifactId>
            <version>${structurizr.version}</version>
        </dependency>
        <dependency>
            <groupId>com.structurizr</groupId>
            <artifactId>structurizr-export</artifactId>
            <version>${structurizr.version}</version>
        </dependency>
        <dependency>
            <groupId>com.structurizr</groupId>
            <artifactId>structurizr-autolayout</artifactId>
            <version>${structurizr.version}</version>
        </dependency>
        
        <!-- XML Parser for iBatis -->
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.3</version>
        </dependency>
        
        <!-- JSON Processing -->
        <dependency>
            <groupId>cohttp://m.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        
        <!-- Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.2 애플리케이션 설정 (application.properties)

# Application Name
spring.application.name=itf
server.port=8080

# Database Configuration
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/itf
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver

# JPA/Hibernate Configuration
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect

# Logging Configuration
logging.level.cohttp://m.viw.itf=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# Custom Properties
app.source.java-path=C:/work_back/viw_ep_edi/src/main/java/com/viw/esp/online
app.source.ibatis-path=C:/work_back/viw_erp_wms/src/main/resources/sql

3.3 메인 애플리케이션 클래스

package cohttp://m.viw.itf;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class ItfApplication extends SpringBootServletInitializer {
    
    public static void main(String[] args) {
        SpringApplication.run(ItfApplication.class, args);
    }
}

3.4 설정 클래스

package cohttp://m.viw.itf.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.file.Path;
import java.nio.file.Paths;

@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Value("${app.source.java-path}")
    private String javaSourcePath;

    @Value("${app.source.ibatis-path}")
    private String ibatisSourcePath;

    @Bean
    public Path javaSourceDirectory() {
        return Paths.get(javaSourcePath);
    }

    @Bean
    public Path ibatisSourceDirectory() {
        return Paths.get(ibatisSourcePath);
    }
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

3.5 엔티티 클래스

Project.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "project")
@Data
public class Project {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @Column(nullable = false)
    private String name;
    
    private String description;
    
    @Column(name = "java_source_path", nullable = false)
    private String javaSourcePath;
    
    @Column(name = "ibatis_source_path", nullable = false)
    private String ibatisSourcePath;
    
    @Column(name = "analysis_date")
    private LocalDateTime analysisDate;
    
    private String version;
    
    @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Package> packages = new ArrayList<>();
    
    @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<SqlQuery> sqlQueries = new ArrayList<>();
    
    @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Dependency> dependencies = new ArrayList<>();
    
    @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Diagram> diagrams = new ArrayList<>();
}

Package.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "package")
@Data
public class Package {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @ManyToOne
    @JoinColumn(name = "project_id", nullable = false)
    private Project project;
    
    @Column(nullable = false)
    private String name;
    
    @Column(name = "full_path", nullable = false)
    private String fullPath;
    
    @OneToMany(mappedBy = "pkg", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<CodeClass> classes = new ArrayList<>();
}

CodeClass.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "class")
@Data
public class CodeClass {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @ManyToOne
    @JoinColumn(name = "project_id", nullable = false)
    private Project project;
    
    @ManyToOne
    @JoinColumn(name = "package_id", nullable = false)
    private Package pkg;
    
    @Column(nullable = false)
    private String name;
    
    @Column(name = "full_name", nullable = false)
    private String fullName;
    
    @Column(name = "file_path", nullable = false)
    private String filePath;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "class_type", nullable = false)
    private ClassType classType;
    
    @Column(name = "access_modifier")
    private String accessModifier;
    
    @Column(name = "extends_class")
    private String extendsClass;
    
    @Column(name = "implements_interfaces")
    private String implementsInterfaces;
    
    @Column(name = "is_component")
    private Boolean isComponent = false;
    
    @Column(name = "component_type")
    private String componentType;
    
    @OneToMany(mappedBy = "codeClass", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Method> methods = new ArrayList<>();
    
    public enum ClassType {
        CLASS, INTERFACE, ENUM, ANNOTATION
    }
}

Method.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "method")
@Data
public class Method {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @ManyToOne
    @JoinColumn(name = "class_id", nullable = false)
    private CodeClass codeClass;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false)
    private String signature;
    
    @Column(name = "return_type")
    private String returnType;
    
    private String parameters;
    
    @Column(name = "access_modifier")
    private String accessModifier;
    
    @Column(name = "is_static")
    private Boolean isStatic = false;
    
    @Column(name = "start_line")
    private Integer startLine;
    
    @Column(name = "end_line")
    private Integer endLine;
}

SqlQuery.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "sql_query")
@Data
public class SqlQuery {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @ManyToOne
    @JoinColumn(name = "project_id", nullable = false)
    private Project project;
    
    @Column(nullable = false)
    private String namespace;
    
    @Column(name = "query_id", nullable = false)
    private String queryId;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "query_type", nullable = false)
    private QueryType queryType;
    
    @Column(name = "query_text", nullable = false, columnDefinition = "TEXT")
    private String queryText;
    
    @Column(name = "file_path", nullable = false)
    private String filePath;
    
    public enum QueryType {
        SELECT, INSERT, UPDATE, DELETE, PROCEDURE, OTHER
    }
}

Dependency.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "dependency")
@Data
public class Dependency {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @ManyToOne
    @JoinColumn(name = "project_id", nullable = false)
    private Project project;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "source_type", nullable = false)
    private ElementType sourceType;
    
    @Column(name = "source_id", nullable = false)
    private Integer sourceId;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "target_type", nullable = false)
    private ElementType targetType;
    
    @Column(name = "target_id", nullable = false)
    private Integer targetId;
    
    @Column(name = "dependency_type", nullable = false)
    private String dependencyType;
    
    @Column(name = "line_number")
    private Integer lineNumber;
    
    public enum ElementType {
        CLASS, METHOD, SQL
    }
}

Diagram.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "diagram")
@Data
public class Diagram {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @ManyToOne
    @JoinColumn(name = "project_id", nullable = false)
    private Project project;
    
    @Column(nullable = false)
    private String name;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "diagram_type", nullable = false)
    private DiagramType diagramType;
    
    private String description;
    
    @Column(name = "creation_date")
    private LocalDateTime creationDate;
    
    @Column(name = "last_modified")
    private LocalDateTime lastModified;
    
    private String author;
    
    @Column(name = "layout_data", columnDefinition = "TEXT")
    private String layoutData;
    
    @OneToMany(mappedBy = "diagram", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<DiagramElement> elements = new ArrayList<>();
    
    @OneToMany(mappedBy = "diagram", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<DiagramRelationship> relationships = new ArrayList<>();
    
    public enum DiagramType {
        SEQUENCE, CLASS, COMPONENT, PACKAGE, ACTIVITY, FLOWCHART, MINDMAP, UML, CONTAINER
    }
}

DiagramElement.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "diagram_element")
@Data
public class DiagramElement {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @ManyToOne
    @JoinColumn(name = "diagram_id", nullable = false)
    private Diagram diagram;
    
    @Column(name = "element_type", nullable = false)
    private String elementType;
    
    @Column(name = "element_id", nullable = false)
    private Integer elementId;
    
    @Column(name = "x_position")
    private Integer xPosition;
    
    @Column(name = "y_position")
    private Integer yPosition;
    
    private Integer width;
    
    private Integer height;
    
    @Column(name = "custom_properties", columnDefinition = "TEXT")
    private String customProperties;
}

DiagramRelationship.java:

package cohttp://m.viw.itf.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "diagram_relationship")
@Data
public class DiagramRelationship {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    @ManyToOne
    @JoinColumn(name = "diagram_id", nullable = false)
    private Diagram diagram;
    
    @ManyToOne
    @JoinColumn(name = "source_element_id", nullable = false)
    private DiagramElement sourceElement;
    
    @ManyToOne
    @JoinColumn(name = "target_element_id", nullable = false)
    private DiagramElement targetElement;
    
    @Column(name = "relationship_type", nullable = false)
    private String relationshipType;
    
    private String label;
    
    @Column(name = "control_points", columnDefinition = "TEXT")
    private String controlPoints;
    
    @Column(name = "custom_properties", columnDefinition = "TEXT")
    private String customProperties;
}

3.6 리포지토리 인터페이스

ProjectRepository.java:

package cohttp://m.viw.itf.repository;

import cohttp://m.viw.itf.entity.Project;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProjectRepository extends JpaRepository<Project, Integer> {
}

PackageRepository.java:

package cohttp://m.viw.itf.repository;

import cohttp://m.viw.itf.entity.Package;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface PackageRepository extends JpaRepository<Package, Integer> {
    
    List<Package> findByProjectId(Integer projectId);
    
    Optional<Package> findByProjectIdAndFullPath(Integer projectId, String fullPath);
}

CodeClassRepository.java:

package cohttp://m.viw.itf.repository;

import cohttp://m.viw.itf.entity.CodeClass;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface CodeClassRepository extends JpaRepository<CodeClass, Integer> {
    
    List<CodeClass> findByProjectId(Integer projectId);
    
    List<CodeClass> findByPkgId(Integer packageId);
    
    Optional<CodeClass> findByProjectIdAndFullName(Integer projectId, String fullName);
}

MethodRepository.java:

package cohttp://m.viw.itf.repository;

import cohttp://m.viw.itf.entity.Method;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface MethodRepository extends JpaRepository<Method, Integer> {
    
    List<Method> findByCodeClassId(Integer classId);
}

SqlQueryRepository.java:

package cohttp://m.viw.itf.repository;

import cohttp://m.viw.itf.entity.SqlQuery;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface SqlQueryRepository extends JpaRepository<SqlQuery, Integer> {
    
    List<SqlQuery> findByProjectId(Integer projectId);
    
    Optional<SqlQuery> findByProjectIdAndNamespaceAndQueryId(Integer projectId, String namespace, String queryId);
}

DependencyRepository.java:

package cohttp://m.viw.itf.repository;

import cohttp://m.viw.itf.entity.Dependency;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface DependencyRepository extends JpaRepository<Dependency, Integer> {
    
    List<Dependency> findByProjectId(Integer projectId);
    
    List<Dependency> findByProjectIdAndSourceTypeAndSourceId(
            Integer projectId, Dependency.ElementType sourceType, Integer sourceId);
    
    List<Dependency> findByProjectIdAndTargetTypeAndTargetId(
            Integer projectId, Dependency.ElementType targetType, Integer targetId);
    
    @Query("SELECT d FROM Dependency d WHERE d.project.id = :projectId AND " +
           "((d.sourceType = :type AND d.sourceId = :id) OR " +
           "(d.targetType = :type AND d.targetId = :id))")
    List<Dependency> findAllByElement(
            @Param("projectId") Integer projectId,
            @Param("type") Dependency.ElementType type,
            @Param("id") Integer id);
}

DiagramRepository.java:

package cohttp://m.viw.itf.repository;

import cohttp://m.viw.itf.entity.Diagram;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface DiagramRepository extends JpaRepository<Diagram, Integer> {
    
    List<Diagram> findByProjectId(Integer projectId);
    
    List<Diagram> findByProjectIdAndDiagramType(Integer projectId, Diagram.DiagramType diagramType);
}

3.7 소스 코드 파서 구현

JavaSourceParser.java:

package cohttp://m.viw.itf.parser;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParserConfiguration;
import cohttp://m.github.javaparser.ast.CompilationUnit;
import cohttp://m.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import cohttp://m.github.javaparser.ast.body.MethodDeclaration;
import cohttp://m.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import cohttp://m.viw.itf.entity.*;
import cohttp://m.viw.itf.entity.Package;
import cohttp://m.viw.itf.repository.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.FileNotFoundException;
import java.nio.file.Path;
import java.util.Optional;

@Component
@RequiredArgsConstructor
@Slf4j
public class JavaSourceParser {

    private final ProjectRepository projectRepository;
    private final PackageRepository packageRepository;
    private final CodeClassRepository codeClassRepository;
    private final MethodRepository methodRepository;
    private final DependencyRepository dependencyRepository;
    private final Path javaSourceDirectory;
    
    private JavaParser javaParser;
    private Project currentProject;
    
    @Transactional
    public void parseJavaFiles(Project project) {
        this.currentProject = project;
        initializeJavaParser();
        
        File rootDir = javaSourceDirectory.toFile();
        if (!rootDir.exists() || !rootDir.isDirectory()) {
            log.error("Java source directory does not exist or is not a directory: {}", javaSourceDirectory);
            return;
        }
        
        processDirectory(rootDir);
    }
    
    private void initializeJavaParser() {
        // Configure symbol solver
        CombinedTypeSolver typeSolver = new CombinedTypeSolver();
        typeSolver.add(new ReflectionTypeSolver());
        typeSolver.add(new JavaParserTypeSolver(javaSourceDirectory.toFile()));
        
        JavaSymbolSolver symbolSolver = new JavaSymbolSolver(typeSolver);
        
        ParserConfiguration config = new ParserConfiguration();
        config.setSymbolResolver(symbolSolver);
        
        this.javaParser = new JavaParser(config);
    }
    
    private void processDirectory(File directory) {
        File[] files = directory.listFiles();
        if (files == null) return;
        
        for (File file : files) {
            if (file.isDirectory()) {
                processDirectory(file);
            } else if (file.getName().endsWith(".java")) {
                try {
                    log.debug("Parsing Java file: {}", file.getPath());
                    parseJavaFile(file);
                } catch (Exception e) {
                    log.error("Error parsing Java file: {}", file.getPath(), e);
                }
            }
        }
    }
    
    private void parseJavaFile(File file) throws FileNotFoundException {
        CompilationUnit cu = javaParser.parse(file).getResult().orElseThrow(() -> 
                new FileNotFoundException("Could not parse file: " + file.getPath()));
        
        String packageName = cu.getPackageDeclaration()
                .map(pd -> pd.getName().asString())
                .orElse("");
        
        // Get or create package
        Package pkg = getOrCreatePackage(packageName);
        
        // Visit class declarations
        cu.accept(new ClassVisitor(file, pkg), null);
    }
    
    private Package getOrCreatePackage(String packageName) {
        return packageRepository.findByProjectIdAndFullPath(currentProject.getId(), packageName)
                .orElseGet(() -> {
                    Package pkg = new Package();
                    pkg.setProject(currentProject);
                    pkg.setName(getSimplePackageName(packageName));
                    pkg.setFullPath(packageName);
                    return packageRepository.save(pkg);
                });
    }
    
    private String getSimplePackageName(String fullPath) {
        int lastDotIndex = fullPath.lastIndexOf('.');
        return lastDotIndex > 0 ? fullPath.substring(lastDotIndex + 1) : fullPath;
    }
    
    private class ClassVisitor extends VoidVisitorAdapter<Void> {
        private final File file;
        private final Package pkg;
        
        public ClassVisitor(File file, Package pkg) {
            this.file = file;
            this.pkg = pkg;
        }
        
        @Override
        public void visit(ClassOrInterfaceDeclaration classDecl, Void arg) {
            super.visit(classDecl, arg);
            
            try {
                String className = classDecl.getNameAsString();
                String fullClassName = pkg.getFullPath() + "." + className;
                
                CodeClass codeClass = new CodeClass();
                codeClass.setProject(currentProject);
                codeClass.setPkg(pkg);
                codeClass.setName(className);
                codeClass.setFullName(fullClassName);
                codeClass.setFilePath(file.getAbsolutePath());
                codeClass.setClassType(classDecl.isInterface() ? 
                        CodeClass.ClassType.INTERFACE : CodeClass.ClassType.CLASS);
                codeClass.setAccessModifier(classDecl.getAccessSpecifier().asString());
                
                // Handle extends
                if (classDecl.getExtendedTypes().size() > 0) {
                    codeClass.setExtendsClass(classDecl.getExtendedTypes().get(0).getNameAsString());
                }
                
                // Handle implements
                if (classDecl.getImplementedTypes().size() > 0) {
                    StringBuilder sb = new StringBuilder();
                    classDecl.getImplementedTypes().forEach(type -> 
                            sb.append(type.getNameAsString()).append(","));
                    codeClass.setImplementsInterfaces(sb.toString());
                }
                
                // Check for Spring annotations to identify components
                classDecl.getAnnotations().forEach(annotation -> {
                    String annotationName = annotation.getNameAsString();
                    if (annotationName.equals("Component") || 
                            annotationName.equals("Service") || 
                            annotationName.equals("Repository") || 
                            annotationName.equals("Controller") || 
                            annotationName.equals("RestController")) {
                        codeClass.setIsComponent(true);
                        codeClass.setComponentType(annotationName);
                    }
                });
                
                CodeClass savedClass = codeClassRepository.save(codeClass);
                
                // Process methods
                classDecl.getMethods().forEach(methodDecl -> 
                        processMethod(methodDecl, savedClass));
                
                // Process dependencies (inheritance, implementation)
                processClassDependencies(savedClass, classDecl);
                
            } catch (Exception e) {
                log.error("Error processing class: {}", classDecl.getNameAsString(), e);
            }
        }
        
        private void processMethod(MethodDeclaration methodDecl, CodeClass codeClass) {
            String methodName = methodDecl.getNameAsString();
            String signature = methodDecl.getDeclarationAsString(false, false, false);
            
            Method method = new Method();
            method.setCodeClass(codeClass);
            method.setName(methodName);
            method.setSignature(signature);
            method.setReturnType(methodDecl.getType().asString());
            method.setAccessModifier(methodDecl.getAccessSpecifier().asString());
            method.setIsStatic(methodDecl.isStatic());
            
            // Process parameters
            if (methodDecl.getParameters().size() > 0) {
                StringBuilder sb = new StringBuilder();
                methodDecl.getParameters().forEach(param -> 
                        sb.append(param.getType().asString()).append(" ")
                          .append(param.getNameAsString()).append(","));
                method.setParameters(sb.toString());
            }
            
            // Get line numbers
            methodDecl.getBegin().ifPresent(position -> method.setStartLine(position.line));
            methodDecl.getEnd().ifPresent(position -> method.setEndLine(position.line));
            
            Method savedMethod = methodRepository.save(method);
            
            // Process method dependencies
            processMethodDependencies(savedMethod, methodDecl);
        }
        
        private void processClassDependencies(CodeClass codeClass, ClassOrInterfaceDeclaration classDecl) {
            // If class extends another class
            if (classDecl.getExtendedTypes().size() > 0) {
                String extendedClassName = classDecl.getExtendedTypes().get(0).getNameAsString();
                // Try to find the extended class in the database
                Optional<CodeClass> extendedClass = codeClassRepository.findByProjectIdAndFullName(
                        currentProject.getId(), extendedClassName);
                
                extendedClass.ifPresent(target -> {
                    Dependency dependency = new Dependency();
                    dependency.setProject(currentProject);
                    dependency.setSourceType(Dependency.ElementType.CLASS);
                    dependency.setSourceId(codeClass.getId());
                    dependency.setTargetType(Dependency.ElementType.CLASS);
                    dependency.setTargetId(target.getId());
                    dependency.setDependencyType("EXTENDS");
                    dependencyRepository.save(dependency);
                });
            }
            
            // If class implements interfaces
            classDecl.getImplementedTypes().forEach(implementedType -> {
                String interfaceName = implementedType.getNameAsString();
                // Try to find the interface in the database
                Optional<CodeClass> interfaceClass = codeClassRepository.findByProjectIdAndFullName(
                        currentProject.getId(), interfaceName);
                
                interfaceClass.ifPresent(target -> {
                    Dependency dependency = new Dependency();
                    dependency.setProject(currentProject);
                    dependency.setSourceType(Dependency.ElementType.CLASS);
                    dependency.setSourceId(codeClass.getId());
                    dependency.setTargetType(Dependency.ElementType.CLASS);
                    dependency.setTargetId(target.getId());
                    dependency.setDependencyType("IMPLEMENTS");
                    dependencyRepository.save(dependency);
                });
            });
        }
        
        private void processMethodDependencies(Method method, MethodDeclaration methodDecl) {
            // Method dependency analysis would analyze method calls
            // This is a simplified version - actual implementation would be more complex
            methodDecl.findAll(cohttp://m.github.javaparser.ast.expr.MethodCallExpr.class)
                    .forEach(methodCall -> {
                        // Try to resolve the called method
                        // This is simplified - actual resolution is more complex
                        String calledMethodName = methodCall.getNameAsString();
                        
                        // For demonstration - find methods with the same name
                        methodRepository.findAll().stream()
                                .filter(m -> m.getName().equals(calledMethodName) && 
                                        !m.getId().equals(method.getId()))
                                .findFirst()
                                .ifPresent(targetMethod -> {
                                    Dependency dependency = new Dependency();
                                    dependency.setProject(currentProject);
                                    dependency.setSourceType(Dependency.ElementType.METHOD);
                                    dependency.setSourceId(method.getId());
                                    dependency.setTargetType(Dependency.ElementType.METHOD);
                                    dependency.setTargetId(targetMethod.getId());
                                    dependency.setDependencyType("CALLS");
                                    
                                    methodCall.getBegin().ifPresent(position -> 
                                            dependency.setLineNumber(position.line));
                                    
                                    dependencyRepository.save(dependency);
                                });
                    });
        }
    }
}

IBatisSourceParser.java:

package cohttp://m.viw.itf.parser;

import cohttp://m.viw.itf.entity.Dependency;
import cohttp://m.viw.itf.entity.Method;
import cohttp://m.viw.itf.entity.Project;
import cohttp://m.viw.itf.entity.SqlQuery;
import cohttp://m.viw.itf.repository.DependencyRepository;
import cohttp://m.viw.itf.repository.MethodRepository;
import cohttp://m.viw.itf.repository.SqlQueryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
@RequiredArgsConstructor
@Slf4j
public class IBatisSourceParser {

    private final SqlQueryRepository sqlQueryRepository;
    private final MethodRepository methodRepository;
    private final DependencyRepository dependencyRepository;
    private final Path ibatisSourceDirectory;
    
    @Transactional
    public void parseIBatisFiles(Project project) {
        File rootDir = ibatisSourceDirectory.toFile();
        if (!rootDir.exists() || !rootDir.isDirectory()) {
            log.error("iBatis source directory does not exist or is not a directory: {}", ibatisSourceDirectory);
            return;
        }
        
        processDirectory(rootDir, project);
        analyzeJavaSqlDependencies(project);
    }
    
    private void processDirectory(File directory, Project project) {
        File[] files = directory.listFiles();
        if (files == null) return;
        
        for (File file : files) {
            if (file.isDirectory()) {
                processDirectory(file, project);
            } else if (file.getName().endsWith(".xml")) {
                try {
                    log.debug("Parsing iBatis file: {}", file.getPath());
                    parseIBatisFile(file, project);
                } catch (Exception e) {
                    log.error("Error parsing iBatis file: {}", file.getPath(), e);
                }
            }
        }
    }
    
    private void parseIBatisFile(File file, Project project) throws DocumentException {
        SAXReader reader = new SAXReader();
        Document document = reader.read(file);
        Element rootElement = document.getRootElement();
        
        if (!rootElement.getName().equals("sqlMap")) {
            return; // Not an iBatis SQL map file
        }
        
        String namespace = rootElement.attributeValue("namespace");
        if (namespace == null) {
            namespace = file.getName().replace(".xml", "");
        }
        
        // Process SELECT statements
        processQueryElements(rootElement.elements("select"), namespace, SqlQuery.QueryType.SELECT, file, project);
        
        // Process INSERT statements
        processQueryElements(rootElement.elements("insert"), namespace, SqlQuery.QueryType.INSERT, file, project);
        
        // Process UPDATE statements
        processQueryElements(rootElement.elements("update"), namespace, SqlQuery.QueryType.UPDATE, file, project);
        
        // Process DELETE statements
        processQueryElements(rootElement.elements("delete"), namespace, SqlQuery.QueryType.DELETE, file, project);
        
        // Process PROCEDURE statements
        processQueryElements(rootElement.elements("procedure"), namespace, SqlQuery.QueryType.PROCEDURE, file, project);
    }
    
    private void processQueryElements(List<Element> elements, String namespace, SqlQuery.QueryType queryType, 
                                     File file, Project project) {
        for (Element element : elements) {
            String id = element.attributeValue("id");
            if (id == null) continue;
            
            SqlQuery sqlQuery = new SqlQuery();
            sqlQuery.setProject(project);
            sqlQuery.setNamespace(namespace);
            sqlQuery.setQueryId(id);
            sqlQuery.setQueryType(queryType);
            sqlQuery.setQueryText(element.getTextTrim());
            sqlQuery.setFilePath(file.getAbsolutePath());
            
            sqlQueryRepository.save(sqlQuery);
        }
    }
    
    private void analyzeJavaSqlDependencies(Project project) {
        List<Method> allMethods = methodRepository.findAll();
        List<SqlQuery> allSqlQueries = sqlQueryRepository.findByProjectId(project.getId());
        
        for (Method method : allMethods) {
            // This is a simplified approach. In a real application, you would need a more robust analysis.
            String methodSource = getMethodSource(method);
            if (methodSource == null) continue;
            
            for (SqlQuery sqlQuery : allSqlQueries) {
                // Typical iBatis call patterns
                String ibatisPattern = String.format("sqlMap\\.\\w+\\([\"']%s\\.%s[\"']", 
                        Pattern.quote(sqlQuery.getNamespace()), Pattern.quote(sqlQuery.getQueryId()));
                
                Pattern pattern = Pattern.compile(ibatisPattern);
                Matcher matcher = pattern.matcher(methodSource);
                
                if (matcher.find()) {
                    Dependency dependency = new Dependency();
                    dependency.setProject(project);
                    dependency.setSourceType(Dependency.ElementType.METHOD);
                    dependency.setSourceId(method.getId());
                    dependency.setTargetType(Dependency.ElementType.SQL);
                    dependency.setTargetId(sqlQuery.getId());
                    dependency.setDependencyType("SQL_CALL");
                    dependency.setLineNumber(getLineNumber(methodSource, matcher.start()));
                    
                    dependencyRepository.save(dependency);
                }
            }
        }
    }
    
    // Simplified method to get method source - would need actual file reading in a real implementation
    private String getMethodSource(Method method) {
        // In a real implementation, you would read from the file
        // This is a placeholder
        return "public void someMethod() { sqlMap.queryForObject(\"namespace.queryId\", param); }";
    }
    
    private Integer getLineNumber(String source, int position) {
        // Count newlines up to the position
        int lines = 1;
        for (int i = 0; i < position; i++) {
            if (source.charAt(i) == '\n') {
                lines++;
            }
        }
        return lines;
    }
}

3.8 다이어그램 생성 서비스

DiagramService.java:

package cohttp://m.viw.itf.service;

import cohttp://m.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.structurizr.Workspace;
import cohttp://m.structurizr.export.plantuml.PlantUMLDiagramExporter;
import cohttp://m.structurizr.model.*;
import cohttp://m.structurizr.view.*;
import cohttp://m.viw.itf.entity.*;
import cohttp://m.viw.itf.entity.Package;
import cohttp://m.viw.itf.repository.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class DiagramService {

    private final ProjectRepository projectRepository;
    private final PackageRepository packageRepository;
    private final CodeClassRepository codeClassRepository;
    private final MethodRepository methodRepository;
    private final SqlQueryRepository sqlQueryRepository;
    private final DependencyRepository dependencyRepository;
    private final DiagramRepository diagramRepository;
    private final ObjectMapper objectMapper;
    
    @Transactional
    public Integer createComponentDiagram(Integer projectId, String name, String description) {
        Project project = projectRepository.findById(projectId)
                .orElseThrow(() -> new RuntimeException("Project not found: " + projectId));
        
        // Create a Structurizr workspace for the component diagram
        Workspace workspace = new Workspace(project.getName(), project.getDescription());
        Model model = workspace.getModel();
        
        // Create a software system to represent the project
        SoftwareSystem system = model.addSoftwareSystem(project.getName(), project.getDescription());
        
        // Find all component classes (Spring annotations)
        List<CodeClass> componentClasses = codeClassRepository.findByProjectId(projectId).stream()
                .filter(CodeClass::getIsComponent)
                .collect(Collectors.toList());
        
        // Create a container to group all components
        Container container = system.addContainer("Application", "Application Container", "Java/Spring");
        
        // Map of component ID to Structurizr Component
        Map<Integer, Component> componentMap = new HashMap<>();
        
        // Create components
        for (CodeClass componentClass : componentClasses) {
            String technology = componentClass.getComponentType() != null ? 
                    "Spring " + componentClass.getComponentType() : "Spring Component";
            
            Component component = container.addComponent(
                    componentClass.getName(),
                    componentClass.getFullName(),
                    technology);
            
            componentMap.put(componentClass.getId(), component);
        }
        
        // Add relationships between components
        for (CodeClass sourceClass : componentClasses) {
            // Find dependencies where this class is the source
            List<Dependency> dependencies = dependencyRepository.findByProjectIdAndSourceTypeAndSourceId(
                    projectId, Dependency.ElementType.CLASS, sourceClass.getId());
            
            for (Dependency dependency : dependencies) {
                if (dependency.getTargetType() == Dependency.ElementType.CLASS) {
                    Component sourceComponent = componentMap.get(sourceClass.getId());
                    Component targetComponent = componentMap.get(dependency.getTargetId());
                    
                    if (sourceComponent != null && targetComponent != null) {
                        sourceComponent.uses(targetComponent, dependency.getDependencyType());
                    }
                }
            }
        }
        
        // Create component diagram
        ViewSet viewSet = workspace.getViews();
        ComponentView componentView = viewSet.createComponentView(container, name, description);
        componentView.addAllComponents();
        
        // Export as PlantUML
        PlantUMLDiagramExporter exporter = new PlantUMLDiagramExporter();
        String plantUmlDefinition = exporter.export(componentView);
        
        // Save to diagram table
        Diagram diagram = new Diagram();
        diagram.setProject(project);
        diagram.setName(name);
        diagram.setDiagramType(Diagram.DiagramType.COMPONENT);
        diagram.setDescription(description);
        diagram.setCreationDate(LocalDateTime.now());
        diagram.setLastModified(LocalDateTime.now());
        diagram.setLayoutData(plantUmlDefinition);
        
        Diagram savedDiagram = diagramRepository.save(diagram);
        
        // Create diagram elements for each component
        for (CodeClass componentClass : componentClasses) {
            DiagramElement element = new DiagramElement();
            element.setDiagram(savedDiagram);
            element.setElementType("CLASS");
            element.setElementId(componentClass.getId());
            
            try {
                Map<String, Object> properties = new HashMap<>();
                properties.put("name", componentClass.getName());
                properties.put("type", componentClass.getComponentType());
                element.setCustomProperties(objectMapper.writeValueAsString(properties));
            } catch (JsonProcessingException e) {
                log.error("Error serializing properties", e);
            }
            
            // Positions would be set by a layout algorithm or user input
            // For now, assign random positions
            element.setXPosition(new Random().nextInt(800));
            element.setYPosition(new Random().nextInt(600));
            
            savedDiagram.getElements().add(element);
        }
        
        return savedDiagram.getId();
    }
    
    @Transactional
    public Integer createSequenceDiagram(Integer projectId, Integer methodId, String name, String description) {
        Project project = projectRepository.findById(projectId)
                .orElseThrow(() -> new RuntimeException("Project not found: " + projectId));
        
        Method rootMethod = methodRepository.findById(methodId)
                .orElseThrow(() -> new RuntimeException("Method not found: " + methodId));
        
        // Create a Structurizr workspace for the sequence diagram
        Workspace workspace = new Workspace(project.getName(), project.getDescription());
        Model model = workspace.getModel();
        
        // Create a software system
        SoftwareSystem system = model.addSoftwareSystem(project.getName(), project.getDescription());
        
        // Create a container
        Container container = system.addContainer("Application", "Application Container", "Java/Spring");
        
        // Create components for all classes involved
        Map<Integer, Component> componentMap = new HashMap<>();
        Set<Integer> processedMethodIds = new HashSet<>();
        
        // Start with the root method's class
        addComponentForClass(componentMap, container, rootMethod.getCodeClass());
        
        // Recursively process method calls
        processMethodCalls(projectId, methodId, processedMethodIds, componentMap, container);
        
        // Create dynamic view (sequence diagram)
        ViewSet viewSet = workspace.getViews();
        DynamicView dynamicView = viewSet.createDynamicView(container, name, description);
        
        // Start with the root method
        Component rootComponent = componentMap.get(rootMethod.getCodeClass().getId());
        
        // Build sequence of calls
        buildSequenceDiagram(dynamicView, rootComponent, projectId, methodId, rootMethod, new HashSet<>());
        
        // Export as PlantUML
        PlantUMLDiagramExporter exporter = new PlantUMLDiagramExporter();
        String plantUmlDefinition = exporter.export(dynamicView);
        
        // Save to diagram table
        Diagram diagram = new Diagram();
        diagram.setProject(project);
        diagram.setName(name);
        diagram.setDiagramType(Diagram.DiagramType.SEQUENCE);
        diagram.setDescription(description);
        diagram.setCreationDate(LocalDateTime.now());
        diagram.setLastModified(LocalDateTime.now());
        diagram.setLayoutData(plantUmlDefinition);
        
        return diagramRepository.save(diagram).getId();
    }
    
    private void addComponentForClass(Map<Integer, Component> componentMap, Container container, CodeClass codeClass) {
        if (!componentMap.containsKey(codeClass.getId())) {
            Component component = container.addComponent(
                    codeClass.getName(),
                    codeClass.getFullName(),
                    codeClass.getIsComponent() ? ("Spring " + codeClass.getComponentType()) : "Java Class");
            
            componentMap.put(codeClass.getId(), component);
        }
    }
    
    private void processMethodCalls(Integer projectId, Integer methodId, Set<Integer> processedMethodIds,
                                    Map<Integer, Component> componentMap, Container container) {
        if (processedMethodIds.contains(methodId)) {
            return; // Already processed
        }
        
        processedMethodIds.add(methodId);
        
        // Find method calls from this method
        List<Dependency> methodCalls = dependencyRepository.findByProjectIdAndSourceTypeAndSourceId(
                projectId, Dependency.ElementType.METHOD, methodId);
        
        for (Dependency dependency : methodCalls) {
            if (dependency.getTargetType() == Dependency.ElementType.METHOD) {
                Method targetMethod = methodRepository.findById(dependency.getTargetId())
                        .orElse(null);
                
                if (targetMethod != null) {
                    // Add component for target method's class
                    addComponentForClass(componentMap, container, targetMethod.getCodeClass());
                    
                    // Recursively process method calls
                    processMethodCalls(projectId, targetMethod.getId(), processedMethodIds, componentMap, container);
                }
            } else if (dependency.getTargetType() == Dependency.ElementType.SQL) {
                // Add a special component for database
                if (!componentMap.containsKey(-1)) { // Use a special ID for database
                    Component dbComponent = container.addComponent("Database", "Database", "MariaDB");
                    componentMap.put(-1, dbComponent);
                }
            }
        }
    }
    
    private void buildSequenceDiagram(DynamicView dynamicView, Component rootComponent,
                                     Integer projectId, Integer methodId, Method method, Set<Integer> processedCalls) {
        // Find method calls from this method
        List<Dependency> methodCalls = dependencyRepository.findByProjectIdAndSourceTypeAndSourceId(
                projectId, Dependency.ElementType.METHOD, methodId);
        
        for (Dependency dependency : methodCalls) {
            if (processedCalls.contains(dependency.getId())) {
                continue; // Avoid cycles
            }
            
            processedCalls.add(dependency.getId());
            
            if (dependency.getTargetType() == Dependency.ElementType.METHOD) {
                Method targetMethod = methodRepository.findById(dependency.getTargetId())
                        .orElse(null);
                
                if (targetMethod != null) {
                    Component targetComponent = dynamicView.getModel().getComponents().stream()
                            .filter(c -> c.getName().equals(targetMethod.getCodeClass().getName()))
                            .findFirst()
                            .orElse(null);
                    
                    if (targetComponent != null) {
                        dynamicView.add(rootComponent, targetMethod.getName(), targetComponent);
                        
                        // Recursively build sequence
                        buildSequenceDiagram(dynamicView, targetComponent, projectId, 
                                targetMethod.getId(), targetMethod, processedCalls);
                    }
                }
            } else if (dependency.getTargetType() == Dependency.ElementType.SQL) {
                SqlQuery sqlQuery = sqlQueryRepository.findById(dependency.getTargetId())
                        .orElse(null);
                
                if (sqlQuery != null) {
                    Component dbComponent = dynamicView.getModel().getComponents().stream()
                            .filter(c -> c.getName().equals("Database"))
                            .findFirst()
                            .orElse(null);
                    
                    if (dbComponent != null) {
                        dynamicView.add(rootComponent, 
                                sqlQuery.getNamespace() + "." + sqlQuery.getQueryId(), 
                                dbComponent);
                    }
                }
            }
        }
    }
    
    // Other diagram creation methods would follow a similar pattern
    @Transactional
    public Integer createClassDiagram(Integer projectId, String name, String description) {
        // Implementation similar to createComponentDiagram
        // but would include all classes, not just components
        return 0;
    }
    
    @Transactional
    public Integer createPackageDiagram(Integer projectId, String name, String description) {
        // Implementation would show package dependencies
        return 0;
    }
    
    @Transactional
    public String getDiagramContent(Integer diagramId) {
        Diagram diagram = diagramRepository.findById(diagramId)
                .orElseThrow(() -> new RuntimeException("Diagram not found: " + diagramId));
        
        return diagram.getLayoutData();
    }
    
    @Transactional
    public void updateDiagramLayout(Integer diagramId, String layoutData) {
        Diagram diagram = diagramRepository.findById(diagramId)
                .orElseThrow(() -> new RuntimeException("Diagram not found: " + diagramId));
        
        diagram.setLayoutData(layoutData);
        diagram.setLastModified(LocalDateTime.now());
        
        diagramRepository.save(diagram);
    }
}

3.9 분석 서비스

SourceAnalysisService.java:

package cohttp://m.viw.itf.service;

import cohttp://m.viw.itf.entity.Project;
import cohttp://m.viw.itf.parser.IBatisSourceParser;
import cohttp://m.viw.itf.parser.JavaSourceParser;
import cohttp://m.viw.itf.repository.ProjectRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
@Slf4j
public class SourceAnalysisService {

    private final ProjectRepository projectRepository;
    private final JavaSourceParser javaSourceParser;
    private final IBatisSourceParser ibatisSourceParser;
    
    @Transactional
    public Integer createAndAnalyzeProject(String name, String description, String javaSourcePath, 
                                         String ibatisSourcePath, String version) {
        log.info("Creating and analyzing new project: {}", name);
        
        // Create new project
        Project project = new Project();
        project.setName(name);
        project.setDescription(description);
        project.setJavaSourcePath(javaSourcePath);
        project.setIbatisSourcePath(ibatisSourcePath);
        project.setAnalysisDate(LocalDateTime.now());
        project.setVersion(version);
        
        Project savedProject = projectRepository.save(project);
        
        // Analyze source code
        analyzeProject(savedProject.getId());
        
        return savedProject.getId();
    }
    
    @Transactional
    public void analyzeProject(Integer projectId) {
        log.info("Analyzing project: {}", projectId);
        
        Project project = projectRepository.findById(projectId)
                .orElseThrow(() -> new RuntimeException("Project not found: " + projectId));
        
        // Parse Java source files
        javaSourceParser.parseJavaFiles(project);
        
        // Parse iBatis source files
        ibatisSourceParser.parseIBatisFiles(project);
        
        // Update analysis date
        project.setAnalysisDate(LocalDateTime.now());
        projectRepository.save(project);
        
        log.info("Analysis completed for project: {}", projectId);
    }
}

3.10 REST API 컨트롤러

ProjectController.java:

package cohttp://m.viw.itf.controller;

import cohttp://m.viw.itf.dto.ProjectCreateRequest;
import cohttp://m.viw.itf.entity.Project;
import cohttp://m.viw.itf.repository.ProjectRepository;
import cohttp://m.viw.itf.service.SourceAnalysisService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("/api/projects")
@RequiredArgsConstructor
public class ProjectController {

    private final ProjectRepository projectRepository;
    private final SourceAnalysisService sourceAnalysisService;
    
    @GetMapping
    public ResponseEntity<List<Project>> getAllProjects() {
        return ResponseEntity.ok(projectRepository.findAll());
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<Project> getProject(@PathVariable Integer id) {
        return projectRepository.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
    
    @PostMapping
    public ResponseEntity<Project> createProject(@Valid @RequestBody ProjectCreateRequest request) {
        Integer projectId = sourceAnalysisService.createAndAnalyzeProject(
                request.getName(),
                request.getDescription(),
                request.getJavaSourcePath(),
                request.getIbatisSourcePath(),
                request.getVersion()
        );
        
        return projectRepository.findById(projectId)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
    
    @PostMapping("/{id}/analyze")
    public ResponseEntity<Void> analyzeProject(@PathVariable Integer id) {
        sourceAnalysisService.analyzeProject(id);
        return ResponseEntity.ok().build();
    }
}

DiagramController.java:

package cohttp://m.viw.itf.controller;

import cohttp://m.viw.itf.dto.DiagramCreateRequest;
import cohttp://m.viw.itf.dto.DiagramResponse;
import cohttp://m.viw.itf.dto.DiagramUpdateRequest;
import cohttp://m.viw.itf.entity.Diagram;
import cohttp://m.viw.itf.repository.DiagramRepository;
import cohttp://m.viw.itf.service.DiagramService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/diagrams")
@RequiredArgsConstructor
public class DiagramController {

    private final DiagramRepository diagramRepository;
    private final DiagramService diagramService;
    
    @GetMapping("/project/{projectId}")
    public ResponseEntity<List<DiagramResponse>> getDiagramsByProject(@PathVariable Integer projectId) {
        List<Diagram> diagrams = diagramRepository.findByProjectId(projectId);
        
        List<DiagramResponse> response = diagrams.stream()
                .map(diagram -> new DiagramResponse(
                        diagram.getId(),
                        diagram.getName(),
                        diagram.getDiagramType().toString(),
                        diagram.getDescription(),
                        diagram.getCreationDate(),
                        diagram.getLastModified()
                ))
                .collect(Collectors.toList());
        
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<String> getDiagramContent(@PathVariable Integer id) {
        String content = diagramService.getDiagramContent(id);
        return ResponseEntity.ok(content);
    }
    
    @PostMapping("/component")
    public ResponseEntity<Integer> createComponentDiagram(@Valid @RequestBody DiagramCreateRequest request) {
        Integer diagramId = diagramService.createComponentDiagram(
                request.getProjectId(),
                request.getName(),
                request.getDescription()
        );
        
        return ResponseEntity.ok(diagramId);
    }
    
    @PostMapping("/sequence")
    public ResponseEntity<Integer> createSequenceDiagram(
            @Valid @RequestBody DiagramCreateRequest request,
            @RequestParam Integer methodId) {
        
        Integer diagramId = diagramService.createSequenceDiagram(
                request.getProjectId(),
                methodId,
                request.getName(),
                request.getDescription()
        );
        
        return ResponseEntity.ok(diagramId);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Void> updateDiagramLayout(
            @PathVariable Integer id,
            @Valid @RequestBody DiagramUpdateRequest request) {
        
        diagramService.updateDiagramLayout(id, request.getLayoutData());
        return ResponseEntity.ok().build();
    }
}

ClassController.java:

package cohttp://m.viw.itf.controller;

import cohttp://m.viw.itf.dto.ClassResponse;
import cohttp://m.viw.itf.entity.CodeClass;
import cohttp://m.viw.itf.repository.CodeClassRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/classes")
@RequiredArgsConstructor
public class ClassController {

    private final CodeClassRepository codeClassRepository;
    
    @GetMapping("/project/{projectId}")
    public ResponseEntity<List<ClassResponse>> getClassesByProject(@PathVariable Integer projectId) {
        List<CodeClass> classes = codeClassRepository.findByProjectId(projectId);
        
        List<ClassResponse> response = classes.stream()
                .map(codeClass -> new ClassResponse(
                        codeClass.getId(),
                        codeClass.getName(),
                        codeClass.getFullName(),
                        codeClass.getPkg().getFullPath(),
                        codeClass.getClassType().toString(),
                        codeClass.getIsComponent(),
                        codeClass.getComponentType()
                ))
                .collect(Collectors.toList());
        
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/package/{packageId}")
    public ResponseEntity<List<ClassResponse>> getClassesByPackage(@PathVariable Integer packageId) {
        List<CodeClass> classes = codeClassRepository.findByPkgId(packageId);
        
        List<ClassResponse> response = classes.stream()
                .map(codeClass -> new ClassResponse(
                        codeClass.getId(),
                        codeClass.getName(),
                        codeClass.getFullName(),
                        codeClass.getPkg().getFullPath(),
                        codeClass.getClassType().toString(),
                        codeClass.getIsComponent(),
                        codeClass.getComponentType()
                ))
                .collect(Collectors.toList());
        
        return ResponseEntity.ok(response);
    }
}

3.11 DTO 클래스

ProjectCreateRequest.java:

package cohttp://m.viw.itf.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;

@Data
public class ProjectCreateRequest {
    
    @NotBlank
    private String name;
    
    private String description;
    
    @NotBlank
    private String javaSourcePath;
    
    @NotBlank
    private String ibatisSourcePath;
    
    private String version;
}

DiagramCreateRequest.java:

package cohttp://m.viw.itf.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class DiagramCreateRequest {
    
    @NotNull
    private Integer projectId;
    
    @NotBlank
    private String name;
    
    private String description;
}

DiagramUpdateRequest.java:

package cohttp://m.viw.itf.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;

@Data
public class DiagramUpdateRequest {
    
    @NotBlank
    private String layoutData;
}

DiagramResponse.java:

package cohttp://m.viw.itf.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
public class DiagramResponse {
    
    private Integer id;
    private String name;
    private String type;
    private String description;
    private LocalDateTime creationDate;
    private LocalDateTime lastModified;
}

ClassResponse.java:

package cohttp://m.viw.itf.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ClassResponse {
    
    private Integer id;
    private String name;
    private String fullName;
    private String packagePath;
    private String type;
    private Boolean isComponent;
    private String componentType;
}

4. 프론트엔드 구현

4.1 package.json

{
  "name": "itf-frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@ag-grid-community/client-side-row-model": "26.1.0",
    "@ag-grid-community/core": "26.1.0",
    "@ag-grid-community/react": "26.1.0",
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "@types/jest": "^26.0.15",
    "@types/node": "^12.0.0",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "@types/react-router-dom": "^5.1.7",
    "axios": "^0.21.1",
    "bootstrap": "^5.0.1",
    "d3": "^7.0.0",
    "file-saver": "^2.0.5",
    "html2canvas": "^1.3.2",
    "html2pdf.js": "^0.9.3",
    "jspdf": "^2.3.1",
    "mermaid": "^8.11.0",
    "mobx": "^6.3.2",
    "mobx-react-lite": "^3.2.0",
    "react": "^17.0.2",
    "react-bootstrap": "^1.6.1",
    "react-dom": "^17.0.2",
    "react-hook-form": "^7.14.1",
    "react-router-dom": "^5.2.0",
    "react-scripts": "4.0.3",
    "reactflow": "^9.5.4",
    "typescript": "^4.1.2",
    "web-vitals": "^1.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/d3": "^7.0.0",
    "@types/file-saver": "^2.0.3"
  }
}

4.2 TypeScript 타입 정의 (src/types/index.ts)

// Project types
export interface Project {
  id: number;
  name: string;
  description: string;
  javaSourcePath: string;
  ibatisSourcePath: string;
  analysisDate: string;
  version: string;
}

export interface ProjectCreateRequest {
  name: string;
  description?: string;
  javaSourcePath: string;
  ibatisSourcePath: string;
  version?: string;
}

// Class types
export interface CodeClass {
  id: number;
  name: string;
  fullName: string;
  packagePath: string;
  type: string;
  isComponent: boolean;
  componentType?: string;
}

// Package types
export interface Package {
  id: number;
  name: string;
  fullPath: string;
}

// Method types
export interface Method {
  id: number;
  name: string;
  signature: string;
  returnType: string;
  parameters: string;
  accessModifier: string;
  isStatic: boolean;
}

// SQL types
export interface SqlQuery {
  id: number;
  namespace: string;
  queryId: string;
  queryType: string;
  queryText: string;
  filePath: string;
}

// Dependency types
export interface Dependency {
  id: number;
  sourceType: string;
  sourceId: number;
  targetType: string;
  targetId: number;
  dependencyType: string;
  lineNumber?: number;
}

// Diagram types
export interface Diagram {
  id: number;
  name: string;
  type: string;
  description: string;
  creationDate: string;
  lastModified: string;
}

export interface DiagramCreateRequest {
  projectId: number;
  name: string;
  description?: string;
}

export interface DiagramUpdateRequest {
  layoutData: string;
}

// Element types for diagrams
export interface DiagramElement {
  id: number;
  elementType: string;
  elementId: number;
  xPosition: number;
  yPosition: number;
  width?: number;
  height?: number;
  customProperties?: any;
}

export interface DiagramRelationship {
  id: number;
  sourceElementId: number;
  targetElementId: number;
  relationshipType: string;
  label?: string;
  controlPoints?: any;
  customProperties?: any;
}

4.3 API 서비스 (src/services/api.ts)

import axios from 'axios';
import { 
  Project, 
  ProjectCreateRequest, 
  CodeClass, 
  Method, 
  SqlQuery, 
  Diagram, 
  DiagramCreateRequest, 
  DiagramUpdateRequest 
} from '../types';

const API_BASE_URL = 'http://localhost:8080/api';

// Configure axios
axios.defaults.baseURL = API_BASE_URL;
axios.defaults.headers.common['Content-Type'] = 'application/json';

// Project API
export const projectApi = {
  getAllProjects: async (): Promise<Project[]> => {
    const response = await axios.get('/projects');
    return response.data;
  },
  
  getProject: async (id: number): Promise<Project> => {
    const response = await axios.get(`/projects/${id}`);
    return response.data;
  },
  
  createProject: async (project: ProjectCreateRequest): Promise<Project> => {
    const response = await axios.post('/projects', project);
    return response.data;
  },
  
  analyzeProject: async (id: number): Promise<void> => {
    await axios.post(`/projects/${id}/analyze`);
  }
};

// Class API
export const classApi = {
  getClassesByProject: async (projectId: number): Promise<CodeClass[]> => {
    const response = await axios.get(`/classes/project/${projectId}`);
    return response.data;
  },
  
  getClassesByPackage: async (packageId: number): Promise<CodeClass[]> => {
    const response = await axios.get(`/classes/package/${packageId}`);
    return response.data;
  },
  
  getMethodsByClass: async (classId: number): Promise<Method[]> => {
    const response = await axios.get(`/methods/class/${classId}`);
    return response.data;
  }
};

// SQL API
export const sqlApi = {
  getSqlQueriesByProject: async (projectId: number): Promise<SqlQuery[]> => {
    const response = await axios.get(`/sql/project/${projectId}`);
    return response.data;
  }
};

// Diagram API
export const diagramApi = {
  getDiagramsByProject: async (projectId: number): Promise<Diagram[]> => {
    const response = await axios.get(`/diagrams/project/${projectId}`);
    return response.data;
  },
  
  getDiagramContent: async (id: number): Promise<string> => {
    const response = await axios.get(`/diagrams/${id}`);
    return response.data;
  },
  
  createComponentDiagram: async (request: DiagramCreateRequest): Promise<number> => {
    const response = await axios.post('/diagrams/component', request);
    return response.data;
  },
  
  createSequenceDiagram: async (request: DiagramCreateRequest, methodId: number): Promise<number> => {
    const response = await axios.post(`/diagrams/sequence?methodId=${methodId}`, request);
    return response.data;
  },
  
  updateDiagramLayout: async (id: number, request: DiagramUpdateRequest): Promise<void> => {
    await axios.put(`/diagrams/${id}`, request);
  }
};

4.4 MobX 상태 관리 (src/store/index.ts)

import { makeAutoObservable, runInAction } from 'mobx';
import { 
  Project, 
  CodeClass, 
  Method, 
  SqlQuery, 
  Diagram 
} from '../types';
import { projectApi, classApi, sqlApi, diagramApi } from '../services/api';

class RootStore {
  // Projects
  projects: Project[] = [];
  selectedProject: Project | null = null;
  isLoadingProjects = false;
  
  // Classes
  classes: CodeClass[] = [];
  selectedClass: CodeClass | null = null;
  isLoadingClasses = false;
  
  // Methods
  methods: Method[] = [];
  selectedMethod: Method | null = null;
  isLoadingMethods = false;
  
  // SQL Queries
  sqlQueries: SqlQuery[] = [];
  isLoadingSqlQueries = false;
  
  // Diagrams
  diagrams: Diagram[] = [];
  selectedDiagram: Diagram | null = null;
  diagramContent: string = '';
  isLoadingDiagrams = false;
  
  constructor() {
    makeAutoObservable(this);
  }
  
  // Project actions
  async loadProjects() {
    this.isLoadingProjects = true;
    try {
      const projects = await projectApi.getAllProjects();
      runInAction(() => {
        this.projects = projects;
        this.isLoadingProjects = false;
      });
    } catch (error) {
      runInAction(() => {
        this.isLoadingProjects = false;
        console.error('Failed to load projects', error);
      });
    }
  }
  
  async loadProject(id: number) {
    this.isLoadingProjects = true;
    try {
      const project = await projectApi.getProject(id);
      runInAction(() => {
        this.selectedProject = project;
        this.isLoadingProjects = false;
      });
      return project;
    } catch (error) {
      runInAction(() => {
        this.isLoadingProjects = false;
        console.error(`Failed to load project id=${id}`, error);
      });
      return null;
    }
  }
  
  async createProject(project: Project) {
    try {
      const createdProject = await projectApi.createProject(project);
      runInAction(() => {
        this.projects.push(createdProject);
        this.selectedProject = createdProject;
      });
      return createdProject;
    } catch (error) {
      console.error('Failed to create project', error);
      return null;
    }
  }
  
  // Class actions
  async loadClassesByProject(projectId: number) {
    this.isLoadingClasses = true;
    try {
      const classes = await classApi.getClassesByProject(projectId);
      runInAction(() => {
        this.classes = classes;
        this.isLoadingClasses = false;
      });
    } catch (error) {
      runInAction(() => {
        this.isLoadingClasses = false;
        console.error(`Failed to load classes for project id=${projectId}`, error);
      });
    }
  }
  
  setSelectedClass(cls: CodeClass | null) {
    this.selectedClass = cls;
  }
  
  // Method actions
  async loadMethodsByClass(classId: number) {
    this.isLoadingMethods = true;
    try {
      const methods = await classApi.getMethodsByClass(classId);
      runInAction(() => {
        this.methods = methods;
        this.isLoadingMethods = false;
      });
    } catch (error) {
      runInAction(() => {
        this.isLoadingMethods = false;
        console.error(`Failed to load methods for class id=${classId}`, error);
      });
    }
  }
  
  setSelectedMethod(method: Method | null) {
    this.selectedMethod = method;
  }
  
  // SQL actions
  async loadSqlQueriesByProject(projectId: number) {
    this.isLoadingSqlQueries = true;
    try {
      const queries = await sqlApi.getSqlQueriesByProject(projectId);
      runInAction(() => {
        this.sqlQueries = queries;
        this.isLoadingSqlQueries = false;
      });
    } catch (error) {
      runInAction(() => {
        this.isLoadingSqlQueries = false;
        console.error(`Failed to load SQL queries for project id=${projectId}`, error);
      });
    }
  }
  
  // Diagram actions
  async loadDiagramsByProject(projectId: number) {
    this.isLoadingDiagrams = true;
    try {
      const diagrams = await diagramApi.getDiagramsByProject(projectId);
      runInAction(() => {
        this.diagrams = diagrams;
        this.isLoadingDiagrams = false;
      });
    } catch (error) {
      runInAction(() => {
        this.isLoadingDiagrams = false;
        console.error(`Failed to load diagrams for project id=${projectId}`, error);
      });
    }
  }
  
  async loadDiagramContent(id: number) {
    this.isLoadingDiagrams = true;
    try {
      const content = await diagramApi.getDiagramContent(id);
      runInAction(() => {
        this.diagramContent = content;
        this.isLoadingDiagrams = false;
      });
    } catch (error) {
      runInAction(() => {
        this.isLoadingDiagrams = false;
        console.error(`Failed to load diagram content for id=${id}`, error);
      });
    }
  }
  
  setSelectedDiagram(diagram: Diagram | null) {
    this.selectedDiagram = diagram;
  }
  
  async createComponentDiagram(projectId: number, name: string, description?: string) {
    try {
      const diagramId = await diagramApi.createComponentDiagram({
        projectId,
        name,
        description
      });
      await this.loadDiagramsByProject(projectId);
      return diagramId;
    } catch (error) {
      console.error('Failed to create component diagram', error);
      return null;
    }
  }
  
  async createSequenceDiagram(projectId: number, methodId: number, name: string, description?: string) {
    try {
      const diagramId = await diagramApi.createSequenceDiagram({
        projectId,
        name,
        description
      }, methodId);
      await this.loadDiagramsByProject(projectId);
      return diagramId;
    } catch (error) {
      console.error('Failed to create sequence diagram', error);
      return null;
    }
  }
  
  async updateDiagramLayout(id: number, layoutData: string) {
    try {
      await diagramApi.updateDiagramLayout(id, { layoutData });
      // Refresh content
      await this.loadDiagramContent(id);
    } catch (error) {
      console.error(`Failed to update diagram layout for id=${id}`, error);
    }
  }
}

export const rootStore = new RootStore();

4.5 React 컴포넌트

App.tsx

import React from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import { Container } from 'react-bootstrap';
import Navigation from './components/Navigation';
import HomePage from './pages/HomePage';
import ProjectListPage from './pages/ProjectListPage';
import ProjectDetailPage from './pages/ProjectDetailPage';
import DiagramListPage from './pages/DiagramListPage';
import DiagramDetailPage from './pages/DiagramDetailPage';
import CreateDiagramPage from './pages/CreateDiagramPage';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';

const App: React.FC = () => {
  return (
    <Router>
      <Navigation />
      <Container fluid className="app-container">
        <Switch>
          <Route exact path="/" component={HomePage} />
          <Route exact path="/projects" component={ProjectListPage} />
          <Route exact path="/projects/:projectId" component={ProjectDetailPage} />
          <Route exact path="/projects/:projectId/diagrams" component={DiagramListPage} />
          <Route exact path="/diagrams/:diagramId" component={DiagramDetailPage} />
          <Route exact path="/projects/:projectId/create-diagram" component={CreateDiagramPage} />
          <Redirect to="/" />
        </Switch>
      </Container>
    </Router>
  );
};

export default App;

src/components/Navigation.tsx

import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Navbar, Nav, Container } from 'react-bootstrap';

const Navigation: React.FC = () => {
  const location = useLocation();
  
  return (
    <Navbar bg="dark" variant="dark" expand="lg">
      <Container>
        <Navbar.Brand as={Link} to="/">Code Visualizer</Navbar.Brand>
        <Navbar.Toggle aria-controls="basic-navbar-nav" />
        <Navbar.Collapse id="basic-navbar-nav">
          <Nav className="me-auto" activeKey={location.pathname}>
            <Nav.Link as={Link} to="/">Home</Nav.Link>
            <Nav.Link as={Link} to="/projects">Projects</Nav.Link>
          </Nav>
        </Navbar.Collapse>
      </Container>
    </Navbar>
  );
};

export default Navigation;

src/pages/HomePage.tsx

import React from 'react';
import { Link } from 'react-router-dom';
import { Container, Row, Col, Card, Button } from 'react-bootstrap';

const HomePage: React.FC = () => {
  return (
    <Container className="py-5">
      <Row className="mb-4">
        <Col>
          <h1>Code Visualization Tool</h1>
          <p className="lead">
            Analyze Java and iBatis source code to generate interactive diagrams that 
            help you understand complex systems.
          </p>
        </Col>
      </Row>
      
      <Row>
        <Col md={4} className="mb-4">
          <Card>
            <Card.Body>
              <Card.Title>Source Code Analysis</Card.Title>
              <Card.Text>
                Analyze Java and iBatis source code to extract class structures, 
                methods, and dependencies.
              </Card.Text>
              <Button as={Link} to="/projects" variant="primary">
                View Projects
              </Button>
            </Card.Body>
          </Card>
        </Col>
        
        <Col md={4} className="mb-4">
          <Card>
            <Card.Body>
              <Card.Title>Interactive Diagrams</Card.Title>
              <Card.Text>
                Generate sequence diagrams, class diagrams, component diagrams,
                and more from your code.
              </Card.Text>
              <Button as={Link} to="/projects" variant="primary">
                Create Diagrams
              </Button>
            </Card.Body>
          </Card>
        </Col>
        
        <Col md={4} className="mb-4">
          <Card>
            <Card.Body>
              <Card.Title>Export & Share</Card.Title>
              <Card.Text>
                Export diagrams as PDF files for documentation and sharing
                with your team.
              </Card.Text>
              <Button as={Link} to="/projects" variant="primary">
                View Diagrams
              </Button>
            </Card.Body>
          </Card>
        </Col>
      </Row>
    </Container>
  );
};

export default HomePage;

src/pages/ProjectListPage.tsx

import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { Link } from 'react-router-dom';
import { Container, Row, Col, Card, Button, Table, Spinner, Modal, Form } from 'react-bootstrap';
import { useForm } from 'react-hook-form';
import { rootStore } from '../store';
import { ProjectCreateRequest } from '../types';

const ProjectListPage: React.FC = observer(() => {
  const { projects, isLoadingProjects, loadProjects, createProject } = rootStore;
  const [showModal, setShowModal] = useState(false);
  const { register, handleSubmit, formState: { errors }, reset } = useForm<ProjectCreateRequest>();
  
  useEffect(() => {
    loadProjects();
  }, []);
  
  const handleCreateProject = async (data: ProjectCreateRequest) => {
    await createProject(data as any);
    setShowModal(false);
    reset();
  };
  
  return (
    <Container className="py-4">
      <Row className="mb-4">
        <Col>
          <h1>Projects</h1>
        </Col>
        <Col xs="auto">
          <Button variant="primary" onClick={() => setShowModal(true)}>
            Create New Project
          </Button>
        </Col>
      </Row>
      
      {isLoadingProjects ? (
        <div className="text-center py-5">
          <Spinner animation="border" role="status">
            <span className="visually-hidden">Loading...</span>
          </Spinner>
        </div>
      ) : projects.length === 0 ? (
        <Card className="text-center py-5">
          <Card.Body>
            <Card.Title>No Projects Found</Card.Title>
            <Card.Text>
              Create a new project to get started with code analysis and visualization.
            </Card.Text>
            <Button variant="primary" onClick={() => setShowModal(true)}>
              Create New Project
            </Button>
          </Card.Body>
        </Card>
      ) : (
        <Table striped bordered hover>
          <thead>
            <tr>
              <th>Name</th>
              <th>Description</th>
              <th>Analysis Date</th>
              <th>Version</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {projects.map(project => (
              <tr key={project.id}>
                <td>{project.name}</td>
                <td>{project.description}</td>
                <td>{new Date(project.analysisDate).toLocaleString()}</td>
                <td>{project.version}</td>
                <td>
                  <Button 
                    variant="outline-primary" 
                    size="sm"
                    as={Link} 
                    to={`/projects/${project.id}`}
                    className="me-2"
                  >
                    Details
                  </Button>
                  <Button 
                    variant="outline-success" 
                    size="sm"
                    as={Link} 
                    to={`/projects/${project.id}/diagrams`}
                  >
                    Diagrams
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </Table>
      )}
      
      {/* Create Project Modal */}
      <Modal show={showModal} onHide={() => setShowModal(false)}>
        <Modal.Header closeButton>
          <Modal.Title>Create New Project</Modal.Title>
        </Modal.Header>
        <Form onSubmit={handleSubmit(handleCreateProject)}>
          <Modal.Body>
            <Form.Group className="mb-3">
              <Form.Label>Project Name</Form.Label>
              <Form.Control 
                type="text" 
                {...register('name', { required: 'Project name is required' })}
                isInvalid={!!errors.name}
              />
              <Form.Control.Feedback type="invalid">
                {errors.name?.message}
              </Form.Control.Feedback>
            </Form.Group>
            
            <Form.Group className="mb-3">
              <Form.Label>Description</Form.Label>
              <Form.Control 
                as="textarea" 
                rows={3} 
                {...register('description')}
              />
            </Form.Group>
            
            <Form.Group className="mb-3">
              <Form.Label>Java Source Path</Form.Label>
              <Form.Control 
                type="text" 
                {...register('javaSourcePath', { required: 'Java source path is required' })}
                isInvalid={!!errors.javaSourcePath}
                placeholder="C:\work_back\viw_ep_edi\src\main\java\com\viw\esp\online"
              />
              <Form.Control.Feedback type="invalid">
                {errors.javaSourcePath?.message}
              </Form.Control.Feedback>
            </Form.Group>
            
            <Form.Group className="mb-3">
              <Form.Label>iBatis Source Path</Form.Label>
              <Form.Control 
                type="text" 
                {...register('ibatisSourcePath', { required: 'iBatis source path is required' })}
                isInvalid={!!errors.ibatisSourcePath}
                placeholder="C:\work_back\viw_erp_wms\src\main\resources\sql"
              />
              <Form.Control.Feedback type="invalid">
                {errors.ibatisSourcePath?.message}
              </Form.Control.Feedback>
            </Form.Group>
            
            <Form.Group className="mb-3">
              <Form.Label>Version</Form.Label>
              <Form.Control 
                type="text" 
                {...register('version')}
              />
            </Form.Group>
          </Modal.Body>
          <Modal.Footer>
            <Button variant="secondary" onClick={() => setShowModal(false)}>
              Cancel
            </Button>
            <Button variant="primary" type="submit">
              Create Project
            </Button>
          </Modal.Footer>
        </Form>
      </Modal>
    </Container>
  );
});

export default ProjectListPage;

src/pages/ProjectDetailPage.tsx

import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { useParams, Link } from 'react-router-dom';
import { Container, Row, Col, Card, Button, Tabs, Tab, ListGroup, Badge, Spinner } from 'react-bootstrap';
import { rootStore } from '../store';
import { CodeClass, Method, SqlQuery } from '../types';

interface ParamTypes {
  projectId: string;
}

const ProjectDetailPage: React.FC = observer(() => {
  const { projectId } = useParams<ParamTypes>();
  const { 
    selectedProject, 
    classes, 
    methods, 
    sqlQueries,
    isLoadingProject, 
    isLoadingClasses, 
    isLoadingMethods, 
    isLoadingSqlQueries,
    loadProject,
    loadClassesByProject,
    loadMethodsByClass,
    loadSqlQueriesByProject,
    setSelectedClass
  } = rootStore;
  
  const [selectedClassId, setSelectedClassId] = useState<number | null>(null);
  
  useEffect(() => {
    const id = parseInt(projectId);
    loadProject(id);
    loadClassesByProject(id);
    loadSqlQueriesByProject(id);
  }, [projectId]);
  
  const handleClassClick = (cls: CodeClass) => {
    setSelectedClassId(cls.id);
    setSelectedClass(cls);
    loadMethodsByClass(cls.id);
  };
  
  if (isLoadingProject) {
    return (
      <div className="text-center py-5">
        <Spinner animation="border" role="status">
          <span className="visually-hidden">Loading project...</span>
        </Spinner>
      </div>
    );
  }
  
  if (!selectedProject) {
    return (
      <Container className="py-4">
        <Card className="text-center py-5">
          <Card.Body>
            <Card.Title>Project Not Found</Card.Title>
            <Button as={Link} to="/projects" variant="primary">
              Back to Projects
            </Button>
          </Card.Body>
        </Card>
      </Container>
    );
  }
  
  return (
    <Container fluid className="py-4">
      <Row className="mb-4">
        <Col>
          <h1>{selectedProject.name}</h1>
          <p>{selectedProject.description}</p>
        </Col>
        <Col xs="auto">
          <Button 
            variant="primary" 
            as={Link} 
            to={`/projects/${projectId}/create-diagram`}
            className="me-2"
          >
            Create Diagram
          </Button>
          <Button 
            variant="outline-primary" 
            as={Link} 
            to={`/projects/${projectId}/diagrams`}
          >
            View Diagrams
          </Button>
        </Col>
      </Row>
      
      <Row>
        <Col>
          <Tabs defaultActiveKey="classes" className="mb-3">
            <Tab eventKey="classes" title="Classes">
              <Row>
                <Col md={4}>
                  <Card>
                    <Card.Header>
                      <strong>Classes</strong>
                      {isLoadingClasses && (
                        <Spinner animation="border" size="sm" className="ms-2" />
                      )}
                    </Card.Header>
                    <ListGroup variant="flush" style={{ maxHeight: '600px', overflowY: 'auto' }}>
                      {classes.map(cls => (
                        <ListGroup.Item 
                          key={cls.id} 
                          action 
                          active={selectedClassId === cls.id}
                          onClick={() => handleClassClick(cls)}
                        >
                          <div className="d-flex justify-content-between align-items-center">
                            <div>
                              <div className="fw-bold">{cls.name}</div>
                              <small className="text-muted">{cls.packagePath}</small>
                            </div>
                            <div>
                              <Badge bg={cls.type === 'CLASS' ? 'primary' : 'info'}>
                                {cls.type}
                              </Badge>
                              {cls.isComponent && (
                                <Badge bg="success" className="ms-1">
                                  {cls.componentType}
                                </Badge>
                              )}
                            </div>
                          </div>
                        </ListGroup.Item>
                      ))}
                    </ListGroup>
                  </Card>
                </Col>
                <Col md={8}>
                  <Card>
                    <Card.Header>
                      <strong>Methods</strong>
                      {isLoadingMethods && (
                        <Spinner animation="border" size="sm" className="ms-2" />
                      )}
                    </Card.Header>
                    <ListGroup variant="flush" style={{ maxHeight: '600px', overflowY: 'auto' }}>
                      {selectedClassId === null ? (
                        <ListGroup.Item>Select a class to view its methods</ListGroup.Item>
                      ) : methods.length === 0 ? (
                        <ListGroup.Item>No methods found</ListGroup.Item>
                      ) : (
                        methods.map(method => (
                          <ListGroup.Item key={method.id}>
                            <div className="d-flex justify-content-between align-items-center">
                              <div>
                                <div className="fw-bold">{method.name}</div>
                                <small className="text-muted">{method.signature}</small>
                              </div>
                              <div>
                                <Button 
                                  variant="outline-primary" 
                                  size="sm"
                                  as={Link}
                                  to={`/projects/${projectId}/create-diagram?type=sequence&methodId=${method.id}`}
                                >
                                  Sequence Diagram
                                </Button>
                              </div>
                            </div>
                          </ListGroup.Item>
                        ))
                      )}
                    </ListGroup>
                  </Card>
                </Col>
              </Row>
            </Tab>
            <Tab eventKey="sql" title="SQL Queries">
              <Card>
                <Card.Header>
                  <strong>SQL Queries</strong>
                  {isLoadingSqlQueries && (
                    <Spinner animation="border" size="sm" className="ms-2" />
                  )}
                </Card.Header>
                <ListGroup variant="flush" style={{ maxHeight: '600px', overflowY: 'auto' }}>
                  {sqlQueries.length === 0 ? (
                    <ListGroup.Item>No SQL queries found</ListGroup.Item>
                  ) : (
                    sqlQueries.map(query => (
                      <ListGroup.Item key={query.id}>
                        <div>
                          <div className="d-flex justify-content-between">
                            <div className="fw-bold">
                              {query.namespace}.{query.queryId}
                            </div>
                            <Badge bg={
                              query.queryType === 'SELECT' ? 'success' :
                              query.queryType === 'INSERT' ? 'primary' :
                              query.queryType === 'UPDATE' ? 'warning' :
                              query.queryType === 'DELETE' ? 'danger' : 'secondary'
                            }>
                              {query.queryType}
                            </Badge>
                          </div>
                          <small className="text-muted d-block mt-1">{query.filePath}</small>
                          <pre className="mt-2 p-2 bg-light rounded">
                            <code>{query.queryText.substring(0, 200)}...</code>
                          </pre>
                        </div>
                      </ListGroup.Item>
                    ))
                  )}
                </ListGroup>
              </Card>
            </Tab>
          </Tabs>
        </Col>
      </Row>
    </Container>
  );
});

export default ProjectDetailPage;

src/pages/DiagramListPage.tsx

import React, { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import { useParams, Link } from 'react-router-dom';
import { Container, Row, Col, Card, Button, Spinner, Table } from 'react-bootstrap';
import { rootStore } from '../store';

interface ParamTypes {
  projectId: string;
}

const DiagramListPage: React.FC = observer(() => {
  const { projectId } = useParams<ParamTypes>();
  const { 
    selectedProject, 
    diagrams, 
    isLoadingProject, 
    isLoadingDiagrams,
    loadProject,
    loadDiagramsByProject
  } = rootStore;
  
  useEffect(() => {
    const id = parseInt(projectId);
    loadProject(id);
    loadDiagramsByProject(id);
  }, [projectId]);
  
  if (isLoadingProject || isLoadingDiagrams) {
    return (
      <div className="text-center py-5">
        <Spinner animation="border" role="status">
          <span className="visually-hidden">Loading...</span>
        </Spinner>
      </div>
    );
  }
  
  if (!selectedProject) {
    return (
      <Container className="py-4">
        <Card className="text-center py-5">
          <Card.Body>
            <Card.Title>Project Not Found</Card.Title>
            <Button as={Link} to="/projects" variant="primary">
              Back to Projects
            </Button>
          </Card.Body>
        </Card>
      </Container>
    );
  }
  
  return (
    <Container className="py-4">
      <Row className="mb-4">
        <Col>
          <h1>Diagrams for {selectedProject.name}</h1>
        </Col>
        <Col xs="auto">
          <Button 
            variant="primary" 
            as={Link} 
            to={`/projects/${projectId}/create-diagram`}
          >
            Create New Diagram
          </Button>
        </Col>
      </Row>
      
      {diagrams.length === 0 ? (
        <Card className="text-center py-5">
          <Card.Body>
            <Card.Title>No Diagrams Found</Card.Title>
            <Card.Text>
              Create a new diagram to visualize the structure and behavior of your code.
            </Card.Text>
            <Button 
              variant="primary" 
              as={Link} 
              to={`/projects/${projectId}/create-diagram`}
            >
              Create New Diagram
            </Button>
          </Card.Body>
        </Card>
      ) : (
        <Table striped bordered hover>
          <thead>
            <tr>
              <th>Name</th>
              <th>Type</th>
              <th>Description</th>
              <th>Last Modified</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {diagrams.map(diagram => (
              <tr key={diagram.id}>
                <td>{diagram.name}</td>
                <td>{diagram.type}</td>
                <td>{diagram.description}</td>
                <td>{new Date(diagram.lastModified).toLocaleString()}</td>
                <td>
                  <Button 
                    variant="outline-primary" 
                    size="sm"
                    as={Link} 
                    to={`/diagrams/${diagram.id}`}
                  >
                    View
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </Table>
      )}
      
      <div className="mt-4">
        <Button variant="secondary" as={Link} to={`/projects/${projectId}`}>
          Back to Project
        </Button>
      </div>
    </Container>
  );
});

export default DiagramListPage;

src/pages/DiagramDetailPage.tsx

import React, { useEffect, useRef } from 'react';
import { observer } from 'mobx-react-lite';
import { useParams, Link } from 'react-router-dom';
import { Container, Row, Col, Card, Button, Spinner } from 'react-bootstrap';
import mermaid from 'mermaid';
import { rootStore } from '../store';
import { exportToPdf } from '../utils/export';

interface ParamTypes {
  diagramId: string;
}

const DiagramDetailPage: React.FC = observer(() => {
  const { diagramId } = useParams<ParamTypes>();
  const { 
    selectedDiagram,
    diagramContent,
    isLoadingDiagrams,
    setSelectedDiagram,
    loadDiagramContent
  } = rootStore;
  
  const diagramRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    const id = parseInt(diagramId);
    
    // Configure mermaid
    mermaid.initialize({
      startOnLoad: true,
      theme: 'default',
      securityLevel: 'loose'
    });
    
    loadDiagramContent(id);
    
    // Clean up
    return () => {
      setSelectedDiagram(null);
    };
  }, [diagramId]);
  
  useEffect(() => {
    if (diagramContent && diagramRef.current) {
      // Clear previous content
      diagramRef.current.innerHTML = '';
      
      // Set content
      diagramRef.current.innerHTML = diagramContent;
      
      // Initialize mermaid diagrams
      mermaid.init(undefined, diagramRef.current);
    }
  }, [diagramContent]);
  
  const handleExportPdf = () => {
    if (diagramRef.current && selectedDiagram) {
      exportToPdf(diagramRef.current, selectedDiagram.name);
    }
  };
  
  if (isLoadingDiagrams) {
    return (
      <div className="text-center py-5">
        <Spinner animation="border" role="status">
          <span className="visually-hidden">Loading diagram...</span>
        </Spinner>
      </div>
    );
  }
  
  return (
    <Container fluid className="py-4">
      <Row className="mb-3">
        <Col>
          <h1>{selectedDiagram?.name}</h1>
          <p>{selectedDiagram?.description}</p>
        </Col>
        <Col xs="auto">
          <Button 
            variant="primary" 
            onClick={handleExportPdf}
          >
            Export to PDF
          </Button>
        </Col>
      </Row>
      
      <Card>
        <Card.Body>
          <div ref={diagramRef} className="diagram-container">
            {/* Diagram content will be rendered here */}
          </div>
        </Card.Body>
      </Card>
    </Container>
  );
});

export default DiagramDetailPage;

src/pages/CreateDiagramPage.tsx

import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { useParams, useLocation, useHistory } from 'react-router-dom';
import { Container, Row, Col, Card, Button, Form, Spinner } from 'react-bootstrap';
import { useForm } from 'react-hook-form';
import { rootStore } from '../store';
import { DiagramCreateRequest } from '../types';

interface ParamTypes {
  projectId: string;
}

const CreateDiagram

tag: none

댓글추가