fix: React 프론트엔드 버그 수정 및 모바일 반응형 개선
- CORS: React 포트(5174) 허용 추가 (application.yml) - ProjectController: /my 엔드포인트 userId 파라미터 선택사항으로 변경 - API 응답 안전 처리: toArray 유틸 추가, 모든 뷰에서 배열 보장 - DataTable emptyMessage: JSX → 문자열로 변경 (PrimeReact 호환) - LoginView: Password → InputText type=password 변경 - 모바일 사이드바: Vue/React 양쪽 inline width 제거로 슬라이드 정상 동작 - 모바일 서브메뉴 텍스트 표시 CSS 수정 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이 Commit은 다음에 포함되어 있습니다:
@@ -8,6 +8,8 @@ import org.springframework.data.domain.Pageable;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import kr.co.accura.wbx.spring.auth.WbxUserDetails;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -45,8 +47,11 @@ public class ProjectController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/my")
|
@GetMapping("/my")
|
||||||
public ResponseEntity<List<ProjectDto>> myProjects(@RequestParam Long userId) {
|
public ResponseEntity<List<ProjectDto>> myProjects(
|
||||||
return ResponseEntity.ok(projectService.findMyProjects(userId));
|
@AuthenticationPrincipal WbxUserDetails user,
|
||||||
|
@RequestParam(required = false) Long userId) {
|
||||||
|
Long resolvedUserId = userId != null ? userId : user.getId();
|
||||||
|
return ResponseEntity.ok(projectService.findMyProjects(resolvedUserId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/members")
|
@GetMapping("/{id}/members")
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ wbx:
|
|||||||
admin-ui:
|
admin-ui:
|
||||||
enabled: true
|
enabled: true
|
||||||
cors:
|
cors:
|
||||||
allowed-origins: ${CORS_ORIGINS:http://localhost:5173}
|
allowed-origins: ${CORS_ORIGINS:http://localhost:5173,http://localhost:5174}
|
||||||
notification:
|
notification:
|
||||||
sse-enabled: true
|
sse-enabled: true
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* API 응답에서 안전하게 배열을 추출합니다.
|
||||||
|
* - 응답이 배열이면 그대로 반환
|
||||||
|
* - 응답이 { items: [...] } 형태면 items 반환
|
||||||
|
* - 그 외 빈 배열 반환
|
||||||
|
*/
|
||||||
|
export function toArray<T>(data: unknown): T[] {
|
||||||
|
if (Array.isArray(data)) return data as T[];
|
||||||
|
if (data && typeof data === 'object' && 'items' in data) {
|
||||||
|
const items = (data as Record<string, unknown>).items;
|
||||||
|
if (Array.isArray(items)) return items as T[];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
@@ -59,4 +59,28 @@
|
|||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__menu .p-panelmenu-panel {
|
||||||
|
.p-panelmenu-header-content,
|
||||||
|
.p-menuitem-content {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-panelmenu-content {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-menuitem-text {
|
||||||
|
display: inline !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모바일에서 서브메뉴 텍스트 표시 보장
|
||||||
|
&--mobile &__nav {
|
||||||
|
.p-menuitem-text,
|
||||||
|
.p-panelmenu-header-content span {
|
||||||
|
display: inline !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ export default function AppSidebar({ visible, collapsed, mobile }: Props) {
|
|||||||
}));
|
}));
|
||||||
}, [currentUser, navigate]);
|
}, [currentUser, navigate]);
|
||||||
|
|
||||||
const sidebarWidth = collapsed ? `${LAYOUT.sidebarCollapsedWidth}px` : `${LAYOUT.sidebarWidth}px`;
|
|
||||||
|
|
||||||
const classNames = [
|
const classNames = [
|
||||||
'app-sidebar',
|
'app-sidebar',
|
||||||
visible && 'app-sidebar--visible',
|
visible && 'app-sidebar--visible',
|
||||||
@@ -45,8 +43,12 @@ export default function AppSidebar({ visible, collapsed, mobile }: Props) {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
|
const style = mobile
|
||||||
|
? undefined
|
||||||
|
: { width: collapsed ? `${LAYOUT.sidebarCollapsedWidth}px` : `${LAYOUT.sidebarWidth}px` };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={classNames} style={{ width: sidebarWidth }}>
|
<aside className={classNames} style={style}>
|
||||||
<div className="app-sidebar__header">
|
<div className="app-sidebar__header">
|
||||||
<img src={logo} alt="WTM" className="app-sidebar__logo" />
|
<img src={logo} alt="WTM" className="app-sidebar__logo" />
|
||||||
{!collapsed && <span className="app-sidebar__title">WTM</span>}
|
{!collapsed && <span className="app-sidebar__title">WTM</span>}
|
||||||
|
|||||||
@@ -77,12 +77,7 @@ export default function BaseCrudTable<T extends Record<string, any>>({
|
|||||||
className="crud-table__datatable"
|
className="crud-table__datatable"
|
||||||
onRowSelect={(e) => onRowSelect?.(e.data as T)}
|
onRowSelect={(e) => onRowSelect?.(e.data as T)}
|
||||||
onPage={onPage}
|
onPage={onPage}
|
||||||
emptyMessage={
|
emptyMessage={emptyMessage}
|
||||||
<div className="crud-table__empty">
|
|
||||||
<i className="pi pi-inbox" style={{ fontSize: '2rem' }} />
|
|
||||||
<p>{emptyMessage}</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import BaseCrudTable from '@/core/components/BaseCrudTable';
|
|||||||
import BasePageHeader from '@/core/components/BasePageHeader';
|
import BasePageHeader from '@/core/components/BasePageHeader';
|
||||||
import { approvalService } from '../approval.service';
|
import { approvalService } from '../approval.service';
|
||||||
import { TIMESHEET_STATUS } from '@/core/constants/app.constants';
|
import { TIMESHEET_STATUS } from '@/core/constants/app.constants';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
|
|
||||||
export default function ApprovalHistoryView() {
|
export default function ApprovalHistoryView() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -23,7 +24,7 @@ export default function ApprovalHistoryView() {
|
|||||||
if (dateTo) params.to = dateTo.toISOString().slice(0, 10);
|
if (dateTo) params.to = dateTo.toISOString().slice(0, 10);
|
||||||
|
|
||||||
approvalService.getHistory(params)
|
approvalService.getHistory(params)
|
||||||
.then(({ data }) => setHistory((data as { items?: unknown[] }).items as Record<string, unknown>[] ?? data as Record<string, unknown>[]))
|
.then(({ data }) => setHistory(toArray<Record<string, unknown>>(data)))
|
||||||
.catch(() => setHistory([]))
|
.catch(() => setHistory([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [statusFilter, dateFrom, dateTo]);
|
}, [statusFilter, dateFrom, dateTo]);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import BasePageHeader from '@/core/components/BasePageHeader';
|
|||||||
import { approvalService } from '../approval.service';
|
import { approvalService } from '../approval.service';
|
||||||
import { TIMESHEET_STATUS } from '@/core/constants/app.constants';
|
import { TIMESHEET_STATUS } from '@/core/constants/app.constants';
|
||||||
import type { Approval } from '../approval.types';
|
import type { Approval } from '../approval.types';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
|
|
||||||
export default function ApprovalPendingView() {
|
export default function ApprovalPendingView() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -16,7 +17,7 @@ export default function ApprovalPendingView() {
|
|||||||
function load() {
|
function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
approvalService.getPending()
|
approvalService.getPending()
|
||||||
.then(({ data }) => setApprovals((data as { items?: Approval[] }).items ?? data as Approval[]))
|
.then(({ data }) => setApprovals(toArray<Approval>(data)))
|
||||||
.catch(() => setApprovals([]))
|
.catch(() => setApprovals([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, type FormEvent } from 'react';
|
import { useState, type FormEvent } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { InputText } from 'primereact/inputtext';
|
import { InputText } from 'primereact/inputtext';
|
||||||
import { Password } from 'primereact/password';
|
|
||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
import { Card } from 'primereact/card';
|
import { Card } from 'primereact/card';
|
||||||
import { Message } from 'primereact/message';
|
import { Message } from 'primereact/message';
|
||||||
@@ -55,13 +54,11 @@ export default function LoginView() {
|
|||||||
|
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
<label className="form-field__label">비밀번호</label>
|
<label className="form-field__label">비밀번호</label>
|
||||||
<Password
|
<InputText
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
type="password"
|
||||||
placeholder="비밀번호 입력"
|
placeholder="비밀번호 입력"
|
||||||
feedback={false}
|
|
||||||
toggleMask
|
|
||||||
inputStyle={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -39,14 +39,16 @@ export default function DashboardView() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
dashboardService.getDashboard()
|
dashboardService.getDashboard()
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
setStats(data.stats ?? defaultStats);
|
const d = data as Record<string, unknown>;
|
||||||
setPendingApprovals(data.pendingApprovals ?? []);
|
setStats(Array.isArray(d.stats) ? d.stats as DashboardStat[] : defaultStats);
|
||||||
|
setPendingApprovals(Array.isArray(d.pendingApprovals) ? d.pendingApprovals as Record<string, unknown>[]
|
||||||
|
: Array.isArray(d.recentTimesheets) ? d.recentTimesheets as Record<string, unknown>[] : []);
|
||||||
setWeeklyHoursData({
|
setWeeklyHoursData({
|
||||||
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: '시수',
|
label: '시수',
|
||||||
backgroundColor: 'var(--p-primary-color)',
|
backgroundColor: '#3B82F6',
|
||||||
data: data.weeklyHours ?? [0, 0, 0, 0, 0, 0],
|
data: Array.isArray(d.weeklyHours) ? d.weeklyHours : [0, 0, 0, 0, 0, 0],
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -54,7 +56,7 @@ export default function DashboardView() {
|
|||||||
setStats(defaultStats);
|
setStats(defaultStats);
|
||||||
setWeeklyHoursData({
|
setWeeklyHoursData({
|
||||||
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||||
datasets: [{ label: '시수', backgroundColor: 'var(--p-primary-color)', data: [0, 0, 0, 0, 0, 0] }],
|
datasets: [{ label: '시수', backgroundColor: '#3B82F6', data: [0, 0, 0, 0, 0, 0] }],
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@@ -109,7 +111,7 @@ export default function DashboardView() {
|
|||||||
|
|
||||||
<Card title="결재 대기 목록" className="dashboard-view__approvals-card">
|
<Card title="결재 대기 목록" className="dashboard-view__approvals-card">
|
||||||
<DataTable value={pendingApprovals} rows={5} paginator={pendingApprovals.length > 5} size="small" stripedRows
|
<DataTable value={pendingApprovals} rows={5} paginator={pendingApprovals.length > 5} size="small" stripedRows
|
||||||
emptyMessage={<div style={{ textAlign: 'center', padding: '1rem', color: 'var(--p-text-muted-color)' }}>결재 대기 건이 없습니다.</div>}
|
emptyMessage="결재 대기 건이 없습니다."
|
||||||
>
|
>
|
||||||
<Column field="requesterName" header="요청자" />
|
<Column field="requesterName" header="요청자" />
|
||||||
<Column field="projectName" header="프로젝트" />
|
<Column field="projectName" header="프로젝트" />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ProjectFormDialog from '../components/ProjectFormDialog';
|
|||||||
import { projectService } from '../project.service';
|
import { projectService } from '../project.service';
|
||||||
import { PROJECT_STATUS } from '@/core/constants/app.constants';
|
import { PROJECT_STATUS } from '@/core/constants/app.constants';
|
||||||
import type { Project } from '../project.types';
|
import type { Project } from '../project.types';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
|
|
||||||
export default function ProjectListView() {
|
export default function ProjectListView() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -20,7 +21,7 @@ export default function ProjectListView() {
|
|||||||
function load() {
|
function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
projectService.getAll()
|
projectService.getAll()
|
||||||
.then(({ data }) => setProjects((data as { items?: Project[] }).items ?? data as Project[]))
|
.then(({ data }) => setProjects(toArray<Project>(data)))
|
||||||
.catch(() => setProjects([]))
|
.catch(() => setProjects([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Chart } from 'primereact/chart';
|
|||||||
import BasePageHeader from '@/core/components/BasePageHeader';
|
import BasePageHeader from '@/core/components/BasePageHeader';
|
||||||
import { reportService } from '../report.service';
|
import { reportService } from '../report.service';
|
||||||
import { projectService } from '@/modules/project/project.service';
|
import { projectService } from '@/modules/project/project.service';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
|
|
||||||
export default function ReportView() {
|
export default function ReportView() {
|
||||||
const [projects, setProjects] = useState<{ id: number; name: string }[]>([]);
|
const [projects, setProjects] = useState<{ id: number; name: string }[]>([]);
|
||||||
@@ -28,7 +29,7 @@ export default function ReportView() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
projectService.getAll()
|
projectService.getAll()
|
||||||
.then(({ data }) => setProjects((data as { items?: unknown[] }).items as { id: number; name: string }[] ?? data as { id: number; name: string }[]))
|
.then(({ data }) => setProjects(toArray<{ id: number; name: string }>(data)))
|
||||||
.catch(() => setProjects([]));
|
.catch(() => setProjects([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export default function ReportView() {
|
|||||||
if (dateTo) params.to = dateTo.toISOString().slice(0, 10);
|
if (dateTo) params.to = dateTo.toISOString().slice(0, 10);
|
||||||
|
|
||||||
reportService.getProjectHours(params)
|
reportService.getProjectHours(params)
|
||||||
.then(({ data }) => setData(Array.isArray(data) ? data : (data as { items?: unknown[] }).items as Record<string, unknown>[] ?? []))
|
.then(({ data }) => setData(toArray<Record<string, unknown>>(data)))
|
||||||
.catch(() => setData([]))
|
.catch(() => setData([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import TealUploadDialog from '../components/TealUploadDialog';
|
|||||||
import { tealService } from '../teal.service';
|
import { tealService } from '../teal.service';
|
||||||
import { projectService } from '@/modules/project/project.service';
|
import { projectService } from '@/modules/project/project.service';
|
||||||
import type { TealEntry, TealVersion } from '../teal.types';
|
import type { TealEntry, TealVersion } from '../teal.types';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
|
|
||||||
export default function TealListView() {
|
export default function TealListView() {
|
||||||
const [projects, setProjects] = useState<{ id: number; name: string; code: string }[]>([]);
|
const [projects, setProjects] = useState<{ id: number; name: string; code: string }[]>([]);
|
||||||
@@ -21,14 +22,14 @@ export default function TealListView() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
projectService.getAll()
|
projectService.getAll()
|
||||||
.then(({ data }) => setProjects((data as { items?: unknown[] }).items as { id: number; name: string; code: string }[] ?? data as { id: number; name: string; code: string }[]))
|
.then(({ data }) => setProjects(toArray<{ id: number; name: string; code: string }>(data)))
|
||||||
.catch(() => setProjects([]));
|
.catch(() => setProjects([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProjectId) { setVersions([]); setEntries([]); return; }
|
if (!selectedProjectId) { setVersions([]); setEntries([]); return; }
|
||||||
tealService.getVersions(selectedProjectId)
|
tealService.getVersions(selectedProjectId)
|
||||||
.then(({ data }) => setVersions(data as TealVersion[]))
|
.then(({ data }) => setVersions(toArray<TealVersion>(data)))
|
||||||
.catch(() => setVersions([]));
|
.catch(() => setVersions([]));
|
||||||
}, [selectedProjectId]);
|
}, [selectedProjectId]);
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ export default function TealListView() {
|
|||||||
if (!selectedProjectId || !selectedVersionId) { setEntries([]); return; }
|
if (!selectedProjectId || !selectedVersionId) { setEntries([]); return; }
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
tealService.getActive(selectedProjectId)
|
tealService.getActive(selectedProjectId)
|
||||||
.then(({ data }) => setEntries(data as TealEntry[]))
|
.then(({ data }) => setEntries(toArray<TealEntry>(data)))
|
||||||
.catch(() => setEntries([]))
|
.catch(() => setEntries([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [selectedProjectId, selectedVersionId]);
|
}, [selectedProjectId, selectedVersionId]);
|
||||||
@@ -46,7 +47,7 @@ export default function TealListView() {
|
|||||||
await tealService.upload(selectedProjectId, file, effectiveDate);
|
await tealService.upload(selectedProjectId, file, effectiveDate);
|
||||||
setUploadVisible(false);
|
setUploadVisible(false);
|
||||||
const { data } = await tealService.getVersions(selectedProjectId);
|
const { data } = await tealService.getVersions(selectedProjectId);
|
||||||
setVersions(data as TealVersion[]);
|
setVersions(toArray<TealVersion>(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import BaseCrudTable from '@/core/components/BaseCrudTable';
|
|||||||
import BasePageHeader from '@/core/components/BasePageHeader';
|
import BasePageHeader from '@/core/components/BasePageHeader';
|
||||||
import { timesheetService } from '../timesheet.service';
|
import { timesheetService } from '../timesheet.service';
|
||||||
import { TIMESHEET_STATUS } from '@/core/constants/app.constants';
|
import { TIMESHEET_STATUS } from '@/core/constants/app.constants';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
|
|
||||||
export default function TimesheetHistoryView() {
|
export default function TimesheetHistoryView() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -25,7 +26,7 @@ export default function TimesheetHistoryView() {
|
|||||||
if (dateTo) params.to = dateTo.toISOString().slice(0, 10);
|
if (dateTo) params.to = dateTo.toISOString().slice(0, 10);
|
||||||
|
|
||||||
timesheetService.getHistory(params)
|
timesheetService.getHistory(params)
|
||||||
.then(({ data }) => setHistory((data as { items?: unknown[] }).items as Record<string, unknown>[] ?? data as Record<string, unknown>[]))
|
.then(({ data }) => setHistory(toArray<Record<string, unknown>>(data)))
|
||||||
.catch(() => setHistory([]))
|
.catch(() => setHistory([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [statusFilter, dateFrom, dateTo]);
|
}, [statusFilter, dateFrom, dateTo]);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import BasePageHeader from '@/core/components/BasePageHeader';
|
|||||||
import TimesheetEntryRow from '../components/TimesheetEntryRow';
|
import TimesheetEntryRow from '../components/TimesheetEntryRow';
|
||||||
import { timesheetService } from '../timesheet.service';
|
import { timesheetService } from '../timesheet.service';
|
||||||
import { projectService } from '@/modules/project/project.service';
|
import { projectService } from '@/modules/project/project.service';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
import { TIMESHEET_RULES, TIMESHEET_STATUS, ENTRY_TYPES } from '@/core/constants/app.constants';
|
import { TIMESHEET_RULES, TIMESHEET_STATUS, ENTRY_TYPES } from '@/core/constants/app.constants';
|
||||||
import type { Timesheet, TimesheetEntry, EntryType } from '../timesheet.types';
|
import type { Timesheet, TimesheetEntry, EntryType } from '../timesheet.types';
|
||||||
import './TimesheetWeekView.scss';
|
import './TimesheetWeekView.scss';
|
||||||
@@ -126,7 +127,7 @@ export default function TimesheetWeekView() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
projectService.getMy()
|
projectService.getMy()
|
||||||
.then(({ data }) => setProjects((data as { items?: unknown[] }).items as { id: number; name: string }[] ?? data as { id: number; name: string }[]))
|
.then(({ data }) => setProjects(toArray<{ id: number; name: string }>(data)))
|
||||||
.catch(() => setProjects([]));
|
.catch(() => setProjects([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import BasePageHeader from '@/core/components/BasePageHeader';
|
|||||||
import UserFormDialog from '../components/UserFormDialog';
|
import UserFormDialog from '../components/UserFormDialog';
|
||||||
import { userService } from '../user.service';
|
import { userService } from '../user.service';
|
||||||
import type { User } from '../user.types';
|
import type { User } from '../user.types';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
|
|
||||||
const ROLE_SEVERITY: Record<string, string> = {
|
const ROLE_SEVERITY: Record<string, string> = {
|
||||||
SA: 'danger', PM: 'warning', PCM: 'info', DL: 'success', PTK: 'secondary', USER: 'contrast',
|
SA: 'danger', PM: 'warning', PCM: 'info', DL: 'success', PTK: 'secondary', USER: 'contrast',
|
||||||
@@ -21,7 +22,7 @@ export default function UserListView() {
|
|||||||
function load() {
|
function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
userService.getAll()
|
userService.getAll()
|
||||||
.then(({ data }) => setUsers((data as { items?: User[] }).items ?? data as User[]))
|
.then(({ data }) => setUsers(toArray<User>(data)))
|
||||||
.catch(() => setUsers([]))
|
.catch(() => setUsers([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import WbsUploadDialog from '../components/WbsUploadDialog';
|
|||||||
import { wbsService } from '../wbs.service';
|
import { wbsService } from '../wbs.service';
|
||||||
import { projectService } from '@/modules/project/project.service';
|
import { projectService } from '@/modules/project/project.service';
|
||||||
import type { WbsVersion, WbsNode } from '../wbs.types';
|
import type { WbsVersion, WbsNode } from '../wbs.types';
|
||||||
|
import { toArray } from '@/core/api/utils';
|
||||||
|
|
||||||
function nodesToTreeNodes(nodes: WbsNode[]): TreeNode[] {
|
function nodesToTreeNodes(nodes: WbsNode[]): TreeNode[] {
|
||||||
return nodes.map((n) => ({
|
return nodes.map((n) => ({
|
||||||
@@ -31,14 +32,14 @@ export default function WbsTreeView() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
projectService.getAll()
|
projectService.getAll()
|
||||||
.then(({ data }) => setProjects((data as { items?: unknown[] }).items as { id: number; name: string; code: string }[] ?? data as { id: number; name: string; code: string }[]))
|
.then(({ data }) => setProjects(toArray<{ id: number; name: string; code: string }>(data)))
|
||||||
.catch(() => setProjects([]));
|
.catch(() => setProjects([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProjectId) { setVersions([]); return; }
|
if (!selectedProjectId) { setVersions([]); return; }
|
||||||
wbsService.getVersions(selectedProjectId)
|
wbsService.getVersions(selectedProjectId)
|
||||||
.then(({ data }) => setVersions(data as WbsVersion[]))
|
.then(({ data }) => setVersions(toArray<WbsVersion>(data)))
|
||||||
.catch(() => setVersions([]));
|
.catch(() => setVersions([]));
|
||||||
}, [selectedProjectId]);
|
}, [selectedProjectId]);
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ export default function WbsTreeView() {
|
|||||||
if (!selectedProjectId || !selectedVersion) { setTreeNodes([]); return; }
|
if (!selectedProjectId || !selectedVersion) { setTreeNodes([]); return; }
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
wbsService.getVersionNodes(selectedProjectId, selectedVersion)
|
wbsService.getVersionNodes(selectedProjectId, selectedVersion)
|
||||||
.then(({ data }) => setTreeNodes(nodesToTreeNodes(data as WbsNode[])))
|
.then(({ data }) => setTreeNodes(nodesToTreeNodes(toArray<WbsNode>(data))))
|
||||||
.catch(() => setTreeNodes([]))
|
.catch(() => setTreeNodes([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [selectedProjectId, selectedVersion]);
|
}, [selectedProjectId, selectedVersion]);
|
||||||
@@ -56,7 +57,7 @@ export default function WbsTreeView() {
|
|||||||
await wbsService.upload(selectedProjectId, file, effectiveDate);
|
await wbsService.upload(selectedProjectId, file, effectiveDate);
|
||||||
setUploadVisible(false);
|
setUploadVisible(false);
|
||||||
const { data } = await wbsService.getVersions(selectedProjectId);
|
const { data } = await wbsService.getVersions(selectedProjectId);
|
||||||
setVersions(data as WbsVersion[]);
|
setVersions(toArray<WbsVersion>(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ function filterByRole(items: typeof MENU_ITEMS, roles: string[]) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarWidth = computed(() =>
|
const sidebarStyle = computed(() =>
|
||||||
props.collapsed ? `${LAYOUT.sidebarCollapsedWidth}px` : `${LAYOUT.sidebarWidth}px`,
|
props.mobile ? {} : { width: props.collapsed ? `${LAYOUT.sidebarCollapsedWidth}px` : `${LAYOUT.sidebarWidth}px` },
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ const sidebarWidth = computed(() =>
|
|||||||
'app-sidebar--collapsed': collapsed,
|
'app-sidebar--collapsed': collapsed,
|
||||||
'app-sidebar--mobile': mobile,
|
'app-sidebar--mobile': mobile,
|
||||||
}"
|
}"
|
||||||
:style="{ width: sidebarWidth }"
|
:style="sidebarStyle"
|
||||||
>
|
>
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="app-sidebar__header">
|
<div class="app-sidebar__header">
|
||||||
|
|||||||
새 Issue에서 참조
사용자 차단