diff --git a/.env b/.env index 543c31d..3164fdb 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ PM_USER_ID=b21364 -PM_PASSWORD=b21364!. \ No newline at end of file +PM_PASSWORD=b21364!.`nDB_HOST=localhost`nDB_USER=root`nDB_PASSWORD=45278434`nDB_NAME=crawling diff --git a/__pycache__/analyze.cpython-312.pyc b/__pycache__/analyze.cpython-312.pyc index 9492ae5..f44c613 100644 Binary files a/__pycache__/analyze.cpython-312.pyc and b/__pycache__/analyze.cpython-312.pyc differ diff --git a/__pycache__/crawler_service.cpython-312.pyc b/__pycache__/crawler_service.cpython-312.pyc index c396c7a..fb26ded 100644 Binary files a/__pycache__/crawler_service.cpython-312.pyc and b/__pycache__/crawler_service.cpython-312.pyc differ diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc index b5cf328..64c72d1 100644 Binary files a/__pycache__/server.cpython-312.pyc and b/__pycache__/server.cpython-312.pyc differ diff --git a/backups/crawler_service_v1_dom.py.bak b/backups/crawler_service_v1_dom.py.bak new file mode 100644 index 0000000..30b4384 --- /dev/null +++ b/backups/crawler_service_v1_dom.py.bak @@ -0,0 +1,141 @@ +import os +import re +import asyncio +import json +from playwright.async_api import async_playwright +from dotenv import load_dotenv + +load_dotenv() + +async def run_crawler_service(): + """ + Playwright를 이용해 데이터를 수집하고 SSE(Server-Sent Events)용 제너레이터를 반환합니다. + """ + user_id = os.getenv("PM_USER_ID") + password = os.getenv("PM_PASSWORD") + + if not user_id or not password: + yield f"data: {json.dumps({'type': 'log', 'message': '오류: .env 파일에 계정 정보가 없습니다.'})}\n\n" + return + + results = [] + + async with async_playwright() as p: + browser = None + try: + yield f"data: {json.dumps({'type': 'log', 'message': '브라우저 실행 중...'})}\n\n" + browser = await p.chromium.launch(headless=True, args=[ + "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-blink-features=AutomationControlled" + ]) + context = await browser.new_context( + viewport={'width': 1920, 'height': 1080}, + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" + ) + page = await context.new_page() + + yield f"data: {json.dumps({'type': 'log', 'message': '사이트 접속 및 로그인 중...'})}\n\n" + await page.goto("https://overseas.projectmastercloud.com/", wait_until="domcontentloaded") + + await page.click("#login-by-id", timeout=10000) + await page.fill("#user_id", user_id) + await page.fill("#user_pw", password) + await page.click("#login-btn") + + yield f"data: {json.dumps({'type': 'log', 'message': '대시보드 목록 대기 중...'})}\n\n" + await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=60000) + + locators = page.locator("h4.list__contents_aria_group_body_list_item_label") + count = await locators.count() + yield f"data: {json.dumps({'type': 'log', 'message': f'총 {count}개의 프로젝트 발견. 수집 시작.'})}\n\n" + + for i in range(count): + try: + proj = page.locator("h4.list__contents_aria_group_body_list_item_label").nth(i) + project_name = (await proj.inner_text()).strip() + + yield f"data: {json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] {project_name} - 시작'})}\n\n" + await proj.scroll_into_view_if_needed() + await proj.click(force=True) + + await asyncio.sleep(5) + await page.wait_for_selector("div.footer", state="visible", timeout=20000) + + recent_log = "기존데이터유지" + file_count = 0 + + # 로그 수집 + try: + log_btn_sel = "body > div.footer > div.left > div.wrap.log-wrap > div.title.text" + log_btn = page.locator(log_btn_sel).first + if await log_btn.is_visible(timeout=5000): + await log_btn.click(force=True) + await asyncio.sleep(5) + + date_sel = "article.archive-modal .log-body .date .text" + user_sel = "article.archive-modal .log-body .user .text" + act_sel = "article.archive-modal .log-body .activity .text" + + if await page.locator(date_sel).count() > 0: + raw_date = (await page.locator(date_sel).first.inner_text()).strip() + user_name = (await page.locator(user_sel).first.inner_text()).strip() + activity = (await page.locator(act_sel).first.inner_text()).strip() + formatted_date = re.sub(r'[-/]', '.', raw_date)[:10] + recent_log = f"{formatted_date}, {user_name}, {activity}" + yield f"data: {json.dumps({'type': 'log', 'message': f' - [로그] 수집 완료'})}\n\n" + + await page.click("article.archive-modal div.close", timeout=3000) + await asyncio.sleep(1.5) + except: pass + + # 구성 수집 + try: + sitemap_btn_sel = "body > div.footer > div.left > div.wrap.site-map-wrap" + sitemap_btn = page.locator(sitemap_btn_sel).first + if await sitemap_btn.is_visible(timeout=5000): + await sitemap_btn.click(force=True) + + popup_page = None + for _ in range(20): + for p_item in context.pages: + if "composition" in p_item.url: + popup_page = p_item + break + if popup_page: break + await asyncio.sleep(0.5) + + if popup_page: + target_selector = "#composition-list h6:nth-child(3)" + await asyncio.sleep(5) # 로딩 대기 + locators_h6 = popup_page.locator(target_selector) + h6_count = await locators_h6.count() + current_total = 0 + for j in range(h6_count): + text = (await locators_h6.nth(j).inner_text()).strip() + nums = re.findall(r'\d+', text.split('\n')[-1]) + if nums: current_total += int(nums[0]) + file_count = current_total + yield f"data: {json.dumps({'type': 'log', 'message': f' - [구성] {file_count}개 확인'})}\n\n" + await popup_page.close() + except: pass + + results.append({"projectName": project_name, "recentLog": recent_log, "fileCount": file_count}) + + # 홈 복귀 + await page.locator("div.header div.title div").first.click(force=True) + await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=20000) + await asyncio.sleep(2) + + except Exception: + await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") + + yield f"data: {json.dumps({'type': 'done', 'data': results})}\n\n" + + except GeneratorExit: + # SSE 연결이 클라이언트 측에서 먼저 끊겼을 때 실행 + if browser: await browser.close() + except Exception as e: + yield f"data: {json.dumps({'type': 'log', 'message': f'치명적 오류: {str(e)}'})}\n\n" + finally: + if browser: await browser.close() diff --git a/composition_debug.png b/composition_debug.png new file mode 100644 index 0000000..5ccabb3 Binary files /dev/null and b/composition_debug.png differ diff --git a/crawler_service.py b/crawler_service.py index bd84df4..b291493 100644 --- a/crawler_service.py +++ b/crawler_service.py @@ -2,101 +2,159 @@ import os import re import asyncio import json -import csv import traceback +import sys +import threading +import queue +import pymysql from datetime import datetime from playwright.async_api import async_playwright from dotenv import load_dotenv load_dotenv() +def get_db_connection(): + """MySQL 데이터베이스 연결을 반환합니다.""" + return pymysql.connect( + host='localhost', + user='root', + password='45278434', + database='crawling', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + def clean_date_string(date_str): - """ - 날짜 문자열에서 YY.MM.DD 형식만 추출합니다. - """ if not date_str: return "" match = re.search(r'(\d{2})[./-](\d{2})[./-](\d{2})', date_str) if match: - return f"{match.group(1)}.{match.group(2)}.{match.group(3)}" - return date_str[:8] + return f"20{match.group(1)}.{match.group(2)}.{match.group(3)}" + return date_str[:10].replace("-", ".") -async def run_crawler_service(): - """ - 상세 패킷을 강제 호출하여 팝업 없이 상세 파일 개수를 수집하며 모든 성공 로직을 보존한 크롤러입니다. - """ - user_id = os.getenv("PM_USER_ID") - password = os.getenv("PM_PASSWORD") +def parse_log_id(log_id): + """ID 구조: 로그고유번호_시간_활동한 사람_활동내용_활동대상""" + if not log_id or "_" not in log_id: return log_id + try: + parts = log_id.split('_') + if len(parts) >= 4: + date_part = clean_date_string(parts[1]) + activity = parts[3].strip() + activity = re.sub(r'\(.*?\)', '', activity).strip() + return f"{date_part}, {activity}" + except: pass + return log_id - if not user_id or not password: - yield f"data: {json.dumps({'type': 'log', 'message': '오류: .env 파일에 계정 정보가 없습니다.'})}\n\n" - return - - results = [] - - async with async_playwright() as p: - browser = None - try: - yield f"data: {json.dumps({'type': 'log', 'message': '브라우저 엔진 (데이터 강제 유도 모드) 가동...'})}\n\n" - browser = await p.chromium.launch(headless=False, args=["--no-sandbox", "--disable-dev-shm-usage"]) - context = await browser.new_context(viewport={'width': 1600, 'height': 900}) - - captured_data = {"log": None, "tree": None} - async def global_interceptor(response): - url = response.url - try: - # 상세 패킷 감시 (params[resourcePath]=/ 가 포함된 상세 응답 우선) - if "getTreeObject" in url: - data = await response.json() - if data.get('currentTreeObject', {}).get('folder'): - captured_data["tree"] = data - elif "Log" in url: - captured_data["log"] = await response.json() - except: pass - context.on("response", global_interceptor) - - page = await context.new_page() - - # --- 1. 로그인 (안정 로직) --- - await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") - if await page.locator("#login-by-id").is_visible(timeout=5000): - await page.click("#login-by-id") - await page.fill("#user_id", user_id) - await page.fill("#user_pw", password) - await page.click("#login-btn") - - await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=60000) - await asyncio.sleep(5) - - names = await page.locator("h4.list__contents_aria_group_body_list_item_label").all_inner_texts() - project_names = [n.strip() for n in names if n.strip()] - count = len(project_names) - yield f"data: {json.dumps({'type': 'log', 'message': f'총 {count}개의 프로젝트 수집 시작.'})}\n\n" - - for i, project_name in enumerate(project_names): - captured_data["log"] = None - captured_data["tree"] = None - yield f"data: {json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] {project_name} 수집 시작'})}\n\n" +def crawler_thread_worker(msg_queue, user_id, password): + if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + async def run(): + async with async_playwright() as p: + browser = None + try: + msg_queue.put(json.dumps({'type': 'log', 'message': '브라우저 엔진 가동 (전 기능 완벽 복구 모드)...'})) + browser = await p.chromium.launch(headless=False, args=["--no-sandbox"]) + context = await browser.new_context(viewport={'width': 1600, 'height': 900}) - try: - # 상세 페이지 진입 (안정 로직) - target_el = page.get_by_text(project_name).first - await target_el.scroll_into_view_if_needed() - box = await target_el.bounding_box() - if box: await page.mouse.click(box['x'] + 5, box['y'] + 5) - else: await target_el.click(force=True) - - await page.wait_for_selector("text=활동로그", timeout=30000) - await asyncio.sleep(3) + captured_data = {"tree": None, "_is_root_archive": False, "_tree_url": "", "project_list": []} - recent_log = "데이터 없음" - file_count = 0 - - # --- 2. 활동로그 수집 (100% 복구 로직) --- + async def global_interceptor(response): + url = response.url try: + if "getAllList" in url: + data = await response.json() + captured_data["project_list"] = data.get("data", []) + elif "getTreeObject" in url: + # [핵심 복원] 정확한 루트 경로 판별 로직 + is_root = False + if "params[resourcePath]=" in url: + path_val = url.split("params[resourcePath]=")[1].split("&")[0] + if path_val in ["%2F", "/"]: is_root = True + + if is_root: + data = await response.json() + captured_data["tree"] = data + captured_data["_is_root_archive"] = "archive" in url + captured_data["_tree_url"] = url + except: pass + + context.on("response", global_interceptor) + page = await context.new_page() + await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") + + # 로그인 + if await page.locator("#login-by-id").is_visible(timeout=5000): + await page.click("#login-by-id") + await page.fill("#user_id", user_id) + await page.fill("#user_pw", password) + await page.click("#login-btn") + + # 리스트 로딩 대기 + await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=60000) + await asyncio.sleep(3) + + # [Phase 1] DB 기초 정보 동기화 (엄격한 매칭) + if captured_data["project_list"]: + conn = get_db_connection() + try: + with conn.cursor() as cursor: + for p_info in captured_data["project_list"]: + p_nm = p_info.get("project_nm") + try: + sql = """ + INSERT INTO overseas_projects (project_id, project_nm, short_nm, master, continent, country) + VALUES (%s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + project_id = VALUES(project_id), project_nm = VALUES(project_nm), + short_nm = VALUES(short_nm), master = VALUES(master), + continent = VALUES(continent), country = VALUES(country) + """ + cursor.execute(sql, (p_info.get("project_id"), p_nm, p_info.get("short_nm", "").strip(), + p_info.get("master"), p_info.get("large_class"), p_info.get("mid_class"))) + except: continue + conn.commit() + msg_queue.put(json.dumps({'type': 'log', 'message': f'DB 기초 정보 동기화 완료 ({len(captured_data["project_list"])}개)'})) + finally: conn.close() + + # [Phase 2] h4 태그 기반 수집 루프 + names = await page.locator("h4.list__contents_aria_group_body_list_item_label").all_inner_texts() + project_names = list(dict.fromkeys([n.strip() for n in names if n.strip()])) + count = len(project_names) + + for i, project_name in enumerate(project_names): + # 현재 프로젝트의 고유 ID 매칭 (저장용) + p_match = next((p for p in captured_data["project_list"] if p.get('project_nm') == project_name or p.get('short_nm', '').strip() == project_name), None) + current_p_id = p_match.get('project_id') if p_match else None + + captured_data["tree"] = None + captured_data["_is_root_archive"] = False + msg_queue.put(json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] {project_name} 수집 시작'})) + + try: + # 1. 프로젝트 진입 ([완전 복원] 좌표 클릭) + target_el = page.locator(f"h4.list__contents_aria_group_body_list_item_label:has-text('{project_name}')").first + await target_el.scroll_into_view_if_needed() + box = await target_el.bounding_box() + if box: await page.mouse.click(box['x'] + 5, box['y'] + 5) + else: await target_el.click(force=True) + + await page.wait_for_selector("text=활동로그", timeout=30000) + await asyncio.sleep(2) + + recent_log = "데이터 없음" + file_count = 0 + + # 2. 활동로그 ([완전 복원] 3회 재시도 + 좌표 클릭 + 날짜 필터) modal_opened = False for _ in range(3): log_btn = page.get_by_text("활동로그").first - await page.evaluate("(el) => el.click()", await log_btn.element_handle()) + btn_box = await log_btn.bounding_box() + if btn_box: await page.mouse.click(btn_box['x'] + 5, btn_box['y'] + 5) + else: await page.evaluate("(el) => el.click()", await log_btn.element_handle()) + try: await page.wait_for_selector("article.archive-modal", timeout=5000) modal_opened = True @@ -104,96 +162,91 @@ async def run_crawler_service(): except: await asyncio.sleep(1) if modal_opened: + # 날짜 필터 입력 inputs = await page.locator("article.archive-modal input").all() for inp in inputs: - itype = await inp.get_attribute("type") - iname = (await inp.get_attribute("name") or "").lower() - iclass = (await inp.get_attribute("class") or "").lower() - if itype == "date" or "start" in iname or "start" in iclass: + if (await inp.get_attribute("type")) == "date": await inp.fill("2020-01-01") break - captured_data["log"] = None apply_btn = page.locator("article.archive-modal").get_by_text("적용").first if await apply_btn.is_visible(): await apply_btn.click() - await asyncio.sleep(4) - - if captured_data["log"]: - data = captured_data["log"] - logs = data.get('logData', []) or data.get('result', []) - if logs and isinstance(logs, list) and len(logs) > 0: - top = logs[0] - rd = top.get('log_date') or top.get('date') or "" - u = top.get('user_name') or top.get('user') or "" - c = top.get('activity_content') or top.get('activity') or "" - recent_log = f"{clean_date_string(rd)}, {u}, {c}" - - if recent_log == "데이터 없음": - modal = page.locator("article.archive-modal") - try: - d_v = (await modal.locator(".log-body .date .text").first.inner_text()).strip() - u_v = (await modal.locator(".log-body .user .text").first.inner_text()).strip() - a_v = (await modal.locator(".log-body .activity .text").first.inner_text()).strip() - if d_v: recent_log = f"{clean_date_string(d_v)}, {u_v}, {a_v}" - except: pass - - yield f"data: {json.dumps({'type': 'log', 'message': f' - [로그 결과] {recent_log}'})}\n\n" + await asyncio.sleep(5) # 렌더링 보장 + log_elements = await page.locator("article.archive-modal div[id*='_']").all() + if log_elements: + raw_id = await log_elements[0].get_attribute("id") + recent_log = parse_log_id(raw_id) + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [분석] 최신 로그 ID 추출 성공: {recent_log}'})) + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [최종 결과] {recent_log}'})) await page.keyboard.press("Escape") - else: - yield f"data: {json.dumps({'type': 'log', 'message': ' - [로그] 모달 진입 실패'})}\n\n" - except Exception as le: - yield f"data: {json.dumps({'type': 'log', 'message': f' - [로그] 오류: {str(le)}'})}\n\n" - # --- 3. 구성 수집 (상세 패킷 강제 유도) --- - try: - # [지능형 유도] 상세 정보를 포함한 API 요청을 브라우저가 직접 날리게 함 - await page.evaluate("""() => { - fetch('/api/getTreeObject?params[storageType]=CLOUD¶ms[resourcePath]=/'); + # 3. 구성 수집 ([완전 복원] BaseURL fetch + 정밀 합산) + await page.evaluate("""() => { + const baseUrl = window.location.origin + window.location.pathname.split('/').slice(0, 2).join('/'); + fetch(`${baseUrl}/archive/getTreeObject?params[storageType]=CLOUD¶ms[resourcePath]=/`); }""") - - # 패킷 대기 (최대 10초) - for _ in range(20): - if captured_data["tree"]: break + for _ in range(30): + if captured_data["_is_root_archive"]: break await asyncio.sleep(0.5) if captured_data["tree"]: - data = captured_data["tree"] - # 분석된 딕셔너리 구조 합산 - folders = data.get('currentTreeObject', {}).get('folder', {}) - total_files = 0 + data_root = captured_data["tree"] + tree = data_root.get('currentTreeObject', data_root) if isinstance(data_root, dict) else {} + total = 0 + # 루트 파일 합산 + rf = tree.get("file", {}) + total += len(rf) if isinstance(rf, (dict, list)) else 0 + # 폴더별 filesCount 합산 + folders = tree.get("folder", {}) if isinstance(folders, dict): - for folder_name, folder_info in folders.items(): - total_files += int(folder_info.get('filesCount', 0)) - file_count = total_files - yield f"data: {json.dumps({'type': 'log', 'message': f' - [구성] 상세 합산 성공 ({file_count}개)'})}\n\n" - else: - yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 상세 데이터 응답 없음'})}\n\n" - except: pass + for f in folders.values(): + c = f.get("filesCount", "0") + total += int(c) if str(c).isdigit() else 0 + file_count = total + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [구성] 데이터 채택 성공: ...{captured_data.get("_tree_url", "")[-40:]}'})) + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [구성] 최종 정밀 합산 성공 ({file_count}개)'})) - results.append({"projectName": project_name, "recentLog": recent_log, "fileCount": file_count}) - await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") - await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=20000) - - except Exception as e: - yield f"data: {json.dumps({'type': 'log', 'message': f' - [{project_name}] 건너뜀 (사유: {str(e)})'})}\n\n" - await page.goto("https://overseas.projectmastercloud.com/dashboard") + # 4. DB 실시간 저장 (ID 기반) + if current_p_id: + conn = get_db_connection() + try: + with conn.cursor() as cursor: + sql = "UPDATE overseas_projects SET recent_log = %s, file_count = %s WHERE project_id = %s" + cursor.execute(sql, (recent_log, file_count, current_p_id)) + conn.commit() + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [DB] 업데이트 완료 (ID: {current_p_id})'})) + finally: conn.close() - # --- 4. CSV 파일 저장 --- - try: - today_str = datetime.now().strftime("%Y.%m.%d") - csv_path = f"crawling_result {today_str}.csv" - with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f: - writer = csv.DictWriter(f, fieldnames=["projectName", "recentLog", "fileCount"]) - writer.writeheader() - writer.writerows(results) - yield f"data: {json.dumps({'type': 'log', 'message': f'✅ 모든 데이터가 {csv_path}에 저장되었습니다.'})}\n\n" - except Exception as fe: - yield f"data: {json.dumps({'type': 'log', 'message': f'❌ CSV 저장 실패: {str(fe)}'})}\n\n" + await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded") + + except Exception as e: + msg_queue.put(json.dumps({'type': 'log', 'message': f' - [{project_name}] 건너뜀: {str(e)}'})) + await page.goto("https://overseas.projectmastercloud.com/dashboard") - yield f"data: {json.dumps({'type': 'done', 'data': results})}\n\n" - - except Exception as e: - yield f"data: {json.dumps({'type': 'log', 'message': f'치명적 오류: {str(e)}'})}\n\n" - finally: - if browser: await browser.close() + msg_queue.put(json.dumps({'type': 'done', 'data': []})) + + except Exception as e: + msg_queue.put(json.dumps({'type': 'log', 'message': f'치명적 오류: {str(e)}'})) + finally: + if browser: await browser.close() + msg_queue.put(None) + + loop.run_until_complete(run()) + loop.close() + +async def run_crawler_service(): + user_id = os.getenv("PM_USER_ID") + password = os.getenv("PM_PASSWORD") + msg_queue = queue.Queue() + thread = threading.Thread(target=crawler_thread_worker, args=(msg_queue, user_id, password)) + thread.start() + while True: + try: + msg = await asyncio.to_thread(msg_queue.get, timeout=1.0) + if msg is None: break + yield f"data: {msg}\n\n" + except queue.Empty: + if not thread.is_alive(): break + await asyncio.sleep(0.1) + thread.join() diff --git a/crawling_result 2026.03.06.csv b/crawling_result 2026.03.06.csv new file mode 100644 index 0000000..bd6d6c4 --- /dev/null +++ b/crawling_result 2026.03.06.csv @@ -0,0 +1,42 @@ +projectName,recentLog,fileCount +ITTC 관개 교육센터,"26.01.29, 박진규, 폴더 삭제",16 +비엔티안 메콩강 관리 2차,"25.12.07, 나쉬, 파일 업로드",260 +만달레이 철도 개량 감리,"25.11.19, 이태훈, 보안참여자 권한 추가",298 +푸옥호아 양수 발전,"26.02.23, 이철호, 폴더 이름 변경",139 +아시르 지잔 고속도로,"26.03.04, 이태훈, 보안참여자 권한 추가",73 +지방 도로 복원,"26.03.04, 이태훈, 보안참여자 권한 추가",0 +타슈켄트 철도,"26.02.05, 조항언, 파일 업로드",51 +Habbaniyah Shuaiba AirBase,"26.03.04, 이태훈, 보안참여자 권한 추가",0 +시엠립 하수처리 개선,"26.02.09, 이태훈, 보안참여자 권한 추가",221 +반테 민체이 관개 홍수저감,"25.12.07, 나쉬, 파일 업로드",44 +메콩유역 수자원 관리 기후적응,"25.11.19, 이태훈, 보안참여자 권한 추가",0 +잘랄아바드 상수도 계획,"26.03.06, -, 폴더 자동 삭제 (파일 개수 미달)",58 +CAREC 도로 감리,"26.03.04, 이태훈, 보안참여자 권한 추가",0 +펀잡 홍수 방재,"25.12.08, 콰윰 아딜, 폴더 삭제",0 +KP 아보타바드 상수도,"26.02.26, 정기일, 파일 업로드",240 +홍수 복원 InFRA2,"25.12.18, -, 폴더 자동 삭제 (파일 개수 미달)",6 +PGN 해상교량 BID2,"26.03.04, 이태훈, 보안참여자 권한 추가",631 +홍수 관리 Package5B,"25.12.02, 조명훈, 폴더 이름 변경",14 +족자~바웬 도로사업,"26.03.06, 시스템관리-Savannah, 참관자 권한 추가",0 +테치만 상수도 확장,"26.02.09, 이태훈, 보안참여자 권한 추가",0 +기니 벼 재배단지,"26.01.07, -, 폴더 자동 삭제 (파일 개수 미달)",43 +부수쿠마 분뇨 자원화 2단계,"26.02.09, 이태훈, 보안참여자 권한 추가",9 +우간다 벼 재배단지,"25.12.08, 박수빈, 파일 업로드",52 +Adeaa-Becho 지하수 관개,"25.12.29, -, 폴더 자동 삭제 (파일 개수 미달)",140 +도도타군 관개,"25.12.30, -, 폴더 자동 삭제 (파일 개수 미달)",142 +지하수 관개 환경설계,"26.02.09, 이태훈, 보안참여자 권한 추가",0 +Dodoma 하수 설계감리,"26.02.09, 이태훈, 보안참여자 권한 추가",32 +Iringa 상하수도 개선,"26.02.09, 이태훈, 보안참여자 권한 추가",0 +도도마 유수율 상수도개선,"26.02.12, 서하연, 부관리자 권한 추가",35 +잔지바르 쌀 생산,"25.12.08, 박수빈, 파일 업로드",23 +SALDEORO 수력발전 28MW,"25.11.19, 이태훈, 보안참여자 권한 추가",0 +LaPaz Danli 상수도,"26.02.09, 이태훈, 보안참여자 권한 추가",60 +에스꼬마 차라짜니 도로,"26.03.06, -, 폴더 자동 삭제 (파일 개수 미달)",0 +마모레 교량도로,"26.03.04, 이태훈, 보안참여자 권한 추가",120 +Bombeo-Colomi 도로설계,"26.03.04, 이태훈, 보안참여자 권한 추가",48 +AI 폐기물,"25.11.19, 이태훈, 보안참여자 권한 추가",0 +도로 통행료 현대화,"26.02.25, 류창수, 폴더 삭제",0 +Barranca 상하수도 확장,"26.02.09, 이태훈, 보안참여자 권한 추가",44 +태평양 철도,"26.02.24, -, 폴더 자동 삭제 (파일 개수 미달)",101 +필리핀 사무소,"26.03.06, 한형남, 파일 다운로드",316 +PGN 해상교량 BID2,"26.03.04, 이태훈, 보안참여자 권한 추가",631 diff --git a/crawling_result 2026.03.09.csv b/crawling_result 2026.03.09.csv new file mode 100644 index 0000000..b1bc926 --- /dev/null +++ b/crawling_result 2026.03.09.csv @@ -0,0 +1,42 @@ +projectName,recentLog,fileCount +비엔티안 메콩강 관리 2차,212487_25-12-07 12:22:26_나쉬_파일 업로드_/서류/06. 전문가파견/01. 파견/251031~260208_전문가 파견(이범주).zip,260 +ITTC 관개 교육센터,225728_26-01-29 09:10:21_박진규_폴더 삭제_/서류/ggg,16 +만달레이 철도 개량 감리,207041_25-11-19 16:54:36_이태훈_보안참여자 권한 추가_홍아름_김혜인,298 +푸옥호아 양수 발전,233465_26-02-23 10:24:46_이철호_폴더 이름 변경_/6 준공/3 준공도서 26년 2월 작성예정 발주처 협의중_/6 준공/3 준공도서 26년 3월 작성예정 발주처 협의중,139 +아시르 지잔 고속도로,234455_26-03-04 14:31:52_이태훈_보안참여자 권한 추가_복진훈,73 +타슈켄트 철도,228919_26-02-05 10:08:11_조항언_파일 업로드_/02_성과품/B_중간보고/BB_중간보고서/[러문] INTERIM REPORT_0115_F.pdf,51 +지방 도로 복원,234456_26-03-04 14:32:07_이태훈_보안참여자 권한 추가_복진훈,0 +Habbaniyah Shuaiba AirBase,234457_26-03-04 14:32:28_이태훈_보안참여자 권한 추가_복진훈,0 +시엠립 하수처리 개선,231205_26-02-09 11:03:05_이태훈_보안참여자 권한 추가_김창환_배형원,221 +반테 민체이 관개 홍수저감,212512_25-12-07 12:35:02_나쉬_파일 업로드_/서류/04. 기성/18차 기성금/06. 통장사본.pdf,44 +메콩유역 수자원 관리 기후적응,207047_25-11-19 17:01:19_이태훈_보안참여자 권한 추가_홍아름_김혜인,0 +잘랄아바드 상수도 계획,234860_26-03-06 10:24:27_-_폴더 자동 삭제 (파일 개수 미달)_/4. MP 성과품/3. 최종보고서/초안 제출,58 +펀잡 홍수 방재,212686_25-12-08 13:05:24_콰윰 아딜_폴더 삭제_/RFP,0 +CAREC 도로 감리,234458_26-03-04 14:32:50_이태훈_보안참여자 권한 추가_복진훈,0 +KP 아보타바드 상수도,234120_26-02-26 20:58:46_정기일_파일 업로드_/99 참고자료/99 체코 두코바니 원전/00 2025년 입찰/Engineering work instruction_241206.pdf,240 +PGN 해상교량 BID2,234454_26-03-04 14:31:31_이태훈_보안참여자 권한 추가_복진훈,631 +홍수 복원 InFRA2,214399_25-12-18 09:05:49_-_폴더 자동 삭제 (파일 개수 미달)_/서류/3.경비신청 및 정산/1. 신청,6 +홍수 관리 Package5B,211353_25-12-02 10:04:18_조명훈_폴더 이름 변경_/서류/01. 계약서/01. 계약서_/서류/01. 계약서/01. 사업계약서,14 +족자~바웬 도로사업,234908_26-03-06 13:36:39_시스템관리-Savannah_참관자 권한 추가_이호성,0 +테치만 상수도 확장,231210_26-02-09 11:06:41_이태훈_보안참여자 권한 추가_배형원_정낙훈,0 +기니 벼 재배단지,216888_26-01-07 11:07:23_-_폴더 자동 삭제 (파일 개수 미달)_/계약서류/경비 신청 및 정산/01. 신청,43 +우간다 벼 재배단지,"212622_25-12-08 11:17:57_박수빈_파일 업로드_/계약서류/경비 신청 및 정산/01. 현장경비 신청전표/11-20251029-J0401-006 (9,10월 현장운영비).pdf",52 +부수쿠마 분뇨 자원화 2단계,231215_26-02-09 11:08:34_이태훈_보안참여자 권한 추가_김창환_배형원,9 +지하수 관개 환경설계,231212_26-02-09 11:07:35_이태훈_보안참여자 권한 추가_김창환_배형원,0 +Adeaa-Becho 지하수 관개,215553_25-12-29 09:36:23_-_폴더 자동 삭제 (파일 개수 미달)_/Topographic Survey/측량조사성과품/Appendix B - List of GPS Control Points,140 +도도타군 관개,215706_25-12-30 09:17:43_-_폴더 자동 삭제 (파일 개수 미달)_/서류/03.경비신청 및 정산/02.사내정산 2차,142 +Iringa 상하수도 개선,231216_26-02-09 11:10:18_이태훈_보안참여자 권한 추가_김창환_배형원,0 +Dodoma 하수 설계감리,231217_26-02-09 11:11:06_이태훈_보안참여자 권한 추가_김창환_배형원,32 +도도마 유수율 상수도개선,232629_26-02-12 17:26:13_서하연_부관리자 권한 추가_정기일,35 +잔지바르 쌀 생산,212641_25-12-08 11:43:37_박수빈_파일 업로드_/서류/경비신청 및 정산/01. 25년/11-20250409-J0401-005 (운영경비 2분기 송금).pdf,23 +SALDEORO 수력발전 28MW,207029_25-11-19 16:44:04_이태훈_보안참여자 권한 추가_홍아름_김혜인,0 +LaPaz Danli 상수도,231219_26-02-09 11:12:37_이태훈_보안참여자 권한 추가_배형원_정낙훈,60 +에스꼬마 차라짜니 도로,234837_26-03-06 08:00:21_-_폴더 자동 삭제 (파일 개수 미달)_/EOI/001/EOI서류,0 +Bombeo-Colomi 도로설계,234463_26-03-04 14:34:24_이태훈_보안참여자 권한 추가_복진훈,48 +마모레 교량도로,234462_26-03-04 14:33:56_이태훈_보안참여자 권한 추가_복진훈,120 +AI 폐기물,207034_25-11-19 16:50:49_이태훈_보안참여자 권한 추가_홍아름_김혜인,0 +도로 통행료 현대화,233938_26-02-25 14:39:08_류창수_폴더 삭제_/필리핀다바오프로젝트,0 +Barranca 상하수도 확장,231220_26-02-09 11:13:28_이태훈_보안참여자 권한 추가_김창환_배형원,44 +태평양 철도,233798_26-02-24 17:52:33_-_폴더 자동 삭제 (파일 개수 미달)_/01_수행문서/D_보고자료/D6_최종보고,101 +필리핀 사무소,234973_26-03-09 09:35:45_한형남_파일 다운로드_/3. DPTMP(Davao Public Transportation Modernization Project)/AFCS/2.배포자료/PHI DPTMP AFCS Package PIM 04March2026.pdf_/3. DPTMP(Davao Public Transportation Modernization Project)/AFCS/2.배포자료/PAFCS PIM v04March2026.pdf,322 +PGN 해상교량 BID2,234454_26-03-04 14:31:31_이태훈_보안참여자 권한 추가_복진훈,631 diff --git a/crwaling_result.csv b/crwaling_result.csv new file mode 100644 index 0000000..5560a02 --- /dev/null +++ b/crwaling_result.csv @@ -0,0 +1,42 @@ +projectName,recentLog,fileCount +ITTC 관개 교육센터,"26.01.29, 박진규, 폴더 삭제",16 +비엔티안 메콩강 관리 2차,"25.12.07, 나쉬, 파일 업로드",260 +만달레이 철도 개량 감리,"25.11.19, 이태훈, 보안참여자 권한 추가",298 +푸옥호아 양수 발전,"26.02.23, 이철호, 폴더 이름 변경",139 +아시르 지잔 고속도로,"26.03.04, 이태훈, 보안참여자 권한 추가",75 +지방 도로 복원,"26.03.04, 이태훈, 보안참여자 권한 추가",0 +타슈켄트 철도,"26.02.05, 조항언, 파일 업로드",51 +Habbaniyah Shuaiba AirBase,"26.03.04, 이태훈, 보안참여자 권한 추가",0 +시엠립 하수처리 개선,"26.02.09, 이태훈, 보안참여자 권한 추가",222 +반테 민체이 관개 홍수저감,"25.12.07, 나쉬, 파일 업로드",0 +메콩유역 수자원 관리 기후적응,"25.11.19, 이태훈, 보안참여자 권한 추가",0 +잘랄아바드 상수도 계획,"26.03.06, -, 폴더 자동 삭제 (파일 개수 미달)",58 +CAREC 도로 감리,"26.03.04, 이태훈, 보안참여자 권한 추가",0 +펀잡 홍수 방재,"25.12.08, 콰윰 아딜, 폴더 삭제",0 +KP 아보타바드 상수도,"26.02.26, 정기일, 파일 업로드",240 +홍수 복원 InFRA2,"25.12.18, -, 폴더 자동 삭제 (파일 개수 미달)",6 +PGN 해상교량 BID2,"26.03.04, 이태훈, 보안참여자 권한 추가",0 +홍수 관리 Package5B,"25.12.02, 조명훈, 폴더 이름 변경",14 +족자~바웬 도로사업,"26.03.06, 시스템관리-Savannah, 참관자 권한 추가",0 +테치만 상수도 확장,"26.02.09, 이태훈, 보안참여자 권한 추가",0 +기니 벼 재배단지,"26.01.07, -, 폴더 자동 삭제 (파일 개수 미달)",44 +부수쿠마 분뇨 자원화 2단계,"26.02.09, 이태훈, 보안참여자 권한 추가",9 +우간다 벼 재배단지,"25.12.08, 박수빈, 파일 업로드",52 +Adeaa-Becho 지하수 관개,"25.12.29, -, 폴더 자동 삭제 (파일 개수 미달)",140 +도도타군 관개,"25.12.30, -, 폴더 자동 삭제 (파일 개수 미달)",0 +지하수 관개 환경설계,"26.02.09, 이태훈, 보안참여자 권한 추가",0 +Dodoma 하수 설계감리,"26.02.09, 이태훈, 보안참여자 권한 추가",32 +Iringa 상하수도 개선,"26.02.09, 이태훈, 보안참여자 권한 추가",0 +도도마 유수율 상수도개선,"26.02.12, 서하연, 부관리자 권한 추가",35 +잔지바르 쌀 생산,"25.12.08, 박수빈, 파일 업로드",23 +SALDEORO 수력발전 28MW,"25.11.19, 이태훈, 보안참여자 권한 추가",0 +LaPaz Danli 상수도,"26.02.09, 이태훈, 보안참여자 권한 추가",65 +에스꼬마 차라짜니 도로,"26.03.06, -, 폴더 자동 삭제 (파일 개수 미달)",0 +마모레 교량도로,"26.03.04, 이태훈, 보안참여자 권한 추가",120 +Bombeo-Colomi 도로설계,"26.03.04, 이태훈, 보안참여자 권한 추가",49 +AI 폐기물,"25.11.19, 이태훈, 보안참여자 권한 추가",0 +도로 통행료 현대화,"26.02.25, 류창수, 폴더 삭제",0 +Barranca 상하수도 확장,"26.02.09, 이태훈, 보안참여자 권한 추가",50 +태평양 철도,"26.02.24, -, 폴더 자동 삭제 (파일 개수 미달)",101 +필리핀 사무소,"26.03.06, 한형남, 파일 다운로드",0 +PGN 해상교량 BID2,"26.03.04, 이태훈, 보안참여자 권한 추가",637 diff --git a/db_2026.03.09.csv b/db_2026.03.09.csv new file mode 100644 index 0000000..475e24e --- /dev/null +++ b/db_2026.03.09.csv @@ -0,0 +1,42 @@ +[PM Overseas 프로젝트 현황],,2026.03.04,,,,,,<<활동로그가 없는 프로젝트 (8),, +,,,,,,,,,, +No.,프로젝트 명,담당부서,담당자,종료(예정)일,최근 활동로그,과업개요 작성 유무,파일 수,비고,, +1,라오스 ITTC 관개 교육센터 PMC,수자원1부,방노성,2025.12.20,"2026.01.29, 폴더 삭제",O,16,2026.01.29 로그는 테스트 활동 추정,종료(예정)일 지남,진행 +2,라오스 비엔티안 메콩강 관리 2차 DD,수자원1부,방노성,2026.05.31,"2025.12.07, 파일업로드",X,260,탭 1개에 모든파일 업로드,, +3,미얀마 만달레이 철도 개량 감리 CS,철도사업부,김태헌,2027.11.17,"2025.11.17, 폴더이름변경",O,298,,, +4,베트남 푸옥호아 양수 발전 FS,수력부,이철호,2025.11.30,"2026.02.23, 폴더이름변경",O,139,준공도서 3월 작성예정,종료(예정)일 지남,준공 +5,사우디아라비아 아시르 지잔 고속도로 FS,도로부,공태원,2025.11.21,"2026.02.09, 파일다운로드",O,73,,종료(예정)일 지남,준공 +6,우즈베키스탄 지방 도로 복원 MP,도로부,장진영,2029.04.28,X,X,0,,, +7,우즈베키스탄 타슈켄트 철도 FS,철도사업부,김태헌,2026.03.20,"2026.02.05, 파일업로드",O,51,,, +8,이라크 Habbaniyah Shuaiba AirBase PD,도로부,강동구,2026.12.31,X,X,0,,, +9,메콩유역 수자원 관리 기후적응 MP,수자원1부,정귀한,2025.12.31,X,X,0,,종료(예정)일 지남,준공 +10,캄보디아 반테 민체이 관개 홍수저감 MP,수자원1부,이대주,2026.08.28,"2025.12.07, 파일업로드",X,44,,, +11,캄보디아 시엠립 하수처리 개선 DD,물환경사업1부,변역근,2028.12.18,"2026.02.06, AI 요약",O,221,,, +12,키르기스스탄 잘랄아바드 상수도 계획 MP,물환경사업1부,변기상,2025.12.31,"2026.02.12, 파일업로드",X,60,,종료(예정)일 지남,준공 +13,파키스탄 펀잡 홍수 방재 PMC,수자원1부,방노성,2027.12.31,"2025.12.08, 폴더삭제",O,0,,, +14,파키스탄 KP 아보타바드 상수도 PMC,물환경사업2부,변기상,2026.12.31,"2026.02.26, 파일업로드",O,240,,, +15,파키스탄 CAREC 도로 감리 DD,도로부,황효섭,2026.10.26,X,X,0,,, +16,필리핀 홍수 복원 InFRA2 DD,수자원1부,이대주,2026.08.07,"2025.12.01, 폴더삭제",O,6,최근로그 >> 폴더자동삭제(파일 개수 미달),, +17,필리핀 홍수 관리 Package5B MP,수자원1부,이희철,2026.05.31,"2025.12.02, 폴더이름변경",O,14,,, +18,필리핀 PGN 해상교량 BID2 IDC,구조부,이상희,2026.05.31,"2026.02.11, 파일다운로드",O,631,,, +19,가나 테치만 상수도 확장 DS,물환경사업2부,-,2029.04.25,X,X,0,책임자 및 담당자 설정X,, +20,기니 벼 재배단지 PMC,수자원1부,이대주,2028.12.20,"2025.12.08, 파일업로드",O,43,최근로그 >> 폴더자동삭제(파일 개수 미달),, +21,우간다 벼 재배단지 PMC,수자원1부,방노성,2028.12.20,"2025.12.08, 파일업로드",O,52,,, +22,우간다 부수쿠마 분뇨 자원화 2단계 PMC,물환경사업2부,변기상,2026.12.31,"2026.02.05, 파일업로드",X,9,,, +23,에티오피아 지하수 관개 환경설계 DD,물환경사업2부,변기상,2026.06.23,X,X,0,,, +24,에티오피아 도도타군 관개 PMC,수자원1부,방노성,2026.12.31,"2025.12.01, 폴더이름변경",O,144,탭 1개에 모든파일 업로드 // 최근로그 >> 폴더자동삭제(파일 개수 미달),, +25,에티오피아 Adeaa-Becho 지하수 관개 MP,수자원1부,방노성,2026.07.31,"2025.11.21, 파일업로드",O,146,최근로그 >> 폴더자동삭제(파일 개수 미달),, +26,탄자니아 Iringa 상하수도 개선 CS,물환경사업1부,백운영,2029.06.08,"2026.02.03, 폴더생성",X,0,,, +27,탄자니아 Dodoma 하수 설계감리 DD,물환경사업2부,변기상,2027.07.08,"2026.02.04, 폴더삭제",X,32,,, +28,탄자니아 잔지바르 쌀 생산 PMC,수자원1부,방노성,2027.12.20,"2025.12.08, 파일 업로드",O,23,,, +29,탄자니아 도도마 유수율 상수도개선 PMC,물환경사업1부,박순석,2026.12.31,"2026.02.12, 부관리자권한추가",X,35,,, +30,아르헨티나 SALDEORO 수력발전 28MW DD,플랜트1부,양정모,2026.01.31,X,X,0,,종료(예정)일 지남,준공 +31,온두라스 LaPaz Danli 상수도 CS,물환경사업2부,-,2027.02.23,"2026.01.29, 파일 삭제",O,60,"책임자 및 담당자 설정 X, 실 관리부서는 해외사업부, 더미파일 다수",, +32,볼리비아 에스꼬마 차라짜니 도로 CS,도로부,전홍찬,2029.12.15,"2026.02.06, 파일업로드",X,1,,, +33,볼리비아 마모레 교량도로 FS,도로부,황효섭,2025.10.17,"2026.02.06, 파일업로드",X,120,,종료(예정)일 지남,준공 +34,볼리비아 Bombeo-Colomi 도로설계 DD,도로부,황효섭,2026.07.24,"2025.12.05, 파일삭제",O,48,"더미파일(폴더유지용) 12개, 실 관리부서는 해외사업부",, +35,콜롬비아 AI 폐기물 FS,플랜트1부,서재희,2026.02.27,X,X,0,,종료(예정)일 지남, +36,파라과이 도로 통행료 현대화 MP,교통계획부,오제훈,2025.10.24,"2025.02.25, 폴더삭제",X,0,,종료(예정)일 지남,준공 +37,페루 Barranca 상하수도 확장 DD,물환경사업2부,변기상,2026.03.08,"2025.11.14, 파일업로드",O,44,"더미파일(폴더유지용) 27개, 실 관리부서는 해외사업부",, +38,엘살바도르 태평양 철도 FS,철도사업부,김태헌,2025.12.31,"2026.02.24, 폴더자동삭제",X,101,,종료(예정)일 지남,준공 +39,필리핀 사무소,해외사업부,한형남,,"2026-03-09, 파일다운로드",과업개요 페이지 없음,323,,, diff --git a/db_2026.03.10.csv b/db_2026.03.10.csv new file mode 100644 index 0000000..4708abf --- /dev/null +++ b/db_2026.03.10.csv @@ -0,0 +1,42 @@ +[PM Overseas 프로젝트 현황],,2026.03.04,,,,,,<<활동로그가 없는 프로젝트 (8),, +,,,,,,,,,, +No.,프로젝트 명,담당부서,담당자,종료(예정)일,최근 활동로그,과업개요 작성 유무,파일 수,비고,, +1,라오스 ITTC 관개 교육센터 PMC,수자원1부,방노성,2025.12.20,"2026.01.29, 폴더 삭제",O,16,2026.01.29 로그는 테스트 활동 추정,종료(예정)일 지남,진행 +2,라오스 비엔티안 메콩강 관리 2차 DD,수자원1부,방노성,2026.05.31,"2025.12.07, 파일업로드",X,260,탭 1개에 모든파일 업로드,, +3,미얀마 만달레이 철도 개량 감리 CS,철도사업부,김태헌,2027.11.17,"2025.11.17, 폴더이름변경",O,298,,, +4,베트남 푸옥호아 양수 발전 FS,수력부,이철호,2025.11.30,"2026.02.23, 폴더이름변경",O,139,준공도서 3월 작성예정,종료(예정)일 지남,준공 +5,사우디아라비아 아시르 지잔 고속도로 FS,도로부,공태원,2025.11.21,"2026.02.09, 파일다운로드",O,73,,종료(예정)일 지남,준공 +6,우즈베키스탄 지방 도로 복원 MP,도로부,장진영,2029.04.28,X,X,0,,, +7,우즈베키스탄 타슈켄트 철도 FS,철도사업부,김태헌,2026.03.20,"2026.02.05, 파일업로드",O,51,,, +8,이라크 Habbaniyah Shuaiba AirBase PD,도로부,강동구,2026.12.31,X,X,0,,, +9,메콩유역 수자원 관리 기후적응 MP,수자원1부,정귀한,2025.12.31,X,X,0,,종료(예정)일 지남,준공 +10,캄보디아 반테 민체이 관개 홍수저감 MP,수자원1부,이대주,2026.08.28,"2025.12.07, 파일업로드",X,44,,, +11,캄보디아 시엠립 하수처리 개선 DD,물환경사업1부,변역근,2028.12.18,"2026.02.06, AI 요약",O,221,,, +12,키르기스스탄 잘랄아바드 상수도 계획 MP,물환경사업1부,변기상,2025.12.31,"2026.02.12, 파일업로드",X,60,,종료(예정)일 지남,준공 +13,파키스탄 펀잡 홍수 방재 PMC,수자원1부,방노성,2027.12.31,"2025.12.08, 폴더삭제",O,0,,, +14,파키스탄 KP 아보타바드 상수도 PMC,물환경사업2부,변기상,2026.12.31,"2026.02.26, 파일업로드",O,240,,, +15,파키스탄 CAREC 도로 감리 DD,도로부,황효섭,2026.10.26,X,X,0,,, +16,필리핀 홍수 복원 InFRA2 DD,수자원1부,이대주,2026.08.07,"2025.12.01, 폴더삭제",O,6,최근로그 >> 폴더자동삭제(파일 개수 미달),, +17,필리핀 홍수 관리 Package5B MP,수자원1부,이희철,2026.05.31,"2025.12.02, 폴더이름변경",O,14,,, +18,필리핀 PGN 해상교량 BID2 IDC,구조부,이상희,2026.05.31,"2026.02.11, 파일다운로드",O,631,,, +19,가나 테치만 상수도 확장 DS,물환경사업2부,-,2029.04.25,X,X,0,책임자 및 담당자 설정X,, +20,기니 벼 재배단지 PMC,수자원1부,이대주,2028.12.20,"2025.12.08, 파일업로드",O,43,최근로그 >> 폴더자동삭제(파일 개수 미달),, +21,우간다 벼 재배단지 PMC,수자원1부,방노성,2028.12.20,"2025.12.08, 파일업로드",O,52,,, +22,우간다 부수쿠마 분뇨 자원화 2단계 PMC,물환경사업2부,변기상,2026.12.31,"2026.02.05, 파일업로드",X,9,,, +23,에티오피아 지하수 관개 환경설계 DD,물환경사업2부,변기상,2026.06.23,X,X,0,,, +24,에티오피아 도도타군 관개 PMC,수자원1부,방노성,2026.12.31,"2025.12.01, 폴더이름변경",O,144,탭 1개에 모든파일 업로드 // 최근로그 >> 폴더자동삭제(파일 개수 미달),, +25,에티오피아 Adeaa-Becho 지하수 관개 MP,수자원1부,방노성,2026.07.31,"2025.11.21, 파일업로드",O,146,최근로그 >> 폴더자동삭제(파일 개수 미달),, +26,탄자니아 Iringa 상하수도 개선 CS,물환경사업1부,백운영,2029.06.08,"2026.02.03, 폴더생성",X,0,,, +27,탄자니아 Dodoma 하수 설계감리 DD,물환경사업2부,변기상,2027.07.08,"2026.02.04, 폴더삭제",X,32,,, +28,탄자니아 잔지바르 쌀 생산 PMC,수자원1부,방노성,2027.12.20,"2025.12.08, 파일 업로드",O,23,,, +29,탄자니아 도도마 유수율 상수도개선 PMC,물환경사업1부,박순석,2026.12.31,"2026.02.12, 부관리자권한추가",X,35,,, +30,아르헨티나 SALDEORO 수력발전 28MW DD,플랜트1부,양정모,2026.01.31,X,X,0,,종료(예정)일 지남,준공 +31,온두라스 LaPaz Danli 상수도 CS,물환경사업2부,-,2027.02.23,"2026.01.29, 파일 삭제",O,60,"책임자 및 담당자 설정 X, 실 관리부서는 해외사업부, 더미파일 다수",, +32,볼리비아 에스꼬마 차라짜니 도로 CS,도로부,전홍찬,2029.12.15,"2026.02.06, 파일업로드",X,1,,, +33,볼리비아 마모레 교량도로 FS,도로부,황효섭,2025.10.17,"2026.02.06, 파일업로드",X,120,,종료(예정)일 지남,준공 +34,볼리비아 Bombeo-Colomi 도로설계 DD,도로부,황효섭,2026.07.24,"2025.12.05, 파일삭제",O,48,"더미파일(폴더유지용) 12개, 실 관리부서는 해외사업부",, +35,콜롬비아 AI 폐기물 FS,플랜트1부,서재희,2026.02.27,X,X,0,,종료(예정)일 지남, +36,파라과이 도로 통행료 현대화 MP,교통계획부,오제훈,2025.10.24,"2025.02.25, 폴더삭제",X,0,,종료(예정)일 지남,준공 +37,페루 Barranca 상하수도 확장 DD,물환경사업2부,변기상,2026.03.08,"2025.11.14, 파일업로드",O,44,"더미파일(폴더유지용) 27개, 실 관리부서는 해외사업부",, +38,엘살바도르 태평양 철도 FS,철도사업부,김태헌,2025.12.31,"2026.02.24, 폴더자동삭제",X,101,,종료(예정)일 지남,준공 +39,필리핀 사무소,해외사업부,한형남,,"2026.03.10, PDF 변환",과업개요 페이지 없음,829,,, diff --git a/diag_folders.py b/diag_folders.py new file mode 100644 index 0000000..d63fa8e --- /dev/null +++ b/diag_folders.py @@ -0,0 +1,66 @@ +import asyncio, os, json, re, sys +from playwright.async_api import async_playwright +from dotenv import load_dotenv + +load_dotenv() + +async def run_diagnostics(): + user_id = os.getenv("PM_USER_ID") + password = os.getenv("PM_PASSWORD") + + async with async_playwright() as p: + browser = await p.chromium.launch(headless=False) + context = await browser.new_context(viewport={"width": 1600, "height": 900}) + page = await context.new_page() + + print(">>> 로그인 중...") + await page.goto("https://overseas.projectmastercloud.com/dashboard") + if await page.locator("#login-by-id").is_visible(timeout=5000): + await page.click("#login-by-id") + await page.fill("#user_id", user_id) + await page.fill("#user_pw", password) + await page.click("#login-btn") + + await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=60000) + + project_name = "필리핀 사무소" + print(f">>> [{project_name}] 폴더 전수 조사 시작...") + + target_el = page.get_by_text(project_name).first + await target_el.scroll_into_view_if_needed() + await target_el.click(force=True) + + await asyncio.sleep(10) # 충분한 로딩 대기 + + print("\n" + "="*60) + print(f"{'No':<4} | {'Folder Name':<40} | {'Files'}") + print("-" * 60) + + # fetch 결과를 직접 리턴받음 + tree_data = await page.evaluate("""async () => { + const resp = await fetch('/api/getTreeObject?params[storageType]=CLOUD¶ms[resourcePath]=/'); + return await resp.json(); + }""") + + if tree_data: + tree = tree_data.get('currentTreeObject', {}) + folders = tree.get('folder', {}) + folder_items = list(folders.values()) if isinstance(folders, dict) else (folders if isinstance(folders, list) else []) + + total_sum = 0 + for i, f in enumerate(folder_items): + name = f.get('name', 'Unknown') + count = int(f.get('filesCount', 0)) + total_sum += count + print(f"{i+1:<4} | {name:<40} | {count}개") + + print("-" * 60) + print(f">>> 총 {len(folder_items)}개 폴더 발견 | 전체 파일 합계: {total_sum}개") + print("="*60) + else: + print(">>> [오류] 데이터를 가져오지 못했습니다.") + + await browser.close() + +if __name__ == "__main__": + asyncio.run(run_diagnostics()) diff --git a/js/dashboard.js b/js/dashboard.js index d057f79..792fb01 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -37,19 +37,9 @@ async function init() { rawData.forEach((item, index) => { const projectName = item[0]; - let continent = ""; - let country = ""; - - if (projectName.endsWith("사무소")) { - continent = "지사"; - country = projectName.split(" ")[0]; - } else if (projectName.startsWith("메콩유역")) { - country = "캄보디아"; - continent = "아시아"; - } else { - country = projectName.split(" ")[0]; - continent = continentMap[country] || "기타"; - } + // DB에서 넘어온 대륙과 국가 정보 사용 (item[5], item[6]) + let continent = item[5] || "기타"; + let country = item[6] || "미분류"; if (!groupedData[continent]) groupedData[continent] = {}; if (!groupedData[continent][country]) groupedData[continent][country] = []; diff --git a/log_debug.png b/log_debug.png new file mode 100644 index 0000000..dcb29f1 Binary files /dev/null and b/log_debug.png differ diff --git a/server.log b/server.log index d3b7c7b..9a46719 100644 Binary files a/server.log and b/server.log differ diff --git a/server.py b/server.py index 32b06fa..5a9be86 100644 --- a/server.py +++ b/server.py @@ -42,53 +42,52 @@ app.add_middleware( # --- HTML 라우팅 --- -import csv +import pymysql + +def get_db_connection(): + return pymysql.connect( + host='localhost', + user='root', + password='45278434', + database='crawling', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) @app.get("/project-data") async def get_project_data(): """ - sheet.csv 파일을 읽어서 프로젝트 현황 데이터를 반환 + MySQL overseas_projects 테이블에서 프로젝트 현황 데이터를 반환 """ - projects = [] try: - if not os.path.exists("sheet.csv"): - return [] - - with open("sheet.csv", mode="r", encoding="utf-8-sig") as f: - reader = csv.reader(f) - rows = list(reader) - - # "No." 헤더를 찾아 데이터 시작점 결정 - start_idx = None - for i, row in enumerate(rows): - if any("No." in cell for cell in row): - start_idx = i + 1 - break - - if start_idx is not None: - for row in rows[start_idx:]: - if len(row) >= 8: - # [프로젝트명, 담당부서, 담당자, 최근활동로그, 파일수] - # 복구된 sheet.csv 형식에 맞춰 인덱스 추출 (1, 2, 3, 5, 7) - try: - # 파일 수 숫자로 변환 (공백 제거 후 처리) - raw_count = row[7].strip() - file_count = int(raw_count) if raw_count.isdigit() else 0 - except (ValueError, IndexError): - file_count = 0 - - projects.append([ - row[1], # 프로젝트 명 - row[2], # 담당부서 - row[3], # 담당자 - row[5], # 최근 활동로그 - file_count # 파일 수 - ]) + conn = get_db_connection() + try: + with conn.cursor() as cursor: + # 대시보드에 필요한 모든 정보를 쿼리 (short_nm 포함) + cursor.execute("SELECT project_nm, short_nm, department, master, recent_log, file_count, continent, country FROM overseas_projects ORDER BY id ASC") + rows = cursor.fetchall() + + # 프론트엔드 기대 형식에 맞춰 반환 + # [표시될 프로젝트명(short_nm), 담당부서, 담당자, 최근활동로그, 파일수, 대륙, 국가] + projects = [] + for row in rows: + # short_nm이 있으면 그것을 쓰고, 없으면 project_nm 사용 + display_name = row['short_nm'] if row['short_nm'] and row['short_nm'].strip() else row['project_nm'] + projects.append([ + display_name, + row['department'], + row['master'], + row['recent_log'], + row['file_count'], + row['continent'], + row['country'] + ]) + return projects + finally: + conn.close() except Exception as e: - print(f"Error reading sheet.csv: {e}") + print(f"Error fetching from DB: {e}") return {"error": str(e)} - - return projects @app.get("/") async def root(request: Request): diff --git a/server_reboot.log b/server_reboot.log new file mode 100644 index 0000000..8a8f47f Binary files /dev/null and b/server_reboot.log differ diff --git a/server_revert.log b/server_revert.log new file mode 100644 index 0000000..9554606 Binary files /dev/null and b/server_revert.log differ diff --git a/server_startup.log b/server_startup.log new file mode 100644 index 0000000..6432ab7 Binary files /dev/null and b/server_startup.log differ diff --git a/test_main_filtered.py b/test_main_filtered.py new file mode 100644 index 0000000..ceed5ba --- /dev/null +++ b/test_main_filtered.py @@ -0,0 +1,33 @@ +import asyncio, os, json, queue, threading +from crawler_service import crawler_thread_worker +from dotenv import load_dotenv + +load_dotenv() + +async def run_main_test(): + user_id = os.getenv("PM_USER_ID") + password = os.getenv("PM_PASSWORD") + msg_queue = queue.Queue() + + thread = threading.Thread(target=crawler_thread_worker, args=(msg_queue, user_id, password)) + thread.start() + + print(">>> 메인 워커 실행 중 (필리핀 사무소)...") + try: + while True: + msg_raw = await asyncio.to_thread(msg_queue.get, timeout=300) + if msg_raw is None: break + + msg = json.loads(msg_raw) + if msg["type"] == "log": + print(f"[LOG] {msg['message']}") + elif msg["type"] == "done": + print(f"\n[DONE] 최종 결과: {msg['data']}") + break + except queue.Empty: + print(">>> 타임아웃") + finally: + thread.join() + +if __name__ == "__main__": + asyncio.run(run_main_test())