lscsc/Process_dash_pub
0
1# -*- coding: utf-8 -*-2 3import streamlit as st4import plotly.graph_objects as go5import pandas as pd6import numpy as np7import warnings8import platform9import plotly.express as px10from scipy import stats11import streamlit.components.v1 as components12from datetime import datetime, timedelta13import os14import logging15from pathlib import Path16import json17from functools import lru_cache18import base64 # 이미지 인코딩을 위해 추가19import re # 정규식 처리를 위해 추가20 21# 로깅 설정22logging.basicConfig(23 level=logging.INFO,24 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'25)26logger = logging.getLogger('품질대시보드')27 28# 상수 정의29CONFIG = {30 "APP_TITLE": "품질 공정정보 대시보드",31 "APP_ICON": "📊",32 "PASSWORD": "1234",33 "VERSION": "1.0.2", # 버전 업데이트34 "LAST_UPDATE": "2025-03-19", # 날짜 업데이트35 "DATA_PATHS": {36 "process_info": "process_info/process_info.xlsx",37 "quality_issues": "quality_issues/quality_issues.xlsx",38 "process_specs": "process_specs/process_specs.xlsx",39 "process_capability": "process_capability/process_capability.xlsx",40 "defect_rates": "defect_rates/defect_rates.xlsx",41 "customer_info": "customer_info/customer_info.xlsx",42 "process_infographic": "process_infographic/GF_process_infographic.html"43 },44 "PROCESS_IMAGES_PATH": "process_image", # 공정 이미지 경로 추가45 "PROCESS_ICONS": {46 "Batch": "flask",47 "Forming": "industry",48 "Sizing": "tint",49 "Winding": "sync",50 "Drying": "fire",51 "Packing": "box"52 },53 "PROCESS_COLORS": {54 "Batch": "#4285F4", # 파란색55 "Forming": "#EA4335", # 빨간색56 "Sizing": "#FBBC05", # 노란색57 "Winding": "#34A853", # 녹색58 "Drying": "#FF6D01", # 주황색59 "Packing": "#46BDC6" # 청록색60 },61 "PROCESS_NAME_MAPPING": {62 "Batch": "Batch(배합)",63 "Forming": "섬유화",64 "Sizing": "SIZE",65 "Winding": "Winding",66 "Drying": "Drying",67 "Packing": "포장",68 "인수검사": "인수검사",69 "용융": "용융",70 "Chopping": "Chopping",71 "Pelletizer": "Pelletizer",72 "Water Spray": "Water Spray"73 },74 "STOPWORDS": [75 '인한', '로', '으로', '에', '에서', '의', '이', '가', '을', '를', '은', '는', 76 '와', '과', '이나', '또는', '및', '등', '에서의', '로의', '로써', '로서', '하여', 77 '하는', '하게', '하다', '한', '된', '되는', '되어', '때문에', '때문', '통한', 78 '통해', '의한', '의해', '에의한', '에의해', '불균일로', '편차로', '마모로', '저하로', '발생'79 ]80}81 82# 페이지 설정83def setup_page():84 """페이지 기본 설정"""85 st.set_page_config(86 page_title=CONFIG["APP_TITLE"],87 page_icon=CONFIG["APP_ICON"],88 layout="wide",89 initial_sidebar_state="expanded"90 )91 92 # 경고 메시지 무시93 warnings.filterwarnings('ignore')94 95 # 스타일 적용96 add_sidebar_styles()97 add_main_styles()98 99# 스타일 함수100def add_sidebar_styles():101 """사이드바 스타일 추가 함수 - 간결한 디자인"""102 st.markdown(103 """104 <style>105 /* 간결한 사이드바 기본 스타일 */106 [data-testid="stSidebar"] {107 background-color: #f8f9fa;108 padding: 1rem 0.5rem;109 color: #333;110 }111 112 /* 사이드바 내용 컨테이너 */113 [data-testid="stSidebar"] > div:first-child {114 padding: 1rem 0.5rem;115 }116 117 /* 사이드바 제목 스타일 */118 [data-testid="stSidebar"] [data-testid="stMarkdown"] h1 {119 font-size: 1.3rem;120 color: #2C3E50;121 font-weight: 500;122 margin-bottom: 1rem;123 padding-bottom: 0.5rem;124 border-bottom: 1px solid #e0e0e0;125 }126 127 /* 사이드바 소제목 스타일 */128 [data-testid="stSidebar"] [data-testid="stMarkdown"] h3 {129 font-size: 1rem;130 color: #2C3E50;131 margin-top: 1rem;132 margin-bottom: 0.5rem;133 font-weight: 500;134 }135 136 /* 구분선 스타일 */137 [data-testid="stSidebar"] hr {138 border-color: #e0e0e0;139 margin: 1rem 0;140 }141 142 /* 라디오 버튼 컨테이너 스타일 */143 [data-testid="stSidebar"] [data-testid="stRadio"] > div {144 display: flex;145 flex-direction: column;146 gap: 0.25rem;147 }148 149 /* 라디오 버튼 레이블 스타일 */150 [data-testid="stSidebar"] [data-testid="stRadio"] > div > label {151 border-radius: 4px;152 padding: 0.5rem 0.5rem 0.5rem 1.5rem; /* 왼쪽 패딩 증가 */153 transition: all 0.2s ease;154 border-left: 2px solid transparent;155 }156 157 /* 라디오 버튼 호버 효과 */158 [data-testid="stSidebar"] [data-testid="stRadio"] > div > label:hover {159 background-color: rgba(0, 0, 0, 0.05);160 border-left: 2px solid #2563eb;161 }162 163 /* 선택된 라디오 버튼 스타일 */164 [data-testid="stSidebar"] [data-testid="stRadio"] > div > label[aria-checked="true"] {165 background-color: rgba(37, 99, 235, 0.1);166 border-left: 2px solid #2563eb;167 }168 169 /* 버튼 스타일 */170 [data-testid="stSidebar"] button {171 width: 100%;172 background-color: #f1f3f5;173 color: #333;174 border-radius: 4px;175 padding: 0.5rem;176 font-weight: 500;177 border: 1px solid #dee2e6;178 margin-top: 1rem;179 transition: all 0.2s ease;180 }181 182 /* 버튼 호버 효과 */183 [data-testid="stSidebar"] button:hover {184 background-color: #e9ecef;185 border-color: #ced4da;186 }187 188 /* 로고 컨테이너 스타일 */189 .sidebar-logo-container {190 text-align: center;191 padding: 0.5rem 0;192 margin-bottom: 1rem;193 }194 195 /* 로고 이미지 스타일 */196 .sidebar-logo-container img {197 max-width: 60px;198 height: auto;199 }200 201 /* 사이드바 푸터 스타일 */202 .sidebar-footer {203 margin-top: 1rem;204 padding-top: 0.5rem;205 border-top: 1px solid #e0e0e0;206 font-size: 0.75rem;207 color: #6c757d;208 text-align: center;209 }210 </style>211 """, 212 unsafe_allow_html=True213 )214 215def add_main_styles():216 """메인 컨텐츠 스타일 추가 함수 - 간결한 디자인"""217 st.markdown(218 """219 <style>220 /* 전체 페이지 스타일 */221 .stApp {222 background-color: #ffffff;223 }224 225 /* 메인 컨테이너 스타일 */226 [data-testid="stAppViewContainer"] {227 background-color: #ffffff;228 padding: 0.5rem;229 }230 231 /* 메인 컨텐츠 영역 스타일 */232 .main .block-container {233 padding: 1.5rem;234 max-width: 1200px;235 margin: 0 auto;236 }237 238 /* 타이틀 스타일 */239 h1 {240 color: #2C3E50;241 font-weight: 600;242 font-size: 1.8rem;243 margin-bottom: 2rem;244 padding-bottom: 0.5rem;245 border-bottom: 1px solid #f0f0f0;246 }247 248 /* 서브 타이틀 스타일 */249 h2 {250 color: #2C3E50;251 margin-top: 2rem;252 margin-bottom: 1rem;253 font-weight: 500;254 font-size: 1.4rem;255 }256 257 /* h3 특별 스타일 - 메인 타이틀과의 간격 */258 h3 {259 color: #2C3E50;260 margin-top: 3rem;261 margin-bottom: 1rem;262 font-weight: 500;263 font-size: 1.2rem;264 padding-left: 1rem; /* 왼쪽 여백 증가 */265 border-left: 4px solid #2563EB; /* 왼쪽 테두리 추가 */266 }267 268 /* 위젯 간격 조정 */269 .stSelectbox, .stMultiSelect {270 margin-bottom: 0.75rem;271 }272 273 /* 메트릭 카드 스타일 */274 [data-testid="stMetric"] {275 background-color: #f8f9fa;276 border-radius: 6px;277 padding: 0.75rem !important;278 border-left: 3px solid #2563eb;279 }280 281 /* 메트릭 레이블 스타일 */282 [data-testid="stMetric"] > div:first-child {283 color: #6c757d;284 }285 286 /* 메트릭 값 스타일 */287 [data-testid="stMetric"] > div:nth-child(2) {288 font-size: 1.3rem;289 font-weight: 600;290 color: #2C3E50;291 }292 293 /* 데이터프레임 스타일 */294 .dataframe {295 border-collapse: collapse !important;296 width: 100% !important;297 border-radius: 6px;298 overflow: hidden;299 }300 301 .dataframe th {302 background-color: #f1f3f5 !important;303 color: #495057 !important;304 font-weight: 500 !important;305 padding: 0.5rem 0.75rem !important;306 text-align: left !important;307 }308 309 .dataframe td {310 padding: 0.5rem 0.75rem !important;311 border-top: 1px solid #f0f0f0 !important;312 }313 314 .dataframe tr:nth-child(even) {315 background-color: #f8f9fa !important;316 }317 318 /* 탭 스타일 개선 */319 .stTabs [data-baseweb="tab-list"] {320 gap: 0;321 border-bottom: 1px solid #e0e0e0;322 }323 324 .stTabs [data-baseweb="tab"] {325 height: 40px;326 white-space: pre-wrap;327 background-color: transparent;328 border-radius: 0;329 border-bottom: 2px solid transparent;330 padding: 0 1rem;331 font-weight: 500;332 color: #6c757d;333 transition: all 0.2s ease;334 }335 336 .stTabs [aria-selected="true"] {337 color: #2563eb !important;338 border-bottom: 2px solid #2563eb !important;339 background-color: transparent !important;340 }341 342 /* 이슈 박스 스타일 */343 .issue-box {344 background-color: #f8f9fa;345 border-radius: 6px;346 padding: 1rem;347 margin: 0.75rem 0;348 border-left: 3px solid #2563eb;349 }350 351 /* 공정 박스 스타일 */352 .process-box {353 background-color: #f8f9fa;354 border-radius: 6px;355 padding: 1rem;356 margin: 0.75rem 0;357 border-left: 3px solid #2563eb;358 }359 360 /* 불량 박스 스타일 */361 .defect-box {362 background-color: #f8f9fa;363 border-radius: 6px;364 padding: 1rem;365 margin: 0.75rem 0;366 border-left: 3px solid #2563eb;367 }368 369 /* 서브헤더 스타일 추가 */370 .stMarkdown h3 {371 margin-left: 0.5rem;372 padding-left: 1rem;373 }374 375 /* 추가 마크다운 내 소제목 스타일 */376 .stMarkdown h4 {377 margin-left: 0.75rem;378 }379 </style>380 """, 381 unsafe_allow_html=True382 )383 384# 인증 관련 함수385def check_password():386 """비밀번호 확인 함수"""387 if "password_correct" not in st.session_state:388 st.session_state["password_correct"] = False389 390 if not st.session_state["password_correct"]:391 # 비밀번호 입력 필드를 한 번만 표시392 with st.container():393 password = st.text_input(394 "비밀번호를 입력하세요", 395 type="password",396 key="password_input"397 )398 399 if password:400 if password == CONFIG["PASSWORD"]:401 st.session_state["password_correct"] = True402 # 비밀번호가 맞으면 페이지 리프레시403 st.rerun()404 else:405 st.error("비밀번호가 올바르지 않습니다.")406 return False407 return False408 409 return True410 411# 세션 상태 초기화412def initialize_session_state():413 """세션 상태 초기화 함수"""414 # 기본 세션 상태 변수들 초기화415 default_states = {416 'password_correct': False,417 'selected_product': None,418 'selected_data': None,419 'tab_selection': "공정 현황",420 'data_loaded': False,421 'data_dict': {},422 'last_data_refresh': None423 }424 425 # 세션 상태에 없는 변수들만 초기화426 for key, value in default_states.items():427 if key not in st.session_state:428 st.session_state[key] = value429 430# 데이터 관련 함수431@lru_cache(maxsize=32)432def safe_read_excel(filepath, default_data=None):433 """안전하게 엑셀 파일을 읽는 함수 (캐싱 적용)"""434 try:435 if os.path.exists(filepath):436 logger.info(f"파일 로드 중: {filepath}")437 return pd.read_excel(filepath)438 else:439 logger.warning(f"파일을 찾을 수 없습니다: {filepath}")440 return default_data if default_data is not None else pd.DataFrame()441 except Exception as e:442 logger.error(f"파일 읽기 오류 ({filepath}): {str(e)}")443 return default_data if default_data is not None else pd.DataFrame()444 445def load_data():446 """데이터 로드 함수 (개선된 버전)"""447 # 이미 로드된 데이터가 있고, 마지막 새로고침 시간이 30분 이내면 재사용448 if (st.session_state.data_loaded and 449 st.session_state.last_data_refresh is not None and450 (datetime.now() - st.session_state.last_data_refresh).total_seconds() < 1800):451 logger.info("캐시된 데이터 사용")452 return st.session_state.data_dict453 454 try:455 # 데이터 사전 초기화456 data_dict = {}457 458 # 각 데이터 파일 로드 (안전하게)459 for key, path in CONFIG["DATA_PATHS"].items():460 if key != "process_infographic": # HTML 파일은 별도 처리461 data_dict[key] = safe_read_excel(path)462 463 # 날짜 형식 변환 (품질이슈정보의 날짜 컬럼)464 if not data_dict['quality_issues'].empty and '기준일' in data_dict['quality_issues'].columns:465 data_dict['quality_issues']['기준일'] = pd.to_datetime(data_dict['quality_issues']['기준일'], errors='coerce')466 467 # 데이터 로드 상태 및 데이터 저장468 st.session_state.data_loaded = True469 st.session_state.data_dict = data_dict470 st.session_state.last_data_refresh = datetime.now()471 472 # 성공 메시지 제거 (st.success 라인 삭제)473 logger.info("모든 데이터 로드 완료")474 475 return data_dict476 except Exception as e:477 error_msg = f"데이터 로드 중 오류 발생: {str(e)}"478 logger.error(error_msg)479 st.error(error_msg)480 return {}481 482def load_process_infographic():483 """공정 인포그래픽 HTML 로드 함수"""484 try:485 filepath = CONFIG["DATA_PATHS"]["process_infographic"]486 if os.path.exists(filepath):487 with open(filepath, 'r', encoding='utf-8') as f:488 html_content = f.read()489 return html_content490 else:491 logger.warning(f"인포그래픽 파일을 찾을 수 없습니다: {filepath}")492 # 대체 HTML 콘텐츠 제공493 return """494 <div style="text-align: center; padding: 20px; background-color: #f8f9fa; border-radius: 10px;">495 <h3>인포그래픽을 불러올 수 없습니다</h3>496 <p>파일을 찾을 수 없거나 접근할 수 없습니다.</p>497 </div>498 """499 except Exception as e:500 logger.warning(f"인포그래픽 로드 중 오류 발생: {str(e)}")501 # 오류 발생 시 대체 HTML 콘텐츠 제공502 return """503 <div style="text-align: center; padding: 20px; background-color: #f8f9fa; border-radius: 10px;">504 <h3>인포그래픽 로드 오류</h3>505 <p>파일을 읽는 중 오류가 발생했습니다.</p>506 </div>507 """508 509# 계산 및 분석 함수510def calculate_process_capability(data, ucl, lcl, sigma_level=3):511 """공정능력지수 계산 함수"""512 try:513 if len(data) == 0:514 return {515 'Cp': 0,516 'Cpu': 0,517 'Cpl': 0,518 'Cpk': 0,519 'PPM': 0520 }521 522 mean = data.mean()523 std = data.std()524 525 if std == 0:526 return {527 'Cp': float('inf'),528 'Cpu': float('inf'),529 'Cpl': float('inf'),530 'Cpk': float('inf'),531 'PPM': 0532 }533 534 # 공정능력지수 계산535 cp = (ucl - lcl) / (6 * std) if std != 0 else float('inf')536 cpu = (ucl - mean) / (3 * std) if std != 0 else float('inf')537 cpl = (mean - lcl) / (3 * std) if std != 0 else float('inf')538 cpk = min(cpu, cpl)539 540 # 예상불량률 계산 (ppm 단위)541 z_upper = (ucl - mean) / std if std != 0 else float('inf')542 z_lower = (mean - lcl) / std if std != 0 else float('inf')543 ppm_upper = stats.norm.sf(z_upper) * 1000000544 ppm_lower = stats.norm.sf(z_lower) * 1000000545 total_ppm = ppm_upper + ppm_lower546 547 return {548 'Cp': cp,549 'Cpu': cpu,550 'Cpl': cpl,551 'Cpk': cpk,552 'PPM': total_ppm553 }554 555 except Exception as e:556 logger.error(f"공정능력지수 계산 중 오류 발생: {str(e)}")557 return {558 'Cp': 0,559 'Cpu': 0,560 'Cpl': 0,561 'Cpk': 0,562 'PPM': 0563 }564 565def extract_keywords(text, stopwords=None, min_length=2):566 """텍스트에서 키워드 추출 함수"""567 if stopwords is None:568 stopwords = CONFIG["STOPWORDS"]569 570 # 텍스트를 단어로 분리571 words = text.replace(',', ' ').replace('.', ' ').replace('(', ' ').replace(')', ' ').split()572 573 keywords = {}574 575 for word in words:576 word = word.strip()577 578 # 불용어 제거 및 길이 체크579 if word not in stopwords and len(word) >= min_length:580 # 단어 끝에 붙은 '로', '에', '의' 등 제거581 for suffix in ['로', '에', '의', '을', '를', '은', '는']:582 if word.endswith(suffix) and len(word) > len(suffix):583 word = word[:-len(suffix)]584 585 # 단어가 여전히 최소 길이 이상인지 확인586 if len(word) >= min_length:587 if word in keywords:588 keywords[word] += 1589 else:590 keywords[word] = 1591 592 return keywords593 594# 이미지 관련 유틸리티 함수 추가595def get_image_as_base64(file_path):596 """이미지 파일을 base64로 인코딩하여 반환하는 함수"""597 try:598 if os.path.exists(file_path):599 with open(file_path, "rb") as image_file:600 encoded = base64.b64encode(image_file.read()).decode()601 return f"data:image/png;base64,{encoded}"602 else:603 logger.warning(f"이미지 파일이 존재하지 않습니다: {file_path}")604 return None605 except Exception as e:606 logger.error(f"이미지 인코딩 중 오류 발생: {str(e)}")607 return None608 609# 시각화 함수610def create_process_flow_html(process_stages, process_info_df):611 """공정 흐름도 HTML 생성 함수 - 이미지 처리 개선 버전"""612 # 기본 색상 및 아이콘 설정613 default_color = "#6C757D"614 default_icon = "cog"615 616 # 공정명과 이미지 파일명 매핑 - 모두 소문자로 통일617 process_image_mapping = {618 "batch": "mixing.png",619 "forming": "fiberizing.png",620 "sizing": "sizing.png", 621 "winding": "winding.png",622 "drying": "fbd.png",623 "packing": "packing.png",624 "인수검사": "packing.png",625 "용융": "furnace.png",626 "chopping": "chopping.png",627 "pelletizer": "pelletizer.png",628 "water spray": "water_spray.png", # 공백 대신 언더스코어 사용629 "fbd": "fbd.png",630 # 누락된 공정 이미지 매핑 추가631 "성형": "fiberizing.png", # '성형'은 'forming'과 동일하게 매핑632 "가공": "fbd.png", # 가공 이미지633 "검사/포장": "packing.png", # 검사/포장 이미지634 # 추가 매핑 - 특수문자 및 대소문자 변환 전에 매핑되도록635 "검사": "packing.png"636 }637 638 # 디버깅을 위한 로깅 추가639 logger.info(f"공정 단계 목록: {process_stages}")640 641 # 이미지 파일 디렉토리 확인 로깅642 image_dir = CONFIG['PROCESS_IMAGES_PATH']643 if os.path.exists(image_dir):644 logger.info(f"이미지 디렉토리 존재: {image_dir}")645 # 디렉토리 내 파일 목록 로깅646 files = os.listdir(image_dir)647 logger.info(f"디렉토리 내 파일 목록: {files}")648 else:649 logger.error(f"이미지 디렉토리가 존재하지 않음: {image_dir}")650 651 # HTML로 공정 흐름도 생성652 flow_html = """653 <style>654 /* 공정 흐름도 컨테이너 스타일 */655 .flow-container {656 display: flex;657 flex-wrap: nowrap;658 overflow-x: auto;659 padding: 1rem 0;660 align-items: center;661 max-width: 100%;662 }663 664 /* 각 공정 단계 스타일 */665 .flow-step {666 display: flex;667 flex-direction: row;668 align-items: center;669 flex: 0 0 auto;670 margin-right: 1rem;671 }672 673 /* 화살표 스타일 */674 .arrow {675 margin: 0 1rem;676 color: #6c757d;677 font-size: 1.2rem;678 }679 680 /* 아이콘 컨테이너 스타일 */681 .flow-icon {682 width: 70px;683 height: 70px;684 border-radius: 50%;685 border: 2px solid #4285F4;686 display: flex;687 align-items: center;688 justify-content: center;689 margin-right: 0.5rem;690 background-color: white;691 overflow: hidden;692 cursor: pointer;693 flex-shrink: 0;694 }695 696 /* 아이콘 이미지 스타일 */697 .flow-icon img {698 width: 50px;699 height: 50px;700 object-fit: contain;701 }702 703 /* 공정 아이콘 스타일 */704 .flow-icon i {705 font-size: 2rem;706 }707 708 /* 공정 단계 콘텐츠 스타일 */709 .flow-step-content {710 display: flex;711 flex-direction: column;712 min-width: 100px;713 max-width: 150px;714 }715 716 /* 공정 단계 제목 스타일 */717 .flow-step-title {718 font-weight: bold;719 margin-bottom: 0.25rem;720 color: #333;721 }722 723 /* 공정 단계 설명 스타일 */724 .flow-step-desc {725 font-size: 0.8rem;726 color: #666;727 white-space: nowrap;728 overflow: hidden;729 text-overflow: ellipsis;730 }731 732 /* 마지막 단계 스타일 */733 .flow-step.last-step {734 margin-right: 0;735 }736 737 /* 반응형 스타일 */738 @media (max-width: 768px) {739 .flow-icon {740 width: 50px;741 height: 50px;742 }743 744 .flow-icon img {745 width: 35px;746 height: 35px;747 }748 749 .flow-step-content {750 min-width: 80px;751 }752 }753 </style>754 755 <div class="flow-container">756 """757 758 # 각 공정 단계별 HTML 생성759 for i, stage in enumerate(process_stages):760 # 해당 공정 단계의 세부 공정들 추출761 sub_processes = process_info_df[process_info_df['표준단위공정'] == stage]['실공정(Step)'].tolist()762 sub_process_text = ", ".join(sub_processes)763 764 # 아이콘 및 색상 결정765 icon = CONFIG["PROCESS_ICONS"].get(stage, default_icon)766 color = CONFIG["PROCESS_COLORS"].get(stage, default_color)767 768 # 마지막 단계 여부 확인769 is_last = (i == len(process_stages) - 1)770 last_class = " last-step" if is_last else ""771 772 # 이미지 파일 경로 결정 (매핑 로직 개선)773 image_file = None774 775 # 1. 원본 이름으로 매핑 먼저 시도 (특수문자가 있는 경우도 처리)776 if stage in process_image_mapping:777 image_file = process_image_mapping[stage]778 logger.info(f"원본 이름으로 이미지 매핑: '{stage}' → {image_file}")779 780 # 2. 소문자 변환 후 매핑 시도781 elif stage.lower() in process_image_mapping:782 image_file = process_image_mapping[stage.lower()]783 logger.info(f"소문자 변환으로 이미지 매핑: '{stage}' → {image_file}")784 785 # 3. 특수문자 제거 및 공백 언더스코어 변환 후 매핑 시도786 else:787 # 특수문자를 공백으로 치환하고 연속된 공백을 하나로 합침788 normalized_stage = re.sub(r'[^\w\s]', ' ', stage).lower()789 normalized_stage = re.sub(r'\s+', ' ', normalized_stage).strip()790 791 # 공백을 언더스코어로 변경792 normalized_stage_us = normalized_stage.replace(' ', '_')793 794 if normalized_stage in process_image_mapping:795 image_file = process_image_mapping[normalized_stage]796 logger.info(f"정규화 이름으로 이미지 매핑: '{stage}' → '{normalized_stage}' → {image_file}")797 elif normalized_stage_us in process_image_mapping:798 image_file = process_image_mapping[normalized_stage_us]799 logger.info(f"언더스코어 변환으로 이미지 매핑: '{stage}' → '{normalized_stage_us}' → {image_file}")800 else:801 # 부분 매칭 시도 (예: "검사/포장" → "검사"로 부분 매칭)802 for key in process_image_mapping:803 if key in stage.lower() or stage.lower() in key:804 image_file = process_image_mapping[key]805 logger.info(f"부분 매칭으로 이미지 매핑: '{stage}' 내에서 '{key}' 발견 → {image_file}")806 break807 808 if not image_file:809 logger.warning(f"단계 '{stage}'에 대한 이미지 파일 매핑 없음, 기본 아이콘 사용")810 811 # 이미지 파일 경로 완성 및 존재 여부 확인812 image_path = None813 base64_image = None814 815 if image_file:816 # 절대 경로 사용817 image_path = os.path.join(os.path.abspath(CONFIG['PROCESS_IMAGES_PATH']), image_file)818 logger.info(f"이미지 경로: {image_path}")819 820 if os.path.exists(image_path):821 try:822 base64_image = get_image_as_base64(image_path)823 logger.info(f"이미지 인코딩 성공: {image_path}")824 except Exception as e:825 logger.error(f"이미지 인코딩 오류: {image_path}, 오류: {str(e)}")826 else:827 # 파일이 없을 경우 대체 파일 시도 (확장자 변경)828 alt_extensions = ['.jpg', '.jpeg', '.gif', '.png']829 for ext in alt_extensions:830 alt_path = os.path.splitext(image_path)[0] + ext831 if os.path.exists(alt_path):832 try:833 base64_image = get_image_as_base64(alt_path)834 logger.info(f"대체 이미지 인코딩 성공: {alt_path}")835 break836 except Exception as e:837 logger.error(f"대체 이미지 인코딩 오류: {alt_path}, 오류: {str(e)}")838 839 if not base64_image:840 logger.warning(f"이미지 파일이 존재하지 않음: {image_path}")841 842 # 세부 공정 설명 추출 (첫 번째 세부 공정의 설명 사용)843 process_description = ""844 if sub_processes:845 first_sub_process = process_info_df[846 (process_info_df['표준단위공정'] == stage) & 847 (process_info_df['실공정(Step)'] == sub_processes[0])848 ]849 if not first_sub_process.empty:850 process_description = first_sub_process.iloc[0].get('공정내용', '')851 852 # base64로 인코딩된 이미지가 있는 경우와 없는 경우 처리853 if base64_image:854 # 이미지가 있는 경우 base64 인코딩된 이미지 표시855 icon_content = f"""856 <img src="{base64_image}" alt="{stage}" style="width: 100%; height: 100%; object-fit: contain;">857 """858 else:859 # 이미지가 없는 경우 아이콘 표시860 icon_content = f"""861 <i class="fas fa-{icon}" style="color: {color}; font-size: 2rem;"></i>862 """863 864 # 단계별 HTML 추가865 flow_html += f"""866 <div class="flow-step{last_class}">867 <div class="flow-icon" style="border-color: {color};">868 {icon_content}869 </div>870 <div class="flow-step-content">871 <div class="flow-step-title">{stage}</div>872 <div class="flow-step-desc">{sub_process_text[:30]}{'...' if len(sub_process_text) > 30 else ''}</div>873 </div>874 {'' if is_last else '<div class="arrow"><i class="fas fa-chevron-right"></i></div>'}875 </div>876 """877 878 # FontAwesome 추가879 flow_html += """880 </div>881 882 <!-- FontAwesome 추가 -->883 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">884 """885 886 return flow_html887 888def create_bar_chart(data, x, y, title, color_scale='Viridis', height=400):889 """막대 차트 생성 함수"""890 fig = px.bar(891 data,892 x=x,893 y=y,894 title=title,895 color=y,896 color_continuous_scale=color_scale897 )898 899 fig.update_layout(900 xaxis_title=x,901 yaxis_title=y,902 height=height903 )904 905 return fig906 907def create_pie_chart(data, values, names, title, hole=0.4, color_sequence=None):908 """파이 차트 생성 함수"""909 fig = px.pie(910 data,911 values=values,912 names=names,913 title=title,914 hole=hole,915 color_discrete_sequence=color_sequence916 )917 918 fig.update_traces(textposition='inside', textinfo='percent+label')919 return fig920 921def create_line_chart(x, y, title, line_color='blue', marker_size=8, height=400):922 """선 차트 생성 함수"""923 fig = go.Figure()924 925 fig.add_trace(go.Scatter(926 x=x,927 y=y,928 mode='lines+markers',929 name='데이터',930 line=dict(color=line_color, width=2),931 marker=dict(size=marker_size)932 ))933 934 fig.update_layout(935 title=title,936 xaxis_title="X축",937 yaxis_title="Y축",938 height=height939 )940 941 return fig942 943def create_pareto_chart(data, x, y, title, height=500):944 """파레토 차트 생성 함수"""945 fig = go.Figure()946 947 # 불량량 막대 그래프948 fig.add_trace(go.Bar(949 x=data[x],950 y=data[y],951 name='불량량',952 marker=dict(color='indianred')953 ))954 955 # 누적 불량량 계산956 data['누적비율'] = data[y].cumsum() / data[y].sum() * 100957 958 # 누적 비율 선 그래프959 fig.add_trace(go.Scatter(960 x=data[x],961 y=data['누적비율'],962 name='누적 비율',963 mode='lines+markers',964 yaxis='y2',965 line=dict(color='royalblue', width=2),966 marker=dict(size=8)967 ))968 969 # 80% 기준선970 fig.add_trace(go.Scatter(971 x=[data[x].iloc[0], data[x].iloc[-1]],972 y=[80, 80],973 name='80% 기준',974 mode='lines',975 yaxis='y2',976 line=dict(color='green', dash='dash')977 ))978 979 # 그래프 레이아웃 설정980 fig.update_layout(981 title=title,982 xaxis_title=x,983 yaxis_title=y,984 yaxis2=dict(985 title="누적 비율 (%)",986 overlaying='y',987 side='right',988 range=[0, 100]989 ),990 legend=dict(991 orientation="h",992 yanchor="bottom",993 y=1.02,994 xanchor="right",995 x=1996 ),997 height=height998 )999 1000 return fig1001 1002def create_histogram_with_normal(values, usl, lsl, mean, std, title, height=400):1003 """히스토그램 및 정규분포 생성 함수"""1004 hist_fig = go.Figure()1005 1006 # 히스토그램 추가1007 hist_fig.add_trace(go.Histogram(1008 x=values,1009 name='검사값 분포',1010 opacity=0.7,1011 marker=dict(color='royalblue'),1012 histnorm='probability density'1013 ))1014 1015 # 정규분포 곡선 추가1016 x_range = np.linspace(values.min() - 0.5, values.max() + 0.5, 100)1017 y_range = stats.norm.pdf(x_range, mean, std)1018 1019 hist_fig.add_trace(go.Scatter(1020 x=x_range,1021 y=y_range,1022 mode='lines',1023 name='정규분포',1024 line=dict(color='red', width=2)1025 ))1026 1027 # 규격 상한/하한 추가1028 hist_fig.add_trace(go.Scatter(1029 x=[usl, usl],1030 y=[0, max(y_range) * 1.2],1031 mode='lines',1032 name='상한 규격',1033 line=dict(color='green', width=2, dash='dash')1034 ))1035 1036 hist_fig.add_trace(go.Scatter(1037 x=[lsl, lsl],1038 y=[0, max(y_range) * 1.2],1039 mode='lines',1040 name='하한 규격',1041 line=dict(color='green', width=2, dash='dash')1042 ))1043 1044 # 평균선 추가1045 hist_fig.add_trace(go.Scatter(1046 x=[mean, mean],1047 y=[0, max(y_range) * 1.2],1048 mode='lines',1049 name='평균',1050 line=dict(color='black', width=2)1051 ))1052 1053 # 그래프 레이아웃 설정1054 hist_fig.update_layout(1055 title=title,1056 xaxis_title="검사값",1057 yaxis_title="확률 밀도",1058 legend=dict(1059 orientation="h",1060 yanchor="bottom",1061 y=1.02,1062 xanchor="right",1063 x=11064 ),1065 height=height1066 )1067 1068 return hist_fig1069 1070# 화면 표시 함수1071def display_process_overview(data):1072 """공정 현황 개요 표시 함수"""1073 if not isinstance(data, dict):1074 st.error("유효하지 않은 데이터 형식입니다.")1075 return1076 1077 # 빈 공간 추가1078 st.markdown("<div style='margin-top: 30px;'></div>", unsafe_allow_html=True)1079 st.markdown("### 유리 장섬유 생산공정 개요")1080 1081 # 공정 정보 데이터프레임 가져오기 (data 딕셔너리에서)1082 process_info_df = data.get('process_info', pd.DataFrame())1083 1084 # 공정 규격 정보 가져오기1085 process_specs_df = data.get('process_specs', pd.DataFrame())1086 1087 # 공정 정보가 비어 있는 경우 처리1088 if process_info_df.empty:1089 st.warning("공정 정보를 불러올 수 없습니다.")1090 return1091 1092 # 공정 흐름도 시각화 (개선된 버전)1093 st.markdown("#### 공정 흐름도")1094 1095 # 공정 단계 추출1096 process_stages = process_info_df['표준단위공정'].unique()1097 1098 # 공정 흐름도 HTML 생성 및 표시 (base64 인코딩된 이미지 포함)1099 flow_html = create_process_flow_html(process_stages, process_info_df)1100 1101 # 이미지가 포함된 공정 흐름도는 높이를 더 크게 설정1102 st.components.v1.html(flow_html, height=300)1103 1104 # 공정 단계 선택 및 세부 정보 표시1105 st.markdown("#### 공정 단계별 상세 정보")1106 1107 # 공정 단계 선택과 세부 공정 선택을 좌우로 배치1108 col_select1, col_select2 = st.columns(2)1109 1110 with col_select1:1111 # 공정 단계 선택1112 selected_stage = st.selectbox("공정 단계 선택", process_stages)1113 1114 # 선택된 공정 단계의 세부 공정 표시1115 sub_processes = process_info_df[process_info_df['표준단위공정'] == selected_stage]1116 1117 with col_select2:1118 if not sub_processes.empty:1119 # 세부 공정 선택1120 selected_sub = st.selectbox("세부 공정 선택", sub_processes['실공정(Step)'].tolist())1121 1122 if not sub_processes.empty:1123 # 선택된 세부 공정 정보 표시1124 process_detail = sub_processes[sub_processes['실공정(Step)'] == selected_sub].iloc[0]1125 1126 # 공정 정보 카드 표시1127 col1, col2 = st.columns(2)1128 1129 with col1:1130 st.markdown(f"""1131 <div class="process-box">1132 <h4 style="margin-top: 0;">공정 개요</h4>1133 <p><strong>공정명:</strong> {selected_sub}</p>1134 <p><strong>공정내용:</strong> {process_detail['공정내용']}</p>1135 </div>1136 """, unsafe_allow_html=True)1137 1138 with col2:1139 st.markdown(f"""1140 <div class="process-box">1141 <h4 style="margin-top: 0;">품질 관리 포인트</h4>1142 <p><strong>품질 인자:</strong> {process_detail['품질 인자']}</p>1143 </div>1144 """, unsafe_allow_html=True)1145 1146 # 공정 조건 인자와 개선 포인트 표시1147 col1, col2 = st.columns(2)1148 1149 with col1:1150 st.markdown(f"""1151 <div class="process-box">1152 <h4 style="margin-top: 0;">공정 조건 인자</h4>1153 <p>{process_detail['공정 조건 인자']}</p>1154 </div>1155 """, unsafe_allow_html=True)1156 1157 with col2:1158 st.markdown(f"""1159 <div class="issue-box">1160 <h4 style="margin-top: 0;">개선 Point (문제점)</h4>1161 <p>{process_detail['개선 Point (문제점)']}</p>1162 </div>1163 """, unsafe_allow_html=True)1164 1165 # 제품 및 공정규격 정보 표시1166 if not process_specs_df.empty:1167 st.markdown("#### 공정 규격 정보")1168 1169 # 공정명으로 규격 정보 필터링1170 # 매핑된 공정명으로 필터링1171 mapped_process_name = CONFIG["PROCESS_NAME_MAPPING"].get(selected_stage, selected_sub)1172 filtered_specs = process_specs_df[process_specs_df['공정명'].str.contains(mapped_process_name, na=False)]1173 1174 # 세부 공정명으로 추가 필터링 시도1175 if filtered_specs.empty:1176 filtered_specs = process_specs_df[process_specs_df['공정명'].str.contains(selected_sub, na=False)]1177 1178 if not filtered_specs.empty:1179 # 탭 생성: 규격 정보와 차트1180 spec_tabs = st.tabs(["규격 정보", "규격 시각화"])1181 1182 with spec_tabs[0]:1183 # 규격 정보 표시1184 st.dataframe(1185 filtered_specs,1186 column_config={1187 "공정명": st.column_config.TextColumn("공정명", width="medium"),1188 "작업설명": st.column_config.TextColumn("작업설명", width="large"),1189 "설비명": st.column_config.TextColumn("설비명", width="medium"),1190 "구분": st.column_config.TextColumn("구분", width="small"),1191 "항목": st.column_config.TextColumn("항목", width="medium"),1192 "CTP/CTQ": st.column_config.TextColumn("CTP/CTQ", width="small"),1193 "규격": st.column_config.TextColumn("규격", width="large"),1194 "평가(측정) 방법": st.column_config.TextColumn("평가(측정) 방법", width="medium"),1195 "주기": st.column_config.TextColumn("주기", width="small"),1196 "관리방법": st.column_config.TextColumn("관리방법", width="medium"),1197 "대응계획": st.column_config.TextColumn("대응계획", width="large")1198 },1199 use_container_width=True,1200 hide_index=True