From 3d13c0b1f32a7a34bd88cf1dcbf0ecbd9ee4e22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Wed, 18 Feb 2026 06:41:28 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88=ED=98=B8/?= =?UTF-8?q?=EC=A3=BC=EB=AF=BC=EB=B2=88=ED=98=B8=20=ED=8F=AC=EB=A7=B7?= =?UTF-8?q?=ED=8C=85=20=EB=B0=8F=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EB=A7=A4=EC=B6=9C=20=ED=86=B5=EA=B3=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전화번호 포맷팅 (010-1234-5678 형식) 전역 적용 - 주민번호 마스킹 포맷팅 (980520-1****** 형식) - 대시보드에 총 마일리지, 이번달 매출, 마진, 마진율 통계 추가 Co-Authored-By: Claude Opus 4.6 --- static/app.js | 90 ++++++++++++++++++++++++++++++++++++++++---- templates/index.html | 32 ++++++++++++++++ 2 files changed, 114 insertions(+), 8 deletions(-) diff --git a/static/app.js b/static/app.js index 228fff7..eb83017 100644 --- a/static/app.js +++ b/static/app.js @@ -14,6 +14,57 @@ let currentLotAllocation = { // 재고 계산 모드 (localStorage에 저장) let inventoryCalculationMode = localStorage.getItem('inventoryMode') || 'all'; +// ==================== 포맷팅 함수 ==================== + +// 전화번호 포맷팅 (010-1234-5678 형식) +function formatPhoneNumber(phone) { + if (!phone) return '-'; + + // 숫자만 추출 + const cleaned = phone.replace(/\D/g, ''); + + // 길이에 따라 다른 포맷 적용 + if (cleaned.length === 11) { + // 010-1234-5678 + return cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); + } else if (cleaned.length === 10) { + // 02-1234-5678 또는 031-123-4567 + if (cleaned.startsWith('02')) { + return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '$1-$2-$3'); + } else { + return cleaned.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3'); + } + } else if (cleaned.length === 9) { + // 02-123-4567 + return cleaned.replace(/(\d{2})(\d{3})(\d{4})/, '$1-$2-$3'); + } + + return phone; // 포맷팅할 수 없는 경우 원본 반환 +} + +// 주민번호 포맷팅 (980520-1****** 형식) +function formatJuminNumber(jumin, masked = true) { + if (!jumin) return '-'; + + // 숫자만 추출 + const cleaned = jumin.replace(/\D/g, ''); + + if (cleaned.length >= 13) { + const front = cleaned.substring(0, 6); + const back = cleaned.substring(6, 13); + + if (masked) { + // 뒷자리 마스킹 + return `${front.replace(/(\d{2})(\d{2})(\d{2})/, '$1$2$3')}-${back.charAt(0)}******`; + } else { + // 전체 표시 (편집 시) + return `${front.replace(/(\d{2})(\d{2})(\d{2})/, '$1$2$3')}-${back}`; + } + } + + return jumin; // 포맷팅할 수 없는 경우 원본 반환 +} + $(document).ready(function() { // 페이지 네비게이션 $('.sidebar .nav-link').on('click', function(e) { @@ -70,23 +121,40 @@ $(document).ready(function() { // 대시보드 데이터 로드 function loadDashboard() { - // 환자 수 + // 환자 수 + 총 마일리지 $.get('/api/patients', function(response) { if (response.success) { $('#totalPatients').text(response.data.length); + const totalMileage = response.data.reduce((sum, p) => sum + (p.mileage_balance || 0), 0); + $('#totalMileage').text(totalMileage.toLocaleString()); } }); // 재고 현황 (저장된 모드 사용) loadInventorySummary(); - // 오늘 조제 수 및 최근 조제 내역 + // 오늘 조제 수, 이번달 매출/마진, 최근 조제 내역 $.get('/api/compounds', function(response) { if (response.success) { const today = new Date().toISOString().split('T')[0]; + const currentMonth = new Date().toISOString().slice(0, 7); const todayCompounds = response.data.filter(c => c.compound_date === today); $('#todayCompounds').text(todayCompounds.length); + // 이번달 매출/마진 계산 + const monthData = response.data.filter(c => + c.compound_date && c.compound_date.startsWith(currentMonth) && + ['PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED'].includes(c.status) + ); + const monthSales = monthData.reduce((sum, c) => sum + (c.actual_payment_amount || c.sell_price_total || 0), 0); + const monthCost = monthData.reduce((sum, c) => sum + (c.cost_total || 0), 0); + const monthProfit = monthSales - monthCost; + const profitRate = monthSales > 0 ? ((monthProfit / monthSales) * 100).toFixed(1) : 0; + + $('#monthSales').text(monthSales.toLocaleString()); + $('#monthProfit').text(monthProfit.toLocaleString()); + $('#profitRate').text(profitRate + '%'); + // 최근 조제 내역 (최근 5개) const tbody = $('#recentCompounds'); tbody.empty(); @@ -97,10 +165,16 @@ $(document).ready(function() { let statusBadge = ''; switch(compound.status) { case 'PREPARED': - statusBadge = '조제완료'; + statusBadge = '조제완료'; break; - case 'DISPENSED': - statusBadge = '출고완료'; + case 'PAID': + statusBadge = '결제완료'; + break; + case 'DELIVERED': + statusBadge = '배송완료'; + break; + case 'COMPLETED': + statusBadge = '판매완료'; break; case 'CANCELLED': statusBadge = '취소'; @@ -153,7 +227,7 @@ $(document).ready(function() { tbody.append(` ${patient.name} - ${patient.phone} + ${formatPhoneNumber(patient.phone)} ${patient.gender === 'M' ? '남' : patient.gender === 'F' ? '여' : '-'} ${patient.birth_date || '-'} @@ -1363,7 +1437,7 @@ $(document).ready(function() { ${response.data.length - index} ${compound.compound_date || ''}
${compound.created_at ? compound.created_at.split(' ')[1] : ''} ${compound.patient_name || '직접조제'} - ${compound.patient_phone || '-'} + ${formatPhoneNumber(compound.patient_phone)} ${compound.formula_name || '직접조제'} ${compound.je_count || 0} ${compound.cheop_total || 0} @@ -1442,7 +1516,7 @@ $(document).ready(function() { // 환자 정보 $('#detailPatientName').text(data.patient_name || '직접조제'); - $('#detailPatientPhone').text(data.patient_phone || '-'); + $('#detailPatientPhone').text(formatPhoneNumber(data.patient_phone)); $('#detailCompoundDate').text(data.compound_date || '-'); // 처방 정보 (가감방 표시 포함) diff --git a/templates/index.html b/templates/index.html index 0951182..6281072 100644 --- a/templates/index.html +++ b/templates/index.html @@ -168,6 +168,38 @@ + +
+
+
+
총 마일리지
+
0
+ 전체 환자 보유액 +
+
+
+
+
이번달 매출
+
0
+ 결제 완료 기준 +
+
+
+
+
이번달 마진
+
0
+ 매출 - 원가 (마일리지 제외) +
+
+
+
+
마진율
+
0%
+ 이번달 평균 +
+
+
+