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