Compare commits

..

6 Commits

Author SHA1 Message Date
Hyungi Ahn
59cbcebb94 chore: docker-compose MLX 환경변수 추가 및 인프라 정비
- ai-service에 MLX_BASE_URL, MLX_TEXT_MODEL 환경변수 추가
- OLLAMA_TEXT_MODEL 기본값 qwen3:8b로 변경
- MariaDB healthcheck 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:18:31 +09:00
Hyungi Ahn
11cffbd920 refactor: System2/3, User Management SSO 인증 통합
- System2 신고: SSO JWT 인증 전환, API base 정리
- System3 부적합: SSO 인증 매니저 통합, 권한 체계 정비
- User Management: SSO 토큰 기반 사용자 관리 API 연동

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:18:23 +09:00
Hyungi Ahn
61c810bd47 refactor: 프론트엔드 SSO 인증 통합 및 API 경로 정리
- Gateway 로그인/포탈 페이지 SSO 연동
- System1 web/fastapi-bridge API base URL 동적 설정
- SSO 토큰 기반 인증 흐름 통일
- deprecated JS 파일 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:18:09 +09:00
Hyungi Ahn
ec755ed52f refactor: System1 API 인증 체계 SSO 전환 및 마이그레이션 정비
- SSO JWT 인증으로 전환 (auth.service.js)
- worker_id → user_id 마이그레이션 완료
- departments 연동, CORS 미들웨어 정리
- 불필요 파일 삭제 (tk_database.db, visitRequestController.js)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:18:00 +09:00
Hyungi Ahn
2f7e083db0 feat: AI 서비스 MLX 듀얼 백엔드 및 모델 최적화
- MLX(맥미니 27B) 우선 → Ollama(조립컴 9B) fallback 구조
- pydantic-settings 기반 config 전환
- health check에 MLX 상태 추가
- 텍스트 모델 qwen3:8b → qwen3.5:9b-q8_0 변경

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:17:50 +09:00
Hyungi Ahn
cad662473b fix: SSO Auth CORS 정책 강화 및 Redis 세션 지원 추가
- CORS origin 검증 로직 추가 (운영 도메인 + localhost + 192.168.x.x)
- Redis 기반 세션/토큰 관리 유틸 추가
- departments 테이블 JOIN 지원 (findByUsername, findById)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:17:42 +09:00
155 changed files with 2565 additions and 4067 deletions

View File

@@ -1,24 +1,30 @@
import os
from pydantic_settings import BaseSettings
class Settings:
OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://100.111.160.84:11434")
OLLAMA_TEXT_MODEL: str = os.getenv("OLLAMA_TEXT_MODEL", "qwen2.5:14b-instruct-q4_K_M")
OLLAMA_EMBED_MODEL: str = os.getenv("OLLAMA_EMBED_MODEL", "bge-m3")
OLLAMA_TIMEOUT: int = int(os.getenv("OLLAMA_TIMEOUT", "120"))
class Settings(BaseSettings):
OLLAMA_BASE_URL: str = "http://100.111.160.84:11434"
OLLAMA_TEXT_MODEL: str = "qwen3:8b"
OLLAMA_EMBED_MODEL: str = "bge-m3"
OLLAMA_TIMEOUT: int = 120
DB_HOST: str = os.getenv("DB_HOST", "mariadb")
DB_PORT: int = int(os.getenv("DB_PORT", "3306"))
DB_USER: str = os.getenv("DB_USER", "hyungi_user")
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "")
DB_NAME: str = os.getenv("DB_NAME", "hyungi")
MLX_BASE_URL: str = "https://llm.hyungi.net"
MLX_TEXT_MODEL: str = "/Users/hyungi/mlx-models/Qwen3.5-27B-4bit"
SECRET_KEY: str = os.getenv("SECRET_KEY", "")
DB_HOST: str = "mariadb"
DB_PORT: int = 3306
DB_USER: str = "hyungi_user"
DB_PASSWORD: str = ""
DB_NAME: str = "hyungi"
SECRET_KEY: str = ""
ALGORITHM: str = "HS256"
SYSTEM1_API_URL: str = os.getenv("SYSTEM1_API_URL", "http://system1-api:3005")
CHROMA_PERSIST_DIR: str = os.getenv("CHROMA_PERSIST_DIR", "/app/data/chroma")
METADATA_DB_PATH: str = os.getenv("METADATA_DB_PATH", "/app/data/metadata.db")
SYSTEM1_API_URL: str = "http://system1-api:3005"
CHROMA_PERSIST_DIR: str = "/app/data/chroma"
METADATA_DB_PATH: str = "/app/data/metadata.db"
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -1,10 +1,28 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from routers import health, embeddings, classification, daily_report, rag
from db.vector_store import vector_store
from db.metadata_store import metadata_store
from services.ollama_client import ollama_client
from middlewares.auth import verify_token
PUBLIC_PATHS = {"/", "/api/ai/health", "/api/ai/models"}
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.method == "OPTIONS" or request.url.path in PUBLIC_PATHS:
return await call_next(request)
try:
request.state.user = await verify_token(request)
except Exception as e:
return JSONResponse(status_code=401, content={"detail": str(e.detail) if hasattr(e, "detail") else "인증 실패"})
return await call_next(request)
@asynccontextmanager
@@ -12,6 +30,7 @@ async def lifespan(app: FastAPI):
vector_store.initialize()
metadata_store.initialize()
yield
await ollama_client.close()
app = FastAPI(
@@ -21,14 +40,25 @@ app = FastAPI(
lifespan=lifespan,
)
ALLOWED_ORIGINS = [
"https://tkfb.technicalkorea.net",
"https://tkreport.technicalkorea.net",
"https://tkqc.technicalkorea.net",
"https://tkuser.technicalkorea.net",
]
if os.getenv("ENV", "production") == "development":
ALLOWED_ORIGINS += ["http://localhost:30080", "http://localhost:30180", "http://localhost:30280"]
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(AuthMiddleware)
app.include_router(health.router, prefix="/api/ai")
app.include_router(embeddings.router, prefix="/api/ai")
app.include_router(classification.router, prefix="/api/ai")

View File

@@ -0,0 +1,24 @@
from fastapi import Request, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError, ExpiredSignatureError
from config import settings
security = HTTPBearer(auto_error=False)
async def verify_token(request: Request) -> dict:
"""JWT 토큰 검증. SSO 서비스와 동일한 시크릿 사용."""
auth: HTTPAuthorizationCredentials = await security(request)
if not auth:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization 헤더가 필요합니다")
if not settings.SECRET_KEY:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="서버 인증 설정 오류")
try:
payload = jwt.decode(auth.credentials, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="토큰이 만료되었습니다")
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="유효하지 않은 토큰입니다")

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services.classification_service import (
classify_issue,
@@ -26,7 +26,7 @@ async def classify(req: ClassifyRequest):
result = await classify_issue(req.description, req.detail_notes)
return {"available": True, **result}
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")
@router.post("/summarize")
@@ -35,7 +35,7 @@ async def summarize(req: SummarizeRequest):
result = await summarize_issue(req.description, req.detail_notes, req.solution)
return {"available": True, **result}
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")
@router.post("/classify-and-summarize")
@@ -44,4 +44,4 @@ async def classify_and_summarize_endpoint(req: ClassifyRequest):
result = await classify_and_summarize(req.description, req.detail_notes)
return {"available": True, **result}
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Request
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from services.report_service import generate_daily_report
from datetime import date
@@ -19,7 +19,7 @@ async def daily_report(req: DailyReportRequest, request: Request):
result = await generate_daily_report(report_date, req.project_id, token)
return {"available": True, **result}
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")
@router.post("/report/preview")
@@ -30,4 +30,4 @@ async def report_preview(req: DailyReportRequest, request: Request):
result = await generate_daily_report(report_date, req.project_id, token)
return {"available": True, "preview": True, **result}
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, BackgroundTasks, Query
from fastapi import APIRouter, BackgroundTasks, HTTPException, Query
from pydantic import BaseModel
from services.embedding_service import (
sync_all_issues,
@@ -53,7 +53,7 @@ async def get_similar(issue_id: int, n_results: int = Query(default=5, le=20)):
results = await search_similar_by_id(issue_id, n_results)
return {"available": True, "results": results, "query_issue_id": issue_id}
except Exception as e:
return {"available": False, "results": [], "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")
@router.post("/similar/search")
@@ -69,7 +69,7 @@ async def search_similar(req: SearchRequest):
)
return {"available": True, "results": results}
except Exception as e:
return {"available": False, "results": [], "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")
@router.get("/embeddings/stats")

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services.rag_service import (
rag_suggest_solution,
@@ -30,7 +30,7 @@ async def suggest_solution(issue_id: int):
try:
return await rag_suggest_solution(issue_id)
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")
@router.post("/rag/ask")
@@ -38,7 +38,7 @@ async def ask_question(req: AskRequest):
try:
return await rag_ask(req.question, req.project_id)
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")
@router.post("/rag/pattern")
@@ -46,7 +46,7 @@ async def analyze_pattern(req: PatternRequest):
try:
return await rag_analyze_pattern(req.description, req.n_results)
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")
@router.post("/rag/classify")
@@ -54,4 +54,4 @@ async def classify_with_rag(req: ClassifyRequest):
try:
return await rag_classify_with_context(req.description, req.detail_notes)
except Exception as e:
return {"available": False, "error": str(e)}
raise HTTPException(status_code=500, detail="AI 서비스 처리 중 오류가 발생했습니다")

View File

@@ -1,5 +1,6 @@
import json
from services.ollama_client import ollama_client
from services.utils import load_prompt, parse_json_response
from config import settings
@@ -7,13 +8,8 @@ CLASSIFY_PROMPT_PATH = "prompts/classify_issue.txt"
SUMMARIZE_PROMPT_PATH = "prompts/summarize_issue.txt"
def _load_prompt(path: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read()
async def classify_issue(description: str, detail_notes: str = "") -> dict:
template = _load_prompt(CLASSIFY_PROMPT_PATH)
template = load_prompt(CLASSIFY_PROMPT_PATH)
prompt = template.format(
description=description or "",
detail_notes=detail_notes or "",
@@ -32,7 +28,7 @@ async def classify_issue(description: str, detail_notes: str = "") -> dict:
async def summarize_issue(
description: str, detail_notes: str = "", solution: str = ""
) -> dict:
template = _load_prompt(SUMMARIZE_PROMPT_PATH)
template = load_prompt(SUMMARIZE_PROMPT_PATH)
prompt = template.format(
description=description or "",
detail_notes=detail_notes or "",

View File

@@ -37,26 +37,46 @@ def build_metadata(issue: dict) -> dict:
return meta
async def sync_all_issues() -> dict:
issues = get_all_issues()
BATCH_SIZE = 10
async def _sync_issues_batch(issues: list[dict]) -> tuple[int, int]:
"""배치 단위로 임베딩 생성 후 벡터 스토어에 저장"""
synced = 0
skipped = 0
# 유효한 이슈와 텍스트 준비
valid = []
for issue in issues:
doc_text = build_document_text(issue)
if not doc_text.strip():
skipped += 1
continue
valid.append((issue, doc_text))
# 배치 단위로 임베딩 생성
for i in range(0, len(valid), BATCH_SIZE):
batch = valid[i:i + BATCH_SIZE]
texts = [doc_text for _, doc_text in batch]
try:
embedding = await ollama_client.generate_embedding(doc_text)
vector_store.upsert(
doc_id=f"issue_{issue['id']}",
document=doc_text,
embedding=embedding,
metadata=build_metadata(issue),
)
synced += 1
except Exception as e:
skipped += 1
embeddings = await ollama_client.batch_embeddings(texts)
for (issue, doc_text), embedding in zip(batch, embeddings):
vector_store.upsert(
doc_id=f"issue_{issue['id']}",
document=doc_text,
embedding=embedding,
metadata=build_metadata(issue),
)
synced += 1
except Exception:
skipped += len(batch)
return synced, skipped
async def sync_all_issues() -> dict:
issues = get_all_issues()
synced, skipped = await _sync_issues_batch(issues)
if issues:
max_id = max(i["id"] for i in issues)
metadata_store.set_last_synced_id(max_id)
@@ -83,26 +103,11 @@ async def sync_single_issue(issue_id: int) -> dict:
async def sync_incremental() -> dict:
last_id = metadata_store.get_last_synced_id()
issues = get_issues_since(last_id)
synced = 0
for issue in issues:
doc_text = build_document_text(issue)
if not doc_text.strip():
continue
try:
embedding = await ollama_client.generate_embedding(doc_text)
vector_store.upsert(
doc_id=f"issue_{issue['id']}",
document=doc_text,
embedding=embedding,
metadata=build_metadata(issue),
)
synced += 1
except Exception:
pass
synced, skipped = await _sync_issues_batch(issues)
if issues:
max_id = max(i["id"] for i in issues)
metadata_store.set_last_synced_id(max_id)
return {"synced": synced, "new_issues": len(issues)}
return {"synced": synced, "skipped": skipped, "new_issues": len(issues)}
async def search_similar_by_id(issue_id: int, n_results: int = 5) -> list[dict]:

View File

@@ -1,3 +1,4 @@
import asyncio
import httpx
from config import settings
@@ -6,29 +7,55 @@ class OllamaClient:
def __init__(self):
self.base_url = settings.OLLAMA_BASE_URL
self.timeout = httpx.Timeout(float(settings.OLLAMA_TIMEOUT), connect=10.0)
self._client: httpx.AsyncClient | None = None
async def _get_client(self) -> httpx.AsyncClient:
if self._client is None or self._client.is_closed:
self._client = httpx.AsyncClient(timeout=self.timeout)
return self._client
async def close(self):
if self._client and not self._client.is_closed:
await self._client.aclose()
self._client = None
async def generate_embedding(self, text: str) -> list[float]:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.base_url}/api/embeddings",
json={"model": settings.OLLAMA_EMBED_MODEL, "prompt": text},
)
response.raise_for_status()
return response.json()["embedding"]
client = await self._get_client()
response = await client.post(
f"{self.base_url}/api/embeddings",
json={"model": settings.OLLAMA_EMBED_MODEL, "prompt": text},
)
response.raise_for_status()
return response.json()["embedding"]
async def batch_embeddings(self, texts: list[str]) -> list[list[float]]:
results = []
for text in texts:
emb = await self.generate_embedding(text)
results.append(emb)
return results
async def batch_embeddings(self, texts: list[str], concurrency: int = 5) -> list[list[float]]:
semaphore = asyncio.Semaphore(concurrency)
async def _embed(text: str) -> list[float]:
async with semaphore:
return await self.generate_embedding(text)
return await asyncio.gather(*[_embed(t) for t in texts])
async def generate_text(self, prompt: str, system: str = None) -> str:
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
async with httpx.AsyncClient(timeout=self.timeout) as client:
client = await self._get_client()
try:
response = await client.post(
f"{settings.MLX_BASE_URL}/chat/completions",
json={
"model": settings.MLX_TEXT_MODEL,
"messages": messages,
"max_tokens": 2048,
"temperature": 0.3,
},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
except Exception:
response = await client.post(
f"{self.base_url}/api/chat",
json={
@@ -42,16 +69,21 @@ class OllamaClient:
return response.json()["message"]["content"]
async def check_health(self) -> dict:
result = {}
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0)) as client:
response = await client.get(f"{self.base_url}/api/tags")
models = response.json().get("models", [])
return {
"status": "connected",
"models": [m["name"] for m in models],
}
client = await self._get_client()
response = await client.get(f"{self.base_url}/api/tags")
models = response.json().get("models", [])
result["ollama"] = {"status": "connected", "models": [m["name"] for m in models]}
except Exception:
return {"status": "disconnected"}
result["ollama"] = {"status": "disconnected"}
try:
client = await self._get_client()
response = await client.get(f"{settings.MLX_BASE_URL}/health")
result["mlx"] = {"status": "connected", "model": settings.MLX_TEXT_MODEL}
except Exception:
result["mlx"] = {"status": "disconnected"}
return result
ollama_client = OllamaClient()

View File

@@ -1,11 +1,7 @@
from services.ollama_client import ollama_client
from services.embedding_service import search_similar_by_text, build_document_text
from services.db_client import get_issue_by_id
def _load_prompt(path: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read()
from services.utils import load_prompt
def _format_retrieved_issues(results: list[dict]) -> str:
@@ -55,7 +51,7 @@ async def rag_suggest_solution(issue_id: int) -> dict:
break
context = _format_retrieved_issues(similar)
template = _load_prompt("prompts/rag_suggest_solution.txt")
template = load_prompt("prompts/rag_suggest_solution.txt")
prompt = template.format(
description=issue.get("description", ""),
detail_notes=issue.get("detail_notes", ""),
@@ -87,7 +83,7 @@ async def rag_ask(question: str, project_id: int = None) -> dict:
)
context = _format_retrieved_issues(results)
template = _load_prompt("prompts/rag_qa.txt")
template = load_prompt("prompts/rag_qa.txt")
prompt = template.format(
question=question,
retrieved_cases=context,
@@ -113,7 +109,7 @@ async def rag_analyze_pattern(description: str, n_results: int = 10) -> dict:
results = await search_similar_by_text(description, n_results=n_results)
context = _format_retrieved_issues(results)
template = _load_prompt("prompts/rag_pattern.txt")
template = load_prompt("prompts/rag_pattern.txt")
prompt = template.format(
description=description,
retrieved_cases=context,
@@ -142,7 +138,7 @@ async def rag_classify_with_context(description: str, detail_notes: str = "") ->
similar = await search_similar_by_text(query, n_results=5)
context = _format_retrieved_issues(similar)
template = _load_prompt("prompts/rag_classify.txt")
template = load_prompt("prompts/rag_classify.txt")
prompt = template.format(
description=description,
detail_notes=detail_notes,

View File

@@ -1,58 +1,38 @@
import asyncio
import httpx
from services.ollama_client import ollama_client
from services.db_client import get_daily_qc_stats, get_issues_for_date
from services.utils import load_prompt
from config import settings
REPORT_PROMPT_PATH = "prompts/daily_report.txt"
def _load_prompt(path: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read()
async def _fetch_one(client: httpx.AsyncClient, url: str, params: dict, headers: dict):
try:
r = await client.get(url, params=params, headers=headers)
if r.status_code == 200:
return r.json()
except Exception:
pass
return None
async def _fetch_system1_data(date_str: str, token: str) -> dict:
headers = {"Authorization": f"Bearer {token}"}
data = {"attendance": None, "work_reports": None, "patrol": None}
params = {"date": date_str}
base = settings.SYSTEM1_API_URL
try:
async with httpx.AsyncClient(timeout=15.0) as client:
# 근태
try:
r = await client.get(
f"{settings.SYSTEM1_API_URL}/api/attendance/daily-status",
params={"date": date_str},
headers=headers,
)
if r.status_code == 200:
data["attendance"] = r.json()
except Exception:
pass
# 작업보고
try:
r = await client.get(
f"{settings.SYSTEM1_API_URL}/api/daily-work-reports/summary",
params={"date": date_str},
headers=headers,
)
if r.status_code == 200:
data["work_reports"] = r.json()
except Exception:
pass
# 순회점검
try:
r = await client.get(
f"{settings.SYSTEM1_API_URL}/api/patrol/today-status",
params={"date": date_str},
headers=headers,
)
if r.status_code == 200:
data["patrol"] = r.json()
except Exception:
pass
attendance, work_reports, patrol = await asyncio.gather(
_fetch_one(client, f"{base}/api/attendance/daily-status", params, headers),
_fetch_one(client, f"{base}/api/daily-work-reports/summary", params, headers),
_fetch_one(client, f"{base}/api/patrol/today-status", params, headers),
)
except Exception:
pass
return data
attendance = work_reports = patrol = None
return {"attendance": attendance, "work_reports": work_reports, "patrol": patrol}
def _format_attendance(data) -> str:
@@ -102,7 +82,7 @@ async def generate_daily_report(
qc_stats = get_daily_qc_stats(date_str)
qc_issues = get_issues_for_date(date_str)
template = _load_prompt(REPORT_PROMPT_PATH)
template = load_prompt(REPORT_PROMPT_PATH)
prompt = template.format(
date=date_str,
attendance_data=_format_attendance(system1_data["attendance"]),

View File

@@ -0,0 +1,22 @@
import json
import os
_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def load_prompt(path: str) -> str:
full_path = os.path.join(_BASE_DIR, path)
with open(full_path, "r", encoding="utf-8") as f:
return f.read()
def parse_json_response(raw: str) -> dict:
"""LLM 응답에서 JSON을 추출합니다."""
start = raw.find("{")
end = raw.rfind("}") + 1
if start == -1 or end == 0:
return {}
try:
return json.loads(raw[start:end])
except json.JSONDecodeError:
return {}

View File

@@ -21,34 +21,12 @@ services:
ports:
- "30306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
timeout: 20s
retries: 10
networks:
- tk-network
postgres:
image: postgres:15-alpine
container_name: tk-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-mproject}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-mproject}
TZ: Asia/Seoul
PGTZ: Asia/Seoul
volumes:
- postgres_data:/var/lib/postgresql/data
- ./system3-nonconformance/api/migrations:/docker-entrypoint-initdb.d
ports:
- "30432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mproject}"]
timeout: 10s
retries: 5
networks:
- tk-network
redis:
image: redis:6-alpine
container_name: tk-redis
@@ -321,9 +299,11 @@ services:
- "30400:8000"
environment:
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://100.111.160.84:11434}
- OLLAMA_TEXT_MODEL=${OLLAMA_TEXT_MODEL:-qwen2.5:14b-instruct-q4_K_M}
- OLLAMA_TEXT_MODEL=${OLLAMA_TEXT_MODEL:-qwen3:8b}
- OLLAMA_EMBED_MODEL=${OLLAMA_EMBED_MODEL:-bge-m3}
- OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT:-120}
- MLX_BASE_URL=${MLX_BASE_URL:-https://llm.hyungi.net}
- MLX_TEXT_MODEL=${MLX_TEXT_MODEL:-/Users/hyungi/mlx-models/Qwen3.5-27B-4bit}
- DB_HOST=mariadb
- DB_PORT=3306
- DB_USER=${MYSQL_USER:-hyungi_user}
@@ -404,9 +384,6 @@ volumes:
mariadb_data:
external: true
name: tkfb-package_db_data
postgres_data:
external: true
name: tkqc-package_postgres_data
system1_uploads:
external: true
name: tkfb_api_uploads

View File

@@ -161,8 +161,8 @@
// redirect 파라미터가 있으면 해당 URL로, 없으면 포털로
var redirect = new URLSearchParams(location.search).get('redirect');
// Open redirect 방지: 상대 경로 또는 같은 도메인만 허용
if (redirect && (redirect.startsWith('/') && !redirect.startsWith('//')) && !redirect.includes('://')) {
// Open redirect 방지: 같은 origin의 상대 경로만 허용
if (redirect && /^\/[a-zA-Z0-9]/.test(redirect) && !redirect.includes('://') && !redirect.includes('//')) {
window.location.href = redirect;
} else {
window.location.href = '/';

View File

@@ -221,9 +221,9 @@
ssoCookie.remove('sso_token');
ssoCookie.remove('sso_user');
ssoCookie.remove('sso_refresh_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('sso_refresh_token');
['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(function(k) {
localStorage.removeItem(k);
});
fetch('/auth/logout', { method: 'POST' }).catch(function(){});
location.reload();
}

View File

@@ -48,9 +48,9 @@
cookieRemove('sso_token');
cookieRemove('sso_user');
cookieRemove('sso_refresh_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('sso_refresh_token');
['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(function(k) {
localStorage.removeItem(k);
});
window.location.href = this.getLoginUrl();
},

View File

@@ -6,12 +6,16 @@
const jwt = require('jsonwebtoken');
const userModel = require('../models/userModel');
const redis = require('../utils/redis');
const JWT_SECRET = process.env.SSO_JWT_SECRET;
const JWT_EXPIRES_IN = process.env.SSO_JWT_EXPIRES_IN || '7d';
const JWT_REFRESH_SECRET = process.env.SSO_JWT_REFRESH_SECRET;
const JWT_REFRESH_EXPIRES_IN = process.env.SSO_JWT_REFRESH_EXPIRES_IN || '30d';
const MAX_LOGIN_ATTEMPTS = 5;
const LOGIN_LOCKOUT_SECONDS = 300; // 5분
/**
* JWT 토큰 페이로드 생성 (모든 시스템 공통 구조)
*/
@@ -47,16 +51,29 @@ async function login(req, res, next) {
return res.status(400).json({ success: false, error: '사용자명과 비밀번호를 입력하세요' });
}
// 로그인 시도 횟수 확인
const attemptKey = `login_attempts:${username}`;
const attempts = parseInt(await redis.get(attemptKey)) || 0;
if (attempts >= MAX_LOGIN_ATTEMPTS) {
return res.status(429).json({ success: false, error: '로그인 시도 횟수를 초과했습니다. 5분 후 다시 시도하세요' });
}
const user = await userModel.findByUsername(username);
if (!user) {
await redis.incr(attemptKey);
await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS);
return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' });
}
const valid = await userModel.verifyPassword(password, user.password_hash);
if (!valid) {
await redis.incr(attemptKey);
await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS);
return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' });
}
// 로그인 성공 시 시도 횟수 초기화
await redis.del(attemptKey);
await userModel.updateLastLogin(user.user_id);
const payload = createTokenPayload(user);

View File

@@ -10,12 +10,25 @@
const express = require('express');
const cors = require('cors');
const authRoutes = require('./routes/authRoutes');
const { initRedis } = require('./utils/redis');
const app = express();
const PORT = process.env.PORT || 3000;
const allowedOrigins = [
'https://tkfb.technicalkorea.net',
'https://tkreport.technicalkorea.net',
'https://tkqc.technicalkorea.net',
'https://tkuser.technicalkorea.net',
];
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push('http://localhost:30000', 'http://localhost:30080', 'http://localhost:30180', 'http://localhost:30280', 'http://localhost:30380');
}
app.use(cors({
origin: true,
origin: function(origin, cb) {
if (!origin || allowedOrigins.includes(origin) || /^http:\/\/(192\.168\.\d+\.\d+|localhost)(:\d+)?$/.test(origin)) return cb(null, true);
cb(new Error('CORS blocked: ' + origin));
},
credentials: true
}));
app.use(express.json());
@@ -42,7 +55,8 @@ app.use((err, req, res, next) => {
});
});
app.listen(PORT, () => {
app.listen(PORT, async () => {
await initRedis();
console.log(`SSO Auth Service running on port ${PORT}`);
});

1246
sso-auth-service/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
"cors": "^2.8.5",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mysql2": "^3.14.1"
"mysql2": "^3.14.1",
"redis": "^4.6.0"
}
}

View File

@@ -0,0 +1,85 @@
/**
* Redis 클라이언트 (로그인 시도 제한, 토큰 블랙리스트)
*
* Redis 미연결 시 메모리 폴백으로 동작
*/
const { createClient } = require('redis');
const REDIS_HOST = process.env.REDIS_HOST || 'redis';
const REDIS_PORT = process.env.REDIS_PORT || 6379;
let client = null;
let connected = false;
// 메모리 폴백 (Redis 미연결 시)
const memoryStore = new Map();
async function initRedis() {
try {
client = createClient({ url: `redis://${REDIS_HOST}:${REDIS_PORT}` });
client.on('error', () => { connected = false; });
client.on('connect', () => { connected = true; });
await client.connect();
console.log('Redis 연결 성공');
} catch {
console.warn('Redis 연결 실패 - 메모리 폴백 사용');
connected = false;
}
}
async function get(key) {
if (connected) {
return await client.get(key);
}
const entry = memoryStore.get(key);
if (!entry) return null;
if (entry.expiry && entry.expiry < Date.now()) {
memoryStore.delete(key);
return null;
}
return entry.value;
}
async function set(key, value, ttlSeconds) {
if (connected) {
await client.set(key, value, { EX: ttlSeconds });
} else {
memoryStore.set(key, {
value,
expiry: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null,
});
}
}
async function del(key) {
if (connected) {
await client.del(key);
} else {
memoryStore.delete(key);
}
}
async function incr(key) {
if (connected) {
return await client.incr(key);
}
const entry = memoryStore.get(key);
const current = entry ? parseInt(entry.value) || 0 : 0;
const next = current + 1;
memoryStore.set(key, { value: String(next), expiry: entry?.expiry || null });
return next;
}
async function expire(key, ttlSeconds) {
if (connected) {
await client.expire(key, ttlSeconds);
} else {
const entry = memoryStore.get(key);
if (entry) {
entry.expiry = Date.now() + ttlSeconds * 1000;
}
}
}
module.exports = { initRedis, get, set, del, incr, expire };

View File

@@ -7,8 +7,11 @@ WORKDIR /usr/src/app
# 패키지 파일 복사 (캐싱 최적화)
COPY package*.json ./
# 프로덕션 의존성만 설치
RUN npm install --omit=dev
# 프로덕션 의존성만 설치 (sharp용 빌드 도구 포함)
RUN apk add --no-cache --virtual .build-deps python3 make g++ && \
npm install --omit=dev && \
npm install sharp && \
apk del .build-deps
# 앱 소스 복사
COPY . .

View File

@@ -64,12 +64,7 @@ function setupMiddlewares(app) {
code: 'RATE_LIMIT_EXCEEDED'
},
standardHeaders: true,
legacyHeaders: false,
// 인증된 사용자는 더 많은 요청 허용
skip: (req) => {
// Authorization 헤더가 있으면 Rate Limit 완화
return req.headers.authorization && req.headers.authorization.startsWith('Bearer ');
}
legacyHeaders: false
});
// 로그인 시도 제한 (브루트포스 방지)

View File

@@ -16,13 +16,11 @@ const notificationRecipientController = {
// 전체 수신자 목록 (유형별 그룹화)
getAll: async (req, res) => {
try {
console.log('🔔 알림 수신자 목록 조회 시작');
const recipients = await notificationRecipientModel.getAll();
console.log('✅ 알림 수신자 목록 조회 완료:', recipients);
res.json({ success: true, data: recipients });
} catch (error) {
console.error(' 수신자 목록 조회 오류:', error.message);
console.error(' 스택:', error.stack);
console.error(' 수신자 목록 조회 오류:', error.message);
console.error(' 스택:', error.stack);
res.status(500).json({ success: false, error: '수신자 목록 조회 실패', detail: error.message });
}
},

View File

@@ -682,8 +682,7 @@ const resetUserPassword = asyncHandler(async (req, res) => {
throw new NotFoundError('사용자를 찾을 수 없습니다');
}
// 비밀번호를 000000으로 초기화
const hashedPassword = await bcrypt.hash('000000', 10);
const hashedPassword = await bcrypt.hash(process.env.DEFAULT_PASSWORD || 'changeme!1', 10);
await db.execute(
'UPDATE users SET password = ?, password_changed_at = NULL, updated_at = NOW() WHERE user_id = ?',
[hashedPassword, id]

View File

@@ -21,37 +21,32 @@ const getAnalysisFilters = asyncHandler(async (req, res) => {
const db = await getDb();
try {
// 프로젝트 목록
const [projects] = await db.query(`
SELECT DISTINCT p.project_id, p.project_name
FROM projects p
INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
ORDER BY p.project_name
`);
// 작업자 목록
const [workers] = await db.query(`
SELECT DISTINCT w.user_id, w.worker_name
FROM workers w
INNER JOIN daily_work_reports dwr ON w.user_id = dwr.user_id
ORDER BY w.worker_name
`);
// 작업 유형 목록
const [workTypes] = await db.query(`
SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name
FROM work_types wt
INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id
ORDER BY wt.name
`);
// 날짜 범위
const [dateRange] = await db.query(`
SELECT
MIN(report_date) as min_date,
MAX(report_date) as max_date
FROM daily_work_reports
`);
const [[projects], [workers], [workTypes], [dateRange]] = await Promise.all([
db.query(`
SELECT DISTINCT p.project_id, p.project_name
FROM projects p
INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
ORDER BY p.project_name
`),
db.query(`
SELECT DISTINCT w.user_id, w.worker_name
FROM workers w
INNER JOIN daily_work_reports dwr ON w.user_id = dwr.user_id
ORDER BY w.worker_name
`),
db.query(`
SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name
FROM work_types wt
INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id
ORDER BY wt.name
`),
db.query(`
SELECT
MIN(report_date) as min_date,
MAX(report_date) as max_date
FROM daily_work_reports
`),
]);
logger.info('분석 필터 데이터 조회 성공', {
projects: projects.length,
@@ -131,115 +126,108 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
WHERE ${whereClause}
`;
const [overallStats] = await db.query(overallSql, queryParams);
// 2. 일별 통계
const dailyStatsSql = `
SELECT
dwr.report_date,
SUM(dwr.work_hours) as daily_hours,
COUNT(*) as daily_entries,
COUNT(DISTINCT dwr.user_id) as daily_workers
FROM daily_work_reports dwr
WHERE ${whereClause}
GROUP BY dwr.report_date
ORDER BY dwr.report_date ASC
`;
const [dailyStats] = await db.query(dailyStatsSql, queryParams);
// 3. 일별 에러 통계
const dailyErrorStatsSql = `
SELECT
dwr.report_date,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors,
COUNT(*) as daily_total,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as daily_error_rate
FROM daily_work_reports dwr
WHERE ${whereClause}
GROUP BY dwr.report_date
ORDER BY dwr.report_date ASC
`;
const [dailyErrorStats] = await db.query(dailyErrorStatsSql, queryParams);
// 4. 에러 유형별 분석
const errorAnalysisSql = `
SELECT
et.id as error_type_id,
et.name as error_type_name,
COUNT(*) as error_count,
SUM(dwr.work_hours) as error_hours,
ROUND((COUNT(*) / (SELECT COUNT(*) FROM daily_work_reports WHERE error_type_id IS NOT NULL)) * 100, 2) as error_percentage
FROM daily_work_reports dwr
LEFT JOIN error_types et ON dwr.error_type_id = et.id
WHERE ${whereClause} AND dwr.error_type_id IS NOT NULL
GROUP BY et.id, et.name
ORDER BY error_count DESC
`;
const [errorAnalysis] = await db.query(errorAnalysisSql, queryParams);
// 5. 작업 유형별 분석
const workTypeAnalysisSql = `
SELECT
wt.id as work_type_id,
wt.name as work_type_name,
COUNT(*) as work_count,
SUM(dwr.work_hours) as total_hours,
AVG(dwr.work_hours) as avg_hours,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
WHERE ${whereClause}
GROUP BY wt.id, wt.name
ORDER BY total_hours DESC
`;
const [workTypeAnalysis] = await db.query(workTypeAnalysisSql, queryParams);
// 6. 작업자별 성과 분석
const workerAnalysisSql = `
SELECT
w.user_id,
w.worker_name,
COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours,
AVG(dwr.work_hours) as avg_hours_per_entry,
COUNT(DISTINCT dwr.project_id) as projects_worked,
COUNT(DISTINCT dwr.report_date) as working_days,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE ${whereClause}
GROUP BY w.user_id, w.worker_name
ORDER BY total_hours DESC
`;
const [workerAnalysis] = await db.query(workerAnalysisSql, queryParams);
// 7. 프로젝트별 분석
const projectAnalysisSql = `
SELECT
p.project_id,
p.project_name,
COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.user_id) as workers_count,
COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN projects p ON dwr.project_id = p.project_id
WHERE ${whereClause}
GROUP BY p.project_id, p.project_name
ORDER BY total_hours DESC
`;
const [projectAnalysis] = await db.query(projectAnalysisSql, queryParams);
const [
[overallStats],
[dailyStats],
[dailyErrorStats],
[errorAnalysis],
[workTypeAnalysis],
[workerAnalysis],
[projectAnalysis],
] = await Promise.all([
// 1. 전체 요약 통계
db.query(overallSql, queryParams),
// 2. 일별 통계
db.query(`
SELECT
dwr.report_date,
SUM(dwr.work_hours) as daily_hours,
COUNT(*) as daily_entries,
COUNT(DISTINCT dwr.user_id) as daily_workers
FROM daily_work_reports dwr
WHERE ${whereClause}
GROUP BY dwr.report_date
ORDER BY dwr.report_date ASC
`, queryParams),
// 3. 일별 에러 통계
db.query(`
SELECT
dwr.report_date,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors,
COUNT(*) as daily_total,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as daily_error_rate
FROM daily_work_reports dwr
WHERE ${whereClause}
GROUP BY dwr.report_date
ORDER BY dwr.report_date ASC
`, queryParams),
// 4. 에러 유형별 분석
db.query(`
SELECT
et.id as error_type_id,
et.name as error_type_name,
COUNT(*) as error_count,
SUM(dwr.work_hours) as error_hours,
ROUND((COUNT(*) / (SELECT COUNT(*) FROM daily_work_reports WHERE error_type_id IS NOT NULL)) * 100, 2) as error_percentage
FROM daily_work_reports dwr
LEFT JOIN error_types et ON dwr.error_type_id = et.id
WHERE ${whereClause} AND dwr.error_type_id IS NOT NULL
GROUP BY et.id, et.name
ORDER BY error_count DESC
`, queryParams),
// 5. 작업 유형별 분석
db.query(`
SELECT
wt.id as work_type_id,
wt.name as work_type_name,
COUNT(*) as work_count,
SUM(dwr.work_hours) as total_hours,
AVG(dwr.work_hours) as avg_hours,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
WHERE ${whereClause}
GROUP BY wt.id, wt.name
ORDER BY total_hours DESC
`, queryParams),
// 6. 작업자별 성과 분석
db.query(`
SELECT
w.user_id,
w.worker_name,
COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours,
AVG(dwr.work_hours) as avg_hours_per_entry,
COUNT(DISTINCT dwr.project_id) as projects_worked,
COUNT(DISTINCT dwr.report_date) as working_days,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE ${whereClause}
GROUP BY w.user_id, w.worker_name
ORDER BY total_hours DESC
`, queryParams),
// 7. 프로젝트별 분석
db.query(`
SELECT
p.project_id,
p.project_name,
COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.user_id) as workers_count,
COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN projects p ON dwr.project_id = p.project_id
WHERE ${whereClause}
GROUP BY p.project_id, p.project_name
ORDER BY total_hours DESC
`, queryParams),
]);
logger.info('기간별 분석 데이터 조회 성공', {
start_date,

View File

@@ -33,7 +33,7 @@ exports.createWorker = asyncHandler(async (req, res) => {
try {
const db = await getDb();
const username = await generateUniqueUsername(workerData.worker_name, db);
const hashedPassword = await bcrypt.hash('1234', 10);
const hashedPassword = await bcrypt.hash(process.env.DEFAULT_PASSWORD || 'changeme!1', 10);
// User 역할 조회
const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']);
@@ -139,13 +139,6 @@ exports.updateWorker = asyncHandler(async (req, res) => {
const workerData = { ...req.body, user_id: id };
const createAccount = req.body.create_account;
console.log('🔧 작업자 수정 요청:', {
user_id: id,
받은데이터: req.body,
처리할데이터: workerData,
create_account: createAccount
});
// 먼저 현재 작업자 정보 조회 (계정 여부 확인용, user_id 기준)
const currentWorker = await workerModel.getByUserId(id);
@@ -166,7 +159,7 @@ exports.updateWorker = asyncHandler(async (req, res) => {
// 계정 생성
try {
const username = await generateUniqueUsername(workerData.worker_name, db);
const hashedPassword = await bcrypt.hash('1234', 10);
const hashedPassword = await bcrypt.hash(process.env.DEFAULT_PASSWORD || 'changeme!1', 10);
const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']);

View File

@@ -13,10 +13,8 @@ async function createAttendanceTables() {
database: 'hyungi'
});
console.log('✅ MySQL 연결 성공');
// 1. 근로 유형 테이블 생성
console.log('📋 근로 유형 테이블 생성 중...');
await connection.execute(`
CREATE TABLE IF NOT EXISTS work_attendance_types (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -30,7 +28,6 @@ async function createAttendanceTables() {
`);
// 2. 휴가 유형 테이블 생성
console.log('🏖️ 휴가 유형 테이블 생성 중...');
await connection.execute(`
CREATE TABLE IF NOT EXISTS vacation_types (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -45,7 +42,6 @@ async function createAttendanceTables() {
`);
// 3. 일일 근태 기록 테이블 생성
console.log('📊 일일 근태 기록 테이블 생성 중...');
await connection.execute(`
CREATE TABLE IF NOT EXISTS daily_attendance_records (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -73,7 +69,6 @@ async function createAttendanceTables() {
`);
// 4. 작업자 휴가 잔여 관리 테이블 생성
console.log('📅 휴가 잔여 관리 테이블 생성 중...');
await connection.execute(`
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -92,7 +87,6 @@ async function createAttendanceTables() {
`);
// 5. 기본 데이터 삽입
console.log('📝 기본 데이터 삽입 중...');
// 근로 유형 기본 데이터
await connection.execute(`
@@ -116,7 +110,6 @@ async function createAttendanceTables() {
`);
// 6. 휴가 전용 작업 유형 추가
console.log('🏖️ 휴가 전용 작업 유형 추가 중...');
await connection.execute(`
INSERT IGNORE INTO work_types (name, description, is_active) VALUES
('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE)
@@ -128,40 +121,31 @@ async function createAttendanceTables() {
ALTER TABLE daily_work_reports
ADD COLUMN attendance_record_id INT NULL COMMENT '근태 기록 ID' AFTER updated_by
`);
console.log('✅ daily_work_reports 테이블에 attendance_record_id 컬럼 추가됨');
} catch (error) {
if (error.code !== 'ER_DUP_FIELDNAME') {
console.log('⚠️ attendance_record_id 컬럼 추가 실패:', error.message);
} else {
console.log('✅ attendance_record_id 컬럼이 이미 존재함');
}
}
// 8. 인덱스 추가
try {
await connection.execute(`CREATE INDEX idx_attendance_record ON daily_work_reports(attendance_record_id)`);
console.log('✅ attendance_record_id 인덱스 추가됨');
} catch (error) {
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
}
console.log('🎉 근태 관리 DB 설정 완료!');
console.log('');
console.log('📋 생성된 테이블:');
console.log(' - work_attendance_types (근로 유형)');
console.log(' - vacation_types (휴가 유형)');
console.log(' - daily_attendance_records (일일 근태 기록)');
console.log(' - worker_vacation_balance (휴가 잔여 관리)');
console.log('');
console.log('✅ 기본 데이터도 모두 삽입되었습니다.');
} catch (error) {
console.error(' DB 설정 중 오류 발생:', error);
console.error(' DB 설정 중 오류 발생:', error);
// 다른 연결 정보로 시도
if (error.code === 'ECONNREFUSED' || error.code === 'ER_ACCESS_DENIED_ERROR') {
console.log('');
console.log('💡 다른 DB 연결 정보를 시도해보세요:');
console.log(' - host: localhost 또는 127.0.0.1');
console.log(' - port: 3306 (기본값)');
console.log(' - user: root 또는 다른 사용자');
@@ -181,11 +165,10 @@ async function createAttendanceTables() {
if (require.main === module) {
createAttendanceTables()
.then(() => {
console.log('✅ 설정 완료');
process.exit(0);
})
.catch((error) => {
console.error(' 설정 실패:', error);
console.error(' 설정 실패:', error);
process.exit(1);
});
}

View File

@@ -11,7 +11,6 @@ exports.up = async function(knex) {
.comment('재직 상태 (employed: 재직, resigned: 퇴사)');
});
console.log('✅ workers 테이블에 employment_status 컬럼 추가 완료');
};
/**
@@ -23,5 +22,4 @@ exports.down = async function(knex) {
table.dropColumn('employment_status');
});
console.log('✅ workers 테이블에서 employment_status 컬럼 삭제 완료');
};

View File

@@ -8,7 +8,6 @@
*/
exports.up = async function(knex) {
console.log('⏳ Workers 테이블에 salary, base_annual_leave 컬럼 추가 중...');
await knex.schema.alterTable('workers', (table) => {
// 급여 정보 (선택 사항, NULL 허용)
@@ -18,16 +17,13 @@ exports.up = async function(knex) {
table.integer('base_annual_leave').defaultTo(15).notNullable().comment('기본 연차 일수');
});
console.log('✅ Workers 테이블 컬럼 추가 완료');
};
exports.down = async function(knex) {
console.log('⏳ Workers 테이블에서 salary, base_annual_leave 컬럼 제거 중...');
await knex.schema.alterTable('workers', (table) => {
table.dropColumn('salary');
table.dropColumn('base_annual_leave');
});
console.log('✅ Workers 테이블 컬럼 제거 완료');
};

View File

@@ -10,7 +10,6 @@
*/
exports.up = async function(knex) {
console.log('⏳ 출근/근태 관련 테이블 생성 중...');
// 1. 출근 유형 테이블
await knex.schema.createTable('work_attendance_types', (table) => {
@@ -22,7 +21,6 @@ exports.up = async function(knex) {
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
});
console.log('✅ work_attendance_types 테이블 생성 완료');
// 초기 데이터 입력
await knex('work_attendance_types').insert([
@@ -32,7 +30,6 @@ exports.up = async function(knex) {
{ type_code: 'ABSENT', type_name: '결근', description: '무단 결근' },
{ type_code: 'VACATION', type_name: '휴가', description: '승인된 휴가' }
]);
console.log('✅ work_attendance_types 초기 데이터 입력 완료');
// 2. 휴가 유형 테이블
await knex.schema.createTable('vacation_types', (table) => {
@@ -44,7 +41,6 @@ exports.up = async function(knex) {
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
});
console.log('✅ vacation_types 테이블 생성 완료');
// 초기 데이터 입력
await knex('vacation_types').insert([
@@ -53,7 +49,6 @@ exports.up = async function(knex) {
{ type_code: 'SICK', type_name: '병가', deduct_days: 1.0 },
{ type_code: 'SPECIAL', type_name: '경조사', deduct_days: 0 }
]);
console.log('✅ vacation_types 초기 데이터 입력 완료');
// 3. 일일 출근 기록 테이블
await knex.schema.createTable('daily_attendance_records', (table) => {
@@ -78,7 +73,6 @@ exports.up = async function(knex) {
table.foreign('vacation_type_id').references('vacation_types.id');
table.foreign('created_by').references('users.user_id');
});
console.log('✅ daily_attendance_records 테이블 생성 완료');
// 4. 작업자 연차 잔액 테이블
await knex.schema.createTable('worker_vacation_balance', (table) => {
@@ -95,18 +89,14 @@ exports.up = async function(knex) {
table.unique(['worker_id', 'year']);
table.foreign('worker_id').references('workers.worker_id').onDelete('CASCADE');
});
console.log('✅ worker_vacation_balance 테이블 생성 완료');
console.log('✅ 모든 출근/근태 관련 테이블 생성 완료');
};
exports.down = async function(knex) {
console.log('⏳ 출근/근태 관련 테이블 제거 중...');
await knex.schema.dropTableIfExists('worker_vacation_balance');
await knex.schema.dropTableIfExists('daily_attendance_records');
await knex.schema.dropTableIfExists('vacation_types');
await knex.schema.dropTableIfExists('work_attendance_types');
console.log('✅ 모든 출근/근태 관련 테이블 제거 완료');
};

View File

@@ -14,7 +14,6 @@ const bcrypt = require('bcrypt');
const { generateUniqueUsername } = require('../../utils/hangulToRoman');
exports.up = async function(knex) {
console.log('⏳ 기존 작업자들에게 계정 자동 생성 중...');
// 1. 계정이 없는 작업자 조회
const workersWithoutAccount = await knex('workers')
@@ -28,10 +27,8 @@ exports.up = async function(knex) {
'workers.annual_leave'
);
console.log(`📊 계정이 없는 작업자: ${workersWithoutAccount.length}`);
if (workersWithoutAccount.length === 0) {
console.log(' 계정이 필요한 작업자가 없습니다.');
return;
}
@@ -69,7 +66,6 @@ exports.up = async function(knex) {
updated_at: knex.fn.now()
});
console.log(`${worker.worker_name} (ID: ${worker.worker_id}) → username: ${username}`);
successCount++;
// 현재 연도 연차 잔액 초기화
@@ -84,21 +80,15 @@ exports.up = async function(knex) {
});
} catch (error) {
console.error(` ${worker.worker_name} 계정 생성 실패:`, error.message);
console.error(` ${worker.worker_name} 계정 생성 실패:`, error.message);
errorCount++;
}
}
console.log(`\n📊 작업 완료: 성공 ${successCount}명, 실패 ${errorCount}`);
console.log(`🔐 초기 비밀번호: ${initialPassword} (모든 계정 공통)`);
console.log('⚠️ 사용자들에게 첫 로그인 후 비밀번호를 변경하도록 안내해주세요!');
};
exports.down = async function(knex) {
console.log('⏳ 자동 생성된 계정 제거 중...');
// 이 마이그레이션으로 생성된 계정은 구분하기 어려우므로
// rollback 시 주의가 필요합니다.
console.log('⚠️ 경고: 이 마이그레이션의 rollback은 권장하지 않습니다.');
console.log(' 필요시 수동으로 users 테이블을 관리하세요.');
};

View File

@@ -8,7 +8,6 @@
*/
exports.up = async function(knex) {
console.log('⏳ 게스트 역할 추가 중...');
// 1. Guest 역할 추가
const [guestRoleId] = await knex('roles').insert({
@@ -17,7 +16,6 @@ exports.up = async function(knex) {
created_at: knex.fn.now(),
updated_at: knex.fn.now()
});
console.log(`✅ Guest 역할 추가 완료 (ID: ${guestRoleId})`);
// 2. 게스트 전용 페이지 추가
await knex('pages').insert({
@@ -29,13 +27,10 @@ exports.up = async function(knex) {
created_at: knex.fn.now(),
updated_at: knex.fn.now()
});
console.log('✅ 게스트 전용 페이지 추가 완료 (신고 채널)');
console.log('✅ 게스트 역할 추가 완료');
};
exports.down = async function(knex) {
console.log('⏳ 게스트 역할 제거 중...');
// 페이지 제거
await knex('pages')
@@ -47,5 +42,4 @@ exports.down = async function(knex) {
.where('name', 'Guest')
.delete();
console.log('✅ 게스트 역할 제거 완료');
};

View File

@@ -11,7 +11,6 @@
*/
exports.up = async function(knex) {
console.log('⏳ TBM 시스템 테이블 생성 중...');
// 1. TBM 세션 테이블 (아침 미팅)
await knex.schema.createTable('tbm_sessions', (table) => {
@@ -35,7 +34,6 @@ exports.up = async function(knex) {
table.foreign('project_id').references('projects.project_id').onDelete('SET NULL');
table.foreign('created_by').references('users.user_id');
});
console.log('✅ tbm_sessions 테이블 생성 완료');
// 2. TBM 팀 구성 테이블 (리더가 선택한 팀원들)
await knex.schema.createTable('tbm_team_assignments', (table) => {
@@ -53,7 +51,6 @@ exports.up = async function(knex) {
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
table.foreign('worker_id').references('workers.worker_id');
});
console.log('✅ tbm_team_assignments 테이블 생성 완료');
// 3. TBM 안전 체크리스트 마스터 테이블
await knex.schema.createTable('tbm_safety_checks', (table) => {
@@ -69,7 +66,6 @@ exports.up = async function(knex) {
table.index('check_category');
});
console.log('✅ tbm_safety_checks 테이블 생성 완료');
// 초기 안전 체크리스트 데이터
await knex('tbm_safety_checks').insert([
@@ -97,7 +93,6 @@ exports.up = async function(knex) {
{ check_category: 'EMERGENCY', check_item: '소화기 위치 확인', display_order: 31, is_required: true },
{ check_category: 'EMERGENCY', check_item: '응급처치 키트 위치 확인', display_order: 32, is_required: true },
]);
console.log('✅ tbm_safety_checks 초기 데이터 입력 완료');
// 4. TBM 안전 체크 기록 테이블
await knex.schema.createTable('tbm_safety_records', (table) => {
@@ -115,7 +110,6 @@ exports.up = async function(knex) {
table.foreign('check_id').references('tbm_safety_checks.check_id');
table.foreign('checked_by').references('users.user_id');
});
console.log('✅ tbm_safety_records 테이블 생성 완료');
// 5. 작업 인계 테이블 (반차/조퇴 시)
await knex.schema.createTable('team_handovers', (table) => {
@@ -140,13 +134,10 @@ exports.up = async function(knex) {
table.foreign('to_leader_id').references('workers.worker_id');
table.foreign('confirmed_by').references('users.user_id');
});
console.log('✅ team_handovers 테이블 생성 완료');
console.log('✅ 모든 TBM 시스템 테이블 생성 완료');
};
exports.down = async function(knex) {
console.log('⏳ TBM 시스템 테이블 제거 중...');
await knex.schema.dropTableIfExists('team_handovers');
await knex.schema.dropTableIfExists('tbm_safety_records');
@@ -154,5 +145,4 @@ exports.down = async function(knex) {
await knex.schema.dropTableIfExists('tbm_team_assignments');
await knex.schema.dropTableIfExists('tbm_sessions');
console.log('✅ 모든 TBM 시스템 테이블 제거 완료');
};

View File

@@ -6,7 +6,6 @@
*/
exports.up = async function(knex) {
console.log('⏳ TBM 페이지 등록 중...');
// TBM 페이지 추가
await knex('pages').insert([
@@ -21,13 +20,10 @@ exports.up = async function(knex) {
}
]);
console.log('✅ TBM 페이지 등록 완료');
};
exports.down = async function(knex) {
console.log('⏳ TBM 페이지 제거 중...');
await knex('pages').where('page_key', 'tbm').del();
console.log('✅ TBM 페이지 제거 완료');
};

View File

@@ -24,7 +24,6 @@ exports.up = function(knex) {
table.index('work_type_id');
table.index('is_active');
}).then(() => {
console.log('✅ tasks 테이블 생성 완료');
});
};

View File

@@ -25,7 +25,6 @@ exports.up = function(knex) {
table.index('work_type_id');
table.index('task_id');
}).then(() => {
console.log('✅ tbm_sessions 테이블에 work_type_id, task_id 컬럼 추가 완료');
});
};

View File

@@ -143,10 +143,8 @@ exports.up = async function(knex) {
}
]);
console.log('✅ 현재 사용 중인 페이지 목록 업데이트 완료');
};
exports.down = async function(knex) {
await knex('pages').del();
console.log('✅ 페이지 목록 삭제 완료');
};

View File

@@ -10,7 +10,6 @@ exports.up = async function(knex) {
table.string('layout_image', 500).nullable().comment('작업장 레이아웃 이미지 경로');
});
console.log('✅ workplaces 테이블에 layout_image 컬럼 추가 완료');
};
exports.down = async function(knex) {
@@ -18,5 +17,4 @@ exports.down = async function(knex) {
table.dropColumn('layout_image');
});
console.log('✅ workplaces 테이블에서 layout_image 컬럼 제거 완료');
};

View File

@@ -39,10 +39,8 @@ exports.up = async function(knex) {
table.index('status');
});
console.log('✅ equipments 테이블 생성 완료');
};
exports.down = async function(knex) {
await knex.schema.dropTableIfExists('equipments');
console.log('✅ equipments 테이블 삭제 완료');
};

View File

@@ -48,10 +48,8 @@ exports.up = async function(knex) {
table.index(['start_date', 'end_date'], 'idx_vacation_requests_dates');
});
console.log('✅ vacation_requests 테이블 생성 완료');
};
exports.down = async function(knex) {
await knex.schema.dropTableIfExists('vacation_requests');
console.log('✅ vacation_requests 테이블 삭제 완료');
};

View File

@@ -45,7 +45,6 @@ exports.up = async function(knex) {
}
]);
console.log('✅ 출퇴근 관리 페이지 4개 등록 완료');
// Admin 사용자(user_id=1)에게 페이지 접근 권한 부여
const adminUserId = 1;
@@ -66,7 +65,6 @@ exports.up = async function(knex) {
}));
await knex('user_page_access').insert(accessRecords);
console.log('✅ Admin 사용자에게 출퇴근 관리 페이지 접근 권한 부여 완료');
};
exports.down = async function(knex) {
@@ -80,5 +78,4 @@ exports.down = async function(knex) {
])
.delete();
console.log('✅ 출퇴근 관리 페이지 삭제 완료');
};

View File

@@ -18,9 +18,7 @@ exports.up = async function(knex) {
.whereNotNull('id')
.update({ is_present: true });
console.log('✅ is_present 컬럼 추가 완료');
} else {
console.log('⏭️ is_present 컬럼이 이미 존재합니다');
}
};

View File

@@ -33,7 +33,6 @@ exports.up = async function(knex) {
}
]);
console.log('✅ 휴가 관리 페이지 분리 완료 (기존 1개 → 신규 2개)');
};
exports.down = async function(knex) {
@@ -53,5 +52,4 @@ exports.down = async function(knex) {
display_order: 50
});
console.log('✅ 휴가 관리 페이지 롤백 완료');
};

View File

@@ -39,7 +39,6 @@ exports.up = async function(knex) {
description: '경조사 휴가 (무급)'
});
console.log('✅ vacation_types 테이블 확장 완료');
};
exports.down = async function(knex) {
@@ -51,5 +50,4 @@ exports.down = async function(knex) {
table.dropColumn('is_system');
});
console.log('✅ vacation_types 테이블 롤백 완료');
};

View File

@@ -37,7 +37,6 @@ exports.up = async function(knex) {
COMMENT '잔여 일수'
`);
console.log('✅ vacation_balance_details 테이블 생성 완료');
// 기존 worker_vacation_balance 데이터를 vacation_balance_details로 마이그레이션
const existingBalances = await knex('worker_vacation_balance').select('*');
@@ -75,7 +74,6 @@ exports.up = async function(knex) {
await knex('vacation_balance_details').insert(balanceDetails);
console.log(`${balanceDetails.length}건의 기존 휴가 데이터 마이그레이션 완료`);
}
};
@@ -83,5 +81,4 @@ exports.down = async function(knex) {
// vacation_balance_details 테이블 삭제
await knex.schema.dropTableIfExists('vacation_balance_details');
console.log('✅ vacation_balance_details 테이블 롤백 완료');
};

View File

@@ -26,7 +26,6 @@ exports.up = async function(knex) {
}
]);
console.log('✅ 휴가 관리 신규 페이지 2개 등록 완료');
};
exports.down = async function(knex) {
@@ -34,5 +33,4 @@ exports.down = async function(knex) {
.whereIn('page_key', ['annual-vacation-overview', 'vacation-allocation'])
.del();
console.log('✅ 휴가 관리 페이지 롤백 완료');
};

View File

@@ -140,7 +140,6 @@ exports.up = async function(knex) {
table.index('request_id', 'idx_request_id');
});
console.log('✅ 출입 신청 및 안전교육 시스템 테이블 생성 완료');
};
exports.down = async function(knex) {
@@ -149,5 +148,4 @@ exports.down = async function(knex) {
await knex.schema.dropTableIfExists('workplace_visit_requests');
await knex.schema.dropTableIfExists('visit_purpose_types');
console.log('✅ 출입 신청 및 안전교육 시스템 테이블 삭제 완료');
};

View File

@@ -36,7 +36,6 @@ exports.up = async function(knex) {
display_order: 61
});
console.log('✅ 출입 신청 및 안전관리 페이지 등록 완료');
};
exports.down = async function(knex) {
@@ -46,5 +45,4 @@ exports.down = async function(knex) {
'safety-training-conduct'
]).delete();
console.log('✅ 출입 신청 및 안전관리 페이지 삭제 완료');
};

View File

@@ -36,7 +36,6 @@ exports.up = async function(knex) {
display_order: 18
});
console.log('✅ 문제 신고 페이지 등록 완료');
};
exports.down = async function(knex) {
@@ -46,5 +45,4 @@ exports.down = async function(knex) {
'issue-detail'
]).delete();
console.log('✅ 문제 신고 페이지 삭제 완료');
};

View File

@@ -19,9 +19,7 @@ exports.up = async function(knex) {
table.decimal('purchase_price', 15, 0).nullable().after('supplier').comment('구입가격');
}
});
console.log('✅ equipments 테이블에 supplier, purchase_price 컬럼 추가 완료');
} else {
console.log(' supplier, purchase_price 컬럼이 이미 존재합니다. 스킵합니다.');
}
};
@@ -31,5 +29,4 @@ exports.down = async function(knex) {
table.dropColumn('purchase_price');
});
console.log('✅ equipments 테이블에서 supplier, purchase_price 컬럼 삭제 완료');
};

View File

@@ -10,7 +10,6 @@
*/
exports.up = async function(knex) {
console.log('⏳ 일일순회점검 시스템 테이블 생성 중...');
// 1. 순회점검 체크리스트 마스터 테이블
await knex.schema.createTable('patrol_checklist_items', (table) => {
@@ -32,7 +31,6 @@ exports.up = async function(knex) {
table.foreign('workplace_id').references('workplaces.workplace_id').onDelete('CASCADE');
table.foreign('category_id').references('workplace_categories.category_id').onDelete('CASCADE');
});
console.log('✅ patrol_checklist_items 테이블 생성 완료');
// 초기 순회점검 체크리스트 데이터
await knex('patrol_checklist_items').insert([
@@ -58,7 +56,6 @@ exports.up = async function(knex) {
{ check_category: 'ENVIRONMENT', check_item: '환기 상태', display_order: 31, is_required: true },
{ check_category: 'ENVIRONMENT', check_item: '누수/누유 여부', display_order: 32, is_required: true },
]);
console.log('✅ patrol_checklist_items 초기 데이터 입력 완료');
// 2. 순회점검 세션 테이블
await knex.schema.createTable('daily_patrol_sessions', (table) => {
@@ -80,7 +77,6 @@ exports.up = async function(knex) {
table.foreign('inspector_id').references('users.user_id');
table.foreign('category_id').references('workplace_categories.category_id').onDelete('SET NULL');
});
console.log('✅ daily_patrol_sessions 테이블 생성 완료');
// 3. 순회점검 체크 기록 테이블
await knex.schema.createTable('patrol_check_records', (table) => {
@@ -100,7 +96,6 @@ exports.up = async function(knex) {
table.foreign('workplace_id').references('workplaces.workplace_id').onDelete('CASCADE');
table.foreign('check_item_id').references('patrol_checklist_items.item_id').onDelete('CASCADE');
});
console.log('✅ patrol_check_records 테이블 생성 완료');
// 4. 작업장 물품 현황 테이블
await knex.schema.createTable('workplace_items', (table) => {
@@ -129,7 +124,6 @@ exports.up = async function(knex) {
table.foreign('created_by').references('users.user_id');
table.foreign('updated_by').references('users.user_id');
});
console.log('✅ workplace_items 테이블 생성 완료');
// 물품 유형 코드 테이블 (선택적 확장용)
await knex.schema.createTable('item_types', (table) => {
@@ -148,13 +142,10 @@ exports.up = async function(knex) {
{ type_code: 'tool', type_name: '공구/장비', icon: '🔧', color: '#8b5cf6', display_order: 4 },
{ type_code: 'other', type_name: '기타', icon: '📍', color: '#6b7280', display_order: 5 },
]);
console.log('✅ item_types 테이블 생성 및 초기 데이터 완료');
console.log('✅ 모든 일일순회점검 시스템 테이블 생성 완료');
};
exports.down = async function(knex) {
console.log('⏳ 일일순회점검 시스템 테이블 제거 중...');
await knex.schema.dropTableIfExists('item_types');
await knex.schema.dropTableIfExists('workplace_items');
@@ -162,5 +153,4 @@ exports.down = async function(knex) {
await knex.schema.dropTableIfExists('daily_patrol_sessions');
await knex.schema.dropTableIfExists('patrol_checklist_items');
console.log('✅ 모든 일일순회점검 시스템 테이블 제거 완료');
};

View File

@@ -98,7 +98,6 @@ exports.up = async function(knex) {
]);
if (unmapped.length > 0) {
console.log('⚠️ 매핑되지 않은 error_type_id 발견:', unmapped);
console.log(' 이 데이터는 수동으로 확인 필요');
}
@@ -107,6 +106,5 @@ exports.up = async function(knex) {
exports.down = async function(knex) {
// 롤백은 복잡하므로 로그만 출력
console.log('⚠️ 이 마이그레이션은 자동 롤백을 지원하지 않습니다.');
console.log(' 데이터 복구가 필요한 경우 백업에서 복원해주세요.');
};

View File

@@ -22,9 +22,7 @@ exports.up = async function(knex) {
WHERE u.worker_id IS NOT NULL AND w.department_id IS NOT NULL
`);
console.log('✅ users.department_id 컬럼 추가 및 기존 데이터 backfill 완료');
} else {
console.log('⏭️ users.department_id 컬럼이 이미 존재합니다');
}
};

View File

@@ -23,7 +23,6 @@ exports.up = async function(knex) {
table.boolean('is_production').defaultTo(false).comment('생산직 부서 여부');
});
await knex.raw(`UPDATE departments SET is_production = TRUE WHERE department_name LIKE '%생산%'`);
console.log('✅ departments.is_production 추가 완료');
}
// ============================================================
@@ -41,7 +40,6 @@ exports.up = async function(knex) {
SET s.department_id = d.department_id
WHERE s.department IS NOT NULL
`);
console.log('✅ sso_users.department_id 추가 및 백필 완료');
}
// ============================================================
@@ -62,7 +60,6 @@ exports.up = async function(knex) {
`);
// user_id에 인덱스 추가
await knex.raw(`ALTER TABLE workers ADD INDEX idx_workers_user_id (user_id)`);
console.log('✅ workers.user_id 추가 및 백필 완료');
}
// ============================================================
@@ -85,7 +82,6 @@ exports.up = async function(knex) {
for (const tableName of tablesWithWorkerId) {
const tableExists = await knex.schema.hasTable(tableName);
if (!tableExists) {
console.log(`⏭️ ${tableName} 테이블이 존재하지 않음, 건너뜀`);
continue;
}
@@ -103,7 +99,6 @@ exports.up = async function(knex) {
`);
// 인덱스 추가
await knex.raw(`ALTER TABLE ${tableName} ADD INDEX idx_${tableName}_user_id (user_id)`);
console.log(`${tableName}.user_id 추가 및 백필 완료`);
}
}
@@ -121,7 +116,6 @@ exports.up = async function(knex) {
SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
console.log('✅ DailyIssueReports.user_id 추가 및 백필 완료');
}
}
@@ -139,7 +133,6 @@ exports.up = async function(knex) {
SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
console.log('✅ WorkReports.user_id 추가 및 백필 완료');
}
}
@@ -156,7 +149,6 @@ exports.up = async function(knex) {
WHERE w.user_id IS NOT NULL
`);
await knex.raw(`ALTER TABLE tbm_sessions ADD INDEX idx_tbm_sessions_leader_user_id (leader_user_id)`);
console.log('✅ tbm_sessions.leader_user_id 추가 및 백필 완료');
}
// team_handovers: from/to_leader_id → from/to_leader_user_id 추가
@@ -178,10 +170,8 @@ exports.up = async function(knex) {
SET t.to_leader_user_id = w2.user_id
WHERE w2.user_id IS NOT NULL
`);
console.log('✅ team_handovers.from/to_leader_user_id 추가 및 백필 완료');
}
console.log('🎉 Phase 1 마이그레이션 완료: 모든 테이블에 user_id 컬럼 추가 및 백필 완료');
};
exports.down = async function(knex) {
@@ -211,7 +201,6 @@ exports.down = async function(knex) {
await knex.schema.table(tableName, (table) => {
table.dropColumn(columnName);
});
console.log(`↩️ ${tableName}.${columnName} 제거`);
}
}
@@ -224,7 +213,6 @@ exports.down = async function(knex) {
await knex.schema.table(tableName, (table) => {
table.dropColumn('user_id');
});
console.log(`↩️ ${tableName}.user_id 제거`);
}
}
}

View File

@@ -12,7 +12,6 @@ const { getDb } = require('../../dbPool');
async function migrate() {
const db = await getDb();
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...\n');
try {
// 1. 수정 대상 확인 (TBM 기반이면서 work_type_id가 task_id와 다른 경우)
@@ -33,15 +32,12 @@ async function migrate() {
ORDER BY dwr.report_date DESC
`);
console.log(`📊 수정 대상: ${checkResult.length}개 레코드\n`);
if (checkResult.length === 0) {
console.log('✅ 수정할 데이터가 없습니다.');
return;
}
// 수정 대상 샘플 출력
console.log('📋 수정 대상 샘플 (최대 10개):');
console.log('─'.repeat(80));
checkResult.slice(0, 10).forEach(row => {
console.log(` ID: ${row.id} | ${row.worker_name} | ${row.report_date}`);
@@ -62,7 +58,6 @@ async function migrate() {
AND dwr.work_type_id != ta.task_id
`);
console.log(`\n✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
// 3. 수정 결과 확인
const [verifyResult] = await db.query(`
@@ -80,7 +75,6 @@ async function migrate() {
LIMIT 5
`);
console.log('\n📋 수정 후 샘플 확인:');
console.log('─'.repeat(80));
verifyResult.forEach(row => {
console.log(` ID: ${row.id} | work_type_id: ${row.work_type_id} | task: ${row.task_name || 'N/A'} | 공정: ${row.work_type_name || 'N/A'}`);
@@ -88,7 +82,7 @@ async function migrate() {
console.log('─'.repeat(80));
} catch (error) {
console.error(' 마이그레이션 실패:', error.message);
console.error(' 마이그레이션 실패:', error.message);
throw error;
}
}
@@ -96,10 +90,9 @@ async function migrate() {
// 실행
migrate()
.then(() => {
console.log('\n🎉 마이그레이션 완료!');
process.exit(0);
})
.catch(err => {
console.error('\n💥 마이그레이션 실패:', err);
console.error('\n 마이그레이션 실패:', err);
process.exit(1);
});

View File

@@ -48,18 +48,14 @@ const server = app.listen(PORT, () => {
env: process.env.NODE_ENV || 'development',
nodeVersion: process.version
});
console.log(`\n🚀 서버가 포트 ${PORT}에서 실행 중입니다.`);
console.log(`📚 API 문서: http://localhost:${PORT}/api-docs\n`);
});
// Graceful Shutdown
const gracefulShutdown = (signal) => {
logger.info(`${signal} 신호 수신 - 서버 종료 시작`);
console.log(`\n🛑 ${signal} 신호를 받았습니다. 서버를 종료합니다...`);
server.close(async () => {
logger.info('HTTP 서버 종료 완료');
console.log('✅ HTTP 서버가 정상적으로 종료되었습니다.');
// 리소스 정리
try {
@@ -79,7 +75,7 @@ const gracefulShutdown = (signal) => {
// 30초 후 강제 종료
setTimeout(() => {
logger.error('강제 종료 - 정상 종료 시간 초과');
console.error(' 정상 종료 실패, 강제 종료합니다.');
console.error(' 정상 종료 실패, 강제 종료합니다.');
process.exit(1);
}, 30000);
};
@@ -94,7 +90,7 @@ process.on('unhandledRejection', (reason, promise) => {
reason: reason,
promise: promise
});
console.error('⚠️ 처리되지 않은 Promise 거부:', reason);
console.error(' 처리되지 않은 Promise 거부:', reason);
if (process.env.NODE_ENV === 'development') {
process.exit(1);
@@ -107,7 +103,7 @@ process.on('uncaughtException', (error) => {
error: error.message,
stack: error.stack
});
console.error('💥 처리되지 않은 예외:', error);
console.error(' 처리되지 않은 예외:', error);
gracefulShutdown('UNCAUGHT_EXCEPTION');
});
@@ -117,11 +113,10 @@ process.on('uncaughtException', (error) => {
if (cache.initRedis) {
await cache.initRedis();
logger.info('캐시 시스템 초기화 완료');
console.log('✅ 캐시 시스템 초기화 완료');
}
} catch (error) {
logger.warn('캐시 시스템 초기화 실패 - 계속 진행', { error: error.message });
console.warn('⚠️ 캐시 시스템 초기화 실패:', error.message);
console.warn(' 캐시 시스템 초기화 실패:', error.message);
}
})();

View File

@@ -109,7 +109,6 @@ class MonthlyStatusModel {
updatedCount++;
}
console.log(`${year}${month}월 집계 재계산 완료: ${updatedCount}`);
return { success: true, updatedCount };
} catch (error) {

View File

@@ -1,284 +0,0 @@
const visitRequestModel = require('../models/visitRequestModel');
const logger = require('../utils/logger');
// ==================== 출입 신청 관리 ====================
exports.createVisitRequest = async (req, res) => {
try {
const requester_id = req.user.user_id;
const requestData = { requester_id, ...req.body };
const requiredFields = ['visitor_company', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
for (const field of requiredFields) {
if (!requestData[field]) {
return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
}
}
const requestId = await visitRequestModel.createVisitRequest(requestData);
res.status(201).json({
success: true,
message: '출입 신청이 성공적으로 생성되었습니다.',
data: { request_id: requestId }
});
} catch (err) {
logger.error('출입 신청 생성 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 생성 중 오류가 발생했습니다.' });
}
};
exports.getAllVisitRequests = async (req, res) => {
try {
const filters = {
status: req.query.status,
visit_date: req.query.visit_date,
start_date: req.query.start_date,
end_date: req.query.end_date,
requester_id: req.query.requester_id,
category_id: req.query.category_id
};
const requests = await visitRequestModel.getAllVisitRequests(filters);
res.json({ success: true, data: requests });
} catch (err) {
logger.error('출입 신청 목록 조회 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 목록 조회 중 오류가 발생했습니다.' });
}
};
exports.getVisitRequestById = async (req, res) => {
try {
const request = await visitRequestModel.getVisitRequestById(req.params.id);
if (!request) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, data: request });
} catch (err) {
logger.error('출입 신청 조회 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 조회 중 오류가 발생했습니다.' });
}
};
exports.updateVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.updateVisitRequest(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 수정되었습니다.' });
} catch (err) {
logger.error('출입 신청 수정 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 수정 중 오류가 발생했습니다.' });
}
};
exports.deleteVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.deleteVisitRequest(req.params.id);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 삭제되었습니다.' });
} catch (err) {
logger.error('출입 신청 삭제 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 삭제 중 오류가 발생했습니다.' });
}
};
exports.approveVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.approveVisitRequest(req.params.id, req.user.user_id);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 승인되었습니다.' });
} catch (err) {
logger.error('출입 신청 승인 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 승인 중 오류가 발생했습니다.' });
}
};
exports.rejectVisitRequest = async (req, res) => {
try {
const rejectionData = {
approved_by: req.user.user_id,
rejection_reason: req.body.rejection_reason || '사유 없음'
};
const result = await visitRequestModel.rejectVisitRequest(req.params.id, rejectionData);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 반려되었습니다.' });
} catch (err) {
logger.error('출입 신청 반려 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 반려 중 오류가 발생했습니다.' });
}
};
// ==================== 방문 목적 관리 ====================
exports.getAllVisitPurposes = async (req, res) => {
try {
const purposes = await visitRequestModel.getAllVisitPurposes();
res.json({ success: true, data: purposes });
} catch (err) {
logger.error('방문 목적 조회 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 조회 중 오류가 발생했습니다.' });
}
};
exports.getActiveVisitPurposes = async (req, res) => {
try {
const purposes = await visitRequestModel.getActiveVisitPurposes();
res.json({ success: true, data: purposes });
} catch (err) {
logger.error('활성 방문 목적 조회 오류:', err);
res.status(500).json({ success: false, message: '활성 방문 목적 조회 중 오류가 발생했습니다.' });
}
};
exports.createVisitPurpose = async (req, res) => {
try {
if (!req.body.purpose_name) {
return res.status(400).json({ success: false, message: 'purpose_name은 필수 입력 항목입니다.' });
}
const purposeId = await visitRequestModel.createVisitPurpose(req.body);
res.status(201).json({
success: true,
message: '방문 목적이 추가되었습니다.',
data: { purpose_id: purposeId }
});
} catch (err) {
logger.error('방문 목적 추가 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 추가 중 오류가 발생했습니다.' });
}
};
exports.updateVisitPurpose = async (req, res) => {
try {
const result = await visitRequestModel.updateVisitPurpose(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '방문 목적을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '방문 목적이 수정되었습니다.' });
} catch (err) {
logger.error('방문 목적 수정 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 수정 중 오류가 발생했습니다.' });
}
};
exports.deleteVisitPurpose = async (req, res) => {
try {
const result = await visitRequestModel.deleteVisitPurpose(req.params.id);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '방문 목적을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '방문 목적이 삭제되었습니다.' });
} catch (err) {
logger.error('방문 목적 삭제 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 삭제 중 오류가 발생했습니다.' });
}
};
// ==================== 안전교육 기록 관리 ====================
exports.createTrainingRecord = async (req, res) => {
try {
const trainingData = { trainer_id: req.user.user_id, ...req.body };
const requiredFields = ['request_id', 'training_date', 'training_start_time'];
for (const field of requiredFields) {
if (!trainingData[field]) {
return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
}
}
const trainingId = await visitRequestModel.createTrainingRecord(trainingData);
// 안전교육 기록 생성 후 출입 신청 상태를 training_completed로 변경
try {
await visitRequestModel.updateVisitRequestStatus(trainingData.request_id, 'training_completed');
} catch (statusErr) {
logger.error('출입 신청 상태 업데이트 오류:', statusErr);
}
res.status(201).json({
success: true,
message: '안전교육 기록이 생성되었습니다.',
data: { training_id: trainingId }
});
} catch (err) {
logger.error('안전교육 기록 생성 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 생성 중 오류가 발생했습니다.' });
}
};
exports.getTrainingRecordByRequestId = async (req, res) => {
try {
const record = await visitRequestModel.getTrainingRecordByRequestId(req.params.requestId);
res.json({ success: true, data: record || null });
} catch (err) {
logger.error('안전교육 기록 조회 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 조회 중 오류가 발생했습니다.' });
}
};
exports.updateTrainingRecord = async (req, res) => {
try {
const result = await visitRequestModel.updateTrainingRecord(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '안전교육 기록이 수정되었습니다.' });
} catch (err) {
logger.error('안전교육 기록 수정 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 수정 중 오류가 발생했습니다.' });
}
};
exports.completeTraining = async (req, res) => {
try {
const trainingId = req.params.id;
const signatureData = req.body.signature_data;
if (!signatureData) {
return res.status(400).json({ success: false, message: '서명 데이터가 필요합니다.' });
}
const result = await visitRequestModel.completeTraining(trainingId, signatureData);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
}
// 교육 완료 후 출입 신청 상태 변경
try {
const record = await visitRequestModel.getTrainingRecordByRequestId(trainingId);
if (record) {
await visitRequestModel.updateVisitRequestStatus(record.request_id, 'training_completed');
}
} catch (statusErr) {
logger.error('출입 신청 상태 업데이트 오류:', statusErr);
}
res.json({ success: true, message: '안전교육이 완료되었습니다.' });
} catch (err) {
logger.error('안전교육 완료 처리 오류:', err);
res.status(500).json({ success: false, message: '안전교육 완료 처리 중 오류가 발생했습니다.' });
}
};
exports.getTrainingRecords = async (req, res) => {
try {
const filters = {
training_date: req.query.training_date,
start_date: req.query.start_date,
end_date: req.query.end_date,
trainer_id: req.query.trainer_id
};
const records = await visitRequestModel.getTrainingRecords(filters);
res.json({ success: true, data: records });
} catch (err) {
logger.error('안전교육 기록 목록 조회 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 목록 조회 중 오류가 발생했습니다.' });
}
};

View File

@@ -174,7 +174,6 @@ const remove = async (userId) => {
try {
await conn.beginTransaction();
console.log(`🗑️ 작업자 삭제 시작: user_id=${userId}`);
// 안전한 삭제: 각 테이블을 개별적으로 처리하고 오류가 발생해도 계속 진행
const tables = [
@@ -195,10 +194,8 @@ const remove = async (userId) => {
try {
const [result] = await conn.query(table.query, [userId]);
if (result.affectedRows > 0) {
console.log(`${table.name} 테이블 ${table.action}: ${result.affectedRows}`);
}
} catch (tableError) {
console.log(`⚠️ ${table.name} 테이블 ${table.action} 실패 (무시): ${tableError.message}`);
}
}
@@ -207,14 +204,13 @@ const remove = async (userId) => {
`DELETE FROM workers WHERE user_id = ?`,
[userId]
);
console.log(`✅ 작업자 삭제 완료: ${result.affectedRows}`);
await conn.commit();
return result.affectedRows;
} catch (err) {
await conn.rollback();
console.error(` 작업자 삭제 오류 (user_id: ${userId}):`, err);
console.error(` 작업자 삭제 오류 (user_id: ${userId}):`, err);
throw new Error(`작업자 삭제 중 오류가 발생했습니다: ${err.message}`);
} finally {
conn.release();

View File

@@ -9,20 +9,12 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const mysql = require('mysql2/promise');
const { getDb } = require('../dbPool');
const { verifyToken } = require('../middlewares/auth');
const { validatePassword, getPasswordError } = require('../utils/passwordValidator');
const router = express.Router();
const authController = require('../controllers/authController');
// DB 연결 설정
const dbConfig = {
host: process.env.DB_HOST || 'db_hyungi_net',
user: process.env.DB_USER || 'hyungi',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'hyungi'
};
// 로그인 시도 추적 (메모리 기반 - 실제로는 Redis 권장)
const loginAttempts = new Map();
@@ -143,7 +135,7 @@ router.post('/refresh-token', async (req, res) => {
return res.status(401).json({ error: '유효하지 않은 토큰입니다.' });
}
const connection = await mysql.createConnection(dbConfig);
const connection = await getDb();
// 사용자 정보 조회
const [users] = await connection.execute(
@@ -151,8 +143,6 @@ router.post('/refresh-token', async (req, res) => {
[decoded.user_id]
);
await connection.end();
if (users.length === 0) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
@@ -167,7 +157,7 @@ router.post('/refresh-token', async (req, res) => {
access_level: user.access_level,
name: user.name || user.username
},
process.env.JWT_SECRET || 'your-secret-key',
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
@@ -224,7 +214,7 @@ router.post('/change-password', verifyToken, async (req, res) => {
});
}
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
// 현재 사용자의 비밀번호 조회
const [users] = await connection.execute(
@@ -290,10 +280,6 @@ router.post('/change-password', verifyToken, async (req, res) => {
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
@@ -334,7 +320,7 @@ router.post('/admin/change-password', verifyToken, async (req, res) => {
});
}
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
// 대상 사용자 확인
const [users] = await connection.execute(
@@ -391,10 +377,6 @@ router.post('/admin/change-password', verifyToken, async (req, res) => {
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
@@ -453,7 +435,7 @@ router.get('/me', verifyToken, async (req, res) => {
try {
const userId = req.user.user_id;
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
const [rows] = await connection.execute(
'SELECT user_id, username, name, email, access_level, last_login_at, created_at FROM users WHERE user_id = ?',
[userId]
@@ -477,10 +459,6 @@ router.get('/me', verifyToken, async (req, res) => {
} catch (error) {
console.error('Get current user error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
} finally {
if (connection) {
await connection.end();
}
}
});
@@ -516,7 +494,7 @@ router.post('/register', verifyToken, async (req, res) => {
});
}
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
// 사용자명 중복 체크
const [existing] = await connection.execute(
@@ -586,10 +564,6 @@ router.post('/register', verifyToken, async (req, res) => {
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
@@ -600,7 +574,7 @@ router.get('/users', verifyToken, async (req, res) => {
let connection;
try {
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
// 기본 쿼리 (role 테이블과 JOIN)
let query = `
@@ -656,10 +630,6 @@ router.get('/users', verifyToken, async (req, res) => {
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
} finally {
if (connection) {
await connection.end();
}
}
});
@@ -691,7 +661,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
}
}
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
// 사용자 존재 확인
const [existing] = await connection.execute(
@@ -802,10 +772,6 @@ router.put('/users/:id', verifyToken, async (req, res) => {
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
@@ -834,7 +800,7 @@ router.delete('/users/:id', verifyToken, async (req, res) => {
});
}
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
// 사용자 존재 확인
const [existing] = await connection.execute(
@@ -871,10 +837,6 @@ router.delete('/users/:id', verifyToken, async (req, res) => {
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
@@ -887,17 +849,13 @@ router.post('/logout', verifyToken, async (req, res) => {
// 로그아웃 시간 기록 (선택사항)
let connection;
try {
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
await connection.execute(
'UPDATE login_logs SET logout_time = NOW() WHERE user_id = ? AND logout_time IS NULL ORDER BY login_time DESC LIMIT 1',
[req.user.user_id]
);
} catch (error) {
console.error('로그아웃 기록 실패:', error);
} finally {
if (connection) {
await connection.end();
}
}
res.json({
@@ -916,7 +874,7 @@ router.get('/login-history', verifyToken, async (req, res) => {
const { limit = 50, offset = 0 } = req.query;
const userId = req.user.user_id;
connection = await mysql.createConnection(dbConfig);
connection = await getDb();
// 본인의 로그인 이력만 조회 (관리자는 전체 조회 가능)
let query = `
@@ -958,10 +916,6 @@ router.get('/login-history', verifyToken, async (req, res) => {
} catch (error) {
console.error('Get login history error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
} finally {
if (connection) {
await connection.end();
}
}
});

View File

@@ -41,7 +41,6 @@ router.get('/check-overwrite', (req, res) => {
});
}
console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${user_id} (누적입력모드)`);
// 누적입력 시스템에서는 항상 덮어쓰기 가능 (실제로는 누적만 함)
res.json({

View File

@@ -8,7 +8,6 @@ router.post('/setup-monthly-status', async (req, res) => {
try {
const db = await getDb();
console.log('📊 월별 집계 테이블 생성 중...');
// 1. 월별 작업자 상태 집계 테이블
await db.execute(`
@@ -86,7 +85,6 @@ router.post('/setup-monthly-status', async (req, res) => {
) COMMENT='월별 일자별 요약 테이블 (캘린더 최적화용)'
`);
console.log('📊 집계 프로시저 생성 중...');
// 3. 집계 업데이트 프로시저
await db.execute(`DROP PROCEDURE IF EXISTS UpdateMonthlyWorkerStatus`);
@@ -239,7 +237,6 @@ router.post('/setup-monthly-status', async (req, res) => {
END
`);
console.log('📊 트리거 생성 중...');
// 4. 트리거 생성
await db.execute(`DROP TRIGGER IF EXISTS tr_daily_work_reports_insert`);
@@ -276,7 +273,6 @@ router.post('/setup-monthly-status', async (req, res) => {
END
`);
console.log('📊 기존 데이터로 집계 테이블 초기화 중...');
// 5. 기존 작업 데이터로 집계 테이블 초기화
const [existingDates] = await db.execute(`
@@ -302,7 +298,6 @@ router.post('/setup-monthly-status', async (req, res) => {
}
if (i % 100 === 0) {
console.log(`📊 집계 초기화 진행률: ${processedCount}/${existingDates.length}`);
}
}
@@ -329,7 +324,7 @@ router.post('/setup-monthly-status', async (req, res) => {
});
} catch (error) {
console.error(' 월별 집계 시스템 설정 오류:', error);
console.error(' 월별 집계 시스템 설정 오류:', error);
res.status(500).json({
success: false,
message: '월별 집계 시스템 설정 중 오류가 발생했습니다.',
@@ -340,12 +335,10 @@ router.post('/setup-monthly-status', async (req, res) => {
router.post('/setup-attendance-db', async (req, res) => {
try {
console.log('🚀 근태 관리 DB 설정 API 호출됨');
const db = await getDb();
// 1. 근로 유형 테이블 생성
console.log('📋 근로 유형 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS work_attendance_types (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -359,7 +352,6 @@ router.post('/setup-attendance-db', async (req, res) => {
`);
// 2. 휴가 유형 테이블 생성
console.log('🏖️ 휴가 유형 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS vacation_types (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -374,7 +366,6 @@ router.post('/setup-attendance-db', async (req, res) => {
`);
// 3. 일일 근태 기록 테이블 생성
console.log('📊 일일 근태 기록 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS daily_attendance_records (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -394,7 +385,6 @@ router.post('/setup-attendance-db', async (req, res) => {
`);
// 4. 작업자별 휴가 잔여 관리 테이블 생성
console.log('👥 작업자별 휴가 잔여 관리 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -410,7 +400,6 @@ router.post('/setup-attendance-db', async (req, res) => {
`);
// 5. 기본 데이터 삽입
console.log('📝 기본 데이터 삽입 중...');
// 근로 유형 기본 데이터
await db.execute(`
@@ -446,7 +435,7 @@ router.post('/setup-attendance-db', async (req, res) => {
});
} catch (error) {
console.error(' DB 설정 API 오류:', error);
console.error(' DB 설정 API 오류:', error);
res.status(500).json({
success: false,
message: 'DB 설정 중 오류가 발생했습니다.',
@@ -460,7 +449,6 @@ router.post('/add-overtime-warning', async (req, res) => {
try {
const db = await getDb();
console.log('⚠️ 12시간 초과 상태 컬럼 추가 중...');
// 1. monthly_summary 테이블에 컬럼 추가
try {
@@ -468,10 +456,8 @@ router.post('/add-overtime-warning', async (req, res) => {
ALTER TABLE monthly_summary
ADD COLUMN overtime_warning_workers INT DEFAULT 0 COMMENT '확인필요(12시간초과) 작업자 수' AFTER error_workers
`);
console.log('✅ overtime_warning_workers 컬럼 추가 완료');
} catch (error) {
if (error.code === 'ER_DUP_FIELDNAME') {
console.log(' overtime_warning_workers 컬럼이 이미 존재합니다.');
} else {
throw error;
}
@@ -482,10 +468,8 @@ router.post('/add-overtime-warning', async (req, res) => {
ALTER TABLE monthly_summary
ADD COLUMN has_overtime_warning BOOLEAN DEFAULT FALSE COMMENT '확인필요 상태 있음' AFTER has_errors
`);
console.log('✅ has_overtime_warning 컬럼 추가 완료');
} catch (error) {
if (error.code === 'ER_DUP_FIELDNAME') {
console.log(' has_overtime_warning 컬럼이 이미 존재합니다.');
} else {
throw error;
}
@@ -551,7 +535,6 @@ router.post('/add-overtime-warning', async (req, res) => {
last_updated = CURRENT_TIMESTAMP;
END
`);
console.log('✅ UpdateDailySummary 프로시저 업데이트 완료');
res.json({
success: true,
@@ -561,7 +544,7 @@ router.post('/add-overtime-warning', async (req, res) => {
});
} catch (error) {
console.error(' 12시간 초과 상태 설정 오류:', error);
console.error(' 12시간 초과 상태 설정 오류:', error);
res.status(500).json({
success: false,
message: '12시간 초과 상태 설정 실패',
@@ -575,7 +558,6 @@ router.post('/migrate-existing-data', async (req, res) => {
try {
const db = await getDb();
console.log('🔄 기존 데이터 마이그레이션 시작...');
// 1. 기존 데이터 범위 확인
const [dateRange] = await db.execute(`
@@ -595,12 +577,10 @@ router.post('/migrate-existing-data', async (req, res) => {
}
const { min_date, max_date, total_reports } = dateRange[0];
console.log(`📊 데이터 범위: ${min_date} ~ ${max_date} (총 ${total_reports}건)`);
// 2. 기존 monthly_worker_status, monthly_summary 데이터 삭제
await db.execute('DELETE FROM monthly_summary');
await db.execute('DELETE FROM monthly_worker_status');
console.log('🗑️ 기존 집계 데이터 삭제 완료');
// 3. 날짜별로 작업자별 상태 재계산
const [allDates] = await db.execute(`
@@ -610,7 +590,6 @@ router.post('/migrate-existing-data', async (req, res) => {
ORDER BY report_date, worker_id
`, [min_date, max_date]);
console.log(`🔄 ${allDates.length}개 날짜-작업자 조합 처리 중...`);
let processedCount = 0;
for (const { report_date, worker_id } of allDates) {
@@ -620,10 +599,9 @@ router.post('/migrate-existing-data', async (req, res) => {
processedCount++;
if (processedCount % 50 === 0) {
console.log(`📈 진행률: ${processedCount}/${allDates.length} (${Math.round(processedCount/allDates.length*100)}%)`);
}
} catch (error) {
console.error(` ${report_date} ${worker_id} 처리 오류:`, error.message);
console.error(` ${report_date} ${worker_id} 처리 오류:`, error.message);
}
}
@@ -631,7 +609,6 @@ router.post('/migrate-existing-data', async (req, res) => {
const [workerStatusCount] = await db.execute('SELECT COUNT(*) as count FROM monthly_worker_status');
const [summaryCount] = await db.execute('SELECT COUNT(*) as count FROM monthly_summary');
console.log(`✅ 마이그레이션 완료:`);
console.log(` - monthly_worker_status: ${workerStatusCount[0].count}`);
console.log(` - monthly_summary: ${summaryCount[0].count}`);
@@ -649,7 +626,7 @@ router.post('/migrate-existing-data', async (req, res) => {
});
} catch (error) {
console.error(' 데이터 마이그레이션 오류:', error);
console.error(' 데이터 마이그레이션 오류:', error);
res.status(500).json({
success: false,
message: '데이터 마이그레이션 실패',
@@ -705,7 +682,7 @@ router.get('/check-data-status', async (req, res) => {
});
} catch (error) {
console.error(' DB 상태 확인 오류:', error);
console.error(' DB 상태 확인 오류:', error);
res.status(500).json({
success: false,
message: 'DB 상태 확인 실패',

View File

@@ -2,6 +2,7 @@
const express = require('express');
const router = express.Router();
const systemController = require('../controllers/systemController');
const userController = require('../controllers/userController');
const { requireAuth, requireRole } = require('../middlewares/auth');
// 모든 라우트에 인증 및 시스템 권한 확인 적용
@@ -46,31 +47,31 @@ router.get('/users/stats', systemController.getUserStats);
* GET /api/system/users
* 모든 사용자 목록 조회
*/
router.get('/users', systemController.getAllUsers);
router.get('/users', userController.getAllUsers);
/**
* POST /api/system/users
* 새 사용자 생성
*/
router.post('/users', systemController.createUser);
router.post('/users', userController.createUser);
/**
* PUT /api/system/users/:id
* 사용자 정보 수정
*/
router.put('/users/:id', systemController.updateUser);
router.put('/users/:id', userController.updateUser);
/**
* DELETE /api/system/users/:id
* 사용자 삭제
*/
router.delete('/users/:id', systemController.deleteUser);
router.delete('/users/:id', userController.deleteUser);
/**
* POST /api/system/users/:id/reset-password
* 사용자 비밀번호 재설정
*/
router.post('/users/:id/reset-password', systemController.resetUserPassword);
router.post('/users/:id/reset-password', userController.resetUserPassword);
// ===== 시스템 로그 관련 =====
@@ -219,7 +220,6 @@ router.post('/migrations/fix-work-type-id', async (req, res) => {
const { getDb } = require('../dbPool');
const db = await getDb();
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...');
// 1. 수정 대상 확인
const [checkResult] = await db.query(`
@@ -256,7 +256,6 @@ router.post('/migrations/fix-work-type-id', async (req, res) => {
AND dwr.work_type_id != ta.task_id
`);
console.log(`✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
// 3. 수정된 샘플 조회
const [samples] = await db.query(`

View File

@@ -56,15 +56,19 @@ const loginService = async (username, password, ipAddress, userAgent) => {
await userModel.resetLoginAttempts(user.user_id);
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET 환경변수가 설정되지 않았습니다');
}
const token = jwt.sign(
{ user_id: user.user_id, username: user.username, role: user.role_name, role_id: user.role_id, access_level: user.access_level, name: user.name || user.username },
process.env.JWT_SECRET || 'your-secret-key',
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
const refreshToken = jwt.sign(
{ user_id: user.user_id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET || 'your-refresh-secret',
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
);

View File

@@ -4,12 +4,10 @@ const path = require('path');
async function setupAttendanceDB() {
try {
console.log('🚀 근태 관리 DB 설정 시작...');
const db = await getDb();
// 1. 근로 유형 테이블 생성
console.log('📋 근로 유형 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS work_attendance_types (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -23,7 +21,6 @@ async function setupAttendanceDB() {
`);
// 2. 휴가 유형 테이블 생성
console.log('🏖️ 휴가 유형 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS vacation_types (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -38,7 +35,6 @@ async function setupAttendanceDB() {
`);
// 3. 일일 근태 기록 테이블 생성
console.log('📊 일일 근태 기록 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS daily_attendance_records (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -66,7 +62,6 @@ async function setupAttendanceDB() {
`);
// 4. 작업자 휴가 잔여 관리 테이블 생성
console.log('📅 휴가 잔여 관리 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
id INT PRIMARY KEY AUTO_INCREMENT,
@@ -85,7 +80,6 @@ async function setupAttendanceDB() {
`);
// 5. 기본 데이터 삽입
console.log('📝 기본 데이터 삽입 중...');
// 근로 유형 기본 데이터
await db.execute(`
@@ -109,7 +103,6 @@ async function setupAttendanceDB() {
`);
// 6. 휴가 전용 작업 유형 추가
console.log('🏖️ 휴가 전용 작업 유형 추가 중...');
await db.execute(`
INSERT IGNORE INTO work_types (name, description, is_active) VALUES
('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE)
@@ -121,42 +114,32 @@ async function setupAttendanceDB() {
ALTER TABLE daily_work_reports
ADD COLUMN attendance_record_id INT NULL COMMENT '근태 기록 ID' AFTER updated_by
`);
console.log('✅ daily_work_reports 테이블에 attendance_record_id 컬럼 추가됨');
} catch (error) {
if (error.code !== 'ER_DUP_FIELDNAME') {
console.log('⚠️ attendance_record_id 컬럼 추가 실패 (이미 존재할 수 있음):', error.message);
} else {
console.log('✅ attendance_record_id 컬럼이 이미 존재함');
}
}
// 8. 인덱스 추가
try {
await db.execute(`CREATE INDEX idx_attendance_record ON daily_work_reports(attendance_record_id)`);
console.log('✅ attendance_record_id 인덱스 추가됨');
} catch (error) {
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
}
try {
await db.execute(`CREATE INDEX idx_daily_work_reports_worker_date ON daily_work_reports(worker_id, report_date)`);
console.log('✅ worker_date 인덱스 추가됨');
} catch (error) {
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
}
console.log('🎉 근태 관리 DB 설정 완료!');
console.log('');
console.log('📋 생성된 테이블:');
console.log(' - work_attendance_types (근로 유형)');
console.log(' - vacation_types (휴가 유형)');
console.log(' - daily_attendance_records (일일 근태 기록)');
console.log(' - worker_vacation_balance (휴가 잔여 관리)');
console.log('');
console.log('✅ 기본 데이터도 모두 삽입되었습니다.');
} catch (error) {
console.error(' DB 설정 중 오류 발생:', error);
console.error(' DB 설정 중 오류 발생:', error);
throw error;
}
}
@@ -165,11 +148,10 @@ async function setupAttendanceDB() {
if (require.main === module) {
setupAttendanceDB()
.then(() => {
console.log('✅ 설정 완료');
process.exit(0);
})
.catch((error) => {
console.error(' 설정 실패:', error);
console.error(' 설정 실패:', error);
process.exit(1);
});
}

View File

@@ -41,7 +41,6 @@ const initRedis = async () => {
});
redisClient.on('connect', () => {
console.log('✅ Redis 캐시 연결 성공');
});
await redisClient.connect();
@@ -200,7 +199,6 @@ const createCacheMiddleware = (keyGenerator, ttl = TTL.MEDIUM) => {
const cachedData = await get(cacheKey);
if (cachedData) {
console.log(`🎯 캐시 히트: ${cacheKey}`);
return res.json(cachedData);
}
@@ -212,7 +210,6 @@ const createCacheMiddleware = (keyGenerator, ttl = TTL.MEDIUM) => {
// 성공 응답만 캐시
if (res.statusCode >= 200 && res.statusCode < 300) {
set(cacheKey, data, ttl).then(() => {
console.log(`💾 캐시 저장: ${cacheKey}`);
});
}

View File

@@ -5,7 +5,6 @@ function getApiBaseUrl() {
const protocol = window.location.protocol;
const port = window.location.port;
console.log('🌐 감지된 환경:', { hostname, protocol, port });
// 🔗 nginx 프록시를 통한 접근 (권장)
// nginx가 /api/ 요청을 백엔드로 프록시하므로 포트 없이 접근
@@ -18,12 +17,11 @@ function getApiBaseUrl() {
? `${protocol}//${hostname}:${port}/api`
: `${protocol}//${hostname}/api`;
console.log('✅ nginx 프록시 사용:', baseUrl);
return baseUrl;
}
// 🚨 백업: 직접 접근 (nginx 프록시 실패시에만)
console.warn('⚠️ 직접 API 접근 (백업 모드)');
console.warn(' 직접 API 접근 (백업 모드)');
return `${protocol}//${hostname}:8000/api`;
}
@@ -64,12 +62,11 @@ export async function apiCall(url, options = {}) {
};
try {
console.log(`📡 API 호출: ${url}`);
const response = await fetch(url, finalOptions);
// 인증 만료 처리
if (response.status === 401) {
console.error(' 인증 만료');
console.error(' 인증 만료');
localStorage.removeItem('sso_token');
alert('인증이 만료되었습니다. 다시 로그인해주세요.');
window.location.href = '/';
@@ -89,11 +86,10 @@ export async function apiCall(url, options = {}) {
}
const result = await response.json();
console.log(`✅ API 성공: ${url}`);
return result;
} catch (error) {
console.error(` API 오류 (${url}):`, error);
console.error(` API 오류 (${url}):`, error);
// 네트워크 오류 vs 서버 오류 구분
if (error.name === 'TypeError' && error.message.includes('fetch')) {
@@ -105,8 +101,6 @@ export async function apiCall(url, options = {}) {
}
// 디버깅 정보
console.log('🔗 API Base URL:', API);
console.log('🌐 Current Location:', {
hostname: window.location.hostname,
protocol: window.location.protocol,
port: window.location.port,
@@ -116,21 +110,17 @@ console.log('🌐 Current Location:', {
// 🧪 API 연결 테스트 함수 (개발용)
export async function testApiConnection() {
try {
console.log('🧪 API 연결 테스트 시작...');
const response = await fetch(`${API}/health`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
console.log('✅ API 연결 성공!');
return true;
} else {
console.log('❌ API 연결 실패:', response.status);
return false;
}
} catch (error) {
console.log('❌ API 연결 오류:', error.message);
return false;
}
}

View File

@@ -50,7 +50,6 @@ function getCacheStatus() {
*/
function clearCache() {
dateStatusCache.clear();
console.log('📦 캐시가 클리어되었습니다.');
}
/**
@@ -77,7 +76,6 @@ function updatePerformanceUI() {
*/
function logPerformanceStatus() {
const status = getCacheStatus();
console.log('📊 성능 상태:', status);
updatePerformanceUI();
}
@@ -118,7 +116,7 @@ function getCurrentUser() {
}
try {
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo');
const userInfo = localStorage.getItem('sso_user');
if (userInfo) {
return JSON.parse(userInfo);
}
@@ -391,18 +389,14 @@ async function calculateDateStatus(dateStr) {
}
try {
console.log(`📊 ${dateStr} 상태 계산 시작 - 순차 호출`);
// 1단계: WorkReports 먼저 가져오기
console.log(`📝 1단계: WorkReports 조회 중...`);
const workReports = await fetchWorkReports(dateStr);
// 2초 대기 (서버 부하 방지)
console.log(`⏳ 2초 대기 중... (서버 부하 방지)`);
await new Promise(resolve => setTimeout(resolve, 2000));
// 2단계: DailyWorkReports 가져오기
console.log(`📊 2단계: DailyWorkReports 조회 중...`);
const dailyReports = await fetchDailyWorkReports(dateStr);
let status;
@@ -423,7 +417,6 @@ async function calculateDateStatus(dateStr) {
// 캐시에 저장
dateStatusCache.set(dateStr, status);
console.log(`${dateStr} 상태 계산 완료: ${status}`);
return status;
} catch (error) {
console.error('날짜 상태 계산 오류:', error);
@@ -564,10 +557,9 @@ async function loadAndUpdateDateStatus(dateStr, buttonElement) {
}`;
}
console.log(`${dateStr} 상태 로드 완료: ${status}`);
} catch (error) {
console.error(` ${dateStr} 상태 로드 실패:`, error);
console.error(` ${dateStr} 상태 로드 실패:`, error);
buttonElement.classList.remove('loading-state');
buttonElement.classList.add('error-state');
buttonElement.title = `${dateStr} - 로드 실패: ${error.message}`;
@@ -589,18 +581,14 @@ async function loadAndUpdateDateStatus(dateStr, buttonElement) {
*/
async function getWorkersForDate(dateStr) {
try {
console.log(`👥 ${dateStr} 작업자 데이터 조합 시작 - 순차 호출`);
// 1단계: WorkReports 먼저 가져오기
console.log(`📝 1단계: WorkReports 조회 중...`);
const workReports = await fetchWorkReports(dateStr);
// 2초 대기 (서버 부하 방지)
console.log(`⏳ 2초 대기 중... (서버 부하 방지)`);
await new Promise(resolve => setTimeout(resolve, 2000));
// 2단계: DailyWorkReports 가져오기
console.log(`📊 2단계: DailyWorkReports 조회 중...`);
const dailyReports = await fetchDailyWorkReports(dateStr);
const workerMap = new Map();
@@ -645,7 +633,6 @@ async function getWorkersForDate(dateStr) {
validationStatus: getValidationStatus(worker)
}));
console.log(`${dateStr} 작업자 데이터 조합 완료: ${result.length}`);
return result;
} catch (error) {
@@ -1022,11 +1009,6 @@ async function init() {
window.saveEditedWork = saveEditedWork;
window.deleteWorker = deleteWorker;
console.log('✅ 근태 검증 관리 시스템 초기화 완료 (API 통합)');
console.log(`🔗 API 경로: ${API}`);
console.log(`📊 설정: 동시 최대 ${RATE_LIMIT.maxConcurrent}개 요청, ${RATE_LIMIT.delayBetweenRequests}ms 딜레이`);
console.log('🔄 API 호출 방식: 통합 설정 + 순차 호출');
console.log('🚫 429 에러 방지: 각 날짜당 최소 5초 간격');
} catch (error) {
console.error('초기화 오류:', error);

View File

@@ -4,7 +4,6 @@ import { isLoggedIn, getUser, clearAuthData } from './auth.js';
// 즉시 실행 함수로 스코프를 보호하고 로직을 실행
(function() {
if (!isLoggedIn()) {
console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.');
clearAuthData(); // 만약을 위해 한번 더 정리
window.location.href = '/login';
return; // 이후 코드 실행 방지
@@ -14,13 +13,12 @@ import { isLoggedIn, getUser, clearAuthData } from './auth.js';
// 사용자 정보가 유효한지 확인 (토큰은 있지만 유저 정보가 깨졌을 경우)
if (!currentUser || !currentUser.username || !currentUser.role) {
console.error('🚨 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.');
console.error(' 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.');
clearAuthData();
window.location.href = '/login';
return;
}
console.log(`${currentUser.username}(${currentUser.role})님 인증 성공.`);
// 역할 기반 메뉴 제어 로직은 각 컴포넌트 로더(load-navbar.js 등)로 이전함.
// 전역 변수 할당(window.currentUser) 제거.

View File

@@ -206,6 +206,4 @@ form?.addEventListener('submit', async (e) => {
// 페이지 로드 시 현재 사용자 정보 표시
document.addEventListener('DOMContentLoaded', () => {
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
console.log('🔐 비밀번호 변경 페이지 로드됨');
console.log('👤 현재 사용자:', user.username || 'Unknown');
});

View File

@@ -42,7 +42,7 @@ function getCurrentUser() {
}
try {
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
const userInfo = localStorage.getItem('sso_user');
if (userInfo) {
const parsed = JSON.parse(userInfo);
console.log('localStorage에서 가져온 사용자 정보:', parsed);
@@ -94,7 +94,6 @@ async function loadData() {
try {
showMessage('데이터를 불러오는 중...', 'loading');
console.log('🔗 통합 API 설정을 사용한 기본 데이터 로딩 시작...');
await loadWorkers();
await loadProjects();
await loadWorkTypes();
@@ -119,7 +118,6 @@ async function loadWorkers() {
console.log('Workers API 호출 중... (통합 API 사용)');
const data = await apiCall(`${API}/workers`);
workers = Array.isArray(data) ? data : (data.workers || []);
console.log('✅ Workers 로드 성공:', workers.length);
} catch (error) {
console.error('작업자 로딩 오류:', error);
throw error;
@@ -131,7 +129,6 @@ async function loadProjects() {
console.log('Projects API 호출 중... (통합 API 사용)');
const data = await apiCall(`${API}/projects`);
projects = Array.isArray(data) ? data : (data.projects || []);
console.log('✅ Projects 로드 성공:', projects.length);
} catch (error) {
console.error('프로젝트 로딩 오류:', error);
throw error;
@@ -143,12 +140,10 @@ async function loadWorkTypes() {
const data = await apiCall(`${API}/daily-work-reports/work-types`);
if (Array.isArray(data) && data.length > 0) {
workTypes = data;
console.log('✅ 작업 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 작업 유형 API 사용 불가, 기본값 사용');
workTypes = [
{id: 1, name: 'Base'},
{id: 2, name: 'Vessel'},
@@ -162,12 +157,10 @@ async function loadWorkStatusTypes() {
const data = await apiCall(`${API}/daily-work-reports/work-status-types`);
if (Array.isArray(data) && data.length > 0) {
workStatusTypes = data;
console.log('✅ 업무 상태 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 업무 상태 유형 API 사용 불가, 기본값 사용');
workStatusTypes = [
{id: 1, name: '정규'},
{id: 2, name: '에러'}
@@ -180,12 +173,10 @@ async function loadErrorTypes() {
const data = await apiCall(`${API}/daily-work-reports/error-types`);
if (Array.isArray(data) && data.length > 0) {
errorTypes = data;
console.log('✅ 에러 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 에러 유형 API 사용 불가, 기본값 사용');
errorTypes = [
{id: 1, name: '설계미스'},
{id: 2, name: '외주작업 불량'},
@@ -429,10 +420,9 @@ async function saveWorkReport() {
body: JSON.stringify(requestData)
});
console.log('✅ 저장 성공 (통합 API):', result);
totalSaved++;
} catch (error) {
console.error(' 저장 실패:', error);
console.error(' 저장 실패:', error);
totalFailed++;
const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음';
@@ -508,10 +498,8 @@ async function loadTodayWorkers() {
queryParams += `&created_by=${currentUser.id}`;
}
console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`);
const rawData = await apiCall(`${API}/daily-work-reports?${queryParams}`);
console.log('📊 당일 작업 데이터 (통합 API):', rawData);
let data = [];
if (Array.isArray(rawData)) {
@@ -789,14 +777,13 @@ async function saveEditedWork() {
body: JSON.stringify(updateData)
});
console.log('✅ 수정 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
refreshTodayWorkers();
} catch (error) {
console.error(' 수정 실패:', error);
console.error(' 수정 실패:', error);
showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
@@ -817,14 +804,13 @@ async function deleteWorkItem(workId) {
method: 'DELETE'
});
console.log('✅ 삭제 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success');
// 화면 새로고침
refreshTodayWorkers();
} catch (error) {
console.error(' 삭제 실패:', error);
console.error(' 삭제 실패:', error);
showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
@@ -877,7 +863,6 @@ async function init() {
setupEventListeners();
loadTodayWorkers();
console.log('✅ 시스템 초기화 완료 (통합 API 설정 적용)');
} catch (error) {
console.error('초기화 오류:', error);

View File

@@ -1,11 +1,9 @@
// /js/group-leader-dashboard.js
// 그룹장 전용 대시보드 기능
console.log('📊 그룹장 대시보드 스크립트 로딩');
// 팀 현황 새로고침
async function refreshTeamStatus() {
console.log('🔄 팀 현황 새로고침 시작');
try {
// 로딩 상태 표시
@@ -24,7 +22,7 @@ async function refreshTeamStatus() {
}, 1000);
} catch (error) {
console.error(' 팀 현황 로딩 실패:', error);
console.error(' 팀 현황 로딩 실패:', error);
const teamList = document.getElementById('team-list');
if (teamList) {
teamList.innerHTML = '<div style="text-align: center; padding: 20px; color: #f44336;">❌ 로딩 실패</div>';
@@ -64,7 +62,6 @@ function updateTeamStatusUI() {
if (presentEl) presentEl.textContent = presentCount;
if (absentEl) absentEl.textContent = absentCount;
console.log('✅ 팀 현황 업데이트 완료');
}
// 환영 메시지 개인화
@@ -74,21 +71,18 @@ function personalizeWelcome() {
if (user && user.name && welcomeMsg) {
welcomeMsg.textContent = `${user.name}님의 실시간 팀 현황 및 작업 모니터링`;
console.log('✅ 환영 메시지 개인화 완료');
}
}
// 페이지 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('🚀 그룹장 대시보드 초기화 시작');
// 사용자 정보 확인
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
console.log('👤 현재 사용자:', user);
// 권한 확인
if (user.access_level !== 'group_leader') {
console.warn('⚠️ 그룹장 권한 없음:', user.access_level);
console.warn(' 그룹장 권한 없음:', user.access_level);
// 필요시 다른 페이지로 리다이렉트
}
@@ -96,7 +90,6 @@ document.addEventListener('DOMContentLoaded', function() {
personalizeWelcome();
updateTeamStatusUI();
console.log('✅ 그룹장 대시보드 초기화 완료');
});
// 전역 함수로 내보내기 (HTML에서 사용)

View File

@@ -135,10 +135,9 @@ document.addEventListener('DOMContentLoaded', async () => {
updateTime();
setInterval(updateTime, 1000);
console.log('✅ 네비게이션 바 로딩 완료');
} catch (error) {
console.error('🔴 네비게이션 바 로딩 중 오류 발생:', error);
console.error(' 네비게이션 바 로딩 중 오류 발생:', error);
navbarContainer.innerHTML = '<p>네비게이션 바를 불러오는 데 실패했습니다.</p>';
}
});

View File

@@ -92,7 +92,6 @@ async function initializeSections() {
// 5. 모든 수정이 완료된 HTML을 실제 DOM에 한 번에 삽입
mainContainer.innerHTML = doc.body.innerHTML;
console.log(`${currentUser.role} 역할의 섹션 로딩 완료.`);
} catch (error) {
console.error('섹션 로딩 중 오류 발생:', error);

View File

@@ -58,10 +58,9 @@ document.addEventListener('DOMContentLoaded', async () => {
// 3. 수정 완료된 HTML을 실제 DOM에 삽입
sidebarContainer.innerHTML = doc.body.innerHTML;
console.log('✅ 사이드바 로딩 및 필터링 완료');
} catch (error) {
console.error('🔴 사이드바 로딩 실패:', error);
console.error(' 사이드바 로딩 실패:', error);
sidebarContainer.innerHTML = '<p>메뉴 로딩 실패</p>';
}
});

View File

@@ -47,7 +47,7 @@ function getCurrentUser() {
}
try {
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
const userInfo = localStorage.getItem('sso_user');
if (userInfo) {
const parsed = JSON.parse(userInfo);
console.log('localStorage에서 가져온 사용자 정보:', parsed);
@@ -128,7 +128,6 @@ async function loadWorkers() {
console.log('작업자 데이터 로딩 중... (통합 API)');
const data = await apiCall(`${API}/workers`);
workers = Array.isArray(data) ? data : (data.workers || []);
console.log('✅ 작업자 로드 성공:', workers.length);
} catch (error) {
console.error('작업자 로딩 오류:', error);
throw error;
@@ -142,32 +141,27 @@ async function loadWorkData(date) {
// 1차: view_all=true로 전체 데이터 시도
let queryParams = `date=${date}&view_all=true`;
console.log(`🔍 1차 시도: ${API}/daily-work-reports?${queryParams}`);
let data = await apiCall(`${API}/daily-work-reports?${queryParams}`);
workData = Array.isArray(data) ? data : (data.data || []);
// 데이터가 없으면 다른 방법들 시도
if (workData.length === 0) {
console.log('⚠️ view_all로 데이터 없음, 다른 방법 시도...');
// 2차: admin=true로 시도
queryParams = `date=${date}&admin=true`;
console.log(`🔍 2차 시도: ${API}/daily-work-reports?${queryParams}`);
data = await apiCall(`${API}/daily-work-reports?${queryParams}`);
workData = Array.isArray(data) ? data : (data.data || []);
if (workData.length === 0) {
// 3차: 날짜 경로 파라미터로 시도
console.log(`🔍 3차 시도: ${API}/daily-work-reports/date/${date}`);
data = await apiCall(`${API}/daily-work-reports/date/${date}`);
workData = Array.isArray(data) ? data : (data.data || []);
if (workData.length === 0) {
// 4차: 기본 파라미터만으로 시도
console.log(`🔍 4차 시도: ${API}/daily-work-reports?date=${date}`);
data = await apiCall(`${API}/daily-work-reports?date=${date}`);
workData = Array.isArray(data) ? data : (data.data || []);
@@ -175,15 +169,11 @@ async function loadWorkData(date) {
}
}
console.log(`✅ 최종 작업 데이터 로드 결과: ${workData.length}`);
// 디버깅을 위한 상세 로그
if (workData.length > 0) {
console.log('📊 로드된 데이터 샘플:', workData.slice(0, 3));
const uniqueWorkers = [...new Set(workData.map(w => w.worker_name))];
console.log('👥 데이터에 포함된 작업자들:', uniqueWorkers);
} else {
console.log('❌ 해당 날짜에 작업 데이터가 없거나 접근 권한이 없습니다.');
}
return workData;
@@ -195,10 +185,8 @@ async function loadWorkData(date) {
// 구체적인 에러 정보 표시
if (error.message.includes('403')) {
console.log('🔒 권한 부족으로 인한 접근 제한');
throw new Error('해당 날짜의 데이터에 접근할 권한이 없습니다.');
} else if (error.message.includes('404')) {
console.log('📭 해당 날짜에 데이터 없음');
throw new Error('해당 날짜에 입력된 작업 데이터가 없습니다.');
} else {
throw error;
@@ -339,7 +327,6 @@ function displayDashboard(data) {
filteredWorkData = data.workers;
setupFiltering();
console.log('✅ 대시보드 표시 완료');
}
// 요약 섹션 표시
@@ -767,7 +754,6 @@ async function saveEditedWork(workId) {
body: JSON.stringify(updateData)
});
console.log('✅ 수정 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
@@ -777,7 +763,7 @@ async function saveEditedWork(workId) {
await loadDashboardData();
} catch (error) {
console.error(' 수정 실패:', error);
console.error(' 수정 실패:', error);
showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
@@ -798,7 +784,6 @@ async function deleteWorkItem(workId) {
method: 'DELETE'
});
console.log('✅ 삭제 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success');
closeWorkerDetailModal();
@@ -807,7 +792,7 @@ async function deleteWorkItem(workId) {
await loadDashboardData();
} catch (error) {
console.error(' 삭제 실패:', error);
console.error(' 삭제 실패:', error);
showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
@@ -912,7 +897,6 @@ async function init() {
// 이벤트 리스너 설정
setupEventListeners();
console.log('✅ 관리자 대시보드 초기화 완료 (통합 API 설정 적용)');
// 자동으로 오늘 데이터 로드
loadDashboardData();

View File

@@ -117,6 +117,5 @@ function showError(message) {
// 페이지 로드 시 실행
document.addEventListener('DOMContentLoaded', () => {
console.log('👤 프로필 페이지 로드됨');
loadProfile();
});

View File

@@ -71,7 +71,6 @@ class WorkReportReviewManager {
{id: 3, name: '입고지연'}, {id: 4, name: '작업 불량'}
];
} catch (error) {
console.log('⚠️ 일부 API 사용 불가, 기본값 사용');
}
// 휴가 정보 로드
@@ -101,13 +100,11 @@ class WorkReportReviewManager {
this.attendanceData = await response.json();
console.log('휴가 정보 로드 완료:', this.attendanceData.length);
} else if (response.status === 404) {
console.log('⚠️ 휴가 API 없음, 더미 데이터 생성');
this.attendanceData = this.generateDummyAttendance();
} else {
throw new Error(`휴가 정보 로드 실패: ${response.status}`);
}
} catch (error) {
console.log('⚠️ 휴가 정보 로드 오류, 더미 데이터 사용:', error.message);
this.attendanceData = this.generateDummyAttendance();
}
}
@@ -204,7 +201,6 @@ class WorkReportReviewManager {
if (response.status === 404 || response.status === 500) {
// API가 아직 준비되지 않은 경우 더미 데이터 사용
this.reports = this.generateDummyData();
console.log('⚠️ API 응답 오류, 더미 데이터 사용:', response.status);
if (response.status === 404) {
this.showMessage('⚠️ 검토 API가 준비되지 않아 더미 데이터를 표시합니다.', 'warning');
} else {
@@ -227,7 +223,6 @@ class WorkReportReviewManager {
this.updateTable();
} catch (error) {
console.log('⚠️ 네트워크 오류로 더미 데이터 사용:', error.message);
// 더미 데이터로 대체
this.reports = this.generateDummyData();
this.validateWorkHours();

View File

@@ -499,14 +499,13 @@ async function saveEditedWork(workId) {
body: JSON.stringify(updateData)
});
console.log('✅ 수정 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
refreshCurrentDay(); // 현재 날짜 데이터 새로고침
} catch (error) {
console.error(' 수정 실패:', error);
console.error(' 수정 실패:', error);
showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error');
// 버튼 복원
@@ -538,13 +537,12 @@ async function deleteWorkItem(workId) {
method: 'DELETE'
});
console.log('✅ 삭제 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success');
refreshCurrentDay(); // 현재 날짜 데이터 새로고침
} catch (error) {
console.error(' 삭제 실패:', error);
console.error(' 삭제 실패:', error);
showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
@@ -597,7 +595,7 @@ async function deleteWorkerAllWorks(date, workerName) {
refreshCurrentDay(); // 현재 날짜 데이터 새로고침
} catch (error) {
console.error(' 전체 삭제 실패:', error);
console.error(' 전체 삭제 실패:', error);
showMessage('작업 삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
@@ -638,7 +636,6 @@ function showConfirmDialog(title, message, warning) {
// 기본 데이터 로드 (통합 API 사용)
async function loadBasicData() {
try {
console.log('🔗 통합 API 설정을 사용한 기본 데이터 로딩...');
const promises = [
// 프로젝트 로드
@@ -695,7 +692,6 @@ async function loadBasicData() {
errorTypes
};
console.log('✅ 기본 데이터 로드 완료 (통합 API):', basicData);
} catch (error) {
console.error('기본 데이터 로드 실패:', error);
}
@@ -764,7 +760,6 @@ async function init() {
// 기본 데이터 미리 로드
await loadBasicData();
console.log('✅ 검토 페이지 초기화 완료 (통합 API 설정 적용)');
} catch (error) {
console.error('초기화 오류:', error);

View File

@@ -1,318 +0,0 @@
/**
* Daily Work Report - Module Loader
* 작업보고서 모듈을 초기화하고 연결하는 메인 진입점
*
* 로드 순서:
* 1. state.js - 전역 상태 관리
* 2. utils.js - 유틸리티 함수
* 3. api.js - API 클라이언트
* 4. index.js - 이 파일 (메인 컨트롤러)
*/
class DailyWorkReportController {
constructor() {
this.state = window.DailyWorkReportState;
this.api = window.DailyWorkReportAPI;
this.utils = window.DailyWorkReportUtils;
this.initialized = false;
console.log('[Controller] DailyWorkReportController 생성');
}
/**
* 초기화
*/
async init() {
if (this.initialized) {
console.log('[Controller] 이미 초기화됨');
return;
}
console.log('[Controller] 초기화 시작...');
try {
// 이벤트 리스너 설정
this.setupEventListeners();
// 기본 데이터 로드
await this.api.loadAllData();
// TBM 탭이 기본
await this.switchTab('tbm');
this.initialized = true;
console.log('[Controller] 초기화 완료');
} catch (error) {
console.error('[Controller] 초기화 실패:', error);
window.showMessage?.('초기화 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
/**
* 이벤트 리스너 설정
*/
setupEventListeners() {
// 탭 버튼
const tbmBtn = document.getElementById('tbmReportTab');
const completedBtn = document.getElementById('completedReportTab');
if (tbmBtn) {
tbmBtn.addEventListener('click', () => this.switchTab('tbm'));
}
if (completedBtn) {
completedBtn.addEventListener('click', () => this.switchTab('completed'));
}
// 완료 보고서 날짜 변경
const completedDateInput = document.getElementById('completedReportDate');
if (completedDateInput) {
completedDateInput.addEventListener('change', () => this.loadCompletedReports());
}
console.log('[Controller] 이벤트 리스너 설정 완료');
}
/**
* 탭 전환
*/
async switchTab(tab) {
this.state.setCurrentTab(tab);
const tbmBtn = document.getElementById('tbmReportTab');
const completedBtn = document.getElementById('completedReportTab');
const tbmSection = document.getElementById('tbmReportSection');
const completedSection = document.getElementById('completedReportSection');
// 모든 탭 버튼 비활성화
tbmBtn?.classList.remove('active');
completedBtn?.classList.remove('active');
// 모든 섹션 숨기기
if (tbmSection) tbmSection.style.display = 'none';
if (completedSection) completedSection.style.display = 'none';
// 선택된 탭 활성화
if (tab === 'tbm') {
tbmBtn?.classList.add('active');
if (tbmSection) tbmSection.style.display = 'block';
await this.loadTbmData();
} else if (tab === 'completed') {
completedBtn?.classList.add('active');
if (completedSection) completedSection.style.display = 'block';
// 오늘 날짜로 초기화
const dateInput = document.getElementById('completedReportDate');
if (dateInput) {
dateInput.value = this.utils.getKoreaToday();
}
await this.loadCompletedReports();
}
}
/**
* TBM 데이터 로드
*/
async loadTbmData() {
try {
await this.api.loadIncompleteTbms();
await this.api.loadDailyIssuesForTbms();
// 렌더링은 기존 함수 사용 (점진적 마이그레이션)
if (typeof window.renderTbmWorkList === 'function') {
window.renderTbmWorkList();
}
} catch (error) {
console.error('[Controller] TBM 데이터 로드 오류:', error);
window.showMessage?.('TBM 데이터를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
/**
* 완료 보고서 로드
*/
async loadCompletedReports() {
try {
const dateInput = document.getElementById('completedReportDate');
const date = dateInput?.value || this.utils.getKoreaToday();
const reports = await this.api.loadCompletedReports(date);
// 렌더링은 기존 함수 사용
if (typeof window.renderCompletedReports === 'function') {
window.renderCompletedReports(reports);
}
} catch (error) {
console.error('[Controller] 완료 보고서 로드 오류:', error);
window.showMessage?.('완료 보고서를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
/**
* TBM 작업보고서 제출
*/
async submitTbmWorkReport(index) {
try {
const tbm = this.state.incompleteTbms[index];
if (!tbm) {
throw new Error('TBM 데이터를 찾을 수 없습니다.');
}
// 유효성 검사
const totalHoursInput = document.getElementById(`totalHours_${index}`);
const totalHours = parseFloat(totalHoursInput?.value);
if (!totalHours || totalHours <= 0) {
window.showMessage?.('작업시간을 입력해주세요.', 'warning');
return;
}
// 부적합 시간 계산
const defects = this.state.tempDefects[index] || [];
const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
const regularHours = totalHours - errorHours;
if (regularHours < 0) {
window.showMessage?.('부적합 시간이 총 작업시간을 초과할 수 없습니다.', 'warning');
return;
}
// API 데이터 구성
const user = this.state.getCurrentUser();
const reportData = {
tbm_session_id: tbm.session_id,
tbm_assignment_id: tbm.assignment_id,
user_id: tbm.user_id,
project_id: tbm.project_id,
work_type_id: tbm.work_type_id,
report_date: this.utils.formatDateForApi(tbm.session_date),
total_hours: totalHours,
regular_hours: regularHours,
error_hours: errorHours,
work_status_id: errorHours > 0 ? 2 : 1,
created_by: user?.user_id || user?.id,
defects: defects.map(d => ({
category_id: d.category_id,
item_id: d.item_id,
issue_report_id: d.issue_report_id,
defect_hours: d.defect_hours,
note: d.note
}))
};
const result = await this.api.submitTbmWorkReport(reportData);
window.showSaveResultModal?.(
'success',
'제출 완료',
`${tbm.worker_name}의 작업보고서가 제출되었습니다.`
);
// 목록 새로고침
await this.loadTbmData();
} catch (error) {
console.error('[Controller] 제출 오류:', error);
window.showSaveResultModal?.(
'error',
'제출 실패',
error.message || '작업보고서 제출 중 오류가 발생했습니다.'
);
}
}
/**
* 세션 일괄 제출
*/
async batchSubmitSession(sessionKey) {
const rows = document.querySelectorAll(`tr[data-session-key="${sessionKey}"][data-type="tbm"]`);
const indices = [];
rows.forEach(row => {
const index = parseInt(row.dataset.index);
const totalHoursInput = document.getElementById(`totalHours_${index}`);
if (totalHoursInput?.value && parseFloat(totalHoursInput.value) > 0) {
indices.push(index);
}
});
if (indices.length === 0) {
window.showMessage?.('제출할 항목이 없습니다. 작업시간을 입력해주세요.', 'warning');
return;
}
const confirmed = confirm(`${indices.length}건의 작업보고서를 일괄 제출하시겠습니까?`);
if (!confirmed) return;
let successCount = 0;
let failCount = 0;
for (const index of indices) {
try {
await this.submitTbmWorkReport(index);
successCount++;
} catch (error) {
failCount++;
console.error(`[Controller] 일괄 제출 오류 (index: ${index}):`, error);
}
}
if (failCount === 0) {
window.showSaveResultModal?.('success', '일괄 제출 완료', `${successCount}건이 성공적으로 제출되었습니다.`);
} else {
window.showSaveResultModal?.('warning', '일괄 제출 부분 완료', `성공: ${successCount}건, 실패: ${failCount}`);
}
}
/**
* 상태 디버그
*/
debug() {
console.log('[Controller] 상태 디버그:');
this.state.debug();
}
}
// 전역 인스턴스 생성
window.DailyWorkReportController = new DailyWorkReportController();
// 하위 호환성: 기존 전역 함수들
window.switchTab = (tab) => window.DailyWorkReportController.switchTab(tab);
window.submitTbmWorkReport = (index) => window.DailyWorkReportController.submitTbmWorkReport(index);
window.batchSubmitTbmSession = (sessionKey) => window.DailyWorkReportController.batchSubmitSession(sessionKey);
// 사용자 정보 함수
window.getUser = () => window.DailyWorkReportState.getUser();
window.getCurrentUser = () => window.DailyWorkReportState.getCurrentUser();
// 날짜 그룹 토글 (UI 함수)
window.toggleDateGroup = function(dateStr) {
const group = document.querySelector(`.date-group[data-date="${dateStr}"]`);
if (!group) return;
const isExpanded = group.classList.contains('expanded');
const content = group.querySelector('.date-group-content');
const icon = group.querySelector('.date-toggle-icon');
if (isExpanded) {
group.classList.remove('expanded');
group.classList.add('collapsed');
if (content) content.style.display = 'none';
if (icon) icon.textContent = '▶';
} else {
group.classList.remove('collapsed');
group.classList.add('expanded');
if (content) content.style.display = 'block';
if (icon) icon.textContent = '▼';
}
};
// DOMContentLoaded 이벤트에서 초기화
document.addEventListener('DOMContentLoaded', () => {
// 약간의 지연 후 초기화 (다른 스크립트 로드 대기)
setTimeout(() => {
window.DailyWorkReportController.init();
}, 100);
});
console.log('[Module] daily-work-report/index.js 로드 완료');

View File

@@ -1,51 +0,0 @@
// /js/work-report-api.js
import { apiGet, apiPost } from './api-helper.js';
/**
* 작업 보고서 작성을 위해 필요한 초기 데이터(작업자, 프로젝트, 태스크)를 가져옵니다.
* Promise.all을 사용하여 병렬로 API를 호출합니다.
* @returns {Promise<{workers: Array, projects: Array, tasks: Array}>}
*/
export async function getInitialData() {
try {
const [allWorkers, projects, tasks] = await Promise.all([
apiGet('/workers'),
apiGet('/projects'),
apiGet('/tasks')
]);
// 활성화된 작업자만 필터링
const workers = allWorkers.filter(worker => {
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
});
// 데이터 형식 검증
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
throw new Error('서버에서 받은 데이터 형식이 올바르지 않습니다.');
}
// 작업자 목록은 ID 기준으로 정렬
workers.sort((a, b) => a.user_id - b.user_id);
return { workers, projects, tasks };
} catch (error) {
console.error('초기 데이터 로딩 중 오류 발생:', error);
// 에러를 다시 던져서 호출한 쪽에서 처리할 수 있도록 함
throw error;
}
}
/**
* 작성된 작업 보고서 데이터를 서버에 전송합니다.
* @param {Array<object>} reportData - 전송할 작업 보고서 데이터 배열
* @returns {Promise<object>} - 서버의 응답 결과
*/
export async function createWorkReport(reportData) {
try {
const result = await apiPost('/workreports', reportData);
return result;
} catch (error) {
console.error('작업 보고서 생성 요청 실패:', error);
throw error;
}
}

View File

@@ -1,79 +0,0 @@
// /js/work-report-create.js
import { renderCalendar } from './calendar.js';
import { getInitialData, createWorkReport } from './work-report-api.js';
import { initializeReportTable, getReportData } from './work-report-ui.js';
// 전역 상태 변수
let selectedDate = '';
/**
* 날짜가 선택되었을 때 실행되는 콜백 함수.
* 초기 데이터를 로드하고 테이블을 렌더링합니다.
* @param {string} date - 선택된 날짜 (YYYY-MM-DD 형식)
*/
async function onDateSelect(date) {
selectedDate = date;
const tableBody = document.getElementById('reportBody');
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">데이터를 불러오는 중...</td></tr>';
try {
const initialData = await getInitialData();
initializeReportTable(initialData);
} catch (error) {
alert('데이터를 불러오는 데 실패했습니다: ' + error.message);
tableBody.innerHTML = '<tr><td colspan="8" class="text-center error">오류 발생! 데이터를 불러올 수 없습니다.</td></tr>';
}
}
/**
* '전체 등록' 버튼 클릭 시 실행되는 이벤트 핸들러.
* 폼 데이터를 서버에 전송합니다.
*/
async function handleSubmit() {
if (!selectedDate) {
alert('먼저 달력에서 날짜를 선택해주세요.');
return;
}
const reportData = getReportData();
if (!reportData) {
// getReportData 내부에서 이미 alert으로 사용자에게 알림
return;
}
// 각 항목에 선택된 날짜 추가
const payload = reportData.map(item => ({ ...item, date: selectedDate }));
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '등록 중...';
try {
const result = await createWorkReport(payload);
if (result.success) {
alert('✅ 작업 보고서가 성공적으로 등록되었습니다!');
// 성공 후 폼을 다시 로드하거나, 다른 페이지로 이동 등의 로직 추가 가능
onDateSelect(selectedDate); // 현재 날짜의 폼을 다시 로드
} else {
throw new Error(result.error || '알 수 없는 오류로 등록에 실패했습니다.');
}
} catch (error) {
alert('❌ 등록 실패: ' + error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '전체 등록';
}
}
/**
* 페이지 초기화 함수
*/
function initializePage() {
renderCalendar('calendar', onDateSelect);
const submitBtn = document.getElementById('submitBtn');
submitBtn.addEventListener('click', handleSubmit);
}
// DOM이 로드되면 페이지 초기화를 시작합니다.
document.addEventListener('DOMContentLoaded', initializePage);

View File

@@ -1,141 +0,0 @@
// /js/work-report-ui.js
const DEFAULT_PROJECT_ID = '13'; // 나중에는 API나 설정에서 받아오는 것이 좋음
const DEFAULT_TASK_ID = '15';
/**
* 주어진 데이터를 바탕으로 <select> 요소의 <option>들을 생성합니다.
* @param {Array<object>} items - 옵션으로 만들 데이터 배열
* @param {string} valueField - <option>의 value 속성에 사용할 필드 이름
* @param {string} textField - <option>의 텍스트에 사용할 필드 이름
* @returns {string} - 생성된 HTML 옵션 문자열
*/
function createOptions(items, valueField, textField) {
return items.map(item => `<option value="${item[valueField]}">${textField(item)}</option>`).join('');
}
/**
* 테이블의 모든 행 번호를 다시 매깁니다.
* @param {HTMLTableSectionElement} tableBody - tbody 요소
*/
function updateRowNumbers(tableBody) {
tableBody.querySelectorAll('tr').forEach((tr, index) => {
tr.cells[0].textContent = index + 1;
});
}
/**
* 하나의 작업 보고서 행(tr)을 생성합니다.
* @param {object} worker - 작업자 정보
* @param {Array} projects - 전체 프로젝트 목록
* @param {Array} tasks - 전체 태스크 목록
* @param {number} index - 행 번호
* @returns {HTMLTableRowElement} - 생성된 tr 요소
*/
function createReportRow(worker, projects, tasks, index) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${index + 1}</td>
<td>
<input type="hidden" name="user_id" value="${worker.user_id}">
${worker.worker_name}
</td>
<td><select name="project_id">${createOptions(projects, 'project_id', p => p.project_name)}</select></td>
<td><select name="task_id">${createOptions(tasks, 'task_id', t => `${t.category}:${t.subcategory}`)}</select></td>
<td>
<select name="overtime">
<option value="">없음</option>
${[1, 2, 3, 4].map(n => `<option>${n}</option>`).join('')}
</select>
</td>
<td>
<select name="work_type">
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(t => `<option>${t}</option>`).join('')}
</select>
</td>
<td><input type="text" name="memo" placeholder="메모"></td>
<td><button type="button" class="remove-btn">x</button></td>
`;
// 이벤트 리스너 설정
const workTypeSelect = tr.querySelector('[name="work_type"]');
const projectSelect = tr.querySelector('[name="project_id"]');
const taskSelect = tr.querySelector('[name="task_id"]');
workTypeSelect.addEventListener('change', () => {
const isDisabled = ['연차', '휴무', '유급'].includes(workTypeSelect.value);
projectSelect.disabled = isDisabled;
taskSelect.disabled = isDisabled;
if (isDisabled) {
projectSelect.value = DEFAULT_PROJECT_ID;
taskSelect.value = DEFAULT_TASK_ID;
}
});
tr.querySelector('.remove-btn').addEventListener('click', () => {
tr.remove();
updateRowNumbers(tr.parentElement);
});
return tr;
}
/**
* 작업 보고서 테이블을 초기화하고 데이터를 채웁니다.
* @param {{workers: Array, projects: Array, tasks: Array}} initialData - 초기 데이터
*/
export function initializeReportTable(initialData) {
const tableBody = document.getElementById('reportBody');
if (!tableBody) return;
tableBody.innerHTML = ''; // 기존 내용 초기화
const { workers, projects, tasks } = initialData;
if (!workers || workers.length === 0) {
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">등록할 작업자 정보가 없습니다.</td></tr>';
return;
}
workers.forEach((worker, index) => {
const row = createReportRow(worker, projects, tasks, index);
tableBody.appendChild(row);
});
}
/**
* 테이블에서 폼 데이터를 추출하여 배열로 반환합니다.
* @returns {Array<object>|null} - 추출된 데이터 배열 또는 유효성 검사 실패 시 null
*/
export function getReportData() {
const tableBody = document.getElementById('reportBody');
const rows = tableBody.querySelectorAll('tr');
if (rows.length === 0 || (rows.length === 1 && rows[0].cells.length < 2)) {
alert('등록할 내용이 없습니다.');
return null;
}
const reportData = [];
const workerIds = new Set();
for (const tr of rows) {
const workerId = tr.querySelector('[name="user_id"]').value;
if (workerIds.has(workerId)) {
alert(`오류: 작업자 '${tr.cells[1].textContent.trim()}'가 중복 등록되었습니다.`);
return null;
}
workerIds.add(workerId);
reportData.push({
user_id: workerId,
project_id: tr.querySelector('[name="project_id"]').value,
task_id: tr.querySelector('[name="task_id"]').value,
overtime_hours: tr.querySelector('[name="overtime"]').value || 0,
work_details: tr.querySelector('[name="work_type"]').value,
memo: tr.querySelector('[name="memo"]').value
});
}
return reportData;
}

View File

@@ -50,13 +50,11 @@ const elements = {
// ========== 초기화 ========== //
document.addEventListener('DOMContentLoaded', async () => {
console.log('🔧 관리자 설정 페이지 초기화 시작');
try {
await initializePage();
console.log('✅ 관리자 설정 페이지 초기화 완료');
} catch (error) {
console.error(' 페이지 초기화 오류:', error);
console.error(' 페이지 초기화 오류:', error);
showToast('페이지를 불러오는 중 오류가 발생했습니다.', 'error');
}
});
@@ -75,7 +73,6 @@ function setupUserInfo() {
const authData = getAuthData();
if (authData && authData.user) {
currentUser = authData.user;
console.log('👤 사용자 정보 로드 완료:', currentUser.name, currentUser.role);
}
}
@@ -150,13 +147,11 @@ function setupEventListeners() {
// ========== 사용자 관리 ========== //
async function loadUsers() {
try {
console.log('👥 사용자 목록 로딩...');
// 실제 API에서 사용자 데이터 가져오기
const response = await window.apiCall('/users');
users = Array.isArray(response) ? response : (response.data || []);
console.log(`✅ 사용자 ${users.length}명 로드 완료`);
// 필터링된 사용자 목록 초기화
filteredUsers = [...users];
@@ -165,7 +160,7 @@ async function loadUsers() {
renderUsersTable();
} catch (error) {
console.error(' 사용자 목록 로딩 오류:', error);
console.error(' 사용자 목록 로딩 오류:', error);
showToast('사용자 목록을 불러오는 중 오류가 발생했습니다.', 'error');
users = [];
filteredUsers = [];
@@ -556,9 +551,8 @@ async function loadAllPages() {
try {
const response = await apiCall('/pages');
allPages = response.data || response || [];
console.log('📄 페이지 목록 로드:', allPages.length, '개');
} catch (error) {
console.error(' 페이지 목록 로드 오류:', error);
console.error(' 페이지 목록 로드 오류:', error);
allPages = [];
}
}
@@ -568,9 +562,8 @@ async function loadUserPageAccess(userId) {
try {
const response = await apiCall(`/users/${userId}/page-access`);
userPageAccess = response.data?.pageAccess || [];
console.log(`👤 사용자 ${userId} 페이지 권한 로드:`, userPageAccess.length, '개');
} catch (error) {
console.error(' 사용자 페이지 권한 로드 오류:', error);
console.error(' 사용자 페이지 권한 로드 오류:', error);
userPageAccess = [];
}
}
@@ -595,15 +588,13 @@ async function savePageAccess(userId, containerId = null) {
const pageAccessData = Array.from(pageAccessMap.values());
console.log('📤 페이지 권한 저장:', userId, pageAccessData);
await apiCall(`/users/${userId}/page-access`, 'PUT', {
pageAccess: pageAccessData
});
console.log('✅ 페이지 권한 저장 완료');
} catch (error) {
console.error(' 페이지 권한 저장 오류:', error);
console.error(' 페이지 권한 저장 오류:', error);
throw error;
}
}
@@ -647,7 +638,7 @@ async function managePageAccess(userId) {
// 모달 표시
document.getElementById('pageAccessModal').style.display = 'flex';
} catch (error) {
console.error(' 페이지 권한 관리 모달 오류:', error);
console.error(' 페이지 권한 관리 모달 오류:', error);
showToast('페이지 권한 관리를 열 수 없습니다.', 'error');
}
}
@@ -775,7 +766,7 @@ async function savePageAccessFromModal() {
closePageAccessModal();
} catch (error) {
console.error(' 페이지 권한 저장 오류:', error);
console.error(' 페이지 권한 저장 오류:', error);
showToast('페이지 권한 저장에 실패했습니다.', 'error');
}
}

View File

@@ -35,7 +35,7 @@ if ('caches' in window) {
* sso_token이 없으면 기존 token도 확인 (하위 호환)
*/
window.getSSOToken = function() {
return cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('token');
return cookieGet('sso_token') || localStorage.getItem('sso_token');
};
/**
@@ -43,10 +43,6 @@ if ('caches' in window) {
*/
window.getSSOUser = function() {
var raw = cookieGet('sso_user') || localStorage.getItem('sso_user');
if (!raw) {
// 기존 user 키도 확인 (하위 호환)
raw = localStorage.getItem('user');
}
try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; }
};
@@ -69,13 +65,9 @@ if ('caches' in window) {
cookieRemove('sso_token');
cookieRemove('sso_user');
cookieRemove('sso_refresh_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('sso_refresh_token');
// 기존 키도 삭제 (하위 호환)
localStorage.removeItem('token');
localStorage.removeItem('user');
localStorage.removeItem('userPageAccess');
['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(function(k) {
localStorage.removeItem(k);
});
};
// ==================== 보안 유틸리티 (XSS 방지) ====================

View File

@@ -7,12 +7,10 @@ function getApiBaseUrl() {
const protocol = window.location.protocol;
const port = window.location.port;
console.log('🌐 감지된 환경:', { hostname, protocol, port });
// 🔗 외부 도메인 (Cloudflare Tunnel) - Gateway nginx가 /api/를 프록시
if (hostname.includes('technicalkorea.net')) {
const baseUrl = `${protocol}//${hostname}${config.api.path}`;
console.log('✅ Gateway 프록시 사용:', baseUrl);
return baseUrl;
}
@@ -21,27 +19,24 @@ function getApiBaseUrl() {
hostname === 'localhost' || hostname === '127.0.0.1' ||
hostname.includes('.local') || hostname.includes('hyungi')) {
const baseUrl = `${protocol}//${hostname}:${config.api.port}${config.api.path}`;
console.log('✅ 로컬 직접 접근:', baseUrl);
return baseUrl;
}
// 🚨 기타: 포트 없이 상대 경로
const baseUrl = `${protocol}//${hostname}${config.api.path}`;
console.log('✅ 기본 프록시 사용:', baseUrl);
return baseUrl;
}
// API 설정
const API_URL = getApiBaseUrl();
// 전역 변수로 설정
window.API = API_URL;
window.API_BASE_URL = API_URL;
// 전역 변수로 설정 (api-base.js가 이미 설정한 경우 유지)
if (!window.API) window.API = API_URL;
if (!window.API_BASE_URL) window.API_BASE_URL = API_URL;
function ensureAuthenticated() {
const token = localStorage.getItem('sso_token');
if (!token || token === 'undefined' || token === 'null') {
console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.');
clearAuthData(); // 만약을 위해 한번 더 정리
redirectToLogin();
return false; // 이후 코드 실행 방지
@@ -49,7 +44,6 @@ function ensureAuthenticated() {
// 토큰 만료 확인
if (isTokenExpired(token)) {
console.log('🚨 토큰이 만료되었습니다. 로그인 페이지로 이동합니다.');
clearAuthData();
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
redirectToLogin();
@@ -75,8 +69,6 @@ function isTokenExpired(token) {
function clearAuthData() {
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('userInfo');
localStorage.removeItem('currentUser');
// SSO 쿠키도 삭제 (로그인 페이지 자동 리다이렉트 방지)
var cookieDomain = window.location.hostname.includes('technicalkorea.net')
? '; domain=.technicalkorea.net' : '';
@@ -112,12 +104,10 @@ async function apiCall(url, method = 'GET', data = null) {
}
try {
console.log(`📡 API 호출: ${fullUrl} (${method})`);
const response = await fetch(fullUrl, options);
// 인증 만료 처리
if (response.status === 401) {
console.error('🚨 인증 실패: 토큰이 만료되었거나 유효하지 않습니다.');
clearAuthData();
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
redirectToLogin();
@@ -132,8 +122,7 @@ async function apiCall(url, method = 'GET', data = null) {
if (contentType && contentType.includes('application/json')) {
const errorData = await response.json();
console.error('📋 서버 에러 상세:', errorData);
// 에러 메시지 추출 (여러 형식 지원)
if (typeof errorData === 'string') {
errorMessage = errorData;
@@ -150,23 +139,18 @@ async function apiCall(url, method = 'GET', data = null) {
}
} else {
const errorText = await response.text();
console.error('📋 서버 에러 텍스트:', errorText);
errorMessage = errorText || errorMessage;
errorMessage = errorText || errorMessage;
}
} catch (e) {
console.error('📋 에러 파싱 중 예외 발생:', e.message);
// 파싱 실패해도 HTTP 상태 코드는 전달
}
throw new Error(errorMessage);
}
const result = await response.json();
console.log(`✅ API 성공: ${fullUrl}`);
return result;
} catch (error) {
console.error(`❌ API 오류 (${fullUrl}):`, error);
console.error('❌ 에러 전체 내용:', JSON.stringify(error, null, 2));
// 네트워크 오류 vs 서버 오류 구분
if (error.name === 'TypeError' && error.message.includes('fetch')) {
@@ -177,36 +161,6 @@ async function apiCall(url, method = 'GET', data = null) {
}
}
// 디버깅 정보
console.log('🔗 API Base URL:', API);
console.log('🌐 Current Location:', {
hostname: window.location.hostname,
protocol: window.location.protocol,
port: window.location.port,
href: window.location.href
});
// 🧪 API 연결 테스트 함수 (개발용)
async function testApiConnection() {
try {
console.log('🧪 API 연결 테스트 시작...');
const response = await fetch(`${API}/health`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
console.log('✅ API 연결 성공!');
return true;
} else {
console.log('❌ API 연결 실패:', response.status);
return false;
}
} catch (error) {
console.log('❌ API 연결 오류:', error.message);
return false;
}
}
// API 헬퍼 함수들
async function apiGet(url) {
@@ -225,35 +179,26 @@ async function apiDelete(url) {
return apiCall(url, 'DELETE');
}
// 전역 함수로 설정
// 전역 함수로 설정 (api-base.js가 이미 등록한 것은 덮어쓰지 않음)
window.ensureAuthenticated = ensureAuthenticated;
window.getAuthHeaders = getAuthHeaders;
window.apiCall = apiCall;
if (!window.getAuthHeaders) window.getAuthHeaders = getAuthHeaders;
if (!window.apiCall) window.apiCall = apiCall;
window.apiGet = apiGet;
window.apiPost = apiPost;
window.apiPut = apiPut;
window.apiDelete = apiDelete;
window.testApiConnection = testApiConnection;
window.isTokenExpired = isTokenExpired;
window.clearAuthData = clearAuthData;
// 개발 모드에서 자동 테스트
if (window.location.hostname === 'localhost' || window.location.hostname.startsWith('192.168.')) {
setTimeout(() => {
testApiConnection();
}, 1000);
}
if (!window.clearAuthData) window.clearAuthData = clearAuthData;
// 주기적으로 토큰 만료 확인 (5분마다)
setInterval(() => {
const token = localStorage.getItem('sso_token');
if (token && isTokenExpired(token)) {
console.log('🚨 주기적 확인: 토큰이 만료되었습니다.');
clearAuthData();
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
redirectToLogin();
}
}, config.app.tokenRefreshInterval); // 5분마다 확인
}, config.app.tokenRefreshInterval);
// ES6 모듈 export
export { API_URL as API_BASE_URL, API_URL as API, apiCall, getAuthHeaders };

View File

@@ -8,7 +8,7 @@ const API_BASE_URL = window.API_BASE_URL || 'http://localhost:30005/api';
function getToken() {
// SSO 토큰 우선, 기존 token 폴백
if (window.getSSOToken) return window.getSSOToken();
const token = localStorage.getItem('sso_token') || localStorage.getItem('token');
const token = localStorage.getItem('sso_token');
return token && token !== 'undefined' && token !== 'null' ? token : null;
}
@@ -16,8 +16,6 @@ function clearAuthData() {
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('token');
localStorage.removeItem('user');
}
/**

View File

@@ -12,26 +12,24 @@
// ===== 인증 함수 (api-base.js의 SSO 함수 활용) =====
function isLoggedIn() {
const token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
const token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
return token && token !== 'undefined' && token !== 'null';
}
function getUser() {
if (window.getSSOUser) return window.getSSOUser();
const user = localStorage.getItem('sso_user') || localStorage.getItem('user');
const user = localStorage.getItem('sso_user');
try { return user ? JSON.parse(user) : null; } catch(e) { return null; }
}
function getToken() {
return window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
return window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
}
function clearAuthData() {
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('token');
localStorage.removeItem('user');
localStorage.removeItem('userPageAccess');
}

View File

@@ -50,7 +50,6 @@ function getCacheStatus() {
*/
function clearCache() {
dateStatusCache.clear();
console.log('📦 캐시가 클리어되었습니다.');
}
/**
@@ -77,7 +76,6 @@ function updatePerformanceUI() {
*/
function logPerformanceStatus() {
const status = getCacheStatus();
console.log('📊 성능 상태:', status);
updatePerformanceUI();
}
@@ -118,7 +116,7 @@ function getCurrentUser() {
}
try {
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo');
const userInfo = localStorage.getItem('sso_user');
if (userInfo) {
return JSON.parse(userInfo);
}
@@ -391,18 +389,14 @@ async function calculateDateStatus(dateStr) {
}
try {
console.log(`📊 ${dateStr} 상태 계산 시작 - 순차 호출`);
// 1단계: WorkReports 먼저 가져오기
console.log(`📝 1단계: WorkReports 조회 중...`);
const workReports = await fetchWorkReports(dateStr);
// 2초 대기 (서버 부하 방지)
console.log(`⏳ 2초 대기 중... (서버 부하 방지)`);
await new Promise(resolve => setTimeout(resolve, 2000));
// 2단계: DailyWorkReports 가져오기
console.log(`📊 2단계: DailyWorkReports 조회 중...`);
const dailyReports = await fetchDailyWorkReports(dateStr);
let status;
@@ -423,7 +417,6 @@ async function calculateDateStatus(dateStr) {
// 캐시에 저장
dateStatusCache.set(dateStr, status);
console.log(`${dateStr} 상태 계산 완료: ${status}`);
return status;
} catch (error) {
console.error('날짜 상태 계산 오류:', error);
@@ -564,10 +557,9 @@ async function loadAndUpdateDateStatus(dateStr, buttonElement) {
}`;
}
console.log(`${dateStr} 상태 로드 완료: ${status}`);
} catch (error) {
console.error(` ${dateStr} 상태 로드 실패:`, error);
console.error(` ${dateStr} 상태 로드 실패:`, error);
buttonElement.classList.remove('loading-state');
buttonElement.classList.add('error-state');
buttonElement.title = `${dateStr} - 로드 실패: ${error.message}`;
@@ -589,18 +581,14 @@ async function loadAndUpdateDateStatus(dateStr, buttonElement) {
*/
async function getWorkersForDate(dateStr) {
try {
console.log(`👥 ${dateStr} 작업자 데이터 조합 시작 - 순차 호출`);
// 1단계: WorkReports 먼저 가져오기
console.log(`📝 1단계: WorkReports 조회 중...`);
const workReports = await fetchWorkReports(dateStr);
// 2초 대기 (서버 부하 방지)
console.log(`⏳ 2초 대기 중... (서버 부하 방지)`);
await new Promise(resolve => setTimeout(resolve, 2000));
// 2단계: DailyWorkReports 가져오기
console.log(`📊 2단계: DailyWorkReports 조회 중...`);
const dailyReports = await fetchDailyWorkReports(dateStr);
const workerMap = new Map();
@@ -645,7 +633,6 @@ async function getWorkersForDate(dateStr) {
validationStatus: getValidationStatus(worker)
}));
console.log(`${dateStr} 작업자 데이터 조합 완료: ${result.length}`);
return result;
} catch (error) {
@@ -1022,11 +1009,6 @@ async function init() {
window.saveEditedWork = saveEditedWork;
window.deleteWorker = deleteWorker;
console.log('✅ 근태 검증 관리 시스템 초기화 완료 (API 통합)');
console.log(`🔗 API 경로: ${API}`);
console.log(`📊 설정: 동시 최대 ${RATE_LIMIT.maxConcurrent}개 요청, ${RATE_LIMIT.delayBetweenRequests}ms 딜레이`);
console.log('🔄 API 호출 방식: 통합 설정 + 순차 호출');
console.log('🚫 429 에러 방지: 각 날짜당 최소 5초 간격');
} catch (error) {
console.error('초기화 오류:', error);

View File

@@ -123,7 +123,6 @@ async function checkPageAccess(pageKey) {
// 즉시 실행 함수로 스코프를 보호하고 로직을 실행
(async function() {
if (!isLoggedIn()) {
console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.');
clearAuthData(); // 만약을 위해 한번 더 정리
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
return; // 이후 코드 실행 방지
@@ -133,31 +132,28 @@ async function checkPageAccess(pageKey) {
// 사용자 정보가 유효한지 확인 (토큰은 있지만 유저 정보가 깨졌을 경우)
if (!currentUser || !currentUser.username) {
console.error('🚨 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.');
console.error(' 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.');
clearAuthData();
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
return;
}
const userRole = currentUser.role || currentUser.access_level || '사용자';
console.log(`${currentUser.username}(${userRole})님 인증 성공.`);
// 페이지 접근 권한 체크 (Admin은 건너뛰기)
if (currentUser.role !== 'Admin' && currentUser.role !== 'System Admin') {
const pageKey = getCurrentPageKey();
if (pageKey) {
console.log(`🔍 페이지 권한 체크: ${pageKey}`);
const hasAccess = await checkPageAccess(pageKey);
if (!hasAccess) {
console.error(`🚫 페이지 접근 권한이 없습니다: ${pageKey}`);
console.error(` 페이지 접근 권한이 없습니다: ${pageKey}`);
alert('이 페이지에 접근할 권한이 없습니다.');
window.location.href = '/pages/dashboard.html';
return;
}
console.log(`✅ 페이지 접근 권한 확인됨: ${pageKey}`);
}
}

View File

@@ -19,7 +19,7 @@ export function parseJwt(token) {
*/
export function getToken() {
if (window.getSSOToken) return window.getSSOToken();
return localStorage.getItem('sso_token') || localStorage.getItem('token');
return localStorage.getItem('sso_token');
}
/**
@@ -27,7 +27,7 @@ export function getToken() {
*/
export function getUser() {
if (window.getSSOUser) return window.getSSOUser();
const raw = localStorage.getItem('sso_user') || localStorage.getItem('user');
const raw = localStorage.getItem('sso_user');
try {
return raw ? JSON.parse(raw) : null;
} catch(e) {
@@ -37,16 +37,11 @@ export function getUser() {
/**
* 로그인 성공 후 토큰과 사용자 정보를 저장합니다.
* 하위 호환성을 위해 sso_token/sso_user와 token/user 모두에 저장합니다.
* sso_token/sso_user 키로 저장합니다.
*/
export function saveAuthData(token, user) {
const userStr = JSON.stringify(user);
// SSO 키
localStorage.setItem('sso_token', token);
localStorage.setItem('sso_user', userStr);
// 하위 호환 키 (캐시된 구버전 app-init.js 대응)
localStorage.setItem('token', token);
localStorage.setItem('user', userStr);
localStorage.setItem('sso_user', JSON.stringify(user));
}
/**
@@ -56,8 +51,6 @@ export function clearAuthData() {
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('token');
localStorage.removeItem('user');
localStorage.removeItem('userPageAccess');
}

View File

@@ -205,6 +205,4 @@ form?.addEventListener('submit', async (e) => {
// 페이지 로드 시 현재 사용자 정보 표시
document.addEventListener('DOMContentLoaded', () => {
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
console.log('🔐 비밀번호 변경 페이지 로드됨');
console.log('👤 현재 사용자:', user.username || 'Unknown');
});

View File

@@ -43,14 +43,14 @@ async function getComponentHtml(componentName, componentPath) {
export async function loadComponent(componentName, containerSelector, domProcessor = null) {
const container = document.querySelector(containerSelector);
if (!container) {
console.warn(`⚠️ 컴포넌트를 삽입할 컨테이너를 찾을 수 없습니다: ${containerSelector} (선택사항일 수 있음)`);
console.warn(` 컴포넌트를 삽입할 컨테이너를 찾을 수 없습니다: ${containerSelector} (선택사항일 수 있음)`);
return;
}
const componentPath = config.components[componentName];
if (!componentPath) {
console.error(`🔴 설정 파일(config.js)에서 '${componentName}' 컴포넌트의 경로를 찾을 수 없습니다.`);
container.innerHTML = `<p>${componentName} 로딩 실패</p>`;
console.error(` 설정 파일(config.js)에서 '${componentName}' 컴포넌트의 경로를 찾을 수 없습니다.`);
container.textContent = `${componentName} 로딩 실패`;
return;
}
@@ -72,10 +72,9 @@ export async function loadComponent(componentName, containerSelector, domProcess
container.innerHTML = htmlText;
}
console.log(`✅ '${componentName}' 컴포넌트 로딩 완료: ${containerSelector}`);
} catch (error) {
console.error(`🔴 '${componentName}' 컴포넌트 로딩 실패:`, error);
container.innerHTML = `<p>${componentName} 로딩에 실패했습니다. 관리자에게 문의하세요.</p>`;
console.error(` '${componentName}' 컴포넌트 로딩 실패:`, error);
container.textContent = `${componentName} 로딩에 실패했습니다. 관리자에게 문의하세요.`;
}
}

View File

@@ -1,7 +1,7 @@
// daily-work-report.js - 브라우저 호환 버전
// =================================================================
// 🌐 API 설정 (window 객체에서 가져오기)
// API 설정 (window 객체에서 가져오기)
// =================================================================
// API 설정은 api-config.js에서 window 객체에 설정됨
@@ -183,7 +183,7 @@ function formatDateForApi(date) {
*/
function getUser() {
if (window.getSSOUser) return window.getSSOUser();
const raw = localStorage.getItem('sso_user') || localStorage.getItem('user');
const raw = localStorage.getItem('sso_user');
try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; }
}
@@ -233,7 +233,7 @@ function renderTbmWorkList() {
<div style="margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0;">작업보고서 목록</h3>
<button type="button" class="btn-add-work" onclick="addManualWorkRow()">
작업 추가
작업 추가
</button>
</div>
`;
@@ -247,7 +247,7 @@ function renderTbmWorkList() {
<span class="tbm-session-info" style="color: white; font-weight: 500;">TBM에 없는 작업을 추가로 입력할 수 있습니다</span>
</div>
<button type="button" class="btn-batch-submit" onclick="submitAllManualWorkReports()" style="background: #fff; color: #d97706; border: none; padding: 0.4rem 0.8rem; border-radius: 4px; font-weight: 600; cursor: pointer; font-size: 0.8rem;">
📤 일괄 제출
일괄 제출
</button>
</div>
<div class="tbm-table-container">
@@ -326,7 +326,7 @@ function renderTbmWorkList() {
html += `
<div class="issue-reminder-section">
<div class="issue-reminder-header">
<span class="issue-reminder-icon">⚠️</span>
<span class="issue-reminder-icon"></span>
<span class="issue-reminder-title">당일 신고된 문제</span>
<span class="issue-reminder-count">${relatedIssues.length}건</span>
</div>
@@ -349,7 +349,7 @@ function renderTbmWorkList() {
${relatedIssues.length > 5 ? `<div class="issue-reminder-more">외 ${relatedIssues.length - 5}건 더 있음</div>` : ''}
</div>
<div class="issue-reminder-hint">
💡 위 문제로 인해 작업이 지연되었다면, 아래에서 부적합 시간을 추가해주세요.
위 문제로 인해 작업이 지연되었다면, 아래에서 부적합 시간을 추가해주세요.
</div>
</div>
`;
@@ -474,7 +474,7 @@ function renderTbmWorkList() {
<button type="button"
class="btn-batch-submit"
onclick="batchSubmitTbmSession('${key}')">
📤 이 세션 일괄제출 (${group.items.length}건)
이 세션 일괄제출 (${group.items.length}건)
</button>
</div>
</div>
@@ -572,7 +572,7 @@ window.submitTbmWorkReport = async function(index) {
}
// 부적합 원인 유효성 검사 (issue_report_id 또는 category_id 또는 error_type_id 필요)
console.log('🔍 부적합 검증 시작:', defects.map(d => ({
console.log(' 부적합 검증 시작:', defects.map(d => ({
defect_hours: d.defect_hours,
category_id: d.category_id,
item_id: d.item_id,
@@ -583,7 +583,7 @@ window.submitTbmWorkReport = async function(index) {
const invalidDefects = defects.filter(d => d.defect_hours > 0 && !d.error_type_id && !d.issue_report_id && !d.category_id && !d.item_id);
if (invalidDefects.length > 0) {
console.error(' 유효하지 않은 부적합:', invalidDefects);
console.error(' 유효하지 않은 부적합:', invalidDefects);
showMessage('부적합 시간이 있는 항목은 원인을 선택해주세요.', 'error');
return;
}
@@ -610,8 +610,6 @@ window.submitTbmWorkReport = async function(index) {
work_status_id: errorHours > 0 ? 2 : 1
};
console.log('🔍 TBM 제출 데이터:', JSON.stringify(reportData, null, 2));
console.log('🔍 부적합 원인:', defects);
try {
const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', reportData);
@@ -623,7 +621,7 @@ window.submitTbmWorkReport = async function(index) {
// 부적합 원인이 있으면 저장 (이슈 기반 또는 레거시)
if (defects.length > 0 && response.data?.report_id) {
const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0);
console.log('📋 부적합 원인 필터링:', {
console.log(' 부적합 원인 필터링:', {
전체: defects.length,
유효: validDefects.length,
validDefects: validDefects.map(d => ({
@@ -645,20 +643,17 @@ window.submitTbmWorkReport = async function(index) {
note: d.note || ''
}));
console.log('📤 부적합 저장 요청:', defectsToSend);
const defectResponse = await window.apiCall(`/daily-work-reports/${response.data.report_id}/defects`, 'PUT', {
defects: defectsToSend
});
if (!defectResponse.success) {
console.error(' 부적합 저장 실패:', defectResponse);
console.error(' 부적합 저장 실패:', defectResponse);
showMessage('작업보고서는 저장되었으나 부적합 원인 저장에 실패했습니다.', 'warning');
} else {
console.log('✅ 부적합 저장 성공:', defectResponse);
}
} else {
console.log('⚠️ 유효한 부적합 항목이 없어 저장 건너뜀');
}
}
@@ -808,7 +803,7 @@ window.batchSubmitTbmSession = async function(sessionKey) {
'success',
'일괄제출 완료',
`${totalCount}건의 작업보고서가 모두 성공적으로 제출되었습니다.`,
results.success.map(name => ` ${name}`)
results.success.map(name => ` ${name}`)
);
} else if (successCount === 0) {
// 모두 실패
@@ -816,13 +811,13 @@ window.batchSubmitTbmSession = async function(sessionKey) {
'error',
'일괄제출 실패',
`${totalCount}건의 작업보고서가 모두 실패했습니다.`,
results.failed.map(msg => ` ${msg}`)
results.failed.map(msg => ` ${msg}`)
);
} else {
// 일부 성공, 일부 실패
const details = [
...results.success.map(name => ` ${name} - 성공`),
...results.failed.map(msg => ` ${msg}`)
...results.success.map(name => ` ${name} - 성공`),
...results.failed.map(msg => ` ${msg}`)
];
showSaveResultModal(
'warning',
@@ -840,7 +835,7 @@ window.batchSubmitTbmSession = async function(sessionKey) {
} finally {
submitBtn.classList.remove('is-loading');
submitBtn.disabled = false;
submitBtn.textContent = `📤 이 세션 일괄제출 (${sessionRows.length}건)`;
submitBtn.textContent = ` 이 세션 일괄제출 (${sessionRows.length}건)`;
}
};
@@ -892,7 +887,7 @@ window.addManualWorkRow = function() {
<input type="hidden" id="workplace_${manualIndex}">
<div id="workplaceDisplay_${manualIndex}" class="workplace-select-box" style="display: flex; flex-direction: column; gap: 0.25rem; padding: 0.5rem; background: #f9fafb; border: 2px solid #e5e7eb; border-radius: 6px; min-height: 60px; cursor: pointer;" onclick="openWorkplaceMapForManual('${manualIndex}')">
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: #6b7280; font-weight: 500;">
<span>🗺️</span>
<span></span>
<span>작업장소</span>
</div>
<div id="workplaceText_${manualIndex}" style="font-size: 0.8rem; color: #9ca3af; font-style: italic;">
@@ -923,7 +918,7 @@ window.addManualWorkRow = function() {
제출
</button>
<button type="button" class="btn-delete-compact" onclick="removeManualWorkRow('${manualIndex}')" style="margin-left: 4px;">
</button>
</td>
`;
@@ -1049,13 +1044,13 @@ window.openWorkplaceMapForManual = async function(manualIndex) {
const safeImage = escapeHtml(cat.layout_image || '');
return `
<button type="button" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceCategory(${safeId}, "${safeName.replace(/"/g, '&quot;')}", "${safeImage.replace(/"/g, '&quot;')}")'>
<span style="margin-right: 0.5rem;">🏭</span>
<span style="margin-right: 0.5rem;"></span>
${safeName}
</button>
`;
}).join('') + `
<button type="button" class="btn btn-secondary" style="width: 100%; text-align: left; margin-top: 0.5rem; background-color: #f0f9ff; border-color: #0ea5e9;" onclick='selectExternalWorkplace()'>
<span style="margin-right: 0.5rem;">🌐</span>
<span style="margin-right: 0.5rem;"></span>
외부 (외근/연차/휴무 등)
</button>
`;
@@ -1107,7 +1102,7 @@ window.selectWorkplaceCategory = async function(categoryId, categoryName, layout
const safeName = escapeHtml(wp.workplace_name);
return `
<button type="button" id="workplace-${safeId}" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceFromList(${safeId}, "${safeName.replace(/"/g, '&quot;')}")'>
<span style="margin-right: 0.5rem;">📍</span>
<span style="margin-right: 0.5rem;"></span>
${safeName}
</button>
`;
@@ -1136,7 +1131,6 @@ async function loadWorkplaceMap(categoryId, layoutImagePath, workplaces) {
? layoutImagePath
: `${apiBaseUrl}${layoutImagePath}`;
console.log('🖼️ 이미지 로드 시도:', fullImageUrl);
// 지도 영역 데이터 로드
const regionsResponse = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`);
@@ -1164,11 +1158,10 @@ async function loadWorkplaceMap(categoryId, layoutImagePath, workplaces) {
// 클릭 이벤트 리스너 추가
mapCanvas.onclick = handleMapClick;
console.log(`✅ 작업장 지도 로드 완료: ${mapRegions.length}개 영역`);
};
mapImage.onerror = function() {
console.error(' 지도 이미지 로드 실패');
console.error(' 지도 이미지 로드 실패');
document.getElementById('layoutMapArea').style.display = 'none';
showMessage('지도를 불러올 수 없어 리스트로 표시합니다.', 'warning');
};
@@ -1176,7 +1169,7 @@ async function loadWorkplaceMap(categoryId, layoutImagePath, workplaces) {
mapImage.src = fullImageUrl;
} catch (error) {
console.error(' 작업장 지도 로드 오류:', error);
console.error(' 작업장 지도 로드 오류:', error);
document.getElementById('layoutMapArea').style.display = 'none';
}
}
@@ -1301,12 +1294,12 @@ window.confirmWorkplaceSelection = function() {
if (displayDiv) {
displayDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: #059669; font-weight: 600;">
<span></span>
<span></span>
<span>작업장소 선택됨</span>
</div>
<div style="font-size: 0.8rem; color: #111827; font-weight: 500;">
<div style="color: #6b7280; font-size: 0.7rem; margin-bottom: 2px;">🏭 ${escapeHtml(selectedWorkplaceCategoryName)}</div>
<div>📍 ${escapeHtml(selectedWorkplaceName)}</div>
<div style="color: #6b7280; font-size: 0.7rem; margin-bottom: 2px;"> ${escapeHtml(selectedWorkplaceCategoryName)}</div>
<div> ${escapeHtml(selectedWorkplaceName)}</div>
</div>
`;
displayDiv.style.background = '#ecfdf5';
@@ -1354,11 +1347,11 @@ window.selectExternalWorkplace = function() {
if (displayDiv) {
displayDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: #0284c7; font-weight: 600;">
<span></span>
<span></span>
<span>외부 선택됨</span>
</div>
<div style="font-size: 0.8rem; color: #111827; font-weight: 500;">
<div>🌐 ${escapeHtml(externalWorkplaceName)}</div>
<div> ${escapeHtml(externalWorkplaceName)}</div>
</div>
`;
displayDiv.style.background = '#f0f9ff';
@@ -1778,10 +1771,10 @@ function renderCompletedReports(reports) {
</div>
<div class="report-actions" style="display: flex; gap: 0.5rem; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid #e5e7eb;">
<button type="button" class="btn btn-sm btn-secondary" onclick='openEditReportModal(${JSON.stringify(report).replace(/'/g, "&#39;")})' style="flex: 1; padding: 0.4rem 0.6rem; font-size: 0.75rem;">
✏️ 수정
수정
</button>
<button type="button" class="btn btn-sm btn-danger" onclick="deleteWorkReport(${report.id})" style="flex: 1; padding: 0.4rem 0.6rem; font-size: 0.75rem; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca;">
🗑️ 삭제
삭제
</button>
</div>
</div>
@@ -1997,7 +1990,7 @@ function getCurrentUser() {
}
try {
const token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
const token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token'));
if (token) {
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
@@ -2009,7 +2002,7 @@ function getCurrentUser() {
}
try {
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('user') || localStorage.getItem('userInfo');
const userInfo = localStorage.getItem('sso_user');
if (userInfo) return JSON.parse(userInfo);
} catch (error) {
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
@@ -2044,16 +2037,16 @@ function showSaveResultModal(type, title, message, details = null) {
let icon = '';
switch (type) {
case 'success':
icon = '';
icon = '';
break;
case 'error':
icon = '';
icon = '';
break;
case 'warning':
icon = '⚠️';
icon = '';
break;
default:
icon = '';
icon = '';
}
// 모달 내용 구성
@@ -2157,7 +2150,6 @@ async function loadData() {
try {
showMessage('데이터를 불러오는 중...', 'loading');
console.log('🔗 통합 API 설정을 사용한 기본 데이터 로딩 시작...');
await loadWorkers();
await loadProjects();
await loadWorkTypes();
@@ -2190,8 +2182,6 @@ async function loadWorkers() {
return notResigned;
});
console.log(`✅ Workers 로드 성공: ${workers.length}명 (전체: ${allWorkers.length}명)`);
console.log(`📊 필터링 조건: employment_status≠resigned (퇴사자만 제외)`);
} catch (error) {
console.error('작업자 로딩 오류:', error);
throw error;
@@ -2203,7 +2193,6 @@ async function loadProjects() {
console.log('Projects API 호출 중... (활성 프로젝트만)');
const data = await window.apiCall(`/projects/active/list`);
projects = Array.isArray(data) ? data : (data.data || data.projects || []);
console.log('✅ 활성 프로젝트 로드 성공:', projects.length);
} catch (error) {
console.error('프로젝트 로딩 오류:', error);
throw error;
@@ -2216,12 +2205,10 @@ async function loadWorkTypes() {
const data = response.data || response;
if (Array.isArray(data) && data.length > 0) {
workTypes = data;
console.log('✅ 작업 유형 API 사용 (통합 설정):', workTypes.length + '개');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 작업 유형 API 사용 불가, 기본값 사용:', error.message);
workTypes = [
{ id: 1, name: 'Base' },
{ id: 2, name: 'Vessel' },
@@ -2236,12 +2223,10 @@ async function loadWorkStatusTypes() {
const data = response.data || response;
if (Array.isArray(data) && data.length > 0) {
workStatusTypes = data;
console.log('✅ 업무 상태 유형 API 사용 (통합 설정):', workStatusTypes.length + '개');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 업무 상태 유형 API 사용 불가, 기본값 사용');
workStatusTypes = [
{ id: 1, name: '정규' },
{ id: 2, name: '에러' }
@@ -2266,7 +2251,6 @@ async function loadErrorTypes() {
const catResponse = await window.apiCall('/work-issues/categories/type/nonconformity');
if (catResponse && catResponse.success && Array.isArray(catResponse.data)) {
issueCategories = catResponse.data;
console.log(`✅ 부적합 카테고리 ${issueCategories.length}개 로드`);
// 모든 아이템 로드
const itemResponse = await window.apiCall('/work-issues/items');
@@ -2274,11 +2258,9 @@ async function loadErrorTypes() {
// 부적합 카테고리의 아이템만 필터링
const categoryIds = issueCategories.map(c => c.category_id);
issueItems = itemResponse.data.filter(item => categoryIds.includes(item.category_id));
console.log(`✅ 부적합 아이템 ${issueItems.length}개 로드`);
}
}
} catch (error) {
console.log('⚠️ 신고 카테고리 로드 실패:', error);
issueCategories = [];
issueItems = [];
}
@@ -2287,7 +2269,6 @@ async function loadErrorTypes() {
// TBM 팀 구성 자동 불러오기
async function loadTbmTeamForDate(date) {
try {
console.log('🛠️ TBM 팀 구성 조회 중:', date);
const response = await window.apiCall(`/tbm/sessions/date/${date}`);
if (response && response.success && response.data && response.data.length > 0) {
@@ -2300,16 +2281,14 @@ async function loadTbmTeamForDate(date) {
const teamRes = await window.apiCall(`/tbm/sessions/${targetSession.session_id}/team`);
if (teamRes && teamRes.success && teamRes.data) {
const teamWorkerIds = teamRes.data.map(m => m.user_id);
console.log(`✅ TBM 팀 구성 로드 성공: ${teamWorkerIds.length}`);
return teamWorkerIds;
}
}
}
console.log(' 해당 날짜의 TBM 팀 구성이 없습니다.');
return [];
} catch (error) {
console.error(' TBM 팀 구성 조회 오류:', error);
console.error(' TBM 팀 구성 조회 오류:', error);
return [];
}
}
@@ -2340,7 +2319,7 @@ async function populateWorkerGrid() {
font-size: 0.875rem;
`;
infoDiv.innerHTML = `
<strong>🛠️ TBM 팀 구성 자동 적용</strong><br>
<strong> TBM 팀 구성 자동 적용</strong><br>
오늘 TBM에서 구성된 팀원 ${tbmWorkerIds.length}명이 자동으로 선택되었습니다.
`;
grid.appendChild(infoDiv);
@@ -2389,29 +2368,25 @@ function toggleWorkerSelection(workerId, btnElement) {
// 작업 항목 추가
function addWorkEntry() {
console.log('🔧 addWorkEntry 함수 호출됨');
const container = document.getElementById('workEntriesList');
console.log('🔧 컨테이너:', container);
workEntryCounter++;
console.log('🔧 작업 항목 카운터:', workEntryCounter);
const entryDiv = document.createElement('div');
entryDiv.className = 'work-entry';
entryDiv.dataset.id = workEntryCounter;
console.log('🔧 생성된 작업 항목 div:', entryDiv);
entryDiv.innerHTML = `
<div class="work-entry-header">
<div class="work-entry-title">작업 항목 #${workEntryCounter}</div>
<button type="button" class="remove-work-btn" onclick="event.stopPropagation(); removeWorkEntry(${workEntryCounter})" title="이 작업 삭제">
🗑️ 삭제
삭제
</button>
</div>
<div class="work-entry-grid">
<div class="form-field-group">
<div class="form-field-label">
<span class="form-field-icon">🏗️</span>
<span class="form-field-icon"></span>
프로젝트
</div>
<select class="form-select project-select" required>
@@ -2422,7 +2397,7 @@ function addWorkEntry() {
<div class="form-field-group">
<div class="form-field-label">
<span class="form-field-icon">⚙️</span>
<span class="form-field-icon"></span>
작업 유형
</div>
<select class="form-select work-type-select" required>
@@ -2435,7 +2410,7 @@ function addWorkEntry() {
<div class="work-entry-full">
<div class="form-field-group">
<div class="form-field-label">
<span class="form-field-icon">📊</span>
<span class="form-field-icon"></span>
업무 상태
</div>
<select class="form-select work-status-select" required>
@@ -2447,7 +2422,7 @@ function addWorkEntry() {
<div class="error-type-section work-entry-full">
<div class="form-field-label">
<span class="form-field-icon">⚠️</span>
<span class="form-field-icon"></span>
에러 유형
</div>
<select class="form-select error-type-select">
@@ -2458,7 +2433,7 @@ function addWorkEntry() {
<div class="time-input-section work-entry-full">
<div class="form-field-label">
<span class="form-field-icon"></span>
<span class="form-field-icon"></span>
작업 시간 (시간)
</div>
<input type="number" class="form-select time-input"
@@ -2479,12 +2454,8 @@ function addWorkEntry() {
`;
container.appendChild(entryDiv);
console.log('🔧 작업 항목이 컨테이너에 추가됨');
console.log('🔧 현재 컨테이너 내용:', container.innerHTML.length, '문자');
console.log('🔧 현재 .work-entry 개수:', container.querySelectorAll('.work-entry').length);
setupWorkEntryEvents(entryDiv);
console.log('🔧 이벤트 설정 완료');
}
// 작업 항목 이벤트 설정
@@ -2546,15 +2517,11 @@ function setupWorkEntryEvents(entryDiv) {
// 작업 항목 제거
function removeWorkEntry(id) {
console.log('🗑️ removeWorkEntry 호출됨, id:', id);
const entry = document.querySelector(`.work-entry[data-id="${id}"]`);
console.log('🗑️ 찾은 entry:', entry);
if (entry) {
entry.remove();
updateTotalHours();
console.log('✅ 작업 항목 삭제 완료');
} else {
console.log('❌ 작업 항목을 찾을 수 없음');
}
}
@@ -2573,7 +2540,7 @@ function updateTotalHours() {
if (total > 24) {
display.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)';
display.textContent += ' ⚠️ 24시간 초과';
display.textContent += '24시간 초과';
} else {
display.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
}
@@ -2593,8 +2560,6 @@ async function saveWorkReport() {
}
const entries = document.querySelectorAll('.work-entry');
console.log('🔍 찾은 작업 항목들:', entries);
console.log('🔍 작업 항목 개수:', entries.length);
if (entries.length === 0) {
showSaveResultModal(
@@ -2606,10 +2571,8 @@ async function saveWorkReport() {
}
const newWorkEntries = [];
console.log('🔍 작업 항목 수집 시작...');
for (const entry of entries) {
console.log('🔍 작업 항목 처리 중:', entry);
const projectSelect = entry.querySelector('.project-select');
const workTypeSelect = entry.querySelector('.work-type-select');
@@ -2617,7 +2580,7 @@ async function saveWorkReport() {
const errorTypeSelect = entry.querySelector('.error-type-select');
const timeInput = entry.querySelector('.time-input');
console.log('🔍 선택된 요소들:', {
console.log(' 선택된 요소들:', {
projectSelect,
workTypeSelect,
workStatusSelect,
@@ -2631,7 +2594,7 @@ async function saveWorkReport() {
const errorTypeId = errorTypeSelect?.value;
const workHours = timeInput?.value;
console.log('🔍 수집된 값들:', {
console.log(' 수집된 값들:', {
projectId,
workTypeId,
workStatusId,
@@ -2665,8 +2628,7 @@ async function saveWorkReport() {
work_hours: parseFloat(workHours)
};
console.log('🔍 생성된 작업 항목:', workEntry);
console.log('🔍 작업 항목 상세:', {
console.log(' 작업 항목 상세:', {
project_id: workEntry.project_id,
work_type_id: workEntry.work_type_id,
work_status_id: workEntry.work_status_id,
@@ -2676,13 +2638,11 @@ async function saveWorkReport() {
newWorkEntries.push(workEntry);
}
console.log('🔍 최종 수집된 작업 항목들:', newWorkEntries);
console.log('🔍 총 작업 항목 개수:', newWorkEntries.length);
try {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '💾 저장 중...';
submitBtn.textContent = ' 저장 중...';
const currentUser = getCurrentUser();
let totalSaved = 0;
@@ -2706,18 +2666,13 @@ async function saveWorkReport() {
created_by: currentUser?.user_id || currentUser?.id
};
console.log('🔄 배열 형태로 전송:', requestData);
console.log('🔄 work_entries:', requestData.work_entries);
console.log('🔄 work_entries[0] 상세:', requestData.work_entries[0]);
console.log('🔄 전송 데이터 JSON:', JSON.stringify(requestData, null, 2));
try {
const result = await window.apiCall(`/daily-work-reports`, 'POST', requestData);
console.log('✅ 저장 성공:', result);
totalSaved++;
} catch (error) {
console.error(' 저장 실패:', error);
console.error(' 저장 실패:', error);
totalFailed++;
failureDetails.push(`${workerName}: ${error.message}`);
@@ -2765,7 +2720,7 @@ async function saveWorkReport() {
} finally {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = false;
submitBtn.textContent = '💾 작업보고서 저장';
submitBtn.textContent = ' 작업보고서 저장';
}
}
@@ -2801,7 +2756,7 @@ async function loadTodayWorkers() {
const today = getKoreaToday();
const currentUser = getCurrentUser();
content.innerHTML = '<div class="loading-spinner">📊 내가 입력한 오늘의 작업 현황을 불러오는 중... (통합 API)</div>';
content.innerHTML = '<div class="loading-spinner"> 내가 입력한 오늘의 작업 현황을 불러오는 중... (통합 API)</div>';
section.style.display = 'block';
// 본인이 입력한 데이터만 조회 (통합 API 사용)
@@ -2812,10 +2767,8 @@ async function loadTodayWorkers() {
queryParams += `&created_by=${currentUser.id}`;
}
console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`);
const rawData = await window.apiCall(`/daily-work-reports?${queryParams}`);
console.log('📊 당일 작업 데이터 (통합 API):', rawData);
let data = [];
if (Array.isArray(rawData)) {
@@ -2830,7 +2783,7 @@ async function loadTodayWorkers() {
console.error('당일 작업자 로드 오류:', error);
content.innerHTML = `
<div class="no-data-message">
오늘의 작업 현황을 불러올 수 없습니다.<br>
오늘의 작업 현황을 불러올 수 없습니다.<br>
<small>${error.message}</small>
</div>
`;
@@ -2844,7 +2797,7 @@ function displayMyDailyWorkers(data, date) {
if (!Array.isArray(data) || data.length === 0) {
content.innerHTML = `
<div class="no-data-message">
📝 내가 오늘(${date}) 입력한 작업이 없습니다.<br>
내가 오늘(${date}) 입력한 작업이 없습니다.<br>
<small>새로운 작업을 추가해보세요!</small>
</div>
`;
@@ -2866,9 +2819,9 @@ function displayMyDailyWorkers(data, date) {
const headerHtml = `
<div class="daily-workers-header">
<h4>📊 내가 입력한 오늘(${escapeHtml(date)}) 작업 현황 - 총 ${parseInt(totalWorkers) || 0}명, ${parseInt(totalWorks) || 0}개 작업</h4>
<h4> 내가 입력한 오늘(${escapeHtml(date)}) 작업 현황 - 총 ${parseInt(totalWorkers) || 0}명, ${parseInt(totalWorks) || 0}개 작업</h4>
<button class="refresh-btn" onclick="refreshTodayWorkers()">
🔄 새로고침
새로고침
</button>
</div>
`;
@@ -2891,34 +2844,34 @@ function displayMyDailyWorkers(data, date) {
<div class="individual-work-item">
<div class="work-details-grid">
<div class="detail-item">
<div class="detail-label">🏗️ 프로젝트</div>
<div class="detail-label"> 프로젝트</div>
<div class="detail-value">${projectName}</div>
</div>
<div class="detail-item">
<div class="detail-label">⚙️ 작업종류</div>
<div class="detail-label"> 작업종류</div>
<div class="detail-value">${workTypeName}</div>
</div>
<div class="detail-item">
<div class="detail-label">📊 작업상태</div>
<div class="detail-label"> 작업상태</div>
<div class="detail-value">${workStatusName}</div>
</div>
<div class="detail-item">
<div class="detail-label"> 작업시간</div>
<div class="detail-label"> 작업시간</div>
<div class="detail-value">${workHours}시간</div>
</div>
${errorTypeName ? `
<div class="detail-item">
<div class="detail-label"> 에러유형</div>
<div class="detail-label"> 에러유형</div>
<div class="detail-value">${errorTypeName}</div>
</div>
` : ''}
</div>
<div class="action-buttons">
<button class="edit-btn" onclick="editWorkItem('${workId}')">
✏️ 수정
수정
</button>
<button class="delete-btn" onclick="deleteWorkItem('${workId}')">
🗑️ 삭제
삭제
</button>
</div>
</div>
@@ -2928,7 +2881,7 @@ function displayMyDailyWorkers(data, date) {
return `
<div class="worker-status-item">
<div class="worker-header">
<div class="worker-name">👤 ${escapeHtml(workerName)}</div>
<div class="worker-name"> ${escapeHtml(workerName)}</div>
<div class="worker-total-hours">총 ${parseFloat(totalHours)}시간</div>
</div>
<div class="individual-works-container">
@@ -2970,12 +2923,12 @@ function showEditModal(workData) {
<div class="edit-modal" id="editModal">
<div class="edit-modal-content">
<div class="edit-modal-header">
<h3>✏️ 작업 수정</h3>
<h3> 작업 수정</h3>
<button class="close-modal-btn" onclick="closeEditModal()">×</button>
</div>
<div class="edit-modal-body">
<div class="edit-form-group">
<label>🏗️ 프로젝트</label>
<label> 프로젝트</label>
<select class="edit-select" id="editProject">
<option value="">프로젝트 선택</option>
${projects.map(p => `
@@ -2987,7 +2940,7 @@ function showEditModal(workData) {
</div>
<div class="edit-form-group">
<label>⚙️ 작업 유형</label>
<label> 작업 유형</label>
<select class="edit-select" id="editWorkType">
<option value="">작업 유형 선택</option>
${workTypes.map(wt => `
@@ -2999,7 +2952,7 @@ function showEditModal(workData) {
</div>
<div class="edit-form-group">
<label>📊 업무 상태</label>
<label> 업무 상태</label>
<select class="edit-select" id="editWorkStatus">
<option value="">업무 상태 선택</option>
${workStatusTypes.map(ws => `
@@ -3011,7 +2964,7 @@ function showEditModal(workData) {
</div>
<div class="edit-form-group" id="editErrorTypeGroup" style="${workData.work_status_id == 2 ? '' : 'display: none;'}">
<label> 에러 유형</label>
<label> 에러 유형</label>
<select class="edit-select" id="editErrorType">
<option value="">에러 유형 선택</option>
${errorTypes.map(et => `
@@ -3023,7 +2976,7 @@ function showEditModal(workData) {
</div>
<div class="edit-form-group">
<label> 작업 시간</label>
<label> 작업 시간</label>
<input type="number" class="edit-input" id="editWorkHours"
value="${workData.work_hours}"
min="0" max="24" step="0.5">
@@ -3031,7 +2984,7 @@ function showEditModal(workData) {
</div>
<div class="edit-modal-footer">
<button class="btn btn-secondary" onclick="closeEditModal()">취소</button>
<button class="btn btn-success" onclick="saveEditedWork()">💾 저장</button>
<button class="btn btn-success" onclick="saveEditedWork()"> 저장</button>
</div>
</div>
</div>
@@ -3093,14 +3046,13 @@ async function saveEditedWork() {
body: JSON.stringify(updateData)
});
console.log('✅ 수정 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success');
showMessage(' 작업이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
refreshTodayWorkers();
} catch (error) {
console.error(' 수정 실패:', error);
console.error(' 수정 실패:', error);
showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
@@ -3121,14 +3073,13 @@ async function deleteWorkItem(workId) {
method: 'DELETE'
});
console.log('✅ 삭제 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success');
showMessage(' 작업이 성공적으로 삭제되었습니다!', 'success');
// 화면 새로고침
refreshTodayWorkers();
} catch (error) {
console.error(' 삭제 실패:', error);
console.error(' 삭제 실패:', error);
showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
@@ -3164,7 +3115,6 @@ async function init() {
// TBM 작업 목록 로드 (기본 탭)
await loadIncompleteTbms();
console.log('✅ 시스템 초기화 완료 (통합 API 설정 적용)');
} catch (error) {
console.error('초기화 오류:', error);
@@ -3422,7 +3372,6 @@ function renderInlineDefectList(index) {
const defects = tempDefects[index] || [];
console.log(`📝 [renderInlineDefectList] index=${index}, 부적합 수=${defects.length}`, defects);
// 이슈가 있으면 이슈 선택 UI, 없으면 레거시 UI
if (nonconformityIssues.length > 0) {
@@ -3430,7 +3379,7 @@ function renderInlineDefectList(index) {
let html = `
<div class="defect-issue-section">
<div class="defect-issue-header">
<span class="defect-issue-title">📋 ${escapeHtml(workerWorkplaceName || '작업장소')} 관련 부적합</span>
<span class="defect-issue-title"> ${escapeHtml(workerWorkplaceName || '작업장소')} 관련 부적합</span>
<span class="defect-issue-count">${parseInt(nonconformityIssues.length) || 0}건</span>
</div>
<div class="defect-issue-list">

View File

@@ -68,7 +68,7 @@ class DailyWorkReportState extends BaseState {
}
try {
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
const userInfo = localStorage.getItem('sso_user');
if (userInfo) {
return JSON.parse(userInfo);
}

View File

@@ -327,16 +327,13 @@ async function openEquipmentModal(equipmentId = null) {
// 다음 관리번호 로드
async function loadNextEquipmentCode() {
try {
console.log('📋 다음 관리번호 조회 중...');
const response = await axios.get('/equipments/next-code');
console.log('📋 다음 관리번호 응답:', response.data);
if (response.data.success) {
document.getElementById('equipmentCode').value = response.data.data.next_code;
console.log('✅ 다음 관리번호 설정:', response.data.data.next_code);
}
} catch (error) {
console.error(' 다음 관리번호 조회 실패:', error);
console.error(' 에러 상세:', error.response?.data || error.message);
console.error(' 다음 관리번호 조회 실패:', error);
console.error(' 에러 상세:', error.response?.data || error.message);
// 오류 시 기본값으로 빈 값 유지 (사용자가 직접 입력)
}
}

View File

@@ -2,7 +2,6 @@
// 그룹장 전용 대시보드 - 실시간 근태 및 작업 현황 (Real Data Version)
import { apiCall } from './api-config.js';
console.log('📊 그룹장 대시보드 스크립트 로딩 (Live Data)');
// 상태별 스타일/텍스트 매핑
const STATUS_MAP = {

Some files were not shown because too many files have changed in this diff Show More