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