Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6a2681d
feat: add flake.nix for NixOS development
7591yj Mar 1, 2026
bf84212
fix: template.yaml merge conflict
7591yj Mar 1, 2026
2495a89
fix: consolidate pyproject.toml; remove pytest.ini
7591yj Mar 1, 2026
053ebb5
feat: update config.py to use OIDC and S3-compliant settings
7591yj Mar 1, 2026
5b32084
feat: add oidc_auth.py; remove cognito_auth.py
7591yj Mar 1, 2026
1e4a38d
refactor: remove unnecessary comments; update to use agonistics
7591yj Mar 1, 2026
d6e73de
test: rewrite
7591yj Mar 1, 2026
c74d33b
feat: add flake.lock
7591yj Mar 1, 2026
a43f012
fix: logger import failing at module time
7591yj Mar 1, 2026
f0ea5f0
fix: query overwriting variable, loosing filter
7591yj Mar 1, 2026
5f67199
refactor: remove dead code and cognito remnants
7591yj Mar 1, 2026
9b9b87f
refactor: remove OtherSettings
7591yj Mar 1, 2026
5c4935f
refactor: migrate to pydantic v2
7591yj Mar 1, 2026
6466a4d
fix: JWKS cache TTL
7591yj Mar 1, 2026
82d725f
refactor: deduplicate s3
7591yj Mar 1, 2026
6de6be3
feat: add logging for routers
7591yj Mar 1, 2026
0f7bcdf
fix: exception chaining; use UTC
7591yj Mar 1, 2026
2952da7
refactor: deduplicate admin endpoint for files.py
7591yj Mar 1, 2026
3ede89b
refactor: add enums.py to remove magic strings
7591yj Mar 1, 2026
d09880c
feat: add logging for alembic/env.py
7591yj Mar 1, 2026
671a3c2
fix: change configs to pydantic v2 style
7591yj Mar 1, 2026
ff72023
fix: lint
7591yj Mar 1, 2026
0452c6f
chore: reformat
7591yj Mar 1, 2026
49a2401
chore: prefer English to Korean
7591yj Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 28 additions & 16 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
ENVIRONMENT=
# Copy to .env and fill in the values

# ==================
# Database Configuration
# ==================
DB_USER=
# Environment
ENVIRONMENT=dev
DEBUG=true

# Database (PostgreSQL)
DB_USER=postgres
DB_PASSWORD=
DB_HOST=
DB_PORT=
DB_NAME=
DB_HOST=localhost
DB_PORT=5432
DB_NAME=postgres

# ==================
# AWS S3 Configuration
# ==================
AWS_REGION=
S3_BUCKET_NAME=
# S3-Compatible Storage
# Leave STORAGE_ENDPOINT_URL blank to use AWS S3
# Set to http://localhost:9000 for MinIO, or your R2/B2 endpoint for Cloudflare R2 / Backblaze B2
STORAGE_ENDPOINT_URL=
STORAGE_BUCKET_NAME=bizlenz-files
STORAGE_REGION=us-east-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
COGNITO_USER_POOL_ID=
COGNITO_CLIENT_ID=
COGNITO_CLIENT_SECRET=

# Auth; Generic OIDC
# AUTH_JWKS_URL: e.g. https://your-auth-server/api/auth/jwks (better-auth default)
# AUTH_ISSUER: e.g. https://your-auth-server
# AUTH_AUDIENCE: e.g. bizlenz
AUTH_JWKS_URL=
AUTH_ISSUER=
AUTH_AUDIENCE=

# Google Gemini AI
GOOGLE_API_KEY=
GEMINI_MODEL_ANALYSIS=gemini-2.5-flash
43 changes: 13 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [ "main", "feature/**" ] # ✅ 수정됨: feature → main, feature/** 로 확장
branches: ["main", "feature/**"]
pull_request:
branches: [ "main", "feature/**" ] # ✅ 수정됨: PR 대상도 main 포함
branches: ["main", "feature/**"]
workflow_dispatch:

jobs:
Expand All @@ -25,48 +25,31 @@ jobs:
- name: Install dependencies
run: uv pip install -r requirements.txt


- name: Create .env file
run: echo "${{ secrets.ENV_FILE }}" | tr '\n' '\n' > .env

- name: Install uv audit
run: uv pip install uv-audit

- name: Create .env file
- name: Create minimal .env for CI
run: |
echo "S3_BUCKET=${{ secrets.S3_BUCKET }}" > .env
echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" >> .env
echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> .env
echo "AWS_REGION=${{ secrets.AWS_REGION }}" >> .env
echo "ENVIRONMENT=ci" > .env
echo "STORAGE_REGION=us-east-1" >> .env
echo "STORAGE_BUCKET_NAME=test-bucket" >> .env
echo "AWS_ACCESS_KEY_ID=test" >> .env
echo "AWS_SECRET_ACCESS_KEY=test" >> .env

- name: Ruff Lint
continue-on-error: true
uses: astral-sh/ruff-action@v3
with:
args: check --output-format=github .
# 5. Ruff로 코드 린트 검사

- name: Ruff Format automatically
continue-on-error: true
uses: astral-sh/ruff-action@v3
with:
args: format .

- name: Ruff Format Check
continue-on-error: true
uses: astral-sh/ruff-action@v3
with:
args: format --check . # 6. Ruff로 코드 포맷 검사


- name: Run uv audit
run: uv run uv-audit

args: format --check .

- name: Run pytest
run: uv run pytest # 8. Pytest로 자동화 테스트 실행


env:
CI: true
PYTHONPATH: src
run: uv run python -m pytest

- name: Build Docker image
run: docker build -t my-app:${{ github.sha }} .
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: format lint test clean
.PHONY: format lint test test-ci clean dev

UV_RUN := uv run

Expand All @@ -12,9 +12,16 @@ format:
lint:
$(UV_RUN) ruff check src/

# Run tests with optional test extras installed
test:
$(UV_RUN) pytest
uv sync --extra test
CI=true $(UV_RUN) pytest

# Same but explicit for CI environments
test-ci:
uv pip install -r requirements.txt
CI=true $(UV_RUN) pytest

clean:
find . -type f -name "*.pyc" -delete
find . -type d -name "__pycache__" -delete
find . -type d -name "__pycache__" -delete
84 changes: 48 additions & 36 deletions alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,87 +1,100 @@
import logging
import os
import sys
from logging.config import fileConfig
from pathlib import Path

sys.path.append(str(Path(__file__).resolve().parents[1] / "src"))

import os
from logging.config import fileConfig
from alembic import context # noqa: E402
from dotenv import load_dotenv # noqa: E402
from sqlalchemy import engine_from_config, pool # noqa: E402

from dotenv import load_dotenv
from sqlalchemy import engine_from_config, pool
from alembic import context
# Base and models must be imported after sys.path.append
from app.database import Base # noqa: E402
from app.models import models # noqa: E402, F401

# Base와 models는 sys.path.append 이후에 import
from app.database import Base
from app.models import models # noqa: F401
logger = logging.getLogger("alembic.env")

# .env 로드
# Load .env
env_path = Path(__file__).resolve().parents[1] / ".env"
load_dotenv(dotenv_path=env_path)

# Alembic 설정
# Alembic configuration
config = context.config

# 로깅 설정
# Logging configuration
if config.config_file_name is not None:
fileConfig(config.config_file_name)

target_metadata = Base.metadata


def get_database_url() -> tuple[str, dict]:
"""환경변수로부터 DATABASE_URL을 생성하고 검증합니다."""
# Docker 테스트 환경 체크
"""Build and validate DATABASE_URL from environment variables."""

# Check for Docker test environment
if os.getenv("TESTING") == "docker":
return "postgresql://test_user:test123@localhost:5433/bizlenz_test", {
"type": "postgresql", "host": "localhost", "port": "5433", "db": "bizlenz_test"
"type": "postgresql",
"host": "localhost",
"port": "5433",
"db": "bizlenz_test",
}
# SQLite 테스트 환경

# SQLite test environment
if os.getenv("TESTING") == "true":
return "sqlite:///:memory:", {"type": "sqlite", "location": "memory"}
# 기존 PostgreSQL 로직

# PostgreSQL
db_user = os.getenv("DB_USER", "postgres")
db_pass = os.getenv("DB_PASSWORD", "")
db_host = os.getenv("DB_HOST", "localhost")
db_port = os.getenv("DB_PORT", "5432")
db_name = os.getenv("DB_NAME", "postgres")

# 필수 환경변수 검증 (실제 값이 있는지 확인)
if not os.getenv("DB_HOST"): # 기본값이 아닌 실제 환경변수 확인
raise ValueError("DB_HOST 환경변수는 반드시 설정해야 합니다")
# Validate required env vars (ensure actual value is set, not just the default)
if not os.getenv("DB_HOST"):
raise ValueError("DB_HOST environment variable must be set")

# DATABASE_URL 생성
# Build DATABASE_URL
database_url = (
f"postgresql+psycopg2://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}"
)

# 연결 정보도 함께 반환
# Return connection info alongside the URL
db_info = {"user": db_user, "host": db_host, "port": db_port, "name": db_name}

return database_url, db_info


# DATABASE_URL 설정
# Configure DATABASE_URL
try:
DATABASE_URL, db_info = get_database_url()
config.set_main_option("sqlalchemy.url", DATABASE_URL)

if os.getenv("TESTING") == "docker":
print(f"Docker 테스트 환경: PostgreSQL {db_info['host']}:{db_info['port']}/{db_info['db']}")
logger.info(
"Docker test env: PostgreSQL %s:%s/%s",
db_info["host"],
db_info["port"],
db_info["db"],
)
elif os.getenv("TESTING") == "true":
print("테스트 환경: SQLite 메모리 DB 사용")
logger.info("Test env: SQLite in-memory DB")
else:
print(
f"데이터베이스 연결 설정 완료: {db_info['user']}@{db_info['host']}:{db_info['port']}/{db_info['name']}"
logger.info(
"DB connection configured: %s@%s:%s/%s",
db_info["user"],
db_info["host"],
db_info["port"],
db_info["name"],
)
except ValueError as e:
print(f"환경변수 설정 오류: {e}")
print(".env 파일을 확인하고 필수 환경변수를 설정해주세요.")
logger.error("Environment variable error: %s", e)
sys.exit(1)
except Exception as e:
print(f"예상치 못한 오류: {e}")
logger.error("Unexpected error: %s", e)
sys.exit(1)


Expand Down Expand Up @@ -114,12 +127,11 @@ def run_migrations_online() -> None:
context.run_migrations()

except Exception as e:
print(f"데이터베이스 연결 실패: {e}")
print("데이터베이스 서버 상태와 연결 정보를 확인해주세요.")
logger.error("Database connection failed: %s", e)
raise


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
run_migrations_online()
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,55 @@
Create Date: 2025-08-29 13:37:21.342226

"""

from collections.abc import Sequence

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '3da9a3e1c145'
down_revision: str | Sequence[str] | None = '1288367832cd'
revision: str = "3da9a3e1c145"
down_revision: str | Sequence[str] | None = "1288367832cd"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('business_plans', sa.Column('latest_job_id', sa.Integer(), nullable=True, comment='가장 최근 분석 작업 ID (상태 조회용)'))
op.create_index('idx_business_plans_latest_job', 'business_plans', ['latest_job_id'], unique=False)
op.create_foreign_key('fk_business_plans_latest_job_id', 'business_plans', 'analysis_jobs', ['latest_job_id'], ['id'], ondelete='SET NULL')
op.add_column(
"business_plans",
sa.Column(
"latest_job_id",
sa.Integer(),
nullable=True,
comment="Most recent analysis job ID (for status lookup)",
),
)
op.create_index(
"idx_business_plans_latest_job",
"business_plans",
["latest_job_id"],
unique=False,
)
op.create_foreign_key(
"fk_business_plans_latest_job_id",
"business_plans",
"analysis_jobs",
["latest_job_id"],
["id"],
ondelete="SET NULL",
)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('fk_business_plans_latest_job_id', 'business_plans', type_='foreignkey')
op.drop_index('idx_business_plans_latest_job', table_name='business_plans')
op.drop_column('business_plans', 'latest_job_id')
# ### end Alembic commands ###
op.drop_constraint(
"fk_business_plans_latest_job_id", "business_plans", type_="foreignkey"
)
op.drop_index("idx_business_plans_latest_job", table_name="business_plans")
op.drop_column("business_plans", "latest_job_id")
# ### end Alembic commands ###
Loading
Loading