===== SSH ANSWER ===== created_at=2026-05-06 10:16:01 title=probe_155_source_via_web_trace_v1 file=/volume1/web/gpt/repair_jobs/ssh_answers/20260506_101601_probe_155_source_via_web_trace_v1.txt ===== probe 155 source via web trace v1 ===== time=2026-05-06 10:16:01 ===== PersonContextSynthesizer 금액 계산부 ===== 70 * LLM 요약 앞에 항상 붙인다. 71 */ 72 private static function buildFixedFactAnswer(array $s): string 73 { 74 $totals = $s['totals'] ?? []; 75 $counts = $s['counts'] ?? []; 76 77 $quoteTotal = (float)($totals['quotation_confirmed_total'] ?? 0); 78 $incomeConfirmed = (float)($totals['income_confirmed_total'] ?? 0); 79 $incomeCandidateTotal = (float)($totals['income_candidate_total'] ?? 0); 80 81 $incomeCandidateCount = (int)($counts['incomes_candidates'] ?? 0); 82 $incomeConfirmedCount = (int)($counts['incomes_confirmed'] ?? 0); 83 84 $unpaidByConfirmed = max(0, $quoteTotal - $incomeConfirmed); 85 86 $lines = []; 87 // INCOME_UX_REORDER_V1_SAFE 88 $lines[] = '공사/견적 확정 금액은 ' . self::fmtAmt($quoteTotal) . '입니다.'; 89 90 if ($incomeCandidateCount > 0) { 91 $lines[] = '현재 확인된 입금은 약 ' . self::fmtAmt($incomeCandidateTotal) . '입니다. 입금 후보 ' . $incomeCandidateCount . '건이 잡혀 있습니다.'; 92 $lines[] = '다만 이 입금들은 아직 현장에 확정 연결되지 않은 상태입니다.'; 93 $lines[] = '확정 입금 기준으로는 ' . self::fmtAmt($incomeConfirmed) . '만 반영되어 있습니다.'; 94 } else { 95 $lines[] = '현재 시스템에 확정 연결된 입금은 ' . self::fmtAmt($incomeConfirmed) . '이고, 확정 기준으로는 ' . self::fmtAmt($unpaidByConfirmed) . '이 아직 안 들어온 상태입니다.'; 96 } 97 98 if ($incomeCandidateCount > 0) { 99 // INCOME_UX_REMOVE_DUPLICATE_V1 100 // 후보 입금 요약은 위 INCOME_UX_REORDER_V1_SAFE 블록에서 이미 안내하므로 중복 출력하지 않는다. 101 102 $candidateLines = []; 103 $candidates = $s['incomes_candidates'] ?? []; 104 foreach (array_slice(is_array($candidates) ? $candidates : [], 0, 3) as $r) { 105 $parts = []; 106 $parts[] = '#' . (int)($r['id'] ?? 0); 107 $parts[] = self::fmtAmt($r['amount'] ?? 0); 108 $date = trim((string)($r['date'] ?? $r['paid_at'] ?? '')); 109 if ($date !== '') { 110 $parts[] = $date; 111 } 112 $memo = trim((string)($r['memo'] ?? $r['title'] ?? $r['payer_name'] ?? '')); 113 if ($memo !== '') { 114 $parts[] = $memo; 115 } 116 $candidateLines[] = '- 입금 후보 ' . implode(' / ', $parts); 117 } 118 119 if ($candidateLines) { 120 $lines[] = "입금 후보 상세:\n" . implode("\n", $candidateLines); 121 } 122 123 $lines[] = '지금 할 일: 입금 후보가 이 공사 입금이 맞는지 확인하고, 맞으면 현장 연결만 처리하면 됩니다.'; 124 } else { 125 $lines[] = '입금 후보도 없어서 실제 미입금 가능성이 높습니다.'; 126 $lines[] = '지금 할 일: 고객에게 입금 여부를 바로 확인해야 합니다.'; 127 } 128 129 return implode("\n", $lines); 130 } ===== PersonContextSnapshot 견적/합계 계산부 ===== 130 131 if ($expenseCandidates) { 132 $uncertainties[] = '일부 지출은 project_id 확정 연결이 아니라 site/memo/vendor 키워드로만 잡힌 후보입니다.'; 133 } 134 135 if ($incomeCandidates) { 136 $uncertainties[] = '일부 입금은 project_id 확정 연결이 아니라 고객 ID 또는 메모 키워드로만 잡힌 후보입니다.'; 137 } 138 139 if ($quotationCandidates) { 140 $uncertainties[] = '일부 견적은 project_id 확정 연결이 아니라 고객명/키워드로 잡힌 후보입니다.'; 141 } 142 143 return [ 144 'version' => 'person_context_snapshot_v1_2', 145 'query' => $query, 146 'aliases' => $aliases, 147 148 'customers' => $customers, 149 'projects' => $projects, 150 151 'quotations' => $quotations, 152 'quotations_confirmed' => $quotationConfirmed, 153 'quotations_candidates' => $quotationCandidates, 154 155 // 하위 호환: 기존 incomes/expenses는 확정만 담는다. 156 'incomes' => $incomeConfirmed, 157 'expenses' => $expenseConfirmed, 158 159 'incomes_confirmed' => $incomeConfirmed, 160 'incomes_candidates' => $incomeCandidates, 161 162 'expenses_confirmed' => $expenseConfirmed, 163 'expenses_candidates' => $expenseCandidates, 164 165 'histories' => $histories, 166 'likely_related_records' => $likelyRelatedRecords, 167 'totals' => $totals, 168 'uncertainties' => array_values(array_unique($uncertainties)), 169 170 'counts' => [ 171 'customers' => count($customers), 172 'projects' => count($projects), 173 174 'quotations' => count($quotations), 175 'quotations_confirmed' => count($quotationConfirmed), 176 'quotations_candidates' => count($quotationCandidates), 177 178 'incomes' => count($incomeConfirmed), 179 'incomes_confirmed' => count($incomeConfirmed), 180 'incomes_candidates' => count($incomeCandidates), 181 182 'expenses' => count($expenseConfirmed), 183 'expenses_confirmed' => count($expenseConfirmed), 184 'expenses_candidates' => count($expenseCandidates), 185 186 'histories' => count($histories), 187 'likely_related_records' => count($likelyRelatedRecords), 188 ], 189 ]; 190 } 250 } 251 252 /** @param array> $rows */ 253 private static function sumAmount(array $rows): float 254 { 255 $sum = 0.0; 256 foreach ($rows as $r) { 257 $sum += (float)($r['amount'] ?? 0); 258 } 259 return $sum; 260 } 261 262 263 /** 264 * @param array $aliases 265 * @param array> $projects 266 * @param array> $quotationConfirmed 267 * @param array> $quotationCandidates 268 * @param array> $incomeConfirmed 269 * @param array> $incomeCandidates 270 * @param array> $expenseConfirmed 271 * @param array> $expenseCandidates 272 * @param array> $histories 273 * @return array> 274 */ 275 private static function buildLikelyRelatedRecords( 276 string $query, 277 array $aliases, 278 array $projects, 279 array $quotationConfirmed, 280 array $quotationCandidates, 281 array $incomeConfirmed, 282 array $incomeCandidates, 283 array $expenseConfirmed, 284 array $expenseCandidates, 285 array $histories 286 ): array { 287 $projectTerms = self::projectTerms($projects); 288 $records = []; 289 290 $groups = [ 291 '확정 견적' => $quotationConfirmed, 292 '견적 후보' => $quotationCandidates, 293 '확정 입금' => $incomeConfirmed, 294 '입금 후보' => $incomeCandidates, 295 '확정 지출' => $expenseConfirmed, 296 '지출 후보' => $expenseCandidates, 297 '메모/히스토리' => $histories, 298 ]; 299 300 foreach ($groups as $type => $rows) { 301 foreach ($rows as $row) { 302 $text = self::recordText($row); 303 $score = self::relatedScore($text, $aliases, $projectTerms); 304 305 if ($score <= 0) { 306 continue; 307 } 308 309 $date = self::recordDate($row); 310 311 $records[] = [ 312 'type' => $type, 313 'date' => $date, 314 'title' => self::recordTitle($row), 315 'amount' => (float)($row['amount'] ?? 0), 316 'summary' => self::shortText($text, 120), 317 'score' => $score, 318 'reason' => self::scoreReason($score, $text, $aliases, $projectTerms), 319 'is_confirmed' => str_starts_with($type, '확정'), 320 'raw' => $row, ===== 명인환경 웹 API 답변 ===== **고객 정보** - 명인환경 김재현 · 01087546953 · 사업자 공사/견적 확정 금액은 155만원입니다. 현재 확인된 입금은 약 150만원입니다. 입금 후보 2건이 잡혀 있습니다. 다만 이 입금들은 아직 현장에 확정 연결되지 않은 상태입니다. 확정 입금 기준으로는 0원만 반영되어 있습니다. 입금 후보 상세: - 입금 후보 #236 / 110만원 / 2026-01-09 / 내용: 명인환경 전원반 5라인 신설 - 입금 후보 #24 / 40만원 / 2025-12-02 / 내용: 완도 청소년 수련관 투광기 8 라인 신설 / 명인환경 지금 할 일: 입금 후보가 이 공사 입금이 맞는지 확인하고, 맞으면 현장 연결만 처리하면 됩니다. --- [GPT 추천 연결 후보 - 저장/확정 아님] - 입금 후보 #236 → 현장 #29 완도 청소년 수련관 투광기 8 라인 신설 / 이유: customer_id 일치 · score=210 - 입금 후보 #24 → 현장 #29 완도 청소년 수련관 투광기 8 라인 신설 / 이유: 입금 메모와 현장명 유사 · score=190 --- 금액 줄만 --- - 명인환경 김재현 · 01087546953 · 사업자 공사/견적 확정 금액은 155만원입니다. 현재 확인된 입금은 약 150만원입니다. 입금 후보 2건이 잡혀 있습니다. 다만 이 입금들은 아직 현장에 확정 연결되지 않은 상태입니다. 확정 입금 기준으로는 0원만 반영되어 있습니다. 입금 후보 상세: - 입금 후보 #236 / 110만원 / 2026-01-09 / 내용: 명인환경 전원반 5라인 신설 - 입금 후보 #24 / 40만원 / 2025-12-02 / 내용: 완도 청소년 수련관 투광기 8 라인 신설 / 명인환경 지금 할 일: 입금 후보가 이 공사 입금이 맞는지 확인하고, 맞으면 현장 연결만 처리하면 됩니다. - 입금 후보 #236 → 현장 #29 완도 청소년 수련관 투광기 8 라인 신설 / 이유: customer_id 일치 · score=210 - 입금 후보 #24 → 현장 #29 완도 청소년 수련관 투광기 8 라인 신설 / 이유: 입금 메모와 현장명 유사 · score=190 ===== DONE =====