자재관리: - 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장 - 재고현황 컴포넌트 리팩토링 출고관리: - 출하관리 생성/수정/목록/상세 개선 - 차량배차관리 상세/수정/목록 기능 보강 생산관리: - 작업지시서 WIP 생산 모달 신규 추가 - 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가 - 작업자화면 기능 대폭 확장 (카드/목록 개선) - 검사성적서 모달 개선 품질관리: - 실적보고서 관리 페이지 신규 추가 - 검사관리 문서/타입/목데이터 개선 단가관리: - 단가배포 페이지 및 컴포넌트 신규 추가 - 단가표 관리 페이지 및 컴포넌트 신규 추가 공통: - 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard) - 메뉴 폴링 훅 개선, 레이아웃 수정 - 모바일 줌/패닝 CSS 수정 - locale 유틸 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
445 lines
11 KiB
TypeScript
445 lines
11 KiB
TypeScript
'use server';
|
|
|
|
import type {
|
|
PriceDistributionListItem,
|
|
PriceDistributionDetail,
|
|
PriceDistributionFormData,
|
|
PriceDistributionStats,
|
|
DistributionStatus,
|
|
PriceDistributionItem,
|
|
} from './types';
|
|
|
|
// ============================================================================
|
|
// 목데이터
|
|
// ============================================================================
|
|
|
|
const MOCK_ITEMS: PriceDistributionItem[] = [
|
|
{
|
|
id: 'item-1',
|
|
pricingCode: '121212',
|
|
itemCode: '123123',
|
|
itemType: '반제품',
|
|
itemName: '품목명A',
|
|
specification: 'ST',
|
|
unit: 'EA',
|
|
purchasePrice: 10000,
|
|
processingCost: 5000,
|
|
marginRate: 50.0,
|
|
salesPrice: 20000,
|
|
status: '사용',
|
|
author: '홍길동',
|
|
changedDate: '2026-01-15',
|
|
},
|
|
{
|
|
id: 'item-2',
|
|
pricingCode: '121213',
|
|
itemCode: '123124',
|
|
itemType: '완제품',
|
|
itemName: '품목명B',
|
|
specification: '규격B',
|
|
unit: 'SET',
|
|
purchasePrice: 8000,
|
|
processingCost: 3000,
|
|
marginRate: 40.0,
|
|
salesPrice: 14000,
|
|
status: '사용',
|
|
author: '김철수',
|
|
changedDate: '2026-01-16',
|
|
},
|
|
{
|
|
id: 'item-3',
|
|
pricingCode: '121214',
|
|
itemCode: '123125',
|
|
itemType: '반제품',
|
|
itemName: '품목명C',
|
|
specification: 'ST',
|
|
unit: 'EA',
|
|
purchasePrice: 15000,
|
|
processingCost: 5000,
|
|
marginRate: 50.0,
|
|
salesPrice: 27000,
|
|
status: '사용',
|
|
author: '이영희',
|
|
changedDate: '2026-01-17',
|
|
},
|
|
{
|
|
id: 'item-4',
|
|
pricingCode: '121215',
|
|
itemCode: '123126',
|
|
itemType: '원자재',
|
|
itemName: '품목명D',
|
|
specification: 'AL',
|
|
unit: 'KG',
|
|
purchasePrice: 5000,
|
|
processingCost: 2000,
|
|
marginRate: 60.0,
|
|
salesPrice: 10000,
|
|
status: '사용',
|
|
author: '박민수',
|
|
changedDate: '2026-01-18',
|
|
},
|
|
{
|
|
id: 'item-5',
|
|
pricingCode: '121216',
|
|
itemCode: '123127',
|
|
itemType: '완제품',
|
|
itemName: '품목명E',
|
|
specification: '규격E',
|
|
unit: 'SET',
|
|
purchasePrice: 20000,
|
|
processingCost: 8000,
|
|
marginRate: 50.0,
|
|
salesPrice: 38000,
|
|
status: '사용',
|
|
author: '홍길동',
|
|
changedDate: '2026-01-19',
|
|
},
|
|
{
|
|
id: 'item-6',
|
|
pricingCode: '121217',
|
|
itemCode: '123128',
|
|
itemType: '반제품',
|
|
itemName: '품목명F',
|
|
specification: 'ST',
|
|
unit: 'EA',
|
|
purchasePrice: 12000,
|
|
processingCost: 4000,
|
|
marginRate: 50.0,
|
|
salesPrice: 22000,
|
|
status: '사용',
|
|
author: '김철수',
|
|
changedDate: '2026-01-20',
|
|
},
|
|
{
|
|
id: 'item-7',
|
|
pricingCode: '121218',
|
|
itemCode: '123129',
|
|
itemType: '원자재',
|
|
itemName: '품목명G',
|
|
specification: 'SUS',
|
|
unit: 'KG',
|
|
purchasePrice: 7000,
|
|
processingCost: 3000,
|
|
marginRate: 45.0,
|
|
salesPrice: 13000,
|
|
status: '사용',
|
|
author: '이영희',
|
|
changedDate: '2026-01-21',
|
|
},
|
|
];
|
|
|
|
const MOCK_LIST: PriceDistributionListItem[] = [
|
|
{
|
|
id: '1',
|
|
distributionNo: '121212',
|
|
distributionName: '2025년 1월',
|
|
status: 'finalized',
|
|
author: '김대표',
|
|
createdAt: '2026-01-15',
|
|
revisionCount: 3,
|
|
},
|
|
{
|
|
id: '2',
|
|
distributionNo: '121213',
|
|
distributionName: '2025년 1월',
|
|
status: 'revision',
|
|
author: '김대표',
|
|
createdAt: '2026-01-20',
|
|
revisionCount: 1,
|
|
},
|
|
{
|
|
id: '3',
|
|
distributionNo: '121214',
|
|
distributionName: '2025년 1월',
|
|
status: 'initial',
|
|
author: '김대표',
|
|
createdAt: '2026-01-25',
|
|
revisionCount: 0,
|
|
},
|
|
{
|
|
id: '4',
|
|
distributionNo: '121215',
|
|
distributionName: '2025년 1월',
|
|
status: 'revision',
|
|
author: '김대표',
|
|
createdAt: '2026-01-28',
|
|
revisionCount: 2,
|
|
},
|
|
{
|
|
id: '5',
|
|
distributionNo: '121216',
|
|
distributionName: '2025년 1월',
|
|
status: 'finalized',
|
|
author: '김대표',
|
|
createdAt: '2026-02-01',
|
|
revisionCount: 4,
|
|
},
|
|
{
|
|
id: '6',
|
|
distributionNo: '121217',
|
|
distributionName: '2025년 1월',
|
|
status: 'initial',
|
|
author: '김대표',
|
|
createdAt: '2026-02-03',
|
|
revisionCount: 0,
|
|
},
|
|
{
|
|
id: '7',
|
|
distributionNo: '121218',
|
|
distributionName: '2025년 1월',
|
|
status: 'revision',
|
|
author: '김대표',
|
|
createdAt: '2026-02-03',
|
|
revisionCount: 1,
|
|
},
|
|
];
|
|
|
|
const MOCK_DETAILS: Record<string, PriceDistributionDetail> = {
|
|
'1': {
|
|
id: '1',
|
|
distributionNo: '121212',
|
|
distributionName: '2025년 1월',
|
|
status: 'finalized',
|
|
createdAt: '2026-01-15',
|
|
documentNo: '',
|
|
effectiveDate: '2026-01-01',
|
|
officePhone: '02-1234-1234',
|
|
orderPhone: '02-1234-1234',
|
|
author: '김대표',
|
|
items: MOCK_ITEMS,
|
|
},
|
|
'2': {
|
|
id: '2',
|
|
distributionNo: '121213',
|
|
distributionName: '2025년 1월',
|
|
status: 'revision',
|
|
createdAt: '2026-01-20',
|
|
documentNo: '',
|
|
effectiveDate: '2026-01-01',
|
|
officePhone: '02-1234-1234',
|
|
orderPhone: '02-1234-1234',
|
|
author: '김대표',
|
|
items: MOCK_ITEMS.slice(0, 5),
|
|
},
|
|
'3': {
|
|
id: '3',
|
|
distributionNo: '121214',
|
|
distributionName: '2025년 1월',
|
|
status: 'initial',
|
|
createdAt: '2026-01-25',
|
|
documentNo: '',
|
|
effectiveDate: '2026-02-01',
|
|
officePhone: '02-1234-1234',
|
|
orderPhone: '02-1234-1234',
|
|
author: '김대표',
|
|
items: MOCK_ITEMS.slice(0, 3),
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// API 함수 (목데이터 기반)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 단가배포 목록 조회
|
|
*/
|
|
export async function getPriceDistributionList(params?: {
|
|
page?: number;
|
|
size?: number;
|
|
q?: string;
|
|
status?: DistributionStatus;
|
|
dateFrom?: string;
|
|
dateTo?: string;
|
|
}): Promise<{
|
|
success: boolean;
|
|
data?: { items: PriceDistributionListItem[]; total: number };
|
|
error?: string;
|
|
}> {
|
|
let items = [...MOCK_LIST];
|
|
|
|
if (params?.status) {
|
|
items = items.filter((item) => item.status === params.status);
|
|
}
|
|
|
|
if (params?.q) {
|
|
const q = params.q.toLowerCase();
|
|
items = items.filter(
|
|
(item) =>
|
|
item.distributionNo.toLowerCase().includes(q) ||
|
|
item.distributionName.toLowerCase().includes(q) ||
|
|
item.author.toLowerCase().includes(q)
|
|
);
|
|
}
|
|
|
|
if (params?.dateFrom) {
|
|
items = items.filter((item) => item.createdAt >= params.dateFrom!);
|
|
}
|
|
|
|
if (params?.dateTo) {
|
|
items = items.filter((item) => item.createdAt <= params.dateTo!);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: { items, total: items.length },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 단가배포 통계
|
|
*/
|
|
export async function getPriceDistributionStats(): Promise<{
|
|
success: boolean;
|
|
data?: PriceDistributionStats;
|
|
error?: string;
|
|
}> {
|
|
const total = MOCK_LIST.length;
|
|
const initial = MOCK_LIST.filter((p) => p.status === 'initial').length;
|
|
const revision = MOCK_LIST.filter((p) => p.status === 'revision').length;
|
|
const finalized = MOCK_LIST.filter((p) => p.status === 'finalized').length;
|
|
return { success: true, data: { total, initial, revision, finalized } };
|
|
}
|
|
|
|
/**
|
|
* 단가배포 상세 조회
|
|
*/
|
|
export async function getPriceDistributionById(id: string): Promise<{
|
|
success: boolean;
|
|
data?: PriceDistributionDetail;
|
|
error?: string;
|
|
}> {
|
|
const detail = MOCK_DETAILS[id];
|
|
if (!detail) {
|
|
// 목록에 있는 항목은 기본 상세 데이터 생성
|
|
const listItem = MOCK_LIST.find((item) => item.id === id);
|
|
if (listItem) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
id: listItem.id,
|
|
distributionNo: listItem.distributionNo,
|
|
distributionName: listItem.distributionName,
|
|
status: listItem.status,
|
|
createdAt: listItem.createdAt,
|
|
documentNo: '',
|
|
effectiveDate: '2026-01-01',
|
|
officePhone: '02-1234-1234',
|
|
orderPhone: '02-1234-1234',
|
|
author: listItem.author,
|
|
items: MOCK_ITEMS,
|
|
},
|
|
};
|
|
}
|
|
return { success: false, error: '단가배포를 찾을 수 없습니다.' };
|
|
}
|
|
return { success: true, data: detail };
|
|
}
|
|
|
|
/**
|
|
* 단가배포 등록 (현재 단가표 기준 자동 생성)
|
|
*/
|
|
export async function createPriceDistribution(): Promise<{
|
|
success: boolean;
|
|
data?: PriceDistributionDetail;
|
|
error?: string;
|
|
}> {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = now.getMonth() + 1;
|
|
let name = `${year}년 ${month}월`;
|
|
|
|
// 중복 체크 → (N) 추가
|
|
const existing = MOCK_LIST.filter((item) =>
|
|
item.distributionName.startsWith(name)
|
|
);
|
|
if (existing.length > 0) {
|
|
name = `${name}(${existing.length})`;
|
|
}
|
|
|
|
const newId = String(Date.now());
|
|
const newItem: PriceDistributionDetail = {
|
|
id: newId,
|
|
distributionNo: String(Math.floor(100000 + Math.random() * 900000)),
|
|
distributionName: name,
|
|
status: 'initial',
|
|
createdAt: now.toISOString().split('T')[0],
|
|
documentNo: '',
|
|
effectiveDate: now.toISOString().split('T')[0],
|
|
officePhone: '',
|
|
orderPhone: '',
|
|
author: '현재사용자',
|
|
items: MOCK_ITEMS,
|
|
};
|
|
|
|
return { success: true, data: newItem };
|
|
}
|
|
|
|
/**
|
|
* 단가배포 수정
|
|
*/
|
|
export async function updatePriceDistribution(
|
|
id: string,
|
|
data: PriceDistributionFormData
|
|
): Promise<{
|
|
success: boolean;
|
|
data?: PriceDistributionDetail;
|
|
error?: string;
|
|
}> {
|
|
const detailData = MOCK_DETAILS[id];
|
|
const listItem = MOCK_LIST.find((item) => item.id === id);
|
|
if (!detailData && !listItem) {
|
|
return { success: false, error: '단가배포를 찾을 수 없습니다.' };
|
|
}
|
|
|
|
const detail: PriceDistributionDetail = detailData || {
|
|
id,
|
|
distributionNo: listItem!.distributionNo,
|
|
distributionName: data.distributionName,
|
|
status: 'revision' as DistributionStatus,
|
|
createdAt: listItem!.createdAt,
|
|
documentNo: data.documentNo,
|
|
effectiveDate: data.effectiveDate,
|
|
officePhone: data.officePhone,
|
|
orderPhone: data.orderPhone,
|
|
author: listItem!.author,
|
|
items: MOCK_ITEMS,
|
|
};
|
|
|
|
const updated: PriceDistributionDetail = {
|
|
...detail,
|
|
distributionName: data.distributionName,
|
|
documentNo: data.documentNo,
|
|
effectiveDate: data.effectiveDate,
|
|
officePhone: data.officePhone,
|
|
orderPhone: data.orderPhone,
|
|
status: detail.status === 'initial' ? 'revision' : detail.status,
|
|
};
|
|
|
|
return { success: true, data: updated };
|
|
}
|
|
|
|
/**
|
|
* 단가배포 최종확정
|
|
*/
|
|
export async function finalizePriceDistribution(id: string): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
const exists = MOCK_LIST.find((item) => item.id === id) || MOCK_DETAILS[id];
|
|
if (!exists) {
|
|
return { success: false, error: '단가배포를 찾을 수 없습니다.' };
|
|
}
|
|
return { success: true };
|
|
}
|
|
|
|
/**
|
|
* 단가배포 삭제
|
|
*/
|
|
export async function deletePriceDistribution(ids: string[]): Promise<{
|
|
success: boolean;
|
|
deletedCount?: number;
|
|
error?: string;
|
|
}> {
|
|
return { success: true, deletedCount: ids.length };
|
|
}
|