Java & iBatis 소스 코드 분석 및 시각화 도구 개발
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