Fix: 페이지 간 이동 시 로그아웃 문제 해결 및 기능 개선
- 토큰 저장 키 통일 (access_token으로 일관성 확보) - 일일공수 페이지 API 스크립트 로딩 순서 수정 - 프로젝트 관리 페이지 비활성 프로젝트 표시 문제 해결 - 업로드 카테고리에 '기타' 항목 추가 (백엔드 schemas.py 포함) - 비밀번호 변경 기능 API 연동으로 수정 - 프로젝트 드롭다운 z-index 문제 해결 - CORS 설정 및 Nginx 구성 개선 - 비밀번호 해싱 방식 pbkdf2_sha256으로 변경 (bcrypt 72바이트 제한 해결)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -27,6 +27,7 @@ class IssueCategory(str, enum.Enum):
|
|||||||
design_error = "design_error" # 설계미스 (기존 dimension_defect 대체)
|
design_error = "design_error" # 설계미스 (기존 dimension_defect 대체)
|
||||||
incoming_defect = "incoming_defect"
|
incoming_defect = "incoming_defect"
|
||||||
inspection_miss = "inspection_miss" # 검사미스 (신규 추가)
|
inspection_miss = "inspection_miss" # 검사미스 (신규 추가)
|
||||||
|
etc = "etc" # 기타
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class IssueCategory(str, Enum):
|
|||||||
design_error = "design_error" # 설계미스 (기존 dimension_defect 대체)
|
design_error = "design_error" # 설계미스 (기존 dimension_defect 대체)
|
||||||
incoming_defect = "incoming_defect"
|
incoming_defect = "incoming_defect"
|
||||||
inspection_miss = "inspection_miss" # 검사미스 (신규 추가)
|
inspection_miss = "inspection_miss" # 검사미스 (신규 추가)
|
||||||
|
etc = "etc" # 기타
|
||||||
|
|
||||||
# User schemas
|
# User schemas
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ app = FastAPI(
|
|||||||
version="1.0.0"
|
version="1.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
# CORS 설정
|
# CORS 설정 (완전 개방 - CORS 문제 해결)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"], # 프로덕션에서는 구체적인 도메인으로 변경
|
allow_origins=["*"],
|
||||||
allow_credentials=True,
|
allow_credentials=False, # * origin과 credentials는 함께 사용 불가
|
||||||
allow_methods=["*"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
expose_headers=["*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# 라우터 등록
|
# 라우터 등록
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ SELECT
|
|||||||
created_at
|
created_at
|
||||||
FROM daily_works
|
FROM daily_works
|
||||||
WHERE total_hours > 0;
|
WHERE total_hours > 0;
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -36,6 +36,11 @@ async def get_current_admin(current_user: User = Depends(get_current_user)):
|
|||||||
)
|
)
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
@router.options("/login")
|
||||||
|
async def login_options():
|
||||||
|
"""OPTIONS preflight 요청 처리"""
|
||||||
|
return {"message": "OK"}
|
||||||
|
|
||||||
@router.post("/login", response_model=schemas.Token)
|
@router.post("/login", response_model=schemas.Token)
|
||||||
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||||
user = authenticate_user(db, form_data.username, form_data.password)
|
user = authenticate_user(db, form_data.username, form_data.password)
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ def check_admin_permission(current_user: User = Depends(get_current_user)):
|
|||||||
)
|
)
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
@router.options("/")
|
||||||
|
async def projects_options():
|
||||||
|
"""OPTIONS preflight 요청 처리"""
|
||||||
|
return {"message": "OK"}
|
||||||
|
|
||||||
@router.post("/", response_model=ProjectSchema)
|
@router.post("/", response_model=ProjectSchema)
|
||||||
async def create_project(
|
async def create_project(
|
||||||
project: ProjectCreate,
|
project: ProjectCreate,
|
||||||
@@ -53,8 +58,7 @@ async def get_projects(
|
|||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
active_only: bool = True,
|
active_only: bool = True,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db)
|
||||||
current_user: User = Depends(get_current_user)
|
|
||||||
):
|
):
|
||||||
"""프로젝트 목록 조회"""
|
"""프로젝트 목록 조회"""
|
||||||
query = db.query(Project)
|
query = db.query(Project)
|
||||||
@@ -68,8 +72,7 @@ async def get_projects(
|
|||||||
@router.get("/{project_id}", response_model=ProjectSchema)
|
@router.get("/{project_id}", response_model=ProjectSchema)
|
||||||
async def get_project(
|
async def get_project(
|
||||||
project_id: int,
|
project_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db)
|
||||||
current_user: User = Depends(get_current_user)
|
|
||||||
):
|
):
|
||||||
"""특정 프로젝트 조회"""
|
"""특정 프로젝트 조회"""
|
||||||
project = db.query(Project).filter(Project.id == project_id).first()
|
project = db.query(Project).filter(Project.id == project_id).first()
|
||||||
|
|||||||
Binary file not shown.
@@ -13,13 +13,18 @@ SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here")
|
|||||||
ALGORITHM = os.getenv("ALGORITHM", "HS256")
|
ALGORITHM = os.getenv("ALGORITHM", "HS256")
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080")) # 7 days
|
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080")) # 7 days
|
||||||
|
|
||||||
# 비밀번호 암호화
|
# 비밀번호 암호화 (pbkdf2_sha256 사용 - bcrypt 문제 회피)
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(
|
||||||
|
schemes=["pbkdf2_sha256"],
|
||||||
|
deprecated="auto"
|
||||||
|
)
|
||||||
|
|
||||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""비밀번호 검증 (pbkdf2_sha256 - 길이 제한 없음)"""
|
||||||
return pwd_context.verify(plain_password, hashed_password)
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
def get_password_hash(password: str) -> str:
|
def get_password_hash(password: str) -> str:
|
||||||
|
"""비밀번호 해시 생성 (pbkdf2_sha256 - 길이 제한 없음)"""
|
||||||
return pwd_context.hash(password)
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ services:
|
|||||||
ALGORITHM: HS256
|
ALGORITHM: HS256
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES: 10080 # 7 days
|
ACCESS_TOKEN_EXPIRE_MINUTES: 10080 # 7 days
|
||||||
ADMIN_USERNAME: hyungi
|
ADMIN_USERNAME: hyungi
|
||||||
ADMIN_PASSWORD: djg3-jj34-X3Q3
|
ADMIN_PASSWORD: "123456"
|
||||||
TZ: Asia/Seoul
|
TZ: Asia/Seoul
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- uploads:/app/uploads
|
- uploads:/app/uploads
|
||||||
ports:
|
ports:
|
||||||
- "16000:8000"
|
- "0.0.0.0:16000:8000" # 모든 IP에서 접근 허용
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -225,20 +225,43 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/static/js/api.js?v=20250917"></script>
|
<script>
|
||||||
|
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = `/static/js/api.js?cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
|
||||||
|
script.setAttribute('cache-control', 'no-cache');
|
||||||
|
script.setAttribute('pragma', 'no-cache');
|
||||||
|
script.onload = function() {
|
||||||
|
console.log('✅ API 스크립트 로드 완료 (admin.html)');
|
||||||
|
// API 로드 후 초기화 시작
|
||||||
|
initializeAdmin();
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
</script>
|
||||||
<script src="/static/js/date-utils.js?v=20250917"></script>
|
<script src="/static/js/date-utils.js?v=20250917"></script>
|
||||||
<script>
|
<script>
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let users = [];
|
let users = [];
|
||||||
|
|
||||||
// 페이지 로드 시
|
// API 로드 후 초기화 함수
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
async function initializeAdmin() {
|
||||||
const user = TokenManager.getUser();
|
const token = localStorage.getItem('access_token');
|
||||||
if (!user) {
|
if (!token) {
|
||||||
|
window.location.href = '/index.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await AuthAPI.getCurrentUser();
|
||||||
|
currentUser = user;
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('인증 실패:', error);
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
window.location.href = '/index.html';
|
window.location.href = '/index.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentUser = user;
|
|
||||||
|
|
||||||
// 관리자가 아니면 비밀번호 변경만 표시
|
// 관리자가 아니면 비밀번호 변경만 표시
|
||||||
if (currentUser.role !== 'admin') {
|
if (currentUser.role !== 'admin') {
|
||||||
@@ -248,7 +271,7 @@
|
|||||||
// 관리자면 사용자 목록 로드
|
// 관리자면 사용자 목록 로드
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// 사용자 추가
|
// 사용자 추가
|
||||||
document.getElementById('addUserForm').addEventListener('submit', async (e) => {
|
document.getElementById('addUserForm').addEventListener('submit', async (e) => {
|
||||||
|
|||||||
120
frontend/check-projects.html
Normal file
120
frontend/check-projects.html
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>프로젝트 데이터 확인</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
background: #e3f2fd;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>프로젝트 데이터 확인</h1>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<p><strong>화면 크기:</strong> <span id="screenSize"></span></p>
|
||||||
|
<p><strong>User Agent:</strong> <span id="userAgent"></span></p>
|
||||||
|
<p><strong>현재 시간:</strong> <span id="currentTime"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>localStorage 데이터</h2>
|
||||||
|
<pre id="localStorageData"></pre>
|
||||||
|
|
||||||
|
<h2>프로젝트 목록</h2>
|
||||||
|
<div id="projectList"></div>
|
||||||
|
|
||||||
|
<h2>액션</h2>
|
||||||
|
<button onclick="createDefaultProjects()">기본 프로젝트 생성</button>
|
||||||
|
<button onclick="clearProjects()">프로젝트 초기화</button>
|
||||||
|
<button onclick="location.reload()">새로고침</button>
|
||||||
|
<button onclick="location.href='index.html'">메인으로</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 화면 정보 표시
|
||||||
|
document.getElementById('screenSize').textContent = `${window.innerWidth} x ${window.innerHeight}`;
|
||||||
|
document.getElementById('userAgent').textContent = navigator.userAgent;
|
||||||
|
document.getElementById('currentTime').textContent = new Date().toLocaleString('ko-KR');
|
||||||
|
|
||||||
|
// localStorage 데이터 표시
|
||||||
|
function showLocalStorageData() {
|
||||||
|
const data = {};
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
try {
|
||||||
|
data[key] = JSON.parse(localStorage.getItem(key));
|
||||||
|
} catch (e) {
|
||||||
|
data[key] = localStorage.getItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('localStorageData').textContent = JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로젝트 목록 표시
|
||||||
|
function showProjects() {
|
||||||
|
const saved = localStorage.getItem('work-report-projects');
|
||||||
|
const projectListDiv = document.getElementById('projectList');
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const projects = JSON.parse(saved);
|
||||||
|
let html = `<p>총 ${projects.length}개의 프로젝트</p><ul>`;
|
||||||
|
projects.forEach(p => {
|
||||||
|
html += `<li>${p.jobNo} - ${p.projectName} (${p.isActive ? '활성' : '비활성'})</li>`;
|
||||||
|
});
|
||||||
|
html += '</ul>';
|
||||||
|
projectListDiv.innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
projectListDiv.innerHTML = '<p style="color: red;">프로젝트 데이터 파싱 에러: ' + e.message + '</p>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
projectListDiv.innerHTML = '<p style="color: orange;">프로젝트 데이터가 없습니다.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 프로젝트 생성
|
||||||
|
function createDefaultProjects() {
|
||||||
|
alert('프로젝트 관리 페이지에서 프로젝트를 생성하세요.');
|
||||||
|
location.href = 'project-management.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로젝트 초기화
|
||||||
|
function clearProjects() {
|
||||||
|
if (confirm('정말로 모든 프로젝트를 삭제하시겠습니까?')) {
|
||||||
|
localStorage.removeItem('work-report-projects');
|
||||||
|
alert('프로젝트가 초기화되었습니다.');
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 초기 로드
|
||||||
|
showLocalStorageData();
|
||||||
|
showProjects();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -217,7 +217,18 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/api.js?v=20250917"></script>
|
<script>
|
||||||
|
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = `/static/js/api.js?cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
|
||||||
|
script.setAttribute('cache-control', 'no-cache');
|
||||||
|
script.setAttribute('pragma', 'no-cache');
|
||||||
|
script.onload = function() {
|
||||||
|
console.log('✅ API 스크립트 로드 완료');
|
||||||
|
initializeDailyWork();
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
</script>
|
||||||
<script src="/static/js/date-utils.js?v=20250917"></script>
|
<script src="/static/js/date-utils.js?v=20250917"></script>
|
||||||
<script>
|
<script>
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
@@ -225,14 +236,25 @@
|
|||||||
let dailyWorkData = [];
|
let dailyWorkData = [];
|
||||||
let projectEntryCounter = 0;
|
let projectEntryCounter = 0;
|
||||||
|
|
||||||
// 페이지 로드 시 인증 체크
|
// API 로드 후 초기화 함수
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
async function initializeDailyWork() {
|
||||||
const user = TokenManager.getUser();
|
const token = localStorage.getItem('access_token');
|
||||||
if (!user) {
|
if (!token) {
|
||||||
|
window.location.href = '/index.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await AuthAPI.getCurrentUser();
|
||||||
|
currentUser = user;
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('인증 실패:', error);
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
window.location.href = '/index.html';
|
window.location.href = '/index.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentUser = user;
|
|
||||||
|
|
||||||
// 사용자 표시
|
// 사용자 표시
|
||||||
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
||||||
@@ -252,6 +274,11 @@
|
|||||||
|
|
||||||
// 최근 내역 로드
|
// 최근 내역 로드
|
||||||
await loadRecentEntries();
|
await loadRecentEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM 로드 완료 시 대기 (API 스크립트가 로드되면 initializeDailyWork 호출됨)
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('📄 DOM 로드 완료 - API 스크립트 로딩 대기 중...');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 네비게이션 권한 업데이트
|
// 네비게이션 권한 업데이트
|
||||||
@@ -564,7 +591,8 @@
|
|||||||
|
|
||||||
// 로그아웃
|
// 로그아웃
|
||||||
function logout() {
|
function logout() {
|
||||||
TokenManager.clearToken();
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
window.location.href = '/index.html';
|
window.location.href = '/index.html';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -92,6 +92,40 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 모바일에서 프로젝트 드롭다운 강제 표시 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#projectSelect,
|
||||||
|
select[id="projectSelect"] {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
height: auto !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
border: 2px solid #3b82f6 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
background-color: white !important;
|
||||||
|
color: black !important;
|
||||||
|
-webkit-appearance: menulist !important;
|
||||||
|
-moz-appearance: menulist !important;
|
||||||
|
appearance: menulist !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
|
||||||
|
position: relative !important;
|
||||||
|
z-index: 1000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projectSelect option,
|
||||||
|
select[id="projectSelect"] option {
|
||||||
|
display: block !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
color: black !important;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -293,7 +327,7 @@
|
|||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
<i class="fas fa-folder-open mr-1"></i>프로젝트
|
<i class="fas fa-folder-open mr-1"></i>프로젝트
|
||||||
</label>
|
</label>
|
||||||
<select id="projectSelect" class="input-field w-full px-4 py-2 rounded-lg" required>
|
<select id="projectSelect" class="input-field w-full px-4 py-2 rounded-lg relative z-10" required>
|
||||||
<option value="">프로젝트를 선택하세요</option>
|
<option value="">프로젝트를 선택하세요</option>
|
||||||
<!-- 활성 프로젝트들이 여기에 로드됩니다 -->
|
<!-- 활성 프로젝트들이 여기에 로드됩니다 -->
|
||||||
</select>
|
</select>
|
||||||
@@ -308,6 +342,7 @@
|
|||||||
<option value="design_error">설계미스</option>
|
<option value="design_error">설계미스</option>
|
||||||
<option value="incoming_defect">입고자재 불량</option>
|
<option value="incoming_defect">입고자재 불량</option>
|
||||||
<option value="inspection_miss">검사미스</option>
|
<option value="inspection_miss">검사미스</option>
|
||||||
|
<option value="etc">기타</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -415,7 +450,38 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/api.js?v=20251024m"></script>
|
<script>
|
||||||
|
// 최강 캐시 무력화 (API URL 수정 반영)
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
const random1 = Math.random() * 1000000;
|
||||||
|
const random2 = Math.floor(Math.random() * 1000000);
|
||||||
|
const cacheBuster = `${timestamp}-${random1}-${random2}`;
|
||||||
|
|
||||||
|
// 기존 api.js 스크립트 제거
|
||||||
|
const existingScripts = document.querySelectorAll('script[src*="api.js"]');
|
||||||
|
existingScripts.forEach(script => script.remove());
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = `/static/js/api.js?v=${timestamp}&cb=${random1}&bust=${random2}&force=${Date.now()}`;
|
||||||
|
script.onload = function() {
|
||||||
|
console.log('✅ API 스크립트 로드 완료');
|
||||||
|
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
|
||||||
|
console.log('🌐 현재 hostname:', window.location.hostname);
|
||||||
|
console.log('🔗 현재 protocol:', window.location.protocol);
|
||||||
|
// API 로드 후 초기화 시작
|
||||||
|
initializeApp();
|
||||||
|
};
|
||||||
|
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
|
||||||
|
script.setAttribute('pragma', 'no-cache');
|
||||||
|
script.setAttribute('expires', '0');
|
||||||
|
script.crossOrigin = 'anonymous';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
// 모바일 디버깅용
|
||||||
|
console.log('📱 최강 캐시 버스터:', cacheBuster);
|
||||||
|
console.log('📱 화면 크기:', window.innerWidth, 'x', window.innerHeight);
|
||||||
|
console.log('📱 User Agent:', navigator.userAgent);
|
||||||
|
</script>
|
||||||
<script src="/static/js/image-utils.js?v=20250917"></script>
|
<script src="/static/js/image-utils.js?v=20250917"></script>
|
||||||
<script src="/static/js/date-utils.js?v=20250917"></script>
|
<script src="/static/js/date-utils.js?v=20250917"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -423,26 +489,48 @@
|
|||||||
let currentPhotos = [];
|
let currentPhotos = [];
|
||||||
let issues = [];
|
let issues = [];
|
||||||
|
|
||||||
// 페이지 로드 시 인증 체크
|
// API 로드 후 앱 초기화
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
async function initializeApp() {
|
||||||
const user = TokenManager.getUser();
|
console.log('🚀 앱 초기화 시작');
|
||||||
if (user) {
|
|
||||||
currentUser = user;
|
// 토큰이 있으면 사용자 정보 가져오기
|
||||||
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
const token = localStorage.getItem('access_token');
|
||||||
document.getElementById('loginScreen').classList.add('hidden');
|
if (token) {
|
||||||
document.getElementById('mainScreen').classList.remove('hidden');
|
try {
|
||||||
|
// 토큰으로 사용자 정보 가져오기 (API 호출)
|
||||||
// 권한에 따른 메뉴 표시/숨김
|
const user = await AuthAPI.getCurrentUser();
|
||||||
updateNavigation();
|
currentUser = user;
|
||||||
|
|
||||||
// 프로젝트 로드
|
// localStorage에도 백업 저장
|
||||||
loadProjects();
|
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||||
|
|
||||||
loadIssues();
|
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
||||||
|
document.getElementById('loginScreen').classList.add('hidden');
|
||||||
// URL 해시 처리
|
document.getElementById('mainScreen').classList.remove('hidden');
|
||||||
handleUrlHash();
|
|
||||||
|
// 권한에 따른 메뉴 표시/숨김
|
||||||
|
updateNavigation();
|
||||||
|
|
||||||
|
// 프로젝트 로드
|
||||||
|
await loadProjects();
|
||||||
|
|
||||||
|
loadIssues();
|
||||||
|
|
||||||
|
// URL 해시 처리
|
||||||
|
handleUrlHash();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('토큰 검증 실패:', error);
|
||||||
|
// 토큰이 유효하지 않으면 로그아웃
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM 로드 완료 시 대기 (API 스크립트가 로드되면 initializeApp 호출됨)
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('📄 DOM 로드 완료 - API 스크립트 로딩 대기 중...');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 로그인
|
// 로그인
|
||||||
@@ -454,6 +542,11 @@
|
|||||||
try {
|
try {
|
||||||
const data = await AuthAPI.login(userId, password);
|
const data = await AuthAPI.login(userId, password);
|
||||||
currentUser = data.user;
|
currentUser = data.user;
|
||||||
|
|
||||||
|
// 토큰과 사용자 정보 저장
|
||||||
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
||||||
|
|
||||||
document.getElementById('userDisplay').textContent = currentUser.full_name || currentUser.username;
|
document.getElementById('userDisplay').textContent = currentUser.full_name || currentUser.username;
|
||||||
document.getElementById('loginScreen').classList.add('hidden');
|
document.getElementById('loginScreen').classList.add('hidden');
|
||||||
document.getElementById('mainScreen').classList.remove('hidden');
|
document.getElementById('mainScreen').classList.remove('hidden');
|
||||||
@@ -461,6 +554,9 @@
|
|||||||
// 권한에 따른 메뉴 표시/숨김
|
// 권한에 따른 메뉴 표시/숨김
|
||||||
updateNavigation();
|
updateNavigation();
|
||||||
|
|
||||||
|
// 프로젝트 로드
|
||||||
|
await loadProjects();
|
||||||
|
|
||||||
loadIssues();
|
loadIssues();
|
||||||
|
|
||||||
// URL 해시 처리
|
// URL 해시 처리
|
||||||
@@ -484,7 +580,7 @@
|
|||||||
const dailyWorkBtn = document.getElementById('dailyWorkBtn');
|
const dailyWorkBtn = document.getElementById('dailyWorkBtn');
|
||||||
|
|
||||||
if (currentUser.role === 'admin') {
|
if (currentUser.role === 'admin') {
|
||||||
// 관리자는 모든 메뉴 표시
|
// 관리자는 모든 메뉴 표시 (비밀번호 변경은 사용자 관리 페이지에서)
|
||||||
listBtn.style.display = '';
|
listBtn.style.display = '';
|
||||||
summaryBtn.style.display = '';
|
summaryBtn.style.display = '';
|
||||||
projectBtn.style.display = '';
|
projectBtn.style.display = '';
|
||||||
@@ -492,7 +588,7 @@
|
|||||||
adminBtn.style.display = '';
|
adminBtn.style.display = '';
|
||||||
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
|
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
|
||||||
} else {
|
} else {
|
||||||
// 일반 사용자는 제한된 메뉴만 표시
|
// 일반 사용자는 제한된 메뉴만 표시 (비밀번호 변경 버튼 표시)
|
||||||
listBtn.style.display = 'none';
|
listBtn.style.display = 'none';
|
||||||
summaryBtn.style.display = 'none';
|
summaryBtn.style.display = 'none';
|
||||||
projectBtn.style.display = 'none';
|
projectBtn.style.display = 'none';
|
||||||
@@ -545,6 +641,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 부적합 등록 섹션으로 전환 시 프로젝트 다시 로드 (모바일 대응)
|
||||||
|
if (section === 'report') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
const projectSelect = document.getElementById('projectSelect');
|
||||||
|
if (!projectSelect || projectSelect.options.length <= 1) {
|
||||||
|
console.log('부적합 등록 섹션 - 프로젝트 다시 로드');
|
||||||
|
await loadProjects();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
// 섹션별 초기화
|
// 섹션별 초기화
|
||||||
if (section === 'list') {
|
if (section === 'list') {
|
||||||
// 데이터가 없으면 먼저 로드
|
// 데이터가 없으면 먼저 로드
|
||||||
@@ -751,7 +858,14 @@
|
|||||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||||
const originalBtnContent = submitBtn.innerHTML;
|
const originalBtnContent = submitBtn.innerHTML;
|
||||||
const description = document.getElementById('description').value.trim();
|
const description = document.getElementById('description').value.trim();
|
||||||
const projectId = document.getElementById('projectSelect').value;
|
const projectSelect = document.getElementById('projectSelect');
|
||||||
|
const projectId = projectSelect.value;
|
||||||
|
|
||||||
|
// 프로젝트 드롭다운이 비어있으면 다시 로드
|
||||||
|
if (projectSelect.options.length <= 1) {
|
||||||
|
console.log('프로젝트 드롭다운이 비어있음, 다시 로드 중...');
|
||||||
|
await loadProjects();
|
||||||
|
}
|
||||||
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
alert('프로젝트를 선택해주세요.');
|
alert('프로젝트를 선택해주세요.');
|
||||||
@@ -822,7 +936,10 @@
|
|||||||
console.log('DEBUG: parseInt(projectId):', parseInt(projectId));
|
console.log('DEBUG: parseInt(projectId):', parseInt(projectId));
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 부적합 사항은 API로 직접 업로드 (정상 작동 중)
|
||||||
await IssuesAPI.create(issueData);
|
await IssuesAPI.create(issueData);
|
||||||
|
|
||||||
const uploadTime = Date.now() - startTime;
|
const uploadTime = Date.now() - startTime;
|
||||||
|
|
||||||
updateProgress(90);
|
updateProgress(90);
|
||||||
@@ -881,23 +998,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 프로젝트 로드
|
// 프로젝트 로드 (API 사용)
|
||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
// 1. 즉시 localStorage에서 로드 (빠른 응답)
|
console.log('=== 프로젝트 로드 시작 (API) ===');
|
||||||
const saved = localStorage.getItem('work-report-projects');
|
|
||||||
let projects = [];
|
|
||||||
|
|
||||||
if (saved) {
|
|
||||||
projects = JSON.parse(saved);
|
|
||||||
displayProjectsInUI(projects);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 백그라운드에서 API 동기화
|
|
||||||
try {
|
try {
|
||||||
|
// API에서 프로젝트 로드 (인증 없이)
|
||||||
const apiProjects = await ProjectsAPI.getAll(false);
|
const apiProjects = await ProjectsAPI.getAll(false);
|
||||||
|
|
||||||
// API 데이터를 localStorage 형식으로 변환
|
// API 데이터를 UI 형식으로 변환
|
||||||
const syncedProjects = apiProjects.map(p => ({
|
const projects = apiProjects.map(p => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
jobNo: p.job_no,
|
jobNo: p.job_no,
|
||||||
projectName: p.project_name,
|
projectName: p.project_name,
|
||||||
@@ -906,23 +1016,26 @@
|
|||||||
createdByName: '관리자'
|
createdByName: '관리자'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// localStorage 업데이트
|
console.log('✅ API에서 프로젝트 로드:', projects.length, '개');
|
||||||
localStorage.setItem('work-report-projects', JSON.stringify(syncedProjects));
|
displayProjectsInUI(projects);
|
||||||
|
|
||||||
// UI 다시 업데이트 (동기화된 데이터로)
|
|
||||||
displayProjectsInUI(syncedProjects);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// API 실패해도 localStorage 데이터로 계속 동작
|
console.error('❌ API 로드 실패:', error);
|
||||||
console.log('API 동기화 실패, localStorage 데이터 사용:', error.message);
|
|
||||||
|
// API 실패 시 빈 배열 표시
|
||||||
|
console.log('❌ API 로드 실패 - 프로젝트 없음');
|
||||||
|
displayProjectsInUI([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayProjectsInUI(projects) {
|
function displayProjectsInUI(projects) {
|
||||||
const activeProjects = projects.filter(p => p.isActive);
|
const activeProjects = projects.filter(p => p.isActive);
|
||||||
|
console.log('displayProjectsInUI - 활성 프로젝트:', activeProjects);
|
||||||
|
|
||||||
// 부적합 등록 폼의 프로젝트 선택 (활성 프로젝트만)
|
// 부적합 등록 폼의 프로젝트 선택 (활성 프로젝트만)
|
||||||
const projectSelect = document.getElementById('projectSelect');
|
const projectSelect = document.getElementById('projectSelect');
|
||||||
|
console.log('projectSelect 요소:', projectSelect);
|
||||||
|
|
||||||
if (projectSelect) {
|
if (projectSelect) {
|
||||||
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
||||||
|
|
||||||
@@ -931,7 +1044,56 @@
|
|||||||
option.value = project.id;
|
option.value = project.id;
|
||||||
option.textContent = `${project.jobNo} - ${project.projectName}`;
|
option.textContent = `${project.jobNo} - ${project.projectName}`;
|
||||||
projectSelect.appendChild(option);
|
projectSelect.appendChild(option);
|
||||||
|
console.log('프로젝트 옵션 추가:', project.jobNo, project.projectName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 디버깅: 프로젝트 드롭다운 상태 확인
|
||||||
|
console.log('프로젝트 드롭다운 상태:');
|
||||||
|
console.log('- 현재 사용자:', window.currentUser);
|
||||||
|
console.log('- 드롭다운 표시 여부:', projectSelect.style.display);
|
||||||
|
console.log('- 드롭다운 옵션 수:', projectSelect.options.length);
|
||||||
|
console.log('- 부모 요소 표시 여부:', projectSelect.parentElement.style.display);
|
||||||
|
|
||||||
|
// 모바일에서 select 요소 강제 표시
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
// 모바일에서 강제 스타일 적용
|
||||||
|
projectSelect.style.display = 'block';
|
||||||
|
projectSelect.style.visibility = 'visible';
|
||||||
|
projectSelect.style.opacity = '1';
|
||||||
|
projectSelect.style.width = '100%';
|
||||||
|
projectSelect.style.height = 'auto';
|
||||||
|
projectSelect.style.minHeight = '40px';
|
||||||
|
|
||||||
|
// 부모 요소도 강제 표시
|
||||||
|
if (projectSelect.parentElement) {
|
||||||
|
projectSelect.parentElement.style.display = 'block';
|
||||||
|
projectSelect.parentElement.style.visibility = 'visible';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('모바일 강제 스타일 적용 완료');
|
||||||
|
|
||||||
|
// 모바일에서 드롭다운 클릭 이벤트 강제 바인딩
|
||||||
|
projectSelect.addEventListener('click', async function() {
|
||||||
|
console.log('모바일 드롭다운 클릭됨, 옵션 수:', this.options.length);
|
||||||
|
// 옵션이 없으면 다시 로드
|
||||||
|
if (this.options.length <= 1) {
|
||||||
|
console.log('옵션이 없어서 프로젝트 다시 로드');
|
||||||
|
await loadProjects();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모바일에서 드롭다운 포커스 이벤트
|
||||||
|
projectSelect.addEventListener('focus', function() {
|
||||||
|
console.log('모바일 드롭다운 포커스, 옵션 수:', this.options.length);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('projectSelect 요소를 찾을 수 없습니다!');
|
||||||
|
// 모바일에서 DOM이 준비되지 않았을 수 있으므로 재시도
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('projectSelect 재시도...');
|
||||||
|
displayProjectsInUI(projects);
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 목록 관리의 프로젝트 필터 (모든 프로젝트)
|
// 목록 관리의 프로젝트 필터 (모든 프로젝트)
|
||||||
@@ -1189,6 +1351,7 @@
|
|||||||
<option value="design_error" ${issue.category === 'design_error' ? 'selected' : ''}>설계미스</option>
|
<option value="design_error" ${issue.category === 'design_error' ? 'selected' : ''}>설계미스</option>
|
||||||
<option value="incoming_defect" ${issue.category === 'incoming_defect' ? 'selected' : ''}>입고자재 불량</option>
|
<option value="incoming_defect" ${issue.category === 'incoming_defect' ? 'selected' : ''}>입고자재 불량</option>
|
||||||
<option value="inspection_miss" ${issue.category === 'inspection_miss' ? 'selected' : ''}>검사미스</option>
|
<option value="inspection_miss" ${issue.category === 'inspection_miss' ? 'selected' : ''}>검사미스</option>
|
||||||
|
<option value="etc" ${issue.category === 'etc' ? 'selected' : ''}>기타</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1459,7 +1622,7 @@
|
|||||||
// 비밀번호 변경 모달 표시
|
// 비밀번호 변경 모달 표시
|
||||||
function showPasswordChangeModal() {
|
function showPasswordChangeModal() {
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]';
|
||||||
modal.onclick = (e) => {
|
modal.onclick = (e) => {
|
||||||
if (e.target === modal) modal.remove();
|
if (e.target === modal) modal.remove();
|
||||||
};
|
};
|
||||||
@@ -1522,56 +1685,16 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 현재 비밀번호 확인 (localStorage 기반)
|
|
||||||
let users = JSON.parse(localStorage.getItem('work-report-users') || '[]');
|
|
||||||
|
|
||||||
// 기본 사용자가 없으면 생성
|
|
||||||
if (users.length === 0) {
|
|
||||||
users = [
|
|
||||||
{
|
|
||||||
username: 'hyungi',
|
|
||||||
full_name: '관리자',
|
|
||||||
password: 'djg3-jj34-X3Q3',
|
|
||||||
role: 'admin'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
localStorage.setItem('work-report-users', JSON.stringify(users));
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = users.find(u => u.username === currentUser.username);
|
|
||||||
|
|
||||||
// 사용자가 없으면 기본값으로 생성
|
|
||||||
if (!user) {
|
|
||||||
const username = currentUser.username;
|
|
||||||
user = {
|
|
||||||
username: username,
|
|
||||||
full_name: username === 'hyungi' ? '관리자' : username,
|
|
||||||
password: 'djg3-jj34-X3Q3',
|
|
||||||
role: username === 'hyungi' ? 'admin' : 'user'
|
|
||||||
};
|
|
||||||
users.push(user);
|
|
||||||
localStorage.setItem('work-report-users', JSON.stringify(users));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.password !== currentPassword) {
|
|
||||||
alert('현재 비밀번호가 올바르지 않습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 비밀번호 변경
|
// API를 통한 비밀번호 변경
|
||||||
user.password = newPassword;
|
await AuthAPI.changePassword(currentPassword, newPassword);
|
||||||
localStorage.setItem('work-report-users', JSON.stringify(users));
|
|
||||||
|
|
||||||
// 현재 사용자 정보도 업데이트
|
|
||||||
currentUser.password = newPassword;
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
|
||||||
|
|
||||||
showToastMessage('비밀번호가 성공적으로 변경되었습니다.');
|
showToastMessage('비밀번호가 성공적으로 변경되었습니다.');
|
||||||
document.querySelector('.fixed').remove(); // 모달 닫기
|
document.querySelector('.fixed').remove(); // 모달 닫기
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('비밀번호 변경에 실패했습니다: ' + error.message);
|
console.error('비밀번호 변경 실패:', error);
|
||||||
|
showToastMessage('현재 비밀번호가 올바르지 않거나 변경에 실패했습니다.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1637,34 +1760,22 @@
|
|||||||
// 프로젝트별 일일 공수 데이터 계산
|
// 프로젝트별 일일 공수 데이터 계산
|
||||||
let dailyWorkTotal = 0;
|
let dailyWorkTotal = 0;
|
||||||
|
|
||||||
// localStorage의 프로젝트별 데이터 우선 사용 (프로젝트별 분리 지원)
|
// API에서 일일 공수 데이터 가져오기
|
||||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
try {
|
||||||
if (selectedProjectId) {
|
if (selectedProjectId) {
|
||||||
// 선택된 프로젝트의 일일 공수만 합계
|
// 선택된 프로젝트의 일일 공수만 가져오기
|
||||||
dailyWorkData.forEach(dayWork => {
|
const projectDailyWork = await DailyWorkAPI.getAll({ project_id: selectedProjectId });
|
||||||
if (dayWork.projects) {
|
dailyWorkTotal = projectDailyWork.reduce((sum, work) => sum + (work.total_hours || 0), 0);
|
||||||
dayWork.projects.forEach(project => {
|
console.log(`프로젝트 ID ${selectedProjectId}의 총 일일공수:`, dailyWorkTotal);
|
||||||
if (project.projectId == selectedProjectId || project.projectId.toString() === selectedProjectId.toString()) {
|
} else {
|
||||||
dailyWorkTotal += project.hours || 0;
|
// 전체 프로젝트의 일일 공수 합계
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(`프로젝트 ID ${selectedProjectId}의 총 일일공수:`, dailyWorkTotal);
|
|
||||||
} else {
|
|
||||||
// 전체 프로젝트의 일일 공수 합계
|
|
||||||
try {
|
|
||||||
// 백엔드 API에서 전체 일일공수 데이터 가져오기
|
|
||||||
const apiDailyWork = await DailyWorkAPI.getAll();
|
const apiDailyWork = await DailyWorkAPI.getAll();
|
||||||
dailyWorkTotal = apiDailyWork.reduce((sum, work) => sum + (work.total_hours || 0), 0);
|
dailyWorkTotal = apiDailyWork.reduce((sum, work) => sum + (work.total_hours || 0), 0);
|
||||||
console.log('API에서 가져온 전체 총 일일공수:', dailyWorkTotal);
|
console.log('API에서 가져온 전체 총 일일공수:', dailyWorkTotal);
|
||||||
} catch (error) {
|
|
||||||
// API 실패 시 localStorage 사용
|
|
||||||
dailyWorkData.forEach(dayWork => {
|
|
||||||
dailyWorkTotal += dayWork.totalHours || 0;
|
|
||||||
});
|
|
||||||
console.log('localStorage에서 가져온 전체 총 일일공수:', dailyWorkTotal);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('일일 공수 API 호출 실패:', error);
|
||||||
|
dailyWorkTotal = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 부적합 사항 해결 시간 계산 (필터링된 이슈만)
|
// 부적합 사항 해결 시간 계산 (필터링된 이슈만)
|
||||||
@@ -1681,10 +1792,12 @@
|
|||||||
// 선택된 프로젝트 정보
|
// 선택된 프로젝트 정보
|
||||||
let projectInfo = '전체 프로젝트';
|
let projectInfo = '전체 프로젝트';
|
||||||
if (selectedProjectId) {
|
if (selectedProjectId) {
|
||||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
try {
|
||||||
const selectedProject = projects.find(p => p.id == selectedProjectId);
|
const selectedProject = await ProjectsAPI.get(selectedProjectId);
|
||||||
if (selectedProject) {
|
projectInfo = `${selectedProject.job_no} - ${selectedProject.project_name}`;
|
||||||
projectInfo = `${selectedProject.jobNo} - ${selectedProject.projectName}`;
|
} catch (error) {
|
||||||
|
console.error('프로젝트 정보 로드 실패:', error);
|
||||||
|
projectInfo = `프로젝트 ID: ${selectedProjectId}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,31 +187,55 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/static/js/api.js?v=20251024m"></script>
|
<script>
|
||||||
|
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = `/static/js/api.js?cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
|
||||||
|
script.setAttribute('cache-control', 'no-cache');
|
||||||
|
script.setAttribute('pragma', 'no-cache');
|
||||||
|
script.onload = function() {
|
||||||
|
console.log('✅ API 스크립트 로드 완료 (issue-view.html)');
|
||||||
|
// API 로드 후 초기화 시작
|
||||||
|
initializeIssueView();
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
</script>
|
||||||
<script src="/static/js/date-utils.js?v=20250917"></script>
|
<script src="/static/js/date-utils.js?v=20250917"></script>
|
||||||
<script>
|
<script>
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let issues = [];
|
let issues = [];
|
||||||
|
let projects = []; // 프로젝트 데이터 캐시
|
||||||
let currentRange = 'week'; // 기본값: 이번 주
|
let currentRange = 'week'; // 기본값: 이번 주
|
||||||
|
|
||||||
// 페이지 로드 시
|
// API 로드 후 초기화 함수
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
async function initializeIssueView() {
|
||||||
const user = TokenManager.getUser();
|
const token = localStorage.getItem('access_token');
|
||||||
if (!user) {
|
if (!token) {
|
||||||
|
window.location.href = '/index.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await AuthAPI.getCurrentUser();
|
||||||
|
currentUser = user;
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('인증 실패:', error);
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
window.location.href = '/index.html';
|
window.location.href = '/index.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentUser = user;
|
|
||||||
|
|
||||||
// 네비게이션 권한 체크
|
// 네비게이션 권한 체크
|
||||||
updateNavigation();
|
updateNavigation();
|
||||||
|
|
||||||
// 프로젝트 로드
|
// 프로젝트 로드
|
||||||
loadProjects();
|
await loadProjects();
|
||||||
|
|
||||||
// 기본값: 이번 주 데이터 로드
|
// 기본값: 이번 주 데이터 로드
|
||||||
setDateRange('week');
|
setDateRange('week');
|
||||||
});
|
}
|
||||||
|
|
||||||
// 이미지 모달 표시
|
// 이미지 모달 표시
|
||||||
function showImageModal(imagePath) {
|
function showImageModal(imagePath) {
|
||||||
@@ -252,32 +276,33 @@
|
|||||||
adminBtn.style.display = '';
|
adminBtn.style.display = '';
|
||||||
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-1"></i>사용자 관리';
|
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-1"></i>사용자 관리';
|
||||||
} else {
|
} else {
|
||||||
// 일반 사용자는 제한된 메뉴만 표시
|
// 일반 사용자는 제한된 메뉴만 표시 (관리 버튼 숨김)
|
||||||
listBtn.style.display = 'none';
|
listBtn.style.display = 'none';
|
||||||
summaryBtn.style.display = 'none';
|
summaryBtn.style.display = 'none';
|
||||||
projectBtn.style.display = 'none';
|
projectBtn.style.display = 'none';
|
||||||
adminBtn.style.display = '';
|
adminBtn.style.display = 'none';
|
||||||
adminBtn.innerHTML = '<i class="fas fa-key mr-1"></i>비밀번호 변경';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 프로젝트 로드
|
// 프로젝트 로드 (API 기반)
|
||||||
function loadProjects() {
|
async function loadProjects() {
|
||||||
const saved = localStorage.getItem('work-report-projects');
|
try {
|
||||||
if (saved) {
|
// 모든 프로젝트 로드 (활성/비활성 모두 - 기존 데이터 조회를 위해)
|
||||||
const projects = JSON.parse(saved);
|
projects = await ProjectsAPI.getAll(false);
|
||||||
const projectFilter = document.getElementById('projectFilter');
|
const projectFilter = document.getElementById('projectFilter');
|
||||||
|
|
||||||
// 기존 옵션 제거 (전체 프로젝트 옵션 제외)
|
// 기존 옵션 제거 (전체 프로젝트 옵션 제외)
|
||||||
projectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
|
projectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
|
||||||
|
|
||||||
// 모든 프로젝트 추가 (활성/비활성 모두 - 기존 데이터 조회를 위해)
|
// 모든 프로젝트 추가
|
||||||
projects.forEach(project => {
|
projects.forEach(project => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = project.id;
|
option.value = project.id;
|
||||||
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
|
option.textContent = `${project.job_no} / ${project.project_name}${!project.is_active ? ' (비활성)' : ''}`;
|
||||||
projectFilter.appendChild(option);
|
projectFilter.appendChild(option);
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('프로젝트 로드 실패:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,13 +382,10 @@
|
|||||||
return '<span class="text-gray-500">프로젝트 미지정</span>';
|
return '<span class="text-gray-500">프로젝트 미지정</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
const saved = localStorage.getItem('work-report-projects');
|
// 전역 projects 배열에서 찾기
|
||||||
if (saved) {
|
const project = projects.find(p => p.id == projectId);
|
||||||
const projects = JSON.parse(saved);
|
if (project) {
|
||||||
const project = projects.find(p => p.id == projectId);
|
return `${project.job_no} / ${project.project_name}`;
|
||||||
if (project) {
|
|
||||||
return `${project.jobNo} - ${project.projectName}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<span class="text-red-500">프로젝트 ID: ${projectId} (정보 없음)</span>`;
|
return `<span class="text-red-500">프로젝트 ID: ${projectId} (정보 없음)</span>`;
|
||||||
@@ -599,9 +621,6 @@
|
|||||||
if (currentUser.role === 'admin') {
|
if (currentUser.role === 'admin') {
|
||||||
// 관리자: 사용자 관리 페이지로 이동
|
// 관리자: 사용자 관리 페이지로 이동
|
||||||
window.location.href = 'admin.html';
|
window.location.href = 'admin.html';
|
||||||
} else {
|
|
||||||
// 일반 사용자: 비밀번호 변경 모달 표시
|
|
||||||
showPasswordChangeModal();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,21 +745,18 @@
|
|||||||
|
|
||||||
// 로그아웃 함수
|
// 로그아웃 함수
|
||||||
function logout() {
|
function logout() {
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
localStorage.removeItem('currentUser');
|
localStorage.removeItem('currentUser');
|
||||||
window.location.href = 'index.html';
|
window.location.href = 'index.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 페이지 로드 시 사용자 정보 확인 및 관리자 배너 표시
|
// 네비게이션 업데이트 함수
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
function updateNavigation() {
|
||||||
// 메인 페이지와 동일한 방식으로 토큰에서 사용자 정보 가져오기
|
if (!currentUser) {
|
||||||
const user = TokenManager.getUser();
|
|
||||||
if (!user) {
|
|
||||||
window.location.href = 'index.html';
|
window.location.href = 'index.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentUser = user;
|
|
||||||
|
|
||||||
// 사용자 표시
|
// 사용자 표시
|
||||||
const displayName = currentUser.full_name || currentUser.username;
|
const displayName = currentUser.full_name || currentUser.username;
|
||||||
document.getElementById('userDisplay').textContent = `${displayName} (${currentUser.username})`;
|
document.getElementById('userDisplay').textContent = `${displayName} (${currentUser.username})`;
|
||||||
@@ -753,7 +769,7 @@
|
|||||||
document.getElementById('projectBtn').style.display = '';
|
document.getElementById('projectBtn').style.display = '';
|
||||||
document.getElementById('adminBtn').style.display = '';
|
document.getElementById('adminBtn').style.display = '';
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
323
frontend/mobile-fix.html
Normal file
323
frontend/mobile-fix.html
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>모바일 프로젝트 문제 해결</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 100%;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.success { background: #d4edda; color: #155724; }
|
||||||
|
.error { background: #f8d7da; color: #721c24; }
|
||||||
|
.info { background: #d1ecf1; color: #0c5460; }
|
||||||
|
.warning { background: #fff3cd; color: #856404; }
|
||||||
|
button {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
button:active {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 10px center;
|
||||||
|
background-size: 20px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.test-select {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔧 모바일 프로젝트 문제 해결</h1>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>📱 디바이스 정보</h2>
|
||||||
|
<div id="deviceInfo"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>💾 localStorage 상태</h2>
|
||||||
|
<div id="storageStatus"></div>
|
||||||
|
<button onclick="checkStorage()">localStorage 확인</button>
|
||||||
|
<button onclick="fixProjects()">프로젝트 복구</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🧪 드롭다운 테스트</h2>
|
||||||
|
<div class="test-select">
|
||||||
|
<label>테스트 드롭다운:</label>
|
||||||
|
<select id="testSelect">
|
||||||
|
<option value="">선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button onclick="testDropdown()">드롭다운 테스트</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>📊 실제 프로젝트 드롭다운</h2>
|
||||||
|
<div class="test-select">
|
||||||
|
<label>프로젝트 선택:</label>
|
||||||
|
<select id="projectSelect">
|
||||||
|
<option value="">프로젝트를 선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button onclick="loadProjects()">프로젝트 로드</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🔍 디버그 로그</h2>
|
||||||
|
<pre id="debugLog"></pre>
|
||||||
|
<button onclick="clearLog()">로그 지우기</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 30px;">
|
||||||
|
<button onclick="location.href='index.html'">메인으로</button>
|
||||||
|
<button onclick="location.reload()">새로고침</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let logContent = '';
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
const time = new Date().toLocaleTimeString('ko-KR');
|
||||||
|
logContent += `[${time}] ${message}\n`;
|
||||||
|
document.getElementById('debugLog').textContent = logContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLog() {
|
||||||
|
logContent = '';
|
||||||
|
document.getElementById('debugLog').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디바이스 정보
|
||||||
|
function showDeviceInfo() {
|
||||||
|
const info = `
|
||||||
|
<div class="status info">
|
||||||
|
<strong>화면 크기:</strong> ${window.innerWidth} x ${window.innerHeight}<br>
|
||||||
|
<strong>User Agent:</strong> ${navigator.userAgent}<br>
|
||||||
|
<strong>플랫폼:</strong> ${navigator.platform}<br>
|
||||||
|
<strong>모바일 여부:</strong> ${window.innerWidth <= 768 ? '예' : '아니오'}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('deviceInfo').innerHTML = info;
|
||||||
|
log('디바이스 정보 표시 완료');
|
||||||
|
}
|
||||||
|
|
||||||
|
// localStorage 확인
|
||||||
|
function checkStorage() {
|
||||||
|
log('localStorage 확인 시작');
|
||||||
|
const statusDiv = document.getElementById('storageStatus');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 프로젝트 데이터 확인
|
||||||
|
const projectData = localStorage.getItem('work-report-projects');
|
||||||
|
if (projectData) {
|
||||||
|
const projects = JSON.parse(projectData);
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="status success">
|
||||||
|
✅ 프로젝트 데이터 있음: ${projects.length}개
|
||||||
|
</div>
|
||||||
|
<pre>${JSON.stringify(projects, null, 2)}</pre>
|
||||||
|
`;
|
||||||
|
log(`프로젝트 ${projects.length}개 발견`);
|
||||||
|
} else {
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="status warning">
|
||||||
|
⚠️ 프로젝트 데이터 없음
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
log('프로젝트 데이터 없음');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 데이터 확인
|
||||||
|
const userData = localStorage.getItem('currentUser');
|
||||||
|
if (userData) {
|
||||||
|
const user = JSON.parse(userData);
|
||||||
|
statusDiv.innerHTML += `
|
||||||
|
<div class="status success">
|
||||||
|
✅ 사용자: ${user.username} (${user.role})
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
log(`사용자: ${user.username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="status error">
|
||||||
|
❌ 에러: ${error.message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
log(`에러: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로젝트 복구
|
||||||
|
function fixProjects() {
|
||||||
|
log('프로젝트 복구 시작');
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
jobNo: 'TKR-25009R',
|
||||||
|
projectName: 'M Project',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
createdByName: '관리자'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
jobNo: 'TKG-24011P',
|
||||||
|
projectName: 'TKG Project',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
createdByName: '관리자'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||||
|
log('프로젝트 데이터 저장 완료');
|
||||||
|
|
||||||
|
alert('프로젝트가 복구되었습니다!');
|
||||||
|
checkStorage();
|
||||||
|
loadProjects();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log(`복구 실패: ${error.message}`);
|
||||||
|
alert('복구 실패: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 드롭다운 테스트
|
||||||
|
function testDropdown() {
|
||||||
|
log('드롭다운 테스트 시작');
|
||||||
|
const select = document.getElementById('testSelect');
|
||||||
|
|
||||||
|
// 옵션 추가
|
||||||
|
select.innerHTML = '<option value="">선택하세요</option>';
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = i;
|
||||||
|
option.textContent = `테스트 옵션 ${i}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`테스트 옵션 ${select.options.length - 1}개 추가됨`);
|
||||||
|
|
||||||
|
// 이벤트 리스너
|
||||||
|
select.onchange = function() {
|
||||||
|
log(`선택됨: ${this.value} - ${this.options[this.selectedIndex].text}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로젝트 로드
|
||||||
|
function loadProjects() {
|
||||||
|
log('프로젝트 로드 시작');
|
||||||
|
const select = document.getElementById('projectSelect');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('work-report-projects');
|
||||||
|
if (!saved) {
|
||||||
|
log('localStorage에 프로젝트 없음');
|
||||||
|
alert('프로젝트 데이터가 없습니다. "프로젝트 복구" 버튼을 눌러주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = JSON.parse(saved);
|
||||||
|
const activeProjects = projects.filter(p => p.isActive);
|
||||||
|
|
||||||
|
log(`활성 프로젝트 ${activeProjects.length}개 발견`);
|
||||||
|
|
||||||
|
// 드롭다운 초기화
|
||||||
|
select.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
||||||
|
|
||||||
|
// 프로젝트 옵션 추가
|
||||||
|
activeProjects.forEach(project => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = project.id;
|
||||||
|
option.textContent = `${project.jobNo} - ${project.projectName}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
log(`옵션 추가: ${project.jobNo} - ${project.projectName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`드롭다운에 ${select.options.length - 1}개 프로젝트 표시됨`);
|
||||||
|
|
||||||
|
// 이벤트 리스너
|
||||||
|
select.onchange = function() {
|
||||||
|
log(`프로젝트 선택됨: ${this.value}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log(`프로젝트 로드 에러: ${error.message}`);
|
||||||
|
alert('프로젝트 로드 실패: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 로드 시 실행
|
||||||
|
window.onload = function() {
|
||||||
|
log('페이지 로드 완료');
|
||||||
|
showDeviceInfo();
|
||||||
|
checkStorage();
|
||||||
|
testDropdown();
|
||||||
|
loadProjects();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -122,85 +122,176 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- API 스크립트 먼저 로드 (최강 캐시 무력화) -->
|
||||||
|
<script>
|
||||||
|
// 브라우저 캐시 완전 무력화
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
const random1 = Math.random() * 1000000;
|
||||||
|
const random2 = Math.floor(Math.random() * 1000000);
|
||||||
|
const cacheBuster = `${timestamp}-${random1}-${random2}`;
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = `/static/js/api.js?force-reload=${cacheBuster}&no-cache=${timestamp}&bust=${random2}`;
|
||||||
|
script.onload = function() {
|
||||||
|
console.log('✅ API 스크립트 로드 완료');
|
||||||
|
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
|
||||||
|
console.log('🌐 현재 hostname:', window.location.hostname);
|
||||||
|
console.log('🔗 현재 protocol:', window.location.protocol);
|
||||||
|
// API 로드 후 인증 체크 시작
|
||||||
|
setTimeout(checkAdminAccess, 100);
|
||||||
|
};
|
||||||
|
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
|
||||||
|
script.setAttribute('pragma', 'no-cache');
|
||||||
|
script.setAttribute('expires', '0');
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
console.log('🚀 캐시 버스터:', cacheBuster);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- API 스크립트 동적 로딩 -->
|
||||||
|
<script>
|
||||||
|
// 최강 캐시 무력화로 API 스크립트 로드
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
const random1 = Math.random() * 1000000;
|
||||||
|
const random2 = Math.floor(Math.random() * 1000000);
|
||||||
|
const cacheBuster = `${timestamp}-${random1}-${random2}`;
|
||||||
|
|
||||||
|
// 기존 api.js 스크립트 제거
|
||||||
|
const existingScripts = document.querySelectorAll('script[src*="api.js"]');
|
||||||
|
existingScripts.forEach(script => script.remove());
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = `/static/js/api.js?v=${timestamp}&cb=${random1}&bust=${random2}&force=${Date.now()}`;
|
||||||
|
script.onload = function() {
|
||||||
|
console.log('✅ API 스크립트 로드 완료 (project-management.html)');
|
||||||
|
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
|
||||||
|
// API 로드 후 인증 확인 시작
|
||||||
|
checkAdminAccess();
|
||||||
|
};
|
||||||
|
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
|
||||||
|
script.setAttribute('pragma', 'no-cache');
|
||||||
|
script.setAttribute('expires', '0');
|
||||||
|
script.crossOrigin = 'anonymous';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
console.log('📱 프로젝트 관리 - 캐시 버스터:', cacheBuster);
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 사용자 확인 (관리자만 접근 가능)
|
// 사용자 확인 (관리자만 접근 가능)
|
||||||
const currentUserData = localStorage.getItem('currentUser');
|
let currentUser = null;
|
||||||
if (!currentUserData) {
|
|
||||||
alert('로그인이 필요합니다.');
|
async function initAuth() {
|
||||||
window.location.href = 'index.html';
|
console.log('인증 초기화 시작');
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
console.log('토큰 존재:', !!token);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.log('토큰 없음 - 로그인 페이지로 이동');
|
||||||
|
alert('로그인이 필요합니다.');
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('API로 사용자 정보 가져오는 중...');
|
||||||
|
const user = await AuthAPI.getCurrentUser();
|
||||||
|
console.log('사용자 정보:', user);
|
||||||
|
currentUser = user;
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('인증 실패:', error);
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
alert('로그인이 필요합니다.');
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentUser;
|
async function checkAdminAccess() {
|
||||||
try {
|
const authSuccess = await initAuth();
|
||||||
// JSON 파싱 시도
|
if (!authSuccess) return;
|
||||||
currentUser = JSON.parse(currentUserData);
|
|
||||||
} catch (e) {
|
const username = currentUser.username || currentUser;
|
||||||
// JSON이 아니면 문자열로 처리 (기존 방식 호환)
|
const isAdmin = username === 'hyungi' || currentUser.role === 'admin';
|
||||||
currentUser = { username: currentUserData };
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
alert('관리자만 접근 가능합니다.');
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
||||||
|
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
||||||
|
|
||||||
|
// 프로젝트 로드
|
||||||
|
loadProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = currentUser.username || currentUser;
|
|
||||||
const isAdmin = username === 'hyungi' || currentUser.role === 'admin';
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
alert('관리자만 접근 가능합니다.');
|
|
||||||
window.location.href = 'index.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
|
||||||
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
|
||||||
|
|
||||||
let projects = [];
|
let projects = [];
|
||||||
|
|
||||||
// 프로젝트 데이터 로드
|
// 프로젝트 데이터 로드 (API 기반)
|
||||||
function loadProjects() {
|
async function loadProjects() {
|
||||||
const saved = localStorage.getItem('work-report-projects');
|
console.log('프로젝트 로드 시작 (API)');
|
||||||
if (saved) {
|
|
||||||
projects = JSON.parse(saved);
|
try {
|
||||||
|
// API에서 프로젝트 로드
|
||||||
|
const apiProjects = await ProjectsAPI.getAll(false);
|
||||||
|
|
||||||
|
// API 데이터를 그대로 사용 (필드명 통일)
|
||||||
|
projects = apiProjects;
|
||||||
|
|
||||||
|
console.log('API에서 프로젝트 로드:', projects.length, '개');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API 로드 실패:', error);
|
||||||
|
projects = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
displayProjectList();
|
displayProjectList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 프로젝트 데이터 저장
|
// 프로젝트 데이터 저장 (더 이상 사용하지 않음 - API 기반)
|
||||||
function saveProjects() {
|
// function saveProjects() {
|
||||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
// localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 프로젝트 생성 폼 처리
|
// 프로젝트 생성 폼 처리
|
||||||
document.getElementById('projectForm').addEventListener('submit', (e) => {
|
document.getElementById('projectForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const jobNo = document.getElementById('jobNo').value.trim();
|
const jobNo = document.getElementById('jobNo').value.trim();
|
||||||
const projectName = document.getElementById('projectName').value.trim();
|
const projectName = document.getElementById('projectName').value.trim();
|
||||||
|
|
||||||
// 중복 Job No. 확인
|
// 중복 Job No. 확인
|
||||||
if (projects.some(p => p.jobNo === jobNo)) {
|
if (projects.some(p => p.job_no === jobNo)) {
|
||||||
alert('이미 존재하는 Job No.입니다.');
|
alert('이미 존재하는 Job No.입니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 프로젝트 생성
|
try {
|
||||||
const newProject = {
|
// API를 통한 프로젝트 생성
|
||||||
id: Date.now(),
|
const newProject = await ProjectsAPI.create({
|
||||||
jobNo: jobNo,
|
job_no: jobNo,
|
||||||
projectName: projectName,
|
project_name: projectName
|
||||||
createdBy: 'hyungi',
|
});
|
||||||
createdByName: '관리자',
|
|
||||||
createdAt: new Date().toISOString(),
|
// 성공 메시지
|
||||||
isActive: true
|
alert('프로젝트가 생성되었습니다.');
|
||||||
};
|
|
||||||
|
// 폼 초기화
|
||||||
projects.push(newProject);
|
document.getElementById('projectForm').reset();
|
||||||
saveProjects();
|
|
||||||
|
// 목록 새로고침
|
||||||
// 성공 메시지
|
await loadProjects();
|
||||||
alert('프로젝트가 생성되었습니다.');
|
displayProjectList();
|
||||||
|
|
||||||
// 폼 초기화
|
} catch (error) {
|
||||||
document.getElementById('projectForm').reset();
|
console.error('프로젝트 생성 실패:', error);
|
||||||
|
alert('프로젝트 생성에 실패했습니다: ' + error.message);
|
||||||
// 목록 새로고침
|
}
|
||||||
displayProjectList();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 프로젝트 목록 표시
|
// 프로젝트 목록 표시
|
||||||
@@ -214,8 +305,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 활성 프로젝트와 비활성 프로젝트 분리
|
// 활성 프로젝트와 비활성 프로젝트 분리
|
||||||
const activeProjects = projects.filter(p => p.isActive);
|
const activeProjects = projects.filter(p => p.is_active);
|
||||||
const inactiveProjects = projects.filter(p => !p.isActive);
|
const inactiveProjects = projects.filter(p => !p.is_active);
|
||||||
|
|
||||||
|
console.log('전체 프로젝트:', projects.length, '개');
|
||||||
|
console.log('활성 프로젝트:', activeProjects.length, '개');
|
||||||
|
console.log('비활성 프로젝트:', inactiveProjects.length, '개');
|
||||||
|
console.log('비활성 프로젝트 목록:', inactiveProjects);
|
||||||
|
|
||||||
// 활성 프로젝트 표시
|
// 활성 프로젝트 표시
|
||||||
if (activeProjects.length > 0) {
|
if (activeProjects.length > 0) {
|
||||||
@@ -231,13 +327,13 @@
|
|||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<h3 class="font-semibold text-gray-800">${project.jobNo}</h3>
|
<h3 class="font-semibold text-gray-800">${project.job_no}</h3>
|
||||||
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">활성</span>
|
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">활성</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-600 mb-2">${project.projectName}</p>
|
<p class="text-gray-600 mb-2">${project.project_name}</p>
|
||||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||||
<span><i class="fas fa-user mr-1"></i>${project.createdByName}</span>
|
<span><i class="fas fa-user mr-1"></i>${project.created_by?.full_name || '관리자'}</span>
|
||||||
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.createdAt).toLocaleDateString()}</span>
|
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.created_at).toLocaleDateString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -271,13 +367,13 @@
|
|||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<h3 class="font-semibold text-gray-600">${project.jobNo}</h3>
|
<h3 class="font-semibold text-gray-600">${project.job_no}</h3>
|
||||||
<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">비활성</span>
|
<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">비활성</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-500 mb-2">${project.projectName}</p>
|
<p class="text-gray-500 mb-2">${project.project_name}</p>
|
||||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||||
<span><i class="fas fa-user mr-1"></i>${project.createdByName}</span>
|
<span><i class="fas fa-user mr-1"></i>${project.created_by?.full_name || '관리자'}</span>
|
||||||
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.createdAt).toLocaleDateString()}</span>
|
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.created_at).toLocaleDateString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -296,55 +392,82 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 프로젝트 편집
|
// 프로젝트 편집
|
||||||
function editProject(projectId) {
|
async function editProject(projectId) {
|
||||||
const project = projects.find(p => p.id === projectId);
|
const project = projects.find(p => p.id === projectId);
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
const newName = prompt('프로젝트 이름을 수정하세요:', project.projectName);
|
const newName = prompt('프로젝트 이름을 수정하세요:', project.project_name);
|
||||||
if (newName && newName.trim() && newName.trim() !== project.projectName) {
|
if (newName && newName.trim() && newName.trim() !== project.project_name) {
|
||||||
project.projectName = newName.trim();
|
try {
|
||||||
saveProjects();
|
// API를 통한 프로젝트 업데이트
|
||||||
displayProjectList();
|
await ProjectsAPI.update(projectId, {
|
||||||
alert('프로젝트가 수정되었습니다.');
|
project_name: newName.trim()
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
// 목록 새로고침
|
||||||
// 프로젝트 활성/비활성 토글
|
await loadProjects();
|
||||||
function toggleProjectStatus(projectId) {
|
|
||||||
const project = projects.find(p => p.id === projectId);
|
|
||||||
if (!project) return;
|
|
||||||
|
|
||||||
const action = project.isActive ? '비활성화' : '활성화';
|
|
||||||
if (confirm(`"${project.jobNo}" 프로젝트를 ${action}하시겠습니까?`)) {
|
|
||||||
project.isActive = !project.isActive;
|
|
||||||
saveProjects();
|
|
||||||
displayProjectList();
|
|
||||||
alert(`프로젝트가 ${action}되었습니다.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 프로젝트 삭제 (완전 삭제)
|
|
||||||
function deleteProject(projectId) {
|
|
||||||
const project = projects.find(p => p.id === projectId);
|
|
||||||
if (!project) return;
|
|
||||||
|
|
||||||
const confirmMessage = project.isActive
|
|
||||||
? `"${project.jobNo}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 활성 프로젝트입니다. 먼저 비활성화를 권장합니다.`
|
|
||||||
: `"${project.jobNo}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 이 작업은 되돌릴 수 없습니다.`;
|
|
||||||
|
|
||||||
if (confirm(confirmMessage)) {
|
|
||||||
const index = projects.findIndex(p => p.id === projectId);
|
|
||||||
if (index > -1) {
|
|
||||||
projects.splice(index, 1);
|
|
||||||
saveProjects();
|
|
||||||
displayProjectList();
|
displayProjectList();
|
||||||
alert('프로젝트가 완전히 삭제되었습니다.');
|
alert('프로젝트가 수정되었습니다.');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('프로젝트 수정 실패:', error);
|
||||||
|
alert('프로젝트 수정에 실패했습니다: ' + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 페이지 로드 시 프로젝트 목록 로드
|
// 프로젝트 활성/비활성 토글
|
||||||
loadProjects();
|
async function toggleProjectStatus(projectId) {
|
||||||
|
const project = projects.find(p => p.id === projectId);
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
|
const action = project.is_active ? '비활성화' : '활성화';
|
||||||
|
if (confirm(`"${project.job_no}" 프로젝트를 ${action}하시겠습니까?`)) {
|
||||||
|
try {
|
||||||
|
// API를 통한 프로젝트 상태 업데이트
|
||||||
|
await ProjectsAPI.update(projectId, {
|
||||||
|
is_active: !project.is_active
|
||||||
|
});
|
||||||
|
|
||||||
|
// 목록 새로고침
|
||||||
|
await loadProjects();
|
||||||
|
displayProjectList();
|
||||||
|
alert(`프로젝트가 ${action}되었습니다.`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('프로젝트 상태 변경 실패:', error);
|
||||||
|
alert('프로젝트 상태 변경에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로젝트 삭제 (완전 삭제)
|
||||||
|
async function deleteProject(projectId) {
|
||||||
|
const project = projects.find(p => p.id === projectId);
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
|
const confirmMessage = project.is_active
|
||||||
|
? `"${project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 활성 프로젝트입니다. 먼저 비활성화를 권장합니다.`
|
||||||
|
: `"${project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 이 작업은 되돌릴 수 없습니다.`;
|
||||||
|
|
||||||
|
if (confirm(confirmMessage)) {
|
||||||
|
try {
|
||||||
|
// API를 통한 프로젝트 삭제
|
||||||
|
await ProjectsAPI.delete(projectId);
|
||||||
|
|
||||||
|
// 목록 새로고침
|
||||||
|
await loadProjects();
|
||||||
|
displayProjectList();
|
||||||
|
alert('프로젝트가 완전히 삭제되었습니다.');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('프로젝트 삭제 실패:', error);
|
||||||
|
alert('프로젝트 삭제에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOMContentLoaded 이벤트 제거 - API 스크립트 로드 후 checkAdminAccess() 호출됨
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
// API 기본 설정
|
// API 기본 설정 (Cloudflare 터널 + 로컬 환경 지원)
|
||||||
const API_BASE_URL = '/api';
|
const API_BASE_URL = (() => {
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
const port = window.location.port;
|
||||||
|
|
||||||
|
console.log('🔧 API URL 생성 - hostname:', hostname, 'protocol:', protocol, 'port:', port);
|
||||||
|
|
||||||
|
// 로컬 환경 (포트 있음)
|
||||||
|
if (port === '16080') {
|
||||||
|
const url = `${protocol}//${hostname}:${port}/api`;
|
||||||
|
console.log('🏠 로컬 환경 URL:', url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cloudflare 터널 환경 (m.hyungi.net) - 강제 HTTPS
|
||||||
|
if (hostname === 'm.hyungi.net') {
|
||||||
|
const url = `https://m-api.hyungi.net/api`;
|
||||||
|
console.log('☁️ Cloudflare 환경 URL:', url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기타 환경
|
||||||
|
const url = '/api';
|
||||||
|
console.log('🌐 기타 환경 URL:', url);
|
||||||
|
return url;
|
||||||
|
})();
|
||||||
|
|
||||||
// 토큰 관리
|
// 토큰 관리
|
||||||
const TokenManager = {
|
const TokenManager = {
|
||||||
@@ -76,23 +101,29 @@ const AuthAPI = {
|
|||||||
formData.append('username', username);
|
formData.append('username', username);
|
||||||
formData.append('password', password);
|
formData.append('password', password);
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
try {
|
||||||
method: 'POST',
|
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
headers: {
|
||||||
},
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
body: formData.toString()
|
},
|
||||||
});
|
body: formData.toString()
|
||||||
|
});
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
if (!response.ok) {
|
||||||
throw new Error(error.detail || '로그인 실패');
|
const error = await response.json();
|
||||||
|
console.error('로그인 에러:', error);
|
||||||
|
throw new Error(error.detail || '로그인 실패');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
TokenManager.setToken(data.access_token);
|
||||||
|
TokenManager.setUser(data.user);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('로그인 요청 에러:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
TokenManager.setToken(data.access_token);
|
|
||||||
TokenManager.setUser(data.user);
|
|
||||||
return data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
@@ -103,6 +134,8 @@ const AuthAPI = {
|
|||||||
|
|
||||||
getMe: () => apiRequest('/auth/me'),
|
getMe: () => apiRequest('/auth/me'),
|
||||||
|
|
||||||
|
getCurrentUser: () => apiRequest('/auth/me'),
|
||||||
|
|
||||||
createUser: (userData) => apiRequest('/auth/users', {
|
createUser: (userData) => apiRequest('/auth/users', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(userData)
|
body: JSON.stringify(userData)
|
||||||
@@ -252,12 +285,12 @@ function checkAdminAuth() {
|
|||||||
const ProjectsAPI = {
|
const ProjectsAPI = {
|
||||||
getAll: (activeOnly = false) => {
|
getAll: (activeOnly = false) => {
|
||||||
const params = activeOnly ? '?active_only=true' : '';
|
const params = activeOnly ? '?active_only=true' : '';
|
||||||
return apiRequest(`/projects${params}`);
|
return apiRequest(`/projects/${params}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
get: (id) => apiRequest(`/projects/${id}`),
|
get: (id) => apiRequest(`/projects/${id}`),
|
||||||
|
|
||||||
create: (projectData) => apiRequest('/projects', {
|
create: (projectData) => apiRequest('/projects/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(projectData)
|
body: JSON.stringify(projectData)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ class AuthCommon {
|
|||||||
'dailyWorkBtn',
|
'dailyWorkBtn',
|
||||||
'listBtn',
|
'listBtn',
|
||||||
'summaryBtn',
|
'summaryBtn',
|
||||||
'projectBtn',
|
'projectBtn'
|
||||||
'adminBtn'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
adminMenus.forEach(menuId => {
|
adminMenus.forEach(menuId => {
|
||||||
@@ -49,6 +48,20 @@ class AuthCommon {
|
|||||||
element.style.display = isAdmin ? '' : 'none';
|
element.style.display = isAdmin ? '' : 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 관리자 버튼 처리 (드롭다운 vs 단순 버튼)
|
||||||
|
const adminBtnContainer = document.getElementById('adminBtnContainer');
|
||||||
|
const userPasswordBtn = document.getElementById('userPasswordBtn');
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
// 관리자: 드롭다운 메뉴 표시
|
||||||
|
if (adminBtnContainer) adminBtnContainer.style.display = '';
|
||||||
|
if (userPasswordBtn) userPasswordBtn.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// 일반 사용자: 비밀번호 변경 버튼만 표시
|
||||||
|
if (adminBtnContainer) adminBtnContainer.style.display = 'none';
|
||||||
|
if (userPasswordBtn) userPasswordBtn.style.display = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static logout() {
|
static logout() {
|
||||||
|
|||||||
@@ -33,9 +33,22 @@ class CommonHeader {
|
|||||||
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
||||||
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
||||||
</a>
|
</a>
|
||||||
<a href="admin.html" class="nav-link" style="display:none;" id="adminBtn">
|
<div class="relative" style="display:none;" id="adminBtnContainer">
|
||||||
<i class="fas fa-users-cog mr-2"></i>관리
|
<button class="nav-link" id="adminBtn" onclick="toggleAdminMenu()">
|
||||||
</a>
|
<i class="fas fa-users-cog mr-2"></i>관리 <i class="fas fa-chevron-down ml-1"></i>
|
||||||
|
</button>
|
||||||
|
<div id="adminMenu" class="absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-50 hidden">
|
||||||
|
<a href="admin.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||||
|
<i class="fas fa-users-cog mr-2"></i>사용자 관리
|
||||||
|
</a>
|
||||||
|
<button onclick="showPasswordChangeModal()" class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||||
|
<i class="fas fa-key mr-2"></i>비밀번호 변경
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="nav-link" style="display:none;" id="userPasswordBtn" onclick="showPasswordChangeModal()">
|
||||||
|
<i class="fas fa-key mr-2"></i>비밀번호 변경
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -104,3 +117,21 @@ function showSection(sectionName) {
|
|||||||
window.showSection(sectionName);
|
window.showSection(sectionName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 관리자 메뉴 토글
|
||||||
|
function toggleAdminMenu() {
|
||||||
|
const menu = document.getElementById('adminMenu');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메뉴 외부 클릭 시 닫기
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
const adminBtn = document.getElementById('adminBtn');
|
||||||
|
const adminMenu = document.getElementById('adminMenu');
|
||||||
|
|
||||||
|
if (adminBtn && adminMenu && !adminBtn.contains(event.target) && !adminMenu.contains(event.target)) {
|
||||||
|
adminMenu.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
104
frontend/sync-projects-from-db.html
Normal file
104
frontend/sync-projects-from-db.html
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>프로젝트 DB → localStorage 동기화</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.success { background: #d4edda; color: #155724; }
|
||||||
|
.error { background: #f8d7da; color: #721c24; }
|
||||||
|
.info { background: #d1ecf1; color: #0c5460; }
|
||||||
|
pre { background: #f4f4f4; padding: 10px; overflow-x: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>프로젝트 DB → localStorage 동기화</h1>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
<pre id="result"></pre>
|
||||||
|
|
||||||
|
<button onclick="syncProjects()">동기화 시작</button>
|
||||||
|
<button onclick="location.href='index.html'">메인으로</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// DB의 프로젝트 데이터 (backend에서 확인한 데이터)
|
||||||
|
const dbProjects = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
jobNo: 'TKR-25009R',
|
||||||
|
projectName: 'M Project',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: '2025-10-24T09:49:42.456272+09:00',
|
||||||
|
createdByName: '관리자'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
jobNo: 'TKG-24011P',
|
||||||
|
projectName: 'TKG Project',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: '2025-10-24T10:59:49.71909+09:00',
|
||||||
|
createdByName: '관리자'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function showStatus(message, type = 'info') {
|
||||||
|
const statusDiv = document.getElementById('status');
|
||||||
|
statusDiv.className = 'status ' + type;
|
||||||
|
statusDiv.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncProjects() {
|
||||||
|
try {
|
||||||
|
// 기존 localStorage 데이터 확인
|
||||||
|
const existing = localStorage.getItem('work-report-projects');
|
||||||
|
if (existing) {
|
||||||
|
showStatus('기존 localStorage 데이터가 있습니다. 덮어쓰시겠습니까?', 'info');
|
||||||
|
if (!confirm('기존 프로젝트 데이터를 DB 데이터로 덮어쓰시겠습니까?')) {
|
||||||
|
showStatus('동기화 취소됨', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// localStorage에 저장
|
||||||
|
localStorage.setItem('work-report-projects', JSON.stringify(dbProjects));
|
||||||
|
|
||||||
|
// 결과 표시
|
||||||
|
document.getElementById('result').textContent = JSON.stringify(dbProjects, null, 2);
|
||||||
|
showStatus('✅ DB 프로젝트 2개를 localStorage로 동기화 완료!', 'success');
|
||||||
|
|
||||||
|
// 2초 후 메인으로 이동
|
||||||
|
setTimeout(() => {
|
||||||
|
alert('동기화가 완료되었습니다. 메인 페이지로 이동합니다.');
|
||||||
|
location.href = 'index.html';
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showStatus('❌ 동기화 실패: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 로드 시 현재 상태 표시
|
||||||
|
window.onload = () => {
|
||||||
|
const current = localStorage.getItem('work-report-projects');
|
||||||
|
if (current) {
|
||||||
|
showStatus('현재 localStorage에 프로젝트 데이터가 있습니다.', 'info');
|
||||||
|
document.getElementById('result').textContent = 'Current: ' + current;
|
||||||
|
} else {
|
||||||
|
showStatus('localStorage에 프로젝트 데이터가 없습니다.', 'info');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -16,7 +16,29 @@ http {
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
|
# 경기도 IP 대역만 허용 (주요 ISP)
|
||||||
|
allow 211.0.0.0/8; # KT
|
||||||
|
allow 175.0.0.0/8; # KT
|
||||||
|
allow 121.0.0.0/8; # SK브로드밴드
|
||||||
|
allow 1.0.0.0/8; # SK브로드밴드
|
||||||
|
allow 112.0.0.0/8; # LG유플러스
|
||||||
|
allow 106.0.0.0/8; # LG유플러스
|
||||||
|
allow 127.0.0.1; # 로컬호스트
|
||||||
|
allow 192.168.0.0/16; # 내부 네트워크
|
||||||
|
deny all; # 나머지 모든 IP 차단
|
||||||
|
|
||||||
|
# HTML 파일 캐시 제어
|
||||||
|
location ~* \.(html)$ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires -1;
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Last-Modified $date_gmt;
|
||||||
|
add_header ETag "";
|
||||||
|
if_modified_since off;
|
||||||
|
}
|
||||||
|
|
||||||
# 프론트엔드 파일 서빙
|
# 프론트엔드 파일 서빙
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
@@ -24,15 +46,21 @@ http {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# JS/CSS 파일 캐시 제어
|
# JS/CSS 파일 캐시 제어 (모바일 강화)
|
||||||
location ~* \.(js|css)$ {
|
location ~* \.(js|css)$ {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
expires -1;
|
expires -1;
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||||
add_header Pragma "no-cache";
|
add_header Pragma "no-cache";
|
||||||
|
add_header Last-Modified $date_gmt;
|
||||||
|
add_header ETag "";
|
||||||
|
if_modified_since off;
|
||||||
|
# 모바일 캐시 방지 추가 헤더
|
||||||
|
add_header Vary "User-Agent";
|
||||||
|
add_header X-Accel-Expires "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
# API 프록시
|
# API 프록시 (백엔드에서 CORS 처리)
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://backend:8000/api/;
|
proxy_pass http://backend:8000/api/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
Reference in New Issue
Block a user