Compare commits
5 Commits
9bb2ecd703
...
909340ff76
| Author | SHA1 | Date | |
|---|---|---|---|
| 909340ff76 | |||
| 130ea35d29 | |||
| b3bbde55fa | |||
| bfc477bedc | |||
| 604c29403f |
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"gitea": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": [
|
|
||||||
"@andrebuzeli/git-mcp@latest"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"GITEA_HOST": "https://gitea.hmac.kr",
|
|
||||||
"GITEA_ACCESS_TOKEN": "34a35034b9335b5129c8bfcd27e841d83f0aeaed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
README.md
35
README.md
@@ -3,38 +3,3 @@
|
|||||||
1. **언어 설정**: 영어로 생각하되, 모든 답변은 한국어로 작성한다. (일본어, 중국어는 절대 사용하지 않는다.)
|
1. **언어 설정**: 영어로 생각하되, 모든 답변은 한국어로 작성한다. (일본어, 중국어는 절대 사용하지 않는다.)
|
||||||
2. **수정 권한 제한**: 사용자가 명시적으로 지시한 사항 외에는 **절대 절대 절대** 코드를 임의로 수정하지 않는다.
|
2. **수정 권한 제한**: 사용자가 명시적으로 지시한 사항 외에는 **절대 절대 절대** 코드를 임의로 수정하지 않는다.
|
||||||
3. **로그 기록 철저**: 모달 오픈 여부, 수집 성공/실패 여부 등 진행 상황을 실시간 로그에 상세히 표시한다.
|
3. **로그 기록 철저**: 모달 오픈 여부, 수집 성공/실패 여부 등 진행 상황을 실시간 로그에 상세히 표시한다.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 디자인 가이드 (Design System)
|
|
||||||
|
|
||||||
이 프로젝트는 `tokens.json`에 정의된 디자인 시스템을 준수합니다.
|
|
||||||
|
|
||||||
### 1. 컬러 시스템 (Colors)
|
|
||||||
- **Primary**: `#1E5149` (primary-lv-6) - 브랜드 핵심 컬러
|
|
||||||
- **Background**: `#FFFFFF` (Light Default) / `#F9FAFB` (Light Muted)
|
|
||||||
- **Point Colors**:
|
|
||||||
- Blue: `#0D8DF2` (Info)
|
|
||||||
- Green: `#4DB251` (Success)
|
|
||||||
- Red: `#F21D0D` (Error)
|
|
||||||
- Yellow: `#FFBF00` (Warning)
|
|
||||||
- **Special**: `ai_color` (Purple-Blue Gradient) - AI 관련 요소 전용
|
|
||||||
|
|
||||||
### 2. 타이포그래피 (Typography)
|
|
||||||
- **Font Family**: `Pretendard`, `sans-serif`
|
|
||||||
- **Scale**:
|
|
||||||
- **H1**: 20px / ExtraBold (pretendard-0)
|
|
||||||
- **H2**: 16px / SemiBold (pretendard-1)
|
|
||||||
- **H3/H4**: 14px / SemiBold or Regular
|
|
||||||
- **Body/P**: 12px / Regular (pretendard-2)
|
|
||||||
|
|
||||||
### 3. 레이아웃 및 간격 (Dimensions)
|
|
||||||
- **Spacing Unit**: Base 4px (xs: 4px, sm: 8px, md: 16px, lg: 32px, xl: 64px)
|
|
||||||
- **Border Radius**: sm: 4px, lg: 8px, xl: 16px
|
|
||||||
- **Shadow**: `0 8px 24px rgba(0,0,0,0.16)` (box__drop-shadow)
|
|
||||||
|
|
||||||
### 4. 컴포넌트 규칙
|
|
||||||
- **Buttons**: `borderRadius.lg (8px)` 적용, Primary 배경색 사용
|
|
||||||
- **Cards**: `borderRadius.lg (8px)` 적용, Subtle Shadow 활용
|
|
||||||
- **Topbar**: Height 36px, `headercolor` 그라데이션 적용 가능
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
76
analyze.py
76
analyze.py
@@ -19,74 +19,66 @@ def analyze_file_content(filename: str):
|
|||||||
return {"error": "File not found"}
|
return {"error": "File not found"}
|
||||||
|
|
||||||
log_steps = []
|
log_steps = []
|
||||||
|
content_parts = []
|
||||||
|
|
||||||
# Layer 1: 제목 분석 (Quick)
|
# Layer 1: 제목 분석
|
||||||
log_steps.append("1. 레이어: 파일 제목(Title) 스캔 중...")
|
log_steps.append("1. 레이어: 파일 제목 분석 중...")
|
||||||
title_text = filename.lower().replace(" ", "")
|
title_text = filename.lower().replace(" ", "")
|
||||||
|
|
||||||
# Layer 2: 텍스트 추출 (Fast)
|
# Layer 2: 텍스트 추출
|
||||||
log_steps.append("2. 레이어: PDF 텍스트 엔진(Extraction) 가동...")
|
log_steps.append("2. 레이어: PDF 텍스트 엔진 가동...")
|
||||||
text_content = ""
|
|
||||||
try:
|
try:
|
||||||
if filename.lower().endswith(".pdf"):
|
if filename.lower().endswith(".pdf"):
|
||||||
reader = PdfReader(file_path)
|
reader = PdfReader(file_path)
|
||||||
for page in reader.pages[:5]: # 전체가 아닌 핵심 페이지 위주
|
text_extracted = ""
|
||||||
page_txt = page.extract_text()
|
for page in reader.pages[:5]:
|
||||||
if page_txt: text_content += page_txt + "\n"
|
text = page.extract_text()
|
||||||
text_content = unicodedata.normalize('NFC', text_content)
|
if text: text_extracted += text + "\n"
|
||||||
log_steps.append(f" - 텍스트 데이터 확보 완료 ({len(text_content)}자)")
|
if text_extracted.strip():
|
||||||
except:
|
norm = unicodedata.normalize('NFC', text_extracted)
|
||||||
log_steps.append(" - 텍스트 추출 실패")
|
content_parts.append(norm)
|
||||||
|
log_steps.append(f" - 텍스트 추출 성공 ({len(norm)}자)")
|
||||||
|
except: pass
|
||||||
|
|
||||||
# Layer 3: OCR 정밀 분석 (Deep)
|
# Layer 3: OCR 강제 실행
|
||||||
log_steps.append("3. 레이어: OCR 이미지 스캔(Vision) 강제 실행...")
|
log_steps.append("3. 레이어: OCR 이미지 스캔 강제 실행...")
|
||||||
ocr_content = ""
|
|
||||||
if OCR_AVAILABLE and os.path.exists(TESSERACT_PATH):
|
if OCR_AVAILABLE and os.path.exists(TESSERACT_PATH):
|
||||||
try:
|
try:
|
||||||
# 상징적인 첫 페이지 위주 OCR (성능과 정확도 타협)
|
|
||||||
images = convert_from_path(file_path, first_page=1, last_page=2, poppler_path=POPPLER_PATH)
|
images = convert_from_path(file_path, first_page=1, last_page=2, poppler_path=POPPLER_PATH)
|
||||||
for i, img in enumerate(images):
|
for i, img in enumerate(images):
|
||||||
page_ocr = pytesseract.image_to_string(img, lang='kor+eng')
|
page_text = pytesseract.image_to_string(img, lang='kor+eng')
|
||||||
ocr_content += unicodedata.normalize('NFC', page_ocr) + "\n"
|
norm_ocr = unicodedata.normalize('NFC', page_text)
|
||||||
log_steps.append(f" - OCR 스캔 완료 ({len(ocr_content)}자)")
|
content_parts.append(norm_ocr)
|
||||||
|
log_steps.append(f" - OCR 정밀 분석 완료")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_steps.append(f" - OCR 오류: {str(e)[:20]}")
|
log_steps.append(f" - OCR 오류: {str(e)[:20]}")
|
||||||
|
|
||||||
|
full_content = "\n".join(content_parts)
|
||||||
|
search_pool = (full_content + " " + filename).lower().replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
# 3중 레이어 데이터 통합
|
|
||||||
full_pool = (title_text + " | " + text_content + " | " + ocr_content).lower().replace(" ", "").replace("\n", "")
|
|
||||||
|
|
||||||
# 분석 초기화
|
|
||||||
result = {
|
result = {
|
||||||
"suggested_path": "분석실패",
|
"suggested_path": "분석실패",
|
||||||
"confidence": "Low",
|
"confidence": "Low",
|
||||||
"log_steps": log_steps,
|
"log_steps": log_steps,
|
||||||
"raw_text": f"--- TITLE ---\n{filename}\n\n--- TEXT ---\n{text_content[:1000]}\n\n--- OCR ---\n{ocr_content[:1000]}",
|
"raw_text": full_content,
|
||||||
"reason": "학습된 키워드 일치 항목 없음"
|
"reason": "일치하는 키워드가 본문 및 제목에서 발견되지 않음"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 최종 추천 로직 (합의 알고리즘)
|
# 정밀 분류 로직
|
||||||
is_eocheon = any(k in full_pool for k in ["어천", "공주"])
|
if "실정보고" in search_pool:
|
||||||
|
if any(k in search_pool for k in ["어천", "공주"]):
|
||||||
if "실정보고" in full_pool or "실정" in full_pool:
|
if "품질" in search_pool:
|
||||||
if is_eocheon:
|
|
||||||
if "품질" in full_pool:
|
|
||||||
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 품질관리"
|
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 품질관리"
|
||||||
result["reason"] = "3중 레이어 분석: 실정보고+어천공주+품질관리 키워드 통합 검출"
|
elif any(k in search_pool for k in ["토지", "임대", "가설사무실"]):
|
||||||
elif any(k in full_pool for k in ["토지", "임대"]):
|
|
||||||
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 기타"
|
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 기타"
|
||||||
result["reason"] = "3중 레이어 분석: 토지임대 관련 실정보고(어천-공주) 확인"
|
|
||||||
else:
|
else:
|
||||||
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 기타"
|
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 기타"
|
||||||
result["reason"] = "3중 레이어 분석: 실정보고(어천-공주) 문서 판정"
|
|
||||||
result["confidence"] = "100%"
|
result["confidence"] = "100%"
|
||||||
else:
|
result["reason"] = "3중 레이어 합의를 통해 실정보고(어천-공주) 핵심 키워드 완벽 일치"
|
||||||
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 기타" # 폴백
|
|
||||||
result["confidence"] = "80%"
|
elif "품질" in search_pool:
|
||||||
result["reason"] = "실정보고 키워드는 발견되었으나 프로젝트명 교차 검증 실패 (기본값 제안)"
|
|
||||||
|
|
||||||
elif "품질" in full_pool:
|
|
||||||
result["suggested_path"] = "공사관리 > 품질 관리 > 품질시험계획서"
|
result["suggested_path"] = "공사관리 > 품질 관리 > 품질시험계획서"
|
||||||
result["confidence"] = "90%"
|
result["confidence"] = "90%"
|
||||||
result["reason"] = "텍스트/OCR 레이어에서 품질 관리 지표 다수 식별"
|
result["reason"] = "품질 관리 지표 다수 식별"
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from fastapi.responses import StreamingResponse, FileResponse
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from playwright.async_api import async_playwright
|
from playwright.async_api import async_playwright
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from analyze import analyze_file_content
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -26,37 +25,10 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/dashboard")
|
@app.get("/")
|
||||||
async def get_dashboard():
|
async def get_dashboard():
|
||||||
return FileResponse("dashboard.html")
|
return FileResponse("dashboard.html")
|
||||||
|
|
||||||
@app.get("/mailTest")
|
|
||||||
async def get_mail_test():
|
|
||||||
return FileResponse("mailTest.html")
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
@app.get("/analyze-file")
|
|
||||||
async def analyze_file(filename: str):
|
|
||||||
return analyze_file_content(filename)
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
return FileResponse("index.html")
|
|
||||||
|
|
||||||
@app.get("/sync")
|
@app.get("/sync")
|
||||||
async def sync_data():
|
async def sync_data():
|
||||||
async def event_generator():
|
async def event_generator():
|
||||||
@@ -125,18 +97,18 @@ async def sync_data():
|
|||||||
|
|
||||||
modal_sel = "article.archive-modal"
|
modal_sel = "article.archive-modal"
|
||||||
if await page.locator(modal_sel).is_visible():
|
if await page.locator(modal_sel).is_visible():
|
||||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [로그] 모달 발견. 데이터 로딩 대기...'})}\n\n"
|
yield f"data: {json.dumps({'type': 'log', 'message': ' - [로그] 모달 발견. 데이터 추출 중...'})}\n\n"
|
||||||
# .log-body 내부의 데이터만 타겟팅하도록 수정
|
# 사용자 제공 정밀 셀렉터 기반 추출
|
||||||
date_sel = "article.archive-modal .log-body .date .text"
|
date_sel = "body > article.archive-modal > div > div > div.modal-body > div.log-wrap > div.log-item-wrap.log-body.scrollbar.scroll-container > div.date > div.text"
|
||||||
user_sel = "article.archive-modal .log-body .user .text"
|
user_sel = "body > article.archive-modal > div > div > div.modal-body > div.log-wrap > div.log-item-wrap.log-body.scrollbar.scroll-container > div.user > div.text"
|
||||||
act_sel = "article.archive-modal .log-body .activity .text"
|
act_sel = "body > article.archive-modal > div > div > div.modal-body > div.log-wrap > div.log-item-wrap.log-body.scrollbar.scroll-container > div.activity > div.text"
|
||||||
|
|
||||||
# 데이터가 나타날 때까지 최대 15초 대기
|
# 데이터가 나타날 때까지 반복 대기
|
||||||
success_log = False
|
success_log = False
|
||||||
for _ in range(15):
|
for _ in range(10):
|
||||||
if await page.locator(date_sel).count() > 0:
|
if await page.locator(date_sel).count() > 0:
|
||||||
raw_date = (await page.locator(date_sel).first.inner_text()).strip()
|
raw_date = (await page.locator(date_sel).first.inner_text()).strip()
|
||||||
if raw_date:
|
if raw_date and "활동시간" not in raw_date:
|
||||||
success_log = True
|
success_log = True
|
||||||
break
|
break
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
@@ -176,33 +148,32 @@ async def sync_data():
|
|||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
if popup_page:
|
if popup_page:
|
||||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 창 발견. 데이터 로딩 대기 (최대 30초)...'})}\n\n"
|
yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 창 발견. 데이터 로딩 대기 (최대 80초)...'})}\n\n"
|
||||||
# 사용자 제공 정밀 선택자 적용 (nth-child(3)가 실제 데이터)
|
target_selector = "#composition-list h6"
|
||||||
target_selector = "#composition-list h6:nth-child(3)"
|
|
||||||
success_comp = False
|
success_comp = False
|
||||||
|
|
||||||
# 최대 30초간 데이터가 나타날 때까지 대기
|
# 최대 80초간 끝까지 대기
|
||||||
for _ in range(30):
|
for _ in range(80):
|
||||||
h6_count = await popup_page.locator(target_selector).count()
|
h6_count = await popup_page.locator(target_selector).count()
|
||||||
if h6_count > 0:
|
if h6_count > 5: # 일정 개수 이상의 목록이 나타나면 로딩 시작으로 간주
|
||||||
success_comp = True
|
success_comp = True
|
||||||
break
|
break
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
if success_comp:
|
if success_comp:
|
||||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 데이터 감지됨. 최종 렌더링 대기...'})}\n\n"
|
yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 데이터 감지됨. 15초간 최종 렌더링 대기...'})}\n\n"
|
||||||
await asyncio.sleep(10) # 렌더링 안정화를 위한 대기
|
await asyncio.sleep(15) # 완전한 로딩을 위한 강제 대기
|
||||||
|
|
||||||
# 모든 h6:nth-child(3) 요소를 순회하며 숫자 합산
|
# 유연한 데이터 수집
|
||||||
locators_h6 = popup_page.locator(target_selector)
|
locators_h6 = popup_page.locator(target_selector)
|
||||||
h6_count = await locators_h6.count()
|
h6_count = await locators_h6.count()
|
||||||
current_total = 0
|
current_total = 0
|
||||||
for j in range(h6_count):
|
for j in range(h6_count):
|
||||||
text = (await locators_h6.nth(j).inner_text()).strip()
|
text = (await locators_h6.nth(j).inner_text()).strip()
|
||||||
# 텍스트 내에서 숫자만 추출 (여러 줄일 경우 마지막 줄 기준)
|
|
||||||
nums = re.findall(r'\d+', text.split('\n')[-1])
|
nums = re.findall(r'\d+', text.split('\n')[-1])
|
||||||
if nums:
|
if nums:
|
||||||
current_total += int(nums[0])
|
val = int(nums[0])
|
||||||
|
if val < 5000: current_total += val
|
||||||
|
|
||||||
file_count = current_total
|
file_count = current_total
|
||||||
yield f"data: {json.dumps({'type': 'log', 'message': f' - [구성] 성공 ({file_count}개)'})}\n\n"
|
yield f"data: {json.dumps({'type': 'log', 'message': f' - [구성] 성공 ({file_count}개)'})}\n\n"
|
||||||
|
|||||||
129
dashboard.html
129
dashboard.html
@@ -1,37 +1,32 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Project Master Overseas 관리자</title>
|
<title>Project Master Overseas 관리자</title>
|
||||||
<link rel="stylesheet" as="style" crossorigin
|
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
||||||
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
|
||||||
<link rel="stylesheet" href="style/style.css">
|
<link rel="stylesheet" href="style/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="topbar">
|
<nav class="topbar">
|
||||||
<div class="topbar-header">
|
<div class="topbar-header">
|
||||||
<a href="/">
|
<h2>Project Master Overseas</h2>
|
||||||
<h2>Project Master Test</h2>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<li class="nav-item active" onclick="location.href='/dashboard'">대시보드</li>
|
<li class="nav-item active">대시보드</li>
|
||||||
<li class="nav-item" onclick="alert('준비 중입니다.')">문의사항</li>
|
<li class="nav-item">문의사항 <span class="badge" style="background:#FFFFFF;color:var(--primary-color); border-radius:10px; font-weight: bold; padding: 2px 5px;">12</span></li>
|
||||||
<li class="nav-item" onclick="alert('준비 중입니다.')">로그관리</li>
|
<li class="nav-item">로그관리</li>
|
||||||
<li class="nav-item" onclick="alert('준비 중입니다.')">파일관리</li>
|
<li class="nav-item">파일관리</li>
|
||||||
<li class="nav-item" onclick="alert('준비 중입니다.')">인원관리</li>
|
<li class="nav-item">인원관리</li>
|
||||||
<li class="nav-item" onclick="alert('준비 중입니다.')">공지사항</li>
|
<li class="nav-item">공지사항</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<header>
|
<header>
|
||||||
<div style="display:flex; align-items:center;">
|
<div style="display:flex; align-items:center;">
|
||||||
<h1>프로젝트 현황</h1>
|
<h1>대시보드 현황</h1>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; align-items:center;">
|
<div style="display:flex; align-items:center;">
|
||||||
<button id="syncBtn" class="sync-btn" onclick="syncData()">
|
<button id="syncBtn" class="sync-btn" onclick="syncData()">
|
||||||
@@ -43,11 +38,8 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 실시간 로그 콘솔 추가 -->
|
<!-- 실시간 로그 콘솔 추가 -->
|
||||||
<div id="logConsole"
|
<div id="logConsole" style="display:none; background:#000; color:#0f0; font-family:monospace; padding:15px; margin-bottom:20px; border-radius:4px; max-height:200px; overflow-y:auto; font-size:12px; line-height:1.5;">
|
||||||
style="display:none; background:#000; color:#0f0; font-family:monospace; padding:15px; margin-bottom:20px; border-radius:4px; max-height:200px; overflow-y:auto; font-size:12px; line-height:1.5;">
|
<div style="color:#fff; border-bottom:1px solid #333; margin-bottom:10px; padding-bottom:5px; font-weight:bold;">실시간 수집 로그 [PM Overseas]</div>
|
||||||
<div
|
|
||||||
style="color:#fff; border-bottom:1px solid #333; margin-bottom:10px; padding-bottom:5px; font-weight:bold;">
|
|
||||||
실시간 수집 로그 [PM Overseas]</div>
|
|
||||||
<div id="logBody"></div>
|
<div id="logBody"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -100,8 +92,8 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const continentMap = {
|
const continentMap = {
|
||||||
"라오스": "아시아", "미얀마": "아시아", "베트남": "아시아", "사우디아라비아": "아시아",
|
"라오스": "아시아", "미얀마": "아시아", "베트남": "아시아", "사우디아라비아": "아시아",
|
||||||
"우즈베키스탄": "아시아", "이라크": "아시아", "캄보디아": "아시아",
|
"우즈베키스탄": "아시아", "이라크": "아시아", "캄보디아": "아시아",
|
||||||
"키르기스스탄": "아시아", "파키스탄": "아시아", "필리핀": "아시아",
|
"키르기스스탄": "아시아", "파키스탄": "아시아", "필리핀": "아시아",
|
||||||
"아르헨티나": "아메리카", "온두라스": "아메리카", "볼리비아": "아메리카", "콜롬비아": "아메리카",
|
"아르헨티나": "아메리카", "온두라스": "아메리카", "볼리비아": "아메리카", "콜롬비아": "아메리카",
|
||||||
"파라과이": "아메리카", "페루": "아메리카", "엘살바도르": "아메리카",
|
"파라과이": "아메리카", "페루": "아메리카", "엘살바도르": "아메리카",
|
||||||
@@ -124,7 +116,7 @@
|
|||||||
const projectName = item[0];
|
const projectName = item[0];
|
||||||
let continent = "";
|
let continent = "";
|
||||||
let country = "";
|
let country = "";
|
||||||
|
|
||||||
if (projectName.endsWith("사무소")) {
|
if (projectName.endsWith("사무소")) {
|
||||||
continent = "지사";
|
continent = "지사";
|
||||||
country = projectName.split(" ")[0];
|
country = projectName.split(" ")[0];
|
||||||
@@ -135,10 +127,10 @@
|
|||||||
country = projectName.split(" ")[0];
|
country = projectName.split(" ")[0];
|
||||||
continent = continentMap[country] || "기타";
|
continent = continentMap[country] || "기타";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupedData[continent]) groupedData[continent] = {};
|
if (!groupedData[continent]) groupedData[continent] = {};
|
||||||
if (!groupedData[continent][country]) groupedData[continent][country] = [];
|
if (!groupedData[continent][country]) groupedData[continent][country] = [];
|
||||||
|
|
||||||
groupedData[continent][country].push({ item, index });
|
groupedData[continent][country].push({ item, index });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,7 +141,7 @@
|
|||||||
sortedContinents.forEach(continent => {
|
sortedContinents.forEach(continent => {
|
||||||
const continentGroup = document.createElement('div');
|
const continentGroup = document.createElement('div');
|
||||||
continentGroup.className = 'continent-group';
|
continentGroup.className = 'continent-group';
|
||||||
|
|
||||||
let continentHtml = `
|
let continentHtml = `
|
||||||
<div class="continent-header" onclick="toggleGroup(this)">
|
<div class="continent-header" onclick="toggleGroup(this)">
|
||||||
<span>${continent}</span>
|
<span>${continent}</span>
|
||||||
@@ -169,40 +161,48 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="country-body">
|
<div class="country-body">
|
||||||
<div class="accordion-container">
|
<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]));
|
const sortedProjects = groupedData[continent][country].sort((a, b) => a.item[0].localeCompare(b.item[0]));
|
||||||
|
|
||||||
sortedProjects.forEach(({ item, index }) => {
|
sortedProjects.forEach(({item, index}) => {
|
||||||
const projectName = item[0];
|
const projectName = item[0];
|
||||||
const dept = item[1];
|
const dept = item[1];
|
||||||
const admin = item[2];
|
const admin = item[2];
|
||||||
const recentLogRaw = item[3];
|
const recentLogRaw = item[3];
|
||||||
const fileCount = item[4];
|
const fileCount = item[4];
|
||||||
|
const personnelCount = Math.floor(Math.random()*15)+3; // 인원은 시트에 없으므로 임의 할당 유지
|
||||||
|
|
||||||
const recentLog = recentLogRaw === "X" ? "기록 없음" : recentLogRaw;
|
const recentLog = recentLogRaw === "X" ? "기록 없음" : recentLogRaw;
|
||||||
const logTime = recentLog !== "기록 없음" ? recentLog.split(',')[0] : "기록 없음";
|
const logTime = recentLog !== "기록 없음" ? recentLog.split(',')[0] : "기록 없음";
|
||||||
|
|
||||||
// 상태 클래스 결정
|
|
||||||
let statusClass = "";
|
|
||||||
if (fileCount === 0) statusClass = "status-error";
|
|
||||||
else if (recentLog === "기록 없음") statusClass = "status-warning";
|
|
||||||
|
|
||||||
continentHtml += `
|
continentHtml += `
|
||||||
<div class="accordion-item ${statusClass}">
|
<div class="accordion-item">
|
||||||
<div class="accordion-header" onclick="toggleAccordion(this)">
|
<div class="accordion-header" onclick="toggleAccordion(this)">
|
||||||
<div class="repo-title" title="${projectName}">${projectName}</div>
|
<div>
|
||||||
<div class="repo-dept">${dept}</div>
|
<span class="header-label">프로젝트 명</span>
|
||||||
<div class="repo-admin">${admin}</div>
|
<span class="header-value" title="${projectName}">${projectName}</span>
|
||||||
<div class="repo-files ${fileCount === 0 ? 'warning-text' : ''}">${fileCount}</div>
|
</div>
|
||||||
<div class="repo-log ${recentLog === '기록 없음' ? 'warning-text' : ''}" title="${recentLog}">${recentLog}</div>
|
<div>
|
||||||
|
<span class="header-label">담당부서</span>
|
||||||
|
<span class="header-value">${dept}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="header-label">관리자</span>
|
||||||
|
<span class="header-value">${admin}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="header-label">파일 수</span>
|
||||||
|
<span class="header-value">${fileCount}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="header-label">인원</span>
|
||||||
|
<span class="header-value">${personnelCount}명</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="header-label">최근 로그</span>
|
||||||
|
<span class="header-value" style="color:var(--text-sub); font-size:11px;" title="${recentLog}">${recentLog}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
@@ -245,36 +245,44 @@
|
|||||||
continentHtml += `
|
continentHtml += `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
continentGroup.innerHTML = continentHtml;
|
continentGroup.innerHTML = continentHtml;
|
||||||
container.appendChild(continentGroup);
|
container.appendChild(continentGroup);
|
||||||
});
|
});
|
||||||
|
|
||||||
const allContinents = container.querySelectorAll('.continent-group');
|
const allContinents = container.querySelectorAll('.continent-group');
|
||||||
allContinents.forEach(continent => {
|
allContinents.forEach(continent => {
|
||||||
continent.classList.add('active');
|
continent.classList.add('active');
|
||||||
|
continent.querySelector('.continent-header .toggle-icon').textContent = '▲';
|
||||||
});
|
});
|
||||||
|
|
||||||
const allCountries = container.querySelectorAll('.country-group');
|
const allCountries = container.querySelectorAll('.country-group');
|
||||||
allCountries.forEach(country => {
|
allCountries.forEach(country => {
|
||||||
country.classList.add('active');
|
country.classList.add('active');
|
||||||
|
country.querySelector('.country-header .toggle-icon').textContent = '▲';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleGroup(header) {
|
function toggleGroup(header) {
|
||||||
const group = header.parentElement;
|
const group = header.parentElement;
|
||||||
|
const icon = header.querySelector('.toggle-icon');
|
||||||
group.classList.toggle('active');
|
group.classList.toggle('active');
|
||||||
|
if (group.classList.contains('active')) {
|
||||||
|
icon.textContent = '▲';
|
||||||
|
} else {
|
||||||
|
icon.textContent = '▼';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAccordion(header) {
|
function toggleAccordion(header) {
|
||||||
const item = header.parentElement;
|
const item = header.parentElement;
|
||||||
const container = item.parentElement;
|
const container = item.parentElement;
|
||||||
|
|
||||||
const allItems = container.querySelectorAll('.accordion-item');
|
const allItems = container.querySelectorAll('.accordion-item');
|
||||||
allItems.forEach(el => {
|
allItems.forEach(el => {
|
||||||
if (el !== item) el.classList.remove('active');
|
if(el !== item) el.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
item.classList.toggle('active');
|
item.classList.toggle('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,11 +290,11 @@
|
|||||||
const btn = document.getElementById('syncBtn');
|
const btn = document.getElementById('syncBtn');
|
||||||
const logConsole = document.getElementById('logConsole');
|
const logConsole = document.getElementById('logConsole');
|
||||||
const logBody = document.getElementById('logBody');
|
const logBody = document.getElementById('logBody');
|
||||||
|
|
||||||
btn.classList.add('loading');
|
btn.classList.add('loading');
|
||||||
btn.innerHTML = `<span class="spinner"></span> 동기화 중 (진행 상황 확인 중...)`;
|
btn.innerHTML = `<span class="spinner"></span> 동기화 중 (진행 상황 확인 중...)`;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
logConsole.style.display = 'block';
|
logConsole.style.display = 'block';
|
||||||
logBody.innerHTML = ''; // 이전 로그 삭제
|
logBody.innerHTML = ''; // 이전 로그 삭제
|
||||||
|
|
||||||
@@ -299,7 +307,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/sync`);
|
const response = await fetch(`/sync`);
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
@@ -309,21 +317,21 @@
|
|||||||
|
|
||||||
const chunk = decoder.decode(value);
|
const chunk = decoder.decode(value);
|
||||||
const lines = chunk.split('\n');
|
const lines = chunk.split('\n');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
const payload = JSON.parse(line.substring(6));
|
const payload = JSON.parse(line.substring(6));
|
||||||
|
|
||||||
if (payload.type === 'log') {
|
if (payload.type === 'log') {
|
||||||
addLog(payload.message);
|
addLog(payload.message);
|
||||||
} else if (payload.type === 'done') {
|
} else if (payload.type === 'done') {
|
||||||
const newData = payload.data;
|
const newData = payload.data;
|
||||||
newData.forEach(scrapedItem => {
|
newData.forEach(scrapedItem => {
|
||||||
const target = rawData.find(item =>
|
const target = rawData.find(item =>
|
||||||
item[0].replace(/\s/g, '').includes(scrapedItem.projectName.replace(/\s/g, '')) ||
|
item[0].replace(/\s/g,'').includes(scrapedItem.projectName.replace(/\s/g,'')) ||
|
||||||
scrapedItem.projectName.replace(/\s/g, '').includes(item[0].replace(/\s/g, ''))
|
scrapedItem.projectName.replace(/\s/g,'').includes(item[0].replace(/\s/g,''))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
// 기존 데이터 유지 마커 확인
|
// 기존 데이터 유지 마커 확인
|
||||||
if (scrapedItem.recentLog !== "기존데이터유지") {
|
if (scrapedItem.recentLog !== "기존데이터유지") {
|
||||||
@@ -332,7 +340,7 @@
|
|||||||
target[4] = scrapedItem.fileCount;
|
target[4] = scrapedItem.fileCount;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('projectAccordion').innerHTML = '';
|
document.getElementById('projectAccordion').innerHTML = '';
|
||||||
init();
|
init();
|
||||||
addLog(">>> 모든 동기화 작업이 완료되었습니다!");
|
addLog(">>> 모든 동기화 작업이 완료되었습니다!");
|
||||||
@@ -356,5 +364,4 @@
|
|||||||
init();
|
init();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
266
debug_modal.html
266
debug_modal.html
@@ -1,266 +0,0 @@
|
|||||||
|
|
||||||
<div class="wrap">
|
|
||||||
<article class="log-filter" style="display: flex;">
|
|
||||||
<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="" style="">
|
|
||||||
</div>
|
|
||||||
<div class="log-date-wrap">
|
|
||||||
<span class="category">종료</span>
|
|
||||||
<input type="date" value="" style="">
|
|
||||||
</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="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>파일 업로드</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="renameTarget" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>이름 변경</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="removeTarget" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>삭제</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="downloadTarget" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>다운로드</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="relocateTarget" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>파일 이동</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="createFolder" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>새 폴더 생성</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="setDataPermission_folder" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>폴더 권한 설정</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="convertPdf" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>PDF 변환</span>
|
|
||||||
</label>
|
|
||||||
<span class="category">유저관련</span>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="editAuthor" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>작성자 변경</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="deletePermission" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>권한 삭제</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="addPermission" checked="" style="">
|
|
||||||
<span class="--checkbox"></span>
|
|
||||||
<span>권한 추가</span>
|
|
||||||
</label>
|
|
||||||
<span class="category">기타</span>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value="summarizeAI" checked="" style="">
|
|
||||||
<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 class="btn dev-menu-btn permission-min-dev">
|
|
||||||
<div class="text">개발자 메뉴</div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
<!-- <div class="right-wrap">
|
|
||||||
<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>
|
|
||||||
</ul>
|
|
||||||
<button class="project-step" id="project-step-btn">
|
|
||||||
<h5 class="project-step__label --step__active">진행</h5>
|
|
||||||
<i class="project-step__icon"></i>
|
|
||||||
</button>
|
|
||||||
<h5 class="" id="project-step-capsule">진행</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 class="project-manager-title">프로젝트 관리자</div>
|
|
||||||
<div class="project-manager-name"></div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
<div class="close"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" style="">
|
|
||||||
<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" style="display: flex; flex-direction: column;">
|
|
||||||
<div class="project-name-wrap" style="display: flex; gap:1rem;">
|
|
||||||
<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" style="display: flex;"> 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" style="display: flex; gap:1rem;">
|
|
||||||
<div class="project-manager-title">프로젝트 관리자</div>
|
|
||||||
<div class="project-manager-name">방노성 전무이사</div>
|
|
||||||
</div>
|
|
||||||
<div class="project-location-wrap" style="display: flex; gap:1rem;">
|
|
||||||
<div class="project-location-title">프로젝트 위치</div>
|
|
||||||
<div class="project-location-lat"><div class="project-location-lat">위도 18.068579</div></div>
|
|
||||||
<div class="project-location-lon"><div class="project-location-lon">경도 102.65966</div></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" _echarts_instance_="ec_1772068581031" 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="input-wrap"></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 class="selected-user-item-wrap">
|
|
||||||
<div class="text">선택 결과</div>
|
|
||||||
<div class="selected-user-item"></div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
<div class="btn-wrap" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
BIN
debug_modal.png
BIN
debug_modal.png
Binary file not shown.
|
Before Width: | Height: | Size: 289 KiB |
131
index.html
131
index.html
@@ -1,142 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Project Master Portal</title>
|
<title>Project Master Portal</title>
|
||||||
<link rel="stylesheet" as="style" crossorigin
|
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
||||||
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
|
||||||
<link rel="stylesheet" href="style/style.css">
|
<link rel="stylesheet" href="style/style.css">
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
display: block;
|
|
||||||
/* style.css의 flex: min-height 100vh 해제 */
|
|
||||||
background-color: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.portal-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: calc(100vh - 36px);
|
|
||||||
margin-top: 36px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portal-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: white;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 40px;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text-main);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portal-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.portal-card .icon {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
background-color: var(--hover-bg);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 32px;
|
|
||||||
color: var(--primary-color);
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portal-card:hover .icon {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portal-card h2 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portal-card p {
|
|
||||||
color: var(--text-sub);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
.button-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="topbar">
|
<nav class="topbar">
|
||||||
<div class="topbar-header">
|
<div class="topbar-header"><a href="/"><h2>Project Master Test</h2></a></div>
|
||||||
<a href="/"><h2>Project Master Test</h2></a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="portal-container">
|
<div class="portal-container">
|
||||||
<div class="portal-header">
|
<div class="button-grid" style="display:grid; grid-template-columns:repeat(2, 1fr); gap:30px;">
|
||||||
<h1>Project Master 테스트</h1>
|
<a href="/dashboard" class="portal-card"><div class="icon">📊</div><h2>대시보드</h2></a>
|
||||||
<p>원하시는 서비스에 접속하려면 아래 버튼을 클릭하세요.</p>
|
<a href="/mailTest" class="portal-card"><div class="icon">✉️</div><h2>메일 테스트</h2></a>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-grid">
|
|
||||||
<a href="/dashboard" class="portal-card">
|
|
||||||
<div class="icon">📊</div>
|
|
||||||
<h2>관리자 페이지 테스트</h2>
|
|
||||||
<p>관리자 페이지 테스트 입니다.</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/mailTest" class="portal-card">
|
|
||||||
<div class="icon">✉️</div>
|
|
||||||
<h2>메일 테스트</h2>
|
|
||||||
<p>메일 기능 테스트 페이지입니다.</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -9,9 +9,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="topbar">
|
<nav class="topbar">
|
||||||
<div class="topbar-header">
|
<div class="topbar-header"><a href="/"><h2>Project Master Test</h2></a></div>
|
||||||
<a href="/"><h2>Project Master Test</h2></a>
|
|
||||||
</div>
|
|
||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<li class="nav-item" onclick="location.href='/dashboard'">대시보드</li>
|
<li class="nav-item" onclick="location.href='/dashboard'">대시보드</li>
|
||||||
<li class="nav-item active" onclick="location.href='/mailTest'">메일관리</li>
|
<li class="nav-item active" onclick="location.href='/mailTest'">메일관리</li>
|
||||||
@@ -27,9 +25,9 @@
|
|||||||
<option>베트남 푸옥호아 발전소</option>
|
<option>베트남 푸옥호아 발전소</option>
|
||||||
</select>
|
</select>
|
||||||
<ul class="folder-list">
|
<ul class="folder-list">
|
||||||
<li class="folder-item active" style="list-style:none;"><span>📥 수신함</span><span>12</span></li>
|
<li class="folder-item active"><span>📥 수신함</span><span>12</span></li>
|
||||||
<li class="folder-item" style="list-style:none;"><span>📤 발신함</span></li>
|
<li class="folder-item"><span>📤 발신함</span></li>
|
||||||
<li class="folder-item" style="list-style:none;"><span>📁 중요메일</span></li>
|
<li class="folder-item"><span>📁 중요메일</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -61,8 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mail-body">
|
<div class="mail-body">
|
||||||
안녕하세요. 이태훈 선임연구원님.<br><br>
|
안녕하세요. 이태훈 선임연구원님.<br><br>
|
||||||
라오스 ITTC 관개 교육센터 착공식과 관련하여 정부 측 인사의 일정을 반영한 최종 공문을 송부합니다.<br>
|
라오스 ITTC 관개 교육센터 착공식과 관련하여 정부 측 인사의 일정을 반영한 최종 공문을 송부합니다.
|
||||||
첨부파일의 세부 계획안을 검토하신 후, 프로젝트 대시보드의 '과업계획' 탭에 업로드 및 공유 부탁드립니다.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="attachment-area">
|
<div class="attachment-area">
|
||||||
@@ -83,29 +80,23 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentFiles = [];
|
let currentFiles = [];
|
||||||
|
|
||||||
async function loadAttachments() {
|
async function loadAttachments() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/attachments');
|
const res = await fetch('/attachments');
|
||||||
currentFiles = await res.json();
|
currentFiles = await res.json();
|
||||||
renderFiles();
|
renderFiles();
|
||||||
} catch (e) {
|
} catch (e) { console.error(e); }
|
||||||
console.error("Failed to load attachments:", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFiles() {
|
function renderFiles() {
|
||||||
const isAiActive = document.getElementById('aiToggle').checked;
|
const isAiActive = document.getElementById('aiToggle').checked;
|
||||||
const container = document.getElementById('attachmentList');
|
const container = document.getElementById('attachmentList');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
currentFiles.forEach((file, index) => {
|
currentFiles.forEach((file, index) => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'attachment-item-wrap';
|
item.className = 'attachment-item-wrap';
|
||||||
item.style.marginBottom = "8px";
|
item.style.marginBottom = "8px";
|
||||||
|
|
||||||
const btnAiClass = isAiActive ? 'btn-ai' : 'btn-normal';
|
const btnAiClass = isAiActive ? 'btn-ai' : 'btn-normal';
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="attachment-item">
|
<div class="attachment-item">
|
||||||
<div class="file-info">
|
<div class="file-info">
|
||||||
@@ -114,16 +105,14 @@
|
|||||||
<div style="font-size:12px; font-weight:700;">${file.name}</div>
|
<div style="font-size:12px; font-weight:700;">${file.name}</div>
|
||||||
<div style="font-size:10px; color:var(--text-sub);">${file.size}</div>
|
<div style="font-size:10px; color:var(--text-sub);">${file.size}</div>
|
||||||
</div>
|
</div>
|
||||||
<span id="recommend-${index}" class="ai-recommend" style="display:none;">추천 위치 탐색 중...</span>
|
<span id="recommend-${index}" class="ai-recommend" style="display:none;">분석 대기 중...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn-upload ${btnAiClass}" onclick="startAnalysis(${index})">AI 분석</button>
|
<button class="btn-upload ${btnAiClass}" onclick="startAnalysis(${index})">AI 분석</button>
|
||||||
<button class="btn-upload btn-normal" onclick="confirmUpload(${index})">파일 업로드</button>
|
<button class="btn-upload btn-normal" onclick="confirmUpload(${index})">파일 업로드</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="log-area-${index}" class="file-log-area">
|
<div id="log-area-${index}" class="file-log-area"><div id="log-content-${index}"></div></div>
|
||||||
<div id="log-content-${index}"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
container.appendChild(item);
|
container.appendChild(item);
|
||||||
});
|
});
|
||||||
@@ -134,66 +123,41 @@
|
|||||||
const logArea = document.getElementById(`log-area-${index}`);
|
const logArea = document.getElementById(`log-area-${index}`);
|
||||||
const logContent = document.getElementById(`log-content-${index}`);
|
const logContent = document.getElementById(`log-content-${index}`);
|
||||||
const recLabel = document.getElementById(`recommend-${index}`);
|
const recLabel = document.getElementById(`recommend-${index}`);
|
||||||
|
|
||||||
logArea.classList.add('active');
|
logArea.classList.add('active');
|
||||||
logContent.innerHTML = '<div class="log-line log-info">>>> 3중 레이어 AI 분석 엔진 가동...</div>';
|
logContent.innerHTML = '<div class="log-line log-info">>>> 3중 레이어 AI 엔진 가동...</div>';
|
||||||
recLabel.style.display = 'inline-block';
|
recLabel.style.display = 'inline-block';
|
||||||
recLabel.innerText = '분석 중...';
|
recLabel.innerText = '분석 중...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/analyze-file?filename=${encodeURIComponent(file.name)}`);
|
const res = await fetch(`/analyze-file?filename=${encodeURIComponent(file.name)}`);
|
||||||
const analysis = await res.json();
|
const analysis = await res.json();
|
||||||
|
|
||||||
analysis.log_steps.forEach(step => {
|
analysis.log_steps.forEach(step => {
|
||||||
const line = document.createElement('div');
|
const line = document.createElement('div');
|
||||||
line.className = 'log-line';
|
line.className = 'log-line';
|
||||||
line.innerText = " " + step;
|
line.innerText = " " + step;
|
||||||
logContent.appendChild(line);
|
logContent.appendChild(line);
|
||||||
});
|
});
|
||||||
|
const resLine = document.createElement('div');
|
||||||
const resultLine = document.createElement('div');
|
resLine.className = 'log-line log-success';
|
||||||
resultLine.className = 'log-line log-success';
|
resLine.style.marginTop = "8px";
|
||||||
resultLine.style.marginTop = "8px";
|
resLine.innerHTML = `[결과] ${analysis.suggested_path}<br>└ 근거: ${analysis.reason}`;
|
||||||
resultLine.innerHTML = `[결과] ${analysis.suggested_path}<br>└ ${analysis.reason}`;
|
logContent.appendChild(resLine);
|
||||||
logContent.appendChild(resultLine);
|
|
||||||
|
|
||||||
// 원본 보기 추가
|
|
||||||
const details = document.createElement('details');
|
const details = document.createElement('details');
|
||||||
details.style.marginTop = "5px";
|
details.innerHTML = `<summary style="color:#da8cf1; cursor:pointer; font-size:10px; margin-top:5px;">[추출 원본 데이터 보기]</summary><div style="color:#a0aec0; padding:8px; background:#2d3748; margin-top:5px; white-space:pre-wrap; max-height:150px; overflow-y:auto; border-radius:4px; font-size:10px;">${analysis.raw_text}</div>`;
|
||||||
details.innerHTML = `
|
|
||||||
<summary style="color:#da8cf1; cursor:pointer; font-size:10px;">[추출 원본 데이터 확인]</summary>
|
|
||||||
<div style="color:#a0aec0; padding:8px; background:#2d3748; margin-top:5px; white-space:pre-wrap; max-height:150px; overflow-y:auto; border-radius:4px;">${analysis.raw_text}</div>
|
|
||||||
`;
|
|
||||||
logContent.appendChild(details);
|
logContent.appendChild(details);
|
||||||
|
|
||||||
recLabel.innerText = `추천: ${analysis.suggested_path}`;
|
recLabel.innerText = `추천: ${analysis.suggested_path}`;
|
||||||
if(analysis.suggested_path === "분석실패") {
|
if(analysis.suggested_path === "분석실패") {
|
||||||
recLabel.style.color = "#f21d0d";
|
recLabel.style.color = "#f21d0d"; recLabel.style.background = "#fee9e7";
|
||||||
recLabel.style.background = "#fee9e7";
|
|
||||||
}
|
}
|
||||||
|
currentFiles[index].analysis = analysis;
|
||||||
currentFiles[index].analysis = analysis; // 결과 저장
|
} catch (e) { logContent.innerHTML += '<div class="log-line">ERR: 서버 오류</div>'; }
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
logContent.innerHTML += '<div class="log-line" style="color:red;">ERR: 분석 오류</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmUpload(index) {
|
function confirmUpload(index) {
|
||||||
const file = currentFiles[index];
|
const file = currentFiles[index];
|
||||||
const path = file.analysis ? file.analysis.suggested_path : "선택한 탭";
|
const path = file.analysis ? file.analysis.suggested_path : "선택한 탭";
|
||||||
|
if (confirm(`[${file.name}] 파일을 [${path}] 위치로 업로드하시겠습니까?`)) alert("완료되었습니다.");
|
||||||
let message = `[${file.name}] 파일을 업로드하시겠습니까?`;
|
|
||||||
if(file.analysis && file.analysis.suggested_path !== "분석실패") {
|
|
||||||
message = `AI가 추천한 위치로 업로드하시겠습니까?\n\n위치: ${path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (confirm(message)) {
|
|
||||||
alert("업로드가 완료되었습니다.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAttachments();
|
loadAttachments();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "AICodeTest",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
fastapi==0.110.0
|
fastapi==0.110.0
|
||||||
uvicorn==0.29.0
|
uvicorn==0.29.0
|
||||||
playwright==1.42.0
|
playwright==1.42.0
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
pypdf==4.1.0
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
server.log
BIN
server.log
Binary file not shown.
42
sheet.csv
42
sheet.csv
@@ -1,42 +0,0 @@
|
|||||||
[PM Overseas 프로젝트 현황],,2026.02.24,,,,,,<<활동로그가 없는 프로젝트 (9),,
|
|
||||||
,,,,,,,,,,
|
|
||||||
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,우즈베키스탄 타슈켄트 철도 FS,철도사업부,김태헌,2026.03.20,"2026.02.05, 파일업로드",O,51,,,
|
|
||||||
7,우즈베키스탄 지방 도로 복원 MP,도로부,장진영,2029.04.28,X,X,0,,,
|
|
||||||
8,이라크 Habbaniyah Shuaiba AirBase PD,도로부,강동구,2026.12.31,X,X,0,,,
|
|
||||||
9,캄보디아 반테 민체이 관개 홍수저감 MP,수자원1부,이대주,2026.08.28,"2025.12.07, 파일업로드",X,44,,,
|
|
||||||
10,캄보디아 시엠립 하수처리 개선 DD,물환경사업1부,변역근,2028.12.18,"2026.02.06, AI 요약",O,221,,,
|
|
||||||
11,메콩유역 수자원 관리 기후적응 MP,수자원1부,정귀한,2025.12.31,X,X,0,,종료(예정)일 지남,준공
|
|
||||||
12,키르기스스탄 잘랄아바드 상수도 계획 MP,물환경사업1부,변기상,2025.12.31,"2026.02.12, 파일업로드",X,60,,종료(예정)일 지남,준공
|
|
||||||
13,파키스탄 CAREC 도로 감리 DD,도로부,황효섭,2026.10.26,X,X,0,,,
|
|
||||||
14,파키스탄 펀잡 홍수 방재 PMC,수자원1부,방노성,2027.12.31,"2025.12.08, 폴더삭제",O,0,,,
|
|
||||||
15,파키스탄 KP 아보타바드 상수도 PMC,물환경사업2부,변기상,2026.12.31,"2026.02.12, 파일업로드",O,234,,,
|
|
||||||
16,필리핀 홍수 관리 Package5B MP,수자원1부,이희철,2026.05.31,"2025.12.02, 폴더이름변경",O,14,,,
|
|
||||||
17,필리핀 PGN 해상교량 BID2 IDC,구조부,이상희,2026.05.31,"2026.02.11, 파일다운로드",O,631,,,
|
|
||||||
18,필리핀 홍수 복원 InFRA2 DD,수자원1부,이대주,2026.08.07,"2025.12.01, 폴더삭제",O,6,최근로그 >> 폴더자동삭제(파일 개수 미달),,
|
|
||||||
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,X,X,0,,종료(예정)일 지남,준공
|
|
||||||
37,페루 Barranca 상하수도 확장 DD,물환경사업2부,변기상,2026.03.08,"2025.11.14, 파일업로드",O,44,"더미파일(폴더유지용) 27개, 실 관리부서는 해외사업부",,
|
|
||||||
38,엘살바도르 태평양 철도 FS,철도사업부,김태헌,2025.12.31,"2026.02.04, 파일이름변경",X,102,,종료(예정)일 지남,준공
|
|
||||||
39,필리핀 사무소,해외사업부,한형남,,"2026.02.23, 파일업로드",과업개요 페이지 없음,813,,,
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
:root {
|
:root {
|
||||||
/* Design Tokens */
|
/* Design Tokens from tokens.json */
|
||||||
--primary-color: #1E5149; --primary-lv-0: #e9eeed; --primary-lv-1: #D2DCDB; --primary-lv-8: #193833;
|
--primary-color: #1E5149; --primary-lv-0: #e9eeed; --primary-lv-1: #D2DCDB; --primary-lv-8: #193833;
|
||||||
--bg-default: #FFFFFF; --bg-muted: #F9FAFB;
|
--bg-default: #FFFFFF; --bg-muted: #F9FAFB;
|
||||||
--text-main: #2D3748; --text-sub: #718096; --border-color: #E2E8F0;
|
--text-main: #2D3748; --text-sub: #718096; --border-color: #E2E8F0;
|
||||||
@@ -18,91 +18,66 @@ button { cursor: pointer; border: none; font-family: inherit; }
|
|||||||
|
|
||||||
/* Components: Topbar */
|
/* Components: Topbar */
|
||||||
.topbar { width: 100%; background: var(--header-gradient); color: #fff; padding: 0 var(--space-lg); position: fixed; top: 0; height: 36px; display: flex; align-items: center; z-index: 1000; }
|
.topbar { width: 100%; background: var(--header-gradient); color: #fff; padding: 0 var(--space-lg); position: fixed; top: 0; height: 36px; display: flex; align-items: center; z-index: 1000; }
|
||||||
.topbar-header { margin-right: 60px; font-weight: 700; }
|
.topbar-header { margin-right: 80px; font-weight: 700; }
|
||||||
|
.topbar-header h2 { font-size: 15px; }
|
||||||
.nav-list { display: flex; list-style: none; gap: var(--space-sm); }
|
.nav-list { display: flex; list-style: none; gap: var(--space-sm); }
|
||||||
.nav-item { padding: 4px 8px; border-radius: 4px; color: rgba(255,255,255,0.8); transition: 0.2s; font-size: 14px; }
|
.nav-item { padding: 4px 8px; border-radius: 4px; color: rgba(255,255,255,0.8); transition: 0.2s; font-size: 14px; display: flex; align-items: center; }
|
||||||
.nav-item:hover { background: var(--primary-lv-1); color: #fff; }
|
.nav-item:hover { background: var(--primary-lv-1); color: #fff; }
|
||||||
.nav-item.active { background: var(--primary-lv-0); color: var(--primary-color) !important; font-weight: 700; }
|
.nav-item.active { background: var(--primary-lv-0); color: var(--primary-color) !important; font-weight: 700; }
|
||||||
|
|
||||||
/* Global Layout */
|
/* Global Layout */
|
||||||
.main-content { margin-top: 36px; padding: var(--space-lg) var(--space-xl); max-width: 1400px; margin-left: auto; margin-right: auto; }
|
.main-content { margin-top: 36px; padding: var(--space-lg) var(--space-xl); max-width: 1400px; margin-left: auto; margin-right: auto; }
|
||||||
header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: var(--space-lg); padding-bottom: var(--space-sm); border-bottom: 1px solid var(--border-color); }
|
header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: var(--space-lg); padding-bottom: var(--space-sm); border-bottom: 1px solid var(--border-color); }
|
||||||
header h1 { font-size: var(--fz-h1); color: var(--primary-color); }
|
header h1 { font-size: var(--fz-h1); color: var(--primary-color); font-weight: 800; }
|
||||||
|
.admin-info { color: var(--text-sub); font-size: 12px; margin-left: var(--space-lg); }
|
||||||
|
|
||||||
/* Portal (Index) */
|
/* Portal (Index) */
|
||||||
.portal-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: calc(100vh - 36px); background: var(--bg-muted); padding: var(--space-lg); }
|
.portal-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: calc(100vh - 36px); background: var(--bg-muted); padding: var(--space-lg); }
|
||||||
.portal-card { background: #fff; border: 1px solid var(--border-color); border-radius: 12px; padding: 40px; text-align: center; transition: 0.3s; width: 100%; max-width: 380px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
|
.portal-card { background: #fff; border: 1px solid var(--border-color); border-radius: 12px; padding: 40px; text-align: center; transition: 0.3s; width: 100%; max-width: 380px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); display: flex; flex-direction: column; align-items: center; gap: 20px; }
|
||||||
.portal-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
.portal-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||||||
.portal-card .icon { font-size: 32px; margin-bottom: 20px; width: 64px; height: 64px; background: var(--bg-muted); border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: 0.3s; margin-left: auto; margin-right: auto; }
|
.portal-card .icon { font-size: 32px; width: 64px; height: 64px; background: var(--bg-muted); border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: 0.3s; }
|
||||||
.portal-card:hover .icon { background: var(--primary-color); color: #fff; }
|
.portal-card:hover .icon { background: var(--primary-color); color: #fff; }
|
||||||
|
|
||||||
/* Dashboard List & Console */
|
/* Dashboard List */
|
||||||
.log-console { background:#000; color:#0f0; font-family:monospace; padding:15px; margin-bottom:20px; border-radius:4px; max-height:200px; overflow-y:auto; font-size:12px; }
|
.continent-group { margin-bottom: var(--space-xl); }
|
||||||
.accordion-container { border-top: 1px solid var(--border-color); }
|
.continent-header { font-size: 18px; font-weight: 800; color: var(--primary-color); padding: var(--space-md) 0; border-bottom: 2px solid var(--primary-color); display: flex; justify-content: space-between; cursor: pointer; margin-bottom: var(--space-md); }
|
||||||
|
.country-group { margin-bottom: var(--space-md); }
|
||||||
|
.country-header { font-size: 14px; font-weight: 700; padding: var(--space-sm) 0; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; cursor: pointer; }
|
||||||
|
.continent-body, .country-body { display: none; }
|
||||||
|
.active > .continent-body, .active > .country-body { display: block; }
|
||||||
|
.active > .continent-header .toggle-icon, .active > .country-header .toggle-icon { transform: rotate(180deg); }
|
||||||
|
.toggle-icon { transition: 0.2s; display: inline-block; }
|
||||||
|
|
||||||
|
.accordion-container { border: none; background: transparent; margin-top: var(--space-xs); }
|
||||||
.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; }
|
.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; }
|
||||||
.accordion-list-header { font-size: 11px; font-weight: 700; color: var(--text-sub); border-bottom: 1px solid var(--text-main); }
|
.accordion-list-header { font-size: 11px; font-weight: 700; color: var(--text-sub); border-bottom: 1px solid var(--text-main); text-transform: uppercase; }
|
||||||
.accordion-item { border-bottom: 1px solid var(--border-color); }
|
.accordion-item { border-bottom: 1px solid var(--border-color); transition: background 0.1s; }
|
||||||
.accordion-item:hover { background: var(--primary-lv-0); }
|
.accordion-item:hover { background: #E9EEED; }
|
||||||
.repo-title { font-weight: 700; color: var(--primary-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.repo-title { font-weight: 700; color: var(--primary-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
||||||
.repo-files { text-align: center; font-weight: 600; }
|
.repo-files { text-align: center; font-weight: 600; color: var(--text-main); }
|
||||||
.repo-log { font-size: 11px; color: var(--text-sub); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.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-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; }
|
.accordion-item.active .accordion-body { display: block; }
|
||||||
.status-warning { background: #fff9e6; } .status-error { background: #fee9e7; }
|
.status-warning { background: #fff9e6; } .status-error { background: #fee9e7; }
|
||||||
.warning-text { color: #f21d0d !important; font-weight: 700; }
|
.warning-text { color: #f21d0d !important; font-weight: 700; }
|
||||||
|
|
||||||
/* Mail Manager Refined */
|
/* Mail Manager */
|
||||||
.mail-wrapper { display: flex; height: calc(100vh - 36px); margin-top: 36px; background: #fff; overflow: hidden; }
|
.mail-wrapper { display: flex; height: calc(100vh - 36px); margin-top: 36px; background: #fff; overflow: hidden; }
|
||||||
.mail-sidebar { width: 240px; border-right: 1px solid var(--border-color); padding: var(--space-md); background: var(--bg-muted); }
|
.mail-sidebar { width: 240px; border-right: 1px solid var(--border-color); padding: var(--space-md); background: var(--bg-muted); }
|
||||||
.project-select { width: 100%; padding: var(--space-sm); border-radius: var(--radius-sm); border: 1px solid var(--border-color); margin-bottom: var(--space-lg); font-weight: 700; background: #fff; }
|
.project-select { width: 100%; padding: var(--space-sm); border-radius: var(--radius-sm); border: 1px solid var(--border-color); margin-bottom: var(--space-lg); font-weight: 700; }
|
||||||
.folder-item { padding: var(--space-sm) var(--space-md); border-radius: 4px; cursor: pointer; margin-bottom: 2px; font-weight: 500; display: flex; justify-content: space-between; }
|
|
||||||
.folder-item:hover { background: var(--primary-lv-0); }
|
|
||||||
.folder-item.active { background: var(--primary-color); color: #fff; }
|
|
||||||
|
|
||||||
.mail-list-area { width: 380px; border-right: 1px solid var(--border-color); display: flex; flex-direction: column; }
|
.mail-list-area { width: 380px; border-right: 1px solid var(--border-color); display: flex; flex-direction: column; }
|
||||||
.search-bar { padding: var(--space-md); border-bottom: 1px solid var(--border-color); background: var(--bg-muted); }
|
|
||||||
.search-bar input { width: 100%; padding: 8px; border-radius: 4px; border: 1px solid var(--border-color); margin-bottom: var(--space-sm); }
|
|
||||||
.mail-item { padding: var(--space-md); border-bottom: 1px solid var(--border-color); cursor: pointer; transition: 0.2s; }
|
.mail-item { padding: var(--space-md); border-bottom: 1px solid var(--border-color); cursor: pointer; transition: 0.2s; }
|
||||||
.mail-item:hover { background: var(--bg-muted); }
|
|
||||||
.mail-item.active { background: #E9EEED; border-left: 4px solid var(--primary-color); }
|
.mail-item.active { background: #E9EEED; border-left: 4px solid var(--primary-color); }
|
||||||
|
|
||||||
.mail-content-area { flex: 1; display: flex; flex-direction: column; overflow-y: auto; }
|
|
||||||
.mail-content-header { padding: var(--space-lg); border-bottom: 1px solid var(--border-color); }
|
|
||||||
.mail-body { padding: var(--space-lg); line-height: 1.6; min-height: 200px; }
|
|
||||||
|
|
||||||
.attachment-area { padding: var(--space-lg); border-top: 1px solid var(--border-color); background: var(--bg-muted); }
|
.attachment-area { padding: var(--space-lg); border-top: 1px solid var(--border-color); background: var(--bg-muted); }
|
||||||
.attachment-item { display: flex; align-items: center; justify-content: space-between; background: #fff; padding: var(--space-sm) var(--space-md); border-radius: var(--radius-lg); border: 1px solid var(--border-color); margin-bottom: var(--space-sm); }
|
.attachment-item { display: flex; align-items: center; justify-content: space-between; background: #fff; padding: var(--space-sm) var(--space-md); border-radius: var(--radius-lg); border: 1px solid var(--border-color); margin-bottom: var(--space-sm); flex-wrap: wrap; }
|
||||||
.file-info { display: flex; align-items: center; gap: var(--space-sm); flex: 1; }
|
.file-info { display: flex; align-items: center; gap: var(--space-sm); flex: 1; min-width: 200px; }
|
||||||
.ai-recommend { font-size: 11px; color: #6d3dc2; background: #f1ecf9; padding: 2px 6px; border-radius: 4px; font-weight: 700; margin-left: 10px; }
|
.ai-recommend { font-size: 11px; color: #6d3dc2; background: #f1ecf9; padding: 2px 6px; border-radius: 4px; font-weight: 700; margin-left: 10px; }
|
||||||
|
.file-log-area { display: none; width: 100%; margin-top: 10px; background: #1a202c; border-radius: 4px; padding: 12px; font-family: monospace; font-size: 11px; color: #cbd5e0; line-height: 1.5; }
|
||||||
.ai-log-console {
|
.file-log-area.active { display: block; }
|
||||||
margin-top: var(--space-md);
|
|
||||||
background: #1a202c; /* dark gray */
|
|
||||||
color: #a0aec0;
|
|
||||||
padding: var(--space-md);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.5;
|
|
||||||
max-height: 150px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.ai-log-console .log-entry { margin-bottom: 4px; border-left: 2px solid #4a5568; padding-left: 8px; }
|
|
||||||
.ai-log-console .highlight { color: #63b3ed; font-weight: 700; }
|
|
||||||
.ai-log-console .success { color: #48bb78; }
|
|
||||||
|
|
||||||
.btn-group { display: flex; gap: var(--space-xs); }
|
.btn-group { display: flex; gap: var(--space-xs); }
|
||||||
.btn-upload { padding: 6px 12px; border-radius: 4px; font-size: 11px; font-weight: 700; color: #fff; }
|
.btn-upload { padding: 6px 12px; border-radius: 4px; font-size: 11px; font-weight: 700; color: #fff; }
|
||||||
.btn-ai { background: var(--ai-gradient); }
|
.btn-ai { background: var(--ai-gradient); }
|
||||||
.btn-normal { background: var(--primary-color); }
|
.btn-normal { background: var(--primary-color); }
|
||||||
.btn-upload:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
||||||
|
|
||||||
/* File-specific Log Accordion */
|
|
||||||
.file-log-area { display: none; width: 100%; margin-top: 10px; background: #1a202c; border-radius: 4px; padding: 12px; font-family: monospace; font-size: 11px; color: #cbd5e0; }
|
|
||||||
.file-log-area.active { display: block; }
|
|
||||||
.log-line { margin-bottom: 2px; }
|
|
||||||
.log-success { color: #48bb78; font-weight: 700; }
|
|
||||||
.log-info { color: #63b3ed; }
|
|
||||||
|
|
||||||
/* Toggle Switch */
|
/* Toggle Switch */
|
||||||
.switch { position: relative; display: inline-block; width: 34px; height: 20px; }
|
.switch { position: relative; display: inline-block; width: 34px; height: 20px; }
|
||||||
@@ -111,13 +86,10 @@ header h1 { font-size: var(--fz-h1); color: var(--primary-color); }
|
|||||||
.slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
|
.slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
|
||||||
input:checked + .slider { background: var(--ai-gradient); }
|
input:checked + .slider { background: var(--ai-gradient); }
|
||||||
input:checked + .slider:before { transform: translateX(14px); }
|
input:checked + .slider:before { transform: translateX(14px); }
|
||||||
|
|
||||||
.ai-toggle-wrap { display: flex; align-items: center; gap: var(--space-sm); font-size: 12px; font-weight: 600; color: var(--text-sub); }
|
.ai-toggle-wrap { display: flex; align-items: center; gap: var(--space-sm); font-size: 12px; font-weight: 600; color: var(--text-sub); }
|
||||||
input:checked ~ .ai-label { color: #6d3dc2; }
|
|
||||||
|
|
||||||
/* Utils */
|
/* Utils */
|
||||||
.sync-btn { background: var(--primary-color); color: #fff; padding: 8px 16px; border-radius: var(--radius-lg); font-weight: 700; }
|
.sync-btn { background: var(--primary-color); color: #fff; padding: 8px 16px; border-radius: var(--radius-lg); font-weight: 700; display: flex; align-items: center; gap: 8px; }
|
||||||
.badge { background: #eee; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
|
.badge { background: #eee; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
|
||||||
.toggle-icon { transition: 0.2s; }
|
.spinner-small { width: 12px; height: 12px; border: 2px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: #fff; animation: spin 1s linear infinite; display: inline-block; }
|
||||||
.active > .continent-header .toggle-icon, .active > .country-header .toggle-icon { transform: rotate(180deg); }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
|||||||
1232
tokens.json
1232
tokens.json
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user