CoolFace
Apppublic

lscsc/Process_dash_pub

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
optimized_dashboard-1.py1167 linesDownload Raw Back to backup
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}')">&times;</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}')">&times;</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    # 나머지 코드 생략...