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 process_specs_df = data.get('process_specs', pd.DataFrame())
580
581 # 공정 정보가 비어 있는 경우 처리
582 if process_info_df.empty:
583 st.warning("공정 정보를 불러올 수 없습니다.")
584 return
585
586 # 공정 흐름도 시각화 (개선된 버전)
587 st.markdown("#### 공정 흐름도")
588
589 # 공정 단계 추출
590 process_stages = process_info_df['표준단위공정'].unique()
591
592 # 각 공정 단계별 아이콘 매핑
593 process_icons = {
594 "Batch": "flask",
595 "Forming": "industry",
596 "Sizing": "tint",
597 "Winding": "sync",
598 "Drying": "fire",
599 "Packing": "box"
600 }
601
602 # 공정 단계별 색상 매핑
603 process_colors = {
604 "Batch": "#4285F4", # 파란색
605 "Forming": "#EA4335", # 빨간색
606 "Sizing": "#FBBC05", # 노란색
607 "Winding": "#34A853", # 녹색
608 "Drying": "#FF6D01", # 주황색
609 "Packing": "#46BDC6" # 청록색
610 }
611
612 # 기본 색상 및 아이콘 설정
613 default_color = "#6C757D"
614 default_icon = "cog"
615
616 # HTML로 공정 흐름도 생성
617 flow_html = """
618 <style>
619 .flow-container {
620 display: flex;
621 justify-content: space-between;
622 align-items: center;
623 margin: 30px 0;
624 position: relative;
625 overflow-x: auto;
626 padding: 20px 10px;
627 }
628
629 .flow-container::before {
630 content: "";
631 position: absolute;
632 top: 50%;
633 left: 70px;
634 right: 70px;
635 height: 3px;
636 background: linear-gradient(to right, #e0e0e0, #2563EB, #e0e0e0);
637 z-index: 1;
638 }
639
640 .flow-step {
641 display: flex;
642 flex-direction: column;
643 align-items: center;
644 position: relative;
645 z-index: 2;
646 min-width: 120px;
647 }
648
649 .flow-icon {
650 width: 80px;
651 height: 80px;
652 border-radius: 50%;
653 display: flex;
654 align-items: center;
655 justify-content: center;
656 margin-bottom: 12px;
657 color: white;
658 font-size: 30px;
659 box-shadow: 0 4px 8px rgba(0,0,0,0.1);
660 position: relative;
661 background: white;
662 border: 3px solid;
663 transition: transform 0.3s ease, box-shadow 0.3s ease;
664 }
665
666 .flow-icon:hover {
667 transform: translateY(-5px);
668 box-shadow: 0 6px 12px rgba(0,0,0,0.15);
669 }
670
671 .flow-step-title {
672 font-size: 16px;
673 font-weight: 600;
674 text-align: center;
675 color: #333;
676 }
677
678 .flow-step-desc {
679 font-size: 13px;
680 color: #666;
681 text-align: center;
682 margin-top: 5px;
683 max-width: 120px;
684 }
685
686 .arrow {
687 position: absolute;
688 top: 50%;
689 transform: translateY(-50%);
690 right: -20px;
691 color: #2563EB;
692 font-size: 24px;
693 z-index: 3;
694 }
695
696 .last-step .arrow {
697 display: none;
698 }
699
700 @media (max-width: 768px) {
701 .flow-container {
702 flex-direction: column;
703 align-items: flex-start;
704 overflow-x: visible;
705 }
706
707 .flow-container::before {
708 top: 70px;
709 bottom: 70px;
710 left: 40px;
711 width: 3px;
712 height: auto;
713 background: linear-gradient(to bottom, #e0e0e0, #2563EB, #e0e0e0);
714 }
715
716 .flow-step {
717 flex-direction: row;
718 margin-bottom: 30px;
719 width: 100%;
720 }
721
722 .flow-icon {
723 margin-right: 20px;
724 margin-bottom: 0;
725 }
726
727 .flow-step-content {
728 text-align: left;
729 }
730
731 .flow-step-title {
732 text-align: left;
733 }
734
735 .flow-step-desc {
736 text-align: left;
737 max-width: none;
738 }
739
740 .arrow {
741 transform: rotate(90deg);
742 right: auto;
743 top: auto;
744 bottom: -25px;
745 left: 40px;
746 }
747 }
748 </style>
749
750 <div class="flow-container">
751 """
752
753 # 각 공정 단계별 HTML 생성
754 for i, stage in enumerate(process_stages):
755 # 해당 공정 단계의 세부 공정들 추출
756 sub_processes = process_info_df[process_info_df['표준단위공정'] == stage]['실공정(Step)'].tolist()
757 sub_process_text = ", ".join(sub_processes)
758
759 # 아이콘 및 색상 결정
760 icon = process_icons.get(stage, default_icon)
761 color = process_colors.get(stage, default_color)
762
763 # 마지막 단계 여부 확인
764 is_last = (i == len(process_stages) - 1)
765 last_class = " last-step" if is_last else ""
766
767 # 단계별 HTML 추가
768 flow_html += f"""
769 <div class="flow-step{last_class}">
770 <div class="flow-icon" style="border-color: {color};">
771 <i class="fas fa-{icon}" style="color: {color};"></i>
772 </div>
773 <div class="flow-step-content">
774 <div class="flow-step-title">{stage}</div>
775 <div class="flow-step-desc">{sub_process_text[:30]}{'...' if len(sub_process_text) > 30 else ''}</div>
776 </div>
777 {'' if is_last else '<div class="arrow"><i class="fas fa-chevron-right"></i></div>'}
778 </div>
779 """
780
781 flow_html += """
782 </div>
783 """
784
785 # HTML 흐름도 표시
786 st.components.v1.html(flow_html, height=200)
787
788 # 공정 단계 선택 및 세부 정보 표시 (기존 코드)
789 st.markdown("#### 공정 단계별 상세 정보")
790
791 # 공정 단계 선택
792 selected_stage = st.selectbox("공정 단계 선택", process_stages)
793
794 # 선택된 공정 단계의 세부 공정 표시
795 sub_processes = process_info_df[process_info_df['표준단위공정'] == selected_stage]
796
797 if not sub_processes.empty:
798 # 세부 공정 선택
799 selected_sub = st.selectbox("세부 공정 선택", sub_processes['실공정(Step)'].tolist())
800
801 # 선택된 세부 공정 정보 표시
802 process_detail = sub_processes[sub_processes['실공정(Step)'] == selected_sub].iloc[0]
803
804 # 공정 정보 카드 표시
805 col1, col2 = st.columns(2)
806
807 with col1:
808 st.markdown(f"""
809 <div class="process-box">
810 <h4 style="margin-top: 0;">공정 개요</h4>
811 <p><strong>공정명:</strong> {selected_sub}</p>
812 <p><strong>공정내용:</strong> {process_detail['공정내용']}</p>
813 </div>
814 """, unsafe_allow_html=True)
815
816 with col2:
817 st.markdown(f"""
818 <div class="process-box">
819 <h4 style="margin-top: 0;">품질 관리 포인트</h4>
820 <p><strong>품질 인자:</strong> {process_detail['품질 인자']}</p>
821 </div>
822 """, unsafe_allow_html=True)
823
824 # 공정 조건 인자와 개선 포인트 표시
825 col1, col2 = st.columns(2)
826
827 with col1:
828 st.markdown(f"""
829 <div class="process-box">
830 <h4 style="margin-top: 0;">공정 조건 인자</h4>
831 <p>{process_detail['공정 조건 인자']}</p>
832 </div>
833 """, unsafe_allow_html=True)
834
835 with col2:
836 st.markdown(f"""
837 <div class="issue-box">
838 <h4 style="margin-top: 0;">개선 Point (문제점)</h4>
839 <p>{process_detail['개선 Point (문제점)']}</p>
840 </div>
841 """, unsafe_allow_html=True)
842
843 # 제품 및 공정규격 정보 표시 (새로 추가된 부분)
844 if not process_specs_df.empty:
845 st.markdown("#### 공정 규격 정보")
846
847 # 공정명으로 규격 정보 필터링
848 # 공정명을 기준으로 매핑 (표준단위공정과 실공정(Step)을 공정명과 매핑)
849 process_name_mapping = {
850 "Batch": "Batch(배합)",
851 "Forming": "섬유화",
852 "Sizing": "SIZE",
853 "Winding": "Winding",
854 "Drying": "Drying",
855 "Packing": "포장",
856 "인수검사": "인수검사",
857 "용융": "용융",
858 "Chopping": "Chopping",
859 "Pelletizer": "Pelletizer",
860 "Water Spray": "Water Spray"
861 }
862
863 # 매핑된 공정명으로 필터링
864 mapped_process_name = process_name_mapping.get(selected_stage, selected_sub)
865 filtered_specs = process_specs_df[process_specs_df['공정명'].str.contains(mapped_process_name, na=False)]
866
867 # 세부 공정명으로 추가 필터링 시도
868 if filtered_specs.empty:
869 filtered_specs = process_specs_df[process_specs_df['공정명'].str.contains(selected_sub, na=False)]
870
871 if not filtered_specs.empty:
872 # 탭 생성: 규격 정보와 차트
873 spec_tabs = st.tabs(["규격 정보", "규격 시각화"])
874
875 with spec_tabs[0]:
876 # 규격 정보 표시
877 st.dataframe(
878 filtered_specs,
879 column_config={
880 "공정명": st.column_config.TextColumn("공정명", width="medium"),
881 "작업설명": st.column_config.TextColumn("작업설명", width="large"),
882 "설비명": st.column_config.TextColumn("설비명", width="medium"),
883 "구분": st.column_config.TextColumn("구분", width="small"),
884 "항목": st.column_config.TextColumn("항목", width="medium"),
885 "CTP/CTQ": st.column_config.TextColumn("CTP/CTQ", width="small"),
886 "규격": st.column_config.TextColumn("규격", width="large"),
887 "평가(측정) 방법": st.column_config.TextColumn("평가(측정) 방법", width="medium"),
888 "주기": st.column_config.TextColumn("주기", width="small"),
889 "관리방법": st.column_config.TextColumn("관리방법", width="medium"),
890 "대응계획": st.column_config.TextColumn("대응계획", width="large")
891 },
892 use_container_width=True,
893 hide_index=True
894 )
895
896 with spec_tabs[1]:
897 # CTP/CTQ 항목 강조 표시
898 ctp_items = filtered_specs[filtered_specs['CTP/CTQ'].notna()]
899
900 if not ctp_items.empty:
901 st.markdown("##### 주요 CTP/CTQ 항목")
902
903 # 주요 항목 카드 형태로 표시
904 cols = st.columns(min(3, len(ctp_items)))
905
906 for i, (_, item) in enumerate(ctp_items.iterrows()):
907 col_idx = i % 3
908 with cols[col_idx]:
909 st.markdown(f"""
910 <div style="background-color: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #4285F4; margin-bottom: 10px;">
911 <h5 style="margin-top: 0; color: #2C3E50;">{item['항목']}</h5>
912 <p><strong>규격:</strong> {item['규격']}</p>
913 <p><strong>측정 방법:</strong> {item['평가(측정) 방법']}</p>
914 <p><strong>주기:</strong> {item['주기']}</p>
915 </div>
916 """, unsafe_allow_html=True)
917
918 # 규격 항목 시각화
919 if '항목' in filtered_specs.columns:
920 # 항목별 개수 계산
921 item_counts = filtered_specs['항목'].value_counts().reset_index()
922 item_counts.columns = ['항목', '개수']
923
924 # 항목별 차트
925 fig = px.bar(
926 item_counts.head(10),
927 x='항목',
928 y='개수',
929 title=f"{mapped_process_name} 주요 관리 항목 TOP 10",
930 color='개수',
931 color_continuous_scale='Blues'
932 )
933
934 fig.update_layout(
935 xaxis_title='관리 항목',
936 yaxis_title='개수',
937 height=400
938 )
939
940 st.plotly_chart(fig, use_container_width=True)
941
942 # 측정 방법 분포
943 if '평가(측정) 방법' in filtered_specs.columns:
944 method_counts = filtered_specs['평가(측정) 방법'].value_counts().reset_index()
945 method_counts.columns = ['측정 방법', '개수']
946
947 fig_pie = px.pie(
948 method_counts,
949 values='개수',
950 names='측정 방법',
951 title=f"{mapped_process_name} 측정 방법 분포",
952 hole=0.4
953 )
954
955 fig_pie.update_traces(textposition='inside', textinfo='percent+label')
956 st.plotly_chart(fig_pie, use_container_width=True)
957 else:
958 st.info(f"{selected_stage} 공정에 대한 규격 정보를 찾을 수 없습니다.")
959 else:
960 st.warning("제품 및 공정규격 정보를 불러올 수 없습니다.")
961 else:
962 st.warning(f"{selected_stage} 공정에 대한 정보를 찾을 수 없습니다.")
963
964 # 전체 공정 맵 표시
965 st.markdown("#### 전체 공정 맵")
966
967 # 공정 데이터 테이블로 표시
968 st.dataframe(
969 process_info_df,
970 column_config={
971 "표준단위공정": st.column_config.TextColumn("표준단위공정", width="medium"),
972 "실공정(Step)": st.column_config.TextColumn("실공정(Step)", width="medium"),
973 "공정내용": st.column_config.TextColumn("공정내용", width="large"),
974 "공정 조건 인자": st.column_config.TextColumn("공정 조건 인자", width="large"),
975 "품질 인자": st.column_config.TextColumn("품질 인자", width="large"),
976 "개선 Point (문제점)": st.column_config.TextColumn("개선 Point (문제점)", width="large"),
977 },
978 use_container_width=True,
979 hide_index=True
980 )
981
982 # 공정별 품질 인자 시각화
983 st.markdown("#### 공정별 품질 인자")
984
985 # 품질 인자 데이터 준비
986 quality_factors = []
987 for _, row in process_info_df.iterrows():
988 factors = row['품질 인자'].split(',')
989 for factor in factors:
990 quality_factors.append({
991 '공정': row['표준단위공정'] + '-' + row['실공정(Step)'],
992 '품질 인자': factor.strip()
993 })
994
995 quality_df = pd.DataFrame(quality_factors)
996
997 # 품질 인자 빈도 계산
998 factor_counts = quality_df['품질 인자'].value_counts().reset_index()
999 factor_counts.columns = ['품질 인자', '빈도']
1000 factor_counts = factor_counts.sort_values('빈도', ascending=False).head(10)
1001
1002 # 품질 인자 빈도 차트
1003 fig = px.bar(
1004 factor_counts,
1005 x='품질 인자',
1006 y='빈도',
1007 title='주요 품질 인자 TOP 10',
1008 color='빈도',
1009 color_continuous_scale='Viridis'
1010 )
1011
1012 fig.update_layout(
1013 xaxis_title='품질 인자',
1014 yaxis_title='언급 빈도',
1015 height=400
1016 )
1017
1018 st.plotly_chart(fig, use_container_width=True)
1019
1020 # 공정별 개선 포인트 시각화
1021 st.markdown("#### 공정별 개선 포인트")
1022
1023 # 개선 포인트 데이터 준비
1024 improvement_points = []
1025 for _, row in process_info_df.iterrows():
1026 points = row['개선 Point (문제점)'].split(',')
1027 for point in points:
1028 improvement_points.append({
1029 '공정': row['표준단위공정'] + '-' + row['실공정(Step)'],
1030 '개선 포인트': point.strip()
1031 })
1032
1033 improvement_df = pd.DataFrame(improvement_points)
1034
1035 # 개선 포인트 워드 클라우드 (텍스트 기반 시각화)
1036 st.markdown("##### 주요 개선 포인트 키워드")
1037
1038 # 개선 포인트 텍스트 모음
1039 all_points = ' '.join(improvement_df['개선 포인트'].tolist())
1040
1041 # 불용어 정의 (의미 없는 조사, 접속사 등)
1042 stopwords = ['인한', '로', '으로', '에', '에서', '의', '이', '가', '을', '를', '은', '는',
1043 '와', '과', '이나', '또는', '및', '등', '에서의', '로의', '로써', '로서', '하여',
1044 '하는', '하게', '하다', '한', '된', '되는', '되어', '때문에', '때문', '통한',
1045 '통해', '의한', '의해', '에의한', '에의해', '불균일로', '편차로', '마모로', '저하로', '발생']
1046
1047 # 유효 키워드 최소 길이
1048 min_keyword_length = 2
1049
1050 # 주요 키워드 추출 (개선된 방식)
1051 keywords = {}
1052
1053 # 텍스트를 단어로 분리
1054 words = all_points.replace(',', ' ').replace('.', ' ').replace('(', ' ').replace(')', ' ').split()
1055
1056 for word in words:
1057 word = word.strip()
1058
1059 # 불용어 제거 및 길이 체크
1060 if word not in stopwords and len(word) >= min_keyword_length:
1061 # 단어 끝에 붙은 '로', '에', '의' 등 제거
1062 for suffix in ['로', '에', '의', '을', '를', '은', '는']:
1063 if word.endswith(suffix) and len(word) > len(suffix):
1064 word = word[:-len(suffix)]
1065
1066 # 단어가 여전히 최소 길이 이상인지 확인
1067 if len(word) >= min_keyword_length:
1068 if word in keywords:
1069 keywords[word] += 1
1070 else:
1071 keywords[word] = 1
1072
1073 # 상위 키워드 추출
1074 top_keywords = sorted(keywords.items(), key=lambda x: x[1], reverse=True)[:20]
1075 top_keywords_df = pd.DataFrame(top_keywords, columns=['키워드', '빈도'])
1076
1077 # 키워드 빈도 차트
1078 fig = px.bar(
1079 top_keywords_df,
1080 x='키워드',
1081 y='빈도',
1082 title='주요 개선 포인트 키워드 TOP 20',
1083 color='빈도',
1084 color_continuous_scale='Reds'
1085 )
1086
1087 fig.update_layout(
1088 xaxis_title='키워드',
1089 yaxis_title='빈도',
1090 height=400
1091 )
1092
1093 st.plotly_chart(fig, use_container_width=True)
1094
1095
1096def display_quality_analysis(data):
1097 """품질 분석 표시 함수"""
1098 st.subheader("품질 분석")
1099
1100 # 공정능력 정보 확인
1101 process_capability = data.get('process_capability', pd.DataFrame())
1102
1103 if process_capability.empty:
1104 st.warning("공정능력 정보를 불러올 수 없습니다.")
1105 return
1106
1107 # 제품 선택
1108 if '제품' in process_capability.columns:
1109 products = process_capability['제품'].unique()
1110 if len(products) > 0:
1111 selected_product = st.selectbox("제품 선택", products)
1112
1113 # 선택된 제품의 공정능력 데이터
1114 product_capability = process_capability[process_capability['제품'] == selected_product]
1115
1116 # 검사항목 선택
1117 if '검사항목' in product_capability.columns and not product_capability.empty:
1118 inspection_items = product_capability['검사항목'].unique()
1119 if len(inspection_items) > 0:
1120 selected_item = st.selectbox("검사항목 선택", inspection_items)
1121
1122 # 선택된 검사항목의 데이터
1123 item_data = product_capability[product_capability['검사항목'] == selected_item]
1124
1125 if not item_data.empty:
1126 # 월별 공정능력 차트
1127 st.markdown("### 월별 공정능력 추이")
1128
1129 # 필요한 컬럼이 있는지 확인
1130 required_columns = ['월', 'Cpk']
1131 if all(col in item_data.columns for col in required_columns):
1132 fig = go.Figure()
1133
1134 # Cpk 추이 그래프
1135 fig.add_trace(go.Scatter(
1136 x=item_data['월'],
1137 y=item_data['Cpk'],
1138 mode='lines+markers',
1139 name='Cpk',
1140 line=dict(color='blue', width=2),
1141 marker=dict(size=8)
1142 ))
1143
1144 # 기준선 (Cpk=1.33)
1145 fig.add_trace(go.Scatter(
1146 x=[item_data['월'].min(), item_data['월'].max()],
1147 y=[1.33, 1.33],
1148 mode='lines',
1149 name='기준 (Cpk=1.33)',
1150 line=dict(color='red', dash='dash')
1151 ))
1152
1153 # 그래프 레이아웃 설정
1154 fig.update_layout(
1155 title=f"{selected_product} - {selected_item} 공정능력 추이",
1156 xaxis_title="월",
1157 yaxis_title="공정능력지수",
1158 legend=dict(
1159 orientation="h",
1160 yanchor="bottom",
1161 y=1.02,
1162 xanchor="right",
1163 x=1
1164 ),
1165 height=400
1166 )
1167
1168 st.plotly_chart(fig, use_container_width=True)
1169 else:
1170 st.warning("공정능력 추이를 표시하는데 필요한 데이터가 없습니다.")
1171
1172 # 검사값 분포 차트
1173 st.markdown("### 검사값 분포")
1174
1175 # 검사값, 규격 상한/하한 데이터
1176 if all(col in item_data.columns for col in ['검사값', '상한', '하한']):
1177 values = item_data['검사값']
1178 if not values.empty:
1179 mean = values.mean()
1180 std = values.std()
1181 usl = item_data['상한'].mean()
1182 lsl = item_data['하한'].mean()
1183
1184 # 히스토그램 생성
1185 hist_fig = go.Figure()
1186
1187 # 히스토그램 추가
1188 hist_fig.add_trace(go.Histogram(
1189 x=values,
1190 name='검사값 분포',
1191 opacity=0.7,
1192 marker=dict(color='royalblue'),
1193 histnorm='probability density'
1194 ))
1195
1196 # 정규분포 곡선 추가
1197 x_range = np.linspace(values.min() - 0.5, values.max() + 0.5, 100)
1198 y_range = stats.norm.pdf(x_range, mean, std)
1199
1200 hist_fig.add_trace(go.Scatter(
