From 6d8bb468c379669329b6365dd6af360a831e3266 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 14 Oct 2025 07:14:55 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드: - POST /dashboard/projects 엔드포인트 추가 - 프로젝트 생성 기능 구현 - 중복 코드 검사 - GET /dashboard/projects 컬럼명 수정 (실제 DB 스키마에 맞춤) - PATCH /dashboard/projects/{id} 컬럼명 수정 프론트엔드: - 메인 대시보드에 프로젝트 관리 섹션 추가 - '➕ 새 프로젝트' 버튼으로 생성 폼 표시/숨김 - '✏️ 이름 수정' 버튼으로 프로젝트 이름 수정 - 프로젝트 생성 폼: - 프로젝트 코드 (필수) - 프로젝트 이름 (필수) - 고객사명 (선택) - 실시간 프로젝트 목록 갱신 - API 연동 완료 --- backend/app/routers/dashboard.py | 100 ++++++++++++++-- frontend/src/App.jsx | 191 +++++++++++++++++++++++++++++-- 2 files changed, 267 insertions(+), 24 deletions(-) diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index 8eafd86..d44ebb1 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -427,6 +427,78 @@ async def get_quick_actions( raise HTTPException(status_code=500, detail=f"빠른 작업 조회 실패: {str(e)}") +@router.post("/projects") +async def create_project( + official_project_code: str = Query(..., description="프로젝트 코드"), + project_name: str = Query(..., description="프로젝트 이름"), + client_name: str = Query(None, description="고객사명"), + current_user: dict = Depends(get_current_user), + db: Session = Depends(get_db) +): + """ + 새 프로젝트 생성 + + Args: + official_project_code: 프로젝트 코드 (예: J24-001) + project_name: 프로젝트 이름 + client_name: 고객사명 (선택) + + Returns: + dict: 생성된 프로젝트 정보 + """ + try: + # 중복 확인 + check_query = text("SELECT id FROM projects WHERE official_project_code = :code") + existing = db.execute(check_query, {"code": official_project_code}).fetchone() + + if existing: + raise HTTPException(status_code=400, detail="이미 존재하는 프로젝트 코드입니다") + + # 프로젝트 생성 + insert_query = text(""" + INSERT INTO projects (official_project_code, project_name, client_name, status) + VALUES (:code, :name, :client, 'active') + RETURNING * + """) + + new_project = db.execute(insert_query, { + "code": official_project_code, + "name": project_name, + "client": client_name + }).fetchone() + + db.commit() + + # 활동 로그 기록 + ActivityLogger.log_activity( + db=db, + user_id=current_user.get('user_id'), + action="CREATE_PROJECT", + target_type="PROJECT", + target_id=new_project.id, + details=f"프로젝트 생성: {official_project_code} - {project_name}" + ) + + return { + "success": True, + "message": "프로젝트가 생성되었습니다", + "project": { + "id": new_project.id, + "official_project_code": new_project.official_project_code, + "project_name": new_project.project_name, + "client_name": new_project.client_name, + "status": new_project.status + } + } + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"프로젝트 생성 실패: {str(e)}") + raise HTTPException(status_code=500, detail=f"프로젝트 생성 실패: {str(e)}") + + @router.get("/projects") async def get_projects( current_user: dict = Depends(get_current_user), @@ -442,10 +514,12 @@ async def get_projects( query = text(""" SELECT id, - job_no, official_project_code, - job_name, - project_type, + project_name, + client_name, + design_project_code, + design_project_name, + status, created_at, updated_at FROM projects @@ -458,11 +532,13 @@ async def get_projects( for row in results: projects.append({ "id": row.id, - "job_no": row.job_no, "official_project_code": row.official_project_code, - "job_name": row.job_name, - "project_name": row.job_name, # 호환성을 위해 추가 - "project_type": row.project_type, + "project_name": row.project_name, + "job_name": row.project_name, # 호환성을 위해 추가 + "client_name": row.client_name, + "design_project_code": row.design_project_code, + "design_project_name": row.design_project_name, + "status": row.status, "created_at": row.created_at.isoformat() if row.created_at else None, "updated_at": row.updated_at.isoformat() if row.updated_at else None }) @@ -506,14 +582,14 @@ async def update_project_name( # 프로젝트 이름 업데이트 update_query = text(""" UPDATE projects - SET job_name = :job_name, + SET project_name = :project_name, updated_at = CURRENT_TIMESTAMP WHERE id = :project_id RETURNING * """) updated = db.execute(update_query, { - "job_name": job_name, + "project_name": job_name, "project_id": project_id }).fetchone() @@ -534,9 +610,9 @@ async def update_project_name( "message": "프로젝트 이름이 수정되었습니다", "project": { "id": updated.id, - "job_no": updated.job_no, - "job_name": updated.job_name, - "official_project_code": updated.official_project_code + "official_project_code": updated.official_project_code, + "project_name": updated.project_name, + "job_name": updated.project_name # 호환성 } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d35d1ec..219b4d7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -23,16 +23,50 @@ function App() { const [projects, setProjects] = useState([]); const [editingProject, setEditingProject] = useState(null); const [editedProjectName, setEditedProjectName] = useState(''); + const [showCreateProject, setShowCreateProject] = useState(false); + const [newProjectCode, setNewProjectCode] = useState(''); + const [newProjectName, setNewProjectName] = useState(''); + const [newClientName, setNewClientName] = useState(''); // 프로젝트 목록 로드 const loadProjects = async () => { try { - const response = await api.get('/projects'); + const response = await api.get('/dashboard/projects'); if (response.data && response.data.projects) { setProjects(response.data.projects); } } catch (error) { console.error('프로젝트 목록 로드 실패:', error); + // API 실패 시 에러를 무시하고 더미 데이터 사용 + } + }; + + // 프로젝트 생성 + const createProject = async () => { + if (!newProjectCode || !newProjectName) { + alert('프로젝트 코드와 이름을 입력해주세요.'); + return; + } + + try { + const response = await api.post(`/dashboard/projects?official_project_code=${encodeURIComponent(newProjectCode)}&project_name=${encodeURIComponent(newProjectName)}&client_name=${encodeURIComponent(newClientName)}`); + + if (response.data.success) { + // 프로젝트 목록 갱신 + await loadProjects(); + + // 폼 초기화 + setShowCreateProject(false); + setNewProjectCode(''); + setNewProjectName(''); + setNewClientName(''); + + alert('프로젝트가 생성되었습니다.'); + } + } catch (error) { + console.error('프로젝트 생성 실패:', error); + const errorMsg = error.response?.data?.detail || '프로젝트 생성에 실패했습니다.'; + alert(errorMsg); } }; @@ -355,21 +389,18 @@ function App() { {/* 메인 콘텐츠 */}
- {/* 프로젝트 선택 */} + {/* 프로젝트 관리 */}

- 📁 프로젝트 선택 + 📁 프로젝트 관리

- {selectedProject && ( +
- )} + {selectedProject && ( + + )} +
+ {/* 프로젝트 생성 폼 */} + {showCreateProject && ( +
+
+ ➕ 새 프로젝트 생성 +
+
+
+ + setNewProjectCode(e.target.value)} + placeholder="예: J24-004" + style={{ + width: '100%', + padding: '10px 12px', + border: '1px solid #d1d5db', + borderRadius: '6px', + fontSize: '14px' + }} + /> +
+
+ + setNewProjectName(e.target.value)} + placeholder="예: 새로운 프로젝트" + style={{ + width: '100%', + padding: '10px 12px', + border: '1px solid #d1d5db', + borderRadius: '6px', + fontSize: '14px' + }} + /> +
+
+ + setNewClientName(e.target.value)} + placeholder="예: ABC 주식회사" + style={{ + width: '100%', + padding: '10px 12px', + border: '1px solid #d1d5db', + borderRadius: '6px', + fontSize: '14px' + }} + /> +
+
+ + +
+
+
+ )} + + +