refactor(WEB): Server Component → Client Component 전면 마이그레이션
- 53개 페이지를 Server Component에서 Client Component로 변환 - Next.js 15에서 Server Component 렌더링 중 쿠키 수정 불가 이슈 해결 - 폐쇄형 ERP 시스템 특성상 SEO 불필요, Client Component 사용이 적합 주요 변경사항: - 모든 페이지에 'use client' 지시어 추가 - use(params) 훅으로 async params 처리 - useState + useEffect로 데이터 페칭 패턴 적용 - skipTokenRefresh 옵션 및 관련 코드 제거 (더 이상 필요 없음) 변환된 페이지: - Settings: 4개 (account-info, notification-settings, permissions, popup-management) - Accounting: 9개 (vendors, sales, deposits, bills, withdrawals, expected-expenses, bad-debt-collection) - Sales: 4개 (quote-management, pricing-management) - Production/Quality/Master-data: 6개 - Material/Outbound: 4개 - Construction: 22개 - Other: 4개 (payment-history, subscription, dev/test-urls) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -198,7 +198,7 @@ export function DashboardSettingsDialog({
|
||||
onClose();
|
||||
}, [settings, onClose]);
|
||||
|
||||
// 커스텀 스위치 (ON/OFF 라벨 포함)
|
||||
// 커스텀 스위치 (라이트 테마용)
|
||||
const ToggleSwitch = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
@@ -210,36 +210,20 @@ export function DashboardSettingsDialog({
|
||||
type="button"
|
||||
onClick={() => onCheckedChange(!checked)}
|
||||
className={cn(
|
||||
'relative inline-flex h-7 w-14 items-center rounded-full transition-colors',
|
||||
checked ? 'bg-cyan-500' : 'bg-gray-300'
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
checked ? 'bg-blue-500' : 'bg-gray-300'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute left-1 text-[10px] font-medium text-white transition-opacity',
|
||||
checked ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
ON
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute right-1 text-[10px] font-medium text-gray-500 transition-opacity',
|
||||
checked ? 'opacity-0' : 'opacity-100'
|
||||
)}
|
||||
>
|
||||
OFF
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block h-5 w-5 transform rounded-full bg-white shadow-md transition-transform',
|
||||
checked ? 'translate-x-8' : 'translate-x-1'
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white shadow-md transition-transform',
|
||||
checked ? 'translate-x-6' : 'translate-x-1'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
// 섹션 행 컴포넌트
|
||||
// 섹션 행 컴포넌트 (라이트 테마)
|
||||
const SectionRow = ({
|
||||
label,
|
||||
checked,
|
||||
@@ -258,11 +242,16 @@ export function DashboardSettingsDialog({
|
||||
children?: React.ReactNode;
|
||||
}) => (
|
||||
<Collapsible open={isExpanded} onOpenChange={onToggleExpand}>
|
||||
<div className="flex items-center justify-between py-2 border-b border-gray-100">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between py-3 px-4 bg-gray-200',
|
||||
children && isExpanded ? 'rounded-t-lg' : 'rounded-lg'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{hasExpand && (
|
||||
<CollapsibleTrigger asChild>
|
||||
<button type="button" className="p-1 hover:bg-gray-100 rounded">
|
||||
<button type="button" className="p-1 hover:bg-gray-300 rounded">
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
@@ -271,12 +260,12 @@ export function DashboardSettingsDialog({
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
<span className="text-sm font-medium text-gray-800">{label}</span>
|
||||
</div>
|
||||
<ToggleSwitch checked={checked} onCheckedChange={onCheckedChange} />
|
||||
</div>
|
||||
{children && (
|
||||
<CollapsibleContent className="pl-6 py-2 space-y-3 bg-gray-50">
|
||||
<CollapsibleContent className="px-4 py-3 space-y-3 bg-gray-50 rounded-b-lg">
|
||||
{children}
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
@@ -285,30 +274,30 @@ export function DashboardSettingsDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && handleCancel()}>
|
||||
<DialogContent className="w-[95vw] max-w-[450px] sm:max-w-[450px] max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-bold">항목 설정</DialogTitle>
|
||||
<DialogContent className="w-[95vw] max-w-[450px] sm:max-w-[450px] max-h-[85vh] overflow-y-auto bg-white border-gray-200 p-0">
|
||||
<DialogHeader className="p-4 border-b border-gray-200">
|
||||
<DialogTitle className="text-lg font-bold text-gray-900">항목 설정</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-3 p-4">
|
||||
{/* 오늘의 이슈 섹션 */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between py-2 border-b-2 border-gray-200">
|
||||
<span className="text-sm font-semibold">오늘의 이슈</span>
|
||||
<div className="space-y-0 rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between py-3 px-4 bg-gray-200">
|
||||
<span className="text-sm font-medium text-gray-800">오늘의 이슈</span>
|
||||
<ToggleSwitch
|
||||
checked={localSettings.todayIssue.enabled}
|
||||
onCheckedChange={handleTodayIssueToggle}
|
||||
/>
|
||||
</div>
|
||||
{localSettings.todayIssue.enabled && (
|
||||
<div className="pl-4 space-y-1">
|
||||
<div className="bg-gray-50">
|
||||
{(Object.keys(TODAY_ISSUE_LABELS) as Array<keyof TodayIssueSettings>).map(
|
||||
(key) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between py-1.5"
|
||||
className="flex items-center justify-between py-2.5 px-6 border-t border-gray-200"
|
||||
>
|
||||
<span className="text-sm text-gray-700">
|
||||
<span className="text-sm text-gray-600">
|
||||
{TODAY_ISSUE_LABELS[key]}
|
||||
</span>
|
||||
<ToggleSwitch
|
||||
@@ -408,36 +397,36 @@ export function DashboardSettingsDialog({
|
||||
)}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-2 p-3 bg-white border rounded text-xs space-y-4">
|
||||
<CollapsibleContent className="mt-2 p-3 bg-white border border-gray-200 rounded text-xs space-y-4">
|
||||
{/* ■ 중소기업 판단 기준표 */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-bold">■</span>
|
||||
<span className="font-bold text-gray-800">■</span>
|
||||
<span className="text-sm font-medium text-gray-800">중소기업 판단 기준표</span>
|
||||
</div>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border px-2 py-1 text-center">조건</th>
|
||||
<th className="border px-2 py-1 text-center">기준</th>
|
||||
<th className="border px-2 py-1 text-center">충족 요건</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">조건</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">기준</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">충족 요건</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">① 매출액</td>
|
||||
<td className="border px-2 py-1 text-center">업종별 상이</td>
|
||||
<td className="border px-2 py-1 text-center">업종별 기준 금액 이하</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">① 매출액</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">업종별 상이</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">업종별 기준 금액 이하</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">② 자산총액</td>
|
||||
<td className="border px-2 py-1 text-center">5,000억원</td>
|
||||
<td className="border px-2 py-1 text-center">미만</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">② 자산총액</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">5,000억원</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">미만</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">③ 독립성</td>
|
||||
<td className="border px-2 py-1 text-center">소유·경영</td>
|
||||
<td className="border px-2 py-1 text-center">대기업 계열 아님</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">③ 독립성</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">소유·경영</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">대기업 계열 아님</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -451,20 +440,20 @@ export function DashboardSettingsDialog({
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border px-2 py-1 text-center">업종 분류</th>
|
||||
<th className="border px-2 py-1 text-center">기준 매출액</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">업종 분류</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">기준 매출액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="border px-2 py-1 text-center">제조업</td><td className="border px-2 py-1 text-center">1,500억원 이하</td></tr>
|
||||
<tr><td className="border px-2 py-1 text-center">건설업</td><td className="border px-2 py-1 text-center">1,000억원 이하</td></tr>
|
||||
<tr><td className="border px-2 py-1 text-center">운수업</td><td className="border px-2 py-1 text-center">1,000억원 이하</td></tr>
|
||||
<tr><td className="border px-2 py-1 text-center">도매업</td><td className="border px-2 py-1 text-center">1,000억원 이하</td></tr>
|
||||
<tr><td className="border px-2 py-1 text-center">소매업</td><td className="border px-2 py-1 text-center">600억원 이하</td></tr>
|
||||
<tr><td className="border px-2 py-1 text-center">정보통신업</td><td className="border px-2 py-1 text-center">600억원 이하</td></tr>
|
||||
<tr><td className="border px-2 py-1 text-center">전문서비스업</td><td className="border px-2 py-1 text-center">600억원 이하</td></tr>
|
||||
<tr><td className="border px-2 py-1 text-center">숙박·음식점업</td><td className="border px-2 py-1 text-center">400억원 이하</td></tr>
|
||||
<tr><td className="border px-2 py-1 text-center">기타 서비스업</td><td className="border px-2 py-1 text-center">400억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">제조업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">1,500억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">건설업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">1,000억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">운수업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">1,000억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">도매업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">1,000억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">소매업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">600억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">정보통신업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">600억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">전문서비스업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">600억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">숙박·음식점업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">400억원 이하</td></tr>
|
||||
<tr><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">기타 서비스업</td><td className="border border-gray-200 px-2 py-1 text-center text-gray-600">400억원 이하</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -477,14 +466,14 @@ export function DashboardSettingsDialog({
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border px-2 py-1 text-center">구분</th>
|
||||
<th className="border px-2 py-1 text-center">기준</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">구분</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">기준</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">5,000억원 미만</td>
|
||||
<td className="border px-2 py-1 text-center">직전 사업연도 말 자산총액</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">5,000억원 미만</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">직전 사업연도 말 자산총액</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -498,31 +487,31 @@ export function DashboardSettingsDialog({
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border px-2 py-1 text-center">구분</th>
|
||||
<th className="border px-2 py-1 text-center">내용</th>
|
||||
<th className="border px-2 py-1 text-center">판정</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">구분</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">내용</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">판정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">독립기업</td>
|
||||
<td className="border px-2 py-1">아래 항목에 모두 해당하지 않음</td>
|
||||
<td className="border px-2 py-1 text-center">충족</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">독립기업</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-gray-600">아래 항목에 모두 해당하지 않음</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">충족</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">기업집단 소속</td>
|
||||
<td className="border px-2 py-1">공정거래법상 상호출자제한 기업집단 소속</td>
|
||||
<td className="border px-2 py-1 text-center">미충족</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">기업집단 소속</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-gray-600">공정거래법상 상호출자제한 기업집단 소속</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">미충족</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">대기업 지분</td>
|
||||
<td className="border px-2 py-1">대기업이 발행주식 30% 이상 보유</td>
|
||||
<td className="border px-2 py-1 text-center">미충족</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">대기업 지분</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-gray-600">대기업이 발행주식 30% 이상 보유</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">미충족</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">관계기업 합산</td>
|
||||
<td className="border px-2 py-1">관계기업 포함 시 매출액·자산 기준 초과</td>
|
||||
<td className="border px-2 py-1 text-center">미충족</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">관계기업 합산</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-gray-600">관계기업 포함 시 매출액·자산 기준 초과</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">미충족</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -531,27 +520,27 @@ export function DashboardSettingsDialog({
|
||||
{/* ■ 판정 결과 */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-bold">■</span>
|
||||
<span className="font-bold text-gray-800">■</span>
|
||||
<span className="text-sm font-medium text-gray-800">판정 결과</span>
|
||||
</div>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border px-2 py-1 text-center">판정</th>
|
||||
<th className="border px-2 py-1 text-center">조건</th>
|
||||
<th className="border px-2 py-1 text-center">접대비 기본한도</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">판정</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">조건</th>
|
||||
<th className="border border-gray-300 px-2 py-1 text-center text-gray-700">접대비 기본한도</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">중소기업</td>
|
||||
<td className="border px-2 py-1 text-center">①②③ 모두 충족</td>
|
||||
<td className="border px-2 py-1 text-center">3,600만원</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">중소기업</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">①②③ 모두 충족</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">3,600만원</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">일반법인</td>
|
||||
<td className="border px-2 py-1 text-center">①②③ 중 하나라도 미충족</td>
|
||||
<td className="border px-2 py-1 text-center">1,200만원</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">일반법인</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">①②③ 중 하나라도 미충족</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center text-gray-600">1,200만원</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -699,11 +688,18 @@ export function DashboardSettingsDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2 sm:justify-center">
|
||||
<Button variant="outline" onClick={handleCancel} className="w-20">
|
||||
<DialogFooter className="flex gap-3 p-4 border-t border-gray-200 sm:justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
className="w-20 bg-gray-100 hover:bg-gray-200 text-gray-700 border-gray-300 rounded-full"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="w-20 bg-blue-600 hover:bg-blue-700">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="w-20 bg-gray-500 hover:bg-gray-600 text-white rounded-full"
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -475,6 +475,186 @@ export async function finalizePricing(id: string): Promise<{ success: boolean; d
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 품목 목록 + 단가 목록 병합 조회
|
||||
// ============================================
|
||||
|
||||
// 품목 API 응답 타입 (GET /api/v1/items)
|
||||
interface ItemApiData {
|
||||
id: number;
|
||||
item_type: string; // FG, PT, SM, RM, CS (품목 유형)
|
||||
code: string;
|
||||
name: string;
|
||||
unit: string;
|
||||
category_id: number | null;
|
||||
created_at: string;
|
||||
deleted_at: string | null;
|
||||
}
|
||||
|
||||
// 단가 목록 조회용 타입
|
||||
interface PriceApiListItem {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
item_type_code: string;
|
||||
item_id: number;
|
||||
client_group_id: number | null;
|
||||
purchase_price: string | null;
|
||||
processing_cost: string | null;
|
||||
loss_rate: string | null;
|
||||
margin_rate: string | null;
|
||||
sales_price: string | null;
|
||||
rounding_rule: 'round' | 'ceil' | 'floor';
|
||||
rounding_unit: number;
|
||||
supplier: string | null;
|
||||
effective_from: string;
|
||||
effective_to: string | null;
|
||||
status: 'draft' | 'active' | 'finalized';
|
||||
is_final: boolean;
|
||||
finalized_at: string | null;
|
||||
finalized_by: number | null;
|
||||
note: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: string | null;
|
||||
}
|
||||
|
||||
// 목록 표시용 타입
|
||||
export interface PricingListItem {
|
||||
id: string;
|
||||
itemId: string;
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
itemType: string;
|
||||
specification?: string;
|
||||
unit: string;
|
||||
purchasePrice?: number;
|
||||
processingCost?: number;
|
||||
salesPrice?: number;
|
||||
marginRate?: number;
|
||||
effectiveDate?: string;
|
||||
status: 'draft' | 'active' | 'finalized' | 'not_registered';
|
||||
currentRevision: number;
|
||||
isFinal: boolean;
|
||||
itemTypeCode: string;
|
||||
}
|
||||
|
||||
// 품목 유형 매핑 (type_code → 프론트엔드 ItemType)
|
||||
function mapItemTypeForList(typeCode?: string): string {
|
||||
switch (typeCode) {
|
||||
case 'FG': return 'FG';
|
||||
case 'PT': return 'PT';
|
||||
case 'SM': return 'SM';
|
||||
case 'RM': return 'RM';
|
||||
case 'CS': return 'CS';
|
||||
default: return 'PT';
|
||||
}
|
||||
}
|
||||
|
||||
// API 상태 → 프론트엔드 상태 매핑
|
||||
function mapStatusForList(apiStatus: string, isFinal: boolean): 'draft' | 'active' | 'finalized' | 'not_registered' {
|
||||
if (isFinal) return 'finalized';
|
||||
switch (apiStatus) {
|
||||
case 'draft': return 'draft';
|
||||
case 'active': return 'active';
|
||||
case 'finalized': return 'finalized';
|
||||
default: return 'draft';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 단가 목록 데이터 조회 (품목 + 단가 병합)
|
||||
*/
|
||||
export async function getPricingListData(): Promise<PricingListItem[]> {
|
||||
try {
|
||||
// 품목 목록 조회
|
||||
const { response: itemsResponse, error: itemsError } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?group_id=1&size=100`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (itemsError || !itemsResponse) {
|
||||
console.error('[PricingActions] Items fetch error:', itemsError?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const itemsResult = await itemsResponse.json();
|
||||
const items: ItemApiData[] = itemsResult.success ? (itemsResult.data?.data || []) : [];
|
||||
|
||||
// 단가 목록 조회
|
||||
const { response: pricingResponse, error: pricingError } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing?size=100`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (pricingError || !pricingResponse) {
|
||||
console.error('[PricingActions] Pricing fetch error:', pricingError?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const pricingResult = await pricingResponse.json();
|
||||
const pricings: PriceApiListItem[] = pricingResult.success ? (pricingResult.data?.data || []) : [];
|
||||
|
||||
// 단가 정보를 빠르게 찾기 위한 Map 생성
|
||||
const pricingMap = new Map<string, PriceApiListItem>();
|
||||
for (const pricing of pricings) {
|
||||
const key = `${pricing.item_type_code}_${pricing.item_id}`;
|
||||
if (!pricingMap.has(key)) {
|
||||
pricingMap.set(key, pricing);
|
||||
}
|
||||
}
|
||||
|
||||
// 품목 목록을 기준으로 병합
|
||||
return items.map((item) => {
|
||||
const key = `${item.item_type}_${item.id}`;
|
||||
const pricing = pricingMap.get(key);
|
||||
|
||||
if (pricing) {
|
||||
return {
|
||||
id: String(pricing.id),
|
||||
itemId: String(item.id),
|
||||
itemCode: item.code,
|
||||
itemName: item.name,
|
||||
itemType: mapItemTypeForList(item.item_type),
|
||||
specification: undefined,
|
||||
unit: item.unit || 'EA',
|
||||
purchasePrice: pricing.purchase_price ? parseFloat(pricing.purchase_price) : undefined,
|
||||
processingCost: pricing.processing_cost ? parseFloat(pricing.processing_cost) : undefined,
|
||||
salesPrice: pricing.sales_price ? parseFloat(pricing.sales_price) : undefined,
|
||||
marginRate: pricing.margin_rate ? parseFloat(pricing.margin_rate) : undefined,
|
||||
effectiveDate: pricing.effective_from,
|
||||
status: mapStatusForList(pricing.status, pricing.is_final),
|
||||
currentRevision: 0,
|
||||
isFinal: pricing.is_final,
|
||||
itemTypeCode: item.item_type,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: `item_${item.id}`,
|
||||
itemId: String(item.id),
|
||||
itemCode: item.code,
|
||||
itemName: item.name,
|
||||
itemType: mapItemTypeForList(item.item_type),
|
||||
specification: undefined,
|
||||
unit: item.unit || 'EA',
|
||||
purchasePrice: undefined,
|
||||
processingCost: undefined,
|
||||
salesPrice: undefined,
|
||||
marginRate: undefined,
|
||||
effectiveDate: undefined,
|
||||
status: 'not_registered' as const,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
itemTypeCode: item.item_type,
|
||||
};
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PricingActions] getPricingListData error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 단가 이력 조회
|
||||
*/
|
||||
|
||||
@@ -35,10 +35,10 @@ interface CategorySectionProps {
|
||||
|
||||
function CategorySection({ title, enabled, onEnabledChange, children }: CategorySectionProps) {
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-100 rounded-lg overflow-hidden">
|
||||
{/* 카테고리 헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-white font-medium">{title}</span>
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-gray-200">
|
||||
<span className="text-gray-800 font-medium">{title}</span>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={onEnabledChange}
|
||||
@@ -46,7 +46,7 @@ function CategorySection({ title, enabled, onEnabledChange, children }: Category
|
||||
/>
|
||||
</div>
|
||||
{/* 하위 항목 */}
|
||||
<div className="bg-gray-700 px-4 py-2 space-y-2">
|
||||
<div className="bg-gray-50 px-4 py-2 space-y-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@ interface ItemRowProps {
|
||||
function ItemRow({ label, checked, onChange, disabled }: ItemRowProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-gray-300 text-sm">{label}</span>
|
||||
<span className="text-gray-600 text-sm">{label}</span>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
@@ -133,17 +133,17 @@ export function ItemSettingsDialog({ isOpen, onClose, settings, onSave }: ItemSe
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="!w-[400px] !max-w-[400px] max-h-[80vh] overflow-y-auto p-0 bg-gray-900 border-gray-700">
|
||||
<DialogContent className="!w-[400px] !max-w-[400px] max-h-[80vh] overflow-y-auto p-0 bg-white border-gray-200">
|
||||
{/* 헤더 */}
|
||||
<DialogHeader className="sticky top-0 bg-gray-900 z-10 px-4 py-3 border-b border-gray-700">
|
||||
<DialogHeader className="sticky top-0 bg-white z-10 px-4 py-3 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="text-white font-medium">항목 설정</DialogTitle>
|
||||
<DialogTitle className="text-gray-900 font-medium">항목 설정</DialogTitle>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-800 rounded transition-colors"
|
||||
className="p-1 hover:bg-gray-100 rounded transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5 text-gray-400" />
|
||||
<X className="h-5 w-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
@@ -315,17 +315,17 @@ export function ItemSettingsDialog({ isOpen, onClose, settings, onSave }: ItemSe
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="sticky bottom-0 bg-gray-900 px-4 py-3 border-t border-gray-700 flex justify-center gap-3">
|
||||
<div className="sticky bottom-0 bg-white px-4 py-3 border-t border-gray-200 flex justify-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="bg-gray-700 border-gray-600 text-white hover:bg-gray-600 min-w-[80px]"
|
||||
className="bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200 min-w-[80px] rounded-full"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-gray-700 text-white hover:bg-gray-600 min-w-[80px]"
|
||||
className="bg-gray-500 text-white hover:bg-gray-600 min-w-[80px] rounded-full"
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user