CoolFace
Apppublic

lscsc/Process_dash_pub

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

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