viruagent-cli 0.3.4 → 0.3.6
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/README.ko.md +21 -5
- package/README.md +21 -5
- package/package.json +1 -1
- package/skills/viruagent.md +98 -9
- package/src/providers/naver/auth.js +180 -0
- package/src/providers/naver/chromeImport.js +195 -0
- package/src/providers/naver/editorConvert.js +198 -0
- package/src/providers/naver/imageUpload.js +111 -0
- package/src/providers/naver/index.js +253 -0
- package/src/providers/naver/selectors.js +23 -0
- package/src/providers/naver/session.js +119 -0
- package/src/providers/naver/utils.js +56 -0
- package/src/providers/tistory/chromeImport.js +86 -41
- package/src/services/naverApiClient.js +493 -0
- package/src/services/providerManager.js +1 -1
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
6
|
+
const BLOG_HOST = 'https://blog.naver.com';
|
|
7
|
+
|
|
8
|
+
const getTimeout = () => 20000;
|
|
9
|
+
|
|
10
|
+
const normalizeCookies = (session) => {
|
|
11
|
+
if (!session) return [];
|
|
12
|
+
|
|
13
|
+
const rawCookies = Array.isArray(session)
|
|
14
|
+
? session
|
|
15
|
+
: Array.isArray(session.cookies)
|
|
16
|
+
? session.cookies
|
|
17
|
+
: [];
|
|
18
|
+
|
|
19
|
+
return rawCookies
|
|
20
|
+
.filter((c) => c && typeof c === 'object')
|
|
21
|
+
.filter((c) => c.name && c.value !== undefined && c.value !== null)
|
|
22
|
+
.filter((c) => {
|
|
23
|
+
if (!c.domain) return true;
|
|
24
|
+
return String(c.domain).includes('naver.com');
|
|
25
|
+
})
|
|
26
|
+
.map((c) => `${c.name}=${c.value}`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const readSessionCookies = (sessionPath) => {
|
|
30
|
+
const resolvedPath = path.resolve(sessionPath);
|
|
31
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
32
|
+
throw new Error(`세션 파일이 없습니다. ${resolvedPath}에 로그인 정보를 먼저 저장하세요.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let raw;
|
|
36
|
+
try {
|
|
37
|
+
raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(`세션 파일 파싱 실패: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cookies = normalizeCookies(raw);
|
|
43
|
+
if (!cookies.length) {
|
|
44
|
+
throw new Error('세션에 유효한 쿠키가 없습니다. 다시 로그인해 주세요.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return cookies.join('; ');
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const createFetchController = () => {
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timeout = setTimeout(() => controller.abort(), getTimeout());
|
|
53
|
+
return { controller, timeout };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const createNaverApiClient = ({ sessionPath }) => {
|
|
57
|
+
let blogId = null;
|
|
58
|
+
|
|
59
|
+
const resetState = () => {
|
|
60
|
+
blogId = null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getSessionCookies = () => readSessionCookies(sessionPath);
|
|
64
|
+
|
|
65
|
+
const getHeaders = (opts = {}) => ({
|
|
66
|
+
Cookie: getSessionCookies(),
|
|
67
|
+
'User-Agent': USER_AGENT,
|
|
68
|
+
Accept: 'application/json',
|
|
69
|
+
...opts,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const encodeRefererUrl = (id, categoryNo = '0', extra = '') => {
|
|
73
|
+
const base = `${BLOG_HOST}/PostWriteForm.naver?blogId=${encodeURIComponent(id)}&categoryNo=${encodeURIComponent(categoryNo)}`;
|
|
74
|
+
return extra ? `${base}&${extra}` : base;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const requestJson = async (url, options = {}) => {
|
|
78
|
+
const { controller, timeout } = createFetchController();
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(url, {
|
|
81
|
+
redirect: 'follow',
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
...options,
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
let detail = '';
|
|
87
|
+
try {
|
|
88
|
+
detail = await response.text();
|
|
89
|
+
detail = detail ? `: ${detail.slice(0, 200)}` : '';
|
|
90
|
+
} catch {}
|
|
91
|
+
throw new Error(`요청 실패: ${response.status} ${response.statusText}${detail}`);
|
|
92
|
+
}
|
|
93
|
+
return response.json();
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const requestText = async (url, options = {}) => {
|
|
100
|
+
const { controller, timeout } = createFetchController();
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(url, {
|
|
103
|
+
redirect: 'follow',
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
...options,
|
|
106
|
+
});
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(`요청 실패: ${response.status} ${response.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
return response.text();
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const initBlog = async () => {
|
|
117
|
+
if (blogId) return blogId;
|
|
118
|
+
|
|
119
|
+
const html = await requestText(`${BLOG_HOST}/MyBlog.naver`, {
|
|
120
|
+
headers: getHeaders({ Referer: BLOG_HOST }),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const match = html.match(/blogId\s*=\s*'([^']+)'/);
|
|
124
|
+
if (!match) {
|
|
125
|
+
// JSON 응답인 경우도 체크
|
|
126
|
+
if (html.includes('로그인') || html.includes('login')) {
|
|
127
|
+
throw new Error('세션이 만료되었습니다. 다시 로그인해 주세요.');
|
|
128
|
+
}
|
|
129
|
+
throw new Error('MyBlog 응답에서 blogId를 찾을 수 없습니다.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
blogId = match[1];
|
|
133
|
+
return blogId;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getToken = async (categoryNo = '0') => {
|
|
137
|
+
const id = blogId || await initBlog();
|
|
138
|
+
const json = await requestJson(
|
|
139
|
+
`${BLOG_HOST}/PostWriteFormSeOptions.naver?blogId=${encodeURIComponent(id)}&categoryNo=${encodeURIComponent(categoryNo)}`,
|
|
140
|
+
{
|
|
141
|
+
headers: getHeaders({
|
|
142
|
+
Referer: encodeRefererUrl(id, categoryNo, 'Redirect=Write'),
|
|
143
|
+
}),
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
const token = json?.result?.token;
|
|
147
|
+
if (!token) throw new Error('Se-Authorization 토큰을 가져올 수 없습니다.');
|
|
148
|
+
return token;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const getCategories = async () => {
|
|
152
|
+
const id = blogId || await initBlog();
|
|
153
|
+
const json = await requestJson(
|
|
154
|
+
`${BLOG_HOST}/PostWriteFormManagerOptions.naver?blogId=${encodeURIComponent(id)}&categoryNo=0`,
|
|
155
|
+
{
|
|
156
|
+
headers: getHeaders({
|
|
157
|
+
Referer: encodeRefererUrl(id, '0'),
|
|
158
|
+
}),
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const categoryList = json?.result?.formView?.categoryListFormView?.categoryFormViewList;
|
|
163
|
+
if (!Array.isArray(categoryList)) return {};
|
|
164
|
+
|
|
165
|
+
const result = {};
|
|
166
|
+
for (const cat of categoryList) {
|
|
167
|
+
if (cat.categoryName && cat.categoryNo !== undefined) {
|
|
168
|
+
result[cat.categoryName] = Number(cat.categoryNo);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const getEditorInfo = async (categoryNo = '0') => {
|
|
175
|
+
const id = blogId || await initBlog();
|
|
176
|
+
const token = await getToken(categoryNo);
|
|
177
|
+
|
|
178
|
+
const configJson = await requestJson(
|
|
179
|
+
'https://platform.editor.naver.com/api/blogpc001/v1/service_config',
|
|
180
|
+
{
|
|
181
|
+
headers: getHeaders({
|
|
182
|
+
Referer: encodeRefererUrl(id, categoryNo),
|
|
183
|
+
'Se-Authorization': token,
|
|
184
|
+
}),
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
const editorId = configJson?.editorInfo?.id;
|
|
188
|
+
if (!editorId) throw new Error('에디터 ID를 가져올 수 없습니다.');
|
|
189
|
+
|
|
190
|
+
const managerJson = await requestJson(
|
|
191
|
+
`${BLOG_HOST}/PostWriteFormManagerOptions.naver?blogId=${encodeURIComponent(id)}&categoryNo=${encodeURIComponent(categoryNo)}`,
|
|
192
|
+
{
|
|
193
|
+
headers: getHeaders({
|
|
194
|
+
Referer: encodeRefererUrl(id, categoryNo),
|
|
195
|
+
}),
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
const editorSource = managerJson?.result?.formView?.editorSource;
|
|
199
|
+
|
|
200
|
+
return { editorId, editorSource: editorSource || 'blogpc001', token };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const getUploadSessionKey = async (token) => {
|
|
204
|
+
const json = await requestJson(
|
|
205
|
+
'https://platform.editor.naver.com/api/blogpc001/v1/photo-uploader/session-key',
|
|
206
|
+
{
|
|
207
|
+
headers: getHeaders({
|
|
208
|
+
Referer: encodeRefererUrl(blogId || '', '0'),
|
|
209
|
+
'Se-Authorization': token,
|
|
210
|
+
}),
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
return json?.sessionKey || null;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const uploadImage = async (imageBuffer, filename, token) => {
|
|
217
|
+
const id = blogId || await initBlog();
|
|
218
|
+
const sessionKey = await getUploadSessionKey(token);
|
|
219
|
+
if (!sessionKey) throw new Error('이미지 업로드 세션 키를 가져올 수 없습니다.');
|
|
220
|
+
|
|
221
|
+
const uploadUrl = `https://blog.upphoto.naver.com/${sessionKey}/simpleUpload/0?userId=${encodeURIComponent(id)}&extractExif=true&extractAnimatedCnt=true&autorotate=true&extractDominantColor=false&denyAnimatedImage=false&skipXcamFiltering=false`;
|
|
222
|
+
|
|
223
|
+
const formData = new FormData();
|
|
224
|
+
const blob = new Blob([imageBuffer], { type: 'image/jpeg' });
|
|
225
|
+
formData.append('image', blob, filename || 'image.jpg');
|
|
226
|
+
|
|
227
|
+
const { controller, timeout } = createFetchController();
|
|
228
|
+
try {
|
|
229
|
+
const response = await fetch(uploadUrl, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: {
|
|
232
|
+
Cookie: getSessionCookies(),
|
|
233
|
+
'User-Agent': USER_AGENT,
|
|
234
|
+
Referer: `${BLOG_HOST}/${encodeURIComponent(id)}`,
|
|
235
|
+
},
|
|
236
|
+
body: formData,
|
|
237
|
+
signal: controller.signal,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new Error(`이미지 업로드 실패: ${response.status}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const xml = await response.text();
|
|
245
|
+
if (!xml.includes('<url>')) {
|
|
246
|
+
throw new Error('이미지 업로드 응답에 URL이 없습니다.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const extractTag = (tag) => {
|
|
250
|
+
const m = xml.match(new RegExp(`<${tag}>([^<]*)</${tag}>`));
|
|
251
|
+
return m ? m[1] : null;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
url: extractTag('url'),
|
|
256
|
+
width: parseInt(extractTag('width') || '600', 10),
|
|
257
|
+
height: parseInt(extractTag('height') || '400', 10),
|
|
258
|
+
fileName: extractTag('fileName') || filename || 'image.jpg',
|
|
259
|
+
fileSize: parseInt(extractTag('fileSize') || '0', 10),
|
|
260
|
+
};
|
|
261
|
+
} finally {
|
|
262
|
+
clearTimeout(timeout);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const convertHtmlToComponents = async (html) => {
|
|
267
|
+
const id = blogId || await initBlog();
|
|
268
|
+
const wrappedHtml = `<html>\n<body>\n<!--StartFragment-->\n${html}\n<!--EndFragment-->\n</body>\n</html>`;
|
|
269
|
+
|
|
270
|
+
const { controller, timeout } = createFetchController();
|
|
271
|
+
try {
|
|
272
|
+
const response = await fetch(
|
|
273
|
+
`https://upconvert.editor.naver.com/blog/html/components?documentWidth=886&userId=${encodeURIComponent(id)}`,
|
|
274
|
+
{
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: {
|
|
277
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
278
|
+
'User-Agent': USER_AGENT,
|
|
279
|
+
Cookie: getSessionCookies(),
|
|
280
|
+
},
|
|
281
|
+
body: wrappedHtml,
|
|
282
|
+
signal: controller.signal,
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (!response.ok) return [];
|
|
287
|
+
return response.json();
|
|
288
|
+
} catch {
|
|
289
|
+
return [];
|
|
290
|
+
} finally {
|
|
291
|
+
clearTimeout(timeout);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const getDefaultCategoryNo = async () => {
|
|
296
|
+
const id = blogId || await initBlog();
|
|
297
|
+
const json = await requestJson(
|
|
298
|
+
`${BLOG_HOST}/PostWriteFormManagerOptions.naver?blogId=${encodeURIComponent(id)}&categoryNo=0`,
|
|
299
|
+
{ headers: getHeaders({ Referer: encodeRefererUrl(id, '0') }) }
|
|
300
|
+
);
|
|
301
|
+
const defaultId = json?.result?.formView?.categoryListFormView?.defaultCategoryId;
|
|
302
|
+
return defaultId !== undefined && defaultId !== null ? String(defaultId) : '1';
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const publishPost = async ({ title, content, categoryNo, tags = '', openType = 2 }) => {
|
|
306
|
+
const id = blogId || await initBlog();
|
|
307
|
+
const resolvedCategoryNo = categoryNo && String(categoryNo) !== '0'
|
|
308
|
+
? String(categoryNo)
|
|
309
|
+
: await getDefaultCategoryNo();
|
|
310
|
+
const { editorId, editorSource, token } = await getEditorInfo(resolvedCategoryNo);
|
|
311
|
+
|
|
312
|
+
// content가 이미 컴포넌트 배열이면 그대로 사용, 아니면 빈 배열
|
|
313
|
+
const contentComponents = Array.isArray(content) ? content : [];
|
|
314
|
+
|
|
315
|
+
const titleComponent = {
|
|
316
|
+
id: `SE-${crypto.randomUUID()}`,
|
|
317
|
+
layout: 'default',
|
|
318
|
+
title: [{
|
|
319
|
+
id: `SE-${crypto.randomUUID()}`,
|
|
320
|
+
nodes: [{
|
|
321
|
+
id: `SE-${crypto.randomUUID()}`,
|
|
322
|
+
value: title,
|
|
323
|
+
'@ctype': 'textNode',
|
|
324
|
+
}],
|
|
325
|
+
'@ctype': 'paragraph',
|
|
326
|
+
}],
|
|
327
|
+
subTitle: null,
|
|
328
|
+
align: 'left',
|
|
329
|
+
'@ctype': 'documentTitle',
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const documentModel = {
|
|
333
|
+
documentId: '',
|
|
334
|
+
document: {
|
|
335
|
+
version: '2.9.0',
|
|
336
|
+
theme: 'default',
|
|
337
|
+
language: 'ko-KR',
|
|
338
|
+
id: editorId,
|
|
339
|
+
components: [titleComponent, ...contentComponents],
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const populationParams = {
|
|
344
|
+
configuration: {
|
|
345
|
+
openType,
|
|
346
|
+
commentYn: true,
|
|
347
|
+
searchYn: true,
|
|
348
|
+
sympathyYn: true,
|
|
349
|
+
scrapType: 2,
|
|
350
|
+
outSideAllowYn: true,
|
|
351
|
+
twitterPostingYn: false,
|
|
352
|
+
facebookPostingYn: false,
|
|
353
|
+
cclYn: false,
|
|
354
|
+
},
|
|
355
|
+
populationMeta: {
|
|
356
|
+
categoryId: resolvedCategoryNo,
|
|
357
|
+
logNo: null,
|
|
358
|
+
directorySeq: 0,
|
|
359
|
+
directoryDetail: null,
|
|
360
|
+
mrBlogTalkCode: null,
|
|
361
|
+
postWriteTimeType: 'now',
|
|
362
|
+
tags: tags || '',
|
|
363
|
+
moviePanelParticipation: false,
|
|
364
|
+
greenReviewBannerYn: false,
|
|
365
|
+
continueSaved: false,
|
|
366
|
+
noticePostYn: false,
|
|
367
|
+
autoByCategoryYn: false,
|
|
368
|
+
postLocationSupportYn: false,
|
|
369
|
+
postLocationJson: null,
|
|
370
|
+
prePostDate: null,
|
|
371
|
+
thisDayPostInfo: null,
|
|
372
|
+
scrapYn: false,
|
|
373
|
+
},
|
|
374
|
+
editorSource,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const body = new URLSearchParams({
|
|
378
|
+
blogId: id,
|
|
379
|
+
documentModel: JSON.stringify(documentModel),
|
|
380
|
+
populationParams: JSON.stringify(populationParams),
|
|
381
|
+
productApiVersion: 'v1',
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const { controller, timeout } = createFetchController();
|
|
385
|
+
try {
|
|
386
|
+
const response = await fetch(`${BLOG_HOST}/RabbitWrite.naver`, {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
headers: {
|
|
389
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
390
|
+
Cookie: getSessionCookies(),
|
|
391
|
+
'User-Agent': USER_AGENT,
|
|
392
|
+
Referer: encodeRefererUrl(id, resolvedCategoryNo, 'Redirect=Write'),
|
|
393
|
+
},
|
|
394
|
+
body: body.toString(),
|
|
395
|
+
signal: controller.signal,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
throw new Error(`글 발행 실패: ${response.status}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const json = await response.json();
|
|
403
|
+
if (!json.isSuccess) {
|
|
404
|
+
throw new Error(`글 발행 실패: ${JSON.stringify(json).slice(0, 200)}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const redirectUrl = json.result?.redirectUrl || '';
|
|
408
|
+
const blogUrlMatch = redirectUrl.match(/blogId=([^&]+)/) || [];
|
|
409
|
+
const logNoMatch = redirectUrl.match(/logNo=([^&]+)/) || [];
|
|
410
|
+
const finalBlogId = blogUrlMatch[1] || id;
|
|
411
|
+
const logNo = logNoMatch[1] || '';
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
entryUrl: logNo ? `https://blog.naver.com/${finalBlogId}/${logNo}` : null,
|
|
416
|
+
redirectUrl,
|
|
417
|
+
raw: json,
|
|
418
|
+
};
|
|
419
|
+
} finally {
|
|
420
|
+
clearTimeout(timeout);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const getPosts = async ({ page = 1, countPerPage = 20 } = {}) => {
|
|
425
|
+
const id = blogId || await initBlog();
|
|
426
|
+
const text = await requestText(
|
|
427
|
+
`${BLOG_HOST}/PostTitleListAsync.naver?blogId=${encodeURIComponent(id)}&viewdate=¤tPage=${page}&categoryNo=0&parentCategoryNo=0&countPerPage=${countPerPage}`,
|
|
428
|
+
{
|
|
429
|
+
headers: getHeaders({ Referer: `${BLOG_HOST}/${id}` }),
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
let json;
|
|
434
|
+
try {
|
|
435
|
+
// 네이버 응답에 잘못된 이스케이프(\')가 포함될 수 있어 정리
|
|
436
|
+
const sanitized = text.replace(/\\'/g, "'");
|
|
437
|
+
json = JSON.parse(sanitized);
|
|
438
|
+
} catch {
|
|
439
|
+
return { items: [], totalCount: 0 };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const postList = Array.isArray(json?.postList) ? json.postList : [];
|
|
443
|
+
const items = postList.map((p) => {
|
|
444
|
+
let title = p.title || '';
|
|
445
|
+
try { title = decodeURIComponent(title.replace(/\+/g, ' ')); } catch { /* use as-is */ }
|
|
446
|
+
return {
|
|
447
|
+
id: p.logNo,
|
|
448
|
+
title,
|
|
449
|
+
categoryNo: p.categoryNo,
|
|
450
|
+
readCount: p.readCount,
|
|
451
|
+
addDate: p.addDate,
|
|
452
|
+
openType: p.openType,
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
return { items, totalCount: items.length };
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const getPost = async ({ postId } = {}) => {
|
|
459
|
+
if (!postId) return null;
|
|
460
|
+
const id = blogId || await initBlog();
|
|
461
|
+
const html = await requestText(
|
|
462
|
+
`${BLOG_HOST}/PostView.naver?blogId=${encodeURIComponent(id)}&logNo=${encodeURIComponent(postId)}`,
|
|
463
|
+
{
|
|
464
|
+
headers: getHeaders({ Referer: `${BLOG_HOST}/${id}` }),
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
if (!html || html.includes('존재하지 않는 포스트')) return null;
|
|
468
|
+
|
|
469
|
+
const titleMatch = html.match(/<title>([^<]*)<\/title>/i);
|
|
470
|
+
return {
|
|
471
|
+
id: String(postId),
|
|
472
|
+
title: titleMatch ? titleMatch[1].replace(/\s*:.*$/, '').trim() : '',
|
|
473
|
+
url: `${BLOG_HOST}/${id}/${postId}`,
|
|
474
|
+
html: html.slice(0, 5000),
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
initBlog,
|
|
480
|
+
getToken,
|
|
481
|
+
getCategories,
|
|
482
|
+
getEditorInfo,
|
|
483
|
+
getUploadSessionKey,
|
|
484
|
+
uploadImage,
|
|
485
|
+
convertHtmlToComponents,
|
|
486
|
+
publishPost,
|
|
487
|
+
getPosts,
|
|
488
|
+
getPost,
|
|
489
|
+
resetState,
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
module.exports = createNaverApiClient;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { getSessionPath } = require('../storage/sessionStore');
|
|
3
3
|
const createTistoryProvider = require('../providers/tistory');
|
|
4
|
-
const createNaverProvider = require('../providers/
|
|
4
|
+
const createNaverProvider = require('../providers/naver');
|
|
5
5
|
|
|
6
6
|
const providerFactory = {
|
|
7
7
|
tistory: createTistoryProvider,
|