""" 서버 설정표 — 켤 때 한 번 읽음. 값 우선순위: 환경변수 > 5_django_backend/.env > 여기 기본값. DB 는 DB_HOST 있으면 PostgreSQL, 없으면 data/app.db (SQLite). ABAP_OPENCODE web/BE/code/config/settings.py 에서 CodeAssist 에 필요한 것만 남김 (산출물·ITO 게이트웨이·bare 배포 관련은 뺌). """ import os import secrets from datetime import timedelta from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent def _load_dotenv(path: Path) -> None: """이름=값 줄을 환경변수로 올림. 이미 있는 환경변수가 우선.""" if not path.exists(): return for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) # 줄 끝 주석(공백 뒤 #)은 값이 아님 — 안 떼면 HTTP 헤더에 한글이 실려 httpx 가 죽음(고객사 실측) v = v.split(" #", 1)[0].split(" #", 1)[0] os.environ.setdefault(k.strip(), v.strip().strip('"\x27')) _load_dotenv(BASE_DIR / ".env") DATA_DIR = BASE_DIR / "data" DATA_DIR.mkdir(parents=True, exist_ok=True) def _env(name: str, default: str = "") -> str: return os.environ.get(name, default).strip() def _env_bool(name: str, default: bool) -> bool: v = _env(name) return default if not v else v.lower() in ("1", "true", "yes", "on") APP_NAME = _env("APP_NAME", "CodeAssist Backend") APP_VERSION = "0.1.0" def _secret_key() -> str: """토큰 서명 키. 바뀌면 전원 재로그인이라 파일에 저장해 재사용.""" env = _env("AUTH_SECRET") if env: return env p = DATA_DIR / ".auth_secret" if p.exists(): return p.read_text(encoding="utf-8").strip() or secrets.token_hex(32) generated = secrets.token_hex(32) p.write_text(generated, encoding="utf-8") return generated SECRET_KEY = _secret_key() DEBUG = _env_bool("DEBUG", True) ALLOWED_HOSTS = ["*"] # ── 바깥 것들 ───────────────────────────────────────────────────── # CodeAssist 전용 OpenCode 인스턴스 (opencode/ workspace 로 띄운 것) OPENCODE_BASE_URL = _env("OPENCODE_BASE_URL", "http://localhost:4096").rstrip("/") # OpenCode 1.18 은 서버 하나가 여러 프로젝트(directory)를 다루고 요청마다 ?directory= 로 고름. # 안 넘기면 마지막에 열린 프로젝트로 가서 세션이 엉뚱한 workspace 에 생김 → 항상 넘긴다. # 기본은 이 repo 의 opencode/ workspace. 서버를 다른 경로에서 띄우면 그 절대경로로. OPENCODE_DIRECTORY = _env("OPENCODE_DIRECTORY") or str((BASE_DIR / "opencode").resolve()) # OpenCode 에 넘길 에이전트 이름 (opencode/.opencode/agent/<이름>.md) OPENCODE_AGENT = _env("OPENCODE_AGENT", "codeassist") # 사내 LLM(FabriX) 게이트웨이 — 고객사에선 OpenCode 가 /api/ito 로 붙고 apps/gateway 가 FabriX 로 중계. # 키 이름은 ABAP_OPENCODE 와 같음(AAF_*). 전부 비어 있으면 /api/ito/chat/completions 만 503 이고 나머진 멀쩡. from apps.gateway.fabrix import ENV_KEYS as _FABRIX_KEYS # noqa: E402 FABRIX_ENV = {k: _env(k) for k in _FABRIX_KEYS} # 세션 컨텍스트 하드 한도 — usage.limit 로 프론트 게이지에 감 CONTEXT_LIMIT_TOKENS = int(_env("CONTEXT_LIMIT_TOKENS", "128000") or "128000") INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", "corsheaders", "apps.accounts", "apps.chat", "apps.gateway", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ] ROOT_URLCONF = "config.urls" ASGI_APPLICATION = "config.asgi.application" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] # ── DB ──────────────────────────────────────────────────────────── _pg_host = _env("DB_HOST") if _pg_host: _schema = _env("DB_SCHEMA", "codeassist") or "codeassist" DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "HOST": _pg_host, "PORT": _env("DB_PORT", "5432") or "5432", "USER": _env("DB_USER"), "PASSWORD": _env("DB_PASSWORD"), "NAME": _env("DB_NAME"), # ASGI 스레드마다 연결 눌러앉는 것 방지 — 요청 끝나면 바로 반납 "CONN_MAX_AGE": 0, "OPTIONS": { "sslmode": "require", "connect_timeout": 10, "options": f"-c search_path={_schema}", }, } } else: DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": DATA_DIR / "app.db", } } DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # ── 인증 ───────────────────────────────────────────────────────── AUTH_USER_MODEL = "accounts.User" AUTH_PASSWORD_VALIDATORS = [] REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "apps.accounts.authentication.BearerOrQueryJWTAuthentication", ], "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], # 모든 응답을 프론트 CommonResponse envelope 로 감쌈 "DEFAULT_RENDERER_CLASSES": ["common.envelope.EnvelopeRenderer"], "DEFAULT_PARSER_CLASSES": ["rest_framework.parsers.JSONParser"], "EXCEPTION_HANDLER": "common.envelope.envelope_exception_handler", "UNAUTHENTICATED_USER": None, } # 프론트가 401 → /auth/refresh 자동 재시도를 하므로 access 는 짧게, refresh 는 길게. SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), "REFRESH_TOKEN_LIFETIME": timedelta(days=14), "AUTH_HEADER_TYPES": ("Bearer",), "USER_ID_FIELD": "id", "USER_ID_CLAIM": "uid", "SIGNING_KEY": SECRET_KEY, } # ── CORS ───────────────────────────────────────────────────────── # dev: Vite proxy 로 same-origin 이라 안 탐. Tauri 빌드는 tauri://localhost 에서 오므로 여기 필요. CORS_ALLOWED_ORIGINS = [ o.strip() for o in _env( "CORS_ORIGINS", "http://localhost:15173,http://tauri.localhost,tauri://localhost" ).split(",") if o.strip() ] CORS_ALLOW_CREDENTIALS = True # 채팅 이미지 4장 × 5MiB + 여유 (프론트 제한과 동일 계열) DATA_UPLOAD_MAX_MEMORY_SIZE = 32 * 1024 * 1024 LANGUAGE_CODE = "ko-kr" TIME_ZONE = "Asia/Seoul" USE_I18N = True USE_TZ = True STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles" LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": {"std": {"format": "%(asctime)s %(levelname)s %(name)s: %(message)s"}}, "handlers": {"console": {"class": "logging.StreamHandler", "formatter": "std"}}, "root": {"handlers": ["console"], "level": "INFO"}, "loggers": { "django.request": {"level": "WARNING"}, "httpx": {"level": "WARNING"}, }, }