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