Skip to content

Commit 0bc26fd

Browse files
authored
Merge pull request #123 from GulSam00/develop
Hotfix auth redirect
2 parents ccc62be + 7ed8004 commit 0bc26fd

8 files changed

Lines changed: 292 additions & 106 deletions

File tree

.github/temp/update-ky-youtube.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ jobs:
2020
ref: feat/songUpdate
2121
persist-credentials: false # 수동 인증으로 푸시 제어
2222

23-
- name: Use Node.js 18
23+
- name: Use Node.js 20
2424
uses: actions/setup-node@v4
2525
with:
26-
node-version: "18"
26+
node-version: "20"
2727

2828
- name: Install pnpm
2929
uses: pnpm/action-setup@v2

.github/workflows/crawl-recent-tj.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ jobs:
1212
steps:
1313
- uses: actions/checkout@v4
1414

15-
- name: Use Node.js 18
15+
- name: Use Node.js 20
1616
uses: actions/setup-node@v4
1717
with:
18-
node-version: "18"
18+
node-version: "20"
1919

2020
- name: Install pnpm
2121
uses: pnpm/action-setup@v2
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Update ky by Youtube
2+
3+
# 실행 일시 중지
4+
on:
5+
schedule:
6+
- cron: "0 14 * * *" # 한국 시간 23:00 실행 (UTC+9 → UTC 14:00)
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write # push 권한을 위해 필요
11+
12+
jobs:
13+
run-npm-task:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout branch
18+
uses: actions/checkout@v4
19+
20+
- name: Use Node.js 20
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: "20"
24+
25+
- name: Install pnpm
26+
uses: pnpm/action-setup@v2
27+
with:
28+
version: 9
29+
run_install: false
30+
31+
- name: Install dependencies
32+
working-directory: packages/crawling
33+
run: pnpm install
34+
35+
- name: Create .env file
36+
working-directory: packages/crawling
37+
run: |
38+
echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> .env
39+
echo "SUPABASE_KEY=${{ secrets.SUPABASE_KEY }}" >> .env
40+
41+
- name: run update script - packages/crawling/crawlYoutube.ts
42+
working-directory: packages/crawling
43+
run: pnpm run ky-youtube

apps/web/src/auth.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ export default function AuthProvider({ children }: { children: React.ReactNode }
3030
return;
3131
}
3232

33-
// if (isPublicPath) {
34-
// setIsAuthChecked(true);
35-
// return;
36-
// }
33+
if (isPublicPath) {
34+
setIsAuthChecked(true);
35+
return;
36+
}
3737

3838
// 이미 인증된 상태면 바로 통과 (하지만 체크는 수행)
3939
const handleAuth = async () => {
Lines changed: 130 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,159 @@
11
import * as cheerio from 'cheerio';
2-
import puppeteer from 'puppeteer';
2+
import puppeteer, { Browser, Page } from 'puppeteer';
33

44
import { getInvalidKYSongsDB, getSongsKyNullDB } from '@/supabase/getDB';
55
import { postInvalidKYSongsDB } from '@/supabase/postDB';
66
import { updateSongsKyDB } from '@/supabase/updateDB';
77
import { Song } from '@/types';
8-
import { saveCrawlYoutubeFailedKYSongs, updateDataLog } from '@/utils/logData';
98

109
import { isValidKYExistNumber } from './isValidKYExistNumber';
1110

12-
// youtube에서 KY 노래방 번호 크롤링
13-
// crawlYoutubeValid에서 진행하는 실제 사이트 검증도 포함
11+
// --- Constants ---
12+
const BASE_YOUTUBE_SEARCH_URL = 'https://www.youtube.com/@KARAOKEKY/search';
13+
// --- Helper Functions ---
1414

15-
// action 우분투 환경에서의 호환을 위해 추가
16-
const browser = await puppeteer.launch({
17-
headless: true,
18-
});
19-
20-
const page = await browser.newPage();
21-
22-
const baseUrl = 'https://www.youtube.com/@KARAOKEKY/search';
23-
24-
const scrapeSongNumber = async (query: string) => {
25-
const searchUrl = `${baseUrl}?query=${encodeURIComponent(query)}`;
26-
27-
// page.goto의 waitUntil 문제였음!
28-
await page.goto(searchUrl, {
29-
waitUntil: 'networkidle2',
30-
timeout: 0,
31-
});
15+
/**
16+
* 텍스트에서 KY 노래방 번호를 추출합니다.
17+
*/
18+
const extractKaraokeNumber = (title: string): string | null => {
19+
const matchResult = title.match(/KY\.\s*(\d{2,5})\)/);
20+
return matchResult ? matchResult[1] : null;
21+
};
3222

33-
const html = await page.content();
34-
const $ = cheerio.load(html);
23+
/**
24+
* 유튜브 검색 결과 페이지에서 노래 번호를 스크래핑합니다.
25+
*/
26+
const scrapeSongNumber = async (page: Page, query: string): Promise<string | null> => {
27+
const searchUrl = `${BASE_YOUTUBE_SEARCH_URL}?query=${encodeURIComponent(query)}`;
3528

36-
// id contents 의 첫번째 ytd-item-section-renderer 찾기
37-
// const firstItem = $("#contents ytd-item-section-renderer").first();
29+
try {
30+
// waitUntil을 통해 네트워크가 안정될 때까지 대기
31+
// 30초 타임아웃 설정 (무한 대기 방지)
32+
await page.goto(searchUrl, {
33+
waitUntil: 'networkidle2',
34+
// timeout: 0,
35+
});
3836

39-
const firstItem = $('ytd-video-renderer').first();
37+
const html = await page.content();
38+
const $ = cheerio.load(html);
4039

41-
// yt-formatted-string 찾기
42-
const title = firstItem.find('yt-formatted-string').first().text().trim();
40+
const firstItem = $('ytd-video-renderer').first();
4341

44-
const karaokeNumber = extractKaraokeNumber(title);
42+
// 검색 결과가 없는 경우 처리
43+
if (firstItem.length === 0) {
44+
return null;
45+
}
4546

46-
return karaokeNumber;
47+
const title = firstItem.find('yt-formatted-string').first().text().trim();
48+
return extractKaraokeNumber(title);
49+
} catch (error) {
50+
console.warn(`[Scraping Failed] Query: ${query}`, error);
51+
return null;
52+
}
4753
};
4854

49-
const extractKaraokeNumber = (title: string) => {
50-
// KY. 찾고 ) 가 올때까지 찾기
51-
const matchResult = title.match(/KY\.\s*(\d{2,5})\)/);
52-
const karaokeNumber = matchResult ? matchResult[1] : null;
53-
return karaokeNumber;
55+
/**
56+
* 성공한 데이터를 DB에 업데이트하고 로그를 남깁니다.
57+
*/
58+
const handleSuccess = async (song: Song, kyNum: string) => {
59+
const result = await updateSongsKyDB({ ...song, num_ky: kyNum });
60+
// console.log(`[Update Success] ${song.title}: ${kyNum}`, result); // 로그 너무 많으면 주석 처리
61+
// updateDataLog(result.success, 'crawlYoutubeSuccess.txt');
5462
};
5563

56-
const updateData = async (data: Song) => {
57-
const result = await updateSongsKyDB(data);
58-
console.log(result);
59-
updateDataLog(result.success, 'crawlYoutubeSuccess.txt');
60-
updateDataLog(result.failed, 'crawlYoutubeFailed.txt');
64+
/**
65+
* 실패한 데이터를 Invalid DB에 저장하고 로그를 남깁니다.
66+
*/
67+
const handleFailure = async (song: Song) => {
68+
await postInvalidKYSongsDB(song);
69+
// updateDataLog(false, 'crawlYoutubeFailed.txt'); // false 로그 처리 방식에 따라 수정 필요
6170
};
6271

63-
// failedSongs을 가져와서 실패한 노래를 건너뛰는 게 아니라 실패 시 update_date를 수정해 작업 순위를 뒤로 미룬다면?
64-
const data = await getSongsKyNullDB();
65-
const failedSongs = await getInvalidKYSongsDB();
72+
// --- Main Logic ---
6673

67-
console.log('getSongsKyNullDB : ', data.length);
68-
console.log('failedSongs : ', failedSongs.length);
69-
let index = 0;
70-
let successCount = 0;
74+
const main = async () => {
75+
console.log('🚀 크롤링 작업을 시작합니다...');
7176

72-
for (const song of data) {
73-
if (failedSongs.find(failedSong => failedSong.id === song.id)) {
74-
continue;
75-
}
76-
const query = song.title + '-' + song.artist;
77+
// 1. 브라우저 초기화
78+
const browser = await puppeteer.launch({
79+
headless: true,
80+
args: ['--no-sandbox', '--disable-setuid-sandbox'], // 리눅스 환경 호환성
81+
});
7782

78-
let resultKyNum = null;
7983
try {
80-
resultKyNum = await scrapeSongNumber(query);
81-
} catch (error) {
82-
continue;
83-
}
84-
85-
if (resultKyNum) {
86-
let isValid = true;
87-
try {
88-
isValid = await isValidKYExistNumber(page, resultKyNum, song.title, song.artist);
89-
} catch (error) {
90-
continue;
84+
const page = await browser.newPage();
85+
86+
// 2. 데이터 가져오기
87+
// Promise.all로 병렬 요청하여 대기 시간 단축
88+
const [targetSongs, failedSongs] = await Promise.all([
89+
getSongsKyNullDB(),
90+
getInvalidKYSongsDB(),
91+
]);
92+
93+
console.log(`📊 ky가 null인 대상 곡: ${targetSongs.length}개`);
94+
console.log(`🚫 이미 실패한 곡(유효하지 않은 KY 노래방 번호): ${failedSongs.length}개`);
95+
console.log(`🎯 추가 가능한 최대 곡 개수: ${targetSongs.length - failedSongs.length}개`);
96+
97+
// 3. 최적화: 실패한 곡 ID를 Set으로 변환 (검색 속도 O(1)로 향상)
98+
const failedSongIds = new Set(failedSongs.map(s => s.id));
99+
100+
let processedCount = 0;
101+
let successCount = 0;
102+
103+
// 4. 순차 처리 루프
104+
for (const song of targetSongs) {
105+
processedCount++;
106+
const query = `${song.title}-${song.artist}`;
107+
108+
// 4-1. 이미 실패했던 곡은 스킵
109+
if (failedSongIds.has(song.id)) {
110+
continue;
111+
}
112+
113+
console.log(`[${processedCount}/${targetSongs.length}] 검색 중: ${query}`);
114+
115+
// 4-2. 스크래핑 시도
116+
const resultKyNum = await scrapeSongNumber(page, query);
117+
118+
if (!resultKyNum) {
119+
// 검색 결과 없음 -> 실패 처리
120+
console.log(`❌ 검색 결과 없음: ${query}`);
121+
await handleFailure(song);
122+
continue;
123+
}
124+
125+
// 4-3. 번호 유효성 검증 (실제 존재하는 번호인지 2차 확인)
126+
let isValid = false;
127+
try {
128+
isValid = await isValidKYExistNumber(page, resultKyNum, song.title, song.artist);
129+
} catch (error) {
130+
console.error(`❌ 검증 중 에러 발생: ${query}`, error);
131+
// 검증 에러 시 일단 실패 처리하거나 continue
132+
continue;
133+
}
134+
135+
if (isValid) {
136+
// 성공 처리
137+
await handleSuccess(song, resultKyNum);
138+
successCount++;
139+
console.log(`✅ 업데이트 완료: ${resultKyNum}`);
140+
} else {
141+
// 유효하지 않은 번호 -> 실패 처리
142+
await handleFailure(song);
143+
console.log(`⚠️ 유효하지 않은 번호: ${resultKyNum}`);
144+
}
91145
}
92146

93-
if (!isValid) {
94-
await postInvalidKYSongsDB(song);
95-
continue;
96-
} else {
97-
await updateData({ ...song, num_ky: resultKyNum });
98-
console.log('update song : ', resultKyNum);
99-
successCount++;
100-
}
101-
} else await postInvalidKYSongsDB(song);
102-
103-
index++;
104-
console.log(query);
105-
console.log('scrapeSongNumber : ', index);
106-
console.log('successCount : ', successCount);
107-
}
147+
console.log('------------------------------------------------');
148+
console.log(`🎉 모든 작업 완료! 총 성공: ${successCount}건`);
149+
} catch (error) {
150+
console.error('🔥 치명적인 에러 발생:', error);
151+
} finally {
152+
// 5. 종료 처리: 에러가 나든 안 나든 브라우저는 반드시 닫음
153+
await browser.close();
154+
console.log('🔒 브라우저 종료됨');
155+
}
156+
};
108157

109-
browser.close();
158+
// 스크립트 실행
159+
main();

0 commit comments

Comments
 (0)