analyze.md - 텍스트 비교 방식으로 분석

This commit is contained in:
2026-02-26 17:49:23 +09:00
parent af9d27bee8
commit feb7cb9004
6 changed files with 1725 additions and 69 deletions

92
analyze.py Normal file
View 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

View File

@@ -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>

142
index.html Normal file
View 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
View 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>

View File

@@ -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

1229
tokens.json Normal file

File diff suppressed because it is too large Load Diff