Commit ff757275 authored by platyhouse's avatar platyhouse

Merge commit 'fc8d9dcf'

parents 6ce2274e fc8d9dcf
No preview for this file type
<?php
namespace platyFramework;
require_once(__DIR__ . "/../ptycommon/model.php");
/**
* ChatGPT API Model
* OpenAI ChatGPT API를 사용하기 위한 클래스
*/
class ChatGPTAPIModel extends model
{
private $apiKey;
private $model = 'gpt-4o';
private $apiUrl = 'https://api.openai.com/v1/chat/completions';
private $maxTokens = 4096;
private $temperature = 1.0;
private $conversationHistory = [];
/**
* 모델 설정
* @param string $model 사용할 ChatGPT 모델명 (gpt-4o, gpt-4-turbo, gpt-3.5-turbo 등)
* @return $this
*/
public function setModel($model)
{
$this->model = $model;
return $this;
}
public function setAPIKey($apiKey)
{
$this->apiKey = $apiKey;
return $this;
}
/**
* 최대 토큰 수 설정
* @param int $maxTokens
* @return $this
*/
public function setMaxTokens($maxTokens)
{
$this->maxTokens = $maxTokens;
return $this;
}
/**
* Temperature 설정
* @param float $temperature 0.0 ~ 2.0
* @return $this
*/
public function setTemperature($temperature)
{
$this->temperature = $temperature;
return $this;
}
/**
* 대화 기록 초기화
* @return $this
*/
public function resetConversation()
{
$this->conversationHistory = [];
return $this;
}
/**
* ChatGPT API 호출
* @param string $message 사용자 메시지
* @param bool $keepHistory 대화 기록 유지 여부
* @return array|false 응답 배열 또는 false
*/
public function get($message, $keepHistory = false)
{
// 현재 메시지를 대화 기록에 추가
$messages = $this->conversationHistory;
$messages[] = [
'role' => 'user',
'content' => $message
];
// API 요청 데이터 구성
$data = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'temperature' => $this->temperature,
'messages' => $messages
];
// API 호출
$response = $this->_sendRequest($data);
if ($response === false) {
return false;
}
// 대화 기록 유지
if ($keepHistory && isset($response['choices'][0]['message']['content'])) {
$this->conversationHistory = $messages;
$this->conversationHistory[] = [
'role' => 'assistant',
'content' => $response['choices'][0]['message']['content']
];
}
return $response;
}
/**
* 시스템 프롬프트와 함께 메시지 전송
* @param string $message 사용자 메시지
* @param string $systemPrompt 시스템 프롬프트
* @return array|false
*/
public function getWithSystem($message, $systemPrompt)
{
$messages = [
[
'role' => 'system',
'content' => $systemPrompt
],
[
'role' => 'user',
'content' => $message
]
];
$data = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'temperature' => $this->temperature,
'messages' => $messages
];
return $this->_sendRequest($data);
}
/**
* API 요청 전송
* @param array $data 요청 데이터
* @return array|false
*/
private function _sendRequest($data)
{
// API 호출 타임아웃 방지
set_time_limit(0);
ini_set('max_execution_time', '0');
$ch = curl_init($this->apiUrl);
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey
];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("ChatGPT API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("ChatGPT API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("ChatGPT API JSON Decode Error: " . json_last_error_msg());
return false;
}
return $result;
}
/**
* 응답에서 텍스트만 추출
* @param array $response API 응답
* @return string|false
*/
public function extractText($response)
{
if (isset($response['choices'][0]['message']['content'])) {
return $response['choices'][0]['message']['content'];
}
return false;
}
/**
* 간단한 텍스트 응답만 받기
* @param string $message
* @return string|false
*/
public function getSimple($message)
{
$response = $this->get($message);
return $this->extractText($response);
}
}
\ No newline at end of file
<?php
namespace platyFramework;
require_once(__DIR__ . "/../ptycommon/model.php");
/**
* Claude API Model
* Anthropic Claude API를 사용하기 위한 클래스
*/
class ClaudeAPIModel extends model
{
private $apiKey;
private $model = 'claude-3-5-sonnet-20241022';
private $apiUrl = 'https://api.anthropic.com/v1/messages';
private $apiVersion = '2023-06-01';
private $maxTokens = 4096;
private $temperature = 1.0;
private $conversationHistory = [];
/**
* 생성자
* @param string $apiKey Anthropic API 키
*/
public function __construct($apiKey)
{
parent::__construct();
$this->apiKey = $apiKey;
}
/**
* 모델 설정
* @param string $model 사용할 Claude 모델명
* @return $this
*/
public function setModel($model)
{
$this->model = $model;
return $this;
}
/**
* 최대 토큰 수 설정
* @param int $maxTokens
* @return $this
*/
public function setMaxTokens($maxTokens)
{
$this->maxTokens = $maxTokens;
return $this;
}
/**
* Temperature 설정
* @param float $temperature 0.0 ~ 1.0
* @return $this
*/
public function setTemperature($temperature)
{
$this->temperature = $temperature;
return $this;
}
/**
* 대화 기록 초기화
* @return $this
*/
public function resetConversation()
{
$this->conversationHistory = [];
return $this;
}
/**
* Claude API 호출
* @param string $message 사용자 메시지
* @param bool $keepHistory 대화 기록 유지 여부
* @return array|false 응답 배열 또는 false
*/
public function get($message, $keepHistory = false)
{
// 현재 메시지를 대화 기록에 추가
$messages = $this->conversationHistory;
$messages[] = [
'role' => 'user',
'content' => $message
];
// API 요청 데이터 구성
$data = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'temperature' => $this->temperature,
'messages' => $messages
];
// API 호출
$response = $this->_sendRequest($data);
if ($response === false) {
return false;
}
// 대화 기록 유지
if ($keepHistory && isset($response['content'][0]['text'])) {
$this->conversationHistory = $messages;
$this->conversationHistory[] = [
'role' => 'assistant',
'content' => $response['content'][0]['text']
];
}
return $response;
}
/**
* 시스템 프롬프트와 함께 메시지 전송
* @param string $message 사용자 메시지
* @param string $systemPrompt 시스템 프롬프트
* @return array|false
*/
public function getWithSystem($message, $systemPrompt)
{
$messages = [
[
'role' => 'user',
'content' => $message
]
];
$data = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'temperature' => $this->temperature,
'system' => $systemPrompt,
'messages' => $messages
];
return $this->_sendRequest($data);
}
/**
* API 요청 전송
* @param array $data 요청 데이터
* @return array|false
*/
private function _sendRequest($data)
{
// API 호출 타임아웃 방지
set_time_limit(0);
ini_set('max_execution_time', '0');
$ch = curl_init($this->apiUrl);
$headers = [
'Content-Type: application/json',
'x-api-key: ' . $this->apiKey,
'anthropic-version: ' . $this->apiVersion
];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("Claude API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("Claude API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Claude API JSON Decode Error: " . json_last_error_msg());
return false;
}
return $result;
}
/**
* 응답에서 텍스트만 추출
* @param array $response API 응답
* @return string|false
*/
public function extractText($response)
{
if (isset($response['content'][0]['text'])) {
return $response['content'][0]['text'];
}
return false;
}
/**
* 간단한 텍스트 응답만 받기
* @param string $message
* @return string|false
*/
public function getSimple($message)
{
$response = $this->get($message);
return $this->extractText($response);
}
}
This diff is collapsed.
<?php
namespace platyFramework;
/**
* @example
* require_once("./ptyLibrary_PHP/aivectorembedding/EmbeddingChunkSplitter.php");
* $m = new EmbeddingChunkSplitter(maxTokens: 1024 * 1023 # chatgpt 4.0 mini 기준 chunks 크기
* , mainRatio: 0.9);
* $chunks = $m->createChunks($markdown);
*/
class EmbeddingChunkSplitter
{
private int $maxTokens;
private float $mainRatio;
// private int $overlapTokens;
/**
* @param int $maxTokens 최대 토큰 수 (기본값: 16000)
* @param float $mainRatio 메인 컨텐츠 비율 (기본값: 0.7, 앞뒤 15%씩 오버랩)
*/
public function __construct(int $maxTokens = 16000, float $mainRatio = 0.7)
{
$this->maxTokens = $maxTokens;
$this->mainRatio = $mainRatio;
// $this->overlapTokens = (int)(($maxTokens * (1 - $mainRatio)) / 2);
// pdfe($this->overlapTokens, "overlapTokens");
}
/**
* 텍스트 배열을 임베딩에 최적화된 청크로 분할
*
* @param array $texts 입력 텍스트 배열
* @return array 분할된 텍스트 청크 배열
*/
public function createChunks(string|array $texts): array
{
$resultTexts = [];
$previousOverlap = '';
if (is_string($texts))
$texts = [$texts];
foreach ($texts as $text) {
// 헤더 추출
$headers = $this->extractHeaders($text);
// pdfe($headers);
$headerText = implode("\n", $headers);
// 본문 추출 (헤더 제외)
$contentLines = $this->extractContentLines($text, $headers);
// pdfe($contentLines, "contentLines");
// 청크를 배열로 관리 (성능 개선)
$currentChunkLines = [$headerText];
$currentTokenCount = $this->estimateTokens($headerText);
if (!empty($previousOverlap)) {
$currentChunkLines[] = $previousOverlap;
$currentTokenCount += $this->estimateTokens($previousOverlap) + 2; // +2 for "\n\n"
}
// 본문 추가
foreach ($contentLines as $i => $line) {
// 현재 라인의 토큰만 계산 (증분 방식으로 O(n²) -> O(n) 개선)
$lineTokens = $this->estimateTokens($line) + 1; // +1 for newline
if ($i % 100 == 0) {
pd("", "[ $i / " . count($contentLines) . " ] chunking");
pd($currentTokenCount, "[ $i / " . count($contentLines) . " ] currentTokenCount");
pd($lineTokens, "[ $i / " . count($contentLines) . " ] lineTokens");
pd($this->maxTokens, "[ $i / " . count($contentLines) . " ] maxTokens");
}
if ($currentTokenCount + $lineTokens > $this->maxTokens) {
// pdf($currentTokenCount, "$i. tokenCount");
// pdf($this->maxTokens, "$i. maxTokens");
// 현재 청크 저장
$currentChunk = implode("\n", $currentChunkLines);
if (!empty(trim($currentChunk))) {
$resultTexts[] = trim($currentChunk);
}
// 오버랩 생성 (마지막 15%)
$previousOverlap = $this->createOverlap($currentChunk);
// 새 청크 시작 (헤더 + 오버랩 + 현재 라인)
$currentChunkLines = [$headerText, $previousOverlap, $line];
$currentTokenCount = $this->estimateTokens($headerText) +
$this->estimateTokens($previousOverlap) +
$lineTokens + 2; // +2 for "\n\n"
} else {
$currentChunkLines[] = $line;
$currentTokenCount += $lineTokens;
}
}
// 마지막 청크 저장
$currentChunk = implode("\n", $currentChunkLines);
if (!empty(trim($currentChunk))) {
// pdf($currentChunk, "last! $currentChunk");
$resultTexts[] = trim($currentChunk);
}
// 다음 문서를 위한 오버랩 생성
$previousOverlap = $this->createOverlap($currentChunk);
}
pdf($resultTexts, "resultTexts", showArray: true);
return $resultTexts;
}
/**
* 마크다운 헤더 추출
*
* @param string $text 입력 텍스트
* @return array 헤더 라인 배열
*/
private function extractHeaders(string $text): array
{
$lines = explode("\n", $text);
$headers = [];
foreach ($lines as $line) {
$trimmed = trim($line);
// #, ##, ###, #### 로 시작하는 헤더 또는 메타데이터(- 로 시작)
if (preg_match('/^#{1,4}\s+/', $trimmed) || preg_match('/^-\s+/', $trimmed)) {
$headers[] = $line;
} else {
// 첫 번째 본문이 나오면 헤더 섹션 종료
if (!empty($trimmed) && count($headers) > 0) {
break;
}
}
}
return $headers;
}
/**
* 본문 라인 추출 (헤더 제외)
*
* @param string $text 입력 텍스트
* @param array $headers 헤더 배열
* @return array 본문 라인 배열
*/
private function extractContentLines(string $text, array $headers): array
{
$lines = explode("\n", $text);
$headerCount = count($headers);
$contentLines = [];
$inContent = false;
foreach ($lines as $line) {
$trimmed = trim($line);
// 헤더 섹션 건너뛰기
if (!$inContent) {
$isHeader = preg_match('/^#{1,4}\s+/', $trimmed) ||
preg_match('/^-\s+/', $trimmed) ||
empty($trimmed);
if (!$isHeader && !empty($trimmed)) {
$inContent = true;
}
}
if ($inContent) {
$contentLines[] = $line;
}
}
return $contentLines;
}
/**
* 토큰 수 추정 (한글 고려)
* voyage-3-large는 대략 1 토큰 ≈ 1.5-2 글자 (한글 기준)
*
* @param string $text 텍스트
* @return int 추정 토큰 수
*/
private function estimateTokens(string $text): int
{
// 빈 문자열 체크
if (empty($text)) return 0;
// 한글 문자 수 계산 (더 효율적인 방식)
$totalChars = mb_strlen($text);
$nonKoreanText = preg_replace('/[\x{AC00}-\x{D7AF}]/u', '', $text);
$koreanChars = $totalChars - mb_strlen($nonKoreanText);
// 영문/숫자/기호 문자 수
$otherChars = $totalChars - $koreanChars;
// 한글: 1.5 글자 ≈ 1 토큰, 영문: 4 글자 ≈ 1 토큰
return (int)(($koreanChars / 1.5) + ($otherChars / 4));
}
/**
* 청크의 마지막 15%로 오버랩 생성
*
* @param string $chunk 현재 청크
* @return string 오버랩 텍스트
*/
private function createOverlap(string $chunk): string
{
$lines = explode("\n", $chunk);
$totalLines = count($lines);
// $overlapLineCount = max(1, (int)($totalLines * 0.15));
$overlapLineCount = max(1, (int)($totalLines * (1 - $this->mainRatio) / 2));
// 마지막 15% 라인 추출
$overlapLines = array_slice($lines, -$overlapLineCount);
return implode("\n", $overlapLines);
}
}
// 사용 예제
/*
$splitter = new EmbeddingChunkSplitter(16000, 0.7);
$texts = [
"# 대기환경보전법\n- 법령 ID : 00177320270110_276715\n...",
"# 대기환경보전법\n- 법령 ID : 00177320270110_276715\n...",
// ... more texts
];
$chunks = $splitter->createChunks($texts);
foreach ($chunks as $index => $chunk) {
echo "=== Chunk $index ===\n";
echo $chunk . "\n\n";
}
*/
\ No newline at end of file
<?php
namespace platyFramework;
//require_once("./db.php");
//require_once("./Elastic.php");
//require_once("./ptyCliLog.php");
ini_set('memory_limit', '2G');
ini_set('max_execution_time', '0');
set_time_limit(0);
require_once(__DIR__ . "/../elastic/Elastic.php");
class SomeEmbeddingItemModel extends ptyItemModel
{
public function processVectorEmbeddingItems($embeddingItemsModel)
{
$log = new ptyCliLog(prefix: "임베딩 처리", color: ptyCliLog::COLOR_GREEN);
$this->embeddingStatus = "IN_PROGRESS_EMBEDDING";
$this->updateWithItemName("embeddingStatus");
$chunks = json_decode($this->chunks, true);
$log->info("docId: {$this->docId}, documentTitle: $this->title, docIndex: $this->docIndex, total chunk count = ".count($chunks));
// embeddings 수집
$embeddings = [];
if (true) {
// $oldItem = $this->db->sqlFetchItem("select * from new_law_items_embedding_all where docId = '{$this->docId}'");
$chunkText = mre($this->chunks);
$q = "select * from new_law_items_embedding_all where chunk_crc = CRC32('$chunkText') AND chunk_sha = UNHEX(SHA2('$chunkText', 256))";
// $log->info("Query = $q");
$oldItem = $this->db->sqlFetchItem($q);
if ($oldItem != null && $oldItem->embeddings != "[]" && $oldItem->embeddings != "") {
$log->info("기존 임베딩에 존재함.");
$embeddings = json_decode($oldItem->embeddings, true);
}
}
// 기존 임베딩이 없으면 생성
if (count($embeddings) == 0) {
foreach ($chunks as $chunkIndex => $chunk) {
$log->info("embedding. documentTitle: $this->title, docIndex: $this->docIndex, current chunkIndex: $chunkIndex");
$embedding = $embeddingItemsModel->getClaudeVoyage3largeVectorEmbedding($chunk);
$embeddings [] = $embedding;
}
}
if (count($embeddings) == 0) {
$this->embeddings = json_encode($embeddings, JSON_UNESCAPED_UNICODE);
$this->embeddingStatus = "FAILURE_EMBEDDING";
$this->updateWithItemName(["embeddings", "embeddingStatus"]);
return;
}
// elastic 에 기록
foreach ($chunks as $chunkIndex => $chunk) {
$log->info("elastic. documentTitle: $this->title, docIndex: $this->docIndex, current chunkIndex: $chunkIndex");
// 개별 chunk 마다 recordElastic 을 기록
$embeddingItemsModel->recordElastic(
serviceName: $this->serviceName,
serviceId: $this->serviceId,
documentTitle: $this->title,
docIndex: $this->docIndex,
chunkIndex: $chunkIndex,
chunk: $chunk,
embedding: $embeddings[$chunkIndex],
);
}
$this->embeddings = json_encode($embeddings, JSON_UNESCAPED_UNICODE);
$this->embeddingStatus = "SUCCESS_ELASTIC";
$this->updateWithItemName(["embeddings", "embeddingStatus"]);
}
}
?>
\ No newline at end of file
This diff is collapsed.
...@@ -57,9 +57,9 @@ class ptyCliLog ...@@ -57,9 +57,9 @@ class ptyCliLog
// 타임스탬프 생성 (밀리초 포함) // 타임스탬프 생성 (밀리초 포함)
$now = microtime(true); $now = microtime(true);
// $milliseconds = sprintf("%03d", ($now - floor($now)) * 1000); $milliseconds = sprintf("%03d", ($now - floor($now)) * 1000);
// $timestamp = date('Y-m-d H:i:s', (int)$now) . '.' . $milliseconds; $timestamp = date('H:i:s', (int)$now) . '.' . $milliseconds;
$timestamp = date('H:i:s', (int)$now); // $timestamp = date('H:i:s', (int)$now);
$typeColor = $this->colors[$type] ?? $this->colors['info']; $typeColor = $this->colors[$type] ?? $this->colors['info'];
$reset = $this->colors['reset']; $reset = $this->colors['reset'];
...@@ -84,20 +84,22 @@ class ptyCliLog ...@@ -84,20 +84,22 @@ class ptyCliLog
$lineNumber = $matches[2]; $lineNumber = $matches[2];
$fileName = basename($filePath); $fileName = basename($filePath);
$functionName = $fileName . ':' . $lineNumber; $functionName = $fileName . ':' . $lineNumber;
} } else {
else { $functionName .= "()";
$functionName.= "()";
} }
$functionName = ptyCliColor::DARK_WHITE . $functionName . " ". $reset; $functionName = ptyCliColor::DARK_WHITE . $functionName . " " . $reset;
} }
} }
} }
// 타임스탬프 + prefix에 지정된 색상 사용, type은 타입 색상 사용 // 타임스탬프 + prefix에 지정된 색상 사용, type은 타입 색상 사용
echo ptyCliColor::DARK_WHITE . $timestamp . " " . $reset; echo ptyCliColor::DARK_WHITE . $timestamp . " " . $reset;
if ($functionName != "") {
echo sprintf("%-45s", $functionName); echo sprintf("%-45s", $functionName);
}
echo $this->color . sprintf("[%-8s] ", strtoupper($this->prefix)) . $reset; echo $this->color . sprintf("[%-8s] ", strtoupper($this->prefix)) . $reset;
echo $typeColor . $message . $reset . "\n"; echo $typeColor . $message . $reset . "\n";
...@@ -169,7 +171,7 @@ class ptyCliLog ...@@ -169,7 +171,7 @@ class ptyCliLog
public function separator($length = 50) public function separator($length = 50)
{ {
$this->log("\n".str_repeat("=", $length), 'info'); $this->log("\n" . str_repeat("=", $length), 'info');
} }
} }
......
...@@ -527,7 +527,7 @@ function ptyJsonReturnFalse($arr = null) ...@@ -527,7 +527,7 @@ function ptyJsonReturnFalse($arr = null)
* @param array|null $arr * @param array|null $arr
* @return mixed * @return mixed
*/ */
function ptyParseStr($str, array &$arr = null) function ptyParseStr($str, ?array &$arr = null)
{ {
$arr = array(); $arr = array();
parse_str($str, $param); parse_str($str, $param);
...@@ -1107,7 +1107,10 @@ function ptyDebugXmp($s, $title = "", int $debugPos = 1, string $terminalColor = ...@@ -1107,7 +1107,10 @@ function ptyDebugXmp($s, $title = "", int $debugPos = 1, string $terminalColor =
echo ptyTerminalColor::reset . "\n\n"; echo ptyTerminalColor::reset . "\n\n";
flush(); flush();
if (ob_get_level() > 0) {
ob_flush(); ob_flush();
}
} else { } else {
if ($title) if ($title)
echo "<br><font color='red'>[ $title ]</font>" . " - " . $callInfo['file'] . ":" . $callInfo['line'] . "<Br><xmp>"; echo "<br><font color='red'>[ $title ]</font>" . " - " . $callInfo['file'] . ":" . $callInfo['line'] . "<Br><xmp>";
...@@ -1192,7 +1195,7 @@ function getCurrentFileAndLine($n) ...@@ -1192,7 +1195,7 @@ function getCurrentFileAndLine($n)
return array( return array(
'file' => $trace[$n]['file'], 'file' => $trace[$n]['file'],
'line' => $trace[$n]['line'], 'line' => $trace[$n]['line'],
'class' => $trace[$n]['class'], 'class' => $trace[$n]['class'] ?? "(UNKNOWN)",
'function' => $trace[$n]['function'] 'function' => $trace[$n]['function']
); );
} else { } else {
...@@ -1229,11 +1232,34 @@ function pd($s = null, string $title = "", int $pos = 2, string $terminalColor = ...@@ -1229,11 +1232,34 @@ function pd($s = null, string $title = "", int $pos = 2, string $terminalColor =
* @param string $terminalColor * @param string $terminalColor
* @return bool * @return bool
*/ */
function pdf($s = null, string $title = "", int $pos = 3, string $terminalColor = \ptyTerminalColor::blueBoldBright): bool function pdf($s = null, string $title = "", int $pos = 3, string $terminalColor = \ptyTerminalColor::blueBoldBright, $showArray = false): bool
{ {
if (!isset($GLOBALS['ispd'])) {
$GLOBALS['ispd'] = 1;
if ($showArray && is_array($s)) {
foreach ($s as $key => $value) {
pd($value, "[$key] " . $title, $pos, $terminalColor);
}
} else {
pd($s, $title, $pos, $terminalColor);
}
// pd($s, $title, $pos, $terminalColor);
$GLOBALS['ispd'] = 0;
return true;
}
$ispd = $GLOBALS['ispd']; $ispd = $GLOBALS['ispd'];
$GLOBALS['ispd'] = 1; $GLOBALS['ispd'] = 1;
if ($showArray && is_array($s)) {
foreach ($s as $key => $value) {
pd($value, "[$key] " . $title, $pos, $terminalColor);
}
} else {
pd($s, $title, $pos, $terminalColor); pd($s, $title, $pos, $terminalColor);
}
$GLOBALS['ispd'] = $ispd; $GLOBALS['ispd'] = $ispd;
// pd() // pd()
...@@ -1446,6 +1472,9 @@ function ptyIsDebugIP() ...@@ -1446,6 +1472,9 @@ function ptyIsDebugIP()
{ {
global $platyFramework; global $platyFramework;
if (!isset($platyFramework->request))
return false;
$ip = $platyFramework->request->ip; $ip = $platyFramework->request->ip;
// IP 제한 // IP 제한
...@@ -1516,9 +1545,9 @@ function ispdv() ...@@ -1516,9 +1545,9 @@ function ispdv()
return false; return false;
} }
function pdfe($s = "", $title = "") function pdfe($s = "", $title = "", $showArray = false)
{ {
pdf($s, $title, pos: 4); pdf($s, $title, pos: 4, showArray: $showArray);
exit; exit;
} }
...@@ -2930,7 +2959,7 @@ function ptyGetTimeDifferenceMinSeconds($timeA, $timeB): string ...@@ -2930,7 +2959,7 @@ function ptyGetTimeDifferenceMinSeconds($timeA, $timeB): string
* @param $datetime * @param $datetime
* @return false|string * @return false|string
*/ */
function ptyGetPastDateTime($datetime = '', int $curTime = null) function ptyGetPastDateTime($datetime = '', ?int $curTime = null)
{ {
if ($datetime == "") if ($datetime == "")
return ""; return "";
...@@ -3196,7 +3225,7 @@ function ptyPerMultiplier($percent) ...@@ -3196,7 +3225,7 @@ function ptyPerMultiplier($percent)
return (100 - $percent) / 100; return (100 - $percent) / 100;
} }
function ptyGetUserAgentBot(string $userAgent = null) function ptyGetUserAgentBot(?string $userAgent = null)
{ {
if (is_null($userAgent) || $userAgent == "") if (is_null($userAgent) || $userAgent == "")
return null; return null;
...@@ -3362,5 +3391,71 @@ function ptyEcho(...$args) ...@@ -3362,5 +3391,71 @@ function ptyEcho(...$args)
echo " " . ptyTerminalColor::reset . "\n"; echo " " . ptyTerminalColor::reset . "\n";
} }
function ptyError($message)
{
die ($message);
}
/**
* 배열의 모든 리프 노드 값을 수집하여 결합
*
* @param array $data 입력 배열
* @param string $separator 구분자
* @return string 결합된 문자열
*/
function ptyRecursiveImplode(string $separator = "\n", string|array $data = [], bool $trim = true): string
{
if (!is_array($data)) {
return $trim ? trim($data) : $data;
}
$result = [];
array_walk_recursive($data, function ($value) use (&$result, $trim) {
$str = (string)$value;
if ($trim) {
$str = trim($str);
// 빈 문자열 제외 (선택사항)
if ($str !== '') {
$result[] = $str;
}
} else {
$result[] = $str;
}
});
return implode($separator, $result);
}
// 사용
// $content = flattenAndImplode($data['항내용']);
function ptyShowThrowable(\Throwable|\Exception $e, ?platyFramework\ptyCliLog $log = null)
{
if (!$log) {
require_once("../cli/ptyCliColor.php");
require_once("../cli/ptyCliLog.php");
$log = new ptyCliLog(prefix: "App", color: ptyCliLog::COLOR_RED);
}
$log->error("Error Type: " . get_class($e));
$log->error("Error Message: " . $e->getMessage());
$log->error("Error Code: " . $e->getCode());
$log->error("Error File: " . $e->getFile());
$log->error("Error Line: " . $e->getLine());
$log->error("Stack Trace:\n" . $e->getTraceAsString());
// 이전 예외가 있으면 (체인된 예외)
if ($e->getPrevious()) {
$log->error("Previous Exception: " . $e->getPrevious()->getMessage());
}
}
function ptyRemoveSpaces($s)
{
$s = str_replace(" ", " ", $s);
$s = str_replace("\n\n", "\n", $s);
return $s;
}
?> ?>
...@@ -81,9 +81,9 @@ class ptyArray extends \ArrayObject ...@@ -81,9 +81,9 @@ class ptyArray extends \ArrayObject
return $this->getArrayCopy(); return $this->getArrayCopy();
} }
public function natsort() public function _natsort()
{ {
parent::natsort(); parent::_natsort();
return $this; return $this;
} }
......
...@@ -58,7 +58,7 @@ class ptyCrawling ...@@ -58,7 +58,7 @@ class ptyCrawling
$this->log->info("페이지 $page - 처리중: $currentUrl"); $this->log->info("페이지 $page - 처리중: $currentUrl");
// getAndSave 메서드를 사용하여 데이터 가져오기 및 저장 // getAndSave 메서드를 사용하여 데이터 가져오기 및 저장
$body = $this->getUrlContents($currentUrl, $reloadCacheTime); $body = $this->_getUrlContents($currentUrl, $reloadCacheTime);
if ($body === false) { if ($body === false) {
$this->log->error("Page $page - Failed to fetch URL"); $this->log->error("Page $page - Failed to fetch URL");
...@@ -66,7 +66,7 @@ class ptyCrawling ...@@ -66,7 +66,7 @@ class ptyCrawling
} }
// 파일명과 캐시 정보 추적 // 파일명과 캐시 정보 추적
$filename = $this->dataDir . '/' . $this->urlToFilename($currentUrl); $filename = $this->dataDir . '/' . $this->_urlToFilename($currentUrl);
$wasFromCache = $this->lastFetchWasFromCache; $wasFromCache = $this->lastFetchWasFromCache;
if ($wasFromCache) { if ($wasFromCache) {
...@@ -76,6 +76,13 @@ class ptyCrawling ...@@ -76,6 +76,13 @@ class ptyCrawling
} }
$crawledFiles[] = $filename; $crawledFiles[] = $filename;
$shouldContinue = $this->onCrawlingPaging($page, $body);
if (!$shouldContinue) {
$this->log->info("Page $page - Callback returned false. Stopping.");
break;
}
/*
// isContinue 콜백이 제공된 경우 호출 // isContinue 콜백이 제공된 경우 호출
if ($isContinue !== null && is_callable($isContinue)) { if ($isContinue !== null && is_callable($isContinue)) {
$shouldContinue = call_user_func($isContinue, $page, $body); $shouldContinue = call_user_func($isContinue, $page, $body);
...@@ -84,6 +91,7 @@ class ptyCrawling ...@@ -84,6 +91,7 @@ class ptyCrawling
break; break;
} }
} }
*/
// 다음 페이지로 이동 // 다음 페이지로 이동
$page++; $page++;
...@@ -104,10 +112,10 @@ class ptyCrawling ...@@ -104,10 +112,10 @@ class ptyCrawling
* @param string $content 저장할 콘텐츠 * @param string $content 저장할 콘텐츠
* @return string|false 성공 시 파일명, 실패 시 false * @return string|false 성공 시 파일명, 실패 시 false
*/ */
private function saveToFile($url, $content) private function _saveToFile($url, $content)
{ {
// URL을 안전한 파일명으로 변환 // URL을 안전한 파일명으로 변환
$filename = $this->urlToFilename($url); $filename = $this->_urlToFilename($url);
$filepath = $this->dataDir . '/' . $filename; $filepath = $this->dataDir . '/' . $filename;
// 필요한 경우 하위 디렉토리 생성 // 필요한 경우 하위 디렉토리 생성
...@@ -129,7 +137,7 @@ class ptyCrawling ...@@ -129,7 +137,7 @@ class ptyCrawling
* @param string $url 변환할 URL * @param string $url 변환할 URL
* @return string 안전한 파일명 * @return string 안전한 파일명
*/ */
private function urlToFilename($url) private function _urlToFilename($url)
{ {
// 프로토콜 제거 // 프로토콜 제거
$filename = preg_replace('#^https?://#', '', $url); $filename = preg_replace('#^https?://#', '', $url);
...@@ -141,8 +149,8 @@ class ptyCrawling ...@@ -141,8 +149,8 @@ class ptyCrawling
$filename = preg_replace('#_+#', '_', $filename); $filename = preg_replace('#_+#', '_', $filename);
// .json 확장자가 없으면 추가 // .json 확장자가 없으면 추가
if (!preg_match('#\.json$#i', $filename)) { if (!preg_match('#\.body#i', $filename)) {
$filename .= '.json'; $filename .= '.body';
} }
return $filename; return $filename;
...@@ -153,7 +161,7 @@ class ptyCrawling ...@@ -153,7 +161,7 @@ class ptyCrawling
* *
* @param string $dir 디렉토리 경로 * @param string $dir 디렉토리 경로
*/ */
public function setDataDir($dir) public function _setDataDir($dir)
{ {
$this->dataDir = $dir; $this->dataDir = $dir;
if (!is_dir($this->dataDir)) { if (!is_dir($this->dataDir)) {
...@@ -161,6 +169,13 @@ class ptyCrawling ...@@ -161,6 +169,13 @@ class ptyCrawling
} }
} }
public function getUrlFileName($url) {
$cacheFilename = $this->_urlToFilename($url);
$cacheFilepath = $this->dataDir . '/' . $cacheFilename;
return $cacheFilepath;
}
/** /**
* URL에서 데이터를 가져오고 파일로 저장 * URL에서 데이터를 가져오고 파일로 저장
* 캐시 확인, fetch, 저장을 하나의 메서드에서 처리 * 캐시 확인, fetch, 저장을 하나의 메서드에서 처리
...@@ -169,13 +184,17 @@ class ptyCrawling ...@@ -169,13 +184,17 @@ class ptyCrawling
* @param int $reloadCacheTime 캐시 유효 시간(초). 0이면 캐시 사용 안 함 * @param int $reloadCacheTime 캐시 유효 시간(초). 0이면 캐시 사용 안 함
* @return string|false 성공 시 응답 본문, 실패 시 false * @return string|false 성공 시 응답 본문, 실패 시 false
*/ */
public function getUrlContents($url, $reloadCacheTime = 60 * 60 * 24 * 30) public function _getUrlContents($url, $reloadCacheTime = 60 * 60 * 24 * 30)
{ {
$this->log->url->verbose("URL 가져오는중: $url"); $this->log->url->verbose("URL 가져오는중: $url");
// 캐시 파일 경로 생성 // 캐시 파일 경로 생성
$cacheFilename = $this->urlToFilename($url); $cacheFilename = $this->_urlToFilename($url);
$cacheFilepath = $this->dataDir . '/' . $cacheFilename; $cacheFilepath = $this->dataDir . '/' . $cacheFilename;
if (file_exists($cacheFilepath)) ;
else if (file_exists($cacheFilepath . ".in_progress")) $cacheFilepath = $cacheFilepath . ".in_progress";
else if (file_exists($cacheFilepath . ".success")) $cacheFilepath = $cacheFilepath . ".success";
// 캐시 사용이 활성화된 경우, 캐시 확인 // 캐시 사용이 활성화된 경우, 캐시 확인
if ($reloadCacheTime > 0 && file_exists($cacheFilepath)) { if ($reloadCacheTime > 0 && file_exists($cacheFilepath)) {
$fileModTime = filemtime($cacheFilepath); $fileModTime = filemtime($cacheFilepath);
...@@ -223,7 +242,7 @@ class ptyCrawling ...@@ -223,7 +242,7 @@ class ptyCrawling
} }
// 파일로 저장 // 파일로 저장
$filename = $this->saveToFile($url, $body); $filename = $this->_saveToFile($url, $body);
if (!$filename) { if (!$filename) {
$this->log->url->error("Failed to save file"); $this->log->url->error("Failed to save file");
return false; return false;
...@@ -237,4 +256,9 @@ class ptyCrawling ...@@ -237,4 +256,9 @@ class ptyCrawling
return $body; return $body;
} }
public function onCrawlingPaging($pageIndex, $body)
{
return true;
}
} }
...@@ -21,6 +21,7 @@ function mre($s = "") ...@@ -21,6 +21,7 @@ function mre($s = "")
return ""; return "";
global $platyFramework; global $platyFramework;
// pdfe($platyFramework->db->_connection);
if ($platyFramework == null || $platyFramework->db == null || $platyFramework->db->_connection == null) { if ($platyFramework == null || $platyFramework->db == null || $platyFramework->db->_connection == null) {
// die ("db is not initialized"); // die ("db is not initialized");
return; return;
...@@ -75,6 +76,18 @@ class ptyMysql ...@@ -75,6 +76,18 @@ class ptyMysql
var $_SQL_SET_ARRAY; var $_SQL_SET_ARRAY;
var $_SQL_LIMIT_ROWCOUNT; var $_SQL_LIMIT_ROWCOUNT;
var $_SQL_JOIN; var $_SQL_JOIN;
var $_SQL_JOIN_ARRAY = [];
var $_SQL_SELECT_ARRAY = [];
var $_SQL_GROUP_BY_ARRAY = [];
var $_SQL_FROM_ARRAY = [];
var $_SQL_UNION_ARRAY = [];
var $_SQL_HAVING = "";
var $_SQL_HAVING_ARRAY = [];
var $pageName;
var $_connection; var $_connection;
var $bDestroymSelect; var $bDestroymSelect;
var $activeCacheFileName = ""; var $activeCacheFileName = "";
...@@ -188,6 +201,7 @@ class ptyMysql ...@@ -188,6 +201,7 @@ class ptyMysql
return false; return false;
} }
mysqli_select_db($this->_connection, $DBName); mysqli_select_db($this->_connection, $DBName);
mysqli_query($this->_connection, "set names " . PLATYFRAMEWORK_DB_CHARSET); mysqli_query($this->_connection, "set names " . PLATYFRAMEWORK_DB_CHARSET);
ptyRecordElapsedTime("mysql connect"); ptyRecordElapsedTime("mysql connect");
...@@ -250,16 +264,6 @@ class ptyMysql ...@@ -250,16 +264,6 @@ class ptyMysql
function __destruct() function __destruct()
{ {
if ($this->bDestroymSelect) {
?>
<SCRIPT language='javascript'>
<!--
mSelect('SearchType', '<?=$_REQUEST[SearchType]?>');
mSelect('SearchType2', '<?=$_REQUEST[SearchType2]?>');
-->
</SCRIPT>
<?
}
} }
function getInsertId() function getInsertId()
...@@ -665,7 +669,7 @@ class ptyMysql ...@@ -665,7 +669,7 @@ class ptyMysql
} }
if ($elapsed_time > 0.2 /* 200ms*/) { if (defined( "PLATYFRAMEWORK_SYSTEM_DIR") && $elapsed_time > 0.2 /* 200ms*/) {
// echo "{$Query} {$elapsed_time} <br>"; // echo "{$Query} {$elapsed_time} <br>";
$slowLog = "\n❗️" . date("Y-m-d H:i:s") . " MYSQL SLOW QUERY\n"; $slowLog = "\n❗️" . date("Y-m-d H:i:s") . " MYSQL SLOW QUERY\n";
...@@ -695,6 +699,7 @@ class ptyMysql ...@@ -695,6 +699,7 @@ class ptyMysql
$slowLog .= "$file[$i]:$line[$i] - $class[$i]::$func[$i]($args[$i]) \n"; $slowLog .= "$file[$i]:$line[$i] - $class[$i]::$func[$i]($args[$i]) \n";
} }
// PLATYFRAMEWORK_SYSTEM_DIR 정의시
$slowLogFileName = PLATYFRAMEWORK_SYSTEM_DIR . "/logs/mysql_slow_log.txt"; $slowLogFileName = PLATYFRAMEWORK_SYSTEM_DIR . "/logs/mysql_slow_log.txt";
file_put_contents($slowLogFileName, $slowLog, FILE_APPEND | LOCK_EX); file_put_contents($slowLogFileName, $slowLog, FILE_APPEND | LOCK_EX);
} }
...@@ -705,6 +710,7 @@ class ptyMysql ...@@ -705,6 +710,7 @@ class ptyMysql
if ($elapsed_time > 3) { if ($elapsed_time > 3) {
pd($Query, "🔴 WARNING TOO SLOW Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright); pd($Query, "🔴 WARNING TOO SLOW Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright);
if (class_exists("platyFramework\\ptylogsItemsModel")) {
ptylogsItemsModel::build()->addLog(level: ptyLogLevel::WARNING, title: "TOO SLOW QUERY", ptylogsItemsModel::build()->addLog(level: ptyLogLevel::WARNING, title: "TOO SLOW QUERY",
content: ["elapsedTime" => $elapsed_time, "query" => $Query], content: ["elapsedTime" => $elapsed_time, "query" => $Query],
serviceName: "database", serviceName: "database",
...@@ -714,9 +720,11 @@ class ptyMysql ...@@ -714,9 +720,11 @@ class ptyMysql
sendUserTimeline: false, sendUserTimeline: false,
sendStats: false, sendStats: false,
); );
}
} else if ($elapsed_time > 1) { } else if ($elapsed_time > 1) {
pd($Query, "🟠 WARNING SLOW Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright, showAllCallStack: false); pd($Query, "🟠 WARNING SLOW Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright, showAllCallStack: false);
if (class_exists("platyFramework\\ptylogsItemsModel")) {
ptylogsItemsModel::build()->addLog(level: ptyLogLevel::WARNING, title: "SLOW QUERY", ptylogsItemsModel::build()->addLog(level: ptyLogLevel::WARNING, title: "SLOW QUERY",
content: ["elapsedTime" => $elapsed_time, "query" => $Query], content: ["elapsedTime" => $elapsed_time, "query" => $Query],
serviceName: "database", serviceName: "database",
...@@ -726,6 +734,7 @@ class ptyMysql ...@@ -726,6 +734,7 @@ class ptyMysql
sendUserTimeline: false, sendUserTimeline: false,
sendStats: false, sendStats: false,
); );
}
} else { } else {
pd($Query, "🟢 Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright); pd($Query, "🟢 Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright);
...@@ -1096,10 +1105,13 @@ class ptyMysql ...@@ -1096,10 +1105,13 @@ class ptyMysql
pdv($a, "sql cache readed #1"); pdv($a, "sql cache readed #1");
foreach ($a->data[0] as $value) { foreach ($a->data[0] as $value) {
/*
global $_queryHistoryItems; global $_queryHistoryItems;
if (!strstr($Query, "t_pty_logs") && !strstr($Query, "t_pty_inbounds") && !strstr($Query, "t_pty_allow_ips") && !strstr($Query, "t_timeline_items")) { if (!strstr($Query, "t_pty_logs") && !strstr($Query, "t_pty_inbounds") && !strstr($Query, "t_pty_allow_ips") && !strstr($Query, "t_timeline_items")) {
$_queryHistoryItems[] = " elapsed cache targeting!"; $_queryHistoryItems[] = " elapsed cache targeting!";
} }
*/
return $value; return $value;
} }
...@@ -2508,7 +2520,7 @@ class ptyMysql ...@@ -2508,7 +2520,7 @@ class ptyMysql
* @author KwangHee Yoo <cpueblo@platyhouse.com> * @author KwangHee Yoo <cpueblo@platyhouse.com>
* @created 2024-04-09 * @created 2024-04-09
*/ */
public function addWhereSignUser(model $model = null): ptyMysql public function addWhereSignUser(?model $model = null): ptyMysql
{ {
if ($model) { if ($model) {
$model->checkSignedInItem(); $model->checkSignedInItem();
...@@ -2549,7 +2561,7 @@ class ptyMysql ...@@ -2549,7 +2561,7 @@ class ptyMysql
* ``` * ```
* *
*/ */
function addWhereEqual($k, $v, bool $enabled = true, string $tableName = null) function addWhereEqual($k, $v, bool $enabled = true, ?string $tableName = null)
{ {
if (isset($tableName)) $tableName = "`$tableName`."; if (isset($tableName)) $tableName = "`$tableName`.";
if (!isset($k) || $k == "") if (!isset($k) || $k == "")
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
<? <?php
namespace platyFramework; namespace platyFramework;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?
header("HTTP/1.0 500 Internal Server Error");
?>
<?php
/**
* 로그인 / 로그아웃 등의 빈 영역에 사용됩니다
*/
?>
</body>
</html>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment