-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
2812 lines (2429 loc) · 107 KB
/
content.js
File metadata and controls
2812 lines (2429 loc) · 107 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// content.js
// --- 전역 변수 및 상수 ---
const SUMMARY_TAB_ID = 'ai-summary-tab';
const SCRIPT_TAB_ID = 'script-tab';
const COMMENTS_TAB_ID = 'comments-summary-tab';
const SUMMARY_CONTENT_ID = 'ai-summary-content';
const COMMENTS_CONTENT_ID = 'comments-summary-content';
// const SCRIPT_CONTENT_ID = 'youtube-script-content'; // 이 ID의 div는 이제 내용을 직접 담지 않을 수 있음
const UI_CONTAINER_ID = 'youtube-ai-summary-ui-container';
const REFRESH_BUTTON_ID = 'ai-summary-refresh-button';
const COMMENTS_REFRESH_BUTTON_ID = 'comments-summary-refresh-button';
// AI 모델 설정 상수
const AI_MODELS = {
'openai-o4-mini': {
id: 'openai-o4-mini',
name: 'OpenAI o4-mini',
provider: 'openai',
model: 'o4-mini',
apiKeyRequired: 'openai_api_key',
maxTokens: 30000,
temperature: 0.7,
endpoint: 'https://api.openai.com/v1/chat/completions'
},
'gemini-2.5-pro': {
id: 'gemini-2.5-pro',
name: 'Google Gemini 2.5 Pro',
provider: 'gemini',
model: 'gemini-2.5-pro',
apiKeyRequired: 'gemini_api_key',
maxTokens: 32000,
temperature: 0.7,
endpoint: 'https://generativelanguage.googleapis.com/v1/models/gemini-2.5-pro:generateContent'
},
'gemini-3-pro-preview': {
id: 'gemini-3-pro-preview',
name: 'Google Gemini 3 Pro Preview',
provider: 'gemini',
model: 'gemini-3-pro-preview',
apiKeyRequired: 'gemini_api_key',
maxTokens: 32000,
temperature: 1,
endpoint: 'https://generativelanguage.googleapis.com/v1/models/gemini-3-pro-preview:generateContent'
}
};
// 댓글 관련 선택자들
const COMMENT_SELECTORS = {
commentSection: '#comments',
sortButton: '#sort-menu-button',
topCommentsOption: 'tp-yt-paper-listbox #menu-item-1',
commentItems: 'ytd-comment-thread-renderer',
commentText: '#content-text',
replyButton: '#show-replies-button',
replyItems: 'ytd-comment-renderer',
authorName: '#author-text span',
likeCount: '#vote-count-middle',
moreRepliesButton: '#show-replies-button'
};
let currentVideoId = null;
let openAIKey = null;
let userPrompt = `다음은 YouTube 동영상의 스크립트(자막) 내용입니다. 이 내용을 바탕으로 다음과 같이 요약해 주세요:
1. 핵심 주제와 메인 아이디어를 간결하게 정리
2. 중요한 포인트들을 순서대로 나열
3. 전체적인 결론이나 메시지를 포함
4. 가능하면 실용적인 정보나 팁이 있다면 별도로 언급
요약은 한국어로 작성하고, 불필요한 반복이나 부가 설명은 제외해 주세요:`; // 개선된 기본 프롬프트
let commentsPrompt = `다음은 YouTube 동영상의 댓글들입니다. 이 댓글들을 분석하여 다음과 같이 요약해 주세요:
1. 전반적인 시청자 반응과 감정 (긍정적/부정적/중립적)
2. 가장 많이 언급되는 주요 논점이나 이슈들
3. 시청자들이 공감하거나 관심있어하는 내용들
4. 건설적인 비판이나 제안사항들
5. 전체적인 댓글의 분위기와 특징
댓글 요약은 한국어로 작성하고, 욕설이나 부적절한 내용은 제외해 주세요:`;
// YouTube의 스크립트 관련 주요 DOM 요소 참조
let ytTranscriptRendererElement = null; // ytd-transcript-renderer
let ytTranscriptContentElement = null; // YouTube 자체 스크립트 내용 컨테이너 (예: div#content.ytd-transcript-renderer)
let ytTranscriptSegmentsContainer = null; // 스크립트 세그먼트들의 실제 부모 (예: #segments-container)
// UI 복구를 위한 주기적 체크
let uiCheckInterval = null;
// 이벤트 리스너 참조를 저장할 변수들
let tabChangeClickListener = null;
let tabChangeKeyListener = null;
// 클립보드 복사를 위한 원본 텍스트 저장 변수
let currentSummaryText = '';
let currentCommentsSummaryText = '';
// 댓글 수집 관련 변수들
let isCollectingComments = false;
let collectedComments = [];
let commentCollectionProgress = { current: 0, total: 100 };
// API 요청 타이머 관련 변수들
let loadingTimer = null;
let loadingStartTime = null;
let commentsLoadingTimer = null;
let commentsLoadingStartTime = null;
// --- 초기화 및 UI 삽입 로직 ---
/**
* 확장 프로그램 UI를 페이지에 삽입합니다.
* ytd-transcript-renderer가 나타난 후 호출됩니다.
* @param {HTMLElement} transcriptRenderer - ytd-transcript-renderer 요소
*/
function injectUI(transcriptRenderer) {
ytTranscriptRendererElement = transcriptRenderer; // 참조 저장
// 1. 스크립트 패널 내부의 실제 콘텐츠 영역 찾기
ytTranscriptContentElement = transcriptRenderer.querySelector('div#content.ytd-transcript-renderer');
ytTranscriptSegmentsContainer = transcriptRenderer.querySelector('#segments-container.ytd-transcript-segment-list-renderer, ytd-transcript-segment-list-renderer');
if (!ytTranscriptContentElement) {
console.warn('[AI 요약] YouTube 스크립트 내용 컨테이너(div#content)를 찾을 수 없습니다.');
return;
}
if (!ytTranscriptSegmentsContainer) {
console.warn('[AI 요약] YouTube 스크립트 세그먼트 컨테이너를 찾을 수 없습니다.');
}
// 2. 이미 UI가 삽입되었는지 확인 (중복 삽입 방지)
if (document.getElementById(UI_CONTAINER_ID)) {
console.log('[AI 요약] UI가 이미 존재합니다.');
updateUIForNewVideo(); // 비디오 변경 시 내용 업데이트 로직 호출
return;
}
// 3. 탭 버튼들 생성
const tabsContainer = document.createElement('div');
tabsContainer.classList.add('yt-ai-summary-tabs');
// 탭 버튼들을 감싸는 왼쪽 컨테이너
const tabsLeftContainer = document.createElement('div');
tabsLeftContainer.classList.add('yt-ai-summary-tabs-left');
const scriptTabButton = document.createElement('button');
scriptTabButton.id = SCRIPT_TAB_ID;
scriptTabButton.textContent = '스크립트';
scriptTabButton.classList.add('yt-ai-summary-tab-button', 'active');
const summaryTabButton = document.createElement('button');
summaryTabButton.id = SUMMARY_TAB_ID;
summaryTabButton.textContent = 'AI 요약';
summaryTabButton.classList.add('yt-ai-summary-tab-button');
const commentsTabButton = document.createElement('button');
commentsTabButton.id = COMMENTS_TAB_ID;
commentsTabButton.textContent = '댓글 요약';
commentsTabButton.classList.add('yt-ai-summary-tab-button');
tabsLeftContainer.appendChild(scriptTabButton);
tabsLeftContainer.appendChild(summaryTabButton);
tabsLeftContainer.appendChild(commentsTabButton);
// 버튼 컨테이너 생성 (복사 + 새로고침 버튼들을 위한)
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 8px;
align-items: center;
`;
// 복사 버튼 생성
const copyButton = document.createElement('button');
copyButton.id = 'ai-summary-copy-button';
copyButton.classList.add('yt-ai-summary-copy-button');
copyButton.title = '요약 내용 복사';
copyButton.disabled = true; // 초기에는 비활성화
// 인라인 스타일 강제 적용 (YouTube CSS 간섭 방지)
copyButton.style.cssText = `
background: none !important;
border: none !important;
cursor: pointer !important;
padding: 8px !important;
border-radius: 4px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
min-width: 32px !important;
min-height: 32px !important;
max-width: 32px !important;
max-height: 32px !important;
visibility: visible !important;
opacity: 0.5 !important;
position: relative !important;
overflow: visible !important;
box-sizing: border-box !important;
color: var(--yt-spec-text-secondary) !important;
transition: all 0.2s ease !important;
margin: 0 !important;
outline: none !important;
transform: none !important;
filter: none !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
flex-basis: auto !important;
`;
// 복사 아이콘 SVG
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 16px !important; height: 16px !important; display: block !important; visibility: visible !important; opacity: 1 !important; pointer-events: none !important; stroke: currentColor !important; fill: none !important;">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
`;
// 새로고침 버튼을 탭바 우측에 배치
const refreshButton = document.createElement('button');
refreshButton.id = REFRESH_BUTTON_ID;
refreshButton.classList.add('yt-ai-summary-refresh-button');
refreshButton.title = '요약 새로고침';
// 인라인 스타일 강제 적용 (YouTube CSS 간섭 방지)
refreshButton.style.cssText = `
background: none !important;
border: none !important;
cursor: pointer !important;
padding: 8px !important;
border-radius: 4px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
min-width: 32px !important;
min-height: 32px !important;
max-width: 32px !important;
max-height: 32px !important;
visibility: visible !important;
opacity: 1 !important;
position: relative !important;
overflow: visible !important;
box-sizing: border-box !important;
color: var(--yt-spec-text-secondary) !important;
transition: all 0.2s ease !important;
margin: 0 !important;
outline: none !important;
transform: none !important;
filter: none !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
flex-basis: auto !important;
`;
// Lucide 새로고침 아이콘 SVG 추가
refreshButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 16px !important; height: 16px !important; display: block !important; visibility: visible !important; opacity: 1 !important; pointer-events: none !important; stroke: currentColor !important; fill: none !important; transform: none !important; filter: none !important;">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
<path d="M21 3v5h-5"></path>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
<path d="M3 21v-5h5"></path>
</svg>
`;
// 버튼들을 컨테이너에 추가
buttonContainer.appendChild(copyButton);
buttonContainer.appendChild(refreshButton);
tabsContainer.appendChild(tabsLeftContainer);
tabsContainer.appendChild(buttonContainer);
// AI 요약 콘텐츠 영역 생성
const summaryContentDiv = document.createElement('div');
summaryContentDiv.id = SUMMARY_CONTENT_ID;
summaryContentDiv.classList.add('yt-ai-summary-content');
summaryContentDiv.style.display = 'none'; // 기본적으로 숨김
// 댓글 요약 콘텐츠 영역 생성
const commentsContentDiv = document.createElement('div');
commentsContentDiv.id = COMMENTS_CONTENT_ID;
commentsContentDiv.classList.add('yt-comments-summary-content');
commentsContentDiv.style.display = 'none'; // 기본적으로 숨김
// 로딩, 에러, 텍스트 요소들 (새로고침 버튼은 이제 탭바에 있으므로 제거)
const loadingMessage = document.createElement('p');
loadingMessage.id = 'ai-summary-loading';
loadingMessage.textContent = '요약 정보를 불러오는 중...';
loadingMessage.style.display = 'none';
const errorMessage = document.createElement('p');
errorMessage.id = 'ai-summary-error';
errorMessage.style.color = 'red';
errorMessage.style.display = 'none';
const summaryTextElement = document.createElement('div');
summaryTextElement.id = 'ai-summary-text';
// AI 요약 내용 구성 (새로고침 버튼 제거)
summaryContentDiv.appendChild(loadingMessage);
summaryContentDiv.appendChild(errorMessage);
summaryContentDiv.appendChild(summaryTextElement);
// 댓글 요약용 로딩, 에러, 텍스트 요소들 생성
const commentsLoadingMessage = document.createElement('p');
commentsLoadingMessage.id = 'comments-summary-loading';
commentsLoadingMessage.textContent = '댓글을 수집하고 있습니다...';
commentsLoadingMessage.style.display = 'none';
const commentsErrorMessage = document.createElement('p');
commentsErrorMessage.id = 'comments-summary-error';
commentsErrorMessage.style.color = 'red';
commentsErrorMessage.style.display = 'none';
const commentsTextElement = document.createElement('div');
commentsTextElement.id = 'comments-summary-text';
// 댓글 요약 내용 구성
commentsContentDiv.appendChild(commentsLoadingMessage);
commentsContentDiv.appendChild(commentsErrorMessage);
commentsContentDiv.appendChild(commentsTextElement);
// 5. 전체 UI 컨테이너 생성
const uiContainer = document.createElement('div');
uiContainer.id = UI_CONTAINER_ID;
uiContainer.classList.add('yt-ai-summary-container');
uiContainer.appendChild(tabsContainer);
uiContainer.appendChild(summaryContentDiv);
uiContainer.appendChild(commentsContentDiv);
// 6. 스크립트 패널 내부에 UI 삽입 (기존 스크립트 내용 위에)
ytTranscriptContentElement.insertBefore(uiContainer, ytTranscriptContentElement.firstChild);
console.log('[AI 요약] UI가 스크립트 패널 내부에 성공적으로 삽입되었습니다.');
// 7. 탭 전환 이벤트 리스너 등록
scriptTabButton.addEventListener('click', () => switchTab(SCRIPT_TAB_ID));
summaryTabButton.addEventListener('click', () => switchTab(SUMMARY_TAB_ID));
commentsTabButton.addEventListener('click', () => switchTab(COMMENTS_TAB_ID));
// 8. 이벤트 리스너 추가
refreshButton.addEventListener('click', handleRefreshSummary);
copyButton.addEventListener('click', handleCopyToClipboard);
// 9. 초기 상태: 스크립트 탭 활성화
switchTab(SCRIPT_TAB_ID);
}
/**
* YouTube 스크립트 패널 또는 적절한 UI 삽입 위치를 찾습니다. (이 함수는 이제 사용 빈도가 줄어들 수 있음)
* 대신 ytd-transcript-renderer를 직접 찾고, 그 내부에서 UI 위치를 결정.
* @returns {HTMLElement|null} UI를 삽입할 부모 요소 (이제는 ytd-transcript-renderer 자체가 기준)
*/
function findTargetPanel() { // 이 함수의 역할이 변경됨
return document.querySelector('ytd-transcript-renderer');
}
/**
* 탭을 전환합니다.
* @param {string} tabIdToActivate 활성화할 탭의 ID
*/
function switchTab(tabIdToActivate) {
const scriptTabBtn = document.getElementById(SCRIPT_TAB_ID);
const summaryTabBtn = document.getElementById(SUMMARY_TAB_ID);
const summaryContent = document.getElementById(SUMMARY_CONTENT_ID);
const commentsTabBtn = document.getElementById(COMMENTS_TAB_ID);
const commentsContent = document.getElementById(COMMENTS_CONTENT_ID);
// UI 요소들이 존재하지 않으면 다시 생성 시도
if (!scriptTabBtn || !summaryTabBtn || !summaryContent || !commentsTabBtn || !commentsContent) {
console.warn('[AI 요약] 탭 또는 AI 요약 내용 영역을 찾을 수 없습니다. UI 복구 시도.');
const transcriptRenderer = document.querySelector('ytd-transcript-renderer');
if (transcriptRenderer && window.location.href.includes('/watch?v=')) {
// 기존 UI가 있다면 제거하고 새로 생성
removeUI();
initializeExtension(transcriptRenderer);
// 복구 후 다시 요소들 찾기
const newScriptTabBtn = document.getElementById(SCRIPT_TAB_ID);
const newSummaryTabBtn = document.getElementById(SUMMARY_TAB_ID);
const newSummaryContent = document.getElementById(SUMMARY_CONTENT_ID);
const newCommentsTabBtn = document.getElementById(COMMENTS_TAB_ID);
const newCommentsContent = document.getElementById(COMMENTS_CONTENT_ID);
if (!newScriptTabBtn || !newSummaryTabBtn || !newSummaryContent || !newCommentsTabBtn || !newCommentsContent) {
console.error('[AI 요약] UI 복구 후에도 탭 요소들을 찾을 수 없습니다.');
return;
}
// 복구된 요소들로 다시 탭 전환 시도
switchTabInternal(tabIdToActivate, newScriptTabBtn, newSummaryTabBtn, newSummaryContent, newCommentsTabBtn, newCommentsContent);
} else {
console.error('[AI 요약] 스크립트 패널을 찾을 수 없어 UI를 복구할 수 없습니다.');
}
return;
}
switchTabInternal(tabIdToActivate, scriptTabBtn, summaryTabBtn, summaryContent, commentsTabBtn, commentsContent);
}
/**
* 실제 탭 전환 로직을 수행합니다.
* @param {string} tabIdToActivate 활성화할 탭의 ID
* @param {HTMLElement} scriptTabBtn 스크립트 탭 버튼
* @param {HTMLElement} summaryTabBtn AI 요약 탭 버튼
* @param {HTMLElement} summaryContent AI 요약 콘텐츠 영역
*/
function switchTabInternal(tabIdToActivate, scriptTabBtn, summaryTabBtn, summaryContent, commentsTabBtn, commentsContent) {
// 탭 전환 후 버튼 스타일 강제 재적용 함수
function enforceButtonStyles() {
const copyButton = document.getElementById('ai-summary-copy-button');
const refreshButton = document.getElementById(REFRESH_BUTTON_ID);
if (copyButton) {
copyButton.style.cssText = `
background: none !important;
border: none !important;
cursor: ${copyButton.disabled ? 'not-allowed' : 'pointer'} !important;
padding: 8px !important;
border-radius: 4px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
min-width: 32px !important;
min-height: 32px !important;
max-width: 32px !important;
max-height: 32px !important;
visibility: visible !important;
opacity: ${copyButton.disabled ? '0.5' : '1'} !important;
position: relative !important;
overflow: visible !important;
box-sizing: border-box !important;
color: var(--yt-spec-text-secondary) !important;
transition: all 0.2s ease !important;
margin: 0 !important;
outline: none !important;
transform: none !important;
filter: none !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
flex-basis: auto !important;
`;
// SVG 스타일도 재적용
const copySvg = copyButton.querySelector('svg');
if (copySvg) {
copySvg.style.cssText = `
width: 16px !important;
height: 16px !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: none !important;
stroke: currentColor !important;
fill: none !important;
transform: none !important;
filter: none !important;
`;
}
}
if (refreshButton) {
refreshButton.style.cssText = `
background: none !important;
border: none !important;
cursor: pointer !important;
padding: 8px !important;
border-radius: 4px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
min-width: 32px !important;
min-height: 32px !important;
max-width: 32px !important;
max-height: 32px !important;
visibility: visible !important;
opacity: 1 !important;
position: relative !important;
overflow: visible !important;
box-sizing: border-box !important;
color: var(--yt-spec-text-secondary) !important;
transition: all 0.2s ease !important;
margin: 0 !important;
outline: none !important;
transform: none !important;
filter: none !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
flex-basis: auto !important;
`;
// SVG 스타일도 재적용
const refreshSvg = refreshButton.querySelector('svg');
if (refreshSvg) {
refreshSvg.style.cssText = `
width: 16px !important;
height: 16px !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: none !important;
stroke: currentColor !important;
fill: none !important;
transform: none !important;
filter: none !important;
`;
}
}
}
if (tabIdToActivate === SCRIPT_TAB_ID) {
// 스크립트 탭 활성화
scriptTabBtn.classList.add('active');
summaryTabBtn.classList.remove('active');
summaryContent.style.display = 'none';
commentsTabBtn.classList.remove('active');
commentsContent.style.display = 'none';
// YouTube 자체 스크립트 내용 표시
if (ytTranscriptSegmentsContainer) {
ytTranscriptSegmentsContainer.style.display = '';
}
// 전체 스크립트 리스트도 표시
const transcriptSegmentList = ytTranscriptRendererElement?.querySelector('ytd-transcript-segment-list-renderer');
if (transcriptSegmentList) {
transcriptSegmentList.style.display = '';
}
// 스크립트 검색 패널도 다시 표시
const transcriptSearchPanel = ytTranscriptRendererElement?.querySelector('ytd-transcript-search-panel-renderer');
if (transcriptSearchPanel) {
transcriptSearchPanel.style.display = '';
}
} else if (tabIdToActivate === SUMMARY_TAB_ID) {
// AI 요약 탭 활성화
scriptTabBtn.classList.remove('active');
summaryTabBtn.classList.add('active');
summaryContent.style.display = 'block';
commentsTabBtn.classList.remove('active');
commentsContent.style.display = 'none';
// YouTube 자체 스크립트 내용 숨김
if (ytTranscriptSegmentsContainer) {
ytTranscriptSegmentsContainer.style.display = 'none';
}
// 전체 스크립트 리스트도 숨김
const transcriptSegmentList = ytTranscriptRendererElement?.querySelector('ytd-transcript-segment-list-renderer');
if (transcriptSegmentList) {
transcriptSegmentList.style.display = 'none';
}
// 스크립트 검색 패널도 숨김
const transcriptSearchPanel = ytTranscriptRendererElement?.querySelector('ytd-transcript-search-panel-renderer');
if (transcriptSearchPanel) {
transcriptSearchPanel.style.display = 'none';
}
// AI 요약 로드 (아직 로드되지 않았다면)
loadAndDisplaySummary();
} else if (tabIdToActivate === COMMENTS_TAB_ID) {
// 댓글 요약 탭 활성화
scriptTabBtn.classList.remove('active');
summaryTabBtn.classList.remove('active');
summaryContent.style.display = 'none';
commentsTabBtn.classList.add('active');
commentsContent.style.display = 'block';
// 댓글 수집 로직 실행
collectComments();
}
// 탭 전환 후 버튼 스타일 강제 재적용 (약간의 지연 후)
setTimeout(enforceButtonStyles, 50);
setTimeout(enforceButtonStyles, 200); // 추가 보장
}
// --- 스크립트 추출 로직 ---
/**
* 현재 YouTube 동영상의 스크립트(자막) 텍스트를 추출합니다.
* @returns {string|null} 추출된 스크립트 텍스트 또는 실패 시 null
*/
function extractTranscriptText() {
console.log('[AI 요약] 스크립트 추출 시도...');
// ytTranscriptSegmentsContainer는 injectUI에서 이미 식별됨
if (!ytTranscriptSegmentsContainer) {
console.warn('[AI 요약] 스크립트 세그먼트 컨테이너를 찾을 수 없습니다 (추출).');
console.log('[AI 요약] 디버깅: ytTranscriptRendererElement:', ytTranscriptRendererElement);
console.log('[AI 요약] 디버깅: ytTranscriptContentElement:', ytTranscriptContentElement);
// 스크립트 패널을 열도록 유도하거나, 사용자에게 알림
const openTranscriptButton = document.querySelector(
'button[aria-label*="transcript"], button[aria-label*="스크립트"]' // "Show transcript", "스크립트 표시" 등
);
if (openTranscriptButton && !isTranscriptPanelOpen()) {
showErrorMessage('스크립트 패널이 닫혀 있습니다. 스크립트를 열고 다시 시도해주세요.');
} else {
showErrorMessage('스크립트 내용을 찾을 수 없습니다. 스크립트가 제공되는 동영상인지 확인해주세요.');
}
return null;
}
console.log('[AI 요약] 디버깅: 스크립트 세그먼트 컨테이너 발견:', ytTranscriptSegmentsContainer);
// Tampermonkey 참고: ytd-transcript-segment-renderer 내부의 .segment-text (yt-formatted-string)
const transcriptSegments = ytTranscriptSegmentsContainer.querySelectorAll(
'ytd-transcript-segment-renderer .segment-text, ytd-transcript-segment-renderer .yt-formatted-string' // 더 많은 케이스 포괄
);
console.log(`[AI 요약] 디버깅: 발견된 스크립트 세그먼트 수: ${transcriptSegments.length}`);
if (!transcriptSegments || transcriptSegments.length === 0) {
console.warn('[AI 요약] 스크립트 세그먼트 요소를 찾을 수 없습니다.');
// 대안적인 선택자들 시도
const alternativeSegments = ytTranscriptSegmentsContainer.querySelectorAll(
'yt-formatted-string, [class*="segment"], [class*="transcript"]'
);
console.log(`[AI 요약] 디버깅: 대안 선택자로 발견된 요소 수: ${alternativeSegments.length}`);
if (alternativeSegments.length > 0) {
console.log('[AI 요약] 디버깅: 대안 요소들 샘플:', Array.from(alternativeSegments).slice(0, 3).map(el => ({
tagName: el.tagName,
className: el.className,
textContent: el.textContent?.substring(0, 50)
})));
}
showErrorMessage('추출할 스크립트 내용이 없습니다.');
return null;
}
let fullTranscript = "";
transcriptSegments.forEach((segment, index) => {
if (segment && segment.textContent) {
const text = segment.textContent.trim();
if (text) {
fullTranscript += text + "\n"; // 각 줄을 새 줄로 구분
// 처음 3개 세그먼트만 디버깅 로그 출력
if (index < 3) {
console.log(`[AI 요약] 디버깅: 세그먼트 ${index + 1}: "${text.substring(0, 50)}..."`);
}
}
}
});
if (fullTranscript.trim() === "") {
console.warn('[AI 요약] 스크립트 내용은 추출되었으나 비어있습니다.');
showErrorMessage('스크립트 내용이 비어있습니다.');
return null;
}
const finalTranscript = fullTranscript.trim();
console.log(`[AI 요약] 스크립트 추출 완료 - 총 길이: ${finalTranscript.length}자`);
console.log(`[AI 요약] 스크립트 미리보기 (처음 200자): ${finalTranscript.substring(0, 200)}...`);
hideErrorMessage();
return finalTranscript;
}
/**
* 스크립트 패널이 열려있는지 확인합니다.
* @returns {boolean} 스크립트 패널이 열려있으면 true
*/
function isTranscriptPanelOpen() {
// 스크립트 패널이 열려있으면 ytd-transcript-renderer가 존재하고 visible 상태
const transcriptRenderer = document.querySelector('ytd-transcript-renderer');
if (!transcriptRenderer) {
console.log('[AI 요약] 디버깅: 스크립트 렌더러가 존재하지 않음');
return false;
}
// 스타일이나 속성으로 숨겨져 있는지 확인
const style = window.getComputedStyle(transcriptRenderer);
const isVisible = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
console.log('[AI 요약] 디버깅: 스크립트 패널 상태 - 존재:', !!transcriptRenderer, '보임:', isVisible);
return isVisible;
}
// --- (fetchAndDisplaySummary, handleRefreshSummary, loadAndDisplaySummary 등 나머지 함수들은 이전과 거의 동일) ---
// ... (이전 코드 내용) ...
// 단, getSettingsFromStorage, storeSummary, getStoredSummary 등은 그대로 유지됩니다.
/**
* 새로운 비디오로 변경되었을 때 UI를 업데이트합니다.
* (예: 이전 요약 지우기, 새 요약 로드)
*/
function updateUIForNewVideo() {
const newVideoId = getCurrentVideoId();
if (newVideoId && newVideoId !== currentVideoId) {
console.log(`[AI 요약] 새 비디오 감지: ${currentVideoId} → ${newVideoId}`);
currentVideoId = newVideoId;
// 기존 요약 내용 초기화
currentSummaryText = '';
currentCommentsSummaryText = '';
// 댓글 수집 데이터 초기화
collectedComments = [];
commentCollectionProgress = { current: 0, total: 100 };
isCollectingComments = false;
// 로딩 타이머 정리
if (loadingTimer) {
clearInterval(loadingTimer);
loadingTimer = null;
}
loadingStartTime = null;
if (commentsLoadingTimer) {
clearInterval(commentsLoadingTimer);
commentsLoadingTimer = null;
}
commentsLoadingStartTime = null;
const summaryContent = document.getElementById(SUMMARY_CONTENT_ID);
const commentsContent = document.getElementById(COMMENTS_CONTENT_ID);
if (summaryContent) {
summaryContent.innerHTML = '';
}
if (commentsContent) {
const commentsTextElement = document.getElementById('comments-summary-text');
const commentsLoadingElement = document.getElementById('comments-summary-loading');
const commentsErrorElement = document.getElementById('comments-summary-error');
if (commentsTextElement) {
commentsTextElement.innerHTML = '';
}
if (commentsLoadingElement) {
commentsLoadingElement.style.display = 'none';
}
if (commentsErrorElement) {
commentsErrorElement.style.display = 'none';
}
}
// 복사 버튼 비활성화
const copyButton = document.getElementById('ai-summary-copy-button');
if (copyButton) {
copyButton.disabled = true;
copyButton.style.opacity = '0.5';
copyButton.title = '요약 내용 복사';
}
// 스크립트 탭으로 전환
switchTab(SCRIPT_TAB_ID);
}
}
// --- 페이지 변경 감지 및 초기화 로직 ---
// observePageChanges, initializeExtension 등 수정
let pageObserver = null; // MutationObserver 인스턴스 저장
/**
* YouTube의 SPA 특성으로 인한 페이지 변경(네비게이션)을 감지하고,
* 필요시 UI를 재삽입하거나 업데이트합니다.
*/
function observePageChanges() {
// YouTube 도메인이 아니면 감시하지 않음
if (!window.location.hostname.includes('youtube.com')) {
console.log('[AI 요약] YouTube가 아닌 도메인에서는 페이지 감시를 시작하지 않습니다.');
return;
}
// yt-navigate-finish 이벤트가 더 안정적일 수 있음
document.addEventListener('yt-navigate-finish', handleNavigation);
// 페이지 포커스/visibility 변경 시에도 UI 상태 체크
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
setTimeout(checkAndRestoreUIFromMain, 1000);
}
});
window.addEventListener('focus', () => {
setTimeout(checkAndRestoreUIFromMain, 1000);
});
// MutationObserver는 스크립트 패널이 동적으로 로드되는 경우를 대비
if (pageObserver) pageObserver.disconnect(); // 기존 옵저버 정리
pageObserver = new MutationObserver((mutationsList, observer) => {
let shouldCheckUI = false;
for (const mutation of mutationsList) {
// ytd-transcript-renderer가 나타났는지 확인
const transcriptRenderer = document.querySelector('ytd-transcript-renderer');
if (transcriptRenderer && !document.getElementById(UI_CONTAINER_ID)) {
console.log('[AI 요약] MutationObserver: ytd-transcript-renderer 감지. UI 초기화 시도.');
initializeExtension(transcriptRenderer);
return;
} else if (!transcriptRenderer && document.getElementById(UI_CONTAINER_ID)) {
// 스크립트 패널이 사라졌는데 우리 UI가 남아있는 경우
console.log('[AI 요약] MutationObserver: ytd-transcript-renderer 사라짐. UI 제거 시도.');
removeUI();
return;
}
// 스크립트 패널 내부 변경사항 감지
if (mutation.type === 'childList') {
const target = mutation.target;
// 스크립트 패널 관련 요소들의 변경 감지
if (target.matches && (
target.matches('ytd-transcript-renderer') ||
target.matches('ytd-transcript-renderer *') ||
target.matches('#content.ytd-transcript-renderer') ||
target.matches('#content.ytd-transcript-renderer *')
)) {
shouldCheckUI = true;
}
// 추가된 노드들 중에 스크립트 관련 요소가 있는지 확인
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
if (addedNode.matches && (
addedNode.matches('ytd-transcript-renderer') ||
addedNode.matches('ytd-transcript-segment-list-renderer') ||
addedNode.querySelector && addedNode.querySelector('ytd-transcript-renderer')
)) {
shouldCheckUI = true;
break;
}
}
}
// 제거된 노드들 중에 우리 UI가 있는지 확인
for (const removedNode of mutation.removedNodes) {
if (removedNode.nodeType === Node.ELEMENT_NODE) {
if (removedNode.id === UI_CONTAINER_ID ||
(removedNode.querySelector && removedNode.querySelector(`#${UI_CONTAINER_ID}`))) {
console.log('[AI 요약] MutationObserver: UI 제거 감지');
shouldCheckUI = true;
break;
}
}
}
}
}
if (shouldCheckUI) {
// 즉시 체크하지 말고 약간의 지연 후 체크 (DOM 변경이 완료된 후)
setTimeout(() => {
const transcriptRenderer = document.querySelector('ytd-transcript-renderer');
if (transcriptRenderer) {
checkAndRestoreUI(transcriptRenderer);
}
}, 500);
}
});
pageObserver.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: false,
attributeOldValue: false,
characterData: false,
characterDataOldValue: false
});
console.log('[AI 요약] 페이지 변경 감시 시작 (MutationObserver for ytd-transcript-renderer).');
// 초기 페이지 로드 시도
const initialTranscriptRenderer = document.querySelector('ytd-transcript-renderer');
if (initialTranscriptRenderer) {
initializeExtension(initialTranscriptRenderer);
}
}
/**
* 메인 스레드에서 UI 상태를 체크하고 복구합니다.
*/
function checkAndRestoreUIFromMain() {
if (!window.location.href.includes('/watch?v=')) {
return;
}
const transcriptRenderer = document.querySelector('ytd-transcript-renderer');
if (transcriptRenderer) {
checkAndRestoreUI(transcriptRenderer);
}
}
function handleNavigation(event) {
console.log('[AI 요약] yt-navigate-finish 이벤트 감지:', event.detail?.pageType, event.detail?.url);
// 이전 UI가 있다면 정리 (필수)
removeUI(); // UI와 참조 변수들 초기화
const isWatchPage = event.detail?.pageType === 'watch' || (event.detail?.url && event.detail.url.includes('/watch?v='));
const isShortsPage = event.detail?.pageType === 'shorts' || (event.detail?.url && event.detail.url.includes('/shorts/')); // Shorts는 스크립트가 거의 없음
if (isWatchPage) { // Shorts 페이지는 스크립트가 일반적이지 않으므로 일단 Watch 페이지만 대상
console.log('[AI 요약] 동영상 시청 페이지로 이동 감지.');
currentVideoId = getVideoIdFromUrl(event.detail?.url); // currentVideoId 업데이트
// ytd-transcript-renderer가 나타날 때까지 기다리거나,
// MutationObserver가 이를 감지하고 initializeExtension을 호출하도록 함.
// 혹은 약간의 지연 후 직접 시도
setTimeout(() => {
const transcriptRenderer = document.querySelector('ytd-transcript-renderer');
if (transcriptRenderer) {
initializeExtension(transcriptRenderer);
} else {
console.log('[AI 요약] 네비게이션 후 ytd-transcript-renderer를 즉시 찾지 못함. MutationObserver가 처리할 수 있음.');
}
}, 500); // YouTube가 DOM을 완전히 업데이트할 시간을 줌
} else {
console.log('[AI 요약] 동영상 시청 페이지가 아님.');
}
}
/**
* 확장 기능의 주요 로직을 시작합니다.
* @param {HTMLElement} transcriptRenderer - ytd-transcript-renderer 요소
*/
function initializeExtension(transcriptRenderer) {
// YouTube 도메인이 아니면 초기화하지 않음
if (!window.location.hostname.includes('youtube.com')) {
console.log('[AI 요약] YouTube가 아닌 도메인에서는 확장프로그램을 초기화하지 않습니다.');
return;
}
if (!transcriptRenderer) {
console.log('[AI 요약] ytd-transcript-renderer가 없어 초기화 실패.');
return;
}
// 현재 비디오 ID 가져오기 및 업데이트
currentVideoId = getCurrentVideoId(); // 현재 URL 기준
if (!currentVideoId && window.location.href.includes('/watch?v=')) {
// yt-navigate-finish에서 이미 설정했거나, 여기서 다시 가져옴
currentVideoId = getVideoIdFromUrl(window.location.href);
}
if (!currentVideoId) {
console.log('[AI 요약] 현재 동영상 ID를 가져올 수 없어 초기화 건너뜁니다.');
return;
}
console.log(`[AI 요약] 확장 기능 초기화 시작 (Video ID: ${currentVideoId})`);
injectUI(transcriptRenderer);
// 주기적 UI 상태 체크 시작
startUIHealthCheck();
// YouTube 탭 전환 감지를 위한 추가 이벤트 설정
setupTabChangeDetection();
}
/**
* YouTube 탭 전환 감지를 위한 이벤트 설정
*/
function setupTabChangeDetection() {
// 기존 이벤트 리스너가 있다면 제거
if (tabChangeClickListener) {
document.removeEventListener('click', tabChangeClickListener, true);
}
if (tabChangeKeyListener) {
document.removeEventListener('keydown', tabChangeKeyListener);
}
// YouTube의 탭 전환은 주로 클릭 이벤트로 발생
tabChangeClickListener = (event) => {
const target = event.target;
// YouTube 탭 버튼 클릭 감지
if (target && (
target.matches('tp-yt-paper-tab') ||
target.matches('tp-yt-paper-tab *') ||
target.matches('[role="tab"]') ||
target.matches('[role="tab"] *') ||
target.closest('tp-yt-paper-tab') ||
target.closest('[role="tab"]')
)) {
// 탭 전환 후 UI 상태 체크
setTimeout(() => {
checkAndRestoreUIFromMain();
}, 2000);
// 추가로 약간 더 지연 후에도 체크 (DOM 변경이 완전히 끝난 후)
setTimeout(() => {
checkAndRestoreUIFromMain();