feat: 프로젝트 활성도 분석 시스템 및 크롤링 인증/중단 기능 구현 - DB 연결 최적화, 활성도 위젯 및 내비게이션, 관리자 인증 모달, 중단 기능, UI 레이아웃 최적화, 코드 리팩토링 및 파일 정리

This commit is contained in:
2026-03-11 14:03:26 +09:00
parent 4a995c11f4
commit 9f06857bea
26 changed files with 587 additions and 1323 deletions

6
.env
View File

@@ -1,2 +1,6 @@
PM_USER_ID=b21364
PM_PASSWORD=b21364!.`nDB_HOST=localhost`nDB_USER=root`nDB_PASSWORD=45278434`nDB_NAME=crawling
PM_PASSWORD=b21364!.
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=45278434
DB_NAME=PM_proto

View File

@@ -37,12 +37,18 @@ AI는 파일을 분류할 때 단순한 키워드 매칭이 아닌, 아래의 **
---
# 프로젝트 관리 규칙
# 🛠️ 개발 및 관리 규칙 (Strict Development Rules)
1. **언어 설정**: 영어로 생각하되, 모든 답변은 한국어로 작성한다. (일본어, 중국어는 절대 사용하지 않는다.)
2. **수정 권한 제한**: 사용자가 명시적으로 지시한 사항 외에는 **절대 절대 절대** 코드를 임의로 수정하지 않는다.
3. **로그 기록 철저**: 모달 오픈 여부, 수집 성공/실패 여부 등 진행 상황을 실시간 로그에 상세히 표시한다.
4. **선보고 후승인**: 모든 기능 수정 및 코드 변경 전에는 예상 방안을 먼저 보고하고, 사용자가 **'진행시켜'**라고 명령한 경우에만 작업을 수행한다.
1. **언어 설정**: 영어로 생각하되, 모든 답변은 **한국어**로 작성한다.
2. **임의 수정 절대 금지 (Zero-Arbitrary Change)**:
- 사용자가 명시적으로 지시한 부분 외에는 **단 한 줄의 코드도, 그 어떤 파일도 임의로 수정, 정리, 리팩토링하지 않는다.**
- 지시받지 않은 다른 파트의 코드는 절대 건드리지 않으며, 영향 범위가 요청 범위를 벗어나지 않도록 '외과 수술식(Surgical) 수정'을 원칙으로 한다.
3. **개선 작업 절차 (Test-First Approach)**:
- 사용자가 개선(Refactoring, Optimization 등)을 지시한 경우, **수정 전 현재 시스템이 정상적으로 잘 작동하는지 먼저 전수 확인**한다.
- 기존 동작 방식과 성능을 기준(Baseline)으로 삼고, 수정 후에도 **기존의 모든 기능이 무결하게 유지되는지 반드시 테스트하여 입증**한다.
- 검증 결과를 바탕으로 "무엇을, 왜, 어떻게" 바꿀지 상세 보고 후, 사용자로부터 **'진행시켜'** 승인을 얻은 뒤에만 집행한다.
4. **선보고 후승인**: 모든 기능 수정 및 코드 변경 전에는 예상 방안을 먼저 보고하고 승인 절차를 거친다.
5. **로그 기록 철저**: 진행 상황(로그인, 수집, 오류 등)을 실시간 로그에 상세히 표시하여 투명성을 확보한다.
---

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -11,15 +11,18 @@ from datetime import datetime
from playwright.async_api import async_playwright
from dotenv import load_dotenv
load_dotenv()
load_dotenv(override=True)
# 글로벌 중단 제어용 이벤트
crawl_stop_event = threading.Event()
def get_db_connection():
"""MySQL 데이터베이스 연결을 반환합니다."""
"""MySQL 데이터베이스 연결을 반환 (환경변수 기반)"""
return pymysql.connect(
host='localhost',
user='root',
password='45278434',
database='crawling',
host=os.getenv('DB_HOST', 'localhost'),
user=os.getenv('DB_USER', 'root'),
password=os.getenv('DB_PASSWORD', '45278434'),
database=os.getenv('DB_NAME', 'PM_proto'),
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
@@ -27,12 +30,10 @@ def get_db_connection():
def clean_date_string(date_str):
if not date_str: return ""
match = re.search(r'(\d{2})[./-](\d{2})[./-](\d{2})', date_str)
if match:
return f"20{match.group(1)}.{match.group(2)}.{match.group(3)}"
if match: return f"20{match.group(1)}.{match.group(2)}.{match.group(3)}"
return date_str[:10].replace("-", ".")
def parse_log_id(log_id):
"""ID 구조: 로그고유번호_시간_활동한 사람_활동내용_활동대상"""
if not log_id or "_" not in log_id: return log_id
try:
parts = log_id.split('_')
@@ -45,6 +46,7 @@ def parse_log_id(log_id):
return log_id
def crawler_thread_worker(msg_queue, user_id, password):
crawl_stop_event.clear()
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
@@ -55,11 +57,18 @@ def crawler_thread_worker(msg_queue, user_id, password):
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})
msg_queue.put(json.dumps({'type': 'log', 'message': '브라우저 엔진 가동 (전 기능 복구 모드)...'}))
browser = await p.chromium.launch(headless=False, args=[
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-blink-features=AutomationControlled"
])
context = await browser.new_context(
viewport={'width': 1600, 'height': 900},
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"
)
captured_data = {"tree": None, "_is_root_archive": False, "_tree_url": "", "project_list": []}
captured_data = {"tree": None, "_is_root_archive": False, "project_list": []}
async def global_interceptor(response):
url = response.url
@@ -68,17 +77,13 @@ def crawler_thread_worker(msg_queue, user_id, password):
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
captured_data["tree"] = await response.json()
captured_data["_is_root_archive"] = True
except: pass
context.on("response", global_interceptor)
@@ -86,54 +91,52 @@ def crawler_thread_worker(msg_queue, user_id, password):
await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded")
# 로그인
if await page.locator("#login-by-id").is_visible(timeout=5000):
if await page.locator("#login-by-id").is_visible(timeout=10000):
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 기초 정보 동기화 (마스터 테이블)
# [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"]:
try:
sql = """
INSERT INTO projects_master (project_id, project_nm, short_nm, master, continent, country)
VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
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_info.get("project_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 마스터 정보 동기화 완료.'}))
sql = """
INSERT INTO projects_master (project_id, project_nm, short_nm, master, continent, country)
VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
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_info.get("project_nm"),
p_info.get("short_nm", "").strip(), p_info.get("master"),
p_info.get("large_class"), p_info.get("mid_class")))
conn.commit()
msg_queue.put(json.dumps({'type': 'log', 'message': 'DB 마스터 정보 동기화 완료.'}))
finally: conn.close()
# [Phase 2] h4 태그 기반 수집 루프
# [Phase 2] 수집 루프
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 매칭 (저장용)
if crawl_stop_event.is_set():
msg_queue.put(json.dumps({'type': 'log', 'message': '>>> 중단 신호 감지: 종료합니다.'}))
break
msg_queue.put(json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] {project_name} 수집 시작'}))
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
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. 프로젝트 진입 ([완전 복원] 좌표 클릭)
# 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()
@@ -143,44 +146,34 @@ def crawler_thread_worker(msg_queue, user_id, password):
await page.wait_for_selector("text=활동로그", timeout=30000)
await asyncio.sleep(2)
recent_log = "데이터 없음"
file_count = 0
recent_log = "데이터 없음"; file_count = 0
# 2. 활동로그 ([완전 복원] 3회 재시도 + 좌표 클릭 + 날짜 필터)
# 2. 활동로그 (날짜 필터 적용 버전)
modal_opened = False
for _ in range(3):
log_btn = page.get_by_text("활동로그").first
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())
await page.get_by_text("활동로그").first.click()
try:
await page.wait_for_selector("article.archive-modal", timeout=5000)
modal_opened = True
break
modal_opened = True; break
except: await asyncio.sleep(1)
if modal_opened:
# 날짜 필터 입력
# 날짜 필터 2020-01-01 적용
inputs = await page.locator("article.archive-modal input").all()
for inp in inputs:
if (await inp.get_attribute("type")) == "date":
await inp.fill("2020-01-01")
break
await inp.fill("2020-01-01"); break
apply_btn = page.locator("article.archive-modal").get_by_text("적용").first
if await apply_btn.is_visible():
await apply_btn.click()
await asyncio.sleep(5) # 렌더링 보장
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}'}))
recent_log = parse_log_id(await log_elements[0].get_attribute("id"))
await page.keyboard.press("Escape")
# 3. 구성 수집 ([완전 복원] BaseURL fetch + 정밀 합산)
# 3. 구성 수집 (API Fetch 방식 - 팝업 없음)
await page.evaluate("""() => {
const baseUrl = window.location.origin + window.location.pathname.split('/').slice(0, 2).join('/');
fetch(`${baseUrl}/archive/getTreeObject?params[storageType]=CLOUD&params[resourcePath]=/`);
@@ -190,43 +183,26 @@ def crawler_thread_worker(msg_queue, user_id, password):
await asyncio.sleep(0.5)
if captured_data["tree"]:
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 합산
tree = captured_data["tree"].get('currentTreeObject', captured_data["tree"])
total = len(tree.get("file", {}))
folders = tree.get("folder", {})
if isinstance(folders, dict):
for f in folders.values():
c = f.get("filesCount", "0")
total += int(c) if str(c).isdigit() else 0
for f in folders.values(): total += int(f.get("filesCount", 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}개)'}))
# 4. DB 실시간 저장 (히스토리 테이블)
# 4. DB 실시간 저장
if current_p_id:
conn = get_db_connection()
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
# 오늘 날짜 히스토리 데이터 삽입 또는 업데이트
sql = """
INSERT INTO projects_history (project_id, crawl_date, recent_log, file_count)
VALUES (%s, CURRENT_DATE(), %s, %s)
ON DUPLICATE KEY UPDATE
recent_log = VALUES(recent_log), file_count = VALUES(file_count)
"""
sql = "INSERT INTO projects_history (project_id, crawl_date, recent_log, file_count) VALUES (%s, CURRENT_DATE(), %s, %s) ON DUPLICATE KEY UPDATE recent_log=VALUES(recent_log), file_count=VALUES(file_count)"
cursor.execute(sql, (current_p_id, recent_log, file_count))
conn.commit()
msg_queue.put(json.dumps({'type': 'log', 'message': f' - [DB] 히스토리 업데이트 완료 (ID: {current_p_id})'}))
finally: conn.close()
msg_queue.put(json.dumps({'type': 'log', 'message': f' - [성공] 로그: {recent_log[:20]}... / 파일: {file_count}'}))
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)}'}))
msg_queue.put(json.dumps({'type': 'log', 'message': f' - {project_name} 실패: {str(e)}'}))
await page.goto("https://overseas.projectmastercloud.com/dashboard")
msg_queue.put(json.dumps({'type': 'done', 'data': []}))
@@ -241,10 +217,8 @@ def crawler_thread_worker(msg_queue, user_id, password):
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 = threading.Thread(target=crawler_thread_worker, args=(msg_queue, os.getenv("PM_USER_ID"), os.getenv("PM_PASSWORD")))
thread.start()
while True:
try:

View File

@@ -1,42 +0,0 @@
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
1 projectName recentLog fileCount
2 ITTC 관개 교육센터 26.01.29, 박진규, 폴더 삭제 16
3 비엔티안 메콩강 관리 2차 25.12.07, 나쉬, 파일 업로드 260
4 만달레이 철도 개량 감리 25.11.19, 이태훈, 보안참여자 권한 추가 298
5 푸옥호아 양수 발전 26.02.23, 이철호, 폴더 이름 변경 139
6 아시르 지잔 고속도로 26.03.04, 이태훈, 보안참여자 권한 추가 73
7 지방 도로 복원 26.03.04, 이태훈, 보안참여자 권한 추가 0
8 타슈켄트 철도 26.02.05, 조항언, 파일 업로드 51
9 Habbaniyah Shuaiba AirBase 26.03.04, 이태훈, 보안참여자 권한 추가 0
10 시엠립 하수처리 개선 26.02.09, 이태훈, 보안참여자 권한 추가 221
11 반테 민체이 관개 홍수저감 25.12.07, 나쉬, 파일 업로드 44
12 메콩유역 수자원 관리 기후적응 25.11.19, 이태훈, 보안참여자 권한 추가 0
13 잘랄아바드 상수도 계획 26.03.06, -, 폴더 자동 삭제 (파일 개수 미달) 58
14 CAREC 도로 감리 26.03.04, 이태훈, 보안참여자 권한 추가 0
15 펀잡 홍수 방재 25.12.08, 콰윰 아딜, 폴더 삭제 0
16 KP 아보타바드 상수도 26.02.26, 정기일, 파일 업로드 240
17 홍수 복원 InFRA2 25.12.18, -, 폴더 자동 삭제 (파일 개수 미달) 6
18 PGN 해상교량 BID2 26.03.04, 이태훈, 보안참여자 권한 추가 631
19 홍수 관리 Package5B 25.12.02, 조명훈, 폴더 이름 변경 14
20 족자~바웬 도로사업 26.03.06, 시스템관리-Savannah, 참관자 권한 추가 0
21 테치만 상수도 확장 26.02.09, 이태훈, 보안참여자 권한 추가 0
22 기니 벼 재배단지 26.01.07, -, 폴더 자동 삭제 (파일 개수 미달) 43
23 부수쿠마 분뇨 자원화 2단계 26.02.09, 이태훈, 보안참여자 권한 추가 9
24 우간다 벼 재배단지 25.12.08, 박수빈, 파일 업로드 52
25 Adeaa-Becho 지하수 관개 25.12.29, -, 폴더 자동 삭제 (파일 개수 미달) 140
26 도도타군 관개 25.12.30, -, 폴더 자동 삭제 (파일 개수 미달) 142
27 지하수 관개 환경설계 26.02.09, 이태훈, 보안참여자 권한 추가 0
28 Dodoma 하수 설계감리 26.02.09, 이태훈, 보안참여자 권한 추가 32
29 Iringa 상하수도 개선 26.02.09, 이태훈, 보안참여자 권한 추가 0
30 도도마 유수율 상수도개선 26.02.12, 서하연, 부관리자 권한 추가 35
31 잔지바르 쌀 생산 25.12.08, 박수빈, 파일 업로드 23
32 SALDEORO 수력발전 28MW 25.11.19, 이태훈, 보안참여자 권한 추가 0
33 LaPaz Danli 상수도 26.02.09, 이태훈, 보안참여자 권한 추가 60
34 에스꼬마 차라짜니 도로 26.03.06, -, 폴더 자동 삭제 (파일 개수 미달) 0
35 마모레 교량도로 26.03.04, 이태훈, 보안참여자 권한 추가 120
36 Bombeo-Colomi 도로설계 26.03.04, 이태훈, 보안참여자 권한 추가 48
37 AI 폐기물 25.11.19, 이태훈, 보안참여자 권한 추가 0
38 도로 통행료 현대화 26.02.25, 류창수, 폴더 삭제 0
39 Barranca 상하수도 확장 26.02.09, 이태훈, 보안참여자 권한 추가 44
40 태평양 철도 26.02.24, -, 폴더 자동 삭제 (파일 개수 미달) 101
41 필리핀 사무소 26.03.06, 한형남, 파일 다운로드 316
42 PGN 해상교량 BID2 26.03.04, 이태훈, 보안참여자 권한 추가 631

View File

@@ -1,42 +0,0 @@
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
1 projectName recentLog fileCount
2 비엔티안 메콩강 관리 2차 212487_25-12-07 12:22:26_나쉬_파일 업로드_/서류/06. 전문가파견/01. 파견/251031~260208_전문가 파견(이범주).zip 260
3 ITTC 관개 교육센터 225728_26-01-29 09:10:21_박진규_폴더 삭제_/서류/ggg 16
4 만달레이 철도 개량 감리 207041_25-11-19 16:54:36_이태훈_보안참여자 권한 추가_홍아름_김혜인 298
5 푸옥호아 양수 발전 233465_26-02-23 10:24:46_이철호_폴더 이름 변경_/6 준공/3 준공도서 26년 2월 작성예정 발주처 협의중_/6 준공/3 준공도서 26년 3월 작성예정 발주처 협의중 139
6 아시르 지잔 고속도로 234455_26-03-04 14:31:52_이태훈_보안참여자 권한 추가_복진훈 73
7 타슈켄트 철도 228919_26-02-05 10:08:11_조항언_파일 업로드_/02_성과품/B_중간보고/BB_중간보고서/[러문] INTERIM REPORT_0115_F.pdf 51
8 지방 도로 복원 234456_26-03-04 14:32:07_이태훈_보안참여자 권한 추가_복진훈 0
9 Habbaniyah Shuaiba AirBase 234457_26-03-04 14:32:28_이태훈_보안참여자 권한 추가_복진훈 0
10 시엠립 하수처리 개선 231205_26-02-09 11:03:05_이태훈_보안참여자 권한 추가_김창환_배형원 221
11 반테 민체이 관개 홍수저감 212512_25-12-07 12:35:02_나쉬_파일 업로드_/서류/04. 기성/18차 기성금/06. 통장사본.pdf 44
12 메콩유역 수자원 관리 기후적응 207047_25-11-19 17:01:19_이태훈_보안참여자 권한 추가_홍아름_김혜인 0
13 잘랄아바드 상수도 계획 234860_26-03-06 10:24:27_-_폴더 자동 삭제 (파일 개수 미달)_/4. MP 성과품/3. 최종보고서/초안 제출 58
14 펀잡 홍수 방재 212686_25-12-08 13:05:24_콰윰 아딜_폴더 삭제_/RFP 0
15 CAREC 도로 감리 234458_26-03-04 14:32:50_이태훈_보안참여자 권한 추가_복진훈 0
16 KP 아보타바드 상수도 234120_26-02-26 20:58:46_정기일_파일 업로드_/99 참고자료/99 체코 두코바니 원전/00 2025년 입찰/Engineering work instruction_241206.pdf 240
17 PGN 해상교량 BID2 234454_26-03-04 14:31:31_이태훈_보안참여자 권한 추가_복진훈 631
18 홍수 복원 InFRA2 214399_25-12-18 09:05:49_-_폴더 자동 삭제 (파일 개수 미달)_/서류/3.경비신청 및 정산/1. 신청 6
19 홍수 관리 Package5B 211353_25-12-02 10:04:18_조명훈_폴더 이름 변경_/서류/01. 계약서/01. 계약서_/서류/01. 계약서/01. 사업계약서 14
20 족자~바웬 도로사업 234908_26-03-06 13:36:39_시스템관리-Savannah_참관자 권한 추가_이호성 0
21 테치만 상수도 확장 231210_26-02-09 11:06:41_이태훈_보안참여자 권한 추가_배형원_정낙훈 0
22 기니 벼 재배단지 216888_26-01-07 11:07:23_-_폴더 자동 삭제 (파일 개수 미달)_/계약서류/경비 신청 및 정산/01. 신청 43
23 우간다 벼 재배단지 212622_25-12-08 11:17:57_박수빈_파일 업로드_/계약서류/경비 신청 및 정산/01. 현장경비 신청전표/11-20251029-J0401-006 (9,10월 현장운영비).pdf 52
24 부수쿠마 분뇨 자원화 2단계 231215_26-02-09 11:08:34_이태훈_보안참여자 권한 추가_김창환_배형원 9
25 지하수 관개 환경설계 231212_26-02-09 11:07:35_이태훈_보안참여자 권한 추가_김창환_배형원 0
26 Adeaa-Becho 지하수 관개 215553_25-12-29 09:36:23_-_폴더 자동 삭제 (파일 개수 미달)_/Topographic Survey/측량조사성과품/Appendix B - List of GPS Control Points 140
27 도도타군 관개 215706_25-12-30 09:17:43_-_폴더 자동 삭제 (파일 개수 미달)_/서류/03.경비신청 및 정산/02.사내정산 2차 142
28 Iringa 상하수도 개선 231216_26-02-09 11:10:18_이태훈_보안참여자 권한 추가_김창환_배형원 0
29 Dodoma 하수 설계감리 231217_26-02-09 11:11:06_이태훈_보안참여자 권한 추가_김창환_배형원 32
30 도도마 유수율 상수도개선 232629_26-02-12 17:26:13_서하연_부관리자 권한 추가_정기일 35
31 잔지바르 쌀 생산 212641_25-12-08 11:43:37_박수빈_파일 업로드_/서류/경비신청 및 정산/01. 25년/11-20250409-J0401-005 (운영경비 2분기 송금).pdf 23
32 SALDEORO 수력발전 28MW 207029_25-11-19 16:44:04_이태훈_보안참여자 권한 추가_홍아름_김혜인 0
33 LaPaz Danli 상수도 231219_26-02-09 11:12:37_이태훈_보안참여자 권한 추가_배형원_정낙훈 60
34 에스꼬마 차라짜니 도로 234837_26-03-06 08:00:21_-_폴더 자동 삭제 (파일 개수 미달)_/EOI/001/EOI서류 0
35 Bombeo-Colomi 도로설계 234463_26-03-04 14:34:24_이태훈_보안참여자 권한 추가_복진훈 48
36 마모레 교량도로 234462_26-03-04 14:33:56_이태훈_보안참여자 권한 추가_복진훈 120
37 AI 폐기물 207034_25-11-19 16:50:49_이태훈_보안참여자 권한 추가_홍아름_김혜인 0
38 도로 통행료 현대화 233938_26-02-25 14:39:08_류창수_폴더 삭제_/필리핀다바오프로젝트 0
39 Barranca 상하수도 확장 231220_26-02-09 11:13:28_이태훈_보안참여자 권한 추가_김창환_배형원 44
40 태평양 철도 233798_26-02-24 17:52:33_-_폴더 자동 삭제 (파일 개수 미달)_/01_수행문서/D_보고자료/D6_최종보고 101
41 필리핀 사무소 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
42 PGN 해상교량 BID2 234454_26-03-04 14:31:31_이태훈_보안참여자 권한 추가_복진훈 631

View File

@@ -1,42 +0,0 @@
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
1 projectName recentLog fileCount
2 ITTC 관개 교육센터 26.01.29, 박진규, 폴더 삭제 16
3 비엔티안 메콩강 관리 2차 25.12.07, 나쉬, 파일 업로드 260
4 만달레이 철도 개량 감리 25.11.19, 이태훈, 보안참여자 권한 추가 298
5 푸옥호아 양수 발전 26.02.23, 이철호, 폴더 이름 변경 139
6 아시르 지잔 고속도로 26.03.04, 이태훈, 보안참여자 권한 추가 75
7 지방 도로 복원 26.03.04, 이태훈, 보안참여자 권한 추가 0
8 타슈켄트 철도 26.02.05, 조항언, 파일 업로드 51
9 Habbaniyah Shuaiba AirBase 26.03.04, 이태훈, 보안참여자 권한 추가 0
10 시엠립 하수처리 개선 26.02.09, 이태훈, 보안참여자 권한 추가 222
11 반테 민체이 관개 홍수저감 25.12.07, 나쉬, 파일 업로드 0
12 메콩유역 수자원 관리 기후적응 25.11.19, 이태훈, 보안참여자 권한 추가 0
13 잘랄아바드 상수도 계획 26.03.06, -, 폴더 자동 삭제 (파일 개수 미달) 58
14 CAREC 도로 감리 26.03.04, 이태훈, 보안참여자 권한 추가 0
15 펀잡 홍수 방재 25.12.08, 콰윰 아딜, 폴더 삭제 0
16 KP 아보타바드 상수도 26.02.26, 정기일, 파일 업로드 240
17 홍수 복원 InFRA2 25.12.18, -, 폴더 자동 삭제 (파일 개수 미달) 6
18 PGN 해상교량 BID2 26.03.04, 이태훈, 보안참여자 권한 추가 0
19 홍수 관리 Package5B 25.12.02, 조명훈, 폴더 이름 변경 14
20 족자~바웬 도로사업 26.03.06, 시스템관리-Savannah, 참관자 권한 추가 0
21 테치만 상수도 확장 26.02.09, 이태훈, 보안참여자 권한 추가 0
22 기니 벼 재배단지 26.01.07, -, 폴더 자동 삭제 (파일 개수 미달) 44
23 부수쿠마 분뇨 자원화 2단계 26.02.09, 이태훈, 보안참여자 권한 추가 9
24 우간다 벼 재배단지 25.12.08, 박수빈, 파일 업로드 52
25 Adeaa-Becho 지하수 관개 25.12.29, -, 폴더 자동 삭제 (파일 개수 미달) 140
26 도도타군 관개 25.12.30, -, 폴더 자동 삭제 (파일 개수 미달) 0
27 지하수 관개 환경설계 26.02.09, 이태훈, 보안참여자 권한 추가 0
28 Dodoma 하수 설계감리 26.02.09, 이태훈, 보안참여자 권한 추가 32
29 Iringa 상하수도 개선 26.02.09, 이태훈, 보안참여자 권한 추가 0
30 도도마 유수율 상수도개선 26.02.12, 서하연, 부관리자 권한 추가 35
31 잔지바르 쌀 생산 25.12.08, 박수빈, 파일 업로드 23
32 SALDEORO 수력발전 28MW 25.11.19, 이태훈, 보안참여자 권한 추가 0
33 LaPaz Danli 상수도 26.02.09, 이태훈, 보안참여자 권한 추가 65
34 에스꼬마 차라짜니 도로 26.03.06, -, 폴더 자동 삭제 (파일 개수 미달) 0
35 마모레 교량도로 26.03.04, 이태훈, 보안참여자 권한 추가 120
36 Bombeo-Colomi 도로설계 26.03.04, 이태훈, 보안참여자 권한 추가 49
37 AI 폐기물 25.11.19, 이태훈, 보안참여자 권한 추가 0
38 도로 통행료 현대화 26.02.25, 류창수, 폴더 삭제 0
39 Barranca 상하수도 확장 26.02.09, 이태훈, 보안참여자 권한 추가 50
40 태평양 철도 26.02.24, -, 폴더 자동 삭제 (파일 개수 미달) 101
41 필리핀 사무소 26.03.06, 한형남, 파일 다운로드 0
42 PGN 해상교량 BID2 26.03.04, 이태훈, 보안참여자 권한 추가 637

View File

@@ -1,42 +0,0 @@
[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,,,
1 [PM Overseas 프로젝트 현황] 2026.03.04 <<활동로그가 없는 프로젝트 (8)
2
3 No. 프로젝트 명 담당부서 담당자 종료(예정)일 최근 활동로그 과업개요 작성 유무 파일 수 비고
4 1 라오스 ITTC 관개 교육센터 PMC 수자원1부 방노성 2025.12.20 2026.01.29, 폴더 삭제 O 16 2026.01.29 로그는 테스트 활동 추정 종료(예정)일 지남 진행
5 2 라오스 비엔티안 메콩강 관리 2차 DD 수자원1부 방노성 2026.05.31 2025.12.07, 파일업로드 X 260 탭 1개에 모든파일 업로드
6 3 미얀마 만달레이 철도 개량 감리 CS 철도사업부 김태헌 2027.11.17 2025.11.17, 폴더이름변경 O 298
7 4 베트남 푸옥호아 양수 발전 FS 수력부 이철호 2025.11.30 2026.02.23, 폴더이름변경 O 139 준공도서 3월 작성예정 종료(예정)일 지남 준공
8 5 사우디아라비아 아시르 지잔 고속도로 FS 도로부 공태원 2025.11.21 2026.02.09, 파일다운로드 O 73 종료(예정)일 지남 준공
9 6 우즈베키스탄 지방 도로 복원 MP 도로부 장진영 2029.04.28 X X 0
10 7 우즈베키스탄 타슈켄트 철도 FS 철도사업부 김태헌 2026.03.20 2026.02.05, 파일업로드 O 51
11 8 이라크 Habbaniyah Shuaiba AirBase PD 도로부 강동구 2026.12.31 X X 0
12 9 메콩유역 수자원 관리 기후적응 MP 수자원1부 정귀한 2025.12.31 X X 0 종료(예정)일 지남 준공
13 10 캄보디아 반테 민체이 관개 홍수저감 MP 수자원1부 이대주 2026.08.28 2025.12.07, 파일업로드 X 44
14 11 캄보디아 시엠립 하수처리 개선 DD 물환경사업1부 변역근 2028.12.18 2026.02.06, AI 요약 O 221
15 12 키르기스스탄 잘랄아바드 상수도 계획 MP 물환경사업1부 변기상 2025.12.31 2026.02.12, 파일업로드 X 60 종료(예정)일 지남 준공
16 13 파키스탄 펀잡 홍수 방재 PMC 수자원1부 방노성 2027.12.31 2025.12.08, 폴더삭제 O 0
17 14 파키스탄 KP 아보타바드 상수도 PMC 물환경사업2부 변기상 2026.12.31 2026.02.26, 파일업로드 O 240
18 15 파키스탄 CAREC 도로 감리 DD 도로부 황효섭 2026.10.26 X X 0
19 16 필리핀 홍수 복원 InFRA2 DD 수자원1부 이대주 2026.08.07 2025.12.01, 폴더삭제 O 6 최근로그 >> 폴더자동삭제(파일 개수 미달)
20 17 필리핀 홍수 관리 Package5B MP 수자원1부 이희철 2026.05.31 2025.12.02, 폴더이름변경 O 14
21 18 필리핀 PGN 해상교량 BID2 IDC 구조부 이상희 2026.05.31 2026.02.11, 파일다운로드 O 631
22 19 가나 테치만 상수도 확장 DS 물환경사업2부 - 2029.04.25 X X 0 책임자 및 담당자 설정X
23 20 기니 벼 재배단지 PMC 수자원1부 이대주 2028.12.20 2025.12.08, 파일업로드 O 43 최근로그 >> 폴더자동삭제(파일 개수 미달)
24 21 우간다 벼 재배단지 PMC 수자원1부 방노성 2028.12.20 2025.12.08, 파일업로드 O 52
25 22 우간다 부수쿠마 분뇨 자원화 2단계 PMC 물환경사업2부 변기상 2026.12.31 2026.02.05, 파일업로드 X 9
26 23 에티오피아 지하수 관개 환경설계 DD 물환경사업2부 변기상 2026.06.23 X X 0
27 24 에티오피아 도도타군 관개 PMC 수자원1부 방노성 2026.12.31 2025.12.01, 폴더이름변경 O 144 탭 1개에 모든파일 업로드 // 최근로그 >> 폴더자동삭제(파일 개수 미달)
28 25 에티오피아 Adeaa-Becho 지하수 관개 MP 수자원1부 방노성 2026.07.31 2025.11.21, 파일업로드 O 146 최근로그 >> 폴더자동삭제(파일 개수 미달)
29 26 탄자니아 Iringa 상하수도 개선 CS 물환경사업1부 백운영 2029.06.08 2026.02.03, 폴더생성 X 0
30 27 탄자니아 Dodoma 하수 설계감리 DD 물환경사업2부 변기상 2027.07.08 2026.02.04, 폴더삭제 X 32
31 28 탄자니아 잔지바르 쌀 생산 PMC 수자원1부 방노성 2027.12.20 2025.12.08, 파일 업로드 O 23
32 29 탄자니아 도도마 유수율 상수도개선 PMC 물환경사업1부 박순석 2026.12.31 2026.02.12, 부관리자권한추가 X 35
33 30 아르헨티나 SALDEORO 수력발전 28MW DD 플랜트1부 양정모 2026.01.31 X X 0 종료(예정)일 지남 준공
34 31 온두라스 LaPaz Danli 상수도 CS 물환경사업2부 - 2027.02.23 2026.01.29, 파일 삭제 O 60 책임자 및 담당자 설정 X, 실 관리부서는 해외사업부, 더미파일 다수
35 32 볼리비아 에스꼬마 차라짜니 도로 CS 도로부 전홍찬 2029.12.15 2026.02.06, 파일업로드 X 1
36 33 볼리비아 마모레 교량도로 FS 도로부 황효섭 2025.10.17 2026.02.06, 파일업로드 X 120 종료(예정)일 지남 준공
37 34 볼리비아 Bombeo-Colomi 도로설계 DD 도로부 황효섭 2026.07.24 2025.12.05, 파일삭제 O 48 더미파일(폴더유지용) 12개, 실 관리부서는 해외사업부
38 35 콜롬비아 AI 폐기물 FS 플랜트1부 서재희 2026.02.27 X X 0 종료(예정)일 지남
39 36 파라과이 도로 통행료 현대화 MP 교통계획부 오제훈 2025.10.24 2025.02.25, 폴더삭제 X 0 종료(예정)일 지남 준공
40 37 페루 Barranca 상하수도 확장 DD 물환경사업2부 변기상 2026.03.08 2025.11.14, 파일업로드 O 44 더미파일(폴더유지용) 27개, 실 관리부서는 해외사업부
41 38 엘살바도르 태평양 철도 FS 철도사업부 김태헌 2025.12.31 2026.02.24, 폴더자동삭제 X 101 종료(예정)일 지남 준공
42 39 필리핀 사무소 해외사업부 한형남 2026-03-09, 파일다운로드 과업개요 페이지 없음 323

View File

@@ -1,42 +0,0 @@
[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,,,
1 [PM Overseas 프로젝트 현황] 2026.03.04 <<활동로그가 없는 프로젝트 (8)
2
3 No. 프로젝트 명 담당부서 담당자 종료(예정)일 최근 활동로그 과업개요 작성 유무 파일 수 비고
4 1 라오스 ITTC 관개 교육센터 PMC 수자원1부 방노성 2025.12.20 2026.01.29, 폴더 삭제 O 16 2026.01.29 로그는 테스트 활동 추정 종료(예정)일 지남 진행
5 2 라오스 비엔티안 메콩강 관리 2차 DD 수자원1부 방노성 2026.05.31 2025.12.07, 파일업로드 X 260 탭 1개에 모든파일 업로드
6 3 미얀마 만달레이 철도 개량 감리 CS 철도사업부 김태헌 2027.11.17 2025.11.17, 폴더이름변경 O 298
7 4 베트남 푸옥호아 양수 발전 FS 수력부 이철호 2025.11.30 2026.02.23, 폴더이름변경 O 139 준공도서 3월 작성예정 종료(예정)일 지남 준공
8 5 사우디아라비아 아시르 지잔 고속도로 FS 도로부 공태원 2025.11.21 2026.02.09, 파일다운로드 O 73 종료(예정)일 지남 준공
9 6 우즈베키스탄 지방 도로 복원 MP 도로부 장진영 2029.04.28 X X 0
10 7 우즈베키스탄 타슈켄트 철도 FS 철도사업부 김태헌 2026.03.20 2026.02.05, 파일업로드 O 51
11 8 이라크 Habbaniyah Shuaiba AirBase PD 도로부 강동구 2026.12.31 X X 0
12 9 메콩유역 수자원 관리 기후적응 MP 수자원1부 정귀한 2025.12.31 X X 0 종료(예정)일 지남 준공
13 10 캄보디아 반테 민체이 관개 홍수저감 MP 수자원1부 이대주 2026.08.28 2025.12.07, 파일업로드 X 44
14 11 캄보디아 시엠립 하수처리 개선 DD 물환경사업1부 변역근 2028.12.18 2026.02.06, AI 요약 O 221
15 12 키르기스스탄 잘랄아바드 상수도 계획 MP 물환경사업1부 변기상 2025.12.31 2026.02.12, 파일업로드 X 60 종료(예정)일 지남 준공
16 13 파키스탄 펀잡 홍수 방재 PMC 수자원1부 방노성 2027.12.31 2025.12.08, 폴더삭제 O 0
17 14 파키스탄 KP 아보타바드 상수도 PMC 물환경사업2부 변기상 2026.12.31 2026.02.26, 파일업로드 O 240
18 15 파키스탄 CAREC 도로 감리 DD 도로부 황효섭 2026.10.26 X X 0
19 16 필리핀 홍수 복원 InFRA2 DD 수자원1부 이대주 2026.08.07 2025.12.01, 폴더삭제 O 6 최근로그 >> 폴더자동삭제(파일 개수 미달)
20 17 필리핀 홍수 관리 Package5B MP 수자원1부 이희철 2026.05.31 2025.12.02, 폴더이름변경 O 14
21 18 필리핀 PGN 해상교량 BID2 IDC 구조부 이상희 2026.05.31 2026.02.11, 파일다운로드 O 631
22 19 가나 테치만 상수도 확장 DS 물환경사업2부 - 2029.04.25 X X 0 책임자 및 담당자 설정X
23 20 기니 벼 재배단지 PMC 수자원1부 이대주 2028.12.20 2025.12.08, 파일업로드 O 43 최근로그 >> 폴더자동삭제(파일 개수 미달)
24 21 우간다 벼 재배단지 PMC 수자원1부 방노성 2028.12.20 2025.12.08, 파일업로드 O 52
25 22 우간다 부수쿠마 분뇨 자원화 2단계 PMC 물환경사업2부 변기상 2026.12.31 2026.02.05, 파일업로드 X 9
26 23 에티오피아 지하수 관개 환경설계 DD 물환경사업2부 변기상 2026.06.23 X X 0
27 24 에티오피아 도도타군 관개 PMC 수자원1부 방노성 2026.12.31 2025.12.01, 폴더이름변경 O 144 탭 1개에 모든파일 업로드 // 최근로그 >> 폴더자동삭제(파일 개수 미달)
28 25 에티오피아 Adeaa-Becho 지하수 관개 MP 수자원1부 방노성 2026.07.31 2025.11.21, 파일업로드 O 146 최근로그 >> 폴더자동삭제(파일 개수 미달)
29 26 탄자니아 Iringa 상하수도 개선 CS 물환경사업1부 백운영 2029.06.08 2026.02.03, 폴더생성 X 0
30 27 탄자니아 Dodoma 하수 설계감리 DD 물환경사업2부 변기상 2027.07.08 2026.02.04, 폴더삭제 X 32
31 28 탄자니아 잔지바르 쌀 생산 PMC 수자원1부 방노성 2027.12.20 2025.12.08, 파일 업로드 O 23
32 29 탄자니아 도도마 유수율 상수도개선 PMC 물환경사업1부 박순석 2026.12.31 2026.02.12, 부관리자권한추가 X 35
33 30 아르헨티나 SALDEORO 수력발전 28MW DD 플랜트1부 양정모 2026.01.31 X X 0 종료(예정)일 지남 준공
34 31 온두라스 LaPaz Danli 상수도 CS 물환경사업2부 - 2027.02.23 2026.01.29, 파일 삭제 O 60 책임자 및 담당자 설정 X, 실 관리부서는 해외사업부, 더미파일 다수
35 32 볼리비아 에스꼬마 차라짜니 도로 CS 도로부 전홍찬 2029.12.15 2026.02.06, 파일업로드 X 1
36 33 볼리비아 마모레 교량도로 FS 도로부 황효섭 2025.10.17 2026.02.06, 파일업로드 X 120 종료(예정)일 지남 준공
37 34 볼리비아 Bombeo-Colomi 도로설계 DD 도로부 황효섭 2026.07.24 2025.12.05, 파일삭제 O 48 더미파일(폴더유지용) 12개, 실 관리부서는 해외사업부
38 35 콜롬비아 AI 폐기물 FS 플랜트1부 서재희 2026.02.27 X X 0 종료(예정)일 지남
39 36 파라과이 도로 통행료 현대화 MP 교통계획부 오제훈 2025.10.24 2025.02.25, 폴더삭제 X 0 종료(예정)일 지남 준공
40 37 페루 Barranca 상하수도 확장 DD 물환경사업2부 변기상 2026.03.08 2025.11.14, 파일업로드 O 44 더미파일(폴더유지용) 27개, 실 관리부서는 해외사업부
41 38 엘살바도르 태평양 철도 FS 철도사업부 김태헌 2025.12.31 2026.02.24, 폴더자동삭제 X 101 종료(예정)일 지남 준공
42 39 필리핀 사무소 해외사업부 한형남 2026.03.10, PDF 변환 과업개요 페이지 없음 829

View File

@@ -1,226 +0,0 @@
<div class="wrap">
<article class="log-filter">
<div class="head">
<span class="title _h3">로그필터</span>
<button class="_button-xsmall reset">초기화</button>
</div>
<div class="body">
<div class="log-date">
<span class="subtitle">활동시간</span>
<div class="log-date-wrap">
<span class="category">시작</span>
<input type="date" value="">
</div>
<div class="log-date-wrap">
<span class="category">종료</span>
<input type="date" value="">
</div>
</div>
<div class="log-user">
<span class="subtitle">사용자</span>
<div class="custom-select-wrap">
<div class="custom-select-display">모든 사용자</div>
<ul class="custom-select-list" style="display: none;"><li data-value="allUser">모든 사용자</li><li data-value="213057">213057 (박진규)</li><li data-value="225044">225044 (박종호)</li><li data-value="B21364">B21364 (이태훈)</li><li data-value="B22027">B22027 (김혜인)</li><li data-value="dev5">dev5 (시스템관리E)</li><li data-value="dev6">dev6 (시스템관리F)</li><li data-value="dev7">dev7 (시스템관리G)</li><li data-value="M07318">M07318 (김원기)</li></ul>
<select id="log-user-select" name="log-user-select" hidden=""><option value="allUser">모든 사용자</option><option value="213057">213057 (박진규)</option><option value="225044">225044 (박종호)</option><option value="B21364">B21364 (이태훈)</option><option value="B22027">B22027 (김혜인)</option><option value="dev5">dev5 (시스템관리E)</option><option value="dev6">dev6 (시스템관리F)</option><option value="dev7">dev7 (시스템관리G)</option><option value="M07318">M07318 (김원기)</option></select>
</div>
</div>
<div class="log-activity">
<div class="head-group">
<span class="subtitle">활동유형</span>
<div class="button-wrap">
<button class="_button-xsmall select-all">전체선택</button>
<button class="_button-xsmall clear-all">전체해제</button>
</div>
</div>
<span class="category">파일 / 폴더관련</span>
<label>
<input type="checkbox" value="uploadData_file" checked="">
<span class="--checkbox"></span>
<span>파일 업로드</span>
</label>
<label>
<input type="checkbox" value="renameTarget" checked="">
<span class="--checkbox"></span>
<span>이름 변경</span>
</label>
<label>
<input type="checkbox" value="removeTarget" checked="">
<span class="--checkbox"></span>
<span>삭제</span>
</label>
<label>
<input type="checkbox" value="downloadTarget" checked="">
<span class="--checkbox"></span>
<span>다운로드</span>
</label>
<label>
<input type="checkbox" value="relocateTarget" checked="">
<span class="--checkbox"></span>
<span>파일 이동</span>
</label>
<label>
<input type="checkbox" value="createFolder" checked="">
<span class="--checkbox"></span>
<span>새 폴더 생성</span>
</label>
<label>
<input type="checkbox" value="setDataPermission_folder" checked="">
<span class="--checkbox"></span>
<span>폴더 권한 설정</span>
</label>
<label>
<input type="checkbox" value="convertPdf" checked="">
<span class="--checkbox"></span>
<span>PDF 변환</span>
</label>
<span class="category">유저관련</span>
<label>
<input type="checkbox" value="editAuthor" checked="">
<span class="--checkbox"></span>
<span>작성자 변경</span>
</label>
<label>
<input type="checkbox" value="deletePermission" checked="">
<span class="--checkbox"></span>
<span>권한 삭제</span>
</label>
<label>
<input type="checkbox" value="addPermission" checked="">
<span class="--checkbox"></span>
<span>권한 추가</span>
</label>
<span class="category">기타</span>
<label>
<input type="checkbox" value="summarizeAI" checked="">
<span class="--checkbox"></span>
<span>AI 요약</span>
</label>
</div>
</div>
<div class="foot">
<button class="_button-medium">적용</button>
</div>
</article>
<div class="modal-wrap">
<div class="modal-header narrow-area">
<div class="title">
<div class="left-wrap">
<div class="title-wrap">
<div class="text">활동로그</div>
<div class="users-count" style="display: none;">1 명</div>
</div>
<div class="btn set-user-permission-btn permission-min-sub-master" style="display: none;">
<div class="text">유저 권한 설정</div>
</div>
</div>
</div>
<div class="close"></div>
</div>
<div class="modal-body">
<div class="connected-users-wrap" style="display: none;">
<div class="user-item-wrap scrollbar"><div class="user-item me" data-user-id="B21364"><img class="profile-image" src="/main/img/archive/empty-profile.svg" style="outline: rgb(24, 114, 89) solid 2px;"><div class="wrap"><div class="top-wrap"><div class="name">이태훈 선임연구원</div><div class="user-permission-sub-master"><h6>부관리자</h6></div><div class="me-badge"><h6></h6></div></div><div class="bottom-wrap"><div class="cur-path">현재 위치: /과업개요</div></div></div></div></div>
<div class="project-setting-wrap">
<div class="project-name-wrap">
<div>프로젝트명</div>
<div class="project-type-wrap" id="project-type-wrap" style="display: none;">
<button class="project-type" id="project-type-btn">
<h5 class="project-type__label --type__support">지원</h5>
<i class="project-type__icon"></i>
</button>
<h5 class="--type-capsule" id="project-type-capsule">시공</h5>
<ul class="project-type__list">
<li class="project-type__list_item --type__construction">시공</li>
<li class="project-type__list_item --type__design">설계</li>
<li class="project-type__list_item --type__surgest">제안</li>
<li class="project-type__list_item --type__research">연구</li>
<li class="project-type__list_item --type__support">지원</li>
<li class="project-type__list_item --type__center">센터</li>
<li class="project-type__list_item --type__survey">측량</li>
</ul>
</div>
<div class="project-type-wrap" id="project-type-wrap-overseas" style="display: flex;">
<button class="project-type" id="project-type-btn-overseas" style="min-width: 107.523px; text-align: center; justify-content: center; display: none; align-items: center;">
<h5 class="project-type__label --type__MP">MP (기본계획)</h5>
<i class="project-type__icon"></i>
</button>
<h5 class="--type-capsule" id="project-type-capsule-overseas" style="min-width: 107.523px; text-align: center; justify-content: center; display: flex; align-items: center;">PMC (실시설계)</h5>
<ul class="project-type__list" style="min-width: 107.523px;">
<li class="project-type__list_item --type__MP" style="padding-left: 5px; padding-right: 5px;">MP (기본계획)</li>
<li class="project-type__list_item --type__DD" style="padding-left: 5px; padding-right: 5px;">DD (실시설계)</li>
<li class="project-type__list_item --type__FS" style="padding-left: 5px; padding-right: 5px;">FS (타당성조사)</li>
<li class="project-type__list_item --type__PD" style="padding-left: 5px; padding-right: 5px;">PD (기본설계)</li>
<li class="project-type__list_item --type__DS" style="padding-left: 5px; padding-right: 5px;">DS (설계감리)</li>
<li class="project-type__list_item --type__CS" style="padding-left: 5px; padding-right: 5px;">CS (시공감리)</li>
<li class="project-type__list_item --type__PMC" style="padding-left: 5px; padding-right: 5px;">PMC (실시설계)</li>
<li class="project-type__list_item --type__IDC" style="padding-left: 5px; padding-right: 5px;">IDC (타당성조사)</li>
<li class="project-type__list_item --type__DR" style="padding-left: 5px; padding-right: 5px;">DR (설계검토)</li>
<li class="project-type__list_item --type__ETC" style="padding-left: 5px; padding-right: 5px;">ETC (기타)</li>
</ul>
</div>
<div class="project-input-wrap" style="display: flex; gap:1rem;">
<div class="project-setting-name" id="project-name-view"> ITTC 관개 교육센터</div>
<input type="text" class="project-setting-name" id="project-name-input" style="display: none; border: 1px solid black;">
</div>
<div class="project-step-wrap">
<button class="project-step" id="project-step-btn" style="display: none;">
<h5 class="project-step__label --step__active">진행</h5>
<i class="project-step__icon"></i>
</button>
<h5 class="project-step-capsule --step-capsule__active" id="project-step-capsule" style="display: flex;">진행</h5>
<ul class="project-step__list">
<li class="project-step__list_item --step__active">진행</li>
<li class="project-step__list_item --step__stop">중지</li>
<li class="project-step__list_item --step__done">완료</li>
<li class="project-step__list_item --step__wait">대기</li>
</ul>
</div>
<div class="peoject-save-wrap">
</div>
</div>
<div class="project-manager-wrap">
<div class="project-manager-title">프로젝트 관리자</div>
<div class="project-manager-name">방노성 전무이사</div>
</div>
<div class="project-location-wrap">
<div class="project-location-title">프로젝트 위치</div>
<div class="project-location-lat">위도 18.068579</div>
<div class="project-location-lon">경도 102.65966</div>
</div>
</div>
<div class="btn-wrap">
<div class="logout-btn">
<div class="image"></div>
<div class="text">로그아웃</div>
</div>
</div>
</div>
<div class="manual-wrap" style="display: none;"></div>
<div class="size-wrap" style="display: none;">
<div class="chart" style="user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); position: relative;"><div style="position: relative; width: 1152px; height: 720px; padding: 0px; margin: 0px; border-width: 0px;"><canvas data-zr-dom-id="zr_0" width="1152" height="720" style="position: absolute; left: 0px; top: 0px; width: 1152px; height: 720px; user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); padding: 0px; margin: 0px; border-width: 0px;"></canvas></div><div class=""></div></div>
<div class="text">저장공간 관련 문의: GSIM 개발팀 이호성 수석연구원</div>
</div>
<div class="log-wrap" style="opacity: 1; display: flex;">
<div class="log-item-wrap log-header">
<div class="log-item">
<div class="date"><div class="text">활동시간</div></div>
<div class="user"><div class="text">사용자</div></div>
<div class="activity"><div class="text">활동유형</div></div>
<div class="log"><div class="text">활동내용</div></div>
</div>
</div>
<div class="log-item-wrap log-body scrollbar scroll-container"></div>
</div>
<div class="text-wrap" style="display: none;">undefined</div>
<div class="project-list-wrap" style="display: none;"></div>
<div class="input-wrap" style="display: none;"></div>
<div class="user-list-wrap" style="display: none;">
<div class="user-item-wrap scrollbar"></div>
</div>
<div class="btn-wrap" style="display: none;"></div>
</div>
</div>
</div>

View File

@@ -1,66 +0,0 @@
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&params[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())

View File

@@ -1,9 +1,25 @@
// 공통 네비게이션 및 유틸리티 로직
/**
* Project Master Overseas Common JS
* 공통 네비게이션, 유틸리티, 전역 이벤트 관리
*/
function navigateTo(path) {
location.href = path;
}
// 상단바 클릭 시 홈으로 이동 등 공통 이벤트 설정
document.addEventListener('DOMContentLoaded', () => {
// 필요한 경우 공통 초기화 로직 추가
// --- 전역 이벤트: 모든 모달창 ESC 키로 닫기 ---
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
// 대시보드 모달
if (typeof closeAuthModal === 'function') closeAuthModal();
if (typeof closeActivityModal === 'function') closeActivityModal();
// 메일 시스템 모달
if (typeof closeModal === 'function') closeModal();
if (typeof closeAddressBook === 'function') closeAddressBook();
}
});
document.addEventListener('DOMContentLoaded', () => {
// 공통 초기화 로직
});

View File

@@ -1,295 +1,220 @@
/**
* Project Master Overseas Dashboard JS
* 기능: 데이터 로드, 활성도 분석, 인증 모달 제어, 크롤링 동기화 및 중단
*/
// --- 글로벌 상태 관리 ---
let rawData = [];
let projectActivityDetails = [];
let isCrawling = false;
const continentMap = {
"라오스": "아시아", "미얀마": "아시아", "베트남": "아시아", "사우디아라비아": "아시아",
"우즈베키스탄": "아시아", "이라크": "아시아", "캄보디아": "아시아",
"키르기스스탄": "아시아", "파키스탄": "아시아", "필리핀": "아시아",
"아르헨티나": "아메리카", "온두라스": "아메리카", "볼리비아": "아메리카", "콜롬비아": "아메리카",
"파라과이": "아메리카", "페루": "아메리카", "엘살바도르": "아메리카",
"가나": "아프리카", "기니": "아프리카", "우간다": "아프리카", "에티오피아": "아프리카", "탄자니아": "아프리카"
};
const continentOrder = {
"아시아": 1,
"아프리카": 2,
"아메리카": 3,
"지사": 4
};
const CONTINENT_ORDER = { "아시아": 1, "아프리카": 2, "아메리카": 3, "지사": 4 };
// --- 초기화 ---
async function init() {
console.log("Dashboard Initializing...");
const container = document.getElementById('projectAccordion');
const baseDateStrong = document.getElementById('baseDate');
if (!container) return;
if (!container) return;
// 1. 가용한 날짜 목록 가져오기 및 셀렉트 박스 생성
await loadAvailableDates();
await loadDataByDate();
}
// --- 데이터 통신 및 로드 ---
async function loadAvailableDates() {
try {
const datesRes = await fetch('/available-dates');
const dates = await datesRes.json();
if (dates && dates.length > 0) {
let selectHtml = `<select id="dateSelector" onchange="loadDataByDate(this.value)" style="margin-left:10px; border:none; background:none; font-weight:700; cursor:pointer;">`;
dates.forEach(d => {
selectHtml += `<option value="${d}">${d}</option>`;
});
selectHtml += `</select>`;
// 기준날짜 텍스트 영역을 셀렉트 박스로 교체
const baseDateInfo = document.querySelector('.base-date-info');
if (baseDateInfo) {
baseDateInfo.innerHTML = `기준날짜: ${selectHtml}`;
}
const response = await fetch('/available-dates');
const dates = await response.json();
if (dates?.length > 0) {
const selectHtml = `
<select id="dateSelector" onchange="loadDataByDate(this.value)"
style="margin-left:10px; border:none; background:none; font-weight:700; cursor:pointer; font-family:inherit; color:inherit;">
${dates.map(d => `<option value="${d}">${d}</option>`).join('')}
</select>`;
const baseDateStrong = document.getElementById('baseDate');
if (baseDateStrong) baseDateStrong.innerHTML = selectHtml;
}
} catch (e) {
console.error("날짜 목록 로드 실패:", e);
}
// 2. 기본 데이터 로드 (최신 날짜)
loadDataByDate();
} catch (e) { console.error("날짜 로드 실패:", e); }
}
async function loadDataByDate(selectedDate = "") {
const container = document.getElementById('projectAccordion');
try {
const url = selectedDate ? `/project-data?date=${selectedDate}` : `/project-data?t=${new Date().getTime()}`;
await loadActivityAnalysis(selectedDate);
const url = selectedDate ? `/project-data?date=${selectedDate}` : `/project-data?t=${Date.now()}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) throw new Error(data.error);
rawData = data.projects || [];
renderDashboard(rawData);
} catch (e) {
console.error("데이터 로드 실패:", e);
alert("데이터를 가져오는 데 실패했습니다.");
}
}
async function loadActivityAnalysis(date = "") {
const dashboard = document.getElementById('activityDashboard');
if (!dashboard) return;
try {
const url = date ? `/project-activity?date=${date}` : `/project-activity`;
const response = await fetch(url);
const data = await response.json();
if (data.error) return;
const { summary, details } = data;
projectActivityDetails = details;
dashboard.innerHTML = `
<div class="activity-card active" onclick="showActivityDetails('active')">
<div class="label">정상 (7일 이내)</div><div class="count">${summary.active}</div>
</div>
<div class="activity-card warning" onclick="showActivityDetails('warning')">
<div class="label">주의 (14일 이내)</div><div class="count">${summary.warning}</div>
</div>
<div class="activity-card stale" onclick="showActivityDetails('stale')">
<div class="label">방치 (14일 초과)</div><div class="count">${summary.stale}</div>
</div>
<div class="activity-card unknown" onclick="showActivityDetails('unknown')">
<div class="label">데이터 없음 (파일 0개 등)</div><div class="count">${summary.unknown}</div>
</div>`;
} catch (e) { console.error("분석 로드 실패:", e); }
}
// --- 렌더링 엔진 ---
function renderDashboard(data) {
const container = document.getElementById('projectAccordion');
container.innerHTML = ''; // 초기화
const groupedData = {};
data.forEach((item, index) => {
let continent = item[5] || "기기타";
let country = item[6] || "미분류";
if (!groupedData[continent]) groupedData[continent] = {};
if (!groupedData[continent][country]) groupedData[continent][country] = [];
groupedData[continent][country].push({ item, index });
});
const sortedContinents = Object.keys(groupedData).sort((a, b) => (continentOrder[a] || 99) - (continentOrder[b] || 99));
sortedContinents.forEach(continent => {
const continentGroup = document.createElement('div');
continentGroup.className = 'continent-group';
let continentHtml = `
<div class="continent-header" onclick="toggleGroup(this)">
<span>${continent}</span>
<span class="toggle-icon">▼</span>
</div>
<div class="continent-body">
`;
const sortedCountries = Object.keys(groupedData[continent]).sort((a, b) => a.localeCompare(b));
sortedCountries.forEach(country => {
continentHtml += `
<div class="country-group">
<div class="country-header" onclick="toggleGroup(this)">
<span>${country}</span>
<span class="toggle-icon">▼</span>
</div>
<div class="country-body">
<div class="accordion-container">
<div class="accordion-list-header">
<div>프로젝트명</div>
<div>담당부서</div>
<div>담당자</div>
<div style="text-align:center;">파일수</div>
<div>최근로그</div>
</div>
`;
const sortedProjects = groupedData[continent][country].sort((a, b) => a.item[0].localeCompare(b.item[0]));
sortedProjects.forEach(({ item, index }) => {
const projectName = item[0];
const dept = item[1];
const admin = item[2];
const recentLogRaw = item[3];
const fileCount = item[4];
const recentLog = recentLogRaw === "X" ? "기록 없음" : recentLogRaw;
const logTime = recentLog !== "기록 없음" ? recentLog.split(',')[0] : "기록 없음";
let statusClass = "";
if (fileCount === 0) statusClass = "status-error";
else if (recentLog === "기록 없음") statusClass = "status-warning";
continentHtml += `
<div class="accordion-item ${statusClass}">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div class="repo-title" title="${projectName}">${projectName}</div>
<div class="repo-dept">${dept}</div>
<div class="repo-admin">${admin}</div>
<div class="repo-files ${fileCount === 0 ? 'warning-text' : ''}">${fileCount}</div>
<div class="repo-log ${recentLog === '기록 없음' ? 'warning-text' : ''}" title="${recentLog}">${recentLog}</div>
</div>
<div class="accordion-body">
<div class="detail-grid">
<div class="detail-section">
<h4>참여 인원 상세</h4>
<table class="data-table">
<thead><tr><th>이름</th><th>소속</th><th>사용자권한</th></tr></thead>
<tbody>
<tr><td>${admin}</td><td>${dept}</td><td>관리자</td></tr>
<tr><td>김철수</td><td>${dept}</td><td>부관리자</td></tr>
<tr><td>박지민</td><td>${dept}</td><td>일반참여자</td></tr>
<tr><td>최유리</td><td>${dept}</td><td>참관자</td></tr>
</tbody>
</table>
</div>
<div class="detail-section">
<h4>최근 문의사항 및 파일 변경 로그</h4>
<table class="data-table">
<thead><tr><th>유형</th><th>내용</th><th>일시</th></tr></thead>
<tbody>
<tr><td><span class="badge">로그</span></td><td>데이터 동기화 완료</td><td>${logTime}</td></tr>
<tr><td><span class="badge" style="background:var(--hover-bg); border: 1px solid var(--border-color); color:var(--primary-color);">문의</span></td><td>프로젝트 접근 권한 요청</td><td>2026-02-23</td></tr>
<tr><td><span class="badge" style="background:var(--primary-color); color:white;">파일</span></td><td>설계도면 v2.pdf 업로드</td><td>2026-02-22</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
});
continentHtml += `
</div>
</div>
</div>
`;
container.innerHTML = '';
const grouped = groupData(data);
Object.keys(grouped).sort((a,b) => (CONTINENT_ORDER[a]||99) - (CONTINENT_ORDER[b]||99)).forEach(continent => {
const continentDiv = document.createElement('div');
continentDiv.className = 'continent-group active';
let html = `<div class="continent-header" onclick="toggleGroup(this)"><span>${continent}</span><span class="toggle-icon">▼</span></div><div class="continent-body">`;
Object.keys(grouped[continent]).sort().forEach(country => {
html += `<div class="country-group active"><div class="country-header" onclick="toggleGroup(this)"><span>${country}</span><span class="toggle-icon">▼</span></div><div class="country-body"><div class="accordion-container">
<div class="accordion-list-header"><div>프로젝트명</div><div>담당부서</div><div>담당자</div><div style="text-align:center;">파일수</div><div>최근로그</div></div>
${grouped[continent][country].sort((a,b)=>a[0].localeCompare(b[0])).map(p => createProjectHtml(p)).join('')}</div></div></div>`;
});
html += `</div>`;
continentDiv.innerHTML = html;
container.appendChild(continentDiv);
});
}
continentHtml += `
function groupData(data) {
const res = {};
data.forEach(item => {
const c1 = item[5] || "기타", c2 = item[6] || "미분류";
if (!res[c1]) res[c1] = {};
if (!res[c1][c2]) res[c1][c2] = [];
res[c1][c2].push(item);
});
return res;
}
function createProjectHtml(p) {
const [name, dept, admin, logRaw, files] = p;
const recentLog = (!logRaw || logRaw === "X" || logRaw === "데이터 없음") ? "기록 없음" : logRaw;
const logTime = recentLog !== "기록 없음" ? recentLog.split(',')[0] : "기록 없음";
const statusClass = (files === 0 || files === null) ? "status-error" : (recentLog === "기록 없음") ? "status-warning" : "";
return `
<div class="accordion-item ${statusClass}">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div class="repo-title" title="${name}">${name}</div><div class="repo-dept">${dept}</div><div class="repo-admin">${admin}</div><div class="repo-files ${statusClass==='status-error'?'warning-text':''}">${files||0}</div><div class="repo-log ${recentLog==='기록 없음'?'warning-text':''}" title="${recentLog}">${recentLog}</div>
</div>
`;
continentGroup.innerHTML = continentHtml;
container.appendChild(continentGroup);
});
const allContinents = container.querySelectorAll('.continent-group');
allContinents.forEach(continent => {
continent.classList.add('active');
});
const allCountries = container.querySelectorAll('.country-group');
allCountries.forEach(country => {
country.classList.add('active');
});
<div class="accordion-body">
<div class="detail-grid">
<div class="detail-section"><h4>참여 인원 상세</h4><table class="data-table"><thead><tr><th>이름</th><th>소속</th><th>권한</th></tr></thead><tbody><tr><td>${admin}</td><td>${dept}</td><td>관리자</td></tr></tbody></table></div>
<div class="detail-section"><h4>최근 활동</h4><table class="data-table"><thead><tr><th>유형</th><th>내용</th><th>일시</th></tr></thead><tbody><tr><td><span class="badge">로그</span></td><td>동기화 완료</td><td>${logTime}</td></tr></tbody></table></div>
</div>
</div>
</div>`;
}
function toggleGroup(header) {
const group = header.parentElement;
group.classList.toggle('active');
}
function toggleAccordion(header) {
const item = header.parentElement;
const container = item.parentElement;
const allItems = container.querySelectorAll('.accordion-item');
allItems.forEach(el => {
if (el !== item) el.classList.remove('active');
});
// --- 이벤트 핸들러 ---
function toggleGroup(h) { h.parentElement.classList.toggle('active'); }
function toggleAccordion(h) {
const item = h.parentElement;
item.parentElement.querySelectorAll('.accordion-item').forEach(el => { if(el!==item) el.classList.remove('active'); });
item.classList.toggle('active');
}
async function syncData() {
const btn = document.getElementById('syncBtn');
const logConsole = document.getElementById('logConsole');
const logBody = document.getElementById('logBody');
function showActivityDetails(status) {
const modal = document.getElementById('activityDetailModal'), tbody = document.getElementById('modalTableBody'), title = document.getElementById('modalTitle');
const names = { active:'정상', warning:'주의', stale:'방치', unknown:'데이터 없음' };
const filtered = (projectActivityDetails || []).filter(d => d.status === status);
title.innerText = `${names[status]} 목록 (${filtered.length}개)`;
tbody.innerHTML = filtered.map(p => {
const o = rawData.find(r => r[0] === p.name);
return `<tr class="modal-row" onclick="scrollToProject('${p.name}')"><td><strong>${p.name}</strong></td><td>${o?o[1]:"-"}</td><td>${o?o[2]:"-"}</td></tr>`;
}).join('');
modal.style.display = 'flex';
}
btn.classList.add('loading');
btn.innerHTML = `<span class="spinner"></span> 동기화 중 (진행 상황 확인 중...)`;
btn.disabled = true;
function closeActivityModal() { document.getElementById('activityDetailModal').style.display = 'none'; }
logConsole.style.display = 'block';
logBody.innerHTML = '';
function addLog(msg) {
const logItem = document.createElement('div');
logItem.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
logBody.appendChild(logItem);
logConsole.scrollTop = logConsole.scrollHeight;
}
try {
console.log("Attempting to connect to /sync...");
const response = await fetch(`/sync`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const payload = JSON.parse(line.substring(6));
if (payload.type === 'log') {
addLog(payload.message);
} else if (payload.type === 'done') {
const newData = payload.data;
newData.forEach(scrapedItem => {
const target = rawData.find(item =>
item[0].replace(/\s/g, '').includes(scrapedItem.projectName.replace(/\s/g, '')) ||
scrapedItem.projectName.replace(/\s/g, '').includes(item[0].replace(/\s/g, ''))
);
if (target) {
if (scrapedItem.recentLog !== "기존데이터유지") {
target[3] = scrapedItem.recentLog;
}
target[4] = scrapedItem.fileCount;
}
});
document.getElementById('projectAccordion').innerHTML = '';
init();
addLog(">>> 모든 동기화 작업이 완료되었습니다!");
alert(`${newData.length}개 프로젝트 동기화 완료!`);
logConsole.style.display = 'none';
}
}
}
}
} catch (e) {
addLog(`오류 발생: ${e.message}`);
alert("서버 연결 실패. 백엔드 서버가 실행 중인지 확인하세요.");
console.error(e);
} finally {
btn.classList.remove('loading');
btn.innerHTML = `<span class="spinner"></span> 데이터 동기화 (크롤링)`;
btn.disabled = false;
function scrollToProject(name) {
closeActivityModal();
const target = Array.from(document.querySelectorAll('.repo-title')).find(t => t.innerText.trim() === name.trim())?.closest('.accordion-header');
if (target) {
let p = target.parentElement;
while (p && p !== document.body) { if (p.classList.contains('continent-group') || p.classList.contains('country-group')) p.classList.add('active'); p = p.parentElement; }
target.parentElement.classList.add('active');
const pos = target.getBoundingClientRect().top + window.pageYOffset - 220;
window.scrollTo({ top: pos, behavior: 'smooth' });
target.style.backgroundColor = 'var(--primary-lv-1)';
setTimeout(() => target.style.backgroundColor = '', 2000);
}
}
// --- 크롤링 및 인증 제어 ---
async function syncData() {
if (isCrawling) {
if (confirm("크롤링을 중단하시겠습니까?")) {
const res = await fetch('/stop-sync');
if ((await res.json()).success) document.getElementById('syncBtn').innerText = "중단 요청 중...";
}
return;
}
const modal = document.getElementById('authModal');
if (modal) {
document.getElementById('authId').value = ''; document.getElementById('authPw').value = '';
document.getElementById('authErrorMessage').style.display = 'none';
modal.style.display = 'flex'; document.getElementById('authId').focus();
}
}
function closeAuthModal() { document.getElementById('authModal').style.display = 'none'; }
async function submitAuth() {
const id = document.getElementById('authId').value, pw = document.getElementById('authPw').value, err = document.getElementById('authErrorMessage');
try {
const res = await fetch('/auth/crawl', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({user_id:id, password:pw}) });
const data = await res.json();
if (data.success) { closeAuthModal(); startCrawlProcess(); }
else { err.innerText = "크롤링을 할 수 없습니다."; err.style.display = 'block'; }
} catch { err.innerText = "서버 연결 실패"; err.style.display = 'block'; }
}
async function startCrawlProcess() {
isCrawling = true;
const btn = document.getElementById('syncBtn'), logC = document.getElementById('logConsole'), logB = document.getElementById('logBody');
btn.classList.add('loading'); btn.style.backgroundColor = 'var(--error-color)'; btn.innerHTML = `<span class="spinner"></span> 크롤링 중단`;
logC.style.display = 'block'; logB.innerHTML = '<div style="color:#aaa; margin-bottom:10px;">>>> 엔진 초기화 중...</div>';
try {
const res = await fetch(`/sync`);
const reader = res.body.getReader(), decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read(); if (done) break;
decoder.decode(value).split('\n').forEach(line => {
if (line.startsWith('data: ')) {
const p = JSON.parse(line.substring(6));
if (p.type === 'log') {
const div = document.createElement('div'); div.innerText = `[${new Date().toLocaleTimeString()}] ${p.message}`;
logB.appendChild(div); logC.scrollTop = logC.scrollHeight;
} else if (p.type === 'done') { init(); alert(`동기화 종료`); logC.style.display = 'none'; }
}
});
}
} catch { alert("스트림 끊김"); }
finally { isCrawling = false; btn.classList.remove('loading'); btn.style.backgroundColor = ''; btn.innerHTML = `<span class="spinner"></span> 데이터 동기화 (크롤링)`; }
}
document.addEventListener('DOMContentLoaded', init);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -4,7 +4,7 @@ import os
def get_db():
return pymysql.connect(
host='localhost', user='root', password='45278434',
database='crawling', charset='utf8mb4'
database=os.getenv('DB_NAME', 'PM_proto'), charset='utf8mb4'
)
def migrate_to_timeseries():

View File

@@ -4,7 +4,7 @@ import os
def get_db():
return pymysql.connect(
host='localhost', user='root', password='45278434',
database='crawling', charset='utf8mb4',
database=os.getenv('DB_NAME', 'PM_proto'), charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)

257
server.py
View File

@@ -1,37 +1,33 @@
import os
import sys
# 한글 환경 및 Tesseract 경로 강제 설정
os.environ["PYTHONIOENCODING"] = "utf-8"
os.environ["TESSDATA_PREFIX"] = r"C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tessdata"
from fastapi import FastAPI
import re
import asyncio
import pymysql
from datetime import datetime
from pydantic import BaseModel
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from analyze import analyze_file_content
from crawler_service import run_crawler_service
import asyncio
from fastapi import Request
from crawler_service import run_crawler_service, crawl_stop_event
# --- 환경 설정 ---
os.environ["PYTHONIOENCODING"] = "utf-8"
# Tesseract 경로는 환경에 따라 다를 수 있으므로 환경변수 우선 사용 권장
TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX", r"C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tessdata")
os.environ["TESSDATA_PREFIX"] = TESSDATA_PREFIX
app = FastAPI(title="Project Master Overseas API")
templates = Jinja2Templates(directory="templates")
# --- 유틸리티: 동기 함수를 스레드 풀에서 실행 ---
async def run_in_threadpool(func, *args):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, func, *args)
# 정적 파일 및 미들웨어 설정
# 정적 파일 마운트
app.mount("/style", StaticFiles(directory="style"), name="style")
app.mount("/js", StaticFiles(directory="js"), name="js")
app.mount("/sample_files", StaticFiles(directory="sample"), name="sample_files")
@app.get("/sample.png")
async def get_sample_img():
return FileResponse("sample.png")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@@ -40,88 +36,29 @@ app.add_middleware(
allow_headers=["*"],
)
# --- 데이터 모델 ---
class AuthRequest(BaseModel):
user_id: str
password: str
# --- HTML 라우팅 ---
import pymysql
# --- 유틸리티 함수 ---
def get_db_connection():
"""MySQL 데이터베이스 연결을 반환 (환경변수 기반)"""
return pymysql.connect(
host='localhost',
user='root',
password='45278434',
database='crawling',
host=os.getenv('DB_HOST', 'localhost'),
user=os.getenv('DB_USER', 'root'),
password=os.getenv('DB_PASSWORD', '45278434'),
database=os.getenv('DB_NAME', 'PM_proto'),
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
@app.get("/available-dates")
async def get_available_dates():
"""
히스토리 테이블에서 유니크한 크롤링 날짜 목록을 반환
"""
try:
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT DISTINCT crawl_date FROM projects_history ORDER BY crawl_date DESC")
rows = cursor.fetchall()
dates = [row['crawl_date'].strftime("%Y.%m.%d") for row in rows if row['crawl_date']]
return dates
finally:
conn.close()
except Exception as e:
return {"error": str(e)}
@app.get("/project-data")
async def get_project_data(date: str = None):
"""
특정 날짜의 데이터를 JOIN하여 반환
"""
try:
conn = get_db_connection()
try:
with conn.cursor() as cursor:
if not date or date == "-":
cursor.execute("SELECT MAX(crawl_date) as last_date FROM projects_history")
target_date_row = cursor.fetchone()
target_date = target_date_row['last_date']
else:
target_date = date.replace(".", "-")
if not target_date:
return {"projects": [], "last_updated": "-"}
# 마스터 정보와 히스토리 정보를 JOIN
sql = """
SELECT m.project_nm, m.short_nm, m.department, m.master,
h.recent_log, h.file_count, m.continent, m.country
FROM projects_master m
JOIN projects_history h ON m.project_id = h.project_id
WHERE h.crawl_date = %s
ORDER BY m.project_id ASC
"""
cursor.execute(sql, (target_date,))
rows = cursor.fetchall()
projects = []
for row in rows:
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": projects, "last_updated": target_date.strftime("%Y.%m.%d") if hasattr(target_date, 'strftime') else str(target_date).replace("-", ".")}
finally:
conn.close()
except Exception as e:
return {"error": str(e)}
async def run_in_threadpool(func, *args):
"""동기 함수를 비차단 방식으로 실행"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, func, *args)
# --- HTML 라우팅 ---
@app.get("/")
async def root(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@@ -131,37 +68,119 @@ async def get_dashboard(request: Request):
return templates.TemplateResponse("dashboard.html", {"request": request})
@app.get("/mailTest")
@app.get("/mailTest.html")
async def get_mail_test(request: Request):
return templates.TemplateResponse("mailTest.html", {"request": request})
# --- 데이터 API ---
@app.get("/attachments")
async def get_attachments():
sample_path = "sample"
if not os.path.exists(sample_path):
os.makedirs(sample_path)
files = []
for f in os.listdir(sample_path):
f_path = os.path.join(sample_path, f)
if os.path.isfile(f_path):
files.append({
"name": f,
"size": f"{os.path.getsize(f_path) / 1024:.1f} KB"
})
return files
# --- 분석 및 수집 API ---
@app.get("/available-dates")
async def get_available_dates():
"""히스토리 날짜 목록 반환"""
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT DISTINCT crawl_date FROM projects_history ORDER BY crawl_date DESC")
rows = cursor.fetchall()
return [row['crawl_date'].strftime("%Y.%m.%d") for row in rows if row['crawl_date']]
except Exception as e:
return {"error": str(e)}
@app.get("/analyze-file")
async def analyze_file(filename: str):
"""
분석 서비스(analyze.py) 호출 - 스레드 풀에서 비차단 방식으로 실행
"""
return await run_in_threadpool(analyze_file_content, filename)
@app.get("/project-data")
async def get_project_data(date: str = None):
"""특정 날짜의 프로젝트 정보 JOIN 반환"""
try:
target_date = date.replace(".", "-") if date and date != "-" else None
with get_db_connection() as conn:
with conn.cursor() as cursor:
if not target_date:
cursor.execute("SELECT MAX(crawl_date) as last_date FROM projects_history")
res = cursor.fetchone()
target_date = res['last_date']
if not target_date: return {"projects": []}
sql = """
SELECT m.project_nm, m.short_nm, m.department, m.master,
h.recent_log, h.file_count, m.continent, m.country
FROM projects_master m
JOIN projects_history h ON m.project_id = h.project_id
WHERE h.crawl_date = %s ORDER BY m.project_id ASC
"""
cursor.execute(sql, (target_date,))
rows = cursor.fetchall()
projects = []
for r in rows:
name = r['short_nm'] if r['short_nm'] and r['short_nm'].strip() else r['project_nm']
projects.append([name, r['department'], r['master'], r['recent_log'], r['file_count'], r['continent'], r['country']])
return {"projects": projects}
except Exception as e:
return {"error": str(e)}
@app.get("/project-activity")
async def get_project_activity(date: str = None):
"""활성도 분석 API"""
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
if not date or date == "-":
cursor.execute("SELECT MAX(crawl_date) as last_date FROM projects_history")
res = cursor.fetchone()
target_date_val = res['last_date'] if res['last_date'] else datetime.now().date()
else:
target_date_val = datetime.strptime(date.replace(".", "-"), "%Y-%m-%d").date()
target_date_dt = datetime.combine(target_date_val, datetime.min.time())
sql = """
SELECT m.project_id, m.project_nm, m.short_nm, h.recent_log, h.file_count
FROM projects_master m
LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s
"""
cursor.execute(sql, (target_date_val,))
rows = cursor.fetchall()
analysis = {"summary": {"active": 0, "warning": 0, "stale": 0, "unknown": 0}, "details": []}
for r in rows:
log, files = r['recent_log'], r['file_count']
status, days = "unknown", 999
if log and log != "데이터 없음" and files and files > 0:
match = re.search(r'(\d{4})\.(\d{2})\.(\d{2})', log)
if match:
diff = (target_date_dt - datetime.strptime(match.group(0), "%Y.%m.%d")).days
status = "active" if diff <= 7 else "warning" if diff <= 14 else "stale"
days = diff
analysis["summary"][status] += 1
analysis["details"].append({"name": r['short_nm'] or r['project_nm'], "status": status, "days_ago": days})
return analysis
except Exception as e:
return {"error": str(e)}
@app.post("/auth/crawl")
async def auth_crawl(req: AuthRequest):
"""크롤링 인증"""
if req.user_id == os.getenv("PM_USER_ID") and req.password == os.getenv("PM_PASSWORD"):
return {"success": True}
return {"success": False, "message": "크롤링을 할 수 없습니다."}
@app.get("/sync")
async def sync_data():
"""
크롤링 서비스(crawler_service.py) 호출
"""
print(">>> /sync request received")
return StreamingResponse(run_crawler_service(), media_type="text_event-stream")
@app.get("/stop-sync")
async def stop_sync():
crawl_stop_event.set()
return {"success": True}
@app.get("/attachments")
async def get_attachments():
path = "sample"
if not os.path.exists(path): os.makedirs(path)
return [{"name": f, "size": f"{os.path.getsize(os.path.join(path, f))/1024:.1f} KB"}
for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
@app.get("/analyze-file")
async def analyze_file(filename: str):
return await run_in_threadpool(analyze_file_content, filename)
@app.get("/sample.png")
async def get_sample_img():
return FileResponse("sample.png")

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,42 +0,0 @@
[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.04, 파일다운로드",과업개요 페이지 없음,817,,,
1 [PM Overseas 프로젝트 현황] 2026.03.04 <<활동로그가 없는 프로젝트 (8)
2
3 No. 프로젝트 명 담당부서 담당자 종료(예정)일 최근 활동로그 과업개요 작성 유무 파일 수 비고
4 1 라오스 ITTC 관개 교육센터 PMC 수자원1부 방노성 2025.12.20 2026.01.29, 폴더 삭제 O 16 2026.01.29 로그는 테스트 활동 추정 종료(예정)일 지남 진행
5 2 라오스 비엔티안 메콩강 관리 2차 DD 수자원1부 방노성 2026.05.31 2025.12.07, 파일업로드 X 260 탭 1개에 모든파일 업로드
6 3 미얀마 만달레이 철도 개량 감리 CS 철도사업부 김태헌 2027.11.17 2025.11.17, 폴더이름변경 O 298
7 4 베트남 푸옥호아 양수 발전 FS 수력부 이철호 2025.11.30 2026.02.23, 폴더이름변경 O 139 준공도서 3월 작성예정 종료(예정)일 지남 준공
8 5 사우디아라비아 아시르 지잔 고속도로 FS 도로부 공태원 2025.11.21 2026.02.09, 파일다운로드 O 73 종료(예정)일 지남 준공
9 6 우즈베키스탄 지방 도로 복원 MP 도로부 장진영 2029.04.28 X X 0
10 7 우즈베키스탄 타슈켄트 철도 FS 철도사업부 김태헌 2026.03.20 2026.02.05, 파일업로드 O 51
11 8 이라크 Habbaniyah Shuaiba AirBase PD 도로부 강동구 2026.12.31 X X 0
12 9 메콩유역 수자원 관리 기후적응 MP 수자원1부 정귀한 2025.12.31 X X 0 종료(예정)일 지남 준공
13 10 캄보디아 반테 민체이 관개 홍수저감 MP 수자원1부 이대주 2026.08.28 2025.12.07, 파일업로드 X 44
14 11 캄보디아 시엠립 하수처리 개선 DD 물환경사업1부 변역근 2028.12.18 2026.02.06, AI 요약 O 221
15 12 키르기스스탄 잘랄아바드 상수도 계획 MP 물환경사업1부 변기상 2025.12.31 2026.02.12, 파일업로드 X 60 종료(예정)일 지남 준공
16 13 파키스탄 펀잡 홍수 방재 PMC 수자원1부 방노성 2027.12.31 2025.12.08, 폴더삭제 O 0
17 14 파키스탄 KP 아보타바드 상수도 PMC 물환경사업2부 변기상 2026.12.31 2026.02.26, 파일업로드 O 240
18 15 파키스탄 CAREC 도로 감리 DD 도로부 황효섭 2026.10.26 X X 0
19 16 필리핀 홍수 복원 InFRA2 DD 수자원1부 이대주 2026.08.07 2025.12.01, 폴더삭제 O 6 최근로그 >> 폴더자동삭제(파일 개수 미달)
20 17 필리핀 홍수 관리 Package5B MP 수자원1부 이희철 2026.05.31 2025.12.02, 폴더이름변경 O 14
21 18 필리핀 PGN 해상교량 BID2 IDC 구조부 이상희 2026.05.31 2026.02.11, 파일다운로드 O 631
22 19 가나 테치만 상수도 확장 DS 물환경사업2부 - 2029.04.25 X X 0 책임자 및 담당자 설정X
23 20 기니 벼 재배단지 PMC 수자원1부 이대주 2028.12.20 2025.12.08, 파일업로드 O 43 최근로그 >> 폴더자동삭제(파일 개수 미달)
24 21 우간다 벼 재배단지 PMC 수자원1부 방노성 2028.12.20 2025.12.08, 파일업로드 O 52
25 22 우간다 부수쿠마 분뇨 자원화 2단계 PMC 물환경사업2부 변기상 2026.12.31 2026.02.05, 파일업로드 X 9
26 23 에티오피아 지하수 관개 환경설계 DD 물환경사업2부 변기상 2026.06.23 X X 0
27 24 에티오피아 도도타군 관개 PMC 수자원1부 방노성 2026.12.31 2025.12.01, 폴더이름변경 O 144 탭 1개에 모든파일 업로드 // 최근로그 >> 폴더자동삭제(파일 개수 미달)
28 25 에티오피아 Adeaa-Becho 지하수 관개 MP 수자원1부 방노성 2026.07.31 2025.11.21, 파일업로드 O 146 최근로그 >> 폴더자동삭제(파일 개수 미달)
29 26 탄자니아 Iringa 상하수도 개선 CS 물환경사업1부 백운영 2029.06.08 2026.02.03, 폴더생성 X 0
30 27 탄자니아 Dodoma 하수 설계감리 DD 물환경사업2부 변기상 2027.07.08 2026.02.04, 폴더삭제 X 32
31 28 탄자니아 잔지바르 쌀 생산 PMC 수자원1부 방노성 2027.12.20 2025.12.08, 파일 업로드 O 23
32 29 탄자니아 도도마 유수율 상수도개선 PMC 물환경사업1부 박순석 2026.12.31 2026.02.12, 부관리자권한추가 X 35
33 30 아르헨티나 SALDEORO 수력발전 28MW DD 플랜트1부 양정모 2026.01.31 X X 0 종료(예정)일 지남 준공
34 31 온두라스 LaPaz Danli 상수도 CS 물환경사업2부 - 2027.02.23 2026.01.29, 파일 삭제 O 60 책임자 및 담당자 설정 X, 실 관리부서는 해외사업부, 더미파일 다수
35 32 볼리비아 에스꼬마 차라짜니 도로 CS 도로부 전홍찬 2029.12.15 2026.02.06, 파일업로드 X 1
36 33 볼리비아 마모레 교량도로 FS 도로부 황효섭 2025.10.17 2026.02.06, 파일업로드 X 120 종료(예정)일 지남 준공
37 34 볼리비아 Bombeo-Colomi 도로설계 DD 도로부 황효섭 2026.07.24 2025.12.05, 파일삭제 O 48 더미파일(폴더유지용) 12개, 실 관리부서는 해외사업부
38 35 콜롬비아 AI 폐기물 FS 플랜트1부 서재희 2026.02.27 X X 0 종료(예정)일 지남
39 36 파라과이 도로 통행료 현대화 MP 교통계획부 오제훈 2025.10.24 2025.02.25, 폴더삭제 X 0 종료(예정)일 지남 준공
40 37 페루 Barranca 상하수도 확장 DD 물환경사업2부 변기상 2026.03.08 2025.11.14, 파일업로드 O 44 더미파일(폴더유지용) 27개, 실 관리부서는 해외사업부
41 38 엘살바도르 태평양 철도 FS 철도사업부 김태헌 2025.12.31 2026.02.24, 폴더자동삭제 X 101 종료(예정)일 지남 준공
42 39 필리핀 사무소 해외사업부 한형남 2026.03.04, 파일다운로드 과업개요 페이지 없음 817

View File

@@ -1,91 +1,72 @@
:root {
--topbar-h: 36px;
--header-h: 56px;
--activity-h: 110px;
--fixed-total-h: calc(var(--topbar-h) + var(--header-h) + var(--activity-h));
--primary-color: #1E5149;
--primary-lv-0: #f0f7f4;
--primary-lv-1: #e1eee9;
--border-color: #e5e7eb;
--bg-muted: #F9FAFB;
--text-main: #111827;
--text-sub: #6B7280;
--error-color: #F21D0D;
}
/* Portal (Index) */
.portal-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 36px);
height: calc(100vh - var(--topbar-h));
background: var(--bg-muted);
padding: var(--space-lg);
margin-top: 36px;
padding: 32px;
margin-top: var(--topbar-h);
}
.portal-header {
text-align: center;
margin-bottom: 50px;
}
.portal-header { text-align: center; margin-bottom: 50px; }
.portal-header h1 { font-size: 28px; color: var(--primary-color); margin-bottom: 10px; font-weight: 800; }
.portal-header p { color: var(--text-sub); font-size: 15px; }
.button-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px; width: 100%; max-width: 800px; }
.portal-card { background: #fff; border: 1px solid var(--border-color); border-radius: 12px; padding: 40px; text-align: center; transition: all 0.3s ease; width: 100%; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; align-items: center; gap: 20px; cursor: pointer; text-decoration: none; }
.portal-card:hover { transform: translateY(-8px); border-color: var(--primary-color); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); }
.portal-card i { font-size: 48px; color: var(--primary-color); }
.portal-card h3 { font-size: 20px; color: var(--text-main); margin: 0; }
.portal-card p { font-size: 14px; color: var(--text-sub); margin: 0; }
.portal-header h1 {
font-size: 28px;
color: var(--primary-color);
margin-bottom: 10px;
}
.portal-header p {
color: var(--text-sub);
font-size: 15px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
width: 100%;
max-width: 800px;
}
.portal-card {
background: #fff;
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 40px;
text-align: center;
transition: 0.3s;
width: 100%;
box-shadow: var(--box-shadow);
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.portal-card:hover {
transform: translateY(-5px);
border-color: var(--primary-color);
box-shadow: var(--box-shadow-lg);
}
/* Dashboard List & Console */
/* Dashboard Layout */
header {
position: fixed;
top: 36px;
left: 0;
right: 0;
z-index: 1000;
background: #fff;
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-md) var(--space-lg);
border-bottom: 1px solid var(--border-color);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
position: fixed; top: var(--topbar-h); left: 0; right: 0; z-index: 1001;
background: #fff; height: var(--header-h); display: flex; justify-content: space-between; align-items: center; padding: 0 32px; border-bottom: 1px solid #f5f5f5;
}
.main-content {
margin-top: 100px;
padding: var(--space-lg);
max-width: 1400px;
margin-left: auto;
margin-right: auto;
.activity-dashboard-wrapper {
position: fixed; top: calc(var(--topbar-h) + var(--header-h)); left: 0; right: 0; z-index: 1000;
background: #fff; height: var(--activity-h); border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 6px rgba(0,0,0,0.03);
}
.activity-dashboard { max-width: 1200px; margin: 0 auto; height: 100%; display: flex; gap: 15px; padding: 10px 32px 20px 32px; }
.activity-card { flex: 1; padding: 12px 15px; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; display: flex; flex-direction: column; justify-content: center; gap: 2px; border-left: 5px solid transparent; }
.activity-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.activity-card.active { background: #e8f5e9; border-left-color: #4DB251; }
.activity-card.warning { background: #fff8e1; border-left-color: #FFBF00; }
.activity-card.stale { background: #ffebee; border-left-color: #F21D0D; }
.activity-card.unknown { background: #f5f5f5; border-left-color: #9e9e9e; }
.activity-card .label { font-size: 11px; font-weight: 600; opacity: 0.7; }
.activity-card .count { font-size: 20px; font-weight: 800; }
.main-content { margin-top: var(--fixed-total-h); padding: 32px; max-width: 1400px; margin-left: auto; margin-right: auto; }
/* 로그 콘솔 - 초기 디자인 복구 (Sticky Terminal 스타일) */
.log-console {
position: sticky;
top: 100px;
top: var(--fixed-total-h);
z-index: 999;
background: #000;
color: #0f0;
font-family: monospace;
font-family: 'Consolas', 'Monaco', monospace;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
@@ -104,212 +85,72 @@ header {
font-weight: bold;
}
.accordion-container {
border-top: 1px solid var(--border-color);
/* 모달 정중앙 배치 */
.activity-modal-overlay {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); z-index: 3000;
display: flex; align-items: center; justify-content: center; padding: 20px;
}
.accordion-list-header,
.accordion-header {
display: grid;
grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr;
gap: var(--space-md);
padding: var(--space-md) var(--space-lg);
align-items: center;
cursor: pointer;
.activity-modal-content {
background: #fff; width: 600px; max-height: 85vh; border-radius: 12px;
display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
}
.accordion-list-header {
position: sticky;
top: 100px;
background: var(--bg-muted);
z-index: 10;
font-size: 11px;
font-weight: 700;
color: var(--text-sub);
border-bottom: 1px solid var(--text-main);
cursor: default;
.auth-modal-content {
background: #fff; width: 400px; border-radius: 16px; padding: 40px;
text-align: center; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
display: flex; flex-direction: column; gap: 25px;
}
.accordion-item {
border-bottom: 1px solid var(--border-color);
}
.modal-header { padding: 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.modal-header h3 { margin: 0; font-size: 16px; color: var(--primary-color); }
.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: var(--text-sub); }
.modal-body { padding: 20px; overflow-y: auto; }
.modal-row { cursor: pointer; border-bottom: 1px solid #f5f5f5; }
.modal-row:hover { background: var(--primary-lv-0); }
.accordion-item:hover {
background: var(--primary-lv-0);
}
/* 인증 모달 내부 요소 */
.auth-header i { font-size: 40px; color: var(--primary-color); margin-bottom: 15px; }
.auth-header h3 { font-size: 22px; color: var(--text-main); margin: 0; font-weight: 800; }
.auth-header p { font-size: 14px; color: var(--text-sub); margin-top: 8px; }
.auth-body { display: flex; flex-direction: column; gap: 15px; text-align: left; }
.input-group { display: flex; flex-direction: column; gap: 6px; }
.input-group label { font-size: 12px; font-weight: 700; color: var(--text-sub); }
.input-group input { padding: 12px 16px; border: 1px solid var(--border-color); border-radius: 8px; font-size: 14px; transition: 0.2s; }
.input-group input:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px var(--primary-lv-1); }
.error-text { color: var(--error-color); font-size: 13px; font-weight: 600; margin-top: 10px; text-align: center; }
.auth-footer { display: flex; gap: 10px; margin-top: 10px; }
.auth-footer button { flex: 1; padding: 12px; border-radius: 8px; font-weight: 700; cursor: pointer; transition: 0.2s; border: none; }
.cancel-btn { background: #f3f4f6; color: var(--text-sub); }
.login-btn { background: var(--primary-color); color: #fff; }
.repo-title {
font-weight: 700;
color: var(--primary-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Accordion Layout */
.accordion-list-header { position: sticky; top: var(--fixed-total-h); background: #fff; z-index: 900; font-size: 11px; font-weight: 700; color: var(--text-sub); padding: 14px 24px; border-bottom: 2px solid var(--primary-color); box-shadow: 0 4px 10px rgba(0,0,0,0.05); display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px; align-items: center; }
.accordion-header { display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px; padding: 16px 24px; align-items: center; cursor: pointer; border-bottom: 1px solid var(--border-color); transition: background 0.1s; }
.accordion-item:hover .accordion-header { background: var(--primary-lv-0); }
.repo-title { font-weight: 700; color: var(--primary-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.repo-dept, .repo-admin { font-size: 12px; color: var(--text-main); }
.repo-files { text-align: center; font-weight: 600; }
.repo-log { font-size: 11px; color: var(--text-sub); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.accordion-body { display: none; padding: 24px; background: var(--bg-muted); border-bottom: 1px solid var(--border-color); }
.accordion-item.active .accordion-body { display: block; }
.status-warning { background: #fffcf0; }
.status-error { background: #fff5f4; }
.warning-text { color: var(--error-color) !important; font-weight: 700; }
.repo-files {
text-align: center;
font-weight: 600;
}
.continent-group, .country-group { margin-bottom: 15px; }
.continent-header, .country-header { background: #fff; padding: 14px 20px; border: 1px solid var(--border-color); border-radius: 8px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; font-weight: 700; }
.continent-header { background: var(--primary-color); color: white; border: none; font-size: 15px; }
.country-header { font-size: 14px; color: var(--text-main); margin-top: 8px; }
.continent-body, .country-body { display: none; padding: 10px 0 10px 15px; }
.active>.continent-body, .active>.country-body { display: block; }
.repo-log {
font-size: 11px;
color: var(--text-sub);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.accordion-body {
display: none;
padding: var(--space-lg);
background: var(--bg-muted);
border-top: 1px solid var(--border-color);
}
.accordion-item.active .accordion-body {
display: block;
}
.status-warning {
background: #fff9e6;
}
.status-error {
background: #fee9e7;
}
.warning-text {
color: #f21d0d !important;
font-weight: 700;
}
/* Multi-level Groups */
.continent-group,
.country-group {
margin-bottom: 10px;
}
.continent-header,
.country-header {
background: #fff;
padding: 12px 20px;
border: 1px solid var(--border-color);
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-weight: 700;
transition: all 0.2s;
}
.continent-header {
background: var(--primary-color);
color: white;
border: none;
font-size: 15px;
}
.country-header {
font-size: 14px;
color: var(--text-main);
margin-top: 5px;
}
.continent-body,
.country-body {
display: none;
padding: 10px 0 10px 20px;
}
.active>.continent-body,
.active>.country-body {
display: block;
}
/* Detail Views */
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.detail-section h4 {
font-size: 13px;
margin-bottom: 10px;
color: var(--text-main);
border-left: 3px solid var(--primary-color);
padding-left: 8px;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.data-table th,
.data-table td {
padding: 8px;
border-bottom: 1px solid var(--border-color);
text-align: left;
}
.data-table th {
color: var(--text-sub);
font-weight: 600;
}
/* Sync Button & Admin Info */
.sync-btn {
display: flex;
align-items: center;
gap: var(--space-sm);
background-color: var(--primary-color);
color: #fff;
padding: 8px 16px;
border-radius: var(--radius-lg);
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: none;
box-shadow: var(--box-shadow);
}
.sync-btn:hover {
background-color: var(--primary-lv-8);
}
.sync-btn.loading .spinner {
display: inline-block;
}
.admin-info {
font-size: 13px;
color: var(--text-sub);
margin-left: var(--space-md);
padding: 6px 12px;
background: var(--bg-muted);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
}
.admin-info strong {
color: var(--primary-color);
font-weight: 700;
}
.base-date-info {
font-size: 13px;
color: var(--text-sub);
background: #f8f9fa;
padding: 6px 15px;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.base-date-info strong {
color: #333;
font-weight: 700;
margin-left: 5px;
}
.data-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.data-table th, .data-table td { padding: 10px 8px; border-bottom: 1px solid var(--border-color); text-align: left; }
.data-table th { color: var(--text-sub); font-weight: 600; background: #fcfcfc; }
.sync-btn { display: flex; align-items: center; gap: 8px; background-color: var(--primary-color); color: #fff; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; transition: 0.2s; }
.admin-info { font-size: 12px; color: var(--text-sub); margin-left: 16px; padding: 6px 12px; background: #f8f9fa; border-radius: 4px; border: 1px solid var(--border-color); }
.admin-info strong { color: var(--primary-color); font-weight: 700; }
.base-date-info { font-size: 13px; color: var(--text-sub); background: #fdfdfd; padding: 6px 15px; border-radius: 6px; border: 1px solid var(--border-color); }
.base-date-info strong { color: #333; font-weight: 700; }

View File

@@ -42,7 +42,14 @@
</div>
</header>
<!-- 실시간 로그 콘솔 추가 -->
<!-- 프로젝트 활성도 대시보드 (전체 너비 래퍼) -->
<div class="activity-dashboard-wrapper">
<div id="activityDashboard" class="activity-dashboard">
<!-- JS에서 동적 삽입 -->
</div>
</div>
<!-- 실시간 로그 콘솔 (본문 내부로 복구) -->
<div id="logConsole" class="log-console" style="display:none;">
<div class="log-console-header">실시간 수집 로그 [PM Overseas]</div>
<div id="logBody"></div>
@@ -53,8 +60,57 @@
</div>
</main>
<!-- 모달 레이어 (최외각 유지) -->
<div id="authModal" class="activity-modal-overlay" style="display:none;">
<div class="auth-modal-content">
<div class="auth-header">
<i class="fas fa-lock"></i>
<h3>크롤링 권한 인증</h3>
<p>시스템 동기화를 위해 관리자 계정으로 로그인하세요.</p>
</div>
<div class="auth-body">
<div class="input-group">
<label>관리자 아이디</label>
<input type="text" id="authId" placeholder="아이디를 입력하세요">
</div>
<div class="input-group">
<label>비밀번호</label>
<input type="password" id="authPw" placeholder="비밀번호를 입력하세요" onkeyup="if(event.key==='Enter') submitAuth()">
</div>
<div id="authErrorMessage" class="error-text" style="display:none;">크롤링을 할 수 없습니다.</div>
</div>
<div class="auth-footer">
<button class="cancel-btn" onclick="closeAuthModal()">취소</button>
<button class="login-btn" onclick="submitAuth()">인증 및 실행</button>
</div>
</div>
</div>
<div id="activityDetailModal" class="activity-modal-overlay" style="display:none;" onclick="closeActivityModal(event)">
<div class="activity-modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3 id="modalTitle">상세 목록</h3>
<button class="close-btn" onclick="closeActivityModal()">&times;</button>
</div>
<div class="modal-body">
<table class="data-table">
<thead>
<tr>
<th>프로젝트명</th>
<th>담당부서</th>
<th>담당자</th>
</tr>
</thead>
<tbody id="modalTableBody">
<!-- JS에서 동적 삽입 -->
</tbody>
</table>
</div>
</div>
</div>
<script src="js/common.js"></script>
<script src="js/dashboard.js"></script>
</body>
</html>
</html>

View File

@@ -1,33 +0,0 @@
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())