diff --git a/backend/database/__pycache__/models.cpython-311.pyc b/backend/database/__pycache__/models.cpython-311.pyc index b9bb198..02df15b 100644 Binary files a/backend/database/__pycache__/models.cpython-311.pyc and b/backend/database/__pycache__/models.cpython-311.pyc differ diff --git a/backend/database/__pycache__/schemas.cpython-311.pyc b/backend/database/__pycache__/schemas.cpython-311.pyc index 34df5b7..fa6b2cc 100644 Binary files a/backend/database/__pycache__/schemas.cpython-311.pyc and b/backend/database/__pycache__/schemas.cpython-311.pyc differ diff --git a/backend/database/models.py b/backend/database/models.py index 33578ad..dd9983a 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -43,6 +43,13 @@ class DisposalReasonType(str, enum.Enum): spam = "spam" # 스팸/오류 custom = "custom" # 직접 입력 +class DepartmentType(str, enum.Enum): + production = "production" # 생산 + quality = "quality" # 품질 + purchasing = "purchasing" # 구매 + design = "design" # 설계 + sales = "sales" # 영업 + class User(Base): __tablename__ = "users" @@ -51,6 +58,7 @@ class User(Base): hashed_password = Column(String, nullable=False) full_name = Column(String) role = Column(Enum(UserRole), default=UserRole.user) + department = Column(Enum(DepartmentType)) # 부서 정보 추가 is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=get_kst_now) diff --git a/backend/database/schemas.py b/backend/database/schemas.py index 17fd630..8e59d1d 100644 --- a/backend/database/schemas.py +++ b/backend/database/schemas.py @@ -32,11 +32,19 @@ class DisposalReasonType(str, Enum): spam = "spam" # 스팸/오류 custom = "custom" # 직접 입력 +class DepartmentType(str, Enum): + production = "production" # 생산 + quality = "quality" # 품질 + purchasing = "purchasing" # 구매 + design = "design" # 설계 + sales = "sales" # 영업 + # User schemas class UserBase(BaseModel): username: str full_name: Optional[str] = None role: UserRole = UserRole.user + department: Optional[DepartmentType] = None class UserCreate(UserBase): password: str @@ -45,6 +53,7 @@ class UserUpdate(BaseModel): full_name: Optional[str] = None password: Optional[str] = None role: Optional[UserRole] = None + department: Optional[DepartmentType] = None is_active: Optional[bool] = None class PasswordChange(BaseModel): diff --git a/backend/migrations/014_add_user_department.sql b/backend/migrations/014_add_user_department.sql new file mode 100644 index 0000000..ed4385b --- /dev/null +++ b/backend/migrations/014_add_user_department.sql @@ -0,0 +1,92 @@ +-- 014_add_user_department.sql +-- 사용자 부서 정보 추가 + +BEGIN; + +-- migration_log 테이블 생성 (멱등성) +CREATE TABLE IF NOT EXISTS migration_log ( + id SERIAL PRIMARY KEY, + migration_file VARCHAR(255) NOT NULL UNIQUE, + executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + status VARCHAR(50), + notes TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 마이그레이션 파일 이름 +DO $$ +DECLARE + migration_name VARCHAR(255) := '014_add_user_department.sql'; + migration_notes TEXT := '사용자 부서 정보 추가: department ENUM 타입 및 users 테이블에 department 컬럼 추가'; + current_status VARCHAR(50); +BEGIN + SELECT status INTO current_status FROM migration_log WHERE migration_file = migration_name; + + IF current_status IS NULL THEN + RAISE NOTICE '--- 마이그레이션 % 시작 ---', migration_name; + + -- department ENUM 타입 생성 + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'department_type') THEN + CREATE TYPE department_type AS ENUM ( + 'production', -- 생산 + 'quality', -- 품질 + 'purchasing', -- 구매 + 'design', -- 설계 + 'sales' -- 영업 + ); + RAISE NOTICE '✅ department_type ENUM 타입이 생성되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ department_type ENUM 타입이 이미 존재합니다.'; + END IF; + + -- users 테이블에 department 컬럼 추가 + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'department') THEN + ALTER TABLE users ADD COLUMN department department_type; + RAISE NOTICE '✅ users.department 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ users.department 컬럼이 이미 존재합니다.'; + END IF; + + -- 인덱스 추가 (부서별 조회 성능 향상) + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'users' AND indexname = 'idx_users_department') THEN + CREATE INDEX idx_users_department ON users (department) WHERE department IS NOT NULL; + RAISE NOTICE '✅ idx_users_department 인덱스가 생성되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ idx_users_department 인덱스가 이미 존재합니다.'; + END IF; + + -- 마이그레이션 검증 + DECLARE + col_count INTEGER; + enum_count INTEGER; + idx_count INTEGER; + BEGIN + SELECT COUNT(*) INTO col_count FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'department'; + SELECT COUNT(*) INTO enum_count FROM pg_type WHERE typname = 'department_type'; + SELECT COUNT(*) INTO idx_count FROM pg_indexes WHERE tablename = 'users' AND indexname = 'idx_users_department'; + + RAISE NOTICE '=== 마이그레이션 검증 결과 ==='; + RAISE NOTICE '추가된 컬럼: %/1개', col_count; + RAISE NOTICE '생성된 ENUM: %/1개', enum_count; + RAISE NOTICE '생성된 인덱스: %/1개', idx_count; + + IF col_count = 1 AND enum_count = 1 AND idx_count = 1 THEN + RAISE NOTICE '✅ 마이그레이션이 성공적으로 완료되었습니다!'; + INSERT INTO migration_log (migration_file, status, notes) VALUES (migration_name, 'SUCCESS', migration_notes); + ELSE + RAISE EXCEPTION '❌ 마이그레이션 검증 실패!'; + END IF; + END; + + -- 부서 ENUM 값 확인 + RAISE NOTICE '=== 부서 ENUM 값 ==='; + PERFORM dblink_exec('dbname=' || current_database(), 'SELECT enumlabel FROM pg_enum WHERE enumtypid = ''department_type''::regtype ORDER BY enumsortorder'); + + ELSIF current_status = 'SUCCESS' THEN + RAISE NOTICE 'ℹ️ 마이그레이션 %는 이미 성공적으로 실행되었습니다. 스킵합니다.', migration_name; + ELSE + RAISE NOTICE '⚠️ 마이그레이션 %는 이전에 실패했습니다. 수동 확인이 필요합니다.', migration_name; + END IF; +END $$; + +COMMIT; diff --git a/frontend/admin.html b/frontend/admin.html index 6e0c874..62d1985 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -141,6 +141,18 @@ > +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + @@ -361,6 +447,7 @@ username: document.getElementById('newUsername').value.trim(), full_name: document.getElementById('newFullName').value.trim(), password: document.getElementById('newPassword').value, + department: document.getElementById('newDepartment').value || null, role: document.getElementById('newRole').value }; @@ -430,14 +517,19 @@ container.innerHTML = users.map(user => `
-
+
${user.full_name || user.username}
-
- ID: ${user.username} - + ID: ${user.username} + ${user.department ? ` + + ${AuthAPI.getDepartmentLabel(user.department)} + + ` : ''} + +