✨ 프로젝트 생성 및 관리 기능 완성
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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:
@@ -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 # 호환성
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user