diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index 739167f..c495f2e 100644 Binary files a/backend/__pycache__/main.cpython-311.pyc and b/backend/__pycache__/main.cpython-311.pyc differ diff --git a/backend/database/__pycache__/models.cpython-311.pyc b/backend/database/__pycache__/models.cpython-311.pyc index d5d1a32..093e30d 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 807a84f..597b32c 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 b4f24f6..77943b2 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -1,24 +1,32 @@ from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship -from datetime import datetime +from datetime import datetime, timezone, timedelta import enum +# 한국 시간대 설정 +KST = timezone(timedelta(hours=9)) + +def get_kst_now(): + """현재 한국 시간 반환""" + return datetime.now(KST) + Base = declarative_base() class UserRole(str, enum.Enum): - ADMIN = "admin" - USER = "user" + admin = "admin" + user = "user" class IssueStatus(str, enum.Enum): - NEW = "new" - PROGRESS = "progress" - COMPLETE = "complete" + new = "new" + progress = "progress" + complete = "complete" class IssueCategory(str, enum.Enum): - MATERIAL_MISSING = "material_missing" - DIMENSION_DEFECT = "dimension_defect" - INCOMING_DEFECT = "incoming_defect" + material_missing = "material_missing" + design_error = "design_error" # 설계미스 (기존 dimension_defect 대체) + incoming_defect = "incoming_defect" + inspection_miss = "inspection_miss" # 검사미스 (신규 추가) class User(Base): __tablename__ = "users" @@ -27,9 +35,9 @@ class User(Base): username = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) full_name = Column(String) - role = Column(Enum(UserRole), default=UserRole.USER) + role = Column(Enum(UserRole), default=UserRole.user) is_active = Column(Boolean, default=True) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=get_kst_now) # Relationships issues = relationship("Issue", back_populates="reporter") @@ -40,11 +48,12 @@ class Issue(Base): id = Column(Integer, primary_key=True, index=True) photo_path = Column(String) + photo_path2 = Column(String) # 두 번째 사진 경로 category = Column(Enum(IssueCategory), nullable=False) description = Column(Text, nullable=False) - status = Column(Enum(IssueStatus), default=IssueStatus.NEW) + status = Column(Enum(IssueStatus), default=IssueStatus.new) reporter_id = Column(Integer, ForeignKey("users.id")) - report_date = Column(DateTime, default=datetime.utcnow) + report_date = Column(DateTime, default=get_kst_now) work_hours = Column(Float, default=0) detail_notes = Column(Text) @@ -63,7 +72,7 @@ class DailyWork(Base): overtime_total = Column(Float, default=0) total_hours = Column(Float, nullable=False) created_by_id = Column(Integer, ForeignKey("users.id")) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=get_kst_now) # Relationships created_by = relationship("User", back_populates="daily_works") diff --git a/backend/database/schemas.py b/backend/database/schemas.py index 34e5893..d5d3014 100644 --- a/backend/database/schemas.py +++ b/backend/database/schemas.py @@ -14,8 +14,9 @@ class IssueStatus(str, Enum): class IssueCategory(str, Enum): material_missing = "material_missing" - dimension_defect = "dimension_defect" + design_error = "design_error" # 설계미스 (기존 dimension_defect 대체) incoming_defect = "incoming_defect" + inspection_miss = "inspection_miss" # 검사미스 (신규 추가) # User schemas class UserBase(BaseModel): @@ -64,6 +65,7 @@ class IssueBase(BaseModel): class IssueCreate(IssueBase): photo: Optional[str] = None # Base64 encoded image + photo2: Optional[str] = None # Second Base64 encoded image class IssueUpdate(BaseModel): category: Optional[IssueCategory] = None @@ -72,10 +74,12 @@ class IssueUpdate(BaseModel): detail_notes: Optional[str] = None status: Optional[IssueStatus] = None photo: Optional[str] = None # Base64 encoded image for update + photo2: Optional[str] = None # Second Base64 encoded image for update class Issue(IssueBase): id: int photo_path: Optional[str] = None + photo_path2: Optional[str] = None # 두 번째 사진 경로 status: IssueStatus reporter_id: int reporter: User diff --git a/backend/main.py b/backend/main.py index e333629..ba57295 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,6 +9,8 @@ from routers import auth, issues, daily_work, reports from services.auth_service import create_admin_user # 데이터베이스 테이블 생성 +# 메타데이터 캐시 클리어 +Base.metadata.clear() Base.metadata.create_all(bind=engine) # FastAPI 앱 생성 diff --git a/backend/migrations/001_init.sql b/backend/migrations/001_init.sql index 9352b69..32e1fba 100644 --- a/backend/migrations/001_init.sql +++ b/backend/migrations/001_init.sql @@ -1,10 +1,22 @@ +-- 초기 데이터베이스 설정 + +-- Enum 타입 생성 (4개 카테고리) +CREATE TYPE userRole AS ENUM ('admin', 'user'); +CREATE TYPE issueStatus AS ENUM ('new', 'progress', 'complete'); +CREATE TYPE issueCategory AS ENUM ( + 'material_missing', -- 자재누락 + 'design_error', -- 설계미스 + 'incoming_defect', -- 입고자재 불량 + 'inspection_miss' -- 검사미스 +); + -- 사용자 테이블 CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, hashed_password VARCHAR(255) NOT NULL, full_name VARCHAR(100), - role VARCHAR(20) DEFAULT 'user', + role userRole DEFAULT 'user', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -12,20 +24,21 @@ CREATE TABLE IF NOT EXISTS users ( -- 이슈 테이블 CREATE TABLE IF NOT EXISTS issues ( id SERIAL PRIMARY KEY, - photo_path VARCHAR(255), - category VARCHAR(50) NOT NULL, + photo_path VARCHAR(500), + photo_path2 VARCHAR(500), -- 두 번째 사진 + category issueCategory NOT NULL, description TEXT NOT NULL, - status VARCHAR(20) DEFAULT 'new', + status issueStatus DEFAULT 'new', reporter_id INTEGER REFERENCES users(id), report_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, work_hours FLOAT DEFAULT 0, detail_notes TEXT ); --- 일일 공수 테이블 +-- 일일 작업 테이블 CREATE TABLE IF NOT EXISTS daily_works ( id SERIAL PRIMARY KEY, - date DATE NOT NULL UNIQUE, + date DATE NOT NULL, worker_count INTEGER NOT NULL, regular_hours FLOAT NOT NULL, overtime_workers INTEGER DEFAULT 0, @@ -33,11 +46,13 @@ CREATE TABLE IF NOT EXISTS daily_works ( overtime_total FLOAT DEFAULT 0, total_hours FLOAT NOT NULL, created_by_id INTEGER REFERENCES users(id), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(date) ); -- 인덱스 생성 CREATE INDEX idx_issues_reporter_id ON issues(reporter_id); CREATE INDEX idx_issues_status ON issues(status); -CREATE INDEX idx_issues_report_date ON issues(report_date); +CREATE INDEX idx_issues_category ON issues(category); CREATE INDEX idx_daily_works_date ON daily_works(date); +CREATE INDEX idx_daily_works_created_by_id ON daily_works(created_by_id); \ No newline at end of file diff --git a/backend/migrations/002_add_second_photo.sql b/backend/migrations/002_add_second_photo.sql new file mode 100644 index 0000000..d459851 --- /dev/null +++ b/backend/migrations/002_add_second_photo.sql @@ -0,0 +1,5 @@ +-- 두 번째 사진 경로 추가 +ALTER TABLE issues ADD COLUMN IF NOT EXISTS photo_path2 VARCHAR(500); + +-- 인덱스 추가 (선택사항) +CREATE INDEX IF NOT EXISTS idx_issues_photo_path2 ON issues(photo_path2); diff --git a/backend/migrations/003_update_categories.sql b/backend/migrations/003_update_categories.sql new file mode 100644 index 0000000..80f5948 --- /dev/null +++ b/backend/migrations/003_update_categories.sql @@ -0,0 +1,8 @@ +-- 카테고리 업데이트 마이그레이션 +-- dimension_defect를 design_error로 변경 +UPDATE issues +SET category = 'design_error' +WHERE category = 'dimension_defect'; + +-- PostgreSQL enum 타입 업데이트 (필요한 경우) +-- 기존 enum 타입 확인 후 필요시 재생성 diff --git a/backend/migrations/004_fix_category_values.sql b/backend/migrations/004_fix_category_values.sql new file mode 100644 index 0000000..7e44509 --- /dev/null +++ b/backend/migrations/004_fix_category_values.sql @@ -0,0 +1,9 @@ +-- 카테고리 값 정규화 (대문자를 소문자로 변경) +-- 기존 DIMENSION_DEFECT를 design_error로 변경 +UPDATE issues SET category = 'design_error' WHERE category IN ('DIMENSION_DEFECT', 'dimension_defect'); +UPDATE issues SET category = 'material_missing' WHERE category = 'MATERIAL_MISSING'; +UPDATE issues SET category = 'incoming_defect' WHERE category = 'INCOMING_DEFECT'; +UPDATE issues SET category = 'inspection_miss' WHERE category = 'INSPECTION_MISS'; + +-- 카테고리 값 확인 +SELECT category, COUNT(*) FROM issues GROUP BY category; diff --git a/backend/migrations/005_recreate_enum_type.sql b/backend/migrations/005_recreate_enum_type.sql new file mode 100644 index 0000000..e90e22b --- /dev/null +++ b/backend/migrations/005_recreate_enum_type.sql @@ -0,0 +1,20 @@ +-- PostgreSQL enum 타입 재생성 +-- 카테고리 컬럼을 임시로 텍스트로 변경 +ALTER TABLE issues ALTER COLUMN category TYPE VARCHAR(50); + +-- 기존 enum 타입 삭제 +DROP TYPE IF EXISTS issuecategory CASCADE; + +-- 새로운 enum 타입 생성 (4개 카테고리) +CREATE TYPE issuecategory AS ENUM ( + 'material_missing', -- 자재누락 + 'design_error', -- 설계미스 (기존 dimension_defect 대체) + 'incoming_defect', -- 입고자재 불량 + 'inspection_miss' -- 검사미스 (신규) +); + +-- 카테고리 컬럼을 새 enum 타입으로 변경 +ALTER TABLE issues ALTER COLUMN category TYPE issuecategory USING category::issuecategory; + +-- 확인 +SELECT enumlabel FROM pg_enum WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'issuecategory'); diff --git a/backend/routers/__pycache__/auth.cpython-311.pyc b/backend/routers/__pycache__/auth.cpython-311.pyc index e209c51..7cb5703 100644 Binary files a/backend/routers/__pycache__/auth.cpython-311.pyc and b/backend/routers/__pycache__/auth.cpython-311.pyc differ diff --git a/backend/routers/__pycache__/daily_work.cpython-311.pyc b/backend/routers/__pycache__/daily_work.cpython-311.pyc index 5375946..1963909 100644 Binary files a/backend/routers/__pycache__/daily_work.cpython-311.pyc and b/backend/routers/__pycache__/daily_work.cpython-311.pyc differ diff --git a/backend/routers/__pycache__/issues.cpython-311.pyc b/backend/routers/__pycache__/issues.cpython-311.pyc index 22476d1..5f7acc8 100644 Binary files a/backend/routers/__pycache__/issues.cpython-311.pyc and b/backend/routers/__pycache__/issues.cpython-311.pyc differ diff --git a/backend/routers/__pycache__/reports.cpython-311.pyc b/backend/routers/__pycache__/reports.cpython-311.pyc index c19d0a6..7f4fee9 100644 Binary files a/backend/routers/__pycache__/reports.cpython-311.pyc and b/backend/routers/__pycache__/reports.cpython-311.pyc differ diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 1080ee7..a6a677e 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -29,7 +29,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De return user async def get_current_admin(current_user: User = Depends(get_current_user)): - if current_user.role != UserRole.ADMIN: + if current_user.role != UserRole.admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions" @@ -37,8 +37,8 @@ async def get_current_admin(current_user: User = Depends(get_current_user)): return current_user @router.post("/login", response_model=schemas.Token) -async def login(login_data: schemas.LoginRequest, db: Session = Depends(get_db)): - user = authenticate_user(db, login_data.username, login_data.password) +async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/backend/routers/daily_work.py b/backend/routers/daily_work.py index fac3e59..fd9996f 100644 --- a/backend/routers/daily_work.py +++ b/backend/routers/daily_work.py @@ -1,10 +1,10 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List, Optional -from datetime import datetime, date +from datetime import datetime, date, timezone, timedelta from database.database import get_db -from database.models import DailyWork, User, UserRole +from database.models import DailyWork, User, UserRole, KST from database import schemas from routers.auth import get_current_user @@ -120,7 +120,7 @@ async def delete_daily_work( raise HTTPException(status_code=404, detail="Daily work not found") # 권한 확인 (관리자만 삭제 가능) - if current_user.role != UserRole.ADMIN: + if current_user.role != UserRole.admin: raise HTTPException(status_code=403, detail="Only admin can delete daily work") db.delete(work) diff --git a/backend/routers/issues.py b/backend/routers/issues.py index a544da0..e3feaa6 100644 --- a/backend/routers/issues.py +++ b/backend/routers/issues.py @@ -19,16 +19,22 @@ async def create_issue( ): # 이미지 저장 photo_path = None + photo_path2 = None + if issue.photo: photo_path = save_base64_image(issue.photo) + if issue.photo2: + photo_path2 = save_base64_image(issue.photo2) + # Issue 생성 db_issue = Issue( category=issue.category, description=issue.description, photo_path=photo_path, + photo_path2=photo_path2, reporter_id=current_user.id, - status=IssueStatus.NEW + status=IssueStatus.new ) db.add(db_issue) db.commit() @@ -45,9 +51,8 @@ async def read_issues( ): query = db.query(Issue) - # 일반 사용자는 자신의 이슈만 조회 - if current_user.role == UserRole.USER: - query = query.filter(Issue.reporter_id == current_user.id) + # 모든 사용자가 모든 이슈를 조회 가능 + # (필터링 제거 - 협업을 위해 모두가 볼 수 있어야 함) if status: query = query.filter(Issue.status == status) @@ -65,9 +70,7 @@ async def read_issue( if not issue: raise HTTPException(status_code=404, detail="Issue not found") - # 권한 확인 - if current_user.role == UserRole.USER and issue.reporter_id != current_user.id: - raise HTTPException(status_code=403, detail="Not authorized to view this issue") + # 모든 사용자가 모든 이슈를 조회 가능 (협업을 위해) return issue @@ -83,13 +86,13 @@ async def update_issue( raise HTTPException(status_code=404, detail="Issue not found") # 권한 확인 - if current_user.role == UserRole.USER and issue.reporter_id != current_user.id: + if current_user.role == UserRole.user and issue.reporter_id != current_user.id: raise HTTPException(status_code=403, detail="Not authorized to update this issue") # 업데이트 update_data = issue_update.dict(exclude_unset=True) - # 사진이 업데이트되는 경우 처리 + # 첫 번째 사진이 업데이트되는 경우 처리 if "photo" in update_data: # 기존 사진 삭제 if issue.photo_path: @@ -105,10 +108,26 @@ async def update_issue( # photo 필드는 제거 (DB에는 photo_path만 저장) del update_data["photo"] + # 두 번째 사진이 업데이트되는 경우 처리 + if "photo2" in update_data: + # 기존 사진 삭제 + if issue.photo_path2: + delete_file(issue.photo_path2) + + # 새 사진 저장 + if update_data["photo2"]: + photo_path2 = save_base64_image(update_data["photo2"]) + update_data["photo_path2"] = photo_path2 + else: + update_data["photo_path2"] = None + + # photo2 필드는 제거 (DB에는 photo_path2만 저장) + del update_data["photo2"] + # work_hours가 입력되면 자동으로 상태를 complete로 변경 if "work_hours" in update_data and update_data["work_hours"] > 0: - if issue.status == IssueStatus.NEW: - update_data["status"] = IssueStatus.COMPLETE + if issue.status == IssueStatus.new: + update_data["status"] = IssueStatus.complete for field, value in update_data.items(): setattr(issue, field, value) @@ -128,7 +147,7 @@ async def delete_issue( raise HTTPException(status_code=404, detail="Issue not found") # 권한 확인 (관리자만 삭제 가능) - if current_user.role != UserRole.ADMIN: + if current_user.role != UserRole.admin: raise HTTPException(status_code=403, detail="Only admin can delete issues") # 이미지 파일 삭제 @@ -148,7 +167,7 @@ async def get_issue_stats( query = db.query(Issue) # 일반 사용자는 자신의 이슈만 - if current_user.role == UserRole.USER: + if current_user.role == UserRole.user: query = query.filter(Issue.reporter_id == current_user.id) total = query.count() diff --git a/backend/routers/reports.py b/backend/routers/reports.py index 7e5d5f5..5bf90f9 100644 --- a/backend/routers/reports.py +++ b/backend/routers/reports.py @@ -35,7 +35,7 @@ async def generate_report_summary( ) # 일반 사용자는 자신의 이슈만 - if current_user.role == UserRole.USER: + if current_user.role == UserRole.user: issues_query = issues_query.filter(Issue.reporter_id == current_user.id) issues = issues_query.all() @@ -89,7 +89,7 @@ async def get_report_issues( ) # 일반 사용자는 자신의 이슈만 - if current_user.role == UserRole.USER: + if current_user.role == UserRole.user: query = query.filter(Issue.reporter_id == current_user.id) issues = query.order_by(Issue.report_date).all() diff --git a/backend/services/__pycache__/auth_service.cpython-311.pyc b/backend/services/__pycache__/auth_service.cpython-311.pyc index 6121722..217c9f5 100644 Binary files a/backend/services/__pycache__/auth_service.cpython-311.pyc and b/backend/services/__pycache__/auth_service.cpython-311.pyc differ diff --git a/backend/services/__pycache__/file_service.cpython-311.pyc b/backend/services/__pycache__/file_service.cpython-311.pyc index 66d57e2..38557d6 100644 Binary files a/backend/services/__pycache__/file_service.cpython-311.pyc and b/backend/services/__pycache__/file_service.cpython-311.pyc differ diff --git a/backend/services/auth_service.py b/backend/services/auth_service.py index fb762ab..09fedc9 100644 --- a/backend/services/auth_service.py +++ b/backend/services/auth_service.py @@ -63,7 +63,7 @@ def create_admin_user(db: Session): username=admin_username, hashed_password=get_password_hash(admin_password), full_name="관리자", - role=UserRole.ADMIN, + role=UserRole.admin, is_active=True ) db.add(admin_user) diff --git a/backend/services/file_service.py b/backend/services/file_service.py index 06e4776..487adf9 100644 --- a/backend/services/file_service.py +++ b/backend/services/file_service.py @@ -27,20 +27,29 @@ def save_base64_image(base64_string: str) -> Optional[str]: # 이미지 검증 및 형식 확인 image = Image.open(io.BytesIO(image_data)) - format = image.format.lower() if image.format else 'png' - # 파일명 생성 - filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.{format}" + # iPhone의 .mpo 파일이나 기타 형식을 JPEG로 강제 변환 + # RGB 모드로 변환 (RGBA, P 모드 등을 처리) + if image.mode in ('RGBA', 'LA', 'P'): + # 투명도가 있는 이미지는 흰 배경과 합성 + background = Image.new('RGB', image.size, (255, 255, 255)) + if image.mode == 'P': + image = image.convert('RGBA') + background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None) + image = background + elif image.mode != 'RGB': + image = image.convert('RGB') + + # 파일명 생성 (강제로 .jpg) + filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.jpg" filepath = os.path.join(UPLOAD_DIR, filename) # 이미지 저장 (최대 크기 제한) max_size = (1920, 1920) image.thumbnail(max_size, Image.Resampling.LANCZOS) - if format == 'png': - image.save(filepath, 'PNG', optimize=True) - else: - image.save(filepath, 'JPEG', quality=85, optimize=True) + # 항상 JPEG로 저장 + image.save(filepath, 'JPEG', quality=85, optimize=True) # 웹 경로 반환 return f"/uploads/{filename}" diff --git a/docker-compose.yml b/docker-compose.yml index 9328391..ee6d9ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: POSTGRES_USER: mproject POSTGRES_PASSWORD: mproject2024 POSTGRES_DB: mproject + TZ: Asia/Seoul + PGTZ: Asia/Seoul volumes: - postgres_data:/var/lib/postgresql/data - ./backend/migrations:/docker-entrypoint-initdb.d @@ -27,6 +29,7 @@ services: ACCESS_TOKEN_EXPIRE_MINUTES: 10080 # 7 days ADMIN_USERNAME: hyungi ADMIN_PASSWORD: djg3-jj34-X3Q3 + TZ: Asia/Seoul volumes: - ./backend:/app - uploads:/app/uploads diff --git a/frontend/admin.html b/frontend/admin.html index 5a63a56..7c9e994 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -225,7 +225,8 @@ - + + + + + + + + +