feat(WEB): 입찰/계약/주문관리 기능 추가 및 견적 상세 리팩토링

- 입찰관리: 목록/상세/수정 페이지 및 목업 데이터
- 계약관리: 목록/상세/수정 페이지 구현
- 주문관리: 수주/발주 목록 및 상세 페이지 구현
- 견적 상세 폼: 섹션별 분리 및 hooks/utils 리팩토링
- 품목관리, 카테고리관리, 단가관리 기능 추가
- 현장설명회/협력업체 폼 개선
- 프린트 유틸리티 공통화 (print-utils.ts)
- 문서 모달 공통 컴포넌트 정리
- IntegratedListTemplateV2, StatCards 개선

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-05 18:59:04 +09:00
parent 4b1a3abf05
commit 386cd30bc0
145 changed files with 25782 additions and 254 deletions

View File

@@ -0,0 +1,5 @@
import { CategoryManagement } from '@/components/business/juil/category-management';
export default function CategoriesPage() {
return <CategoryManagement />;
}

View File

@@ -0,0 +1,14 @@
import { ItemDetailClient } from '@/components/business/juil/item-management';
interface ItemDetailPageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ mode?: string }>;
}
export default async function ItemDetailPage({ params, searchParams }: ItemDetailPageProps) {
const { id } = await params;
const { mode } = await searchParams;
const isEditMode = mode === 'edit';
return <ItemDetailClient itemId={id} isEditMode={isEditMode} />;
}

View File

@@ -0,0 +1,5 @@
import { ItemDetailClient } from '@/components/business/juil/item-management';
export default function ItemNewPage() {
return <ItemDetailClient isNewMode />;
}

View File

@@ -0,0 +1,5 @@
import { ItemManagementClient } from '@/components/business/juil/item-management';
export default function ItemManagementPage() {
return <ItemManagementClient />;
}

View File

@@ -0,0 +1,14 @@
import { LaborDetailClient } from '@/components/business/juil/labor-management';
interface LaborDetailPageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ mode?: string }>;
}
export default async function LaborDetailPage({ params, searchParams }: LaborDetailPageProps) {
const { id } = await params;
const { mode } = await searchParams;
const isEditMode = mode === 'edit';
return <LaborDetailClient laborId={id} isEditMode={isEditMode} />;
}

View File

@@ -0,0 +1,5 @@
import { LaborDetailClient } from '@/components/business/juil/labor-management';
export default function LaborNewPage() {
return <LaborDetailClient isNewMode />;
}

View File

@@ -0,0 +1,5 @@
import { LaborManagementClient } from '@/components/business/juil/labor-management';
export default function LaborManagementPage() {
return <LaborManagementClient />;
}

View File

@@ -0,0 +1,11 @@
import PricingDetailClient from '@/components/business/juil/pricing-management/PricingDetailClient';
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function PricingEditPage({ params }: PageProps) {
const { id } = await params;
return <PricingDetailClient id={id} mode="edit" />;
}

View File

@@ -0,0 +1,11 @@
import PricingDetailClient from '@/components/business/juil/pricing-management/PricingDetailClient';
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function PricingDetailPage({ params }: PageProps) {
const { id } = await params;
return <PricingDetailClient id={id} mode="view" />;
}

View File

@@ -0,0 +1,5 @@
import PricingDetailClient from '@/components/business/juil/pricing-management/PricingDetailClient';
export default function PricingNewPage() {
return <PricingDetailClient mode="create" />;
}

View File

@@ -0,0 +1,5 @@
import PricingListClient from '@/components/business/juil/pricing-management/PricingListClient';
export default function PricingPage() {
return <PricingListClient />;
}

View File

@@ -0,0 +1,19 @@
import { OrderDetailForm } from '@/components/business/juil/order-management';
import { getOrderDetailFull } from '@/components/business/juil/order-management/actions';
import { notFound } from 'next/navigation';
interface OrderEditPageProps {
params: Promise<{ id: string }>;
}
export default async function OrderEditPage({ params }: OrderEditPageProps) {
const { id } = await params;
const result = await getOrderDetailFull(id);
if (!result.success || !result.data) {
notFound();
}
return <OrderDetailForm mode="edit" orderId={id} initialData={result.data} />;
}

View File

@@ -0,0 +1,19 @@
import { OrderDetailForm } from '@/components/business/juil/order-management';
import { getOrderDetailFull } from '@/components/business/juil/order-management/actions';
import { notFound } from 'next/navigation';
interface OrderDetailPageProps {
params: Promise<{ id: string }>;
}
export default async function OrderDetailPage({ params }: OrderDetailPageProps) {
const { id } = await params;
const result = await getOrderDetailFull(id);
if (!result.success || !result.data) {
notFound();
}
return <OrderDetailForm mode="view" orderId={id} initialData={result.data} />;
}

View File

@@ -0,0 +1,5 @@
import { OrderManagementListClient } from '@/components/business/juil/order-management';
export default function OrderManagementPage() {
return <OrderManagementListClient />;
}

View File

@@ -0,0 +1,27 @@
import SiteDetailForm from '@/components/business/juil/site-management/SiteDetailForm';
// 목업 데이터
const MOCK_SITE = {
id: '1',
siteCode: '123-12-12345',
partnerId: '1',
partnerName: '거래처명',
siteName: '현장명',
address: '',
status: 'active' as const,
createdAt: '2025-09-01T00:00:00Z',
updatedAt: '2025-09-01T00:00:00Z',
};
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function SiteEditPage({ params }: PageProps) {
const { id } = await params;
// TODO: API에서 현장 정보 조회
const site = { ...MOCK_SITE, id };
return <SiteDetailForm site={site} mode="edit" />;
}

View File

@@ -0,0 +1,27 @@
import SiteDetailForm from '@/components/business/juil/site-management/SiteDetailForm';
// 목업 데이터
const MOCK_SITE = {
id: '1',
siteCode: '123-12-12345',
partnerId: '1',
partnerName: '거래처명',
siteName: '현장명',
address: '',
status: 'active' as const,
createdAt: '2025-09-01T00:00:00Z',
updatedAt: '2025-09-01T00:00:00Z',
};
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function SiteDetailPage({ params }: PageProps) {
const { id } = await params;
// TODO: API에서 현장 정보 조회
const site = { ...MOCK_SITE, id };
return <SiteDetailForm site={site} mode="view" />;
}

View File

@@ -0,0 +1,5 @@
import { SiteManagementListClient } from '@/components/business/juil/site-management';
export default function SiteManagementPage() {
return <SiteManagementListClient />;
}

View File

@@ -0,0 +1,32 @@
import StructureReviewDetailForm from '@/components/business/juil/structure-review/StructureReviewDetailForm';
// 목업 데이터
const MOCK_REVIEW = {
id: '1',
reviewNumber: '123123',
partnerId: '1',
partnerName: '거래처명A',
siteId: '1',
siteName: '현장A',
requestDate: '2025-12-12',
reviewCompany: '회사명',
reviewerName: '홍길동',
reviewDate: '2025-12-15',
completionDate: null,
status: 'pending' as const,
createdAt: '2025-12-01T00:00:00Z',
updatedAt: '2025-12-01T00:00:00Z',
};
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function StructureReviewEditPage({ params }: PageProps) {
const { id } = await params;
// TODO: API에서 구조검토 정보 조회
const review = { ...MOCK_REVIEW, id };
return <StructureReviewDetailForm review={review} mode="edit" />;
}

View File

@@ -0,0 +1,32 @@
import StructureReviewDetailForm from '@/components/business/juil/structure-review/StructureReviewDetailForm';
// 목업 데이터
const MOCK_REVIEW = {
id: '1',
reviewNumber: '123123',
partnerId: '1',
partnerName: '거래처명A',
siteId: '1',
siteName: '현장A',
requestDate: '2025-12-12',
reviewCompany: '회사명',
reviewerName: '홍길동',
reviewDate: '2025-12-15',
completionDate: null,
status: 'pending' as const,
createdAt: '2025-12-01T00:00:00Z',
updatedAt: '2025-12-01T00:00:00Z',
};
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function StructureReviewDetailPage({ params }: PageProps) {
const { id } = await params;
// TODO: API에서 구조검토 정보 조회
const review = { ...MOCK_REVIEW, id };
return <StructureReviewDetailForm review={review} mode="view" />;
}

View File

@@ -0,0 +1,5 @@
import StructureReviewListClient from '@/components/business/juil/structure-review/StructureReviewListClient';
export default function StructureReviewListPage() {
return <StructureReviewListClient />;
}

View File

@@ -0,0 +1,18 @@
import { BiddingDetailForm, getBiddingDetail } from '@/components/business/juil/bidding';
interface BiddingEditPageProps {
params: Promise<{ id: string }>;
}
export default async function BiddingEditPage({ params }: BiddingEditPageProps) {
const { id } = await params;
const result = await getBiddingDetail(id);
return (
<BiddingDetailForm
mode="edit"
biddingId={id}
initialData={result.data}
/>
);
}

View File

@@ -0,0 +1,18 @@
import { BiddingDetailForm, getBiddingDetail } from '@/components/business/juil/bidding';
interface BiddingDetailPageProps {
params: Promise<{ id: string }>;
}
export default async function BiddingDetailPage({ params }: BiddingDetailPageProps) {
const { id } = await params;
const result = await getBiddingDetail(id);
return (
<BiddingDetailForm
mode="view"
biddingId={id}
initialData={result.data}
/>
);
}

View File

@@ -0,0 +1,201 @@
import { EstimateDetailForm } from '@/components/business/juil/estimates';
import type { EstimateDetail } from '@/components/business/juil/estimates';
interface EstimateEditPageProps {
params: Promise<{ id: string }>;
}
// 목업 데이터 - 추후 API 연동
async function getEstimateDetail(id: string): Promise<EstimateDetail | null> {
// TODO: 실제 API 연동
const mockData: EstimateDetail = {
id,
estimateCode: '123123',
partnerId: '1',
partnerName: '회사명',
projectName: '현장명',
estimatorId: 'hong',
estimatorName: '이름',
itemCount: 21,
estimateAmount: 1420000,
completedDate: null,
bidDate: '2025-12-12',
status: 'pending',
createdAt: '2025-12-01',
updatedAt: '2025-12-01',
createdBy: 'hong',
siteBriefing: {
briefingCode: '123123',
partnerName: '회사명',
companyName: '회사명',
briefingDate: '2025-12-12',
attendee: '이름',
},
bidInfo: {
projectName: '현장명',
bidDate: '2025-12-12',
siteCount: 21,
constructionPeriod: '2026-01-01 ~ 2026-12-10',
constructionStartDate: '2026-01-01',
constructionEndDate: '2026-12-10',
vatType: 'excluded',
workReport: '업무 보고 내용',
documents: [
{
id: '1',
fileName: 'abc.zip',
fileUrl: '#',
fileSize: 1024000,
},
],
},
summaryItems: [
{
id: '1',
name: '서터 심창측공사',
quantity: 1,
unit: '식',
materialCost: 78540000,
laborCost: 15410000,
totalCost: 93950000,
remarks: '',
},
],
expenseItems: [
{
id: '1',
name: 'public_1',
amount: 10000,
},
],
priceAdjustments: [
{
id: '1',
category: '배합비',
unitPrice: 10000,
coating: 10000,
batting: 10000,
boxReinforce: 10500,
painting: 10500,
total: 51000,
},
{
id: '2',
category: '재단비',
unitPrice: 1375,
coating: 0,
batting: 0,
boxReinforce: 0,
painting: 0,
total: 1375,
},
{
id: '3',
category: '판매단가',
unitPrice: 0,
coating: 10000,
batting: 10000,
boxReinforce: 10500,
painting: 10500,
total: 41000,
},
{
id: '4',
category: '조립단가',
unitPrice: 10300,
coating: 10300,
batting: 10300,
boxReinforce: 10500,
painting: 10200,
total: 51600,
},
],
detailItems: [
{
id: '1',
no: 1,
name: 'FS530외/주차',
material: 'screen',
width: 2350,
height: 2500,
quantity: 1,
box: 1,
assembly: 0,
coating: 0,
batting: 0,
mounting: 0,
fitting: 0,
controller: 0,
widthConstruction: 0,
heightConstruction: 0,
materialCost: 1420000,
laborCost: 510000,
quantityPrice: 1930000,
expenseQuantity: 5500,
expenseTotal: 5500,
totalCost: 1930000,
otherCost: 0,
marginCost: 0,
totalPrice: 1930000,
unitPrice: 1420000,
expense: 0,
marginRate: 0,
unitQuantity: 1,
expenseResult: 0,
marginActual: 0,
},
{
id: '2',
no: 2,
name: 'FS530외/주차',
material: 'screen',
width: 7500,
height: 2500,
quantity: 1,
box: 1,
assembly: 0,
coating: 0,
batting: 0,
mounting: 0,
fitting: 0,
controller: 0,
widthConstruction: 0,
heightConstruction: 0,
materialCost: 4720000,
laborCost: 780000,
quantityPrice: 5500000,
expenseQuantity: 5500,
expenseTotal: 5500,
totalCost: 5500000,
otherCost: 0,
marginCost: 0,
totalPrice: 5500000,
unitPrice: 4720000,
expense: 0,
marginRate: 0,
unitQuantity: 1,
expenseResult: 0,
marginActual: 0,
},
],
approval: {
approvers: [],
references: [],
},
};
return mockData;
}
export default async function EstimateEditPage({ params }: EstimateEditPageProps) {
const { id } = await params;
const detail = await getEstimateDetail(id);
return (
<EstimateDetailForm
mode="edit"
estimateId={id}
initialData={detail || undefined}
/>
);
}

View File

@@ -0,0 +1,201 @@
import { EstimateDetailForm } from '@/components/business/juil/estimates';
import type { EstimateDetail } from '@/components/business/juil/estimates';
interface EstimateDetailPageProps {
params: Promise<{ id: string }>;
}
// 목업 데이터 - 추후 API 연동
async function getEstimateDetail(id: string): Promise<EstimateDetail | null> {
// TODO: 실제 API 연동
const mockData: EstimateDetail = {
id,
estimateCode: '123123',
partnerId: '1',
partnerName: '회사명',
projectName: '현장명',
estimatorId: 'hong',
estimatorName: '이름',
itemCount: 21,
estimateAmount: 1420000,
completedDate: null,
bidDate: '2025-12-12',
status: 'pending',
createdAt: '2025-12-01',
updatedAt: '2025-12-01',
createdBy: 'hong',
siteBriefing: {
briefingCode: '123123',
partnerName: '회사명',
companyName: '회사명',
briefingDate: '2025-12-12',
attendee: '이름',
},
bidInfo: {
projectName: '현장명',
bidDate: '2025-12-12',
siteCount: 21,
constructionPeriod: '2026-01-01 ~ 2026-12-10',
constructionStartDate: '2026-01-01',
constructionEndDate: '2026-12-10',
vatType: 'excluded',
workReport: '업무 보고 내용',
documents: [
{
id: '1',
fileName: 'abc.zip',
fileUrl: '#',
fileSize: 1024000,
},
],
},
summaryItems: [
{
id: '1',
name: '서터 심창측공사',
quantity: 1,
unit: '식',
materialCost: 78540000,
laborCost: 15410000,
totalCost: 93950000,
remarks: '',
},
],
expenseItems: [
{
id: '1',
name: 'public_1',
amount: 10000,
},
],
priceAdjustments: [
{
id: '1',
category: '배합비',
unitPrice: 10000,
coating: 10000,
batting: 10000,
boxReinforce: 10500,
painting: 10500,
total: 51000,
},
{
id: '2',
category: '재단비',
unitPrice: 1375,
coating: 0,
batting: 0,
boxReinforce: 0,
painting: 0,
total: 1375,
},
{
id: '3',
category: '판매단가',
unitPrice: 0,
coating: 10000,
batting: 10000,
boxReinforce: 10500,
painting: 10500,
total: 41000,
},
{
id: '4',
category: '조립단가',
unitPrice: 10300,
coating: 10300,
batting: 10300,
boxReinforce: 10500,
painting: 10200,
total: 51600,
},
],
detailItems: [
{
id: '1',
no: 1,
name: 'FS530외/주차',
material: 'screen',
width: 2350,
height: 2500,
quantity: 1,
box: 1,
assembly: 0,
coating: 0,
batting: 0,
mounting: 0,
fitting: 0,
controller: 0,
widthConstruction: 0,
heightConstruction: 0,
materialCost: 1420000,
laborCost: 510000,
quantityPrice: 1930000,
expenseQuantity: 5500,
expenseTotal: 5500,
totalCost: 1930000,
otherCost: 0,
marginCost: 0,
totalPrice: 1930000,
unitPrice: 1420000,
expense: 0,
marginRate: 0,
unitQuantity: 1,
expenseResult: 0,
marginActual: 0,
},
{
id: '2',
no: 2,
name: 'FS530외/주차',
material: 'screen',
width: 7500,
height: 2500,
quantity: 1,
box: 1,
assembly: 0,
coating: 0,
batting: 0,
mounting: 0,
fitting: 0,
controller: 0,
widthConstruction: 0,
heightConstruction: 0,
materialCost: 4720000,
laborCost: 780000,
quantityPrice: 5500000,
expenseQuantity: 5500,
expenseTotal: 5500,
totalCost: 5500000,
otherCost: 0,
marginCost: 0,
totalPrice: 5500000,
unitPrice: 4720000,
expense: 0,
marginRate: 0,
unitQuantity: 1,
expenseResult: 0,
marginActual: 0,
},
],
approval: {
approvers: [],
references: [],
},
};
return mockData;
}
export default async function EstimateDetailPage({ params }: EstimateDetailPageProps) {
const { id } = await params;
const detail = await getEstimateDetail(id);
return (
<EstimateDetailForm
mode="view"
estimateId={id}
initialData={detail || undefined}
/>
);
}

View File

@@ -0,0 +1,5 @@
import { BiddingListClient } from '@/components/business/juil/bidding';
export default function BiddingPage() {
return <BiddingListClient />;
}

View File

@@ -0,0 +1,19 @@
import ContractDetailForm from '@/components/business/juil/contract/ContractDetailForm';
import { getContractDetail } from '@/components/business/juil/contract';
interface ContractEditPageProps {
params: Promise<{ id: string }>;
}
export default async function ContractEditPage({ params }: ContractEditPageProps) {
const { id } = await params;
const result = await getContractDetail(id);
return (
<ContractDetailForm
mode="edit"
contractId={id}
initialData={result.success ? result.data : undefined}
/>
);
}

View File

@@ -0,0 +1,19 @@
import ContractDetailForm from '@/components/business/juil/contract/ContractDetailForm';
import { getContractDetail } from '@/components/business/juil/contract';
interface ContractDetailPageProps {
params: Promise<{ id: string }>;
}
export default async function ContractDetailPage({ params }: ContractDetailPageProps) {
const { id } = await params;
const result = await getContractDetail(id);
return (
<ContractDetailForm
mode="view"
contractId={id}
initialData={result.success ? result.data : undefined}
/>
);
}

View File

@@ -0,0 +1,23 @@
import { HandoverReportDetailForm, getHandoverReportDetail } from '@/components/business/juil/handover-report';
interface HandoverReportEditPageProps {
params: Promise<{
id: string;
locale: string;
}>;
}
export default async function HandoverReportEditPage({ params }: HandoverReportEditPageProps) {
const { id } = await params;
// 서버에서 상세 데이터 조회
const result = await getHandoverReportDetail(id);
return (
<HandoverReportDetailForm
mode="edit"
reportId={id}
initialData={result.data}
/>
);
}

View File

@@ -0,0 +1,23 @@
import { HandoverReportDetailForm, getHandoverReportDetail } from '@/components/business/juil/handover-report';
interface HandoverReportDetailPageProps {
params: Promise<{
id: string;
locale: string;
}>;
}
export default async function HandoverReportDetailPage({ params }: HandoverReportDetailPageProps) {
const { id } = await params;
// 서버에서 상세 데이터 조회
const result = await getHandoverReportDetail(id);
return (
<HandoverReportDetailForm
mode="view"
reportId={id}
initialData={result.data}
/>
);
}

View File

@@ -0,0 +1,5 @@
import { HandoverReportListClient } from '@/components/business/juil/handover-report';
export default function HandoverReportPage() {
return <HandoverReportListClient />;
}

View File

@@ -0,0 +1,5 @@
import { ContractListClient } from '@/components/business/juil/contract';
export default function ContractPage() {
return <ContractListClient />;
}

View File

@@ -453,4 +453,93 @@ html {
[data-slot="sheet-overlay"][data-state="closed"] {
animation: fadeOut 200ms ease-out forwards;
}
/* ==========================================
Print Styles - 인쇄 시 문서만 출력
========================================== */
@media print {
/* A4 용지 설정 */
@page {
size: A4 portrait;
margin: 10mm;
}
/* 배경색 유지 */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
color-adjust: exact !important;
}
/* ========================================
인쇄 스타일 (JavaScript printArea 사용 시 기본값)
======================================== */
/* 기본 설정 - printArea 유틸리티가 새 창에서 인쇄하므로 간단하게 유지 */
html, body {
background: white !important;
}
/* print-hidden 클래스 숨김 */
.print-hidden {
display: none !important;
}
/* ========================================
테이블 & 페이지 설정
======================================== */
/* 페이지 나눔 방지 */
table, figure, .page-break-avoid {
page-break-inside: avoid;
}
/* 인쇄용 테이블 스타일 */
.print-area table {
border-collapse: collapse !important;
}
.print-area th,
.print-area td {
border: 1px solid #000 !important;
}
/* print-area 내부 문서 wrapper - transform 제거 */
.print-area > div {
max-width: none !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
box-shadow: none !important;
transform: none !important;
}
/* 실제 문서 컨테이너 - A4에 맞게 조정 */
.print-area > div > div {
width: 100% !important;
max-width: 190mm !important;
min-height: auto !important;
margin: 0 auto !important;
padding: 5mm !important;
box-shadow: none !important;
font-size: 10pt !important;
}
/* 테이블 폰트 크기 축소 */
.print-area table {
font-size: 9pt !important;
}
.print-area .text-xs {
font-size: 8pt !important;
}
.print-area .text-sm {
font-size: 9pt !important;
}
.print-area .text-3xl {
font-size: 18pt !important;
}
}