✨ 프로젝트 생성 및 관리 기능 완성
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)}")
|
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")
|
@router.get("/projects")
|
||||||
async def get_projects(
|
async def get_projects(
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
@@ -442,10 +514,12 @@ async def get_projects(
|
|||||||
query = text("""
|
query = text("""
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
job_no,
|
|
||||||
official_project_code,
|
official_project_code,
|
||||||
job_name,
|
project_name,
|
||||||
project_type,
|
client_name,
|
||||||
|
design_project_code,
|
||||||
|
design_project_name,
|
||||||
|
status,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM projects
|
FROM projects
|
||||||
@@ -458,11 +532,13 @@ async def get_projects(
|
|||||||
for row in results:
|
for row in results:
|
||||||
projects.append({
|
projects.append({
|
||||||
"id": row.id,
|
"id": row.id,
|
||||||
"job_no": row.job_no,
|
|
||||||
"official_project_code": row.official_project_code,
|
"official_project_code": row.official_project_code,
|
||||||
"job_name": row.job_name,
|
"project_name": row.project_name,
|
||||||
"project_name": row.job_name, # 호환성을 위해 추가
|
"job_name": row.project_name, # 호환성을 위해 추가
|
||||||
"project_type": row.project_type,
|
"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,
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||||
"updated_at": row.updated_at.isoformat() if row.updated_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_query = text("""
|
||||||
UPDATE projects
|
UPDATE projects
|
||||||
SET job_name = :job_name,
|
SET project_name = :project_name,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = :project_id
|
WHERE id = :project_id
|
||||||
RETURNING *
|
RETURNING *
|
||||||
""")
|
""")
|
||||||
|
|
||||||
updated = db.execute(update_query, {
|
updated = db.execute(update_query, {
|
||||||
"job_name": job_name,
|
"project_name": job_name,
|
||||||
"project_id": project_id
|
"project_id": project_id
|
||||||
}).fetchone()
|
}).fetchone()
|
||||||
|
|
||||||
@@ -534,9 +610,9 @@ async def update_project_name(
|
|||||||
"message": "프로젝트 이름이 수정되었습니다",
|
"message": "프로젝트 이름이 수정되었습니다",
|
||||||
"project": {
|
"project": {
|
||||||
"id": updated.id,
|
"id": updated.id,
|
||||||
"job_no": updated.job_no,
|
"official_project_code": updated.official_project_code,
|
||||||
"job_name": updated.job_name,
|
"project_name": updated.project_name,
|
||||||
"official_project_code": updated.official_project_code
|
"job_name": updated.project_name # 호환성
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,16 +23,50 @@ function App() {
|
|||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
const [editingProject, setEditingProject] = useState(null);
|
const [editingProject, setEditingProject] = useState(null);
|
||||||
const [editedProjectName, setEditedProjectName] = useState('');
|
const [editedProjectName, setEditedProjectName] = useState('');
|
||||||
|
const [showCreateProject, setShowCreateProject] = useState(false);
|
||||||
|
const [newProjectCode, setNewProjectCode] = useState('');
|
||||||
|
const [newProjectName, setNewProjectName] = useState('');
|
||||||
|
const [newClientName, setNewClientName] = useState('');
|
||||||
|
|
||||||
// 프로젝트 목록 로드
|
// 프로젝트 목록 로드
|
||||||
const loadProjects = async () => {
|
const loadProjects = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/projects');
|
const response = await api.get('/dashboard/projects');
|
||||||
if (response.data && response.data.projects) {
|
if (response.data && response.data.projects) {
|
||||||
setProjects(response.data.projects);
|
setProjects(response.data.projects);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('프로젝트 목록 로드 실패:', 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,12 +389,31 @@ function App() {
|
|||||||
{/* 메인 콘텐츠 */}
|
{/* 메인 콘텐츠 */}
|
||||||
<div style={{ padding: '32px', maxWidth: '800px', margin: '0 auto' }}>
|
<div style={{ padding: '32px', maxWidth: '800px', margin: '0 auto' }}>
|
||||||
|
|
||||||
{/* 프로젝트 선택 */}
|
{/* 프로젝트 관리 */}
|
||||||
<div style={{ marginBottom: '32px' }}>
|
<div style={{ marginBottom: '32px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||||
<h2 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', margin: 0 }}>
|
<h2 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', margin: 0 }}>
|
||||||
📁 프로젝트 선택
|
📁 프로젝트 관리
|
||||||
</h2>
|
</h2>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateProject(!showCreateProject)}
|
||||||
|
style={{
|
||||||
|
padding: '6px 12px',
|
||||||
|
background: showCreateProject ? '#ef4444' : '#3b82f6',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showCreateProject ? '✕ 닫기' : '➕ 새 프로젝트'}
|
||||||
|
</button>
|
||||||
{selectedProject && (
|
{selectedProject && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -386,6 +439,120 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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
|
<select
|
||||||
value={selectedProject?.official_project_code || ''}
|
value={selectedProject?.official_project_code || ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user