프로젝트 생성 및 관리 기능 완성
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

백엔드:
- POST /dashboard/projects 엔드포인트 추가
- 프로젝트 생성 기능 구현
- 중복 코드 검사
- GET /dashboard/projects 컬럼명 수정 (실제 DB 스키마에 맞춤)
- PATCH /dashboard/projects/{id} 컬럼명 수정

프론트엔드:
- 메인 대시보드에 프로젝트 관리 섹션 추가
- ' 새 프로젝트' 버튼으로 생성 폼 표시/숨김
- '✏️ 이름 수정' 버튼으로 프로젝트 이름 수정
- 프로젝트 생성 폼:
  - 프로젝트 코드 (필수)
  - 프로젝트 이름 (필수)
  - 고객사명 (선택)
- 실시간 프로젝트 목록 갱신
- API 연동 완료
This commit is contained in:
Hyungi Ahn
2025-10-14 07:14:55 +09:00
parent 003983872c
commit 6d8bb468c3
2 changed files with 267 additions and 24 deletions

View File

@@ -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 # 호환성
}
}

View File

@@ -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() {
{/* 메인 콘텐츠 */}
<div style={{ padding: '32px', maxWidth: '800px', margin: '0 auto' }}>
{/* 프로젝트 선택 */}
{/* 프로젝트 관리 */}
<div style={{ marginBottom: '32px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<h2 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', margin: 0 }}>
📁 프로젝트 선택
📁 프로젝트 관리
</h2>
{selectedProject && (
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => {
setEditingProject(selectedProject);
setEditedProjectName(selectedProject.project_name || '');
}}
onClick={() => setShowCreateProject(!showCreateProject)}
style={{
padding: '6px 12px',
background: '#10b981',
background: showCreateProject ? '#ef4444' : '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '6px',
@@ -380,13 +411,149 @@ function App() {
alignItems: 'center',
gap: '4px'
}}
title="프로젝트 이름 수정"
>
이름 수정
{showCreateProject ? '✕ 닫기' : ' 새 프로젝트'}
</button>
)}
{selectedProject && (
<button
onClick={() => {
setEditingProject(selectedProject);
setEditedProjectName(selectedProject.project_name || '');
}}
style={{
padding: '6px 12px',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
title="프로젝트 이름 수정"
>
이름 수정
</button>
)}
</div>
</div>
{/* 프로젝트 생성 폼 */}
{showCreateProject && (
<div style={{
background: '#f0fdf4',
border: '2px solid #10b981',
borderRadius: '8px',
padding: '16px',
marginBottom: '16px'
}}>
<div style={{ marginBottom: '12px', fontWeight: '600', color: '#065f46', fontSize: '14px' }}>
프로젝트 생성
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px', fontWeight: '500', color: '#374151' }}>
프로젝트 코드 <span style={{ color: '#ef4444' }}>*</span>
</label>
<input
type="text"
value={newProjectCode}
onChange={(e) => setNewProjectCode(e.target.value)}
placeholder="예: J24-004"
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '6px',
fontSize: '14px'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px', fontWeight: '500', color: '#374151' }}>
프로젝트 이름 <span style={{ color: '#ef4444' }}>*</span>
</label>
<input
type="text"
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
placeholder="예: 새로운 프로젝트"
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '6px',
fontSize: '14px'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px', fontWeight: '500', color: '#374151' }}>
고객사명 (선택)
</label>
<input
type="text"
value={newClientName}
onChange={(e) => setNewClientName(e.target.value)}
placeholder="예: ABC 주식회사"
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '6px',
fontSize: '14px'
}}
/>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button
onClick={createProject}
style={{
flex: 1,
padding: '12px',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
프로젝트 생성
</button>
<button
onClick={() => {
setShowCreateProject(false);
setNewProjectCode('');
setNewProjectName('');
setNewClientName('');
}}
style={{
padding: '12px 24px',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
취소
</button>
</div>
</div>
</div>
)}
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: '500', color: '#374151' }}>
프로젝트 선택
</label>
<select
value={selectedProject?.official_project_code || ''}
onChange={(e) => {