@things-factory/integration-base 9.0.36 → 9.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/connector/headless-connector.js +222 -18
- package/dist-server/engine/connector/headless-connector.js.map +1 -1
- package/dist-server/engine/task/headless-scrap.js +143 -41
- package/dist-server/engine/task/headless-scrap.js.map +1 -1
- package/dist-server/engine/task/utils/headless-request-with-recovery.d.ts +4 -0
- package/dist-server/engine/task/utils/headless-request-with-recovery.js +153 -36
- package/dist-server/engine/task/utils/headless-request-with-recovery.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const url_1 = require("url");
|
4
4
|
const task_registry_1 = require("../task-registry");
|
5
5
|
const connection_manager_1 = require("../connection-manager");
|
6
|
+
const headless_request_with_recovery_1 = require("./utils/headless-request-with-recovery");
|
6
7
|
async function HeadlessScrap(step, { logger, data, domain }) {
|
7
8
|
const { connection: connectionName, params: stepOptions } = step;
|
8
9
|
const { headers: requestHeaders, path, selectors = [], waitForSelectors, waitForTimeout, maxRetries = 2 } = stepOptions || {};
|
@@ -16,11 +17,22 @@ async function HeadlessScrap(step, { logger, data, domain }) {
|
|
16
17
|
...requestHeaders
|
17
18
|
};
|
18
19
|
let page = null;
|
20
|
+
let pageResource = null; // 리소스 추적 객체
|
19
21
|
let lastError = null;
|
20
22
|
// 재시도 로직 추가
|
21
23
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
22
24
|
try {
|
23
|
-
|
25
|
+
// 페이지 획득
|
26
|
+
const sessionResult = await acquireSessionPage();
|
27
|
+
// reAuthenticateSession의 반환 형태 확인
|
28
|
+
if (sessionResult && typeof sessionResult === 'object' && sessionResult.page) {
|
29
|
+
pageResource = sessionResult; // {page, browser, requiresManualRelease}
|
30
|
+
page = sessionResult.page;
|
31
|
+
}
|
32
|
+
else {
|
33
|
+
page = sessionResult;
|
34
|
+
pageResource = { page, requiresManualRelease: false };
|
35
|
+
}
|
24
36
|
page.on('console', async (msg) => {
|
25
37
|
console.log(`[browser ${msg.type()}] ${msg.text()}`);
|
26
38
|
});
|
@@ -35,26 +47,51 @@ async function HeadlessScrap(step, { logger, data, domain }) {
|
|
35
47
|
console.log(`- Post Data: ${request.postData()}`);
|
36
48
|
}
|
37
49
|
});
|
38
|
-
// 302 리디렉션 감지 추가
|
50
|
+
// 302 리디렉션 감지 추가 - 이벤트 핸들러는 비동기 리소스 정리 불가하므로 단순 로깅만
|
39
51
|
page.on('response', response => {
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
location.includes(
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
52
|
+
try {
|
53
|
+
if ([301, 302, 307, 308].includes(response.status())) {
|
54
|
+
const location = response.headers()['location'] || '';
|
55
|
+
if (location.includes(loginPagePath) ||
|
56
|
+
location.includes('/login') ||
|
57
|
+
location.includes('/signin') ||
|
58
|
+
location.includes('/auth')) {
|
59
|
+
logger.warn(`Login redirect detected during response: ${location}`);
|
60
|
+
// Note: Cannot throw from event handler - will be caught in main flow
|
61
|
+
}
|
48
62
|
}
|
49
63
|
}
|
64
|
+
catch (eventError) {
|
65
|
+
logger.error('Error in response event handler:', eventError);
|
66
|
+
}
|
50
67
|
});
|
51
|
-
//
|
68
|
+
// 세션 검증은 2번째 시도부터만 수행 (첫 번째는 새 페이지이므로 불필요)
|
52
69
|
if (attempt > 0 && validateSession) {
|
70
|
+
logger.debug(`Validating session for connection '${connectionName}' in HeadlessScrap (attempt: ${attempt + 1})`);
|
53
71
|
const isSessionValid = await validateSession(page);
|
54
72
|
if (!isSessionValid) {
|
55
|
-
logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`);
|
56
|
-
await
|
57
|
-
|
73
|
+
logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication (attempt: ${attempt + 1})`);
|
74
|
+
await (0, headless_request_with_recovery_1.safeReleasePageResource)(pageResource, releasePage, logger);
|
75
|
+
try {
|
76
|
+
const reauthResult = await reAuthenticateSession();
|
77
|
+
if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {
|
78
|
+
pageResource = reauthResult;
|
79
|
+
page = reauthResult.page;
|
80
|
+
}
|
81
|
+
else {
|
82
|
+
page = reauthResult;
|
83
|
+
pageResource = { page, requiresManualRelease: false };
|
84
|
+
}
|
85
|
+
logger.info(`Re-authentication successful for connection '${connectionName}' in HeadlessScrap`);
|
86
|
+
}
|
87
|
+
catch (reauthError) {
|
88
|
+
logger.error(`Re-authentication failed for connection '${connectionName}' in HeadlessScrap:`, reauthError);
|
89
|
+
// 재인증 실패 시 이번 시도는 실패로 처리하고 다음 시도로 넘어감
|
90
|
+
if (attempt === maxRetries) {
|
91
|
+
throw new Error(`Re-authentication failed in HeadlessScrap after ${maxRetries + 1} attempts: ${reauthError.message}`);
|
92
|
+
}
|
93
|
+
continue;
|
94
|
+
}
|
58
95
|
}
|
59
96
|
}
|
60
97
|
await page.setExtraHTTPHeaders(headers);
|
@@ -65,7 +102,44 @@ async function HeadlessScrap(step, { logger, data, domain }) {
|
|
65
102
|
currentUrl.includes('/login') ||
|
66
103
|
currentUrl.includes('/signin') ||
|
67
104
|
currentUrl.includes('/auth')) {
|
68
|
-
|
105
|
+
logger.warn(`Login redirect detected in HeadlessScrap: ${currentUrl}`);
|
106
|
+
if (attempt < maxRetries) {
|
107
|
+
logger.warn(`Attempting re-authentication due to login redirect (attempt: ${attempt + 1}/${maxRetries + 1})`);
|
108
|
+
await (0, headless_request_with_recovery_1.safeReleasePageResource)(pageResource, releasePage, logger);
|
109
|
+
try {
|
110
|
+
const reauthResult = await reAuthenticateSession();
|
111
|
+
if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {
|
112
|
+
pageResource = reauthResult;
|
113
|
+
page = reauthResult.page;
|
114
|
+
}
|
115
|
+
else {
|
116
|
+
page = reauthResult;
|
117
|
+
pageResource = { page, requiresManualRelease: false };
|
118
|
+
}
|
119
|
+
logger.info(`Re-authentication successful after login redirect in HeadlessScrap`);
|
120
|
+
// 재인증 후 다시 페이지 이동
|
121
|
+
await page.setExtraHTTPHeaders(headers);
|
122
|
+
await page.goto(new url_1.URL(path, endpoint), { waitUntil: 'networkidle2' });
|
123
|
+
// 재인증 후에도 리디렉션이 발생하는지 확인
|
124
|
+
const newUrl = page.url();
|
125
|
+
if (newUrl.includes(loginPagePath) ||
|
126
|
+
newUrl.includes('/login') ||
|
127
|
+
newUrl.includes('/signin') ||
|
128
|
+
newUrl.includes('/auth')) {
|
129
|
+
throw new Error(`Still redirected to login after re-authentication: ${newUrl}`);
|
130
|
+
}
|
131
|
+
}
|
132
|
+
catch (reauthError) {
|
133
|
+
logger.error(`Re-authentication failed after login redirect in HeadlessScrap:`, reauthError);
|
134
|
+
if (attempt === maxRetries) {
|
135
|
+
throw new Error(`Re-authentication failed after login redirect: ${reauthError.message}`);
|
136
|
+
}
|
137
|
+
continue;
|
138
|
+
}
|
139
|
+
}
|
140
|
+
else {
|
141
|
+
throw new Error(`Login redirect after ${maxRetries + 1} attempts: ${currentUrl}`);
|
142
|
+
}
|
69
143
|
}
|
70
144
|
// waitForSelectors, waitForTimeout 처리 추가
|
71
145
|
if (waitForSelectors) {
|
@@ -88,25 +162,36 @@ async function HeadlessScrap(step, { logger, data, domain }) {
|
|
88
162
|
await page.waitForTimeout(Number(waitForTimeout));
|
89
163
|
}
|
90
164
|
const result = {};
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
element
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
165
|
+
// DOM 요소 추출 - 실패 시에도 안전한 처리
|
166
|
+
try {
|
167
|
+
for (const selector of selectors) {
|
168
|
+
const { text, value } = selector;
|
169
|
+
try {
|
170
|
+
result[text] = await page.$$eval(value, elements => {
|
171
|
+
return elements.map(element => {
|
172
|
+
if (element instanceof HTMLInputElement ||
|
173
|
+
element instanceof HTMLTextAreaElement ||
|
174
|
+
element instanceof HTMLSelectElement) {
|
175
|
+
return element.value;
|
176
|
+
}
|
177
|
+
else {
|
178
|
+
return element.textContent?.trim();
|
179
|
+
}
|
180
|
+
});
|
181
|
+
});
|
182
|
+
}
|
183
|
+
catch (selectorError) {
|
184
|
+
logger.warn(`Failed to extract data for selector '${value}' (${text}):`, selectorError);
|
185
|
+
result[text] = []; // 빈 배열로 설정하여 스크래핑 계속 진행
|
186
|
+
}
|
187
|
+
}
|
105
188
|
}
|
106
|
-
|
107
|
-
|
108
|
-
|
189
|
+
catch (domError) {
|
190
|
+
logger.error(`DOM extraction failed:`, domError);
|
191
|
+
throw domError; // 전체 DOM 추출 실패 시 에러 발생
|
109
192
|
}
|
193
|
+
// 성공시 페이지 릴리즈 후 결과 반환
|
194
|
+
await (0, headless_request_with_recovery_1.safeReleasePageResource)(pageResource, releasePage, logger);
|
110
195
|
return {
|
111
196
|
data: result
|
112
197
|
};
|
@@ -114,22 +199,39 @@ async function HeadlessScrap(step, { logger, data, domain }) {
|
|
114
199
|
catch (error) {
|
115
200
|
lastError = error;
|
116
201
|
logger.error(`HeadlessScrap attempt ${attempt + 1} failed:`, error);
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
// 로그인 관련 에러나 복구 가능한 에러가 아니거나 마지막 재시도면 에러 발생
|
202
|
+
await (0, headless_request_with_recovery_1.safeReleasePageResource)(pageResource, releasePage, logger);
|
203
|
+
page = null;
|
204
|
+
pageResource = null;
|
205
|
+
// 에러 분류 및 복구 가능성 판단 (HeadlessPost와 동일한 로직)
|
122
206
|
const errorMessage = error.message?.toLowerCase() || '';
|
123
|
-
const isRecoverableError = errorMessage.includes('login') ||
|
207
|
+
const isRecoverableError = (errorMessage.includes('login') ||
|
208
|
+
errorMessage.includes('redirect') ||
|
209
|
+
errorMessage.includes('unauthorized') ||
|
210
|
+
errorMessage.includes('forbidden') ||
|
211
|
+
errorMessage.includes('session') ||
|
212
|
+
errorMessage.includes('timeout') ||
|
213
|
+
errorMessage.includes('network') ||
|
214
|
+
errorMessage.includes('connection') ||
|
215
|
+
errorMessage.includes('failed to fetch') ||
|
216
|
+
errorMessage.includes('navigation') ||
|
217
|
+
errorMessage.includes('authentication') ||
|
218
|
+
error.name === 'TimeoutError');
|
219
|
+
// 복구 불가능한 에러이거나 최대 재시도 횟수에 도달한 경우
|
124
220
|
if (!isRecoverableError || attempt === maxRetries) {
|
221
|
+
if (isRecoverableError && attempt === maxRetries) {
|
222
|
+
logger.error(`HeadlessScrap: Recoverable error but max retries (${maxRetries + 1}) reached: ${error.message}`);
|
223
|
+
}
|
224
|
+
else {
|
225
|
+
logger.error(`HeadlessScrap: Non-recoverable error: ${error.message}`);
|
226
|
+
}
|
125
227
|
throw error;
|
126
228
|
}
|
127
229
|
logger.info(`Retrying HeadlessScrap... (${attempt + 2}/${maxRetries + 1})`);
|
128
230
|
}
|
129
231
|
}
|
130
|
-
// 모든 재시도가 실패한 경우
|
131
|
-
if (
|
132
|
-
await
|
232
|
+
// 모든 재시도가 실패한 경우 - 혹시 남은 리소스가 있으면 정리
|
233
|
+
if (pageResource) {
|
234
|
+
await (0, headless_request_with_recovery_1.safeReleasePageResource)(pageResource, releasePage, logger);
|
133
235
|
}
|
134
236
|
throw lastError || new Error('HeadlessScrap failed after all retry attempts');
|
135
237
|
}
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"headless-scrap.js","sourceRoot":"","sources":["../../../server/engine/task/headless-scrap.ts"],"names":[],"mappings":"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","sourcesContent":["import { URL } from 'url'\n\nimport { TaskRegistry } from '../task-registry'\nimport { ConnectionManager } from '../connection-manager'\n\nasync function HeadlessScrap(step, { logger, data, domain }) {\n const { connection: connectionName, params: stepOptions } = step\n const { headers: requestHeaders, path, selectors = [], waitForSelectors, waitForTimeout, maxRetries = 2 } = stepOptions || {}\n\n const connection = await ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n\n if (!connection) {\n throw new Error(`Connection '${connectionName}' is not established.`)\n }\n\n const { endpoint, params: connectionParams, acquireSessionPage, releasePage, validateSession, reAuthenticateSession } = connection\n const loginPagePath = connectionParams?.loginPagePath || '/login'\n\n const headers = {\n ...requestHeaders\n }\n\n let page = null\n let lastError = null\n\n // 재시도 로직 추가\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n page = await acquireSessionPage()\n\n page.on('console', async msg => {\n console.log(`[browser ${msg.type()}] ${msg.text()}`)\n })\n\n page.on('requestfailed', request => {\n console.log('Request failed:')\n console.log(`- URL: ${request.url()}`)\n console.log(`- Method: ${request.method()}`)\n console.log(`- Failure Text: ${request.failure()?.errorText}`)\n console.log(`- Headers:`, request.headers())\n\n // POST 데이터 (필요한 경우)\n if (request.postData()) {\n console.log(`- Post Data: ${request.postData()}`)\n }\n })\n\n // 302 리디렉션 감지 추가\n page.on('response', response => {\n if ([301, 302, 307, 308].includes(response.status())) {\n const location = response.headers()['location'] || ''\n if (location.includes(loginPagePath) || \n location.includes('/login') || \n location.includes('/signin') || \n location.includes('/auth')) {\n logger.warn(`Login redirect detected: ${location}`)\n throw new Error(`Redirected to login page: ${location}`)\n }\n }\n })\n\n // 첫 번째 시도가 아니면 세션 검증\n if (attempt > 0 && validateSession) {\n const isSessionValid = await validateSession(page)\n if (!isSessionValid) {\n logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`)\n await releasePage(page)\n page = await reAuthenticateSession()\n }\n }\n\n await page.setExtraHTTPHeaders(headers)\n await page.goto(new URL(path, endpoint), { waitUntil: 'networkidle2' })\n\n // 페이지 로드 후 로그인 페이지로 리디렉션되었는지 확인\n const currentUrl = page.url()\n if (currentUrl.includes(loginPagePath) || \n currentUrl.includes('/login') || \n currentUrl.includes('/signin') || \n currentUrl.includes('/auth')) {\n throw new Error(`Page redirected to login: ${currentUrl}`)\n }\n\n // waitForSelectors, waitForTimeout 처리 추가\n if (waitForSelectors) {\n try {\n await page.waitForFunction(\n selectorsString => {\n const selectors = selectorsString.split(',').map(s => s.trim())\n return selectors.every(selector => {\n const el = document.querySelector(selector)\n return el && el.textContent && el.textContent.trim().length > 0\n })\n },\n { timeout: waitForTimeout ? Number(waitForTimeout) : 10000 },\n waitForSelectors // 콤마로 구분된 셀렉터 문자열\n )\n } catch (e) {\n logger.error(`waitForSelectors(${waitForSelectors}) 값이 모두 채워지지 않음:`, e)\n throw e\n }\n } else if (waitForTimeout) {\n await page.waitForTimeout(Number(waitForTimeout))\n }\n\n const result = {}\n\n for (const selector of selectors) {\n const { text, value } = selector\n result[text] = await page.$$eval(value, elements => {\n return elements.map(element => {\n if (\n element instanceof HTMLInputElement ||\n element instanceof HTMLTextAreaElement ||\n element instanceof HTMLSelectElement\n ) {\n return element.value\n } else {\n return element.textContent?.trim()\n }\n })\n })\n }\n\n // 성공시 페이지 릴리즈 후 결과 반환\n if (page) {\n await releasePage(page)\n }\n\n return {\n data: result\n }\n\n } catch (error) {\n lastError = error\n logger.error(`HeadlessScrap attempt ${attempt + 1} failed:`, error)\n\n if (page) {\n await releasePage(page)\n page = null\n }\n\n // 로그인 관련 에러나 복구 가능한 에러가 아니거나 마지막 재시도면 에러 발생\n const errorMessage = error.message?.toLowerCase() || ''\n const isRecoverableError = errorMessage.includes('login') || errorMessage.includes('redirect') || errorMessage.includes('unauthorized') || errorMessage.includes('forbidden') || errorMessage.includes('session')\n\n if (!isRecoverableError || attempt === maxRetries) {\n throw error\n }\n\n logger.info(`Retrying HeadlessScrap... (${attempt + 2}/${maxRetries + 1})`)\n }\n }\n\n // 모든 재시도가 실패한 경우\n if (page) {\n await releasePage(page)\n }\n throw lastError || new Error('HeadlessScrap failed after all retry attempts')\n}\n\nHeadlessScrap.parameterSpec = [\n {\n type: 'string',\n name: 'path',\n label: 'path'\n },\n {\n type: 'http-headers',\n name: 'headers',\n label: 'headers'\n },\n {\n type: 'options',\n name: 'selectors',\n label: 'selectors'\n },\n {\n type: 'string',\n name: 'waitForSelectors',\n label: 'wait-for-selectors'\n },\n {\n type: 'string',\n name: 'waitForTimeout',\n label: 'wait-for-timeout'\n },\n {\n type: 'number',\n name: 'maxRetries',\n label: 'maximum-retries',\n value: 2\n }\n]\n\nHeadlessScrap.help = 'integration/task/headless-scrap'\n\nTaskRegistry.registerTaskHandler('headless-scrap', HeadlessScrap)\n"]}
|
1
|
+
{"version":3,"file":"headless-scrap.js","sourceRoot":"","sources":["../../../server/engine/task/headless-scrap.ts"],"names":[],"mappings":";;AAAA,6BAAyB;AAEzB,oDAA+C;AAC/C,8DAAyD;AACzD,2FAAgF;AAEhF,KAAK,UAAU,aAAa,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;IACzD,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAChE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,GAAG,CAAC,EAAE,GAAG,WAAW,IAAI,EAAE,CAAA;IAE7H,MAAM,UAAU,GAAG,MAAM,sCAAiB,CAAC,2BAA2B,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAE9F,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,eAAe,cAAc,uBAAuB,CAAC,CAAA;IACvE,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,WAAW,EAAE,eAAe,EAAE,qBAAqB,EAAE,GAAG,UAAU,CAAA;IAClI,MAAM,aAAa,GAAG,gBAAgB,EAAE,aAAa,IAAI,QAAQ,CAAA;IAEjE,MAAM,OAAO,GAAG;QACd,GAAG,cAAc;KAClB,CAAA;IAED,IAAI,IAAI,GAAG,IAAI,CAAA;IACf,IAAI,YAAY,GAAG,IAAI,CAAA,CAAE,YAAY;IACrC,IAAI,SAAS,GAAG,IAAI,CAAA;IAEpB,YAAY;IACZ,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,SAAS;YACT,MAAM,aAAa,GAAG,MAAM,kBAAkB,EAAE,CAAA;YAEhD,kCAAkC;YAClC,IAAI,aAAa,IAAI,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,EAAE,CAAC;gBAC7E,YAAY,GAAG,aAAa,CAAA,CAAE,yCAAyC;gBACvE,IAAI,GAAG,aAAa,CAAC,IAAI,CAAA;YAC3B,CAAC;iBAAM,CAAC;gBACN,IAAI,GAAG,aAAa,CAAA;gBACpB,YAAY,GAAG,EAAE,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,CAAA;YACvD,CAAC;YAED,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAC,GAAG,EAAC,EAAE;gBAC7B,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;YACtD,CAAC,CAAC,CAAA;YAEF,IAAI,CAAC,EAAE,CAAC,eAAe,EAAE,OAAO,CAAC,EAAE;gBACjC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;gBAC9B,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;gBACtC,OAAO,CAAC,GAAG,CAAC,aAAa,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;gBAC5C,OAAO,CAAC,GAAG,CAAC,mBAAmB,OAAO,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,CAAC,CAAA;gBAC9D,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;gBAE5C,oBAAoB;gBACpB,IAAI,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;oBACvB,OAAO,CAAC,GAAG,CAAC,gBAAgB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;gBACnD,CAAC;YACH,CAAC,CAAC,CAAA;YAEF,oDAAoD;YACpD,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE;gBAC7B,IAAI,CAAC;oBACH,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;wBACrD,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,CAAA;wBACrD,IAAI,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC;4BAChC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC;4BAC3B,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC;4BAC5B,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;4BAC/B,MAAM,CAAC,IAAI,CAAC,4CAA4C,QAAQ,EAAE,CAAC,CAAA;4BACnE,sEAAsE;wBACxE,CAAC;oBACH,CAAC;gBACH,CAAC;gBAAC,OAAO,UAAU,EAAE,CAAC;oBACpB,MAAM,CAAC,KAAK,CAAC,kCAAkC,EAAE,UAAU,CAAC,CAAA;gBAC9D,CAAC;YACH,CAAC,CAAC,CAAA;YAEF,2CAA2C;YAC3C,IAAI,OAAO,GAAG,CAAC,IAAI,eAAe,EAAE,CAAC;gBACnC,MAAM,CAAC,KAAK,CAAC,sCAAsC,cAAc,gCAAgC,OAAO,GAAG,CAAC,GAAG,CAAC,CAAA;gBAChH,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAA;gBAClD,IAAI,CAAC,cAAc,EAAE,CAAC;oBACpB,MAAM,CAAC,IAAI,CAAC,mCAAmC,cAAc,6CAA6C,OAAO,GAAG,CAAC,GAAG,CAAC,CAAA;oBACzH,MAAM,IAAA,wDAAuB,EAAC,YAAY,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;oBAEhE,IAAI,CAAC;wBACH,MAAM,YAAY,GAAG,MAAM,qBAAqB,EAAE,CAAA;wBAClD,IAAI,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,EAAE,CAAC;4BAC1E,YAAY,GAAG,YAAY,CAAA;4BAC3B,IAAI,GAAG,YAAY,CAAC,IAAI,CAAA;wBAC1B,CAAC;6BAAM,CAAC;4BACN,IAAI,GAAG,YAAY,CAAA;4BACnB,YAAY,GAAG,EAAE,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,CAAA;wBACvD,CAAC;wBACD,MAAM,CAAC,IAAI,CAAC,gDAAgD,cAAc,oBAAoB,CAAC,CAAA;oBACjG,CAAC;oBAAC,OAAO,WAAW,EAAE,CAAC;wBACrB,MAAM,CAAC,KAAK,CAAC,4CAA4C,cAAc,qBAAqB,EAAE,WAAW,CAAC,CAAA;wBAC1G,sCAAsC;wBACtC,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;4BAC3B,MAAM,IAAI,KAAK,CAAC,mDAAmD,UAAU,GAAG,CAAC,cAAc,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;wBACvH,CAAC;wBACD,SAAQ;oBACV,CAAC;gBACH,CAAC;YACH,CAAC;YAED,MAAM,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,SAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAA;YAEvE,gCAAgC;YAChC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAC7B,IAAI,UAAU,CAAC,QAAQ,CAAC,aAAa,CAAC;gBAClC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAC7B,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAC9B,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBACjC,MAAM,CAAC,IAAI,CAAC,6CAA6C,UAAU,EAAE,CAAC,CAAA;gBAEtE,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;oBACzB,MAAM,CAAC,IAAI,CAAC,gEAAgE,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAAC,CAAA;oBAC7G,MAAM,IAAA,wDAAuB,EAAC,YAAY,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;oBAEhE,IAAI,CAAC;wBACH,MAAM,YAAY,GAAG,MAAM,qBAAqB,EAAE,CAAA;wBAClD,IAAI,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,EAAE,CAAC;4BAC1E,YAAY,GAAG,YAAY,CAAA;4BAC3B,IAAI,GAAG,YAAY,CAAC,IAAI,CAAA;wBAC1B,CAAC;6BAAM,CAAC;4BACN,IAAI,GAAG,YAAY,CAAA;4BACnB,YAAY,GAAG,EAAE,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,CAAA;wBACvD,CAAC;wBACD,MAAM,CAAC,IAAI,CAAC,oEAAoE,CAAC,CAAA;wBAEjF,kBAAkB;wBAClB,MAAM,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAA;wBACvC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,SAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAA;wBAEvE,yBAAyB;wBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;wBACzB,IAAI,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;4BAC9B,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;4BACzB,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;4BAC1B,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;4BAC7B,MAAM,IAAI,KAAK,CAAC,sDAAsD,MAAM,EAAE,CAAC,CAAA;wBACjF,CAAC;oBACH,CAAC;oBAAC,OAAO,WAAW,EAAE,CAAC;wBACrB,MAAM,CAAC,KAAK,CAAC,iEAAiE,EAAE,WAAW,CAAC,CAAA;wBAC5F,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;4BAC3B,MAAM,IAAI,KAAK,CAAC,kDAAkD,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;wBAC1F,CAAC;wBACD,SAAQ;oBACV,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,KAAK,CAAC,wBAAwB,UAAU,GAAG,CAAC,cAAc,UAAU,EAAE,CAAC,CAAA;gBACnF,CAAC;YACH,CAAC;YAED,yCAAyC;YACzC,IAAI,gBAAgB,EAAE,CAAC;gBACrB,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,eAAe,CACxB,eAAe,CAAC,EAAE;wBAChB,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;wBAC/D,OAAO,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE;4BAChC,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;4BAC3C,OAAO,EAAE,IAAI,EAAE,CAAC,WAAW,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAA;wBACjE,CAAC,CAAC,CAAA;oBACJ,CAAC,EACD,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAC5D,gBAAgB,CAAC,kBAAkB;qBACpC,CAAA;gBACH,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,MAAM,CAAC,KAAK,CAAC,oBAAoB,gBAAgB,kBAAkB,EAAE,CAAC,CAAC,CAAA;oBACvE,MAAM,CAAC,CAAA;gBACT,CAAC;YACH,CAAC;iBAAM,IAAI,cAAc,EAAE,CAAC;gBAC1B,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAA;YACnD,CAAC;YAED,MAAM,MAAM,GAAG,EAAE,CAAA;YAEjB,4BAA4B;YAC5B,IAAI,CAAC;gBACH,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;oBACjC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,QAAQ,CAAA;oBAEhC,IAAI,CAAC;wBACH,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE;4BACjD,OAAO,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE;gCAC5B,IACE,OAAO,YAAY,gBAAgB;oCACnC,OAAO,YAAY,mBAAmB;oCACtC,OAAO,YAAY,iBAAiB,EACpC,CAAC;oCACD,OAAO,OAAO,CAAC,KAAK,CAAA;gCACtB,CAAC;qCAAM,CAAC;oCACN,OAAO,OAAO,CAAC,WAAW,EAAE,IAAI,EAAE,CAAA;gCACpC,CAAC;4BACH,CAAC,CAAC,CAAA;wBACJ,CAAC,CAAC,CAAA;oBACJ,CAAC;oBAAC,OAAO,aAAa,EAAE,CAAC;wBACvB,MAAM,CAAC,IAAI,CAAC,wCAAwC,KAAK,MAAM,IAAI,IAAI,EAAE,aAAa,CAAC,CAAA;wBACvF,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAA,CAAE,wBAAwB;oBAC7C,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,QAAQ,EAAE,CAAC;gBAClB,MAAM,CAAC,KAAK,CAAC,wBAAwB,EAAE,QAAQ,CAAC,CAAA;gBAChD,MAAM,QAAQ,CAAA,CAAE,uBAAuB;YACzC,CAAC;YAED,sBAAsB;YACtB,MAAM,IAAA,wDAAuB,EAAC,YAAY,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;YAEhE,OAAO;gBACL,IAAI,EAAE,MAAM;aACb,CAAA;QAEH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,SAAS,GAAG,KAAK,CAAA;YACjB,MAAM,CAAC,KAAK,CAAC,yBAAyB,OAAO,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;YAEnE,MAAM,IAAA,wDAAuB,EAAC,YAAY,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;YAChE,IAAI,GAAG,IAAI,CAAA;YACX,YAAY,GAAG,IAAI,CAAA;YAEnB,2CAA2C;YAC3C,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;YACvD,MAAM,kBAAkB,GAAG,CACzB,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAC9B,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC;gBACjC,YAAY,CAAC,QAAQ,CAAC,cAAc,CAAC;gBACrC,YAAY,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAClC,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAChC,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAChC,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAChC,YAAY,CAAC,QAAQ,CAAC,YAAY,CAAC;gBACnC,YAAY,CAAC,QAAQ,CAAC,iBAAiB,CAAC;gBACxC,YAAY,CAAC,QAAQ,CAAC,YAAY,CAAC;gBACnC,YAAY,CAAC,QAAQ,CAAC,gBAAgB,CAAC;gBACvC,KAAK,CAAC,IAAI,KAAK,cAAc,CAC9B,CAAA;YAED,kCAAkC;YAClC,IAAI,CAAC,kBAAkB,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;gBAClD,IAAI,kBAAkB,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;oBACjD,MAAM,CAAC,KAAK,CAAC,qDAAqD,UAAU,GAAG,CAAC,cAAc,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;gBAChH,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,KAAK,CAAC,yCAAyC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;gBACxE,CAAC;gBACD,MAAM,KAAK,CAAA;YACb,CAAC;YAED,MAAM,CAAC,IAAI,CAAC,8BAA8B,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7E,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAA,wDAAuB,EAAC,YAAY,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;IAClE,CAAC;IACD,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;AAC/E,CAAC;AAED,aAAa,CAAC,aAAa,GAAG;IAC5B;QACE,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,MAAM;KACd;IACD;QACE,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,SAAS;KACjB;IACD;QACE,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,WAAW;KACnB;IACD;QACE,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,kBAAkB;QACxB,KAAK,EAAE,oBAAoB;KAC5B;IACD;QACE,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,kBAAkB;KAC1B;IACD;QACE,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,YAAY;QAClB,KAAK,EAAE,iBAAiB;QACxB,KAAK,EAAE,CAAC;KACT;CACF,CAAA;AAED,aAAa,CAAC,IAAI,GAAG,iCAAiC,CAAA;AAEtD,4BAAY,CAAC,mBAAmB,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAA","sourcesContent":["import { URL } from 'url'\n\nimport { TaskRegistry } from '../task-registry'\nimport { ConnectionManager } from '../connection-manager'\nimport { safeReleasePageResource } from './utils/headless-request-with-recovery'\n\nasync function HeadlessScrap(step, { logger, data, domain }) {\n const { connection: connectionName, params: stepOptions } = step\n const { headers: requestHeaders, path, selectors = [], waitForSelectors, waitForTimeout, maxRetries = 2 } = stepOptions || {}\n\n const connection = await ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n\n if (!connection) {\n throw new Error(`Connection '${connectionName}' is not established.`)\n }\n\n const { endpoint, params: connectionParams, acquireSessionPage, releasePage, validateSession, reAuthenticateSession } = connection\n const loginPagePath = connectionParams?.loginPagePath || '/login'\n\n const headers = {\n ...requestHeaders\n }\n\n let page = null\n let pageResource = null // 리소스 추적 객체\n let lastError = null\n\n // 재시도 로직 추가\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n // 페이지 획득\n const sessionResult = await acquireSessionPage()\n \n // reAuthenticateSession의 반환 형태 확인\n if (sessionResult && typeof sessionResult === 'object' && sessionResult.page) {\n pageResource = sessionResult // {page, browser, requiresManualRelease}\n page = sessionResult.page\n } else {\n page = sessionResult\n pageResource = { page, requiresManualRelease: false }\n }\n\n page.on('console', async msg => {\n console.log(`[browser ${msg.type()}] ${msg.text()}`)\n })\n\n page.on('requestfailed', request => {\n console.log('Request failed:')\n console.log(`- URL: ${request.url()}`)\n console.log(`- Method: ${request.method()}`)\n console.log(`- Failure Text: ${request.failure()?.errorText}`)\n console.log(`- Headers:`, request.headers())\n\n // POST 데이터 (필요한 경우)\n if (request.postData()) {\n console.log(`- Post Data: ${request.postData()}`)\n }\n })\n\n // 302 리디렉션 감지 추가 - 이벤트 핸들러는 비동기 리소스 정리 불가하므로 단순 로깅만\n page.on('response', response => {\n try {\n if ([301, 302, 307, 308].includes(response.status())) {\n const location = response.headers()['location'] || ''\n if (location.includes(loginPagePath) || \n location.includes('/login') || \n location.includes('/signin') || \n location.includes('/auth')) {\n logger.warn(`Login redirect detected during response: ${location}`)\n // Note: Cannot throw from event handler - will be caught in main flow\n }\n }\n } catch (eventError) {\n logger.error('Error in response event handler:', eventError)\n }\n })\n\n // 세션 검증은 2번째 시도부터만 수행 (첫 번째는 새 페이지이므로 불필요)\n if (attempt > 0 && validateSession) {\n logger.debug(`Validating session for connection '${connectionName}' in HeadlessScrap (attempt: ${attempt + 1})`)\n const isSessionValid = await validateSession(page)\n if (!isSessionValid) {\n logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication (attempt: ${attempt + 1})`)\n await safeReleasePageResource(pageResource, releasePage, logger)\n \n try {\n const reauthResult = await reAuthenticateSession()\n if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {\n pageResource = reauthResult\n page = reauthResult.page\n } else {\n page = reauthResult\n pageResource = { page, requiresManualRelease: false }\n }\n logger.info(`Re-authentication successful for connection '${connectionName}' in HeadlessScrap`)\n } catch (reauthError) {\n logger.error(`Re-authentication failed for connection '${connectionName}' in HeadlessScrap:`, reauthError)\n // 재인증 실패 시 이번 시도는 실패로 처리하고 다음 시도로 넘어감\n if (attempt === maxRetries) {\n throw new Error(`Re-authentication failed in HeadlessScrap after ${maxRetries + 1} attempts: ${reauthError.message}`)\n }\n continue\n }\n }\n }\n\n await page.setExtraHTTPHeaders(headers)\n await page.goto(new URL(path, endpoint), { waitUntil: 'networkidle2' })\n\n // 페이지 로드 후 로그인 페이지로 리디렉션되었는지 확인\n const currentUrl = page.url()\n if (currentUrl.includes(loginPagePath) || \n currentUrl.includes('/login') || \n currentUrl.includes('/signin') || \n currentUrl.includes('/auth')) {\n logger.warn(`Login redirect detected in HeadlessScrap: ${currentUrl}`)\n \n if (attempt < maxRetries) {\n logger.warn(`Attempting re-authentication due to login redirect (attempt: ${attempt + 1}/${maxRetries + 1})`)\n await safeReleasePageResource(pageResource, releasePage, logger)\n \n try {\n const reauthResult = await reAuthenticateSession()\n if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {\n pageResource = reauthResult\n page = reauthResult.page\n } else {\n page = reauthResult\n pageResource = { page, requiresManualRelease: false }\n }\n logger.info(`Re-authentication successful after login redirect in HeadlessScrap`)\n \n // 재인증 후 다시 페이지 이동\n await page.setExtraHTTPHeaders(headers)\n await page.goto(new URL(path, endpoint), { waitUntil: 'networkidle2' })\n \n // 재인증 후에도 리디렉션이 발생하는지 확인\n const newUrl = page.url()\n if (newUrl.includes(loginPagePath) || \n newUrl.includes('/login') || \n newUrl.includes('/signin') || \n newUrl.includes('/auth')) {\n throw new Error(`Still redirected to login after re-authentication: ${newUrl}`)\n }\n } catch (reauthError) {\n logger.error(`Re-authentication failed after login redirect in HeadlessScrap:`, reauthError)\n if (attempt === maxRetries) {\n throw new Error(`Re-authentication failed after login redirect: ${reauthError.message}`)\n }\n continue\n }\n } else {\n throw new Error(`Login redirect after ${maxRetries + 1} attempts: ${currentUrl}`)\n }\n }\n\n // waitForSelectors, waitForTimeout 처리 추가\n if (waitForSelectors) {\n try {\n await page.waitForFunction(\n selectorsString => {\n const selectors = selectorsString.split(',').map(s => s.trim())\n return selectors.every(selector => {\n const el = document.querySelector(selector)\n return el && el.textContent && el.textContent.trim().length > 0\n })\n },\n { timeout: waitForTimeout ? Number(waitForTimeout) : 10000 },\n waitForSelectors // 콤마로 구분된 셀렉터 문자열\n )\n } catch (e) {\n logger.error(`waitForSelectors(${waitForSelectors}) 값이 모두 채워지지 않음:`, e)\n throw e\n }\n } else if (waitForTimeout) {\n await page.waitForTimeout(Number(waitForTimeout))\n }\n\n const result = {}\n\n // DOM 요소 추출 - 실패 시에도 안전한 처리\n try {\n for (const selector of selectors) {\n const { text, value } = selector\n \n try {\n result[text] = await page.$$eval(value, elements => {\n return elements.map(element => {\n if (\n element instanceof HTMLInputElement ||\n element instanceof HTMLTextAreaElement ||\n element instanceof HTMLSelectElement\n ) {\n return element.value\n } else {\n return element.textContent?.trim()\n }\n })\n })\n } catch (selectorError) {\n logger.warn(`Failed to extract data for selector '${value}' (${text}):`, selectorError)\n result[text] = [] // 빈 배열로 설정하여 스크래핑 계속 진행\n }\n }\n } catch (domError) {\n logger.error(`DOM extraction failed:`, domError)\n throw domError // 전체 DOM 추출 실패 시 에러 발생\n }\n\n // 성공시 페이지 릴리즈 후 결과 반환\n await safeReleasePageResource(pageResource, releasePage, logger)\n\n return {\n data: result\n }\n\n } catch (error) {\n lastError = error\n logger.error(`HeadlessScrap attempt ${attempt + 1} failed:`, error)\n\n await safeReleasePageResource(pageResource, releasePage, logger)\n page = null\n pageResource = null\n\n // 에러 분류 및 복구 가능성 판단 (HeadlessPost와 동일한 로직)\n const errorMessage = error.message?.toLowerCase() || ''\n const isRecoverableError = (\n errorMessage.includes('login') || \n errorMessage.includes('redirect') || \n errorMessage.includes('unauthorized') || \n errorMessage.includes('forbidden') || \n errorMessage.includes('session') ||\n errorMessage.includes('timeout') ||\n errorMessage.includes('network') ||\n errorMessage.includes('connection') ||\n errorMessage.includes('failed to fetch') ||\n errorMessage.includes('navigation') ||\n errorMessage.includes('authentication') ||\n error.name === 'TimeoutError'\n )\n\n // 복구 불가능한 에러이거나 최대 재시도 횟수에 도달한 경우\n if (!isRecoverableError || attempt === maxRetries) {\n if (isRecoverableError && attempt === maxRetries) {\n logger.error(`HeadlessScrap: Recoverable error but max retries (${maxRetries + 1}) reached: ${error.message}`)\n } else {\n logger.error(`HeadlessScrap: Non-recoverable error: ${error.message}`)\n }\n throw error\n }\n\n logger.info(`Retrying HeadlessScrap... (${attempt + 2}/${maxRetries + 1})`)\n }\n }\n\n // 모든 재시도가 실패한 경우 - 혹시 남은 리소스가 있으면 정리\n if (pageResource) {\n await safeReleasePageResource(pageResource, releasePage, logger)\n }\n throw lastError || new Error('HeadlessScrap failed after all retry attempts')\n}\n\nHeadlessScrap.parameterSpec = [\n {\n type: 'string',\n name: 'path',\n label: 'path'\n },\n {\n type: 'http-headers',\n name: 'headers',\n label: 'headers'\n },\n {\n type: 'options',\n name: 'selectors',\n label: 'selectors'\n },\n {\n type: 'string',\n name: 'waitForSelectors',\n label: 'wait-for-selectors'\n },\n {\n type: 'string',\n name: 'waitForTimeout',\n label: 'wait-for-timeout'\n },\n {\n type: 'number',\n name: 'maxRetries',\n label: 'maximum-retries',\n value: 2\n }\n]\n\nHeadlessScrap.help = 'integration/task/headless-scrap'\n\nTaskRegistry.registerTaskHandler('headless-scrap', HeadlessScrap)\n"]}
|
@@ -16,3 +16,7 @@ export declare function executeHeadlessRequestWithRecovery(connectionName: strin
|
|
16
16
|
data: any;
|
17
17
|
domain: any;
|
18
18
|
}): Promise<any>;
|
19
|
+
/**
|
20
|
+
* Safely release page resource with comprehensive error handling
|
21
|
+
*/
|
22
|
+
export declare function safeReleasePageResource(pageResource: any, releasePage: Function, logger: any): Promise<void>;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.executeHeadlessRequestWithRecovery = executeHeadlessRequestWithRecovery;
|
4
|
+
exports.safeReleasePageResource = safeReleasePageResource;
|
4
5
|
const utils_1 = require("@things-factory/utils");
|
5
6
|
const connection_manager_1 = require("../../connection-manager");
|
6
7
|
/**
|
@@ -21,12 +22,22 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
21
22
|
const { endpoint, params: connectionParams, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection;
|
22
23
|
const loginPagePath = connectionParams?.loginPagePath || '/login';
|
23
24
|
let page = null;
|
25
|
+
let pageResource = null; // 리소스 추적 객체
|
24
26
|
let lastError = null;
|
25
27
|
// 재시도 로직
|
26
28
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
27
29
|
try {
|
28
30
|
// 페이지 획득
|
29
|
-
|
31
|
+
const sessionResult = await acquireSessionPage();
|
32
|
+
// reAuthenticateSession의 반환 형태 확인
|
33
|
+
if (sessionResult && typeof sessionResult === 'object' && sessionResult.page) {
|
34
|
+
pageResource = sessionResult; // {page, browser, requiresManualRelease}
|
35
|
+
page = sessionResult.page;
|
36
|
+
}
|
37
|
+
else {
|
38
|
+
page = sessionResult;
|
39
|
+
pageResource = { page, requiresManualRelease: false };
|
40
|
+
}
|
30
41
|
// 페이지가 올바른 도메인에 있는지 확인
|
31
42
|
const currentUrl = page.url();
|
32
43
|
const targetDomain = new URL(endpoint).origin;
|
@@ -34,13 +45,34 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
34
45
|
logger.info(`Navigating to target domain: ${targetDomain}`);
|
35
46
|
await page.goto(targetDomain, { waitUntil: 'networkidle2' });
|
36
47
|
}
|
37
|
-
//
|
48
|
+
// 세션 검증은 2번째 시도부터만 수행 (첫 번째는 새 페이지이므로 불필요)
|
49
|
+
// 또는 이전 시도에서 세션 관련 에러가 발생한 경우에만 수행
|
38
50
|
if (attempt > 0) {
|
51
|
+
logger.debug(`Validating session for connection '${connectionName}' (attempt: ${attempt + 1})`);
|
39
52
|
const isSessionValid = await validateSession(page);
|
40
53
|
if (!isSessionValid) {
|
41
|
-
logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`);
|
42
|
-
await releasePage
|
43
|
-
|
54
|
+
logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication (attempt: ${attempt + 1})`);
|
55
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
56
|
+
try {
|
57
|
+
const reauthResult = await reAuthenticateSession();
|
58
|
+
if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {
|
59
|
+
pageResource = reauthResult;
|
60
|
+
page = reauthResult.page;
|
61
|
+
}
|
62
|
+
else {
|
63
|
+
page = reauthResult;
|
64
|
+
pageResource = { page, requiresManualRelease: false };
|
65
|
+
}
|
66
|
+
logger.info(`Re-authentication successful for connection '${connectionName}'`);
|
67
|
+
}
|
68
|
+
catch (reauthError) {
|
69
|
+
logger.error(`Re-authentication failed for connection '${connectionName}':`, reauthError);
|
70
|
+
// 재인증 실패 시 이번 시도는 실패로 처리하고 다음 시도로 넘어감
|
71
|
+
if (attempt === maxRetries) {
|
72
|
+
throw new Error(`Re-authentication failed after ${maxRetries + 1} attempts: ${reauthError.message}`);
|
73
|
+
}
|
74
|
+
continue;
|
75
|
+
}
|
44
76
|
}
|
45
77
|
}
|
46
78
|
// URL 구성
|
@@ -153,50 +185,77 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
153
185
|
if (response.networkError) {
|
154
186
|
if (attempt < maxRetries) {
|
155
187
|
logger.warn(`Network error detected: ${response.error}, retrying... (${attempt + 1}/${maxRetries + 1})`);
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
}
|
188
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
189
|
+
page = null;
|
190
|
+
pageResource = null;
|
160
191
|
continue;
|
161
192
|
}
|
162
193
|
else {
|
163
|
-
|
164
|
-
await releasePage(page);
|
165
|
-
}
|
194
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
166
195
|
throw new Error(`Network error after ${maxRetries + 1} attempts: ${response.error}`);
|
167
196
|
}
|
168
197
|
}
|
169
198
|
// 로그인 리디렉션 감지 처리
|
170
199
|
if (response.redirectedToLogin) {
|
171
200
|
if (attempt < maxRetries) {
|
172
|
-
logger.warn(`Login redirect detected: ${response.location}, re-
|
173
|
-
|
174
|
-
|
175
|
-
|
201
|
+
logger.warn(`Login redirect detected: ${response.location}, performing re-authentication... (${attempt + 1}/${maxRetries + 1})`);
|
202
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
203
|
+
// CRITICAL: 실제 재인증 수행
|
204
|
+
try {
|
205
|
+
const reauthResult = await reAuthenticateSession();
|
206
|
+
if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {
|
207
|
+
pageResource = reauthResult;
|
208
|
+
page = reauthResult.page;
|
209
|
+
}
|
210
|
+
else {
|
211
|
+
page = reauthResult;
|
212
|
+
pageResource = { page, requiresManualRelease: false };
|
213
|
+
}
|
214
|
+
logger.info(`Re-authentication successful after login redirect for connection '${connectionName}'`);
|
215
|
+
}
|
216
|
+
catch (reauthError) {
|
217
|
+
logger.error(`Re-authentication failed after login redirect for connection '${connectionName}':`, reauthError);
|
218
|
+
if (attempt === maxRetries) {
|
219
|
+
throw new Error(`Re-authentication failed after login redirect: ${reauthError.message}`);
|
220
|
+
}
|
221
|
+
continue;
|
176
222
|
}
|
177
223
|
continue;
|
178
224
|
}
|
179
225
|
else {
|
180
|
-
|
181
|
-
await releasePage(page);
|
182
|
-
}
|
226
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
183
227
|
throw new Error(`Login redirect after ${maxRetries + 1} attempts: ${response.error}`);
|
184
228
|
}
|
185
229
|
}
|
186
230
|
// 세션 타임아웃 관련 에러 체크
|
187
231
|
if (!response.ok && isSessionTimeoutError(response.status)) {
|
188
232
|
if (attempt < maxRetries) {
|
189
|
-
logger.warn(`Session timeout detected (${response.status}),
|
190
|
-
|
191
|
-
|
192
|
-
|
233
|
+
logger.warn(`Session timeout detected (${response.status}), performing re-authentication... (${attempt + 1}/${maxRetries + 1})`);
|
234
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
235
|
+
// CRITICAL: 실제 재인증 수행
|
236
|
+
try {
|
237
|
+
const reauthResult = await reAuthenticateSession();
|
238
|
+
if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {
|
239
|
+
pageResource = reauthResult;
|
240
|
+
page = reauthResult.page;
|
241
|
+
}
|
242
|
+
else {
|
243
|
+
page = reauthResult;
|
244
|
+
pageResource = { page, requiresManualRelease: false };
|
245
|
+
}
|
246
|
+
logger.info(`Re-authentication successful after session timeout for connection '${connectionName}'`);
|
247
|
+
}
|
248
|
+
catch (reauthError) {
|
249
|
+
logger.error(`Re-authentication failed after session timeout for connection '${connectionName}':`, reauthError);
|
250
|
+
if (attempt === maxRetries) {
|
251
|
+
throw new Error(`Re-authentication failed after session timeout: ${reauthError.message}`);
|
252
|
+
}
|
253
|
+
continue;
|
193
254
|
}
|
194
255
|
continue;
|
195
256
|
}
|
196
257
|
else {
|
197
|
-
|
198
|
-
await releasePage(page);
|
199
|
-
}
|
258
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
200
259
|
throw new Error(`Session timeout after ${maxRetries + 1} attempts: ${response.error}`);
|
201
260
|
}
|
202
261
|
}
|
@@ -210,18 +269,15 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
210
269
|
status: response.status,
|
211
270
|
headers: response.headers
|
212
271
|
};
|
213
|
-
|
214
|
-
await releasePage(page);
|
215
|
-
}
|
272
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
216
273
|
return result;
|
217
274
|
}
|
218
275
|
catch (error) {
|
219
276
|
lastError = error;
|
220
277
|
logger.error(`Headless request attempt ${attempt + 1} failed:`, error);
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
}
|
278
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
279
|
+
page = null;
|
280
|
+
pageResource = null;
|
225
281
|
// 세션 관련 에러가 아니거나 마지막 재시도면 에러 발생
|
226
282
|
if (!isRecoverableError(error) || attempt === maxRetries) {
|
227
283
|
throw error;
|
@@ -229,9 +285,9 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
229
285
|
logger.info(`Retrying request... (${attempt + 2}/${maxRetries + 1})`);
|
230
286
|
}
|
231
287
|
}
|
232
|
-
// 모든 재시도가 실패한 경우 - 혹시 남은
|
233
|
-
if (
|
234
|
-
await releasePage
|
288
|
+
// 모든 재시도가 실패한 경우 - 혹시 남은 리소스가 있으면 정리
|
289
|
+
if (pageResource) {
|
290
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
235
291
|
}
|
236
292
|
throw lastError || new Error('Request failed after all retry attempts');
|
237
293
|
}
|
@@ -264,4 +320,65 @@ function isRecoverableError(error) {
|
|
264
320
|
];
|
265
321
|
return recoverableErrorKeywords.some(keyword => errorMessage.includes(keyword));
|
266
322
|
}
|
323
|
+
/**
|
324
|
+
* Safely release page resource with comprehensive error handling
|
325
|
+
*/
|
326
|
+
async function safeReleasePageResource(pageResource, releasePage, logger) {
|
327
|
+
if (!pageResource) {
|
328
|
+
return;
|
329
|
+
}
|
330
|
+
try {
|
331
|
+
// Handle different pageResource formats
|
332
|
+
if (pageResource.page) {
|
333
|
+
// This is a complex resource object {page, browser, requiresManualRelease}
|
334
|
+
const { page, browser, requiresManualRelease } = pageResource;
|
335
|
+
if (requiresManualRelease && browser) {
|
336
|
+
// Manual release required - close page first, then release browser
|
337
|
+
try {
|
338
|
+
if (page && !page.isClosed()) {
|
339
|
+
await page.close();
|
340
|
+
logger.info('Page closed during manual resource release');
|
341
|
+
}
|
342
|
+
}
|
343
|
+
catch (closeError) {
|
344
|
+
logger.error('Failed to close page during manual release:', closeError);
|
345
|
+
}
|
346
|
+
// Release browser back to pool
|
347
|
+
try {
|
348
|
+
const { getHeadlessPool } = require('../../resource-pool/headless-pool');
|
349
|
+
const pool = getHeadlessPool();
|
350
|
+
await pool.release(browser);
|
351
|
+
logger.info('Browser manually released to pool');
|
352
|
+
}
|
353
|
+
catch (releaseError) {
|
354
|
+
logger.error('Failed to manually release browser to pool:', releaseError);
|
355
|
+
}
|
356
|
+
}
|
357
|
+
else {
|
358
|
+
// Standard release through releasePage
|
359
|
+
await releasePage(page);
|
360
|
+
logger.info('Page released through standard releasePage method');
|
361
|
+
}
|
362
|
+
}
|
363
|
+
else {
|
364
|
+
// Simple page object - use standard release
|
365
|
+
await releasePage(pageResource);
|
366
|
+
logger.info('Simple page resource released');
|
367
|
+
}
|
368
|
+
}
|
369
|
+
catch (error) {
|
370
|
+
logger.error('Critical error during page resource release:', error);
|
371
|
+
// Last resort: try to force close the page if it exists
|
372
|
+
try {
|
373
|
+
const page = pageResource.page || pageResource;
|
374
|
+
if (page && !page.isClosed()) {
|
375
|
+
await page.close();
|
376
|
+
logger.warn('Force closed page as last resort');
|
377
|
+
}
|
378
|
+
}
|
379
|
+
catch (forceCloseError) {
|
380
|
+
logger.error('Failed to force close page:', forceCloseError);
|
381
|
+
}
|
382
|
+
}
|
383
|
+
}
|
267
384
|
//# sourceMappingURL=headless-request-with-recovery.js.map
|