diff --git a/.agent/references/known-issues.md b/.agent/references/known-issues.md index 4ceba24..f879ec6 100644 --- a/.agent/references/known-issues.md +++ b/.agent/references/known-issues.md @@ -81,3 +81,15 @@ - **원인**: 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의 절대 경로가 환경별로 다를 수 있음. 상대 경로 또는 환경변수 사용 고려 diff --git a/.agent/workflows/helpers/vikunja_helper.py b/.agent/workflows/helpers/vikunja_helper.py index 6ab298a..7ff44f6 100644 --- a/.agent/workflows/helpers/vikunja_helper.py +++ b/.agent/workflows/helpers/vikunja_helper.py @@ -12,6 +12,9 @@ Usage: python vikunja_helper.py list # List all tasks python vikunja_helper.py list todo # List TODO only python vikunja_helper.py list done # List DONE only + python vikunja_helper.py projects # List all Vikunja projects + python vikunja_helper.py report # Project status report (current) + python vikunja_helper.py report # Project status report (specific) """ import sys @@ -170,6 +173,109 @@ def create_task(title: str, description: str = "", done: bool = False, labels: l return result +def list_projects(): + """Vikunja 전체 프로젝트 목록 + 태스크 통계.""" + projects = api_get("/projects") + print("📂 프로젝트 목록:") + for p in projects: + pid = p["id"] + title = p["title"] + # 각 프로젝트의 태스크 수 조회 + try: + tasks = api_get(f"/projects/{pid}/tasks?per_page=200") + todo = sum(1 for t in tasks if not t["done"]) + done = sum(1 for t in tasks if t["done"]) + print(f" #{pid:<3d} {title:<30s} TODO: {todo} DONE: {done}") + except Exception: + print(f" #{pid:<3d} {title:<30s} (조회 실패)") + print(f"\n Total: {len(projects)} projects") + + +def report(project_id: int = None): + """프로젝트 종합 현황 보고 (태스크 + git log + devlog).""" + import subprocess + from pathlib import Path + from datetime import datetime, timedelta + + pid = project_id or PROJECT_ID + + # 1) 프로젝트 이름 조회 + try: + projects = api_get("/projects") + proj_name = next((p["title"] for p in projects if p["id"] == pid), f"Project #{pid}") + except Exception: + proj_name = f"Project #{pid}" + + print(f"=== 프로젝트 현황: {proj_name} (#{pid}) ===") + + # 2) 태스크 현황 + try: + tasks = api_get(f"/projects/{pid}/tasks?per_page=200") + todo_tasks = [t for t in tasks if not t["done"]] + done_tasks = [t for t in tasks if t["done"]] + total = len(tasks) + rate = f"{len(done_tasks)/total*100:.0f}%" if total else "N/A" + + print(f"\n[태스크]") + print(f" TODO: {len(todo_tasks)}건 | DONE: {len(done_tasks)}건 | 완료율: {rate}") + + if todo_tasks: + print(f" 미완료:") + for t in todo_tasks: + labels = ", ".join(l["title"] for l in (t.get("labels") or [])) + label_str = f" [{labels}]" if labels else "" + desc = (t.get("description") or "")[:40].replace("\n", " ") + desc_str = f" {desc}" if desc else "" + print(f" ⬜ #{t['id']} {t['title'][:50]}{label_str}{desc_str}") + + if done_tasks: + # 최근 완료 5건만 표시 + recent_done = sorted(done_tasks, key=lambda t: t.get("done_at", ""), reverse=True)[:5] + print(f" 최근 완료 (최대 5건):") + for t in recent_done: + print(f" ✅ #{t['id']} {t['title'][:50]}") + except Exception as e: + print(f" 태스크 조회 실패: {e}") + + # 3) Git log (현재 디렉토리 기준) + print(f"\n[최근 커밋 5건]") + try: + result = subprocess.run( + ["git", "log", "--oneline", "-5"], + capture_output=True, timeout=10, + encoding="utf-8", errors="replace", + ) + if result.returncode == 0 and result.stdout.strip(): + for line in result.stdout.strip().splitlines(): + print(f" {line}") + else: + print(" (git log 없음)") + except Exception: + print(" (git 실행 불가)") + + # 4) Devlog (오늘/어제) + print(f"\n[Devlog]") + today = datetime.now().strftime("%Y-%m-%d") + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + + devlog_found = False + for date_str in [today, yesterday]: + devlog_path = Path("docs/devlog") / f"{date_str}.md" + if devlog_path.exists(): + content = devlog_path.read_text(encoding="utf-8").strip() + # 최대 500자 + if len(content) > 500: + content = content[:500] + "\n ...(생략)" + print(f" [{date_str}]") + for line in content.splitlines(): + print(f" {line}") + devlog_found = True + break + + if not devlog_found: + print(" (최근 devlog 없음)") + + def main(): if len(sys.argv) < 2: print(__doc__) @@ -208,6 +314,11 @@ def main(): print("Error: title is required") return create_task(title, desc, done=is_done, labels=labels) + elif cmd == "projects": + list_projects() + elif cmd == "report": + pid = int(sys.argv[2]) if len(sys.argv) > 2 else None + report(pid) else: print(f"Unknown command: {cmd}") print(__doc__) diff --git a/api/discord_bot.py b/api/discord_bot.py index 0d5f73b..79f6ff5 100644 --- a/api/discord_bot.py +++ b/api/discord_bot.py @@ -230,6 +230,15 @@ async def on_guild_channel_update(before, after): + +@bot.event +async def on_command_error(ctx, error): + """존재하지 않는 ! 명령어 무시.""" + if isinstance(error, commands.CommandNotFound): + return # 무시 — 로그 오염 방지 + raise error + + @bot.event async def on_message(message: discord.Message): """메시지 수신 — 워크스페이스 채널이면 자동 응답.""" diff --git a/core/gemini_caller.py b/core/gemini_caller.py index c3bacb9..bbbc1c7 100644 --- a/core/gemini_caller.py +++ b/core/gemini_caller.py @@ -99,7 +99,7 @@ class GeminiCaller: for line in lines: if any(noise in line for noise in [ "YOLO mode", "Loaded cached", "Welcome to Gemini", - "Type /help", "Gemini CLI", + "Type /help", "Gemini CLI", "MCP issues detected", ]): continue cleaned.append(line) diff --git a/docs/devlog/2026-03-16.md b/docs/devlog/2026-03-16.md new file mode 100644 index 0000000..af854e7 --- /dev/null +++ b/docs/devlog/2026-03-16.md @@ -0,0 +1,5 @@ +# 2026-03-16 Devlog + +| # | 시간 | 작업 | 커밋 | 상태 | +|---|------|------|------|------| +| 1 | 23:35~11:14 | 봇 기동 버그 3건 수정 (apscheduler, cp949 인코딩, workspaces 경로) + Vikunja 프로젝트 관리 기능 추가 (projects, report 커맨드) + agent 프롬프트 보강 | `` | ✅ | diff --git a/docs/devlog/entries/20260316-001.md b/docs/devlog/entries/20260316-001.md new file mode 100644 index 0000000..acca32d --- /dev/null +++ b/docs/devlog/entries/20260316-001.md @@ -0,0 +1,30 @@ +# 봇 기동 버그 수정 + Vikunja 프로젝트 관리 기능 + +- **시간**: 2026-03-16 23:35~11:14 +- **Commit**: `` +- **Vikunja**: #375 done, #376 done + +## 작업 내용 + +### 봇 기동 버그 3건 수정 +1. **apscheduler 미설치** — `ModuleNotFoundError` → `pip install apscheduler` (3.11.2) +2. **cp949 인코딩 크래시** — `main.py` StreamHandler를 UTF-8 TextIOWrapper로 교체 + 로그 메시지 em-dash→plain dash +3. **workspaces.json 경로 불일치** — `c:\Users\Certes\...` → `c:\Users\Variet-Worker\...` 수정 (WinError 267 해결) + +### 부수 수정 +- `gemini_caller.py` — "MCP issues detected" 노이즈 필터 추가 (`_clean_output`) +- `discord_bot.py` — `on_command_error` 핸들러 추가 (CommandNotFound 로그 오염 방지) + +### Vikunja 프로젝트 관리 기능 추가 +- `vikunja_helper.py`에 `projects` 커맨드 추가 — 전체 Vikunja 프로젝트 목록 + TODO/DONE 통계 +- `vikunja_helper.py`에 `report` 커맨드 추가 — 프로젝트별 종합 현황 (태스크 + git log + devlog) +- `agent.md` 프롬프트 보강 — Vikunja 도구 섹션 확장 (12개 커맨드 + 라벨 가이드) + +## 변경 파일 +- `.agent/workflows/helpers/vikunja_helper.py` — projects, report 함수 + main dispatcher +- `api/discord_bot.py` — on_command_error 핸들러 +- `core/gemini_caller.py` — MCP 노이즈 필터 +- `main.py` — UTF-8 StreamHandler, em-dash 제거 +- `prompts/agent.md` — Vikunja 도구 확장 +- `workspaces.json` — 경로 수정 +- `.agent/references/known-issues.md` — 이슈 2건 추가 diff --git a/main.py b/main.py index 437dd00..8be9fb1 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ FastAPI 서버 + Discord Bot + APScheduler를 동시 실행합니다. """ import asyncio +import io import logging import sys import signal @@ -21,8 +22,10 @@ import config LOG_DIR = Path(__file__).parent / "logs" LOG_DIR.mkdir(exist_ok=True) +_stdout_utf8 = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + handlers = [ - logging.StreamHandler(sys.stdout), + logging.StreamHandler(_stdout_utf8), RotatingFileHandler( LOG_DIR / "variet.log", maxBytes=10 * 1024 * 1024, # 10MB @@ -75,7 +78,7 @@ async def run_scheduler(): try: from apscheduler.schedulers.asyncio import AsyncIOScheduler except ImportError: - logger.warning("APScheduler 미설치 — 스케줄러 비활성화 (pip install apscheduler)") + logger.warning("APScheduler 미설치 - 스케줄러 비활성화 (pip install apscheduler)") return scheduler = AsyncIOScheduler() @@ -91,7 +94,7 @@ async def run_scheduler(): # scheduler.add_job(check_schedule, "cron", hour=18, id="anime_schedule") scheduler.start() - logger.info(f"스케줄러 시작 — {len(scheduler.get_jobs())}개 작업 등록") + logger.info(f"스케줄러 시작 - {len(scheduler.get_jobs())}개 작업 등록") # 스케줄러가 종료되지 않도록 대기 try: diff --git a/prompts/agent.md b/prompts/agent.md index 1602701..84f9353 100644 --- a/prompts/agent.md +++ b/prompts/agent.md @@ -62,15 +62,30 @@ C:\ProgramData\miniforge3\envs\variet-agent\python.exe tools/wiki_client.py dash > **위키 등록 필수**: 조사/리서치 결과는 **반드시** `wiki_client.py create`로 Wiki.js에 등록하세요. 로컬 파일에 쓰지 마세요. -### 🔧 인프라 (Git/태스크) +### 📋 프로젝트 관리 (Vikunja) ```bash -# Vikunja 태스크 +# 전체 프로젝트 목록 (어떤 프로젝트가 있는지, 각 프로젝트의 TODO/DONE 수) +C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py projects + +# 프로젝트 종합 현황 (태스크 + 최근 커밋 + devlog) +C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py report +C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py report + +# 태스크 목록 C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py list -C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py create "제목" "설명" +C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py list todo + +# 태스크 생성/완료/코멘트 +C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py create "제목" "설명" --labels Backend,Priority:High C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py done +C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py comment "메모" +C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py label Backend Priority:High ``` +> **라벨 목록**: Backend, Frontend, Engine, Infra, Test, Priority:High, Priority:Mid, Priority:Low, Agent, Tool, AI/LLM +> 태스크 생성 시 영역 라벨(1개+)과 우선순위 라벨(1개)을 반드시 지정하세요. + ## ⚡ 복수 작품 처리 — 반드시 전부 완료할 것 사용자가 "이번 분기 애니 다운받아줘" 등 **복수 작업**을 요청하면: diff --git a/workspaces.json b/workspaces.json index 97125f8..b95a886 100644 --- a/workspaces.json +++ b/workspaces.json @@ -35,7 +35,7 @@ }, "1480113683849023661": { "name": "variet-agent", - "path": "c:\\Users\\Certes\\Desktop\\variet-agent", + "path": "c:\\Users\\Variet-Worker\\Desktop\\variet-agent", "channel_id": 1480113683849023661, "git": { "url": "",