Files
TK-FB-Project/fastapi-bridge/static/Minutes/safety.html

1635 lines
66 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>안전회의록 작성</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Malgun Gothic', sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #d4653a 0%, #b8956a 100%);
color: white;
padding: 30px;
text-align: center;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgba(255,255,255,0.05) 10px,
rgba(255,255,255,0.05) 20px
);
animation: headerPattern 20s linear infinite;
}
@keyframes headerPattern {
0% { transform: translateX(-100px) translateY(-100px); }
100% { transform: translateX(0) translateY(0); }
}
.header h1 {
font-size: 2.2em;
margin-bottom: 10px;
font-weight: 600;
position: relative;
z-index: 1;
}
.header p {
position: relative;
z-index: 1;
}
.main-content {
padding: 30px;
}
.section-selector {
display: flex;
margin-bottom: 30px;
background: #f8f9fa;
border-radius: 8px;
padding: 8px;
gap: 8px;
}
.section-btn {
flex: 1;
padding: 12px 20px;
border: none;
background: transparent;
color: #6c757d;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.section-btn.active {
background: linear-gradient(135deg, #d4653a 0%, #b8956a 100%);
color: white;
box-shadow: 0 2px 8px rgba(212, 101, 58, 0.3);
}
.section-content {
display: none;
}
.section-content.active {
display: block;
}
.form-section {
background: #fff;
}
.meeting-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 15px;
align-items: center;
margin-bottom: 15px;
}
label {
font-weight: 600;
color: #555;
text-align: right;
padding-right: 10px;
}
input, textarea, select {
width: 100%;
padding: 10px;
border: 2px solid #e1e1e1;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s ease;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #d4653a;
box-shadow: 0 0 0 3px rgba(212, 101, 58, 0.15);
}
.section {
margin-bottom: 30px;
border: 1px solid #e1e1e1;
border-radius: 8px;
overflow: hidden;
}
.section-title {
background: linear-gradient(135deg, #faf7f4 0%, #f9f5f1 100%);
border-bottom: 3px solid #d4653a;
padding: 15px 20px;
font-weight: 600;
color: #495057;
display: flex;
justify-content: space-between;
align-items: center;
}
.section-body {
padding: 20px;
}
.agenda-items {
margin-top: 20px;
}
.agenda-item {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 15px;
overflow: hidden;
}
.agenda-header {
background: linear-gradient(135deg, #f5f1ed 0%, #f7f3ef 100%);
border-bottom: 2px solid #d4653a;
padding: 10px 15px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.agenda-content {
padding: 15px;
}
.agenda-fields {
display: grid;
gap: 15px;
}
.field-row {
display: grid;
grid-template-columns: 100px 1fr;
gap: 10px;
align-items: start;
}
.field-label {
font-weight: 600;
color: #555;
padding-top: 8px;
}
.btn {
background: linear-gradient(135deg, #d4653a 0%, #b8956a 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(212, 101, 58, 0.3);
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
margin-right: 10px;
}
.btn-danger {
background: linear-gradient(135deg, #dc3545 0%, #bd2130 100%);
padding: 8px 16px;
font-size: 12px;
}
.btn-warning {
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
color: #212529;
padding: 6px 12px;
font-size: 12px;
}
.btn-success {
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
padding: 6px 12px;
font-size: 12px;
}
.btn-info {
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
padding: 6px 12px;
font-size: 12px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
margin: 2px;
}
.data-controls {
background: linear-gradient(135deg, #faf7f4 0%, #f9f5f1 100%);
border: 2px solid #d4653a;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.actions {
text-align: center;
padding: 30px;
background: #f8f9fa;
border-top: 1px solid #e1e1e1;
}
.output {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e1e1e1;
}
.output h3 {
margin-bottom: 15px;
color: #495057;
}
.output pre {
background: white;
padding: 15px;
border-radius: 5px;
border: 1px solid #dee2e6;
font-size: 12px;
overflow-x: auto;
white-space: pre-wrap;
}
/* 인수인계 스타일 */
.handover-table {
background: white;
border-radius: 8px;
overflow-x: auto;
border: 1px solid #dee2e6;
}
.handover-group {
border-bottom: 1px solid #f0f0f0;
}
.handover-group:last-child {
border-bottom: none;
}
.handover-category {
background: linear-gradient(135deg, #faf7f4 0%, #f9f5f1 100%);
border-bottom: 2px solid #d4653a;
padding: 15px 20px;
font-weight: 600;
color: #495057;
}
.handover-items {
padding: 0;
}
.handover-item {
padding: 20px;
border-bottom: 1px solid #f8f9fa;
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
align-items: start;
}
.handover-item:last-child {
border-bottom: none;
}
.item-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.item-task {
font-size: 16px;
font-weight: 600;
color: #495057;
line-height: 1.4;
}
.item-details {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.item-detail {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #6c757d;
}
.current-assignee {
background: linear-gradient(135deg, #d4653a 0%, #b8956a 100%);
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
}
.handover-control {
display: flex;
flex-direction: column;
gap: 12px;
}
.new-assignee-input {
padding: 10px;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 14px;
}
.new-assignee-input:focus {
border-color: #d4653a;
outline: none;
box-shadow: 0 0 0 3px rgba(212, 101, 58, 0.15);
}
/* 기존 회의록 스타일 */
.past-item {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
position: relative;
}
.past-item.completed {
background: #f8f9fa;
opacity: 0.7;
}
.past-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.past-item-meta {
font-size: 12px;
color: #6c757d;
display: flex;
align-items: center;
gap: 10px;
}
.past-item-actions {
display: flex;
gap: 8px;
}
.past-item h5 {
color: #495057;
margin-bottom: 12px;
font-size: 16px;
line-height: 1.4;
}
.past-item-content {
font-size: 14px;
line-height: 1.4;
margin-bottom: 12px;
}
.past-item-deadline {
background: linear-gradient(135deg, #fef7e0 0%, #fdf2d9 100%);
border: 2px solid #d4653a;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
color: #8b5a2b;
margin-top: 10px;
font-weight: 600;
}
.filter-controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-input {
flex: 1;
min-width: 200px;
padding: 8px;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 14px;
}
.reference-stats {
background: linear-gradient(135deg, #faf7f4 0%, #f9f5f1 100%);
border: 2px solid #d4653a;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
color: #a0522d;
font-weight: 600;
}
/* 모달 스타일 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 10px;
padding: 30px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #dee2e6;
}
.modal-header h3 {
color: #495057;
margin-bottom: 5px;
}
.modal-body {
margin-bottom: 20px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 15px;
border-top: 1px solid #dee2e6;
}
@media (max-width: 768px) {
.handover-item {
grid-template-columns: 1fr;
gap: 15px;
}
.item-details {
flex-direction: column;
gap: 8px;
}
.form-row, .field-row {
grid-template-columns: 1fr;
gap: 5px;
}
label, .field-label {
text-align: left;
padding-right: 0;
}
.meeting-info {
grid-template-columns: 1fr;
}
.section-selector {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏭 월간 안전회의록</h1>
<p>테크니컬코리아 안전회의록 작성 및 관리 시스템</p>
</div>
<div class="main-content">
<!-- 섹션 선택 버튼 -->
<div class="section-selector">
<button class="section-btn active" onclick="showSection('handover')">📝 업무 인수인계</button>
<button class="section-btn" onclick="showSection('past')">📋 기존 회의록</button>
<button class="section-btn" onclick="showSection('inspection')">🔍 정기점검</button>
<button class="section-btn" onclick="showSection('meeting')">✏️ 회의록 작성</button>
</div>
<!-- 업무 인수인계 섹션 -->
<div id="handover-section" class="section-content active">
<div class="section">
<div class="section-title">
하주현 선임 → 업무 인수인계
<span style="font-size: 14px; color: #6c757d;">출산/육아휴직 대비</span>
</div>
<div class="section-body">
<div class="data-controls">
<button class="btn btn-info" onclick="resetHandoverData()">🔄 인수인계 초기화</button>
<button class="btn btn-secondary" onclick="exportHandoverData()">📁 인수인계 데이터 내보내기</button>
<input type="file" id="handoverFileInput" accept=".json" style="display: none;" onchange="importHandoverData(event)">
<button class="btn btn-secondary" onclick="document.getElementById('handoverFileInput').click()">📂 인수인계 데이터 가져오기</button>
</div>
<div id="handoverContainer" class="handover-table">
<!-- 인수인계 항목들이 여기에 표시됩니다 -->
</div>
</div>
</div>
</div>
<!-- 기존 회의록 섹션 -->
<div id="past-section" class="section-content">
<div class="section">
<div class="section-title">기존 회의록 참조 (미완료 항목)</div>
<div class="section-body">
<div class="data-controls">
<button class="btn btn-info" onclick="resetMeetingData()">🔄 회의록 데이터 초기화</button>
<button class="btn btn-secondary" onclick="exportMeetingData()">📁 회의록 데이터 내보내기</button>
<input type="file" id="meetingFileInput" accept=".json" style="display: none;" onchange="importMeetingData(event)">
<button class="btn btn-secondary" onclick="document.getElementById('meetingFileInput').click()">📂 회의록 데이터 가져오기</button>
<button class="btn btn-warning" onclick="showCompletedItems()">✅ 완료된 항목 보기</button>
</div>
<div class="reference-stats">
<strong>미완료 항목 <span id="totalPastItems">0</span></strong> |
<span id="filteredPastItems">0</span>개 표시됨 |
<strong>완료된 항목 <span id="completedPastItems">0</span></strong>
</div>
<div class="filter-controls">
<input type="text" id="pastSearchFilter" class="filter-input" placeholder="검색어 입력" onkeyup="filterPastItems()">
<select id="pastStatusFilter" class="filter-input" style="flex: none; width: 150px;" onchange="filterPastItems()">
<option value="">전체 상태</option>
<option value="진행중">진행중</option>
<option value="보류">보류</option>
<option value="미결">미결</option>
<option value="완료">완료</option>
</select>
</div>
<div id="pastItems">
<!-- 기존 회의록 항목들이 여기에 표시됩니다 -->
</div>
</div>
</div>
</div>
<!-- 정기점검 섹션 -->
<div id="inspection-section" class="section-content">
<div class="section">
<div class="section-title">정기점검 사항</div>
<div class="section-body">
<div class="data-controls">
<button class="btn btn-info" onclick="resetInspectionData()">🔄 점검 데이터 초기화</button>
<button class="btn btn-secondary" onclick="exportInspectionData()">📁 점검 데이터 내보내기</button>
<input type="file" id="inspectionFileInput" accept=".json" style="display: none;" onchange="importInspectionData(event)">
<button class="btn btn-secondary" onclick="document.getElementById('inspectionFileInput').click()">📂 점검 데이터 가져오기</button>
</div>
<div class="reference-stats">
<strong>정기점검 사항</strong> |
<span id="urgentCount">0</span>개 긴급,
<span id="pendingCount">0</span>개 예정
</div>
<div class="filter-controls">
<input type="text" id="inspectionSearchFilter" class="filter-input" placeholder="점검 항목 검색" onkeyup="filterInspectionItems()">
<select id="inspectionStatusFilter" class="filter-input" style="flex: none; width: 150px;" onchange="filterInspectionItems()">
<option value="">전체</option>
<option value="완료">완료</option>
<option value="예정">예정</option>
<option value="진행">진행</option>
</select>
</div>
<div id="inspectionItems">
<!-- 정기점검 항목들이 여기에 표시됩니다 -->
</div>
</div>
</div>
</div>
<!-- 회의록 작성 섹션 -->
<div id="meeting-section" class="section-content">
<!-- 회의 기본정보 -->
<div class="section">
<div class="section-title">회의 기본정보</div>
<div class="section-body">
<div class="meeting-info">
<div class="form-row">
<label>회의년도:</label>
<input type="number" id="meetingYear" value="2025" min="2020" max="2030">
</div>
<div class="form-row">
<label>회의월:</label>
<select id="meetingMonth">
<option value="">선택</option>
<option value="1">1월</option>
<option value="2">2월</option>
<option value="3">3월</option>
<option value="4">4월</option>
<option value="5">5월</option>
<option value="6">6월</option>
<option value="7">7월</option>
<option value="8">8월</option>
<option value="9">9월</option>
<option value="10">10월</option>
<option value="11">11월</option>
<option value="12">12월</option>
</select>
</div>
<div class="form-row">
<label>회의일자:</label>
<input type="date" id="meetingDate">
</div>
<div class="form-row">
<label>참석자:</label>
<input type="text" id="attendees" placeholder="참석자 명단">
</div>
</div>
</div>
</div>
<!-- 논의사항 -->
<div class="section">
<div class="section-title">
논의사항
<button class="btn btn-secondary" onclick="addAgendaItem()">+ 항목 추가</button>
</div>
<div class="section-body">
<div id="agendaItems" class="agenda-items">
<!-- 논의사항 항목들이 여기에 추가됩니다 -->
</div>
</div>
</div>
<div class="actions">
<button class="btn" onclick="generateOutput()">회의록 생성</button>
<button class="btn btn-secondary" onclick="clearForm()">양식 초기화</button>
<button class="btn btn-secondary" onclick="saveTemplate()">템플릿 저장</button>
</div>
<div id="output" class="output" style="display: none;">
<h3>생성된 회의록</h3>
<pre id="outputContent"></pre>
<button class="btn" onclick="copyToClipboard()">클립보드에 복사</button>
</div>
</div>
</div>
</div>
<!-- 편집 모달 -->
<div id="editModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>회의 안건 수정</h3>
<p style="color: #6c757d; font-size: 14px;">내용을 수정하고 확인 버튼을 눌러주세요.</p>
</div>
<div class="modal-body">
<div class="form-row">
<label>논의사항:</label>
<input type="text" id="editTopic" style="grid-column: 1 / -1;">
</div>
<div class="form-row">
<label>해결방법:</label>
<textarea id="editSolution" rows="3" style="grid-column: 1 / -1;"></textarea>
</div>
<div class="form-row">
<label>담당자:</label>
<input type="text" id="editAssignee" style="grid-column: 1 / -1;">
</div>
<div class="form-row">
<label>상세내용:</label>
<textarea id="editDetails" rows="2" style="grid-column: 1 / -1;"></textarea>
</div>
<div class="form-row">
<label>마감일자:</label>
<input type="date" id="editDeadline" style="grid-column: 1 / -1;">
</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeEditModal()">취소</button>
<button class="btn" onclick="confirmEdit()">확인</button>
</div>
</div>
</div>
<script>
let agendaCounter = 0;
let existingItems = [];
let handoverData = [];
let inspectionData = [];
let currentEditIndex = -1;
let showingCompleted = false;
// 로컬 스토리지 키
const STORAGE_KEYS = {
existingItems: 'safety_meeting_existing_items',
handoverData: 'safety_meeting_handover_data',
inspectionData: 'safety_meeting_inspection_data'
};
// 데이터 저장 함수들
function saveToStorage(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (e) {
console.error('저장 실패:', e);
alert('데이터 저장에 실패했습니다.');
}
}
function loadFromStorage(key, defaultData = []) {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultData;
} catch (e) {
console.error('로드 실패:', e);
return defaultData;
}
}
// 현재 날짜를 기본값으로 설정
document.getElementById('meetingDate').value = new Date().toISOString().split('T')[0];
document.getElementById('meetingMonth').value = new Date().getMonth() + 1;
// 섹션 전환
function showSection(sectionName) {
// 모든 버튼과 섹션 비활성화
document.querySelectorAll('.section-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.section-content').forEach(content => content.classList.remove('active'));
// 선택된 섹션 활성화
event.target.classList.add('active');
document.getElementById(`${sectionName}-section`).classList.add('active');
}
// 데이터 초기화
function initializeData() {
// 저장된 데이터 로드 또는 기본 데이터 사용
existingItems = loadFromStorage(STORAGE_KEYS.existingItems, getDefaultExistingItems());
handoverData = loadFromStorage(STORAGE_KEYS.handoverData, getDefaultHandoverData());
inspectionData = loadFromStorage(STORAGE_KEYS.inspectionData, getDefaultInspectionData());
displayPastItems();
displayHandoverItems();
displayInspectionItems();
}
// 기본 데이터 함수들
function getDefaultHandoverData() {
return [
{
category: "안전회의",
items: [
{ task: "일정 공유", method: "메일", frequency: "1회/매달", assignee: "하주현" },
{ task: "안전회의록 작성 및 지난달 안전회의록 인쇄 및 서명", method: "-", frequency: "1회/매달", assignee: "하주현" },
{ task: "내용 공유", method: "메일", frequency: "1회/매달", assignee: "하주현" },
{ task: "자료 저장 (서버: [공융 드라이브] - [테크니컬코리아] - [07.안전회의])", method: "서버", frequency: "1회/매달", assignee: "하주현" }
]
},
{
category: "동절기 전열기 관리",
items: [
{ task: "담당자 지정 및 전열기 전원 확인", method: "-", frequency: "매주/동절기", assignee: "하주현" }
]
},
{
category: "소화기 점검",
items: [
{ task: "점검일지 작성", method: "-", frequency: "1회/매달", assignee: "신민기 (=신상균)" },
{ task: "점검일지 자료 저장 (서버: [공융 드라이브] - [테크니컬코리아] - [02.구매물류팀] - [소방안전 박창원,하주현] - [소화기 점검일지])", method: "서버", frequency: "1회/매달", assignee: "하주현" }
]
},
{
category: "건강검진",
items: [
{ task: "제휴 병원 (화성디에스) 건강검진 버스 일정 확인", method: "", frequency: "1회/매년", assignee: "이예린" },
{ task: "일정 공유", method: "메일", frequency: "1회/매년", assignee: "하주현" },
{ task: "건강검진 실시확인서 (직장제출용) 수집", method: "메일", frequency: "1회/매년", assignee: "하주현" },
{ task: "건강검진 실시확인서 (직장제출용) 보관", method: "", frequency: "1회/매년", assignee: "안현기" }
]
},
{
category: "법정의무교육",
items: [
{ task: "일정 공유", method: "메일", frequency: "1회 / 상,하반기", assignee: "이예린" },
{ task: "교육 미이수자 Follow-up", method: "메일", frequency: "1회 / 상,하반기", assignee: "하주현" }
]
}
];
}
function getDefaultInspectionData() {
return [
{ no: 1, item: "소화기 안전 점검 (한달에 한번)", frequency: "1회/ 1개월", status: "확인 완료", assignee: "신상균" },
{ no: 2, item: "소방 관련 센서 점검", frequency: "1회/ 1년", status: "완료 확인 필요", assignee: "임영규" },
{ no: 5, item: "전기 관련 Panel 전기 점검", frequency: "1회 / 3년", status: "25년도 6월 이내 예정", assignee: "박창원" },
{ no: 9, item: "SUS작업장 호이스트 정기 점검", frequency: "1회 / 2년", status: "25년 진행 예정", assignee: "안경희" },
{ no: 10, item: "작업 환경 측정 Vendor", frequency: "1회/ 6개월", status: "25년 진행 예정", assignee: "박창원" },
{ no: 15, item: "안전관리감독자 교육", frequency: "1회/ 1년", status: "25년 진행 예정", assignee: "김두수" },
{ no: 20, item: "사내 보안 SK", frequency: "상시", status: "25년 진행 예정", assignee: "안경희" },
{ no: 21, item: "중대재해법", frequency: "상시", status: "매월 4~10월 진행 예정", assignee: "안경희" }
];
}
function getDefaultExistingItems() {
return [
// 3월
{ month: "3월", no: 1, topic: "산업안전공단 클린사업장 사업 (불꽃감지기) 진행", solution: "", assignee: "안현기", details: "2025-05-29 확인사항 : 확인 중 (공지 안뜸)" },
{ month: "3월", no: 5, topic: "가스실 밸브 가스켓 교체", solution: "목표일자 : 4월 이내", assignee: "안경희", details: "2025-05-29 확인사항 : 6월 이내 완료 예정" },
// 4월
{ month: "4월", no: 1, topic: "안전현수막 설치 예정", solution: "공장동 : 5개 (안전 4개, 품질 1개), 2공장 : 2개 (안전 1개, 품질 1개)", assignee: "안현기", details: "2025-05-29 확인사항 : 6월 이내 설치 예정" },
// 5월
{ month: "5월", no: 1, topic: "지게자 면허증 갱신 절차 확인", solution: "대상 : 취득 후 5년 이상, 교육 방식 : 온라인 or 오프라인, 교육 일정 : 주말만 가능", assignee: "안현기", details: "" },
{ month: "5월", no: 2, topic: "장마철 대비 공장 등 건물 상태 확인", solution: "예상 장마철 일자 : 6월 중순 ~ 7월 중순, 대상 : 사무동, 공장동 천장, 누수관 등", assignee: "김권호", details: "" },
{ month: "5월", no: 4, topic: "파상풍 주사 2차 접종", solution: "대상자 : 신규입사인원 (AMT, 생산팀, 품질팀), 기타 희망자", assignee: "안현기", details: "" }
];
}
// 데이터 리셋 함수들
function resetHandoverData() {
if (confirm('인수인계 데이터를 초기값으로 리셋하시겠습니까?')) {
handoverData = getDefaultHandoverData();
saveToStorage(STORAGE_KEYS.handoverData, handoverData);
displayHandoverItems();
alert('인수인계 데이터가 초기화되었습니다.');
}
}
function resetMeetingData() {
if (confirm('회의록 데이터를 초기값으로 리셋하시겠습니까?')) {
existingItems = getDefaultExistingItems();
saveToStorage(STORAGE_KEYS.existingItems, existingItems);
displayPastItems();
alert('회의록 데이터가 초기화되었습니다.');
}
}
function resetInspectionData() {
if (confirm('정기점검 데이터를 초기값으로 리셋하시겠습니까?')) {
inspectionData = getDefaultInspectionData();
saveToStorage(STORAGE_KEYS.inspectionData, inspectionData);
displayInspectionItems();
alert('정기점검 데이터가 초기화되었습니다.');
}
}
// 데이터 내보내기/가져오기 함수들
function exportHandoverData() {
const dataStr = JSON.stringify(handoverData, null, 2);
downloadJSON(dataStr, '인수인계_데이터.json');
}
function exportMeetingData() {
const dataStr = JSON.stringify(existingItems, null, 2);
downloadJSON(dataStr, '회의록_데이터.json');
}
function exportInspectionData() {
const dataStr = JSON.stringify(inspectionData, null, 2);
downloadJSON(dataStr, '정기점검_데이터.json');
}
function downloadJSON(dataStr, filename) {
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert(`${filename}이 다운로드되었습니다.`);
}
function importHandoverData(event) {
importJSONData(event, (data) => {
handoverData = data;
saveToStorage(STORAGE_KEYS.handoverData, handoverData);
displayHandoverItems();
alert('인수인계 데이터가 가져와졌습니다.');
});
}
function importMeetingData(event) {
importJSONData(event, (data) => {
existingItems = data;
saveToStorage(STORAGE_KEYS.existingItems, existingItems);
displayPastItems();
alert('회의록 데이터가 가져와졌습니다.');
});
}
function importInspectionData(event) {
importJSONData(event, (data) => {
inspectionData = data;
saveToStorage(STORAGE_KEYS.inspectionData, inspectionData);
displayInspectionItems();
alert('정기점검 데이터가 가져와졌습니다.');
});
}
function importJSONData(event, callback) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
callback(data);
} catch (error) {
alert('파일 형식이 잘못되었습니다.');
}
};
reader.readAsText(file);
// 파일 입력 리셋
event.target.value = '';
}
// 인수인계 항목 표시 (개선된 테이블 형태)
function displayHandoverItems() {
const container = document.getElementById('handoverContainer');
if (!handoverData || !container) return;
container.innerHTML = '';
handoverData.forEach((category, categoryIndex) => {
const groupDiv = document.createElement('div');
groupDiv.className = 'handover-group';
const categoryDiv = document.createElement('div');
categoryDiv.className = 'handover-category';
categoryDiv.textContent = category.category;
const itemsDiv = document.createElement('div');
itemsDiv.className = 'handover-items';
category.items.forEach((item, itemIndex) => {
const itemDiv = document.createElement('div');
itemDiv.className = 'handover-item';
itemDiv.innerHTML = `
<div class="item-info">
<div class="item-task">${item.task}</div>
<div class="item-details">
<div class="item-detail">
<span>📅</span>
<span>${item.frequency}</span>
</div>
<div class="item-detail">
<span>👤</span>
<span class="current-assignee">${item.assignee}</span>
</div>
${item.method ? `
<div class="item-detail">
<span>🔧</span>
<span>${item.method}</span>
</div>
` : ''}
</div>
<div style="margin-top: 12px;">
<label style="font-size: 13px; color: #6c757d; margin-bottom: 8px; display: block;">👥 신규 담당자</label>
<input type="text" id="handover-${categoryIndex}-${itemIndex}" class="new-assignee-input" placeholder="새 담당자명을 입력하세요">
</div>
</div>
<div class="handover-control">
<button class="btn btn-success" onclick="processHandover('${category.category}', ${itemIndex}, ${categoryIndex})">
✅ 인계 확인
</button>
</div>
`;
itemsDiv.appendChild(itemDiv);
});
groupDiv.appendChild(categoryDiv);
groupDiv.appendChild(itemsDiv);
container.appendChild(groupDiv);
});
}
// 인수인계 처리 함수
function processHandover(categoryName, itemIndex, categoryIndex) {
const inputId = `handover-${categoryIndex}-${itemIndex}`;
const newAssignee = document.getElementById(inputId).value.trim();
if (!newAssignee) {
alert('새 담당자명을 입력해주세요.');
return;
}
const category = handoverData.find(cat => cat.category === categoryName);
const item = category.items[itemIndex];
const oldAssignee = item.assignee;
if (confirm(`${categoryName} 업무를 ${oldAssignee}${newAssignee}로 인계하시겠습니까?`)) {
// 담당자 업데이트
item.assignee = newAssignee;
// 저장
saveToStorage(STORAGE_KEYS.handoverData, handoverData);
alert(`${categoryName} 업무 인계 완료!\n${oldAssignee}${newAssignee}`);
// 화면 새로고침
displayHandoverItems();
}
}
// 완료된 항목 보기/숨기기
function showCompletedItems() {
showingCompleted = !showingCompleted;
const button = event.target;
if (showingCompleted) {
button.textContent = '📝 미완료 항목 보기';
button.className = 'btn btn-info';
} else {
button.textContent = '✅ 완료된 항목 보기';
button.className = 'btn btn-warning';
}
displayPastItems();
}
// 기존 회의록 항목 표시 (미완료 항목만 표시)
function displayPastItems() {
const pastItems = document.getElementById('pastItems');
const totalItems = document.getElementById('totalPastItems');
const filteredItems = document.getElementById('filteredPastItems');
const completedItems = document.getElementById('completedPastItems');
if (!existingItems || !pastItems) return;
// 완료/미완료 항목 분리
const completedItemsList = existingItems.filter(item => {
const status = getStatusFromItem(item);
return status === '완료' || item.details.includes('완료');
});
const incompleteItems = existingItems.filter(item => {
const status = getStatusFromItem(item);
return status !== '완료' && !item.details.includes('완료');
});
// 표시할 항목 결정
const itemsToShow = showingCompleted ? completedItemsList : incompleteItems;
totalItems.textContent = incompleteItems.length;
completedItems.textContent = completedItemsList.length;
filteredItems.textContent = itemsToShow.length;
pastItems.innerHTML = '';
if (itemsToShow.length === 0) {
const message = showingCompleted ?
'완료된 항목이 없습니다.' :
'모든 과거 안건이 완료되었습니다! 새로운 안건으로 회의를 진행하세요.';
pastItems.innerHTML = `
<div style="text-align: center; padding: 60px; color: #6c757d;">
<div style="font-size: 64px; margin-bottom: 20px;">${showingCompleted ? '📝' : '🎉'}</div>
<div style="font-size: 18px; font-weight: 600; margin-bottom: 10px;">${message}</div>
</div>
`;
return;
}
itemsToShow.forEach((item, index) => {
const itemDiv = document.createElement('div');
itemDiv.className = showingCompleted ? 'past-item completed' : 'past-item';
itemDiv.setAttribute('data-topic', item.topic.toLowerCase());
itemDiv.setAttribute('data-assignee', (item.assignee || '').toLowerCase());
itemDiv.setAttribute('data-solution', (item.solution || '').toLowerCase());
itemDiv.setAttribute('data-status', getStatusFromItem(item));
let statusBadge = '';
const status = getStatusFromItem(item);
if (status) {
let statusColor;
switch(status) {
case '완료': statusColor = '#28a745'; break;
case '진행중': statusColor = '#007bff'; break;
case '보류': statusColor = '#ffc107'; break;
default: statusColor = '#dc3545'; break;
}
statusBadge = `<span style="background: ${statusColor}; color: white; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500;">${status}</span>`;
} else {
statusBadge = `<span style="background: #dc3545; color: white; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500;">미결</span>`;
}
// 마감일자 생성 (저장된 것이 있으면 사용, 없으면 1개월 후로 설정)
let deadlineStr;
if (item.deadline) {
deadlineStr = new Date(item.deadline).toLocaleDateString('ko-KR');
} else {
const deadline = new Date();
deadline.setMonth(deadline.getMonth() + 1);
deadlineStr = deadline.toLocaleDateString('ko-KR');
}
const originalIndex = existingItems.indexOf(item);
itemDiv.innerHTML = `
<div class="past-item-header">
<div class="past-item-meta">
<span>[${item.month || '월간회의록'}] ${item.no}번</span>
${statusBadge}
</div>
<div class="past-item-actions">
${!showingCompleted ? `
<button class="btn-warning btn-small" onclick="editPastItem(${originalIndex})">수정</button>
<button class="btn-danger btn-small" onclick="clearPastItem(${originalIndex})">완료</button>
` : `
<button class="btn-info btn-small" onclick="reactivatePastItem(${originalIndex})">재활성화</button>
`}
</div>
</div>
<h5>${item.topic}</h5>
<div class="past-item-content">
${item.solution ? `<strong>해결방법:</strong> ${item.solution}<br>` : ''}
${item.assignee ? `<strong>담당자:</strong> ${item.assignee}<br>` : ''}
${item.details ? `<strong>상세내용:</strong> ${item.details}` : ''}
</div>
<div class="past-item-deadline">
<strong>⏰ 마감일자:</strong> ${deadlineStr}
</div>
`;
pastItems.appendChild(itemDiv);
});
}
// 완료된 항목 재활성화
function reactivatePastItem(index) {
if (confirm('이 항목을 다시 미완료 상태로 되돌리시겠습니까?')) {
const item = existingItems[index];
// 완료 표시 제거
item.details = item.details.replace(/ \(완료\)/g, '');
// 저장
saveToStorage(STORAGE_KEYS.existingItems, existingItems);
displayPastItems();
alert('항목이 미완료 상태로 변경되었습니다.');
}
}
// 기존 항목 편집
function editPastItem(index) {
currentEditIndex = index;
const item = existingItems[index];
document.getElementById('editTopic').value = item.topic;
document.getElementById('editSolution').value = item.solution || '';
document.getElementById('editAssignee').value = item.assignee || '';
document.getElementById('editDetails').value = item.details || '';
// 마감일자 설정
if (item.deadline) {
document.getElementById('editDeadline').value = item.deadline;
} else {
const deadline = new Date();
deadline.setMonth(deadline.getMonth() + 1);
document.getElementById('editDeadline').value = deadline.toISOString().split('T')[0];
}
document.getElementById('editModal').style.display = 'block';
}
// 편집 모달 닫기
function closeEditModal() {
document.getElementById('editModal').style.display = 'none';
currentEditIndex = -1;
}
// 편집 확인
function confirmEdit() {
if (currentEditIndex === -1) return;
const item = existingItems[currentEditIndex];
item.topic = document.getElementById('editTopic').value;
item.solution = document.getElementById('editSolution').value;
item.assignee = document.getElementById('editAssignee').value;
item.details = document.getElementById('editDetails').value;
item.deadline = document.getElementById('editDeadline').value;
// 저장
saveToStorage(STORAGE_KEYS.existingItems, existingItems);
closeEditModal();
displayPastItems();
alert('수정이 완료되었습니다.');
}
// 기존 항목 클리어 (완료 처리)
function clearPastItem(index) {
if (confirm('이 항목을 완료 처리하시겠습니까?')) {
existingItems[index].details += ' (완료)';
// 저장
saveToStorage(STORAGE_KEYS.existingItems, existingItems);
displayPastItems();
alert('항목이 완료 처리되었습니다.');
}
}
// 정기점검 항목 표시
function displayInspectionItems() {
const inspectionItems = document.getElementById('inspectionItems');
const urgentCount = document.getElementById('urgentCount');
const pendingCount = document.getElementById('pendingCount');
if (!inspectionData || !inspectionItems) return;
inspectionItems.innerHTML = '';
let urgent = 0, pending = 0;
inspectionData.forEach((item, index) => {
const itemDiv = document.createElement('div');
let itemClass = 'past-item';
// 상태에 따른 클래스 추가
if (item.status.includes('예정') || item.status.includes('진행')) {
if (item.status.includes('25년') || item.status.includes('2025')) {
itemClass += ' urgent-item';
urgent++;
} else {
itemClass += ' pending-item';
pending++;
}
} else if (item.status.includes('완료')) {
itemClass += ' complete-item';
}
itemDiv.className = itemClass;
itemDiv.setAttribute('data-item', item.item.toLowerCase());
itemDiv.setAttribute('data-status', item.status.toLowerCase());
itemDiv.innerHTML = `
<div class="past-item-header">
<div class="past-item-meta">
<span>${item.frequency}</span>
<span>${item.assignee}</span>
</div>
<div class="past-item-actions">
<button class="btn-success btn-small" onclick="copyInspectionToCurrent(${index})">→ 복사</button>
</div>
</div>
<h5>${item.item}</h5>
<div class="past-item-content">
<strong>상태:</strong> ${item.status}
</div>
`;
inspectionItems.appendChild(itemDiv);
});
urgentCount.textContent = urgent;
pendingCount.textContent = pending;
}
// 정기점검 항목을 현재 폼에 복사
function copyInspectionToCurrent(index) {
const item = inspectionData[index];
// 회의록 작성 섹션으로 이동
showSection('meeting');
document.querySelector('[onclick="showSection(\'meeting\')"]').classList.add('active');
addAgendaItem();
const lastItem = document.querySelector('.agenda-item:last-child');
lastItem.querySelector('.agenda-topic').value = item.item;
lastItem.querySelector('.agenda-assignee').value = item.assignee;
lastItem.querySelector('.agenda-details').value = `주기: ${item.frequency}, 현재상태: ${item.status}`;
// 상태에 따른 진행상태 설정
if (item.status.includes('완료')) {
lastItem.querySelector('.agenda-status').value = '완료';
} else if (item.status.includes('진행') || item.status.includes('예정')) {
lastItem.querySelector('.agenda-status').value = '진행중';
}
alert('정기점검 항목이 회의록에 추가되었습니다.');
}
// 상태 추출 함수
function getStatusFromItem(item) {
if (item.details) {
if (item.details.includes('완료')) return '완료';
if (item.details.includes('진행중') || item.details.includes('진행 중')) return '진행중';
if (item.details.includes('보류')) return '보류';
if (item.details.includes('미결')) return '미결';
}
return '';
}
// 필터링 기능들
function filterPastItems() {
const searchTerm = document.getElementById('pastSearchFilter').value.toLowerCase();
const statusFilter = document.getElementById('pastStatusFilter').value;
const items = document.querySelectorAll('#pastItems .past-item');
let visibleCount = 0;
items.forEach(item => {
const topic = item.getAttribute('data-topic');
const assignee = item.getAttribute('data-assignee');
const solution = item.getAttribute('data-solution');
const status = item.getAttribute('data-status');
const matchesSearch = !searchTerm ||
topic.includes(searchTerm) ||
assignee.includes(searchTerm) ||
solution.includes(searchTerm);
const matchesStatus = !statusFilter || status === statusFilter ||
(statusFilter === '미결' && !status);
if (matchesSearch && matchesStatus) {
item.style.display = 'block';
visibleCount++;
} else {
item.style.display = 'none';
}
});
document.getElementById('filteredPastItems').textContent = visibleCount;
}
function filterInspectionItems() {
const searchTerm = document.getElementById('inspectionSearchFilter').value.toLowerCase();
const statusFilter = document.getElementById('inspectionStatusFilter').value;
const items = document.querySelectorAll('#inspectionItems .past-item');
items.forEach(item => {
const itemText = item.getAttribute('data-item');
const status = item.getAttribute('data-status');
const matchesSearch = !searchTerm || itemText.includes(searchTerm);
const matchesStatus = !statusFilter ||
(statusFilter === '완료' && status.includes('완료')) ||
(statusFilter === '예정' && (status.includes('예정') || status.includes('진행 예정'))) ||
(statusFilter === '진행' && status.includes('진행'));
if (matchesSearch && matchesStatus) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
// 논의사항 항목 추가
function addAgendaItem() {
agendaCounter++;
const agendaItems = document.getElementById('agendaItems');
const agendaItem = document.createElement('div');
agendaItem.className = 'agenda-item';
agendaItem.innerHTML = `
<div class="agenda-header">
<span>논의사항 ${agendaCounter}</span>
<button class="btn btn-danger" onclick="removeAgendaItem(this)">삭제</button>
</div>
<div class="agenda-content">
<div class="agenda-fields">
<div class="field-row">
<div class="field-label">논의사항:</div>
<input type="text" class="agenda-topic" placeholder="논의할 사항을 입력하세요">
</div>
<div class="field-row">
<div class="field-label">해결방법:</div>
<textarea class="agenda-solution" rows="3" placeholder="해결방법 또는 조치사항을 입력하세요"></textarea>
</div>
<div class="field-row">
<div class="field-label">담당자:</div>
<input type="text" class="agenda-assignee" placeholder="담당자명">
</div>
<div class="field-row">
<div class="field-label">상세내용:</div>
<textarea class="agenda-details" rows="2" placeholder="추가 상세내용"></textarea>
</div>
<div class="field-row">
<div class="field-label">진행상태:</div>
<select class="agenda-status">
<option value="">선택</option>
<option value="완료">완료</option>
<option value="진행중">진행중</option>
<option value="보류">보류</option>
<option value="미결">미결</option>
</select>
</div>
<div class="field-row">
<div class="field-label">마감일자:</div>
<input type="date" class="agenda-deadline">
</div>
</div>
</div>
`;
agendaItems.appendChild(agendaItem);
}
// 논의사항 항목 삭제
function removeAgendaItem(button) {
const agendaItem = button.closest('.agenda-item');
agendaItem.remove();
// 항목 번호 재정렬
const items = document.querySelectorAll('.agenda-item .agenda-header span');
items.forEach((item, index) => {
item.textContent = `논의사항 ${index + 1}`;
});
}
// 회의록 생성
function generateOutput() {
const year = document.getElementById('meetingYear').value;
const month = document.getElementById('meetingMonth').value;
const date = document.getElementById('meetingDate').value;
const attendees = document.getElementById('attendees').value;
let output = `월간 안전회의록\n`;
output += `==========================================\n\n`;
output += `회의 정보\n`;
output += `- 회의년도: ${year}\n`;
output += `- 회의월: ${month}\n`;
output += `- 회의일자: ${date}\n`;
output += `- 참석자: ${attendees}\n\n`;
const agendaItems = document.querySelectorAll('.agenda-item');
if (agendaItems.length > 0) {
output += `논의사항\n`;
output += `==========================================\n\n`;
agendaItems.forEach((item, index) => {
const topic = item.querySelector('.agenda-topic').value;
const solution = item.querySelector('.agenda-solution').value;
const assignee = item.querySelector('.agenda-assignee').value;
const details = item.querySelector('.agenda-details').value;
const status = item.querySelector('.agenda-status').value;
const deadline = item.querySelector('.agenda-deadline').value;
output += `${index + 1}. ${topic}\n`;
if (solution) output += ` 해결방법: ${solution}\n`;
if (assignee) output += ` 담당자: ${assignee}\n`;
if (details) output += ` 상세내용: ${details}\n`;
if (status) output += ` 진행상태: ${status}\n`;
if (deadline) output += ` 마감일자: ${new Date(deadline).toLocaleDateString('ko-KR')}\n`;
output += `\n`;
});
}
output += `==========================================\n`;
output += `작성일: ${new Date().toLocaleDateString('ko-KR')}\n`;
output += `작성자: [작성자명]\n`;
document.getElementById('outputContent').textContent = output;
document.getElementById('output').style.display = 'block';
// 스크롤을 결과로 이동
document.getElementById('output').scrollIntoView({ behavior: 'smooth' });
}
// 클립보드에 복사
function copyToClipboard() {
const outputContent = document.getElementById('outputContent').textContent;
navigator.clipboard.writeText(outputContent).then(() => {
alert('회의록이 클립보드에 복사되었습니다!');
}).catch(err => {
console.error('복사 실패:', err);
alert('복사에 실패했습니다.');
});
}
// 양식 초기화
function clearForm() {
if (confirm('모든 내용을 초기화하시겠습니까?')) {
document.getElementById('attendees').value = '';
document.getElementById('agendaItems').innerHTML = '';
document.getElementById('output').style.display = 'none';
agendaCounter = 0;
}
}
// 템플릿 저장
function saveTemplate() {
const templateData = {
year: document.getElementById('meetingYear').value,
month: document.getElementById('meetingMonth').value,
attendees: document.getElementById('attendees').value,
agendaItems: []
};
const agendaItems = document.querySelectorAll('.agenda-item');
agendaItems.forEach(item => {
templateData.agendaItems.push({
topic: item.querySelector('.agenda-topic').value,
solution: item.querySelector('.agenda-solution').value,
assignee: item.querySelector('.agenda-assignee').value,
details: item.querySelector('.agenda-details').value,
status: item.querySelector('.agenda-status').value,
deadline: item.querySelector('.agenda-deadline').value
});
});
const templateJson = JSON.stringify(templateData, null, 2);
downloadJSON(templateJson, `안전회의록_템플릿_${new Date().toISOString().split('T')[0]}.json`);
}
// 모달 외부 클릭 시 닫기
window.onclick = function(event) {
const modal = document.getElementById('editModal');
if (event.target === modal) {
closeEditModal();
}
}
// 페이지 로드 시 데이터 로드
window.onload = function() {
addAgendaItem();
initializeData();
};
</script>
</body>
</html>