앱 메인 파일(app.py) 삭제 및 데이터베이스 관련 파일 수정: 데이터베이스 테이블 이름 변경, UI 업데이트 강제 호출 추가, 고장 코드 검색 및 관련 기능 개선
This commit is contained in:
parent
391cb0dc7e
commit
189ea70f3a
74
app.py
74
app.py
|
|
@ -1,74 +0,0 @@
|
||||||
import flet as ft
|
|
||||||
from database.db_manager import DatabaseManager
|
|
||||||
from ui.fault_finder import FaultFinderUI
|
|
||||||
|
|
||||||
def main(page: ft.Page):
|
|
||||||
"""
|
|
||||||
메인 애플리케이션 함수입니다.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
page (ft.Page): Flet 페이지 객체
|
|
||||||
"""
|
|
||||||
# 페이지 설정
|
|
||||||
page.title = "고장 코드 검색 애플리케이션"
|
|
||||||
page.theme_mode = ft.ThemeMode.LIGHT
|
|
||||||
page.window_width = 1000
|
|
||||||
page.window_height = 800
|
|
||||||
page.window_min_width = 800
|
|
||||||
page.window_min_height = 600
|
|
||||||
page.padding = 10
|
|
||||||
page.bgcolor = ft.colors.WHITE
|
|
||||||
page.scroll = ft.ScrollMode.AUTO
|
|
||||||
|
|
||||||
# 데이터베이스 관리자 초기화
|
|
||||||
db_manager = DatabaseManager(db_path="fault_codes.db")
|
|
||||||
|
|
||||||
# UI 컴포넌트 초기화
|
|
||||||
fault_finder_ui = FaultFinderUI(page, db_manager)
|
|
||||||
|
|
||||||
# 앱 타이틀
|
|
||||||
app_title = ft.Container(
|
|
||||||
content=ft.Row([
|
|
||||||
ft.Icon(ft.icons.SEARCH, color=ft.colors.BLUE, size=32),
|
|
||||||
ft.Text("철도차량 고장코드 검색 시스템", size=24, weight=ft.FontWeight.BOLD),
|
|
||||||
], alignment=ft.MainAxisAlignment.CENTER),
|
|
||||||
margin=ft.margin.only(bottom=10),
|
|
||||||
padding=10,
|
|
||||||
bgcolor=ft.colors.BLUE_50,
|
|
||||||
border_radius=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 메인 컨텐츠 영역
|
|
||||||
main_content = ft.Container(
|
|
||||||
content=fault_finder_ui.container,
|
|
||||||
expand=True,
|
|
||||||
bgcolor=ft.colors.WHITE,
|
|
||||||
border_radius=10,
|
|
||||||
border=ft.border.all(1, ft.colors.BLUE_200),
|
|
||||||
padding=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 푸터
|
|
||||||
footer = ft.Container(
|
|
||||||
content=ft.Row([
|
|
||||||
ft.Text("© 2023 철도차량 정비 시스템", size=12, color=ft.colors.GREY_700),
|
|
||||||
ft.Text("버전 1.0", size=12, color=ft.colors.GREY_700),
|
|
||||||
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
|
|
||||||
margin=ft.margin.only(top=10),
|
|
||||||
padding=10,
|
|
||||||
bgcolor=ft.colors.BLUE_50,
|
|
||||||
border_radius=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 레이아웃 구성
|
|
||||||
page.add(
|
|
||||||
app_title,
|
|
||||||
main_content,
|
|
||||||
footer,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 초기 데이터 로드
|
|
||||||
fault_finder_ui.load_all_codes()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
ft.app(target=main)
|
|
||||||
|
|
@ -18,7 +18,7 @@ def clean_text(text):
|
||||||
prefixes = [
|
prefixes = [
|
||||||
"반응 : ", "반응\n1)", "반응\n0)",
|
"반응 : ", "반응\n1)", "반응\n0)",
|
||||||
"검지조건 : ", "검지조건\n1)", "검지조건\n0)",
|
"검지조건 : ", "검지조건\n1)", "검지조건\n0)",
|
||||||
"소거 : ", "소거조건\n1)", "소거조건\n0)",
|
"소거조건 : ", "소거조건\n1)", "소거조건\n0)",
|
||||||
"조치방법 : ", "조치방법\n1)", "조치방법\n0)",
|
"조치방법 : ", "조치방법\n1)", "조치방법\n0)",
|
||||||
"관련도면 : ", "관련도면\n1)", "관련도면\n0)"
|
"관련도면 : ", "관련도면\n1)", "관련도면\n0)"
|
||||||
]
|
]
|
||||||
|
|
@ -57,11 +57,11 @@ def clean_database():
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 테이블의 모든 레코드 가져오기
|
# 테이블의 모든 레코드 가져오기
|
||||||
cursor.execute("SELECT * FROM woojin200")
|
cursor.execute("SELECT * FROM fault_code_list")
|
||||||
records = cursor.fetchall()
|
records = cursor.fetchall()
|
||||||
|
|
||||||
# 컬럼 이름 가져오기
|
# 컬럼 이름 가져오기
|
||||||
cursor.execute("PRAGMA table_info(woojin200)")
|
cursor.execute("PRAGMA table_info(fault_code_list)")
|
||||||
columns = [column[1] for column in cursor.fetchall()]
|
columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
|
||||||
# 업데이트할 레코드 준비
|
# 업데이트할 레코드 준비
|
||||||
|
|
@ -71,22 +71,22 @@ def clean_database():
|
||||||
record_dict = dict(zip(columns, record))
|
record_dict = dict(zip(columns, record))
|
||||||
|
|
||||||
# 각 텍스트 필드 정제
|
# 각 텍스트 필드 정제
|
||||||
record_dict['reaction'] = clean_text(record_dict['reaction'])
|
# record_dict['reaction'] = clean_text(record_dict['reaction'])
|
||||||
record_dict['detect_condition'] = clean_text(record_dict['detect_condition'])
|
# record_dict['detect_condition'] = clean_text(record_dict['detect_condition'])
|
||||||
record_dict['clear_condition'] = clean_text(record_dict['clear_condition'])
|
record_dict['clear_condition'] = clean_text(record_dict['clear_condition'])
|
||||||
|
|
||||||
# action에서 drawing 추출
|
# action에서 drawing 추출
|
||||||
record_dict['action'], record_dict['drawing'] = extract_drawing(record_dict['action'])
|
# record_dict['action'], record_dict['drawing'] = extract_drawing(record_dict['action'])
|
||||||
record_dict['action'] = clean_text(record_dict['action'])
|
# record_dict['action'] = clean_text(record_dict['action'])
|
||||||
|
|
||||||
# 업데이트할 레코드 준비
|
# 업데이트할 레코드 준비
|
||||||
updated_record = tuple(record_dict[col] for col in columns)
|
updated_record = tuple(record_dict[col] for col in columns)
|
||||||
updated_records.append(updated_record)
|
updated_records.append(updated_record)
|
||||||
|
|
||||||
# 테이블 업데이트
|
# 테이블 업데이트
|
||||||
cursor.execute("DELETE FROM woojin200")
|
cursor.execute("DELETE FROM fault_code_list")
|
||||||
cursor.executemany(f"""
|
cursor.executemany(f"""
|
||||||
INSERT INTO woojin200 ({','.join(columns)})
|
INSERT INTO fault_code_list ({','.join(columns)})
|
||||||
VALUES ({','.join(['?']*len(columns))})
|
VALUES ({','.join(['?']*len(columns))})
|
||||||
""", updated_records)
|
""", updated_records)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def clean_fault_code_list():
|
||||||
|
"""
|
||||||
|
fault_code_list 테이블에서 외래키(manufacturer_id)가 설정되지 않은 레코드를 삭제합니다.
|
||||||
|
이 스크립트는 관리자용으로, 데이터베이스 정리 전용입니다.
|
||||||
|
"""
|
||||||
|
conn = sqlite3.connect('fault_codes.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 현재 상태 확인
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM fault_code_list")
|
||||||
|
total_records = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM fault_code_list WHERE manufacturer_id IS NULL")
|
||||||
|
null_records = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
print(f"현재 총 레코드 수: {total_records}")
|
||||||
|
print(f"manufacturer_id가 NULL인 레코드 수: {null_records}")
|
||||||
|
|
||||||
|
# 외래키 제약 활성화
|
||||||
|
cursor.execute("PRAGMA foreign_keys = ON")
|
||||||
|
|
||||||
|
# 외래키가 설정되지 않은 레코드 삭제
|
||||||
|
cursor.execute("DELETE FROM fault_code_list WHERE manufacturer_id IS NULL")
|
||||||
|
deleted = cursor.rowcount
|
||||||
|
|
||||||
|
print(f"삭제된 레코드 수: {deleted}")
|
||||||
|
|
||||||
|
# 제작사 테이블 중복 제거
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM manufacturers
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM manufacturers
|
||||||
|
GROUP BY name
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
deleted_manufacturers = cursor.rowcount
|
||||||
|
print(f"제거된 중복 제작사 수: {deleted_manufacturers}")
|
||||||
|
|
||||||
|
# 외래키 설정 업데이트
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE fault_code_list
|
||||||
|
SET manufacturer_id = (
|
||||||
|
SELECT id FROM manufacturers
|
||||||
|
WHERE name = '우진'
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE manufacturer_id IS NULL
|
||||||
|
""")
|
||||||
|
|
||||||
|
updated = cursor.rowcount
|
||||||
|
print(f"업데이트된 레코드 수: {updated}")
|
||||||
|
|
||||||
|
# 최종 상태 확인
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM fault_code_list")
|
||||||
|
final_records = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM manufacturers")
|
||||||
|
manufacturer_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
print(f"최종 레코드 수: {final_records}")
|
||||||
|
print(f"제작사 수: {manufacturer_count}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("데이터베이스 정리가 완료되었습니다.")
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
conn.rollback()
|
||||||
|
print(f"오류 발생: {e}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
user_input = input("이 스크립트는 데이터베이스에서 외래키가 설정되지 않은 레코드를 삭제합니다. 계속하시겠습니까? (y/n): ")
|
||||||
|
if user_input.lower() == 'y':
|
||||||
|
clean_fault_code_list()
|
||||||
|
else:
|
||||||
|
print("스크립트 실행이 취소되었습니다.")
|
||||||
|
|
@ -2,7 +2,7 @@ import sqlite3
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
class DatabaseManager:
|
class DatabaseManager:
|
||||||
"""데이터베이스 관리 클래스"""
|
"""데이터베이스 관리 클래스 - 조회 전용"""
|
||||||
|
|
||||||
def __init__(self, db_path: str = "fault_codes.db"):
|
def __init__(self, db_path: str = "fault_codes.db"):
|
||||||
"""
|
"""
|
||||||
|
|
@ -11,93 +11,181 @@ class DatabaseManager:
|
||||||
db_path (str): 데이터베이스 파일 경로
|
db_path (str): 데이터베이스 파일 경로
|
||||||
"""
|
"""
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self._create_tables()
|
self.conn = None
|
||||||
|
|
||||||
def _create_tables(self):
|
def _get_connection(self):
|
||||||
"""필요한 테이블 생성"""
|
"""데이터베이스 연결을 반환합니다."""
|
||||||
conn = sqlite3.connect(self.db_path)
|
return sqlite3.connect(self.db_path)
|
||||||
|
|
||||||
|
def get_manufacturers(self) -> List[str]:
|
||||||
|
"""제작사 목록을 가져옵니다."""
|
||||||
|
conn = self._get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT name FROM manufacturers ORDER BY name")
|
||||||
# 고장 코드 테이블 생성
|
manufacturers = [row[0] for row in cursor.fetchall()]
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS fault_codes (
|
|
||||||
code TEXT PRIMARY KEY,
|
|
||||||
description TEXT,
|
|
||||||
solution TEXT
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
return manufacturers
|
||||||
def search_fault_codes(self, search_term: str) -> List[Dict[str, str]]:
|
|
||||||
"""
|
def get_fault_types(self) -> List[str]:
|
||||||
고장 코드 검색
|
"""고장 타입 목록을 가져옵니다."""
|
||||||
Args:
|
conn = self._get_connection()
|
||||||
search_term (str): 검색어
|
cursor = conn.cursor()
|
||||||
Returns:
|
cursor.execute("SELECT DISTINCT fault_type FROM fault_code_list WHERE fault_type IS NOT NULL ORDER BY fault_type")
|
||||||
List[Dict[str, str]]: 검색 결과 목록
|
types = [row[0] for row in cursor.fetchall()]
|
||||||
"""
|
conn.close()
|
||||||
conn = sqlite3.connect(self.db_path)
|
return types
|
||||||
|
|
||||||
|
def get_fault_codes(self, manufacturer: Optional[str] = None,
|
||||||
|
fault_type: Optional[str] = None) -> List[Dict]:
|
||||||
|
"""고장 코드 목록을 가져옵니다."""
|
||||||
|
conn = self._get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
if search_term:
|
query = """
|
||||||
# 코드나 설명에서 검색어 찾기
|
SELECT f.fault_code, f.fault_name, f.fault_type, m.name as manufacturer
|
||||||
cursor.execute('''
|
FROM fault_code_list f
|
||||||
SELECT fault_code, fault_name, fault_level, fault_type,
|
LEFT JOIN manufacturers m ON f.manufacturer_id = m.id
|
||||||
fault_detail, reaction, detect_condition, clear_condition, action
|
WHERE f.manufacturer_id IS NOT NULL
|
||||||
FROM fault_codes
|
"""
|
||||||
WHERE fault_name LIKE ? OR fault_detail LIKE ?
|
params = []
|
||||||
ORDER BY fault_code
|
|
||||||
''', (f'%{search_term}%', f'%{search_term}%'))
|
|
||||||
else:
|
|
||||||
# 전체 목록 가져오기
|
|
||||||
cursor.execute('''
|
|
||||||
SELECT fault_code, fault_name, fault_level, fault_type,
|
|
||||||
fault_detail, reaction, detect_condition, clear_condition, action
|
|
||||||
FROM fault_codes
|
|
||||||
ORDER BY fault_code
|
|
||||||
''')
|
|
||||||
|
|
||||||
|
if manufacturer:
|
||||||
|
query += " AND m.name = ?"
|
||||||
|
params.append(manufacturer)
|
||||||
|
if fault_type:
|
||||||
|
query += " AND f.fault_type = ?"
|
||||||
|
params.append(fault_type)
|
||||||
|
|
||||||
|
query += " ORDER BY f.fault_code"
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
results = cursor.fetchall()
|
results = cursor.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'code': str(code),
|
'code': row[0],
|
||||||
'description': f"[{level}] {name} - {detail}",
|
'name': row[1],
|
||||||
'solution': f"조치방법:\n{action}\n\n검지조건:\n{detect}\n\n소거조건:\n{clear}"
|
'type': row[2],
|
||||||
|
'manufacturer': row[3]
|
||||||
}
|
}
|
||||||
for code, name, level, type_, detail, reaction, detect, clear, action in results
|
for row in results
|
||||||
]
|
]
|
||||||
|
|
||||||
def add_fault(self, code: str, description: str, solution: Optional[str] = None,
|
def search_fault_codes(self, search_term: str) -> List[Dict]:
|
||||||
category: Optional[str] = None) -> bool:
|
"""고장 코드를 검색합니다."""
|
||||||
"""
|
conn = self._get_connection()
|
||||||
새로운 고장 코드를 추가합니다.
|
cursor = conn.cursor()
|
||||||
|
|
||||||
Args:
|
query = """
|
||||||
code (str): 고장 코드
|
SELECT f.fault_code, f.fault_name, f.fault_type, m.name as manufacturer
|
||||||
description (str): 설명
|
FROM fault_code_list f
|
||||||
solution (Optional[str]): 해결 방법
|
LEFT JOIN manufacturers m ON f.manufacturer_id = m.id
|
||||||
category (Optional[str]): 카테고리
|
WHERE f.manufacturer_id IS NOT NULL
|
||||||
|
AND (f.fault_code LIKE ? OR f.fault_name LIKE ?)
|
||||||
Returns:
|
ORDER BY f.fault_code
|
||||||
bool: 추가 성공 여부
|
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
cursor.execute(query, (f'%{search_term}%', f'%{search_term}%'))
|
||||||
cursor = conn.cursor()
|
results = cursor.fetchall()
|
||||||
cursor.execute("""
|
conn.close()
|
||||||
INSERT INTO fault_codes (code, description, solution, category)
|
|
||||||
VALUES (?, ?, ?, ?)
|
return [
|
||||||
""", (code, description, solution, category))
|
{
|
||||||
conn.commit()
|
'code': row[0],
|
||||||
return True
|
'name': row[1],
|
||||||
except sqlite3.Error:
|
'type': row[2],
|
||||||
return False
|
'manufacturer': row[3]
|
||||||
|
}
|
||||||
|
for row in results
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_abbreviations(self) -> List[Dict]:
|
||||||
|
"""약어 목록을 가져옵니다."""
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT abbreviation, description FROM abbreviations ORDER BY abbreviation")
|
||||||
|
results = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'abbreviation': row[0],
|
||||||
|
'description': row[1]
|
||||||
|
}
|
||||||
|
for row in results
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_drawings(self) -> List[Dict]:
|
||||||
|
"""도면 목록을 가져옵니다."""
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT drawing_code, drawing_name, related_device FROM drawings ORDER BY drawing_code")
|
||||||
|
results = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'code': row[0],
|
||||||
|
'name': row[1],
|
||||||
|
'device': row[2]
|
||||||
|
}
|
||||||
|
for row in results
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_signals(self) -> List[Dict]:
|
||||||
|
"""신호 목록을 가져옵니다."""
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT signal_code, signal_name, signal_type FROM signals ORDER BY signal_code")
|
||||||
|
results = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'code': row[0],
|
||||||
|
'name': row[1],
|
||||||
|
'type': row[2]
|
||||||
|
}
|
||||||
|
for row in results
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_fault_detail(self, fault_code: str) -> Optional[Dict]:
|
||||||
|
"""특정 고장 코드의 상세 정보를 가져옵니다."""
|
||||||
|
conn = self._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT f.fault_code, f.fault_name, f.fault_level, f.fault_type,
|
||||||
|
f.fault_detail, f.reaction, f.detect_condition, f.clear_condition,
|
||||||
|
f.action, f.drawing, m.name as manufacturer
|
||||||
|
FROM fault_code_list f
|
||||||
|
LEFT JOIN manufacturers m ON f.manufacturer_id = m.id
|
||||||
|
WHERE f.fault_code = ? AND f.manufacturer_id IS NOT NULL
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(query, (fault_code,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'code': result[0],
|
||||||
|
'name': result[1],
|
||||||
|
'level': result[2],
|
||||||
|
'type': result[3],
|
||||||
|
'detail': result[4],
|
||||||
|
'reaction': result[5],
|
||||||
|
'detect_condition': result[6],
|
||||||
|
'clear_condition': result[7],
|
||||||
|
'action': result[8],
|
||||||
|
'drawing': result[9],
|
||||||
|
'manufacturer': result[10]
|
||||||
|
}
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""데이터베이스 연결을 종료합니다."""
|
"""데이터베이스 연결을 종료합니다."""
|
||||||
if hasattr(self, 'conn'):
|
if hasattr(self, 'conn') and self.conn:
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
BIN
fault_codes.db
BIN
fault_codes.db
Binary file not shown.
3
main.py
3
main.py
|
|
@ -28,6 +28,7 @@ def main(page: ft.Page):
|
||||||
|
|
||||||
# 초기 로딩 시 전체 코드 목록 표시
|
# 초기 로딩 시 전체 코드 목록 표시
|
||||||
fault_finder.load_all_codes()
|
fault_finder.load_all_codes()
|
||||||
|
page.update() # UI 업데이트 강제 호출
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
ft.app(target=main)
|
ft.app(target=main)
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue