Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -265,7 +265,6 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
|
||||
// 파일 다운로드 핸들러
|
||||
const handleFileDownload = useCallback((fileName: string) => {
|
||||
console.log('파일 다운로드:', fileName);
|
||||
// TODO: 실제 다운로드 로직
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -582,28 +582,26 @@ export function ApprovalBox() {
|
||||
},
|
||||
],
|
||||
|
||||
headerActions: ({ selectedItems, onClearSelection }) => (
|
||||
selectionActions: ({ selectedItems, onClearSelection }) => canApprove ? (
|
||||
<>
|
||||
{selectedItems.size > 0 && canApprove && (
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleApproveClick(selectedItems, onClearSelection)}
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => handleRejectClick(selectedItems, onClearSelection)}
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
반려
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={() => handleApproveClick(selectedItems, onClearSelection)}
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => handleRejectClick(selectedItems, onClearSelection)}
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
반려
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
) : null,
|
||||
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -73,19 +73,15 @@ export function DocumentDetailModal({
|
||||
};
|
||||
|
||||
const handleSharePdf = () => {
|
||||
console.log('PDF 다운로드');
|
||||
};
|
||||
|
||||
const handleShareEmail = () => {
|
||||
console.log('이메일 공유');
|
||||
};
|
||||
|
||||
const handleShareFax = () => {
|
||||
console.log('팩스 전송');
|
||||
};
|
||||
|
||||
const handleShareKakao = () => {
|
||||
console.log('카카오톡 공유');
|
||||
};
|
||||
|
||||
const renderDocument = () => {
|
||||
|
||||
@@ -546,32 +546,8 @@ export function DraftBox() {
|
||||
];
|
||||
},
|
||||
|
||||
headerActions: ({ selectedItems, onClearSelection }) => (
|
||||
headerActions: () => (
|
||||
<div className="ml-auto flex gap-2">
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
handleSubmit(selectedItems);
|
||||
onClearSelection();
|
||||
}}
|
||||
>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
상신
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
handleDelete(selectedItems);
|
||||
onClearSelection();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button variant="outline" onClick={handleSendNotification}>
|
||||
<Bell className="h-4 w-4 mr-2" />
|
||||
문서완료
|
||||
@@ -579,6 +555,33 @@ export function DraftBox() {
|
||||
</div>
|
||||
),
|
||||
|
||||
selectionActions: ({ selectedItems, onClearSelection }) => (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
handleSubmit(selectedItems);
|
||||
onClearSelection();
|
||||
}}
|
||||
>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
상신
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
handleDelete(selectedItems);
|
||||
onClearSelection();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
|
||||
@@ -499,19 +499,17 @@ export function ReferenceBox() {
|
||||
},
|
||||
filterTitle: '참조함 필터',
|
||||
|
||||
headerActions: ({ selectedItems: selected, onClearSelection }) => (
|
||||
selected.size > 0 ? (
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button variant="default" onClick={handleMarkReadClick}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
열람
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleMarkUnreadClick}>
|
||||
<EyeOff className="h-4 w-4 mr-2" />
|
||||
미열람
|
||||
</Button>
|
||||
</div>
|
||||
) : null
|
||||
selectionActions: () => (
|
||||
<>
|
||||
<Button size="sm" variant="default" onClick={handleMarkReadClick}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
열람
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleMarkUnreadClick}>
|
||||
<EyeOff className="h-4 w-4 mr-2" />
|
||||
미열람
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
|
||||
renderTableRow: (item, index, globalIndex, handlers) => {
|
||||
|
||||
@@ -58,7 +58,6 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
|
||||
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
|
||||
|
||||
console.log('[GoogleMap] API Key 존재:', !!apiKey);
|
||||
|
||||
if (!apiKey) {
|
||||
setError('구글맵 API 키가 설정되지 않았습니다.');
|
||||
@@ -67,14 +66,12 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
|
||||
// 이미 로드된 경우
|
||||
if (window.google && window.google.maps) {
|
||||
console.log('[GoogleMap] 이미 로드됨');
|
||||
setIsLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 로딩 중인 경우 (중복 로드 방지)
|
||||
if (window.googleMapsLoading) {
|
||||
console.log('[GoogleMap] 이미 로딩 중...');
|
||||
// 로딩 완료 대기
|
||||
const checkLoaded = setInterval(() => {
|
||||
if (window.google && window.google.maps) {
|
||||
@@ -88,7 +85,6 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
// 기존 스크립트 태그 확인 (중복 방지)
|
||||
const existingScript = document.querySelector('script[src*="maps.googleapis.com"]');
|
||||
if (existingScript) {
|
||||
console.log('[GoogleMap] 기존 스크립트 발견, 로드 대기');
|
||||
const checkLoaded = setInterval(() => {
|
||||
if (window.google && window.google.maps) {
|
||||
clearInterval(checkLoaded);
|
||||
@@ -103,7 +99,6 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
|
||||
// 콜백 함수 등록
|
||||
window.initGoogleMap = () => {
|
||||
console.log('[GoogleMap] 초기화 콜백 실행됨');
|
||||
window.googleMapsLoading = false;
|
||||
setIsLoaded(true);
|
||||
};
|
||||
@@ -120,7 +115,6 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
console.log('[GoogleMap] 스크립트 태그 추가됨');
|
||||
|
||||
return () => {
|
||||
// cleanup - 스크립트는 제거하지 않음 (다른 컴포넌트에서 사용 가능)
|
||||
@@ -135,7 +129,6 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
const lat = typeof siteLocation.lat === 'number' && !isNaN(siteLocation.lat) ? siteLocation.lat : 37.5458;
|
||||
const lng = typeof siteLocation.lng === 'number' && !isNaN(siteLocation.lng) ? siteLocation.lng : 126.8718;
|
||||
|
||||
console.log('[GoogleMap] 지도 초기화 시작, 좌표:', lat, lng);
|
||||
|
||||
const map = new window.google.maps.Map(mapRef.current, {
|
||||
center: { lat, lng },
|
||||
@@ -180,7 +173,6 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[GoogleMap] 지도 초기화 완료');
|
||||
}, [isLoaded, siteLocation]);
|
||||
|
||||
// GPS 위치 추적
|
||||
@@ -192,11 +184,9 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[GoogleMap] GPS 추적 시작');
|
||||
|
||||
// 위치 업데이트 핸들러
|
||||
const handlePositionUpdate = (latitude: number, longitude: number) => {
|
||||
console.log('[GoogleMap] 위치 업데이트:', latitude, longitude);
|
||||
|
||||
// 기존 마커 제거
|
||||
if (markerRef.current) {
|
||||
@@ -228,7 +218,6 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
);
|
||||
|
||||
const isInRange = distance <= siteLocation.radius;
|
||||
console.log('[GoogleMap] 거리:', Math.round(distance), 'm, 범위 내:', isInRange);
|
||||
|
||||
if (onDistanceChange) {
|
||||
onDistanceChange(distance, isInRange, { lat: latitude, lng: longitude });
|
||||
@@ -253,15 +242,9 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
|
||||
hostname === 'dev.codebridge-x.com' ||
|
||||
process.env.NODE_ENV === 'development';
|
||||
|
||||
console.log('[GoogleMap] 환경 체크:', {
|
||||
hostname,
|
||||
isDevelopment,
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
});
|
||||
|
||||
// 개발 환경에서 GPS 실패 시 테스트 위치로 시뮬레이션
|
||||
if (isDevelopment) {
|
||||
console.log('[GoogleMap] 🎯 개발 모드: 테스트 위치로 시뮬레이션 (본사 근처 50m)');
|
||||
// 본사 좌표에서 약간 떨어진 위치 (약 50m)
|
||||
const testLat = siteLocation.lat + 0.0003;
|
||||
const testLng = siteLocation.lng + 0.0003;
|
||||
|
||||
@@ -108,16 +108,9 @@ export function LoginPage() {
|
||||
};
|
||||
}
|
||||
|
||||
console.log('✅ 로그인 성공:', data.message);
|
||||
console.log('📦 사용자 정보:', data.user);
|
||||
console.log('📋 메뉴 정보 (API):', data.menus);
|
||||
console.log('👥 역할 정보:', data.roles);
|
||||
console.log('🏢 테넌트 정보:', data.tenant);
|
||||
console.log('🔐 토큰은 안전한 HttpOnly 쿠키에 저장됨 (JavaScript 접근 불가)');
|
||||
|
||||
// API 메뉴를 MenuItem 구조로 변환
|
||||
const transformedMenus = transformApiMenusToMenuItems(data.menus || []);
|
||||
console.log('🔄 변환된 메뉴 구조:', transformedMenus);
|
||||
|
||||
// 서버에서 받은 사용자 정보를 localStorage에 저장 (대시보드에서 사용)
|
||||
const userData = {
|
||||
@@ -129,7 +122,6 @@ export function LoginPage() {
|
||||
roles: data.roles || [],
|
||||
tenant: data.tenant || {},
|
||||
};
|
||||
console.log('💾 localStorage에 저장할 데이터:', userData);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
|
||||
// 메뉴 폴링 재시작 플래그 설정 (세션 만료 후 재로그인 시)
|
||||
|
||||
@@ -186,7 +186,6 @@ export function SignupPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Signup successful:', data);
|
||||
|
||||
// Navigate to login page after successful signup
|
||||
router.push("/login?registered=true");
|
||||
|
||||
@@ -175,7 +175,6 @@ export function CEODashboard() {
|
||||
|
||||
// 당월 예상 지출 클릭 (deprecated - 개별 카드 클릭으로 대체)
|
||||
const handleMonthlyExpenseClick = useCallback(() => {
|
||||
console.log('당월 예상 지출 클릭');
|
||||
}, []);
|
||||
|
||||
// 카드/가지급금 관리 카드 클릭 (개별 카드 클릭 시 상세 모달)
|
||||
@@ -250,7 +249,6 @@ export function CEODashboard() {
|
||||
color: string;
|
||||
content: string;
|
||||
}) => {
|
||||
console.log('일정 저장:', formData);
|
||||
// TODO: API 호출하여 일정 저장
|
||||
setIsScheduleModalOpen(false);
|
||||
setSelectedSchedule(null);
|
||||
@@ -258,7 +256,6 @@ export function CEODashboard() {
|
||||
|
||||
// 일정 삭제
|
||||
const handleScheduleDelete = useCallback((id: string) => {
|
||||
console.log('일정 삭제:', id);
|
||||
// TODO: API 호출하여 일정 삭제
|
||||
setIsScheduleModalOpen(false);
|
||||
setSelectedSchedule(null);
|
||||
|
||||
@@ -24,7 +24,6 @@ import { DetailPageSkeleton } from "@/components/ui/skeleton";
|
||||
*/
|
||||
|
||||
export function Dashboard() {
|
||||
console.log('🎨 CEO Dashboard component rendering...');
|
||||
return (
|
||||
<Suspense fallback={<DetailPageSkeleton />}>
|
||||
<CEODashboard />
|
||||
|
||||
@@ -10,7 +10,6 @@ import { DetailPageSkeleton } from "@/components/ui/skeleton";
|
||||
* 건설/공사 프로젝트 중심의 메트릭과 현황을 보여줍니다.
|
||||
*/
|
||||
export function ConstructionDashboard() {
|
||||
console.log('🏗️ Construction Dashboard rendering...');
|
||||
return (
|
||||
<Suspense fallback={<DetailPageSkeleton />}>
|
||||
<ConstructionMainDashboard />
|
||||
|
||||
@@ -103,7 +103,6 @@ export async function getBiddingList(filter?: BiddingFilter): Promise<{
|
||||
data?: BiddingListResponse;
|
||||
error?: string;
|
||||
}> {
|
||||
console.log('🔍 [getBiddingList] 시작, filter:', filter);
|
||||
|
||||
try {
|
||||
const queryParams: Record<string, string> = {};
|
||||
@@ -149,7 +148,6 @@ export async function getBiddingList(filter?: BiddingFilter): Promise<{
|
||||
if (filter?.page) queryParams.page = String(filter.page);
|
||||
if (filter?.size) queryParams.per_page = String(filter.size);
|
||||
|
||||
console.log('🔍 [getBiddingList] API 호출: /biddings, params:', queryParams);
|
||||
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
@@ -162,12 +160,10 @@ export async function getBiddingList(filter?: BiddingFilter): Promise<{
|
||||
};
|
||||
}>('/biddings', { params: queryParams });
|
||||
|
||||
console.log('✅ [getBiddingList] API 응답:', JSON.stringify(response, null, 2).slice(0, 500));
|
||||
|
||||
const paginatedData = response.data;
|
||||
const items = (paginatedData.data || []).map(transformBidding);
|
||||
|
||||
console.log('✅ [getBiddingList] 변환된 items 개수:', items.length);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -109,9 +109,6 @@ export default function EstimateDetailForm({
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 🔍 디버깅: 저장 전 formData 확인 (브라우저 콘솔)
|
||||
console.log('🔍 [handleConfirmSave] formData.detailItems:', formData.detailItems?.length, '개');
|
||||
console.log('🔍 [handleConfirmSave] formData.priceAdjustmentData:', formData.priceAdjustmentData);
|
||||
console.log('🔍 [handleConfirmSave] formData 전체:', formData);
|
||||
|
||||
// 현재 사용자 이름을 견적자로 설정하고 저장 (상태는 사용자 선택값 유지)
|
||||
const result = await updateEstimate(estimateId, {
|
||||
|
||||
@@ -738,14 +738,9 @@ export async function getEstimateDetail(id: string): Promise<{
|
||||
const response = await apiClient.get<{ success: boolean; data: ApiQuote }>(`/quotes/${id}`);
|
||||
|
||||
// 🔍 디버깅: 로드된 데이터 확인
|
||||
console.log('🔍 [getEstimateDetail] API response:', response);
|
||||
console.log('🔍 [getEstimateDetail] response.data:', response.data);
|
||||
console.log('🔍 [getEstimateDetail] response.data.options:', response.data?.options);
|
||||
|
||||
const transformed = transformQuoteToEstimateDetail(response.data);
|
||||
|
||||
console.log('✅ [getEstimateDetail] transformed.detailItems:', transformed.detailItems?.length, '개');
|
||||
console.log('✅ [getEstimateDetail] transformed.priceAdjustmentData:', transformed.priceAdjustmentData);
|
||||
|
||||
return { success: true, data: transformed };
|
||||
} catch (error) {
|
||||
@@ -830,15 +825,9 @@ export async function updateEstimate(
|
||||
const apiData = transformToApiRequest(data);
|
||||
|
||||
// 🔍 디버깅: 저장 데이터 확인
|
||||
console.log('🔍 [updateEstimate] formData.detailItems:', data.detailItems?.length, '개');
|
||||
console.log('🔍 [updateEstimate] formData.priceAdjustmentData:', data.priceAdjustmentData);
|
||||
console.log('🔍 [updateEstimate] apiData:', apiData);
|
||||
console.log('🔍 [updateEstimate] apiData.options:', (apiData as { options?: unknown }).options);
|
||||
|
||||
const response = await apiClient.put<ApiQuote>(`/quotes/${id}`, apiData);
|
||||
|
||||
console.log('✅ [updateEstimate] response:', response);
|
||||
console.log('✅ [updateEstimate] response.options:', response.options);
|
||||
|
||||
return { success: true, data: transformQuoteToEstimate(response) };
|
||||
} catch (error) {
|
||||
|
||||
@@ -174,9 +174,11 @@ export function EstimateDetailTableSection({
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle className="text-lg whitespace-nowrap">견적 상세</CardTitle>
|
||||
{!isViewMode && (
|
||||
<span className="text-sm text-muted-foreground">전체 {detailItems.length}건</span>
|
||||
{!isViewMode && selectedCount > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">{selectedCount}건 선택</span>
|
||||
<span className="text-sm text-muted-foreground">/</span>
|
||||
<span className="text-sm text-primary font-medium">{selectedCount}개 항목 선택됨</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
@@ -184,7 +186,7 @@ export function EstimateDetailTableSection({
|
||||
className="bg-gray-900 hover:bg-gray-800"
|
||||
onClick={onRemoveSelected}
|
||||
>
|
||||
삭제
|
||||
선택삭제
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -59,20 +59,21 @@ export function ExpenseDetailSection({
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle className="text-lg">공과 상세</CardTitle>
|
||||
{!isViewMode && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">{selectedCount}건 선택</span>
|
||||
<span className="text-sm text-muted-foreground">전체 {expenseItems.length}건</span>
|
||||
{!isViewMode && selectedCount > 0 && (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">/</span>
|
||||
<span className="text-sm text-primary font-medium">{selectedCount}개 항목 선택됨</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="bg-gray-900 hover:bg-gray-800"
|
||||
onClick={onRemoveSelected}
|
||||
disabled={selectedCount === 0}
|
||||
>
|
||||
삭제
|
||||
선택삭제
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isViewMode && (
|
||||
|
||||
@@ -360,7 +360,6 @@ export async function updateIssue(
|
||||
data: Partial<Issue>
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('Update issue:', id, data);
|
||||
// 실제 구현에서는 DB 업데이트
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -374,7 +373,6 @@ export async function createIssue(
|
||||
data: Omit<Issue, 'id' | 'createdAt' | 'updatedAt'>
|
||||
): Promise<{ success: boolean; data?: Issue; error?: string }> {
|
||||
try {
|
||||
console.log('Create issue:', data);
|
||||
const newIssue: Issue = {
|
||||
...data,
|
||||
id: `new-${Date.now()}`,
|
||||
@@ -393,7 +391,6 @@ export async function withdrawIssue(
|
||||
id: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('Withdraw issue:', id);
|
||||
// 실제 구현에서는 DB 상태 업데이트 (삭제가 아닌 철회 상태로 변경)
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -407,7 +404,6 @@ export async function withdrawIssues(
|
||||
ids: string[]
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('Withdraw issues:', ids);
|
||||
// 실제 구현에서는 DB 상태 일괄 업데이트
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -165,7 +165,6 @@ function transformItemToApi(data: ItemFormData): Record<string, unknown> {
|
||||
export async function getItemList(
|
||||
params: ItemListParams = {}
|
||||
): Promise<{ success: boolean; data?: ItemListResponse; error?: string }> {
|
||||
console.log('📥 [getItemList] 호출됨, params:', params);
|
||||
try {
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
@@ -191,7 +190,6 @@ export async function getItemList(
|
||||
queryParams.active = params.status === '사용' || params.status === '승인' ? '1' : '0';
|
||||
}
|
||||
|
||||
console.log('📤 [getItemList] API 호출:', queryParams);
|
||||
const response = await apiClient.get<{
|
||||
data: ApiItem[];
|
||||
meta?: { total: number; current_page: number; per_page: number };
|
||||
@@ -200,7 +198,6 @@ export async function getItemList(
|
||||
per_page?: number;
|
||||
last_page?: number;
|
||||
}>('/items', { params: queryParams });
|
||||
console.log('📥 [getItemList] API 응답:', JSON.stringify(response).slice(0, 500));
|
||||
|
||||
// API 응답 구조 처리
|
||||
// Laravel 페이지네이션 응답: { current_page, data: [...], last_page, per_page, total, ... }
|
||||
@@ -242,7 +239,6 @@ export async function getItemList(
|
||||
meta = { total: 0, current_page: 1, per_page: 20 };
|
||||
}
|
||||
|
||||
console.log('📊 [getItemList] 파싱된 items 수:', items.length, ', meta:', meta);
|
||||
|
||||
// Frontend 필터링 (Backend에서 지원하지 않는 필터)
|
||||
let transformedItems = items.map(transformItem);
|
||||
@@ -399,7 +395,6 @@ export async function getItem(id: string): Promise<{
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get<{ success: boolean; message: string; data: ApiItem }>(`/items/${id}`);
|
||||
console.log('📥 [getItem] API 응답:', JSON.stringify(response).slice(0, 300));
|
||||
|
||||
// API 응답 구조: { success, message, data: {...item...} }
|
||||
const item = response.data;
|
||||
@@ -425,11 +420,8 @@ export async function createItem(data: ItemFormData): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
console.log('📤 [createItem] 입력 데이터:', data);
|
||||
const apiData = transformItemToApi(data);
|
||||
console.log('📤 [createItem] 변환된 API 데이터:', apiData);
|
||||
const response = await apiClient.post<{ success: boolean; data: { id: number }; id?: number }>('/items', apiData);
|
||||
console.log('📥 [createItem] API 응답:', response);
|
||||
|
||||
// API 응답 구조: { success, message, data: { id } } 또는 { id }
|
||||
const itemId = response.data?.id ?? response.id;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { getPresetStyle } from '@/lib/utils/status-config';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -11,7 +12,7 @@ interface ProjectCardProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function ProjectCard({ project, isSelected, onClick }: ProjectCardProps) {
|
||||
const ProjectCard = memo(function ProjectCard({ project, isSelected, onClick }: ProjectCardProps) {
|
||||
// 상태 뱃지 색상
|
||||
const getStatusBadge = (status: ProjectStatus, hasUrgentIssue: boolean) => {
|
||||
if (hasUrgentIssue) {
|
||||
@@ -83,4 +84,6 @@ export default function ProjectCard({ project, isSelected, onClick }: ProjectCar
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ProjectCard;
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { getPresetStyle } from '@/lib/utils/status-config';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -12,7 +13,7 @@ interface StageCardProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function StageCard({ stage, isSelected, onClick }: StageCardProps) {
|
||||
const StageCard = memo(function StageCard({ stage, isSelected, onClick }: StageCardProps) {
|
||||
// 상태 뱃지 색상
|
||||
const getStatusBadge = (status: StageCardStatus) => {
|
||||
switch (status) {
|
||||
@@ -82,4 +83,6 @@ export default function StageCard({ stage, isSelected, onClick }: StageCardProps
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default StageCard;
|
||||
|
||||
@@ -421,7 +421,6 @@ export async function updateProject(
|
||||
data: Partial<Project>
|
||||
): Promise<{ success: boolean; data?: Project; error?: string }> {
|
||||
try {
|
||||
console.log('Update project:', id, data);
|
||||
|
||||
const existingProject = mockProjects.find((p) => p.id === id);
|
||||
if (!existingProject) {
|
||||
@@ -545,7 +544,6 @@ export async function updateProjectEnd(
|
||||
data: ProjectEndFormData
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('Project end:', data);
|
||||
|
||||
const project = mockProjects.find((p) => p.id === data.projectId);
|
||||
if (!project) {
|
||||
@@ -1039,7 +1037,6 @@ export async function deleteConstructionManagement(
|
||||
id: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('Delete construction management:', id);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('deleteConstructionManagement error:', error);
|
||||
@@ -1052,7 +1049,6 @@ export async function deleteConstructionManagements(
|
||||
ids: string[]
|
||||
): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
|
||||
try {
|
||||
console.log('Delete construction managements:', ids);
|
||||
return { success: true, deletedCount: ids.length };
|
||||
} catch (error) {
|
||||
console.error('deleteConstructionManagements error:', error);
|
||||
@@ -1199,7 +1195,6 @@ export async function updateConstructionManagementDetail(
|
||||
data: Partial<ConstructionDetailFormData>
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('Update construction detail:', id, data);
|
||||
// 실제 구현에서는 DB 업데이트
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -1213,7 +1208,6 @@ export async function completeConstruction(
|
||||
id: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('Complete construction:', id);
|
||||
// 실제 구현에서는 상태를 completed로 변경
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -74,21 +74,20 @@ export function OrderDetailItemTable({
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
{/* 왼쪽: 발주 상세, N건 선택, 삭제 */}
|
||||
{/* 왼쪽: 발주 상세, 전체 N건 / N개 선택됨 / 선택삭제 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle className="text-lg">발주 상세</CardTitle>
|
||||
{isEditMode && (
|
||||
<span className="text-sm text-muted-foreground">전체 {category.items.length}건</span>
|
||||
{isEditMode && selectedItems.size > 0 && (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{selectedItems.size}건 선택
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">/</span>
|
||||
<span className="text-sm text-primary font-medium">{selectedItems.size}개 항목 선택됨</span>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onDeleteSelectedItems}
|
||||
disabled={selectedItems.size === 0}
|
||||
>
|
||||
삭제
|
||||
선택삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -75,7 +75,6 @@ export default function ProgressBillingDetailForm({
|
||||
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
// TODO: API 호출
|
||||
console.log('Save billing data:', formData);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
toast.success('저장되었습니다.');
|
||||
router.push('/ko/construction/billing/progress-billing-management/' + billingId);
|
||||
|
||||
@@ -234,7 +234,6 @@ export async function saveProgressBilling(
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
console.log('Save progress billing:', { id, data });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -266,7 +265,6 @@ export async function deleteProgressBilling(id: string): Promise<{
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
console.log('Delete progress billing:', id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -296,11 +294,9 @@ export async function updateProgressBillingStatus(
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
console.log('Update progress billing status:', { id, status });
|
||||
|
||||
// 기성청구완료 시 매출 자동 등록 로직
|
||||
if (status === 'completed') {
|
||||
console.log('Auto-register sales for completed billing:', id);
|
||||
// TODO: 매출 자동 등록 API 호출
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,6 @@ export function useProgressBillingDetailForm({
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: API 호출
|
||||
console.log('Save billing data:', formData);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
setShowSaveDialog(false);
|
||||
router.push('/construction/billing/progress-billing-management/' + billingId);
|
||||
@@ -110,7 +109,6 @@ export function useProgressBillingDetailForm({
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: API 호출
|
||||
console.log('Delete billing:', billingId);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
setShowDeleteDialog(false);
|
||||
router.push('/construction/billing/progress-billing-management');
|
||||
|
||||
@@ -50,8 +50,14 @@ export function PhotoTable({
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-lg">사진대지</CardTitle>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
총 {items.length}건{selectedItems.size > 0 && ', ' + selectedItems.size + '건 선택'}
|
||||
전체 {items.length}건
|
||||
</span>
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">/</span>
|
||||
<span className="text-sm text-primary font-medium">{selectedItems.size}개 항목 선택됨</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isEditMode && selectedItems.size > 0 && (
|
||||
<Button size="sm" onClick={handleApply}>
|
||||
|
||||
@@ -58,8 +58,14 @@ export function ProgressBillingItemTable({
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-lg">기성청구 내역</CardTitle>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
총 {items.length}건{selectedItems.size > 0 && `, ${selectedItems.size}건 선택`}
|
||||
전체 {items.length}건
|
||||
</span>
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">/</span>
|
||||
<span className="text-sm text-primary font-medium">{selectedItems.size}개 항목 선택됨</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isEditMode && selectedItems.size > 0 && (
|
||||
<Button size="sm" onClick={handleApply}>
|
||||
|
||||
@@ -101,13 +101,10 @@ export default function SiteBriefingForm({ mode, briefingId, initialData }: Site
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
// DEBUG: 초기 데이터 확인
|
||||
console.log('[SiteBriefingForm] initialData:', initialData);
|
||||
console.log('[SiteBriefingForm] initialData.attendee:', initialData?.attendee);
|
||||
|
||||
// 폼 데이터
|
||||
const [formData, setFormData] = useState<SiteBriefingFormData>(() => {
|
||||
const data = initialData ? siteBriefingToFormData(initialData) : getEmptySiteBriefingFormData();
|
||||
console.log('[SiteBriefingForm] formData.attendeeItems:', data.attendeeItems);
|
||||
return data;
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { cn } from '@/components/ui/utils';
|
||||
import { formatDate, checkIsToday } from './utils';
|
||||
import { formatCalendarDate, checkIsToday } from './utils';
|
||||
import { EVENT_COLORS } from './types';
|
||||
import type { DayTimeViewProps, ScheduleEvent } from './types';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
@@ -22,7 +22,7 @@ export function DayTimeView({
|
||||
onEventClick,
|
||||
}: DayTimeViewProps) {
|
||||
const today = checkIsToday(currentDate);
|
||||
const dayStr = formatDate(currentDate, 'yyyy-MM-dd');
|
||||
const dayStr = formatCalendarDate(currentDate, 'yyyy-MM-dd');
|
||||
const weekdayLabel = format(currentDate, 'EEEE', { locale: ko });
|
||||
|
||||
// 시간 슬롯 생성
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { cn } from '@/components/ui/utils';
|
||||
import { getWeekDays, getWeekdayHeaders, formatDate, checkIsToday, isSameDate } from './utils';
|
||||
import { getWeekDays, getWeekdayHeaders, formatCalendarDate, checkIsToday, isSameDate } from './utils';
|
||||
import { EVENT_COLORS } from './types';
|
||||
import type { WeekTimeViewProps, ScheduleEvent } from './types';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
@@ -42,7 +42,7 @@ export function WeekTimeView({
|
||||
const map = new Map<string, { allDay: ScheduleEvent[]; timed: ScheduleEvent[] }>();
|
||||
|
||||
weekDays.forEach((day) => {
|
||||
const dateStr = formatDate(day, 'yyyy-MM-dd');
|
||||
const dateStr = formatCalendarDate(day, 'yyyy-MM-dd');
|
||||
map.set(dateStr, { allDay: [], timed: [] });
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ export function WeekTimeView({
|
||||
const eventEndDate = parseISO(event.endDate);
|
||||
|
||||
weekDays.forEach((day) => {
|
||||
const dateStr = formatDate(day, 'yyyy-MM-dd');
|
||||
const dateStr = formatCalendarDate(day, 'yyyy-MM-dd');
|
||||
// 이벤트가 이 날짜를 포함하는지 확인
|
||||
if (day >= eventStartDate && day <= eventEndDate) {
|
||||
const bucket = map.get(dateStr);
|
||||
@@ -134,7 +134,7 @@ export function WeekTimeView({
|
||||
<span className="text-[10px] text-muted-foreground">종일</span>
|
||||
</div>
|
||||
{weekDays.map((day, i) => {
|
||||
const dateStr = formatDate(day, 'yyyy-MM-dd');
|
||||
const dateStr = formatCalendarDate(day, 'yyyy-MM-dd');
|
||||
const allDayEvents = eventsByDate.get(dateStr)?.allDay || [];
|
||||
return (
|
||||
<div
|
||||
@@ -174,7 +174,7 @@ export function WeekTimeView({
|
||||
</div>
|
||||
{/* 요일별 셀 */}
|
||||
{weekDays.map((day, i) => {
|
||||
const dateStr = formatDate(day, 'yyyy-MM-dd');
|
||||
const dateStr = formatCalendarDate(day, 'yyyy-MM-dd');
|
||||
const timedEvents = eventsByDate.get(dateStr)?.timed || [];
|
||||
const slotEvents = timedEvents.filter((event) => {
|
||||
if (!event.startTime) return false;
|
||||
|
||||
@@ -93,7 +93,7 @@ export function isSameDate(date1: Date | null, date2: Date): boolean {
|
||||
/**
|
||||
* 날짜 포맷
|
||||
*/
|
||||
export function formatDate(date: Date, formatStr: string = 'yyyy-MM-dd'): string {
|
||||
export function formatCalendarDate(date: Date, formatStr: string = 'yyyy-MM-dd'): string {
|
||||
return format(date, formatStr, { locale: ko });
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +348,6 @@ export function AttendanceManagement() {
|
||||
}, [attendanceDialogMode, selectedAttendance]);
|
||||
|
||||
const handleSubmitReason = useCallback((data: ReasonFormData) => {
|
||||
console.log('Submit reason:', data);
|
||||
// 문서 작성 화면으로 이동
|
||||
router.push(`/ko/hr/documents/new?type=${data.reasonType}`);
|
||||
}, [router]);
|
||||
|
||||
@@ -32,21 +32,28 @@ export function DepartmentToolbar({
|
||||
{/* 선택 카운트 + 버튼 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
총 {totalCount}건 {selectedCount > 0 && `| ${selectedCount}건 선택`}
|
||||
전체 {totalCount}건
|
||||
</span>
|
||||
{selectedCount > 0 && (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">/</span>
|
||||
<span className="text-sm text-primary font-medium whitespace-nowrap">{selectedCount}개 항목 선택됨</span>
|
||||
</>
|
||||
)}
|
||||
{selectedCount > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
선택삭제
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" onClick={onAdd}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={onDelete}
|
||||
disabled={selectedCount === 0}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronRight, ChevronDown, Plus, Pencil, Trash2 } from 'lucide-react';
|
||||
@@ -10,7 +11,7 @@ import type { DepartmentTreeItemProps } from './types';
|
||||
* - 무제한 깊이 지원
|
||||
* - depth에 따른 동적 들여쓰기
|
||||
*/
|
||||
export function DepartmentTreeItem({
|
||||
export const DepartmentTreeItem = memo(function DepartmentTreeItem({
|
||||
department,
|
||||
depth,
|
||||
expandedIds,
|
||||
@@ -114,4 +115,4 @@ export function DepartmentTreeItem({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -725,7 +725,6 @@ export function EmployeeManagement() {
|
||||
open={userInviteOpen}
|
||||
onOpenChange={setUserInviteOpen}
|
||||
onInvite={(data) => {
|
||||
console.log('[EmployeeManagement] Invite user - API 미구현:', data);
|
||||
setUserInviteOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -397,38 +397,39 @@ export function SalaryManagement() {
|
||||
onEndDateChange: setEndDate,
|
||||
},
|
||||
|
||||
headerActions: ({ selectedItems: selected }) => (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 지급완료/지급예정 버튼 - 선택된 항목이 있을 때만 표시 */}
|
||||
{selected.size > 0 && (
|
||||
<>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleMarkCompleted}
|
||||
disabled={isActionLoading}
|
||||
>
|
||||
{isActionLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
지급완료
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleMarkScheduled}
|
||||
disabled={isActionLoading}
|
||||
>
|
||||
{isActionLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Clock className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
지급예정
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
selectionActions: () => (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={handleMarkCompleted}
|
||||
disabled={isActionLoading}
|
||||
>
|
||||
{isActionLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
지급완료
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleMarkScheduled}
|
||||
disabled={isActionLoading}
|
||||
>
|
||||
{isActionLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Clock className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
지급예정
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
|
||||
headerActions: () => (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{canExport && (
|
||||
<Button variant="outline" onClick={() => toast.info('엑셀 다운로드 기능은 준비 중입니다.')}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
|
||||
@@ -596,40 +596,23 @@ export function VacationManagement() {
|
||||
}
|
||||
}, [mainTab, handleApproveClick, handleRejectClick]);
|
||||
|
||||
// ===== 헤더 액션 (탭별 버튼들만 - DateRangeSelector와 검색창은 공통 옵션 사용) =====
|
||||
const headerActions = useCallback(({ selectedItems: selected }: { selectedItems: Set<string>; onClearSelection?: () => void; onRefresh?: () => void }) => (
|
||||
// ===== 헤더 액션 (탭별 버튼들만 - 선택 액션은 selectionActions로 분리) =====
|
||||
const headerActions = useCallback(() => (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 탭별 액션 버튼 */}
|
||||
{mainTab === 'grant' && (
|
||||
<Button onClick={() => setGrantDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
부여등록
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{mainTab === 'request' && (
|
||||
<>
|
||||
{/* 버튼 순서: 승인 → 거절 → 휴가신청 (휴가신청 버튼 위치 고정) */}
|
||||
{selected.size > 0 && (
|
||||
<>
|
||||
<Button variant="default" onClick={() => handleApproveClick(selected)}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => handleRejectClick(selected)}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
거절
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={() => setRequestDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
휴가신청
|
||||
</Button>
|
||||
</>
|
||||
<Button onClick={() => setRequestDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
휴가신청
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
), [mainTab, handleApproveClick, handleRejectClick]);
|
||||
), [mainTab]);
|
||||
|
||||
// ===== filterConfig 기반 통합 필터 시스템 =====
|
||||
const filterConfig: FilterFieldConfig[] = useMemo(() => [
|
||||
@@ -729,6 +712,22 @@ export function VacationManagement() {
|
||||
|
||||
headerActions: headerActions,
|
||||
|
||||
selectionActions: ({ selectedItems: selected }) => {
|
||||
if (mainTab !== 'request') return null;
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="default" onClick={() => handleApproveClick(selected)}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" onClick={() => handleRejectClick(selected)}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
거절
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
renderTableRow: renderTableRow,
|
||||
|
||||
renderMobileCard: renderMobileCard,
|
||||
@@ -827,6 +826,8 @@ export function VacationManagement() {
|
||||
statCards,
|
||||
filterConfig,
|
||||
headerActions,
|
||||
handleApproveClick,
|
||||
handleRejectClick,
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
grantDialogOpen,
|
||||
|
||||
@@ -101,8 +101,7 @@ export function useFieldDetection({
|
||||
// "구매 부품", "PURCHASED", "구매부품" 등 다양한 형태 지원
|
||||
const isPurchased = currentPartType.includes('구매') || currentPartType.toUpperCase() === 'PURCHASED';
|
||||
|
||||
// console.log('[useFieldDetection] 부품 유형 감지:', { partTypeFieldKey: foundPartTypeKey, currentPartType, isBending, isAssembly, isPurchased });
|
||||
|
||||
//
|
||||
return {
|
||||
partTypeFieldKey: foundPartTypeKey,
|
||||
selectedPartType: currentPartType,
|
||||
@@ -133,8 +132,7 @@ export function useFieldDetection({
|
||||
fieldKey.includes('부품구성');
|
||||
|
||||
if (isCheckbox && isBomRelated) {
|
||||
// console.log('[useFieldDetection] BOM 체크박스 필드 발견:', { fieldKey, fieldName });
|
||||
return field.field_key || `field_${field.id}`;
|
||||
// return field.field_key || `field_${field.id}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,12 +152,10 @@ export function useFieldDetection({
|
||||
fieldKey.includes('부품구성');
|
||||
|
||||
if (isCheckbox && isBomRelated) {
|
||||
// console.log('[useFieldDetection] BOM 체크박스 필드 발견 (직접필드):', { fieldKey, fieldName });
|
||||
return field.field_key || `field_${field.id}`;
|
||||
// return field.field_key || `field_${field.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('[useFieldDetection] BOM 체크박스 필드를 찾지 못함');
|
||||
return '';
|
||||
}, [structure]);
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ export function useFileHandling({
|
||||
if (typeof filesRaw === 'string') {
|
||||
try {
|
||||
filesRaw = JSON.parse(filesRaw);
|
||||
console.log('[useFileHandling] files JSON 문자열 파싱 완료');
|
||||
} catch (e) {
|
||||
console.error('[useFileHandling] files JSON 파싱 실패:', e);
|
||||
filesRaw = undefined;
|
||||
@@ -125,12 +124,6 @@ export function useFileHandling({
|
||||
const files = filesRaw as FilesObject | undefined;
|
||||
|
||||
// 2025-12-15: 파일 로드 디버깅
|
||||
console.log('[useFileHandling] 파일 로드 시작');
|
||||
console.log('[useFileHandling] initialData.files (raw):', initialData.files);
|
||||
console.log('[useFileHandling] filesRaw 타입:', typeof filesRaw);
|
||||
console.log('[useFileHandling] files 변수:', files);
|
||||
console.log('[useFileHandling] specification_file:', files?.specification_file);
|
||||
console.log('[useFileHandling] certification_file:', files?.certification_file);
|
||||
|
||||
// 전개도 파일 (배열의 마지막 파일 = 최신 파일을 가져옴)
|
||||
const bendingFileArr = files?.bending_diagram;
|
||||
@@ -138,12 +131,9 @@ export function useFileHandling({
|
||||
? bendingFileArr[bendingFileArr.length - 1]
|
||||
: undefined;
|
||||
if (bendingFile) {
|
||||
console.log('[useFileHandling] bendingFile 전체 객체:', bendingFile);
|
||||
console.log('[useFileHandling] bendingFile 키 목록:', Object.keys(bendingFile));
|
||||
setExistingBendingDiagram(bendingFile.file_path);
|
||||
// API에서 id 또는 file_id로 올 수 있음
|
||||
const bendingFileId = bendingFile.id || bendingFile.file_id;
|
||||
console.log('[useFileHandling] bendingFile ID 추출:', { id: bendingFile.id, file_id: bendingFile.file_id, final: bendingFileId });
|
||||
setExistingBendingDiagramFileId(bendingFileId as number);
|
||||
} else if (initialData.bending_diagram) {
|
||||
setExistingBendingDiagram(initialData.bending_diagram as string);
|
||||
@@ -154,14 +144,11 @@ export function useFileHandling({
|
||||
const specFile = specFileArr && specFileArr.length > 0
|
||||
? specFileArr[specFileArr.length - 1]
|
||||
: undefined;
|
||||
console.log('[useFileHandling] specFile 전체 객체:', specFile);
|
||||
console.log('[useFileHandling] specFile 키 목록:', specFile ? Object.keys(specFile) : 'undefined');
|
||||
if (specFile?.file_path) {
|
||||
setExistingSpecificationFile(specFile.file_path);
|
||||
setExistingSpecificationFileName(specFile.file_name || '시방서');
|
||||
// API에서 id 또는 file_id로 올 수 있음
|
||||
const specFileId = specFile.id || specFile.file_id;
|
||||
console.log('[useFileHandling] specFile ID 추출:', { id: specFile.id, file_id: specFile.file_id, final: specFileId });
|
||||
setExistingSpecificationFileId(specFileId as number || null);
|
||||
} else {
|
||||
// 파일이 없으면 상태 초기화 (이전 값 제거)
|
||||
@@ -175,14 +162,11 @@ export function useFileHandling({
|
||||
const certFile = certFileArr && certFileArr.length > 0
|
||||
? certFileArr[certFileArr.length - 1]
|
||||
: undefined;
|
||||
console.log('[useFileHandling] certFile 전체 객체:', certFile);
|
||||
console.log('[useFileHandling] certFile 키 목록:', certFile ? Object.keys(certFile) : 'undefined');
|
||||
if (certFile?.file_path) {
|
||||
setExistingCertificationFile(certFile.file_path);
|
||||
setExistingCertificationFileName(certFile.file_name || '인정서');
|
||||
// API에서 id 또는 file_id로 올 수 있음
|
||||
const certFileId = certFile.id || certFile.file_id;
|
||||
console.log('[useFileHandling] certFile ID 추출:', { id: certFile.id, file_id: certFile.file_id, final: certFileId });
|
||||
setExistingCertificationFileId(certFileId as number || null);
|
||||
} else {
|
||||
// 파일이 없으면 상태 초기화 (이전 값 제거)
|
||||
@@ -244,13 +228,6 @@ export function useFileHandling({
|
||||
onBendingDiagramDeleted?: () => void;
|
||||
}
|
||||
) => {
|
||||
console.log('[useFileHandling] handleDeleteFile 호출:', {
|
||||
fileType,
|
||||
propItemId,
|
||||
existingBendingDiagramFileId,
|
||||
existingSpecificationFileId,
|
||||
existingCertificationFileId,
|
||||
});
|
||||
|
||||
if (!propItemId) {
|
||||
console.error('[useFileHandling] propItemId가 없습니다');
|
||||
@@ -267,7 +244,6 @@ export function useFileHandling({
|
||||
fileId = existingCertificationFileId;
|
||||
}
|
||||
|
||||
console.log('[useFileHandling] 삭제할 파일 ID:', fileId);
|
||||
|
||||
if (!fileId) {
|
||||
console.error('[useFileHandling] 파일 ID를 찾을 수 없습니다:', fileType);
|
||||
|
||||
@@ -34,12 +34,10 @@ export function useFormStructure(
|
||||
const initData = await itemMasterApi.init();
|
||||
|
||||
// 단위 옵션 저장 (SimpleUnitOption 형식으로 변환)
|
||||
console.log('[useFormStructure] API initData.unitOptions:', initData.unitOptions);
|
||||
const simpleUnitOptions: SimpleUnitOption[] = (initData.unitOptions || []).map((u) => ({
|
||||
label: u.unit_name,
|
||||
value: u.unit_code,
|
||||
}));
|
||||
console.log('[useFormStructure] Processed unitOptions:', simpleUnitOptions.length, 'items');
|
||||
setUnitOptions(simpleUnitOptions);
|
||||
|
||||
// 2. 품목 유형에 해당하는 페이지 찾기
|
||||
|
||||
@@ -74,8 +74,7 @@ export function usePartTypeHandling({
|
||||
|
||||
// 이전 값이 있고, 현재 값과 다른 경우에만 초기화
|
||||
if (prevPartType && prevPartType !== currentPartType) {
|
||||
// console.log('[usePartTypeHandling] 부품 유형 변경 감지:', prevPartType, '→', currentPartType);
|
||||
|
||||
//
|
||||
// setTimeout으로 다음 틱에서 초기화 실행
|
||||
// → 부품 유형 Select 값 변경이 먼저 완료된 후 초기화
|
||||
setTimeout(() => {
|
||||
@@ -121,8 +120,7 @@ export function usePartTypeHandling({
|
||||
|
||||
// 중복 제거 후 초기화
|
||||
const uniqueFields = [...new Set(fieldsToReset)];
|
||||
// console.log('[usePartTypeHandling] 초기화할 필드:', uniqueFields);
|
||||
|
||||
//
|
||||
uniqueFields.forEach((fieldKey) => {
|
||||
setFieldValue(fieldKey, '');
|
||||
});
|
||||
@@ -152,11 +150,6 @@ export function usePartTypeHandling({
|
||||
}, 0);
|
||||
|
||||
const sumString = totalSum.toString();
|
||||
console.log('[usePartTypeHandling] bendingDetails 폭 합계 → formData 동기화:', {
|
||||
widthSumKey: bendingFieldKeys.widthSum,
|
||||
totalSum,
|
||||
bendingDetailsCount: bendingDetails.length,
|
||||
});
|
||||
|
||||
setFieldValue(bendingFieldKeys.widthSum, sumString);
|
||||
bendingWidthSumSyncedRef.current = true;
|
||||
@@ -175,14 +168,12 @@ export function usePartTypeHandling({
|
||||
|
||||
// 품목명이 변경되었고, 이전 값이 있었을 때만 종류 필드 초기화
|
||||
if (prevItemNameValue && prevItemNameValue !== currentItemNameValue) {
|
||||
// console.log('[usePartTypeHandling] 품목명 변경 감지:', prevItemNameValue, '→', currentItemNameValue);
|
||||
|
||||
//
|
||||
// 모든 종류 필드 값 초기화
|
||||
allCategoryKeysWithIds.forEach(({ key }) => {
|
||||
const currentVal = (formData[key] as string) || '';
|
||||
if (currentVal) {
|
||||
// console.log('[usePartTypeHandling] 종류 필드 초기화:', key);
|
||||
setFieldValue(key, '');
|
||||
// setFieldValue(key, '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,7 +129,6 @@ export default function DynamicItemForm({
|
||||
useEffect(() => {
|
||||
if (mode === 'edit' && initialBomLines && initialBomLines.length > 0) {
|
||||
setBomLines(initialBomLines);
|
||||
console.log('[DynamicItemForm] initialBomLines로 BOM 데이터 로드:', initialBomLines.length, '건');
|
||||
}
|
||||
}, [mode, initialBomLines]);
|
||||
|
||||
@@ -159,7 +158,6 @@ export default function DynamicItemForm({
|
||||
.map((item: { code?: string; item_code?: string }) => item.code || item.item_code || '')
|
||||
.filter((code: string) => code);
|
||||
setExistingItemCodes(codes);
|
||||
// console.log('[DynamicItemForm] PT 기존 품목코드 로드:', codes.length, '개');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[DynamicItemForm] PT 품목코드 조회 실패:', err);
|
||||
@@ -213,18 +211,9 @@ export default function DynamicItemForm({
|
||||
const [isEditDataMapped, setIsEditDataMapped] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[DynamicItemForm] Edit useEffect 체크:', {
|
||||
mode,
|
||||
hasStructure: !!structure,
|
||||
hasInitialData: !!initialData,
|
||||
isEditDataMapped,
|
||||
structureSections: structure?.sections?.length,
|
||||
});
|
||||
|
||||
if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return;
|
||||
|
||||
console.log('[DynamicItemForm] Edit mode: initialData 직접 로드 (field_key 통일됨)');
|
||||
console.log('[DynamicItemForm] initialData:', initialData);
|
||||
|
||||
// structure의 field_key들 확인
|
||||
const fieldKeys: string[] = [];
|
||||
@@ -233,8 +222,6 @@ export default function DynamicItemForm({
|
||||
fieldKeys.push(f.field.field_key || `field_${f.field.id}`);
|
||||
});
|
||||
});
|
||||
console.log('[DynamicItemForm] structure field_keys:', fieldKeys);
|
||||
console.log('[DynamicItemForm] initialData keys:', Object.keys(initialData));
|
||||
|
||||
// field_key가 통일되었으므로 initialData를 그대로 사용
|
||||
// 기존 레거시 데이터(98_unit 형식)도 그대로 동작
|
||||
@@ -348,7 +335,6 @@ export default function DynamicItemForm({
|
||||
// PT (절곡/조립) 전개도 이미지 업로드
|
||||
if (selectedItemType === 'PT' && (isBendingPart || isAssemblyPart) && bendingDiagramFile) {
|
||||
try {
|
||||
console.log('[DynamicItemForm] 전개도 파일 업로드 시작:', bendingDiagramFile.name);
|
||||
await uploadItemFile(itemId, bendingDiagramFile, 'bending_diagram', {
|
||||
fieldKey: 'bending_diagram',
|
||||
// 수정 모드: 기존 파일 ID가 있으면 덮어쓰기, 없으면 새 파일 등록
|
||||
@@ -359,7 +345,6 @@ export default function DynamicItemForm({
|
||||
type: d.shaded ? 'shaded' : 'normal',
|
||||
})) : undefined,
|
||||
});
|
||||
console.log('[DynamicItemForm] 전개도 파일 업로드 성공');
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicItemForm] 전개도 파일 업로드 실패:', error);
|
||||
@@ -370,13 +355,11 @@ export default function DynamicItemForm({
|
||||
// FG (제품) 시방서 업로드
|
||||
if (selectedItemType === 'FG' && specificationFile) {
|
||||
try {
|
||||
console.log('[DynamicItemForm] 시방서 파일 업로드 시작:', specificationFile.name);
|
||||
await uploadItemFile(itemId, specificationFile, 'specification', {
|
||||
fieldKey: 'specification_file',
|
||||
// 수정 모드: 기존 파일 ID가 있으면 덮어쓰기, 없으면 새 파일 등록
|
||||
fileId: existingSpecificationFileId ?? undefined,
|
||||
});
|
||||
console.log('[DynamicItemForm] 시방서 파일 업로드 성공');
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicItemForm] 시방서 파일 업로드 실패:', error);
|
||||
@@ -387,7 +370,6 @@ export default function DynamicItemForm({
|
||||
// FG (제품) 인정서 업로드
|
||||
if (selectedItemType === 'FG' && certificationFile) {
|
||||
try {
|
||||
console.log('[DynamicItemForm] 인정서 파일 업로드 시작:', certificationFile.name);
|
||||
// formData에서 인정서 관련 필드 추출
|
||||
const certNumber = Object.entries(formData).find(([key]) =>
|
||||
key.includes('certification_number') || key.includes('인정번호')
|
||||
@@ -407,7 +389,6 @@ export default function DynamicItemForm({
|
||||
certificationStartDate: certStartDate,
|
||||
certificationEndDate: certEndDate,
|
||||
});
|
||||
console.log('[DynamicItemForm] 인정서 파일 업로드 성공');
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicItemForm] 인정서 파일 업로드 실패:', error);
|
||||
@@ -457,7 +438,6 @@ export default function DynamicItemForm({
|
||||
|
||||
// 2025-12-09: field_key 통일로 변환 로직 제거
|
||||
// formData의 field_key가 백엔드 필드명과 일치하므로 직접 사용
|
||||
console.log('[DynamicItemForm] 저장 시 formData:', formData);
|
||||
|
||||
// is_active 필드만 boolean 변환 (드롭다운 값 → boolean)
|
||||
const convertedData: Record<string, any> = {};
|
||||
@@ -509,8 +489,7 @@ export default function DynamicItemForm({
|
||||
finalSpec = convertedData.spec;
|
||||
}
|
||||
|
||||
// console.log('[DynamicItemForm] 품목명/규격 결정:', { finalName, finalSpec });
|
||||
|
||||
//
|
||||
// 품목코드 결정
|
||||
// 2025-12-11: 수정 모드에서는 기존 코드 유지 (자동생성으로 코드가 변경되는 버그 수정)
|
||||
// 생성 모드에서만 자동생성 코드 사용
|
||||
@@ -569,20 +548,17 @@ export default function DynamicItemForm({
|
||||
} : {}),
|
||||
} as DynamicFormData;
|
||||
|
||||
// console.log('[DynamicItemForm] 제출 데이터:', submitData);
|
||||
|
||||
//
|
||||
// 2025-12-11: 품목코드 중복 체크 (조립/절곡 부품만 해당)
|
||||
// PT-조립부품, PT-절곡부품은 품목코드가 자동생성되므로 중복 체크 필요
|
||||
const needsDuplicateCheck = selectedItemType === 'PT' && (isAssemblyPart || isBendingPart) && finalCode;
|
||||
|
||||
if (needsDuplicateCheck) {
|
||||
console.log('[DynamicItemForm] 품목코드 중복 체크:', finalCode);
|
||||
|
||||
// 수정 모드에서는 자기 자신 제외 (propItemId)
|
||||
const excludeId = mode === 'edit' ? propItemId : undefined;
|
||||
const duplicateResult = await checkItemCodeDuplicate(finalCode, excludeId);
|
||||
|
||||
console.log('[DynamicItemForm] 중복 체크 결과:', duplicateResult);
|
||||
|
||||
if (duplicateResult.isDuplicate) {
|
||||
// 중복 발견 → 다이얼로그 표시
|
||||
@@ -983,8 +959,7 @@ export default function DynamicItemForm({
|
||||
const isBomRequired = bomValue === true || bomValue === 'true' || bomValue === '1' || bomValue === 1;
|
||||
|
||||
// 디버깅 로그
|
||||
// console.log('[DynamicItemForm] BOM 체크 디버깅:', { bomRequiredFieldKey, bomValue, isBomRequired });
|
||||
|
||||
//
|
||||
if (!isBomRequired) return null;
|
||||
|
||||
return (
|
||||
@@ -1021,7 +996,6 @@ export default function DynamicItemForm({
|
||||
const blob = new Blob([uint8Array], { type: mimeType });
|
||||
const file = new File([blob], `bending_diagram_${Date.now()}.png`, { type: mimeType });
|
||||
setBendingDiagramFile(file);
|
||||
console.log('[DynamicItemForm] 드로잉 캔버스 → File 변환 성공:', file.name);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicItemForm] 드로잉 캔버스 → File 변환 실패:', error);
|
||||
|
||||
@@ -120,7 +120,6 @@ function mapApiResponseToFormData(data: ItemApiResponse): DynamicFormData {
|
||||
formData['is_active'] = true;
|
||||
}
|
||||
|
||||
console.log('[ItemDetailEdit] mapApiResponseToFormData 결과:', formData);
|
||||
|
||||
return formData;
|
||||
}
|
||||
@@ -166,7 +165,6 @@ export function ItemDetailEdit({ itemCode, itemType: urlItemType, itemId: urlIte
|
||||
queryParams.append('include_bom', 'true');
|
||||
}
|
||||
|
||||
console.log('[ItemDetailEdit] Fetching:', { urlItemId, urlItemType, isMaterial });
|
||||
const queryString = queryParams.toString();
|
||||
const response = await fetch(`/api/proxy/items/${urlItemId}${queryString ? `?${queryString}` : ''}`);
|
||||
|
||||
@@ -185,15 +183,6 @@ export function ItemDetailEdit({ itemCode, itemType: urlItemType, itemId: urlIte
|
||||
|
||||
if (result.success && result.data) {
|
||||
const apiData = result.data as ItemApiResponse;
|
||||
console.log('========== [ItemDetailEdit] API 원본 데이터 (백엔드 응답) ==========');
|
||||
console.log('id:', apiData.id);
|
||||
console.log('specification:', apiData.specification);
|
||||
console.log('unit:', apiData.unit);
|
||||
console.log('is_active:', apiData.is_active);
|
||||
console.log('files:', (apiData as any).files); // 파일 데이터 확인
|
||||
console.log('전체:', apiData);
|
||||
console.log('==============================================================');
|
||||
|
||||
// ID, 품목 유형 저장
|
||||
// Product: product_type, Material: material_type 또는 type_code
|
||||
setItemId(apiData.id);
|
||||
@@ -202,13 +191,6 @@ export function ItemDetailEdit({ itemCode, itemType: urlItemType, itemId: urlIte
|
||||
|
||||
// 폼 데이터로 변환
|
||||
const formData = mapApiResponseToFormData(apiData);
|
||||
console.log('========== [ItemDetailEdit] 폼에 전달되는 initialData ==========');
|
||||
console.log('specification:', formData['specification']);
|
||||
console.log('unit:', formData['unit']);
|
||||
console.log('is_active:', formData['is_active']);
|
||||
console.log('files:', formData['files']); // 파일 데이터 확인
|
||||
console.log('전체:', formData);
|
||||
console.log('==========================================================');
|
||||
setInitialData(formData);
|
||||
|
||||
// BOM 데이터 별도 API 호출 (expandBomItems로 품목 정보 포함)
|
||||
@@ -238,7 +220,6 @@ export function ItemDetailEdit({ itemCode, itemType: urlItemType, itemId: urlIte
|
||||
}));
|
||||
|
||||
setInitialBomLines(mappedBomLines);
|
||||
console.log('[ItemDetailEdit] BOM 데이터 로드 (expanded):', mappedBomLines.length, '건', mappedBomLines);
|
||||
}
|
||||
} catch (bomErr) {
|
||||
console.error('[ItemDetailEdit] BOM 조회 실패:', bomErr);
|
||||
@@ -328,14 +309,6 @@ export function ItemDetailEdit({ itemCode, itemType: urlItemType, itemId: urlIte
|
||||
}
|
||||
|
||||
// API 호출
|
||||
console.log('========== [ItemDetailEdit] 수정 요청 데이터 ==========');
|
||||
console.log('URL:', updateUrl);
|
||||
console.log('Method:', method);
|
||||
console.log('specification:', submitData.specification);
|
||||
console.log('unit:', submitData.unit);
|
||||
console.log('is_active:', submitData.is_active);
|
||||
console.log('전체:', submitData);
|
||||
console.log('=================================================');
|
||||
|
||||
const response = await fetch(updateUrl, {
|
||||
method,
|
||||
|
||||
@@ -26,9 +26,6 @@ function mapApiResponseToItemMaster(data: Record<string, unknown>): ItemMaster {
|
||||
// details 객체 추출 (PT 부품의 상세 정보가 여기에 저장됨)
|
||||
const details = (data.details || {}) as Record<string, unknown>;
|
||||
|
||||
console.log('[mapApiResponseToItemMaster] data.details:', data.details);
|
||||
console.log('[mapApiResponseToItemMaster] details.part_type:', details.part_type);
|
||||
console.log('[mapApiResponseToItemMaster] details.bending_details:', details.bending_details);
|
||||
|
||||
return {
|
||||
id: String(data.id || ''),
|
||||
@@ -169,7 +166,6 @@ export function ItemDetailView({ itemCode, itemType, itemId }: ItemDetailViewPro
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
console.log('[ItemDetailView] Fetching item:', { itemCode, itemType, itemId });
|
||||
|
||||
// 모든 품목: GET /api/proxy/items/{id} (id 기반 통일)
|
||||
if (!itemId) {
|
||||
@@ -185,7 +181,6 @@ export function ItemDetailView({ itemCode, itemType, itemId }: ItemDetailViewPro
|
||||
queryParams.append('include_bom', 'true');
|
||||
}
|
||||
|
||||
console.log('[ItemDetailView] Fetching:', { itemId, itemType, isMaterial });
|
||||
const queryString = queryParams.toString();
|
||||
const response = await fetch(`/api/proxy/items/${itemId}${queryString ? `?${queryString}` : ''}`);
|
||||
|
||||
@@ -201,7 +196,6 @@ export function ItemDetailView({ itemCode, itemType, itemId }: ItemDetailViewPro
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ItemDetailView] API Response:', result);
|
||||
|
||||
if (result.success && result.data) {
|
||||
let mappedItem = mapApiResponseToItemMaster(result.data);
|
||||
@@ -229,7 +223,6 @@ export function ItemDetailView({ itemCode, itemType, itemId }: ItemDetailViewPro
|
||||
isBending: Boolean(bomItem.is_bending ?? false),
|
||||
})),
|
||||
};
|
||||
console.log('[ItemDetailView] BOM 데이터 로드 (expanded):', mappedItem.bom?.length, '건');
|
||||
}
|
||||
} catch (bomErr) {
|
||||
console.error('[ItemDetailView] BOM 조회 실패:', bomErr);
|
||||
|
||||
@@ -154,13 +154,11 @@ export default function ItemListClient() {
|
||||
if (!itemToDelete) return;
|
||||
|
||||
try {
|
||||
console.log('[Delete] 삭제 요청:', itemToDelete);
|
||||
|
||||
// 2025-12-15: 백엔드 동적 테이블 라우팅 - 모든 품목이 /items 엔드포인트 사용
|
||||
// /products/materials 라우트 삭제됨
|
||||
const deleteUrl = `/api/proxy/items/${itemToDelete.id}?item_type=${itemToDelete.itemType}`;
|
||||
|
||||
console.log('[Delete] URL:', deleteUrl, '(itemType:', itemToDelete.itemType, ')');
|
||||
|
||||
const response = await fetch(deleteUrl, {
|
||||
method: 'DELETE',
|
||||
@@ -175,7 +173,6 @@ export default function ItemListClient() {
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[Delete] 응답:', { status: response.status, result });
|
||||
|
||||
if (result.success) {
|
||||
refresh();
|
||||
@@ -330,7 +327,6 @@ export default function ItemListClient() {
|
||||
|
||||
// TODO: 실제 API 호출로 데이터 저장
|
||||
// 지금은 파싱 결과만 확인
|
||||
console.log('[Excel Upload] 파싱 결과:', result.data);
|
||||
alert(`${result.data.length}건의 데이터가 파싱되었습니다.\n(실제 등록 기능은 추후 구현 예정)`);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -55,7 +55,6 @@ export function DraggableField({ field, index, moveField, onDelete, onEdit }: Dr
|
||||
const data = JSON.parse(e.dataTransfer.getData('application/json'));
|
||||
// 2025-12-03: 타입 체크 - 필드 드래그만 처리
|
||||
if (data.type !== 'field') {
|
||||
console.log('[DraggableField] 필드 드래그가 아님, 무시:', data);
|
||||
return;
|
||||
}
|
||||
if (data.id !== field.id) {
|
||||
|
||||
@@ -462,25 +462,12 @@ export function FieldDialog({
|
||||
<DialogFooter className="shrink-0 bg-white z-10 px-6 py-4 border-t">
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log('[FieldDialog] 🔵 저장 버튼 클릭!', {
|
||||
fieldInputMode,
|
||||
editingFieldId,
|
||||
selectedMasterFieldId,
|
||||
newFieldName,
|
||||
newFieldKey,
|
||||
isNameEmpty,
|
||||
isKeyEmpty,
|
||||
isKeyInvalid,
|
||||
});
|
||||
setIsSubmitted(true);
|
||||
// 2025-11-28: field_key validation 추가
|
||||
const shouldValidate = isCustomMode || editingFieldId;
|
||||
console.log('[FieldDialog] 🔵 shouldValidate:', shouldValidate);
|
||||
if (shouldValidate && (isNameEmpty || isKeyEmpty || isKeyInvalid)) {
|
||||
console.log('[FieldDialog] ❌ 유효성 검사 실패로 return');
|
||||
return;
|
||||
}
|
||||
console.log('[FieldDialog] ✅ handleAddField 호출');
|
||||
await handleAddField();
|
||||
setIsSubmitted(false);
|
||||
}}>저장</Button>
|
||||
|
||||
@@ -47,7 +47,6 @@ export function useDeleteManagement({ itemPages }: UseDeleteManagementProps): Us
|
||||
const sectionIds = pageToDelete?.sections.map(s => s.id) || [];
|
||||
const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || [];
|
||||
deleteItemPage(pageId);
|
||||
console.log('페이지 삭제 완료:', { pageId, removedSections: sectionIds.length, removedFields: fieldIds.length });
|
||||
}, [itemPages, deleteItemPage]);
|
||||
|
||||
// 섹션 삭제 핸들러
|
||||
@@ -56,14 +55,12 @@ export function useDeleteManagement({ itemPages }: UseDeleteManagementProps): Us
|
||||
const sectionToDelete = page?.sections.find(s => s.id === sectionId);
|
||||
const fieldIds = sectionToDelete?.fields?.map(f => f.id) || [];
|
||||
deleteSection(Number(sectionId));
|
||||
console.log('섹션 삭제 완료:', { sectionId, removedFields: fieldIds.length });
|
||||
}, [itemPages, deleteSection]);
|
||||
|
||||
// 필드 연결 해제 핸들러
|
||||
const handleUnlinkField = useCallback(async (_pageId: string, sectionId: string, fieldId: string) => {
|
||||
try {
|
||||
await unlinkFieldFromSection(Number(sectionId), Number(fieldId));
|
||||
console.log('필드 연결 해제 완료:', fieldId);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('필드 연결 해제 실패:', error);
|
||||
@@ -105,7 +102,6 @@ export function useDeleteManagement({ itemPages }: UseDeleteManagementProps): Us
|
||||
|
||||
setAttributeSubTabs([]);
|
||||
|
||||
console.log('🗑️ 모든 품목기준관리 데이터가 초기화되었습니다');
|
||||
toast.success('✅ 모든 데이터가 초기화되었습니다!\n계층구조, 섹션, 항목, 속성이 모두 삭제되었습니다.');
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -153,22 +153,8 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
|
||||
// 필드 추가 (2025-11-27: async/await 추가 - 다른 탭 실시간 동기화)
|
||||
const handleAddField = async (selectedPage: ItemPage | undefined) => {
|
||||
console.log('[useFieldManagement] 🟢 handleAddField 시작!', {
|
||||
selectedPage: selectedPage?.id,
|
||||
selectedSectionForField,
|
||||
newFieldName,
|
||||
newFieldKey,
|
||||
fieldInputMode,
|
||||
selectedMasterFieldId,
|
||||
});
|
||||
|
||||
if (!selectedPage || !selectedSectionForField || !newFieldName.trim() || !newFieldKey.trim()) {
|
||||
console.log('[useFieldManagement] ❌ 필수값 누락으로 return', {
|
||||
selectedPage: !!selectedPage,
|
||||
selectedSectionForField,
|
||||
newFieldName: newFieldName.trim(),
|
||||
newFieldKey: newFieldKey.trim(),
|
||||
});
|
||||
toast.error('모든 필수 항목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
@@ -221,7 +207,6 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
|
||||
try {
|
||||
if (editingFieldId) {
|
||||
console.log('Updating field:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldId: editingFieldId, fieldName: newField.field_name });
|
||||
await updateField(Number(editingFieldId), newField);
|
||||
|
||||
// 항목관리 탭의 마스터 항목도 업데이트 (동일한 fieldKey가 있으면)
|
||||
@@ -238,7 +223,6 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
|
||||
toast.success('항목이 섹션에 수정되었습니다!');
|
||||
} else {
|
||||
console.log('Adding field to section:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldName: newField.field_name });
|
||||
|
||||
// 섹션에 항목 추가 (await로 완료 대기)
|
||||
// 2025-11-27: addItemMasterField 호출 제거 - 중복 필드 생성 방지
|
||||
@@ -256,8 +240,6 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
|
||||
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등)
|
||||
if (error instanceof ApiError) {
|
||||
console.log('🔍 ApiError.errors:', error.errors); // 디버깅용
|
||||
|
||||
// errors 객체에서 첫 번째 에러 메시지 추출 → AlertDialog로 표시
|
||||
if (error.errors && Object.keys(error.errors).length > 0) {
|
||||
const firstKey = Object.keys(error.errors)[0];
|
||||
@@ -306,7 +288,6 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
// 필드 삭제
|
||||
const handleDeleteField = (pageId: string, sectionId: string, fieldId: string) => {
|
||||
deleteField(Number(fieldId));
|
||||
console.log('필드 삭제 완료:', fieldId);
|
||||
};
|
||||
|
||||
// 폼 초기화
|
||||
|
||||
@@ -62,7 +62,6 @@ export function useInitialDataLoading({
|
||||
// Context와 병행 운영 - 점진적 마이그레이션
|
||||
try {
|
||||
await initFromApi();
|
||||
console.log('✅ [Zustand] Store initialized');
|
||||
} catch (zustandError) {
|
||||
// Zustand 초기화 실패해도 Context로 fallback
|
||||
console.warn('⚠️ [Zustand] Init failed, falling back to Context:', zustandError);
|
||||
@@ -78,7 +77,6 @@ export function useInitialDataLoading({
|
||||
if (data.sections && data.sections.length > 0) {
|
||||
const transformedSections = transformSectionsResponse(data.sections);
|
||||
loadIndependentSections(transformedSections);
|
||||
console.log('✅ 독립 섹션 로드:', transformedSections.length);
|
||||
}
|
||||
|
||||
// 3. 섹션 템플릿 로드
|
||||
@@ -106,10 +104,6 @@ export function useInitialDataLoading({
|
||||
loadItemMasterFields(transformedFields as any);
|
||||
loadIndependentFields(independentOnlyFields);
|
||||
|
||||
console.log('✅ 필드 로드:', {
|
||||
total: transformedFields.length,
|
||||
independent: independentOnlyFields.length,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 커스텀 탭 로드
|
||||
@@ -124,13 +118,6 @@ export function useInitialDataLoading({
|
||||
setUnitOptions(transformedUnits);
|
||||
}
|
||||
|
||||
console.log('✅ Initial data loaded:', {
|
||||
pages: data.pages?.length || 0,
|
||||
sections: data.sections?.length || 0,
|
||||
fields: data.fields?.length || 0,
|
||||
customTabs: data.customTabs?.length || 0,
|
||||
unitOptions: data.unitOptions?.length || 0,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.errors) {
|
||||
|
||||
@@ -127,8 +127,6 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
|
||||
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어) → AlertDialog로 표시
|
||||
if (error instanceof ApiError) {
|
||||
console.log('🔍 ApiError.errors:', error.errors); // 디버깅용
|
||||
|
||||
// errors 객체에서 첫 번째 에러 메시지 추출
|
||||
if (error.errors && Object.keys(error.errors).length > 0) {
|
||||
const firstKey = Object.keys(error.errors)[0];
|
||||
@@ -202,8 +200,6 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
|
||||
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등) → AlertDialog로 표시
|
||||
if (error instanceof ApiError) {
|
||||
console.log('🔍 ApiError.errors:', error.errors); // 디버깅용
|
||||
|
||||
// errors 객체에서 첫 번째 에러 메시지 추출
|
||||
if (error.errors && Object.keys(error.errors).length > 0) {
|
||||
const firstKey = Object.keys(error.errors)[0];
|
||||
|
||||
@@ -80,7 +80,6 @@ export function usePageManagement(): UsePageManagementReturn {
|
||||
migrationDoneRef.current.add(page.id);
|
||||
});
|
||||
|
||||
console.log(`절대경로가 자동으로 생성되었습니다 (${pagesToMigrate.length}개 페이지)`);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemPages.length]); // itemPages 길이가 변경될 때만 체크
|
||||
|
||||
@@ -180,11 +179,6 @@ export function usePageManagement(): UsePageManagementReturn {
|
||||
setSelectedPageId(remainingPages[0]?.id || null);
|
||||
}
|
||||
|
||||
console.log('페이지 삭제 완료:', {
|
||||
pageId,
|
||||
sectionsMovedToIndependent: sectionCount,
|
||||
fieldsPreserved: fieldCount
|
||||
});
|
||||
};
|
||||
|
||||
// 페이지명 수정
|
||||
|
||||
@@ -85,13 +85,6 @@ export function useSectionManagement(): UseSectionManagementReturn {
|
||||
bom_items: sectionType === 'BOM' ? [] : undefined
|
||||
};
|
||||
|
||||
console.log('Adding section to page:', {
|
||||
pageId: selectedPage.id,
|
||||
page_name: selectedPage.page_name,
|
||||
sectionTitle: newSection.title,
|
||||
sectionType: newSection.section_type,
|
||||
currentSectionCount: selectedPage.sections.length,
|
||||
});
|
||||
|
||||
try {
|
||||
// 페이지에 섹션 추가 (API 호출)
|
||||
@@ -99,9 +92,6 @@ export function useSectionManagement(): UseSectionManagementReturn {
|
||||
// 별도의 addSectionTemplate 호출 불필요 (자동 동기화)
|
||||
await addSectionToPage(selectedPage.id, newSection);
|
||||
|
||||
console.log('Section added to page:', {
|
||||
sectionTitle: newSection.title
|
||||
});
|
||||
|
||||
resetSectionForm();
|
||||
toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 추가되었습니다!`);
|
||||
@@ -127,12 +117,6 @@ export function useSectionManagement(): UseSectionManagementReturn {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Linking existing section to page:', {
|
||||
sectionId: template.id,
|
||||
sectionName: template.template_name,
|
||||
pageId: selectedPage.id,
|
||||
orderNo: selectedPage.sections.length + 1,
|
||||
});
|
||||
|
||||
try {
|
||||
// 기존 섹션을 페이지에 연결 (entity_relationships에 레코드 추가)
|
||||
@@ -176,7 +160,6 @@ export function useSectionManagement(): UseSectionManagementReturn {
|
||||
const handleUnlinkSection = async (pageId: number, sectionId: number) => {
|
||||
try {
|
||||
await unlinkSectionFromPage(pageId, sectionId);
|
||||
console.log('섹션 연결 해제 완료:', { pageId, sectionId });
|
||||
toast.success('섹션 연결이 해제되었습니다');
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
@@ -193,10 +176,6 @@ export function useSectionManagement(): UseSectionManagementReturn {
|
||||
|
||||
try {
|
||||
await deleteSection(sectionId);
|
||||
console.log('섹션 삭제 완료:', {
|
||||
sectionId,
|
||||
removedFields: fieldIds.length
|
||||
});
|
||||
toast.success('섹션이 삭제되었습니다!');
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
|
||||
@@ -174,7 +174,6 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
console.log('Adding independent section (from section tab):', newSectionData);
|
||||
|
||||
try {
|
||||
await createIndependentSection(newSectionData);
|
||||
@@ -211,7 +210,6 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM'
|
||||
};
|
||||
|
||||
console.log('Updating section (from template handler):', { id: editingSectionTemplateId, updateData });
|
||||
try {
|
||||
// updateSection 호출 (템플릿이 아닌 실제 섹션 API)
|
||||
await updateSection(editingSectionTemplateId, updateData);
|
||||
@@ -266,7 +264,6 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
bom_items: template.section_type === 'BOM' ? [] : undefined
|
||||
};
|
||||
|
||||
console.log('Loading template to section:', template.template_name, 'newSection:', newSection);
|
||||
addSectionToPage(selectedPage.id, newSection);
|
||||
setSelectedTemplateId(null);
|
||||
setIsLoadTemplateDialogOpen(false);
|
||||
@@ -365,8 +362,6 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
|
||||
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등) → AlertDialog로 표시
|
||||
if (error instanceof ApiError) {
|
||||
console.log('🔍 ApiError.errors:', error.errors); // 디버깅용
|
||||
|
||||
// errors 객체에서 첫 번째 에러 메시지 추출
|
||||
if (error.errors && Object.keys(error.errors).length > 0) {
|
||||
const firstKey = Object.keys(error.errors)[0];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import type { ItemPage, ItemSection, BOMItem } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, ItemSection, ItemField, BOMItem } from '@/contexts/ItemMasterContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -33,12 +33,12 @@ interface HierarchyTabProps {
|
||||
setEditingSectionTitle: (title: string) => void;
|
||||
hasUnsavedChanges: boolean;
|
||||
pendingChanges: {
|
||||
pages: any[];
|
||||
sections: any[];
|
||||
fields: any[];
|
||||
masterFields: any[];
|
||||
attributes: any[];
|
||||
sectionTemplates: any[];
|
||||
pages: Record<string, unknown>[];
|
||||
sections: Record<string, unknown>[];
|
||||
fields: Record<string, unknown>[];
|
||||
masterFields: Record<string, unknown>[];
|
||||
attributes: Record<string, unknown>[];
|
||||
sectionTemplates: Record<string, unknown>[];
|
||||
};
|
||||
selectedSectionForField: number | null;
|
||||
setSelectedSectionForField: (id: number | null) => void;
|
||||
@@ -46,8 +46,8 @@ interface HierarchyTabProps {
|
||||
setNewSectionType: Dispatch<SetStateAction<'fields' | 'bom'>>;
|
||||
|
||||
// Functions
|
||||
updateItemPage: (id: number, data: any) => void;
|
||||
trackChange: (type: 'pages' | 'sections' | 'fields' | 'masterFields' | 'attributes' | 'sectionTemplates', id: string, action: 'add' | 'update', data: any, attributeType?: string) => void;
|
||||
updateItemPage: (id: number, data: Partial<ItemPage>) => void;
|
||||
trackChange: (type: 'pages' | 'sections' | 'fields' | 'masterFields' | 'attributes' | 'sectionTemplates', id: string, action: 'add' | 'update', data: Record<string, unknown>, attributeType?: string) => void;
|
||||
deleteItemPage: (id: number) => void;
|
||||
duplicatePage: (id: number) => void;
|
||||
setIsPageDialogOpen: (open: boolean) => void;
|
||||
@@ -59,7 +59,7 @@ interface HierarchyTabProps {
|
||||
unlinkSection: (pageId: number, sectionId: number) => void; // 연결 해제 (삭제 아님)
|
||||
updateSection: (sectionId: number, updates: Partial<ItemSection>) => Promise<void>;
|
||||
deleteField: (pageId: string, sectionId: string, fieldId: string) => void; // 2025-11-27: 연결 해제로 변경 (삭제 아님, 항목 탭에 유지)
|
||||
handleEditField: (sectionId: string, field: any) => void;
|
||||
handleEditField: (sectionId: string, field: ItemField) => void;
|
||||
// 2025-12-03: ID 기반으로 변경 (index stale 문제 해결)
|
||||
moveField: (sectionId: number, dragFieldId: number, hoverFieldId: number) => void | Promise<void>;
|
||||
// 2025-11-26 추가: 섹션/필드 불러오기
|
||||
@@ -372,7 +372,6 @@ export function HierarchyTab({
|
||||
bomItems={section.bom_items || []}
|
||||
onAddItem={async (item) => {
|
||||
// 2025-11-27: API 함수로 BOM 항목 추가
|
||||
console.log('[HierarchyTab] BOM 추가 시작:', { sectionId: section.id, item, addBOMItemExists: !!addBOMItem });
|
||||
if (addBOMItem) {
|
||||
try {
|
||||
await addBOMItem(section.id, {
|
||||
@@ -383,7 +382,6 @@ export function HierarchyTab({
|
||||
unit: item.unit,
|
||||
spec: item.spec,
|
||||
});
|
||||
console.log('[HierarchyTab] BOM 추가 성공');
|
||||
toast.success('BOM 항목이 추가되었습니다');
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
|
||||
@@ -71,14 +71,6 @@ export function SectionsTab({
|
||||
}: SectionsTabProps) {
|
||||
// 2025-11-27: prop 변경 추적 (디버깅용)
|
||||
useEffect(() => {
|
||||
console.log('[SectionsTab] 📥 sectionTemplates prop changed:', {
|
||||
count: sectionTemplates.length,
|
||||
sections: sectionTemplates.map(s => ({
|
||||
id: s.id,
|
||||
name: s.template_name,
|
||||
fieldsCount: s.fields?.length || 0
|
||||
}))
|
||||
});
|
||||
}, [sectionTemplates]);
|
||||
|
||||
return (
|
||||
@@ -118,16 +110,6 @@ export function SectionsTab({
|
||||
{/* 일반 섹션 탭 */}
|
||||
<TabsContent value="general">
|
||||
{(() => {
|
||||
console.log('[SectionsTab] 🔄 Rendering section templates:', {
|
||||
totalTemplates: sectionTemplates.length,
|
||||
generalTemplates: sectionTemplates.filter(t => t.section_type !== 'BOM').length,
|
||||
templates: sectionTemplates.map(t => ({
|
||||
id: t.id,
|
||||
template_name: t.template_name,
|
||||
section_type: t.section_type,
|
||||
fieldsCount: t.fields?.length || 0 // 필드 개수 추가
|
||||
}))
|
||||
});
|
||||
return null;
|
||||
})()}
|
||||
{sectionTemplates.filter(t => t.section_type !== 'BOM').length === 0 ? (
|
||||
|
||||
@@ -162,14 +162,6 @@ export function InspectionCreate({ id }: Props) {
|
||||
}
|
||||
|
||||
// TODO: API 호출
|
||||
console.log('검사 저장:', {
|
||||
targetId: selectedTargetId,
|
||||
inspectionDate,
|
||||
inspector,
|
||||
lotNo,
|
||||
items: inspectionItems,
|
||||
opinion,
|
||||
});
|
||||
|
||||
setShowSuccess(true);
|
||||
}, [validateForm, selectedTargetId, inspectionDate, inspector, lotNo, inspectionItems, opinion]);
|
||||
|
||||
@@ -21,7 +21,6 @@ interface Props {
|
||||
export function ReceivingReceiptDialog({ open, onOpenChange, detail }: Props) {
|
||||
const handleDownload = () => {
|
||||
// TODO: PDF 다운로드 기능
|
||||
console.log('PDF 다운로드:', detail);
|
||||
};
|
||||
|
||||
const toolbarExtra = (
|
||||
|
||||
@@ -151,11 +151,11 @@ export function DateRangeSelector({
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 날짜 범위 선택 */}
|
||||
{!hideDateInputs && (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<div className="flex items-center gap-1 w-full xl:w-auto xl:shrink-0">
|
||||
<DatePicker
|
||||
value={startDate}
|
||||
onChange={onStartDateChange}
|
||||
className="w-[170px]"
|
||||
className="flex-1 xl:flex-none xl:w-[170px]"
|
||||
size="sm"
|
||||
displayFormat="yyyy년 MM월 dd일"
|
||||
/>
|
||||
@@ -163,7 +163,7 @@ export function DateRangeSelector({
|
||||
<DatePicker
|
||||
value={endDate}
|
||||
onChange={onEndDateChange}
|
||||
className="w-[170px]"
|
||||
className="flex-1 xl:flex-none xl:w-[170px]"
|
||||
size="sm"
|
||||
displayFormat="yyyy년 MM월 dd일"
|
||||
/>
|
||||
@@ -187,11 +187,11 @@ export function DateRangeSelector({
|
||||
<div className="flex flex-col xl:flex-row xl:flex-wrap xl:items-center gap-2">
|
||||
{/* 날짜 범위 선택 */}
|
||||
{!hideDateInputs && (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<div className="flex items-center gap-1 w-full xl:w-auto xl:shrink-0">
|
||||
<DatePicker
|
||||
value={startDate}
|
||||
onChange={onStartDateChange}
|
||||
className="w-[170px]"
|
||||
className="flex-1 xl:flex-none xl:w-[170px]"
|
||||
size="sm"
|
||||
displayFormat="yyyy년 MM월 dd일"
|
||||
/>
|
||||
@@ -199,7 +199,7 @@ export function DateRangeSelector({
|
||||
<DatePicker
|
||||
value={endDate}
|
||||
onChange={onEndDateChange}
|
||||
className="w-[170px]"
|
||||
className="flex-1 xl:flex-none xl:w-[170px]"
|
||||
size="sm"
|
||||
displayFormat="yyyy년 MM월 dd일"
|
||||
/>
|
||||
|
||||
@@ -81,6 +81,7 @@ interface DataTableProps<T extends object> {
|
||||
}
|
||||
|
||||
// 셀 렌더러
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 다양한 셀 타입(text/date/icon/badge 등)을 다형적으로 처리
|
||||
function renderCell<T>(column: Column<T>, value: any, row: T, index?: number): ReactNode {
|
||||
// 값 포맷팅 또는 render 호출
|
||||
let formattedValue: any;
|
||||
@@ -182,7 +183,7 @@ function renderCell<T>(column: Column<T>, value: any, row: T, index?: number): R
|
||||
}
|
||||
|
||||
// 정렬 함수
|
||||
function getAlignClass(column: Column<any>): string {
|
||||
function getAlignClass<T>(column: Column<T>): string {
|
||||
if (column.align) {
|
||||
return column.align === "center" ? "text-center" : column.align === "right" ? "text-right" : "text-left";
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@ export async function updateVehicleDispatch(
|
||||
_data: VehicleDispatchEditFormData
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('[VehicleDispatchActions] updateVehicleDispatch:', id, _data);
|
||||
// Mock: 항상 성공 반환
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -175,7 +175,6 @@ export function PricingListClient({
|
||||
|
||||
const handleHistory = (item: PricingListItem) => {
|
||||
// TODO: 이력 다이얼로그 열기
|
||||
console.log('이력 조회:', item.id);
|
||||
};
|
||||
|
||||
// 탭 옵션
|
||||
@@ -334,7 +333,6 @@ export function PricingListClient({
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// TODO: API 연동 시 품목 마스터 동기화 로직 구현
|
||||
console.log('품목 마스터 동기화');
|
||||
}}
|
||||
className="ml-auto gap-2"
|
||||
>
|
||||
|
||||
@@ -90,7 +90,6 @@ export async function getWorkOrders(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('[WorkOrderActions] GET work-orders:', url);
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
@@ -148,7 +147,6 @@ export async function getWorkOrderStats(): Promise<{
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/stats`;
|
||||
|
||||
console.log('[WorkOrderActions] GET stats:', url);
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
@@ -192,7 +190,6 @@ export async function getWorkOrderById(id: string): Promise<{
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`;
|
||||
|
||||
console.log('[WorkOrderActions] GET work-order:', url);
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
@@ -249,7 +246,6 @@ export async function createWorkOrder(
|
||||
team_id: data.teamId,
|
||||
};
|
||||
|
||||
console.log('[WorkOrderActions] POST work-order request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders`,
|
||||
@@ -264,7 +260,6 @@ export async function createWorkOrder(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] POST work-order response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -292,7 +287,6 @@ export async function updateWorkOrder(
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[WorkOrderActions] PUT work-order request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
|
||||
@@ -307,7 +301,6 @@ export async function updateWorkOrder(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PUT work-order response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -340,7 +333,6 @@ export async function deleteWorkOrder(id: string): Promise<{ success: boolean; e
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] DELETE work-order response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -363,7 +355,6 @@ export async function updateWorkOrderStatus(
|
||||
status: WorkOrderStatus
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
console.log('[WorkOrderActions] PATCH status request:', { status });
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`,
|
||||
@@ -378,7 +369,6 @@ export async function updateWorkOrderStatus(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH status response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -410,7 +400,6 @@ export async function assignWorkOrder(
|
||||
const body: { assignee_ids: number[]; team_id?: number } = { assignee_ids: ids };
|
||||
if (teamId) body.team_id = teamId;
|
||||
|
||||
console.log('[WorkOrderActions] PATCH assign request:', body);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/assign`,
|
||||
@@ -425,7 +414,6 @@ export async function assignWorkOrder(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH assign response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -451,7 +439,6 @@ export async function toggleBendingField(
|
||||
field: string
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
console.log('[WorkOrderActions] PATCH bending toggle request:', { field });
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/bending/toggle`,
|
||||
@@ -466,7 +453,6 @@ export async function toggleBendingField(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH bending toggle response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -496,7 +482,6 @@ export async function addWorkOrderIssue(
|
||||
}
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
console.log('[WorkOrderActions] POST issue request:', data);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/issues`,
|
||||
@@ -511,7 +496,6 @@ export async function addWorkOrderIssue(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] POST issue response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -537,7 +521,6 @@ export async function resolveWorkOrderIssue(
|
||||
issueId: string
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
console.log('[WorkOrderActions] PATCH issue resolve:', { workOrderId, issueId });
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`,
|
||||
@@ -549,7 +532,6 @@ export async function resolveWorkOrderIssue(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH issue resolve response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -585,7 +567,6 @@ export async function updateWorkOrderItemStatus(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
console.log('[WorkOrderActions] PATCH item status request:', { workOrderId, itemId, status });
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/status`,
|
||||
@@ -600,7 +581,6 @@ export async function updateWorkOrderItemStatus(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH item status response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -699,7 +679,6 @@ export async function saveInspectionData(
|
||||
data: unknown
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log('[WorkOrderActions] POST inspection data:', { workOrderId, processType });
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection`,
|
||||
@@ -717,7 +696,6 @@ export async function saveInspectionData(
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] POST inspection response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -867,7 +845,6 @@ export async function getSalesOrdersForWorkOrder(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('[WorkOrderActions] GET orders for work-order:', url);
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
@@ -947,7 +924,6 @@ export async function getDepartmentsWithUsers(): Promise<{
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/tree?with_users=1`;
|
||||
|
||||
console.log('[WorkOrderActions] GET departments with users:', url);
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
@@ -1019,7 +995,6 @@ export async function getProcessOptions(): Promise<{
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/options`;
|
||||
|
||||
console.log('[WorkOrderActions] GET process options:', url);
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ export function WorkResultList() {
|
||||
|
||||
// ===== 상세 보기 핸들러 =====
|
||||
const handleView = useCallback((item: WorkResult) => {
|
||||
console.log('상세 보기:', item.id);
|
||||
// TODO: 상세 보기 기능 구현
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -53,12 +53,6 @@ export function IssueReportModal({ open, onOpenChange, order }: IssueReportModal
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[이슈보고]', {
|
||||
orderId: order?.id,
|
||||
orderNo: order?.orderNo,
|
||||
issueType: selectedType,
|
||||
description,
|
||||
});
|
||||
|
||||
setShowSuccessAlert(true);
|
||||
};
|
||||
|
||||
@@ -735,7 +735,6 @@ export default function WorkerScreen() {
|
||||
// 자재 수정 핸들러
|
||||
const handleEditMaterial = useCallback(
|
||||
(itemId: string, material: MaterialListItem) => {
|
||||
console.log('[WorkerScreen] editMaterial:', itemId, material);
|
||||
// 추후 구현
|
||||
},
|
||||
[]
|
||||
@@ -744,7 +743,6 @@ export default function WorkerScreen() {
|
||||
// 자재 삭제 핸들러
|
||||
const handleDeleteMaterial = useCallback(
|
||||
(itemId: string, materialId: string) => {
|
||||
console.log('[WorkerScreen] deleteMaterial:', itemId, materialId);
|
||||
// 추후 구현
|
||||
},
|
||||
[]
|
||||
|
||||
@@ -412,31 +412,36 @@ export function PerformanceReportList() {
|
||||
</div>
|
||||
) : undefined,
|
||||
|
||||
// 헤더 액션 (선택 기반 버튼)
|
||||
headerActions: ({ selectedItems, onClearSelection, onRefresh }) => {
|
||||
// 선택 액션 (체크박스 선택 시 테이블 좌측에 표시)
|
||||
selectionActions: ({ selectedItems, onClearSelection, onRefresh }) => {
|
||||
if (currentTab !== 'quarterly') return null;
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" onClick={() => handleConfirm(selectedItems, onClearSelection, onRefresh)}>
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
선택 확정
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleUnconfirm(selectedItems, onClearSelection, onRefresh)}>
|
||||
<Undo2 className="h-4 w-4 mr-1" />
|
||||
확정 해제
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleDistribute(selectedItems, onClearSelection, onRefresh)}>
|
||||
<Send className="h-4 w-4 mr-1" />
|
||||
배포
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleOpenMemoModal(selectedItems)}>
|
||||
<Pencil className="h-4 w-4 mr-1" />
|
||||
메모
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
// 헤더 액션 (항상 표시)
|
||||
headerActions: () => {
|
||||
if (currentTab !== 'quarterly') return null;
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<Button size="sm" onClick={() => handleConfirm(selectedItems, onClearSelection, onRefresh)}>
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
선택 확정
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleUnconfirm(selectedItems, onClearSelection, onRefresh)}>
|
||||
<Undo2 className="h-4 w-4 mr-1" />
|
||||
확정 해제
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleDistribute(selectedItems, onClearSelection, onRefresh)}>
|
||||
<Send className="h-4 w-4 mr-1" />
|
||||
배포
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleOpenMemoModal(selectedItems)}>
|
||||
<Pencil className="h-4 w-4 mr-1" />
|
||||
메모
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={handleExcelDownload}>
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
확정건 엑셀다운로드
|
||||
|
||||
@@ -819,7 +819,6 @@ export function LocationDetailPanel({
|
||||
unitPrice: updatedGrandTotal,
|
||||
totalPrice: updatedGrandTotal * location.quantity,
|
||||
});
|
||||
console.log(`[품목 추가] ${item.code} - ${item.name} → ${categoryLabel} (${categoryCode}), 단가: ${unitPrice}`);
|
||||
}}
|
||||
tabLabel={detailTabs.find((t) => t.value === activeTab)?.label}
|
||||
/>
|
||||
|
||||
@@ -56,12 +56,10 @@ export function QuotePreviewModal({
|
||||
const vatIncluded = quoteData.vatType === 'included';
|
||||
|
||||
const handleDuplicate = () => {
|
||||
console.log('[테스트] 복제');
|
||||
onDuplicate?.();
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
console.log('[테스트] 수정');
|
||||
onEdit?.();
|
||||
};
|
||||
|
||||
|
||||
@@ -323,15 +323,6 @@ export function QuoteRegistration({
|
||||
|
||||
// editingQuote가 변경되면 formData 업데이트 및 calculationResults 초기화
|
||||
useEffect(() => {
|
||||
console.log('[QuoteRegistration] useEffect editingQuote:', JSON.stringify({
|
||||
hasEditingQuote: !!editingQuote,
|
||||
itemCount: editingQuote?.items?.length,
|
||||
item0: editingQuote?.items?.[0] ? {
|
||||
quantity: editingQuote.items[0].quantity,
|
||||
wingSize: editingQuote.items[0].wingSize,
|
||||
inspectionFee: editingQuote.items[0].inspectionFee,
|
||||
} : null,
|
||||
}, null, 2));
|
||||
if (editingQuote) {
|
||||
setFormData(editingQuote);
|
||||
// 수정 모드 진입 시 이전 산출 결과 초기화
|
||||
@@ -449,10 +440,6 @@ export function QuoteRegistration({
|
||||
field: keyof QuoteFormData,
|
||||
value: string | QuoteItem[]
|
||||
) => {
|
||||
// DEBUG: manager, contact, remarks 필드 변경 추적
|
||||
if (field === 'manager' || field === 'contact' || field === 'remarks') {
|
||||
console.log(`[handleFieldChange] ${field} 변경:`, value);
|
||||
}
|
||||
setFormData({ ...formData, [field]: value });
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => {
|
||||
@@ -617,11 +604,6 @@ export function QuoteRegistration({
|
||||
};
|
||||
|
||||
// 렌더링 직전 디버그 로그
|
||||
console.log('[QuoteRegistration] 렌더링 직전 formData.items[0]:', JSON.stringify({
|
||||
quantity: formData.items[0]?.quantity,
|
||||
wingSize: formData.items[0]?.wingSize,
|
||||
inspectionFee: formData.items[0]?.inspectionFee,
|
||||
}, null, 2));
|
||||
|
||||
// 폼 콘텐츠 렌더링
|
||||
const renderFormContent = useCallback(
|
||||
|
||||
@@ -222,7 +222,6 @@ export function QuoteRegistrationV2({
|
||||
useDevFill("quoteV2", useCallback(() => {
|
||||
// BOM이 있는 제품만 필터링
|
||||
const productsWithBom = finishedGoods.filter((fg) => fg.has_bom === true || (fg.bom && Array.isArray(fg.bom) && fg.bom.length > 0));
|
||||
console.log(`[DevFill] BOM 있는 제품: ${productsWithBom.length}개 / 전체: ${finishedGoods.length}개`);
|
||||
|
||||
// 랜덤 개소 생성 함수
|
||||
const createRandomLocation = (index: number): LocationItem => {
|
||||
@@ -611,12 +610,6 @@ export function QuoteRegistrationV2({
|
||||
const apiData = result.data as BomBulkResponse;
|
||||
const bomResponseItems = apiData.items || [];
|
||||
|
||||
console.log('[QuoteRegistrationV2] BOM 계산 결과:', {
|
||||
success: apiData.success,
|
||||
summary: apiData.summary,
|
||||
itemsCount: bomResponseItems.length,
|
||||
firstItem: bomResponseItems[0],
|
||||
});
|
||||
|
||||
// 결과 반영 (수동 추가 품목 보존)
|
||||
const updatedLocations = formData.locations.map((loc, index) => {
|
||||
@@ -638,13 +631,6 @@ export function QuoteRegistrationV2({
|
||||
const mergedItems = [...(bomResult.items || []), ...manualItems];
|
||||
const mergedGrandTotal = bomResult.grand_total + manualTotal;
|
||||
|
||||
console.log(`[QuoteRegistrationV2] Location ${index} bomResult:`, {
|
||||
items: bomResult.items?.length,
|
||||
manualItems: manualItems.length,
|
||||
mergedItems: mergedItems.length,
|
||||
subtotals: bomResult.subtotals,
|
||||
grand_total: mergedGrandTotal,
|
||||
});
|
||||
|
||||
return {
|
||||
...loc,
|
||||
|
||||
@@ -488,10 +488,6 @@ export function transformQuoteToFormData(quote: Quote): QuoteFormData {
|
||||
const amountPerItem = Math.round(totalBomAmount / itemCount);
|
||||
|
||||
// 디버깅 로그
|
||||
console.log('[transformQuoteToFormData] quote.calculationInputs:', JSON.stringify(quote.calculationInputs, null, 2));
|
||||
console.log('[transformQuoteToFormData] calcInputs:', JSON.stringify(calcInputs, null, 2));
|
||||
console.log('[transformQuoteToFormData] quote.items.length:', quote.items.length);
|
||||
console.log('[transformQuoteToFormData] totalBomAmount:', totalBomAmount, 'amountPerItem:', amountPerItem);
|
||||
|
||||
return {
|
||||
id: quote.id,
|
||||
@@ -588,7 +584,6 @@ export function transformApiDataToFormData(apiData: QuoteApiData): QuoteFormData
|
||||
const itemCount = calcInputs.length || 1;
|
||||
const amountPerItem = Math.round(totalBomAmount / itemCount);
|
||||
|
||||
console.log('[transformApiDataToFormData] totalBomAmount:', totalBomAmount, 'itemCount:', itemCount, 'amountPerItem:', amountPerItem);
|
||||
|
||||
return {
|
||||
id: String(apiData.id),
|
||||
@@ -1135,23 +1130,6 @@ export function transformFormDataToApi(formData: QuoteFormData): Record<string,
|
||||
items: itemsData,
|
||||
};
|
||||
|
||||
// 디버그: 전송되는 데이터 확인
|
||||
console.log('[transformFormDataToApi] 전송 데이터:', JSON.stringify({
|
||||
author: result.author,
|
||||
manager: result.manager,
|
||||
contact: result.contact,
|
||||
site_name: result.site_name,
|
||||
completion_date: result.completion_date,
|
||||
remarks: result.remarks,
|
||||
quantity: result.quantity,
|
||||
items_count: result.items?.length,
|
||||
items_sample: result.items?.slice(0, 3).map(i => ({
|
||||
item_name: i.item_name,
|
||||
quantity: i.quantity,
|
||||
base_quantity: i.base_quantity,
|
||||
calculated_quantity: i.calculated_quantity,
|
||||
})),
|
||||
}, null, 2));
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -128,7 +128,6 @@ interface NotificationSectionProps {
|
||||
}
|
||||
|
||||
function NotificationSection({ title, enabled, onEnabledChange, children }: NotificationSectionProps) {
|
||||
console.log(`[NotificationSection] ${title} enabled:`, enabled);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -137,7 +136,6 @@ function NotificationSection({ title, enabled, onEnabledChange, children }: Noti
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
console.log(`[Switch] ${title} clicked:`, checked);
|
||||
onEnabledChange(checked);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -157,12 +157,9 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi
|
||||
try {
|
||||
// 메뉴 트리 로드
|
||||
const menusResult = await fetchPermissionMenus();
|
||||
console.log('[PermissionDetail] menusResult:', menusResult);
|
||||
if (menusResult.success && menusResult.data) {
|
||||
console.log('[PermissionDetail] menus (flat):', menusResult.data.menus);
|
||||
// 플랫 배열을 트리 구조로 변환
|
||||
const treeMenus = buildMenuTree(menusResult.data.menus as FlatMenuItem[]);
|
||||
console.log('[PermissionDetail] menus (tree):', treeMenus);
|
||||
setMenuTree(treeMenus);
|
||||
setPermissionTypes(menusResult.data.permission_types);
|
||||
} else {
|
||||
|
||||
@@ -83,10 +83,10 @@ export interface DevMetadata {
|
||||
componentName: string;
|
||||
pagePath: string;
|
||||
description: string;
|
||||
apis?: any[];
|
||||
dataStructures?: any[];
|
||||
dbSchema?: any[];
|
||||
businessLogic?: any[];
|
||||
apis?: unknown[];
|
||||
dataStructures?: unknown[];
|
||||
dbSchema?: unknown[];
|
||||
businessLogic?: unknown[];
|
||||
}
|
||||
|
||||
export interface IntegratedListTemplateV2Props<T = any> {
|
||||
@@ -212,6 +212,8 @@ export interface IntegratedListTemplateV2Props<T = any> {
|
||||
onToggleSelectAll: () => void;
|
||||
getItemId: (item: T) => string; // 아이템에서 ID 추출
|
||||
onBulkDelete?: () => void; // 일괄 삭제 핸들러
|
||||
/** 선택 액션 버튼 (테이블 왼쪽 "전체 N건 / N개 선택됨" 옆에 표시) */
|
||||
selectionActions?: ReactNode;
|
||||
|
||||
// 테이블 표시 옵션
|
||||
showCheckbox?: boolean; // 체크박스 표시 여부 (기본: true)
|
||||
@@ -280,6 +282,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
onToggleSelectAll,
|
||||
getItemId,
|
||||
onBulkDelete,
|
||||
selectionActions,
|
||||
showCheckbox = true, // 기본값 true
|
||||
showRowNumber = true, // 기본값 true (번호 컬럼은 renderTableRow에서 처리)
|
||||
renderTableRow,
|
||||
@@ -637,7 +640,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
)}
|
||||
{/* 버튼 영역 (오른쪽 배치, 공간 부족시 자연스럽게 줄바꿈) */}
|
||||
{(headerActions || createButton) && (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="mobile-actions-grid xl:flex xl:items-center xl:gap-2 xl:shrink-0">
|
||||
{/* 헤더 액션 (엑셀 다운로드 등 추가 버튼들) */}
|
||||
{headerActions}
|
||||
{/* 등록 버튼 */}
|
||||
@@ -665,8 +668,8 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
|
||||
{/* 탭 - 카드 밖 (tabsPosition === 'above-stats') */}
|
||||
{tabsPosition === 'above-stats' && tabs && tabs.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="flex gap-2 min-w-max">
|
||||
<div className="xl:overflow-x-auto">
|
||||
<div className="mobile-tab-grid flex gap-2 xl:min-w-max">
|
||||
{tabs.map((tab) => (
|
||||
<TabChip
|
||||
key={tab.value}
|
||||
@@ -728,7 +731,35 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
{/* 데스크톱 (1280px+) - TabChip 탭 */}
|
||||
<div className="hidden xl:block mb-4">
|
||||
<div className="flex flex-wrap gap-2 justify-between items-center">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* 왼쪽: 전체 건수 + 선택 정보 + 선택삭제 */}
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
전체 {pagination.totalItems}건
|
||||
</span>
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<span className="text-primary font-medium">
|
||||
{selectedItems.size}개 항목 선택됨
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{selectedItems.size >= 1 && onBulkDelete && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBulkDeleteClick}
|
||||
className="flex items-center gap-2 bg-gray-900 text-white hover:bg-gray-800 hover:text-white"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
선택삭제
|
||||
</Button>
|
||||
)}
|
||||
{/* 선택 액션 버튼 (상신, 승인, 삭제 등) */}
|
||||
{selectedItems.size > 0 && selectionActions}
|
||||
</div>
|
||||
{/* 오른쪽: 탭 + 헤더 액션 + 필터 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{tabsPosition !== 'above-stats' && tabs && tabs.map((tab) => (
|
||||
<TabChip
|
||||
key={tab.value}
|
||||
@@ -739,37 +770,16 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
color={tab.color as any}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 선택된 항목 수 표시 */}
|
||||
{selectedItems.size > 0 && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{selectedItems.size}개 항목 선택됨
|
||||
</span>
|
||||
)}
|
||||
{/* 테이블 헤더 액션 (총 N건 등) - 필터 앞에 배치 */}
|
||||
{tableHeaderActions}
|
||||
{/* filterConfig 기반 자동 필터 (PC) */}
|
||||
{renderAutoFilters()}
|
||||
{selectedItems.size >= 1 && onBulkDelete && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBulkDeleteClick}
|
||||
className="flex items-center gap-2 bg-gray-900 text-white hover:bg-gray-800 hover:text-white"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
선택 삭제({selectedItems.size})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 모바일/태블릿 (~1279px) - TabChip 탭 */}
|
||||
{tabsPosition !== 'above-stats' && tabs && tabs.length > 0 && (
|
||||
<div className="xl:hidden mb-4 overflow-x-auto">
|
||||
<div className="flex gap-2 min-w-max">
|
||||
<div className="xl:hidden mb-4">
|
||||
<div className="mobile-tab-grid flex gap-2">
|
||||
{tabs.map((tab) => (
|
||||
<TabChip
|
||||
key={tab.value}
|
||||
@@ -787,18 +797,28 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
{/* 탭 컨텐츠 */}
|
||||
{(tabs || [{ value: 'default', label: '', count: 0 }]).map((tab) => (
|
||||
<TabsContent key={tab.value} value={tab.value} className="mt-0">
|
||||
{/* 모바일/태블릿/소형 노트북 (~1279px) - 선택 삭제 버튼 */}
|
||||
{selectedItems.size >= 2 && onBulkDelete && (
|
||||
<div className="xl:hidden fixed bottom-0 left-0 right-0 p-4 bg-white border-t shadow-lg z-50">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
onClick={handleBulkDeleteClick}
|
||||
className="w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
선택 삭제 ({selectedItems.size})
|
||||
</Button>
|
||||
{/* 모바일/태블릿/소형 노트북 (~1279px) - 선택 액션 + 선택 삭제 하단 고정 바 */}
|
||||
{selectedItems.size > 0 && (selectionActions || onBulkDelete) && (
|
||||
<div className="xl:hidden fixed bottom-0 left-0 right-0 p-3 bg-white border-t shadow-lg z-50">
|
||||
<div className="flex items-center gap-2 mb-2 text-sm">
|
||||
<span className="text-primary font-medium">
|
||||
{selectedItems.size}개 선택됨
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 overflow-x-auto">
|
||||
{selectionActions}
|
||||
{selectedItems.size >= 1 && onBulkDelete && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBulkDeleteClick}
|
||||
className="flex items-center gap-2 bg-gray-900 text-white hover:bg-gray-800 hover:text-white shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
선택삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -542,10 +542,6 @@ export function UniversalListPage<T>({
|
||||
let dataToDownload: T[];
|
||||
|
||||
// 디버깅: 데이터 개수 확인
|
||||
console.log('[Excel] clientSideFiltering:', config.clientSideFiltering);
|
||||
console.log('[Excel] rawData.length:', rawData.length);
|
||||
console.log('[Excel] filteredData.length:', filteredData.length);
|
||||
console.log('[Excel] fetchAllUrl:', fetchAllUrl);
|
||||
|
||||
// fetchAllUrl이 있으면 서버에서 전체 데이터 페이지별 순차 조회 (clientSideFiltering 여부 무관)
|
||||
if (fetchAllUrl) {
|
||||
@@ -584,7 +580,6 @@ export function UniversalListPage<T>({
|
||||
const total = firstResult.data?.total ?? firstPageData.length;
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||
|
||||
console.log(`[Excel] 전체 ${total}건, ${totalPages}페이지 조회 시작`);
|
||||
|
||||
// 2) 나머지 페이지 병렬 호출
|
||||
const allData: T[] = [...(firstPageData as T[])];
|
||||
@@ -621,7 +616,6 @@ export function UniversalListPage<T>({
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Excel] 총 ${allData.length}건 조회 완료`);
|
||||
dataToDownload = allData;
|
||||
}
|
||||
// fetchAllUrl 없으면 현재 로드된 데이터 사용
|
||||
@@ -958,6 +952,11 @@ export function UniversalListPage<T>({
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
getItemId={effectiveGetItemId}
|
||||
onBulkDelete={config.actions?.deleteItem ? handleBulkDeleteClick : undefined}
|
||||
selectionActions={config.selectionActions?.({
|
||||
selectedItems: effectiveSelectedItems,
|
||||
onClearSelection: () => externalSelection ? undefined : setSelectedItems(new Set()),
|
||||
onRefresh: fetchData,
|
||||
})}
|
||||
// 표시 옵션
|
||||
showCheckbox={config.showCheckbox}
|
||||
showRowNumber={config.showRowNumber}
|
||||
|
||||
@@ -257,6 +257,16 @@ export interface UniversalListConfig<T> {
|
||||
onClearSelection: () => void;
|
||||
onRefresh: () => void;
|
||||
}) => ReactNode;
|
||||
/**
|
||||
* 선택 액션 버튼 (테이블 왼쪽 "전체 N건 / N개 선택됨" 옆에 표시)
|
||||
* - 체크박스 선택 시에만 표시되는 버튼들 (상신, 승인, 삭제 등)
|
||||
* - headerActions에서 선택 의존 버튼을 분리하여 이쪽으로 이동
|
||||
*/
|
||||
selectionActions?: (params: {
|
||||
selectedItems: Set<string>;
|
||||
onClearSelection: () => void;
|
||||
onRefresh: () => void;
|
||||
}) => ReactNode;
|
||||
/** 커스텀 액션 버튼 (상신, 승인 등) */
|
||||
customActions?: CustomAction<T>[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user