fix: production save errors and import HR dashboard data
Resolve invalid task creator IDs, fix API routing and file uploads on Vercel, and replace dummy seed data with HR_Dashboard import.
This commit is contained in:
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { apiClient } from '../../lib/apiClient';
|
||||
import { apiClient, getApiErrorMessage } from '../../lib/apiClient';
|
||||
import { isProjectTask, isRoutineTask } from '../../lib/taskType';
|
||||
import { SortableTaskCard } from './TaskCard';
|
||||
import { ContextMenu } from '../common/ContextMenu';
|
||||
@@ -181,28 +181,31 @@ export function DepartmentColumn({ title: initialTitle, titleEn, subtitle: initi
|
||||
|
||||
const displayTitle = title.replace(/\s*부문$/, '');
|
||||
|
||||
const handleAdd = (data: TaskFormData) => {
|
||||
create.mutate({
|
||||
title: data.title,
|
||||
section: data.section || null,
|
||||
taskType: data.taskType || null,
|
||||
status: data.status as Task['status'],
|
||||
progress: data.progress,
|
||||
description: data.description || null,
|
||||
issueNote: data.issueNote || null,
|
||||
startDate: data.startDate || null,
|
||||
dueDate: data.dueDate || null,
|
||||
showDate: data.showDate,
|
||||
showDescription: data.showDescription,
|
||||
showStatus: data.showStatus,
|
||||
showIssue: data.showIssue,
|
||||
showProgress: data.showProgress,
|
||||
keywords: data.keywords || null,
|
||||
quarter: data.quarter,
|
||||
priority: 'MEDIUM',
|
||||
creatorId: 'system',
|
||||
} as any);
|
||||
setShowAddModal(false);
|
||||
const handleAdd = async (data: TaskFormData) => {
|
||||
try {
|
||||
await create.mutateAsync({
|
||||
title: data.title,
|
||||
section: data.section || null,
|
||||
taskType: data.taskType || null,
|
||||
status: data.status as Task['status'],
|
||||
progress: data.progress,
|
||||
description: data.description || null,
|
||||
issueNote: data.issueNote || null,
|
||||
startDate: data.startDate || null,
|
||||
dueDate: data.dueDate || null,
|
||||
showDate: data.showDate,
|
||||
showDescription: data.showDescription,
|
||||
showStatus: data.showStatus,
|
||||
showIssue: data.showIssue,
|
||||
showProgress: data.showProgress,
|
||||
keywords: data.keywords || null,
|
||||
quarter: data.quarter,
|
||||
priority: 'MEDIUM',
|
||||
} as Partial<Task>);
|
||||
setShowAddModal(false);
|
||||
} catch (err: unknown) {
|
||||
alert(getApiErrorMessage(err, '업무 추가에 실패했습니다.'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (data: TaskFormData) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { apiClient } from '../../lib/apiClient';
|
||||
import { apiClient, getApiErrorMessage } from '../../lib/apiClient';
|
||||
import { TaskModal } from '../common/TaskModal';
|
||||
import type { TaskFormData } from '../common/TaskModal';
|
||||
import type { Task } from '../../types';
|
||||
@@ -61,23 +61,26 @@ export function TaskManager({ tasks, sectionOptions, quarter, onClose }: TaskMan
|
||||
return true;
|
||||
});
|
||||
|
||||
const handleAdd = (data: TaskFormData) => {
|
||||
create.mutate({
|
||||
title: data.title, section: data.section || null, tag: data.tag || null,
|
||||
taskType: data.taskType || null, status: data.status, progress: data.progress,
|
||||
description: data.description || null, issueNote: data.issueNote || null,
|
||||
startDate: data.startDate || null, dueDate: data.dueDate || null,
|
||||
showDate: data.showDate,
|
||||
showDescription: data.showDescription,
|
||||
showStatus: data.showStatus,
|
||||
showIssue: data.showIssue,
|
||||
showProgress: data.showProgress,
|
||||
keywords: data.keywords || null,
|
||||
quarter: data.quarter,
|
||||
priority: 'MEDIUM',
|
||||
creatorId: 'system',
|
||||
});
|
||||
setModalMode(null);
|
||||
const handleAdd = async (data: TaskFormData) => {
|
||||
try {
|
||||
await create.mutateAsync({
|
||||
title: data.title, section: data.section || null, tag: data.tag || null,
|
||||
taskType: data.taskType || null, status: data.status, progress: data.progress,
|
||||
description: data.description || null, issueNote: data.issueNote || null,
|
||||
startDate: data.startDate || null, dueDate: data.dueDate || null,
|
||||
showDate: data.showDate,
|
||||
showDescription: data.showDescription,
|
||||
showStatus: data.showStatus,
|
||||
showIssue: data.showIssue,
|
||||
showProgress: data.showProgress,
|
||||
keywords: data.keywords || null,
|
||||
quarter: data.quarter,
|
||||
priority: 'MEDIUM',
|
||||
});
|
||||
setModalMode(null);
|
||||
} catch (err: unknown) {
|
||||
alert(getApiErrorMessage(err, '업무 추가에 실패했습니다.'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (data: TaskFormData) => {
|
||||
|
||||
@@ -3,10 +3,13 @@ import { io, type Socket } from 'socket.io-client';
|
||||
|
||||
const SocketContext = createContext<Socket | null>(null);
|
||||
|
||||
// 같은 네트워크 팀원도 접속 가능: 백엔드 주소를 현재 페이지 호스트에서 자동 감지
|
||||
const RENDER_API = 'https://eene-dashboard-backend.onrender.com';
|
||||
|
||||
const SOCKET_URL =
|
||||
import.meta.env.VITE_SOCKET_URL ||
|
||||
`${window.location.protocol}//${window.location.hostname}:4000`;
|
||||
(import.meta.env.PROD
|
||||
? RENDER_API
|
||||
: `${window.location.protocol}//${window.location.hostname}:4000`);
|
||||
|
||||
export function SocketProvider({ children }: { children: ReactNode }) {
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// 개발: Vite 프록시 → /api (localhost:4000)
|
||||
// 배포: VITE_API_URL=https://xxx.onrender.com 설정 시 그 주소 사용
|
||||
// 배포: VITE_API_URL 미설정 시 Render 백엔드 기본값 사용
|
||||
const RENDER_API = 'https://eene-dashboard-backend.onrender.com';
|
||||
const baseURL = import.meta.env.VITE_API_URL
|
||||
? `${import.meta.env.VITE_API_URL}/api`
|
||||
: '/api';
|
||||
: import.meta.env.PROD
|
||||
? `${RENDER_API}/api`
|
||||
: '/api';
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL,
|
||||
@@ -13,7 +16,19 @@ export const apiClient = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
if (config.data instanceof FormData) {
|
||||
delete config.headers['Content-Type'];
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(res) => res,
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
export function getApiErrorMessage(err: unknown, fallback: string): string {
|
||||
const ax = err as { response?: { data?: { message?: string }; status?: number }; message?: string };
|
||||
return ax.response?.data?.message || ax.message || fallback;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../lib/apiClient';
|
||||
import { apiClient, getApiErrorMessage } from '../lib/apiClient';
|
||||
import { onDualMonitorEvent } from '../lib/dualMonitor';
|
||||
import { ContextMenu } from '../components/common/ContextMenu';
|
||||
import { FeedbackModal, type FeedbackFormData } from '../components/detail/FeedbackModal';
|
||||
@@ -272,9 +272,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
|
||||
if (item.displayName.trim()) {
|
||||
form.append('displayName', item.displayName.trim());
|
||||
}
|
||||
await apiClient.post(`/files/upload/${task.id}`, form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
await apiClient.post(`/files/upload/${task.id}`, form);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -313,9 +311,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
|
||||
for (const rep of filePayload.replacements) {
|
||||
const form = new FormData();
|
||||
form.append('file', rep.file);
|
||||
await apiClient.post(`/files/${rep.id}/replace`, form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
await apiClient.post(`/files/${rep.id}/replace`, form);
|
||||
}
|
||||
for (const edit of filePayload.existingEdits) {
|
||||
const original = files.find((f) => f.id === edit.id);
|
||||
@@ -334,17 +330,13 @@ function DetailView({ task }: { task: TaskWithRelations }) {
|
||||
await uploadFiles(milestoneId, filePayload.uploads);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const ax = err as { response?: { data?: { message?: string } }; message?: string };
|
||||
const msg = ax.response?.data?.message || ax.message || '파일 처리에 실패했습니다.';
|
||||
alert(`단계는 저장됐지만 ${msg}`);
|
||||
alert(`단계는 저장됐지만 ${getApiErrorMessage(err, '파일 처리에 실패했습니다.')}`);
|
||||
}
|
||||
|
||||
await qc.invalidateQueries({ queryKey: ['task', task.id] });
|
||||
setStageModal(null);
|
||||
} catch (err: unknown) {
|
||||
const ax = err as { response?: { data?: { message?: string } }; message?: string };
|
||||
const msg = ax.response?.data?.message || ax.message || '단계 저장에 실패했습니다.';
|
||||
alert(msg);
|
||||
alert(getApiErrorMessage(err, '단계 저장에 실패했습니다.'));
|
||||
} finally {
|
||||
setStageSaving(false);
|
||||
}
|
||||
@@ -372,9 +364,7 @@ function DetailView({ task }: { task: TaskWithRelations }) {
|
||||
await qc.invalidateQueries({ queryKey: ['task', task.id] });
|
||||
setFeedbackModal(null);
|
||||
} catch (err: unknown) {
|
||||
const ax = err as { response?: { data?: { message?: string } }; message?: string };
|
||||
const msg = ax.response?.data?.message || ax.message || '피드백 저장에 실패했습니다.';
|
||||
alert(msg);
|
||||
alert(getApiErrorMessage(err, '피드백 저장에 실패했습니다.'));
|
||||
} finally {
|
||||
setFeedbackSaving(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user