diff --git a/ADMIN_PLAN.md b/ADMIN_PLAN.md
new file mode 100644
index 0000000..800be20
--- /dev/null
+++ b/ADMIN_PLAN.md
@@ -0,0 +1,31 @@
+# 데이터 분석 관리자 페이지 기획안
+
+## 1. 프로젝트 개요
+본 프로젝트는 데이터 분석 프로세스 및 프로젝트 리소스를 통합 관리하기 위한 관리자 대시보드입니다. 사용자 인터랙션 관리부터 시스템 로그, 리소스 현황을 한눈에 파악하는 것을 목표로 합니다.
+
+## 2. 주요 기능 상세
+
+### ① 문의 및 요구사항 관리 (Inquiry Management)
+- 사용자의 분석 요청 및 시스템 문의 사항 리스트업
+- 상태값(대기, 처리중, 완료) 관리 및 답변 등록 기능
+
+### ② 로그 관리 (Log Management)
+- **최근 로그**: 실시간으로 발생하는 시스템 및 분석 작업 로그 출력
+- **전체 로그**: 날짜별, 프로젝트별 필터링을 통한 로그 기록 조회 및 내보내기
+
+### ③ 파일 관리 (File Management)
+- 프로젝트별 데이터셋, 분석 결과물 파일 개수 및 용량 통계
+- 파일 확장자별 구성 비율(CSV, JSON, Python 등) 시각화 지표 제공
+
+### ④ 인원 관리 (Personnel Management)
+- 프로젝트 참여 인원 현황 조회
+- 사용자별 권한(관리자, 분석가, 뷰어) 부여 및 수정 기능
+
+### ⑤ 공지사항 (Notice & Patch Notes)
+- 분석 모델 업데이트, 시스템 점검, 패치 내역 공유
+- 사용자 대상 공지사항 작성 및 게시판 관리
+
+## 3. UI/UX 가이드라인
+- **Layout**: 좌측 내비게이션 바(Sidebar) + 상단 헤더(Header) + 중앙 컨텐츠 영역
+- **Theme**: 신뢰감을 주는 Dark Blue / White 톤의 깨끗한 디자인
+- **Responsiveness**: 다양한 해상도에 대응하는 반응형 레이아웃 구성
diff --git a/crawler_api.py b/crawler_api.py
new file mode 100644
index 0000000..e47383e
--- /dev/null
+++ b/crawler_api.py
@@ -0,0 +1,165 @@
+import os
+import re
+import asyncio
+import json
+import traceback
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import StreamingResponse
+from playwright.async_api import async_playwright
+from dotenv import load_dotenv
+
+load_dotenv()
+
+app = FastAPI()
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=False,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+@app.get("/sync")
+async def sync_data():
+ async def event_generator():
+ user_id = os.getenv("PM_USER_ID")
+ password = os.getenv("PM_PASSWORD")
+
+ if not user_id or not password:
+ yield f"data: {json.dumps({'type': 'log', 'message': '오류: .env 파일에 계정 정보가 없습니다.'})}\n\n"
+ return
+
+ TIMEOUT_MS = 600000
+ results = []
+
+ async with async_playwright() as p:
+ yield f"data: {json.dumps({'type': 'log', 'message': '브라우저 실행 중...'})}\n\n"
+ browser = await p.chromium.launch(headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"])
+ context = await browser.new_context(viewport={'width': 1920, 'height': 1080})
+ context.set_default_timeout(60000)
+ page = await context.new_page()
+
+ try:
+ yield f"data: {json.dumps({'type': 'log', 'message': '사이트 접속 및 로그인 중...'})}\n\n"
+ await page.goto("https://overseas.projectmastercloud.com/", wait_until="domcontentloaded")
+
+ await page.click("#login-by-id", timeout=10000)
+ await page.fill("#user_id", user_id)
+ await page.fill("#user_pw", password)
+ await page.click("#login-btn")
+
+ yield f"data: {json.dumps({'type': 'log', 'message': '대시보드 목록 대기 중...'})}\n\n"
+ await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=60000)
+
+ locators = page.locator("h4.list__contents_aria_group_body_list_item_label")
+ count = await locators.count()
+ yield f"data: {json.dumps({'type': 'log', 'message': f'총 {count}개의 프로젝트 발견. 수집 시작.'})}\n\n"
+
+ for i in range(count):
+ try:
+ proj = page.locator("h4.list__contents_aria_group_body_list_item_label").nth(i)
+ project_name = (await proj.inner_text()).strip()
+
+ yield f"data: {json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] {project_name} - 수집 시작...'})}\n\n"
+ await proj.scroll_into_view_if_needed()
+ await proj.click(force=True)
+ await page.wait_for_selector("div.footer", timeout=20000)
+
+ recent_log = "없음"
+ file_count = 0
+
+ # 1단계: 활동로그 수집
+ try:
+ log_btn = page.locator("div.wrap.log-wrap > div.title.text").first
+ if await log_btn.is_visible(timeout=5000):
+ await log_btn.click()
+ await page.wait_for_timeout(2000) # 로그 로딩 여유
+ log_content = page.locator("div.wrap.log-wrap .content-area").first
+ if await log_content.is_visible(timeout=5000):
+ content = await log_content.inner_text()
+ lines = [l.strip() for l in content.split("\n") if len(l.strip()) > 2]
+ if lines: recent_log = lines[0]
+ await page.locator("body > article.archive-modal div.close").first.click()
+ await page.wait_for_timeout(1000)
+ except: pass
+
+ # 2단계: 구성(파일 수) 수집 - 안정성 대폭 강화
+ try:
+ sitemap_btn = page.locator("div.wrap.site-map-wrap > div").first
+ if await sitemap_btn.is_visible(timeout=5000):
+ await sitemap_btn.click()
+
+ popup_page = None
+ for _ in range(20):
+ for p_item in context.pages:
+ if "composition-tab.html" in p_item.url:
+ popup_page = p_item
+ break
+ if popup_page: break
+ await asyncio.sleep(0.5)
+
+ if popup_page:
+ yield f"data: {json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] 구성 데이터 로딩 대기 중 (여유있게)...'})}\n\n"
+ await popup_page.wait_for_load_state("domcontentloaded")
+
+ # 데이터가 로드될 때까지 점진적으로 대기 (최대 7초)
+ for _ in range(7):
+ h6_check = popup_page.locator("#composition-list li h6:nth-child(3)")
+ if await h6_check.count() > 0:
+ break
+ await asyncio.sleep(1)
+
+ # 최종 데이터를 가져오기 전 마지막 2초 추가 대기 (완전한 렌더링 확인)
+ await asyncio.sleep(2)
+
+ target_h6_locators = popup_page.locator("#composition-list li h6:nth-child(3)")
+ h6_count = await target_h6_locators.count()
+
+ yield f"data: {json.dumps({'type': 'log', 'message': f'[{i+1}/{count}] 총 {h6_count}개의 항목 로드됨. 합산 중...'})}\n\n"
+
+ current_total = 0
+ for j in range(h6_count):
+ text = (await target_h6_locators.nth(j).inner_text()).strip()
+ last_line = text.split('\n')[-1]
+ nums = re.findall(r'\d+', last_line)
+ if nums:
+ val = int(nums[0])
+ if val < 10000: # 1만개 미만만 합산 (연도 필터링)
+ current_total += val
+
+ file_count = current_total
+ await popup_page.close()
+ await page.bring_to_front()
+ except Exception as e:
+ yield f"data: {json.dumps({'type': 'log', 'message': f'!!! 구성 수집 지연: {str(e)[:30]}'})}\n\n"
+
+ summary_msg = f"[{i+1}/{count}] 수집 완료 - 파일: {file_count}개, 최근로그: {recent_log[:40]}..."
+ yield f"data: {json.dumps({'type': 'log', 'message': summary_msg})}\n\n"
+
+ results.append({"projectName": project_name, "recentLog": recent_log, "fileCount": file_count})
+
+ # 3단계: 복귀
+ home_btn = page.locator("div.header div.title div").first
+ try:
+ await home_btn.click(force=True, timeout=10000)
+ await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=20000)
+ except:
+ await page.goto("https://overseas.projectmastercloud.com/dashboard", wait_until="domcontentloaded")
+ await page.wait_for_selector("h4.list__contents_aria_group_body_list_item_label", timeout=20000)
+ await page.wait_for_timeout(1500)
+
+ except Exception as e_proj:
+ yield f"data: {json.dumps({'type': 'log', 'message': f'!!! {i+1}번째 프로젝트 실패 (건너뜀)'})}\n\n"
+ await page.goto("https://overseas.projectmastercloud.com/", wait_until="domcontentloaded")
+ await page.wait_for_timeout(3000)
+
+ yield f"data: {json.dumps({'type': 'done', 'data': results})}\n\n"
+
+ except Exception as e:
+ yield f"data: {json.dumps({'type': 'log', 'message': f'치명적 오류: {str(e)}'})}\n\n"
+ finally:
+ await browser.close()
+
+ return StreamingResponse(event_generator(), media_type="text_event-stream")
diff --git a/dashboard.html b/dashboard.html
new file mode 100644
index 0000000..8f74889
--- /dev/null
+++ b/dashboard.html
@@ -0,0 +1,365 @@
+
+
+
+
+
+ Project Master Overseas 관리자
+
+
+
+
+
+
+
+
+
+
+
대시보드 현황
+
+
+
+
접속자: 이태훈[전체관리자]
+
+
+
+
+
+
실시간 수집 로그 [PM Overseas]
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..23b8545
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+fastapi==0.110.0
+uvicorn==0.29.0
+playwright==1.42.0
+python-dotenv==1.0.1
\ No newline at end of file
diff --git a/sample.png b/sample.png
new file mode 100644
index 0000000..74c6b47
Binary files /dev/null and b/sample.png differ
diff --git a/style/style.css b/style/style.css
new file mode 100644
index 0000000..a59542c
--- /dev/null
+++ b/style/style.css
@@ -0,0 +1,412 @@
+:root {
+ --primary-color: #1E5149;
+ --bg-color: #FFFFFF;
+ --text-main: #222222;
+ --text-sub: #666666;
+ --border-color: #E5E7EB;
+ /* 매우 연한 회색 라인 */
+ --hover-bg: #F9FAFB;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Pretendard', sans-serif;
+ font-size: 13px;
+ color: var(--text-main);
+ background-color: var(--bg-color);
+ display: flex;
+ min-height: 100vh;
+}
+
+/* Topbar */
+.topbar {
+ width: 100%;
+ background-color: #1E5149;
+ /* sample.png 탑바 다크 슬레이트 배경색 */
+ color: #FFFFFF;
+ padding: 0 1.5rem;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ z-index: 100;
+}
+
+.topbar-header {
+ margin-right: 2.5rem;
+}
+
+.topbar-header h2 {
+ font-size: 15px;
+ font-weight: 600;
+ letter-spacing: -0.3px;
+}
+
+.nav-list {
+ list-style: none;
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+
+.nav-item {
+ padding: 0 1rem;
+ height: 28px;
+ border-radius: 4px;
+ margin: 0 2px;
+ cursor: pointer;
+ transition: all 0.2s;
+ color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+}
+
+.nav-item:hover,
+.nav-item.active {
+ background-color: #E9EEED;
+ color: #1E5149;
+ font-weight: 500;
+}
+
+/* Main Content */
+.main-content {
+ margin-top: 36px;
+ flex: 1;
+ padding: 2rem 2.5rem;
+ width: 100%;
+ max-width: 1400px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+header {
+ margin-bottom: 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ padding-bottom: 0.8rem;
+ border-bottom: 1px solid var(--border-color);
+ /* 선 굵기와 색상 얇게 */
+}
+
+header h1 {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--primary-color);
+}
+
+.admin-info {
+ color: var(--text-sub);
+ font-size: 12px;
+}
+
+/* Multi-level Accordion (Minimalist/No Box Design) */
+.continent-group {
+ margin-bottom: 3rem;
+}
+
+.continent-header {
+ color: var(--text-main);
+ padding: 0.5rem 0;
+ font-size: 16px;
+ font-weight: 700;
+ cursor: pointer;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ border-bottom: 2px solid var(--text-main);
+ margin-bottom: 1rem;
+}
+
+.continent-body {
+ display: none;
+}
+
+.continent-group.active > .continent-body {
+ display: block;
+}
+
+.country-group {
+ margin-bottom: 2rem;
+ padding-bottom: 2rem;
+ border-bottom: 1px dashed var(--border-color); /* 국가 사이 구분선 */
+}
+
+.country-group:last-child {
+ margin-bottom: 1rem;
+ padding-bottom: 0;
+ border-bottom: none;
+}
+
+.country-header {
+ color: var(--primary-color);
+ padding: 0.5rem 0;
+ font-size: 14px;
+ font-weight: 700;
+ cursor: pointer;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin-bottom: 0.5rem;
+}
+
+.country-body {
+ display: none;
+ padding-left: 0.5rem; /* Slight indent instead of borders */
+}
+
+.country-group.active > .country-body {
+ display: block;
+}
+
+.toggle-icon {
+ font-size: 10px;
+ margin-left: 8px;
+ color: #999;
+}
+
+/* Accordion Styles (Projects - Row Based) */
+.accordion-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.accordion-item {
+ border-bottom: 1px solid var(--border-color);
+}
+
+.accordion-item:last-child {
+ border-bottom: none;
+}
+
+.accordion-header {
+ display: grid;
+ grid-template-columns: 2.5fr 1fr 1fr 1fr 1fr 2fr;
+ gap: 1rem;
+ padding: 1rem 0;
+ cursor: pointer;
+ align-items: center;
+ background-color: transparent;
+ transition: opacity 0.2s;
+}
+
+.accordion-item:hover .accordion-header {
+ opacity: 0.7;
+}
+
+.accordion-item.active .accordion-header {
+ /* No border-bottom or background change for active */
+}
+
+.accordion-header > div {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.header-label {
+ font-size: 11px;
+ color: var(--text-sub);
+ margin-bottom: 3px;
+ display: block;
+ font-weight: 400;
+}
+
+.header-value {
+ font-weight: 500;
+ font-size: 13px;
+ color: var(--text-main);
+}
+
+.accordion-body {
+ display: none;
+ padding: 1.5rem 0;
+ background-color: transparent;
+}
+
+.accordion-item.active .accordion-body {
+ display: block;
+}
+
+.detail-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 3rem;
+}
+
+.detail-section h4 {
+ margin-bottom: 1rem;
+ color: var(--text-main);
+ font-size: 13px;
+ font-weight: 600;
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 0.5rem;
+}
+
+/* Table Styles - Super Minimal Line Style */
+.data-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.data-table th,
+.data-table td {
+ padding: 8px 4px;
+ border-bottom: 1px solid var(--border-color);
+ text-align: left;
+}
+
+.data-table th {
+ color: var(--text-sub);
+ font-weight: 400;
+ font-size: 12px;
+}
+
+.data-table td {
+ font-size: 12px;
+ color: var(--text-main);
+}
+
+.data-table tr:last-child td {
+ border-bottom: none;
+}
+
+/* General Utilities */
+.badge {
+ background: #EEEEEE;
+ color: #555555;
+ padding: 2px 6px;
+ border-radius: 2px;
+ /* 라운드 거의 없앰 */
+ font-size: 11px;
+ font-weight: 500;
+}
+
+.status-up {
+ color: #D32F2F;
+ font-weight: 500;
+}
+
+.status-down {
+ color: #1976D2;
+ font-weight: 500;
+}
+
+/* Sync Button */
+.sync-btn {
+ background-color: var(--primary-color);
+ color: #FFFFFF;
+ border: 1px solid var(--primary-color);
+ padding: 6px 14px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.2s, opacity 0.2s;
+ margin-right: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.sync-btn:hover {
+ background-color: #153A34;
+}
+
+.sync-btn:disabled {
+ background-color: #A0B2AF;
+ border-color: #A0B2AF;
+ cursor: not-allowed;
+}
+
+/* Spinner */
+.spinner {
+ display: none;
+ width: 12px;
+ height: 12px;
+ border: 2px solid rgba(255,255,255,0.3);
+ border-radius: 50%;
+ border-top-color: #fff;
+ animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.sync-btn.loading .spinner {
+ display: inline-block;
+}
+
+/* --- Responsive Design --- */
+@media screen and (max-width: 1024px) {
+ .accordion-header {
+ grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1.5fr;
+ }
+}
+
+@media screen and (max-width: 768px) {
+ .topbar {
+ overflow-x: auto;
+ white-space: nowrap;
+ padding: 0 1rem;
+ }
+ /* 스크롤바 숨김 */
+ .topbar::-webkit-scrollbar {
+ display: none;
+ }
+ .main-content {
+ padding: 1.5rem 1rem;
+ }
+ header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ }
+ .detail-grid {
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+ }
+ /* 모바일에서 아코디언 헤더를 다단으로 배치 */
+ .accordion-header {
+ grid-template-columns: 1fr 1fr;
+ row-gap: 1rem;
+ }
+ /* 프로젝트 명과 최근 로그는 공간을 넓게 쓰도록 설정 */
+ .accordion-header > div:nth-child(1),
+ .accordion-header > div:nth-child(6) {
+ grid-column: span 2;
+ }
+ .continent-header {
+ font-size: 15px;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ /* 아주 작은 화면에서는 1열로 배치 */
+ .accordion-header {
+ grid-template-columns: 1fr;
+ }
+ .accordion-header > div {
+ grid-column: span 1 !important;
+ }
+ .topbar-header h2 {
+ font-size: 13px;
+ margin-right: 1rem;
+ }
+ .nav-item {
+ padding: 0 0.5rem;
+ font-size: 12px;
+ }
+}
\ No newline at end of file