Files
tk-factory-services/system2-report/web/pages/safety/issue-report.html
Hyungi Ahn 7637be33f3 feat: TBM 모바일 시스템 + 작업 분할/이동 + 권한 통합
TBM 시스템:
- 4단계 워크플로우 (draft→세부편집→완료→작업보고)
- 모바일 전용 TBM 페이지 (tbm-mobile.html) + 3단계 생성 위자드
- 작업자 작업 분할 (work_hours + split_seq)
- 작업자 이동 보내기/빼오기 (tbm_transfers 테이블)
- 생성 시 중복 배정 방지 (당일 배정 현황 조회)
- 데스크탑 TBM 페이지 세부편집 기능 추가

작업보고서:
- 모바일 전용 작업보고서 페이지 (report-create-mobile.html)
- TBM에서 사전 등록된 work_hours 자동 반영

권한 시스템:
- tkuser user_page_permissions 테이블과 system1 페이지 접근 연동
- pageAccessRoutes를 userRoutes보다 먼저 등록 (라우트 우선순위 수정)
- TKUSER_DEFAULT_ACCESS 폴백 추가 (개인→부서→기본값 3단계)
- 권한 캐시키 갱신 (userPageAccess_v2)

기타:
- app-init.js 캐시 버스팅 (v=5)
- iOS Safari touch-action: manipulation 적용
- KST 타임존 날짜 버그 수정 (toISOString UTC 이슈)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 07:46:21 +09:00

690 lines
23 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, maximum-scale=1.0, user-scalable=no">
<title>신고 등록 | (주)테크니컬코리아</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=2" 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;
padding-bottom: env(safe-area-inset-bottom);
-webkit-font-smoothing: antialiased;
}
/* Step indicator */
.step-indicator {
display: flex;
justify-content: center;
align-items: center;
padding: 0.75rem 0.5rem;
gap: 0;
background: white;
border-bottom: 1px solid #e5e7eb;
}
.step {
display: flex;
align-items: center;
gap: 0.125rem;
font-size: 0.625rem;
color: #9ca3af;
}
.step .step-dot {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.625rem;
font-weight: 700;
background: #e5e7eb;
color: #9ca3af;
flex-shrink: 0;
}
.step.active .step-dot { background: #ef4444; color: white; }
.step.active { color: #ef4444; font-weight: 600; }
.step.completed .step-dot { background: #10b981; color: white; }
.step.completed { color: #10b981; }
.step-line {
width: 16px;
height: 2px;
background: #e5e7eb;
margin: 0 0.125rem;
flex-shrink: 0;
}
/* Sections */
.report-section {
margin: 0.75rem;
background: white;
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.section-title {
font-size: 0.9375rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-title .sn {
width: 22px;
height: 22px;
border-radius: 50%;
background: #ef4444;
color: white;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
flex-shrink: 0;
}
/* Type buttons */
.type-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.625rem;
}
.type-btn {
padding: 1.125rem 0.5rem;
border: 2px solid #e5e7eb;
border-radius: 0.75rem;
background: white;
cursor: pointer;
text-align: center;
transition: all 0.15s;
-webkit-tap-highlight-color: transparent;
}
.type-btn:active { transform: scale(0.97); }
.type-btn .type-icon { font-size: 1.75rem; margin-bottom: 0.5rem; }
.type-btn .type-label { font-size: 0.8125rem; font-weight: 600; color: #374151; }
.type-btn.selected { border-color: #ef4444; background: #fef2f2; }
.type-btn.selected .type-label { color: #dc2626; }
/* Map */
.factory-select {
width: 100%;
padding: 0.625rem 0.875rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
margin-bottom: 0.75rem;
background: white;
-webkit-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%236b7280' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2rem;
}
.map-container {
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
margin-bottom: 0.75rem;
}
.map-container canvas { width: 100%; display: block; }
.location-info {
padding: 0.75rem;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 0.5rem;
font-size: 0.8125rem;
color: #166534;
margin-bottom: 0.75rem;
line-height: 1.5;
}
.location-info.empty {
background: #f9fafb;
border-color: #e5e7eb;
color: #9ca3af;
}
.custom-location-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8125rem;
color: #6b7280;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.custom-location-toggle input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: #ef4444;
}
#customLocationInput {
display: none;
margin-top: 0.5rem;
}
#customLocationInput.visible { display: block; }
#customLocationInput input {
width: 100%;
padding: 0.625rem 0.875rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
}
#customLocationInput input:focus {
outline: none;
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239,68,68,0.1);
}
/* Project/Work selection - Accordion */
.project-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.project-group {
border: 1.5px solid #e5e7eb;
border-radius: 0.75rem;
overflow: hidden;
}
.project-group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1rem;
background: #f9fafb;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.project-group-header:active { background: #f3f4f6; }
.project-group-header .group-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.project-group-header .group-icon {
font-size: 1.125rem;
flex-shrink: 0;
}
.project-group-header .group-title {
font-size: 0.875rem;
font-weight: 600;
color: #1f2937;
}
.project-group-header .group-count {
font-size: 0.6875rem;
background: #e5e7eb;
color: #6b7280;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-weight: 600;
}
.project-group-header .group-arrow {
font-size: 0.75rem;
color: #9ca3af;
transition: transform 0.2s;
}
.project-group.open .group-arrow { transform: rotate(180deg); }
.project-group.tbm-group { border-color: #93c5fd; }
.project-group.tbm-group .project-group-header { background: #eff6ff; }
.project-group.tbm-group .group-count { background: #dbeafe; color: #1d4ed8; }
.project-group-body {
display: none;
border-top: 1px solid #e5e7eb;
}
.project-group.open .project-group-body { display: block; }
.project-card {
padding: 0.875rem 1rem;
cursor: pointer;
transition: background 0.12s;
-webkit-tap-highlight-color: transparent;
border-bottom: 1px solid #f3f4f6;
}
.project-card:last-child { border-bottom: none; }
.project-card:active { background: #f9fafb; }
.project-card.selected { background: #f5f3ff; }
.project-card-title {
font-size: 0.8125rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 0.125rem;
}
.project-card-desc {
font-size: 0.75rem;
color: #6b7280;
}
.project-card .tbm-info {
font-size: 0.6875rem;
color: #2563eb;
margin-top: 0.25rem;
}
/* 프로젝트 모름 */
.project-skip {
padding: 0.875rem 1rem;
border: 1.5px dashed #d1d5db;
border-radius: 0.75rem;
text-align: center;
font-size: 0.8125rem;
color: #6b7280;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.project-skip:active { background: #f9fafb; }
.project-skip.selected {
border-color: #8b5cf6;
border-style: solid;
background: #f5f3ff;
color: #7c3aed;
font-weight: 600;
}
.project-empty {
text-align: center;
padding: 1.5rem 1rem;
color: #9ca3af;
font-size: 0.8125rem;
}
/* 선택된 항목 표시 */
.project-group-header .group-selected {
font-size: 0.6875rem;
color: #7c3aed;
font-weight: 600;
margin-left: 0.25rem;
}
/* Category & Item */
#categoryContainer { display: none; }
.subsection-title {
font-size: 0.8125rem;
font-weight: 600;
color: #6b7280;
margin-bottom: 0.5rem;
}
.category-grid, #itemGrid {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.category-btn, .item-btn {
padding: 0.5rem 0.875rem;
border: 1.5px solid #d1d5db;
border-radius: 2rem;
background: white;
font-size: 0.8125rem;
cursor: pointer;
transition: all 0.12s;
-webkit-tap-highlight-color: transparent;
white-space: nowrap;
}
.category-btn:active, .item-btn:active { transform: scale(0.97); }
.category-btn.selected {
border-color: #ef4444;
background: #fef2f2;
color: #dc2626;
font-weight: 600;
}
.item-btn.selected {
border-color: #2563eb;
background: #eff6ff;
color: #1d4ed8;
font-weight: 600;
}
.item-btn.custom-input-btn {
border-style: dashed;
color: #6b7280;
}
.item-section {
margin-top: 1rem;
padding-top: 0.875rem;
border-top: 1px solid #f3f4f6;
}
#customItemInput {
display: none;
margin-top: 0.75rem;
gap: 0.5rem;
align-items: center;
}
#customItemInput input {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
}
#customItemInput input:focus {
outline: none;
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239,68,68,0.1);
}
#customItemInput button {
padding: 0.5rem 0.75rem;
border: none;
border-radius: 0.5rem;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.custom-confirm { background: #2563eb; color: white; }
.custom-cancel { background: #f3f4f6; color: #374151; }
/* Photo & Details */
.photo-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.photo-slot {
aspect-ratio: 1;
border: 2px dashed #d1d5db;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
overflow: hidden;
background: #f9fafb;
-webkit-tap-highlight-color: transparent;
}
.photo-slot .add-icon { font-size: 1.25rem; color: #9ca3af; }
.photo-slot.has-photo { border-style: solid; border-color: #10b981; }
.photo-slot.has-photo .add-icon { display: none; }
.photo-slot img { width: 100%; height: 100%; object-fit: cover; }
.photo-slot .remove-btn {
display: none;
position: absolute;
top: 2px;
right: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(0,0,0,0.6);
color: white;
border: none;
font-size: 0.625rem;
cursor: pointer;
align-items: center;
justify-content: center;
z-index: 2;
line-height: 1;
}
.photo-slot.has-photo .remove-btn { display: flex; }
.photo-hint {
font-size: 0.75rem;
color: #9ca3af;
margin-bottom: 0.75rem;
}
.description-textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.description-textarea:focus {
outline: none;
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239,68,68,0.1);
}
/* Submit */
.submit-section {
padding: 0.75rem;
padding-bottom: calc(1rem + env(safe-area-inset-bottom));
}
#submitBtn {
width: 100%;
padding: 0.9375rem;
background: #ef4444;
color: white;
border: none;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: background 0.2s;
-webkit-tap-highlight-color: transparent;
}
#submitBtn:disabled { background: #d1d5db; cursor: not-allowed; }
#submitBtn:not(:disabled):active { background: #dc2626; }
/* 가로모드 전체화면 지도 오버레이 */
.landscape-overlay {
position: fixed;
inset: 0;
z-index: 10000;
background: rgba(0, 0, 0, 0.95);
display: flex;
align-items: center;
justify-content: center;
}
.landscape-inner {
display: flex;
flex-direction: column;
background: #fff;
overflow: hidden;
}
.landscape-inner.rotated {
width: 100vh;
height: 100vw;
transform: translate(-50%, -50%) rotate(90deg);
position: absolute;
top: 50%;
left: 50%;
}
.landscape-inner.no-rotate {
width: 100vw;
height: 100vh;
}
.landscape-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
flex-shrink: 0;
}
.landscape-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
}
.landscape-close-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.2);
color: white;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
}
.landscape-canvas-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #f1f5f9;
padding: 0.5rem;
}
.landscape-canvas-wrap canvas {
max-width: 100%;
max-height: 100%;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
}
.landscape-trigger-btn {
display: none;
}
@media (max-width: 768px) {
.landscape-trigger-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
color: white;
border: none;
border-radius: 8px;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
margin-top: 0.5rem;
-webkit-tap-highlight-color: transparent;
}
.landscape-trigger-btn:active {
transform: scale(0.97);
opacity: 0.85;
}
}
/* Responsive */
@media (min-width: 480px) {
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
}
@media (max-width: 360px) {
.type-grid { gap: 0.5rem; }
.type-btn { padding: 0.875rem 0.375rem; }
.type-btn .type-icon { font-size: 1.5rem; }
.photo-grid { gap: 0.375rem; }
}
</style>
</head>
<body>
<!-- Step Indicator (5 steps) -->
<div class="step-indicator">
<div class="step active"><span class="step-dot">1</span><span>유형</span></div>
<div class="step-line"></div>
<div class="step"><span class="step-dot">2</span><span>위치</span></div>
<div class="step-line"></div>
<div class="step"><span class="step-dot">3</span><span>작업</span></div>
<div class="step-line"></div>
<div class="step"><span class="step-dot">4</span><span>항목</span></div>
<div class="step-line"></div>
<div class="step"><span class="step-dot">5</span><span>사진</span></div>
</div>
<!-- Step 1: Type Selection -->
<div class="report-section">
<div class="section-title"><span class="sn">1</span>신고 유형</div>
<div class="type-grid">
<button type="button" class="type-btn" data-type="nonconformity">
<div class="type-icon">&#128270;</div>
<div class="type-label">부적합</div>
</button>
<button type="button" class="type-btn" data-type="facility">
<div class="type-icon">&#128295;</div>
<div class="type-label">시설설비</div>
</button>
<button type="button" class="type-btn" data-type="safety">
<div class="type-icon">&#9888;&#65039;</div>
<div class="type-label">안전</div>
</button>
</div>
</div>
<!-- Step 2: Location -->
<div class="report-section">
<div class="section-title"><span class="sn">2</span>위치 선택</div>
<select id="factorySelect" class="factory-select">
<option value="">공장 선택</option>
</select>
<div class="map-container">
<canvas id="issueMapCanvas"></canvas>
</div>
<button type="button" class="landscape-trigger-btn" id="landscapeTriggerBtn" onclick="openLandscapeMap()" style="display:none;">
&#128250; 전체화면 지도로 선택
</button>
<div id="selectedLocationInfo" class="location-info empty">
지도에서 작업장을 클릭하여 위치를 선택하세요
</div>
<label class="custom-location-toggle">
<input type="checkbox" id="useCustomLocation">
기타 위치 직접 입력
</label>
<div id="customLocationInput">
<input type="text" id="customLocation" placeholder="위치를 입력하세요">
</div>
</div>
<!-- Step 3: Project/Work Selection -->
<div class="report-section" id="projectContainer">
<div class="section-title"><span class="sn">3</span>프로젝트/작업 선택</div>
<div id="projectList" class="project-list">
<div class="project-empty">위치를 먼저 선택하세요</div>
</div>
</div>
<!-- Step 4: Category & Item -->
<div class="report-section" id="categoryContainer">
<div class="section-title"><span class="sn">4</span>세부 항목</div>
<div class="subsection-title">카테고리</div>
<div class="category-grid" id="categoryGrid"></div>
<div class="item-section">
<div class="subsection-title">항목</div>
<div id="itemGrid"></div>
<div id="customItemInput">
<input type="text" id="customItemName" placeholder="항목명을 입력하세요">
<button type="button" class="custom-confirm" onclick="confirmCustomItem()">확인</button>
<button type="button" class="custom-cancel" onclick="cancelCustomItem()">취소</button>
</div>
</div>
</div>
<!-- Step 5: Photos + Description -->
<div class="report-section">
<div class="section-title"><span class="sn">5</span>사진 및 상세</div>
<div class="photo-grid">
<div class="photo-slot" data-index="0"><span class="add-icon">+</span><button class="remove-btn">&times;</button></div>
<div class="photo-slot" data-index="1"><span class="add-icon">+</span><button class="remove-btn">&times;</button></div>
<div class="photo-slot" data-index="2"><span class="add-icon">+</span><button class="remove-btn">&times;</button></div>
<div class="photo-slot" data-index="3"><span class="add-icon">+</span><button class="remove-btn">&times;</button></div>
<div class="photo-slot" data-index="4"><span class="add-icon">+</span><button class="remove-btn">&times;</button></div>
</div>
<div class="photo-hint">* 사진 1장 이상 필수 (최대 5장, 카메라 촬영 또는 앨범에서 선택)</div>
<textarea id="additionalDescription" class="description-textarea" placeholder="추가 설명을 입력하세요 (선택사항)"></textarea>
</div>
<!-- Submit -->
<div class="submit-section">
<button type="button" id="submitBtn" disabled onclick="submitReport()">신고 제출</button>
</div>
<!-- 가로모드 전체화면 지도 오버레이 -->
<div id="landscapeOverlay" class="landscape-overlay" style="display:none;">
<div id="landscapeInner" class="landscape-inner">
<div class="landscape-header">
<h3>&#127981; 작업장 선택</h3>
<button type="button" class="landscape-close-btn" onclick="closeLandscapeMap()">×</button>
</div>
<div class="landscape-canvas-wrap">
<canvas id="landscapeCanvas"></canvas>
</div>
</div>
</div>
<!-- Hidden file input for camera/gallery -->
<input type="file" id="photoInput" accept="image/*" style="display:none">
<script src="/js/issue-report.js?v=6"></script>
</body>
</html>