🔧 볼트 재질 정보 개선 및 A320/A194M 패턴 지원
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- bolt_classifier.py: A320/A194M 조합 패턴 처리 로직 추가 - material_grade_extractor.py: A320/A194M 패턴 추출 개선 - integrated_classifier.py: SPECIAL, U_BOLT 카테고리 우선 분류 - 데이터베이스: 492개 볼트의 material_grade를 완전한 형태로 업데이트 - A320/A194M GR B8/8: 78개 - A193/A194 GR B7/2H: 414개 - 프론트엔드: BOLT 카테고리 전용 UI (길이 표시) - Excel 내보내기: BOLT용 컬럼 순서 및 재질 정보 개선 - SPECIAL, U_BOLT 카테고리 지원 추가
This commit is contained in:
@@ -111,8 +111,15 @@
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.materials-header h1 {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
@@ -120,10 +127,18 @@
|
||||
|
||||
.job-info {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.material-count-inline {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
background: #f3f4f6;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.material-count {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
@@ -132,10 +147,27 @@
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 메인 헤더 */
|
||||
.materials-header {
|
||||
background: white;
|
||||
padding: 8px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 카테고리 필터 */
|
||||
.category-filters {
|
||||
background: white;
|
||||
padding: 16px 24px;
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
@@ -262,69 +294,598 @@
|
||||
margin: 0;
|
||||
min-width: 1500px;
|
||||
overflow-x: auto;
|
||||
max-height: calc(100vh - 200px); /* 최대 높이만 제한 */
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detailed-grid-header {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 80px 250px 80px 80px 450px 120px 150px 100px;
|
||||
padding: 12px 24px;
|
||||
/* 기본 그리드는 사용하지 않음 - 각 카테고리별 전용 클래스 사용 */
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
background: #f9fafb;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.detailed-grid-header > div {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.detailed-grid-header > div {
|
||||
border-right: 1px solid #d1d5db;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.detailed-grid-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.detailed-grid-header > div:last-child,
|
||||
.detailed-grid-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* PIPE 전용 헤더 - 9개 컬럼 */
|
||||
.detailed-grid-header.pipe-header {
|
||||
grid-template-columns: 60px 90px 130px 80px 100px 250px 120px 200px 100px !important;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.detailed-grid-header.pipe-header > div,
|
||||
.detailed-grid-header.pipe-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.detailed-grid-header.pipe-header > div:last-child,
|
||||
.detailed-grid-header.pipe-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* PIPE 전용 행 - 9개 컬럼 */
|
||||
.detailed-material-row.pipe-row {
|
||||
grid-template-columns: 60px 90px 130px 80px 100px 250px 120px 200px 100px !important;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-material-row.pipe-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detailed-material-row.pipe-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* SPECIAL 전용 헤더 - 10개 컬럼 */
|
||||
.detailed-grid-header.special-header {
|
||||
grid-template-columns: 60px 90px 150px 80px 100px 200px 120px 120px 200px 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.detailed-grid-header.special-header > div,
|
||||
.detailed-grid-header.special-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.detailed-grid-header.special-header > div:last-child,
|
||||
.detailed-grid-header.special-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* SPECIAL 전용 행 - 10개 컬럼 */
|
||||
.detailed-material-row.special-row {
|
||||
grid-template-columns: 60px 90px 150px 80px 100px 200px 120px 120px 200px 100px;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
/* BOLT 전용 헤더 - 9개 컬럼 */
|
||||
.detailed-grid-header.bolt-header {
|
||||
grid-template-columns: 60px 90px 130px 80px 100px 250px 120px 200px 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* BOLT 전용 행 - 9개 컬럼 */
|
||||
.detailed-material-row.bolt-row {
|
||||
grid-template-columns: 60px 90px 130px 80px 100px 250px 120px 200px 100px;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-material-row.special-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* BOLT 헤더 테두리 */
|
||||
.detailed-grid-header.bolt-header > div,
|
||||
.detailed-grid-header.bolt-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.detailed-grid-header.bolt-header > div:last-child,
|
||||
.detailed-grid-header.bolt-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* BOLT 행 테두리 */
|
||||
.detailed-material-row.bolt-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detailed-material-row.bolt-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* BOLT 타입 배지 */
|
||||
.type-badge.bolt {
|
||||
background: #7c3aed;
|
||||
color: white;
|
||||
border: 2px solid #6d28d9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* U-BOLT 전용 헤더 - 8개 컬럼 */
|
||||
.detailed-grid-header.ubolt-header {
|
||||
grid-template-columns: 60px 90px 130px 80px 200px 120px 200px 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* U-BOLT 전용 행 - 8개 컬럼 */
|
||||
.detailed-material-row.ubolt-row {
|
||||
grid-template-columns: 60px 90px 130px 80px 200px 120px 200px 100px;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
/* U-BOLT 헤더 테두리 */
|
||||
.detailed-grid-header.ubolt-header > div,
|
||||
.detailed-grid-header.ubolt-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
.detailed-grid-header.ubolt-header > div:last-child,
|
||||
.detailed-grid-header.ubolt-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* U-BOLT 행 테두리 */
|
||||
.detailed-material-row.ubolt-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
.detailed-material-row.ubolt-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* U-BOLT 타입 배지 */
|
||||
.type-badge.ubolt {
|
||||
background: #059669;
|
||||
color: white;
|
||||
border: 2px solid #047857;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* URETHANE 타입 배지 */
|
||||
.type-badge.urethane {
|
||||
background: #ea580c;
|
||||
color: white;
|
||||
border: 2px solid #c2410c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detailed-material-row.special-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 플랜지 전용 헤더 - 10개 컬럼 */
|
||||
.detailed-grid-header.flange-header {
|
||||
grid-template-columns: 40px 80px 150px 80px 100px 80px 350px 100px 150px 100px;
|
||||
grid-template-columns: 60px 100px 150px 80px 100px 80px 350px 100px 150px 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.detailed-grid-header.flange-header > div,
|
||||
.detailed-grid-header.flange-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.detailed-grid-header.flange-header > div:last-child,
|
||||
.detailed-grid-header.flange-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 플랜지 전용 행 - 10개 컬럼 */
|
||||
.detailed-material-row.flange-row {
|
||||
grid-template-columns: 40px 80px 150px 80px 100px 80px 350px 100px 150px 100px;
|
||||
grid-template-columns: 60px 100px 150px 80px 100px 80px 350px 100px 150px 100px;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-material-row.flange-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detailed-material-row.flange-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 피팅 전용 헤더 - 10개 컬럼 */
|
||||
.detailed-grid-header.fitting-header {
|
||||
grid-template-columns: 40px 80px 280px 80px 80px 140px 350px 100px 150px 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-grid-header.fitting-header > div,
|
||||
.detailed-grid-header.fitting-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.detailed-grid-header.fitting-header > div:last-child,
|
||||
.detailed-grid-header.fitting-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 피팅 전용 행 - 10개 컬럼 */
|
||||
.detailed-material-row.fitting-row {
|
||||
grid-template-columns: 40px 80px 280px 80px 80px 140px 350px 100px 150px 100px;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-material-row.fitting-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detailed-material-row.fitting-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 밸브 전용 헤더 - 9개 컬럼 (스케줄 제거, 타입 너비 증가) */
|
||||
.detailed-grid-header.valve-header {
|
||||
grid-template-columns: 40px 220px 100px 80px 80px 350px 100px 150px 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-grid-header.valve-header > div,
|
||||
.detailed-grid-header.valve-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.detailed-grid-header.valve-header > div:last-child,
|
||||
.detailed-grid-header.valve-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 밸브 전용 행 - 9개 컬럼 (스케줄 제거, 타입 너비 증가) */
|
||||
.detailed-material-row.valve-row {
|
||||
grid-template-columns: 40px 220px 100px 80px 80px 350px 100px 150px 100px;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-material-row.valve-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detailed-material-row.valve-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 가스켓 전용 헤더 - 11개 컬럼 (타입 좁게, 상세내역 넓게, 두께 추가) */
|
||||
.detailed-grid-header.gasket-header {
|
||||
grid-template-columns: 40px 80px 120px 80px 80px 100px 400px 60px 80px 150px 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-grid-header.gasket-header > div,
|
||||
.detailed-grid-header.gasket-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.detailed-grid-header.gasket-header > div:last-child,
|
||||
.detailed-grid-header.gasket-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 가스켓 전용 행 - 11개 컬럼 (타입 좁게, 상세내역 넓게, 두께 추가) */
|
||||
.detailed-material-row.gasket-row {
|
||||
grid-template-columns: 40px 80px 120px 80px 80px 100px 400px 60px 80px 150px 100px;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-material-row.gasket-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detailed-material-row.gasket-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 필터링 가능한 헤더 스타일 */
|
||||
.filterable-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.filterable-header:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.filterable-header:hover .header-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sort-btn, .filter-btn {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 1px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 9px;
|
||||
color: #64748b;
|
||||
transition: all 0.15s ease;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sort-btn:hover, .filter-btn:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.sort-btn.active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 필터 드롭다운 */
|
||||
.filter-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 2px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-height: 240px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: dropdownFadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-search {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #fafbfc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-search input {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
background: white;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.filter-search input:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.filter-search input::placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.clear-filter-btn {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.clear-filter-btn:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 160px;
|
||||
}
|
||||
|
||||
.filter-option-header {
|
||||
padding: 4px 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.filter-option:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.filter-option-more {
|
||||
padding: 4px 10px;
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* 액션 바 스타일 개선 */
|
||||
.filter-info {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.clear-filters-btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.clear-filters-btn:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
/* UNKNOWN 전용 헤더 - 5개 컬럼 */
|
||||
.detailed-grid-header.unknown-header {
|
||||
grid-template-columns: 40px 100px 1fr 150px 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-grid-header.unknown-header > div,
|
||||
.detailed-grid-header.unknown-header .filterable-header {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.detailed-grid-header.unknown-header > div:last-child,
|
||||
.detailed-grid-header.unknown-header .filterable-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* UNKNOWN 전용 행 - 5개 컬럼 */
|
||||
.detailed-material-row.unknown-row {
|
||||
grid-template-columns: 40px 100px 1fr 150px 100px;
|
||||
padding: 8px 0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.detailed-material-row.unknown-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detailed-material-row.unknown-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* UNKNOWN 설명 셀 스타일 */
|
||||
@@ -345,14 +906,25 @@
|
||||
|
||||
.detailed-material-row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 80px 250px 80px 80px 450px 120px 150px 100px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
/* 기본 그리드는 사용하지 않음 - 각 카테고리별 전용 클래스 사용 */
|
||||
padding: 12px 0;
|
||||
margin: 0 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
align-items: center;
|
||||
transition: background 0.15s;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detailed-material-row .material-cell {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.detailed-material-row .material-cell:last-child {
|
||||
border-right: none;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.detailed-material-row:hover {
|
||||
background: #fafbfc;
|
||||
}
|
||||
@@ -364,28 +936,62 @@
|
||||
.material-cell {
|
||||
overflow: visible !important;
|
||||
text-overflow: initial !important;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: normal !important;
|
||||
padding-right: 12px;
|
||||
word-break: break-word;
|
||||
min-width: 120px;
|
||||
max-width: none !important;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.material-cell > * {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 사용자 요구사항 입력 필드 */
|
||||
.user-req-input {
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-req-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 1px #3b82f6;
|
||||
}
|
||||
|
||||
.material-cell input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 타입 배지 */
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.type-badge.pipe {
|
||||
@@ -418,6 +1024,14 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.type-badge.special {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
border: 2px solid #b91c1c;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 4px rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
.type-badge.unknown {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
@@ -428,11 +1042,6 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.type-badge.unknown {
|
||||
background: #9ca3af;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 텍스트 스타일 */
|
||||
.subtype-text,
|
||||
.size-text,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -150,7 +150,7 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
// 구매 수량 계산
|
||||
const purchaseInfo = calculatePurchaseQuantity(material);
|
||||
|
||||
// 품목명 생성 (간단하게)
|
||||
// 품목명 생성 (카테고리별 상세 처리)
|
||||
let itemName = '';
|
||||
if (category === 'PIPE') {
|
||||
itemName = material.pipe_details?.manufacturing_method || 'PIPE';
|
||||
@@ -161,34 +161,237 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
} else if (category === 'VALVE') {
|
||||
itemName = 'VALVE';
|
||||
} else if (category === 'GASKET') {
|
||||
itemName = 'GASKET';
|
||||
// 가스켓 상세 타입 추출
|
||||
if (material.gasket_details) {
|
||||
const gasketType = material.gasket_details.gasket_type || '';
|
||||
const gasketSubtype = material.gasket_details.gasket_subtype || '';
|
||||
|
||||
if (gasketSubtype && gasketSubtype !== gasketType) {
|
||||
itemName = gasketSubtype;
|
||||
} else if (gasketType) {
|
||||
itemName = gasketType;
|
||||
} else {
|
||||
itemName = 'GASKET';
|
||||
}
|
||||
} else {
|
||||
// gasket_details가 없으면 description에서 추출
|
||||
if (cleanDescription.includes('SWG') || cleanDescription.includes('SPIRAL')) {
|
||||
itemName = 'SWG';
|
||||
} else if (cleanDescription.includes('RTJ') || cleanDescription.includes('RING')) {
|
||||
itemName = 'RTJ';
|
||||
} else if (cleanDescription.includes('FF') || cleanDescription.includes('FULL FACE')) {
|
||||
itemName = 'FF';
|
||||
} else if (cleanDescription.includes('RF') || cleanDescription.includes('RAISED')) {
|
||||
itemName = 'RF';
|
||||
} else {
|
||||
itemName = 'GASKET';
|
||||
}
|
||||
}
|
||||
} else if (category === 'BOLT') {
|
||||
itemName = 'BOLT';
|
||||
} else {
|
||||
itemName = category || 'UNKNOWN';
|
||||
}
|
||||
|
||||
// 압력 등급 추출
|
||||
// 압력 등급 추출 (카테고리별 처리)
|
||||
let pressure = '-';
|
||||
const pressureMatch = cleanDescription.match(/(\d+)LB/i);
|
||||
if (pressureMatch) {
|
||||
pressure = `${pressureMatch[1]}LB`;
|
||||
|
||||
if (category === 'GASKET') {
|
||||
// 가스켓의 경우 gasket_details에서 압력등급 추출
|
||||
if (material.gasket_details && material.gasket_details.pressure_rating) {
|
||||
pressure = material.gasket_details.pressure_rating;
|
||||
} else {
|
||||
// gasket_details가 없으면 description에서 추출
|
||||
const pressureMatch = cleanDescription.match(/(\d+)LB/i);
|
||||
if (pressureMatch) {
|
||||
pressure = `${pressureMatch[1]}LB`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 다른 카테고리는 기존 방식
|
||||
const pressureMatch = cleanDescription.match(/(\d+)LB/i);
|
||||
if (pressureMatch) {
|
||||
pressure = `${pressureMatch[1]}LB`;
|
||||
}
|
||||
}
|
||||
|
||||
// 스케줄 추출
|
||||
// 스케줄/길이 추출 (카테고리별 처리)
|
||||
let schedule = '-';
|
||||
const scheduleMatch = cleanDescription.match(/SCH\s*(\d+[A-Z]*(?:\s*[xX×]\s*SCH\s*\d+[A-Z]*)?)/i);
|
||||
if (scheduleMatch) {
|
||||
schedule = scheduleMatch[0];
|
||||
|
||||
if (category === 'BOLT') {
|
||||
// 볼트의 경우 길이 정보 추출
|
||||
const lengthPatterns = [
|
||||
/(\d+(?:\.\d+)?)\s*LG/i, // 145.0000 LG, 75 LG
|
||||
/(\d+(?:\.\d+)?)\s*mm/i, // 50mm
|
||||
/(\d+(?:\.\d+)?)\s*MM/i, // 50MM
|
||||
/,\s*(\d+(?:\.\d+)?)\s*LG/i, // ", 145.0000 LG" 형태
|
||||
/LG[,\s]*(\d+(?:\.\d+)?)/i // LG, 75 형태
|
||||
];
|
||||
|
||||
for (const pattern of lengthPatterns) {
|
||||
const match = cleanDescription.match(pattern);
|
||||
if (match) {
|
||||
let lengthValue = match[1];
|
||||
// 소수점 제거 (145.0000 → 145)
|
||||
if (lengthValue.includes('.') && lengthValue.endsWith('.0000')) {
|
||||
lengthValue = lengthValue.split('.')[0];
|
||||
} else if (lengthValue.includes('.') && /\.0+$/.test(lengthValue)) {
|
||||
lengthValue = lengthValue.split('.')[0];
|
||||
}
|
||||
schedule = `${lengthValue}mm`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 다른 카테고리는 스케줄 추출
|
||||
const scheduleMatch = cleanDescription.match(/SCH\s*(\d+[A-Z]*(?:\s*[xX×]\s*SCH\s*\d+[A-Z]*)?)/i);
|
||||
if (scheduleMatch) {
|
||||
schedule = scheduleMatch[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 재질 추출 (ASTM 등)
|
||||
// 재질 추출 (카테고리별 처리)
|
||||
let grade = '-';
|
||||
const gradeMatch = cleanDescription.match(/(ASTM\s+[A-Z0-9\s]+(?:TP\d+|GR\s*[A-Z0-9]+|WP\d+)?)/i);
|
||||
if (gradeMatch) {
|
||||
grade = gradeMatch[1].trim();
|
||||
|
||||
if (category === 'GASKET') {
|
||||
// 가스켓의 경우 재질 필드에는 H/F/I/O 타입 정보 표시
|
||||
const hfioMatch = cleanDescription.match(/H\/F\/I\/O/i);
|
||||
if (hfioMatch) {
|
||||
grade = 'H/F/I/O';
|
||||
} else {
|
||||
// 다른 가스켓 타입들
|
||||
if (cleanDescription.includes('SWG') || cleanDescription.includes('SPIRAL')) {
|
||||
grade = 'SWG';
|
||||
} else if (cleanDescription.includes('RTJ') || cleanDescription.includes('RING')) {
|
||||
grade = 'RTJ';
|
||||
} else if (cleanDescription.includes('FF') || cleanDescription.includes('FULL FACE')) {
|
||||
grade = 'FF';
|
||||
} else if (cleanDescription.includes('RF') || cleanDescription.includes('RAISED')) {
|
||||
grade = 'RF';
|
||||
} else {
|
||||
grade = 'GASKET';
|
||||
}
|
||||
}
|
||||
} else if (category === 'BOLT') {
|
||||
// 볼트 전용 재질 추출 (복합 ASTM 패턴 지원)
|
||||
const boltGradePatterns = [
|
||||
// 복합 ASTM 패턴 (A193/A194 GR B7/2H)
|
||||
/(ASTM\s+A\d+\/A\d+\s+GR\s+[A-Z0-9\/]+)/i,
|
||||
// 단일 ASTM 패턴 (ASTM A193 GR B7)
|
||||
/(ASTM\s+A\d+\s+GR\s+[A-Z0-9]+)/i,
|
||||
// ASTM 번호만 (ASTM A193/A194)
|
||||
/(ASTM\s+A\d+(?:\/A\d+)?)/i,
|
||||
// 일반 ASTM 패턴
|
||||
/(ASTM\s+[A-Z0-9\s\/]+(?:TP\d+|GR\s*[A-Z0-9\/]+|WP\d+)?)/i
|
||||
];
|
||||
|
||||
for (const pattern of boltGradePatterns) {
|
||||
const match = cleanDescription.match(pattern);
|
||||
if (match) {
|
||||
grade = match[1].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ASTM이 없는 경우 기본 재질 패턴 시도
|
||||
if (grade === '-') {
|
||||
const basicGradeMatch = cleanDescription.match(/(A\d+(?:\/A\d+)?\s+(?:GR\s+)?[A-Z0-9\/]+)/i);
|
||||
if (basicGradeMatch) {
|
||||
grade = basicGradeMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 백엔드에서 제공하는 재질 정보 우선 사용
|
||||
if (material.full_material_grade && material.full_material_grade !== '-') {
|
||||
grade = material.full_material_grade;
|
||||
} else if (material.material_grade && material.material_grade !== '-') {
|
||||
grade = material.material_grade;
|
||||
}
|
||||
} else {
|
||||
// 기존 ASTM 패턴 (다른 카테고리용)
|
||||
const gradeMatch = cleanDescription.match(/(ASTM\s+[A-Z0-9\s]+(?:TP\d+|GR\s*[A-Z0-9]+|WP\d+)?)/i);
|
||||
if (gradeMatch) {
|
||||
grade = gradeMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 카테고리별 상세 정보 추출
|
||||
let detailInfo = '';
|
||||
let gasketMaterial = '';
|
||||
let gasketThickness = '';
|
||||
|
||||
if (category === 'BOLT') {
|
||||
// 볼트의 경우 표면처리 정보 추출
|
||||
const surfaceTreatments = [];
|
||||
|
||||
// 원본 설명에서 표면처리 패턴 확인
|
||||
const surfacePatterns = {
|
||||
'ELEC.GALV': 'ELEC.GALV',
|
||||
'ELEC GALV': 'ELEC.GALV',
|
||||
'GALVANIZED': 'GALVANIZED',
|
||||
'GALV': 'GALV',
|
||||
'HOT DIP GALV': 'HDG',
|
||||
'HDG': 'HDG',
|
||||
'ZINC PLATED': 'ZINC PLATED',
|
||||
'ZINC': 'ZINC',
|
||||
'PLAIN': 'PLAIN'
|
||||
};
|
||||
|
||||
for (const [pattern, treatment] of Object.entries(surfacePatterns)) {
|
||||
if (cleanDescription.includes(pattern)) {
|
||||
surfaceTreatments.push(treatment);
|
||||
break; // 첫 번째 매치만 사용
|
||||
}
|
||||
}
|
||||
|
||||
detailInfo = surfaceTreatments.join(', ') || '-';
|
||||
} else if (category === 'GASKET') {
|
||||
// 실제 재질 구성 정보 (SS304/GRAPHITE/SS304/SS304)
|
||||
if (material.gasket_details) {
|
||||
const materialType = material.gasket_details.material_type || '';
|
||||
const fillerMaterial = material.gasket_details.filler_material || '';
|
||||
|
||||
if (materialType && fillerMaterial) {
|
||||
// DB에서 가져온 정보로 구성
|
||||
gasketMaterial = `${materialType}/${fillerMaterial}`;
|
||||
}
|
||||
}
|
||||
|
||||
// gasket_details가 없거나 불완전하면 description에서 추출
|
||||
if (!gasketMaterial) {
|
||||
// SS304/GRAPHITE/SS304/SS304 패턴 추출
|
||||
const fullMaterialMatch = cleanDescription.match(/SS304\/GRAPHITE\/SS304\/SS304/i);
|
||||
if (fullMaterialMatch) {
|
||||
gasketMaterial = 'SS304/GRAPHITE/SS304/SS304';
|
||||
} else {
|
||||
// 간단한 패턴 (SS304/GRAPHITE)
|
||||
const simpleMaterialMatch = cleanDescription.match(/(SS\d+|304|316)\s*[\/+]\s*(GRAPHITE|PTFE|VITON|EPDM)/i);
|
||||
if (simpleMaterialMatch) {
|
||||
gasketMaterial = `${simpleMaterialMatch[1]}/${simpleMaterialMatch[2]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 두께 정보 별도 추출
|
||||
if (material.gasket_details && material.gasket_details.thickness) {
|
||||
gasketThickness = `${material.gasket_details.thickness}mm`;
|
||||
} else {
|
||||
// description에서 두께 추출 (4.5mm 패턴)
|
||||
const thicknessMatch = cleanDescription.match(/(\d+\.?\d*)\s*mm/i);
|
||||
if (thicknessMatch) {
|
||||
gasketThickness = `${thicknessMatch[1]}mm`;
|
||||
}
|
||||
}
|
||||
|
||||
// 기타 상세 정보 (Fire Safe 등)
|
||||
const otherDetails = [];
|
||||
if (material.gasket_details && material.gasket_details.fire_safe) {
|
||||
otherDetails.push('Fire Safe');
|
||||
}
|
||||
|
||||
detailInfo = otherDetails.join(', ');
|
||||
}
|
||||
|
||||
// 새로운 엑셀 양식에 맞춘 데이터 구조
|
||||
const base = {
|
||||
'TAGNO': '', // 비워둠
|
||||
@@ -197,18 +400,46 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
'통화구분': 'KRW', // 기본값
|
||||
'단가': 1, // 일괄 1로 설정
|
||||
'크기': material.size_spec || '-',
|
||||
'압력등급': pressure,
|
||||
'스케줄': schedule,
|
||||
'재질': grade,
|
||||
'사용자요구': '',
|
||||
'관리항목1': '', // 빈칸
|
||||
'관리항목7': '', // 빈칸
|
||||
'관리항목8': '', // 빈칸
|
||||
'관리항목9': '', // 빈칸
|
||||
'관리항목10': '', // 빈칸
|
||||
'납기일(YYYY-MM-DD)': new Date().toISOString().split('T')[0] // 오늘 날짜
|
||||
'압력등급': pressure
|
||||
};
|
||||
|
||||
// 카테고리별 전용 컬럼 구성
|
||||
if (category === 'GASKET') {
|
||||
// 가스켓 전용 컬럼 순서
|
||||
base['타입/구조'] = grade; // H/F/I/O, SWG 등 (스케줄 대신)
|
||||
base['재질'] = gasketMaterial || '-'; // SS304/GRAPHITE/SS304/SS304
|
||||
base['두께'] = gasketThickness || '-'; // 4.5mm
|
||||
base['사용자요구'] = material.user_requirement || '';
|
||||
base['관리항목8'] = ''; // 빈칸
|
||||
base['관리항목9'] = ''; // 빈칸
|
||||
base['관리항목10'] = ''; // 빈칸
|
||||
base['납기일(YYYY-MM-DD)'] = new Date().toISOString().split('T')[0]; // 가장 마지막
|
||||
} else if (category === 'BOLT') {
|
||||
// 볼트 전용 컬럼 순서 (스케줄 → 길이)
|
||||
base['길이'] = schedule; // 볼트는 길이 정보
|
||||
base['재질'] = grade;
|
||||
base['추가요구'] = detailInfo || '-'; // 상세내역 → 추가요구로 변경
|
||||
base['사용자요구'] = material.user_requirement || '';
|
||||
base['관리항목1'] = ''; // 빈칸
|
||||
base['관리항목7'] = ''; // 빈칸
|
||||
base['관리항목8'] = ''; // 빈칸
|
||||
base['관리항목9'] = ''; // 빈칸
|
||||
base['관리항목10'] = ''; // 빈칸
|
||||
base['납기일(YYYY-MM-DD)'] = new Date().toISOString().split('T')[0]; // 가장 마지막
|
||||
} else {
|
||||
// 다른 카테고리는 기존 방식
|
||||
base['스케줄'] = schedule;
|
||||
base['재질'] = grade;
|
||||
base['상세내역'] = detailInfo || '-';
|
||||
base['사용자요구'] = material.user_requirement || '';
|
||||
base['관리항목1'] = ''; // 빈칸
|
||||
base['관리항목7'] = ''; // 빈칸
|
||||
base['관리항목8'] = ''; // 빈칸
|
||||
base['관리항목9'] = ''; // 빈칸
|
||||
base['관리항목10'] = ''; // 빈칸
|
||||
base['납기일(YYYY-MM-DD)'] = new Date().toISOString().split('T')[0]; // 가장 마지막
|
||||
}
|
||||
|
||||
// 비교 모드인 경우 추가 정보
|
||||
if (includeComparison) {
|
||||
if (material.previous_quantity !== undefined) {
|
||||
@@ -245,46 +476,96 @@ export const exportMaterialsToExcel = (materials, filename, additionalInfo = {})
|
||||
// 새 워크북 생성
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
// 요약 시트 생성
|
||||
const summaryData = [
|
||||
['파일 정보', ''],
|
||||
['파일명', additionalInfo.filename || ''],
|
||||
['Job No', additionalInfo.jobNo || ''],
|
||||
['리비전', additionalInfo.revision || ''],
|
||||
['업로드일', additionalInfo.uploadDate || new Date().toLocaleDateString()],
|
||||
['총 자재 수', consolidatedMaterials.length],
|
||||
['', ''],
|
||||
['카테고리별 요약', ''],
|
||||
['카테고리', '수량']
|
||||
];
|
||||
|
||||
// 카테고리별 요약 추가 (합쳐진 자재 기준)
|
||||
Object.entries(categoryGroups).forEach(([category, items]) => {
|
||||
const consolidatedItems = consolidateMaterials(items);
|
||||
summaryData.push([category, consolidatedItems.length]);
|
||||
});
|
||||
|
||||
const summarySheet = XLSX.utils.aoa_to_sheet(summaryData);
|
||||
XLSX.utils.book_append_sheet(workbook, summarySheet, '요약');
|
||||
|
||||
// 전체 자재 시트 (합쳐진 자재)
|
||||
const allMaterialsFormatted = consolidatedMaterials.map(material => formatMaterialForExcel(material));
|
||||
const allSheet = XLSX.utils.json_to_sheet(allMaterialsFormatted);
|
||||
XLSX.utils.book_append_sheet(workbook, allSheet, '전체 자재');
|
||||
|
||||
// 카테고리별 시트 생성 (합쳐진 자재)
|
||||
Object.entries(categoryGroups).forEach(([category, items]) => {
|
||||
const consolidatedItems = consolidateMaterials(items);
|
||||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material));
|
||||
const categorySheet = XLSX.utils.json_to_sheet(formattedItems);
|
||||
|
||||
// 시트명에서 특수문자 제거 (엑셀 시트명 규칙)
|
||||
const sheetName = category.replace(/[\\\/\*\?\[\]]/g, '_').substring(0, 31);
|
||||
XLSX.utils.book_append_sheet(workbook, categorySheet, sheetName);
|
||||
if (formattedItems.length > 0) {
|
||||
const categorySheet = XLSX.utils.json_to_sheet(formattedItems);
|
||||
|
||||
// 헤더 스타일링 (연하늘색 배경)
|
||||
const range = XLSX.utils.decode_range(categorySheet['!ref']);
|
||||
|
||||
// 헤더 행에 스타일 적용 (첫 번째 행)
|
||||
for (let col = range.s.c; col <= range.e.c; col++) {
|
||||
const cellRef = XLSX.utils.encode_cell({ r: 0, c: col });
|
||||
|
||||
if (categorySheet[cellRef]) {
|
||||
// 기존 셀 값 유지하면서 스타일만 추가
|
||||
const cellValue = categorySheet[cellRef].v;
|
||||
const cellType = categorySheet[cellRef].t;
|
||||
|
||||
categorySheet[cellRef] = {
|
||||
v: cellValue,
|
||||
t: cellType || 's',
|
||||
s: {
|
||||
fill: {
|
||||
patternType: "solid",
|
||||
fgColor: { rgb: "B3D9FF" }
|
||||
},
|
||||
font: {
|
||||
bold: true,
|
||||
color: { rgb: "000000" },
|
||||
sz: 12,
|
||||
name: "맑은 고딕"
|
||||
},
|
||||
alignment: {
|
||||
horizontal: "center",
|
||||
vertical: "center"
|
||||
},
|
||||
border: {
|
||||
top: { style: "thin", color: { rgb: "666666" } },
|
||||
bottom: { style: "thin", color: { rgb: "666666" } },
|
||||
left: { style: "thin", color: { rgb: "666666" } },
|
||||
right: { style: "thin", color: { rgb: "666666" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 컬럼 너비 자동 조정
|
||||
const colWidths = [];
|
||||
if (formattedItems.length > 0) {
|
||||
const headers = Object.keys(formattedItems[0]);
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
let maxWidth = header.length; // 헤더 길이
|
||||
|
||||
// 각 행의 데이터 길이 확인
|
||||
formattedItems.forEach(item => {
|
||||
const cellValue = String(item[header] || '');
|
||||
maxWidth = Math.max(maxWidth, cellValue.length);
|
||||
});
|
||||
|
||||
// 최소 10, 최대 50으로 제한
|
||||
colWidths[index] = { wch: Math.min(Math.max(maxWidth + 2, 10), 50) };
|
||||
});
|
||||
}
|
||||
|
||||
categorySheet['!cols'] = colWidths;
|
||||
|
||||
// 시트명에서 특수문자 제거 (엑셀 시트명 규칙)
|
||||
const sheetName = category.replace(/[\\\/\*\?\[\]]/g, '_').substring(0, 31);
|
||||
XLSX.utils.book_append_sheet(workbook, categorySheet, sheetName);
|
||||
}
|
||||
});
|
||||
|
||||
// 파일 저장
|
||||
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||
// 워크북 속성 설정 (스타일 지원)
|
||||
workbook.Props = {
|
||||
Title: "자재 목록",
|
||||
Subject: "TK-MP-Project 자재 관리",
|
||||
Author: "TK-MP System",
|
||||
CreatedDate: new Date()
|
||||
};
|
||||
|
||||
// 파일 저장 (스타일 포함)
|
||||
const excelBuffer = XLSX.write(workbook, {
|
||||
bookType: 'xlsx',
|
||||
type: 'array',
|
||||
cellStyles: true // 스타일 활성화
|
||||
});
|
||||
const data = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
|
||||
const finalFilename = `${filename}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
|
||||
Reference in New Issue
Block a user