# Known Issues & Lessons Learned > **이 파일은 SSOT(Single Source of Truth)입니다.** > 디버깅이나 구현 전에 **반드시** 이 파일을 확인하세요. > 세션 종료 시 새로 발견된 이슈를 이 파일에 추가합니다. --- ## 포맷 각 항목은 아래 형식을 따릅니다: ```markdown ### [날짜] [키워드] — 한줄 요약 - **증상**: 무엇이 잘못되었는가 - **원인**: 근본 원인 - **해결**: 올바른 해결 방법 - **주의**: 재발 방지를 위한 교훈 ``` --- ## 공통 이슈 ### [2026-03-08] PowerShell curl — Invoke-WebRequest 충돌 - **증상**: `curl` 명령이 예상과 다른 응답 형식을 반환 - **원인**: PowerShell에서 `curl`은 `Invoke-WebRequest`의 별칭 - **해결**: **`curl.exe`**를 명시적으로 사용 - **주의**: HTTP 관련 모든 명령에서 `curl.exe` 사용 필수 ### [2026-03-08] PowerShell npm — 실행 정책 오류 - **증상**: `npm run` 명령이 `실행 정책` 관련 오류로 실패 - **원인**: PowerShell 스크립트 실행 정책이 제한적으로 설정됨 - **해결**: `cmd /c npm run dev` 형식으로 cmd를 통해 실행 - **주의**: npm 관련 명령은 항상 `cmd /c` 접두어 사용 권장 --- ## 프로젝트별 이슈 > 아래에 프로젝트 특화 이슈를 추가하세요. ### [2026-03-08] nas_scanner docstring — Unicode escape SyntaxError - **증상**: `from tools.nas_scanner import NasScanner` 시 `SyntaxError: (unicode error) 'unicodeescape' codec can't decode \N` - **원인**: docstring 내 `\\NasData` 경로에서 `\N`이 Python Unicode named escape로 해석 - **해결**: docstring을 `r"""..."""` (raw string)으로 변경 - **주의**: Windows 경로(`\\`, `\N`, `\U` 등)가 포함된 docstring은 반드시 `r"""`로 작성 ### [2026-03-08] title_matcher _kata_to_hira — 장음기호 깨짐 - **증상**: `フリーレン` → `ふり゜れん` (ー가 ゜로 변환) - **원인**: 카타카나 범위 `0x30A0~0x30FF`에 기호 문자(`ー` U+30FC) 포함 - **해결**: 범위를 `0x30A1~0x30F6`으로 좁혀 실제 문자만 변환 - **주의**: 유니코드 범위 지정 시 기호/구두점 문자 포함 여부 확인 필수 ### [2026-03-08] anime_pipeline — Nyaa 검색 0건 반환 - **증상**: 한자 포함 원제의 로마자 변환 결과(`葬送nofuriren`) + suffix 고정으로 Nyaa 검색 실패 - **원인**: 단일 검색 전략, suffix(ASW HEVC) 항상 부착 - **해결**: 6단계 fallback 전략 (romaji±suffix → 원제±suffix → 한글±suffix) - **주의**: 외부 API 검색 시 반드시 다중 전략 + suffix 토글 구현 ### [2026-03-15] _extract_episode — v2/S01E10 패턴 미인식 → 중복 다운로드 - **증상**: NAS에 ep9, 10이 있는데 재다운로드. ASW `- 10v2` 릴리스 에피소드 추출 실패 - **원인**: 정규식 `[-–]\s*(\d{1,4})(?:\s|$|\.|\[)`이 v2 접미사 미처리. `S01E10`은 하이픈 패턴이 `S01`의 `01`을 먼저 매칭 - **해결**: (1) SxxExx 패턴을 최우선 체크, (2) `(?:v\d)?` 추가로 version suffix 허용, (3) `\(` 추가로 SubsPlease 포맷 지원 - **주의**: 에피소드 추출 정규식 수정 시 반드시 v2/v3 릴리스 + SxxExx + 한글(N화) + false positive(29-sai) 테스트 포함 ### [2026-03-15] _add_torrents — 릴리스 그룹 불일치 다운로드 - **증상**: NAS에 `[ASW] HEVC` 파일(~300MB)만 있는데 `CR WEB-DL DUAL`(1.4GB) 릴리스를 다운 - **원인**: 스코어링이 ASW에 +100을 주지만, ASW 릴리스가 없는 에피소드에서 아무 릴리스나 선택 - **해결**: NAS 기존 파일의 릴리스 그룹(`[ASW]`)을 감지하여 같은 그룹만 허용. 매칭 없으면 스킵 - **주의**: Nyaa 토렌트 제목에 영어+일본어 제목이 모두 포함되어 키워드 필터만으로는 불충분 ### [2026-03-15] _download_subtitles — 기존 자막 덮어쓰기 위험 - **증상**: 이미 수동으로 배치한 자막 파일을 Anissia 자막으로 덮어쓸 수 있음 - **원인**: 기존 자막 파일 존재 여부를 확인하지 않고 전 에피소드 자막 다운로드 시도 - **해결**: NAS 폴더의 기존 자막 파일을 에피소드별로 스캔, 이미 있으면 스킵 - **주의**: 자막 처리 시 사용자 수동 입력 파일의 보존을 항상 고려 ### [2026-03-15] Wiki.js GraphQL — update mutation에 tags 누락 시 에러 - **증상**: `update_page()` 호출 시 `Cannot read properties of undefined` 백엔드 에러 - **원인**: Wiki.js `update` mutation이 `tags` 파라미터 생략 시 내부적으로 undefined 처리하여 crash - **해결**: `update_page()`에서 `tags`가 None이면 `get_page()`로 기존 tags를 먼저 조회하여 항상 전달 - **주의**: Wiki.js GraphQL mutation은 optional로 보이는 필드도 생략 시 에러 가능. 항상 모든 필드를 명시적으로 전달 ### [2026-03-16] main.py StreamHandler — cp949 콘솔에서 한글/특수문자 UnicodeEncodeError - **증상**: 봇 기동 시 `UnicodeEncodeError: 'cp949' codec can't encode character '\u2014'` 로 프로세스 비정상 종료 - **원인**: `logging.StreamHandler(sys.stdout)` 기본값이 시스템 인코딩(cp949) 사용. 로그 메시지의 em-dash(`—`) 등 유니코드 문자가 인코딩 불가 - **해결**: `io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")`로 StreamHandler를 UTF-8 고정 - **주의**: Windows 한글 환경에서 **모든 콘솔 출력**에 cp949 인코딩 문제 발생 가능. `subprocess.run`도 `encoding="utf-8", errors="replace"` 명시 필수 ### [2026-03-16] workspaces.json — 다른 PC 사용자 경로로 봇 기동 실패 - **증상**: `[WinError 267] 디렉터리 이름이 올바르지 않습니다` — Gemini agent 호출 시 cwd 오류 - **원인**: `workspaces.json`의 path가 다른 PC의 사용자 경로(`c:\Users\Certes\...`)로 하드코딩 - **해결**: 현재 머신의 사용자 경로(`c:\Users\Variet-Worker\...`)로 수정 - **주의**: 멀티 환경 배포 시 workspaces.json의 절대 경로가 환경별로 다를 수 있음. 상대 경로 또는 환경변수 사용 고려 ### [2026-03-18] config.py — .env 값의 따옴표가 값에 포함됨 - **증상**: `.env`에 `KEY="val,ue"` 형식으로 쓰면 비밀번호에 `"` 따옴표가 포함되어 인증 실패 - **원인**: `config.py`의 수동 `.env` 파서가 `value.strip()`만 하고 따옴표를 제거하지 않음 - **해결**: 양쪽 따옴표(`"..."` 또는 `'...'`) 감지 후 제거하는 로직 추가 - **주의**: `.env`에 특수문자(`,`, `&`, `#` 등) 포함 비밀번호는 반드시 따옴표로 감싸야 함 ### [2026-03-18] Mailcow IMAP — 앱 비밀번호 LOGIN 커맨드 거부 - **증상**: `imaplib.login()` 호출 시 `AUTHENTICATIONFAILED` — 비밀번호 맞는데도 실패 - **원인**: Mailcow 앱 비밀번호는 IMAP `LOGIN` 커맨드를 거부하고 `PLAIN` 인증만 지원 - **해결**: `conn.authenticate("PLAIN", lambda x: ("\0" + user + "\0" + pw).encode())` 사용 - **주의**: 일반 계정 비밀번호는 `LOGIN`도 가능하지만, 앱 비밀번호는 반드시 `PLAIN` auth 필요 ### [2026-03-18] WebDAV SEARCH — Nextcloud 501 Not Implemented - **증상**: `nc_files.search()` 결과 0건, 로그에 `WebDAV SEARCH 실패: 501` - **원인**: Nextcloud 인스턴스가 WebDAV `SEARCH` 메서드를 지원하지 않음 - **해결**: SEARCH 실패 시 PROPFIND depth=99 → 로컬 필터 폴백 (`nc_files.py`) - **주의**: PROPFIND depth=99는 파일 수가 많으면 느릴 수 있음. 추후 OCS 파일 검색 API 검토 ### [2026-03-18] anime_handler — action 분기 누락 - **증상**: 자연어 "자막 최신화" → "무엇을 도와드릴까요?" 표시 - **원인**: unified prompt가 `action: "download", title: ""` 반환 → `download and title` 조건 불충족 → else 분기 - **해결**: `download and not title` 분기 추가 (filter에 "sub" 포함 시 `batch_download(sub_only)`) - **주의**: 새 action 추가 시 unified prompt와 handler 양쪽 매핑 반드시 확인 ### [2026-03-18] discord_bot — 모듈 함수명 불일치 - **증상**: `cannot import name 'handle_anime_action'` - **원인**: 존재하지 않는 함수명으로 import (실제: `handle_anime_message`) - **해결**: import 수정 + 시그니처 확인 `(message, parsed)` - **주의**: 핸들러 연결 시 반드시 실제 모듈의 함수명/시그니처 확인 후 코드 작성 ### [2026-03-18] anime_handler — batch_download list 반환값 crash - **증상**: title 없이 배치 다운로드 시 `AttributeError: 'list' object has no attribute 'message'` - **원인**: `batch_download()`는 `list[DownloadResult]`를 반환하지만, 렌더링 코드가 단일 `result.message` 접근 - **해결**: `isinstance(result, list)` 체크 추가 → list면 합산 Embed 렌더링 - **주의**: pipeline 메서드 반환 타입을 반드시 확인하고 handler에서 처리할 것 ### [2026-03-18] anime_handler — NLU title에 범위 한정자 진입 - **증상**: "이번분기 애니 업데이트" → `title="이번분기"` → `download("이번분기")` → "검색 결과가 없습니다" - **원인**: handler line 90이 `title` truthy면 무조건 단건 다운로드. title 유효성 검증 없음 - **해결**: `download()` resolve 실패 + episode 미지정 시 `batch_download()` fallback 추가 - **주의**: AI NLU 출력을 무비판적으로 신뢰하지 말 것. 코드에서 반드시 방어적 검증 필요 ### [2026-03-19] anime_handler — batch mode "sub_only" 오판 - **증상**: "이번분기 애니 자막있는것까지 업데이트" → 영상 다운로드 안 됨 - **원인**: filter에 "sub" 있으면 `batch_download(mode="sub_only")` 호출 → `_execute_download` line 522가 영상 스킵 - **해결**: filter의 "sub"은 "자막도 포함"이지 "자막만"이 아님 → 모든 batch를 `mode="auto"`로 - **주의**: `sub_only`는 명시적 `action="sub_only"`일 때만 사용. filter 값으로 mode 결정하지 말 것 ### [2026-03-19] subtitle_downloader — Content-Disposition 후 존재 체크 누락 - **증상**: 이미 NAS에 있는 자막을 매번 다시 다운로드 - **원인**: Google Drive `sub.filename = "subtitle_{fileId}"` → 1차 존재 체크 통과 → 다운로드 → Content-Disposition에서 실제 파일명 → **존재 체크 없이 바로 write_bytes** → 기존 파일 덮어씀 - **해결**: Content-Disposition 파일명 결정 후, write 전에 2차 존재 체크 추가 - **주의**: 다운로드 전 체크만으로는 부족. **실제 파일명을 알게 된 시점에서 반드시 재확인** ### [2026-03-19] discord_bot — unified 분류 timeout 60초 부족 - **증상**: 커피갤러리 요약 등 복잡한 요청 시 "시간 초과 (60초)" 에러 - **원인**: `discord_bot.py:388` `timeout=60` 하드코딩 - **해결**: `timeout=120`으로 변경 - **주의**: 분류 프롬프트 복잡도에 따라 timeout 조정 필요. 기본값은 넉넉히 ### [2026-03-19] discord_bot — chat 응답 raw JSON 출력 - **증상**: "일렉기타 갤러리 요약" → `"mode": "chat", "response": "..."` JSON 구조가 그대로 Discord에 출력 - **원인**: Gemini가 JSON 문자열 안에 raw newline 출력 → `json.loads(strict=True)` 파싱 실패 → fallback이 raw 텍스트 전체를 response에 넣음 - **해결**: `json.loads(strict=False)` + chat 응답 항상 Embed 사용 + raw JSON 감지 시 response 텍스트만 추출 - **주의**: `json.loads`는 항상 `strict=False` 사용. chat 응답은 항상 Embed ### [2026-03-19] subtitle — discovered_ep=None 시 에피소드 체크 전면 우회 - **증상**: `existing_sub_eps = {1..10}` 전부 있는데도 Google Drive에서 10개 ZIP 재다운로드 - **원인**: 블로그 포스트 제목에 에피소드 번호 없음 → `discovered_ep = None` → line 761/777 스킵 조건(`is not None`) 전부 False → 무조건 다운로드. `_extract_archive`도 기존 파일 존재 체크 없이 덮어씀 - **해결**: (1) `discovered_ep is None` + 모든 영상 에피소드에 자막 있으면 URL 페치 자체 스킵 (2) ZIP 해제 시 `out_path.exists()` 체크 - **주의**: 에피소드 기반 스킵은 `discovered_ep`와 `sub.episode` 모두 None이면 완전 무력화. 반드시 **보충 체크** 필요 ### [2026-03-21] wiki — singleByPath 미존재 페이지 에러 - **증상**: `upsert_page("new/path")` 호출 시 `RuntimeError: Wiki.js API 오류: This page does not exist` - **원인**: Wiki.js `singleByPath` 쿼리는 미존재 페이지에 대해 null 대신 **GraphQL error**를 반환. 기존 `list_pages()` → 루프 패턴은 이 문제 없었음 - **해결**: `find_page()`에서 `RuntimeError` catch → `"does not exist"` 포함 시 `None` 반환. `wiki_debate.py`도 `_query()`에서 동일 처리 - **주의**: Wiki.js GraphQL API는 not-found를 에러로 처리하는 경우가 있음. 새 쿼리 추가 시 반드시 미존재 케이스 테스트 ### [2026-03-22] wiki — 빈 content 페이지 생성 거부 - **증상**: `debate_handler.start()` 초기화 시 `input-*`, `response-*` 페이지 생성 실패 — "Page content cannot be empty" - **원인**: Wiki.js `pages.create` mutation이 content가 빈 문자열(`""`)이면 거부. 핸들러가 response/input 초기화 시 `""` 전달 - **해결**: 모든 빈 content를 placeholder 텍스트(`*(대기 중)*`)로 변경 - **주의**: Wiki.js에 페이지 생성/수정 시 **절대로 빈 content를 전달하지 말 것**. 항상 최소 placeholder 필요 ### [2026-03-22] debate — Gemini slug에 따옴표 포함 - **증상**: 토론 Wiki 페이지 경로에 `"` 포함 → 브라우저에서 접근 불가, `singleByPath`로 조회 불가 - **원인**: Gemini에게 제목 요약 요청 시 `"신용리스크를 측정하는..."` 처럼 따옴표 포함 응답 → `WikiClient.slugify()`가 따옴표 미제거 - **해결**: (1) 프롬프트를 영문 2-3단어 키워드로 변경, (2) `.strip('"\'')` + `re.sub(r'[^\w\s-]', '')` 로 특수문자 제거, (3) slug 30자 제한 - **주의**: AI 응답을 slug/경로/파일명에 사용 시 **반드시 특수문자 sanitize** 필수. 따옴표, 괄호, 마크다운 기호 등 모두 제거 ### [2026-03-23] unified prompt — JSON 여는 중괄호 누락 - **증상**: Gemini 응답에 `{"mode": ...}` 대신 `"mode": ...}`로 출력되어 JSON 파싱이 실패하고 chat 롤백됨 - **원인**: 통합 프롬프트 특성상 LLM이 가끔 첫 `{`를 생략하거나 코드문법 없이 바로 출력함 - **해결**: `api/discord_bot.py` 의 `_parse_unified_response` 에 `{` 누락 시 `{...}` 로 감싸 재파싱하는 fallback 추가 - **주의**: LLM의 JSON 출력 포맷이 항상 완벽할 것이라 가정하지 말고 방어적 파싱 로직 적용 필수