ERP-node/docs/품목정보.html

3916 lines
167 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>품목 기본정보</title>
<!-- CSS 파일 연결 -->
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/userOptions.css">
<link rel="stylesheet" href="css/excelUpload.css">
<!-- SheetJS 라이브러리 (엑셀 파일 처리) -->
<script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f8f9fa;
padding: 0;
margin: 0;
height: 100vh;
overflow: hidden;
}
.page-container {
padding: 10px;
height: 100vh;
display: flex;
flex-direction: column;
gap: 10px;
}
/* 검색 섹션 스타일 */
.search-section {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-start;
}
.search-fields-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-end;
flex: 1;
min-width: 0;
}
.search-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.search-field label {
font-size: 13px;
font-weight: 600;
color: #374151;
}
.search-field input,
.search-field select {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
min-width: 180px;
}
.search-field input:focus,
.search-field select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-buttons {
display: flex;
gap: 8px;
}
.search-right-buttons {
display: flex;
gap: 8px;
margin-left: auto;
flex-shrink: 0;
align-self: flex-start;
}
/* 데이터 테이블 섹션 */
.table-section {
flex: 1;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
#dataTableContainer {
overflow: auto;
position: relative;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 2px solid #e5e7eb;
}
.table-title {
font-size: 16px;
font-weight: 700;
color: #1f2937;
display: flex;
align-items: center;
gap: 15px;
}
/* 그룹바이 컨트롤 */
.groupby-container {
display: flex;
align-items: center;
gap: 8px;
}
.groupby-select {
padding: 6px 12px;
border: 2px solid #3b82f6;
background: #eff6ff;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
color: #3b82f6;
outline: none;
transition: all 0.2s;
min-width: 140px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.groupby-select:hover {
background: #dbeafe;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
}
.groupby-select:focus {
background: #f3f4f6;
color: #3b82f6;
}
.groupby-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: #1e40af;
}
.groupby-tag-remove {
cursor: pointer;
font-size: 14px;
color: #6b7280;
transition: color 0.2s;
}
.groupby-tag-remove:hover {
color: #ef4444;
}
.groupby-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.table-actions {
display: flex;
gap: 8px;
}
.table-container {
flex: 1;
overflow: auto;
position: relative;
}
.data-table {
width: max-content;
min-width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.data-table thead {
position: sticky;
top: 0;
background: #f9fafb;
z-index: 10;
}
.data-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #374151;
font-size: 13px;
border-bottom: 1px solid #f3f4f6;
white-space: nowrap;
position: relative;
overflow: visible;
min-width: 60px;
}
.data-table th .resize-handle {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
user-select: none;
background: transparent;
z-index: 10;
}
.data-table th .resize-handle:hover {
background: #3b82f6;
}
.data-table td {
padding: 12px;
border-bottom: 1px solid #f3f4f6;
font-size: 13px;
color: #1f2937;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 그리드선 숨김 모드 */
.data-table.hide-grid td,
.data-table.hide-grid th {
border-bottom: none;
border-right: none;
}
.data-table.hide-grid tbody tr {
border-bottom: 1px solid #f9fafb;
}
.data-table tbody tr:hover {
background: #f9fafb;
}
.data-table tbody tr.selected {
background: #eff6ff;
}
.data-table .empty-state {
text-align: center;
padding: 60px 20px;
color: #9ca3af;
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 12px;
}
.empty-state-text {
font-size: 14px;
font-weight: 500;
}
/* 버튼 스타일 */
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.btn-secondary {
background: #f3f4f6;
color: #6b7280;
}
.btn-secondary:hover {
background: #e5e7eb;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-small {
padding: 5px 10px;
font-size: 12px;
}
/* 체크박스 스타일 - 기본적으로 숨김 */
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}
input[type="checkbox"]:checked {
opacity: 1;
}
/* 테이블 행에 마우스 오버 시 체크박스 보이기 */
.data-table tbody tr:hover input[type="checkbox"] {
opacity: 0.3;
}
.data-table tbody tr:hover input[type="checkbox"]:checked {
opacity: 1;
}
/* 헤더 체크박스는 항상 보이기 */
.data-table thead input[type="checkbox"] {
opacity: 1;
}
/* 그룹화된 테이블 스타일 */
.group-header {
background: #f3f4f6;
font-weight: 700;
color: #374151;
padding: 12px;
cursor: pointer;
user-select: none;
position: sticky;
top: 0;
z-index: 5;
border-bottom: 2px solid #e5e7eb;
}
.group-header:hover {
background: #e5e7eb;
}
.group-header-content {
display: flex;
align-items: center;
gap: 8px;
}
.group-header input[type="checkbox"] {
opacity: 1;
}
.group-toggle {
font-size: 12px;
transition: transform 0.2s;
}
.group-toggle.collapsed {
transform: rotate(-90deg);
}
.group-count {
color: #3b82f6;
font-weight: 700;
}
.group-rows {
transition: all 0.2s;
}
.group-rows.collapsed {
display: none;
}
/* 카드형 체크박스 숨김 처리 */
.card-item {
position: relative;
}
.card-item input[type="checkbox"] {
opacity: 0;
transition: opacity 0.2s;
}
.card-item input[type="checkbox"]:checked {
opacity: 1;
}
.card-item:hover input[type="checkbox"] {
opacity: 0.3;
}
.card-item:hover input[type="checkbox"]:checked {
opacity: 1;
}
/* 테이블 내 체크박스 */
.data-table input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.data-table tbody tr {
cursor: pointer;
}
/* 뱃지 스타일 */
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.badge-primary {
background: #dbeafe;
color: #1e40af;
}
.badge-success {
background: #d1fae5;
color: #065f46;
}
.badge-warning {
background: #fef3c7;
color: #92400e;
}
.badge-danger {
background: #fee2e2;
color: #991b1b;
}
/* 모달 공통 스타일 */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
/* 사용자 옵션 모달 */
.user-options-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
align-items: center;
justify-content: center;
}
.user-options-modal.active {
display: flex;
}
.user-options-content {
background: white;
border-radius: 12px;
width: 700px;
max-width: 95%;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.user-options-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 2px solid #e5e7eb;
}
.user-options-header h2 {
font-size: 18px;
font-weight: 700;
color: #1f2937;
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
.user-options-close {
background: none;
border: none;
font-size: 24px;
color: #6b7280;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s;
}
.user-options-close:hover {
background: #f3f4f6;
color: #1f2937;
}
.user-options-tabs {
display: flex;
gap: 4px;
padding: 0 24px;
border-bottom: 1px solid #e5e7eb;
}
.user-options-tab {
padding: 12px 20px;
background: none;
border: none;
border-bottom: 3px solid transparent;
font-size: 14px;
font-weight: 600;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
}
.user-options-tab:hover {
background: #f9fafb;
color: #3b82f6;
}
.user-options-tab.active {
color: #3b82f6;
border-bottom-color: #3b82f6;
}
.user-options-body {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
}
.user-options-footer {
padding: 16px 24px;
border-top: 2px solid #e5e7eb;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.option-field-item {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 12px;
}
.option-field-item.dragging {
opacity: 0.5;
}
.drag-handle {
cursor: move;
color: #9ca3af;
font-size: 18px;
}
.option-field-checkbox {
width: 20px;
height: 20px;
}
.option-field-name {
flex: 1;
font-weight: 600;
color: #1f2937;
}
.option-field-width {
width: 80px;
}
.freeze-option {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #f9fafb;
border-radius: 8px;
margin-bottom: 12px;
}
.freeze-option label {
flex: 1;
font-weight: 600;
color: #1f2937;
}
.freeze-option input[type="number"] {
width: 100px;
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 6px;
}
/* 토스트 메시지 */
.toast-message {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%) translateY(-100px);
background: white;
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
gap: 12px;
z-index: 10000;
opacity: 0;
transition: all 0.3s ease;
}
.toast-message.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.toast-icon {
font-size: 24px;
}
.toast-text {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
/* 품목 추가/수정 모달 */
.item-modal-content {
background: white;
border-radius: 12px;
width: 600px;
max-width: 95%;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.item-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 2px solid #e5e7eb;
}
.item-modal-header h2 {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.item-modal-close {
background: none;
border: none;
font-size: 24px;
color: #6b7280;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s;
}
.item-modal-close:hover {
background: #f3f4f6;
color: #1f2937;
}
.item-modal-body {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.item-form-field {
margin-bottom: 16px;
}
.item-form-field input,
.item-form-field select {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
transition: all 0.2s;
}
.item-form-field input:focus,
.item-form-field select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.item-form-field input::placeholder {
color: #9ca3af;
}
.item-modal-footer {
padding: 16px 24px;
border-top: 2px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.continuous-input-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #6b7280;
cursor: pointer;
}
.continuous-input-label input[type="checkbox"] {
opacity: 1;
}
.modal-buttons {
display: flex;
gap: 8px;
}
/* 코드변경 모달 */
.code-change-modal-content {
background: white;
border-radius: 12px;
width: 700px;
max-width: 95%;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.code-change-options {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 24px;
}
.code-change-option {
padding: 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.code-change-option:hover {
border-color: #3b82f6;
background: #f0f9ff;
}
.code-change-option.selected {
border-color: #3b82f6;
background: #eff6ff;
}
.code-change-option-title {
font-weight: 600;
color: #1f2937;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.code-change-option-desc {
font-size: 13px;
color: #6b7280;
}
.code-change-form {
display: none;
margin-top: 16px;
padding-top: 16px;
border-top: 2px solid #e5e7eb;
}
.code-change-form.active {
display: block;
}
.code-change-field {
margin-bottom: 16px;
}
.code-change-field label {
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
.code-change-field input,
.code-change-field select {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
}
.warning-box {
background: #fef3c7;
border: 2px solid #fbbf24;
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
.warning-box-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #92400e;
margin-bottom: 8px;
}
.warning-box-content {
font-size: 13px;
color: #78350f;
line-height: 1.6;
}
.preview-box {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
.preview-box-title {
font-weight: 600;
color: #374151;
margin-bottom: 12px;
}
.preview-table {
width: 100%;
font-size: 13px;
}
.preview-table td {
padding: 6px 8px;
border-bottom: 1px solid #e5e7eb;
}
.preview-table td:first-child {
color: #6b7280;
width: 120px;
}
.preview-table td:last-child {
font-weight: 600;
color: #1f2937;
}
.arrow-icon {
color: #3b82f6;
font-weight: bold;
}
</style>
</head>
<body>
<div class="page-container">
<!-- 검색 섹션 -->
<div id="searchSection"></div>
<!-- 데이터 테이블 섹션 -->
<div class="table-section">
<div class="table-header">
<div class="table-title" style="display: flex; align-items: center; gap: 15px; flex: 1;">
<div style="font-size: 14px; color: #6b7280;">
<span id="totalCount" style="color: #3b82f6; font-weight: 700;">15</span>
</div>
<select class="groupby-select" id="groupByField" onchange="addGroupBy()">
<option value="">⚙️ Group by</option>
<option value="status">상태</option>
<option value="category">구분</option>
<option value="type">유형</option>
<option value="stockUnit">재고단위</option>
<option value="createdBy">등록자</option>
</select>
<div class="groupby-tags" id="groupByTags"></div>
</div>
<div class="table-actions">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" id="includeInactive" style="opacity: 1;">
<span style="font-size: 13px; color: #6b7280;">미사용 포함</span>
</label>
<button class="btn btn-primary" onclick="openEditModal()">✏️ 수정</button>
<button class="btn btn-success" onclick="openAddModal()"> 추가</button>
<button class="btn btn-secondary" onclick="openCodeChangeModal()">🔄 코드변경</button>
<button class="btn btn-danger" onclick="deleteSelected()">🗑️ 삭제</button>
</div>
</div>
<div id="dataTableContainer"></div>
</div>
</div>
<!-- 사용자 옵션 모달 -->
<div class="user-options-modal" id="userOptionsModal">
<div class="user-options-content">
<div class="user-options-header">
<h2>⚙️ 옵션 설정</h2>
<button class="user-options-close" onclick="closeUserOptionsModal(true)"></button>
</div>
<div class="user-options-tabs">
<button class="user-options-tab active" onclick="switchOptionsTab('searchFields')">검색필드 설정</button>
<button class="user-options-tab" onclick="switchOptionsTab('columnDisplay')">컬럼 표시/숨기기</button>
<button class="user-options-tab" onclick="switchOptionsTab('otherOptions')">기타옵션</button>
</div>
<div class="user-options-body" id="userOptionsBody">
<!-- 탭 내용이 여기에 표시됨 -->
</div>
<div class="user-options-footer">
<button class="btn btn-secondary" onclick="closeUserOptionsModal(true)">취소</button>
<button class="btn btn-primary" id="saveOptionsBtn" onclick="saveUserOptions()">💾 저장</button>
</div>
</div>
</div>
<!-- 품목 추가/수정 모달 -->
<div class="modal-overlay" id="itemModal">
<div class="item-modal-content">
<div class="item-modal-header">
<h2 id="itemModalTitle">품목 추가</h2>
<button class="item-modal-close" onclick="closeItemModal()"></button>
</div>
<div class="item-modal-body" id="itemModalBody">
<!-- 폼 필드가 동적으로 생성됨 -->
</div>
<div class="item-modal-footer">
<label class="continuous-input-label">
<input type="checkbox" id="continuousInput">
<span>연속입력</span>
</label>
<div class="modal-buttons">
<button class="btn btn-secondary" onclick="closeItemModal()">취소</button>
<button class="btn btn-primary" onclick="saveItem()">💾 저장</button>
</div>
</div>
</div>
</div>
<!-- 코드변경 모달 -->
<div class="modal-overlay" id="codeChangeModal">
<div class="code-change-modal-content">
<div class="item-modal-header">
<h2>🔄 코드변경 관리</h2>
<button class="item-modal-close" onclick="closeCodeChangeModal()"></button>
</div>
<div class="item-modal-body">
<div class="code-change-options">
<div class="code-change-option" id="renameOption" onclick="selectCodeChangeOption('rename')">
<div class="code-change-option-title">
<span>✏️</span>
<span>품번 변경</span>
</div>
<div class="code-change-option-desc">
선택한 품목의 품번코드를 새로운 코드로 변경합니다.
</div>
</div>
<div class="code-change-option" id="mergeOption" onclick="selectCodeChangeOption('merge')">
<div class="code-change-option-title">
<span>🔗</span>
<span>다른 품번에 합병</span>
</div>
<div class="code-change-option-desc">
선택한 품목을 다른 품목에 합병합니다. 기존 데이터는 통합됩니다.
</div>
</div>
</div>
<!-- 품번 변경 폼 -->
<div class="code-change-form" id="renameForm">
<div class="code-change-field">
<label>현재 품번</label>
<input type="text" id="currentCode" readonly style="background: #f3f4f6;">
</div>
<div class="code-change-field">
<label>새 품번 *</label>
<input type="text" id="newCode" placeholder="새로운 품번코드를 입력하세요">
</div>
<div class="preview-box" id="renamePreview" style="display: none;">
<div class="preview-box-title">📋 변경 미리보기</div>
<table class="preview-table">
<tr>
<td>품번코드</td>
<td><span id="previewOldCode"></span> <span class="arrow-icon"></span> <span id="previewNewCode"></span></td>
</tr>
<tr>
<td>품명</td>
<td id="previewItemName"></td>
</tr>
</table>
</div>
</div>
<!-- 합병 폼 -->
<div class="code-change-form" id="mergeForm">
<div class="code-change-field">
<label>삭제될 품번 (현재 선택)</label>
<input type="text" id="sourceCode" readonly style="background: #f3f4f6;">
</div>
<div class="code-change-field">
<label>통합될 품번 *</label>
<select id="targetCode" onchange="updateMergePreview()">
<option value="">품번을 선택하세요</option>
</select>
</div>
<div class="warning-box">
<div class="warning-box-title">
<span>⚠️</span>
<span>주의사항</span>
</div>
<div class="warning-box-content">
• 합병 작업은 되돌릴 수 없습니다.<br>
• 삭제될 품번의 <strong>모든 데이터</strong>(재고, 거래내역, 생산이력 등)가 통합될 품번으로 이관됩니다.<br>
• 삭제될 품번은 영구적으로 삭제되며 복구할 수 없습니다.<br>
• 관련 문서 및 보고서의 품번도 함께 변경됩니다.
</div>
</div>
<div class="preview-box" id="mergePreview" style="display: none;">
<div class="preview-box-title">📋 합병 미리보기</div>
<table class="preview-table">
<tr>
<td>삭제될 품번</td>
<td id="previewSourceCode"></td>
</tr>
<tr>
<td></td>
<td style="text-align: center; color: #ef4444; font-size: 20px;"></td>
</tr>
<tr>
<td>통합될 품번</td>
<td id="previewTargetCode"></td>
</tr>
<tr>
<td colspan="2" style="padding-top: 12px; color: #6b7280; font-size: 12px;">
* 모든 관련 데이터가 통합될 품번으로 이관됩니다.
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="item-modal-footer">
<div></div>
<div class="modal-buttons">
<button class="btn btn-secondary" onclick="closeCodeChangeModal()">취소</button>
<button class="btn btn-primary" onclick="executeCodeChange()">✓ 실행</button>
</div>
</div>
</div>
</div>
<!-- 토스트 메시지 -->
<div id="toastMessage" class="toast-message">
<span class="toast-icon"></span>
<span class="toast-text"></span>
</div>
<!-- JavaScript 파일 연결 -->
<!-- SheetJS 라이브러리 (엑셀 처리용) -->
<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>
<script src="js/common.js"></script>
<script src="js/components/searchSection.js"></script>
<script src="js/components/dataTable.js"></script>
<script src="js/components/modal.js"></script>
<script src="js/components/userOptions.js"></script>
<script src="js/components/excelUpload.js"></script>
<script src="js/components/groupBy.js"></script>
<script src="js/components/webcamCapture.js"></script>
<script>
// Group By 컴포넌트 인스턴스
let groupByComponent;
window.groupByFields = []; // GroupBy 필드 배열
// 품목 모달 관련 변수
let itemModalMode = 'add'; // 'add' or 'edit'
let editingItemId = null;
// 품목 입력 필드 정의
const itemFormFields = [
{ id: 'itemCode', label: '품번코드', type: 'text', required: true },
{ id: 'itemName', label: '품명', type: 'text', required: true },
{ id: 'spec', label: '규격', type: 'text', required: false },
{ id: 'material', label: '재질', type: 'text', required: false },
{ id: 'stockUnit', label: '재고단위', type: 'select', required: true,
options: ['EA', 'kg', 'L', 'Sheet', 'Box'] },
{ id: 'weight', label: '중량', type: 'number', required: false },
{ id: 'weightUnit', label: '중량단위', type: 'select', required: false,
options: ['g', 'kg', 'kg/L', 't'] },
{ id: 'category', label: '구분', type: 'select', required: true,
options: ['원자재', '중간재', '완제품'] },
{ id: 'type', label: '유형', type: 'select', required: false,
options: ['반도체용', '태양광용', '산업용', '의료용', '건축용', '사출용', '화장품용'] },
{ id: 'memo', label: '메모', type: 'text', required: false },
{ id: 'status', label: '사용여부', type: 'select', required: true,
options: ['정상', '품절', '대기', '단종'] }
];
// 샘플 데이터 (실리콘 회사)
let itemsData = [
{
id: 1,
selected: false,
status: '정상',
itemCode: 'SIL-2025-001',
itemName: '고순도 실리콘 웨이퍼',
spec: '8인치',
material: 'Si(99.999%)',
stockUnit: 'EA',
weight: 125,
weightUnit: 'g',
image: '📦',
category: '원자재',
type: '반도체용',
memo: '클린룸 보관 필수',
createdBy: '김반도',
createdDate: '2025-01-20',
modifiedDate: '2025-01-20'
},
{
id: 2,
selected: false,
status: '정상',
itemCode: 'SIL-2025-002',
itemName: '실리콘 잉곳',
spec: 'Φ200mm x 1000mm',
material: 'Poly-Si',
stockUnit: 'EA',
weight: 15.5,
weightUnit: 'kg',
image: '📦',
category: '원자재',
type: '태양광용',
memo: '다결정 실리콘',
createdBy: '박태양',
createdDate: '2025-01-19',
modifiedDate: '2025-01-21'
},
{
id: 3,
selected: false,
status: '품절',
itemCode: 'SIL-2025-003',
itemName: '실리콘 분말',
spec: '325 mesh',
material: 'Si powder',
stockUnit: 'kg',
weight: 25,
weightUnit: 'kg',
image: '📦',
category: '원자재',
type: '산업용',
memo: '내화학성 코팅용',
createdBy: '이소재',
createdDate: '2025-01-18',
modifiedDate: '2025-01-18'
},
{
id: 4,
selected: false,
status: '정상',
itemCode: 'SIL-2025-004',
itemName: 'PDMS 실리콘',
spec: '점도 1000 cSt',
material: 'Polydimethylsiloxane',
stockUnit: 'L',
weight: 0.97,
weightUnit: 'kg/L',
image: '📦',
category: '중간재',
type: '의료용',
memo: '생체적합성 인증',
createdBy: '정화학',
createdDate: '2025-01-17',
modifiedDate: '2025-01-20'
},
{
id: 5,
selected: false,
status: '정상',
itemCode: 'SIL-2025-005',
itemName: '실리콘 고무 시트',
spec: '1000x1000x2mm',
material: 'VMQ',
stockUnit: 'Sheet',
weight: 2.1,
weightUnit: 'kg',
image: '📦',
category: '완제품',
type: '산업용',
memo: '내열 200℃',
createdBy: '최고무',
createdDate: '2025-01-16',
modifiedDate: '2025-01-19'
},
{
id: 6,
selected: false,
status: '정상',
itemCode: 'SIL-2025-006',
itemName: '실리콘 실란트',
spec: '310ml 카트리지',
material: 'RTV-1',
stockUnit: 'EA',
weight: 0.35,
weightUnit: 'kg',
image: '📦',
category: '완제품',
type: '건축용',
memo: '중성 경화형',
createdBy: '강건축',
createdDate: '2025-01-15',
modifiedDate: '2025-01-15'
},
{
id: 7,
selected: false,
status: '대기',
itemCode: 'SIL-2025-007',
itemName: 'LSR 실리콘',
spec: 'Shore A 40',
material: 'Liquid Silicone Rubber',
stockUnit: 'kg',
weight: 20,
weightUnit: 'kg',
image: '📦',
category: '중간재',
type: '사출용',
memo: '2액형 혼합',
createdBy: '윤사출',
createdDate: '2025-01-14',
modifiedDate: '2025-01-21'
},
{
id: 8,
selected: false,
status: '정상',
itemCode: 'SIL-2025-008',
itemName: '실리콘 오일',
spec: '점도 100 cSt',
material: 'Silicone Oil',
stockUnit: 'L',
weight: 0.95,
weightUnit: 'kg/L',
image: '📦',
category: '원자재',
type: '화장품용',
memo: 'FDA 승인',
createdBy: '서화장',
createdDate: '2025-01-13',
modifiedDate: '2025-01-13'
}
];
// 검색 섹션 초기화
function initSearchSection() {
// 저장된 설정 가져오기 (컴포넌트 방식)
const savedFields = getSearchFieldsConfig('itemInfo');
// 저장된 설정이 있으면 applySearchFieldsConfig 사용
if (savedFields) {
applySearchFieldsConfig();
return;
}
// 기본 검색 섹션
const searchHtml = `
<div class="search-section">
<div class="search-row">
<div class="search-fields-container">
<div class="search-field">
<select id="status" style="min-width: 120px;">
<option value="">상태</option>
<option value="정상">정상</option>
<option value="품절">품절</option>
<option value="단종">단종</option>
</select>
</div>
<div class="search-field">
<input type="text" id="itemCode" placeholder="품번코드" style="min-width: 150px;">
</div>
<div class="search-field">
<input type="text" id="itemName" placeholder="품명" style="min-width: 200px;">
</div>
<div class="search-buttons">
<button class="btn btn-primary" onclick="performSearch()">
🔍 검색
</button>
<button class="btn btn-secondary" onclick="resetSearch()">
초기화
</button>
</div>
</div>
<div class="search-right-buttons">
<button class="btn btn-secondary" onclick="openUserOptions()">
⚙️ 사용자옵션
</button>
<button class="btn btn-primary" onclick="openWebcamModal()">
📷 사진촬영
</button>
<button class="btn btn-success" onclick="downloadExcel()">
📥 다운로드
</button>
<button class="btn btn-success" onclick="uploadExcel()">
📤 업로드
</button>
</div>
</div>
</div>
`;
document.getElementById('searchSection').innerHTML = searchHtml;
}
// 그룹바이 함수들은 groupBy.js 컴포넌트로 이동됨
// addGroupBy, removeGroupBy, renderGroupByTags는 컴포넌트가 자동으로 처리
// 데이터 뷰 새로고침
function refreshDataView() {
const viewMode = localStorage.getItem('viewMode') || 'table';
if (viewMode === 'card') {
renderCardView();
} else {
if (groupByComponent && groupByComponent.isGrouped()) {
renderGroupedTable();
} else {
const columnsConfig = localStorage.getItem('columnsConfig');
if (columnsConfig) {
applyColumnsConfig();
} else {
initDataTable();
}
}
}
}
// 그룹화된 데이터 생성 - 컴포넌트 사용
function createGroupedData(data, fields) {
if (groupByComponent) {
return groupByComponent.createGroupedData(data, fields);
}
if (fields && fields.length === 0) return { ungrouped: data };
const grouped = {};
data.forEach(item => {
// 다중 레벨 그룹 키 생성
const groupKey = fields.map(field => {
const value = item[field] || '(없음)';
return `${groupByFieldNames[field]}: ${value}`;
}).join(' > ');
if (!grouped[groupKey]) {
grouped[groupKey] = [];
}
grouped[groupKey].push(item);
});
return grouped;
}
// 그룹화된 테이블 렌더링
function renderGroupedTable() {
const container = document.getElementById('dataTableContainer');
// 컬럼 설정 가져오기
const savedColumns = JSON.parse(localStorage.getItem('columnsConfig') || 'null');
const defaultColumns = [
{ id: 'selected', name: '선택', checked: true, width: 60 },
{ id: 'status', name: '상태', checked: true, width: 80 },
{ id: 'itemCode', name: '품번코드', checked: true, width: 140 },
{ id: 'itemName', name: '품명', checked: true, width: 200 },
{ id: 'spec', name: '규격', checked: true, width: 150 },
{ id: 'material', name: '재질', checked: true, width: 180 },
{ id: 'stockUnit', name: '재고단위', checked: true, width: 100 },
{ id: 'weight', name: '중량', checked: true, width: 80 },
{ id: 'weightUnit', name: '단위', checked: true, width: 80 },
{ id: 'image', name: '이미지', checked: true, width: 80 },
{ id: 'category', name: '구분', checked: true, width: 100 },
{ id: 'type', name: '유형', checked: true, width: 100 },
{ id: 'memo', name: '메모', checked: true, width: 180 },
{ id: 'createdBy', name: '등록자', checked: true, width: 100 },
{ id: 'createdDate', name: '등록일', checked: true, width: 120 },
{ id: 'modifiedDate', name: '최종수정일', checked: true, width: 120 }
];
const columns = savedColumns || defaultColumns;
const visibleColumns = columns.filter(c => c.checked);
// 컬럼 매핑
const columnMap = {
'selected': { field: 'selected', label: '<input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()">', align: 'center',
formatter: (value, row) => `<input type="checkbox" ${value ? 'checked' : ''} onclick="event.stopPropagation();" onchange="toggleSelect(${row.id})">` },
'status': { field: 'status', label: '상태', align: 'center',
formatter: (value) => {
const badgeMap = {
'정상': { text: '정상', class: 'badge-success' },
'품절': { text: '품절', class: 'badge-danger' },
'대기': { text: '대기', class: 'badge-warning' }
};
return formatters.badge(value, badgeMap);
}
},
'itemCode': { field: 'itemCode', label: '품번코드' },
'itemName': { field: 'itemName', label: '품명', formatter: (value) => `<strong>${value}</strong>` },
'spec': { field: 'spec', label: '규격' },
'material': { field: 'material', label: '재질' },
'stockUnit': { field: 'stockUnit', label: '재고단위', align: 'center' },
'weight': { field: 'weight', label: '중량', align: 'right', formatter: (value) => value.toLocaleString() },
'weightUnit': { field: 'weightUnit', label: '단위', align: 'center' },
'image': { field: 'image', label: '이미지', align: 'center' },
'category': { field: 'category', label: '구분', align: 'center',
formatter: (value) => {
const badgeMap = {
'원자재': { text: '원자재', class: 'badge-primary' },
'중간재': { text: '중간재', class: 'badge-warning' },
'완제품': { text: '완제품', class: 'badge-success' }
};
return formatters.badge(value, badgeMap);
}
},
'type': { field: 'type', label: '유형', align: 'center' },
'memo': { field: 'memo', label: '메모' },
'createdBy': { field: 'createdBy', label: '등록자' },
'createdDate': { field: 'createdDate', label: '등록일', align: 'center' },
'modifiedDate': { field: 'modifiedDate', label: '최종수정일', align: 'center' }
};
// 컬럼 구성
const tableColumns = visibleColumns.map(col => {
const baseCol = columnMap[col.id] || { field: col.id, label: col.name };
return { ...baseCol, width: col.width + 'px' };
});
// 그룹화된 데이터 생성
const groupedData = createGroupedData(itemsData, window.groupByFields || []);
// 테이블 헤더
const headerHtml = tableColumns.map(col => {
const align = col.align || 'left';
const width = col.width ? `style="width: ${col.width};"` : '';
return `<th ${width} style="text-align: ${align};">${col.label}</th>`;
}).join('');
// 그룹별로 행 생성
let bodyHtml = '';
let groupIndex = 0;
for (const [groupKey, groupItems] of Object.entries(groupedData)) {
const colSpan = tableColumns.length;
// 그룹 내 전체 선택 여부 확인
const allSelected = groupItems.every(item => item.selected);
const someSelected = groupItems.some(item => item.selected);
// 그룹 헤더
bodyHtml += `
<tr class="group-header">
<td colspan="${colSpan}">
<div class="group-header-content">
<input type="checkbox"
${allSelected ? 'checked' : ''}
${someSelected && !allSelected ? 'indeterminate' : ''}
onclick="event.stopPropagation(); toggleGroupSelect(${groupIndex})"
id="groupCheckbox${groupIndex}"
style="margin-right: 8px;">
<span class="group-toggle" id="groupToggle${groupIndex}" onclick="toggleGroup(${groupIndex})">▼</span>
<span onclick="toggleGroup(${groupIndex})" style="flex: 1;">${groupKey}</span>
<span class="group-count" onclick="toggleGroup(${groupIndex})">(${groupItems.length}개)</span>
</div>
</td>
</tr>
`;
// 그룹 데이터 행들
const groupRowsHtml = groupItems.map((row, rowIndex) => {
const cellsHtml = tableColumns.map(col => {
let value = row[col.field];
// 포맷터 적용
if (col.formatter) {
value = col.formatter(value, row, rowIndex);
}
// null/undefined 처리
if (value === null || value === undefined) {
value = '-';
}
const align = col.align || 'left';
return `<td style="text-align: ${align};">${value}</td>`;
}).join('');
return `<tr class="group-row">${cellsHtml}</tr>`;
}).join('');
bodyHtml += `
<tbody class="group-rows" id="groupRows${groupIndex}" data-group-items='${JSON.stringify(groupItems.map(i => i.id))}'>
${groupRowsHtml}
</tbody>
`;
groupIndex++;
}
// 전체 테이블 HTML
const tableHtml = `
<div class="table-container">
<table class="data-table">
<thead>
<tr>${headerHtml}</tr>
</thead>
${bodyHtml}
</table>
</div>
`;
container.innerHTML = tableHtml;
// 컬럼 리사이즈 기능 추가
addColumnResizeHandles();
// 행 클릭 이벤트 추가
addRowClickEvents();
updateTotalCount();
}
// 그룹 토글
function toggleGroup(groupIndex) {
const groupRows = document.getElementById(`groupRows${groupIndex}`);
const toggle = document.getElementById(`groupToggle${groupIndex}`);
if (groupRows && toggle) {
groupRows.classList.toggle('collapsed');
toggle.classList.toggle('collapsed');
}
}
// 그룹별 전체 선택/해제
function toggleGroupSelect(groupIndex) {
const groupRows = document.getElementById(`groupRows${groupIndex}`);
const checkbox = document.getElementById(`groupCheckbox${groupIndex}`);
if (!groupRows || !checkbox) return;
const checked = checkbox.checked;
const groupItemIds = JSON.parse(groupRows.getAttribute('data-group-items'));
// 해당 그룹의 모든 아이템 선택/해제
groupItemIds.forEach(id => {
const item = itemsData.find(i => i.id === id);
if (item) {
item.selected = checked;
}
});
// 화면 다시 렌더링
refreshDataView();
}
// 데이터 테이블 초기화
function initDataTable() {
const columns = [
{
field: 'selected',
label: '<input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()">',
width: '60px',
align: 'center',
formatter: (value, row) => `<input type="checkbox" ${value ? 'checked' : ''} onclick="event.stopPropagation();" onchange="toggleSelect(${row.id})">`
},
{
field: 'status',
label: '상태',
width: '80px',
align: 'center',
formatter: (value) => {
const badgeMap = {
'정상': { text: '정상', class: 'badge-success' },
'품절': { text: '품절', class: 'badge-danger' },
'대기': { text: '대기', class: 'badge-warning' }
};
return formatters.badge(value, badgeMap);
}
},
{ field: 'itemCode', label: '품번코드', width: '140px' },
{ field: 'itemName', label: '품명', width: '200px', formatter: (value) => `<strong>${value}</strong>` },
{ field: 'spec', label: '규격', width: '150px' },
{ field: 'material', label: '재질', width: '180px' },
{ field: 'stockUnit', label: '재고단위', width: '100px', align: 'center' },
{ field: 'weight', label: '중량', width: '80px', align: 'right', formatter: (value) => value.toLocaleString() },
{ field: 'weightUnit', label: '단위', width: '80px', align: 'center' },
{ field: 'image', label: '이미지', width: '80px', align: 'center' },
{
field: 'category',
label: '구분',
width: '100px',
align: 'center',
formatter: (value) => {
const badgeMap = {
'원자재': { text: '원자재', class: 'badge-primary' },
'중간재': { text: '중간재', class: 'badge-warning' },
'완제품': { text: '완제품', class: 'badge-success' }
};
return formatters.badge(value, badgeMap);
}
},
{ field: 'type', label: '유형', width: '100px', align: 'center' },
{ field: 'memo', label: '메모', width: '180px' },
{ field: 'createdBy', label: '등록자', width: '100px' },
{ field: 'createdDate', label: '등록일', width: '120px', align: 'center' },
{ field: 'modifiedDate', label: '최종수정일', width: '120px', align: 'center' }
];
const tableHtml = createDataTable({
columns: columns,
data: itemsData,
emptyMessage: '등록된 품목이 없습니다'
});
document.getElementById('dataTableContainer').innerHTML = tableHtml;
// 컬럼 리사이즈 기능 추가
addColumnResizeHandles();
// 행 클릭 이벤트 추가
addRowClickEvents();
updateTotalCount();
}
// 행 클릭 이벤트 추가
function addRowClickEvents() {
const table = document.querySelector('.data-table');
if (!table) return;
const rows = table.querySelectorAll('tbody tr');
rows.forEach((row, index) => {
row.addEventListener('click', function(e) {
// 체크박스 클릭인 경우 무시
if (e.target.type === 'checkbox') {
return;
}
// 행의 체크박스 찾기
const checkbox = this.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = !checkbox.checked;
// 데이터 업데이트
const item = itemsData[index];
if (item) {
item.selected = checkbox.checked;
if (checkbox.checked) {
this.classList.add('selected');
} else {
this.classList.remove('selected');
}
}
}
});
});
}
// 컬럼 리사이즈 핸들 추가
function addColumnResizeHandles() {
const table = document.querySelector('.data-table');
if (!table) return;
const headers = table.querySelectorAll('th');
headers.forEach((th, index) => {
// 리사이즈 핸들 추가
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
th.appendChild(resizeHandle);
let startX, startWidth, thElement;
resizeHandle.addEventListener('mousedown', function(e) {
thElement = this.parentElement;
startX = e.pageX;
startWidth = thElement.offsetWidth;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
e.stopPropagation();
});
function handleMouseMove(e) {
if (thElement) {
const diff = e.pageX - startX;
const newWidth = Math.max(60, startWidth + diff);
thElement.style.width = newWidth + 'px';
// 같은 인덱스의 td들도 동일한 너비 적용
const tbody = table.querySelector('tbody');
if (tbody) {
const rows = tbody.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells[index]) {
cells[index].style.width = newWidth + 'px';
}
});
}
}
}
function handleMouseUp() {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
thElement = null;
}
});
}
// 검색 실행
function performSearch() {
const searchType = document.getElementById('searchType').value;
const keyword = document.getElementById('searchKeyword').value.toLowerCase();
console.log('검색:', searchType, keyword);
// TODO: 실제 검색 로직 구현
alert(`검색 실행: ${searchType} - ${keyword}`);
}
// 검색 초기화
function resetSearch() {
const searchType = document.getElementById('searchType');
const searchKeyword = document.getElementById('searchKeyword');
if (searchType) searchType.value = '전체';
if (searchKeyword) searchKeyword.value = '';
// 데이터 뷰 새로고침
refreshDataView();
}
// 전체 선택/해제 (테이블형)
function toggleSelectAll() {
const checkbox = document.getElementById('selectAllCheckbox');
const checked = checkbox.checked;
itemsData.forEach(item => item.selected = checked);
// 화면 업데이트
const checkboxes = document.querySelectorAll('.data-table tbody input[type="checkbox"]');
const rows = document.querySelectorAll('.data-table tbody tr');
checkboxes.forEach((cb, index) => {
cb.checked = checked;
if (checked) {
rows[index].classList.add('selected');
} else {
rows[index].classList.remove('selected');
}
});
}
// 전체 선택/해제 (카드형)
function toggleSelectAllCard() {
const checkbox = document.getElementById('selectAllCheckboxCard');
const checked = checkbox.checked;
itemsData.forEach(item => item.selected = checked);
// 카드형 다시 렌더링
renderCardView();
}
// 개별 선택
function toggleSelect(id) {
const item = itemsData.find(i => i.id === id);
if (item) {
item.selected = !item.selected;
const row = event.target.closest('tr');
if (row) {
if (item.selected) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
}
// 전체 선택 체크박스 상태 업데이트
updateSelectAllCheckbox();
}
}
// 카드형 헤더 컬럼 리사이즈
let cardResizing = false;
let cardResizeColId = null;
let cardResizeStartX = 0;
let cardResizeStartWidth = 0;
function startCardResize(event, colId) {
event.stopPropagation();
event.preventDefault();
cardResizing = true;
cardResizeColId = colId;
cardResizeStartX = event.clientX;
// 현재 컬럼의 넓이 가져오기
const headerCol = event.target.parentElement;
cardResizeStartWidth = headerCol.offsetWidth;
document.addEventListener('mousemove', doCardResize);
document.addEventListener('mouseup', stopCardResize);
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
}
function doCardResize(event) {
if (!cardResizing) return;
const diff = event.clientX - cardResizeStartX;
const newWidth = Math.max(40, cardResizeStartWidth + diff);
// 헤더 컬럼 넓이 변경
const headerCols = document.querySelectorAll(`.card-header-col[data-col-id="${cardResizeColId}"]`);
headerCols.forEach(col => {
col.style.width = newWidth + 'px';
});
// 데이터 컬럼 넓이 변경
const dataCols = document.querySelectorAll(`.card-data-col[data-col-id="${cardResizeColId}"]`);
dataCols.forEach(col => {
col.style.width = newWidth + 'px';
});
}
function stopCardResize(event) {
if (!cardResizing) return;
cardResizing = false;
document.removeEventListener('mousemove', doCardResize);
document.removeEventListener('mouseup', stopCardResize);
document.body.style.cursor = '';
document.body.style.userSelect = '';
// localStorage에 저장
if (cardResizeColId) {
const savedColumns = JSON.parse(localStorage.getItem('columnsConfig') || '[]');
const headerCol = document.querySelector(`.card-header-col[data-col-id="${cardResizeColId}"]`);
if (headerCol && savedColumns.length > 0) {
const newWidth = headerCol.offsetWidth;
const colIndex = savedColumns.findIndex(c => c.id === cardResizeColId);
if (colIndex !== -1) {
savedColumns[colIndex].width = newWidth;
localStorage.setItem('columnsConfig', JSON.stringify(savedColumns));
}
}
}
cardResizeColId = null;
}
// 카드형에서 다중 선택/해제
function toggleCardSelect(id, event) {
const item = itemsData.find(item => item.id === id);
if (!item) return;
// Ctrl 또는 Cmd 키가 눌려있으면 다중 선택 모드
if (event.ctrlKey || event.metaKey) {
// 현재 항목만 토글
item.selected = !item.selected;
}
// Shift 키가 눌려있으면 범위 선택
else if (event.shiftKey) {
// 마지막 선택된 항목 찾기
const selectedItems = itemsData.filter(i => i.selected);
if (selectedItems.length > 0) {
const lastSelectedIndex = itemsData.indexOf(selectedItems[selectedItems.length - 1]);
const currentIndex = itemsData.findIndex(i => i.id === id);
const start = Math.min(lastSelectedIndex, currentIndex);
const end = Math.max(lastSelectedIndex, currentIndex);
// 범위 내 모든 항목 선택
for (let i = start; i <= end; i++) {
itemsData[i].selected = true;
}
} else {
item.selected = true;
}
}
// 일반 클릭: 단일 선택
else {
// 이미 선택된 항목을 다시 클릭하면 해제만
if (item.selected) {
item.selected = false;
} else {
// 모든 선택 해제 후 현재 항목만 선택
itemsData.forEach(i => i.selected = false);
item.selected = true;
}
}
// 화면 다시 렌더링
renderCardView();
updateSelectAllCheckbox();
}
// 전체 선택 체크박스 상태 업데이트
function updateSelectAllCheckbox() {
const allSelected = itemsData.every(item => item.selected);
const someSelected = itemsData.some(item => item.selected);
// 테이블형 체크박스
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
if (selectAllCheckbox) {
selectAllCheckbox.checked = allSelected;
selectAllCheckbox.indeterminate = someSelected && !allSelected;
}
// 카드형 체크박스
const selectAllCheckboxCard = document.getElementById('selectAllCheckboxCard');
if (selectAllCheckboxCard) {
selectAllCheckboxCard.checked = allSelected;
selectAllCheckboxCard.indeterminate = someSelected && !allSelected;
}
}
// 총 개수 업데이트
function updateTotalCount() {
document.getElementById('totalCount').textContent = itemsData.length;
}
// 품목 추가 모달 열기
function openAddModal() {
const selected = itemsData.filter(item => item.selected);
// 선택된 항목이 있으면 복사하여 추가
if (selected.length > 0) {
if (selected.length > 1) {
showToast('복사는 한 개씩만 가능합니다', '⚠️', true);
return;
}
itemModalMode = 'add';
editingItemId = null;
document.getElementById('itemModalTitle').textContent = '품목 복사 추가';
renderItemForm(selected[0]); // 선택된 데이터로 폼 채우기
} else {
// 선택 항목 없으면 빈 폼
itemModalMode = 'add';
editingItemId = null;
document.getElementById('itemModalTitle').textContent = '품목 추가';
renderItemForm();
}
document.getElementById('itemModal').classList.add('active');
// 첫 번째 입력 필드에 포커스
setTimeout(() => {
const firstInput = document.querySelector('#itemModalBody input, #itemModalBody select');
if (firstInput) firstInput.focus();
}, 100);
}
// 품목 수정 모달 열기
function openEditModal() {
const selected = itemsData.filter(item => item.selected);
if (selected.length === 0) {
showToast('수정할 항목을 선택하세요', '⚠️', true);
return;
}
if (selected.length > 1) {
showToast('수정은 한 개씩만 가능합니다', '⚠️', true);
return;
}
itemModalMode = 'edit';
editingItemId = selected[0].id;
document.getElementById('itemModalTitle').textContent = '품목 수정';
renderItemForm(selected[0]);
document.getElementById('itemModal').classList.add('active');
// 첫 번째 입력 필드에 포커스
setTimeout(() => {
const firstInput = document.querySelector('#itemModalBody input, #itemModalBody select');
if (firstInput) firstInput.focus();
}, 100);
}
// 품목 모달 닫기
function closeItemModal() {
document.getElementById('itemModal').classList.remove('active');
editingItemId = null;
}
// 품목 입력 폼 렌더링
function renderItemForm(data = null) {
const body = document.getElementById('itemModalBody');
let html = '';
itemFormFields.forEach((field, index) => {
const value = data ? (data[field.id] || '') : '';
const isLast = index === itemFormFields.length - 1;
if (field.type === 'select') {
html += `
<div class="item-form-field">
<select id="field_${field.id}"
data-field-index="${index}"
${field.required ? 'required' : ''}
onkeydown="handleFormKeyDown(event, ${index}, ${isLast})">
<option value="">${field.label}${field.required ? ' *' : ''}</option>
${field.options.map(opt =>
`<option value="${opt}" ${value === opt ? 'selected' : ''}>${opt}</option>`
).join('')}
</select>
</div>
`;
} else if (field.type === 'number') {
html += `
<div class="item-form-field">
<input type="number"
id="field_${field.id}"
data-field-index="${index}"
placeholder="${field.label}${field.required ? ' *' : ''}"
value="${value}"
${field.required ? 'required' : ''}
onkeydown="handleFormKeyDown(event, ${index}, ${isLast})"
step="any">
</div>
`;
} else {
html += `
<div class="item-form-field">
<input type="text"
id="field_${field.id}"
data-field-index="${index}"
placeholder="${field.label}${field.required ? ' *' : ''}"
value="${value}"
${field.required ? 'required' : ''}
onkeydown="handleFormKeyDown(event, ${index}, ${isLast})">
</div>
`;
}
});
body.innerHTML = html;
}
// 폼 엔터키 처리
function handleFormKeyDown(event, currentIndex, isLast) {
if (event.key === 'Enter') {
event.preventDefault();
if (isLast) {
// 마지막 필드에서 엔터: 저장
saveItem();
} else {
// 다음 필드로 이동
const nextField = document.querySelector(`[data-field-index="${currentIndex + 1}"]`);
if (nextField) {
nextField.focus();
}
}
}
}
// 품목 저장
function saveItem() {
const formData = {};
let hasError = false;
// 폼 데이터 수집 및 검증
itemFormFields.forEach(field => {
const input = document.getElementById(`field_${field.id}`);
const value = input.value.trim();
if (field.required && !value) {
input.style.borderColor = '#ef4444';
hasError = true;
setTimeout(() => {
input.style.borderColor = '';
}, 2000);
} else {
formData[field.id] = value;
}
});
if (hasError) {
showToast('필수 항목을 입력하세요', '⚠️', true);
return;
}
if (itemModalMode === 'add') {
// 새 품목 추가
const newItem = {
id: itemsData.length > 0 ? Math.max(...itemsData.map(i => i.id)) + 1 : 1,
selected: false,
...formData,
image: '📦',
createdBy: '사용자',
createdDate: new Date().toISOString().split('T')[0],
modifiedDate: new Date().toISOString().split('T')[0]
};
itemsData.push(newItem);
showToast('품목이 추가되었습니다', '✓');
} else {
// 기존 품목 수정
const item = itemsData.find(i => i.id === editingItemId);
if (item) {
Object.assign(item, formData);
item.modifiedDate = new Date().toISOString().split('T')[0];
showToast('품목이 수정되었습니다', '✓');
}
}
// 화면 갱신
refreshDataView();
// 연속입력 체크 여부 확인
const continuousInput = document.getElementById('continuousInput').checked;
if (continuousInput && itemModalMode === 'add') {
// 연속입력: 폼 초기화 후 첫 필드에 포커스
renderItemForm();
setTimeout(() => {
const firstInput = document.querySelector('#itemModalBody input, #itemModalBody select');
if (firstInput) firstInput.focus();
}, 100);
} else {
// 모달 닫기
closeItemModal();
}
}
// 코드 변경 관리 모달 열기
function openCodeChangeModal() {
const selected = itemsData.filter(item => item.selected);
if (selected.length === 0) {
showToast('변경할 품목을 선택하세요', '⚠️', true);
return;
}
if (selected.length > 1) {
showToast('코드 변경은 한 개씩만 가능합니다', '⚠️', true);
return;
}
// 모달 초기화
document.querySelectorAll('.code-change-option').forEach(opt => opt.classList.remove('selected'));
document.querySelectorAll('.code-change-form').forEach(form => form.classList.remove('active'));
document.getElementById('newCode').value = '';
document.getElementById('renamePreview').style.display = 'none';
document.getElementById('mergePreview').style.display = 'none';
// 현재 선택된 품목 정보 설정
const item = selected[0];
document.getElementById('currentCode').value = item.itemCode;
document.getElementById('sourceCode').value = item.itemCode;
// 합병 대상 품번 목록 생성 (자신 제외)
const targetSelect = document.getElementById('targetCode');
targetSelect.innerHTML = '<option value="">품번을 선택하세요</option>';
itemsData.forEach(i => {
if (i.id !== item.id) {
targetSelect.innerHTML += `<option value="${i.id}">${i.itemCode} - ${i.itemName}</option>`;
}
});
document.getElementById('codeChangeModal').classList.add('active');
}
// 코드 변경 모달 닫기
function closeCodeChangeModal() {
document.getElementById('codeChangeModal').classList.remove('active');
}
// 코드 변경 옵션 선택
let selectedCodeChangeOption = null;
function selectCodeChangeOption(option) {
selectedCodeChangeOption = option;
// 모든 옵션 선택 해제
document.querySelectorAll('.code-change-option').forEach(opt => {
opt.classList.remove('selected');
});
// 모든 폼 숨기기
document.querySelectorAll('.code-change-form').forEach(form => {
form.classList.remove('active');
});
// 선택된 옵션 활성화
if (option === 'rename') {
document.getElementById('renameOption').classList.add('selected');
document.getElementById('renameForm').classList.add('active');
// 입력 필드 이벤트 추가
const newCodeInput = document.getElementById('newCode');
newCodeInput.oninput = updateRenamePreview;
} else if (option === 'merge') {
document.getElementById('mergeOption').classList.add('selected');
document.getElementById('mergeForm').classList.add('active');
}
}
// 품번 변경 미리보기 업데이트
function updateRenamePreview() {
const newCode = document.getElementById('newCode').value.trim();
const currentCode = document.getElementById('currentCode').value;
const selected = itemsData.filter(item => item.selected)[0];
if (newCode) {
document.getElementById('previewOldCode').textContent = currentCode;
document.getElementById('previewNewCode').textContent = newCode;
document.getElementById('previewItemName').textContent = selected.itemName;
document.getElementById('renamePreview').style.display = 'block';
} else {
document.getElementById('renamePreview').style.display = 'none';
}
}
// 합병 미리보기 업데이트
function updateMergePreview() {
const targetId = document.getElementById('targetCode').value;
const sourceCode = document.getElementById('sourceCode').value;
if (targetId) {
const targetItem = itemsData.find(i => i.id === parseInt(targetId));
if (targetItem) {
document.getElementById('previewSourceCode').textContent = sourceCode;
document.getElementById('previewTargetCode').textContent =
`${targetItem.itemCode} - ${targetItem.itemName}`;
document.getElementById('mergePreview').style.display = 'block';
}
} else {
document.getElementById('mergePreview').style.display = 'none';
}
}
// 코드 변경 실행
function executeCodeChange() {
if (!selectedCodeChangeOption) {
showToast('변경 방식을 선택하세요', '⚠️', true);
return;
}
const selected = itemsData.filter(item => item.selected)[0];
if (selectedCodeChangeOption === 'rename') {
// 품번 변경
const newCode = document.getElementById('newCode').value.trim();
if (!newCode) {
showToast('새 품번을 입력하세요', '⚠️', true);
return;
}
// 중복 체크
const exists = itemsData.some(i => i.itemCode === newCode && i.id !== selected.id);
if (exists) {
showToast('이미 사용 중인 품번입니다', '⚠️', true);
return;
}
// 확인 메시지
if (!confirm(`품번을 "${selected.itemCode}"에서 "${newCode}"로 변경하시겠습니까?\n\n※ 관련된 모든 데이터의 품번이 함께 변경됩니다.`)) {
return;
}
// 품번 변경 실행
selected.itemCode = newCode;
selected.modifiedDate = new Date().toISOString().split('T')[0];
showToast('품번이 변경되었습니다', '✓');
} else if (selectedCodeChangeOption === 'merge') {
// 품번 합병
const targetId = document.getElementById('targetCode').value;
if (!targetId) {
showToast('통합될 품번을 선택하세요', '⚠️', true);
return;
}
const targetItem = itemsData.find(i => i.id === parseInt(targetId));
// 확인 메시지
if (!confirm(`⚠️ 품번 합병 확인\n\n삭제될 품번: ${selected.itemCode}\n통합될 품번: ${targetItem.itemCode}\n\n※ 이 작업은 되돌릴 수 없습니다.\n${selected.itemCode}의 모든 데이터(재고, 거래내역 등)가 ${targetItem.itemCode}로 통합됩니다.\n${selected.itemCode}는 영구적으로 삭제됩니다.\n\n정말 실행하시겠습니까?`)) {
return;
}
// 합병 실행 (실제로는 서버에서 처리)
// 여기서는 단순히 항목 삭제로 시뮬레이션
itemsData = itemsData.filter(i => i.id !== selected.id);
targetItem.modifiedDate = new Date().toISOString().split('T')[0];
showToast(`품번이 합병되었습니다 (${selected.itemCode}${targetItem.itemCode})`, '✓');
}
// 화면 갱신
refreshDataView();
closeCodeChangeModal();
}
// 선택 항목 삭제
function deleteSelected() {
const selected = itemsData.filter(item => item.selected);
if (selected.length === 0) {
showToast('삭제할 항목을 선택하세요', '⚠️', true);
return;
}
if (confirm(`선택한 ${selected.length}개 항목을 삭제하시겠습니까?`)) {
itemsData = itemsData.filter(item => !item.selected);
refreshDataView();
showToast('삭제되었습니다', '✓');
}
}
// 엑셀 다운로드
function downloadExcel() {
showToast('엑셀 다운로드 기능 (추후 구현)', '');
}
// 엑셀 업로드
function uploadExcel() {
openExcelUploadModal();
}
// 거래처 목록 반환 (엑셀 업로드 컴포넌트에서 사용)
function getCompanyList() {
// TODO: 실제 API에서 거래처 목록 가져오기
// 현재는 샘플 데이터 반환
return [
'ABC 주식회사',
'XYZ 상사',
'대한물산',
'글로벌트레이딩',
'한국제조',
'서울상사',
'부산물산',
'인천무역'
];
}
// 전역 함수로 노출
window.getCompanyList = getCompanyList;
// 사용자 옵션 모달 열기
function openUserOptionsModal() {
document.getElementById('userOptionsModal').classList.add('active');
// 첫 번째 탭을 강제로 활성화하고 렌더링
const firstTab = document.querySelector('.user-options-tab[onclick*="searchFields"]');
if (firstTab) {
document.querySelectorAll('.user-options-tab').forEach(tab => tab.classList.remove('active'));
firstTab.classList.add('active');
}
switchOptionsTab('searchFields', true);
}
// 사용자 옵션 모달 닫기
function closeUserOptionsModal(force = false) {
// force가 true이거나 자동 닫기 설정이 켜져있으면 닫기
const shouldAutoClose = localStorage.getItem('autoCloseModal');
if (force || shouldAutoClose === null || shouldAutoClose === 'true') {
document.getElementById('userOptionsModal').classList.remove('active');
}
}
// 옵션 탭 전환
function switchOptionsTab(tabName, forceRender = false) {
const body = document.getElementById('userOptionsBody');
// 현재 활성화된 탭 확인
const currentActiveTab = document.querySelector('.user-options-tab.active');
const isAlreadyActive = currentActiveTab && currentActiveTab.getAttribute('onclick').includes(tabName);
// 이미 활성화된 탭이고 강제 렌더링이 아니면 렌더링하지 않음
if (isAlreadyActive && !forceRender && body.innerHTML.trim() !== '') {
console.log('✋ 탭이 이미 활성화되어 있어 렌더링을 건너뜁니다:', tabName);
return;
}
console.log('🔄 탭 전환 및 렌더링:', tabName);
// 탭 버튼 활성화
document.querySelectorAll('.user-options-tab').forEach(tab => {
tab.classList.remove('active');
if (tab.getAttribute('onclick').includes(tabName)) {
tab.classList.add('active');
}
});
// 탭 내용 표시
switch(tabName) {
case 'searchFields':
body.innerHTML = renderSearchFieldsTab();
break;
case 'columnDisplay':
body.innerHTML = renderColumnDisplayTab();
addColumnDragEvents();
break;
case 'otherOptions':
body.innerHTML = renderOtherOptionsTab();
break;
}
}
// 검색필드 설정 탭 렌더링
function renderSearchFieldsTab() {
// localStorage에서 저장된 설정 불러오기
const savedFields = JSON.parse(localStorage.getItem('searchFieldsConfig') || 'null');
const defaultFields = [
{ id: 'status', name: '상태', checked: true, width: 120, type: 'select' },
{ id: 'itemCode', name: '품번코드', checked: true, width: 150, type: 'text' },
{ id: 'itemName', name: '품명', checked: true, width: 200, type: 'text' },
{ id: 'spec', name: '규격', checked: false, width: 150, type: 'text' },
{ id: 'material', name: '재질', checked: false, width: 180, type: 'text' },
{ id: 'stockUnit', name: '재고단위', checked: false, width: 100, type: 'select' },
{ id: 'weight', name: '중량', checked: false, width: 100, type: 'text' },
{ id: 'weightUnit', name: '단위', checked: false, width: 100, type: 'select' },
{ id: 'category', name: '구분', checked: false, width: 100, type: 'select' },
{ id: 'type', name: '유형', checked: false, width: 120, type: 'select' },
{ id: 'memo', name: '메모', checked: false, width: 180, type: 'text' },
{ id: 'createdBy', name: '등록자', checked: false, width: 100, type: 'text' },
{ id: 'createdDate', name: '등록일', checked: false, width: 120, type: 'text' },
{ id: 'modifiedDate', name: '최종수정일', checked: false, width: 120, type: 'text' }
];
const searchFields = savedFields || defaultFields;
// 처음 사용자 옵션을 열었을 때 기본값을 localStorage에 저장
if (!savedFields) {
console.log('💾 기본 검색필드 설정 저장 중...');
localStorage.setItem('searchFieldsConfig', JSON.stringify(defaultFields));
}
let html = '<p style="color: #6b7280; margin-bottom: 16px;">✓ 표시할 필드를 선택하고, 드래그하여 순서를 변경할 수 있습니다.</p>';
searchFields.forEach((field, index) => {
html += `
<div class="option-field-item" draggable="true" data-index="${index}" data-field-id="${field.id}">
<div class="drag-handle">☰</div>
<input type="checkbox" class="option-field-checkbox" ${field.checked ? 'checked' : ''}>
<div class="option-field-name">${field.name}</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 13px; color: #6b7280;">너비:</span>
<input type="number" class="option-field-width" value="${field.width}" min="60" style="padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 4px;">
<span style="font-size: 11px; color: #9ca3af;">px</span>
</div>
<select class="option-field-type" style="padding: 6px 10px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 13px;">
<option value="text" ${field.type === 'text' ? 'selected' : ''}>텍스트</option>
<option value="select" ${field.type === 'select' ? 'selected' : ''}>콤보박스</option>
<option value="multi" ${field.type === 'multi' ? 'selected' : ''}>다중 검색</option>
</select>
</div>
`;
});
setTimeout(() => addSearchFieldDragEvents(), 0);
return html;
}
// 컬럼 표시/숨기기 탭 렌더링
function renderColumnDisplayTab() {
// localStorage에서 저장된 설정 불러오기
const savedColumns = JSON.parse(localStorage.getItem('columnsConfig') || 'null');
const viewMode = localStorage.getItem('viewMode') || 'table';
const defaultColumns = [
{ id: 'selected', name: '선택', checked: true, width: 60 },
{ id: 'status', name: '상태', checked: true, width: 80 },
{ id: 'itemCode', name: '품번코드', checked: true, width: 140 },
{ id: 'itemName', name: '품명', checked: true, width: 200 },
{ id: 'spec', name: '규격', checked: true, width: 150 },
{ id: 'material', name: '재질', checked: true, width: 180 },
{ id: 'stockUnit', name: '재고단위', checked: true, width: 100 },
{ id: 'weight', name: '중량', checked: true, width: 80 },
{ id: 'weightUnit', name: '단위', checked: true, width: 80 },
{ id: 'image', name: '이미지', checked: true, width: 80 },
{ id: 'category', name: '구분', checked: true, width: 100 },
{ id: 'type', name: '유형', checked: true, width: 100 },
{ id: 'memo', name: '메모', checked: true, width: 180 },
{ id: 'createdBy', name: '등록자', checked: true, width: 100 },
{ id: 'createdDate', name: '등록일', checked: true, width: 120 },
{ id: 'modifiedDate', name: '최종수정일', checked: true, width: 120 }
];
const columns = savedColumns || defaultColumns;
let html = '<p style="color: #6b7280; margin-bottom: 16px;">✓ 표시할 컬럼을 선택하고, 드래그하여 순서를 변경할 수 있습니다.</p>';
columns.forEach((col, index) => {
const canDrag = true; // 모든 컬럼 드래그 가능
html += `
<div class="option-field-item" draggable="${canDrag}" data-index="${index}" data-col-id="${col.id}">
<div class="drag-handle">☰</div>
<input type="checkbox" class="option-field-checkbox" ${col.checked ? 'checked' : ''}>
<div class="option-field-name">${col.name}</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 13px; color: #6b7280;">너비:</span>
<input type="number" class="option-field-width" value="${col.width}" min="40" style="padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 4px;">
<span style="font-size: 11px; color: #9ca3af;">px</span>
</div>
</div>
`;
});
setTimeout(() => addColumnDragEvents(), 0);
return html;
}
// 기타옵션 탭 렌더링
function renderOtherOptionsTab() {
// 저장된 설정 불러오기
const freezeCount = localStorage.getItem('freezeColumnCount') || '0';
const gridLinesVisible = localStorage.getItem('gridLinesVisible');
const showGrid = gridLinesVisible === null || gridLinesVisible === 'true';
const toastEnabled = localStorage.getItem('toastEnabled');
const showToast = toastEnabled === null || toastEnabled === 'true';
const savedViewMode = localStorage.getItem('viewMode') || 'table';
const viewMode = savedViewMode;
const savedAutoClose = localStorage.getItem('autoCloseModal');
const autoCloseModal = savedAutoClose === null || savedAutoClose === 'true';
return `
<div style="margin-bottom: 24px;">
<h3 style="font-size: 15px; font-weight: 600; color: #1f2937; margin-bottom: 12px;">📌 틀고정</h3>
<p style="color: #6b7280; font-size: 13px; margin-bottom: 12px;">✓ 왼쪽을 기준으로 고정할 컬럼 개수를 선택하세요.</p>
<div class="freeze-option">
<label>고정할 컬럼 개수 (왼쪽부터)</label>
<input type="number" id="freezeColumnCount" value="${freezeCount}" min="0" max="5">
</div>
<div style="background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; padding: 12px; margin-top: 12px;">
<p style="font-size: 12px; color: #075985; margin: 0;">
<strong>💡 사용 방법:</strong> 예) 2를 입력하면 첫 번째와 두 번째 컬럼이 고정되어 가로 스크롤 시에도 항상 표시됩니다.
</p>
</div>
</div>
<div style="border-top: 2px solid #e5e7eb; padding-top: 24px; margin-bottom: 24px;">
<h3 style="font-size: 15px; font-weight: 600; color: #1f2937; margin-bottom: 12px;">🔲 그리드선</h3>
<p style="color: #6b7280; font-size: 13px; margin-bottom: 12px;">✓ 데이터 테이블의 그리드선 표시 여부를 선택하세요.</p>
<div style="display: flex; flex-direction: column; gap: 12px;">
<label style="display: flex; align-items: center; gap: 12px; padding: 16px; background: #f9fafb; border-radius: 8px; cursor: pointer; border: 2px solid ${showGrid ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s;">
<input type="radio" name="gridLines" value="show" ${showGrid ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
<div>
<div style="font-weight: 600; color: #1f2937; margin-bottom: 4px;">그리드선 보이기</div>
<div style="font-size: 12px; color: #6b7280;">테이블 셀 사이에 구분선이 표시됩니다</div>
</div>
</label>
<label style="display: flex; align-items: center; gap: 12px; padding: 16px; background: #f9fafb; border-radius: 8px; cursor: pointer; border: 2px solid ${!showGrid ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s;">
<input type="radio" name="gridLines" value="hide" ${!showGrid ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
<div>
<div style="font-weight: 600; color: #1f2937; margin-bottom: 4px;">그리드선 감추기</div>
<div style="font-size: 12px; color: #6b7280;">테이블이 깔끔하게 표시됩니다</div>
</div>
</label>
</div>
</div>
<div style="border-top: 2px solid #e5e7eb; padding-top: 24px; margin-bottom: 24px;">
<h3 style="font-size: 15px; font-weight: 600; color: #1f2937; margin-bottom: 12px;">💬 메시지창</h3>
<p style="color: #6b7280; font-size: 13px; margin-bottom: 12px;">✓ 작업 완료 시 화면 상단에 표시되는 메시지 사용 여부를 선택하세요.</p>
<div style="display: flex; flex-direction: column; gap: 12px;">
<label style="display: flex; align-items: center; gap: 12px; padding: 16px; background: #f9fafb; border-radius: 8px; cursor: pointer; border: 2px solid ${showToast ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s;">
<input type="radio" name="toastMessage" value="show" ${showToast ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
<div>
<div style="font-weight: 600; color: #1f2937; margin-bottom: 4px;">메시지창 사용</div>
<div style="font-size: 12px; color: #6b7280;">저장, 삭제 등 작업 완료 시 안내 메시지가 표시됩니다</div>
</div>
</label>
<label style="display: flex; align-items: center; gap: 12px; padding: 16px; background: #f9fafb; border-radius: 8px; cursor: pointer; border: 2px solid ${!showToast ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s;">
<input type="radio" name="toastMessage" value="hide" ${!showToast ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
<div>
<div style="font-weight: 600; color: #1f2937; margin-bottom: 4px;">메시지창 사용 안 함</div>
<div style="font-size: 12px; color: #6b7280;">메시지 없이 조용하게 작업이 수행됩니다</div>
</div>
</label>
</div>
<div style="background: #fef3c7; border: 1px solid #fde68a; border-radius: 8px; padding: 12px; margin-top: 12px;">
<p style="font-size: 12px; color: #92400e; margin: 0;">
<strong>⚠️ 참고:</strong> 메시지창을 사용 안 함으로 설정해도 오류 메시지는 계속 표시됩니다.
</p>
</div>
</div>
<div style="border-top: 2px solid #e5e7eb; padding-top: 24px; margin-bottom: 24px;">
<h3 style="font-size: 15px; font-weight: 600; color: #1f2937; margin-bottom: 12px;">👁️ 보기 모드</h3>
<p style="color: #6b7280; font-size: 13px; margin-bottom: 12px;">✓ 데이터 표시 방식을 선택하세요.</p>
<div style="display: flex; flex-direction: column; gap: 12px;">
<label style="display: flex; align-items: center; gap: 12px; padding: 16px; background: #f9fafb; border-radius: 8px; cursor: pointer; border: 2px solid ${viewMode === 'table' ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s;">
<input type="radio" name="viewMode" value="table" ${viewMode === 'table' ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
<div style="flex: 1;">
<div style="font-weight: 600; color: #1f2937; margin-bottom: 4px;">📊 테이블형</div>
<div style="font-size: 12px; color: #6b7280;">행과 열로 구성된 표 형태로 많은 데이터를 한눈에 볼 수 있습니다</div>
</div>
</label>
<label style="display: flex; align-items: center; gap: 12px; padding: 16px; background: #f9fafb; border-radius: 8px; cursor: pointer; border: 2px solid ${viewMode === 'card' ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s;">
<input type="radio" name="viewMode" value="card" ${viewMode === 'card' ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
<div style="flex: 1;">
<div style="font-weight: 600; color: #1f2937; margin-bottom: 4px;">📦 카드형</div>
<div style="font-size: 12px; color: #6b7280;">각 항목이 카드 형태로 표시되어 시각적으로 구분하기 쉽습니다</div>
</div>
</label>
</div>
<div style="background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; padding: 12px; margin-top: 12px;">
<p style="font-size: 12px; color: #075985; margin: 0;">
<strong>💡 추천:</strong> 많은 데이터를 비교할 때는 테이블형, 개별 항목을 자세히 볼 때는 카드형이 적합합니다.
</p>
</div>
</div>
<div style="border-top: 2px solid #e5e7eb; padding-top: 24px;">
<h3 style="font-size: 15px; font-weight: 600; color: #1f2937; margin-bottom: 12px;">🔄 모달 자동 닫기</h3>
<p style="color: #6b7280; font-size: 13px; margin-bottom: 12px;">✓ 옵션 저장 후 모달창 자동 닫힘 여부를 선택하세요.</p>
<div style="display: flex; flex-direction: column; gap: 12px;">
<label style="display: flex; align-items: center; gap: 12px; padding: 16px; background: #f9fafb; border-radius: 8px; cursor: pointer; border: 2px solid ${autoCloseModal ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s;">
<input type="radio" name="autoCloseModal" value="true" ${autoCloseModal ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
<div style="flex: 1;">
<div style="font-weight: 600; color: #1f2937; margin-bottom: 4px;">자동 닫기</div>
<div style="font-size: 12px; color: #6b7280;">저장 또는 취소 버튼 클릭 시 모달이 자동으로 닫힙니다</div>
</div>
</label>
<label style="display: flex; align-items: center; gap: 12px; padding: 16px; background: #f9fafb; border-radius: 8px; cursor: pointer; border: 2px solid ${!autoCloseModal ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s;">
<input type="radio" name="autoCloseModal" value="false" ${!autoCloseModal ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
<div style="flex: 1;">
<div style="font-weight: 600; color: #1f2937; margin-bottom: 4px;">수동 닫기</div>
<div style="font-size: 12px; color: #6b7280;">저장 후에도 모달이 유지되어 연속 작업이 가능합니다</div>
</div>
</label>
</div>
<div style="background: #fef3c7; border: 1px solid #fde68a; border-radius: 8px; padding: 12px; margin-top: 12px;">
<p style="font-size: 12px; color: #92400e; margin: 0;">
<strong>💡 팁:</strong> 여러 옵션을 연속으로 수정할 때는 "수동 닫기"를 권장합니다.
</p>
</div>
</div>
`;
}
// 검색필드 드래그 이벤트 추가
function addSearchFieldDragEvents() {
const items = document.querySelectorAll('#userOptionsBody .option-field-item[draggable="true"]');
let draggedItem = null;
items.forEach(item => {
item.addEventListener('dragstart', function(e) {
draggedItem = this;
this.style.opacity = '0.5';
});
item.addEventListener('dragend', function(e) {
this.style.opacity = '1';
});
item.addEventListener('dragover', function(e) {
e.preventDefault();
});
item.addEventListener('drop', function(e) {
e.preventDefault();
if (draggedItem !== this) {
const allItems = [...this.parentElement.querySelectorAll('.option-field-item')];
const draggedIndex = allItems.indexOf(draggedItem);
const targetIndex = allItems.indexOf(this);
if (draggedIndex < targetIndex) {
this.parentElement.insertBefore(draggedItem, this.nextSibling);
} else {
this.parentElement.insertBefore(draggedItem, this);
}
}
});
});
}
// 컬럼 드래그 이벤트 추가
function addColumnDragEvents() {
const items = document.querySelectorAll('#userOptionsBody .option-field-item[draggable="true"]');
let draggedItem = null;
items.forEach(item => {
item.addEventListener('dragstart', function(e) {
draggedItem = this;
this.style.opacity = '0.5';
});
item.addEventListener('dragend', function(e) {
this.style.opacity = '1';
});
item.addEventListener('dragover', function(e) {
e.preventDefault();
});
item.addEventListener('drop', function(e) {
e.preventDefault();
if (draggedItem !== this) {
const allItems = [...this.parentElement.querySelectorAll('.option-field-item')];
const draggedIndex = allItems.indexOf(draggedItem);
const targetIndex = allItems.indexOf(this);
if (draggedIndex < targetIndex) {
this.parentElement.insertBefore(draggedItem, this.nextSibling);
} else {
this.parentElement.insertBefore(draggedItem, this);
}
}
});
});
}
// 토스트 메시지 표시
function showToast(message, icon = '✓', force = false) {
const toastEnabled = localStorage.getItem('toastEnabled');
// force가 true이거나 경고/정보 아이콘인 경우는 설정과 관계없이 표시
if (!force && icon === '✓' && toastEnabled === 'false') {
return; // 성공 메시지만 설정에 따라 숨김
}
const toast = document.getElementById('toastMessage');
const toastIcon = toast.querySelector('.toast-icon');
const toastText = toast.querySelector('.toast-text');
toastIcon.textContent = icon;
toastText.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// 사용자 옵션 저장
function saveUserOptions() {
console.log('🔥🔥🔥 saveUserOptions 함수 호출됨! 🔥🔥🔥');
const body = document.getElementById('userOptionsBody');
const items = body.querySelectorAll('.option-field-item');
console.log('📦 body 요소:', body);
console.log('📋 items 개수:', items.length);
// 현재 활성화된 탭 확인
const activeTab = document.querySelector('.user-options-tab.active');
const tabText = activeTab ? activeTab.textContent.trim() : '';
console.log('💾 저장 시작 - 활성 탭:', tabText, '아이템 수:', items.length);
if (tabText === '검색필드 설정') {
// 검색필드 설정 저장
const searchFields = [];
items.forEach((item, index) => {
const fieldId = item.getAttribute('data-field-id');
const checkbox = item.querySelector('.option-field-checkbox');
const widthInput = item.querySelector('.option-field-width');
const typeSelect = item.querySelector('.option-field-type');
const name = item.querySelector('.option-field-name').textContent;
console.log(` 필드 ${index}: ${name} (${fieldId}) - 체크: ${checkbox.checked}`);
searchFields.push({
id: fieldId,
name: name,
checked: checkbox.checked,
width: parseInt(widthInput.value),
type: typeSelect.value,
order: index
});
});
console.log('✅ 검색필드 저장:', searchFields);
localStorage.setItem('searchFieldsConfig', JSON.stringify(searchFields));
// 검색 섹션 다시 렌더링
applySearchFieldsConfig();
} else if (tabText === '컬럼 표시/숨기기') {
// 컬럼 설정 저장
const columns = [];
items.forEach((item, index) => {
const colId = item.getAttribute('data-col-id');
const checkbox = item.querySelector('.option-field-checkbox');
const widthInput = item.querySelector('.option-field-width');
const name = item.querySelector('.option-field-name').textContent;
columns.push({
id: colId,
name: name,
checked: checkbox.checked,
width: parseInt(widthInput.value),
order: index
});
});
localStorage.setItem('columnsConfig', JSON.stringify(columns));
// 테이블 다시 렌더링
applyColumnsConfig();
} else if (tabText === '기타옵션') {
// 그리드선 설정 적용
const gridLines = document.querySelector('input[name="gridLines"]:checked');
if (gridLines) {
const table = document.querySelector('.data-table');
if (table) {
if (gridLines.value === 'hide') {
table.classList.add('hide-grid');
} else {
table.classList.remove('hide-grid');
}
}
localStorage.setItem('gridLinesVisible', gridLines.value === 'show');
}
// 틀고정 설정 저장
const freezeCount = document.getElementById('freezeColumnCount');
if (freezeCount) {
localStorage.setItem('freezeColumnCount', freezeCount.value);
applyFreezeColumns(parseInt(freezeCount.value));
}
// 메시지창 설정 저장
const toastMessage = document.querySelector('input[name="toastMessage"]:checked');
if (toastMessage) {
localStorage.setItem('toastEnabled', toastMessage.value === 'show');
}
// 보기 모드 설정 저장
const viewMode = document.querySelector('input[name="viewMode"]:checked');
if (viewMode) {
localStorage.setItem('viewMode', viewMode.value);
applyViewMode(viewMode.value);
}
// 모달 자동 닫기 설정 저장
const autoCloseModal = document.querySelector('input[name="autoCloseModal"]:checked');
if (autoCloseModal) {
localStorage.setItem('autoCloseModal', autoCloseModal.value);
}
}
// 자동 닫기 설정 확인
const shouldAutoClose = localStorage.getItem('autoCloseModal');
if (shouldAutoClose === null || shouldAutoClose === 'true') {
closeUserOptionsModal();
}
showToast('사용자 옵션이 저장되었습니다', '✓');
}
// 검색필드 설정 적용
window.applySearchFieldsConfig = function applySearchFieldsConfig() {
const savedFields = getSearchFieldsConfig('itemInfo');
console.log('🔍 검색필드 설정 적용:', savedFields);
if (!savedFields) {
console.log('❌ 저장된 검색필드 설정이 없습니다.');
return;
}
// 체크된 필드만 필터링
const visibleFields = savedFields.filter(f => f.checked);
console.log('✅ 체크된 필드:', visibleFields.length, visibleFields);
let searchHtml = '<div class="search-section"><div class="search-row">';
// 검색 필드들을 감싸는 컨테이너 시작
searchHtml += '<div class="search-fields-container">';
// 필드 생성
visibleFields.forEach(field => {
if (field.type === 'select') {
searchHtml += `
<div class="search-field">
<select id="${field.id}" style="min-width: ${field.width}px;">
<option value="">${field.name}</option>
<option value="전체">전체</option>
</select>
</div>
`;
} else if (field.type === 'multi') {
searchHtml += `
<div class="search-field">
<input type="text" id="${field.id}" placeholder="${field.name} (다중검색)" style="min-width: ${field.width}px;">
</div>
`;
} else {
searchHtml += `
<div class="search-field">
<input type="text" id="${field.id}" placeholder="${field.name}" style="min-width: ${field.width}px;">
</div>
`;
}
});
// 검색/초기화 버튼들
searchHtml += `
<div class="search-buttons">
<button class="btn btn-primary" onclick="performSearch()">🔍 검색</button>
<button class="btn btn-secondary" onclick="resetSearch()">초기화</button>
</div>
`;
// 검색 필드 컨테이너 닫기
searchHtml += '</div>';
// 오른쪽 버튼들 (항상 맨 오른쪽에 고정)
searchHtml += `
<div class="search-right-buttons">
<button class="btn btn-secondary" onclick="openUserOptions()">⚙️ 사용자옵션</button>
<button class="btn btn-primary" onclick="openWebcamModal()">📷 사진촬영</button>
<button class="btn btn-success" onclick="downloadExcel()">📥 다운로드</button>
<button class="btn btn-success" onclick="uploadExcel()">📤 업로드</button>
</div>
</div></div>`;
console.log('📝 검색 섹션 HTML 생성 완료');
const searchSection = document.getElementById('searchSection');
if (searchSection) {
searchSection.innerHTML = searchHtml;
console.log('✅ 검색 섹션 업데이트 완료');
} else {
console.error('❌ searchSection 엘리먼트를 찾을 수 없습니다!');
}
}
// 컬럼 설정 적용
function applyColumnsConfig(forceTable = false) {
const savedColumns = JSON.parse(localStorage.getItem('columnsConfig') || 'null');
const viewMode = localStorage.getItem('viewMode') || 'table';
// 카드형이고 강제 테이블 전환이 아니면 카드형으로 렌더링
if (viewMode === 'card' && !forceTable) {
renderCardView();
return;
}
if (!savedColumns) {
initDataTable();
return;
}
// 체크된 컬럼만 필터링
const visibleColumns = savedColumns.filter(c => c.checked);
// 컬럼 매핑
const columnMap = {
'selected': { field: 'selected', label: '<input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()">', align: 'center',
formatter: (value, row) => `<input type="checkbox" ${value ? 'checked' : ''} onclick="event.stopPropagation();" onchange="toggleSelect(${row.id})">` },
'status': { field: 'status', label: '상태', align: 'center',
formatter: (value) => {
const badgeMap = {
'정상': { text: '정상', class: 'badge-success' },
'품절': { text: '품절', class: 'badge-danger' },
'대기': { text: '대기', class: 'badge-warning' }
};
return formatters.badge(value, badgeMap);
}
},
'itemCode': { field: 'itemCode', label: '품번코드' },
'itemName': { field: 'itemName', label: '품명', formatter: (value) => `<strong>${value}</strong>` },
'spec': { field: 'spec', label: '규격' },
'material': { field: 'material', label: '재질' },
'stockUnit': { field: 'stockUnit', label: '재고단위', align: 'center' },
'weight': { field: 'weight', label: '중량', align: 'right', formatter: (value) => value.toLocaleString() },
'weightUnit': { field: 'weightUnit', label: '단위', align: 'center' },
'image': { field: 'image', label: '이미지', align: 'center' },
'category': { field: 'category', label: '구분', align: 'center',
formatter: (value) => {
const badgeMap = {
'원자재': { text: '원자재', class: 'badge-primary' },
'중간재': { text: '중간재', class: 'badge-warning' },
'완제품': { text: '완제품', class: 'badge-success' }
};
return formatters.badge(value, badgeMap);
}
},
'type': { field: 'type', label: '유형', align: 'center' },
'memo': { field: 'memo', label: '메모' },
'createdBy': { field: 'createdBy', label: '등록자' },
'createdDate': { field: 'createdDate', label: '등록일', align: 'center' },
'modifiedDate': { field: 'modifiedDate', label: '최종수정일', align: 'center' }
};
// 컬럼 구성
const columns = visibleColumns.map(col => {
const baseCol = columnMap[col.id] || { field: col.id, label: col.name };
return { ...baseCol, width: col.width + 'px' };
});
// 테이블 재생성
const tableHtml = createDataTable({
data: itemsData,
columns: columns
});
document.getElementById('dataTableContainer').innerHTML = tableHtml;
updateTotalCount();
addColumnResizeHandles();
addRowClickEvents();
}
// 보기 모드 적용
function applyViewMode(mode) {
const container = document.getElementById('dataTableContainer');
if (!container) return;
if (mode === 'card') {
renderCardView();
} else {
renderTableView();
}
}
// 카드형 렌더링
function renderCardView() {
const container = document.getElementById('dataTableContainer');
// 그룹화가 있는 경우
if (groupByFields.length > 0) {
renderGroupedCardView();
return;
}
// 컬럼 설정 가져오기
const savedColumns = JSON.parse(localStorage.getItem('columnsConfig') || 'null');
const defaultColumns = [
{ id: 'selected', name: '선택', checked: true, width: 60 },
{ id: 'status', name: '상태', checked: true, width: 80 },
{ id: 'itemCode', name: '품번코드', checked: true, width: 140 },
{ id: 'itemName', name: '품명', checked: true, width: 200 },
{ id: 'spec', name: '규격', checked: true, width: 150 },
{ id: 'material', name: '재질', checked: true, width: 180 },
{ id: 'stockUnit', name: '재고단위', checked: true, width: 100 },
{ id: 'weight', name: '중량', checked: true, width: 80 },
{ id: 'weightUnit', name: '단위', checked: true, width: 80 },
{ id: 'image', name: '이미지', checked: true, width: 80 },
{ id: 'category', name: '구분', checked: true, width: 100 },
{ id: 'type', name: '유형', checked: true, width: 100 },
{ id: 'memo', name: '메모', checked: true, width: 180 },
{ id: 'createdBy', name: '등록자', checked: true, width: 100 },
{ id: 'createdDate', name: '등록일', checked: true, width: 120 },
{ id: 'modifiedDate', name: '최종수정일', checked: true, width: 120 }
];
const columns = savedColumns || defaultColumns;
const visibleColumns = columns.filter(c => c.checked);
// 그리드선 설정 확인
const gridLines = localStorage.getItem('gridLines') || 'show';
const borderStyle = gridLines === 'hide' ? 'border: none;' : 'border: 1px solid #e5e7eb;';
// 카드형: 품목별로 가로로 길게 표시
let html = '<div style="padding: 10px; display: flex; flex-direction: column; gap: 10px;">';
itemsData.forEach(item => {
// 상태 뱃지 색상
const statusBadge = item.status === '정상' ? 'badge-success' :
item.status === '품절' ? 'badge-danger' : 'badge-warning';
// 구분(category) 뱃지 색상
const categoryBadge = item.category === '원자재' ? 'badge-primary' :
item.category === '중간재' ? 'badge-warning' : 'badge-success';
const cardBorderStyle = gridLines === 'hide' ?
`border: 2px solid ${item.selected ? '#3b82f6' : 'transparent'};` :
`border: 2px solid ${item.selected ? '#3b82f6' : '#e5e7eb'};`;
html += `
<div class="card-item" style="background: white; ${cardBorderStyle} border-radius: 8px; padding: 16px; cursor: pointer; transition: all 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; flex-wrap: wrap; gap: 20px; align-items: center;"
onclick="toggleCardSelect(${item.id}, event)"
onmouseover="this.style.boxShadow='0 4px 12px rgba(0,0,0,0.15)'"
onmouseout="this.style.boxShadow='0 1px 3px rgba(0,0,0,0.1)'">
`;
visibleColumns.forEach(col => {
const colWidth = parseInt(col.width) || 100;
if (col.id === 'selected') {
html += `
<div style="display: flex; align-items: center; gap: 6px; width: ${colWidth}px;">
<input type="checkbox" ${item.selected ? 'checked' : ''} onclick="event.stopPropagation();"
style="width: 18px; height: 18px; cursor: pointer; pointer-events: none;">
</div>
`;
} else if (col.id === 'status') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span class="badge ${statusBadge}" style="font-size: 12px; padding: 4px 10px;">${item.status}</span>
</div>
`;
} else if (col.id === 'itemCode') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.itemCode}</span>
</div>
`;
} else if (col.id === 'itemName') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 14px; font-weight: 600; color: #1f2937;">${item.itemName}</span>
</div>
`;
} else if (col.id === 'spec') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.spec}</span>
</div>
`;
} else if (col.id === 'material') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.material}</span>
</div>
`;
} else if (col.id === 'stockUnit') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.stockUnit}</span>
</div>
`;
} else if (col.id === 'weight') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af; text-align: right;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937; font-weight: 600; text-align: right;">${item.weight.toLocaleString()}</span>
</div>
`;
} else if (col.id === 'weightUnit') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.weightUnit}</span>
</div>
`;
} else if (col.id === 'image') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 32px;">${item.image}</span>
</div>
`;
} else if (col.id === 'category') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span class="badge ${categoryBadge}" style="font-size: 12px; padding: 4px 10px;">${item.category}</span>
</div>
`;
} else if (col.id === 'type') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.type}</span>
</div>
`;
} else if (col.id === 'memo') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 12px; color: #6b7280;">${item.memo || '-'}</span>
</div>
`;
} else if (col.id === 'createdBy') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 12px; color: #1f2937;">${item.createdBy}</span>
</div>
`;
} else if (col.id === 'createdDate') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 12px; color: #6b7280;">${item.createdDate}</span>
</div>
`;
} else if (col.id === 'modifiedDate') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 12px; color: #6b7280;">${item.modifiedDate}</span>
</div>
`;
}
});
html += `</div>`;
});
html += '</div>';
container.innerHTML = html;
updateSelectAllCheckbox();
}
// 그룹화된 카드형 렌더링
function renderGroupedCardView() {
const container = document.getElementById('dataTableContainer');
// 컬럼 설정 가져오기
const savedColumns = JSON.parse(localStorage.getItem('columnsConfig') || 'null');
const defaultColumns = [
{ id: 'selected', name: '선택', checked: true, width: 60 },
{ id: 'status', name: '상태', checked: true, width: 80 },
{ id: 'itemCode', name: '품번코드', checked: true, width: 140 },
{ id: 'itemName', name: '품명', checked: true, width: 200 },
{ id: 'spec', name: '규격', checked: true, width: 150 },
{ id: 'material', name: '재질', checked: true, width: 180 },
{ id: 'stockUnit', name: '재고단위', checked: true, width: 100 },
{ id: 'weight', name: '중량', checked: true, width: 80 },
{ id: 'weightUnit', name: '단위', checked: true, width: 80 },
{ id: 'image', name: '이미지', checked: true, width: 80 },
{ id: 'category', name: '구분', checked: true, width: 100 },
{ id: 'type', name: '유형', checked: true, width: 100 },
{ id: 'memo', name: '메모', checked: true, width: 180 },
{ id: 'createdBy', name: '등록자', checked: true, width: 100 },
{ id: 'createdDate', name: '등록일', checked: true, width: 120 },
{ id: 'modifiedDate', name: '최종수정일', checked: true, width: 120 }
];
const columns = savedColumns || defaultColumns;
const visibleColumns = columns.filter(c => c.checked);
// 그룹화된 데이터 생성
const groupedData = createGroupedData(itemsData, window.groupByFields || []);
// 그리드선 설정 확인
const gridLines = localStorage.getItem('gridLines') || 'show';
let html = '<div style="padding: 10px; display: flex; flex-direction: column; gap: 15px;">';
let groupIndex = 0;
for (const [groupKey, groupItems] of Object.entries(groupedData)) {
// 그룹 내 전체 선택 여부 확인
const allSelected = groupItems.every(item => item.selected);
// 그룹 헤더
html += `
<div style="background: #f3f4f6; border-radius: 8px; padding: 12px; user-select: none;">
<div style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox"
${allSelected ? 'checked' : ''}
onclick="toggleCardGroupSelect(${groupIndex})"
id="cardGroupCheckbox${groupIndex}"
style="opacity: 1;">
<span id="cardGroupToggle${groupIndex}"
style="font-size: 12px; transition: transform 0.2s; cursor: pointer;"
onclick="toggleCardGroup(${groupIndex})">▼</span>
<span style="font-weight: 700; color: #374151; flex: 1; cursor: pointer;"
onclick="toggleCardGroup(${groupIndex})">${groupKey}</span>
<span style="color: #3b82f6; font-weight: 700; cursor: pointer;"
onclick="toggleCardGroup(${groupIndex})">(${groupItems.length}개)</span>
</div>
</div>
`;
// 그룹 카드들
html += `<div id="cardGroupItems${groupIndex}" style="display: flex; flex-direction: column; gap: 10px;">`;
groupItems.forEach(item => {
// 상태 뱃지 색상
const statusBadge = item.status === '정상' ? 'badge-success' :
item.status === '품절' ? 'badge-danger' : 'badge-warning';
// 구분(category) 뱃지 색상
const categoryBadge = item.category === '원자재' ? 'badge-primary' :
item.category === '중간재' ? 'badge-warning' : 'badge-success';
const cardBorderStyle = gridLines === 'hide' ?
`border: 2px solid ${item.selected ? '#3b82f6' : 'transparent'};` :
`border: 2px solid ${item.selected ? '#3b82f6' : '#e5e7eb'};`;
html += `
<div class="card-item" style="background: white; ${cardBorderStyle} border-radius: 8px; padding: 16px; cursor: pointer; transition: all 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; flex-wrap: wrap; gap: 20px; align-items: center;"
onclick="toggleCardSelect(${item.id}, event)"
onmouseover="this.style.boxShadow='0 4px 12px rgba(0,0,0,0.15)'"
onmouseout="this.style.boxShadow='0 1px 3px rgba(0,0,0,0.1)'">
`;
visibleColumns.forEach(col => {
const colWidth = parseInt(col.width) || 100;
if (col.id === 'selected') {
html += `
<div style="display: flex; align-items: center; gap: 6px; width: ${colWidth}px;">
<input type="checkbox" ${item.selected ? 'checked' : ''} onclick="event.stopPropagation();"
style="width: 18px; height: 18px; cursor: pointer; pointer-events: none;">
</div>
`;
} else if (col.id === 'status') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span class="badge ${statusBadge}" style="font-size: 12px; padding: 4px 10px;">${item.status}</span>
</div>
`;
} else if (col.id === 'itemCode') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.itemCode}</span>
</div>
`;
} else if (col.id === 'itemName') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 14px; font-weight: 600; color: #1f2937;">${item.itemName}</span>
</div>
`;
} else if (col.id === 'spec') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.spec}</span>
</div>
`;
} else if (col.id === 'material') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.material}</span>
</div>
`;
} else if (col.id === 'stockUnit') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.stockUnit}</span>
</div>
`;
} else if (col.id === 'weight') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af; text-align: right;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937; font-weight: 600; text-align: right;">${item.weight.toLocaleString()}</span>
</div>
`;
} else if (col.id === 'weightUnit') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.weightUnit}</span>
</div>
`;
} else if (col.id === 'image') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 32px;">${item.image}</span>
</div>
`;
} else if (col.id === 'category') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span class="badge ${categoryBadge}" style="font-size: 12px; padding: 4px 10px;">${item.category}</span>
</div>
`;
} else if (col.id === 'type') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 13px; color: #1f2937;">${item.type}</span>
</div>
`;
} else if (col.id === 'memo') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 12px; color: #6b7280;">${item.memo || '-'}</span>
</div>
`;
} else if (col.id === 'createdBy') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 12px; color: #1f2937;">${item.createdBy}</span>
</div>
`;
} else if (col.id === 'createdDate') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 12px; color: #6b7280;">${item.createdDate}</span>
</div>
`;
} else if (col.id === 'modifiedDate') {
html += `
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center; width: ${colWidth}px;">
<span style="font-size: 11px; font-weight: 600; color: #9ca3af;">${col.name}</span>
<span style="font-size: 12px; color: #6b7280;">${item.modifiedDate}</span>
</div>
`;
}
});
html += `</div>`;
});
html += '</div>';
groupIndex++;
}
html += '</div>';
container.innerHTML = html;
updateSelectAllCheckbox();
}
// 카드형 그룹 토글
function toggleCardGroup(groupIndex) {
const groupItems = document.getElementById(`cardGroupItems${groupIndex}`);
const toggle = document.getElementById(`cardGroupToggle${groupIndex}`);
if (groupItems && toggle) {
if (groupItems.style.display === 'none') {
groupItems.style.display = 'flex';
toggle.style.transform = 'rotate(0deg)';
} else {
groupItems.style.display = 'none';
toggle.style.transform = 'rotate(-90deg)';
}
}
}
// 카드형 그룹별 전체 선택/해제
function toggleCardGroupSelect(groupIndex) {
const checkbox = document.getElementById(`cardGroupCheckbox${groupIndex}`);
if (!checkbox) return;
const checked = checkbox.checked;
// 현재 그룹화된 데이터에서 해당 그룹의 아이템 찾기
const groupedData = createGroupedData(itemsData, window.groupByFields || []);
const groupKeys = Object.keys(groupedData);
if (groupIndex < groupKeys.length) {
const groupKey = groupKeys[groupIndex];
const groupItems = groupedData[groupKey];
// 해당 그룹의 모든 아이템 선택/해제
groupItems.forEach(item => {
item.selected = checked;
});
// 화면 다시 렌더링
refreshDataView();
}
}
// 테이블형 렌더링
function renderTableView() {
const columnsConfig = localStorage.getItem('columnsConfig');
if (columnsConfig) {
applyColumnsConfig();
} else {
initDataTable();
}
}
// 틀고정 적용
function applyFreezeColumns(count) {
if (count <= 0) return;
const table = document.querySelector('.data-table');
if (!table) return;
// 모든 고정 스타일 제거
table.querySelectorAll('th, td').forEach(cell => {
cell.style.position = '';
cell.style.left = '';
cell.style.zIndex = '';
cell.style.background = '';
});
// 새로운 고정 적용
const headers = table.querySelectorAll('thead th');
let leftOffset = 0;
for (let i = 0; i < Math.min(count, headers.length); i++) {
const th = headers[i];
th.style.position = 'sticky';
th.style.left = leftOffset + 'px';
th.style.zIndex = '10';
th.style.background = '#f9fafb';
// 같은 인덱스의 모든 td에도 적용
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const td = row.children[i];
if (td) {
td.style.position = 'sticky';
td.style.left = leftOffset + 'px';
td.style.zIndex = '9';
td.style.background = 'white';
}
});
leftOffset += th.offsetWidth;
}
}
// 저장된 설정 불러오기
function loadUserSettings() {
// 그리드선 설정 불러오기
const gridLinesVisible = localStorage.getItem('gridLinesVisible');
if (gridLinesVisible !== null) {
const table = document.querySelector('.data-table');
if (table && gridLinesVisible === 'false') {
table.classList.add('hide-grid');
}
}
// 검색필드 설정 적용
const searchFieldsConfig = localStorage.getItem('searchFieldsConfig');
if (searchFieldsConfig) {
applySearchFieldsConfig();
}
// 컬럼 설정 적용
const columnsConfig = localStorage.getItem('columnsConfig');
if (columnsConfig) {
applyColumnsConfig();
}
// 틀고정 설정 적용
const freezeCount = localStorage.getItem('freezeColumnCount');
if (freezeCount && parseInt(freezeCount) > 0) {
setTimeout(() => applyFreezeColumns(parseInt(freezeCount)), 100);
}
// 보기 모드 설정 적용
const viewMode = localStorage.getItem('viewMode');
if (viewMode) {
setTimeout(() => applyViewMode(viewMode), 150);
}
}
// localStorage 초기화 (개발용 - 순서 수정 반영)
function resetAllSettings() {
localStorage.removeItem('columnsConfig');
localStorage.removeItem('searchFieldsConfig');
localStorage.removeItem('freezeColumnCount');
localStorage.removeItem('gridLines');
localStorage.removeItem('toastEnabled');
localStorage.removeItem('viewMode');
localStorage.removeItem('autoCloseModal');
location.reload();
}
// 초기화
document.addEventListener('DOMContentLoaded', function() {
// 개발 모드: 컬럼 구조가 완전히 변경되었으므로 기존 설정 초기화
const needsReset = localStorage.getItem('columnsVersion');
if (needsReset !== 'v6') {
// v5 -> v6: Group By 컴포넌트 전환
migrateToComponentSettings();
localStorage.setItem('columnsVersion', 'v6');
}
// Group By 컴포넌트 초기화 (기존 HTML 요소 활용)
groupByComponent = new GroupByComponent({
selectId: 'groupByField',
tagsId: 'groupByTags',
fields: {
'status': '상태',
'category': '구분',
'type': '유형',
'stockUnit': '재고단위',
'createdBy': '등록자'
},
onGroupChange: () => {
// 전역 변수 동기화 후 검색
window.groupByFields = groupByComponent.getGroupByFields();
console.log('GroupBy 변경됨:', window.groupByFields);
refreshDataView();
}
});
// GroupBy 전역 함수 연결
window.addGroupBy = function() {
groupByComponent.addGroupBy();
};
window.removeGroupBy = function(field) {
groupByComponent.removeGroupBy(field);
};
// 저장된 그룹화 옵션 복원
setTimeout(() => {
restoreGroupingOptions_ItemInfo();
}, 300);
// 사용자옵션 컴포넌트 초기화
initializeUserOptionsComponent();
// 사용자 설정 먼저 로드
loadUserSettings();
// 검색필드 설정이 없으면 기본 검색 섹션 초기화
const searchFieldsConfig = localStorage.getItem('itemInfo_searchFieldsConfig');
if (!searchFieldsConfig) {
initSearchSection();
}
// 테이블 초기화
initDataTable();
// 엑셀 업로드 모달 초기화
createExcelUploadModal();
});
// 기존 설정을 컴포넌트 형식으로 마이그레이션
function migrateToComponentSettings() {
// 검색필드 설정 마이그레이션
const oldSearchFields = localStorage.getItem('searchFieldsConfig');
if (oldSearchFields) {
localStorage.setItem('itemInfo_searchFieldsConfig', oldSearchFields);
}
// 컬럼 설정 마이그레이션
const oldColumns = localStorage.getItem('columnsConfig');
if (oldColumns) {
localStorage.setItem('itemInfo_columnsConfig', oldColumns);
}
// 틀고정 설정 마이그레이션
const oldFreezeCount = localStorage.getItem('freezeColumnCount');
if (oldFreezeCount) {
localStorage.setItem('itemInfo_freezeColumnCount', oldFreezeCount);
}
// 그리드선 설정 마이그레이션
const oldGridLines = localStorage.getItem('gridLinesVisible');
if (oldGridLines !== null) {
localStorage.setItem('itemInfo_gridLinesVisible', oldGridLines);
}
// 메시지창 설정 마이그레이션
const oldToastEnabled = localStorage.getItem('toastEnabled');
if (oldToastEnabled !== null) {
localStorage.setItem('itemInfo_toastEnabled', oldToastEnabled);
}
// 보기 모드 설정 마이그레이션
const oldViewMode = localStorage.getItem('viewMode');
if (oldViewMode) {
localStorage.setItem('itemInfo_viewMode', oldViewMode);
}
// 모달 자동 닫기 설정 마이그레이션
const oldAutoClose = localStorage.getItem('autoCloseModal');
if (oldAutoClose !== null) {
localStorage.setItem('itemInfo_autoCloseModal', oldAutoClose);
}
}
// 사용자옵션 컴포넌트 초기화
// ========== 저장된 그룹화 옵션 복원 ==========
function restoreGroupingOptions_ItemInfo() {
if (typeof getGroupByColumn === 'function') {
const savedColumn = getGroupByColumn('itemInfo');
console.log('💾 [품목정보] 저장된 그룹화 옵션:', { savedColumn });
if (savedColumn && groupByComponent) {
groupByComponent.addGrouping(savedColumn);
window.groupByFields = groupByComponent.getGroupByFields();
refreshDataView();
console.log('✅ [품목정보] 그룹화 옵션 복원 완료');
}
}
}
function initializeUserOptionsComponent() {
const modalHtml = createUserOptionsModal({
pageId: 'itemInfo',
enableGrouping: true,
groupingColumns: [
{ key: 'status', label: '상태' },
{ key: 'category', label: '구분' },
{ key: 'type', label: '유형' },
{ key: 'stockUnit', label: '재고단위' },
{ key: 'createdBy', label: '등록자' }
],
searchFields: [
{ id: 'status', label: '상태', type: 'select', width: 120 },
{ id: 'itemCode', label: '품번코드', type: 'text', width: 150 },
{ id: 'itemName', label: '품명', type: 'text', width: 200 },
{ id: 'spec', label: '규격', type: 'text', width: 150 },
{ id: 'material', label: '재질', type: 'text', width: 180 },
{ id: 'stockUnit', label: '재고단위', type: 'select', width: 100 },
{ id: 'weight', label: '중량', type: 'text', width: 100 },
{ id: 'weightUnit', label: '단위', type: 'select', width: 100 },
{ id: 'category', label: '구분', type: 'select', width: 100 },
{ id: 'type', label: '유형', type: 'select', width: 120 },
{ id: 'memo', label: '메모', type: 'text', width: 180 },
{ id: 'createdBy', label: '등록자', type: 'text', width: 100 },
{ id: 'createdDate', label: '등록일', type: 'text', width: 120 },
{ id: 'modifiedDate', label: '최종수정일', type: 'text', width: 120 }
],
columns: [
{ id: 'selected', label: '선택', width: 60 },
{ id: 'status', label: '상태', width: 80 },
{ id: 'itemCode', label: '품번코드', width: 140 },
{ id: 'itemName', label: '품명', width: 200 },
{ id: 'spec', label: '규격', width: 150 },
{ id: 'material', label: '재질', width: 180 },
{ id: 'stockUnit', label: '재고단위', width: 100 },
{ id: 'weight', label: '중량', width: 80 },
{ id: 'weightUnit', label: '단위', width: 80 },
{ id: 'image', label: '이미지', width: 80 },
{ id: 'category', label: '구분', width: 100 },
{ id: 'type', label: '유형', width: 100 },
{ id: 'memo', label: '메모', width: 180 },
{ id: 'createdBy', label: '등록자', width: 100 },
{ id: 'createdDate', label: '등록일', width: 120 },
{ id: 'modifiedDate', label: '최종수정일', width: 120 }
],
enableViewMode: true, // 보기 모드 활성화
enableFreezeColumns: true, // 틀고정 활성화
enableGridLines: true, // 그리드선 활성화
onSave: function() {
console.log('품목정보 사용자 옵션 저장됨');
applySearchFieldsConfig();
applyColumnsConfig();
// 저장된 그룹화 옵션 즉시 적용
restoreGroupingOptions_ItemInfo();
applyAllSettings();
}
});
document.body.insertAdjacentHTML('beforeend', modalHtml);
}
// 모든 설정 적용
function applyAllSettings() {
// 그리드선 설정
const gridLinesVisible = getGridLinesVisible('itemInfo');
const table = document.querySelector('.data-table');
if (table) {
if (gridLinesVisible) {
table.classList.remove('hide-grid');
} else {
table.classList.add('hide-grid');
}
}
// 틀고정 설정
const freezeCount = getFreezeColumnCount('itemInfo');
applyFreezeColumns(freezeCount);
// 보기 모드 설정
const viewMode = getViewMode('itemInfo');
applyViewMode(viewMode);
}
</script>
</body>
</html>