feat: MySQL DB 연동 및 크롤링 로직 정상화 (ID 매칭 및 데이터 정밀화)
This commit is contained in:
2
.env
2
.env
@@ -1,2 +1,2 @@
|
||||
PM_USER_ID=b21364
|
||||
PM_PASSWORD=b21364!.
|
||||
PM_PASSWORD=b21364!.`nDB_HOST=localhost`nDB_USER=root`nDB_PASSWORD=45278434`nDB_NAME=crawling
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
141
backups/crawler_service_v1_dom.py.bak
Normal file
141
backups/crawler_service_v1_dom.py.bak
Normal file
@@ -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()
|
||||
BIN
composition_debug.png
Normal file
BIN
composition_debug.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -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()
|
||||
|
||||
42
crawling_result 2026.03.06.csv
Normal file
42
crawling_result 2026.03.06.csv
Normal file
@@ -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
|
||||
|
42
crawling_result 2026.03.09.csv
Normal file
42
crawling_result 2026.03.09.csv
Normal file
@@ -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
|
||||
|
42
crwaling_result.csv
Normal file
42
crwaling_result.csv
Normal file
@@ -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
|
||||
|
42
db_2026.03.09.csv
Normal file
42
db_2026.03.09.csv
Normal file
@@ -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,,,
|
||||
|
42
db_2026.03.10.csv
Normal file
42
db_2026.03.10.csv
Normal file
@@ -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,,,
|
||||
|
66
diag_folders.py
Normal file
66
diag_folders.py
Normal file
@@ -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())
|
||||
@@ -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] = [];
|
||||
|
||||
BIN
log_debug.png
Normal file
BIN
log_debug.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
BIN
server.log
BIN
server.log
Binary file not shown.
77
server.py
77
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):
|
||||
|
||||
BIN
server_reboot.log
Normal file
BIN
server_reboot.log
Normal file
Binary file not shown.
BIN
server_revert.log
Normal file
BIN
server_revert.log
Normal file
Binary file not shown.
BIN
server_startup.log
Normal file
BIN
server_startup.log
Normal file
Binary file not shown.
33
test_main_filtered.py
Normal file
33
test_main_filtered.py
Normal file
@@ -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())
|
||||
Reference in New Issue
Block a user