Files
TK-FB-Project/web-ui/pages/common/daily-work-report.html
Hyungi Ahn 790d12fe13 feat: 토큰 만료 시 자동 로그아웃 기능 추가 및 테이블명 수정
🔐 토큰 만료 시 자동 로그아웃 기능:
1. JWT 토큰 만료 시간 대폭 연장:
   - 액세스 토큰: 24시간 → 7일
   - 리프레시 토큰: 7일 → 30일
   - 사용자 편의성 크게 향상

2. 토큰 만료 감지 및 처리:
   - isTokenExpired() 함수 추가
   - JWT 페이로드 파싱하여 exp 확인
   - 현재 시간과 비교하여 만료 여부 판단

3. 자동 로그아웃 처리:
   - API 호출 시 401 오류 감지
   - 주기적 토큰 만료 확인 (5분마다)
   - 만료 시 자동 인증 데이터 정리
   - 사용자 알림 후 로그인 페이지 리다이렉트

4. 개선된 인증 데이터 관리:
   - clearAuthData() 함수로 통합 관리
   - token, user, userInfo, currentUser 모두 정리
   - 메모리 누수 방지

🐛 데이터베이스 테이블명 수정:
1. projectModel.js:
   - Projects → projects (대문자 → 소문자)
   - 실제 DB 테이블명과 일치

2. taskModel.js:
   - Tasks → tasks (대문자 → 소문자)
   - 실제 DB 테이블명과 일치

3. API 오류 해결:
   - '테이블이 존재하지 않습니다' 오류 수정
   - projects, tasks API 정상 작동

 사용자 경험 개선:
- 토큰 만료로 인한 예상치 못한 오류 방지
- 명확한 만료 알림 메시지
- 자동 로그아웃으로 보안 강화
- 더 긴 세션 유지로 편의성 향상

🔧 기술적 개선:
- JWT 페이로드 안전한 파싱
- 에러 핸들링 강화
- 주기적 백그라운드 확인
- 전역 함수로 재사용성 향상

🎯 결과:
- 안정적인 인증 시스템
- 사용자 친화적인 세션 관리
- 보안성과 편의성의 균형
- API 호출 오류 해결

테스트:
- 토큰 만료 후 자동 로그아웃 확인
- projects, tasks API 정상 작동 확인
2025-11-03 12:49:25 +09:00

1062 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">
<title>일일 작업보고서 작성 | (주)테크니컬코리아</title>
<link rel="stylesheet" href="/css/main-layout.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
/* 메인 레이아웃 조정 */
.main-layout-with-navbar {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content-wrapper {
flex: 1;
padding: 20px;
}
.work-form-container {
max-width: 1000px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 2.5rem;
padding: 3rem 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 16px;
box-shadow: 0 6px 24px rgba(0,0,0,0.12);
}
.page-header h1 {
font-size: 2.5rem;
margin-bottom: 0.8rem;
font-weight: 700;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.card {
background: white;
border-radius: 16px;
padding: 2.5rem;
box-shadow: 0 6px 24px rgba(0,0,0,0.08);
margin-bottom: 2.5rem;
}
/* 스테퍼 스타일 */
.step-section {
margin-bottom: 32px;
opacity: 0.5;
transition: opacity 0.3s ease;
}
.step-section.active {
opacity: 1;
}
.step-section.completed {
opacity: 0.8;
}
.step-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.step-number {
background: #007bff;
color: white;
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 18px;
}
.step-number.completed {
background: #28a745;
}
.step-title {
font-size: 22px;
font-weight: 700;
color: #333;
}
/* 작업자 선택 그리드 */
.worker-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 16px;
margin-bottom: 32px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.worker-btn {
padding: 24px 16px;
border: 3px solid #e1e5e9;
border-radius: 12px;
background: white;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
font-size: 16px;
font-weight: 600;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.worker-btn:hover {
border-color: #007bff;
background: #f8f9fa;
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
}
.worker-btn.selected {
border-color: #007bff;
background: #007bff;
color: white;
box-shadow: 0 4px 16px rgba(0, 123, 255, 0.3);
}
/* 폼 그룹 */
.form-group {
margin-bottom: 2rem;
}
.form-group label {
display: block;
margin-bottom: 12px;
font-weight: 700;
color: #555;
font-size: 18px;
}
.large-select {
width: 100%;
padding: 20px;
border: 3px solid #e1e5e9;
border-radius: 12px;
font-size: 18px;
background: white;
cursor: pointer;
transition: border-color 0.3s;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.large-select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.15);
}
/* 작업 항목 스타일 */
.work-entry {
border: 2px solid #e1e5e9;
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
background: white;
position: relative;
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
}
.work-entry-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #f0f0f0;
}
.work-entry-title {
font-weight: 700;
color: #333;
font-size: 20px;
}
.remove-work-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 48px;
height: 48px;
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
}
.remove-work-btn:hover {
background: #c82333;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
}
.work-entry-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 20px;
}
.time-input-row {
grid-column: span 2;
}
.quick-time-buttons {
display: flex;
gap: 12px;
margin-top: 16px;
flex-wrap: wrap;
justify-content: center;
}
.quick-time-btn {
padding: 16px 24px;
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 12px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s;
min-width: 80px;
text-align: center;
}
.quick-time-btn:hover {
border-color: #007bff;
background: #e7f3ff;
transform: translateY(-1px);
}
.quick-time-btn:active {
background: #007bff;
color: white;
transform: translateY(0);
}
.total-hours-display {
text-align: center;
font-size: 24px;
font-weight: 700;
padding: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 16px;
margin-bottom: 24px;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
.error-type-section {
display: none;
}
.error-type-section.visible {
display: block;
margin-top: 24px;
padding: 24px;
background: #fff3cd;
border: 2px solid #ffeaa7;
border-radius: 12px;
}
/* 버튼 스타일 */
.btn {
padding: 20px 40px;
border: none;
border-radius: 12px;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
text-decoration: none;
min-height: 60px;
box-shadow: 0 3px 12px rgba(0,0,0,0.1);
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-block {
width: 100%;
justify-content: center;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 123, 255, 0.3);
}
.btn-success:hover:not(:disabled) {
background: #1e7e34;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.3);
}
.btn-secondary:hover {
background: #545b62;
transform: translateY(-2px);
}
/* 뒤로가기 버튼 */
.back-btn {
background: rgba(255,255,255,0.95);
color: #667eea;
border: 3px solid #667eea;
padding: 16px 32px;
border-radius: 12px;
text-decoration: none;
font-weight: 700;
font-size: 18px;
display: inline-flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
transition: all 0.3s ease;
box-shadow: 0 3px 12px rgba(102, 126, 234, 0.2);
}
.back-btn:hover {
background: #667eea;
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
/* 알림 메시지 */
.message {
padding: 20px 32px;
border-radius: 12px;
margin-bottom: 32px;
font-weight: 600;
font-size: 18px;
box-shadow: 0 3px 12px rgba(0,0,0,0.1);
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 2px solid #f5c6cb;
}
.message.success {
background: #d4edda;
color: #155724;
border: 2px solid #c3e6cb;
}
.message.loading {
background: #cce5ff;
color: #0066cc;
border: 2px solid #99d6ff;
}
.message.warning {
background: #fff3cd;
color: #856404;
border: 2px solid #ffeaa7;
}
/* 당일 작업자 현황 스타일 */
.daily-workers-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #f0f0f0;
}
.refresh-btn {
background: #28a745;
color: white;
border: none;
border-radius: 8px;
padding: 12px 20px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.refresh-btn:hover {
background: #1e7e34;
transform: translateY(-1px);
}
.worker-status-grid {
display: grid;
gap: 16px;
}
.worker-status-item {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
border-left: 4px solid #007bff;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.worker-status-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
}
/* 작업자 헤더 스타일 */
.worker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px 12px 0 0;
margin: -20px -20px 20px -20px;
}
.worker-name {
font-size: 18px;
font-weight: 700;
margin: 0;
}
.worker-total-hours {
background: rgba(255,255,255,0.2);
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 700;
}
/* 개별 작업 항목 스타일 */
.individual-works-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.individual-work-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.individual-work-item:hover {
background: #e9ecef;
transform: translateX(4px);
}
.work-details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
flex: 1;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-label {
font-size: 12px;
color: #666;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-value {
font-size: 14px;
color: #333;
font-weight: 600;
}
/* 액션 버튼 스타일 */
.action-buttons {
display: flex;
gap: 8px;
margin-left: 16px;
}
.edit-btn, .delete-btn {
padding: 8px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.3s;
white-space: nowrap;
}
.edit-btn {
background: #007bff;
color: white;
}
.edit-btn:hover {
background: #0056b3;
transform: translateY(-1px);
}
.delete-btn {
background: #dc3545;
color: white;
}
.delete-btn:hover {
background: #c82333;
transform: translateY(-1px);
}
/* 🛠️ 수정 모달 스타일 */
.edit-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.edit-modal-content {
background: white;
border-radius: 16px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-50px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.edit-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 2px solid #f0f0f0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 16px 16px 0 0;
}
.edit-modal-header h3 {
margin: 0;
font-size: 20px;
font-weight: 700;
}
.close-modal-btn {
background: rgba(255,255,255,0.2);
color: white;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
font-size: 18px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.close-modal-btn:hover {
background: rgba(255,255,255,0.3);
transform: rotate(90deg);
}
.edit-modal-body {
padding: 24px;
}
.edit-form-group {
margin-bottom: 20px;
}
.edit-form-group label {
display: block;
margin-bottom: 8px;
font-weight: 700;
color: #555;
font-size: 14px;
}
.edit-select, .edit-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 16px;
background: white;
transition: border-color 0.3s;
}
.edit-select:focus, .edit-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.edit-modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 24px;
border-top: 2px solid #f0f0f0;
background: #f8f9fa;
border-radius: 0 0 16px 16px;
}
.no-data-message {
text-align: center;
padding: 40px 20px;
color: #666;
font-size: 16px;
background: #f8f9fa;
border-radius: 12px;
border: 2px dashed #dee2e6;
}
.loading-spinner {
text-align: center;
padding: 40px 20px;
color: #007bff;
font-size: 16px;
}
/* 가이드 카드 */
.guide-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
max-width: 800px;
margin: 0 auto;
}
.guide-item {
text-align: center;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.guide-item:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
border-color: #667eea;
}
.guide-icon {
font-size: 28px;
margin-bottom: 12px;
}
.guide-item strong {
display: block;
font-size: 16px;
font-weight: 700;
margin-bottom: 8px;
color: #333;
}
/* 반응형 디자인 */
@media (max-width: 1200px) and (min-width: 769px) {
.work-form-container {
max-width: 900px;
}
.worker-grid {
max-width: 700px;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
}
}
@media (max-width: 768px) {
.content-wrapper {
padding: 16px;
}
.worker-grid {
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
gap: 12px;
max-width: 600px;
}
.worker-btn {
padding: 20px 12px;
font-size: 15px;
min-height: 70px;
}
.work-entry-row {
grid-template-columns: 1fr;
gap: 20px;
}
.time-input-row {
grid-column: span 1;
}
.card {
padding: 2rem;
}
.page-header {
padding: 2rem 1.5rem;
}
.page-header h1 {
font-size: 2rem;
}
.btn {
padding: 18px 32px;
font-size: 17px;
}
.step-number {
width: 40px;
height: 40px;
font-size: 16px;
}
.step-title {
font-size: 20px;
}
.quick-time-buttons {
justify-content: center;
}
.quick-time-btn {
padding: 14px 20px;
font-size: 15px;
}
.work-details-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.individual-work-item {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.action-buttons {
margin-left: 0;
justify-content: center;
}
.worker-header {
flex-direction: column;
gap: 8px;
text-align: center;
}
.edit-modal-content {
width: 95%;
margin: 20px;
}
.edit-modal-footer {
flex-direction: column;
}
.edit-btn, .delete-btn {
flex: 1;
padding: 12px;
font-size: 14px;
}
}
@media (max-width: 480px) {
.content-wrapper {
padding: 12px;
}
.worker-grid {
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
max-width: 400px;
}
.card {
padding: 1.5rem;
}
.page-header h1 {
font-size: 1.7rem;
}
.btn {
padding: 16px 24px;
font-size: 16px;
}
.quick-time-btn {
padding: 12px 16px;
font-size: 14px;
min-width: 60px;
}
.detail-item {
text-align: center;
}
.detail-label {
font-size: 11px;
}
.detail-value {
font-size: 13px;
}
.edit-modal-body {
padding: 16px;
}
.edit-modal-header {
padding: 16px;
}
.edit-modal-footer {
padding: 16px;
}
}
</style>
</head>
<body>
<div class="main-layout-with-navbar">
<!-- 네비게이션 바 -->
<div id="navbar-container"></div>
<div class="content-wrapper">
<div class="work-form-container">
<!-- 뒤로가기 버튼 -->
<a href="javascript:history.back()" class="back-btn">
← 뒤로가기
</a>
<div class="page-header">
<h1>✍️ 일일 작업보고서 작성</h1>
<p class="subtitle">단계별로 오늘의 작업 내용을 간편하게 기록하고 관리하세요.</p>
</div>
<!-- 메시지 영역 -->
<div id="message-container"></div>
<!-- 1단계: 날짜 선택 -->
<div id="step1" class="step-section card active">
<div class="step-header">
<div class="step-number">1</div>
<div class="step-title">작업 날짜 선택</div>
</div>
<div class="form-group">
<input type="date" id="reportDate" class="large-select" required>
</div>
<button type="button" class="btn btn-primary" id="nextStep1">다음 단계</button>
</div>
<!-- 2단계: 작업자 선택 -->
<div id="step2" class="step-section card">
<div class="step-header">
<div class="step-number">2</div>
<div class="step-title">작업자 선택</div>
</div>
<div id="workerGrid" class="worker-grid">
<!-- 작업자 버튼들이 여기에 동적으로 추가됩니다 -->
</div>
<button type="button" class="btn btn-primary" id="nextStep2" disabled>다음 단계</button>
</div>
<!-- 3단계: 작업 내역 입력 -->
<div id="step3" class="step-section card">
<div class="step-header">
<div class="step-number">3</div>
<div class="step-title">작업 내역 입력</div>
</div>
<!-- 총 작업시간 표시 -->
<div class="total-hours-display" id="totalHoursDisplay">
총 작업시간: 0시간
</div>
<!-- 작업 항목들 -->
<div id="workEntriesList">
<!-- 작업 항목들이 여기에 동적으로 추가됩니다 -->
</div>
<!-- 작업 추가 버튼 -->
<button type="button" class="btn btn-secondary btn-block" id="addWorkBtn">
작업 추가
</button>
<!-- 저장 버튼 -->
<button type="button" class="btn btn-success btn-block" id="submitBtn" style="margin-top: 1rem;">
💾 작업보고서 저장
</button>
</div>
<!-- 📊 내가 입력한 당일 작업 현황 (수정/삭제 가능) -->
<div class="card" id="dailyWorkersSection" style="display: none;">
<h3>📊 내가 입력한 작업 현황</h3>
<p style="color: #666; margin-bottom: 20px;">
✏️ 내가 입력한 작업만 표시되며, 각 작업을 <strong>수정</strong>하거나 <strong>삭제</strong>할 수 있습니다.
</p>
<div id="dailyWorkersContent">
<!-- 작업자 현황이 여기에 표시됩니다 -->
</div>
</div>
<!-- 사용법 안내 -->
<div class="card">
<h3>📖 사용 가이드</h3>
<div class="guide-grid">
<div class="guide-item">
<div class="guide-icon">📅</div>
<strong>1단계</strong><br>
작업 날짜 선택
</div>
<div class="guide-item">
<div class="guide-icon">👤</div>
<strong>2단계</strong><br>
작업자 선택 (터치)
</div>
<div class="guide-item">
<div class="guide-icon">🔧</div>
<strong>3단계</strong><br>
작업 내역 입력
</div>
<div class="guide-item">
<div class="guide-icon">💾</div>
<strong>완료</strong><br>
저장하여 마무리
</div>
<div class="guide-item">
<div class="guide-icon">✏️</div>
<strong>관리</strong><br>
입력한 작업 수정/삭제
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 스크립트 -->
<script src="/js/api-config.js"></script>
<script src="/js/load-navbar.js"></script>
<script src="/js/daily-work-report.js"></script>
</body>
</html>