Help
RSS
API
Feed
Maltego
Contact
Domain > byebye.gloryjang.com
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2025-12-19
104.21.92.174
(
ClassC
)
2026-02-25
172.67.196.212
(
ClassC
)
Port 80
HTTP/1.1 200 OKDate: Wed, 25 Feb 2026 11:49:53 GMTContent-Type: text/htmlContent-Length: 118188Connection: keep-aliveCF-Cache-Status: HITCache-Control: public, max-age0, must-revalidateETag: 0a756d0a445f9c22d63fc453300d2ef3Report-To: {group:cf-nel,max_age:604800,endpoints:{url:https://a.nel.cloudflare.com/report/v4?seD7rpfx5st4T099jVJyg4n9FpW%2B%2Bnza%2BZnuaGhx2aOOUeCUH03%2FjQNQm7CZboi2q%2BaOCrN%2BdI%2FyMbKBji07ZYbjht7K7EWi3hxAMdcfgQmAFUqeE}}Nel: {report_to:cf-nel,success_fraction:0.0,max_age:604800}Server: cloudflareCF-RAY: 9d36fb5f6994d106-PDXalt-svc: h3:443; ma86400 !DOCTYPE html>html langko>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title>출하 기록 관리 시스템 - 한울축산/title> link relstylesheet hrefhttps://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css> style> :root { --primary-blue: #1565C0; --light-blue: #E3F2FD; --hover-blue: #1976D2; --bg-color: #F8F9FA; --text-main: #263238; --text-sub: #546E7A; --urgent-red: #C62828; --warning-orange: #EF6C00; --info-blue: #0277BD; --success-green: #2E7D32; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; background: var(--bg-color); min-height: 100vh; padding: 20px; color: var(--text-main); } .container { max-width: 1400px; margin: 0 auto; } .header { background: white; border-radius: 16px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); border: 1px solid #ECEFF1; display: flex; align-items: center; justify-content: space-between; } .header-content { display: flex; align-items: center; gap: 16px; } .logo-container { width: 80px; height: 80px; border-radius: 50%; overflow: hidden; background: var(--light-blue); border: 3px solid var(--primary-blue); flex-shrink: 0; } .logo-container img { width: 100%; height: 100%; object-fit: cover; } .header-title h1 { font-size: 24px; font-weight: 700; color: var(--primary-blue); margin-bottom: 4px; text-align: left; } .header-subtitle { font-size: 0.85em; color: var(--text-sub); font-weight: 400; text-align: left; } .sync-status { display: flex; align-items: center; gap: 8px; background: var(--light-blue); padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; } .sync-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success-green); } .sync-dot.syncing { background: var(--warning-orange); animation: pulse 1s infinite; } .sync-dot.error { background: var(--urgent-red); } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .tabs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; } .tab-card { background: white; border-radius: 12px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.3s ease; border: 2px solid #ECEFF1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); } .tab-card:hover { border-color: var(--primary-blue); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(21, 101, 192, 0.15); } .tab-card.active { border-color: var(--primary-blue); background: var(--light-blue); } .tab-icon { font-size: 2.5em; margin-bottom: 12px; } .tab-title { font-size: 1.1em; font-weight: 700; color: var(--text-main); } .tab-content { display: none; animation: fadeIn 0.3s ease; } .tab-content.active { display: block; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .form-section { background: white; padding: 24px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #ECEFF1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); } .form-section h3 { color: var(--primary-blue); margin-bottom: 20px; font-size: 1.2em; font-weight: 700; } .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 20px; } .form-group { display: flex; flex-direction: column; } .form-group label { margin-bottom: 8px; color: var(--text-main); font-weight: 600; font-size: 0.9em; } .form-group input, .form-group select { padding: 12px; border: 1px solid #ECEFF1; border-radius: 8px; font-size: 16px; transition: all 0.3s ease; font-family: Pretendard, sans-serif; } .form-group input:focus, .form-group select:focus { outline: none; border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(21, 101, 192, 0.1); } .form-group input:disabled, .form-group input:read-only { background: #ECEFF1; cursor: not-allowed; } .btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 1em; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin-top: 10px; font-family: Pretendard, sans-serif; } .btn-primary { background: var(--primary-blue); color: white; } .btn-primary:hover { background: var(--hover-blue); transform: scale(1.02); } .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .btn-secondary { background: var(--text-sub); color: white; } .btn-secondary:hover { background: var(--text-main); } .btn-danger { background: var(--urgent-red); color: white; } .btn-danger:hover { background: #B71C1C; } .btn-small { padding: 6px 12px; font-size: 0.85em; margin: 0 4px; } .building-input-section { background: var(--light-blue); padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid var(--primary-blue); } .building-input-header { font-weight: 700; color: var(--primary-blue); margin-bottom: 16px; font-size: 1.05em; } .building-input-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; } .building-input-item { display: flex; align-items: center; gap: 8px; } .building-input-item label { font-weight: 600; color: var(--text-main); min-width: 100px; font-size: 0.9em; } .building-input-item input { flex: 1; padding: 10px; border: 1px solid #ECEFF1; border-radius: 6px; font-size: 0.95em; } .total-check { margin-top: 16px; padding: 12px; background: white; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; font-weight: 600; } .total-check.match { color: var(--success-green); border: 2px solid var(--success-green); } .total-check.mismatch { color: var(--urgent-red); border: 2px solid var(--urgent-red); } .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; } .card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); border: 1px solid #ECEFF1; transition: all 0.3s ease; } .card:hover { transform: scale(1.02); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); } .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .card-title { font-size: 1.15em; color: var(--text-main); font-weight: 700; } .card-icon { font-size: 1.5em; } .card-value { font-size: 2em; font-weight: 700; color: var(--primary-blue); margin-bottom: 8px; } .card-label { color: var(--text-sub); font-size: 0.9em; font-weight: 500; } .region-card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); border: 1px solid #ECEFF1; margin-bottom: 20px; } .region-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 2px solid var(--primary-blue); } .region-title { font-size: 1.3em; font-weight: 700; color: var(--text-main); } .region-total { font-size: 1.2em; font-weight: 700; color: var(--primary-blue); margin-left: auto; } .buildings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; } .building-card { background: var(--light-blue); padding: 18px; border-radius: 10px; border: 1px solid var(--primary-blue); } .building-name { font-weight: 700; color: var(--text-main); margin-bottom: 4px; font-size: 1em; display: flex; justify-content: space-between; align-items: center; } .building-total { color: var(--primary-blue); font-size: 1.1em; } .building-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 12px; margin-bottom: 12px; } .stat-item { text-align: center; padding: 12px 8px; background: white; border-radius: 8px; } .stat-value { font-size: 1.3em; font-weight: 700; color: var(--primary-blue); } .stat-label { font-size: 0.75em; color: var(--text-sub); margin-top: 4px; font-weight: 600; } .building-actions { display: flex; gap: 8px; margin-top: 12px; } .building-actions button { flex: 1; padding: 8px; border: none; border-radius: 6px; font-size: 0.85em; font-weight: 600; cursor: pointer; transition: all 0.3s ease; font-family: Pretendard, sans-serif; } .btn-complete { background: var(--success-green); color: white; } .btn-complete:hover { background: #1B5E20; } .btn-transfer { background: var(--info-blue); color: white; } .btn-transfer:hover { background: #01579B; } .scheduled-select { background: var(--light-blue); padding: 16px; border-radius: 10px; margin: 16px 0; border: 1px solid var(--primary-blue); } .scheduled-header { font-weight: 700; color: var(--primary-blue); margin-bottom: 12px; font-size: 1em; } .scheduled-item { padding: 14px; background: white; border-radius: 8px; margin-bottom: 10px; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; } .scheduled-item:hover { border-color: var(--primary-blue); transform: scale(1.02); } .scheduled-item.selected { border-color: var(--primary-blue); background: var(--light-blue); } .scheduled-item-header { font-weight: 700; color: var(--primary-blue); margin-bottom: 6px; } .scheduled-item-details { font-size: 0.9em; color: var(--text-sub); } .calendar { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); border: 1px solid #ECEFF1; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .calendar-header h3 { color: var(--text-main); font-size: 1.3em; font-weight: 700; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav button { padding: 10px 18px; border: 1px solid var(--primary-blue); background: white; color: var(--primary-blue); border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s ease; font-family: Pretendard, sans-serif; } .calendar-nav button:hover { background: var(--primary-blue); color: white; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 10px; } .calendar-day-header { text-align: center; padding: 12px; font-weight: 700; color: var(--text-sub); font-size: 0.9em; } .calendar-day { aspect-ratio: 1; border: 1px solid #ECEFF1; border-radius: 8px; padding: 10px; cursor: pointer; transition: all 0.3s ease; position: relative; background: white; } .calendar-day:hover { border-color: var(--primary-blue); transform: scale(1.05); box-shadow: 0 2px 8px rgba(21, 101, 192, 0.15); } .calendar-day.other-month { opacity: 0.3; } .calendar-day.today { border-color: var(--primary-blue); background: var(--light-blue); font-weight: 700; } .day-number { font-weight: 600; color: var(--text-main); margin-bottom: 6px; } .day-indicator { width: 6px; height: 6px; border-radius: 50%; margin: 3px auto; } .indicator-scheduled { background: var(--warning-orange); } .indicator-completed { background: var(--success-green); } .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; animation: fadeIn 0.3s ease; } .modal.active { display: flex; align-items: center; justify-content: center; } .modal-content { background: white; border-radius: 12px; padding: 28px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; animation: slideUp 0.3s ease; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } @keyframes slideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 2px solid var(--primary-blue); } .modal-header h3 { color: var(--text-main); font-size: 1.3em; font-weight: 700; } .modal-close { background: none; border: none; font-size: 1.8em; cursor: pointer; color: var(--text-sub); padding: 0; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.3s ease; } .modal-close:hover { background: var(--light-blue); color: var(--primary-blue); } .modal-icon { font-size: 3em; text-align: center; margin-bottom: 16px; } .modal-message { text-align: center; font-size: 1.1em; font-weight: 600; color: var(--text-main); margin-bottom: 20px; } .modal-buttons { display: flex; gap: 12px; margin-top: 20px; } .modal-buttons button { flex: 1; } .record-item { background: var(--light-blue); padding: 16px; border-radius: 8px; margin-bottom: 12px; border-left: 4px solid var(--primary-blue); } .record-header { display: flex; justify-content: space-between; margin-bottom: 12px; } .record-type { font-weight: 700; color: var(--primary-blue); } .record-status { padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 600; } .status-scheduled { background: #FFF3E0; color: var(--warning-orange); } .status-completed { background: #E8F5E9; color: var(--success-green); } .record-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 12px; } .record-detail { display: flex; flex-direction: column; } .detail-label { font-size: 0.85em; color: var(--text-sub); margin-bottom: 4px; font-weight: 600; } .detail-value { font-weight: 700; color: var(--text-main); } .loading { text-align: center; padding: 40px; color: var(--text-sub); } .loading-spinner { border: 4px solid #ECEFF1; border-top: 4px solid var(--primary-blue); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto 20px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .table-container { overflow-x: auto; margin-top: 20px; } table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; } th { background: var(--primary-blue); color: white; padding: 14px; text-align: left; font-weight: 700; } td { padding: 14px; border-bottom: 1px solid #E0E0E0; } tr:hover { background: var(--light-blue); } .empty-state { text-align: center; padding: 60px 20px; color: var(--text-sub); } .empty-state-icon { font-size: 4em; margin-bottom: 16px; opacity: 0.3; } @media (max-width: 768px) { .header { flex-direction: column; text-align: center; } .header-left { flex-direction: column; } .tabs { grid-template-columns: repeat(4, 1fr); gap: 6px; } .tab-card { padding: 12px 4px; } .tab-icon { font-size: 1.3em; } .tab-title { font-size: 0.75em; } .form-grid { grid-template-columns: 1fr; } .dashboard-grid { grid-template-columns: 1fr; } .buildings-grid { grid-template-columns: 1fr; } .building-stats { grid-template-columns: repeat(2, 1fr); } .calendar-grid { gap: 4px; } .calendar-day { padding: 4px; font-size: 0.85em; } .record-details { grid-template-columns: 1fr; } .modal-buttons { flex-direction: column; } } /style> !-- SheetJS 라이브러리 (Excel 파일 읽기) --> script srchttps://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js>/script>/head>body> div classcontainer> !-- 헤더 --> div classheader> div classheader-content> div classlogo-container> img srclogo.png alt한울축산> /div> div classheader-title> h1>출하 기록 관리 시스템/h1> span classheader-subtitle>출하 기록 관리/span> /div> /div> div classsync-status> span classsync-dot idsyncDot>/span> span idsyncText>동기화 대기/span> /div> /div> !-- 탭 카드 --> div classtabs> div classtab-card active onclickswitchTab(input)> div classtab-icon>📝/div> div classtab-title>데이터 입력/div> /div> div classtab-card onclickswitchTab(dashboard)> div classtab-icon>📊/div> div classtab-title>대시보드/div> /div> div classtab-card onclickswitchTab(calendar)> div classtab-icon>📅/div> div classtab-title>달력/div> /div> div classtab-card onclickswitchTab(backfat)> div classtab-icon>📈/div> div classtab-title>등지방 분포도/div> /div> /div> !-- 탭 1: 데이터 입력 --> div idtab-input classtab-content active> !-- 출하 예정 등록 --> div classform-section> h3>📋 출하 예정 등록/h3> form idscheduledForm> div classform-grid> div classform-group> label>출하 예정 날짜 */label> input typedate idscheduled-date required> /div> div classform-group> label>지역 */label> select idscheduled-location required> option value>선택하세요/option> option value영광>영광/option> option value함평>함평/option> /select> /div> div classform-group> label>예정 두수 */label> select idscheduled-headcount-type required onchangetoggleScheduledHeadcountInput()> option value80>80두/option> option valuecustom>기타 (직접 입력)/option> /select> /div> div classform-group idscheduled-headcount-custom-group styledisplay: none;> label>예정 두수 입력 */label> input typenumber idscheduled-headcount-custom min1> /div> div classform-group> label>도축장 */label> select idscheduled-slaughterhouse-type required onchangetoggleSlaughterhouseInput()> option value>선택하세요/option> option value함평 함평 (명주푸드)>함평 함평 (명주푸드)/option> option value나주 축공 (무등)>나주 축공 (무등)/option> option value광주 삼호 (누리천하)>광주 삼호 (누리천하)/option> option value나주 중앙 (다솔)>나주 중앙 (다솔)/option> option valuecustom>기타 (직접 입력)/option> /select> /div> div classform-group idscheduled-slaughterhouse-custom-group styledisplay: none;> label>도축장 직접 입력 */label> input typetext idscheduled-slaughterhouse-custom> /div> /div> button typesubmit classbtn btn-primary>등록하기/button> /form> /div> !-- 출하 등록 --> div classform-section> h3>✅ 출하 등록 (실제 출하)/h3> form idactualForm> div classform-grid> div classform-group> label>출하 날짜 */label> input typedate idactual-date required onchangeloadScheduledForDate()> /div> /div> !-- 출하 예정 목록 --> div idscheduled-list-container styledisplay: none;> div classscheduled-select> div classscheduled-header> 📋 이 날짜의 출하 예정 목록 (선택하면 정보가 자동 입력됩니다) /div> div idscheduled-list>/div> /div> /div> div classform-grid> div classform-group> label>지역 */label> select idactual-location required onchangeloadBuildingsByLocation()> option value>선택하세요/option> option value영광>영광/option> option value함평>함평/option> /select> /div> div classform-group> label>도축장 */label> select idactual-slaughterhouse-type required onchangetoggleActualSlaughterhouseInput()> option value>선택하세요/option> option value함평 함평 (명주푸드)>함평 함평 (명주푸드)/option> option value나주 축공 (무등)>나주 축공 (무등)/option> option value광주 삼호 (누리천하)>광주 삼호 (누리천하)/option> option value나주 중앙 (다솔)>나주 중앙 (다솔)/option> option valuecustom>기타 (직접 입력)/option> /select> /div> div classform-group idactual-slaughterhouse-custom-group styledisplay: none;> label>도축장 직접 입력 */label> input typetext idactual-slaughterhouse-custom> /div> /div> div classform-grid> div classform-group> label>총 중량 (kg) */label> input typenumber idactual-weight min1 step0.1 required> /div> div classform-group> label>총 두수 */label> input typenumber idactual-headcount min1 required> /div> div classform-group> label>두당 무게 (kg)/label> input typetext idactual-avg-weight readonly> /div> /div> !-- 돈사별 출하 두수 입력 --> div idbuilding-input-container styledisplay: none;> div classbuilding-input-section> div classbuilding-input-header>🏠 돈사별 출하 두수 입력 (비육돈에서 차감됩니다)/div> div classbuilding-input-grid idbuilding-inputs>/div> div classtotal-check idtotal-check> span>돈사별 합계:/span> span idbuilding-total>0두/span> /div> /div> /div> input typehidden idselected-scheduled-id> button typesubmit classbtn btn-primary>등록하기/button> /form> /div> !-- 돈사 재고 입력 --> div classform-section> h3>🏠 돈사 두수 입력/h3> div classform-grid> div classform-group> label>지역 */label> select idinventory-location onchangeupdateBuildingOptions()> option value>선택하세요/option> option value영광>영광/option> option value함평>함평/option> /select> /div> div classform-group> label>돈사 */label> select idinventory-building onchangeloadInventoryData()> option value>먼저 지역을 선택하세요/option> /select> /div> /div> form idinventoryForm> div classform-grid> div classform-group> label>육성돈 두수/label> input typenumber idinventory-grower min0 value0> /div> div classform-group> label>비육돈 두수/label> input typenumber idinventory-finisher min0 value0> /div> div classform-group> label>사고 두수/label> input typenumber idinventory-accident min0 value0> /div> /div> button typesubmit classbtn btn-primary>입력하기/button> /form> /div> /div> !-- 탭 2: 대시보드 --> div idtab-dashboard classtab-content> div iddashboard-loading classloading> div classloading-spinner>/div> p>데이터를 불러오는 중.../p> /div> div iddashboard-content styledisplay: none;> !-- 돈사별 재고 --> div idinventory-section>/div> h3 stylemargin: 30px 0 20px; color: var(--text-main); font-weight: 700;>📅 이번 주 출하 예정/h3> div idweekly-schedule>/div> h3 stylemargin: 30px 0 20px; color: var(--text-main); font-weight: 700;>📊 이번 달 출하 실적/h3> div idmonthly-stats classdashboard-grid>/div> h3 stylemargin: 30px 0 20px; color: var(--text-main); font-weight: 700;>📝 전체 출하 기록/h3> !-- 필터 및 검색 -->div stylebackground: white; padding: 16px; border-radius: 12px; margin-bottom: 16px; border: 1px solid #ECEFF1;> div styledisplay: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; align-items: end;> div> label styledisplay: block; margin-bottom: 4px; font-size: 0.85em; color: var(--text-sub);>시작 날짜/label> input typedate idfilter-date-start stylewidth: 100%; padding: 8px; border: 1px solid #ECEFF1; border-radius: 6px;> /div> div> label styledisplay: block; margin-bottom: 4px; font-size: 0.85em; color: var(--text-sub);>종료 날짜/label> input typedate idfilter-date-end stylewidth: 100%; padding: 8px; border: 1px solid #ECEFF1; border-radius: 6px;> /div> div> label styledisplay: block; margin-bottom: 4px; font-size: 0.85em; color: var(--text-sub);>지역 필터/label> select idfilter-location stylewidth: 100%; padding: 8px; border: 1px solid #ECEFF1; border-radius: 6px;> option value>전체/option> option value영광>영광/option> option value함평>함평/option> /select> /div> div styledisplay: flex; gap: 8px;> button classbtn btn-primary onclickapplyFilters() styleflex: 1;>🔍 검색/button> button classbtn onclickresetFilters() styleflex: 1;>↺ 초기화/button> /div> /div> /div> div idrecent-shipments>/div> !-- 페이지네이션 --> div idpagination styledisplay: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 20px; flex-wrap: wrap;>/div> /div> /div> !-- 탭 3: 달력 --> div idtab-calendar classtab-content> div classcalendar> div classcalendar-header> h3 idcalendar-title>/h3> div classcalendar-nav> button onclickchangeMonth(-1)>◀ 이전/button> button onclickchangeMonth(0)>오늘/button> button onclickchangeMonth(1)>다음 ▶/button> /div> /div> div classcalendar-grid> div classcalendar-day-header>일/div> div classcalendar-day-header>월/div> div classcalendar-day-header>화/div> div classcalendar-day-header>수/div> div classcalendar-day-header>목/div> div classcalendar-day-header>금/div> div classcalendar-day-header>토/div> /div> div idcalendar-days classcalendar-grid>/div> /div> /div> !-- 탭 4: 등지방 분포도 --> div idtab-backfat classtab-content> div classform-section> h3>📈 등지방 분포도/h3> !-- 파일 업로드 & 히스토리 --> div styledisplay: grid; grid-template-columns: 1fr auto; gap: 16px; margin-bottom: 24px;> div> label styledisplay: block; margin-bottom: 8px; font-weight: 600;>Excel 파일 업로드/label> input typefile idbackfatFile accept.xls,.xlsx stylepadding: 12px; border: 1px solid #ECEFF1; border-radius: 8px; width: 100%;> div styledisplay: flex; gap: 12px; margin-top: 12px;> button classbtn btn-primary onclickprocessBackfatFile()>📊 분석하기/button> button classbtn btn-secondary onclicksaveCurrentBackfatData() idsaveBackfatBtn styledisplay: none;>💾 데이터 저장/button> /div> /div> div> button classbtn btn-secondary onclickshowBackfatHistory() styleheight: 100%; min-width: 120px;> 📁 히스토리br/>보기 /button> /div> /div> !-- 통계 요약 --> div idbackfat-stats styledisplay: none; margin-bottom: 24px;> div styledisplay: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;> div stylebackground: var(--light-blue); padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>총 두수/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--primary-blue); idtotal-count>0/div> /div> div stylebackground: #FFF3E0; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>1+ 등급/div> div stylefont-size: 1.8em; font-weight: 700; color: #EF6C00; idgrade-1plus>0/div> /div> div stylebackground: #E8F5E9; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>1 등급/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--success-green); idgrade-1>0/div> /div> div stylebackground: #FFEBEE; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>2 등급/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--urgent-red); idgrade-2>0/div> /div> div stylebackground: #F5F5F5; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>평균 도체중/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--text-main); idavg-weight>0/div> /div> div stylebackground: #F5F5F5; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>평균 등지방/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--text-main); idavg-backfat>0/div> /div> /div> /div> !-- AI 분석 --> div idai-analysis styledisplay: none; margin-bottom: 24px;> div stylebackground: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white;> div styledisplay: flex; align-items: center; gap: 10px; margin-bottom: 16px;> div stylefont-size: 1.5em;>🤖/div> h3 stylemargin: 0; font-size: 1.1em;>AI 분석 리포트/h3> /div> div idai-comments styledisplay: flex; flex-direction: column; gap: 10px;>/div> /div> /div> !-- 개선 제안 --> div idai-suggestions styledisplay: none; margin-bottom: 24px;> div stylebackground: white; border: 2px solid #E8F5E9; padding: 20px; border-radius: 12px;> div styledisplay: flex; align-items: center; gap: 10px; margin-bottom: 16px;> div stylefont-size: 1.5em;>💡/div> h3 stylemargin: 0; font-size: 1.1em; color: var(--text-main);>개선 제안/h3> /div> div idsuggestion-list styledisplay: flex; flex-direction: column; gap: 12px;>/div> /div> /div> !-- 분포도 차트 --> div idbackfat-chart styledisplay: none;> canvas idchartCanvas stylewidth: 100%; max-width: 1200px; margin: 0 auto; display: block;>/canvas> /div> !-- 안내 메시지 --> div idbackfat-guide styletext-align: center; padding: 60px 20px; color: var(--text-sub);> div stylefont-size: 3em; margin-bottom: 16px;>📊/div> div stylefont-size: 1.1em; font-weight: 600; margin-bottom: 8px;>등지방 분포도/div> div>Excel 파일을 업로드하면 등지방 분포도를 확인할 수 있습니다./div> /div> /div> /div> /div> !-- 모달 --> div idmodal classmodal> div classmodal-content> div classmodal-header> h3 idmodal-title>/h3> button classmodal-close onclickcloseModal()>×/button> /div> div idmodal-body>/div> /div> /div> !-- 등지방 히스토리 모달 --> div idbackfat-history-modal classmodal> div classmodal-content> div classmodal-header> h3>📁 등지방 분석 히스토리/h3> button classmodal-close onclickcloseBackfatHistoryModal()>×/button> /div> div idbackfat-history-body stylemax-height: 500px; overflow-y: auto;> div idbackfat-history-list>/div> /div> /div> /div> script> const SCRIPT_URL https://script.google.com/macros/s/AKfycbxdrTsPPckiyfT5o8xBG5wbWnCPYJkqRVODY0GLvS-2rB5ZZaSfCW_jvYhngrZvoc915A/exec; let currentYear new Date().getFullYear(); let currentMonth new Date().getMonth(); let calendarData {}; let currentScheduledList ; let currentInventory {}; let currentBackfatData null; // 현재 분석 중인 등지방 데이터 // 페이지네이션 관련 변수 let allShipments ; // 전체 출하 기록 let filteredShipments ; // 필터링된 출하 기록 let currentPage 1; let itemsPerPage 10; // 페이지당 항목 수 let currentSortColumn null; let currentSortDirection desc; // asc or desc // API 호출 async function callAPI(action, data null) { try { let url `${SCRIPT_URL}?action${action}`; if (data) { Object.keys(data).forEach(key > { url + `&${key}${encodeURIComponent(datakey)}`; }); } const response await fetch(url, { method: GET, redirect: follow }); return await response.json(); } catch (error) { console.error(API 호출 오류:, error); return { success: false, message: error.message }; } } // 탭 전환 function switchTab(tabName) { document.querySelectorAll(.tab-card).forEach(tab > tab.classList.remove(active)); document.querySelectorAll(.tab-content).forEach(content > content.classList.remove(active)); event.target.closest(.tab-card).classList.add(active); document.getElementById(`tab-${tabName}`).classList.add(active); if (tabName dashboard) { loadDashboardData(); } if (tabName calendar) { loadCalendar(); } } // 예정 두수 입력 토글 function toggleScheduledHeadcountInput() { const type document.getElementById(scheduled-headcount-type).value; const customGroup document.getElementById(scheduled-headcount-custom-group); const customInput document.getElementById(scheduled-headcount-custom); if (type custom) { customGroup.style.display block; customInput.required true; } else { customGroup.style.display none; customInput.required false; } } // 도축장 입력 토글 (출하 예정) function toggleSlaughterhouseInput() { const type document.getElementById(scheduled-slaughterhouse-type).value; const customGroup document.getElementById(scheduled-slaughterhouse-custom-group); const customInput document.getElementById(scheduled-slaughterhouse-custom); if (type custom) { customGroup.style.display block; customInput.required true; } else { customGroup.style.display none; customInput.required false; } } // 도축장 입력 토글 (출하 등록) function toggleActualSlaughterhouseInput() { const type document.getElementById(actual-slaughterhouse-type).value; const customGroup document.getElementById(actual-slaughterhouse-custom-group); const customInput document.getElementById(actual-slaughterhouse-custom); if (type custom) { customGroup.style.display block; customInput.required true; } else { customGroup.style.display none; customInput.required false; } } // 출하 날짜 선택 시 출하 예정 목록 로드 async function loadScheduledForDate() { const date document.getElementById(actual-date).value; if (!date) return; const result await callAPI(getScheduledByDate, { date: date }); if (result.success && result.scheduled.length > 0) { currentScheduledList result.scheduled; renderScheduledList(result.scheduled); document.getElementById(scheduled-list-container).style.display block; } else { document.getElementById(scheduled-list-container).style.display none; currentScheduledList ; } } // 출하 예정 목록 렌더링 function renderScheduledList(scheduledList) { const container document.getElementById(scheduled-list); container.innerHTML ; scheduledList.forEach(item > { const div document.createElement(div); div.className scheduled-item; div.onclick () > selectScheduledItem(item); div.innerHTML ` div classscheduled-item-header>${item.location} - ${item.headCount}두/div> div classscheduled-item-details> 도축장: ${item.slaughterhouse || -} /div> `; container.appendChild(div); }); } // 출하 예정 항목 선택 function selectScheduledItem(item) { document.querySelectorAll(.scheduled-item).forEach(el > el.classList.remove(selected)); event.currentTarget.classList.add(selected); document.getElementById(actual-location).value item.location; // 도축장 선택 const slaughterhouseType document.getElementById(actual-slaughterhouse-type); const options Array.from(slaughterhouseType.options); const matchingOption options.find(opt > opt.value item.slaughterhouse); if (matchingOption) { slaughterhouseType.value item.slaughterhouse; toggleActualSlaughterhouseInput(); } else if (item.slaughterhouse) { slaughterhouseType.value custom; toggleActualSlaughterhouseInput(); document.getElementById(actual-slaughterhouse-custom).value item.slaughterhouse; } document.getElementById(selected-scheduled-id).value item.id; // 돈사 목록 로드 loadBuildingsByLocation(); } // 지역별 돈사 목록 로드 및 재고 가져오기 (한 번에) async function loadBuildingsByLocation() { const location document.getElementById(actual-location).value; if (!location) { document.getElementById(building-input-container).style.display none; return; } const buildings location 영광 ? 비육사 1동, 비육사 3동, 비육사 4동, 비육사 5동 : 비육장; // 한 번의 요청으로 모든 돈사 재고 가져오기 const result await callAPI(getInventoryByLocation, { location }); const buildingData {}; if (result.success && result.inventory) { buildings.forEach(building > { if (result.inventorybuilding) { buildingDatabuilding result.inventorybuilding; } }); } renderBuildingInputs(buildings, buildingData); document.getElementById(building-input-container).style.display block; } // 돈사별 입력 필드 렌더링 function renderBuildingInputs(buildings, buildingData) { const container document.getElementById(building-inputs); container.innerHTML ; buildings.forEach(building > { const data buildingDatabuilding || {}; const finisherCount data.finisherCount || 0; const div document.createElement(div); div.className building-input-item; div.innerHTML ` label>${building}/label> input typenumber classbuilding-count-input data-building${building} data-max${finisherCount} min0 max${finisherCount} value0 placeholder재고: ${finisherCount}두 oninputupdateBuildingTotal() > `; container.appendChild(div); }); updateBuildingTotal(); } // 돈사별 합계 업데이트 function updateBuildingTotal() { const inputs document.querySelectorAll(.building-count-input); let total 0; let hasExceeded false; inputs.forEach(input > { const value parseInt(input.value) || 0; const max parseInt(input.dataset.max) || 0; total + value; if (value > max) { input.style.borderColor var(--urgent-red); hasExceeded true; } else { input.style.borderColor #ECEFF1; } }); const targetTotal parseInt(document.getElementById(actual-headcount).value) || 0; const totalCheck document.getElementById(total-check); const buildingTotal document.getElementById(building-total); buildingTotal.textContent `${total}두`; totalCheck.classList.remove(match, mismatch); if (total targetTotal && total > 0) { totalCheck.classList.add(match); totalCheck.querySelector(span:first-child).textContent ✓ 합계 일치:; } else if (total > 0) { totalCheck.classList.add(mismatch); totalCheck.querySelector(span:first-child).textContent `⚠ 합계 불일치 (목표: ${targetTotal}두):`; } if (hasExceeded) { showFeedbackModal(⚠️ 재고 부족, 일부 돈사의 입력 두수가 재고를 초과했습니다., warning); } } // 총 두수 변경 시 돈사별 합계 업데이트 document.getElementById(actual-headcount)?.addEventListener(input, updateBuildingTotal); // 두당 무게 자동 계산 document.getElementById(actual-weight)?.addEventListener(input, calculateAvgWeight); document.getElementById(actual-headcount)?.addEventListener(input, calculateAvgWeight); function calculateAvgWeight() { const weight parseFloat(document.getElementById(actual-weight).value) || 0; const headCount parseInt(document.getElementById(actual-headcount).value) || 0; if (weight > 0 && headCount > 0) { const avgWeight (weight / headCount).toFixed(2); document.getElementById(actual-avg-weight).value avgWeight; } else { document.getElementById(actual-avg-weight).value ; } } // 피드백 모달 (간단한 알림용) function showFeedbackModal(title, message, type success) { const modal document.getElementById(modal); const modalTitle document.getElementById(modal-title); const modalBody document.getElementById(modal-body); const icon type success ? ✅ : type error ? ❌ : ⚠️; modalTitle.textContent title; modalBody.innerHTML ` div classmodal-icon>${icon}/div> div classmodal-message>${message}/div> button classbtn btn-primary onclickcloseModal()>확인/button> `; modal.classList.add(active); } // 출하 예정 수정 모달 function showEditScheduledModal(item) { const modal document.getElementById(modal); const modalTitle document.getElementById(modal-title); const modalBody document.getElementById(modal-body); modalTitle.textContent 출하 예정 수정; modalBody.innerHTML ` form ideditScheduledForm> div classform-grid> div classform-group> label>날짜 */label> input typedate idedit-scheduled-date value${item.date} required> /div> div classform-group> label>지역 */label> select idedit-scheduled-location required> option value영광 ${item.location 영광 ? selected : }>영광/option> option value함평 ${item.location 함평 ? selected : }>함평/option> /select> /div> div classform-group> label>예정 두수 */label> input typenumber idedit-scheduled-headcount value${item.headCount} min1 required> /div> div classform-group> label>도축장 */label> input typetext idedit-scheduled-slaughterhouse value${item.slaughterhouse || } required> /div> /div> input typehidden idedit-scheduled-id value${item.id}> div classmodal-buttons> button typesubmit classbtn btn-primary>저장/button> button typebutton classbtn btn-danger onclickdeleteScheduled(${item.id})>삭제/button> button typebutton classbtn btn-secondary onclickcloseModal()>취소/button> /div> /form> `; modal.classList.add(active); document.getElementById(editScheduledForm).addEventListener(submit, async function(e) { e.preventDefault(); const data { id: document.getElementById(edit-scheduled-id).value, date: document.getElementById(edit-scheduled-date).value, location: document.getElementById(edit-scheduled-location).value, headCount: document.getElementById(edit-scheduled-headcount).value, slaughterhouse: document.getElementById(edit-scheduled-slaughterhouse).value, meatProcessor: }; const result await callAPI(updateScheduled, data); if (result.success) { showFeedbackModal(✅ 수정 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 수정 실패, result.message, error); } }); } // 출하 예정 삭제 async function deleteScheduled(id) { if (!confirm(정말 삭제하시겠습니까?)) return; const result await callAPI(deleteScheduled, { id: id }); if (result.success) { showFeedbackModal(✅ 삭제 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 삭제 실패, result.message, error); } } // 출하 기록 수정 모달 function showEditActualModal(item) { const modal document.getElementById(modal); const modalTitle document.getElementById(modal-title); const modalBody document.getElementById(modal-body); modalTitle.textContent 출하 기록 수정; modalBody.innerHTML ` form ideditActualForm> div classform-grid> div classform-group> label>날짜 */label> input typedate idedit-actual-date value${item.date} required> /div> div classform-group> label>지역 */label> select idedit-actual-location required> option value영광 ${item.location 영광 ? selected : }>영광/option> option value함평 ${item.location 함평 ? selected : }>함평/option> /select> /div> div classform-group> label>총 중량 (kg) */label> input typenumber idedit-actual-weight value${item.totalWeight} min1 step0.1 required> /div> div classform-group> label>두수 */label> input typenumber idedit-actual-headcount value${item.headCount} min1 required> /div> div classform-group> label>도축장 */label> input typetext idedit-actual-slaughterhouse value${item.slaughterhouse || } required> /div> /div> p stylecolor: var(--warning-orange); text-align: center; margin-top: 16px; font-weight: 600;> ⚠️ 삭제 시 재고가 복구됩니다 /p> input typehidden idedit-actual-id value${item.id}> div classmodal-buttons> button typesubmit classbtn btn-primary>저장/button> button typebutton classbtn btn-danger onclickdeleteActual(${item.id}, ${item.date}, ${item.location})>삭제/button> button typebutton classbtn btn-secondary onclickcloseModal()>취소/button> /div> /form> `; modal.classList.add(active); document.getElementById(editActualForm).addEventListener(submit, async function(e) { e.preventDefault(); const data { id: document.getElementById(edit-actual-id).value, date: document.getElementById(edit-actual-date).value, location: document.getElementById(edit-actual-location).value, totalWeight: document.getElementById(edit-actual-weight).value, headCount: document.getElementById(edit-actual-headcount).value, slaughterhouse: document.getElementById(edit-actual-slaughterhouse).value, meatProcessor: }; const result await callAPI(updateActual, data); if (result.success) { showFeedbackModal(✅ 수정 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 수정 실패, result.message, error); } }); } // 출하 기록 삭제 (재고 복구) async function deleteActual(id, date, location) { if (!confirm(정말 삭제하시겠습니까?\n재고가 복구됩니다.)) return; const result await callAPI(deleteActual, { id: id, date: date, location: location }); if (result.success) { showFeedbackModal(✅ 삭제 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 삭제 실패, result.message, error); } } // 출하 완료 버튼 async function completeShipment(location, building) { if (!confirm(`${building}의 출하를 완료하시겠습니까?\n출하 두수와 사고 두수가 리셋됩니다.`)) return; const result await callAPI(completeShipment, { location: location, building: building }); if (result.success) { showFeedbackModal(✅ 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 실패, result.message, error); } } // 돈사 전환 버튼 async function transferBuilding(location, building) { if (!confirm(`${building}의 육성돈을 비육돈으로 전환하시겠습니까?`)) return; const result await callAPI(transferBuilding, { location: location, building: building }); if (result.success) { showFeedbackModal(✅ 전환 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 전환 실패, result.message, error); } } function closeModal() { document.getElementById(modal).classList.remove(active); } // 돈사 옵션 업데이트 function updateBuildingOptions() { const location document.getElementById(inventory-location).value; const buildingSelect document.getElementById(inventory-building); buildingSelect.innerHTML option value>선택하세요/option>; if (location 영광) { 비육사 1동, 비육사 3동, 비육사 4동, 비육사 5동.forEach(building > { const option document.createElement(option); option.value building; option.textContent building; buildingSelect.appendChild(option); }); } else if (location 함평) { const option document.createElement(option); option.value 비육장; option.textContent 비육장; buildingSelect.appendChild(option); } } // 돈사 선택 시 마지막 재고 데이터 로드 async function loadInventoryData() { const location document.getElementById(inventory-location).value; const building document.getElementById(inventory-building).value; if (!location || !building) return; const result await callAPI(getInventoryByBuilding, { location, building }); if (result.success && result.inventory) { document.getElementById(inventory-grower).value result.inventory.growerCount; document.getElementById(inventory-finisher).value result.inventory.finisherCount; document.getElementById(inventory-accident).value result.inventory.accidentCount; } } // 출하 예정 등록 document.getElementById(scheduledForm).addEventListener(submit, async function(e) { e.preventDefault(); const submitBtn e.target.querySelector(buttontypesubmit); submitBtn.disabled true; submitBtn.textContent 등록 중...; const headcountType document.getElementById(scheduled-headcount-type).value; const headCount headcountType 80 ? 80 : document.getElementById(scheduled-headcount-custom).value; const slaughterhouseType document.getElementById(scheduled-slaughterhouse-type).value; const slaughterhouse slaughterhouseType custom ? document.getElementById(scheduled-slaughterhouse-custom).value : slaughterhouseType; const data { date: document.getElementById(scheduled-date).value, location: document.getElementById(scheduled-location).value, headCount: headCount, slaughterhouse: slaughterhouse, meatProcessor: }; const result await callAPI(addScheduled, data); if (result.success) { showFeedbackModal(✅ 등록 완료, result.message, success); document.getElementById(scheduledForm).reset(); } else { showFeedbackModal(❌ 등록 실패, result.message, error); } submitBtn.disabled false; submitBtn.textContent 등록하기; }); // 출하 등록 document.getElementById(actualForm).addEventListener(submit, async function(e) { e.preventDefault(); // 돈사별 합계 검증 const inputs document.querySelectorAll(.building-count-input); const buildingDetails {}; let buildingTotal 0; inputs.forEach(input > { const building input.dataset.building; const count parseInt(input.value) || 0; buildingDetailsbuilding count; buildingTotal + count; }); const targetTotal parseInt(document.getElementById(actual-headcount).value) || 0; if (buildingTotal ! targetTotal) { showFeedbackModal(⚠️ 두수 불일치, `돈사별 합계(${buildingTotal}두)와 총 두수(${targetTotal}두)가 일치하지 않습니다.`, warning); return; } const submitBtn e.target.querySelector(buttontypesubmit); submitBtn.disabled true; submitBtn.textContent 등록 중...; const slaughterhouseType document.getElementById(actual-slaughterhouse-type).value; const slaughterhouse slaughterhouseType custom ? document.getElementById(actual-slaughterhouse-custom).value : slaughterhouseType; const data { date: document.getElementById(actual-date).value, location: document.getElementById(actual-location).value, totalWeight: document.getElementById(actual-weight).value, headCount: document.getElementById(actual-headcount).value, slaughterhouse: slaughterhouse, meatProcessor: , scheduledId: document.getElementById(selected-scheduled-id).value, buildingDetails: JSON.stringify(buildingDetails) }; const result await callAPI(addActual, data); if (result.success) { showFeedbackModal(✅ 등록 완료, result.message, success); document.getElementById(actualForm).reset(); document.getElementById(actual-avg-weight).value ; document.getElementById(scheduled-list-container).style.display none; document.getElementById(building-input-container).style.display none; } else { showFeedbackModal(❌ 등록 실패, result.message, error); } submitBtn.disabled false; submitBtn.textContent 등록하기; }); // 돈사 재고 입력 document.getElementById(inventoryForm).addEventListener(submit, async function(e) { e.preventDefault(); const location document.getElementById(inventory-location).value; const building document.getElementById(inventory-building).value; if (!location || !building) { showFeedbackModal(⚠️ 입력 오류, 지역과 돈사를 선택해주세요., warning); return; } const submitBtn e.target.querySelector(buttontypesubmit); submitBtn.disabled true; submitBtn.textContent 입력 중...; const data { location: location, building: building, growerCount: document.getElementById(inventory-grower).value, finisherCount: document.getElementById(inventory-finisher).value, accidentCount: document.getElementById(inventory-accident).value }; const result await callAPI(addInventory, data); if (result.success) { showFeedbackModal(✅ 입력 완료, result.message, success); } else { showFeedbackModal(❌ 입력 실패, result.message, error); } submitBtn.disabled false; submitBtn.textContent 입력하기; }); // 대시보드 데이터 로드 async function loadDashboardData() { setSyncStatus(syncing, 불러오는 중...); document.getElementById(dashboard-loading).style.display block; document.getElementById(dashboard-content).style.display none; const result await callAPI(getDashboard); if (result.success) { renderDashboard(result); document.getElementById(dashboard-loading).style.display none; document.getElementById(dashboard-content).style.display block; setSyncStatus(synced, 불러오기 완료); } else { showFeedbackModal(❌ 로드 실패, 데이터 로드 실패: + result.message, error); document.getElementById(dashboard-loading).style.display none; setSyncStatus(error, 불러오기 실패); } } // 대시보드 렌더링 function renderDashboard(data) { renderInventorySection(data.inventory); renderWeeklySchedule(data.weeklySchedule); renderMonthlyStats(data.monthlyStats); renderRecentShipments(data.recentShipments); } // 재고 섹션 렌더링 function renderInventorySection(inventory) { const container document.getElementById(inventory-section); container.innerHTML ; // 영광 카드 const youngkwangCard document.createElement(div); youngkwangCard.className region-card; const youngkwangBuildings 비육사 1동, 비육사 3동, 비육사 4동, 비육사 5동; let youngkwangBuildingsHTML ; youngkwangBuildings.forEach(building > { const data inventory.youngkwangbuilding || { currentCount: 0, finisherCount: 0, growerCount: 0, accidentCount: 0, shipmentCount: 0 }; youngkwangBuildingsHTML + ` div classbuilding-card> div classbuilding-name> span>${building}/span> span classbuilding-total>(${data.currentCount}두)/span> /div> div classbuilding-stats> div classstat-item> div classstat-value>${data.finisherCount}/div> div classstat-label>비육돈/div> /div> div classstat-item> div classstat-value>${data.growerCount}/div> div classstat-label>육성돈/div> /div> div classstat-item> div classstat-value>${data.accidentCount}/div> div classstat-label>사고/div> /div> div classstat-item> div classstat-value>${data.shipmentCount}/div> div classstat-label>출하/div> /div> /div> div classbuilding-actions> button classbtn-complete onclickcompleteShipment(영광, ${building})>출하완료/button> button classbtn-transfer onclicktransferBuilding(영광, ${building})>돈사전환/button> /div> /div> `; }); youngkwangCard.innerHTML ` div classregion-header> span classregion-title>🏠 영광 비육사/span> span classregion-total>(총 ${inventory.youngkwangTotal}두)/span> /div> div classbuildings-grid> ${youngkwangBuildingsHTML} /div> `; container.appendChild(youngkwangCard); // 함평 카드 const hampyeongCard document.createElement(div); hampyeongCard.className region-card; const hampyeongData inventory.hampyeong비육장 || { currentCount: 0, finisherCount: 0, growerCount: 0, accidentCount: 0, shipmentCount: 0 }; hampyeongCard.innerHTML ` div classregion-header> span classregion-title>🏠 함평 비육장/span> span classregion-total>(총 ${inventory.hampyeongTotal}두)/span> /div> div classbuildings-grid> div classbuilding-card> div classbuilding-name> span>비육장/span> span classbuilding-total>(${hampyeongData.currentCount}두)/span> /div> div classbuilding-stats> div classstat-item> div classstat-value>${hampyeongData.finisherCount}/div> div classstat-label>비육돈/div> /div> div classstat-item> div classstat-value>${hampyeongData.growerCount}/div> div classstat-label>육성돈/div> /div> div classstat-item> div classstat-value>${hampyeongData.accidentCount}/div> div classstat-label>사고/div> /div> div classstat-item> div classstat-value>${hampyeongData.shipmentCount}/div> div classstat-label>출하/div> /div> /div> div classbuilding-actions> button classbtn-complete onclickcompleteShipment(함평, 비육장)>출하완료/button> button classbtn-transfer onclicktransferBuilding(함평, 비육장)>돈사전환/button> /div> /div> /div> `; container.appendChild(hampyeongCard); } // 주간 출하 예정 렌더링 function renderWeeklySchedule(schedule) { const container document.getElementById(weekly-schedule); if (schedule.length 0) { container.innerHTML div classempty-state>div classempty-state-icon>📭/div>p>이번 주 출하 예정이 없습니다./p>/div>; return; } container.innerHTML div classtable-container>table>thead>tr>th>날짜/th>th>지역/th>th>예정 두수/th>th>도축장/th>th>상태/th>th>작업/th>/tr>/thead>tbody idschedule-tbody>/tbody>/table>/div>; const tbody document.getElementById(schedule-tbody); schedule.forEach(item > { const row document.createElement(tr); row.innerHTML ` td>${item.date}/td> td>${item.location}/td> td>${item.headCount}두/td> td>${item.slaughterhouse || -}/td> td>span classrecord-status status-scheduled>${item.status}/span>/td> td> button classbtn btn-primary btn-small onclickshowEditScheduledModal(${JSON.stringify(item)})>수정/button> /td> `; tbody.appendChild(row); }); } // 월간 통계 렌더링 function renderMonthlyStats(stats) { const container document.getElementById(monthly-stats); container.innerHTML ` div classcard> div classcard-header> div classcard-title>총 출하 두수/div> div classcard-icon>🐷/div> /div> div classcard-value>${stats.totalHeadCount}/div> div classcard-label>두/div> /div> div classcard> div classcard-header> div classcard-title>총 중량/div> div classcard-icon>⚖️/div> /div> div classcard-value>${Number(stats.totalWeight).toLocaleString()}/div> div classcard-label>kg/div> /div> div classcard> div classcard-header> div classcard-title>평균 두당 무게/div> div classcard-icon>📊/div> /div> div classcard-value>${stats.avgWeight}/div> div classcard-label>kg/div> /div> div classcard> div classcard-header> div classcard-title>지역별 비율/div> div classcard-icon>📍/div> /div> div stylemargin-top: 15px;> div styledisplay: flex; justify-content: space-between; margin-bottom: 10px;> span>영광/span> strong>${stats.youngkwangRatio}%/strong> /div> div styledisplay: flex; justify-content: space-between;> span>함평/span> strong>${stats.hampyeongRatio}%/strong> /div> /div> /div> `; } // 최근 출하 기록 렌더링 (페이지네이션 및 필터 적용) function renderRecentShipments(shipments) { allShipments shipments || ; filteredShipments ...allShipments; currentPage 1; renderShipmentsTable(); } // 출하 기록 테이블 렌더링 function renderShipmentsTable() { const container document.getElementById(recent-shipments); if (filteredShipments.length 0) { container.innerHTML div classempty-state>div classempty-state-icon>📭/div>p>출하 기록이 없습니다./p>/div>; document.getElementById(pagination).innerHTML ; return; } // 페이지네이션 계산 const totalPages Math.ceil(filteredShipments.length / itemsPerPage); const startIndex (currentPage - 1) * itemsPerPage; const endIndex Math.min(startIndex + itemsPerPage, filteredShipments.length); const pageData filteredShipments.slice(startIndex, endIndex); // 테이블 헤더 (정렬 가능) const sortIcon (column) > { if (currentSortColumn column) { return currentSortDirection asc ? ▲ : ▼; } return ; }; container.innerHTML ` div classtable-container> table> thead> tr> th onclicksortTable(date) stylecursor: pointer;>날짜${sortIcon(date)}/th> th onclicksortTable(location) stylecursor: pointer;>지역${sortIcon(location)}/th> th onclicksortTable(headCount) stylecursor: pointer;>두수${sortIcon(headCount)}/th> th onclicksortTable(totalWeight) stylecursor: pointer;>총 중량${sortIcon(totalWeight)}/th> th onclicksortTable(avgWeight) stylecursor: pointer;>평균 두당 무게${sortIcon(avgWeight)}/th> th>작업/th> /tr> /thead> tbody idshipments-tbody>/tbody> /table> /div> div styletext-align: center; margin-top: 12px; color: var(--text-sub); font-size: 0.9em;> 전체 ${filteredShipments.length}건 중 ${startIndex + 1}-${endIndex}번째 표시 /div> `; const tbody document.getElementById(shipments-tbody); pageData.forEach(item > { const row document.createElement(tr); row.innerHTML ` td>${item.date}/td> td>${item.location}/td> td>${item.headCount}두/td> td>${Number(item.totalWeight).toLocaleString()}kg/td> td>${item.avgWeight}kg/td> td> button classbtn btn-primary btn-small onclickshowEditActualModal(${JSON.stringify(item)})>수정/button> /td> `; tbody.appendChild(row); }); // 페이지네이션 렌더링 renderPagination(totalPages); } // 페이지네이션 UI 렌더링 function renderPagination(totalPages) { const container document.getElementById(pagination); if (totalPages 1) { container.innerHTML ; return; } let html ; // 이전 버튼 if (currentPage > 1) { html + `button classbtn btn-small onclickgoToPage(${currentPage - 1})>◀/button>`; } // 페이지 번호 const maxVisible 5; // 최대 표시할 페이지 번호 개수 let startPage Math.max(1, currentPage - Math.floor(maxVisible / 2)); let endPage Math.min(totalPages, startPage + maxVisible - 1); if (endPage - startPage maxVisible - 1) { startPage Math.max(1, endPage - maxVisible + 1); } // 첫 페이지 if (startPage > 1) { html + `button classbtn btn-small onclickgoToPage(1)>1/button>`; if (startPage > 2) { html + `span stylepadding: 0 8px; color: var(--text-sub);>.../span>`; } } // 페이지 번호들 for (let i startPage; i endPage; i++) { if (i currentPage) { html + `button classbtn btn-primary btn-small stylefont-weight: 700;>${i}/button>`; } else { html + `button classbtn btn-small onclickgoToPage(${i})>${i}/button>`; } } // 마지막 페이지 if (endPage totalPages) { if (endPage totalPages - 1) { html + `span stylepadding: 0 8px; color: var(--text-sub);>.../span>`; } html + `button classbtn btn-small onclickgoToPage(${totalPages})>${totalPages}/button>`; } // 다음 버튼 if (currentPage totalPages) { html + `button classbtn btn-small onclickgoToPage(${currentPage + 1})>▶/button>`; } container.innerHTML html; } // 페이지 이동 function goToPage(page) { currentPage page; renderShipmentsTable(); // 스크롤을 테이블 위로 이동 document.getElementById(recent-shipments).scrollIntoView({ behavior: smooth, block: start }); } // 테이블 정렬 function sortTable(column) { if (currentSortColumn column) { // 같은 컬럼 클릭 시 방향 전환 currentSortDirection currentSortDirection asc ? desc : asc; } else { currentSortColumn column; currentSortDirection desc; } filteredShipments.sort((a, b) > { let aVal acolumn; let bVal bcolumn; // 숫자형 데이터 처리 if (column headCount || column totalWeight) { aVal Number(aVal) || 0; bVal Number(bVal) || 0; } else if (column avgWeight) { aVal parseFloat(aVal) || 0; bVal parseFloat(bVal) || 0; } if (currentSortDirection asc) { return aVal > bVal ? 1 : -1; } else { return aVal bVal ? 1 : -1; } }); currentPage 1; // 정렬 후 첫 페이지로 renderShipmentsTable(); } // 필터 적용 function applyFilters() { const dateStartFilter document.getElementById(filter-date-start).value; const dateEndFilter document.getElementById(filter-date-end).value; const locationFilter document.getElementById(filter-location).value; filteredShipments allShipments.filter(item > { let match true; // 시작 날짜 필터 if (dateStartFilter && item.date dateStartFilter) { match false; } // 종료 날짜 필터 if (dateEndFilter && item.date > dateEndFilter) { match false; } // 지역 필터 if (locationFilter && item.location ! locationFilter) { match false; } return match; }); currentPage 1; renderShipmentsTable(); } // 필터 초기화 function resetFilters() { document.getElementById(filter-date-start).value ; document.getElementById(filter-date-end).value ; document.getElementById(filter-location).value ; filteredShipments ...allShipments; currentPage 1; currentSortColumn null; currentSortDirection desc; renderShipmentsTable(); } // 달력 로드 async function loadCalendar() { setSyncStatus(syncing, 달력 로딩 중...); const title `${currentYear}년 ${currentMonth + 1}월`; document.getElementById(calendar-title).textContent title; const result await callAPI(getCalendar, { year: currentYear, month: currentMonth }); if (result.success) { calendarData result.calendar; renderCalendar(); setSyncStatus(synced, 달력 로드 완료); } else { setSyncStatus(error, 달력 로드 실패); } } // 달력 렌더링 function renderCalendar() { const container document.getElementById(calendar-days); container.innerHTML ; const firstDay new Date(currentYear, currentMonth, 1).getDay(); const daysInMonth new Date(currentYear, currentMonth + 1, 0).getDate(); const today new Date(); const prevMonthDays new Date(currentYear, currentMonth, 0).getDate(); for (let i firstDay - 1; i > 0; i--) { const day prevMonthDays - i; const dayDiv createDayElement(day, true, false); container.appendChild(dayDiv); } for (let day 1; day daysInMonth; day++) { const date new Date(currentYear, currentMonth, day); const isToday date.toDateString() today.toDateString(); const dayDiv createDayElement(day, false, isToday); // 로컬 시간대 기준으로 dateKey 생성 const year date.getFullYear(); const month String(date.getMonth() + 1).padStart(2, 0); const dayStr String(date.getDate()).padStart(2, 0); const dateKey `${year}-${month}-${dayStr}`; if (calendarDatadateKey) { const data calendarDatadateKey; if (data.scheduled.length > 0) { const indicator document.createElement(div); indicator.className day-indicator indicator-scheduled; dayDiv.appendChild(indicator); } if (data.actual.length > 0) { const indicator document.createElement(div); indicator.className day-indicator indicator-completed; dayDiv.appendChild(indicator); } dayDiv.onclick () > showDayDetails(dateKey, data); } container.appendChild(dayDiv); } const remainingDays 42 - (firstDay + daysInMonth); for (let day 1; day remainingDays; day++) { const dayDiv createDayElement(day, true, false); container.appendChild(dayDiv); } } function createDayElement(day, isOtherMonth, isToday) { const dayDiv document.createElement(div); dayDiv.className calendar-day; if (isOtherMonth) dayDiv.classList.add(other-month); if (isToday) dayDiv.classList.add(today); const dayNumber document.createElement(div); dayNumber.className day-number; dayNumber.textContent day; dayDiv.appendChild(dayNumber); return dayDiv; } function showDayDetails(dateKey, data) { // dateKey를 직접 파싱하여 로컬 시간대로 처리 const year, month, day dateKey.split(-).map(Number); const modalTitle `${year}년 ${month}월 ${day}일`; document.getElementById(modal-title).textContent modalTitle; let html ; if (data.scheduled.length > 0) { html + h4 stylemargin-bottom: 15px; color: var(--primary-blue); font-weight: 700;>📋 출하 예정/h4>; data.scheduled.forEach(item > { html + ` div classrecord-item> div classrecord-header> div classrecord-type>예정 출하/div> div classrecord-status status-${item.status 예정 ? scheduled : completed}>${item.status}/div> /div> div classrecord-details> div classrecord-detail> div classdetail-label>지역/div> div classdetail-value>${item.location}/div> /div> div classrecord-detail> div classdetail-label>예정 두수/div> div classdetail-value>${item.headCount}두/div> /div> div classrecord-detail> div classdetail-label>도축장/div> div classdetail-value>${item.slaughterhouse || -}/div> /div> /div> /div> `; }); } if (data.actual.length > 0) { html + h4 stylemargin: 20px 0 15px; color: var(--success-green); font-weight: 700;>✅ 실제 출하/h4>; data.actual.forEach(item > { html + ` div classrecord-item styleborder-left-color: var(--success-green);> div classrecord-header> div classrecord-type stylecolor: var(--success-green);>완료된 출하/div> div classrecord-status status-completed>완료/div> /div> div classrecord-details> div classrecord-detail> div classdetail-label>지역/div> div classdetail-value>${item.location}/div> /div> div classrecord-detail> div classdetail-label>출하 두수/div> div classdetail-value>${item.headCount}두/div> /div> div classrecord-detail> div classdetail-label>총 중량/div> div classdetail-value>${Number(item.totalWeight).toLocaleString()}kg/div> /div> div classrecord-detail> div classdetail-label>평균 두당 무게/div> div classdetail-value>${item.avgWeight}kg/div> /div> /div> /div> `; }); } if (html ) { html div classempty-state>div classempty-state-icon>📭/div>p>이 날짜에 기록이 없습니다./p>/div>; } document.getElementById(modal-body).innerHTML html; document.getElementById(modal).classList.add(active); } function changeMonth(delta) { if (delta 0) { const today new Date(); currentYear today.getFullYear(); currentMonth today.getMonth(); } else { currentMonth + delta; if (currentMonth > 11) { currentMonth 0; currentYear++; } else if (currentMonth 0) { currentMonth 11; currentYear--; } } loadCalendar(); } document.getElementById(modal).addEventListener(click, function(e) { if (e.target this) { closeModal(); } }); window.addEventListener(load, function() { const today new Date().toISOString().split(T)0; document.getElementById(scheduled-date).value today; document.getElementById(actual-date).value today; // 페이지 로딩 시 오늘 날짜의 출하 예정 목록도 자동으로 로드 loadScheduledForDate(); setSyncStatus(synced, 준비 완료); }); // 동기화 상태 표시 함수 function setSyncStatus(status, text) { const dot document.getElementById(syncDot); dot.className sync-dot; if (status syncing) dot.classList.add(syncing); if (status error) dot.classList.add(error); document.getElementById(syncText).textContent text; } // 등지방 분포도 - Excel 파일 처리 async function processBackfatFile() { const fileInput document.getElementById(backfatFile); const file fileInput.files0; if (!file) { alert(파일을 선택해주세요.); return; } setSyncStatus(syncing, 파일 분석 중...); try { const data await readExcelFile(file); currentBackfatData data; // 현재 데이터 저장 const stats calculateBackfatStats(data); // 통계 표시 displayBackfatStats(stats); // AI 분석 생성 및 표시 const aiComments generateAIAnalysis(data, stats); displayAIAnalysis(aiComments); // 개선 제안 생성 및 표시 const suggestions generateSuggestions(data); displaySuggestions(suggestions); // 차트 그리기 drawBackfatChart(data); // UI 표시 document.getElementById(backfat-guide).style.display none; document.getElementById(backfat-stats).style.display block; document.getElementById(backfat-chart).style.display block; document.getElementById(saveBackfatBtn).style.display inline-block; // 저장 버튼 표시 setSyncStatus(synced, 분석 완료); } catch (error) { alert(파일 처리 중 오류가 발생했습니다: + error.message); setSyncStatus(error, 분석 실패); } } // Excel 파일 읽기 function readExcelFile(file) { return new Promise((resolve, reject) > { const reader new FileReader(); reader.onload function(e) { try { const data new Uint8Array(e.target.result); const workbook XLSX.read(data, { type: array }); const firstSheet workbook.Sheetsworkbook.SheetNames0; const jsonData XLSX.utils.sheet_to_json(firstSheet, { header: 1 }); // 35행부터 데이터 추출 (인덱스 34부터) const pigData ; for (let i 34; i jsonData.length; i++) { const row jsonDatai; if (row14 && row16) { // 도체중(14), 등지방(16) const weight parseFloat(row14); const backfat parseFloat(row16); if (!isNaN(weight) && !isNaN(backfat)) { pigData.push({ weight, backfat }); } } } if (pigData.length 0) { reject(new Error(유효한 데이터를 찾을 수 없습니다.)); } else { resolve(pigData); } } catch (error) { reject(error); } }; reader.onerror () > reject(new Error(파일 읽기 실패)); reader.readAsArrayBuffer(file); }); } // 등급 판정 함수 function getGrade(weight, backfat) { // 1+ 등급: 도체중 83-93kg, 등지방 17-25mm 미만 if (weight > 83 && weight 93 && backfat > 17 && backfat 25) { return 1+; } // 1 등급 if ((weight > 80 && weight 83 && backfat > 15 && backfat 28) || (weight > 83 && weight 93 && backfat > 15 && backfat 17) || (weight > 83 && weight 93 && backfat > 25 && backfat 28) || (weight > 93 && weight 98 && backfat > 15 && backfat 28)) { return 1; } // 2 등급 return 2; } // 통계 계산 function calculateBackfatStats(data) { let grade1plus 0, grade1 0, grade2 0; let totalWeight 0, totalBackfat 0; data.forEach(pig > { const grade getGrade(pig.weight, pig.backfat); if (grade 1+) grade1plus++; else if (grade 1) grade1++; else grade2++; totalWeight + pig.weight; totalBackfat + pig.backfat; }); return { totalCount: data.length, grade1plus, grade1, grade2, avgWeight: (totalWeight / data.length).toFixed(1), avgBackfat: (totalBackfat / data.length).toFixed(1) }; } // 통계 표시 function displayBackfatStats(stats) { document.getElementById(total-count).textContent stats.totalCount + 두; document.getElementById(grade-1plus).textContent stats.grade1plus + 두; document.getElementById(grade-1).textContent stats.grade1 + 두; document.getElementById(grade-2).textContent stats.grade2 + 두; document.getElementById(avg-weight).textContent stats.avgWeight + kg; document.getElementById(avg-backfat).textContent stats.avgBackfat + mm; } // AI 분석 코멘트 생성 function generateAIAnalysis(data, stats) { const comments ; // 1등급 이상 비율 분석 (1+ + 1등급) const grade1AboveCount stats.grade1plus + stats.grade1; const grade1AbovePercent ((grade1AboveCount / stats.totalCount) * 100).toFixed(1); if (grade1AbovePercent > 75) { comments.push({ icon: ✅, text: `1등급 이상 비율 ${grade1AbovePercent}% (${grade1AboveCount}두) - 매우 우수한 수준입니다!`, type: success }); } else if (grade1AbovePercent > 60) { comments.push({ icon: 👍, text: `1등급 이상 비율 ${grade1AbovePercent}% (${grade1AboveCount}두) - 양호한 수준입니다.`, type: good }); } else { comments.push({ icon: ⚠️, text: `1등급 이상 비율 ${grade1AbovePercent}% (${grade1AboveCount}두) - 개선이 필요합니다.`, type: warning }); } // 평균 도체중 분석 (1등급 기준: 80-98kg) const avgWeight parseFloat(stats.avgWeight); if (avgWeight > 80 && avgWeight 98) { comments.push({ icon: ✅, text: `평균 도체중 ${stats.avgWeight}kg - 1등급 목표 범위 내입니다.`, type: success }); } else if (avgWeight 80) { comments.push({ icon: 📊, text: `평균 도체중 ${stats.avgWeight}kg - 목표보다 낮습니다.`, type: info }); } else { comments.push({ icon: 📊, text: `평균 도체중 ${stats.avgWeight}kg - 목표보다 높습니다.`, type: info }); } // 평균 등지방 분석 (1등급 기준: 15-28mm) const avgBackfat parseFloat(stats.avgBackfat); if (avgBackfat > 15 && avgBackfat 28) { comments.push({ icon: ✅, text: `평균 등지방 ${stats.avgBackfat}mm - 1등급 적정 범위입니다.`, type: success }); } else if (avgBackfat 15) { comments.push({ icon: 📉, text: `평균 등지방 ${stats.avgBackfat}mm - 다소 낮은 편입니다.`, type: info }); } else { comments.push({ icon: 📈, text: `평균 등지방 ${stats.avgBackfat}mm - 다소 높은 편입니다.`, type: info }); } return comments; } // 개선 제안 생성 (4가지 케이스 분석) function generateSuggestions(data) { const suggestions ; // 4가지 구역별 분석 (1등급 기준으로 조정) let highBackfatHighWeight 0; // 등지방↑ 도체중↑ (오른쪽 위) let highBackfatLowWeight 0; // 등지방↑ 도체중↓ (왼쪽 위) let lowBackfatHighWeight 0; // 등지방↓ 도체중↑ (오른쪽 아래) let lowBackfatLowWeight 0; // 등지방↓ 도체중↓ (왼쪽 아래) data.forEach(pig > { const isHighWeight pig.weight > 89; // 1등급 중간값 const isHighBackfat pig.backfat > 21.5; // 1등급 중간값 if (isHighBackfat && isHighWeight) { highBackfatHighWeight++; } else if (isHighBackfat && !isHighWeight) { highBackfatLowWeight++; } else if (!isHighBackfat && isHighWeight) { lowBackfatHighWeight++; } else { lowBackfatLowWeight++; } }); const total data.length; // 1. 과비육 (등지방↑ 도체중↑) if (highBackfatHighWeight > total * 0.15) { suggestions.push({ icon: ⚠️, title: 과비육 개체 발견, desc: `${highBackfatHighWeight}두가 과비육 상태입니다.`, action: 다음 출하는 3-5일 앞당기는 것을 권장합니다., severity: warning }); } // 2. 비효율 비육 (등지방↑ 도체중↓) if (highBackfatLowWeight > total * 0.1) { suggestions.push({ icon: 🔄, title: 비효율적 비육 패턴, desc: `${highBackfatLowWeight}두가 살은 적고 지방만 많은 상태입니다.`, action: 사료 배합 재검토 및 운동량 증가를 권장합니다., severity: warning }); } // 3. 우수 개체 (등지방↓ 도체중↑) if (lowBackfatHighWeight > total * 0.2) { suggestions.push({ icon: ✨, title: 우수 개체 다수, desc: `${lowBackfatHighWeight}두가 이상적인 비육 상태입니다.`, action: 현재 관리 방식을 유지하면 1등급 가능성이 높습니다., severity: success }); } // 4. 미성숙 (등지방↓ 도체중↓) if (lowBackfatLowWeight > total * 0.2) { suggestions.push({ icon: ⏰, title: 미성숙 개체 다수, desc: `${lowBackfatLowWeight}두가 아직 출하 시기가 이릅니다.`, action: 7-10일 더 사육 후 출하를 권장합니다., severity: info }); } // 등지방 극단치 분석 (1등급 기준) const veryHighBackfat data.filter(p > p.backfat > 28).length; const veryLowBackfat data.filter(p > p.backfat 15).length; if (veryHighBackfat > 0) { suggestions.push({ icon: 🔴, title: 등지방 과다 개체, desc: `${veryHighBackfat}두의 등지방이 28mm 이상입니다.`, action: 2등급 가능성 - 즉시 출하를 고려하세요., severity: urgent }); } if (veryLowBackfat > 0) { suggestions.push({ icon: 🔵, title: 등지방 부족 개체, desc: `${veryLowBackfat}두의 등지방이 15mm 미만입니다.`, action: 사료 급여량 증가 또는 출하 연기를 검토하세요., severity: info }); } // 도체중 극단치 분석 const veryLowWeight data.filter(p > p.weight 80).length; const veryHighWeight data.filter(p > p.weight > 98).length; if (veryLowWeight > 0) { suggestions.push({ icon: ⚖️, title: 도체중 부족 개체, desc: `${veryLowWeight}두의 도체중이 80kg 미만입니다.`, action: 출하 시기를 늦추거나 사료 급여량을 늘리세요., severity: info }); } if (veryHighWeight > 0) { suggestions.push({ icon: ⚖️, title: 도체중 과다 개체, desc: `${veryHighWeight}두의 도체중이 98kg 초과입니다.`, action: 사료비 손실 가능성 - 적정 시기 출하를 권장합니다., severity: warning }); } // 개선 제안이 없으면 격려 메시지 if (suggestions.length 0) { suggestions.push({ icon: 🎉, title: 완벽한 관리 상태, desc: 모든 개체가 1등급 적정 범위 내에 있습니다., action: 현재의 사육 관리를 유지하세요!, severity: success }); } return suggestions; } // AI 분석 표시 function displayAIAnalysis(comments) { const container document.getElementById(ai-comments); container.innerHTML ; comments.forEach(comment > { const div document.createElement(div); div.style.cssText display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.15); padding: 12px; border-radius: 8px;; div.innerHTML ` span stylefont-size: 1.2em;>${comment.icon}/span> span styleflex: 1;>${comment.text}/span> `; container.appendChild(div); }); document.getElementById(ai-analysis).style.display block; } // 개선 제안 표시 function displaySuggestions(suggestions) { const container document.getElementById(suggestion-list); container.innerHTML ; const severityColors { urgent: #FFEBEE, warning: #FFF3E0, info: #E3F2FD, success: #E8F5E9 }; const severityBorders { urgent: #EF5350, warning: #FF9800, info: #2196F3, success: #4CAF50 }; suggestions.forEach(suggestion > { const div document.createElement(div); div.style.cssText ` background: ${severityColorssuggestion.severity}; border-left: 4px solid ${severityBorderssuggestion.severity}; padding: 16px; border-radius: 8px; `; div.innerHTML ` div styledisplay: flex; align-items: flex-start; gap: 12px;> span stylefont-size: 1.5em;>${suggestion.icon}/span> div styleflex: 1;> div stylefont-weight: 700; color: var(--text-main); margin-bottom: 6px;>${suggestion.title}/div> div stylecolor: var(--text-sub); font-size: 0.9em; margin-bottom: 8px;>${suggestion.desc}/div> div stylecolor: ${severityBorderssuggestion.severity}; font-weight: 600; font-size: 0.9em;> 💡 ${suggestion.action} /div> /div> /div> `; container.appendChild(div); }); document.getElementById(ai-suggestions).style.display block; } // 분포도 차트 그리기 function drawBackfatChart(data) { const canvas document.getElementById(chartCanvas); const ctx canvas.getContext(2d); // 캔버스 크기 설정 canvas.width 1200; canvas.height 800; // 여백 const margin { top: 60, right: 80, bottom: 80, left: 80 }; const chartWidth canvas.width - margin.left - margin.right; const chartHeight canvas.height - margin.top - margin.bottom; // 배경 흰색 ctx.fillStyle white; ctx.fillRect(0, 0, canvas.width, canvas.height); // 도체중 범위: 65-110kg // 등지방 범위: 5-38mm const weightMin 65, weightMax 110; const backfatMin 5, backfatMax 38; // 스케일 const xScale chartWidth / (weightMax - weightMin); const yScale chartHeight / (backfatMax - backfatMin); ctx.save(); ctx.translate(margin.left, margin.top); // 등급별 배경 영역 그리기 // 1+ 등급 영역 (연한 파란색 배경 + 파란색 테두리) ctx.fillStyle rgba(33, 150, 243, 0.08); const x1plus (83 - weightMin) * xScale; const x2plus (93 - weightMin) * xScale; const y1plus chartHeight - (25 - backfatMin) * yScale; const y2plus chartHeight - (17 - backfatMin) * yScale; ctx.fillRect(x1plus, y1plus, x2plus - x1plus, y2plus - y1plus); ctx.strokeStyle #2196F3; ctx.lineWidth 2; ctx.strokeRect(x1plus, y1plus, x2plus - x1plus, y2plus - y1plus); // 1 등급 영역들 (연한 회색) ctx.fillStyle rgba(158, 158, 158, 0.05); // 80-83, 15-28 ctx.fillRect((80 - weightMin) * xScale, chartHeight - (28 - backfatMin) * yScale, (83 - 80) * xScale, (28 - 15) * yScale); // 83-93, 15-17 ctx.fillRect((83 - weightMin) * xScale, chartHeight - (17 - backfatMin) * yScale, (93 - 83) * xScale, (17 - 15) * yScale); // 83-93, 25-28 ctx.fillRect((83 - weightMin) * xScale, chartHeight - (28 - backfatMin) * yScale, (93 - 83) * xScale, (28 - 25) * yScale); // 93-98, 15-28 ctx.fillRect((93 - weightMin) * xScale, chartHeight - (28 - backfatMin) * yScale, (98 - 93) * xScale, (28 - 15) * yScale); ctx.restore(); // 축 그리기 ctx.strokeStyle #333; ctx.lineWidth 2; ctx.beginPath(); ctx.moveTo(margin.left, margin.top); ctx.lineTo(margin.left, canvas.height - margin.bottom); ctx.lineTo(canvas.width - margin.right, canvas.height - margin.bottom); ctx.stroke(); // X축 눈금 및 그리드 (도체중) ctx.strokeStyle #E0E0E0; ctx.lineWidth 0.5; ctx.fillStyle #333; ctx.font 13px Pretendard; ctx.textAlign center; for (let w weightMin; w weightMax; w + 5) { const x margin.left + (w - weightMin) * xScale; const y canvas.height - margin.bottom; // 그리드선 if (w > weightMin && w weightMax) { ctx.beginPath(); ctx.moveTo(x, margin.top); ctx.lineTo(x, canvas.height - margin.bottom); ctx.stroke(); } // 눈금 ctx.strokeStyle #333; ctx.lineWidth 2; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + 5); ctx.stroke(); ctx.fillText(w, x, y + 22); ctx.strokeStyle #E0E0E0; ctx.lineWidth 0.5; } // Y축 눈금 및 그리드 (등지방) ctx.textAlign right; ctx.textBaseline middle; for (let b backfatMin; b backfatMax; b + 2) { const x margin.left; const y canvas.height - margin.bottom - (b - backfatMin) * yScale; // 그리드선 if (b > backfatMin && b backfatMax) { ctx.strokeStyle #E0E0E0; ctx.lineWidth 0.5; ctx.beginPath(); ctx.moveTo(margin.left, y); ctx.lineTo(canvas.width - margin.right, y); ctx.stroke(); } // 눈금 ctx.strokeStyle #333; ctx.lineWidth 2; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - 5, y); ctx.stroke(); ctx.fillText(b, x - 10, y); } // 축 레이블 ctx.font bold 16px Pretendard; ctx.textAlign center; ctx.textBaseline alphabetic; ctx.fillText(도체중 (kg), canvas.width / 2, canvas.height - 25); ctx.save(); ctx.translate(25, canvas.height / 2); ctx.rotate(-Math.PI / 2); ctx.fillText(등지방 두께 (mm), 0, 0); ctx.restore(); // 제목 ctx.font bold 20px Pretendard; ctx.fillText(등지방 분포도, canvas.width / 2, 30); // 개별 데이터 포인트 그리기 (겹치지 않게) ctx.save(); ctx.translate(margin.left, margin.top); // 같은 좌표에 여러 개가 있을 경우 겹치지 않게 위치 조정 const positions {}; data.forEach(pig > { const grade getGrade(pig.weight, pig.backfat); const key `${pig.weight.toFixed(1)}-${pig.backfat.toFixed(1)}`; if (!positionskey) { positionskey { count: 0, grade: grade }; } positionskey.count++; }); const drawn {}; data.forEach(pig > { const grade getGrade(pig.weight, pig.backfat); const key `${pig.weight.toFixed(1)}-${pig.backfat.toFixed(1)}`; if (!drawnkey) { drawnkey 0; } const baseX (pig.weight - weightMin) * xScale; const baseY chartHeight - (pig.backfat - backfatMin) * yScale; // 같은 위치에 여러 개 있으면 원형으로 배치 const totalCount positionskey.count; let offsetX 0, offsetY 0; if (totalCount > 1) { const angle (drawnkey / totalCount) * 2 * Math.PI; const radius 3; offsetX Math.cos(angle) * radius; offsetY Math.sin(angle) * radius; } const x baseX + offsetX; const y baseY + offsetY; // 등급별 색상 if (grade 1+) { ctx.fillStyle #FF9800; // 주황색 } else if (grade 1) { ctx.fillStyle #FFB2B2; // 연한 빨강 } else { ctx.fillStyle #EF5350; // 빨강 } // 점 그리기 ctx.beginPath(); ctx.arc(x, y, 3, 0, 2 * Math.PI); ctx.fill(); // 테두리 ctx.strokeStyle #333; ctx.lineWidth 0.5; ctx.stroke(); drawnkey++; }); ctx.restore(); // 범례 추가 const legendX canvas.width - margin.right + 20; const legendY margin.top + 20; ctx.font bold 14px Pretendard; ctx.textAlign left; ctx.fillStyle #333; ctx.fillText(등급, legendX, legendY); // 1+ 등급 ctx.fillStyle #FF9800; ctx.beginPath(); ctx.arc(legendX + 10, legendY + 25, 4, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle #333; ctx.font 13px Pretendard; ctx.fillText(1+, legendX + 25, legendY + 28); // 1 등급 ctx.fillStyle #FFB2B2; ctx.beginPath(); ctx.arc(legendX + 10, legendY + 50, 4, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle #333; ctx.fillText(1, legendX + 25, legendY + 53); // 2 등급 ctx.fillStyle #EF5350; ctx.beginPath(); ctx.arc(legendX + 10, legendY + 75, 4, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle #333; ctx.fillText(2, legendX + 25, legendY + 78); } // 등지방 데이터 저장/관리 함수 // 현재 분석 데이터 저장 async function saveCurrentBackfatData() { if (!currentBackfatData || currentBackfatData.length 0) { alert(저장할 데이터가 없습니다.); return; } const memo prompt(메모를 입력하세요 (선택사항):, ); setSyncStatus(syncing, 데이터 저장 중...); try { const formData new FormData(); formData.append(action, saveBackfatData); formData.append(data, JSON.stringify(currentBackfatData)); formData.append(memo, memo || ); const response await fetch(SCRIPT_URL, { method: POST, body: formData }); const result await response.json(); if (result.success) { alert(`저장 완료!\n출하ID: ${result.shipmentId}\n날짜: ${result.date}\n두수: ${result.count}두`); setSyncStatus(synced, 저장 완료); document.getElementById(saveBackfatBtn).style.display none; // 저장 후 버튼 숨김 } else { throw new Error(result.error); } } catch (error) { alert(저장 실패: + error.message); setSyncStatus(error, 저장 실패); } } // 히스토리 모달 표시 async function showBackfatHistory() { setSyncStatus(syncing, 히스토리 불러오는 중...); try { const response await fetch(`${SCRIPT_URL}?actiongetBackfatHistory`); const result await response.json(); if (result.success) { displayBackfatHistoryList(result.history); document.getElementById(backfat-history-modal).style.display flex; setSyncStatus(synced, 히스토리 로드 완료); } else { throw new Error(result.error); } } catch (error) { alert(히스토리 불러오기 실패: + error.message); setSyncStatus(error, 로드 실패); } } // 히스토리 목록 표시 function displayBackfatHistoryList(history) { const container document.getElementById(backfat-history-list); if (history.length 0) { container.innerHTML div styletext-align: center; padding: 40px; color: var(--text-sub);>저장된 데이터가 없습니다./div>; return; } container.innerHTML ; history.forEach(item > { const div document.createElement(div); div.style.cssText border: 1px solid #ECEFF1; border-radius: 8px; padding: 16px; margin-bottom: 12px; background: white;; const dateObj new Date(item.date + + item.time); const dateStr dateObj.toLocaleDateString(ko-KR); const timeStr dateObj.toLocaleTimeString(ko-KR, { hour: 2-digit, minute: 2-digit }); div.innerHTML ` div styledisplay: flex; justify-content: space-between; align-items: center;> div styleflex: 1;> div stylefont-weight: 700; color: var(--text-main); margin-bottom: 4px;> 📅 ${dateStr} ${timeStr} /div> div stylefont-size: 0.9em; color: var(--text-sub);> 🐷 ${item.count}두 ${item.memo ? · + item.memo : } /div> div stylefont-size: 0.85em; color: var(--text-sub); margin-top: 4px;> ID: ${item.shipmentId} /div> /div> div styledisplay: flex; gap: 8px;> button classbtn btn-primary onclickloadBackfatData(${item.shipmentId}) stylepadding: 8px 16px; font-size: 0.9em;> 불러오기 /button> button classbtn btn-danger onclickdeleteBackfatData(${item.shipmentId}) stylepadding: 8px 16px; font-size: 0.9em;> 삭제 /button> /div> /div> `; container.appendChild(div); }); } // 저장된 데이터 불러오기 async function loadBackfatData(shipmentId) { if (!confirm(현재 분석 중인 데이터가 있다면 사라집니다. 계속하시겠습니까?)) { return; } setSyncStatus(syncing, 데이터 불러오는 중...); closeBackfatHistoryModal(); try { const response await fetch(`${SCRIPT_URL}?actionloadBackfatData&shipmentId${shipmentId}`); const result await response.json(); if (result.success) { currentBackfatData result.data; const stats calculateBackfatStats(result.data); // 통계 표시 displayBackfatStats(stats); // AI 분석 생성 및 표시 const aiComments generateAIAnalysis(result.data, stats); displayAIAnalysis(aiComments); // 개선 제안 생성 및 표시 const suggestions generateSuggestions(result.data); displaySuggestions(suggestions); // 차트 그리기 drawBackfatChart(result.data); // UI 표시 document.getElementById(backfat-guide).style.display none; document.getElementById(backfat-stats).style.display block; document.getElementById(backfat-chart).style.display block; document.getElementById(saveBackfatBtn).style.display none; // 이미 저장된 데이터는 저장 버튼 숨김 // 등지방 분포도 탭으로 이동 const tabs document.querySelectorAll(.tab-card); tabs.forEach(tab > tab.classList.remove(active)); tabs3.classList.add(active); // 4번째 탭 (등지방 분포도) setSyncStatus(synced, 데이터 로드 완료); } else { throw new Error(result.error); } } catch (error) { alert(데이터 불러오기 실패: + error.message); setSyncStatus(error, 로드 실패); } } // 데이터 삭제 async function deleteBackfatData(shipmentId) { if (!confirm(정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.)) { return; } setSyncStatus(syncing, 삭제 중...); try { const response await fetch(`${SCRIPT_URL}?actiondeleteBackfatData&shipmentId${shipmentId}`); const result await response.json(); if (result.success) { alert(`${result.deletedCount}개 행이 삭제되었습니다.`); showBackfatHistory(); // 목록 새로고침 setSyncStatus(synced, 삭제 완료); } else { throw new Error(result.error); } } catch (error) { alert(삭제 실패: + error.message); setSyncStatus(error, 삭제 실패); } } // 히스토리 모달 닫기 function closeBackfatHistoryModal() { document.getElementById(backfat-history-modal).style.display none; } /script>/body>/html>
Port 443
HTTP/1.1 200 OKDate: Wed, 25 Feb 2026 11:49:53 GMTContent-Type: text/htmlContent-Length: 118188Connection: keep-aliveCF-Cache-Status: HITCache-Control: public, max-age0, must-revalidateETag: 0a756d0a445f9c22d63fc453300d2ef3Report-To: {group:cf-nel,max_age:604800,endpoints:{url:https://a.nel.cloudflare.com/report/v4?s3hVqX%2Bj05g6dz0RLeHrwXbKmpY2ezUar3p40aCkW81nW2TbWElMV7NHfxenRvCgN3L9%2Fsyf8GtGgzke0KfjUruX%2Fo3fNiPT9V6Vl4dJJX1V8nQ%3D%3D}}Nel: {report_to:cf-nel,success_fraction:0.0,max_age:604800}Server: cloudflareCF-RAY: 9d36fb6148ccfef7-PDXalt-svc: h3:443; ma86400 !DOCTYPE html>html langko>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title>출하 기록 관리 시스템 - 한울축산/title> link relstylesheet hrefhttps://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css> style> :root { --primary-blue: #1565C0; --light-blue: #E3F2FD; --hover-blue: #1976D2; --bg-color: #F8F9FA; --text-main: #263238; --text-sub: #546E7A; --urgent-red: #C62828; --warning-orange: #EF6C00; --info-blue: #0277BD; --success-green: #2E7D32; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; background: var(--bg-color); min-height: 100vh; padding: 20px; color: var(--text-main); } .container { max-width: 1400px; margin: 0 auto; } .header { background: white; border-radius: 16px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); border: 1px solid #ECEFF1; display: flex; align-items: center; justify-content: space-between; } .header-content { display: flex; align-items: center; gap: 16px; } .logo-container { width: 80px; height: 80px; border-radius: 50%; overflow: hidden; background: var(--light-blue); border: 3px solid var(--primary-blue); flex-shrink: 0; } .logo-container img { width: 100%; height: 100%; object-fit: cover; } .header-title h1 { font-size: 24px; font-weight: 700; color: var(--primary-blue); margin-bottom: 4px; text-align: left; } .header-subtitle { font-size: 0.85em; color: var(--text-sub); font-weight: 400; text-align: left; } .sync-status { display: flex; align-items: center; gap: 8px; background: var(--light-blue); padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; } .sync-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success-green); } .sync-dot.syncing { background: var(--warning-orange); animation: pulse 1s infinite; } .sync-dot.error { background: var(--urgent-red); } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .tabs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; } .tab-card { background: white; border-radius: 12px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.3s ease; border: 2px solid #ECEFF1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); } .tab-card:hover { border-color: var(--primary-blue); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(21, 101, 192, 0.15); } .tab-card.active { border-color: var(--primary-blue); background: var(--light-blue); } .tab-icon { font-size: 2.5em; margin-bottom: 12px; } .tab-title { font-size: 1.1em; font-weight: 700; color: var(--text-main); } .tab-content { display: none; animation: fadeIn 0.3s ease; } .tab-content.active { display: block; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .form-section { background: white; padding: 24px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #ECEFF1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); } .form-section h3 { color: var(--primary-blue); margin-bottom: 20px; font-size: 1.2em; font-weight: 700; } .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 20px; } .form-group { display: flex; flex-direction: column; } .form-group label { margin-bottom: 8px; color: var(--text-main); font-weight: 600; font-size: 0.9em; } .form-group input, .form-group select { padding: 12px; border: 1px solid #ECEFF1; border-radius: 8px; font-size: 16px; transition: all 0.3s ease; font-family: Pretendard, sans-serif; } .form-group input:focus, .form-group select:focus { outline: none; border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(21, 101, 192, 0.1); } .form-group input:disabled, .form-group input:read-only { background: #ECEFF1; cursor: not-allowed; } .btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 1em; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin-top: 10px; font-family: Pretendard, sans-serif; } .btn-primary { background: var(--primary-blue); color: white; } .btn-primary:hover { background: var(--hover-blue); transform: scale(1.02); } .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .btn-secondary { background: var(--text-sub); color: white; } .btn-secondary:hover { background: var(--text-main); } .btn-danger { background: var(--urgent-red); color: white; } .btn-danger:hover { background: #B71C1C; } .btn-small { padding: 6px 12px; font-size: 0.85em; margin: 0 4px; } .building-input-section { background: var(--light-blue); padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid var(--primary-blue); } .building-input-header { font-weight: 700; color: var(--primary-blue); margin-bottom: 16px; font-size: 1.05em; } .building-input-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; } .building-input-item { display: flex; align-items: center; gap: 8px; } .building-input-item label { font-weight: 600; color: var(--text-main); min-width: 100px; font-size: 0.9em; } .building-input-item input { flex: 1; padding: 10px; border: 1px solid #ECEFF1; border-radius: 6px; font-size: 0.95em; } .total-check { margin-top: 16px; padding: 12px; background: white; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; font-weight: 600; } .total-check.match { color: var(--success-green); border: 2px solid var(--success-green); } .total-check.mismatch { color: var(--urgent-red); border: 2px solid var(--urgent-red); } .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; } .card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); border: 1px solid #ECEFF1; transition: all 0.3s ease; } .card:hover { transform: scale(1.02); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); } .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .card-title { font-size: 1.15em; color: var(--text-main); font-weight: 700; } .card-icon { font-size: 1.5em; } .card-value { font-size: 2em; font-weight: 700; color: var(--primary-blue); margin-bottom: 8px; } .card-label { color: var(--text-sub); font-size: 0.9em; font-weight: 500; } .region-card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); border: 1px solid #ECEFF1; margin-bottom: 20px; } .region-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 2px solid var(--primary-blue); } .region-title { font-size: 1.3em; font-weight: 700; color: var(--text-main); } .region-total { font-size: 1.2em; font-weight: 700; color: var(--primary-blue); margin-left: auto; } .buildings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; } .building-card { background: var(--light-blue); padding: 18px; border-radius: 10px; border: 1px solid var(--primary-blue); } .building-name { font-weight: 700; color: var(--text-main); margin-bottom: 4px; font-size: 1em; display: flex; justify-content: space-between; align-items: center; } .building-total { color: var(--primary-blue); font-size: 1.1em; } .building-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 12px; margin-bottom: 12px; } .stat-item { text-align: center; padding: 12px 8px; background: white; border-radius: 8px; } .stat-value { font-size: 1.3em; font-weight: 700; color: var(--primary-blue); } .stat-label { font-size: 0.75em; color: var(--text-sub); margin-top: 4px; font-weight: 600; } .building-actions { display: flex; gap: 8px; margin-top: 12px; } .building-actions button { flex: 1; padding: 8px; border: none; border-radius: 6px; font-size: 0.85em; font-weight: 600; cursor: pointer; transition: all 0.3s ease; font-family: Pretendard, sans-serif; } .btn-complete { background: var(--success-green); color: white; } .btn-complete:hover { background: #1B5E20; } .btn-transfer { background: var(--info-blue); color: white; } .btn-transfer:hover { background: #01579B; } .scheduled-select { background: var(--light-blue); padding: 16px; border-radius: 10px; margin: 16px 0; border: 1px solid var(--primary-blue); } .scheduled-header { font-weight: 700; color: var(--primary-blue); margin-bottom: 12px; font-size: 1em; } .scheduled-item { padding: 14px; background: white; border-radius: 8px; margin-bottom: 10px; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; } .scheduled-item:hover { border-color: var(--primary-blue); transform: scale(1.02); } .scheduled-item.selected { border-color: var(--primary-blue); background: var(--light-blue); } .scheduled-item-header { font-weight: 700; color: var(--primary-blue); margin-bottom: 6px; } .scheduled-item-details { font-size: 0.9em; color: var(--text-sub); } .calendar { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); border: 1px solid #ECEFF1; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .calendar-header h3 { color: var(--text-main); font-size: 1.3em; font-weight: 700; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav button { padding: 10px 18px; border: 1px solid var(--primary-blue); background: white; color: var(--primary-blue); border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s ease; font-family: Pretendard, sans-serif; } .calendar-nav button:hover { background: var(--primary-blue); color: white; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 10px; } .calendar-day-header { text-align: center; padding: 12px; font-weight: 700; color: var(--text-sub); font-size: 0.9em; } .calendar-day { aspect-ratio: 1; border: 1px solid #ECEFF1; border-radius: 8px; padding: 10px; cursor: pointer; transition: all 0.3s ease; position: relative; background: white; } .calendar-day:hover { border-color: var(--primary-blue); transform: scale(1.05); box-shadow: 0 2px 8px rgba(21, 101, 192, 0.15); } .calendar-day.other-month { opacity: 0.3; } .calendar-day.today { border-color: var(--primary-blue); background: var(--light-blue); font-weight: 700; } .day-number { font-weight: 600; color: var(--text-main); margin-bottom: 6px; } .day-indicator { width: 6px; height: 6px; border-radius: 50%; margin: 3px auto; } .indicator-scheduled { background: var(--warning-orange); } .indicator-completed { background: var(--success-green); } .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; animation: fadeIn 0.3s ease; } .modal.active { display: flex; align-items: center; justify-content: center; } .modal-content { background: white; border-radius: 12px; padding: 28px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; animation: slideUp 0.3s ease; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } @keyframes slideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 2px solid var(--primary-blue); } .modal-header h3 { color: var(--text-main); font-size: 1.3em; font-weight: 700; } .modal-close { background: none; border: none; font-size: 1.8em; cursor: pointer; color: var(--text-sub); padding: 0; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.3s ease; } .modal-close:hover { background: var(--light-blue); color: var(--primary-blue); } .modal-icon { font-size: 3em; text-align: center; margin-bottom: 16px; } .modal-message { text-align: center; font-size: 1.1em; font-weight: 600; color: var(--text-main); margin-bottom: 20px; } .modal-buttons { display: flex; gap: 12px; margin-top: 20px; } .modal-buttons button { flex: 1; } .record-item { background: var(--light-blue); padding: 16px; border-radius: 8px; margin-bottom: 12px; border-left: 4px solid var(--primary-blue); } .record-header { display: flex; justify-content: space-between; margin-bottom: 12px; } .record-type { font-weight: 700; color: var(--primary-blue); } .record-status { padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 600; } .status-scheduled { background: #FFF3E0; color: var(--warning-orange); } .status-completed { background: #E8F5E9; color: var(--success-green); } .record-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 12px; } .record-detail { display: flex; flex-direction: column; } .detail-label { font-size: 0.85em; color: var(--text-sub); margin-bottom: 4px; font-weight: 600; } .detail-value { font-weight: 700; color: var(--text-main); } .loading { text-align: center; padding: 40px; color: var(--text-sub); } .loading-spinner { border: 4px solid #ECEFF1; border-top: 4px solid var(--primary-blue); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto 20px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .table-container { overflow-x: auto; margin-top: 20px; } table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; } th { background: var(--primary-blue); color: white; padding: 14px; text-align: left; font-weight: 700; } td { padding: 14px; border-bottom: 1px solid #E0E0E0; } tr:hover { background: var(--light-blue); } .empty-state { text-align: center; padding: 60px 20px; color: var(--text-sub); } .empty-state-icon { font-size: 4em; margin-bottom: 16px; opacity: 0.3; } @media (max-width: 768px) { .header { flex-direction: column; text-align: center; } .header-left { flex-direction: column; } .tabs { grid-template-columns: repeat(4, 1fr); gap: 6px; } .tab-card { padding: 12px 4px; } .tab-icon { font-size: 1.3em; } .tab-title { font-size: 0.75em; } .form-grid { grid-template-columns: 1fr; } .dashboard-grid { grid-template-columns: 1fr; } .buildings-grid { grid-template-columns: 1fr; } .building-stats { grid-template-columns: repeat(2, 1fr); } .calendar-grid { gap: 4px; } .calendar-day { padding: 4px; font-size: 0.85em; } .record-details { grid-template-columns: 1fr; } .modal-buttons { flex-direction: column; } } /style> !-- SheetJS 라이브러리 (Excel 파일 읽기) --> script srchttps://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js>/script>/head>body> div classcontainer> !-- 헤더 --> div classheader> div classheader-content> div classlogo-container> img srclogo.png alt한울축산> /div> div classheader-title> h1>출하 기록 관리 시스템/h1> span classheader-subtitle>출하 기록 관리/span> /div> /div> div classsync-status> span classsync-dot idsyncDot>/span> span idsyncText>동기화 대기/span> /div> /div> !-- 탭 카드 --> div classtabs> div classtab-card active onclickswitchTab(input)> div classtab-icon>📝/div> div classtab-title>데이터 입력/div> /div> div classtab-card onclickswitchTab(dashboard)> div classtab-icon>📊/div> div classtab-title>대시보드/div> /div> div classtab-card onclickswitchTab(calendar)> div classtab-icon>📅/div> div classtab-title>달력/div> /div> div classtab-card onclickswitchTab(backfat)> div classtab-icon>📈/div> div classtab-title>등지방 분포도/div> /div> /div> !-- 탭 1: 데이터 입력 --> div idtab-input classtab-content active> !-- 출하 예정 등록 --> div classform-section> h3>📋 출하 예정 등록/h3> form idscheduledForm> div classform-grid> div classform-group> label>출하 예정 날짜 */label> input typedate idscheduled-date required> /div> div classform-group> label>지역 */label> select idscheduled-location required> option value>선택하세요/option> option value영광>영광/option> option value함평>함평/option> /select> /div> div classform-group> label>예정 두수 */label> select idscheduled-headcount-type required onchangetoggleScheduledHeadcountInput()> option value80>80두/option> option valuecustom>기타 (직접 입력)/option> /select> /div> div classform-group idscheduled-headcount-custom-group styledisplay: none;> label>예정 두수 입력 */label> input typenumber idscheduled-headcount-custom min1> /div> div classform-group> label>도축장 */label> select idscheduled-slaughterhouse-type required onchangetoggleSlaughterhouseInput()> option value>선택하세요/option> option value함평 함평 (명주푸드)>함평 함평 (명주푸드)/option> option value나주 축공 (무등)>나주 축공 (무등)/option> option value광주 삼호 (누리천하)>광주 삼호 (누리천하)/option> option value나주 중앙 (다솔)>나주 중앙 (다솔)/option> option valuecustom>기타 (직접 입력)/option> /select> /div> div classform-group idscheduled-slaughterhouse-custom-group styledisplay: none;> label>도축장 직접 입력 */label> input typetext idscheduled-slaughterhouse-custom> /div> /div> button typesubmit classbtn btn-primary>등록하기/button> /form> /div> !-- 출하 등록 --> div classform-section> h3>✅ 출하 등록 (실제 출하)/h3> form idactualForm> div classform-grid> div classform-group> label>출하 날짜 */label> input typedate idactual-date required onchangeloadScheduledForDate()> /div> /div> !-- 출하 예정 목록 --> div idscheduled-list-container styledisplay: none;> div classscheduled-select> div classscheduled-header> 📋 이 날짜의 출하 예정 목록 (선택하면 정보가 자동 입력됩니다) /div> div idscheduled-list>/div> /div> /div> div classform-grid> div classform-group> label>지역 */label> select idactual-location required onchangeloadBuildingsByLocation()> option value>선택하세요/option> option value영광>영광/option> option value함평>함평/option> /select> /div> div classform-group> label>도축장 */label> select idactual-slaughterhouse-type required onchangetoggleActualSlaughterhouseInput()> option value>선택하세요/option> option value함평 함평 (명주푸드)>함평 함평 (명주푸드)/option> option value나주 축공 (무등)>나주 축공 (무등)/option> option value광주 삼호 (누리천하)>광주 삼호 (누리천하)/option> option value나주 중앙 (다솔)>나주 중앙 (다솔)/option> option valuecustom>기타 (직접 입력)/option> /select> /div> div classform-group idactual-slaughterhouse-custom-group styledisplay: none;> label>도축장 직접 입력 */label> input typetext idactual-slaughterhouse-custom> /div> /div> div classform-grid> div classform-group> label>총 중량 (kg) */label> input typenumber idactual-weight min1 step0.1 required> /div> div classform-group> label>총 두수 */label> input typenumber idactual-headcount min1 required> /div> div classform-group> label>두당 무게 (kg)/label> input typetext idactual-avg-weight readonly> /div> /div> !-- 돈사별 출하 두수 입력 --> div idbuilding-input-container styledisplay: none;> div classbuilding-input-section> div classbuilding-input-header>🏠 돈사별 출하 두수 입력 (비육돈에서 차감됩니다)/div> div classbuilding-input-grid idbuilding-inputs>/div> div classtotal-check idtotal-check> span>돈사별 합계:/span> span idbuilding-total>0두/span> /div> /div> /div> input typehidden idselected-scheduled-id> button typesubmit classbtn btn-primary>등록하기/button> /form> /div> !-- 돈사 재고 입력 --> div classform-section> h3>🏠 돈사 두수 입력/h3> div classform-grid> div classform-group> label>지역 */label> select idinventory-location onchangeupdateBuildingOptions()> option value>선택하세요/option> option value영광>영광/option> option value함평>함평/option> /select> /div> div classform-group> label>돈사 */label> select idinventory-building onchangeloadInventoryData()> option value>먼저 지역을 선택하세요/option> /select> /div> /div> form idinventoryForm> div classform-grid> div classform-group> label>육성돈 두수/label> input typenumber idinventory-grower min0 value0> /div> div classform-group> label>비육돈 두수/label> input typenumber idinventory-finisher min0 value0> /div> div classform-group> label>사고 두수/label> input typenumber idinventory-accident min0 value0> /div> /div> button typesubmit classbtn btn-primary>입력하기/button> /form> /div> /div> !-- 탭 2: 대시보드 --> div idtab-dashboard classtab-content> div iddashboard-loading classloading> div classloading-spinner>/div> p>데이터를 불러오는 중.../p> /div> div iddashboard-content styledisplay: none;> !-- 돈사별 재고 --> div idinventory-section>/div> h3 stylemargin: 30px 0 20px; color: var(--text-main); font-weight: 700;>📅 이번 주 출하 예정/h3> div idweekly-schedule>/div> h3 stylemargin: 30px 0 20px; color: var(--text-main); font-weight: 700;>📊 이번 달 출하 실적/h3> div idmonthly-stats classdashboard-grid>/div> h3 stylemargin: 30px 0 20px; color: var(--text-main); font-weight: 700;>📝 전체 출하 기록/h3> !-- 필터 및 검색 -->div stylebackground: white; padding: 16px; border-radius: 12px; margin-bottom: 16px; border: 1px solid #ECEFF1;> div styledisplay: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; align-items: end;> div> label styledisplay: block; margin-bottom: 4px; font-size: 0.85em; color: var(--text-sub);>시작 날짜/label> input typedate idfilter-date-start stylewidth: 100%; padding: 8px; border: 1px solid #ECEFF1; border-radius: 6px;> /div> div> label styledisplay: block; margin-bottom: 4px; font-size: 0.85em; color: var(--text-sub);>종료 날짜/label> input typedate idfilter-date-end stylewidth: 100%; padding: 8px; border: 1px solid #ECEFF1; border-radius: 6px;> /div> div> label styledisplay: block; margin-bottom: 4px; font-size: 0.85em; color: var(--text-sub);>지역 필터/label> select idfilter-location stylewidth: 100%; padding: 8px; border: 1px solid #ECEFF1; border-radius: 6px;> option value>전체/option> option value영광>영광/option> option value함평>함평/option> /select> /div> div styledisplay: flex; gap: 8px;> button classbtn btn-primary onclickapplyFilters() styleflex: 1;>🔍 검색/button> button classbtn onclickresetFilters() styleflex: 1;>↺ 초기화/button> /div> /div> /div> div idrecent-shipments>/div> !-- 페이지네이션 --> div idpagination styledisplay: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 20px; flex-wrap: wrap;>/div> /div> /div> !-- 탭 3: 달력 --> div idtab-calendar classtab-content> div classcalendar> div classcalendar-header> h3 idcalendar-title>/h3> div classcalendar-nav> button onclickchangeMonth(-1)>◀ 이전/button> button onclickchangeMonth(0)>오늘/button> button onclickchangeMonth(1)>다음 ▶/button> /div> /div> div classcalendar-grid> div classcalendar-day-header>일/div> div classcalendar-day-header>월/div> div classcalendar-day-header>화/div> div classcalendar-day-header>수/div> div classcalendar-day-header>목/div> div classcalendar-day-header>금/div> div classcalendar-day-header>토/div> /div> div idcalendar-days classcalendar-grid>/div> /div> /div> !-- 탭 4: 등지방 분포도 --> div idtab-backfat classtab-content> div classform-section> h3>📈 등지방 분포도/h3> !-- 파일 업로드 & 히스토리 --> div styledisplay: grid; grid-template-columns: 1fr auto; gap: 16px; margin-bottom: 24px;> div> label styledisplay: block; margin-bottom: 8px; font-weight: 600;>Excel 파일 업로드/label> input typefile idbackfatFile accept.xls,.xlsx stylepadding: 12px; border: 1px solid #ECEFF1; border-radius: 8px; width: 100%;> div styledisplay: flex; gap: 12px; margin-top: 12px;> button classbtn btn-primary onclickprocessBackfatFile()>📊 분석하기/button> button classbtn btn-secondary onclicksaveCurrentBackfatData() idsaveBackfatBtn styledisplay: none;>💾 데이터 저장/button> /div> /div> div> button classbtn btn-secondary onclickshowBackfatHistory() styleheight: 100%; min-width: 120px;> 📁 히스토리br/>보기 /button> /div> /div> !-- 통계 요약 --> div idbackfat-stats styledisplay: none; margin-bottom: 24px;> div styledisplay: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;> div stylebackground: var(--light-blue); padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>총 두수/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--primary-blue); idtotal-count>0/div> /div> div stylebackground: #FFF3E0; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>1+ 등급/div> div stylefont-size: 1.8em; font-weight: 700; color: #EF6C00; idgrade-1plus>0/div> /div> div stylebackground: #E8F5E9; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>1 등급/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--success-green); idgrade-1>0/div> /div> div stylebackground: #FFEBEE; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>2 등급/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--urgent-red); idgrade-2>0/div> /div> div stylebackground: #F5F5F5; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>평균 도체중/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--text-main); idavg-weight>0/div> /div> div stylebackground: #F5F5F5; padding: 16px; border-radius: 8px; text-align: center;> div stylefont-size: 0.9em; color: var(--text-sub); margin-bottom: 4px;>평균 등지방/div> div stylefont-size: 1.8em; font-weight: 700; color: var(--text-main); idavg-backfat>0/div> /div> /div> /div> !-- AI 분석 --> div idai-analysis styledisplay: none; margin-bottom: 24px;> div stylebackground: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white;> div styledisplay: flex; align-items: center; gap: 10px; margin-bottom: 16px;> div stylefont-size: 1.5em;>🤖/div> h3 stylemargin: 0; font-size: 1.1em;>AI 분석 리포트/h3> /div> div idai-comments styledisplay: flex; flex-direction: column; gap: 10px;>/div> /div> /div> !-- 개선 제안 --> div idai-suggestions styledisplay: none; margin-bottom: 24px;> div stylebackground: white; border: 2px solid #E8F5E9; padding: 20px; border-radius: 12px;> div styledisplay: flex; align-items: center; gap: 10px; margin-bottom: 16px;> div stylefont-size: 1.5em;>💡/div> h3 stylemargin: 0; font-size: 1.1em; color: var(--text-main);>개선 제안/h3> /div> div idsuggestion-list styledisplay: flex; flex-direction: column; gap: 12px;>/div> /div> /div> !-- 분포도 차트 --> div idbackfat-chart styledisplay: none;> canvas idchartCanvas stylewidth: 100%; max-width: 1200px; margin: 0 auto; display: block;>/canvas> /div> !-- 안내 메시지 --> div idbackfat-guide styletext-align: center; padding: 60px 20px; color: var(--text-sub);> div stylefont-size: 3em; margin-bottom: 16px;>📊/div> div stylefont-size: 1.1em; font-weight: 600; margin-bottom: 8px;>등지방 분포도/div> div>Excel 파일을 업로드하면 등지방 분포도를 확인할 수 있습니다./div> /div> /div> /div> /div> !-- 모달 --> div idmodal classmodal> div classmodal-content> div classmodal-header> h3 idmodal-title>/h3> button classmodal-close onclickcloseModal()>×/button> /div> div idmodal-body>/div> /div> /div> !-- 등지방 히스토리 모달 --> div idbackfat-history-modal classmodal> div classmodal-content> div classmodal-header> h3>📁 등지방 분석 히스토리/h3> button classmodal-close onclickcloseBackfatHistoryModal()>×/button> /div> div idbackfat-history-body stylemax-height: 500px; overflow-y: auto;> div idbackfat-history-list>/div> /div> /div> /div> script> const SCRIPT_URL https://script.google.com/macros/s/AKfycbxdrTsPPckiyfT5o8xBG5wbWnCPYJkqRVODY0GLvS-2rB5ZZaSfCW_jvYhngrZvoc915A/exec; let currentYear new Date().getFullYear(); let currentMonth new Date().getMonth(); let calendarData {}; let currentScheduledList ; let currentInventory {}; let currentBackfatData null; // 현재 분석 중인 등지방 데이터 // 페이지네이션 관련 변수 let allShipments ; // 전체 출하 기록 let filteredShipments ; // 필터링된 출하 기록 let currentPage 1; let itemsPerPage 10; // 페이지당 항목 수 let currentSortColumn null; let currentSortDirection desc; // asc or desc // API 호출 async function callAPI(action, data null) { try { let url `${SCRIPT_URL}?action${action}`; if (data) { Object.keys(data).forEach(key > { url + `&${key}${encodeURIComponent(datakey)}`; }); } const response await fetch(url, { method: GET, redirect: follow }); return await response.json(); } catch (error) { console.error(API 호출 오류:, error); return { success: false, message: error.message }; } } // 탭 전환 function switchTab(tabName) { document.querySelectorAll(.tab-card).forEach(tab > tab.classList.remove(active)); document.querySelectorAll(.tab-content).forEach(content > content.classList.remove(active)); event.target.closest(.tab-card).classList.add(active); document.getElementById(`tab-${tabName}`).classList.add(active); if (tabName dashboard) { loadDashboardData(); } if (tabName calendar) { loadCalendar(); } } // 예정 두수 입력 토글 function toggleScheduledHeadcountInput() { const type document.getElementById(scheduled-headcount-type).value; const customGroup document.getElementById(scheduled-headcount-custom-group); const customInput document.getElementById(scheduled-headcount-custom); if (type custom) { customGroup.style.display block; customInput.required true; } else { customGroup.style.display none; customInput.required false; } } // 도축장 입력 토글 (출하 예정) function toggleSlaughterhouseInput() { const type document.getElementById(scheduled-slaughterhouse-type).value; const customGroup document.getElementById(scheduled-slaughterhouse-custom-group); const customInput document.getElementById(scheduled-slaughterhouse-custom); if (type custom) { customGroup.style.display block; customInput.required true; } else { customGroup.style.display none; customInput.required false; } } // 도축장 입력 토글 (출하 등록) function toggleActualSlaughterhouseInput() { const type document.getElementById(actual-slaughterhouse-type).value; const customGroup document.getElementById(actual-slaughterhouse-custom-group); const customInput document.getElementById(actual-slaughterhouse-custom); if (type custom) { customGroup.style.display block; customInput.required true; } else { customGroup.style.display none; customInput.required false; } } // 출하 날짜 선택 시 출하 예정 목록 로드 async function loadScheduledForDate() { const date document.getElementById(actual-date).value; if (!date) return; const result await callAPI(getScheduledByDate, { date: date }); if (result.success && result.scheduled.length > 0) { currentScheduledList result.scheduled; renderScheduledList(result.scheduled); document.getElementById(scheduled-list-container).style.display block; } else { document.getElementById(scheduled-list-container).style.display none; currentScheduledList ; } } // 출하 예정 목록 렌더링 function renderScheduledList(scheduledList) { const container document.getElementById(scheduled-list); container.innerHTML ; scheduledList.forEach(item > { const div document.createElement(div); div.className scheduled-item; div.onclick () > selectScheduledItem(item); div.innerHTML ` div classscheduled-item-header>${item.location} - ${item.headCount}두/div> div classscheduled-item-details> 도축장: ${item.slaughterhouse || -} /div> `; container.appendChild(div); }); } // 출하 예정 항목 선택 function selectScheduledItem(item) { document.querySelectorAll(.scheduled-item).forEach(el > el.classList.remove(selected)); event.currentTarget.classList.add(selected); document.getElementById(actual-location).value item.location; // 도축장 선택 const slaughterhouseType document.getElementById(actual-slaughterhouse-type); const options Array.from(slaughterhouseType.options); const matchingOption options.find(opt > opt.value item.slaughterhouse); if (matchingOption) { slaughterhouseType.value item.slaughterhouse; toggleActualSlaughterhouseInput(); } else if (item.slaughterhouse) { slaughterhouseType.value custom; toggleActualSlaughterhouseInput(); document.getElementById(actual-slaughterhouse-custom).value item.slaughterhouse; } document.getElementById(selected-scheduled-id).value item.id; // 돈사 목록 로드 loadBuildingsByLocation(); } // 지역별 돈사 목록 로드 및 재고 가져오기 (한 번에) async function loadBuildingsByLocation() { const location document.getElementById(actual-location).value; if (!location) { document.getElementById(building-input-container).style.display none; return; } const buildings location 영광 ? 비육사 1동, 비육사 3동, 비육사 4동, 비육사 5동 : 비육장; // 한 번의 요청으로 모든 돈사 재고 가져오기 const result await callAPI(getInventoryByLocation, { location }); const buildingData {}; if (result.success && result.inventory) { buildings.forEach(building > { if (result.inventorybuilding) { buildingDatabuilding result.inventorybuilding; } }); } renderBuildingInputs(buildings, buildingData); document.getElementById(building-input-container).style.display block; } // 돈사별 입력 필드 렌더링 function renderBuildingInputs(buildings, buildingData) { const container document.getElementById(building-inputs); container.innerHTML ; buildings.forEach(building > { const data buildingDatabuilding || {}; const finisherCount data.finisherCount || 0; const div document.createElement(div); div.className building-input-item; div.innerHTML ` label>${building}/label> input typenumber classbuilding-count-input data-building${building} data-max${finisherCount} min0 max${finisherCount} value0 placeholder재고: ${finisherCount}두 oninputupdateBuildingTotal() > `; container.appendChild(div); }); updateBuildingTotal(); } // 돈사별 합계 업데이트 function updateBuildingTotal() { const inputs document.querySelectorAll(.building-count-input); let total 0; let hasExceeded false; inputs.forEach(input > { const value parseInt(input.value) || 0; const max parseInt(input.dataset.max) || 0; total + value; if (value > max) { input.style.borderColor var(--urgent-red); hasExceeded true; } else { input.style.borderColor #ECEFF1; } }); const targetTotal parseInt(document.getElementById(actual-headcount).value) || 0; const totalCheck document.getElementById(total-check); const buildingTotal document.getElementById(building-total); buildingTotal.textContent `${total}두`; totalCheck.classList.remove(match, mismatch); if (total targetTotal && total > 0) { totalCheck.classList.add(match); totalCheck.querySelector(span:first-child).textContent ✓ 합계 일치:; } else if (total > 0) { totalCheck.classList.add(mismatch); totalCheck.querySelector(span:first-child).textContent `⚠ 합계 불일치 (목표: ${targetTotal}두):`; } if (hasExceeded) { showFeedbackModal(⚠️ 재고 부족, 일부 돈사의 입력 두수가 재고를 초과했습니다., warning); } } // 총 두수 변경 시 돈사별 합계 업데이트 document.getElementById(actual-headcount)?.addEventListener(input, updateBuildingTotal); // 두당 무게 자동 계산 document.getElementById(actual-weight)?.addEventListener(input, calculateAvgWeight); document.getElementById(actual-headcount)?.addEventListener(input, calculateAvgWeight); function calculateAvgWeight() { const weight parseFloat(document.getElementById(actual-weight).value) || 0; const headCount parseInt(document.getElementById(actual-headcount).value) || 0; if (weight > 0 && headCount > 0) { const avgWeight (weight / headCount).toFixed(2); document.getElementById(actual-avg-weight).value avgWeight; } else { document.getElementById(actual-avg-weight).value ; } } // 피드백 모달 (간단한 알림용) function showFeedbackModal(title, message, type success) { const modal document.getElementById(modal); const modalTitle document.getElementById(modal-title); const modalBody document.getElementById(modal-body); const icon type success ? ✅ : type error ? ❌ : ⚠️; modalTitle.textContent title; modalBody.innerHTML ` div classmodal-icon>${icon}/div> div classmodal-message>${message}/div> button classbtn btn-primary onclickcloseModal()>확인/button> `; modal.classList.add(active); } // 출하 예정 수정 모달 function showEditScheduledModal(item) { const modal document.getElementById(modal); const modalTitle document.getElementById(modal-title); const modalBody document.getElementById(modal-body); modalTitle.textContent 출하 예정 수정; modalBody.innerHTML ` form ideditScheduledForm> div classform-grid> div classform-group> label>날짜 */label> input typedate idedit-scheduled-date value${item.date} required> /div> div classform-group> label>지역 */label> select idedit-scheduled-location required> option value영광 ${item.location 영광 ? selected : }>영광/option> option value함평 ${item.location 함평 ? selected : }>함평/option> /select> /div> div classform-group> label>예정 두수 */label> input typenumber idedit-scheduled-headcount value${item.headCount} min1 required> /div> div classform-group> label>도축장 */label> input typetext idedit-scheduled-slaughterhouse value${item.slaughterhouse || } required> /div> /div> input typehidden idedit-scheduled-id value${item.id}> div classmodal-buttons> button typesubmit classbtn btn-primary>저장/button> button typebutton classbtn btn-danger onclickdeleteScheduled(${item.id})>삭제/button> button typebutton classbtn btn-secondary onclickcloseModal()>취소/button> /div> /form> `; modal.classList.add(active); document.getElementById(editScheduledForm).addEventListener(submit, async function(e) { e.preventDefault(); const data { id: document.getElementById(edit-scheduled-id).value, date: document.getElementById(edit-scheduled-date).value, location: document.getElementById(edit-scheduled-location).value, headCount: document.getElementById(edit-scheduled-headcount).value, slaughterhouse: document.getElementById(edit-scheduled-slaughterhouse).value, meatProcessor: }; const result await callAPI(updateScheduled, data); if (result.success) { showFeedbackModal(✅ 수정 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 수정 실패, result.message, error); } }); } // 출하 예정 삭제 async function deleteScheduled(id) { if (!confirm(정말 삭제하시겠습니까?)) return; const result await callAPI(deleteScheduled, { id: id }); if (result.success) { showFeedbackModal(✅ 삭제 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 삭제 실패, result.message, error); } } // 출하 기록 수정 모달 function showEditActualModal(item) { const modal document.getElementById(modal); const modalTitle document.getElementById(modal-title); const modalBody document.getElementById(modal-body); modalTitle.textContent 출하 기록 수정; modalBody.innerHTML ` form ideditActualForm> div classform-grid> div classform-group> label>날짜 */label> input typedate idedit-actual-date value${item.date} required> /div> div classform-group> label>지역 */label> select idedit-actual-location required> option value영광 ${item.location 영광 ? selected : }>영광/option> option value함평 ${item.location 함평 ? selected : }>함평/option> /select> /div> div classform-group> label>총 중량 (kg) */label> input typenumber idedit-actual-weight value${item.totalWeight} min1 step0.1 required> /div> div classform-group> label>두수 */label> input typenumber idedit-actual-headcount value${item.headCount} min1 required> /div> div classform-group> label>도축장 */label> input typetext idedit-actual-slaughterhouse value${item.slaughterhouse || } required> /div> /div> p stylecolor: var(--warning-orange); text-align: center; margin-top: 16px; font-weight: 600;> ⚠️ 삭제 시 재고가 복구됩니다 /p> input typehidden idedit-actual-id value${item.id}> div classmodal-buttons> button typesubmit classbtn btn-primary>저장/button> button typebutton classbtn btn-danger onclickdeleteActual(${item.id}, ${item.date}, ${item.location})>삭제/button> button typebutton classbtn btn-secondary onclickcloseModal()>취소/button> /div> /form> `; modal.classList.add(active); document.getElementById(editActualForm).addEventListener(submit, async function(e) { e.preventDefault(); const data { id: document.getElementById(edit-actual-id).value, date: document.getElementById(edit-actual-date).value, location: document.getElementById(edit-actual-location).value, totalWeight: document.getElementById(edit-actual-weight).value, headCount: document.getElementById(edit-actual-headcount).value, slaughterhouse: document.getElementById(edit-actual-slaughterhouse).value, meatProcessor: }; const result await callAPI(updateActual, data); if (result.success) { showFeedbackModal(✅ 수정 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 수정 실패, result.message, error); } }); } // 출하 기록 삭제 (재고 복구) async function deleteActual(id, date, location) { if (!confirm(정말 삭제하시겠습니까?\n재고가 복구됩니다.)) return; const result await callAPI(deleteActual, { id: id, date: date, location: location }); if (result.success) { showFeedbackModal(✅ 삭제 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 삭제 실패, result.message, error); } } // 출하 완료 버튼 async function completeShipment(location, building) { if (!confirm(`${building}의 출하를 완료하시겠습니까?\n출하 두수와 사고 두수가 리셋됩니다.`)) return; const result await callAPI(completeShipment, { location: location, building: building }); if (result.success) { showFeedbackModal(✅ 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 실패, result.message, error); } } // 돈사 전환 버튼 async function transferBuilding(location, building) { if (!confirm(`${building}의 육성돈을 비육돈으로 전환하시겠습니까?`)) return; const result await callAPI(transferBuilding, { location: location, building: building }); if (result.success) { showFeedbackModal(✅ 전환 완료, result.message, success); loadDashboardData(); } else { showFeedbackModal(❌ 전환 실패, result.message, error); } } function closeModal() { document.getElementById(modal).classList.remove(active); } // 돈사 옵션 업데이트 function updateBuildingOptions() { const location document.getElementById(inventory-location).value; const buildingSelect document.getElementById(inventory-building); buildingSelect.innerHTML option value>선택하세요/option>; if (location 영광) { 비육사 1동, 비육사 3동, 비육사 4동, 비육사 5동.forEach(building > { const option document.createElement(option); option.value building; option.textContent building; buildingSelect.appendChild(option); }); } else if (location 함평) { const option document.createElement(option); option.value 비육장; option.textContent 비육장; buildingSelect.appendChild(option); } } // 돈사 선택 시 마지막 재고 데이터 로드 async function loadInventoryData() { const location document.getElementById(inventory-location).value; const building document.getElementById(inventory-building).value; if (!location || !building) return; const result await callAPI(getInventoryByBuilding, { location, building }); if (result.success && result.inventory) { document.getElementById(inventory-grower).value result.inventory.growerCount; document.getElementById(inventory-finisher).value result.inventory.finisherCount; document.getElementById(inventory-accident).value result.inventory.accidentCount; } } // 출하 예정 등록 document.getElementById(scheduledForm).addEventListener(submit, async function(e) { e.preventDefault(); const submitBtn e.target.querySelector(buttontypesubmit); submitBtn.disabled true; submitBtn.textContent 등록 중...; const headcountType document.getElementById(scheduled-headcount-type).value; const headCount headcountType 80 ? 80 : document.getElementById(scheduled-headcount-custom).value; const slaughterhouseType document.getElementById(scheduled-slaughterhouse-type).value; const slaughterhouse slaughterhouseType custom ? document.getElementById(scheduled-slaughterhouse-custom).value : slaughterhouseType; const data { date: document.getElementById(scheduled-date).value, location: document.getElementById(scheduled-location).value, headCount: headCount, slaughterhouse: slaughterhouse, meatProcessor: }; const result await callAPI(addScheduled, data); if (result.success) { showFeedbackModal(✅ 등록 완료, result.message, success); document.getElementById(scheduledForm).reset(); } else { showFeedbackModal(❌ 등록 실패, result.message, error); } submitBtn.disabled false; submitBtn.textContent 등록하기; }); // 출하 등록 document.getElementById(actualForm).addEventListener(submit, async function(e) { e.preventDefault(); // 돈사별 합계 검증 const inputs document.querySelectorAll(.building-count-input); const buildingDetails {}; let buildingTotal 0; inputs.forEach(input > { const building input.dataset.building; const count parseInt(input.value) || 0; buildingDetailsbuilding count; buildingTotal + count; }); const targetTotal parseInt(document.getElementById(actual-headcount).value) || 0; if (buildingTotal ! targetTotal) { showFeedbackModal(⚠️ 두수 불일치, `돈사별 합계(${buildingTotal}두)와 총 두수(${targetTotal}두)가 일치하지 않습니다.`, warning); return; } const submitBtn e.target.querySelector(buttontypesubmit); submitBtn.disabled true; submitBtn.textContent 등록 중...; const slaughterhouseType document.getElementById(actual-slaughterhouse-type).value; const slaughterhouse slaughterhouseType custom ? document.getElementById(actual-slaughterhouse-custom).value : slaughterhouseType; const data { date: document.getElementById(actual-date).value, location: document.getElementById(actual-location).value, totalWeight: document.getElementById(actual-weight).value, headCount: document.getElementById(actual-headcount).value, slaughterhouse: slaughterhouse, meatProcessor: , scheduledId: document.getElementById(selected-scheduled-id).value, buildingDetails: JSON.stringify(buildingDetails) }; const result await callAPI(addActual, data); if (result.success) { showFeedbackModal(✅ 등록 완료, result.message, success); document.getElementById(actualForm).reset(); document.getElementById(actual-avg-weight).value ; document.getElementById(scheduled-list-container).style.display none; document.getElementById(building-input-container).style.display none; } else { showFeedbackModal(❌ 등록 실패, result.message, error); } submitBtn.disabled false; submitBtn.textContent 등록하기; }); // 돈사 재고 입력 document.getElementById(inventoryForm).addEventListener(submit, async function(e) { e.preventDefault(); const location document.getElementById(inventory-location).value; const building document.getElementById(inventory-building).value; if (!location || !building) { showFeedbackModal(⚠️ 입력 오류, 지역과 돈사를 선택해주세요., warning); return; } const submitBtn e.target.querySelector(buttontypesubmit); submitBtn.disabled true; submitBtn.textContent 입력 중...; const data { location: location, building: building, growerCount: document.getElementById(inventory-grower).value, finisherCount: document.getElementById(inventory-finisher).value, accidentCount: document.getElementById(inventory-accident).value }; const result await callAPI(addInventory, data); if (result.success) { showFeedbackModal(✅ 입력 완료, result.message, success); } else { showFeedbackModal(❌ 입력 실패, result.message, error); } submitBtn.disabled false; submitBtn.textContent 입력하기; }); // 대시보드 데이터 로드 async function loadDashboardData() { setSyncStatus(syncing, 불러오는 중...); document.getElementById(dashboard-loading).style.display block; document.getElementById(dashboard-content).style.display none; const result await callAPI(getDashboard); if (result.success) { renderDashboard(result); document.getElementById(dashboard-loading).style.display none; document.getElementById(dashboard-content).style.display block; setSyncStatus(synced, 불러오기 완료); } else { showFeedbackModal(❌ 로드 실패, 데이터 로드 실패: + result.message, error); document.getElementById(dashboard-loading).style.display none; setSyncStatus(error, 불러오기 실패); } } // 대시보드 렌더링 function renderDashboard(data) { renderInventorySection(data.inventory); renderWeeklySchedule(data.weeklySchedule); renderMonthlyStats(data.monthlyStats); renderRecentShipments(data.recentShipments); } // 재고 섹션 렌더링 function renderInventorySection(inventory) { const container document.getElementById(inventory-section); container.innerHTML ; // 영광 카드 const youngkwangCard document.createElement(div); youngkwangCard.className region-card; const youngkwangBuildings 비육사 1동, 비육사 3동, 비육사 4동, 비육사 5동; let youngkwangBuildingsHTML ; youngkwangBuildings.forEach(building > { const data inventory.youngkwangbuilding || { currentCount: 0, finisherCount: 0, growerCount: 0, accidentCount: 0, shipmentCount: 0 }; youngkwangBuildingsHTML + ` div classbuilding-card> div classbuilding-name> span>${building}/span> span classbuilding-total>(${data.currentCount}두)/span> /div> div classbuilding-stats> div classstat-item> div classstat-value>${data.finisherCount}/div> div classstat-label>비육돈/div> /div> div classstat-item> div classstat-value>${data.growerCount}/div> div classstat-label>육성돈/div> /div> div classstat-item> div classstat-value>${data.accidentCount}/div> div classstat-label>사고/div> /div> div classstat-item> div classstat-value>${data.shipmentCount}/div> div classstat-label>출하/div> /div> /div> div classbuilding-actions> button classbtn-complete onclickcompleteShipment(영광, ${building})>출하완료/button> button classbtn-transfer onclicktransferBuilding(영광, ${building})>돈사전환/button> /div> /div> `; }); youngkwangCard.innerHTML ` div classregion-header> span classregion-title>🏠 영광 비육사/span> span classregion-total>(총 ${inventory.youngkwangTotal}두)/span> /div> div classbuildings-grid> ${youngkwangBuildingsHTML} /div> `; container.appendChild(youngkwangCard); // 함평 카드 const hampyeongCard document.createElement(div); hampyeongCard.className region-card; const hampyeongData inventory.hampyeong비육장 || { currentCount: 0, finisherCount: 0, growerCount: 0, accidentCount: 0, shipmentCount: 0 }; hampyeongCard.innerHTML ` div classregion-header> span classregion-title>🏠 함평 비육장/span> span classregion-total>(총 ${inventory.hampyeongTotal}두)/span> /div> div classbuildings-grid> div classbuilding-card> div classbuilding-name> span>비육장/span> span classbuilding-total>(${hampyeongData.currentCount}두)/span> /div> div classbuilding-stats> div classstat-item> div classstat-value>${hampyeongData.finisherCount}/div> div classstat-label>비육돈/div> /div> div classstat-item> div classstat-value>${hampyeongData.growerCount}/div> div classstat-label>육성돈/div> /div> div classstat-item> div classstat-value>${hampyeongData.accidentCount}/div> div classstat-label>사고/div> /div> div classstat-item> div classstat-value>${hampyeongData.shipmentCount}/div> div classstat-label>출하/div> /div> /div> div classbuilding-actions> button classbtn-complete onclickcompleteShipment(함평, 비육장)>출하완료/button> button classbtn-transfer onclicktransferBuilding(함평, 비육장)>돈사전환/button> /div> /div> /div> `; container.appendChild(hampyeongCard); } // 주간 출하 예정 렌더링 function renderWeeklySchedule(schedule) { const container document.getElementById(weekly-schedule); if (schedule.length 0) { container.innerHTML div classempty-state>div classempty-state-icon>📭/div>p>이번 주 출하 예정이 없습니다./p>/div>; return; } container.innerHTML div classtable-container>table>thead>tr>th>날짜/th>th>지역/th>th>예정 두수/th>th>도축장/th>th>상태/th>th>작업/th>/tr>/thead>tbody idschedule-tbody>/tbody>/table>/div>; const tbody document.getElementById(schedule-tbody); schedule.forEach(item > { const row document.createElement(tr); row.innerHTML ` td>${item.date}/td> td>${item.location}/td> td>${item.headCount}두/td> td>${item.slaughterhouse || -}/td> td>span classrecord-status status-scheduled>${item.status}/span>/td> td> button classbtn btn-primary btn-small onclickshowEditScheduledModal(${JSON.stringify(item)})>수정/button> /td> `; tbody.appendChild(row); }); } // 월간 통계 렌더링 function renderMonthlyStats(stats) { const container document.getElementById(monthly-stats); container.innerHTML ` div classcard> div classcard-header> div classcard-title>총 출하 두수/div> div classcard-icon>🐷/div> /div> div classcard-value>${stats.totalHeadCount}/div> div classcard-label>두/div> /div> div classcard> div classcard-header> div classcard-title>총 중량/div> div classcard-icon>⚖️/div> /div> div classcard-value>${Number(stats.totalWeight).toLocaleString()}/div> div classcard-label>kg/div> /div> div classcard> div classcard-header> div classcard-title>평균 두당 무게/div> div classcard-icon>📊/div> /div> div classcard-value>${stats.avgWeight}/div> div classcard-label>kg/div> /div> div classcard> div classcard-header> div classcard-title>지역별 비율/div> div classcard-icon>📍/div> /div> div stylemargin-top: 15px;> div styledisplay: flex; justify-content: space-between; margin-bottom: 10px;> span>영광/span> strong>${stats.youngkwangRatio}%/strong> /div> div styledisplay: flex; justify-content: space-between;> span>함평/span> strong>${stats.hampyeongRatio}%/strong> /div> /div> /div> `; } // 최근 출하 기록 렌더링 (페이지네이션 및 필터 적용) function renderRecentShipments(shipments) { allShipments shipments || ; filteredShipments ...allShipments; currentPage 1; renderShipmentsTable(); } // 출하 기록 테이블 렌더링 function renderShipmentsTable() { const container document.getElementById(recent-shipments); if (filteredShipments.length 0) { container.innerHTML div classempty-state>div classempty-state-icon>📭/div>p>출하 기록이 없습니다./p>/div>; document.getElementById(pagination).innerHTML ; return; } // 페이지네이션 계산 const totalPages Math.ceil(filteredShipments.length / itemsPerPage); const startIndex (currentPage - 1) * itemsPerPage; const endIndex Math.min(startIndex + itemsPerPage, filteredShipments.length); const pageData filteredShipments.slice(startIndex, endIndex); // 테이블 헤더 (정렬 가능) const sortIcon (column) > { if (currentSortColumn column) { return currentSortDirection asc ? ▲ : ▼; } return ; }; container.innerHTML ` div classtable-container> table> thead> tr> th onclicksortTable(date) stylecursor: pointer;>날짜${sortIcon(date)}/th> th onclicksortTable(location) stylecursor: pointer;>지역${sortIcon(location)}/th> th onclicksortTable(headCount) stylecursor: pointer;>두수${sortIcon(headCount)}/th> th onclicksortTable(totalWeight) stylecursor: pointer;>총 중량${sortIcon(totalWeight)}/th> th onclicksortTable(avgWeight) stylecursor: pointer;>평균 두당 무게${sortIcon(avgWeight)}/th> th>작업/th> /tr> /thead> tbody idshipments-tbody>/tbody> /table> /div> div styletext-align: center; margin-top: 12px; color: var(--text-sub); font-size: 0.9em;> 전체 ${filteredShipments.length}건 중 ${startIndex + 1}-${endIndex}번째 표시 /div> `; const tbody document.getElementById(shipments-tbody); pageData.forEach(item > { const row document.createElement(tr); row.innerHTML ` td>${item.date}/td> td>${item.location}/td> td>${item.headCount}두/td> td>${Number(item.totalWeight).toLocaleString()}kg/td> td>${item.avgWeight}kg/td> td> button classbtn btn-primary btn-small onclickshowEditActualModal(${JSON.stringify(item)})>수정/button> /td> `; tbody.appendChild(row); }); // 페이지네이션 렌더링 renderPagination(totalPages); } // 페이지네이션 UI 렌더링 function renderPagination(totalPages) { const container document.getElementById(pagination); if (totalPages 1) { container.innerHTML ; return; } let html ; // 이전 버튼 if (currentPage > 1) { html + `button classbtn btn-small onclickgoToPage(${currentPage - 1})>◀/button>`; } // 페이지 번호 const maxVisible 5; // 최대 표시할 페이지 번호 개수 let startPage Math.max(1, currentPage - Math.floor(maxVisible / 2)); let endPage Math.min(totalPages, startPage + maxVisible - 1); if (endPage - startPage maxVisible - 1) { startPage Math.max(1, endPage - maxVisible + 1); } // 첫 페이지 if (startPage > 1) { html + `button classbtn btn-small onclickgoToPage(1)>1/button>`; if (startPage > 2) { html + `span stylepadding: 0 8px; color: var(--text-sub);>.../span>`; } } // 페이지 번호들 for (let i startPage; i endPage; i++) { if (i currentPage) { html + `button classbtn btn-primary btn-small stylefont-weight: 700;>${i}/button>`; } else { html + `button classbtn btn-small onclickgoToPage(${i})>${i}/button>`; } } // 마지막 페이지 if (endPage totalPages) { if (endPage totalPages - 1) { html + `span stylepadding: 0 8px; color: var(--text-sub);>.../span>`; } html + `button classbtn btn-small onclickgoToPage(${totalPages})>${totalPages}/button>`; } // 다음 버튼 if (currentPage totalPages) { html + `button classbtn btn-small onclickgoToPage(${currentPage + 1})>▶/button>`; } container.innerHTML html; } // 페이지 이동 function goToPage(page) { currentPage page; renderShipmentsTable(); // 스크롤을 테이블 위로 이동 document.getElementById(recent-shipments).scrollIntoView({ behavior: smooth, block: start }); } // 테이블 정렬 function sortTable(column) { if (currentSortColumn column) { // 같은 컬럼 클릭 시 방향 전환 currentSortDirection currentSortDirection asc ? desc : asc; } else { currentSortColumn column; currentSortDirection desc; } filteredShipments.sort((a, b) > { let aVal acolumn; let bVal bcolumn; // 숫자형 데이터 처리 if (column headCount || column totalWeight) { aVal Number(aVal) || 0; bVal Number(bVal) || 0; } else if (column avgWeight) { aVal parseFloat(aVal) || 0; bVal parseFloat(bVal) || 0; } if (currentSortDirection asc) { return aVal > bVal ? 1 : -1; } else { return aVal bVal ? 1 : -1; } }); currentPage 1; // 정렬 후 첫 페이지로 renderShipmentsTable(); } // 필터 적용 function applyFilters() { const dateStartFilter document.getElementById(filter-date-start).value; const dateEndFilter document.getElementById(filter-date-end).value; const locationFilter document.getElementById(filter-location).value; filteredShipments allShipments.filter(item > { let match true; // 시작 날짜 필터 if (dateStartFilter && item.date dateStartFilter) { match false; } // 종료 날짜 필터 if (dateEndFilter && item.date > dateEndFilter) { match false; } // 지역 필터 if (locationFilter && item.location ! locationFilter) { match false; } return match; }); currentPage 1; renderShipmentsTable(); } // 필터 초기화 function resetFilters() { document.getElementById(filter-date-start).value ; document.getElementById(filter-date-end).value ; document.getElementById(filter-location).value ; filteredShipments ...allShipments; currentPage 1; currentSortColumn null; currentSortDirection desc; renderShipmentsTable(); } // 달력 로드 async function loadCalendar() { setSyncStatus(syncing, 달력 로딩 중...); const title `${currentYear}년 ${currentMonth + 1}월`; document.getElementById(calendar-title).textContent title; const result await callAPI(getCalendar, { year: currentYear, month: currentMonth }); if (result.success) { calendarData result.calendar; renderCalendar(); setSyncStatus(synced, 달력 로드 완료); } else { setSyncStatus(error, 달력 로드 실패); } } // 달력 렌더링 function renderCalendar() { const container document.getElementById(calendar-days); container.innerHTML ; const firstDay new Date(currentYear, currentMonth, 1).getDay(); const daysInMonth new Date(currentYear, currentMonth + 1, 0).getDate(); const today new Date(); const prevMonthDays new Date(currentYear, currentMonth, 0).getDate(); for (let i firstDay - 1; i > 0; i--) { const day prevMonthDays - i; const dayDiv createDayElement(day, true, false); container.appendChild(dayDiv); } for (let day 1; day daysInMonth; day++) { const date new Date(currentYear, currentMonth, day); const isToday date.toDateString() today.toDateString(); const dayDiv createDayElement(day, false, isToday); // 로컬 시간대 기준으로 dateKey 생성 const year date.getFullYear(); const month String(date.getMonth() + 1).padStart(2, 0); const dayStr String(date.getDate()).padStart(2, 0); const dateKey `${year}-${month}-${dayStr}`; if (calendarDatadateKey) { const data calendarDatadateKey; if (data.scheduled.length > 0) { const indicator document.createElement(div); indicator.className day-indicator indicator-scheduled; dayDiv.appendChild(indicator); } if (data.actual.length > 0) { const indicator document.createElement(div); indicator.className day-indicator indicator-completed; dayDiv.appendChild(indicator); } dayDiv.onclick () > showDayDetails(dateKey, data); } container.appendChild(dayDiv); } const remainingDays 42 - (firstDay + daysInMonth); for (let day 1; day remainingDays; day++) { const dayDiv createDayElement(day, true, false); container.appendChild(dayDiv); } } function createDayElement(day, isOtherMonth, isToday) { const dayDiv document.createElement(div); dayDiv.className calendar-day; if (isOtherMonth) dayDiv.classList.add(other-month); if (isToday) dayDiv.classList.add(today); const dayNumber document.createElement(div); dayNumber.className day-number; dayNumber.textContent day; dayDiv.appendChild(dayNumber); return dayDiv; } function showDayDetails(dateKey, data) { // dateKey를 직접 파싱하여 로컬 시간대로 처리 const year, month, day dateKey.split(-).map(Number); const modalTitle `${year}년 ${month}월 ${day}일`; document.getElementById(modal-title).textContent modalTitle; let html ; if (data.scheduled.length > 0) { html + h4 stylemargin-bottom: 15px; color: var(--primary-blue); font-weight: 700;>📋 출하 예정/h4>; data.scheduled.forEach(item > { html + ` div classrecord-item> div classrecord-header> div classrecord-type>예정 출하/div> div classrecord-status status-${item.status 예정 ? scheduled : completed}>${item.status}/div> /div> div classrecord-details> div classrecord-detail> div classdetail-label>지역/div> div classdetail-value>${item.location}/div> /div> div classrecord-detail> div classdetail-label>예정 두수/div> div classdetail-value>${item.headCount}두/div> /div> div classrecord-detail> div classdetail-label>도축장/div> div classdetail-value>${item.slaughterhouse || -}/div> /div> /div> /div> `; }); } if (data.actual.length > 0) { html + h4 stylemargin: 20px 0 15px; color: var(--success-green); font-weight: 700;>✅ 실제 출하/h4>; data.actual.forEach(item > { html + ` div classrecord-item styleborder-left-color: var(--success-green);> div classrecord-header> div classrecord-type stylecolor: var(--success-green);>완료된 출하/div> div classrecord-status status-completed>완료/div> /div> div classrecord-details> div classrecord-detail> div classdetail-label>지역/div> div classdetail-value>${item.location}/div> /div> div classrecord-detail> div classdetail-label>출하 두수/div> div classdetail-value>${item.headCount}두/div> /div> div classrecord-detail> div classdetail-label>총 중량/div> div classdetail-value>${Number(item.totalWeight).toLocaleString()}kg/div> /div> div classrecord-detail> div classdetail-label>평균 두당 무게/div> div classdetail-value>${item.avgWeight}kg/div> /div> /div> /div> `; }); } if (html ) { html div classempty-state>div classempty-state-icon>📭/div>p>이 날짜에 기록이 없습니다./p>/div>; } document.getElementById(modal-body).innerHTML html; document.getElementById(modal).classList.add(active); } function changeMonth(delta) { if (delta 0) { const today new Date(); currentYear today.getFullYear(); currentMonth today.getMonth(); } else { currentMonth + delta; if (currentMonth > 11) { currentMonth 0; currentYear++; } else if (currentMonth 0) { currentMonth 11; currentYear--; } } loadCalendar(); } document.getElementById(modal).addEventListener(click, function(e) { if (e.target this) { closeModal(); } }); window.addEventListener(load, function() { const today new Date().toISOString().split(T)0; document.getElementById(scheduled-date).value today; document.getElementById(actual-date).value today; // 페이지 로딩 시 오늘 날짜의 출하 예정 목록도 자동으로 로드 loadScheduledForDate(); setSyncStatus(synced, 준비 완료); }); // 동기화 상태 표시 함수 function setSyncStatus(status, text) { const dot document.getElementById(syncDot); dot.className sync-dot; if (status syncing) dot.classList.add(syncing); if (status error) dot.classList.add(error); document.getElementById(syncText).textContent text; } // 등지방 분포도 - Excel 파일 처리 async function processBackfatFile() { const fileInput document.getElementById(backfatFile); const file fileInput.files0; if (!file) { alert(파일을 선택해주세요.); return; } setSyncStatus(syncing, 파일 분석 중...); try { const data await readExcelFile(file); currentBackfatData data; // 현재 데이터 저장 const stats calculateBackfatStats(data); // 통계 표시 displayBackfatStats(stats); // AI 분석 생성 및 표시 const aiComments generateAIAnalysis(data, stats); displayAIAnalysis(aiComments); // 개선 제안 생성 및 표시 const suggestions generateSuggestions(data); displaySuggestions(suggestions); // 차트 그리기 drawBackfatChart(data); // UI 표시 document.getElementById(backfat-guide).style.display none; document.getElementById(backfat-stats).style.display block; document.getElementById(backfat-chart).style.display block; document.getElementById(saveBackfatBtn).style.display inline-block; // 저장 버튼 표시 setSyncStatus(synced, 분석 완료); } catch (error) { alert(파일 처리 중 오류가 발생했습니다: + error.message); setSyncStatus(error, 분석 실패); } } // Excel 파일 읽기 function readExcelFile(file) { return new Promise((resolve, reject) > { const reader new FileReader(); reader.onload function(e) { try { const data new Uint8Array(e.target.result); const workbook XLSX.read(data, { type: array }); const firstSheet workbook.Sheetsworkbook.SheetNames0; const jsonData XLSX.utils.sheet_to_json(firstSheet, { header: 1 }); // 35행부터 데이터 추출 (인덱스 34부터) const pigData ; for (let i 34; i jsonData.length; i++) { const row jsonDatai; if (row14 && row16) { // 도체중(14), 등지방(16) const weight parseFloat(row14); const backfat parseFloat(row16); if (!isNaN(weight) && !isNaN(backfat)) { pigData.push({ weight, backfat }); } } } if (pigData.length 0) { reject(new Error(유효한 데이터를 찾을 수 없습니다.)); } else { resolve(pigData); } } catch (error) { reject(error); } }; reader.onerror () > reject(new Error(파일 읽기 실패)); reader.readAsArrayBuffer(file); }); } // 등급 판정 함수 function getGrade(weight, backfat) { // 1+ 등급: 도체중 83-93kg, 등지방 17-25mm 미만 if (weight > 83 && weight 93 && backfat > 17 && backfat 25) { return 1+; } // 1 등급 if ((weight > 80 && weight 83 && backfat > 15 && backfat 28) || (weight > 83 && weight 93 && backfat > 15 && backfat 17) || (weight > 83 && weight 93 && backfat > 25 && backfat 28) || (weight > 93 && weight 98 && backfat > 15 && backfat 28)) { return 1; } // 2 등급 return 2; } // 통계 계산 function calculateBackfatStats(data) { let grade1plus 0, grade1 0, grade2 0; let totalWeight 0, totalBackfat 0; data.forEach(pig > { const grade getGrade(pig.weight, pig.backfat); if (grade 1+) grade1plus++; else if (grade 1) grade1++; else grade2++; totalWeight + pig.weight; totalBackfat + pig.backfat; }); return { totalCount: data.length, grade1plus, grade1, grade2, avgWeight: (totalWeight / data.length).toFixed(1), avgBackfat: (totalBackfat / data.length).toFixed(1) }; } // 통계 표시 function displayBackfatStats(stats) { document.getElementById(total-count).textContent stats.totalCount + 두; document.getElementById(grade-1plus).textContent stats.grade1plus + 두; document.getElementById(grade-1).textContent stats.grade1 + 두; document.getElementById(grade-2).textContent stats.grade2 + 두; document.getElementById(avg-weight).textContent stats.avgWeight + kg; document.getElementById(avg-backfat).textContent stats.avgBackfat + mm; } // AI 분석 코멘트 생성 function generateAIAnalysis(data, stats) { const comments ; // 1등급 이상 비율 분석 (1+ + 1등급) const grade1AboveCount stats.grade1plus + stats.grade1; const grade1AbovePercent ((grade1AboveCount / stats.totalCount) * 100).toFixed(1); if (grade1AbovePercent > 75) { comments.push({ icon: ✅, text: `1등급 이상 비율 ${grade1AbovePercent}% (${grade1AboveCount}두) - 매우 우수한 수준입니다!`, type: success }); } else if (grade1AbovePercent > 60) { comments.push({ icon: 👍, text: `1등급 이상 비율 ${grade1AbovePercent}% (${grade1AboveCount}두) - 양호한 수준입니다.`, type: good }); } else { comments.push({ icon: ⚠️, text: `1등급 이상 비율 ${grade1AbovePercent}% (${grade1AboveCount}두) - 개선이 필요합니다.`, type: warning }); } // 평균 도체중 분석 (1등급 기준: 80-98kg) const avgWeight parseFloat(stats.avgWeight); if (avgWeight > 80 && avgWeight 98) { comments.push({ icon: ✅, text: `평균 도체중 ${stats.avgWeight}kg - 1등급 목표 범위 내입니다.`, type: success }); } else if (avgWeight 80) { comments.push({ icon: 📊, text: `평균 도체중 ${stats.avgWeight}kg - 목표보다 낮습니다.`, type: info }); } else { comments.push({ icon: 📊, text: `평균 도체중 ${stats.avgWeight}kg - 목표보다 높습니다.`, type: info }); } // 평균 등지방 분석 (1등급 기준: 15-28mm) const avgBackfat parseFloat(stats.avgBackfat); if (avgBackfat > 15 && avgBackfat 28) { comments.push({ icon: ✅, text: `평균 등지방 ${stats.avgBackfat}mm - 1등급 적정 범위입니다.`, type: success }); } else if (avgBackfat 15) { comments.push({ icon: 📉, text: `평균 등지방 ${stats.avgBackfat}mm - 다소 낮은 편입니다.`, type: info }); } else { comments.push({ icon: 📈, text: `평균 등지방 ${stats.avgBackfat}mm - 다소 높은 편입니다.`, type: info }); } return comments; } // 개선 제안 생성 (4가지 케이스 분석) function generateSuggestions(data) { const suggestions ; // 4가지 구역별 분석 (1등급 기준으로 조정) let highBackfatHighWeight 0; // 등지방↑ 도체중↑ (오른쪽 위) let highBackfatLowWeight 0; // 등지방↑ 도체중↓ (왼쪽 위) let lowBackfatHighWeight 0; // 등지방↓ 도체중↑ (오른쪽 아래) let lowBackfatLowWeight 0; // 등지방↓ 도체중↓ (왼쪽 아래) data.forEach(pig > { const isHighWeight pig.weight > 89; // 1등급 중간값 const isHighBackfat pig.backfat > 21.5; // 1등급 중간값 if (isHighBackfat && isHighWeight) { highBackfatHighWeight++; } else if (isHighBackfat && !isHighWeight) { highBackfatLowWeight++; } else if (!isHighBackfat && isHighWeight) { lowBackfatHighWeight++; } else { lowBackfatLowWeight++; } }); const total data.length; // 1. 과비육 (등지방↑ 도체중↑) if (highBackfatHighWeight > total * 0.15) { suggestions.push({ icon: ⚠️, title: 과비육 개체 발견, desc: `${highBackfatHighWeight}두가 과비육 상태입니다.`, action: 다음 출하는 3-5일 앞당기는 것을 권장합니다., severity: warning }); } // 2. 비효율 비육 (등지방↑ 도체중↓) if (highBackfatLowWeight > total * 0.1) { suggestions.push({ icon: 🔄, title: 비효율적 비육 패턴, desc: `${highBackfatLowWeight}두가 살은 적고 지방만 많은 상태입니다.`, action: 사료 배합 재검토 및 운동량 증가를 권장합니다., severity: warning }); } // 3. 우수 개체 (등지방↓ 도체중↑) if (lowBackfatHighWeight > total * 0.2) { suggestions.push({ icon: ✨, title: 우수 개체 다수, desc: `${lowBackfatHighWeight}두가 이상적인 비육 상태입니다.`, action: 현재 관리 방식을 유지하면 1등급 가능성이 높습니다., severity: success }); } // 4. 미성숙 (등지방↓ 도체중↓) if (lowBackfatLowWeight > total * 0.2) { suggestions.push({ icon: ⏰, title: 미성숙 개체 다수, desc: `${lowBackfatLowWeight}두가 아직 출하 시기가 이릅니다.`, action: 7-10일 더 사육 후 출하를 권장합니다., severity: info }); } // 등지방 극단치 분석 (1등급 기준) const veryHighBackfat data.filter(p > p.backfat > 28).length; const veryLowBackfat data.filter(p > p.backfat 15).length; if (veryHighBackfat > 0) { suggestions.push({ icon: 🔴, title: 등지방 과다 개체, desc: `${veryHighBackfat}두의 등지방이 28mm 이상입니다.`, action: 2등급 가능성 - 즉시 출하를 고려하세요., severity: urgent }); } if (veryLowBackfat > 0) { suggestions.push({ icon: 🔵, title: 등지방 부족 개체, desc: `${veryLowBackfat}두의 등지방이 15mm 미만입니다.`, action: 사료 급여량 증가 또는 출하 연기를 검토하세요., severity: info }); } // 도체중 극단치 분석 const veryLowWeight data.filter(p > p.weight 80).length; const veryHighWeight data.filter(p > p.weight > 98).length; if (veryLowWeight > 0) { suggestions.push({ icon: ⚖️, title: 도체중 부족 개체, desc: `${veryLowWeight}두의 도체중이 80kg 미만입니다.`, action: 출하 시기를 늦추거나 사료 급여량을 늘리세요., severity: info }); } if (veryHighWeight > 0) { suggestions.push({ icon: ⚖️, title: 도체중 과다 개체, desc: `${veryHighWeight}두의 도체중이 98kg 초과입니다.`, action: 사료비 손실 가능성 - 적정 시기 출하를 권장합니다., severity: warning }); } // 개선 제안이 없으면 격려 메시지 if (suggestions.length 0) { suggestions.push({ icon: 🎉, title: 완벽한 관리 상태, desc: 모든 개체가 1등급 적정 범위 내에 있습니다., action: 현재의 사육 관리를 유지하세요!, severity: success }); } return suggestions; } // AI 분석 표시 function displayAIAnalysis(comments) { const container document.getElementById(ai-comments); container.innerHTML ; comments.forEach(comment > { const div document.createElement(div); div.style.cssText display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.15); padding: 12px; border-radius: 8px;; div.innerHTML ` span stylefont-size: 1.2em;>${comment.icon}/span> span styleflex: 1;>${comment.text}/span> `; container.appendChild(div); }); document.getElementById(ai-analysis).style.display block; } // 개선 제안 표시 function displaySuggestions(suggestions) { const container document.getElementById(suggestion-list); container.innerHTML ; const severityColors { urgent: #FFEBEE, warning: #FFF3E0, info: #E3F2FD, success: #E8F5E9 }; const severityBorders { urgent: #EF5350, warning: #FF9800, info: #2196F3, success: #4CAF50 }; suggestions.forEach(suggestion > { const div document.createElement(div); div.style.cssText ` background: ${severityColorssuggestion.severity}; border-left: 4px solid ${severityBorderssuggestion.severity}; padding: 16px; border-radius: 8px; `; div.innerHTML ` div styledisplay: flex; align-items: flex-start; gap: 12px;> span stylefont-size: 1.5em;>${suggestion.icon}/span> div styleflex: 1;> div stylefont-weight: 700; color: var(--text-main); margin-bottom: 6px;>${suggestion.title}/div> div stylecolor: var(--text-sub); font-size: 0.9em; margin-bottom: 8px;>${suggestion.desc}/div> div stylecolor: ${severityBorderssuggestion.severity}; font-weight: 600; font-size: 0.9em;> 💡 ${suggestion.action} /div> /div> /div> `; container.appendChild(div); }); document.getElementById(ai-suggestions).style.display block; } // 분포도 차트 그리기 function drawBackfatChart(data) { const canvas document.getElementById(chartCanvas); const ctx canvas.getContext(2d); // 캔버스 크기 설정 canvas.width 1200; canvas.height 800; // 여백 const margin { top: 60, right: 80, bottom: 80, left: 80 }; const chartWidth canvas.width - margin.left - margin.right; const chartHeight canvas.height - margin.top - margin.bottom; // 배경 흰색 ctx.fillStyle white; ctx.fillRect(0, 0, canvas.width, canvas.height); // 도체중 범위: 65-110kg // 등지방 범위: 5-38mm const weightMin 65, weightMax 110; const backfatMin 5, backfatMax 38; // 스케일 const xScale chartWidth / (weightMax - weightMin); const yScale chartHeight / (backfatMax - backfatMin); ctx.save(); ctx.translate(margin.left, margin.top); // 등급별 배경 영역 그리기 // 1+ 등급 영역 (연한 파란색 배경 + 파란색 테두리) ctx.fillStyle rgba(33, 150, 243, 0.08); const x1plus (83 - weightMin) * xScale; const x2plus (93 - weightMin) * xScale; const y1plus chartHeight - (25 - backfatMin) * yScale; const y2plus chartHeight - (17 - backfatMin) * yScale; ctx.fillRect(x1plus, y1plus, x2plus - x1plus, y2plus - y1plus); ctx.strokeStyle #2196F3; ctx.lineWidth 2; ctx.strokeRect(x1plus, y1plus, x2plus - x1plus, y2plus - y1plus); // 1 등급 영역들 (연한 회색) ctx.fillStyle rgba(158, 158, 158, 0.05); // 80-83, 15-28 ctx.fillRect((80 - weightMin) * xScale, chartHeight - (28 - backfatMin) * yScale, (83 - 80) * xScale, (28 - 15) * yScale); // 83-93, 15-17 ctx.fillRect((83 - weightMin) * xScale, chartHeight - (17 - backfatMin) * yScale, (93 - 83) * xScale, (17 - 15) * yScale); // 83-93, 25-28 ctx.fillRect((83 - weightMin) * xScale, chartHeight - (28 - backfatMin) * yScale, (93 - 83) * xScale, (28 - 25) * yScale); // 93-98, 15-28 ctx.fillRect((93 - weightMin) * xScale, chartHeight - (28 - backfatMin) * yScale, (98 - 93) * xScale, (28 - 15) * yScale); ctx.restore(); // 축 그리기 ctx.strokeStyle #333; ctx.lineWidth 2; ctx.beginPath(); ctx.moveTo(margin.left, margin.top); ctx.lineTo(margin.left, canvas.height - margin.bottom); ctx.lineTo(canvas.width - margin.right, canvas.height - margin.bottom); ctx.stroke(); // X축 눈금 및 그리드 (도체중) ctx.strokeStyle #E0E0E0; ctx.lineWidth 0.5; ctx.fillStyle #333; ctx.font 13px Pretendard; ctx.textAlign center; for (let w weightMin; w weightMax; w + 5) { const x margin.left + (w - weightMin) * xScale; const y canvas.height - margin.bottom; // 그리드선 if (w > weightMin && w weightMax) { ctx.beginPath(); ctx.moveTo(x, margin.top); ctx.lineTo(x, canvas.height - margin.bottom); ctx.stroke(); } // 눈금 ctx.strokeStyle #333; ctx.lineWidth 2; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + 5); ctx.stroke(); ctx.fillText(w, x, y + 22); ctx.strokeStyle #E0E0E0; ctx.lineWidth 0.5; } // Y축 눈금 및 그리드 (등지방) ctx.textAlign right; ctx.textBaseline middle; for (let b backfatMin; b backfatMax; b + 2) { const x margin.left; const y canvas.height - margin.bottom - (b - backfatMin) * yScale; // 그리드선 if (b > backfatMin && b backfatMax) { ctx.strokeStyle #E0E0E0; ctx.lineWidth 0.5; ctx.beginPath(); ctx.moveTo(margin.left, y); ctx.lineTo(canvas.width - margin.right, y); ctx.stroke(); } // 눈금 ctx.strokeStyle #333; ctx.lineWidth 2; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - 5, y); ctx.stroke(); ctx.fillText(b, x - 10, y); } // 축 레이블 ctx.font bold 16px Pretendard; ctx.textAlign center; ctx.textBaseline alphabetic; ctx.fillText(도체중 (kg), canvas.width / 2, canvas.height - 25); ctx.save(); ctx.translate(25, canvas.height / 2); ctx.rotate(-Math.PI / 2); ctx.fillText(등지방 두께 (mm), 0, 0); ctx.restore(); // 제목 ctx.font bold 20px Pretendard; ctx.fillText(등지방 분포도, canvas.width / 2, 30); // 개별 데이터 포인트 그리기 (겹치지 않게) ctx.save(); ctx.translate(margin.left, margin.top); // 같은 좌표에 여러 개가 있을 경우 겹치지 않게 위치 조정 const positions {}; data.forEach(pig > { const grade getGrade(pig.weight, pig.backfat); const key `${pig.weight.toFixed(1)}-${pig.backfat.toFixed(1)}`; if (!positionskey) { positionskey { count: 0, grade: grade }; } positionskey.count++; }); const drawn {}; data.forEach(pig > { const grade getGrade(pig.weight, pig.backfat); const key `${pig.weight.toFixed(1)}-${pig.backfat.toFixed(1)}`; if (!drawnkey) { drawnkey 0; } const baseX (pig.weight - weightMin) * xScale; const baseY chartHeight - (pig.backfat - backfatMin) * yScale; // 같은 위치에 여러 개 있으면 원형으로 배치 const totalCount positionskey.count; let offsetX 0, offsetY 0; if (totalCount > 1) { const angle (drawnkey / totalCount) * 2 * Math.PI; const radius 3; offsetX Math.cos(angle) * radius; offsetY Math.sin(angle) * radius; } const x baseX + offsetX; const y baseY + offsetY; // 등급별 색상 if (grade 1+) { ctx.fillStyle #FF9800; // 주황색 } else if (grade 1) { ctx.fillStyle #FFB2B2; // 연한 빨강 } else { ctx.fillStyle #EF5350; // 빨강 } // 점 그리기 ctx.beginPath(); ctx.arc(x, y, 3, 0, 2 * Math.PI); ctx.fill(); // 테두리 ctx.strokeStyle #333; ctx.lineWidth 0.5; ctx.stroke(); drawnkey++; }); ctx.restore(); // 범례 추가 const legendX canvas.width - margin.right + 20; const legendY margin.top + 20; ctx.font bold 14px Pretendard; ctx.textAlign left; ctx.fillStyle #333; ctx.fillText(등급, legendX, legendY); // 1+ 등급 ctx.fillStyle #FF9800; ctx.beginPath(); ctx.arc(legendX + 10, legendY + 25, 4, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle #333; ctx.font 13px Pretendard; ctx.fillText(1+, legendX + 25, legendY + 28); // 1 등급 ctx.fillStyle #FFB2B2; ctx.beginPath(); ctx.arc(legendX + 10, legendY + 50, 4, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle #333; ctx.fillText(1, legendX + 25, legendY + 53); // 2 등급 ctx.fillStyle #EF5350; ctx.beginPath(); ctx.arc(legendX + 10, legendY + 75, 4, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle #333; ctx.fillText(2, legendX + 25, legendY + 78); } // 등지방 데이터 저장/관리 함수 // 현재 분석 데이터 저장 async function saveCurrentBackfatData() { if (!currentBackfatData || currentBackfatData.length 0) { alert(저장할 데이터가 없습니다.); return; } const memo prompt(메모를 입력하세요 (선택사항):, ); setSyncStatus(syncing, 데이터 저장 중...); try { const formData new FormData(); formData.append(action, saveBackfatData); formData.append(data, JSON.stringify(currentBackfatData)); formData.append(memo, memo || ); const response await fetch(SCRIPT_URL, { method: POST, body: formData }); const result await response.json(); if (result.success) { alert(`저장 완료!\n출하ID: ${result.shipmentId}\n날짜: ${result.date}\n두수: ${result.count}두`); setSyncStatus(synced, 저장 완료); document.getElementById(saveBackfatBtn).style.display none; // 저장 후 버튼 숨김 } else { throw new Error(result.error); } } catch (error) { alert(저장 실패: + error.message); setSyncStatus(error, 저장 실패); } } // 히스토리 모달 표시 async function showBackfatHistory() { setSyncStatus(syncing, 히스토리 불러오는 중...); try { const response await fetch(`${SCRIPT_URL}?actiongetBackfatHistory`); const result await response.json(); if (result.success) { displayBackfatHistoryList(result.history); document.getElementById(backfat-history-modal).style.display flex; setSyncStatus(synced, 히스토리 로드 완료); } else { throw new Error(result.error); } } catch (error) { alert(히스토리 불러오기 실패: + error.message); setSyncStatus(error, 로드 실패); } } // 히스토리 목록 표시 function displayBackfatHistoryList(history) { const container document.getElementById(backfat-history-list); if (history.length 0) { container.innerHTML div styletext-align: center; padding: 40px; color: var(--text-sub);>저장된 데이터가 없습니다./div>; return; } container.innerHTML ; history.forEach(item > { const div document.createElement(div); div.style.cssText border: 1px solid #ECEFF1; border-radius: 8px; padding: 16px; margin-bottom: 12px; background: white;; const dateObj new Date(item.date + + item.time); const dateStr dateObj.toLocaleDateString(ko-KR); const timeStr dateObj.toLocaleTimeString(ko-KR, { hour: 2-digit, minute: 2-digit }); div.innerHTML ` div styledisplay: flex; justify-content: space-between; align-items: center;> div styleflex: 1;> div stylefont-weight: 700; color: var(--text-main); margin-bottom: 4px;> 📅 ${dateStr} ${timeStr} /div> div stylefont-size: 0.9em; color: var(--text-sub);> 🐷 ${item.count}두 ${item.memo ? · + item.memo : } /div> div stylefont-size: 0.85em; color: var(--text-sub); margin-top: 4px;> ID: ${item.shipmentId} /div> /div> div styledisplay: flex; gap: 8px;> button classbtn btn-primary onclickloadBackfatData(${item.shipmentId}) stylepadding: 8px 16px; font-size: 0.9em;> 불러오기 /button> button classbtn btn-danger onclickdeleteBackfatData(${item.shipmentId}) stylepadding: 8px 16px; font-size: 0.9em;> 삭제 /button> /div> /div> `; container.appendChild(div); }); } // 저장된 데이터 불러오기 async function loadBackfatData(shipmentId) { if (!confirm(현재 분석 중인 데이터가 있다면 사라집니다. 계속하시겠습니까?)) { return; } setSyncStatus(syncing, 데이터 불러오는 중...); closeBackfatHistoryModal(); try { const response await fetch(`${SCRIPT_URL}?actionloadBackfatData&shipmentId${shipmentId}`); const result await response.json(); if (result.success) { currentBackfatData result.data; const stats calculateBackfatStats(result.data); // 통계 표시 displayBackfatStats(stats); // AI 분석 생성 및 표시 const aiComments generateAIAnalysis(result.data, stats); displayAIAnalysis(aiComments); // 개선 제안 생성 및 표시 const suggestions generateSuggestions(result.data); displaySuggestions(suggestions); // 차트 그리기 drawBackfatChart(result.data); // UI 표시 document.getElementById(backfat-guide).style.display none; document.getElementById(backfat-stats).style.display block; document.getElementById(backfat-chart).style.display block; document.getElementById(saveBackfatBtn).style.display none; // 이미 저장된 데이터는 저장 버튼 숨김 // 등지방 분포도 탭으로 이동 const tabs document.querySelectorAll(.tab-card); tabs.forEach(tab > tab.classList.remove(active)); tabs3.classList.add(active); // 4번째 탭 (등지방 분포도) setSyncStatus(synced, 데이터 로드 완료); } else { throw new Error(result.error); } } catch (error) { alert(데이터 불러오기 실패: + error.message); setSyncStatus(error, 로드 실패); } } // 데이터 삭제 async function deleteBackfatData(shipmentId) { if (!confirm(정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.)) { return; } setSyncStatus(syncing, 삭제 중...); try { const response await fetch(`${SCRIPT_URL}?actiondeleteBackfatData&shipmentId${shipmentId}`); const result await response.json(); if (result.success) { alert(`${result.deletedCount}개 행이 삭제되었습니다.`); showBackfatHistory(); // 목록 새로고침 setSyncStatus(synced, 삭제 완료); } else { throw new Error(result.error); } } catch (error) { alert(삭제 실패: + error.message); setSyncStatus(error, 삭제 실패); } } // 히스토리 모달 닫기 function closeBackfatHistoryModal() { document.getElementById(backfat-history-modal).style.display none; } /script>/body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]