Compare commits
6 Commits
b3012b8320
...
59cbcebb94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59cbcebb94 | ||
|
|
11cffbd920 | ||
|
|
61c810bd47 | ||
|
|
ec755ed52f | ||
|
|
2f7e083db0 | ||
|
|
cad662473b |
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
24
ai-service/middlewares/auth.py
Normal file
24
ai-service/middlewares/auth.py
Normal 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="유효하지 않은 토큰입니다")
|
||||
@@ -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 서비스 처리 중 오류가 발생했습니다")
|
||||
|
||||
@@ -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 서비스 처리 중 오류가 발생했습니다")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 서비스 처리 중 오류가 발생했습니다")
|
||||
|
||||
@@ -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 "",
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
22
ai-service/services/utils.py
Normal file
22
ai-service/services/utils.py
Normal 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 {}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = '/';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
1246
sso-auth-service/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
85
sso-auth-service/utils/redis.js
Normal file
85
sso-auth-service/utils/redis.js
Normal 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 };
|
||||
@@ -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 . .
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
// 로그인 시도 제한 (브루트포스 방지)
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 컬럼 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -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 테이블 컬럼 제거 완료');
|
||||
};
|
||||
|
||||
@@ -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('✅ 모든 출근/근태 관련 테이블 제거 완료');
|
||||
};
|
||||
|
||||
@@ -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 테이블을 관리하세요.');
|
||||
};
|
||||
|
||||
@@ -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('✅ 게스트 역할 제거 완료');
|
||||
};
|
||||
|
||||
@@ -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 시스템 테이블 제거 완료');
|
||||
};
|
||||
|
||||
@@ -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 페이지 제거 완료');
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ exports.up = function(knex) {
|
||||
table.index('work_type_id');
|
||||
table.index('is_active');
|
||||
}).then(() => {
|
||||
console.log('✅ tasks 테이블 생성 완료');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 컬럼 추가 완료');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -143,10 +143,8 @@ exports.up = async function(knex) {
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 현재 사용 중인 페이지 목록 업데이트 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages').del();
|
||||
console.log('✅ 페이지 목록 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -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 컬럼 제거 완료');
|
||||
};
|
||||
|
||||
@@ -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 테이블 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -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 테이블 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -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('✅ 출퇴근 관리 페이지 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -18,9 +18,7 @@ exports.up = async function(knex) {
|
||||
.whereNotNull('id')
|
||||
.update({ is_present: true });
|
||||
|
||||
console.log('✅ is_present 컬럼 추가 완료');
|
||||
} else {
|
||||
console.log('⏭️ is_present 컬럼이 이미 존재합니다');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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('✅ 휴가 관리 페이지 롤백 완료');
|
||||
};
|
||||
|
||||
@@ -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 테이블 롤백 완료');
|
||||
};
|
||||
|
||||
@@ -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 테이블 롤백 완료');
|
||||
};
|
||||
|
||||
@@ -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('✅ 휴가 관리 페이지 롤백 완료');
|
||||
};
|
||||
|
||||
@@ -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('✅ 출입 신청 및 안전교육 시스템 테이블 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -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('✅ 출입 신청 및 안전관리 페이지 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -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('✅ 문제 신고 페이지 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -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 컬럼 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -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('✅ 모든 일일순회점검 시스템 테이블 제거 완료');
|
||||
};
|
||||
|
||||
@@ -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(' 데이터 복구가 필요한 경우 백업에서 복원해주세요.');
|
||||
};
|
||||
|
||||
@@ -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 컬럼이 이미 존재합니다');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 제거`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@ class MonthlyStatusModel {
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
console.log(`✅ ${year}년 ${month}월 집계 재계산 완료: ${updatedCount}건`);
|
||||
return { success: true, updatedCount };
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -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: '안전교육 기록 목록 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ router.get('/check-overwrite', (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${user_id} (누적입력모드)`);
|
||||
|
||||
// 누적입력 시스템에서는 항상 덮어쓰기 가능 (실제로는 누적만 함)
|
||||
res.json({
|
||||
|
||||
@@ -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 상태 확인 실패',
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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' }
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) 제거.
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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에서 사용)
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
});
|
||||
@@ -92,7 +92,6 @@ async function initializeSections() {
|
||||
// 5. 모든 수정이 완료된 HTML을 실제 DOM에 한 번에 삽입
|
||||
mainContainer.innerHTML = doc.body.innerHTML;
|
||||
|
||||
console.log(`✅ ${currentUser.role} 역할의 섹션 로딩 완료.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('섹션 로딩 중 오류 발생:', error);
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -117,6 +117,5 @@ function showError(message) {
|
||||
|
||||
// 페이지 로드 시 실행
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('👤 프로필 페이지 로드됨');
|
||||
loadProfile();
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 로드 완료');
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 방지) ====================
|
||||
|
||||
@@ -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 };
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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} 로딩에 실패했습니다. 관리자에게 문의하세요.`;
|
||||
}
|
||||
}
|
||||
@@ -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, '"')}", "${safeImage.replace(/"/g, '"')}")'>
|
||||
<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, '"')}")'>
|
||||
<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, "'")})' 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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
// 오류 시 기본값으로 빈 값 유지 (사용자가 직접 입력)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user