CoolFace
Apppublic

lscsc/Process_dash_pub

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
optimized_dashboard-2-1-1.py1788 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 create_process_flow_html(process_stages, process_info_df):611    """공정 흐름도 HTML 생성 함수 - 이미지 처리 개선 버전"""612    # 기본 색상 및 아이콘 설정613    default_color = "#6C757D"614    default_icon = "cog"615    616    # 공정명과 이미지 파일명 매핑 - 모두 소문자로 통일617    process_image_mapping = {618        "batch": "mixing.png",619        "forming": "fiberizing.png",620        "sizing": "sizing.png", 621        "winding": "winding.png",622        "drying": "fbd.png",623        "packing": "packing.png",624        "인수검사": "packing.png",625        "용융": "furnace.png",626        "chopping": "chopping.png",627        "pelletizer": "pelletizer.png",628        "water spray": "water_spray.png",  # 공백 대신 언더스코어 사용629        "fbd": "fbd.png",630        # 누락된 공정 이미지 매핑 추가631        "성형": "fiberizing.png",  # '성형'은 'forming'과 동일하게 매핑632        "가공": "fbd.png", # 가공 이미지633        "검사/포장": "packing.png",  # 검사/포장 이미지634        # 추가 매핑 - 특수문자 및 대소문자 변환 전에 매핑되도록635        "검사": "packing.png"636    }637    638    # 디버깅을 위한 로깅 추가639    logger.info(f"공정 단계 목록: {process_stages}")640    641    # 이미지 파일 디렉토리 확인 로깅642    image_dir = CONFIG['PROCESS_IMAGES_PATH']643    if os.path.exists(image_dir):644        logger.info(f"이미지 디렉토리 존재: {image_dir}")645        # 디렉토리 내 파일 목록 로깅646        files = os.listdir(image_dir)647        logger.info(f"디렉토리 내 파일 목록: {files}")648    else:649        logger.error(f"이미지 디렉토리가 존재하지 않음: {image_dir}")650    651    # HTML로 공정 흐름도 생성652    flow_html = """653    <style>654    /* 공정 흐름도 컨테이너 스타일 */655    .flow-container {656        display: flex;657        flex-wrap: nowrap;658        overflow-x: auto;659        padding: 1rem 0;660        align-items: center;661        max-width: 100%;662    }663    664    /* 각 공정 단계 스타일 */665    .flow-step {666        display: flex;667        flex-direction: row;668        align-items: center;669        flex: 0 0 auto;670        margin-right: 1rem;671    }672    673    /* 화살표 스타일 */674    .arrow {675        margin: 0 1rem;676        color: #6c757d;677        font-size: 1.2rem;678    }679    680    /* 아이콘 컨테이너 스타일 */681    .flow-icon {682        width: 70px;683        height: 70px;684        border-radius: 50%;685        border: 2px solid #4285F4;686        display: flex;687        align-items: center;688        justify-content: center;689        margin-right: 0.5rem;690        background-color: white;691        overflow: hidden;692        cursor: pointer;693        flex-shrink: 0;694    }695    696    /* 아이콘 이미지 스타일 */697    .flow-icon img {698        width: 50px;699        height: 50px;700        object-fit: contain;701    }702    703    /* 공정 아이콘 스타일 */704    .flow-icon i {705        font-size: 2rem;706    }707    708    /* 공정 단계 콘텐츠 스타일 */709    .flow-step-content {710        display: flex;711        flex-direction: column;712        min-width: 100px;713        max-width: 150px;714    }715    716    /* 공정 단계 제목 스타일 */717    .flow-step-title {718        font-weight: bold;719        margin-bottom: 0.25rem;720        color: #333;721    }722    723    /* 공정 단계 설명 스타일 */724    .flow-step-desc {725        font-size: 0.8rem;726        color: #666;727        white-space: nowrap;728        overflow: hidden;729        text-overflow: ellipsis;730    }731    732    /* 마지막 단계 스타일 */733    .flow-step.last-step {734        margin-right: 0;735    }736    737    /* 반응형 스타일 */738    @media (max-width: 768px) {739        .flow-icon {740            width: 50px;741            height: 50px;742        }743        744        .flow-icon img {745            width: 35px;746            height: 35px;747        }748        749        .flow-step-content {750            min-width: 80px;751        }752    }753    </style>754    755    <div class="flow-container">756    """757    758    # 각 공정 단계별 HTML 생성759    for i, stage in enumerate(process_stages):760        # 해당 공정 단계의 세부 공정들 추출761        sub_processes = process_info_df[process_info_df['표준단위공정'] == stage]['실공정(Step)'].tolist()762        sub_process_text = ", ".join(sub_processes)763        764        # 아이콘 및 색상 결정765        icon = CONFIG["PROCESS_ICONS"].get(stage, default_icon)766        color = CONFIG["PROCESS_COLORS"].get(stage, default_color)767        768        # 마지막 단계 여부 확인769        is_last = (i == len(process_stages) - 1)770        last_class = " last-step" if is_last else ""771        772        # 이미지 파일 경로 결정 (매핑 로직 개선)773        image_file = None774        775        # 1. 원본 이름으로 매핑 먼저 시도 (특수문자가 있는 경우도 처리)776        if stage in process_image_mapping:777            image_file = process_image_mapping[stage]778            logger.info(f"원본 이름으로 이미지 매핑: '{stage}' → {image_file}")779            780        # 2. 소문자 변환 후 매핑 시도781        elif stage.lower() in process_image_mapping:782            image_file = process_image_mapping[stage.lower()]783            logger.info(f"소문자 변환으로 이미지 매핑: '{stage}' → {image_file}")784            785        # 3. 특수문자 제거 및 공백 언더스코어 변환 후 매핑 시도786        else:787            # 특수문자를 공백으로 치환하고 연속된 공백을 하나로 합침788            normalized_stage = re.sub(r'[^\w\s]', ' ', stage).lower()789            normalized_stage = re.sub(r'\s+', ' ', normalized_stage).strip()790            791            # 공백을 언더스코어로 변경792            normalized_stage_us = normalized_stage.replace(' ', '_')793            794            if normalized_stage in process_image_mapping:795                image_file = process_image_mapping[normalized_stage]796                logger.info(f"정규화 이름으로 이미지 매핑: '{stage}' → '{normalized_stage}' → {image_file}")797            elif normalized_stage_us in process_image_mapping:798                image_file = process_image_mapping[normalized_stage_us]799                logger.info(f"언더스코어 변환으로 이미지 매핑: '{stage}' → '{normalized_stage_us}' → {image_file}")800            else:801                # 부분 매칭 시도 (예: "검사/포장" → "검사"로 부분 매칭)802                for key in process_image_mapping:803                    if key in stage.lower() or stage.lower() in key:804                        image_file = process_image_mapping[key]805                        logger.info(f"부분 매칭으로 이미지 매핑: '{stage}' 내에서 '{key}' 발견 → {image_file}")806                        break807                808                if not image_file:809                    logger.warning(f"단계 '{stage}'에 대한 이미지 파일 매핑 없음, 기본 아이콘 사용")810        811        # 이미지 파일 경로 완성 및 존재 여부 확인812        image_path = None813        base64_image = None814        815        if image_file:816            # 절대 경로 사용817            image_path = os.path.join(os.path.abspath(CONFIG['PROCESS_IMAGES_PATH']), image_file)818            logger.info(f"이미지 경로: {image_path}")819            820            if os.path.exists(image_path):821                try:822                    base64_image = get_image_as_base64(image_path)823                    logger.info(f"이미지 인코딩 성공: {image_path}")824                except Exception as e:825                    logger.error(f"이미지 인코딩 오류: {image_path}, 오류: {str(e)}")826            else:827                # 파일이 없을 경우 대체 파일 시도 (확장자 변경)828                alt_extensions = ['.jpg', '.jpeg', '.gif', '.png']829                for ext in alt_extensions:830                    alt_path = os.path.splitext(image_path)[0] + ext831                    if os.path.exists(alt_path):832                        try:833                            base64_image = get_image_as_base64(alt_path)834                            logger.info(f"대체 이미지 인코딩 성공: {alt_path}")835                            break836                        except Exception as e:837                            logger.error(f"대체 이미지 인코딩 오류: {alt_path}, 오류: {str(e)}")838                839                if not base64_image:840                    logger.warning(f"이미지 파일이 존재하지 않음: {image_path}")841        842        # 세부 공정 설명 추출 (첫 번째 세부 공정의 설명 사용)843        process_description = ""844        if sub_processes:845            first_sub_process = process_info_df[846                (process_info_df['표준단위공정'] == stage) & 847                (process_info_df['실공정(Step)'] == sub_processes[0])848            ]849            if not first_sub_process.empty:850                process_description = first_sub_process.iloc[0].get('공정내용', '')851        852        # base64로 인코딩된 이미지가 있는 경우와 없는 경우 처리853        if base64_image:854            # 이미지가 있는 경우 base64 인코딩된 이미지 표시855            icon_content = f"""856            <img src="{base64_image}" alt="{stage}" style="width: 100%; height: 100%; object-fit: contain;">857            """858        else:859            # 이미지가 없는 경우 아이콘 표시860            icon_content = f"""861            <i class="fas fa-{icon}" style="color: {color}; font-size: 2rem;"></i>862            """863        864        # 단계별 HTML 추가865        flow_html += f"""866        <div class="flow-step{last_class}">867            <div class="flow-icon" style="border-color: {color};">868                {icon_content}869            </div>870            <div class="flow-step-content">871                <div class="flow-step-title">{stage}</div>872                <div class="flow-step-desc">{sub_process_text[:30]}{'...' if len(sub_process_text) > 30 else ''}</div>873            </div>874            {'' if is_last else '<div class="arrow"><i class="fas fa-chevron-right"></i></div>'}875        </div>876        """877    878    # FontAwesome 추가879    flow_html += """880    </div>881    882    <!-- FontAwesome 추가 -->883    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">884    """885    886    return flow_html887 888def create_bar_chart(data, x, y, title, color_scale='Viridis', height=400):889    """막대 차트 생성 함수"""890    fig = px.bar(891        data,892        x=x,893        y=y,894        title=title,895        color=y,896        color_continuous_scale=color_scale897    )898    899    fig.update_layout(900        xaxis_title=x,901        yaxis_title=y,902        height=height903    )904    905    return fig906 907def create_pie_chart(data, values, names, title, hole=0.4, color_sequence=None):908    """파이 차트 생성 함수"""909    fig = px.pie(910        data,911        values=values,912        names=names,913        title=title,914        hole=hole,915        color_discrete_sequence=color_sequence916    )917    918    fig.update_traces(textposition='inside', textinfo='percent+label')919    return fig920 921def create_line_chart(x, y, title, line_color='blue', marker_size=8, height=400):922    """선 차트 생성 함수"""923    fig = go.Figure()924    925    fig.add_trace(go.Scatter(926        x=x,927        y=y,928        mode='lines+markers',929        name='데이터',930        line=dict(color=line_color, width=2),931        marker=dict(size=marker_size)932    ))933    934    fig.update_layout(935        title=title,936        xaxis_title="X축",937        yaxis_title="Y축",938        height=height939    )940    941    return fig942 943def create_pareto_chart(data, x, y, title, height=500):944    """파레토 차트 생성 함수"""945    fig = go.Figure()946    947    # 불량량 막대 그래프948    fig.add_trace(go.Bar(949        x=data[x],950        y=data[y],951        name='불량량',952        marker=dict(color='indianred')953    ))954    955    # 누적 불량량 계산956    data['누적비율'] = data[y].cumsum() / data[y].sum() * 100957    958    # 누적 비율 선 그래프959    fig.add_trace(go.Scatter(960        x=data[x],961        y=data['누적비율'],962        name='누적 비율',963        mode='lines+markers',964        yaxis='y2',965        line=dict(color='royalblue', width=2),966        marker=dict(size=8)967    ))968    969    # 80% 기준선970    fig.add_trace(go.Scatter(971        x=[data[x].iloc[0], data[x].iloc[-1]],972        y=[80, 80],973        name='80% 기준',974        mode='lines',975        yaxis='y2',976        line=dict(color='green', dash='dash')977    ))978    979    # 그래프 레이아웃 설정980    fig.update_layout(981        title=title,982        xaxis_title=x,983        yaxis_title=y,984        yaxis2=dict(985            title="누적 비율 (%)",986            overlaying='y',987            side='right',988            range=[0, 100]989        ),990        legend=dict(991            orientation="h",992            yanchor="bottom",993            y=1.02,994            xanchor="right",995            x=1996        ),997        height=height998    )999    1000    return fig1001 1002def create_histogram_with_normal(values, usl, lsl, mean, std, title, height=400):1003    """히스토그램 및 정규분포 생성 함수"""1004    hist_fig = go.Figure()1005    1006    # 히스토그램 추가1007    hist_fig.add_trace(go.Histogram(1008        x=values,1009        name='검사값 분포',1010        opacity=0.7,1011        marker=dict(color='royalblue'),1012        histnorm='probability density'1013    ))1014    1015    # 정규분포 곡선 추가1016    x_range = np.linspace(values.min() - 0.5, values.max() + 0.5, 100)1017    y_range = stats.norm.pdf(x_range, mean, std)1018    1019    hist_fig.add_trace(go.Scatter(1020        x=x_range,1021        y=y_range,1022        mode='lines',1023        name='정규분포',1024        line=dict(color='red', width=2)1025    ))1026    1027    # 규격 상한/하한 추가1028    hist_fig.add_trace(go.Scatter(1029        x=[usl, usl],1030        y=[0, max(y_range) * 1.2],1031        mode='lines',1032        name='상한 규격',1033        line=dict(color='green', width=2, dash='dash')1034    ))1035    1036    hist_fig.add_trace(go.Scatter(1037        x=[lsl, lsl],1038        y=[0, max(y_range) * 1.2],1039        mode='lines',1040        name='하한 규격',1041        line=dict(color='green', width=2, dash='dash')1042    ))1043    1044    # 평균선 추가1045    hist_fig.add_trace(go.Scatter(1046        x=[mean, mean],1047        y=[0, max(y_range) * 1.2],1048        mode='lines',1049        name='평균',1050        line=dict(color='black', width=2)1051    ))1052    1053    # 그래프 레이아웃 설정1054    hist_fig.update_layout(1055        title=title,1056        xaxis_title="검사값",1057        yaxis_title="확률 밀도",1058        legend=dict(1059            orientation="h",1060            yanchor="bottom",1061            y=1.02,1062            xanchor="right",1063            x=11064        ),1065        height=height1066    )1067    1068    return hist_fig1069 1070# 화면 표시 함수1071def display_quality_analysis(data):1072    """품질 분석 표시 함수"""1073    st.markdown("<div style='margin-top: 30px;'></div>", unsafe_allow_html=True)1074    st.subheader("품질 분석")1075    1076    # 공정능력 정보 확인1077    process_capability = data.get('process_capability', pd.DataFrame())1078    1079    if process_capability.empty:1080        st.warning("공정능력 정보를 불러올 수 없습니다.")1081        return1082    1083    # 제품 선택1084    if '제품' in process_capability.columns:1085        products = process_capability['제품'].unique()1086        if len(products) > 0:1087            selected_product = st.selectbox("제품 선택", products)1088            1089            # 선택된 제품의 공정능력 데이터1090            product_capability = process_capability[process_capability['제품'] == selected_product]1091            1092            # 검사항목 선택1093            if '검사항목' in product_capability.columns and not product_capability.empty:1094                inspection_items = product_capability['검사항목'].unique()1095                if len(inspection_items) > 0:1096                    selected_item = st.selectbox("검사항목 선택", inspection_items)1097                    1098                    # 선택된 검사항목의 데이터1099                    item_data = product_capability[product_capability['검사항목'] == selected_item]1100                    1101                    if not item_data.empty:1102                        # 월별 공정능력 차트1103                        st.markdown("### 월별 공정능력 추이")1104                        1105                        # 필요한 컬럼이 있는지 확인1106                        required_columns = ['월', 'Cpk']1107                        if all(col in item_data.columns for col in required_columns):1108                            fig = go.Figure()1109                            1110                            # Cpk 추이 그래프1111                            fig.add_trace(go.Scatter(1112                                x=item_data['월'],1113                                y=item_data['Cpk'],1114                                mode='lines+markers',1115                                name='Cpk',1116                                line=dict(color='blue', width=2),1117                                marker=dict(size=8)1118                            ))1119                            1120                            # 기준선 (Cpk=1.33)1121                            fig.add_trace(go.Scatter(1122                                x=[item_data['월'].min(), item_data['월'].max()],1123                                y=[1.33, 1.33],1124                                mode='lines',1125                                name='기준 (Cpk=1.33)',1126                                line=dict(color='red', dash='dash')1127                            ))1128                            1129                            # 그래프 레이아웃 설정1130                            fig.update_layout(1131                                title=f"{selected_product} - {selected_item} 공정능력 추이",1132                                xaxis_title="월",1133                                yaxis_title="공정능력지수",1134                                legend=dict(1135                                    orientation="h",1136                                    yanchor="bottom",1137                                    y=1.02,1138                                    xanchor="right",1139                                    x=11140                                ),1141                                height=4001142                            )1143                            1144                            st.plotly_chart(fig, use_container_width=True)1145                        else:1146                            st.warning("공정능력 추이를 표시하는데 필요한 데이터가 없습니다.")1147                        1148                        # 검사값 분포 차트1149                        st.markdown("### 검사값 분포")1150                        1151                        # 검사값, 규격 상한/하한 데이터1152                        if all(col in item_data.columns for col in ['검사값', '상한', '하한']):1153                            values = item_data['검사값']1154                            if not values.empty:1155                                mean = values.mean()1156                                std = values.std()1157                                usl = item_data['상한'].mean()1158                                lsl = item_data['하한'].mean()1159                                1160                                # 히스토그램 생성1161                                hist_fig = create_histogram_with_normal(1162                                    values, usl, lsl, mean, std,1163                                    title=f"{selected_product} - {selected_item} 검사값 분포"1164                                )1165                                1166                                st.plotly_chart(hist_fig, use_container_width=True)1167                                1168                                # 공정능력 분석 결과1169                                st.markdown("### 공정능력 분석 결과")1170                                1171                                # 공정능력지수 계산1172                                capability = calculate_process_capability(values, usl, lsl)1173                                1174                                col1, col2, col3, col4 = st.columns(4)1175                                1176                                with col1:1177                                    st.metric(label="Cp", value=f"{capability['Cp']:.3f}")1178                                with col2:1179                                    st.metric(label="Cpk", value=f"{capability['Cpk']:.3f}")1180                                with col3:1181                                    st.metric(label="Cpu", value=f"{capability['Cpu']:.3f}")1182                                with col4:1183                                    st.metric(label="Cpl", value=f"{capability['Cpl']:.3f}")1184                                1185                                # 예상 불량률1186                                st.metric(label="예상 불량률 (PPM)", value=f"{capability['PPM']:.2f}")1187                                1188                                # 공정능력 평가1189                                if capability['Cpk'] >= 1.33:1190                                    st.success("공정능력 평가: 우수 (Cpk ≥ 1.33)")1191                                elif capability['Cpk'] >= 1.00:1192                                    st.warning("공정능력 평가: 보통 (1.00 ≤ Cpk < 1.33)")1193                                else:1194                                    st.error("공정능력 평가: 미흡 (Cpk < 1.00)")1195                            else:1196                                st.warning("검사값 데이터가 없습니다.")1197                        else:1198                            st.warning("검사값 분포를 표시하는데 필요한 데이터가 없습니다.")1199                    else:1200                        st.warning(f"{selected_item} 검사항목에 대한 데이터가 없습니다.")

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