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

3916 lines
167 KiB
HTML
Raw Normal View History

<!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>