Files
tk-factory-services/system1-factory/web/pages/admin/workplaces.html
Hyungi Ahn 7fd646e9ba feat: 실시간 알림 시스템 (Web Push + 알림 벨 + 서비스간 알림 연동)
- Phase 1: 모든 서비스 헤더에 알림 벨 UI 추가 (notification-bell.js)
- Phase 2: VAPID Web Push 구독/전송 (push-sw.js, pushSubscription API)
- Phase 3: 내부 알림 API + notifyHelper로 서비스간 알림 연동
  - tksafety: 출입 승인/반려, 안전교육 완료, 방문자 체크인
  - tkpurchase: 일용공 신청, 작업보고서 제출
  - system2-report: 신고 접수/확인/처리완료
- Phase 4: 30일 이상 알림 자동 정리 cron, Redis 캐싱
- CORS에 tkuser/tkpurchase/tksafety 서브도메인 추가
- HTML cache busting 버전 갱신

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:01:44 +09:00

446 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>작업장 관리 - TK 공장관리</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="/static/css/tkfb.css">
<link rel="stylesheet" href="/css/workplace-management.css?v=7">
</head>
<body class="bg-gray-50">
<header class="bg-orange-700 text-white sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-14">
<div class="flex items-center gap-3">
<button id="mobileMenuBtn" class="lg:hidden text-orange-200 hover:text-white"><i class="fas fa-bars text-xl"></i></button>
<i class="fas fa-industry text-xl text-orange-200"></i>
<h1 class="text-lg font-semibold">TK 공장관리</h1>
</div>
<div class="flex items-center gap-4">
<span id="headerUserName" class="text-sm hidden sm:block">-</span>
<div id="headerUserAvatar" class="w-8 h-8 bg-orange-600 rounded-full flex items-center justify-center text-sm font-bold">-</div>
<button onclick="doLogout()" class="text-orange-200 hover:text-white" title="로그아웃"><i class="fas fa-sign-out-alt"></i></button>
</div>
</div>
</div>
</header>
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 fade-in">
<div class="flex gap-6">
<nav id="sideNav" class="hidden lg:flex flex-col gap-1 w-52 flex-shrink-0 pt-2 fixed lg:static z-40 bg-white lg:bg-transparent p-4 lg:p-0 rounded-lg lg:rounded-none shadow-lg lg:shadow-none top-14 left-0 bottom-0 overflow-y-auto"></nav>
<div class="flex-1 min-w-0">
<div class="wp-content">
<!-- 페이지 헤더 -->
<div class="wp-page-header">
<div class="wp-header-content">
<h1 class="wp-page-title">
<span class="wp-page-title-icon">🏭</span>
작업장 관리
</h1>
<p class="wp-page-description">공장 및 작업장을 등록하고 설비 위치를 지도에서 관리합니다</p>
</div>
<div class="wp-header-actions">
<button class="wp-btn wp-btn-primary" onclick="openCategoryModal()">
<span class="wp-btn-icon">🏢</span>
공장 추가
</button>
<button class="wp-btn wp-btn-primary" onclick="openWorkplaceModal()">
<span class="wp-btn-icon">📍</span>
작업장 추가
</button>
<button class="wp-btn wp-btn-secondary" onclick="refreshWorkplaces()">
<span class="wp-btn-icon">🔄</span>
새로고침
</button>
</div>
</div>
<!-- 통계 카드 -->
<div class="wp-stats-row" id="statsRow">
<div class="wp-stat-card">
<div class="wp-stat-icon factory">🏢</div>
<div class="wp-stat-content">
<h3 id="factoryCount">0</h3>
<p>공장</p>
</div>
</div>
<div class="wp-stat-card">
<div class="wp-stat-icon workplace">📍</div>
<div class="wp-stat-content">
<h3 id="totalCount">0</h3>
<p>전체 작업장</p>
</div>
</div>
<div class="wp-stat-card">
<div class="wp-stat-icon active"></div>
<div class="wp-stat-content">
<h3 id="activeCount">0</h3>
<p>활성 작업장</p>
</div>
</div>
<div class="wp-stat-card">
<div class="wp-stat-icon equipment">⚙️</div>
<div class="wp-stat-content">
<h3 id="equipmentCount">0</h3>
<p>등록된 설비</p>
</div>
</div>
</div>
<!-- 공장(카테고리) 탭 -->
<div class="wp-factory-tabs" id="categoryTabs">
<button class="wp-tab-btn active" data-category="" onclick="switchCategory('')">
<span class="wp-tab-icon">🏗️</span>
전체
<span class="wp-tab-count" id="tabAllCount">0</span>
</button>
<!-- 공장 탭들이 여기에 동적으로 생성됩니다 -->
</div>
<!-- 공장 레이아웃 지도 관리 섹션 (카테고리가 선택된 경우에만 표시) -->
<div class="wp-layout-section" id="layoutMapSection" style="display: none;">
<div class="wp-layout-header">
<h2 class="wp-layout-title">
<span class="wp-layout-title-icon">🗺️</span>
<span id="selectedCategoryName"></span> 레이아웃 지도
</h2>
<button class="wp-btn wp-btn-primary" onclick="openLayoutMapModal()">
<span class="wp-btn-icon">⚙️</span>
지도 설정
</button>
</div>
<div class="wp-layout-body">
<div id="layoutMapPreview" class="wp-layout-preview">
<div class="wp-layout-empty">
<div class="wp-layout-empty-icon">🗺️</div>
<p>레이아웃 이미지가 아직 등록되지 않았습니다</p>
</div>
</div>
</div>
</div>
<!-- 작업장 목록 -->
<div class="wp-workplace-section">
<div class="wp-section-header">
<h2 class="wp-section-title">
<span>📋</span>
작업장 목록
</h2>
<div class="wp-section-stats">
<span class="wp-section-stat">전체 <strong id="sectionTotalCount">0</strong></span>
<span class="wp-section-stat">활성 <strong id="sectionActiveCount">0</strong></span>
</div>
</div>
<div class="wp-section-body">
<div class="wp-grid" id="workplaceGrid">
<!-- 작업장 카드들이 여기에 동적으로 생성됩니다 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 공장(카테고리) 추가/수정 모달 -->
<div id="categoryModal" class="wp-modal-overlay" style="display: none;">
<div class="wp-modal">
<div class="wp-modal-header">
<h2 class="wp-modal-title">
<span>🏢</span>
<span id="categoryModalTitle">공장 추가</span>
</h2>
<button class="wp-modal-close" onclick="closeCategoryModal()">×</button>
</div>
<div class="wp-modal-body">
<form id="categoryForm" onsubmit="event.preventDefault(); saveCategory();">
<input type="hidden" id="categoryId">
<div class="wp-form-group">
<label class="wp-form-label required">공장명</label>
<input type="text" id="categoryName" class="wp-form-control" placeholder="예: 제 1공장" required>
</div>
<div class="wp-form-group">
<label class="wp-form-label">설명</label>
<textarea id="categoryDescription" class="wp-form-control" rows="3" placeholder="공장에 대한 설명을 입력하세요"></textarea>
</div>
<div class="wp-form-group">
<label class="wp-form-label">표시 순서</label>
<input type="number" id="categoryOrder" class="wp-form-control" value="0" min="0">
<span class="wp-form-help">숫자가 작을수록 먼저 표시됩니다</span>
</div>
</form>
</div>
<div class="wp-modal-footer">
<button type="button" class="wp-btn wp-btn-outline" onclick="closeCategoryModal()">취소</button>
<button type="button" class="wp-btn wp-btn-danger" id="deleteCategoryBtn" onclick="deleteCategory()" style="display: none;">삭제</button>
<button type="button" class="wp-btn wp-btn-primary" onclick="saveCategory()">저장</button>
</div>
</div>
</div>
<!-- 작업장 추가/수정 모달 -->
<div id="workplaceModal" class="wp-modal-overlay" style="display: none;">
<div class="wp-modal">
<div class="wp-modal-header">
<h2 class="wp-modal-title">
<span>📍</span>
<span id="workplaceModalTitle">작업장 추가</span>
</h2>
<button class="wp-modal-close" onclick="closeWorkplaceModal()">×</button>
</div>
<div class="wp-modal-body">
<form id="workplaceForm" onsubmit="event.preventDefault(); saveWorkplace();">
<input type="hidden" id="workplaceId">
<div class="wp-form-group">
<label class="wp-form-label">소속 공장</label>
<select id="workplaceCategoryId" class="wp-form-control">
<option value="">공장 선택</option>
</select>
</div>
<div class="wp-form-group">
<label class="wp-form-label required">작업장명</label>
<input type="text" id="workplaceName" class="wp-form-control" placeholder="예: 서스작업장, 조립구역" required>
</div>
<div class="wp-form-group">
<label class="wp-form-label">작업장 용도</label>
<select id="workplacePurpose" class="wp-form-control">
<option value="">선택 안 함</option>
<option value="작업구역">작업구역</option>
<option value="설비">설비</option>
<option value="휴게시설">휴게시설</option>
<option value="회의실">회의실</option>
<option value="창고">창고</option>
<option value="기타">기타</option>
</select>
<span class="wp-form-help">작업장의 주요 용도를 선택하세요</span>
</div>
<div class="wp-form-group">
<label class="wp-form-label">표시 순서</label>
<input type="number" id="displayPriority" class="wp-form-control" value="0" min="0">
<span class="wp-form-help">숫자가 작을수록 먼저 표시됩니다</span>
</div>
<div class="wp-form-group">
<label class="wp-form-label">설명</label>
<textarea id="workplaceDescription" class="wp-form-control" rows="3" placeholder="작업장에 대한 설명을 입력하세요"></textarea>
</div>
</form>
</div>
<div class="wp-modal-footer">
<button type="button" class="wp-btn wp-btn-outline" onclick="closeWorkplaceModal()">취소</button>
<button type="button" class="wp-btn wp-btn-danger" id="deleteWorkplaceBtn" onclick="deleteWorkplace()" style="display: none;">삭제</button>
<button type="button" class="wp-btn wp-btn-primary" onclick="saveWorkplace()">저장</button>
</div>
</div>
</div>
<!-- 작업장 지도 관리 모달 (간단 모드) -->
<div id="workplaceMapModal" class="wp-modal-overlay" style="display: none;">
<div class="wp-modal" style="max-width: 600px;">
<div class="wp-modal-header">
<h2 class="wp-modal-title">
<span>🗺️</span>
<span id="workplaceMapModalTitle">작업장 지도 관리</span>
</h2>
<button class="wp-modal-close" onclick="closeWorkplaceMapModal()">×</button>
</div>
<div class="wp-modal-body" style="padding: 24px;">
<!-- 이미지 업로드 섹션 -->
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin-bottom: 20px;">
<h3 style="font-size: 15px; font-weight: 600; margin-bottom: 12px; color: #334155;">📷 작업장 레이아웃 이미지</h3>
<div class="wp-form-group">
<div id="workplaceLayoutPreview" style="background: white; border: 2px dashed #cbd5e1; padding: 20px; border-radius: 8px; text-align: center; min-height: 120px;">
<span style="color: #94a3b8;">업로드된 이미지가 없습니다</span>
</div>
</div>
<div style="display: flex; gap: 8px; align-items: center; margin-top: 12px;">
<input type="file" id="workplaceLayoutFile" accept="image/*" class="wp-form-control" style="flex: 1;" onchange="previewWorkplaceLayoutImage(event)">
<button type="button" class="wp-btn wp-btn-primary" onclick="uploadWorkplaceLayout()">업로드</button>
</div>
<span class="wp-form-help" style="margin-top: 8px; display: block;">JPG, PNG, GIF 형식 지원 (최대 5MB)</span>
</div>
<!-- 설비 배치 버튼 -->
<div style="background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); border-radius: 12px; padding: 24px; text-align: center;">
<div style="font-size: 48px; margin-bottom: 12px;">⚙️</div>
<h3 style="color: white; font-size: 18px; font-weight: 600; margin-bottom: 8px;">설비 위치 편집</h3>
<p style="color: rgba(255,255,255,0.8); font-size: 14px; margin-bottom: 16px;">
전체 화면에서 설비 위치를 쉽게 지정할 수 있습니다
</p>
<button type="button" class="wp-btn" style="background: white; color: #1d4ed8; font-weight: 600; padding: 12px 32px; font-size: 15px;" onclick="openFullscreenEquipmentEditor()">
🖥️ 전체화면 편집 열기
</button>
</div>
<!-- 등록된 설비 요약 -->
<div style="margin-top: 20px; background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4 style="font-size: 14px; font-weight: 600; color: #334155; margin: 0;">📋 등록된 설비</h4>
<span id="workplaceEquipmentCount" style="font-size: 12px; color: #64748b;">0개</span>
</div>
<div id="workplaceEquipmentList" style="max-height: 150px; overflow-y: auto;">
<p style="color: #94a3b8; text-align: center; padding: 12px; font-size: 13px;">아직 정의된 설비가 없습니다</p>
</div>
</div>
</div>
<div class="wp-modal-footer">
<button type="button" class="wp-btn wp-btn-outline" onclick="closeWorkplaceMapModal()">닫기</button>
</div>
</div>
</div>
<!-- 전체화면 설비 배치 편집기 -->
<div id="fullscreenEquipmentEditor" class="fullscreen-editor" style="display: none;">
<div class="fullscreen-editor-header">
<div class="fullscreen-editor-title">
<span>⚙️</span>
<span id="fullscreenEditorTitle">설비 위치 편집</span>
</div>
<div class="fullscreen-editor-actions">
<button type="button" class="editor-btn editor-btn-secondary" onclick="toggleEditorSidebar()">
<span id="sidebarToggleIcon"></span> 패널
</button>
<button type="button" class="editor-btn editor-btn-primary" onclick="closeFullscreenEditor()">
✕ 닫기
</button>
</div>
</div>
<div class="fullscreen-editor-body">
<!-- 메인 캔버스 영역 -->
<div class="fullscreen-canvas-area" id="fullscreenCanvasArea">
<div class="canvas-toolbar">
<span class="toolbar-info">🖱️ 드래그로 영역 선택</span>
<span class="toolbar-zoom" id="canvasZoomInfo">100%</span>
</div>
<div class="canvas-wrapper" id="fullscreenCanvasWrapper">
<canvas id="fullscreenRegionCanvas"></canvas>
</div>
<div class="canvas-help">
<span>💡 마우스로 드래그하여 설비 영역을 지정한 후, 오른쪽 패널에서 설비를 선택하고 저장하세요</span>
</div>
</div>
<!-- 사이드바 패널 -->
<div class="fullscreen-sidebar" id="fullscreenSidebar">
<!-- 설비 선택 -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<h4>🔧 설비 선택</h4>
<span id="fsAvailableEquipmentCount" class="badge badge-success">0개</span>
</div>
<div class="sidebar-section-body">
<select id="fsEquipmentSelect" class="wp-form-control" onchange="fsToggleNewEquipmentFields()">
<option value="">-- 기존 설비 선택 --</option>
</select>
<p class="form-help">이미 배치된 설비는 목록에 표시되지 않습니다</p>
<div id="fsNewEquipmentFields" class="new-equipment-box">
<label>또는 새 설비 등록</label>
<input type="text" id="fsEquipmentCode" class="wp-form-control" placeholder="설비 코드">
<input type="text" id="fsEquipmentName" class="wp-form-control" placeholder="설비명">
</div>
<div class="button-group">
<button type="button" class="wp-btn wp-btn-outline" onclick="fsClearCurrentRegion()">영역 지우기</button>
<button type="button" class="wp-btn wp-btn-primary" onclick="fsSaveEquipmentRegion()">저장</button>
</div>
</div>
</div>
<!-- 등록된 설비 목록 -->
<div class="sidebar-section sidebar-section-flex">
<div class="sidebar-section-header">
<h4>📋 등록된 설비</h4>
<span id="fsRegisteredCount" class="badge">0개</span>
</div>
<div class="sidebar-section-body sidebar-list" id="fsEquipmentList">
<p class="empty-message">등록된 설비가 없습니다</p>
</div>
</div>
</div>
</div>
</div>
<!-- 레이아웃 지도 설정 모달 -->
<div id="layoutMapModal" class="wp-modal-overlay" style="display: none;">
<div class="wp-modal" style="max-width: 90vw; max-height: 90vh; width: 1000px;">
<div class="wp-modal-header">
<h2 class="wp-modal-title">
<span>🗺️</span>
공장 레이아웃 지도 설정
</h2>
<button class="wp-modal-close" onclick="closeLayoutMapModal()">×</button>
</div>
<div class="wp-modal-body" style="overflow-y: auto;">
<!-- Step 1: 이미지 업로드 -->
<div style="border-bottom: 2px solid #e5e7eb; padding-bottom: 20px; margin-bottom: 20px;">
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 12px;">Step 1. 공장 레이아웃 이미지 업로드</h3>
<div class="wp-form-group">
<label class="wp-form-label">현재 이미지</label>
<div id="currentLayoutImage" style="background: #f9fafb; border: 2px dashed #cbd5e1; padding: 20px; border-radius: 8px; text-align: center;">
<span style="color: #94a3b8;">업로드된 이미지가 없습니다</span>
</div>
</div>
<div class="wp-form-group">
<label class="wp-form-label">새 이미지 업로드</label>
<input type="file" id="layoutImageFile" accept="image/*" class="wp-form-control" onchange="previewLayoutImage(event)">
<span class="wp-form-help">JPG, PNG, GIF 형식 지원 (최대 5MB)</span>
</div>
<button type="button" class="wp-btn wp-btn-primary" onclick="uploadLayoutImage()">이미지 업로드</button>
</div>
<!-- Step 2: 작업장 영역 정의 -->
<div>
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 12px;">Step 2. 작업장 영역 정의</h3>
<p style="color: #64748b; font-size: 14px; margin-bottom: 16px;">
이미지 위에 마우스로 드래그하여 각 작업장의 위치를 지정하세요
</p>
<!-- 영역 그리기 캔버스 -->
<div style="position: relative; display: inline-block; margin-bottom: 20px;" id="canvasContainer">
<canvas id="regionCanvas" style="border: 2px solid #cbd5e1; cursor: crosshair; max-width: 100%;"></canvas>
</div>
<!-- 작업장 선택 및 영역 목록 -->
<div class="wp-form-group">
<label class="wp-form-label">작업장 선택</label>
<select id="regionWorkplaceSelect" class="wp-form-control" style="margin-bottom: 12px;">
<option value="">작업장을 선택하세요</option>
</select>
<div style="display: flex; gap: 8px;">
<button type="button" class="wp-btn wp-btn-outline" onclick="clearCurrentRegion()">현재 영역 지우기</button>
<button type="button" class="wp-btn wp-btn-primary" onclick="saveRegion()">선택 영역 저장</button>
</div>
</div>
<!-- 정의된 영역 목록 -->
<div class="wp-form-group">
<label class="wp-form-label">정의된 영역 목록</label>
<div id="regionList" style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; min-height: 100px;">
<!-- 영역 목록이 여기에 표시됩니다 -->
</div>
</div>
</div>
</div>
<div class="wp-modal-footer">
<button type="button" class="wp-btn wp-btn-outline" onclick="closeLayoutMapModal()">닫기</button>
</div>
</div>
</div>
<!-- 작업장 관리 모듈 (리팩토링된 구조) -->
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/workplace-management/state.js?v=1"></script>
<script src="/js/workplace-management/utils.js?v=1"></script>
<script src="/js/workplace-management/api.js?v=1"></script>
<script src="/js/workplace-management/index.js?v=1"></script>
<!-- 기존 UI 로직 (점진적 마이그레이션) -->
<script type="module" src="/js/workplace-management.js?v=9"></script>
<script type="module" src="/js/workplace-layout-map.js?v=2"></script>
<script>initAuth();</script>
</body>
</html>