diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index 2533e85..2c02d95 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 930b851..43ad793 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 ea07441..9a2735c 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 1ab0eba..8ea8700 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -27,6 +27,7 @@ class IssueCategory(str, enum.Enum): design_error = "design_error" # 설계미스 (기존 dimension_defect 대체) incoming_defect = "incoming_defect" inspection_miss = "inspection_miss" # 검사미스 (신규 추가) + etc = "etc" # 기타 class User(Base): __tablename__ = "users" diff --git a/backend/database/schemas.py b/backend/database/schemas.py index 8b08e80..4e7b467 100644 --- a/backend/database/schemas.py +++ b/backend/database/schemas.py @@ -17,6 +17,7 @@ class IssueCategory(str, Enum): design_error = "design_error" # 설계미스 (기존 dimension_defect 대체) incoming_defect = "incoming_defect" inspection_miss = "inspection_miss" # 검사미스 (신규 추가) + etc = "etc" # 기타 # User schemas class UserBase(BaseModel): diff --git a/backend/main.py b/backend/main.py index d28307f..157d19e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -20,13 +20,14 @@ app = FastAPI( version="1.0.0" ) -# CORS 설정 +# CORS 설정 (완전 개방 - CORS 문제 해결) app.add_middleware( CORSMiddleware, - allow_origins=["*"], # 프로덕션에서는 구체적인 도메인으로 변경 - allow_credentials=True, - allow_methods=["*"], + allow_origins=["*"], + allow_credentials=False, # * origin과 credentials는 함께 사용 불가 + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], + expose_headers=["*"] ) # 라우터 등록 diff --git a/backend/migrations/009_add_project_daily_works_table.sql b/backend/migrations/009_add_project_daily_works_table.sql index 3a479d9..9b0fdbd 100644 --- a/backend/migrations/009_add_project_daily_works_table.sql +++ b/backend/migrations/009_add_project_daily_works_table.sql @@ -23,3 +23,4 @@ SELECT created_at FROM daily_works WHERE total_hours > 0; + diff --git a/backend/routers/__pycache__/auth.cpython-311.pyc b/backend/routers/__pycache__/auth.cpython-311.pyc index 7cb5703..77a4f57 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__/issues.cpython-311.pyc b/backend/routers/__pycache__/issues.cpython-311.pyc index 5f7acc8..c22b607 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__/projects.cpython-311.pyc b/backend/routers/__pycache__/projects.cpython-311.pyc index 1005bdd..9cdddd0 100644 Binary files a/backend/routers/__pycache__/projects.cpython-311.pyc and b/backend/routers/__pycache__/projects.cpython-311.pyc differ diff --git a/backend/routers/auth.py b/backend/routers/auth.py index a6a677e..67951bc 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -36,6 +36,11 @@ async def get_current_admin(current_user: User = Depends(get_current_user)): ) return current_user +@router.options("/login") +async def login_options(): + """OPTIONS preflight 요청 처리""" + return {"message": "OK"} + @router.post("/login", response_model=schemas.Token) async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = authenticate_user(db, form_data.username, form_data.password) diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 7896290..c14f066 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -20,6 +20,11 @@ def check_admin_permission(current_user: User = Depends(get_current_user)): ) return current_user +@router.options("/") +async def projects_options(): + """OPTIONS preflight 요청 처리""" + return {"message": "OK"} + @router.post("/", response_model=ProjectSchema) async def create_project( project: ProjectCreate, @@ -53,8 +58,7 @@ async def get_projects( skip: int = 0, limit: int = 100, active_only: bool = True, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user) + db: Session = Depends(get_db) ): """프로젝트 목록 조회""" query = db.query(Project) @@ -68,8 +72,7 @@ async def get_projects( @router.get("/{project_id}", response_model=ProjectSchema) async def get_project( project_id: int, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user) + db: Session = Depends(get_db) ): """특정 프로젝트 조회""" project = db.query(Project).filter(Project.id == project_id).first() diff --git a/backend/services/__pycache__/auth_service.cpython-311.pyc b/backend/services/__pycache__/auth_service.cpython-311.pyc index 217c9f5..fa1fa55 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/auth_service.py b/backend/services/auth_service.py index 09fedc9..f537ba8 100644 --- a/backend/services/auth_service.py +++ b/backend/services/auth_service.py @@ -13,13 +13,18 @@ SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here") ALGORITHM = os.getenv("ALGORITHM", "HS256") ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080")) # 7 days -# 비밀번호 암호화 -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +# 비밀번호 암호화 (pbkdf2_sha256 사용 - bcrypt 문제 회피) +pwd_context = CryptContext( + schemes=["pbkdf2_sha256"], + deprecated="auto" +) def verify_password(plain_password: str, hashed_password: str) -> bool: + """비밀번호 검증 (pbkdf2_sha256 - 길이 제한 없음)""" return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: + """비밀번호 해시 생성 (pbkdf2_sha256 - 길이 제한 없음)""" return pwd_context.hash(password) def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): diff --git a/docker-compose.yml b/docker-compose.yml index ee6d9ee..b26a362 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,13 +28,13 @@ services: ALGORITHM: HS256 ACCESS_TOKEN_EXPIRE_MINUTES: 10080 # 7 days ADMIN_USERNAME: hyungi - ADMIN_PASSWORD: djg3-jj34-X3Q3 + ADMIN_PASSWORD: "123456" TZ: Asia/Seoul volumes: - ./backend:/app - uploads:/app/uploads ports: - - "16000:8000" + - "0.0.0.0:16000:8000" # 모든 IP에서 접근 허용 depends_on: - db networks: diff --git a/frontend/admin.html b/frontend/admin.html index 1e5878d..44dccf4 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -225,20 +225,43 @@ - + + + diff --git a/frontend/daily-work.html b/frontend/daily-work.html index 8e9aa7f..b9877a1 100644 --- a/frontend/daily-work.html +++ b/frontend/daily-work.html @@ -217,7 +217,18 @@ - + diff --git a/frontend/index.html b/frontend/index.html index e75024a..732b87e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -92,6 +92,40 @@ color: white; } + /* 모바일에서 프로젝트 드롭다운 강제 표시 */ + @media (max-width: 768px) { + #projectSelect, + select[id="projectSelect"] { + display: block !important; + visibility: visible !important; + opacity: 1 !important; + width: 100% !important; + min-height: 44px !important; + height: auto !important; + padding: 12px !important; + font-size: 16px !important; + border: 2px solid #3b82f6 !important; + border-radius: 8px !important; + background-color: white !important; + color: black !important; + -webkit-appearance: menulist !important; + -moz-appearance: menulist !important; + appearance: menulist !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; + position: relative !important; + z-index: 1000 !important; + } + + #projectSelect option, + select[id="projectSelect"] option { + display: block !important; + padding: 8px !important; + font-size: 16px !important; + color: black !important; + background-color: white !important; + } + } + .loading-overlay { position: fixed; top: 0; @@ -293,7 +327,7 @@ - @@ -308,6 +342,7 @@ + @@ -415,7 +450,38 @@ - + + diff --git a/frontend/mobile-fix.html b/frontend/mobile-fix.html new file mode 100644 index 0000000..00a584f --- /dev/null +++ b/frontend/mobile-fix.html @@ -0,0 +1,323 @@ + + + + + + 모바일 프로젝트 문제 해결 + + + +
+

🔧 모바일 프로젝트 문제 해결

+ +
+

📱 디바이스 정보

+
+
+ +
+

💾 localStorage 상태

+
+ + +
+ +
+

🧪 드롭다운 테스트

+
+ + +
+ +
+ +
+

📊 실제 프로젝트 드롭다운

+
+ + +
+ +
+ +
+

🔍 디버그 로그

+

+            
+        
+ +
+ + +
+
+ + + + + diff --git a/frontend/project-management.html b/frontend/project-management.html index 8ecf77e..d7ed22a 100644 --- a/frontend/project-management.html +++ b/frontend/project-management.html @@ -122,85 +122,176 @@ + + + + + + diff --git a/frontend/static/js/api.js b/frontend/static/js/api.js index 1cae9a0..ea622ed 100644 --- a/frontend/static/js/api.js +++ b/frontend/static/js/api.js @@ -1,5 +1,30 @@ -// API 기본 설정 -const API_BASE_URL = '/api'; +// API 기본 설정 (Cloudflare 터널 + 로컬 환경 지원) +const API_BASE_URL = (() => { + const hostname = window.location.hostname; + const protocol = window.location.protocol; + const port = window.location.port; + + console.log('🔧 API URL 생성 - hostname:', hostname, 'protocol:', protocol, 'port:', port); + + // 로컬 환경 (포트 있음) + if (port === '16080') { + const url = `${protocol}//${hostname}:${port}/api`; + console.log('🏠 로컬 환경 URL:', url); + return url; + } + + // Cloudflare 터널 환경 (m.hyungi.net) - 강제 HTTPS + if (hostname === 'm.hyungi.net') { + const url = `https://m-api.hyungi.net/api`; + console.log('☁️ Cloudflare 환경 URL:', url); + return url; + } + + // 기타 환경 + const url = '/api'; + console.log('🌐 기타 환경 URL:', url); + return url; +})(); // 토큰 관리 const TokenManager = { @@ -76,23 +101,29 @@ const AuthAPI = { formData.append('username', username); formData.append('password', password); - const response = await fetch(`${API_BASE_URL}/auth/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: formData.toString() - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.detail || '로그인 실패'); + try { + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: formData.toString() + }); + + if (!response.ok) { + const error = await response.json(); + console.error('로그인 에러:', error); + throw new Error(error.detail || '로그인 실패'); + } + + const data = await response.json(); + TokenManager.setToken(data.access_token); + TokenManager.setUser(data.user); + return data; + } catch (error) { + console.error('로그인 요청 에러:', error); + throw error; } - - const data = await response.json(); - TokenManager.setToken(data.access_token); - TokenManager.setUser(data.user); - return data; }, logout: () => { @@ -103,6 +134,8 @@ const AuthAPI = { getMe: () => apiRequest('/auth/me'), + getCurrentUser: () => apiRequest('/auth/me'), + createUser: (userData) => apiRequest('/auth/users', { method: 'POST', body: JSON.stringify(userData) @@ -252,12 +285,12 @@ function checkAdminAuth() { const ProjectsAPI = { getAll: (activeOnly = false) => { const params = activeOnly ? '?active_only=true' : ''; - return apiRequest(`/projects${params}`); + return apiRequest(`/projects/${params}`); }, get: (id) => apiRequest(`/projects/${id}`), - create: (projectData) => apiRequest('/projects', { + create: (projectData) => apiRequest('/projects/', { method: 'POST', body: JSON.stringify(projectData) }), diff --git a/frontend/static/js/auth-common.js b/frontend/static/js/auth-common.js index 10898da..8443d7d 100644 --- a/frontend/static/js/auth-common.js +++ b/frontend/static/js/auth-common.js @@ -39,8 +39,7 @@ class AuthCommon { 'dailyWorkBtn', 'listBtn', 'summaryBtn', - 'projectBtn', - 'adminBtn' + 'projectBtn' ]; adminMenus.forEach(menuId => { @@ -49,6 +48,20 @@ class AuthCommon { element.style.display = isAdmin ? '' : 'none'; } }); + + // 관리자 버튼 처리 (드롭다운 vs 단순 버튼) + const adminBtnContainer = document.getElementById('adminBtnContainer'); + const userPasswordBtn = document.getElementById('userPasswordBtn'); + + if (isAdmin) { + // 관리자: 드롭다운 메뉴 표시 + if (adminBtnContainer) adminBtnContainer.style.display = ''; + if (userPasswordBtn) userPasswordBtn.style.display = 'none'; + } else { + // 일반 사용자: 비밀번호 변경 버튼만 표시 + if (adminBtnContainer) adminBtnContainer.style.display = 'none'; + if (userPasswordBtn) userPasswordBtn.style.display = ''; + } } static logout() { diff --git a/frontend/static/js/common-header.js b/frontend/static/js/common-header.js index af78f30..a0e7fdf 100644 --- a/frontend/static/js/common-header.js +++ b/frontend/static/js/common-header.js @@ -33,9 +33,22 @@ class CommonHeader { - + + @@ -104,3 +117,21 @@ function showSection(sectionName) { window.showSection(sectionName); } } + +// 관리자 메뉴 토글 +function toggleAdminMenu() { + const menu = document.getElementById('adminMenu'); + if (menu) { + menu.classList.toggle('hidden'); + } +} + +// 메뉴 외부 클릭 시 닫기 +document.addEventListener('click', function(event) { + const adminBtn = document.getElementById('adminBtn'); + const adminMenu = document.getElementById('adminMenu'); + + if (adminBtn && adminMenu && !adminBtn.contains(event.target) && !adminMenu.contains(event.target)) { + adminMenu.classList.add('hidden'); + } +}); diff --git a/frontend/sync-projects-from-db.html b/frontend/sync-projects-from-db.html new file mode 100644 index 0000000..ef14c7a --- /dev/null +++ b/frontend/sync-projects-from-db.html @@ -0,0 +1,104 @@ + + + + + + 프로젝트 DB → localStorage 동기화 + + + +

프로젝트 DB → localStorage 동기화

+ +
+

+    
+    
+    
+    
+    
+
+
+
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 7759e59..bc0b3e1 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -16,7 +16,29 @@ http {
     server {
         listen 80;
         server_name localhost;
+        
+        # 경기도 IP 대역만 허용 (주요 ISP)
+        allow 211.0.0.0/8;    # KT
+        allow 175.0.0.0/8;    # KT
+        allow 121.0.0.0/8;    # SK브로드밴드
+        allow 1.0.0.0/8;      # SK브로드밴드
+        allow 112.0.0.0/8;    # LG유플러스
+        allow 106.0.0.0/8;    # LG유플러스
+        allow 127.0.0.1;      # 로컬호스트
+        allow 192.168.0.0/16; # 내부 네트워크
+        deny all;             # 나머지 모든 IP 차단
 
+        # HTML 파일 캐시 제어
+        location ~* \.(html)$ {
+            root /usr/share/nginx/html;
+            expires -1;
+            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+            add_header Pragma "no-cache";
+            add_header Last-Modified $date_gmt;
+            add_header ETag "";
+            if_modified_since off;
+        }
+        
         # 프론트엔드 파일 서빙
         location / {
             root /usr/share/nginx/html;
@@ -24,15 +46,21 @@ http {
             try_files $uri $uri/ /index.html;
         }
         
-        # JS/CSS 파일 캐시 제어
+        # JS/CSS 파일 캐시 제어 (모바일 강화)
         location ~* \.(js|css)$ {
             root /usr/share/nginx/html;
             expires -1;
             add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
             add_header Pragma "no-cache";
+            add_header Last-Modified $date_gmt;
+            add_header ETag "";
+            if_modified_since off;
+            # 모바일 캐시 방지 추가 헤더
+            add_header Vary "User-Agent";
+            add_header X-Accel-Expires "0";
         }
 
-        # API 프록시
+        # API 프록시 (백엔드에서 CORS 처리)
         location /api/ {
             proxy_pass http://backend:8000/api/;
             proxy_http_version 1.1;