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: 1.5rem;261 margin-bottom: 1rem;262 font-weight: 500;263 font-size: 1.2rem;264 padding-left: 0; /* 왼쪽 패딩 제거 */265 }266 267 /* h4 스타일을 h3와 일관되게 설정 */268 h4 {269 color: #2C3E50;270 margin-top: 1.5rem;271 margin-bottom: 1rem;272 font-weight: 500;273 font-size: 1.2rem;274 padding-left: 0; /* 왼쪽 패딩 제거 */275 }276 277 /* 위젯 간격 조정 */278 .stSelectbox, .stMultiSelect {279 margin-bottom: 0.75rem;280 }281 282 /* 메트릭 카드 스타일 */283 [data-testid="stMetric"] {284 background-color: #f8f9fa;285 border-radius: 6px;286 padding: 0.75rem !important;287 border-left: 3px solid #2563eb;288 }289 290 /* 메트릭 레이블 스타일 */291 [data-testid="stMetric"] > div:first-child {292 color: #6c757d;293 }294 295 /* 메트릭 값 스타일 */296 [data-testid="stMetric"] > div:nth-child(2) {297 font-size: 1.3rem;298 font-weight: 600;299 color: #2C3E50;300 }301 302 /* 데이터프레임 스타일 */303 .dataframe {304 border-collapse: collapse !important;305 width: 100% !important;306 border-radius: 6px;307 overflow: hidden;308 }309 310 .dataframe th {311 background-color: #f1f3f5 !important;312 color: #495057 !important;313 font-weight: 500 !important;314 padding: 0.5rem 0.75rem !important;315 text-align: left !important;316 }317 318 .dataframe td {319 padding: 0.5rem 0.75rem !important;320 border-top: 1px solid #f0f0f0 !important;321 }322 323 .dataframe tr:nth-child(even) {324 background-color: #f8f9fa !important;325 }326 327 /* 탭 스타일 개선 */328 .stTabs [data-baseweb="tab-list"] {329 gap: 0;330 border-bottom: 1px solid #e0e0e0;331 }332 333 .stTabs [data-baseweb="tab"] {334 height: 40px;335 white-space: pre-wrap;336 background-color: transparent;337 border-radius: 0;338 border-bottom: 2px solid transparent;339 padding: 0 1rem;340 font-weight: 500;341 color: #6c757d;342 transition: all 0.2s ease;343 }344 345 .stTabs [aria-selected="true"] {346 color: #2563eb !important;347 border-bottom: 2px solid #2563eb !important;348 background-color: transparent !important;349 }350 351 /* 이슈 박스 스타일 */352 .issue-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 .process-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 .defect-box {371 background-color: #f8f9fa;372 border-radius: 6px;373 padding: 1rem;374 margin: 0.75rem 0;375 border-left: 3px solid #2563eb;376 }377 378 /* 서브헤더 스타일 추가 */379 .stMarkdown h3 {380 margin-left: 0;381 padding-left: 0;382 }383 384 /* 추가 마크다운 내 소제목 스타일 */385 .stMarkdown h4 {386 margin-left: 0;387 }388 </style>389 """, 390 unsafe_allow_html=True391 )392 393# 인증 관련 함수394def check_password():395 """비밀번호 확인 함수"""396 if "password_correct" not in st.session_state:397 st.session_state["password_correct"] = False398 399 if not st.session_state["password_correct"]:400 # 비밀번호 입력 필드를 한 번만 표시401 with st.container():402 password = st.text_input(403 "비밀번호를 입력하세요", 404 type="password",405 key="password_input"406 )407 408 if password:409 if password == CONFIG["PASSWORD"]:410 st.session_state["password_correct"] = True411 # 비밀번호가 맞으면 페이지 리프레시412 st.rerun()413 else:414 st.error("비밀번호가 올바르지 않습니다.")415 return False416 return False417 418 return True419 420# 세션 상태 초기화421def initialize_session_state():422 """세션 상태 초기화 함수"""423 # 기본 세션 상태 변수들 초기화424 default_states = {425 'password_correct': False,426 'selected_product': None,427 'selected_data': None,428 'tab_selection': "공정 현황",429 'data_loaded': False,430 'data_dict': {},431 'last_data_refresh': None432 }433 434 # 세션 상태에 없는 변수들만 초기화435 for key, value in default_states.items():436 if key not in st.session_state:437 st.session_state[key] = value438 439# 데이터 관련 함수440@lru_cache(maxsize=32)441def safe_read_excel(filepath, default_data=None):442 """안전하게 엑셀 파일을 읽는 함수 (캐싱 적용)"""443 try:444 if os.path.exists(filepath):445 logger.info(f"파일 로드 중: {filepath}")446 return pd.read_excel(filepath)447 else:448 logger.warning(f"파일을 찾을 수 없습니다: {filepath}")449 return default_data if default_data is not None else pd.DataFrame()450 except Exception as e:451 logger.error(f"파일 읽기 오류 ({filepath}): {str(e)}")452 return default_data if default_data is not None else pd.DataFrame()453 454def load_data():455 """데이터 로드 함수 (개선된 버전)"""456 # 이미 로드된 데이터가 있고, 마지막 새로고침 시간이 30분 이내면 재사용457 if (st.session_state.data_loaded and 458 st.session_state.last_data_refresh is not None and459 (datetime.now() - st.session_state.last_data_refresh).total_seconds() < 1800):460 logger.info("캐시된 데이터 사용")461 return st.session_state.data_dict462 463 try:464 # 데이터 사전 초기화465 data_dict = {}466 467 # 각 데이터 파일 로드 (안전하게)468 for key, path in CONFIG["DATA_PATHS"].items():469 if key != "process_infographic": # HTML 파일은 별도 처리470 data_dict[key] = safe_read_excel(path)471 472 # 날짜 형식 변환 (품질이슈정보의 날짜 컬럼)473 if not data_dict['quality_issues'].empty and '기준일' in data_dict['quality_issues'].columns:474 data_dict['quality_issues']['기준일'] = pd.to_datetime(data_dict['quality_issues']['기준일'], errors='coerce')475 476 # 데이터 로드 상태 및 데이터 저장477 st.session_state.data_loaded = True478 st.session_state.data_dict = data_dict479 st.session_state.last_data_refresh = datetime.now()480 481 # 성공 메시지 제거 (st.success 라인 삭제)482 logger.info("모든 데이터 로드 완료")483 484 return data_dict485 except Exception as e:486 error_msg = f"데이터 로드 중 오류 발생: {str(e)}"487 logger.error(error_msg)488 st.error(error_msg)489 return {}490 491def load_process_infographic():492 """공정 인포그래픽 HTML 로드 함수"""493 try:494 filepath = CONFIG["DATA_PATHS"]["process_infographic"]495 if os.path.exists(filepath):496 with open(filepath, 'r', encoding='utf-8') as f:497 html_content = f.read()498 return html_content499 else:500 logger.warning(f"인포그래픽 파일을 찾을 수 없습니다: {filepath}")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 except Exception as e:509 logger.warning(f"인포그래픽 로드 중 오류 발생: {str(e)}")510 # 오류 발생 시 대체 HTML 콘텐츠 제공511 return """512 <div style="text-align: center; padding: 20px; background-color: #f8f9fa; border-radius: 10px;">513 <h3>인포그래픽 로드 오류</h3>514 <p>파일을 읽는 중 오류가 발생했습니다.</p>515 </div>516 """517 518# 계산 및 분석 함수519def calculate_process_capability(data, ucl, lcl, sigma_level=3):520 """공정능력지수 계산 함수"""521 try:522 if len(data) == 0:523 return {524 'Cp': 0,525 'Cpu': 0,526 'Cpl': 0,527 'Cpk': 0,528 'PPM': 0529 }530 531 mean = data.mean()532 std = data.std()533 534 if std == 0:535 return {536 'Cp': float('inf'),537 'Cpu': float('inf'),538 'Cpl': float('inf'),539 'Cpk': float('inf'),540 'PPM': 0541 }542 543 # 공정능력지수 계산544 cp = (ucl - lcl) / (6 * std) if std != 0 else float('inf')545 cpu = (ucl - mean) / (3 * std) if std != 0 else float('inf')546 cpl = (mean - lcl) / (3 * std) if std != 0 else float('inf')547 cpk = min(cpu, cpl)548 549 # 예상불량률 계산 (ppm 단위)550 z_upper = (ucl - mean) / std if std != 0 else float('inf')551 z_lower = (mean - lcl) / std if std != 0 else float('inf')552 ppm_upper = stats.norm.sf(z_upper) * 1000000553 ppm_lower = stats.norm.sf(z_lower) * 1000000554 total_ppm = ppm_upper + ppm_lower555 556 return {557 'Cp': cp,558 'Cpu': cpu,559 'Cpl': cpl,560 'Cpk': cpk,561 'PPM': total_ppm562 }563 564 except Exception as e:565 logger.error(f"공정능력지수 계산 중 오류 발생: {str(e)}")566 return {567 'Cp': 0,568 'Cpu': 0,569 'Cpl': 0,570 'Cpk': 0,571 'PPM': 0572 }573 574def extract_keywords(text, stopwords=None, min_length=2):575 """텍스트에서 키워드 추출 함수"""576 if stopwords is None:577 stopwords = CONFIG["STOPWORDS"]578 579 # 텍스트를 단어로 분리580 words = text.replace(',', ' ').replace('.', ' ').replace('(', ' ').replace(')', ' ').split()581 582 keywords = {}583 584 for word in words:585 word = word.strip()586 587 # 불용어 제거 및 길이 체크588 if word not in stopwords and len(word) >= min_length:589 # 단어 끝에 붙은 '로', '에', '의' 등 제거590 for suffix in ['로', '에', '의', '을', '를', '은', '는']:591 if word.endswith(suffix) and len(word) > len(suffix):592 word = word[:-len(suffix)]593 594 # 단어가 여전히 최소 길이 이상인지 확인595 if len(word) >= min_length:596 if word in keywords:597 keywords[word] += 1598 else:599 keywords[word] = 1600 601 return keywords602 603# 이미지 관련 유틸리티 함수 추가604def get_image_as_base64(file_path):605 """이미지 파일을 base64로 인코딩하여 반환하는 함수"""606 try:607 if os.path.exists(file_path):608 with open(file_path, "rb") as image_file:609 encoded = base64.b64encode(image_file.read()).decode()610 return f"data:image/png;base64,{encoded}"611 else:612 logger.warning(f"이미지 파일이 존재하지 않습니다: {file_path}")613 return None614 except Exception as e:615 logger.error(f"이미지 인코딩 중 오류 발생: {str(e)}")616 return None617 618# 시각화 함수619def create_process_flow_html(process_stages, process_info_df):620 """공정 흐름도 HTML 생성 함수 - 이미지 처리 개선 버전"""621 # 기본 색상 및 아이콘 설정622 default_color = "#6C757D"623 default_icon = "cog"624 625 # 공정명과 이미지 파일명 매핑 - 모두 소문자로 통일626 process_image_mapping = {627 "batch": "mixing.png",628 "forming": "fiberizing.png",629 "sizing": "sizing.png", 630 "winding": "winding.png",631 "drying": "fbd.png",632 "packing": "packing.png",633 "인수검사": "packing.png",634 "용융": "furnace.png",635 "chopping": "chopping.png",636 "pelletizer": "pelletizer.png",637 "water spray": "water_spray.png", # 공백 대신 언더스코어 사용638 "fbd": "fbd.png",639 # 누락된 공정 이미지 매핑 추가640 "성형": "fiberizing.png", # '성형'은 'forming'과 동일하게 매핑641 "가공": "fbd.png", # 가공 이미지642 "검사/포장": "packing.png", # 검사/포장 이미지643 # 추가 매핑 - 특수문자 및 대소문자 변환 전에 매핑되도록644 "검사": "packing.png"645 }646 647 # 디버깅을 위한 로깅 추가648 logger.info(f"공정 단계 목록: {process_stages}")649 650 # 이미지 파일 디렉토리 확인 로깅651 image_dir = CONFIG['PROCESS_IMAGES_PATH']652 if os.path.exists(image_dir):653 logger.info(f"이미지 디렉토리 존재: {image_dir}")654 # 디렉토리 내 파일 목록 로깅655 files = os.listdir(image_dir)656 logger.info(f"디렉토리 내 파일 목록: {files}")657 else:658 logger.error(f"이미지 디렉토리가 존재하지 않음: {image_dir}")659 660 # HTML로 공정 흐름도 생성661 flow_html = """662 <style>663 /* 공정 흐름도 컨테이너 스타일 */664 .flow-container {665 display: flex;666 flex-wrap: nowrap;667 overflow-x: auto;668 padding: 1rem 0;669 align-items: center;670 max-width: 100%;671 }672 673 /* 각 공정 단계 스타일 */674 .flow-step {675 display: flex;676 flex-direction: row;677 align-items: center;678 flex: 0 0 auto;679 margin-right: 1rem;680 }681 682 /* 화살표 스타일 */683 .arrow {684 margin: 0 1rem;685 color: #6c757d;686 font-size: 1.2rem;687 }688 689 /* 아이콘 컨테이너 스타일 */690 .flow-icon {691 width: 70px;692 height: 70px;693 border-radius: 50%;694 border: 2px solid #4285F4;695 display: flex;696 align-items: center;697 justify-content: center;698 margin-right: 0.5rem;699 background-color: white;700 overflow: hidden;701 cursor: pointer;702 flex-shrink: 0;703 }704 705 /* 아이콘 이미지 스타일 */706 .flow-icon img {707 width: 50px;708 height: 50px;709 object-fit: contain;710 }711 712 /* 공정 아이콘 스타일 */713 .flow-icon i {714 font-size: 2rem;715 }716 717 /* 공정 단계 콘텐츠 스타일 */718 .flow-step-content {719 display: flex;720 flex-direction: column;721 min-width: 100px;722 max-width: 150px;723 }724 725 /* 공정 단계 제목 스타일 */726 .flow-step-title {727 font-weight: bold;728 margin-bottom: 0.25rem;729 color: #333;730 }731 732 /* 공정 단계 설명 스타일 */733 .flow-step-desc {734 font-size: 0.8rem;735 color: #666;736 white-space: nowrap;737 overflow: hidden;738 text-overflow: ellipsis;739 }740 741 /* 마지막 단계 스타일 */742 .flow-step.last-step {743 margin-right: 0;744 }745 746 /* 반응형 스타일 */747 @media (max-width: 768px) {748 .flow-icon {749 width: 50px;750 height: 50px;751 }752 753 .flow-icon img {754 width: 35px;755 height: 35px;756 }757 758 .flow-step-content {759 min-width: 80px;760 }761 }762 </style>763 764 <div class="flow-container">765 """766 767 # 각 공정 단계별 HTML 생성768 for i, stage in enumerate(process_stages):769 # 해당 공정 단계의 세부 공정들 추출770 sub_processes = process_info_df[process_info_df['표준단위공정'] == stage]['실공정(Step)'].tolist()771 sub_process_text = ", ".join(sub_processes)772 773 # 아이콘 및 색상 결정774 icon = CONFIG["PROCESS_ICONS"].get(stage, default_icon)775 color = CONFIG["PROCESS_COLORS"].get(stage, default_color)776 777 # 마지막 단계 여부 확인778 is_last = (i == len(process_stages) - 1)779 last_class = " last-step" if is_last else ""780 781 # 이미지 파일 경로 결정 (매핑 로직 개선)782 image_file = None783 784 # 1. 원본 이름으로 매핑 먼저 시도 (특수문자가 있는 경우도 처리)785 if stage in process_image_mapping:786 image_file = process_image_mapping[stage]787 logger.info(f"원본 이름으로 이미지 매핑: '{stage}' → {image_file}")788 789 # 2. 소문자 변환 후 매핑 시도790 elif stage.lower() in process_image_mapping:791 image_file = process_image_mapping[stage.lower()]792 logger.info(f"소문자 변환으로 이미지 매핑: '{stage}' → {image_file}")793 794 # 3. 특수문자 제거 및 공백 언더스코어 변환 후 매핑 시도795 else:796 # 특수문자를 공백으로 치환하고 연속된 공백을 하나로 합침797 normalized_stage = re.sub(r'[^\w\s]', ' ', stage).lower()798 normalized_stage = re.sub(r'\s+', ' ', normalized_stage).strip()799 800 # 공백을 언더스코어로 변경801 normalized_stage_us = normalized_stage.replace(' ', '_')802 803 if normalized_stage in process_image_mapping:804 image_file = process_image_mapping[normalized_stage]805 logger.info(f"정규화 이름으로 이미지 매핑: '{stage}' → '{normalized_stage}' → {image_file}")806 elif normalized_stage_us in process_image_mapping:807 image_file = process_image_mapping[normalized_stage_us]808 logger.info(f"언더스코어 변환으로 이미지 매핑: '{stage}' → '{normalized_stage_us}' → {image_file}")809 else:810 # 부분 매칭 시도 (예: "검사/포장" → "검사"로 부분 매칭)811 for key in process_image_mapping:812 if key in stage.lower() or stage.lower() in key:813 image_file = process_image_mapping[key]814 logger.info(f"부분 매칭으로 이미지 매핑: '{stage}' 내에서 '{key}' 발견 → {image_file}")815 break816 817 if not image_file:818 logger.warning(f"단계 '{stage}'에 대한 이미지 파일 매핑 없음, 기본 아이콘 사용")819 820 # 이미지 파일 경로 완성 및 존재 여부 확인821 image_path = None822 base64_image = None823 824 if image_file:825 # 절대 경로 사용826 image_path = os.path.join(os.path.abspath(CONFIG['PROCESS_IMAGES_PATH']), image_file)827 logger.info(f"이미지 경로: {image_path}")828 829 if os.path.exists(image_path):830 try:831 base64_image = get_image_as_base64(image_path)832 logger.info(f"이미지 인코딩 성공: {image_path}")833 except Exception as e:834 logger.error(f"이미지 인코딩 오류: {image_path}, 오류: {str(e)}")835 else:836 # 파일이 없을 경우 대체 파일 시도 (확장자 변경)837 alt_extensions = ['.jpg', '.jpeg', '.gif', '.png']838 for ext in alt_extensions:839 alt_path = os.path.splitext(image_path)[0] + ext840 if os.path.exists(alt_path):841 try:842 base64_image = get_image_as_base64(alt_path)843 logger.info(f"대체 이미지 인코딩 성공: {alt_path}")844 break845 except Exception as e:846 logger.error(f"대체 이미지 인코딩 오류: {alt_path}, 오류: {str(e)}")847 848 if not base64_image:849 logger.warning(f"이미지 파일이 존재하지 않음: {image_path}")850 851 # 세부 공정 설명 추출 (첫 번째 세부 공정의 설명 사용)852 process_description = ""853 if sub_processes:854 first_sub_process = process_info_df[855 (process_info_df['표준단위공정'] == stage) & 856 (process_info_df['실공정(Step)'] == sub_processes[0])857 ]858 if not first_sub_process.empty:859 process_description = first_sub_process.iloc[0].get('공정내용', '')860 861 # base64로 인코딩된 이미지가 있는 경우와 없는 경우 처리862 if base64_image:863 # 이미지가 있는 경우 base64 인코딩된 이미지 표시864 icon_content = f"""865 <img src="{base64_image}" alt="{stage}" style="width: 100%; height: 100%; object-fit: contain;">866 """867 else:868 # 이미지가 없는 경우 아이콘 표시869 icon_content = f"""870 <i class="fas fa-{icon}" style="color: {color}; font-size: 2rem;"></i>871 """872 873 # 단계별 HTML 추가874 flow_html += f"""875 <div class="flow-step{last_class}">876 <div class="flow-icon" style="border-color: {color};">877 {icon_content}878 </div>879 <div class="flow-step-content">880 <div class="flow-step-title">{stage}</div>881 <div class="flow-step-desc">{sub_process_text[:30]}{'...' if len(sub_process_text) > 30 else ''}</div>882 </div>883 {'' if is_last else '<div class="arrow"><i class="fas fa-chevron-right"></i></div>'}884 </div>885 """886 887 # FontAwesome 추가888 flow_html += """889 </div>890 891 <!-- FontAwesome 추가 -->892 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">893 """894 895 return flow_html896 897def create_bar_chart(data, x, y, title, color_scale='Viridis', height=400):898 """막대 차트 생성 함수"""899 fig = px.bar(900 data,901 x=x,902 y=y,903 title=title,904 color=y,905 color_continuous_scale=color_scale906 )907 908 fig.update_layout(909 xaxis_title=x,910 yaxis_title=y,911 height=height912 )913 914 return fig915 916def create_pie_chart(data, values, names, title, hole=0.4, color_sequence=None):917 """파이 차트 생성 함수"""918 fig = px.pie(919 data,920 values=values,921 names=names,922 title=title,923 hole=hole,924 color_discrete_sequence=color_sequence925 )926 927 fig.update_traces(textposition='inside', textinfo='percent+label')928 return fig929 930def create_line_chart(x, y, title, line_color='blue', marker_size=8, height=400):931 """선 차트 생성 함수"""932 fig = go.Figure()933 934 fig.add_trace(go.Scatter(935 x=x,936 y=y,937 mode='lines+markers',938 name='데이터',939 line=dict(color=line_color, width=2),940 marker=dict(size=marker_size)941 ))942 943 fig.update_layout(944 title=title,945 xaxis_title="X축",946 yaxis_title="Y축",947 height=height948 )949 950 return fig951 952def create_pareto_chart(data, x, y, title, height=500):953 """파레토 차트 생성 함수"""954 fig = go.Figure()955 956 # 불량량 막대 그래프957 fig.add_trace(go.Bar(958 x=data[x],959 y=data[y],960 name='불량량',961 marker=dict(color='indianred')962 ))963 964 # 누적 불량량 계산965 data['누적비율'] = data[y].cumsum() / data[y].sum() * 100966 967 # 누적 비율 선 그래프968 fig.add_trace(go.Scatter(969 x=data[x],970 y=data['누적비율'],971 name='누적 비율',972 mode='lines+markers',973 yaxis='y2',974 line=dict(color='royalblue', width=2),975 marker=dict(size=8)976 ))977 978 # 80% 기준선979 fig.add_trace(go.Scatter(980 x=[data[x].iloc[0], data[x].iloc[-1]],981 y=[80, 80],982 name='80% 기준',983 mode='lines',984 yaxis='y2',985 line=dict(color='green', dash='dash')986 ))987 988 # 그래프 레이아웃 설정989 fig.update_layout(990 title=title,991 xaxis_title=x,992 yaxis_title=y,993 yaxis2=dict(994 title="누적 비율 (%)",995 overlaying='y',996 side='right',997 range=[0, 100]998 ),999 legend=dict(1000 orientation="h",1001 yanchor="bottom",1002 y=1.02,1003 xanchor="right",1004 x=11005 ),1006 height=height1007 )1008 1009 return fig1010 1011def create_histogram_with_normal(values, usl, lsl, mean, std, title, height=400):1012 """히스토그램 및 정규분포 생성 함수"""1013 hist_fig = go.Figure()1014 1015 # 히스토그램 추가1016 hist_fig.add_trace(go.Histogram(1017 x=values,1018 name='검사값 분포',1019 opacity=0.7,1020 marker=dict(color='royalblue'),1021 histnorm='probability density'1022 ))1023 1024 # 정규분포 곡선 추가1025 x_range = np.linspace(values.min() - 0.5, values.max() + 0.5, 100)1026 y_range = stats.norm.pdf(x_range, mean, std)1027 1028 hist_fig.add_trace(go.Scatter(1029 x=x_range,1030 y=y_range,1031 mode='lines',1032 name='정규분포',1033 line=dict(color='red', width=2)1034 ))1035 1036 # 규격 상한/하한 추가1037 hist_fig.add_trace(go.Scatter(1038 x=[usl, usl],1039 y=[0, max(y_range) * 1.2],1040 mode='lines',1041 name='상한 규격',1042 line=dict(color='green', width=2, dash='dash')1043 ))1044 1045 hist_fig.add_trace(go.Scatter(1046 x=[lsl, lsl],1047 y=[0, max(y_range) * 1.2],1048 mode='lines',1049 name='하한 규격',1050 line=dict(color='green', width=2, dash='dash')1051 ))1052 1053 # 평균선 추가1054 hist_fig.add_trace(go.Scatter(1055 x=[mean, mean],1056 y=[0, max(y_range) * 1.2],1057 mode='lines',1058 name='평균',1059 line=dict(color='black', width=2)1060 ))1061 1062 # 그래프 레이아웃 설정1063 hist_fig.update_layout(1064 title=title,1065 xaxis_title="검사값",1066 yaxis_title="확률 밀도",1067 legend=dict(1068 orientation="h",1069 yanchor="bottom",1070 y=1.02,1071 xanchor="right",1072 x=11073 ),1074 height=height1075 )1076 1077 return hist_fig1078 1079# 화면 표시 함수1080def display_quality_analysis(data):1081 """품질 분석 표시 함수"""1082 st.markdown("<div style='margin-top: 30px;'></div>", unsafe_allow_html=True)1083 st.subheader("품질 분석")1084 1085 # 공정능력 정보 확인1086 process_capability = data.get('process_capability', pd.DataFrame())1087 1088 if process_capability.empty:1089 st.warning("공정능력 정보를 불러올 수 없습니다.")1090 return1091 1092 # 제품 선택1093 if '제품' in process_capability.columns:1094 products = process_capability['제품'].unique()1095 if len(products) > 0:1096 # 제품 선택과 검사항목 선택을 좌우로 배치1097 col_product, col_item = st.columns(2)1098 1099 with col_product:1100 selected_product = st.selectbox("제품 선택", products)1101 1102 # 선택된 제품의 공정능력 데이터1103 product_capability = process_capability[process_capability['제품'] == selected_product]1104 1105 # 검사항목 선택1106 if '검사항목' in product_capability.columns and not product_capability.empty:1107 inspection_items = product_capability['검사항목'].unique()1108 if len(inspection_items) > 0:1109 with col_item:1110 selected_item = st.selectbox("검사항목 선택", inspection_items)1111 1112 # 선택된 검사항목의 데이터1113 item_data = product_capability[product_capability['검사항목'] == selected_item]1114 1115 if not item_data.empty:1116 # 월별 공정능력 차트1117 st.markdown("### 월별 공정능력 추이")1118 1119 # 필요한 컬럼이 있는지 확인1120 required_columns = ['월', 'Cpk']1121 if all(col in item_data.columns for col in required_columns):1122 fig = go.Figure()1123 1124 # Cpk 추이 그래프1125 fig.add_trace(go.Scatter(1126 x=item_data['월'],1127 y=item_data['Cpk'],1128 mode='lines+markers',1129 name='Cpk',1130 line=dict(color='blue', width=2),1131 marker=dict(size=8)1132 ))1133 1134 # 기준선 (Cpk=1.33)1135 fig.add_trace(go.Scatter(1136 x=[item_data['월'].min(), item_data['월'].max()],1137 y=[1.33, 1.33],1138 mode='lines',1139 name='기준 (Cpk=1.33)',1140 line=dict(color='red', dash='dash')1141 ))1142 1143 # 그래프 레이아웃 설정1144 fig.update_layout(1145 title=f"{selected_product} - {selected_item} 공정능력 추이",1146 xaxis_title="월",1147 yaxis_title="공정능력지수",1148 legend=dict(1149 orientation="h",1150 yanchor="bottom",1151 y=1.02,1152 xanchor="right",1153 x=11154 ),1155 height=4001156 )1157 1158 st.plotly_chart(fig, use_container_width=True)1159 else:1160 st.warning("공정능력 추이를 표시하는데 필요한 데이터가 없습니다.")1161 1162 # 검사값 분포 차트1163 st.markdown("### 검사값 분포")1164 1165 # 검사값, 규격 상한/하한 데이터1166 if all(col in item_data.columns for col in ['검사값', '상한', '하한']):1167 values = item_data['검사값']1168 if not values.empty:1169 mean = values.mean()1170 std = values.std()1171 usl = item_data['상한'].mean()1172 lsl = item_data['하한'].mean()1173 1174 # 히스토그램 생성1175 hist_fig = create_histogram_with_normal(1176 values, usl, lsl, mean, std,1177 title=f"{selected_product} - {selected_item} 검사값 분포"1178 )1179 1180 st.plotly_chart(hist_fig, use_container_width=True)1181 1182 # 공정능력 분석 결과1183 st.markdown("### 공정능력 분석 결과")1184 1185 # 공정능력지수 계산1186 capability = calculate_process_capability(values, usl, lsl)1187 1188 col1, col2, col3, col4 = st.columns(4)1189 1190 with col1:1191 st.metric(label="Cp", value=f"{capability['Cp']:.3f}")1192 with col2:1193 st.metric(label="Cpk", value=f"{capability['Cpk']:.3f}")1194 with col3:1195 st.metric(label="Cpu", value=f"{capability['Cpu']:.3f}")1196 with col4:1197 st.metric(label="Cpl", value=f"{capability['Cpl']:.3f}")1198 1199 # 예상 불량률1200 st.metric(label="예상 불량률 (PPM)", value=f"{capability['PPM']:.2f}")