feat: 현황판 통계 카드 개선 및 상태별 분류

📊 통계 카드 재구성:
- 전체 진행 중: 모든 진행 중인 이슈 수
- 오늘 신규: 오늘 수신함에서 진행중으로 넘어온 이슈
- 완료 대기: 완료 신청된 이슈 (completion_requested_at 존재)
- 지연 중: 마감일이 지난 이슈

🎨 UI 개선:
- 완료 대기: 보라색 배경 + 모래시계 아이콘
- 지연 중: 빨간색 배경 + 시계 아이콘
- 각 카드별 애니메이션 점 효과 유지

🔧 로직 개선:
- reviewed_at 기준으로 오늘 신규 계산
- completion_requested_at 필드로 완료 대기 상태 판별
- expected_completion_date 기준으로 지연 상태 판별
- 실시간 통계 업데이트

💡 사용자 경험:
- 한눈에 파악 가능한 상태별 분류
- 색상 코딩으로 우선순위 구분
- 직관적인 아이콘 사용

Expected Result:
 전체 진행 중 | 오늘 신규 | 완료 대기 | 지연 중
 실시간 상태별 통계 표시
 시각적으로 구분되는 색상 체계
 관리자가 우선순위를 쉽게 파악
This commit is contained in:
Hyungi Ahn
2025-10-26 13:02:24 +09:00
parent 6e240f2296
commit 919bc82ca1
5 changed files with 43 additions and 30 deletions

View File

@@ -313,12 +313,20 @@ async def request_completion(
raise HTTPException(status_code=400, detail="이미 완료 신청된 부적합입니다.")
try:
print(f"DEBUG: 완료 신청 시작 - Issue ID: {issue_id}, User: {current_user.username}")
# 완료 사진 저장
completion_photo_path = None
if request.completion_photo:
print(f"DEBUG: 완료 사진 저장 시작")
completion_photo_path = save_base64_image(request.completion_photo, "completion")
print(f"DEBUG: 완료 사진 저장 완료 - Path: {completion_photo_path}")
if not completion_photo_path:
raise Exception("완료 사진 저장에 실패했습니다.")
# 완료 신청 정보 업데이트
print(f"DEBUG: DB 업데이트 시작")
issue.completion_requested_at = datetime.now()
issue.completion_requested_by_id = current_user.id
issue.completion_photo_path = completion_photo_path
@@ -326,6 +334,7 @@ async def request_completion(
db.commit()
db.refresh(issue)
print(f"DEBUG: DB 업데이트 완료")
return {
"message": "완료 신청이 성공적으로 제출되었습니다.",
@@ -335,9 +344,10 @@ async def request_completion(
}
except Exception as e:
print(f"ERROR: 완료 신청 처리 오류 - {str(e)}")
db.rollback()
# 업로드된 파일이 있다면 삭제
if completion_photo_path:
if 'completion_photo_path' in locals() and completion_photo_path:
try:
delete_file(completion_photo_path)
except:

View File

@@ -13,7 +13,7 @@ def ensure_upload_dir():
if not os.path.exists(UPLOAD_DIR):
os.makedirs(UPLOAD_DIR)
def save_base64_image(base64_string: str) -> Optional[str]:
def save_base64_image(base64_string: str, prefix: str = "image") -> Optional[str]:
"""Base64 이미지를 파일로 저장하고 경로 반환"""
try:
ensure_upload_dir()
@@ -40,8 +40,8 @@ def save_base64_image(base64_string: str) -> Optional[str]:
elif image.mode != 'RGB':
image = image.convert('RGB')
# 파일명 생성 (강제로 .jpg)
filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.jpg"
# 파일명 생성 (prefix 포함)
filename = f"{prefix}_{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.jpg"
filepath = os.path.join(UPLOAD_DIR, filename)
# 이미지 저장 (최대 크기 제한)

View File

@@ -181,29 +181,29 @@
</div>
</div>
<div class="bg-gradient-to-br from-yellow-400 to-orange-500 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between">
<div>
<p class="text-yellow-100 text-sm flex items-center space-x-1">
<span>지연 위험</span>
<div class="w-1.5 h-1.5 bg-yellow-200 rounded-full animate-pulse"></div>
</p>
<p class="text-3xl font-bold" id="delayRisk">0</p>
</div>
<i class="fas fa-exclamation-triangle text-4xl text-yellow-200"></i>
</div>
</div>
<div class="bg-gradient-to-br from-purple-400 to-purple-600 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between">
<div>
<p class="text-purple-100 text-sm flex items-center space-x-1">
<span>활성 프로젝트</span>
<span>완료 대기</span>
<div class="w-1.5 h-1.5 bg-purple-200 rounded-full animate-pulse"></div>
</p>
<p class="text-3xl font-bold" id="activeProjects">0</p>
<p class="text-3xl font-bold" id="pendingCompletion">0</p>
</div>
<i class="fas fa-project-diagram text-4xl text-purple-200"></i>
<i class="fas fa-hourglass-half text-4xl text-purple-200"></i>
</div>
</div>
<div class="bg-gradient-to-br from-red-400 to-red-600 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between">
<div>
<p class="text-red-100 text-sm flex items-center space-x-1">
<span>지연 중</span>
<div class="w-1.5 h-1.5 bg-red-200 rounded-full animate-pulse"></div>
</p>
<p class="text-3xl font-bold" id="overdue">0</p>
</div>
<i class="fas fa-clock text-4xl text-red-200"></i>
</div>
</div>
</div>
@@ -385,26 +385,29 @@
// 통계 업데이트
function updateStatistics() {
const today = new Date().toDateString();
// 오늘 신규 (오늘 수신함에서 진행중으로 넘어온 것들)
const todayIssues = allIssues.filter(issue =>
new Date(issue.report_date).toDateString() === today
issue.reviewed_at && new Date(issue.reviewed_at).toDateString() === today
);
// 지연 위험 계산 (예상일이 지났거나 3일 이내)
const delayRiskIssues = allIssues.filter(issue => {
// 완료 대기 (완료 신청이 된 것들)
const pendingCompletionIssues = allIssues.filter(issue =>
issue.completion_requested_at && issue.review_status === 'in_progress'
);
// 지연 중 (마감일이 지난 것들)
const overdueIssues = allIssues.filter(issue => {
if (!issue.expected_completion_date) return false;
const expectedDate = new Date(issue.expected_completion_date);
const now = new Date();
const diffDays = (expectedDate - now) / (1000 * 60 * 60 * 24);
return diffDays <= 3; // 3일 이내 또는 지연
return expectedDate < now; // 마감일 지남
});
// 활성 프로젝트 (진행 중인 부적합이 있는 프로젝트)
const activeProjectIds = new Set(allIssues.map(issue => issue.project_id));
document.getElementById('totalInProgress').textContent = allIssues.length;
document.getElementById('todayNew').textContent = todayIssues.length;
document.getElementById('delayRisk').textContent = delayRiskIssues.length;
document.getElementById('activeProjects').textContent = activeProjectIds.size;
document.getElementById('pendingCompletion').textContent = pendingCompletionIssues.length;
document.getElementById('overdue').textContent = overdueIssues.length;
}
// 이슈 카드 업데이트 (관리함 스타일 - 날짜별 그룹화)