- Replace Playwright standalone browser with CDP connection to user Chrome (bypasses YouTube bot detection by using logged-in Chrome session) - Add video playback, ad detection/skip, and play confirmation before transcript extraction - Extract transcript JS to separate resource files (fix SyntaxError in evaluate) - Add ytInitialPlayerResponse-based transcript extraction as primary method - Fix token refresh: retry on network error during backend restart - Fix null userId logout, CLOB type hint for structured_content - Disable XFCE screen lock/screensaver - Add troubleshooting entries (#10-12) and YouTube transcript guide Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
310 lines
9.3 KiB
Markdown
310 lines
9.3 KiB
Markdown
# 트러블슈팅 가이드
|
||
|
||
## 1. 프론트엔드 502 Bad Gateway
|
||
|
||
### 증상
|
||
- 브라우저에서 `502 Bad Gateway nginx/1.20.1` 표시
|
||
|
||
### 원인
|
||
Next.js standalone 빌드 시 `server.js`가 `sundol-frontend/` 서브디렉토리에 생성되는 경우가 있음.
|
||
PM2가 `.next/standalone/server.js`를 찾지만 실제 파일은 `.next/standalone/sundol-frontend/server.js`에 위치.
|
||
|
||
### 확인 방법
|
||
```bash
|
||
pm2 list # sundol-frontend 상태 확인 (errored 여부)
|
||
pm2 logs sundol-frontend --err --lines 20 # 에러 로그 확인
|
||
# "Cannot find module .../standalone/server.js" 에러가 나면 이 문제
|
||
```
|
||
|
||
### 해결
|
||
```bash
|
||
# server.js 심볼릭 링크 생성
|
||
ln -sf /home/opc/sundol/sundol-frontend/.next/standalone/sundol-frontend/server.js \
|
||
/home/opc/sundol/sundol-frontend/.next/standalone/server.js
|
||
|
||
pm2 restart sundol-frontend
|
||
```
|
||
|
||
> `build.sh`에 이미 자동 처리 로직 포함되어 있음. 문제가 반복되면 build.sh 확인.
|
||
|
||
---
|
||
|
||
## 2. 프론트엔드 static 파일 404
|
||
|
||
### 증상
|
||
- 페이지는 로드되지만 CSS/JS가 404
|
||
- 브라우저 콘솔에 `_next/static/chunks/...js net::ERR_ABORTED 404` 다수 표시
|
||
|
||
### 원인
|
||
standalone 빌드에서 `.next/static` 심볼릭 링크가 올바른 위치에 걸리지 않음.
|
||
`server.js`가 `standalone/sundol-frontend/` 안에서 실행되므로 static도 그 안에 있어야 함.
|
||
|
||
### 해결
|
||
```bash
|
||
# 중첩 디렉토리에도 static 링크 생성
|
||
ln -sf /home/opc/sundol/sundol-frontend/.next/static \
|
||
/home/opc/sundol/sundol-frontend/.next/standalone/sundol-frontend/.next/static
|
||
|
||
pm2 restart sundol-frontend
|
||
```
|
||
|
||
> `build.sh`에 자동 처리 로직 포함되어 있음.
|
||
|
||
---
|
||
|
||
## 3. 브라우저 로그인 모달 (HTTP Basic Auth 팝업)
|
||
|
||
### 증상
|
||
- 페이지 리프레시 시 브라우저 기본 로그인 팝업(username/password)이 뜸
|
||
- 특히 JWT 토큰이 만료되었거나 없을 때 발생
|
||
|
||
### 원인
|
||
Spring Security가 401 응답에 `WWW-Authenticate: Basic` 헤더를 포함.
|
||
브라우저가 이 헤더를 감지하면 자동으로 Basic Auth 로그인 다이얼로그를 표시.
|
||
|
||
### 해결
|
||
`SecurityConfig.java`에 커스텀 `authenticationEntryPoint` 설정:
|
||
|
||
```java
|
||
.exceptionHandling(exceptions -> exceptions
|
||
.authenticationEntryPoint((exchange, ex) -> {
|
||
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
|
||
return exchange.getResponse().setComplete();
|
||
})
|
||
)
|
||
```
|
||
|
||
이렇게 하면 `WWW-Authenticate` 헤더 없이 401만 반환되어 브라우저 팝업이 뜨지 않음.
|
||
프론트엔드의 axios 인터셉터가 401을 감지하여 자동으로 토큰 갱신 처리.
|
||
|
||
---
|
||
|
||
## 4. YouTube 자막 가져오기 실패 (Caption XML 0 chars)
|
||
|
||
### 증상
|
||
- YouTube 트랜스크립트 가져오기 시 "자막 텍스트를 파싱할 수 없습니다" 에러
|
||
- 로그에 `Caption XML fetched: 0 chars` 표시
|
||
|
||
### 원인
|
||
YouTube timedtext API의 caption URL이 서명 기반으로, 브라우저 세션 외부에서 요청하면 빈 응답 반환.
|
||
`HttpURLConnection`이나 `context.request().get()`으로는 쿠키/세션이 유지되지 않음.
|
||
|
||
### 해결
|
||
두 가지 방법으로 fallback 처리:
|
||
|
||
1. **방법 A (우선)**: YouTube 페이지에서 '스크립트 표시' 패널을 열어 DOM에서 직접 텍스트 추출
|
||
2. **방법 B**: caption URL에 `&fmt=json3` 추가 후 `page.evaluate(fetch())`로 브라우저 내에서 요청
|
||
|
||
자세한 내용은 [crawling-guide.md](crawling-guide.md) 참조.
|
||
|
||
---
|
||
|
||
## 5. Playwright 브라우저 창이 사라짐
|
||
|
||
### 증상
|
||
- VNC에서 Playwright Chromium 창이 보이다가 사라짐
|
||
|
||
### 원인
|
||
`page.close()` 호출 시 마지막 탭이 닫히면 브라우저 창이 빈 상태가 됨.
|
||
|
||
### 해결
|
||
`page.close()` 대신 `page.navigate("about:blank")`으로 변경하여 탭을 유지:
|
||
|
||
```java
|
||
try {
|
||
page.navigate("about:blank");
|
||
} catch (Exception ignored) {
|
||
page.close();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Playwright 의존 라이브러리 누락 경고
|
||
|
||
### 증상
|
||
- 백엔드 로그에 `Host system is missing dependencies to run browsers` 경고
|
||
- `libicudata.so.66`, `libwoff2dec.so.1.0.2` 등 누락 표시
|
||
|
||
### 원인
|
||
Playwright가 WebKit 브라우저용 의존성까지 검사함. Chromium만 사용하면 대부분 문제없음.
|
||
|
||
### 확인
|
||
```bash
|
||
# Chromium 실행 가능 여부 직접 테스트
|
||
/home/opc/.cache/ms-playwright/chromium-1161/chrome-linux/chrome --version
|
||
```
|
||
|
||
### 해결 (경고 제거하려면)
|
||
```bash
|
||
sudo dnf install -y libicu woff2 harfbuzz-icu libjpeg-turbo libwebp enchant2 hyphen libffi
|
||
```
|
||
|
||
> `libx264`는 WebKit 전용이므로 Chromium만 사용 시 불필요.
|
||
|
||
---
|
||
|
||
## 7. Git Push 인증 실패
|
||
|
||
### 증상
|
||
- `git push origin main` 시 `could not read Username` 에러
|
||
|
||
### 원인
|
||
`.netrc`이나 `.git-credentials`에 인증 정보가 없음.
|
||
|
||
### 해결
|
||
`.env`의 `GIT_USER`, `GIT_PASSWORD` 사용 (비밀번호에 특수문자 포함 시 URL 인코딩 필요):
|
||
|
||
```bash
|
||
set -a && source /home/opc/sundol/.env && set +a
|
||
ENCODED_PW=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$GIT_PASSWORD', safe=''))")
|
||
git push "https://${GIT_USER}:${ENCODED_PW}@gittea.cloud-handson.com/joungmin/sundol.git" main
|
||
```
|
||
|
||
---
|
||
|
||
## 8. LLM 구조화 내용이 잘림
|
||
|
||
### 증상
|
||
- 지식 정리(구조화) 결과가 중간에 끊김
|
||
|
||
### 원인
|
||
OCI GenAI의 `maxTokens`가 4096으로 제한되어 있어 긴 콘텐츠 정리 시 응답이 잘림.
|
||
|
||
### 해결
|
||
한 번에 전체를 정리하지 않고, 나눠서 요청하는 방식으로 변경:
|
||
|
||
1. **1차 호출**: Abstract + 목차만 생성
|
||
2. **2차~ 호출**: 목차 항목별로 상세 정리 요청 (각각 maxTokens 4096 내에서 충분)
|
||
3. **최종 조합**: 모든 결과를 합침
|
||
|
||
각 섹션 완료 시 DB에 중간 저장하여 프론트엔드에서 실시간 확인 가능.
|
||
|
||
---
|
||
|
||
## 9. VNC 접속 안 됨
|
||
|
||
### 증상
|
||
- VNC 클라이언트에서 접속 불가
|
||
|
||
### 확인
|
||
```bash
|
||
# VNC 서버 실행 확인
|
||
vncserver -list
|
||
|
||
# 방화벽 확인
|
||
sudo firewall-cmd --list-ports
|
||
|
||
# OCI 보안 목록에서 TCP 5901 인바운드 허용 여부 확인
|
||
```
|
||
|
||
### 해결
|
||
```bash
|
||
# VNC 서버 재시작
|
||
vncserver -kill :1
|
||
vncserver :1 -geometry 1920x1080 -depth 24
|
||
|
||
# 방화벽 포트 열기
|
||
sudo firewall-cmd --permanent --add-port=5901/tcp
|
||
sudo firewall-cmd --reload
|
||
```
|
||
|
||
자세한 내용은 [setup-xwindow.md](setup-xwindow.md) 참조.
|
||
|
||
---
|
||
|
||
## 10. VNC 접속 시 스크린 락(화면 잠금) 걸림
|
||
|
||
### 증상
|
||
- VNC로 접속하면 검은 화면에 로그인 다이얼로그가 뜸
|
||
- 잠시 자리를 비운 뒤 접속하면 사용자 비밀번호를 요구
|
||
|
||
### 원인
|
||
XFCE 스크린세이버가 기본 활성화되어 있어, 일정 시간 비활성 후 화면을 잠금.
|
||
|
||
### 해결
|
||
|
||
```bash
|
||
export DISPLAY=:1
|
||
|
||
# 스크린세이버 비활성화
|
||
xfconf-query -c xfce4-screensaver -p /saver/enabled --create -t bool -s false
|
||
|
||
# 화면 잠금 비활성화
|
||
xfconf-query -c xfce4-screensaver -p /lock/enabled --create -t bool -s false
|
||
xfconf-query -c xfce4-screensaver -p /lock/saver-activation/enabled --create -t bool -s false
|
||
|
||
# 모니터 절전(DPMS) 비활성화
|
||
xfconf-query -c xfce4-power-manager -p /xfce4-power-manager/dpms-enabled --create -t bool -s false
|
||
xfconf-query -c xfce4-power-manager -p /xfce4-power-manager/lock-screen-suspend-hibernate --create -t bool -s false
|
||
|
||
# 잠금 명령 제거
|
||
xfconf-query -c xfce4-session -p /general/LockCommand --create -t string -s ""
|
||
|
||
# 실행 중인 screensaver 프로세스 종료
|
||
ps aux | grep -E 'xfce4-screensaver|screensaver-dialog' | grep -v grep | awk '{print $2}' | xargs kill 2>/dev/null
|
||
```
|
||
|
||
---
|
||
|
||
## 11. YouTube 자막 추출 시 광고 때문에 실패
|
||
|
||
### 증상
|
||
- Playwright로 YouTube 페이지 로드 후 자막 패널이 열리지 않음
|
||
- VNC에서 보면 광고가 재생 중
|
||
|
||
### 원인
|
||
YouTube 동영상 앞에 프리롤 광고가 삽입되면 본 영상 UI(자막 패널 등)가 활성화되지 않음.
|
||
|
||
### 해결
|
||
코드에 `waitForAdsToFinish()` 로직이 포함되어 있음:
|
||
- 최대 60초간 2초 간격으로 광고 상태 확인
|
||
- 스킵 버튼(`.ytp-skip-ad-button` 등)이 나타나면 자동 클릭
|
||
- 광고가 끝나면 자막 추출 진행
|
||
|
||
수동으로 확인하려면 VNC에서 Playwright 브라우저 창을 직접 관찰.
|
||
|
||
---
|
||
|
||
## 12. 백엔드 재시작 시 로그인이 풀림
|
||
|
||
### 증상
|
||
- 백엔드(`pm2 restart sundol-backend`) 후 프론트엔드에서 로그인 화면으로 튕김
|
||
- 로그에 `User logged out: null` 표시
|
||
|
||
### 원인
|
||
1. 백엔드 재시작 중 프론트엔드가 API 호출 → 연결 실패(네트워크 에러)
|
||
2. axios 인터셉터가 401로 인식 → refresh 시도 → 서버 아직 안 떠서 실패
|
||
3. `onRefreshFailed` 콜백 → 자동 로그아웃 실행
|
||
|
||
### 해결
|
||
프론트엔드 axios 인터셉터에서 refresh 실패 시 네트워크 에러이면 3초 후 최대 2회 재시도:
|
||
|
||
```typescript
|
||
const attemptRefresh = async (retryCount: number): Promise<string> => {
|
||
try {
|
||
const res = await api.post<LoginResponse>("/api/auth/refresh");
|
||
return res.data.accessToken;
|
||
} catch (err) {
|
||
const isNetworkError = !((err as AxiosError).response);
|
||
if (isNetworkError && retryCount < 2) {
|
||
await new Promise((r) => setTimeout(r, 3000));
|
||
return attemptRefresh(retryCount + 1);
|
||
}
|
||
throw err;
|
||
}
|
||
};
|
||
```
|
||
|
||
백엔드 logout에도 userId null 체크 추가:
|
||
```java
|
||
if (userId == null) {
|
||
log.warn("Logout called with null userId, ignoring");
|
||
return;
|
||
}
|
||
```
|
||
|
||
### 예방
|
||
- 백엔드 재시작 시 기동 완료까지 약 10~25초 소요
|
||
- 프론트엔드는 네트워크 에러 시 최대 9초(3초 × 3회) 대기 후 재시도
|