CoolFace
Apppublic

lscsc/Process_dash_pub

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

Showing the first 1,200 of 1303 lines. Download the file for the rest.