Compare commits
2 Commits
909340ff76
...
9bb2ecd703
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bb2ecd703 | |||
| 6d71f94ca8 |
14
.gemini/settings.json
Normal file
14
.gemini/settings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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,3 +3,38 @@
|
||||
1. **언어 설정**: 영어로 생각하되, 모든 답변은 한국어로 작성한다. (일본어, 중국어는 절대 사용하지 않는다.)
|
||||
2. **수정 권한 제한**: 사용자가 명시적으로 지시한 사항 외에는 **절대 절대 절대** 코드를 임의로 수정하지 않는다.
|
||||
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` 그라데이션 적용 가능
|
||||
|
||||
|
||||
BIN
__pycache__/analyze.cpython-312.pyc
Normal file
BIN
__pycache__/analyze.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/crawler_api.cpython-312.pyc
Normal file
BIN
__pycache__/crawler_api.cpython-312.pyc
Normal file
Binary file not shown.
92
analyze.py
Normal file
92
analyze.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
from pypdf import PdfReader
|
||||
try:
|
||||
import pytesseract
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image
|
||||
TESSERACT_PATH = r'C:\Users\User\AppData\Local\Programs\Tesseract-OCR\tesseract.exe'
|
||||
POPPLER_PATH = r'D:\이태훈\00크롬다운로드\poppler-25.12.0\Library\bin'
|
||||
pytesseract.pytesseract.tesseract_cmd = TESSERACT_PATH
|
||||
OCR_AVAILABLE = True
|
||||
except ImportError:
|
||||
OCR_AVAILABLE = False
|
||||
|
||||
def analyze_file_content(filename: str):
|
||||
file_path = os.path.join("sample", filename)
|
||||
if not os.path.exists(file_path):
|
||||
return {"error": "File not found"}
|
||||
|
||||
log_steps = []
|
||||
|
||||
# Layer 1: 제목 분석 (Quick)
|
||||
log_steps.append("1. 레이어: 파일 제목(Title) 스캔 중...")
|
||||
title_text = filename.lower().replace(" ", "")
|
||||
|
||||
# Layer 2: 텍스트 추출 (Fast)
|
||||
log_steps.append("2. 레이어: PDF 텍스트 엔진(Extraction) 가동...")
|
||||
text_content = ""
|
||||
try:
|
||||
if filename.lower().endswith(".pdf"):
|
||||
reader = PdfReader(file_path)
|
||||
for page in reader.pages[:5]: # 전체가 아닌 핵심 페이지 위주
|
||||
page_txt = page.extract_text()
|
||||
if page_txt: text_content += page_txt + "\n"
|
||||
text_content = unicodedata.normalize('NFC', text_content)
|
||||
log_steps.append(f" - 텍스트 데이터 확보 완료 ({len(text_content)}자)")
|
||||
except:
|
||||
log_steps.append(" - 텍스트 추출 실패")
|
||||
|
||||
# Layer 3: OCR 정밀 분석 (Deep)
|
||||
log_steps.append("3. 레이어: OCR 이미지 스캔(Vision) 강제 실행...")
|
||||
ocr_content = ""
|
||||
if OCR_AVAILABLE and os.path.exists(TESSERACT_PATH):
|
||||
try:
|
||||
# 상징적인 첫 페이지 위주 OCR (성능과 정확도 타협)
|
||||
images = convert_from_path(file_path, first_page=1, last_page=2, poppler_path=POPPLER_PATH)
|
||||
for i, img in enumerate(images):
|
||||
page_ocr = pytesseract.image_to_string(img, lang='kor+eng')
|
||||
ocr_content += unicodedata.normalize('NFC', page_ocr) + "\n"
|
||||
log_steps.append(f" - OCR 스캔 완료 ({len(ocr_content)}자)")
|
||||
except Exception as e:
|
||||
log_steps.append(f" - OCR 오류: {str(e)[:20]}")
|
||||
|
||||
# 3중 레이어 데이터 통합
|
||||
full_pool = (title_text + " | " + text_content + " | " + ocr_content).lower().replace(" ", "").replace("\n", "")
|
||||
|
||||
# 분석 초기화
|
||||
result = {
|
||||
"suggested_path": "분석실패",
|
||||
"confidence": "Low",
|
||||
"log_steps": log_steps,
|
||||
"raw_text": f"--- TITLE ---\n{filename}\n\n--- TEXT ---\n{text_content[:1000]}\n\n--- OCR ---\n{ocr_content[:1000]}",
|
||||
"reason": "학습된 키워드 일치 항목 없음"
|
||||
}
|
||||
|
||||
# 최종 추천 로직 (합의 알고리즘)
|
||||
is_eocheon = any(k in full_pool for k in ["어천", "공주"])
|
||||
|
||||
if "실정보고" in full_pool or "실정" in full_pool:
|
||||
if is_eocheon:
|
||||
if "품질" in full_pool:
|
||||
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 품질관리"
|
||||
result["reason"] = "3중 레이어 분석: 실정보고+어천공주+품질관리 키워드 통합 검출"
|
||||
elif any(k in full_pool for k in ["토지", "임대"]):
|
||||
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 기타"
|
||||
result["reason"] = "3중 레이어 분석: 토지임대 관련 실정보고(어천-공주) 확인"
|
||||
else:
|
||||
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 기타"
|
||||
result["reason"] = "3중 레이어 분석: 실정보고(어천-공주) 문서 판정"
|
||||
result["confidence"] = "100%"
|
||||
else:
|
||||
result["suggested_path"] = "설계변경 > 실정보고(어천~공주) > 기타" # 폴백
|
||||
result["confidence"] = "80%"
|
||||
result["reason"] = "실정보고 키워드는 발견되었으나 프로젝트명 교차 검증 실패 (기본값 제안)"
|
||||
|
||||
elif "품질" in full_pool:
|
||||
result["suggested_path"] = "공사관리 > 품질 관리 > 품질시험계획서"
|
||||
result["confidence"] = "90%"
|
||||
result["reason"] = "텍스트/OCR 레이어에서 품질 관리 지표 다수 식별"
|
||||
|
||||
return result
|
||||
@@ -9,6 +9,7 @@ from fastapi.responses import StreamingResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from playwright.async_api import async_playwright
|
||||
from dotenv import load_dotenv
|
||||
from analyze import analyze_file_content
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -25,10 +26,37 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
@app.get("/dashboard")
|
||||
async def get_dashboard():
|
||||
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")
|
||||
async def sync_data():
|
||||
async def event_generator():
|
||||
@@ -97,18 +125,18 @@ async def sync_data():
|
||||
|
||||
modal_sel = "article.archive-modal"
|
||||
if await page.locator(modal_sel).is_visible():
|
||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [로그] 모달 발견. 데이터 추출 중...'})}\n\n"
|
||||
# 사용자 제공 정밀 셀렉터 기반 추출
|
||||
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 = "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 = "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"
|
||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [로그] 모달 발견. 데이터 로딩 대기...'})}\n\n"
|
||||
# .log-body 내부의 데이터만 타겟팅하도록 수정
|
||||
date_sel = "article.archive-modal .log-body .date .text"
|
||||
user_sel = "article.archive-modal .log-body .user .text"
|
||||
act_sel = "article.archive-modal .log-body .activity .text"
|
||||
|
||||
# 데이터가 나타날 때까지 반복 대기
|
||||
# 데이터가 나타날 때까지 최대 15초 대기
|
||||
success_log = False
|
||||
for _ in range(10):
|
||||
for _ in range(15):
|
||||
if await page.locator(date_sel).count() > 0:
|
||||
raw_date = (await page.locator(date_sel).first.inner_text()).strip()
|
||||
if raw_date and "활동시간" not in raw_date:
|
||||
if raw_date:
|
||||
success_log = True
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
@@ -148,32 +176,33 @@ async def sync_data():
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
if popup_page:
|
||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 창 발견. 데이터 로딩 대기 (최대 80초)...'})}\n\n"
|
||||
target_selector = "#composition-list h6"
|
||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 창 발견. 데이터 로딩 대기 (최대 30초)...'})}\n\n"
|
||||
# 사용자 제공 정밀 선택자 적용 (nth-child(3)가 실제 데이터)
|
||||
target_selector = "#composition-list h6:nth-child(3)"
|
||||
success_comp = False
|
||||
|
||||
# 최대 80초간 끝까지 대기
|
||||
for _ in range(80):
|
||||
# 최대 30초간 데이터가 나타날 때까지 대기
|
||||
for _ in range(30):
|
||||
h6_count = await popup_page.locator(target_selector).count()
|
||||
if h6_count > 5: # 일정 개수 이상의 목록이 나타나면 로딩 시작으로 간주
|
||||
if h6_count > 0:
|
||||
success_comp = True
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if success_comp:
|
||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 데이터 감지됨. 15초간 최종 렌더링 대기...'})}\n\n"
|
||||
await asyncio.sleep(15) # 완전한 로딩을 위한 강제 대기
|
||||
yield f"data: {json.dumps({'type': 'log', 'message': ' - [구성] 데이터 감지됨. 최종 렌더링 대기...'})}\n\n"
|
||||
await asyncio.sleep(10) # 렌더링 안정화를 위한 대기
|
||||
|
||||
# 유연한 데이터 수집
|
||||
# 모든 h6:nth-child(3) 요소를 순회하며 숫자 합산
|
||||
locators_h6 = popup_page.locator(target_selector)
|
||||
h6_count = await locators_h6.count()
|
||||
current_total = 0
|
||||
for j in range(h6_count):
|
||||
text = (await locators_h6.nth(j).inner_text()).strip()
|
||||
# 텍스트 내에서 숫자만 추출 (여러 줄일 경우 마지막 줄 기준)
|
||||
nums = re.findall(r'\d+', text.split('\n')[-1])
|
||||
if nums:
|
||||
val = int(nums[0])
|
||||
if val < 5000: current_total += val
|
||||
current_total += int(nums[0])
|
||||
|
||||
file_count = current_total
|
||||
yield f"data: {json.dumps({'type': 'log', 'message': f' - [구성] 성공 ({file_count}개)'})}\n\n"
|
||||
|
||||
129
dashboard.html
129
dashboard.html
@@ -1,32 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Project Master Overseas 관리자</title>
|
||||
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
||||
<link rel="stylesheet" as="style" crossorigin
|
||||
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
||||
<link rel="stylesheet" href="style/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="topbar">
|
||||
<div class="topbar-header">
|
||||
<h2>Project Master Overseas</h2>
|
||||
<a href="/">
|
||||
<h2>Project Master Test</h2>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item active">대시보드</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">로그관리</li>
|
||||
<li class="nav-item">파일관리</li>
|
||||
<li class="nav-item">인원관리</li>
|
||||
<li class="nav-item">공지사항</li>
|
||||
<li class="nav-item active" onclick="location.href='/dashboard'">대시보드</li>
|
||||
<li class="nav-item" onclick="alert('준비 중입니다.')">문의사항</li>
|
||||
<li class="nav-item" onclick="alert('준비 중입니다.')">로그관리</li>
|
||||
<li class="nav-item" onclick="alert('준비 중입니다.')">파일관리</li>
|
||||
<li class="nav-item" onclick="alert('준비 중입니다.')">인원관리</li>
|
||||
<li class="nav-item" onclick="alert('준비 중입니다.')">공지사항</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<header>
|
||||
<div style="display:flex; align-items:center;">
|
||||
<h1>대시보드 현황</h1>
|
||||
<h1>프로젝트 현황</h1>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center;">
|
||||
<button id="syncBtn" class="sync-btn" onclick="syncData()">
|
||||
@@ -38,8 +43,11 @@
|
||||
</header>
|
||||
|
||||
<!-- 실시간 로그 콘솔 추가 -->
|
||||
<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;">
|
||||
<div style="color:#fff; border-bottom:1px solid #333; margin-bottom:10px; padding-bottom:5px; font-weight:bold;">실시간 수집 로그 [PM Overseas]</div>
|
||||
<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;">
|
||||
<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>
|
||||
|
||||
@@ -92,8 +100,8 @@
|
||||
];
|
||||
|
||||
const continentMap = {
|
||||
"라오스": "아시아", "미얀마": "아시아", "베트남": "아시아", "사우디아라비아": "아시아",
|
||||
"우즈베키스탄": "아시아", "이라크": "아시아", "캄보디아": "아시아",
|
||||
"라오스": "아시아", "미얀마": "아시아", "베트남": "아시아", "사우디아라비아": "아시아",
|
||||
"우즈베키스탄": "아시아", "이라크": "아시아", "캄보디아": "아시아",
|
||||
"키르기스스탄": "아시아", "파키스탄": "아시아", "필리핀": "아시아",
|
||||
"아르헨티나": "아메리카", "온두라스": "아메리카", "볼리비아": "아메리카", "콜롬비아": "아메리카",
|
||||
"파라과이": "아메리카", "페루": "아메리카", "엘살바도르": "아메리카",
|
||||
@@ -116,7 +124,7 @@
|
||||
const projectName = item[0];
|
||||
let continent = "";
|
||||
let country = "";
|
||||
|
||||
|
||||
if (projectName.endsWith("사무소")) {
|
||||
continent = "지사";
|
||||
country = projectName.split(" ")[0];
|
||||
@@ -127,10 +135,10 @@
|
||||
country = projectName.split(" ")[0];
|
||||
continent = continentMap[country] || "기타";
|
||||
}
|
||||
|
||||
|
||||
if (!groupedData[continent]) groupedData[continent] = {};
|
||||
if (!groupedData[continent][country]) groupedData[continent][country] = [];
|
||||
|
||||
|
||||
groupedData[continent][country].push({ item, index });
|
||||
});
|
||||
|
||||
@@ -141,7 +149,7 @@
|
||||
sortedContinents.forEach(continent => {
|
||||
const continentGroup = document.createElement('div');
|
||||
continentGroup.className = 'continent-group';
|
||||
|
||||
|
||||
let continentHtml = `
|
||||
<div class="continent-header" onclick="toggleGroup(this)">
|
||||
<span>${continent}</span>
|
||||
@@ -161,48 +169,40 @@
|
||||
</div>
|
||||
<div class="country-body">
|
||||
<div class="accordion-container">
|
||||
<div class="accordion-list-header">
|
||||
<div>프로젝트명</div>
|
||||
<div>담당부서</div>
|
||||
<div>담당자</div>
|
||||
<div style="text-align:center;">파일수</div>
|
||||
<div>최근로그</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const sortedProjects = groupedData[continent][country].sort((a, b) => a.item[0].localeCompare(b.item[0]));
|
||||
|
||||
sortedProjects.forEach(({item, index}) => {
|
||||
sortedProjects.forEach(({ item, index }) => {
|
||||
const projectName = item[0];
|
||||
const dept = item[1];
|
||||
const admin = item[2];
|
||||
const recentLogRaw = item[3];
|
||||
const fileCount = item[4];
|
||||
const personnelCount = Math.floor(Math.random()*15)+3; // 인원은 시트에 없으므로 임의 할당 유지
|
||||
|
||||
|
||||
const recentLog = recentLogRaw === "X" ? "기록 없음" : recentLogRaw;
|
||||
const logTime = recentLog !== "기록 없음" ? recentLog.split(',')[0] : "기록 없음";
|
||||
|
||||
// 상태 클래스 결정
|
||||
let statusClass = "";
|
||||
if (fileCount === 0) statusClass = "status-error";
|
||||
else if (recentLog === "기록 없음") statusClass = "status-warning";
|
||||
|
||||
continentHtml += `
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-item ${statusClass}">
|
||||
<div class="accordion-header" onclick="toggleAccordion(this)">
|
||||
<div>
|
||||
<span class="header-label">프로젝트 명</span>
|
||||
<span class="header-value" title="${projectName}">${projectName}</span>
|
||||
</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 class="repo-title" title="${projectName}">${projectName}</div>
|
||||
<div class="repo-dept">${dept}</div>
|
||||
<div class="repo-admin">${admin}</div>
|
||||
<div class="repo-files ${fileCount === 0 ? 'warning-text' : ''}">${fileCount}</div>
|
||||
<div class="repo-log ${recentLog === '기록 없음' ? 'warning-text' : ''}" title="${recentLog}">${recentLog}</div>
|
||||
</div>
|
||||
<div class="accordion-body">
|
||||
<div class="detail-grid">
|
||||
@@ -245,44 +245,36 @@
|
||||
continentHtml += `
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
continentGroup.innerHTML = continentHtml;
|
||||
container.appendChild(continentGroup);
|
||||
});
|
||||
|
||||
|
||||
const allContinents = container.querySelectorAll('.continent-group');
|
||||
allContinents.forEach(continent => {
|
||||
continent.classList.add('active');
|
||||
continent.querySelector('.continent-header .toggle-icon').textContent = '▲';
|
||||
});
|
||||
|
||||
const allCountries = container.querySelectorAll('.country-group');
|
||||
allCountries.forEach(country => {
|
||||
country.classList.add('active');
|
||||
country.querySelector('.country-header .toggle-icon').textContent = '▲';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGroup(header) {
|
||||
const group = header.parentElement;
|
||||
const icon = header.querySelector('.toggle-icon');
|
||||
group.classList.toggle('active');
|
||||
if (group.classList.contains('active')) {
|
||||
icon.textContent = '▲';
|
||||
} else {
|
||||
icon.textContent = '▼';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAccordion(header) {
|
||||
const item = header.parentElement;
|
||||
const container = item.parentElement;
|
||||
|
||||
|
||||
const allItems = container.querySelectorAll('.accordion-item');
|
||||
allItems.forEach(el => {
|
||||
if(el !== item) el.classList.remove('active');
|
||||
if (el !== item) el.classList.remove('active');
|
||||
});
|
||||
|
||||
|
||||
item.classList.toggle('active');
|
||||
}
|
||||
|
||||
@@ -290,11 +282,11 @@
|
||||
const btn = document.getElementById('syncBtn');
|
||||
const logConsole = document.getElementById('logConsole');
|
||||
const logBody = document.getElementById('logBody');
|
||||
|
||||
|
||||
btn.classList.add('loading');
|
||||
btn.innerHTML = `<span class="spinner"></span> 동기화 중 (진행 상황 확인 중...)`;
|
||||
btn.disabled = true;
|
||||
|
||||
|
||||
logConsole.style.display = 'block';
|
||||
logBody.innerHTML = ''; // 이전 로그 삭제
|
||||
|
||||
@@ -307,7 +299,7 @@
|
||||
|
||||
try {
|
||||
const response = await fetch(`/sync`);
|
||||
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
@@ -317,21 +309,21 @@
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const payload = JSON.parse(line.substring(6));
|
||||
|
||||
|
||||
if (payload.type === 'log') {
|
||||
addLog(payload.message);
|
||||
} else if (payload.type === 'done') {
|
||||
const newData = payload.data;
|
||||
newData.forEach(scrapedItem => {
|
||||
const target = rawData.find(item =>
|
||||
item[0].replace(/\s/g,'').includes(scrapedItem.projectName.replace(/\s/g,'')) ||
|
||||
scrapedItem.projectName.replace(/\s/g,'').includes(item[0].replace(/\s/g,''))
|
||||
const target = rawData.find(item =>
|
||||
item[0].replace(/\s/g, '').includes(scrapedItem.projectName.replace(/\s/g, '')) ||
|
||||
scrapedItem.projectName.replace(/\s/g, '').includes(item[0].replace(/\s/g, ''))
|
||||
);
|
||||
|
||||
|
||||
if (target) {
|
||||
// 기존 데이터 유지 마커 확인
|
||||
if (scrapedItem.recentLog !== "기존데이터유지") {
|
||||
@@ -340,7 +332,7 @@
|
||||
target[4] = scrapedItem.fileCount;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
document.getElementById('projectAccordion').innerHTML = '';
|
||||
init();
|
||||
addLog(">>> 모든 동기화 작업이 완료되었습니다!");
|
||||
@@ -364,4 +356,5 @@
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
266
debug_modal.html
Normal file
266
debug_modal.html
Normal file
@@ -0,0 +1,266 @@
|
||||
|
||||
<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
Normal file
BIN
debug_modal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 289 KiB |
142
index.html
Normal file
142
index.html
Normal file
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Project Master Portal</title>
|
||||
<link rel="stylesheet" as="style" crossorigin
|
||||
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.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>
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="topbar">
|
||||
<div class="topbar-header">
|
||||
<a href="/"><h2>Project Master Test</h2></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="portal-container">
|
||||
<div class="portal-header">
|
||||
<h1>Project Master 테스트</h1>
|
||||
<p>원하시는 서비스에 접속하려면 아래 버튼을 클릭하세요.</p>
|
||||
</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>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
199
mailTest.html
Normal file
199
mailTest.html
Normal file
@@ -0,0 +1,199 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Project Mail Manager</title>
|
||||
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
||||
<link rel="stylesheet" href="/style/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="topbar">
|
||||
<div class="topbar-header">
|
||||
<a href="/"><h2>Project Master Test</h2></a>
|
||||
</div>
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item" onclick="location.href='/dashboard'">대시보드</li>
|
||||
<li class="nav-item active" onclick="location.href='/mailTest'">메일관리</li>
|
||||
<li class="nav-item">로그관리</li>
|
||||
<li class="nav-item">파일관리</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="mail-wrapper">
|
||||
<aside class="mail-sidebar">
|
||||
<select class="project-select">
|
||||
<option>라오스 ITTC 관개 교육센터</option>
|
||||
<option>베트남 푸옥호아 발전소</option>
|
||||
</select>
|
||||
<ul class="folder-list">
|
||||
<li class="folder-item active" style="list-style:none;"><span>📥 수신함</span><span>12</span></li>
|
||||
<li class="folder-item" style="list-style:none;"><span>📤 발신함</span></li>
|
||||
<li class="folder-item" style="list-style:none;"><span>📁 중요메일</span></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<section class="mail-list-area">
|
||||
<div class="search-bar">
|
||||
<input type="text" placeholder="제목, 내용, 기관 검색...">
|
||||
<div style="display:flex; gap:4px;">
|
||||
<select style="flex:1; padding:4px; font-size:11px;"><option>모든 상대기관</option></select>
|
||||
<select style="flex:1; padding:4px; font-size:11px;"><option>전체 기간</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mail-items-container">
|
||||
<div class="mail-item active">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
|
||||
<span style="font-weight:700; color:var(--primary-color);">라오스 농림부</span>
|
||||
<span style="font-size:11px; color:var(--text-sub);">오후 2:30</span>
|
||||
</div>
|
||||
<div style="font-weight:600; font-size:12px; margin-bottom:4px;">ITTC 교육센터 착공식 일정 협의</div>
|
||||
<div style="font-size:11px; color:var(--text-sub); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">안녕하세요. 착공식 관련하여 첨부 드리는 공문을...</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mail-content-area">
|
||||
<div class="mail-content-header">
|
||||
<h2 style="font-size:18px; color:var(--primary-color); margin-bottom:8px;">ITTC 교육센터 착공식 일정 협의 요청</h2>
|
||||
<div style="font-size:12px; color:var(--text-sub);"><strong>보낸사람</strong> pany.s@lao.gov.la (라오스 농림부)</div>
|
||||
<div style="font-size:12px; color:var(--text-sub);"><strong>날짜</strong> 2026년 2월 26일 14:30</div>
|
||||
</div>
|
||||
<div class="mail-body">
|
||||
안녕하세요. 이태훈 선임연구원님.<br><br>
|
||||
라오스 ITTC 관개 교육센터 착공식과 관련하여 정부 측 인사의 일정을 반영한 최종 공문을 송부합니다.<br>
|
||||
첨부파일의 세부 계획안을 검토하신 후, 프로젝트 대시보드의 '과업계획' 탭에 업로드 및 공유 부탁드립니다.
|
||||
</div>
|
||||
|
||||
<div class="attachment-area">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
||||
<div style="font-weight:700; font-size:13px;">첨부파일 리스트</div>
|
||||
<div class="ai-toggle-wrap">
|
||||
<span class="ai-label">AI 판단</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="aiToggle" checked onchange="renderFiles()">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="attachmentList"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentFiles = [];
|
||||
|
||||
async function loadAttachments() {
|
||||
try {
|
||||
const res = await fetch('/attachments');
|
||||
currentFiles = await res.json();
|
||||
renderFiles();
|
||||
} catch (e) {
|
||||
console.error("Failed to load attachments:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFiles() {
|
||||
const isAiActive = document.getElementById('aiToggle').checked;
|
||||
const container = document.getElementById('attachmentList');
|
||||
container.innerHTML = '';
|
||||
|
||||
currentFiles.forEach((file, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'attachment-item-wrap';
|
||||
item.style.marginBottom = "8px";
|
||||
|
||||
const btnAiClass = isAiActive ? 'btn-ai' : 'btn-normal';
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="attachment-item">
|
||||
<div class="file-info">
|
||||
<span style="font-size:20px;">📄</span>
|
||||
<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>
|
||||
<span id="recommend-${index}" class="ai-recommend" style="display:none;">추천 위치 탐색 중...</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn-upload ${btnAiClass}" onclick="startAnalysis(${index})">AI 분석</button>
|
||||
<button class="btn-upload btn-normal" onclick="confirmUpload(${index})">파일 업로드</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="log-area-${index}" class="file-log-area">
|
||||
<div id="log-content-${index}"></div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async function startAnalysis(index) {
|
||||
const file = currentFiles[index];
|
||||
const logArea = document.getElementById(`log-area-${index}`);
|
||||
const logContent = document.getElementById(`log-content-${index}`);
|
||||
const recLabel = document.getElementById(`recommend-${index}`);
|
||||
|
||||
logArea.classList.add('active');
|
||||
logContent.innerHTML = '<div class="log-line log-info">>>> 3중 레이어 AI 분석 엔진 가동...</div>';
|
||||
recLabel.style.display = 'inline-block';
|
||||
recLabel.innerText = '분석 중...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/analyze-file?filename=${encodeURIComponent(file.name)}`);
|
||||
const analysis = await res.json();
|
||||
|
||||
analysis.log_steps.forEach(step => {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'log-line';
|
||||
line.innerText = " " + step;
|
||||
logContent.appendChild(line);
|
||||
});
|
||||
|
||||
const resultLine = document.createElement('div');
|
||||
resultLine.className = 'log-line log-success';
|
||||
resultLine.style.marginTop = "8px";
|
||||
resultLine.innerHTML = `[결과] ${analysis.suggested_path}<br>└ ${analysis.reason}`;
|
||||
logContent.appendChild(resultLine);
|
||||
|
||||
// 원본 보기 추가
|
||||
const details = document.createElement('details');
|
||||
details.style.marginTop = "5px";
|
||||
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);
|
||||
|
||||
recLabel.innerText = `추천: ${analysis.suggested_path}`;
|
||||
if(analysis.suggested_path === "분석실패") {
|
||||
recLabel.style.color = "#f21d0d";
|
||||
recLabel.style.background = "#fee9e7";
|
||||
}
|
||||
|
||||
currentFiles[index].analysis = analysis; // 결과 저장
|
||||
|
||||
} catch (e) {
|
||||
logContent.innerHTML += '<div class="log-line" style="color:red;">ERR: 분석 오류</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function confirmUpload(index) {
|
||||
const file = currentFiles[index];
|
||||
const path = file.analysis ? file.analysis.suggested_path : "선택한 탭";
|
||||
|
||||
let message = `[${file.name}] 파일을 업로드하시겠습니까?`;
|
||||
if(file.analysis && file.analysis.suggested_path !== "분석실패") {
|
||||
message = `AI가 추천한 위치로 업로드하시겠습니까?\n\n위치: ${path}`;
|
||||
}
|
||||
|
||||
if (confirm(message)) {
|
||||
alert("업로드가 완료되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
loadAttachments();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "AICodeTest",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
fastapi==0.110.0
|
||||
uvicorn==0.29.0
|
||||
playwright==1.42.0
|
||||
python-dotenv==1.0.1
|
||||
python-dotenv==1.0.1
|
||||
pypdf==4.1.0
|
||||
BIN
sample/10.교량배수시설 시공계획서 제출의 건.pdf
Normal file
BIN
sample/10.교량배수시설 시공계획서 제출의 건.pdf
Normal file
Binary file not shown.
BIN
sample/10.품질시험계획서(변경) 승인 요청[어천-공주(4차)].pdf
Normal file
BIN
sample/10.품질시험계획서(변경) 승인 요청[어천-공주(4차)].pdf
Normal file
Binary file not shown.
BIN
sample/11.하도급변경계약(토공사 및 철근콘크리트공사) 제출.pdf
Normal file
BIN
sample/11.하도급변경계약(토공사 및 철근콘크리트공사) 제출.pdf
Normal file
Binary file not shown.
BIN
sample/11.현장이탈계(안전관리자) 승인[어천~공주(4차)].pdf
Normal file
BIN
sample/11.현장이탈계(안전관리자) 승인[어천~공주(4차)].pdf
Normal file
Binary file not shown.
BIN
sample/12.건설공사의 하도급 변경계약 통보(철거공사)에 따른 검토보고[어천-공주(4차)].pdf
Normal file
BIN
sample/12.건설공사의 하도급 변경계약 통보(철거공사)에 따른 검토보고[어천-공주(4차)].pdf
Normal file
Binary file not shown.
BIN
sample/12.계측관리보고서(비탈면, 연약지반, 천안-논산 고속도로 통과구간) 제출의 건.pdf
Normal file
BIN
sample/12.계측관리보고서(비탈면, 연약지반, 천안-논산 고속도로 통과구간) 제출의 건.pdf
Normal file
Binary file not shown.
BIN
sample/35.실정보고[품질관리비 변경반영, 어천-공주(4차)].pdf
Normal file
BIN
sample/35.실정보고[품질관리비 변경반영, 어천-공주(4차)].pdf
Normal file
Binary file not shown.
BIN
sample/38.실정보고 승인알림(품질관리비 추가 반영).pdf
Normal file
BIN
sample/38.실정보고 승인알림(품질관리비 추가 반영).pdf
Normal file
Binary file not shown.
BIN
sample/53.실정보고[지적확정측량비 사용계획 보고, 어천-공주(4차)].pdf
Normal file
BIN
sample/53.실정보고[지적확정측량비 사용계획 보고, 어천-공주(4차)].pdf
Normal file
Binary file not shown.
BIN
sample/56.실정보고 승인 알림 (지적확정측량비 반영).pdf
Normal file
BIN
sample/56.실정보고 승인 알림 (지적확정측량비 반영).pdf
Normal file
Binary file not shown.
BIN
sample/8.현장대리인 변경 승인 알림[어천~공주(4차)].pdf
Normal file
BIN
sample/8.현장대리인 변경 승인 알림[어천~공주(4차)].pdf
Normal file
Binary file not shown.
BIN
sample/9.현장 정기점검에 따른 기술지원기술인 출장 요청.pdf
Normal file
BIN
sample/9.현장 정기점검에 따른 기술지원기술인 출장 요청.pdf
Normal file
Binary file not shown.
BIN
sample/9.현장기술자(현장대리인) 변경 신고서 제출.pdf
Normal file
BIN
sample/9.현장기술자(현장대리인) 변경 신고서 제출.pdf
Normal file
Binary file not shown.
BIN
server.log
Normal file
BIN
server.log
Normal file
Binary file not shown.
42
sheet.csv
Normal file
42
sheet.csv
Normal file
@@ -0,0 +1,42 @@
|
||||
[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,,,
|
||||
|
495
style/style.css
495
style/style.css
@@ -1,412 +1,123 @@
|
||||
:root {
|
||||
--primary-color: #1E5149;
|
||||
--bg-color: #FFFFFF;
|
||||
--text-main: #222222;
|
||||
--text-sub: #666666;
|
||||
--border-color: #E5E7EB;
|
||||
/* 매우 연한 회색 라인 */
|
||||
--hover-bg: #F9FAFB;
|
||||
/* Design Tokens */
|
||||
--primary-color: #1E5149; --primary-lv-0: #e9eeed; --primary-lv-1: #D2DCDB; --primary-lv-8: #193833;
|
||||
--bg-default: #FFFFFF; --bg-muted: #F9FAFB;
|
||||
--text-main: #2D3748; --text-sub: #718096; --border-color: #E2E8F0;
|
||||
--header-gradient: linear-gradient(90deg, #193833 0%, #1e5149 100%);
|
||||
--ai-gradient: linear-gradient(180deg, #da8cf1 0%, #8bb1f2 100%);
|
||||
--space-xs: 4px; --space-sm: 8px; --space-md: 16px; --space-lg: 32px; --space-xl: 64px;
|
||||
--radius-sm: 4px; --radius-lg: 8px;
|
||||
--fz-h1: 20px; --fz-h2: 16px; --fz-body: 13px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* Base Reset */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Pretendard', sans-serif; font-size: var(--fz-body); color: var(--text-main); background: var(--bg-default); min-height: 100vh; }
|
||||
a { text-decoration: none; color: inherit; }
|
||||
button { cursor: pointer; border: none; font-family: inherit; }
|
||||
|
||||
body {
|
||||
font-family: 'Pretendard', sans-serif;
|
||||
font-size: 13px;
|
||||
color: var(--text-main);
|
||||
background-color: var(--bg-color);
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
/* 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-header { margin-right: 60px; font-weight: 700; }
|
||||
.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:hover { background: var(--primary-lv-1); color: #fff; }
|
||||
.nav-item.active { background: var(--primary-lv-0); color: var(--primary-color) !important; font-weight: 700; }
|
||||
|
||||
/* 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;
|
||||
}
|
||||
/* Global Layout */
|
||||
.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 h1 { font-size: var(--fz-h1); color: var(--primary-color); }
|
||||
|
||||
.topbar-header {
|
||||
margin-right: 2.5rem;
|
||||
}
|
||||
/* 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-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: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:hover .icon { background: var(--primary-color); color: #fff; }
|
||||
|
||||
.topbar-header h2 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
/* Dashboard List & Console */
|
||||
.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; }
|
||||
.accordion-container { border-top: 1px solid var(--border-color); }
|
||||
.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-item { border-bottom: 1px solid var(--border-color); }
|
||||
.accordion-item:hover { background: var(--primary-lv-0); }
|
||||
.repo-title { font-weight: 700; color: var(--primary-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.repo-files { text-align: center; font-weight: 600; }
|
||||
.repo-log { font-size: 11px; color: var(--text-sub); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.accordion-body { display: none; padding: var(--space-lg); background: var(--bg-muted); border-top: 1px solid var(--border-color); }
|
||||
.accordion-item.active .accordion-body { display: block; }
|
||||
.status-warning { background: #fff9e6; } .status-error { background: #fee9e7; }
|
||||
.warning-text { color: #f21d0d !important; font-weight: 700; }
|
||||
|
||||
.nav-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
/* Mail Manager Refined */
|
||||
.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); }
|
||||
.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; }
|
||||
.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; }
|
||||
|
||||
.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;
|
||||
}
|
||||
.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:hover { background: var(--bg-muted); }
|
||||
.mail-item.active { background: #E9EEED; border-left: 4px solid var(--primary-color); }
|
||||
|
||||
.nav-item:hover,
|
||||
.nav-item.active {
|
||||
background-color: #E9EEED;
|
||||
color: #1E5149;
|
||||
font-weight: 500;
|
||||
}
|
||||
.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; }
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
margin-top: 36px;
|
||||
flex: 1;
|
||||
padding: 2rem 2.5rem;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.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); }
|
||||
.file-info { display: flex; align-items: center; gap: var(--space-sm); flex: 1; }
|
||||
.ai-recommend { font-size: 11px; color: #6d3dc2; background: #f1ecf9; padding: 2px 6px; border-radius: 4px; font-weight: 700; margin-left: 10px; }
|
||||
|
||||
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 {
|
||||
.ai-log-console {
|
||||
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;
|
||||
color: var(--text-sub);
|
||||
margin-bottom: 3px;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
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; }
|
||||
|
||||
.header-value {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.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-ai { background: var(--ai-gradient); }
|
||||
.btn-normal { background: var(--primary-color); }
|
||||
.btn-upload:hover { opacity: 0.9; transform: translateY(-1px); }
|
||||
|
||||
.accordion-body {
|
||||
display: none;
|
||||
padding: 1.5rem 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
/* 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; }
|
||||
|
||||
.accordion-item.active .accordion-body {
|
||||
display: block;
|
||||
}
|
||||
/* Toggle Switch */
|
||||
.switch { position: relative; display: inline-block; width: 34px; height: 20px; }
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px; }
|
||||
.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:before { transform: translateX(14px); }
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 3rem;
|
||||
}
|
||||
.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; }
|
||||
|
||||
.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;
|
||||
}
|
||||
/* Utils */
|
||||
.sync-btn { background: var(--primary-color); color: #fff; padding: 8px 16px; border-radius: var(--radius-lg); font-weight: 700; }
|
||||
.badge { background: #eee; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
|
||||
.toggle-icon { transition: 0.2s; }
|
||||
.active > .continent-header .toggle-icon, .active > .country-header .toggle-icon { transform: rotate(180deg); }
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
1229
tokens.json
Normal file
1229
tokens.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user