Compare commits

...

6 Commits

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

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

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

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

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

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

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

View File

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

View File

@@ -1,10 +1,28 @@
import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware 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 routers import health, embeddings, classification, daily_report, rag
from db.vector_store import vector_store from db.vector_store import vector_store
from db.metadata_store import metadata_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 @asynccontextmanager
@@ -12,6 +30,7 @@ async def lifespan(app: FastAPI):
vector_store.initialize() vector_store.initialize()
metadata_store.initialize() metadata_store.initialize()
yield yield
await ollama_client.close()
app = FastAPI( app = FastAPI(
@@ -21,14 +40,25 @@ app = FastAPI(
lifespan=lifespan, 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( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=ALLOWED_ORIGINS,
allow_credentials=False, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
app.add_middleware(AuthMiddleware)
app.include_router(health.router, prefix="/api/ai") app.include_router(health.router, prefix="/api/ai")
app.include_router(embeddings.router, prefix="/api/ai") app.include_router(embeddings.router, prefix="/api/ai")
app.include_router(classification.router, prefix="/api/ai") app.include_router(classification.router, prefix="/api/ai")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import json import json
from services.ollama_client import ollama_client from services.ollama_client import ollama_client
from services.utils import load_prompt, parse_json_response
from config import settings from config import settings
@@ -7,13 +8,8 @@ CLASSIFY_PROMPT_PATH = "prompts/classify_issue.txt"
SUMMARIZE_PROMPT_PATH = "prompts/summarize_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: 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( prompt = template.format(
description=description or "", description=description or "",
detail_notes=detail_notes or "", detail_notes=detail_notes or "",
@@ -32,7 +28,7 @@ async def classify_issue(description: str, detail_notes: str = "") -> dict:
async def summarize_issue( async def summarize_issue(
description: str, detail_notes: str = "", solution: str = "" description: str, detail_notes: str = "", solution: str = ""
) -> dict: ) -> dict:
template = _load_prompt(SUMMARIZE_PROMPT_PATH) template = load_prompt(SUMMARIZE_PROMPT_PATH)
prompt = template.format( prompt = template.format(
description=description or "", description=description or "",
detail_notes=detail_notes or "", detail_notes=detail_notes or "",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,34 +21,12 @@ services:
ports: ports:
- "30306:3306" - "30306:3306"
healthcheck: healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
timeout: 20s timeout: 20s
retries: 10 retries: 10
networks: networks:
- tk-network - 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: redis:
image: redis:6-alpine image: redis:6-alpine
container_name: tk-redis container_name: tk-redis
@@ -321,9 +299,11 @@ services:
- "30400:8000" - "30400:8000"
environment: environment:
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://100.111.160.84:11434} - 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_EMBED_MODEL=${OLLAMA_EMBED_MODEL:-bge-m3}
- OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT:-120} - 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_HOST=mariadb
- DB_PORT=3306 - DB_PORT=3306
- DB_USER=${MYSQL_USER:-hyungi_user} - DB_USER=${MYSQL_USER:-hyungi_user}
@@ -404,9 +384,6 @@ volumes:
mariadb_data: mariadb_data:
external: true external: true
name: tkfb-package_db_data name: tkfb-package_db_data
postgres_data:
external: true
name: tkqc-package_postgres_data
system1_uploads: system1_uploads:
external: true external: true
name: tkfb_api_uploads name: tkfb_api_uploads

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,12 +10,25 @@
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const authRoutes = require('./routes/authRoutes'); const authRoutes = require('./routes/authRoutes');
const { initRedis } = require('./utils/redis');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; 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({ 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 credentials: true
})); }));
app.use(express.json()); 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}`); console.log(`SSO Auth Service running on port ${PORT}`);
}); });

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,6 @@ exports.up = async function(knex) {
table.boolean('is_production').defaultTo(false).comment('생산직 부서 여부'); table.boolean('is_production').defaultTo(false).comment('생산직 부서 여부');
}); });
await knex.raw(`UPDATE departments SET is_production = TRUE WHERE department_name LIKE '%생산%'`); 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 SET s.department_id = d.department_id
WHERE s.department IS NOT NULL WHERE s.department IS NOT NULL
`); `);
console.log('✅ sso_users.department_id 추가 및 백필 완료');
} }
// ============================================================ // ============================================================
@@ -62,7 +60,6 @@ exports.up = async function(knex) {
`); `);
// user_id에 인덱스 추가 // user_id에 인덱스 추가
await knex.raw(`ALTER TABLE workers ADD INDEX idx_workers_user_id (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) { for (const tableName of tablesWithWorkerId) {
const tableExists = await knex.schema.hasTable(tableName); const tableExists = await knex.schema.hasTable(tableName);
if (!tableExists) { if (!tableExists) {
console.log(`⏭️ ${tableName} 테이블이 존재하지 않음, 건너뜀`);
continue; continue;
} }
@@ -103,7 +99,6 @@ exports.up = async function(knex) {
`); `);
// 인덱스 추가 // 인덱스 추가
await knex.raw(`ALTER TABLE ${tableName} ADD INDEX idx_${tableName}_user_id (user_id)`); 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 SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL 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 SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL 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 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)`); 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 추가 // 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 SET t.to_leader_user_id = w2.user_id
WHERE w2.user_id IS NOT NULL 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) { exports.down = async function(knex) {
@@ -211,7 +201,6 @@ exports.down = async function(knex) {
await knex.schema.table(tableName, (table) => { await knex.schema.table(tableName, (table) => {
table.dropColumn(columnName); table.dropColumn(columnName);
}); });
console.log(`↩️ ${tableName}.${columnName} 제거`);
} }
} }
@@ -224,7 +213,6 @@ exports.down = async function(knex) {
await knex.schema.table(tableName, (table) => { await knex.schema.table(tableName, (table) => {
table.dropColumn('user_id'); table.dropColumn('user_id');
}); });
console.log(`↩️ ${tableName}.user_id 제거`);
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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