diff --git a/backend/check-data.js b/backend/check-data.js new file mode 100644 index 0000000..3eb9207 --- /dev/null +++ b/backend/check-data.js @@ -0,0 +1,95 @@ +const { Client } = require('pg'); + +async function main() { + const conn = new Client({ + host: 'localhost', + port: 5432, + user: 'postgres', + password: 'turbo123', + database: 'genome_db' + }); + await conn.connect(); + + // 1. 해당 개체의 형질 데이터 확인 + const cowTraitsResult = await conn.query( + "SELECT trait_name, trait_ebv FROM tb_genome_trait_detail WHERE cow_id = 'KOR002191643715' AND del_dt IS NULL ORDER BY trait_name" + ); + const cowTraits = cowTraitsResult.rows; + console.log('=== 개체 KOR002191643715 형질 데이터 ==='); + console.log('형질수:', cowTraits.length); + let totalEbv = 0; + cowTraits.forEach(t => { + console.log(t.trait_name + ': ' + t.trait_ebv); + totalEbv += Number(t.trait_ebv || 0); + }); + console.log('\n*** 내 개체 EBV 합계(선발지수):', totalEbv.toFixed(2)); + + // 2. 해당 개체의 농가 확인 + const cowInfoResult = await conn.query( + "SELECT gr.fk_farm_no, f.farmer_name FROM tb_genome_request gr JOIN tb_cow c ON gr.fk_cow_no = c.pk_cow_no LEFT JOIN tb_farm f ON gr.fk_farm_no = f.pk_farm_no WHERE c.cow_id = 'KOR002191643715' AND gr.del_dt IS NULL LIMIT 1" + ); + const cowInfo = cowInfoResult.rows; + console.log('\n=== 농가 정보 ==='); + console.log('농가번호:', cowInfo[0]?.fk_farm_no, '농장주:', cowInfo[0]?.farmer_name); + + const farmNo = cowInfo[0]?.fk_farm_no; + + // 3. 같은 농가의 모든 개체 EBV 합계 + if (farmNo) { + const farmCowsResult = await conn.query( + `SELECT c.cow_id, SUM(gtd.trait_ebv) as total_ebv, COUNT(*) as trait_count + FROM tb_genome_request gr + JOIN tb_cow c ON gr.fk_cow_no = c.pk_cow_no + JOIN tb_genome_trait_detail gtd ON gtd.cow_id = c.cow_id AND gtd.del_dt IS NULL + WHERE gr.fk_farm_no = $1 AND gr.del_dt IS NULL AND c.del_dt IS NULL + AND gr.chip_sire_name = '일치' + GROUP BY c.cow_id + HAVING COUNT(*) = 35 + ORDER BY total_ebv DESC`, + [farmNo] + ); + const farmCows = farmCowsResult.rows; + console.log('\n=== 같은 농가 개체들 EBV 합계 (35형질 전체) ==='); + console.log('개체수:', farmCows.length); + let farmSum = 0; + farmCows.forEach(c => { + console.log(c.cow_id + ': ' + Number(c.total_ebv).toFixed(2)); + farmSum += Number(c.total_ebv); + }); + if (farmCows.length > 0) { + console.log('\n*** 농가 평균:', (farmSum / farmCows.length).toFixed(2)); + } + } + + // 4. 전체 보은군 평균 + const allCowsResult = await conn.query( + `SELECT c.cow_id, SUM(gtd.trait_ebv) as total_ebv + FROM tb_genome_request gr + JOIN tb_cow c ON gr.fk_cow_no = c.pk_cow_no + JOIN tb_genome_trait_detail gtd ON gtd.cow_id = c.cow_id AND gtd.del_dt IS NULL + WHERE gr.del_dt IS NULL AND c.del_dt IS NULL + AND gr.chip_sire_name = '일치' + GROUP BY c.cow_id + HAVING COUNT(*) = 35` + ); + const allCows = allCowsResult.rows; + console.log('\n=== 보은군 전체 통계 ==='); + console.log('개체수:', allCows.length); + let regionSum = 0; + allCows.forEach(c => regionSum += Number(c.total_ebv)); + if (allCows.length > 0) { + console.log('*** 보은군 평균:', (regionSum / allCows.length).toFixed(2)); + } + + // 5. 최대/최소 확인 + if (allCows.length > 0) { + const maxCow = allCows.reduce((max, c) => Number(c.total_ebv) > Number(max.total_ebv) ? c : max, allCows[0]); + const minCow = allCows.reduce((min, c) => Number(c.total_ebv) < Number(min.total_ebv) ? c : min, allCows[0]); + console.log('\n최대:', maxCow?.cow_id, Number(maxCow?.total_ebv).toFixed(2)); + console.log('최소:', minCow?.cow_id, Number(minCow?.total_ebv).toFixed(2)); + } + + await conn.end(); +} + +main().catch(console.error); diff --git a/backend/check-data2.js b/backend/check-data2.js new file mode 100644 index 0000000..be028dd --- /dev/null +++ b/backend/check-data2.js @@ -0,0 +1,97 @@ +const { Client } = require('pg'); + +async function main() { + const conn = new Client({ + host: 'localhost', + port: 5432, + user: 'postgres', + password: 'turbo123', + database: 'genome_db' + }); + await conn.connect(); + + // getComparisonAverages가 계산하는 방식 확인 + // 농가 1번의 카테고리별 평균 EBV 계산 + const farmNo = 1; + + // 카테고리 매핑 (백엔드와 동일) + const TRAIT_CATEGORY_MAP = { + '12개월령체중': '성장', + '도체중': '생산', '등심단면적': '생산', '등지방두께': '생산', '근내지방도': '생산', + '체고': '체형', '십자': '체형', '체장': '체형', '흉심': '체형', '흉폭': '체형', + '고장': '체형', '요각폭': '체형', '곤폭': '체형', '좌골폭': '체형', '흉위': '체형', + '안심weight': '무게', '등심weight': '무게', '채끝weight': '무게', '목심weight': '무게', + '앞다리weight': '무게', '우둔weight': '무게', '설도weight': '무게', '사태weight': '무게', + '양지weight': '무게', '갈비weight': '무게', + '안심rate': '비율', '등심rate': '비율', '채끝rate': '비율', '목심rate': '비율', + '앞다리rate': '비율', '우둔rate': '비율', '설도rate': '비율', '사태rate': '비율', + '양지rate': '비율', '갈비rate': '비율', + }; + + // 농가 1번의 모든 형질 데이터 조회 + const result = await conn.query(` + SELECT gtd.trait_name, gtd.trait_ebv + FROM tb_genome_trait_detail gtd + JOIN tb_genome_request gr ON gtd.fk_request_no = gr.pk_request_no + WHERE gr.fk_farm_no = $1 + AND gtd.del_dt IS NULL + AND gtd.trait_ebv IS NOT NULL + `, [farmNo]); + + const details = result.rows; + console.log('농가 1번 전체 형질 데이터 수:', details.length); + + // 카테고리별로 합계 계산 + const categoryMap = {}; + for (const d of details) { + const category = TRAIT_CATEGORY_MAP[d.trait_name] || '기타'; + if (!categoryMap[category]) { + categoryMap[category] = { sum: 0, count: 0 }; + } + categoryMap[category].sum += Number(d.trait_ebv); + categoryMap[category].count += 1; + } + + console.log('\n=== getComparisonAverages 방식 (카테고리별 평균) ==='); + const categories = ['성장', '생산', '체형', '무게', '비율']; + let totalAvgEbv = 0; + for (const cat of categories) { + const data = categoryMap[cat]; + const avgEbv = data ? data.sum / data.count : 0; + console.log(`${cat}: 합계=${data?.sum?.toFixed(2)} / 개수=${data?.count} = 평균 ${avgEbv.toFixed(2)}`); + totalAvgEbv += avgEbv; + } + + console.log('\n*** farmAvgZ (카테고리 평균의 합/5):', (totalAvgEbv / categories.length).toFixed(2)); + + // getSelectionIndex 방식 비교 + console.log('\n=== getSelectionIndex 방식 (개체별 합계의 평균) ==='); + const farmCowsResult = await conn.query(` + SELECT c.cow_id, SUM(gtd.trait_ebv) as total_ebv, COUNT(*) as trait_count + FROM tb_genome_request gr + JOIN tb_cow c ON gr.fk_cow_no = c.pk_cow_no + JOIN tb_genome_trait_detail gtd ON gtd.cow_id = c.cow_id AND gtd.del_dt IS NULL + WHERE gr.fk_farm_no = $1 AND gr.del_dt IS NULL AND c.del_dt IS NULL + AND gr.chip_sire_name = '일치' + GROUP BY c.cow_id + HAVING COUNT(*) = 35 + ORDER BY total_ebv DESC + `, [farmNo]); + const farmCows = farmCowsResult.rows; + + let farmSum = 0; + farmCows.forEach(c => farmSum += Number(c.total_ebv)); + const farmAvgScore = farmCows.length > 0 ? farmSum / farmCows.length : 0; + + console.log(`개체수: ${farmCows.length}`); + console.log(`*** farmAvgScore (개체별 합계의 평균): ${farmAvgScore.toFixed(2)}`); + + console.log('\n================================================='); + console.log('farmAvgZ (카테고리 방식):', (totalAvgEbv / categories.length).toFixed(2)); + console.log('farmAvgScore (선발지수 방식):', farmAvgScore.toFixed(2)); + console.log('================================================='); + + await conn.end(); +} + +main().catch(console.error); diff --git a/backend/check-data3.js b/backend/check-data3.js new file mode 100644 index 0000000..4e8d5ae --- /dev/null +++ b/backend/check-data3.js @@ -0,0 +1,114 @@ +const { Client } = require('pg'); + +async function main() { + const conn = new Client({ + host: 'localhost', + port: 5432, + user: 'postgres', + password: 'turbo123', + database: 'genome_db' + }); + await conn.connect(); + + const farmNo = 1; + const cowId = 'KOR002191643715'; + + console.log('======================================================='); + console.log('대시보드 vs 개체상세 차트 비교'); + console.log('=======================================================\n'); + + // ===================================================== + // 1. 대시보드: getFarmRegionRanking API + // - 농가 평균 = 농가 내 개체들의 선발지수 평균 + // - 보은군 평균 = 전체 개체들의 선발지수 평균 + // ===================================================== + console.log('=== 1. 대시보드 (getFarmRegionRanking) ==='); + console.log('보은군 내 농가 위치 차트\n'); + + // 모든 개체별 선발지수 (35개 형질 EBV 합계) + const allCowsResult = await conn.query(` + SELECT c.cow_id, gr.fk_farm_no, SUM(gtd.trait_ebv) as total_ebv + FROM tb_genome_request gr + JOIN tb_cow c ON gr.fk_cow_no = c.pk_cow_no + JOIN tb_genome_trait_detail gtd ON gtd.cow_id = c.cow_id AND gtd.del_dt IS NULL + WHERE gr.del_dt IS NULL AND c.del_dt IS NULL + AND gr.chip_sire_name = '일치' + GROUP BY c.cow_id, gr.fk_farm_no + HAVING COUNT(*) = 35 + `); + const allCows = allCowsResult.rows; + + // 농가별 평균 계산 + const farmScoresMap = new Map(); + for (const cow of allCows) { + const fNo = cow.fk_farm_no; + if (!farmScoresMap.has(fNo)) { + farmScoresMap.set(fNo, []); + } + farmScoresMap.set(fNo, [...farmScoresMap.get(fNo), Number(cow.total_ebv)]); + } + + // 농가별 평균 + const farmAverages = []; + for (const [fNo, scores] of farmScoresMap.entries()) { + const avg = scores.reduce((a, b) => a + b, 0) / scores.length; + farmAverages.push({ farmNo: fNo, avgScore: avg, cowCount: scores.length }); + } + farmAverages.sort((a, b) => b.avgScore - a.avgScore); + + // 보은군 전체 평균 (개체별 합계의 평균) + const regionAvgScore_dashboard = allCows.reduce((sum, c) => sum + Number(c.total_ebv), 0) / allCows.length; + + // 농가 1번 평균 + const myFarm = farmAverages.find(f => f.farmNo === farmNo); + const farmAvgScore_dashboard = myFarm?.avgScore || 0; + + console.log('농가 평균 (개체 선발지수 평균):', farmAvgScore_dashboard.toFixed(2)); + console.log('보은군 평균 (개체 선발지수 평균):', regionAvgScore_dashboard.toFixed(2)); + console.log('차이 (농가 - 보은군):', (farmAvgScore_dashboard - regionAvgScore_dashboard).toFixed(2)); + + // ===================================================== + // 2. 개체 상세: getSelectionIndex API + // - 내 개체 = 개체의 선발지수 (35개 형질 EBV 합계) + // - 농가 평균 = 같은 농가 개체들의 선발지수 평균 + // - 보은군 평균 = 전체 개체들의 선발지수 평균 + // ===================================================== + console.log('\n=== 2. 개체 상세 (getSelectionIndex) ==='); + console.log('농가 및 보은군 내 개체 위치 차트\n'); + + // 내 개체 선발지수 + const myCow = allCows.find(c => c.cow_id === cowId); + const myScore = myCow ? Number(myCow.total_ebv) : 0; + + // 같은 농가 개체들의 평균 + const farmCows = allCows.filter(c => c.fk_farm_no === farmNo); + const farmAvgScore_detail = farmCows.reduce((sum, c) => sum + Number(c.total_ebv), 0) / farmCows.length; + + // 보은군 전체 평균 + const regionAvgScore_detail = regionAvgScore_dashboard; // 동일 + + console.log('내 개체 선발지수:', myScore.toFixed(2)); + console.log('농가 평균:', farmAvgScore_detail.toFixed(2)); + console.log('보은군 평균:', regionAvgScore_detail.toFixed(2)); + console.log(''); + console.log('내 개체 vs 농가평균:', (myScore - farmAvgScore_detail).toFixed(2)); + console.log('내 개체 vs 보은군평균:', (myScore - regionAvgScore_detail).toFixed(2)); + + // ===================================================== + // 3. 비교 요약 + // ===================================================== + console.log('\n======================================================='); + console.log('비교 요약'); + console.log('======================================================='); + console.log(''); + console.log('[대시보드] 농가 vs 보은군 차이:', (farmAvgScore_dashboard - regionAvgScore_dashboard).toFixed(2)); + console.log('[개체상세] 개체 vs 농가 차이:', (myScore - farmAvgScore_detail).toFixed(2)); + console.log('[개체상세] 개체 vs 보은군 차이:', (myScore - regionAvgScore_detail).toFixed(2)); + console.log(''); + console.log('=> 대시보드는 농가평균 vs 보은군평균 비교 (차이 작음)'); + console.log('=> 개체상세는 개별개체 vs 평균 비교 (개체가 우수하면 차이 큼)'); + + await conn.end(); +} + +main().catch(console.error); diff --git a/backend/check-data4.js b/backend/check-data4.js new file mode 100644 index 0000000..163cf2d --- /dev/null +++ b/backend/check-data4.js @@ -0,0 +1,126 @@ +const { Client } = require('pg'); + +async function main() { + const conn = new Client({ + host: 'localhost', + port: 5432, + user: 'postgres', + password: 'turbo123', + database: 'genome_db' + }); + await conn.connect(); + + const cowId = 'KOR002191643715'; + const farmNo = 1; + + console.log('======================================================='); + console.log('선발지수 계산 방식 비교 분석'); + console.log('=======================================================\n'); + + // 1. 해당 개체의 35개 형질 EBV 확인 + const traitsResult = await conn.query(` + SELECT trait_name, trait_ebv + FROM tb_genome_trait_detail + WHERE cow_id = $1 AND del_dt IS NULL + ORDER BY trait_name + `, [cowId]); + const traits = traitsResult.rows; + + console.log('=== 개체 형질 데이터 ==='); + console.log('형질 수:', traits.length); + + // EBV 합계 + const ebvSum = traits.reduce((sum, t) => sum + Number(t.trait_ebv || 0), 0); + console.log('EBV 합계:', ebvSum.toFixed(2)); + + // EBV 평균 + const ebvAvg = ebvSum / traits.length; + console.log('EBV 평균:', ebvAvg.toFixed(2)); + + console.log('\n=== 선발지수 계산 방식 비교 ===\n'); + + // 방식 1: EBV 합계 (getSelectionIndex 방식) + console.log('방식1 - EBV 합계 (weightedSum):', ebvSum.toFixed(2)); + + // 방식 2: EBV 평균 + console.log('방식2 - EBV 평균 (sum/count):', ebvAvg.toFixed(2)); + + // 농가/보은군 평균도 각 방식으로 계산 + console.log('\n=== 농가 평균 계산 방식 비교 ===\n'); + + // 농가 내 모든 개체 + const farmCowsResult = await conn.query(` + SELECT c.cow_id, SUM(gtd.trait_ebv) as sum_ebv, AVG(gtd.trait_ebv) as avg_ebv, COUNT(*) as cnt + FROM tb_genome_request gr + JOIN tb_cow c ON gr.fk_cow_no = c.pk_cow_no + JOIN tb_genome_trait_detail gtd ON gtd.cow_id = c.cow_id AND gtd.del_dt IS NULL + WHERE gr.fk_farm_no = $1 AND gr.del_dt IS NULL AND c.del_dt IS NULL + AND gr.chip_sire_name = '일치' + GROUP BY c.cow_id + HAVING COUNT(*) = 35 + `, [farmNo]); + const farmCows = farmCowsResult.rows; + + // 방식 1: 개체별 합계의 평균 + const farmSumAvg = farmCows.reduce((sum, c) => sum + Number(c.sum_ebv), 0) / farmCows.length; + console.log('방식1 - 개체별 합계의 평균:', farmSumAvg.toFixed(2)); + + // 방식 2: 개체별 평균의 평균 + const farmAvgAvg = farmCows.reduce((sum, c) => sum + Number(c.avg_ebv), 0) / farmCows.length; + console.log('방식2 - 개체별 평균의 평균:', farmAvgAvg.toFixed(2)); + + console.log('\n=== 보은군 평균 계산 방식 비교 ===\n'); + + // 보은군 전체 개체 + const allCowsResult = await conn.query(` + SELECT c.cow_id, SUM(gtd.trait_ebv) as sum_ebv, AVG(gtd.trait_ebv) as avg_ebv, COUNT(*) as cnt + FROM tb_genome_request gr + JOIN tb_cow c ON gr.fk_cow_no = c.pk_cow_no + JOIN tb_genome_trait_detail gtd ON gtd.cow_id = c.cow_id AND gtd.del_dt IS NULL + WHERE gr.del_dt IS NULL AND c.del_dt IS NULL + AND gr.chip_sire_name = '일치' + GROUP BY c.cow_id + HAVING COUNT(*) = 35 + `); + const allCows = allCowsResult.rows; + + // 방식 1: 개체별 합계의 평균 + const regionSumAvg = allCows.reduce((sum, c) => sum + Number(c.sum_ebv), 0) / allCows.length; + console.log('방식1 - 개체별 합계의 평균:', regionSumAvg.toFixed(2)); + + // 방식 2: 개체별 평균의 평균 + const regionAvgAvg = allCows.reduce((sum, c) => sum + Number(c.avg_ebv), 0) / allCows.length; + console.log('방식2 - 개체별 평균의 평균:', regionAvgAvg.toFixed(2)); + + console.log('\n======================================================='); + console.log('결과 비교'); + console.log('=======================================================\n'); + + console.log('만약 "합계" 방식 사용 시:'); + console.log(' 내 개체:', ebvSum.toFixed(2)); + console.log(' 농가 평균:', farmSumAvg.toFixed(2)); + console.log(' 보은군 평균:', regionSumAvg.toFixed(2)); + console.log(' → 내 개체 vs 농가: +', (ebvSum - farmSumAvg).toFixed(2)); + console.log(' → 내 개체 vs 보은군: +', (ebvSum - regionSumAvg).toFixed(2)); + + console.log('\n만약 "평균" 방식 사용 시:'); + console.log(' 내 개체:', ebvAvg.toFixed(2)); + console.log(' 농가 평균:', farmAvgAvg.toFixed(2)); + console.log(' 보은군 평균:', regionAvgAvg.toFixed(2)); + console.log(' → 내 개체 vs 농가: +', (ebvAvg - farmAvgAvg).toFixed(2)); + console.log(' → 내 개체 vs 보은군: +', (ebvAvg - regionAvgAvg).toFixed(2)); + + console.log('\n======================================================='); + console.log('리스트 선발지수 확인 (page.tsx의 GENOMIC_TRAITS)'); + console.log('=======================================================\n'); + + // 리스트에서 보여주는 선발지수는 어떻게 계산되나? + // page.tsx:350-356 확인 필요 + console.log('리스트의 overallScore 계산식 확인 필요:'); + console.log(' - selectionIndex.score 사용 시: 합계 방식'); + console.log(' - GENOMIC_TRAITS.reduce / length 사용 시: 평균 방식'); + + await conn.end(); +} + +main().catch(console.error); diff --git a/backend/check-data5.js b/backend/check-data5.js new file mode 100644 index 0000000..b70f2a0 --- /dev/null +++ b/backend/check-data5.js @@ -0,0 +1,47 @@ +const { Client } = require('pg'); + +async function main() { + const conn = new Client({ + host: 'localhost', + port: 5432, + user: 'postgres', + password: 'turbo123', + database: 'genome_db' + }); + await conn.connect(); + + const farmNo = 1; + + // 농가 내 모든 개체의 선발지수(가중 합계) 조회 + const farmCowsResult = await conn.query(` + SELECT c.cow_id, SUM(gtd.trait_ebv) as sum_ebv + FROM tb_genome_request gr + JOIN tb_cow c ON gr.fk_cow_no = c.pk_cow_no + JOIN tb_genome_trait_detail gtd ON gtd.cow_id = c.cow_id AND gtd.del_dt IS NULL + WHERE gr.fk_farm_no = $1 AND gr.del_dt IS NULL AND c.del_dt IS NULL + AND gr.chip_sire_name = '일치' + GROUP BY c.cow_id + HAVING COUNT(*) = 35 + ORDER BY sum_ebv DESC + `, [farmNo]); + + const farmCows = farmCowsResult.rows; + + console.log('=== 농가 1번 개체별 선발지수 (가중 합계) ===\n'); + + let total = 0; + farmCows.forEach((c, i) => { + const score = Number(c.sum_ebv); + total += score; + console.log(`${i+1}. ${c.cow_id}: ${score.toFixed(2)}`); + }); + + console.log('\n=== 계산 ==='); + console.log('개체 수:', farmCows.length); + console.log('선발지수 총합:', total.toFixed(2)); + console.log('농가 평균 = 총합 / 개체수 =', total.toFixed(2), '/', farmCows.length, '=', (total / farmCows.length).toFixed(2)); + + await conn.end(); +} + +main().catch(console.error); diff --git a/backend/check-data6.js b/backend/check-data6.js new file mode 100644 index 0000000..2296a64 --- /dev/null +++ b/backend/check-data6.js @@ -0,0 +1,66 @@ +const { Client } = require('pg'); + +async function main() { + const conn = new Client({ + host: 'localhost', + port: 5432, + user: 'postgres', + password: 'turbo123', + database: 'genome_db' + }); + await conn.connect(); + + const cowId = 'KOR002191643715'; + + console.log('======================================================='); + console.log('리스트 vs 개체상세 선발지수 비교'); + console.log('=======================================================\n'); + + // 해당 개체의 35개 형질 EBV 조회 + const traitsResult = await conn.query(` + SELECT trait_name, trait_ebv + FROM tb_genome_trait_detail + WHERE cow_id = $1 AND del_dt IS NULL + ORDER BY trait_name + `, [cowId]); + const traits = traitsResult.rows; + + console.log('형질 수:', traits.length); + + // 1. 가중 합계 (weight = 1) + let weightedSum = 0; + let totalWeight = 0; + traits.forEach(t => { + const ebv = Number(t.trait_ebv); + const weight = 1; + weightedSum += ebv * weight; + totalWeight += weight; + }); + + console.log('\n=== 계산 비교 ==='); + console.log('가중 합계 (weightedSum):', weightedSum.toFixed(2)); + console.log('총 가중치 (totalWeight):', totalWeight); + console.log(''); + console.log('리스트 (cow.service.ts) - 가중 합계:', weightedSum.toFixed(2)); + console.log('개체상세 (genome.service.ts) - 가중 합계:', weightedSum.toFixed(2)); + + console.log('\n=== 프론트엔드 가중치 확인 ==='); + console.log('프론트엔드에서 weight / 100 정규화 확인 필요'); + console.log('예: weight 100 → 1, weight 50 → 0.5'); + + // 만약 프론트에서 weight/100을 적용한다면? + console.log('\n=== 만약 weight가 0.01로 적용된다면? ==='); + let weightedSum2 = 0; + let totalWeight2 = 0; + traits.forEach(t => { + const ebv = Number(t.trait_ebv); + const weight = 0.01; // 1/100 + weightedSum2 += ebv * weight; + totalWeight2 += weight; + }); + console.log('가중 합계:', weightedSum2.toFixed(2)); + + await conn.end(); +} + +main().catch(console.error); diff --git a/backend/src/cow/cow.service.ts b/backend/src/cow/cow.service.ts index df053a3..5574017 100644 --- a/backend/src/cow/cow.service.ts +++ b/backend/src/cow/cow.service.ts @@ -223,8 +223,24 @@ export class CowService { */ private async applyGenomeRanking( cows: CowModel[], - traitConditions: TraitRankingCondition[], + inputTraitConditions: TraitRankingCondition[], ): Promise { + // 35개 전체 형질 (기본값) + const ALL_TRAITS = [ + '12개월령체중', + '도체중', '등심단면적', '등지방두께', '근내지방도', + '체고', '십자', '체장', '흉심', '흉폭', '고장', '요각폭', '좌골폭', '곤폭', '흉위', + '안심weight', '등심weight', '채끝weight', '목심weight', '앞다리weight', + '우둔weight', '설도weight', '사태weight', '양지weight', '갈비weight', + '안심rate', '등심rate', '채끝rate', '목심rate', '앞다리rate', + '우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate', + ]; + + // traitConditions가 비어있으면 35개 전체 형질 사용 (개체상세, 대시보드와 동일) + const traitConditions = inputTraitConditions && inputTraitConditions.length > 0 + ? inputTraitConditions + : ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 })); + // 각 개체별로 점수 계산 const cowsWithScore = await Promise.all( cows.map(async (cow) => { @@ -260,7 +276,7 @@ export class CowService { return { entity: { ...cow, unavailableReason: '형질정보없음' }, sortValue: null, details: [] }; } - // Step 4: 가중 평균 계산 + // Step 4: 가중 합계 계산 let weightedSum = 0; // 가중치 적용된 EBV 합계 let totalWeight = 0; // 총 가중치 let hasAllTraits = true; // 모든 선택 형질 존재 여부 @@ -290,10 +306,10 @@ export class CowService { } } - // Step 6: 최종 점수 계산 (가중 평균) + // Step 6: 최종 점수 계산 (가중 합계) // 모든 선택 형질이 있어야만 점수 계산 const sortValue = (hasAllTraits && totalWeight > 0) - ? weightedSum / totalWeight // 가중 평균 = 가중합 / 총가중치 + ? weightedSum // 가중 합계 (개체상세, 대시보드와 동일한 방식) : null; // Step 7: 응답 데이터 구성 diff --git a/frontend/src/app/admin/genome-mapping/page.tsx b/frontend/src/app/admin/genome-mapping/page.tsx index 80e247c..f934b90 100644 --- a/frontend/src/app/admin/genome-mapping/page.tsx +++ b/frontend/src/app/admin/genome-mapping/page.tsx @@ -31,6 +31,7 @@ import { DialogTitle, } from "@/components/ui/dialog" import { useAuthStore } from "@/store/auth-store" +import { AuthGuard } from "@/components/auth/auth-guard" import { IconSearch, IconDownload, @@ -460,9 +461,11 @@ function GenomeMappingContent() { export default function GenomeMappingPage() { return ( - - - - + + + + + + ) } diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index df0bfb7..8780d09 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -13,6 +13,7 @@ import { CardTitle, } from "@/components/ui/card" import { useAuthStore } from "@/store/auth-store" +import { AuthGuard } from "@/components/auth/auth-guard" import { IconFileUpload, IconUsers, @@ -165,9 +166,11 @@ function AdminDashboardContent() { export default function AdminDashboardPage() { return ( - - - - + + + + + + ) } diff --git a/frontend/src/app/admin/upload/page.tsx b/frontend/src/app/admin/upload/page.tsx index f50c0b5..d987031 100644 --- a/frontend/src/app/admin/upload/page.tsx +++ b/frontend/src/app/admin/upload/page.tsx @@ -14,6 +14,7 @@ import { } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { useAuthStore } from "@/store/auth-store" +import { AuthGuard } from "@/components/auth/auth-guard" import { IconUpload, IconCheck, @@ -416,9 +417,11 @@ function AdminUploadContent() { export default function AdminUploadPage() { return ( - - - - + + + + + + ) } diff --git a/frontend/src/app/cow/[cowNo]/genome/_components/category-evaluation-card.tsx b/frontend/src/app/cow/[cowNo]/genome/_components/category-evaluation-card.tsx index e34742b..9d914a7 100644 --- a/frontend/src/app/cow/[cowNo]/genome/_components/category-evaluation-card.tsx +++ b/frontend/src/app/cow/[cowNo]/genome/_components/category-evaluation-card.tsx @@ -468,10 +468,9 @@ export function CategoryEvaluationCard({ content={({ active, payload }) => { if (active && payload && payload.length) { const item = payload[0]?.payload - const breedVal = item?.breedVal ?? 0 - const regionVal = item?.regionVal ?? 0 - const farmVal = item?.farmVal ?? 0 - const percentile = item?.percentile ?? 50 + const epd = item?.epd ?? 0 + const regionEpd = (item?.regionVal ?? 0) * (item?.epd / (item?.breedVal || 1)) || 0 + const farmEpd = (item?.farmVal ?? 0) * (item?.epd / (item?.breedVal || 1)) || 0 return (
@@ -482,21 +481,21 @@ export function CategoryEvaluationCard({ 보은군 평균 - {regionVal > 0 ? '+' : ''}{regionVal.toFixed(2)}σ + {regionEpd > 0 ? '+' : ''}{regionEpd.toFixed(2)}
농가 평균 - {farmVal > 0 ? '+' : ''}{farmVal.toFixed(2)}σ + {farmEpd > 0 ? '+' : ''}{farmEpd.toFixed(2)}
{formatCowNoShort(cowNo)} 개체 - {breedVal > 0 ? '+' : ''}{breedVal.toFixed(2)}σ + {epd > 0 ? '+' : ''}{epd.toFixed(2)}
diff --git a/frontend/src/app/cow/[cowNo]/page.tsx b/frontend/src/app/cow/[cowNo]/page.tsx index c92635a..db7b45c 100644 --- a/frontend/src/app/cow/[cowNo]/page.tsx +++ b/frontend/src/app/cow/[cowNo]/page.tsx @@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { useToast } from "@/hooks/use-toast" -import { ComparisonAveragesDto, TraitComparisonAveragesDto, cowApi, genomeApi, geneApi, GeneDetail } from "@/lib/api" +import { ComparisonAveragesDto, TraitComparisonAveragesDto, cowApi, genomeApi, geneApi, GeneDetail, GenomeRequestDto } from "@/lib/api" import { CowDetail } from "@/types/cow.types" import { GenomeTrait } from "@/types/genome.types" import { useGlobalFilter } from "@/contexts/GlobalFilterContext" @@ -34,6 +34,7 @@ import { TraitDistributionCharts } from "./genome/_components/trait-distribution import { TraitComparison } from "./genome/_components/genome-integrated-comparison" import { CowNumberDisplay } from "@/components/common/cow-number-display" import { isValidGenomeAnalysis, getInvalidReason, getInvalidMessage } from "@/lib/utils/genome-analysis-config" +import { AuthGuard } from "@/components/auth/auth-guard" // 형질명 → 카테고리 매핑 (한우 35개 형질) const TRAIT_CATEGORY_MAP: Record = { @@ -156,6 +157,9 @@ export default function CowOverviewPage() { const [hasGeneData, setHasGeneData] = useState(false) const [hasReproductionData, setHasReproductionData] = useState(false) + // 분석 의뢰 정보 (친자감별 결과 포함) + const [genomeRequest, setGenomeRequest] = useState(null) + // 선발지수 상태 const [selectionIndex, setSelectionIndex] = useState<{ score: number | null; @@ -259,6 +263,15 @@ export default function CowOverviewPage() { const genomeExists = genomeDataResult.length > 0 && !!genomeDataResult[0].genomeCows && genomeDataResult[0].genomeCows.length > 0 setHasGenomeData(genomeExists) + // 분석 의뢰 정보 가져오기 (친자감별 결과 포함) + try { + const requestData = await genomeApi.getRequest(cowNo) + setGenomeRequest(requestData) + } catch (reqErr) { + console.error('분석 의뢰 정보 조회 실패:', reqErr) + setGenomeRequest(null) + } + // 유전자(SNP) 데이터 가져오기 try { const geneDataResult = await geneApi.findByCowId(cowNo) @@ -305,11 +318,11 @@ export default function CowOverviewPage() { '우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate', ] - // 필터가 활성화되고 형질이 선택되어 있으면 가중치 조건 생성 (대시보드와 동일 로직) + // 필터가 활성화되고 형질이 선택되어 있으면 가중치 조건 생성 (리스트와 동일 로직) const finalConditions = filters.isActive && filters.selectedTraits && filters.selectedTraits.length > 0 ? filters.selectedTraits.map(traitNm => ({ traitNm, - weight: (filters.traitWeights as Record)[traitNm] || 1 + weight: ((filters.traitWeights as Record)[traitNm] || 100) / 100 // 0-100 → 0-1로 정규화 })) : ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 })) @@ -463,6 +476,7 @@ export default function CowOverviewPage() { } return ( + @@ -625,7 +639,7 @@ export default function CowOverviewPage() {
- {cow?.sireKpn || '-'} + {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeData[0]?.request?.chipSireName @@ -658,8 +672,8 @@ export default function CowOverviewPage() { 모 개체번호
- {cow?.damCowId ? ( - + {cow?.damCowId && cow.damCowId !== '0' ? ( + ) : ( - )} @@ -700,7 +714,7 @@ export default function CowOverviewPage() { 부 KPN번호
- {cow?.sireKpn || '-'} + {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeData[0]?.request?.chipSireName @@ -731,8 +745,8 @@ export default function CowOverviewPage() {
모 개체번호
- {cow?.damCowId ? ( - + {cow?.damCowId && cow.damCowId !== '0' ? ( + ) : ( - )} @@ -842,7 +856,7 @@ export default function CowOverviewPage() { -
+
접수일
@@ -865,12 +879,6 @@ export default function CowOverviewPage() { {genomeData[0]?.request?.chipType || '-'}
-
-
칩 번호
-
- {genomeData[0]?.request?.chipNo || '-'} -
-
@@ -910,15 +918,253 @@ export default function CowOverviewPage() { )} ) : ( - - - -

유전체 분석 데이터 없음

-

- 이 개체는 아직 유전체 분석이 완료되지 않았습니다. -

-
-
+ <> + {/* 개체 정보 섹션 */} +

개체 정보

+ + + + {/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */} +
+
+
+ 개체번호 +
+
+ +
+
+
+
+ 생년월일 +
+
+ + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+
+
+ 월령 +
+
+ + {cow?.cowBirthDt + ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+
+
+ 유전체 분석일자 +
+
+ - +
+
+
+ {/* 모바일: 좌우 배치 리스트 */} +
+
+ 개체번호 +
+ +
+
+
+ 생년월일 + + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+ 월령 + + {cow?.cowBirthDt + ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+ 분석일자 + - +
+
+
+
+ + {/* 친자확인 섹션 */} +

친자확인 결과

+ + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 부 KPN번호 +
+
+ {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {(() => { + const chipSireName = genomeRequest?.chipSireName + if (chipSireName === '일치') { + return ( + + + 일치 + + ) + } else if (chipSireName && chipSireName !== '일치') { + return ( + + + 불일치 + + ) + } else { + return ( + + 정보없음 + + ) + } + })()} +
+
+
+
+ 모 개체번호 +
+
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {(() => { + const chipDamName = genomeRequest?.chipDamName + if (chipDamName === '일치') { + return ( + + + 일치 + + ) + } else if (chipDamName === '불일치') { + return ( + + + 불일치 + + ) + } else if (chipDamName === '이력제부재') { + return ( + + + 이력제부재 + + ) + } else { + // 정보없음(null/undefined)일 때는 배지 표시 안함 + return null + } + })()} +
+
+
+ {/* 모바일: 세로 리스트 */} +
+
+ 부 KPN +
+ {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {(() => { + const chipSireName = genomeRequest?.chipSireName + if (chipSireName === '일치') { + return ( + + + 일치 + + ) + } else if (chipSireName && chipSireName !== '일치') { + return ( + + + 불일치 + + ) + } else { + return ( + + 정보없음 + + ) + } + })()} +
+
+
+ 모 개체번호 +
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {(() => { + const chipDamName = genomeRequest?.chipDamName + if (chipDamName === '일치') { + return ( + + + 일치 + + ) + } else if (chipDamName === '불일치') { + return ( + + + 불일치 + + ) + } else if (chipDamName === '이력제부재') { + return ( + + + 이력제부재 + + ) + } else { + // 정보없음(null/undefined)일 때는 배지 표시 안함 + return null + } + })()} +
+
+
+
+
+ + {/* 분석불가 메시지 */} + + + +

+ {genomeRequest ? '유전체 분석 불가' : '유전체 미분석'} +

+

+ {genomeRequest + ? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) + : '이 개체는 아직 유전체 분석이 진행되지 않았습니다.' + } +

+
+
+ )} @@ -1023,7 +1269,7 @@ export default function CowOverviewPage() {
- {cow?.sireKpn || '-'} + {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeData[0]?.request?.chipSireName @@ -1056,8 +1302,8 @@ export default function CowOverviewPage() { 모 개체번호
- {cow?.damCowId ? ( - + {cow?.damCowId && cow.damCowId !== '0' ? ( + ) : ( - )} @@ -1097,7 +1343,7 @@ export default function CowOverviewPage() { 부 KPN번호
- {cow?.sireKpn || '-'} + {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeData[0]?.request?.chipSireName @@ -1128,8 +1374,8 @@ export default function CowOverviewPage() {
모 개체번호
- {cow?.damCowId ? ( - + {cow?.damCowId && cow.damCowId !== '0' ? ( + ) : ( - )} @@ -1220,43 +1466,6 @@ export default function CowOverviewPage() {
- {/* 유전자형 필터 */} -
- 유전자형: -
- - - -
-
- {/* 정렬 드롭다운 */}