Files
tk-factory-services/system1-factory/web/pages/work/tbm-mobile.html
Hyungi Ahn af4bd26b06 fix(mobile): TBM 모바일 버튼 반응성 개선 + 로딩 오버레이 추가
touch-action: manipulation으로 더블탭 줌 방지, busy guard로 중복 호출 차단,
waitForApi 전환, CSS 스피너 로딩 오버레이로 비동기 작업 피드백 제공

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 08:40:46 +09:00

2299 lines
89 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>TBM | (주)테크니컬코리아</title>
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/api-base.js"></script>
<script src="/js/app-init.js?v=5" defer></script>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', sans-serif;
background: #f3f4f6;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
touch-action: manipulation;
}
button, .m-tbm-row, .m-tab, .m-new-btn, .m-detail-btn, .m-load-more,
.picker-item, .split-radio-item, .split-session-item, .pull-btn,
.de-save-btn, .de-group-btn, .de-split-btn, .pill-btn, .worker-card,
[onclick] {
touch-action: manipulation;
}
@media (min-width: 480px) {
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
}
/* Header */
.m-header {
position: sticky;
top: 0;
z-index: 100;
background: linear-gradient(135deg, #2563eb, #1d4ed8);
color: white;
padding: 0.875rem 1rem;
padding-top: calc(0.875rem + env(safe-area-inset-top));
}
.m-header-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.m-header h1 {
margin: 0;
font-size: 1.125rem;
font-weight: 700;
}
.m-header .m-date {
font-size: 0.75rem;
opacity: 0.8;
}
.m-new-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
background: rgba(255,255,255,0.2);
color: white;
border: 1.5px solid rgba(255,255,255,0.4);
border-radius: 2rem;
font-size: 0.8125rem;
font-weight: 700;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.m-new-btn:active { background: rgba(255,255,255,0.3); }
/* Tabs */
.m-tabs {
display: flex;
background: white;
border-bottom: 1px solid #e5e7eb;
position: sticky;
top: 0;
z-index: 90;
}
.m-tab {
flex: 1;
padding: 0.75rem;
text-align: center;
font-size: 0.8125rem;
font-weight: 600;
color: #9ca3af;
border: none;
background: none;
cursor: pointer;
border-bottom: 2px solid transparent;
-webkit-tap-highlight-color: transparent;
}
.m-tab.active {
color: #2563eb;
border-bottom-color: #2563eb;
}
.m-tab .tab-count {
display: inline-block;
min-width: 18px;
height: 18px;
line-height: 18px;
border-radius: 9px;
background: #e5e7eb;
color: #6b7280;
font-size: 0.6875rem;
font-weight: 700;
text-align: center;
margin-left: 0.25rem;
padding: 0 0.25rem;
}
.m-tab.active .tab-count {
background: #dbeafe;
color: #1d4ed8;
}
/* Content area */
.m-content {
padding-bottom: calc(76px + env(safe-area-inset-bottom));
min-height: 60vh;
}
/* Date group */
.m-date-group {
padding: 0.5rem 1rem 0.25rem;
}
.m-date-label {
font-size: 0.6875rem;
font-weight: 700;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.02em;
}
/* TBM list row */
.m-tbm-row {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: white;
border-bottom: 1px solid #f3f4f6;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.m-tbm-row:active { background: #f9fafb; }
.m-tbm-row:first-child { border-top: 1px solid #e5e7eb; }
.m-row-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
margin-right: 0.75rem;
}
.m-row-status.draft { background: #f59e0b; }
.m-row-status.completed { background: #10b981; }
.m-row-status.cancelled { background: #ef4444; }
.m-row-body {
flex: 1;
min-width: 0;
}
.m-row-main {
font-size: 0.875rem;
font-weight: 600;
color: #1f2937;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.m-row-sub {
font-size: 0.6875rem;
color: #9ca3af;
margin-top: 0.125rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.m-row-right {
flex-shrink: 0;
text-align: right;
margin-left: 0.75rem;
}
.m-row-count {
font-size: 0.8125rem;
font-weight: 700;
color: #1f2937;
}
.m-row-count-label {
font-size: 0.625rem;
color: #9ca3af;
}
.m-row-time {
font-size: 0.625rem;
color: #9ca3af;
margin-top: 0.125rem;
}
/* TBM detail expanded */
.m-tbm-detail {
display: none;
background: #f9fafb;
padding: 0.75rem 1rem 0.75rem 2.75rem;
border-bottom: 1px solid #e5e7eb;
}
.m-tbm-row.expanded + .m-tbm-detail { display: block; }
.m-detail-row {
display: flex;
padding: 0.25rem 0;
font-size: 0.75rem;
}
.m-detail-label {
color: #6b7280;
width: 50px;
flex-shrink: 0;
}
.m-detail-value {
color: #1f2937;
font-weight: 500;
flex: 1;
}
.m-detail-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid #e5e7eb;
}
.m-detail-btn {
flex: 1;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
background: white;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
text-align: center;
-webkit-tap-highlight-color: transparent;
}
.m-detail-btn:active { background: #f3f4f6; }
.m-detail-btn.primary {
background: #2563eb;
color: white;
border-color: #2563eb;
}
.m-detail-btn.primary:active { background: #1d4ed8; }
.m-detail-btn.danger {
color: #ef4444;
border-color: #fca5a5;
}
/* Empty state */
.m-empty {
text-align: center;
padding: 3rem 1rem;
color: #9ca3af;
}
.m-empty-icon {
font-size: 2.5rem;
margin-bottom: 0.75rem;
opacity: 0.5;
}
.m-empty-text {
font-size: 0.875rem;
}
.m-empty-sub {
font-size: 0.75rem;
margin-top: 0.25rem;
}
/* Load more */
.m-load-more {
display: block;
width: calc(100% - 2rem);
margin: 0.75rem 1rem;
padding: 0.75rem;
border: 1px dashed #d1d5db;
border-radius: 0.75rem;
background: white;
font-size: 0.8125rem;
color: #6b7280;
cursor: pointer;
text-align: center;
-webkit-tap-highlight-color: transparent;
}
.m-load-more:active { background: #f3f4f6; }
/* Loading skeleton */
.m-skeleton {
height: 56px;
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-bottom: 1px solid #f3f4f6;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Bottom nav */
.m-bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 68px;
background: #ffffff;
border-top: 1px solid #e5e7eb;
box-shadow: 0 -2px 12px rgba(0,0,0,0.08);
z-index: 1000;
padding-bottom: env(safe-area-inset-bottom);
display: flex;
align-items: center;
justify-content: space-around;
}
@media (min-width: 480px) {
.m-bottom-nav { max-width: 480px; margin: 0 auto; }
}
.m-nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-decoration: none;
color: #9ca3af;
font-family: inherit;
cursor: pointer;
padding: 0.5rem 0.25rem;
-webkit-tap-highlight-color: transparent;
border: none;
background: none;
}
.m-nav-item.active { color: #2563eb; }
.m-nav-item svg {
width: 26px;
height: 26px;
margin-bottom: 4px;
}
.m-nav-item.active svg { stroke-width: 2.5; }
.m-nav-label {
font-size: 0.6875rem;
font-weight: 500;
}
.m-nav-item.active .m-nav-label { font-weight: 700; }
/* Detail badge */
.m-detail-badge {
display: inline-block;
font-size: 0.625rem;
font-weight: 700;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
margin-left: 0.375rem;
vertical-align: middle;
}
.m-detail-badge.incomplete {
background: #fef3c7;
color: #92400e;
}
.m-detail-badge.complete {
background: #d1fae5;
color: #065f46;
}
/* Worker card status indicator */
.de-worker-card.filled {
background: #f0fdf4;
border-left: 3px solid #10b981;
padding-left: calc(0.625rem - 3px);
}
.de-worker-card.unfilled {
border-left: 3px solid #f59e0b;
padding-left: calc(0.625rem - 3px);
}
.de-worker-status {
font-size: 0.625rem;
font-weight: 600;
margin-left: 0.375rem;
}
.de-worker-status.ok { color: #059669; }
.de-worker-status.missing { color: #d97706; }
/* Group select */
.de-worker-check {
width: 20px;
height: 20px;
accent-color: #2563eb;
margin-right: 0.5rem;
flex-shrink: 0;
}
.de-group-bar {
display: none;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 0.5rem;
padding: 0.5rem 0.625rem;
margin: 0 1rem 0.5rem;
font-size: 0.75rem;
color: #1e40af;
}
.de-group-bar.visible { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.de-group-btn {
padding: 0.25rem 0.5rem;
background: #2563eb;
color: white;
border: none;
border-radius: 0.25rem;
font-size: 0.6875rem;
font-weight: 600;
cursor: pointer;
}
.de-select-all-row {
display: flex;
align-items: center;
padding: 0.375rem 1rem;
font-size: 0.75rem;
color: #6b7280;
border-bottom: 1px solid #e5e7eb;
}
/* Detail edit bottom sheet */
.detail-edit-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 9000;
}
.detail-edit-sheet {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9001;
background: white;
border-radius: 1rem 1rem 0 0;
max-height: 90vh;
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -4px 24px rgba(0,0,0,0.15);
}
.de-header {
position: sticky;
top: 0;
background: white;
padding: 1rem 1rem 0;
border-radius: 1rem 1rem 0 0;
z-index: 1;
}
.de-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.de-header h3 { margin: 0; font-size: 1rem; font-weight: 700; }
.de-close {
background: none;
border: none;
font-size: 1.25rem;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
}
/* Picker popup */
.picker-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 9100;
}
.picker-sheet {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9101;
background: white;
border-radius: 1rem 1rem 0 0;
max-height: 70vh;
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -4px 24px rgba(0,0,0,0.15);
}
.picker-header {
position: sticky;
top: 0;
background: white;
padding: 0.875rem 1rem 0.5rem;
border-radius: 1rem 1rem 0 0;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-header h4 { margin: 0; font-size: 0.9375rem; font-weight: 700; }
.picker-close {
background: none;
border: none;
font-size: 1.125rem;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
}
.picker-list { padding: 0.25rem 0; }
.picker-item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
font-size: 0.875rem;
color: #1f2937;
cursor: pointer;
border-bottom: 1px solid #f3f4f6;
-webkit-tap-highlight-color: transparent;
}
.picker-item:active { background: #f3f4f6; }
.picker-item.selected { background: #eff6ff; color: #1d4ed8; font-weight: 600; }
.picker-item-sub { font-size: 0.6875rem; color: #9ca3af; margin-left: 0.375rem; }
.picker-divider {
padding: 0.375rem 1rem;
font-size: 0.6875rem;
font-weight: 700;
color: #6b7280;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.picker-add-row {
display: flex;
gap: 0.375rem;
padding: 0.625rem 1rem;
border-top: 1px solid #e5e7eb;
background: #f9fafb;
position: sticky;
bottom: 0;
}
.picker-add-input {
flex: 1;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.8125rem;
}
.picker-add-btn {
padding: 0.5rem 0.75rem;
background: #10b981;
color: white;
border: none;
border-radius: 0.375rem;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.de-worker-list { padding: 0 1rem; }
.de-worker-card {
padding: 0.625rem 0;
border-bottom: 1px solid #f3f4f6;
}
.de-worker-card:last-child { border-bottom: none; }
.de-worker-name {
font-size: 0.875rem;
font-weight: 600;
color: #1f2937;
}
.de-worker-job {
font-size: 0.75rem;
color: #6b7280;
}
.de-worker-fields {
display: flex;
flex-direction: column;
gap: 0.375rem;
margin-top: 0.375rem;
}
.de-field-row {
display: flex;
align-items: center;
gap: 0.375rem;
}
.de-field-label {
font-size: 0.6875rem;
color: #6b7280;
width: 32px;
flex-shrink: 0;
}
.de-field-row select {
flex: 1;
padding: 0.4375rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.8125rem;
background: white;
}
.de-save-area {
padding: 0.75rem 1rem 1rem;
position: sticky;
bottom: 0;
background: white;
border-top: 1px solid #e5e7eb;
}
.de-save-btn {
width: 100%;
padding: 0.75rem;
background: #2563eb;
color: white;
border: none;
border-radius: 0.5rem;
font-size: 0.9375rem;
font-weight: 700;
cursor: pointer;
}
.de-save-btn:disabled { opacity: 0.5; }
/* My TBM highlight */
.m-tbm-row.my-tbm {
border-left: 3px solid #2563eb;
}
.m-leader-badge {
display: inline-block;
font-size: 0.625rem;
font-weight: 700;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
margin-left: 0.25rem;
background: #eff6ff;
color: #1d4ed8;
}
.m-transfer-badge {
display: inline-block;
font-size: 0.5625rem;
font-weight: 600;
padding: 0.0625rem 0.3125rem;
border-radius: 0.25rem;
margin-left: 0.25rem;
background: #fef3c7;
color: #92400e;
}
.m-work-hours-tag {
display: inline-block;
font-size: 0.625rem;
font-weight: 600;
padding: 0.0625rem 0.25rem;
border-radius: 0.25rem;
margin-left: 0.25rem;
background: #dbeafe;
color: #1d4ed8;
}
/* Split sheet */
.split-sheet {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9201;
background: white;
border-radius: 1rem 1rem 0 0;
max-height: 85vh;
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -4px 24px rgba(0,0,0,0.15);
}
.split-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 9200;
}
.split-header {
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.split-header h4 { margin: 0 0 0.25rem; font-size: 0.9375rem; font-weight: 700; }
.split-header p { margin: 0; font-size: 0.75rem; color: #6b7280; }
.split-body { padding: 0.75rem 1rem; }
.split-field { margin-bottom: 0.75rem; }
.split-field label { display: block; font-size: 0.75rem; font-weight: 600; color: #374151; margin-bottom: 0.25rem; }
.split-input {
width: 100%;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.875rem;
}
.split-radio-group { display: flex; gap: 0.5rem; margin-top: 0.25rem; }
.split-radio-item {
flex: 1;
padding: 0.5rem;
border: 1.5px solid #d1d5db;
border-radius: 0.5rem;
text-align: center;
font-size: 0.8125rem;
cursor: pointer;
background: white;
}
.split-radio-item.active {
border-color: #2563eb;
background: #eff6ff;
color: #1d4ed8;
font-weight: 600;
}
.split-session-list { margin-top: 0.5rem; }
.split-session-item {
padding: 0.625rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
margin-bottom: 0.375rem;
cursor: pointer;
font-size: 0.8125rem;
}
.split-session-item:active, .split-session-item.active {
border-color: #2563eb;
background: #eff6ff;
}
.split-footer {
padding: 0.75rem 1rem;
border-top: 1px solid #e5e7eb;
}
.split-btn {
width: 100%;
padding: 0.75rem;
background: #2563eb;
color: white;
border: none;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 700;
cursor: pointer;
}
.split-btn:disabled { opacity: 0.5; }
/* Pull sheet (read-only other TBM members) */
.pull-sheet {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9101;
background: white;
border-radius: 1rem 1rem 0 0;
max-height: 85vh;
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -4px 24px rgba(0,0,0,0.15);
}
.pull-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 9100;
}
.pull-header {
position: sticky;
top: 0;
background: white;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
border-radius: 1rem 1rem 0 0;
z-index: 1;
}
.pull-header h4 { margin: 0; font-size: 0.9375rem; font-weight: 700; }
.pull-header p { margin: 0.25rem 0 0; font-size: 0.75rem; color: #6b7280; }
.pull-member-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid #f3f4f6;
}
.pull-member-info { flex: 1; }
.pull-member-name { font-size: 0.875rem; font-weight: 600; color: #1f2937; }
.pull-member-sub { font-size: 0.6875rem; color: #9ca3af; margin-top: 0.125rem; }
.pull-btn {
padding: 0.375rem 0.75rem;
background: #2563eb;
color: white;
border: none;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
flex-shrink: 0;
}
.pull-btn:disabled {
background: #d1d5db;
color: #9ca3af;
cursor: default;
}
/* de-split-btn in detail edit */
.de-split-btn {
padding: 0.25rem 0.5rem;
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.6875rem;
font-weight: 600;
color: #374151;
cursor: pointer;
margin-left: auto;
}
.de-split-btn:active { background: #e5e7eb; }
/* Toast */
.toast-container {
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%);
z-index: 10001;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-10px); }
}
/* Loading overlay */
.m-loading-overlay {
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(255,255,255,0.75);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
}
.m-loading-overlay.active { display: flex; }
.m-loading-spinner {
width: 36px;
height: 36px;
border: 3px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.m-loading-text {
font-size: 0.875rem;
color: #6b7280;
}
</style>
</head>
<body>
<!-- Loading Overlay -->
<div id="loadingOverlay" class="m-loading-overlay">
<div class="m-loading-spinner"></div>
<div class="m-loading-text" id="loadingText">불러오는 중...</div>
</div>
<!-- Header -->
<div class="m-header">
<div class="m-header-top">
<div>
<h1>TBM</h1>
<div class="m-date" id="headerDate"></div>
</div>
<button type="button" class="m-new-btn" onclick="location.href='/pages/work/tbm-create.html'">
+ 새 TBM
</button>
</div>
</div>
<!-- Tabs -->
<div class="m-tabs">
<button type="button" class="m-tab active" data-tab="today" onclick="switchTab('today')">
당일 <span class="tab-count" id="todayCount">0</span>
</button>
<button type="button" class="m-tab" data-tab="all" onclick="switchTab('all')">
전체 <span class="tab-count" id="allCount">0</span>
</button>
</div>
<!-- Content -->
<div class="m-content" id="tbmContent">
<div class="m-skeleton"></div>
<div class="m-skeleton"></div>
<div class="m-skeleton"></div>
<div class="m-skeleton"></div>
</div>
<!-- Bottom Nav -->
<nav class="m-bottom-nav">
<a href="/pages/dashboard.html" class="m-nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span class="m-nav-label"></span>
</a>
<a href="/pages/work/tbm-mobile.html" class="m-nav-item active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
</svg>
<span class="m-nav-label">TBM</span>
</a>
<a href="/pages/work/report-create.html" class="m-nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
</svg>
<span class="m-nav-label">작업보고</span>
</a>
<a href="/pages/attendance/checkin.html" class="m-nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
<span class="m-nav-label">출근</span>
</a>
</nav>
<!-- Toast -->
<div id="toastContainer" class="toast-container"></div>
<!-- TBM 완료 바텀시트 -->
<div id="completeOverlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); z-index:9000;" onclick="closeCompleteSheet()"></div>
<div id="completeSheet" style="display:none; position:fixed; bottom:0; left:0; right:0; z-index:9001; background:white; border-radius:1rem 1rem 0 0; max-height:85vh; overflow-y:auto; padding-bottom:env(safe-area-inset-bottom); box-shadow:0 -4px 24px rgba(0,0,0,0.15);">
<div style="padding:1rem 1rem 0;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem;">
<h3 style="margin:0; font-size:1rem; font-weight:700;">TBM 완료</h3>
<button type="button" onclick="closeCompleteSheet()" style="background:none; border:none; font-size:1.25rem; color:#6b7280; cursor:pointer; padding:0.25rem;"></button>
</div>
<p style="margin:0 0 0.75rem; font-size:0.8125rem; color:#6b7280;">각 작업자의 근태를 선택하세요</p>
</div>
<div id="completeWorkerList" style="padding:0 1rem;"></div>
<div style="padding:0.75rem 1rem 1rem;">
<button type="button" id="completeSheetBtn" onclick="submitCompleteSheet()" style="width:100%; padding:0.75rem; background:#2563eb; color:white; border:none; border-radius:0.5rem; font-size:0.9375rem; font-weight:700; cursor:pointer;">완료 처리</button>
</div>
</div>
<!-- 세부 편집 바텀시트 -->
<div id="detailEditOverlay" class="detail-edit-overlay" onclick="closeDetailEditSheet()"></div>
<div id="detailEditSheet" class="detail-edit-sheet">
<div class="de-header">
<div class="de-header-row">
<h3>세부 내역 입력</h3>
<button type="button" class="de-close" onclick="closeDetailEditSheet()"></button>
</div>
</div>
<div class="de-select-all-row">
<input type="checkbox" class="de-worker-check" id="deSelectAll" onchange="toggleSelectAll()">
<span>전체 선택</span>
<span id="deSelectedCount" style="margin-left:auto; font-weight:600; color:#2563eb;"></span>
</div>
<div class="de-group-bar" id="deGroupBar">
<span id="deGroupLabel">0명 선택</span>
<button type="button" class="de-group-btn" onclick="openPicker('task')">작업 설정</button>
<button type="button" class="de-group-btn" onclick="openPicker('workplace')">장소 설정</button>
</div>
<div class="de-worker-list" id="deWorkerList"></div>
<div class="de-save-area">
<div style="display:flex; gap:0.5rem;">
<button type="button" class="de-save-btn" id="deSaveBtn" onclick="saveDetailEdit()" style="flex:2;">저장</button>
<button type="button" class="de-save-btn" onclick="completeFromDetailSheet()" style="flex:1; background:#10b981;">완료</button>
<button type="button" class="de-save-btn" onclick="deleteFromDetailSheet()" style="flex:0.7; background:#ef4444;">삭제</button>
</div>
</div>
</div>
<!-- 작업/장소 선택 피커 -->
<div id="pickerOverlay" class="picker-overlay" onclick="closePicker()"></div>
<div id="pickerSheet" class="picker-sheet">
<div class="picker-header">
<h4 id="pickerTitle">선택</h4>
<button type="button" class="picker-close" onclick="closePicker()"></button>
</div>
<div class="picker-list" id="pickerList"></div>
<div class="picker-add-row" id="pickerAddRow">
<input type="text" class="picker-add-input" id="pickerAddInput" placeholder="새 항목 추가...">
<button type="button" class="picker-add-btn" id="pickerAddBtn" onclick="addNewItem()">추가</button>
</div>
</div>
<!-- 분할 바텀시트 -->
<div id="splitOverlay" class="split-overlay" onclick="closeSplitSheet()"></div>
<div id="splitSheet" class="split-sheet">
<div class="split-header">
<div style="display:flex; justify-content:space-between; align-items:center;">
<h4 id="splitTitle">작업 분할</h4>
<button type="button" onclick="closeSplitSheet()" style="background:none; border:none; font-size:1.125rem; color:#6b7280; cursor:pointer;"></button>
</div>
<p id="splitSubtitle">작업자의 근무 시간을 분할합니다</p>
</div>
<div class="split-body">
<div class="split-field">
<label>현재 TBM 작업 시간</label>
<input type="number" id="splitHours" class="split-input" step="0.5" min="0.5" max="7.5" placeholder="예: 5">
<div id="splitRemainder" style="font-size:0.75rem; color:#6b7280; margin-top:0.25rem;"></div>
</div>
<div class="split-field">
<label>나머지 시간 배정</label>
<div class="split-radio-group">
<div class="split-radio-item active" id="splitOptKeep" onclick="setSplitOption('keep')">현재 TBM 유지</div>
<div class="split-radio-item" id="splitOptSend" onclick="setSplitOption('send')">다른 반장에게</div>
</div>
</div>
<div class="split-field">
<label>프로젝트 (변경 시 선택)</label>
<select id="splitProjectId" class="split-input" style="padding:0.625rem;">
<option value="">현재 프로젝트 유지</option>
</select>
</div>
<div class="split-field">
<label>공정 (변경 시 선택)</label>
<select id="splitWorkTypeId" class="split-input" style="padding:0.625rem;">
<option value="">현재 공정 유지</option>
</select>
</div>
<div id="splitSessionPicker" style="display:none;">
<div class="split-field">
<label>이동할 TBM 선택</label>
<div class="split-session-list" id="splitSessionList"></div>
</div>
</div>
</div>
<div class="split-footer">
<button type="button" class="split-btn" id="splitSaveBtn" onclick="saveSplit()">분할 저장</button>
</div>
</div>
<!-- 빼오기 바텀시트 -->
<div id="pullOverlay" class="pull-overlay" onclick="closePullSheet()"></div>
<div id="pullSheet" class="pull-sheet">
<div class="pull-header">
<div style="display:flex; justify-content:space-between; align-items:center;">
<h4 id="pullTitle">팀원 목록</h4>
<button type="button" onclick="closePullSheet()" style="background:none; border:none; font-size:1.125rem; color:#6b7280; cursor:pointer;"></button>
</div>
<p id="pullSubtitle"></p>
</div>
<div id="pullMemberList"></div>
</div>
<!-- 빼오기 시간 입력 모달 -->
<div id="pullHoursOverlay" class="split-overlay" style="z-index:9300;" onclick="closePullHoursModal()"></div>
<div id="pullHoursSheet" class="split-sheet" style="z-index:9301; max-height:60vh;">
<div class="split-header">
<div style="display:flex; justify-content:space-between; align-items:center;">
<h4 id="pullHoursTitle">빼오기</h4>
<button type="button" onclick="closePullHoursModal()" style="background:none; border:none; font-size:1.125rem; color:#6b7280; cursor:pointer;"></button>
</div>
<p id="pullHoursSubtitle">이동할 시간을 입력하세요</p>
</div>
<div class="split-body">
<div class="split-field">
<label>빼올 시간</label>
<input type="number" id="pullHoursInput" class="split-input" step="0.5" min="0.5" placeholder="예: 3">
</div>
<div class="split-field">
<label>내 TBM 프로젝트 (선택)</label>
<select id="pullProjectId" class="split-input" style="padding:0.625rem;">
<option value="">내 TBM 프로젝트 사용</option>
</select>
</div>
<div class="split-field">
<label>내 TBM 공정 (선택)</label>
<select id="pullWorkTypeId" class="split-input" style="padding:0.625rem;">
<option value="">내 TBM 공정 사용</option>
</select>
</div>
</div>
<div class="split-footer">
<button type="button" class="split-btn" id="pullHoursSaveBtn" onclick="confirmPull()">빼오기 실행</button>
</div>
</div>
<script src="/js/tbm/state.js?v=1"></script>
<script src="/js/tbm/utils.js?v=1"></script>
<script src="/js/tbm/api.js?v=1"></script>
<script>
(function() {
'use strict';
var currentTab = 'today';
var allSessions = [];
var todaySessions = [];
var currentUser = null;
var loadedDays = 7;
var esc = window.escapeHtml || function(s) { return s || ''; };
var todayAssignments = []; // 당일 배정 현황
// 세부 편집 상태
var deSessionId = null;
var deSession = null;
var deMembers = [];
var deTasks = [];
var deWpCats = [];
var deWpMap = {}; // category_id -> [workplaces]
var deSelected = {}; // index -> boolean (그룹 선택용)
// 피커 상태
var pickerMode = ''; // 'task' | 'workplace'
var pickerWpStep = 'category'; // 'category' | 'place'
var pickerSelectedCatId = null;
// busy guard - 비동기 함수 중복 호출 방지
var _busy = {};
function isBusy(key) { return !!_busy[key]; }
function setBusy(key) { _busy[key] = true; }
function clearBusy(key) { delete _busy[key]; }
function showLoading(msg) {
var el = document.getElementById('loadingOverlay');
if (el) {
document.getElementById('loadingText').textContent = msg || '불러오는 중...';
el.classList.add('active');
}
}
function hideLoading() {
var el = document.getElementById('loadingOverlay');
if (el) el.classList.remove('active');
}
// 초기화
document.addEventListener('DOMContentLoaded', async function() {
var now = new Date();
var days = ['일','월','화','수','목','금','토'];
var dateEl = document.getElementById('headerDate');
if (dateEl) {
dateEl.textContent = now.getFullYear() + '.' +
String(now.getMonth()+1).padStart(2,'0') + '.' +
String(now.getDate()).padStart(2,'0') + ' (' + days[now.getDay()] + ')';
}
try {
await window.waitForApi(8000);
} catch(e) {
document.getElementById('tbmContent').innerHTML =
'<div class="m-empty"><div class="m-empty-icon">&#9888;</div><div class="m-empty-text">서버 연결에 실패했습니다</div><div class="m-empty-sub">페이지를 새로고침해 주세요</div></div>';
return;
}
currentUser = JSON.parse(localStorage.getItem('sso_user') || '{}');
await loadData();
});
function getTodayStr() {
var now = new Date();
return now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
}
async function loadData() {
try {
var today = new Date();
var todayStr = getTodayStr();
var dates = [];
for (var i = 0; i < loadedDays; i++) {
var d = new Date(today);
d.setDate(d.getDate() - i);
dates.push(d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'));
}
var promises = dates.map(function(date) {
return window.apiCall('/tbm/sessions/date/' + date);
});
var results = await Promise.all(promises);
allSessions = [];
results.forEach(function(res) {
if (res && res.success && res.data) {
allSessions = allSessions.concat(res.data);
}
});
// 당일 세션 = 오늘 날짜만
todaySessions = allSessions.filter(function(s) {
var sDate = s.session_date ? s.session_date.split('T')[0] : '';
return sDate === todayStr;
});
document.getElementById('todayCount').textContent = todaySessions.length;
document.getElementById('allCount').textContent = allSessions.length;
renderList();
} catch (error) {
console.error('TBM 로드 오류:', error);
document.getElementById('tbmContent').innerHTML =
'<div class="m-empty"><div class="m-empty-text">데이터를 불러올 수 없습니다</div></div>';
}
}
window.switchTab = function(tab) {
currentTab = tab;
document.querySelectorAll('.m-tab').forEach(function(el) {
el.classList.toggle('active', el.dataset.tab === tab);
});
renderList();
};
function isMySession(s) {
var userId = currentUser.user_id;
var workerId = currentUser.worker_id;
var userName = currentUser.name;
return s.created_by === userId ||
s.leader_id === workerId ||
s.created_by_name === userName;
}
function renderList() {
var sessions = currentTab === 'today' ? todaySessions : allSessions;
var content = document.getElementById('tbmContent');
if (sessions.length === 0) {
var emptyMsg = currentTab === 'today' ?
'오늘 등록된 TBM이 없습니다' : '등록된 TBM이 없습니다';
content.innerHTML =
'<div class="m-empty">' +
'<div class="m-empty-icon">&#128221;</div>' +
'<div class="m-empty-text">' + emptyMsg + '</div>' +
(currentTab === 'all' ? '<div class="m-empty-sub">최근 ' + loadedDays + '일 기준</div>' : '') +
'</div>';
return;
}
var grouped = {};
sessions.forEach(function(s) {
var date = s.session_date ? s.session_date.split('T')[0] : '';
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) { /* ok */ }
else if (s.session_date) { date = new Date(s.session_date).toISOString().split('T')[0]; }
if (!grouped[date]) grouped[date] = [];
grouped[date].push(s);
});
var sortedDates = Object.keys(grouped).sort().reverse();
var todayStr = getTodayStr();
var yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
var html = '';
sortedDates.forEach(function(date) {
var label = date;
if (date === todayStr) label = '오늘';
else if (date === yesterday) label = '어제';
else {
var parts = date.split('-');
var dayNames = ['일','월','화','수','목','금','토'];
var dObj = new Date(date + 'T00:00:00');
label = parseInt(parts[1]) + '/' + parseInt(parts[2]) + ' (' + dayNames[dObj.getDay()] + ')';
}
html += '<div class="m-date-group"><span class="m-date-label">' + label + '</span></div>';
grouped[date].forEach(function(s) {
var sid = s.session_id;
var status = s.status || 'draft';
var leaderName = s.leader_name || s.created_by_name || '미지정';
var memberCount = (parseInt(s.team_member_count) || 0);
var memberNames = s.team_member_names || '';
var subText = memberNames || '팀원 없음';
var isMine = isMySession(s);
var transferCount = parseInt(s.transfer_count) || 0;
var createdTime = '';
if (s.created_at) {
try {
var t = new Date(s.created_at);
createdTime = String(t.getHours()).padStart(2,'0') + ':' + String(t.getMinutes()).padStart(2,'0');
} catch(e) {}
}
var statusLabel = status === 'completed' ? '완료' : (status === 'cancelled' ? '취소' : '진행');
var badge = '';
if (status === 'draft') {
if (!s.task_id) {
badge = '<span class="m-detail-badge incomplete">세부 미입력</span>';
} else {
badge = '<span class="m-detail-badge complete">입력 완료</span>';
}
}
// 이동 뱃지
var transferBadge = '';
if (transferCount > 0) {
transferBadge = '<span class="m-transfer-badge">' + transferCount + '건 이동</span>';
}
// 당일 탭에서 다른 반장의 draft TBM 클릭 → 빼오기 시트
var clickAction;
if (isMine && status === 'draft') {
clickAction = 'openDetailEditSheet(' + sid + ')';
} else if (!isMine && status === 'draft' && currentTab === 'today') {
clickAction = 'openPullSheet(' + sid + ')';
} else if (status !== 'draft') {
clickAction = 'toggleDetail(' + sid + ')';
} else {
clickAction = 'toggleDetail(' + sid + ')';
}
var myTbmClass = isMine ? ' my-tbm' : '';
var leaderDisplay = isMine ? esc(leaderName) : esc(leaderName) + '<span class="m-leader-badge">' + esc(leaderName) + '</span>';
// 내 세션이면 그냥 이름, 남 세션이면 강조
leaderDisplay = esc(leaderName);
if (!isMine && currentTab === 'today') {
leaderDisplay += '<span class="m-leader-badge">타 반장</span>';
}
html += '<div class="m-tbm-row' + myTbmClass + '" data-sid="' + sid + '" onclick="' + clickAction + '">' +
'<div class="m-row-status ' + status + '" title="' + statusLabel + '"></div>' +
'<div class="m-row-body">' +
'<div class="m-row-main">' + leaderDisplay + badge + transferBadge + '</div>' +
'<div class="m-row-sub">' + esc(subText) + '</div>' +
'</div>' +
'<div class="m-row-right">' +
'<div class="m-row-count">' + memberCount + '<span class="m-row-count-label">명</span></div>' +
(createdTime ? '<div class="m-row-time">' + createdTime + '</div>' : '') +
'</div>' +
'</div>';
if (status !== 'draft') {
var taskName = s.task_name || '';
var workplaceName = s.work_location || '';
html += '<div class="m-tbm-detail" id="detail_' + sid + '">' +
'<div class="m-detail-row"><span class="m-detail-label">상태</span><span class="m-detail-value">' + statusLabel + '</span></div>' +
'<div class="m-detail-row"><span class="m-detail-label">입력자</span><span class="m-detail-value">' + esc(leaderName) + '</span></div>' +
(taskName ? '<div class="m-detail-row"><span class="m-detail-label">작업</span><span class="m-detail-value">' + esc(taskName) + '</span></div>' : '') +
(workplaceName ? '<div class="m-detail-row"><span class="m-detail-label">장소</span><span class="m-detail-value">' + esc(workplaceName) + '</span></div>' : '') +
'<div class="m-detail-row"><span class="m-detail-label">인원</span><span class="m-detail-value">' + esc(memberNames || '없음') + ' (' + memberCount + '명)</span></div>' +
'<div class="m-detail-actions">' +
'<button type="button" class="m-detail-btn" onclick="event.stopPropagation();">상세보기</button>' +
'</div>' +
'</div>';
}
});
});
if (currentTab === 'all') {
html += '<button type="button" class="m-load-more" onclick="loadMore()">이전 기록 더 보기</button>';
}
content.innerHTML = html;
}
window.toggleDetail = function(sid) {
var row = document.querySelector('.m-tbm-row[data-sid="' + sid + '"]');
if (!row) return;
document.querySelectorAll('.m-tbm-row.expanded').forEach(function(el) {
if (el !== row) el.classList.remove('expanded');
});
row.classList.toggle('expanded');
};
window.loadMore = function() {
loadedDays += 7;
loadData();
};
// ─── 세부 편집 바텀시트 ───
window.openDetailEditSheet = async function(sid) {
if (isBusy('detailEdit')) return;
setBusy('detailEdit');
showLoading('불러오는 중...');
deSessionId = sid;
deSelected = {};
try {
var sessionP = window.apiCall('/tbm/sessions/' + sid);
var teamP = window.apiCall('/tbm/sessions/' + sid + '/team');
var tasksP = window.apiCall('/tasks/active/list');
var wpCatsP = window.apiCall('/workplaces/categories/active/list');
var wpP = window.apiCall('/workplaces/active/list');
var results = await Promise.all([sessionP, teamP, tasksP, wpCatsP, wpP]);
deSession = (results[0] && results[0].success) ? results[0].data : null;
deMembers = (results[1] && results[1].success) ? results[1].data : [];
deTasks = (results[2] && results[2].success) ? results[2].data : [];
deWpCats = (results[3] && results[3].success) ? results[3].data : [];
var allWorkplaces = (results[4] && results[4].success) ? results[4].data : [];
if (!deSession) { window.showToast('TBM 정보를 불러올 수 없습니다.', 'error'); return; }
if (deMembers.length === 0) { window.showToast('팀원이 없습니다.', 'error'); return; }
// work_type 필터
var workTypeId = deSession.work_type_id || (deMembers[0] && deMembers[0].work_type_id);
if (workTypeId) {
deTasks = deTasks.filter(function(t) { return t.work_type_id == workTypeId; });
}
// 작업장소 맵 (category_id 기준)
deWpMap = {};
allWorkplaces.forEach(function(wp) {
var catId = wp.category_id || 0;
if (!deWpMap[catId]) deWpMap[catId] = [];
deWpMap[catId].push(wp);
});
renderDetailEditSheet();
document.getElementById('deSelectAll').checked = false;
updateGroupBar();
document.getElementById('detailEditOverlay').style.display = 'block';
document.getElementById('detailEditSheet').style.display = 'block';
} catch(e) {
console.error('세부 편집 로드 오류:', e);
window.showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
} finally {
hideLoading();
clearBusy('detailEdit');
}
};
function renderDetailEditSheet() {
var html = '';
deMembers.forEach(function(m, i) {
var hasBoth = m.task_id && m.workplace_id;
var cardClass = hasBoth ? 'filled' : 'unfilled';
var statusHtml = hasBoth
? '<span class="de-worker-status ok">입력완료</span>'
: '<span class="de-worker-status missing">미입력</span>';
// work_hours 표시
var workHoursTag = '';
if (m.work_hours !== null && m.work_hours !== undefined) {
workHoursTag = '<span class="m-work-hours-tag">' + parseFloat(m.work_hours) + 'h</span>';
}
// 분할 항목이면 프로젝트명 표시
var projectTag = '';
if (m.split_seq > 0 && m.project_name) {
projectTag = '<span style="font-size:0.625rem; background:#dbeafe; color:#1e40af; padding:0.0625rem 0.375rem; border-radius:0.25rem; margin-left:0.25rem;">' + esc(m.project_name) + '</span>';
} else if (m.project_name && m.project_id !== deSession.project_id) {
projectTag = '<span style="font-size:0.625rem; background:#fef3c7; color:#92400e; padding:0.0625rem 0.375rem; border-radius:0.25rem; margin-left:0.25rem;">' + esc(m.project_name) + '</span>';
}
var taskOptions = '<option value="">작업 선택...</option>';
deTasks.forEach(function(t) {
var sel = (m.task_id && m.task_id == t.task_id) ? ' selected' : '';
taskOptions += '<option value="' + t.task_id + '"' + sel + '>' + esc(t.task_name) + '</option>';
});
var currentCatId = m.workplace_category_id || '';
var catOptions = '<option value="">분류...</option>';
deWpCats.forEach(function(c) {
var sel = (currentCatId && currentCatId == c.category_id) ? ' selected' : '';
catOptions += '<option value="' + c.category_id + '"' + sel + '>' + esc(c.category_name) + '</option>';
});
var wpOptions = '<option value="">장소...</option>';
if (currentCatId && deWpMap[currentCatId]) {
deWpMap[currentCatId].forEach(function(wp) {
var sel = (m.workplace_id && m.workplace_id == wp.workplace_id) ? ' selected' : '';
wpOptions += '<option value="' + wp.workplace_id + '"' + sel + '>' + esc(wp.workplace_name) + '</option>';
});
}
html += '<div class="de-worker-card ' + cardClass + '" id="de_card_' + i + '">' +
'<div style="display:flex; align-items:center;">' +
'<input type="checkbox" class="de-worker-check" id="de_check_' + i + '" onchange="onWorkerCheck(' + i + ')">' +
'<span class="de-worker-name">' + esc(m.worker_name) + '</span> ' +
'<span class="de-worker-job">' + esc(m.job_type || '') + '</span>' +
workHoursTag +
projectTag +
statusHtml +
'<button type="button" class="de-split-btn" onclick="event.stopPropagation(); openSplitSheet(' + i + ')">분할</button>' +
'</div>' +
'<div class="de-worker-fields">' +
'<div class="de-field-row">' +
'<span class="de-field-label">작업</span>' +
'<select id="de_task_' + i + '" onchange="updateCardStatus(' + i + ')">' + taskOptions + '</select>' +
'</div>' +
'<div class="de-field-row">' +
'<span class="de-field-label">장소</span>' +
'<select id="de_wpcat_' + i + '" onchange="onDeWpCatChange(' + i + ')">' + catOptions + '</select>' +
'<select id="de_wp_' + i + '" onchange="updateCardStatus(' + i + ')">' + wpOptions + '</select>' +
'</div>' +
'</div>' +
'</div>';
});
document.getElementById('deWorkerList').innerHTML = html;
}
window.updateCardStatus = function(idx) {
var card = document.getElementById('de_card_' + idx);
var taskVal = document.getElementById('de_task_' + idx).value;
var wpVal = document.getElementById('de_wp_' + idx).value;
var statusEl = card.querySelector('.de-worker-status');
if (taskVal && wpVal) {
card.className = 'de-worker-card filled';
statusEl.className = 'de-worker-status ok';
statusEl.textContent = '입력완료';
} else {
card.className = 'de-worker-card unfilled';
statusEl.className = 'de-worker-status missing';
statusEl.textContent = '미입력';
}
};
window.onDeWpCatChange = function(idx) {
var catId = document.getElementById('de_wpcat_' + idx).value;
var wpSel = document.getElementById('de_wp_' + idx);
wpSel.innerHTML = '<option value="">장소...</option>';
if (catId && deWpMap[catId]) {
deWpMap[catId].forEach(function(wp) {
wpSel.innerHTML += '<option value="' + wp.workplace_id + '">' + esc(wp.workplace_name) + '</option>';
});
}
updateCardStatus(idx);
};
// ─── 그룹 선택 ───
window.onWorkerCheck = function(idx) {
deSelected[idx] = document.getElementById('de_check_' + idx).checked;
var allChecked = true;
for (var i = 0; i < deMembers.length; i++) {
if (!deSelected[i]) { allChecked = false; break; }
}
document.getElementById('deSelectAll').checked = allChecked;
updateGroupBar();
};
window.toggleSelectAll = function() {
var checked = document.getElementById('deSelectAll').checked;
for (var i = 0; i < deMembers.length; i++) {
deSelected[i] = checked;
document.getElementById('de_check_' + i).checked = checked;
}
updateGroupBar();
};
function getSelectedIndices() {
var arr = [];
for (var i = 0; i < deMembers.length; i++) {
if (deSelected[i]) arr.push(i);
}
return arr;
}
function updateGroupBar() {
var indices = getSelectedIndices();
var bar = document.getElementById('deGroupBar');
var countEl = document.getElementById('deSelectedCount');
var labelEl = document.getElementById('deGroupLabel');
if (indices.length > 0) {
bar.className = 'de-group-bar visible';
labelEl.textContent = indices.length + '명 선택';
countEl.textContent = indices.length + '명';
} else {
bar.className = 'de-group-bar';
countEl.textContent = '';
}
}
// ─── 피커 (작업/장소 선택 팝업) ───
window.openPicker = function(mode) {
var indices = getSelectedIndices();
if (indices.length === 0) {
window.showToast('작업자를 먼저 선택하세요.', 'error');
return;
}
pickerMode = mode;
pickerWpStep = 'category';
pickerSelectedCatId = null;
if (mode === 'task') {
renderTaskPicker();
} else {
renderWorkplaceCatPicker();
}
document.getElementById('pickerOverlay').style.display = 'block';
document.getElementById('pickerSheet').style.display = 'block';
};
window.closePicker = function() {
document.getElementById('pickerOverlay').style.display = 'none';
document.getElementById('pickerSheet').style.display = 'none';
};
function renderTaskPicker() {
document.getElementById('pickerTitle').textContent = '작업 선택';
var listEl = document.getElementById('pickerList');
var html = '';
deTasks.forEach(function(t) {
html += '<div class="picker-item" onclick="pickTask(' + t.task_id + ')">' +
esc(t.task_name) +
'</div>';
});
if (deTasks.length === 0) {
html = '<div style="padding:1.5rem; text-align:center; color:#9ca3af;">등록된 작업이 없습니다</div>';
}
listEl.innerHTML = html;
// 새 작업 추가 영역
var addRow = document.getElementById('pickerAddRow');
addRow.style.display = 'flex';
document.getElementById('pickerAddInput').placeholder = '새 작업명 입력...';
document.getElementById('pickerAddInput').value = '';
document.getElementById('pickerAddBtn').onclick = function() { addNewTask(); };
}
function renderWorkplaceCatPicker() {
pickerWpStep = 'category';
document.getElementById('pickerTitle').textContent = '장소 분류 선택';
var listEl = document.getElementById('pickerList');
var html = '';
deWpCats.forEach(function(c) {
var count = deWpMap[c.category_id] ? deWpMap[c.category_id].length : 0;
html += '<div class="picker-item" onclick="pickWpCategory(' + c.category_id + ')">' +
esc(c.category_name) +
'<span class="picker-item-sub">' + count + '개 장소</span>' +
'</div>';
});
if (deWpCats.length === 0) {
html = '<div style="padding:1.5rem; text-align:center; color:#9ca3af;">등록된 분류가 없습니다</div>';
}
listEl.innerHTML = html;
document.getElementById('pickerAddRow').style.display = 'none';
}
function renderWorkplacePicker(catId) {
pickerWpStep = 'place';
pickerSelectedCatId = catId;
var catName = '';
deWpCats.forEach(function(c) { if (c.category_id == catId) catName = c.category_name; });
document.getElementById('pickerTitle').textContent = esc(catName) + ' - 장소 선택';
var listEl = document.getElementById('pickerList');
var workplaces = deWpMap[catId] || [];
var html = '<div class="picker-item" style="color:#6b7280;" onclick="renderWorkplaceCatPicker()">&#8592; 분류 다시 선택</div>';
workplaces.forEach(function(wp) {
html += '<div class="picker-item" onclick="pickWorkplace(' + catId + ',' + wp.workplace_id + ')">' +
esc(wp.workplace_name) +
'</div>';
});
if (workplaces.length === 0) {
html += '<div style="padding:1rem; text-align:center; color:#9ca3af;">등록된 장소가 없습니다</div>';
}
listEl.innerHTML = html;
document.getElementById('pickerAddRow').style.display = 'none';
}
window.pickTask = function(taskId) {
var indices = getSelectedIndices();
indices.forEach(function(i) {
document.getElementById('de_task_' + i).value = taskId;
updateCardStatus(i);
});
closePicker();
window.showToast(indices.length + '명에게 작업 적용', 'success');
};
window.pickWpCategory = function(catId) {
renderWorkplacePicker(catId);
};
window.pickWorkplace = function(catId, wpId) {
var indices = getSelectedIndices();
indices.forEach(function(i) {
// 분류 설정
document.getElementById('de_wpcat_' + i).value = catId;
// 장소 옵션 갱신
var wpSel = document.getElementById('de_wp_' + i);
wpSel.innerHTML = '<option value="">장소...</option>';
if (deWpMap[catId]) {
deWpMap[catId].forEach(function(wp) {
wpSel.innerHTML += '<option value="' + wp.workplace_id + '">' + esc(wp.workplace_name) + '</option>';
});
}
wpSel.value = wpId;
updateCardStatus(i);
});
closePicker();
window.showToast(indices.length + '명에게 장소 적용', 'success');
};
// ─── 새 작업/공정 추가 ───
async function addNewTask() {
var name = document.getElementById('pickerAddInput').value.trim();
if (!name) { window.showToast('작업명을 입력하세요.', 'error'); return; }
var workTypeId = deSession.work_type_id || (deMembers[0] && deMembers[0].work_type_id) || null;
try {
var res = await window.apiCall('/tasks', 'POST', {
task_name: name,
work_type_id: workTypeId
});
if (res && res.success) {
var newId = res.data.task_id;
// deTasks에 추가
deTasks.push({ task_id: newId, task_name: name, work_type_id: workTypeId });
// 모든 작업자 드롭다운 갱신
for (var i = 0; i < deMembers.length; i++) {
var sel = document.getElementById('de_task_' + i);
var opt = document.createElement('option');
opt.value = newId;
opt.textContent = name;
sel.appendChild(opt);
}
// 피커 다시 렌더링
renderTaskPicker();
window.showToast('작업 "' + name + '" 추가됨', 'success');
} else {
window.showToast('작업 추가 실패', 'error');
}
} catch(e) {
console.error(e);
window.showToast('오류가 발생했습니다.', 'error');
}
}
window.closeDetailEditSheet = function() {
document.getElementById('detailEditOverlay').style.display = 'none';
document.getElementById('detailEditSheet').style.display = 'none';
clearBusy('detailEdit');
};
// 저장 (부분 입력도 허용)
window.saveDetailEdit = async function() {
var members = [];
for (var i = 0; i < deMembers.length; i++) {
var m = deMembers[i];
var taskId = document.getElementById('de_task_' + i).value || null;
var wpCatId = document.getElementById('de_wpcat_' + i).value || null;
var wpId = document.getElementById('de_wp_' + i).value || null;
members.push({
worker_id: m.worker_id,
project_id: m.project_id || deSession.project_id || null,
work_type_id: m.work_type_id || deSession.work_type_id || null,
task_id: taskId ? parseInt(taskId) : null,
workplace_category_id: wpCatId ? parseInt(wpCatId) : null,
workplace_id: wpId ? parseInt(wpId) : null,
work_detail: m.work_detail || null
});
}
var btn = document.getElementById('deSaveBtn');
btn.disabled = true;
btn.textContent = '저장 중...';
try {
await window.apiCall('/tbm/sessions/' + deSessionId + '/team/clear', 'DELETE');
var res = await window.apiCall('/tbm/sessions/' + deSessionId + '/team/batch', 'POST', { members: members });
if (res && res.success) {
closeDetailEditSheet();
window.showToast('세부 내역이 저장되었습니다.', 'success');
await loadData();
} else {
window.showToast('저장에 실패했습니다.', 'error');
}
} catch(e) {
console.error('세부 편집 저장 오류:', e);
window.showToast('오류가 발생했습니다.', 'error');
} finally {
btn.disabled = false;
btn.textContent = '저장';
}
};
// 완료 (미입력 있으면 차단)
window.completeFromDetailSheet = function() {
var incomplete = [];
for (var i = 0; i < deMembers.length; i++) {
var taskVal = document.getElementById('de_task_' + i).value;
var wpVal = document.getElementById('de_wp_' + i).value;
if (!taskVal || !wpVal) {
incomplete.push(deMembers[i].worker_name);
}
}
if (incomplete.length > 0) {
window.showToast('미입력: ' + incomplete.join(', '), 'error');
return;
}
var sid = deSessionId;
saveDetailEdit().then(function() {
window.completeTbm(sid);
});
};
window.deleteFromDetailSheet = function() {
var sid = deSessionId;
closeDetailEditSheet();
window.deleteTbm(sid);
};
// ─── TBM 완료 바텀시트 ───
var completeSessionId = null;
var completeTeamMembers = [];
window.completeTbm = async function(sid) {
if (isBusy('complete')) return;
setBusy('complete');
showLoading('확인 중...');
completeSessionId = sid;
try {
var teamRes = await window.apiCall('/tbm/sessions/' + sid + '/team');
completeTeamMembers = (teamRes && teamRes.data) ? teamRes.data : [];
if (completeTeamMembers.length === 0) {
window.showToast('팀원이 없습니다.', 'error');
return;
}
// 세부 미입력 작업자 체크
var incomplete = completeTeamMembers.filter(function(m) { return !m.task_id || !m.workplace_id; });
if (incomplete.length > 0) {
var names = incomplete.map(function(m) { return m.worker_name; }).join(', ');
window.showToast('세부 미입력: ' + names + ' - 세부 내역을 먼저 입력하세요.', 'error');
return;
}
renderCompleteSheet();
document.getElementById('completeOverlay').style.display = 'block';
document.getElementById('completeSheet').style.display = 'block';
} catch(e) {
console.error(e);
window.showToast('팀원 조회 중 오류가 발생했습니다.', 'error');
} finally {
hideLoading();
clearBusy('complete');
}
};
function renderCompleteSheet() {
var html = '';
completeTeamMembers.forEach(function(m, i) {
html += '<div style="padding:0.625rem 0; border-bottom:1px solid #f3f4f6;">' +
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.375rem;">' +
'<div><strong style="font-size:0.875rem;">' + esc(m.worker_name) + '</strong> <span style="font-size:0.75rem; color:#6b7280;">(' + esc(m.job_type || '') + ')</span></div>' +
'</div>' +
'<div style="display:flex; gap:0.375rem; align-items:center; flex-wrap:wrap;">' +
'<select id="att_type_' + i + '" onchange="onAttTypeChange(' + i + ')" style="flex:1; min-width:120px; padding:0.5rem; border:1px solid #d1d5db; border-radius:0.375rem; font-size:0.8125rem; background:white;">' +
'<option value="regular">정시근로 (8h)</option>' +
'<option value="overtime">연장근무 (8h+)</option>' +
'<option value="annual">연차 (휴무)</option>' +
'<option value="half">반차 (4h)</option>' +
'<option value="quarter">반반차 (6h)</option>' +
'<option value="early">조퇴</option>' +
'</select>' +
'<input type="number" id="att_hours_' + i + '" placeholder="추가시간" step="0.5" min="0" max="8" style="display:none; width:70px; padding:0.5rem; border:1px solid #d1d5db; border-radius:0.375rem; font-size:0.8125rem; text-align:center;">' +
'<span id="att_hint_' + i + '" style="font-size:0.75rem; color:#6b7280;"></span>' +
'</div>' +
'</div>';
});
document.getElementById('completeWorkerList').innerHTML = html;
}
window.onAttTypeChange = function(idx) {
var sel = document.getElementById('att_type_' + idx);
var inp = document.getElementById('att_hours_' + idx);
var hint = document.getElementById('att_hint_' + idx);
var val = sel.value;
if (val === 'overtime') {
inp.style.display = 'block'; inp.placeholder = '+시간'; inp.value = ''; hint.textContent = '';
} else if (val === 'early') {
inp.style.display = 'block'; inp.placeholder = '근무시간'; inp.value = ''; hint.textContent = '';
} else {
inp.style.display = 'none'; inp.value = '';
var labels = { regular:'8h', annual:'연차 자동처리', half:'4h', quarter:'6h' };
hint.textContent = labels[val] || '';
}
};
window.closeCompleteSheet = function() {
document.getElementById('completeOverlay').style.display = 'none';
document.getElementById('completeSheet').style.display = 'none';
};
window.submitCompleteSheet = async function() {
var attendanceData = [];
for (var i = 0; i < completeTeamMembers.length; i++) {
var type = document.getElementById('att_type_' + i).value;
var hoursVal = document.getElementById('att_hours_' + i).value;
var hours = hoursVal ? parseFloat(hoursVal) : null;
if (type === 'overtime' && (!hours || hours <= 0)) {
window.showToast(esc(completeTeamMembers[i].worker_name) + '의 추가 시간을 입력해주세요.', 'error'); return;
}
if (type === 'early' && (!hours || hours <= 0)) {
window.showToast(esc(completeTeamMembers[i].worker_name) + '의 근무 시간을 입력해주세요.', 'error'); return;
}
attendanceData.push({ worker_id: completeTeamMembers[i].worker_id, attendance_type: type, attendance_hours: hours });
}
var btn = document.getElementById('completeSheetBtn');
btn.disabled = true; btn.textContent = '처리 중...';
try {
var now = new Date();
var endTime = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
var res = await window.apiCall('/tbm/sessions/' + completeSessionId + '/complete', 'POST', {
end_time: endTime, attendance_data: attendanceData
});
if (res && res.success) {
closeCompleteSheet();
window.showToast('TBM이 완료 처리되었습니다.', 'success');
await loadData();
} else {
window.showToast('완료 처리에 실패했습니다.', 'error');
}
} catch(e) {
console.error(e);
window.showToast('오류가 발생했습니다.', 'error');
} finally {
btn.disabled = false; btn.textContent = '완료 처리';
}
};
window.deleteTbm = async function(sid) {
if (!confirm('이 TBM을 삭제하시겠습니까?')) return;
try {
var res = await window.apiCall('/tbm/sessions/' + sid, 'DELETE');
if (res && res.success) {
window.showToast('TBM이 삭제되었습니다.', 'success');
await loadData();
} else {
window.showToast('삭제에 실패했습니다.', 'error');
}
} catch(e) {
window.showToast('오류가 발생했습니다.', 'error');
}
};
// ─── 분할 기능 ───
var splitMemberIdx = null;
var splitOption = 'keep'; // 'keep' | 'send'
var splitTargetSessionId = null;
var cachedProjects = null;
var cachedWorkTypes = null;
// 프로젝트/공정 목록 로딩 (캐시)
async function loadProjectsAndWorkTypes() {
if (!cachedProjects) {
try {
var res = await window.apiCall('/projects?is_active=1');
cachedProjects = Array.isArray(res) ? res : (res && res.data ? res.data : []);
cachedProjects = cachedProjects.filter(function(p) { return p.is_active == 1 || p.is_active === true; });
} catch(e) { cachedProjects = []; }
}
if (!cachedWorkTypes) {
try {
var res2 = await window.apiCall('/daily-work-reports/work-types');
cachedWorkTypes = (res2 && res2.success) ? res2.data : (Array.isArray(res2) ? res2 : []);
} catch(e) { cachedWorkTypes = []; }
}
}
function populateProjectSelect(selectId, currentProjectId) {
var sel = document.getElementById(selectId);
var html = '<option value="">원래 프로젝트 유지</option>';
(cachedProjects || []).forEach(function(p) {
html += '<option value="' + p.project_id + '"' + (p.project_id == currentProjectId ? ' selected' : '') + '>' + esc(p.project_name) + '</option>';
});
sel.innerHTML = html;
}
function populateWorkTypeSelect(selectId, currentWorkTypeId) {
var sel = document.getElementById(selectId);
var html = '<option value="">원래 공정 유지</option>';
(cachedWorkTypes || []).forEach(function(wt) {
html += '<option value="' + wt.id + '"' + (wt.id == currentWorkTypeId ? ' selected' : '') + '>' + esc(wt.name) + '</option>';
});
sel.innerHTML = html;
}
window.openSplitSheet = async function(memberIdx) {
if (isBusy('split')) return;
setBusy('split');
showLoading('불러오는 중...');
splitMemberIdx = memberIdx;
splitOption = 'keep';
splitTargetSessionId = null;
var m = deMembers[memberIdx];
var currentHours = m.work_hours === null || m.work_hours === undefined ? 8 : parseFloat(m.work_hours);
document.getElementById('splitTitle').textContent = esc(m.worker_name) + ' 작업 분할';
document.getElementById('splitSubtitle').textContent = '현재 ' + currentHours + 'h 배정';
document.getElementById('splitHours').value = '';
document.getElementById('splitHours').max = currentHours - 0.5;
document.getElementById('splitRemainder').textContent = '';
document.getElementById('splitOptKeep').className = 'split-radio-item active';
document.getElementById('splitOptSend').className = 'split-radio-item';
document.getElementById('splitSessionPicker').style.display = 'none';
// 시간 입력 시 나머지 자동 계산
document.getElementById('splitHours').oninput = function() {
var val = parseFloat(this.value);
if (val && val > 0 && val < currentHours) {
document.getElementById('splitRemainder').textContent = '나머지: ' + (currentHours - val) + 'h';
} else {
document.getElementById('splitRemainder').textContent = '';
}
};
// 프로젝트/공정 목록 로드 + 드롭다운 채우기
await loadProjectsAndWorkTypes();
populateProjectSelect('splitProjectId', null);
populateWorkTypeSelect('splitWorkTypeId', null);
// 다른 세션 목록 로드 (당일)
loadSplitSessionList();
document.getElementById('splitOverlay').style.display = 'block';
document.getElementById('splitSheet').style.display = 'block';
hideLoading();
clearBusy('split');
};
async function loadSplitSessionList() {
var todayStr = getTodayStr();
try {
var res = await window.apiCall('/tbm/sessions/date/' + todayStr);
if (res && res.success) {
var html = '';
res.data.forEach(function(s) {
if (s.session_id === deSessionId) return; // 현재 세션 제외
if (s.status !== 'draft') return; // draft만
var leaderName = s.leader_name || s.created_by_name || '미지정';
var workType = s.work_type_name || '';
html += '<div class="split-session-item" data-sid="' + s.session_id + '" onclick="selectSplitSession(' + s.session_id + ')">' +
esc(leaderName) + (workType ? ' - ' + esc(workType) : '') +
' <span style="font-size:0.6875rem; color:#9ca3af;">(' + (parseInt(s.team_member_count)||0) + '명)</span>' +
'</div>';
});
if (!html) html = '<div style="padding:0.75rem; font-size:0.8125rem; color:#9ca3af; text-align:center;">다른 TBM이 없습니다</div>';
document.getElementById('splitSessionList').innerHTML = html;
}
} catch(e) {
console.error(e);
}
}
window.setSplitOption = function(opt) {
splitOption = opt;
splitTargetSessionId = null;
document.getElementById('splitOptKeep').className = 'split-radio-item' + (opt === 'keep' ? ' active' : '');
document.getElementById('splitOptSend').className = 'split-radio-item' + (opt === 'send' ? ' active' : '');
document.getElementById('splitSessionPicker').style.display = opt === 'send' ? 'block' : 'none';
// 세션 선택 초기화
document.querySelectorAll('.split-session-item').forEach(function(el) { el.classList.remove('active'); });
};
window.selectSplitSession = function(sid) {
splitTargetSessionId = sid;
document.querySelectorAll('.split-session-item').forEach(function(el) {
el.classList.toggle('active', parseInt(el.dataset.sid) === sid);
});
};
window.closeSplitSheet = function() {
document.getElementById('splitOverlay').style.display = 'none';
document.getElementById('splitSheet').style.display = 'none';
clearBusy('split');
};
window.saveSplit = async function() {
var m = deMembers[splitMemberIdx];
var currentHours = m.work_hours === null || m.work_hours === undefined ? 8 : parseFloat(m.work_hours);
var splitHours = parseFloat(document.getElementById('splitHours').value);
if (!splitHours || splitHours <= 0 || splitHours >= currentHours) {
window.showToast('올바른 시간을 입력하세요 (0 < 시간 < ' + currentHours + ')', 'error');
return;
}
var btn = document.getElementById('splitSaveBtn');
btn.disabled = true;
btn.textContent = '처리 중...';
try {
// 프로젝트/공정 선택값
var selProjectId = document.getElementById('splitProjectId').value;
var selWorkTypeId = document.getElementById('splitWorkTypeId').value;
if (splitOption === 'keep') {
var remainHoursKeep = currentHours - splitHours;
var newProjectId = selProjectId ? parseInt(selProjectId) : (m.project_id || null);
var newWorkTypeId = selWorkTypeId ? parseInt(selWorkTypeId) : (m.work_type_id || null);
// 1) 기존 항목: 시간만 줄이기 (프로젝트/공정 유지)
await window.apiCall('/tbm/sessions/' + deSessionId + '/team', 'POST', {
worker_id: m.worker_id,
project_id: m.project_id || null,
work_type_id: m.work_type_id || null,
task_id: m.task_id || null,
workplace_category_id: m.workplace_category_id || null,
workplace_id: m.workplace_id || null,
work_detail: m.work_detail || null,
is_present: true,
work_hours: splitHours
});
// 2) 나머지 시간으로 새 항목 추가 (프로젝트/공정 변경 가능)
await window.apiCall('/tbm/sessions/' + deSessionId + '/team/split', 'POST', {
worker_id: m.worker_id,
work_hours: remainHoursKeep,
project_id: newProjectId,
work_type_id: newWorkTypeId
});
closeSplitSheet();
// 세부 편집 데이터 다시 로드
var teamRes = await window.apiCall('/tbm/sessions/' + deSessionId + '/team');
deMembers = (teamRes && teamRes.success) ? teamRes.data : deMembers;
renderDetailEditSheet();
window.showToast('분할 완료: ' + splitHours + 'h + ' + remainHoursKeep + 'h', 'success');
} else if (splitOption === 'send') {
if (!splitTargetSessionId) {
window.showToast('이동할 TBM을 선택하세요.', 'error');
btn.disabled = false;
btn.textContent = '분할 저장';
return;
}
var remainHours = currentHours - splitHours;
var destProjectId = selProjectId ? parseInt(selProjectId) : (m.project_id || null);
var destWorkTypeId = selWorkTypeId ? parseInt(selWorkTypeId) : (m.work_type_id || null);
// transfer API 호출
var res = await window.apiCall('/tbm/transfers', 'POST', {
transfer_type: 'send',
worker_id: m.worker_id,
source_session_id: deSessionId,
dest_session_id: splitTargetSessionId,
hours: remainHours,
project_id: destProjectId,
work_type_id: destWorkTypeId
});
if (res && res.success) {
closeSplitSheet();
closeDetailEditSheet();
window.showToast('이동 완료' + (res.data && res.data.warning ? ' (' + res.data.warning + ')' : ''), 'success');
await loadData();
} else {
window.showToast(res?.message || '이동 실패', 'error');
}
}
} catch(e) {
console.error('분할 오류:', e);
window.showToast('오류가 발생했습니다.', 'error');
} finally {
btn.disabled = false;
btn.textContent = '분할 저장';
}
};
// ─── 빼오기 기능 ───
var pullSessionId = null;
var pullMembers = [];
var pullWorker = null; // 빼오기 대상
var myDraftSession = null; // 내 draft TBM
window.openPullSheet = async function(sid) {
if (isBusy('pull')) return;
setBusy('pull');
showLoading('불러오는 중...');
pullSessionId = sid;
try {
var res = await window.apiCall('/tbm/sessions/' + sid + '/team');
pullMembers = (res && res.success) ? res.data : [];
var sessionRes = await window.apiCall('/tbm/sessions/' + sid);
var session = (sessionRes && sessionRes.success) ? sessionRes.data : null;
var leaderName = session ? (session.leader_name || session.created_by_name || '미지정') : '미지정';
document.getElementById('pullTitle').textContent = esc(leaderName) + ' 반장 팀';
document.getElementById('pullSubtitle').textContent = pullMembers.length + '명 배정';
// 내 draft TBM 확인
myDraftSession = todaySessions.find(function(s) {
return isMySession(s) && s.status === 'draft';
});
var html = '';
pullMembers.forEach(function(m) {
var hours = m.work_hours === null || m.work_hours === undefined ? 8 : parseFloat(m.work_hours);
var hoursText = hours + 'h';
// 이미 내 TBM에 있는지 확인
var alreadyInMine = false;
// (추후 체크)
var btnHtml = '';
if (!myDraftSession) {
btnHtml = '<button type="button" class="pull-btn" disabled title="내 TBM이 없음">내 TBM 없음</button>';
} else {
btnHtml = '<button type="button" class="pull-btn" onclick="event.stopPropagation(); startPull(' + m.worker_id + ', \'' + esc(m.worker_name).replace(/'/g, "\\'") + '\', ' + hours + ')">빼오기</button>';
}
html += '<div class="pull-member-item">' +
'<div class="pull-member-info">' +
'<div class="pull-member-name">' + esc(m.worker_name) + ' <span class="m-work-hours-tag">' + hoursText + '</span></div>' +
'<div class="pull-member-sub">' + esc(m.job_type || '') + '</div>' +
'</div>' +
btnHtml +
'</div>';
});
if (pullMembers.length === 0) {
html = '<div style="padding:1.5rem; text-align:center; color:#9ca3af;">팀원이 없습니다</div>';
}
document.getElementById('pullMemberList').innerHTML = html;
document.getElementById('pullOverlay').style.display = 'block';
document.getElementById('pullSheet').style.display = 'block';
} catch(e) {
console.error('빼오기 로드 오류:', e);
window.showToast('데이터를 불러올 수 없습니다.', 'error');
} finally {
hideLoading();
clearBusy('pull');
}
};
window.closePullSheet = function() {
document.getElementById('pullOverlay').style.display = 'none';
document.getElementById('pullSheet').style.display = 'none';
clearBusy('pull');
};
window.startPull = async function(workerId, workerName, maxHours) {
pullWorker = { worker_id: workerId, worker_name: workerName, max_hours: maxHours };
document.getElementById('pullHoursTitle').textContent = esc(workerName) + ' 빼오기';
document.getElementById('pullHoursSubtitle').textContent = '최대 ' + maxHours + 'h 가능';
document.getElementById('pullHoursInput').value = maxHours;
document.getElementById('pullHoursInput').max = maxHours;
// 프로젝트/공정 드롭다운 채우기
await loadProjectsAndWorkTypes();
var myProject = myDraftSession ? myDraftSession.project_id : null;
var myWorkType = myDraftSession ? myDraftSession.work_type_id : null;
populateProjectSelect('pullProjectId', myProject);
populateWorkTypeSelect('pullWorkTypeId', myWorkType);
document.getElementById('pullHoursOverlay').style.display = 'block';
document.getElementById('pullHoursSheet').style.display = 'block';
};
window.closePullHoursModal = function() {
document.getElementById('pullHoursOverlay').style.display = 'none';
document.getElementById('pullHoursSheet').style.display = 'none';
};
window.confirmPull = async function() {
var hours = parseFloat(document.getElementById('pullHoursInput').value);
if (!hours || hours <= 0 || hours > pullWorker.max_hours) {
window.showToast('올바른 시간을 입력하세요 (0 < 시간 <= ' + pullWorker.max_hours + ')', 'error');
return;
}
var btn = document.getElementById('pullHoursSaveBtn');
btn.disabled = true;
btn.textContent = '처리 중...';
try {
var pullProjectId = document.getElementById('pullProjectId').value || null;
var pullWorkTypeId = document.getElementById('pullWorkTypeId').value || null;
var res = await window.apiCall('/tbm/transfers', 'POST', {
transfer_type: 'pull',
worker_id: pullWorker.worker_id,
source_session_id: pullSessionId,
dest_session_id: myDraftSession.session_id,
hours: hours,
project_id: pullProjectId ? parseInt(pullProjectId) : null,
work_type_id: pullWorkTypeId ? parseInt(pullWorkTypeId) : null
});
if (res && res.success) {
closePullHoursModal();
closePullSheet();
window.showToast(esc(pullWorker.worker_name) + ' ' + hours + 'h 빼오기 완료' +
(res.data && res.data.warning ? ' (' + res.data.warning + ')' : ''), 'success');
await loadData();
} else {
window.showToast(res?.message || '빼오기 실패', 'error');
}
} catch(e) {
console.error('빼오기 오류:', e);
window.showToast('오류가 발생했습니다.', 'error');
} finally {
btn.disabled = false;
btn.textContent = '빼오기 실행';
}
};
})();
</script>
</body>
</html>