Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -1,18 +1,58 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getBadDebtById } from '@/components/accounting/BadDebtCollection/actions';
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
import type { BadDebtRecord } from '@/components/accounting/BadDebtCollection/types';
|
||||
|
||||
interface EditBadDebtPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function EditBadDebtPage({ params }: EditBadDebtPageProps) {
|
||||
const { id } = await params;
|
||||
const badDebt = await getBadDebtById(id);
|
||||
export default function EditBadDebtPage({ params }: EditBadDebtPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<BadDebtRecord | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!badDebt) {
|
||||
notFound();
|
||||
useEffect(() => {
|
||||
getBadDebtById(id)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
setData(result);
|
||||
} else {
|
||||
setError('데이터를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <BadDebtDetail mode="edit" recordId={id} initialData={badDebt} />;
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '데이터를 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <BadDebtDetail mode="edit" recordId={id} initialData={data} />;
|
||||
}
|
||||
@@ -1,18 +1,58 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getBadDebtById } from '@/components/accounting/BadDebtCollection/actions';
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
import type { BadDebtRecord } from '@/components/accounting/BadDebtCollection/types';
|
||||
|
||||
interface BadDebtDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function BadDebtDetailPage({ params }: BadDebtDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const badDebt = await getBadDebtById(id);
|
||||
export default function BadDebtDetailPage({ params }: BadDebtDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<BadDebtRecord | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!badDebt) {
|
||||
notFound();
|
||||
useEffect(() => {
|
||||
getBadDebtById(id)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
setData(result);
|
||||
} else {
|
||||
setError('데이터를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <BadDebtDetail mode="view" recordId={id} initialData={badDebt} />;
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '데이터를 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <BadDebtDetail mode="view" recordId={id} initialData={data} />;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 악성채권 추심관리 목록 페이지
|
||||
*
|
||||
@@ -7,23 +9,48 @@
|
||||
* - GET /api/v1/bad-debts/summary - 통계 정보
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BadDebtCollection } from '@/components/accounting/BadDebtCollection';
|
||||
import { getBadDebts, getBadDebtSummary } from '@/components/accounting/BadDebtCollection/actions';
|
||||
import type { BadDebtSummary } from '@/components/accounting/BadDebtCollection/types';
|
||||
|
||||
export default async function BadDebtCollectionPage() {
|
||||
// 서버에서 데이터 병렬 조회
|
||||
const [badDebts, summary] = await Promise.all([
|
||||
getBadDebts({ size: 100 }),
|
||||
getBadDebtSummary(),
|
||||
]);
|
||||
const DEFAULT_SUMMARY: BadDebtSummary = {
|
||||
totalCount: 0,
|
||||
totalAmount: 0,
|
||||
collectedAmount: 0,
|
||||
pendingAmount: 0,
|
||||
collectionRate: 0,
|
||||
};
|
||||
|
||||
console.log('[BadDebtPage] Data count:', badDebts.length);
|
||||
console.log('[BadDebtPage] Summary:', summary);
|
||||
export default function BadDebtCollectionPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getBadDebts>>>([]);
|
||||
const [summary, setSummary] = useState<BadDebtSummary>(DEFAULT_SUMMARY);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getBadDebts({ size: 100 }),
|
||||
getBadDebtSummary(),
|
||||
])
|
||||
.then(([badDebts, summaryResult]) => {
|
||||
setData(badDebts);
|
||||
setSummary(summaryResult);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BadDebtCollection
|
||||
initialData={badDebts}
|
||||
initialData={data}
|
||||
initialSummary={summary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +1,46 @@
|
||||
import { cookies } from 'next/headers';
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { BillManagementClient } from '@/components/accounting/BillManagement/BillManagementClient';
|
||||
import type { BillRecord, BillApiData } from '@/components/accounting/BillManagement/types';
|
||||
import { transformApiToFrontend } from '@/components/accounting/BillManagement/types';
|
||||
import { getBills } from '@/components/accounting/BillManagement/actions';
|
||||
import type { BillRecord } from '@/components/accounting/BillManagement/types';
|
||||
|
||||
interface BillsPageProps {
|
||||
searchParams: Promise<{
|
||||
vendorId?: string;
|
||||
type?: string;
|
||||
page?: string;
|
||||
}>;
|
||||
}
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
export default function BillsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const vendorId = searchParams.get('vendorId') || undefined;
|
||||
const billType = searchParams.get('type') || 'received';
|
||||
const page = searchParams.get('page') ? parseInt(searchParams.get('page')!) : 1;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
const [data, setData] = useState<BillRecord[]>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
async function getBills(params: {
|
||||
billType?: string;
|
||||
page?: number;
|
||||
}): Promise<{
|
||||
data: BillRecord[];
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
};
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const queryParams = new URLSearchParams();
|
||||
useEffect(() => {
|
||||
getBills({ billType, page, perPage: 20 })
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [billType, page]);
|
||||
|
||||
if (params.billType && params.billType !== 'all') {
|
||||
queryParams.append('bill_type', params.billType);
|
||||
}
|
||||
if (params.page) {
|
||||
queryParams.append('page', String(params.page));
|
||||
}
|
||||
queryParams.append('per_page', '20');
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bills?${queryParams.toString()}`,
|
||||
{ method: 'GET', headers, cache: 'no-store' }
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BillsPage] Fetch error:', response.status);
|
||||
return {
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const paginatedData = result.data as {
|
||||
data: BillApiData[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
return {
|
||||
data: paginatedData.data.map(transformApiToFrontend),
|
||||
pagination: {
|
||||
currentPage: paginatedData.current_page,
|
||||
lastPage: paginatedData.last_page,
|
||||
perPage: paginatedData.per_page,
|
||||
total: paginatedData.total,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[BillsPage] Fetch error:', error);
|
||||
return {
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function BillsPage({ searchParams }: BillsPageProps) {
|
||||
const params = await searchParams;
|
||||
const vendorId = params.vendorId;
|
||||
const billType = params.type || 'received';
|
||||
const page = params.page ? parseInt(params.page) : 1;
|
||||
|
||||
const { data, pagination } = await getBills({ billType, page });
|
||||
|
||||
return (
|
||||
<BillManagementClient
|
||||
@@ -110,4 +50,4 @@ export default async function BillsPage({ searchParams }: BillsPageProps) {
|
||||
initialBillType={billType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DepositManagement } from '@/components/accounting/DepositManagement';
|
||||
import { getDeposits } from '@/components/accounting/DepositManagement/actions';
|
||||
|
||||
export default async function DepositsPage() {
|
||||
const result = await getDeposits({ perPage: 100 });
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 100,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export default function DepositsPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getDeposits>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getDeposits({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DepositManagement
|
||||
initialData={result.data}
|
||||
initialPagination={result.pagination}
|
||||
initialData={data}
|
||||
initialPagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ExpectedExpenseManagement } from '@/components/accounting/ExpectedExpenseManagement';
|
||||
import { getExpectedExpenses } from '@/components/accounting/ExpectedExpenseManagement/actions';
|
||||
|
||||
export default async function ExpectedExpensesPage() {
|
||||
// 서버에서 초기 데이터 로드
|
||||
const result = await getExpectedExpenses({
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
sortBy: 'expected_payment_date',
|
||||
sortDir: 'asc',
|
||||
});
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 50,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export default function ExpectedExpensesPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getExpectedExpenses>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getExpectedExpenses({
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
sortBy: 'expected_payment_date',
|
||||
sortDir: 'asc',
|
||||
})
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExpectedExpenseManagement
|
||||
initialData={result.data}
|
||||
pagination={result.pagination}
|
||||
initialData={data}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SalesManagement } from '@/components/accounting/SalesManagement';
|
||||
import { getSales } from '@/components/accounting/SalesManagement/actions';
|
||||
|
||||
export default async function SalesPage() {
|
||||
const result = await getSales({ perPage: 100 });
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 100,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export default function SalesPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getSales>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getSales({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SalesManagement
|
||||
initialData={result.data}
|
||||
initialPagination={result.pagination}
|
||||
initialData={data}
|
||||
initialPagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { VendorManagement } from '@/components/accounting/VendorManagement';
|
||||
import { getClients } from '@/components/accounting/VendorManagement/actions';
|
||||
|
||||
export default async function VendorsPage() {
|
||||
const result = await getClients({ size: 100 });
|
||||
export default function VendorsPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getClients>>['data']>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getClients({ size: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VendorManagement
|
||||
initialData={result.data}
|
||||
initialTotal={result.total}
|
||||
initialData={data}
|
||||
initialTotal={total}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { WithdrawalManagement } from '@/components/accounting/WithdrawalManagement';
|
||||
import { getWithdrawals } from '@/components/accounting/WithdrawalManagement/actions';
|
||||
|
||||
export default async function WithdrawalsPage() {
|
||||
const result = await getWithdrawals({ perPage: 100 });
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 100,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export default function WithdrawalsPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getWithdrawals>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getWithdrawals({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WithdrawalManagement
|
||||
initialData={result.data}
|
||||
initialPagination={result.pagination}
|
||||
initialData={data}
|
||||
initialPagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ItemDetailClient } from '@/components/business/construction/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;
|
||||
export default function ItemDetailPage({ params }: ItemDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
return <ItemDetailClient itemId={id} isEditMode={isEditMode} />;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { LaborDetailClient } from '@/components/business/construction/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;
|
||||
export default function LaborDetailPage({ params }: LaborDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
return <LaborDetailClient laborId={id} isEditMode={isEditMode} />;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import PricingDetailClient from '@/components/business/construction/pricing-management/PricingDetailClient';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function PricingEditPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
export default function PricingEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <PricingDetailClient id={id} mode="edit" />;
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import PricingDetailClient from '@/components/business/construction/pricing-management/PricingDetailClient';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function PricingDetailPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
export default function PricingDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <PricingDetailClient id={id} mode="view" />;
|
||||
}
|
||||
@@ -1,19 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { OrderDetailForm } from '@/components/business/construction/order-management';
|
||||
import { getOrderDetailFull } from '@/components/business/construction/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;
|
||||
export default function OrderEditPage({ params }: OrderEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getOrderDetailFull>>['data']>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const result = await getOrderDetailFull(id);
|
||||
useEffect(() => {
|
||||
getOrderDetailFull(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('주문 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('주문 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
notFound();
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <OrderDetailForm mode="edit" orderId={id} initialData={result.data} />;
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '주문 정보를 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <OrderDetailForm mode="edit" orderId={id} initialData={data} />;
|
||||
}
|
||||
@@ -1,19 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { OrderDetailForm } from '@/components/business/construction/order-management';
|
||||
import { getOrderDetailFull } from '@/components/business/construction/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;
|
||||
export default function OrderDetailPage({ params }: OrderDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getOrderDetailFull>>['data']>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const result = await getOrderDetailFull(id);
|
||||
useEffect(() => {
|
||||
getOrderDetailFull(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('주문 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('주문 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
notFound();
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <OrderDetailForm mode="view" orderId={id} initialData={result.data} />;
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '주문 정보를 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <OrderDetailForm mode="view" orderId={id} initialData={data} />;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import SiteDetailForm from '@/components/business/construction/site-management/SiteDetailForm';
|
||||
|
||||
// 목업 데이터
|
||||
@@ -17,11 +20,24 @@ interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function SiteEditPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
export default function SiteEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const [site, setSite] = useState<typeof MOCK_SITE | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// TODO: API에서 현장 정보 조회
|
||||
const site = { ...MOCK_SITE, id };
|
||||
useEffect(() => {
|
||||
// TODO: API에서 현장 정보 조회
|
||||
setSite({ ...MOCK_SITE, id });
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading || !site) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SiteDetailForm site={site} mode="edit" />;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import SiteDetailForm from '@/components/business/construction/site-management/SiteDetailForm';
|
||||
|
||||
// 목업 데이터
|
||||
@@ -17,11 +20,24 @@ interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function SiteDetailPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
export default function SiteDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const [site, setSite] = useState<typeof MOCK_SITE | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// TODO: API에서 현장 정보 조회
|
||||
const site = { ...MOCK_SITE, id };
|
||||
useEffect(() => {
|
||||
// TODO: API에서 현장 정보 조회
|
||||
setSite({ ...MOCK_SITE, id });
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading || !site) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SiteDetailForm site={site} mode="view" />;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import StructureReviewDetailForm from '@/components/business/construction/structure-review/StructureReviewDetailForm';
|
||||
|
||||
// 목업 데이터
|
||||
@@ -22,11 +25,24 @@ interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function StructureReviewEditPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
export default function StructureReviewEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const [review, setReview] = useState<typeof MOCK_REVIEW | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// TODO: API에서 구조검토 정보 조회
|
||||
const review = { ...MOCK_REVIEW, id };
|
||||
useEffect(() => {
|
||||
// TODO: API에서 구조검토 정보 조회
|
||||
setReview({ ...MOCK_REVIEW, id });
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading || !review) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <StructureReviewDetailForm review={review} mode="edit" />;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import StructureReviewDetailForm from '@/components/business/construction/structure-review/StructureReviewDetailForm';
|
||||
|
||||
// 목업 데이터
|
||||
@@ -22,11 +25,24 @@ interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function StructureReviewDetailPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
export default function StructureReviewDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const [review, setReview] = useState<typeof MOCK_REVIEW | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// TODO: API에서 구조검토 정보 조회
|
||||
const review = { ...MOCK_REVIEW, id };
|
||||
useEffect(() => {
|
||||
// TODO: API에서 구조검토 정보 조회
|
||||
setReview({ ...MOCK_REVIEW, id });
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading || !review) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <StructureReviewDetailForm review={review} mode="view" />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { BiddingDetailForm, getBiddingDetail } from '@/components/business/construction/bidding';
|
||||
|
||||
interface BiddingEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function BiddingEditPage({ params }: BiddingEditPageProps) {
|
||||
const { id } = await params;
|
||||
const result = await getBiddingDetail(id);
|
||||
export default function BiddingEditPage({ params }: BiddingEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getBiddingDetail>>['data']>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getBiddingDetail(id)
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BiddingDetailForm
|
||||
mode="edit"
|
||||
biddingId={id}
|
||||
initialData={result.data}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { BiddingDetailForm, getBiddingDetail } from '@/components/business/construction/bidding';
|
||||
|
||||
interface BiddingDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function BiddingDetailPage({ params }: BiddingDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const result = await getBiddingDetail(id);
|
||||
export default function BiddingDetailPage({ params }: BiddingDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getBiddingDetail>>['data']>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getBiddingDetail(id)
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BiddingDetailForm
|
||||
mode="view"
|
||||
biddingId={id}
|
||||
initialData={result.data}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { EstimateDetailForm } from '@/components/business/construction/estimates';
|
||||
import type { EstimateDetail } from '@/components/business/construction/estimates';
|
||||
|
||||
@@ -6,7 +9,7 @@ interface EstimateEditPageProps {
|
||||
}
|
||||
|
||||
// 목업 데이터 - 추후 API 연동
|
||||
async function getEstimateDetail(id: string): Promise<EstimateDetail | null> {
|
||||
function getEstimateDetail(id: string): EstimateDetail {
|
||||
// TODO: 실제 API 연동
|
||||
const mockData: EstimateDetail = {
|
||||
id,
|
||||
@@ -187,15 +190,30 @@ async function getEstimateDetail(id: string): Promise<EstimateDetail | null> {
|
||||
return mockData;
|
||||
}
|
||||
|
||||
export default async function EstimateEditPage({ params }: EstimateEditPageProps) {
|
||||
const { id } = await params;
|
||||
const detail = await getEstimateDetail(id);
|
||||
export default function EstimateEditPage({ params }: EstimateEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const [data, setData] = useState<EstimateDetail | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const detail = getEstimateDetail(id);
|
||||
setData(detail);
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EstimateDetailForm
|
||||
mode="edit"
|
||||
estimateId={id}
|
||||
initialData={detail || undefined}
|
||||
initialData={data || undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { EstimateDetailForm } from '@/components/business/construction/estimates';
|
||||
import type { EstimateDetail } from '@/components/business/construction/estimates';
|
||||
|
||||
@@ -6,7 +9,7 @@ interface EstimateDetailPageProps {
|
||||
}
|
||||
|
||||
// 목업 데이터 - 추후 API 연동
|
||||
async function getEstimateDetail(id: string): Promise<EstimateDetail | null> {
|
||||
function getEstimateDetail(id: string): EstimateDetail {
|
||||
// TODO: 실제 API 연동
|
||||
const mockData: EstimateDetail = {
|
||||
id,
|
||||
@@ -187,15 +190,30 @@ async function getEstimateDetail(id: string): Promise<EstimateDetail | null> {
|
||||
return mockData;
|
||||
}
|
||||
|
||||
export default async function EstimateDetailPage({ params }: EstimateDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const detail = await getEstimateDetail(id);
|
||||
export default function EstimateDetailPage({ params }: EstimateDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const [data, setData] = useState<EstimateDetail | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const detail = getEstimateDetail(id);
|
||||
setData(detail);
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EstimateDetailForm
|
||||
mode="view"
|
||||
estimateId={id}
|
||||
initialData={detail || undefined}
|
||||
initialData={data || undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import PartnerForm from '@/components/business/construction/partners/PartnerForm';
|
||||
import { getPartner } from '@/components/business/construction/partners/actions';
|
||||
|
||||
@@ -5,15 +9,52 @@ interface PartnerEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function PartnerEditPage({ params }: PartnerEditPageProps) {
|
||||
const { id } = await params;
|
||||
const result = await getPartner(id);
|
||||
export default function PartnerEditPage({ params }: PartnerEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getPartner>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getPartner(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('협력업체 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('협력업체 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PartnerForm
|
||||
mode="edit"
|
||||
partnerId={id}
|
||||
initialData={result.success ? result.data : undefined}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import PartnerForm from '@/components/business/construction/partners/PartnerForm';
|
||||
import { getPartner } from '@/components/business/construction/partners/actions';
|
||||
|
||||
@@ -5,15 +9,52 @@ interface PartnerDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function PartnerDetailPage({ params }: PartnerDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const result = await getPartner(id);
|
||||
export default function PartnerDetailPage({ params }: PartnerDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getPartner>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getPartner(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('협력업체 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('협력업체 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PartnerForm
|
||||
mode="view"
|
||||
partnerId={id}
|
||||
initialData={result.success ? result.data : undefined}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { SiteBriefingForm, getSiteBriefing } from '@/components/business/construction/site-briefings';
|
||||
|
||||
interface SiteBriefingEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function SiteBriefingEditPage({ params }: SiteBriefingEditPageProps) {
|
||||
const { id } = await params;
|
||||
const result = await getSiteBriefing(id);
|
||||
export default function SiteBriefingEditPage({ params }: SiteBriefingEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getSiteBriefing>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getSiteBriefing(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('현장 설명회 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('현장 설명회 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SiteBriefingForm
|
||||
mode="edit"
|
||||
briefingId={id}
|
||||
initialData={result.success ? result.data : undefined}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { SiteBriefingForm, getSiteBriefing } from '@/components/business/construction/site-briefings';
|
||||
|
||||
interface SiteBriefingDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function SiteBriefingDetailPage({ params }: SiteBriefingDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const result = await getSiteBriefing(id);
|
||||
export default function SiteBriefingDetailPage({ params }: SiteBriefingDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getSiteBriefing>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getSiteBriefing(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('현장 설명회 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('현장 설명회 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SiteBriefingForm
|
||||
mode="view"
|
||||
briefingId={id}
|
||||
initialData={result.success ? result.data : undefined}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import ContractDetailForm from '@/components/business/construction/contract/ContractDetailForm';
|
||||
import { getContractDetail } from '@/components/business/construction/contract';
|
||||
|
||||
@@ -5,15 +9,52 @@ interface ContractEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function ContractEditPage({ params }: ContractEditPageProps) {
|
||||
const { id } = await params;
|
||||
const result = await getContractDetail(id);
|
||||
export default function ContractEditPage({ params }: ContractEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getContractDetail>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getContractDetail(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('계약 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('계약 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractDetailForm
|
||||
mode="edit"
|
||||
contractId={id}
|
||||
initialData={result.success ? result.data : undefined}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import ContractDetailForm from '@/components/business/construction/contract/ContractDetailForm';
|
||||
import { getContractDetail } from '@/components/business/construction/contract';
|
||||
|
||||
@@ -5,15 +9,52 @@ interface ContractDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function ContractDetailPage({ params }: ContractDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const result = await getContractDetail(id);
|
||||
export default function ContractDetailPage({ params }: ContractDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getContractDetail>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getContractDetail(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('계약 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('계약 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractDetailForm
|
||||
mode="view"
|
||||
contractId={id}
|
||||
initialData={result.success ? result.data : undefined}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { HandoverReportDetailForm, getHandoverReportDetail } from '@/components/business/construction/handover-report';
|
||||
|
||||
interface HandoverReportEditPageProps {
|
||||
@@ -7,17 +11,52 @@ interface HandoverReportEditPageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function HandoverReportEditPage({ params }: HandoverReportEditPageProps) {
|
||||
const { id } = await params;
|
||||
export default function HandoverReportEditPage({ params }: HandoverReportEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getHandoverReportDetail>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 서버에서 상세 데이터 조회
|
||||
const result = await getHandoverReportDetail(id);
|
||||
useEffect(() => {
|
||||
getHandoverReportDetail(id)
|
||||
.then(result => {
|
||||
if (result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('인수인계서 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('인수인계서 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HandoverReportDetailForm
|
||||
mode="edit"
|
||||
reportId={id}
|
||||
initialData={result.data}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { HandoverReportDetailForm, getHandoverReportDetail } from '@/components/business/construction/handover-report';
|
||||
|
||||
interface HandoverReportDetailPageProps {
|
||||
@@ -7,17 +11,52 @@ interface HandoverReportDetailPageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function HandoverReportDetailPage({ params }: HandoverReportDetailPageProps) {
|
||||
const { id } = await params;
|
||||
export default function HandoverReportDetailPage({ params }: HandoverReportDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getHandoverReportDetail>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 서버에서 상세 데이터 조회
|
||||
const result = await getHandoverReportDetail(id);
|
||||
useEffect(() => {
|
||||
getHandoverReportDetail(id)
|
||||
.then(result => {
|
||||
if (result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('인수인계서 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('인수인계서 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HandoverReportDetailForm
|
||||
mode="view"
|
||||
reportId={id}
|
||||
initialData={result.data}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
'use server';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import type { UrlCategory, UrlItem } from './ConstructionTestUrlsClient';
|
||||
|
||||
// 아이콘 매핑
|
||||
const iconMap: Record<string, string> = {
|
||||
'기본': '🏠',
|
||||
'시스템': '💻',
|
||||
'대시보드': '📊',
|
||||
};
|
||||
|
||||
function getIcon(title: string): string {
|
||||
for (const [key, icon] of Object.entries(iconMap)) {
|
||||
if (title.includes(key)) return icon;
|
||||
}
|
||||
return '📄';
|
||||
}
|
||||
|
||||
function parseTableRow(line: string): UrlItem | null {
|
||||
// | 페이지 | URL | 상태 | 형식 파싱
|
||||
const parts = line.split('|').map(p => p.trim()).filter(p => p);
|
||||
|
||||
if (parts.length < 2) return null;
|
||||
if (parts[0] === '페이지' || parts[0].startsWith('---')) return null;
|
||||
|
||||
const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거
|
||||
const url = parts[1].replace(/`/g, ''); // backtick 제거
|
||||
const status = parts[2] || undefined;
|
||||
|
||||
// URL이 /ko로 시작하는지 확인
|
||||
if (!url.startsWith('/ko')) return null;
|
||||
|
||||
return { name, url, status };
|
||||
}
|
||||
|
||||
function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: string } {
|
||||
const lines = content.split('\n');
|
||||
const categories: UrlCategory[] = [];
|
||||
let currentCategory: UrlCategory | null = null;
|
||||
let currentSubCategory: { title: string; items: UrlItem[] } | null = null;
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
// Last Updated 추출
|
||||
const updateMatch = content.match(/Last Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
||||
if (updateMatch) {
|
||||
lastUpdated = updateMatch[1];
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// ## 카테고리 (메인 섹션)
|
||||
if (line.startsWith('## ') && !line.includes('클릭 가능한') && !line.includes('전체 URL') && !line.includes('백엔드 메뉴')) {
|
||||
// 이전 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
currentSubCategory = null;
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim();
|
||||
currentCategory = {
|
||||
title,
|
||||
icon: getIcon(title),
|
||||
items: [],
|
||||
subCategories: [],
|
||||
};
|
||||
currentSubCategory = null;
|
||||
}
|
||||
|
||||
// ### 서브 카테고리
|
||||
else if (line.startsWith('### ') && currentCategory) {
|
||||
// 이전 서브카테고리 저장
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
|
||||
const subTitle = line.replace('### ', '').trim();
|
||||
// "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로
|
||||
if (subTitle === '메인 페이지') {
|
||||
currentSubCategory = null;
|
||||
} else {
|
||||
currentSubCategory = {
|
||||
title: subTitle,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 행 파싱
|
||||
else if (line.startsWith('|') && currentCategory) {
|
||||
const item = parseTableRow(line);
|
||||
if (item) {
|
||||
if (currentSubCategory) {
|
||||
currentSubCategory.items.push(item);
|
||||
} else {
|
||||
currentCategory.items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
// 빈 서브카테고리 제거
|
||||
categories.forEach(cat => {
|
||||
cat.subCategories = cat.subCategories?.filter(sub => sub.items.length > 0);
|
||||
});
|
||||
|
||||
return { categories, lastUpdated };
|
||||
}
|
||||
|
||||
export async function getConstructionTestUrlsData(): Promise<{ categories: UrlCategory[]; lastUpdated: string }> {
|
||||
// md 파일 경로
|
||||
const mdFilePath = path.join(
|
||||
process.cwd(),
|
||||
'claudedocs',
|
||||
'[REF] construction-pages-test-urls.md'
|
||||
);
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(mdFilePath, 'utf-8');
|
||||
return parseMdFile(fileContent);
|
||||
} catch (error) {
|
||||
console.error('Failed to read md file:', error);
|
||||
return { categories: [], lastUpdated: 'N/A' };
|
||||
}
|
||||
}
|
||||
@@ -1,152 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import ConstructionTestUrlsClient, { UrlCategory, UrlItem } from './ConstructionTestUrlsClient';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ConstructionTestUrlsClient, { UrlCategory } from './ConstructionTestUrlsClient';
|
||||
import { getConstructionTestUrlsData } from './actions';
|
||||
|
||||
// 아이콘 매핑
|
||||
const iconMap: Record<string, string> = {
|
||||
'기본': '🏠',
|
||||
'시스템': '💻',
|
||||
'대시보드': '📊',
|
||||
};
|
||||
export default function TestUrlsPage() {
|
||||
const [urlData, setUrlData] = useState<UrlCategory[]>([]);
|
||||
const [lastUpdated, setLastUpdated] = useState('N/A');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
function getIcon(title: string): string {
|
||||
for (const [key, icon] of Object.entries(iconMap)) {
|
||||
if (title.includes(key)) return icon;
|
||||
}
|
||||
return '📄';
|
||||
}
|
||||
useEffect(() => {
|
||||
getConstructionTestUrlsData()
|
||||
.then(result => {
|
||||
setUrlData(result.categories);
|
||||
setLastUpdated(result.lastUpdated);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
function parseTableRow(line: string): UrlItem | null {
|
||||
// | 페이지 | URL | 상태 | 형식 파싱
|
||||
const parts = line.split('|').map(p => p.trim()).filter(p => p);
|
||||
|
||||
if (parts.length < 2) return null;
|
||||
if (parts[0] === '페이지' || parts[0].startsWith('---')) return null;
|
||||
|
||||
const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거
|
||||
const url = parts[1].replace(/`/g, ''); // backtick 제거
|
||||
const status = parts[2] || undefined;
|
||||
|
||||
// URL이 /ko로 시작하는지 확인
|
||||
if (!url.startsWith('/ko')) return null;
|
||||
|
||||
return { name, url, status };
|
||||
}
|
||||
|
||||
function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: string } {
|
||||
const lines = content.split('\n');
|
||||
const categories: UrlCategory[] = [];
|
||||
let currentCategory: UrlCategory | null = null;
|
||||
let currentSubCategory: { title: string; items: UrlItem[] } | null = null;
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
// Last Updated 추출
|
||||
const updateMatch = content.match(/Last Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
||||
if (updateMatch) {
|
||||
lastUpdated = updateMatch[1];
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// ## 카테고리 (메인 섹션)
|
||||
if (line.startsWith('## ') && !line.includes('클릭 가능한') && !line.includes('전체 URL') && !line.includes('백엔드 메뉴')) {
|
||||
// 이전 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
currentSubCategory = null;
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim();
|
||||
currentCategory = {
|
||||
title,
|
||||
icon: getIcon(title),
|
||||
items: [],
|
||||
subCategories: [],
|
||||
};
|
||||
currentSubCategory = null;
|
||||
}
|
||||
|
||||
// ### 서브 카테고리
|
||||
else if (line.startsWith('### ') && currentCategory) {
|
||||
// 이전 서브카테고리 저장
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
|
||||
const subTitle = line.replace('### ', '').trim();
|
||||
// "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로
|
||||
if (subTitle === '메인 페이지') {
|
||||
currentSubCategory = null;
|
||||
} else {
|
||||
currentSubCategory = {
|
||||
title: subTitle,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 행 파싱
|
||||
else if (line.startsWith('|') && currentCategory) {
|
||||
const item = parseTableRow(line);
|
||||
if (item) {
|
||||
if (currentSubCategory) {
|
||||
currentSubCategory.items.push(item);
|
||||
} else {
|
||||
currentCategory.items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
// 빈 서브카테고리 제거
|
||||
categories.forEach(cat => {
|
||||
cat.subCategories = cat.subCategories?.filter(sub => sub.items.length > 0);
|
||||
});
|
||||
|
||||
return { categories, lastUpdated };
|
||||
}
|
||||
|
||||
export default async function TestUrlsPage() {
|
||||
// md 파일 경로
|
||||
const mdFilePath = path.join(
|
||||
process.cwd(),
|
||||
'claudedocs',
|
||||
'[REF] construction-pages-test-urls.md'
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let urlData: UrlCategory[] = [];
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(mdFilePath, 'utf-8');
|
||||
const parsed = parseMdFile(fileContent);
|
||||
urlData = parsed.categories;
|
||||
lastUpdated = parsed.lastUpdated;
|
||||
} catch (error) {
|
||||
console.error('Failed to read md file:', error);
|
||||
// 파일 읽기 실패 시 빈 데이터
|
||||
urlData = [];
|
||||
}
|
||||
|
||||
return <ConstructionTestUrlsClient initialData={urlData} lastUpdated={lastUpdated} />;
|
||||
}
|
||||
|
||||
// 캐싱 비활성화 - 항상 최신 md 파일 읽기
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 0;
|
||||
return <ConstructionTestUrlsClient initialData={urlData} lastUpdated={lastUpdated} />;
|
||||
}
|
||||
157
src/app/[locale]/(protected)/dev/test-urls/actions.ts
Normal file
157
src/app/[locale]/(protected)/dev/test-urls/actions.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
'use server';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import type { UrlCategory, UrlItem } from './TestUrlsClient';
|
||||
|
||||
// 아이콘 매핑
|
||||
const iconMap: Record<string, string> = {
|
||||
'기본': '🏠',
|
||||
'인사관리': '👥',
|
||||
'HR': '👥',
|
||||
'판매관리': '💰',
|
||||
'Sales': '💰',
|
||||
'기준정보관리': '📦',
|
||||
'Master Data': '📦',
|
||||
'생산관리': '🏭',
|
||||
'Production': '🏭',
|
||||
'설정': '⚙️',
|
||||
'Settings': '⚙️',
|
||||
'전자결재': '📝',
|
||||
'Approval': '📝',
|
||||
'회계관리': '💵',
|
||||
'Accounting': '💵',
|
||||
'게시판': '📋',
|
||||
'Board': '📋',
|
||||
'보고서': '📊',
|
||||
'Reports': '📊',
|
||||
};
|
||||
|
||||
function getIcon(title: string): string {
|
||||
for (const [key, icon] of Object.entries(iconMap)) {
|
||||
if (title.includes(key)) return icon;
|
||||
}
|
||||
return '📄';
|
||||
}
|
||||
|
||||
function parseTableRow(line: string): UrlItem | null {
|
||||
// | 페이지 | URL | 상태 | 형식 파싱
|
||||
const parts = line.split('|').map(p => p.trim()).filter(p => p);
|
||||
|
||||
if (parts.length < 2) return null;
|
||||
if (parts[0] === '페이지' || parts[0].startsWith('---')) return null;
|
||||
|
||||
const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거
|
||||
const url = parts[1].replace(/`/g, ''); // backtick 제거
|
||||
const status = parts[2] || undefined;
|
||||
|
||||
// URL이 /ko로 시작하는지 확인
|
||||
if (!url.startsWith('/ko')) return null;
|
||||
|
||||
return { name, url, status };
|
||||
}
|
||||
|
||||
function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: string } {
|
||||
const lines = content.split('\n');
|
||||
const categories: UrlCategory[] = [];
|
||||
let currentCategory: UrlCategory | null = null;
|
||||
let currentSubCategory: { title: string; items: UrlItem[] } | null = null;
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
// Last Updated 추출
|
||||
const updateMatch = content.match(/Last Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
||||
if (updateMatch) {
|
||||
lastUpdated = updateMatch[1];
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// ## 카테고리 (메인 섹션)
|
||||
if (line.startsWith('## ') && !line.includes('클릭 가능한') && !line.includes('전체 URL') && !line.includes('백엔드 메뉴')) {
|
||||
// 이전 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
currentSubCategory = null;
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim();
|
||||
currentCategory = {
|
||||
title,
|
||||
icon: getIcon(title),
|
||||
items: [],
|
||||
subCategories: [],
|
||||
};
|
||||
currentSubCategory = null;
|
||||
}
|
||||
|
||||
// ### 서브 카테고리
|
||||
else if (line.startsWith('### ') && currentCategory) {
|
||||
// 이전 서브카테고리 저장
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
|
||||
const subTitle = line.replace('### ', '').trim();
|
||||
// "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로
|
||||
if (subTitle === '메인 페이지') {
|
||||
currentSubCategory = null;
|
||||
} else {
|
||||
currentSubCategory = {
|
||||
title: subTitle,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 행 파싱
|
||||
else if (line.startsWith('|') && currentCategory) {
|
||||
const item = parseTableRow(line);
|
||||
if (item) {
|
||||
if (currentSubCategory) {
|
||||
currentSubCategory.items.push(item);
|
||||
} else {
|
||||
currentCategory.items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
// 빈 서브카테고리 제거
|
||||
categories.forEach(cat => {
|
||||
cat.subCategories = cat.subCategories?.filter(sub => sub.items.length > 0);
|
||||
});
|
||||
|
||||
return { categories, lastUpdated };
|
||||
}
|
||||
|
||||
export async function getTestUrlsData(): Promise<{ categories: UrlCategory[]; lastUpdated: string }> {
|
||||
// md 파일 경로
|
||||
const mdFilePath = path.join(
|
||||
process.cwd(),
|
||||
'claudedocs',
|
||||
'[REF] all-pages-test-urls.md'
|
||||
);
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(mdFilePath, 'utf-8');
|
||||
return parseMdFile(fileContent);
|
||||
} catch (error) {
|
||||
console.error('Failed to read md file:', error);
|
||||
return { categories: [], lastUpdated: 'N/A' };
|
||||
}
|
||||
}
|
||||
@@ -1,167 +1,30 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import TestUrlsClient, { UrlCategory, UrlItem } from './TestUrlsClient';
|
||||
'use client';
|
||||
|
||||
// 아이콘 매핑
|
||||
const iconMap: Record<string, string> = {
|
||||
'기본': '🏠',
|
||||
'인사관리': '👥',
|
||||
'HR': '👥',
|
||||
'판매관리': '💰',
|
||||
'Sales': '💰',
|
||||
'기준정보관리': '📦',
|
||||
'Master Data': '📦',
|
||||
'생산관리': '🏭',
|
||||
'Production': '🏭',
|
||||
'설정': '⚙️',
|
||||
'Settings': '⚙️',
|
||||
'전자결재': '📝',
|
||||
'Approval': '📝',
|
||||
'회계관리': '💵',
|
||||
'Accounting': '💵',
|
||||
'게시판': '📋',
|
||||
'Board': '📋',
|
||||
'보고서': '📊',
|
||||
'Reports': '📊',
|
||||
};
|
||||
import { useEffect, useState } from 'react';
|
||||
import TestUrlsClient, { UrlCategory } from './TestUrlsClient';
|
||||
import { getTestUrlsData } from './actions';
|
||||
|
||||
function getIcon(title: string): string {
|
||||
for (const [key, icon] of Object.entries(iconMap)) {
|
||||
if (title.includes(key)) return icon;
|
||||
}
|
||||
return '📄';
|
||||
}
|
||||
export default function TestUrlsPage() {
|
||||
const [urlData, setUrlData] = useState<UrlCategory[]>([]);
|
||||
const [lastUpdated, setLastUpdated] = useState('N/A');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
function parseTableRow(line: string): UrlItem | null {
|
||||
// | 페이지 | URL | 상태 | 형식 파싱
|
||||
const parts = line.split('|').map(p => p.trim()).filter(p => p);
|
||||
useEffect(() => {
|
||||
getTestUrlsData()
|
||||
.then(result => {
|
||||
setUrlData(result.categories);
|
||||
setLastUpdated(result.lastUpdated);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (parts.length < 2) return null;
|
||||
if (parts[0] === '페이지' || parts[0].startsWith('---')) return null;
|
||||
|
||||
const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거
|
||||
const url = parts[1].replace(/`/g, ''); // backtick 제거
|
||||
const status = parts[2] || undefined;
|
||||
|
||||
// URL이 /ko로 시작하는지 확인
|
||||
if (!url.startsWith('/ko')) return null;
|
||||
|
||||
return { name, url, status };
|
||||
}
|
||||
|
||||
function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: string } {
|
||||
const lines = content.split('\n');
|
||||
const categories: UrlCategory[] = [];
|
||||
let currentCategory: UrlCategory | null = null;
|
||||
let currentSubCategory: { title: string; items: UrlItem[] } | null = null;
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
// Last Updated 추출
|
||||
const updateMatch = content.match(/Last Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
||||
if (updateMatch) {
|
||||
lastUpdated = updateMatch[1];
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// ## 카테고리 (메인 섹션)
|
||||
if (line.startsWith('## ') && !line.includes('클릭 가능한') && !line.includes('전체 URL') && !line.includes('백엔드 메뉴')) {
|
||||
// 이전 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
currentSubCategory = null;
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim();
|
||||
currentCategory = {
|
||||
title,
|
||||
icon: getIcon(title),
|
||||
items: [],
|
||||
subCategories: [],
|
||||
};
|
||||
currentSubCategory = null;
|
||||
}
|
||||
|
||||
// ### 서브 카테고리
|
||||
else if (line.startsWith('### ') && currentCategory) {
|
||||
// 이전 서브카테고리 저장
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
|
||||
const subTitle = line.replace('### ', '').trim();
|
||||
// "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로
|
||||
if (subTitle === '메인 페이지') {
|
||||
currentSubCategory = null;
|
||||
} else {
|
||||
currentSubCategory = {
|
||||
title: subTitle,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 행 파싱
|
||||
else if (line.startsWith('|') && currentCategory) {
|
||||
const item = parseTableRow(line);
|
||||
if (item) {
|
||||
if (currentSubCategory) {
|
||||
currentSubCategory.items.push(item);
|
||||
} else {
|
||||
currentCategory.items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
// 빈 서브카테고리 제거
|
||||
categories.forEach(cat => {
|
||||
cat.subCategories = cat.subCategories?.filter(sub => sub.items.length > 0);
|
||||
});
|
||||
|
||||
return { categories, lastUpdated };
|
||||
}
|
||||
|
||||
export default async function TestUrlsPage() {
|
||||
// md 파일 경로
|
||||
const mdFilePath = path.join(
|
||||
process.cwd(),
|
||||
'claudedocs',
|
||||
'[REF] all-pages-test-urls.md'
|
||||
);
|
||||
|
||||
let urlData: UrlCategory[] = [];
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(mdFilePath, 'utf-8');
|
||||
const parsed = parseMdFile(fileContent);
|
||||
urlData = parsed.categories;
|
||||
lastUpdated = parsed.lastUpdated;
|
||||
} catch (error) {
|
||||
console.error('Failed to read md file:', error);
|
||||
// 파일 읽기 실패 시 빈 데이터
|
||||
urlData = [];
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <TestUrlsClient initialData={urlData} lastUpdated={lastUpdated} />;
|
||||
}
|
||||
|
||||
// 캐싱 비활성화 - 항상 최신 md 파일 읽기
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 0;
|
||||
}
|
||||
@@ -1,42 +1,62 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 공정 수정 페이지
|
||||
* 공정 수정 페이지 (Client Component)
|
||||
*/
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ProcessForm } from '@/components/process-management';
|
||||
import { getProcessById } from '@/components/process-management/actions';
|
||||
import type { Process } from '@/components/process-management/types';
|
||||
|
||||
export default async function EditProcessPage({
|
||||
export default function EditProcessPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const result = await getProcessById(id);
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Process | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
notFound();
|
||||
useEffect(() => {
|
||||
getProcessById(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('공정 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('공정 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">공정 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ProcessForm mode="edit" initialData={result.data} />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const result = await getProcessById(id);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
title: '공정을 찾을 수 없습니다',
|
||||
};
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '공정을 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${result.data.processName} - 공정 수정`,
|
||||
description: `${result.data.processCode} 공정 수정`,
|
||||
};
|
||||
}
|
||||
return <ProcessForm mode="edit" initialData={data} />;
|
||||
}
|
||||
@@ -1,48 +1,62 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 공정 상세 페이지
|
||||
* 공정 상세 페이지 (Client Component)
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ProcessDetail } from '@/components/process-management';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { getProcessById } from '@/components/process-management/actions';
|
||||
import type { Process } from '@/components/process-management/types';
|
||||
|
||||
export default async function ProcessDetailPage({
|
||||
export default function ProcessDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const result = await getProcessById(id);
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Process | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
notFound();
|
||||
useEffect(() => {
|
||||
getProcessById(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('공정 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('공정 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">공정 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ContentLoadingSpinner text="공정 정보를 불러오는 중..." />}>
|
||||
<ProcessDetail process={result.data} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const result = await getProcessById(id);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
title: '공정을 찾을 수 없습니다',
|
||||
};
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '공정을 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${result.data.processName} - 공정 상세`,
|
||||
description: `${result.data.processCode} 공정 정보`,
|
||||
};
|
||||
}
|
||||
return <ProcessDetail process={data} />;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { ReceivingDetail } from '@/components/material/ReceivingManagement';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function ReceivingDetailPage({ params }: Props) {
|
||||
const { id } = await params;
|
||||
export default function ReceivingDetailPage({ params }: Props) {
|
||||
const { id } = use(params);
|
||||
return <ReceivingDetail id={id} />;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { StockStatusDetail } from '@/components/material/StockStatus';
|
||||
|
||||
interface StockStatusDetailPageProps {
|
||||
@@ -6,7 +9,7 @@ interface StockStatusDetailPageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function StockStatusDetailPage({ params }: StockStatusDetailPageProps) {
|
||||
const { id } = await params;
|
||||
export default function StockStatusDetailPage({ params }: StockStatusDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
return <StockStatusDetail id={id} />;
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 출하관리 - 수정 페이지
|
||||
* 출하관리 - 수정 페이지 (Client Component)
|
||||
* URL: /outbound/shipments/[id]/edit
|
||||
*/
|
||||
|
||||
import { use } from 'react';
|
||||
import { ShipmentEdit } from '@/components/outbound/ShipmentManagement';
|
||||
|
||||
interface ShipmentEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function ShipmentEditPage({ params }: ShipmentEditPageProps) {
|
||||
const { id } = await params;
|
||||
export default function ShipmentEditPage({ params }: ShipmentEditPageProps) {
|
||||
const { id } = use(params);
|
||||
return <ShipmentEdit id={id} />;
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 출하관리 - 상세 페이지
|
||||
* 출하관리 - 상세 페이지 (Client Component)
|
||||
* URL: /outbound/shipments/[id]
|
||||
*/
|
||||
|
||||
import { use } from 'react';
|
||||
import { ShipmentDetail } from '@/components/outbound/ShipmentManagement';
|
||||
|
||||
interface ShipmentDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function ShipmentDetailPage({ params }: ShipmentDetailPageProps) {
|
||||
const { id } = await params;
|
||||
export default function ShipmentDetailPage({ params }: ShipmentDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
return <ShipmentDetail id={id} />;
|
||||
}
|
||||
@@ -1,13 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PaymentHistoryManagement } from '@/components/settings/PaymentHistoryManagement';
|
||||
import { getPayments } from '@/components/settings/PaymentHistoryManagement/actions';
|
||||
|
||||
export default async function PaymentHistoryPage() {
|
||||
const result = await getPayments({ perPage: 100 });
|
||||
export default function PaymentHistoryPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getPayments>>['data']>(undefined);
|
||||
const [pagination, setPagination] = useState<Awaited<ReturnType<typeof getPayments>>['pagination']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getPayments({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PaymentHistoryManagement
|
||||
initialData={result.data}
|
||||
initialPagination={result.pagination}
|
||||
initialData={data}
|
||||
initialPagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 품목 상세 조회 페이지
|
||||
* 품목 상세 조회 페이지 (Client Component)
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import ItemDetailClient from '@/components/items/ItemDetailClient';
|
||||
import type { ItemMaster } from '@/types/item';
|
||||
|
||||
@@ -134,62 +136,53 @@ const mockItems: ItemMaster[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 품목 조회 함수
|
||||
* TODO: API 연동 시 fetchItemByCode()로 교체
|
||||
*/
|
||||
async function getItemByCode(itemCode: string): Promise<ItemMaster | null> {
|
||||
// API 연동 전 mock 데이터 반환
|
||||
// const item = await fetchItemByCode(itemCode);
|
||||
const item = mockItems.find(
|
||||
(item) => item.itemCode === decodeURIComponent(itemCode)
|
||||
);
|
||||
return item || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 상세 페이지
|
||||
*/
|
||||
export default async function ItemDetailPage({
|
||||
export default function ItemDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const item = await getItemByCode(id);
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [item, setItem] = useState<ItemMaster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// API 연동 전 mock 데이터 사용
|
||||
const foundItem = mockItems.find(
|
||||
(item) => item.itemCode === decodeURIComponent(id)
|
||||
);
|
||||
setItem(foundItem || null);
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
notFound();
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">품목을 찾을 수 없습니다.</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Suspense fallback={<div className="text-center py-8">로딩 중...</div>}>
|
||||
<ItemDetailClient item={item} />
|
||||
</Suspense>
|
||||
<ItemDetailClient item={item} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메타데이터 설정
|
||||
*/
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const item = await getItemByCode(id);
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
title: '품목을 찾을 수 없습니다',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${item.itemName} - 품목 상세`,
|
||||
description: `${item.itemCode} 품목 정보`,
|
||||
};
|
||||
}
|
||||
@@ -1,160 +1,16 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 품목 목록 페이지 (Server Component)
|
||||
* 품목 목록 페이지 (Client Component)
|
||||
*
|
||||
* Next.js 15 App Router
|
||||
* 서버에서 데이터 fetching 후 Client Component로 전달
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import ItemListClient from '@/components/items/ItemListClient';
|
||||
import type { ItemMaster } from '@/types/item';
|
||||
|
||||
// Mock 데이터 (API 연동 전 임시)
|
||||
const mockItems: ItemMaster[] = [
|
||||
{
|
||||
id: '1',
|
||||
itemCode: 'KD-FG-001',
|
||||
itemName: '스크린 제품 A',
|
||||
itemType: 'FG',
|
||||
unit: 'EA',
|
||||
specification: '2000x2000',
|
||||
isActive: true,
|
||||
category1: '본체부품',
|
||||
category2: '가이드시스템',
|
||||
salesPrice: 150000,
|
||||
purchasePrice: 100000,
|
||||
productCategory: 'SCREEN',
|
||||
lotAbbreviation: 'KD',
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
itemCode: 'KD-PT-001',
|
||||
itemName: '가이드레일(벽면형)',
|
||||
itemType: 'PT',
|
||||
unit: 'EA',
|
||||
specification: '2438mm',
|
||||
isActive: true,
|
||||
category1: '본체부품',
|
||||
category2: '가이드시스템',
|
||||
category3: '가이드레일',
|
||||
salesPrice: 50000,
|
||||
purchasePrice: 35000,
|
||||
partType: 'ASSEMBLY',
|
||||
partUsage: 'GUIDE_RAIL',
|
||||
installationType: '벽면형',
|
||||
assemblyType: 'M',
|
||||
assemblyLength: '2438',
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
itemCode: 'KD-PT-002',
|
||||
itemName: '절곡품 샘플',
|
||||
itemType: 'PT',
|
||||
unit: 'EA',
|
||||
specification: 'EGI 1.55T',
|
||||
isActive: true,
|
||||
partType: 'BENDING',
|
||||
material: 'EGI 1.55T',
|
||||
length: '2000',
|
||||
salesPrice: 30000,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
itemCode: 'KD-RM-001',
|
||||
itemName: 'SPHC-SD',
|
||||
itemType: 'RM',
|
||||
unit: 'KG',
|
||||
specification: '1.6T x 1219 x 2438',
|
||||
isActive: true,
|
||||
category1: '철강재',
|
||||
purchasePrice: 1500,
|
||||
material: 'SPHC-SD',
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
itemCode: 'KD-SM-001',
|
||||
itemName: '볼트 M6x20',
|
||||
itemType: 'SM',
|
||||
unit: 'EA',
|
||||
specification: 'M6x20',
|
||||
isActive: true,
|
||||
category1: '구조재/부속품',
|
||||
category2: '볼트/너트',
|
||||
purchasePrice: 50,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
itemCode: 'KD-CS-001',
|
||||
itemName: '절삭유',
|
||||
itemType: 'CS',
|
||||
unit: 'L',
|
||||
specification: '20L',
|
||||
isActive: true,
|
||||
purchasePrice: 30000,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
itemCode: 'KD-FG-002',
|
||||
itemName: '철재 제품 B',
|
||||
itemType: 'FG',
|
||||
unit: 'SET',
|
||||
specification: '3000x2500',
|
||||
isActive: false,
|
||||
category1: '본체부품',
|
||||
salesPrice: 200000,
|
||||
productCategory: 'STEEL',
|
||||
lotAbbreviation: 'KD',
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-09T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 품목 목록 조회 함수
|
||||
* TODO: API 연동 시 fetchItems()로 교체
|
||||
*/
|
||||
async function getItems(): Promise<ItemMaster[]> {
|
||||
// API 연동 전 mock 데이터 반환
|
||||
// const items = await fetchItems();
|
||||
return mockItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 목록 페이지
|
||||
*/
|
||||
export default async function ItemsPage() {
|
||||
const items = await getItems();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div className="text-center py-8">로딩 중...</div>}>
|
||||
<ItemListClient />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메타데이터 설정
|
||||
*/
|
||||
export const metadata = {
|
||||
title: '품목 관리',
|
||||
description: '품목 목록 조회 및 관리',
|
||||
};
|
||||
export default function ItemsPage() {
|
||||
return <ItemListClient />;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 작업지시 상세 페이지
|
||||
* 작업지시 상세 페이지 (Client Component)
|
||||
* URL: /production/work-orders/[id]
|
||||
*/
|
||||
|
||||
import { use } from 'react';
|
||||
import { WorkOrderDetail } from '@/components/production/WorkOrders';
|
||||
|
||||
interface PageProps {
|
||||
@@ -11,7 +14,7 @@ interface PageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function WorkOrderDetailPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
export default function WorkOrderDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
return <WorkOrderDetail orderId={id} />;
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 검사 상세/수정 페이지
|
||||
* 검사 상세/수정 페이지 (Client Component)
|
||||
* URL: /quality/inspections/[id]
|
||||
* 수정 모드: /quality/inspections/[id]?mode=edit
|
||||
*/
|
||||
|
||||
import { use } from 'react';
|
||||
import { InspectionDetail } from '@/components/quality/InspectionManagement';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function InspectionDetailPage({ params }: Props) {
|
||||
const { id } = await params;
|
||||
export default function InspectionDetailPage({ params }: Props) {
|
||||
const { id } = use(params);
|
||||
return <InspectionDetail id={id} />;
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 단가 수정 페이지
|
||||
* 단가 수정 페이지 (Client Component)
|
||||
*
|
||||
* 경로: /sales/pricing-management/[id]/edit
|
||||
* API: GET /api/v1/pricing/{id}, PUT /api/v1/pricing/{id}
|
||||
*/
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { PricingFormClient } from '@/components/pricing';
|
||||
import { getPricingById, updatePricing, finalizePricing } from '@/components/pricing/actions';
|
||||
import type { PricingData } from '@/components/pricing';
|
||||
@@ -15,57 +19,79 @@ interface EditPricingPageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function EditPricingPage({ params }: EditPricingPageProps) {
|
||||
const { id } = await params;
|
||||
export default function EditPricingPage({ params }: EditPricingPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<PricingData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 기존 단가 데이터 조회
|
||||
const pricingData = await getPricingById(id);
|
||||
useEffect(() => {
|
||||
getPricingById(id)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
setData(result);
|
||||
} else {
|
||||
setError('단가 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('단가 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (!pricingData) {
|
||||
// 단가 수정 핸들러
|
||||
const handleSave = async (formData: PricingData, isRevision?: boolean, revisionReason?: string) => {
|
||||
const result = await updatePricing(id, formData, revisionReason);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '단가 수정에 실패했습니다.');
|
||||
}
|
||||
console.log('[EditPricingPage] 단가 수정 성공:', result.data, { isRevision, revisionReason });
|
||||
};
|
||||
|
||||
// 단가 확정 핸들러
|
||||
const handleFinalize = async (priceId: string) => {
|
||||
const result = await finalizePricing(priceId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '단가 확정에 실패했습니다.');
|
||||
}
|
||||
console.log('[EditPricingPage] 단가 확정 성공:', result.data);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4">
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-xl font-semibold mb-2">단가 정보를 찾을 수 없습니다</h2>
|
||||
<p className="text-muted-foreground">
|
||||
<h2 className="text-xl font-semibold mb-2">{error || '단가 정보를 찾을 수 없습니다'}</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
올바른 단가 정보로 다시 시도해주세요.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 서버 액션: 단가 수정
|
||||
async function handleSave(data: PricingData, isRevision?: boolean, revisionReason?: string) {
|
||||
'use server';
|
||||
|
||||
const result = await updatePricing(id, data, revisionReason);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '단가 수정에 실패했습니다.');
|
||||
}
|
||||
|
||||
console.log('[EditPricingPage] 단가 수정 성공:', result.data, { isRevision, revisionReason });
|
||||
}
|
||||
|
||||
// 서버 액션: 단가 확정
|
||||
async function handleFinalize(priceId: string) {
|
||||
'use server';
|
||||
|
||||
const result = await finalizePricing(priceId);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '단가 확정에 실패했습니다.');
|
||||
}
|
||||
|
||||
console.log('[EditPricingPage] 단가 확정 성공:', result.data);
|
||||
}
|
||||
|
||||
return (
|
||||
<PricingFormClient
|
||||
mode="edit"
|
||||
initialData={pricingData}
|
||||
initialData={data}
|
||||
onSave={handleSave}
|
||||
onFinalize={handleFinalize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 단가 등록 페이지
|
||||
* 단가 등록 페이지 (Client Component)
|
||||
*
|
||||
* 경로: /sales/pricing-management/create?itemId=xxx
|
||||
* API: POST /api/v1/pricing
|
||||
@@ -7,63 +9,95 @@
|
||||
* item_type_code는 품목 정보에서 자동으로 가져옴 (FG, PT, SM, RM, CS 등)
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { PricingFormClient } from '@/components/pricing';
|
||||
import { getItemInfo, createPricing } from '@/components/pricing/actions';
|
||||
import type { PricingData } from '@/components/pricing';
|
||||
import type { PricingData, ItemInfo } from '@/components/pricing';
|
||||
|
||||
interface CreatePricingPageProps {
|
||||
searchParams: Promise<{
|
||||
itemId?: string;
|
||||
itemCode?: string;
|
||||
}>;
|
||||
}
|
||||
export default function CreatePricingPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const itemId = searchParams.get('itemId') || '';
|
||||
|
||||
export default async function CreatePricingPage({ searchParams }: CreatePricingPageProps) {
|
||||
const params = await searchParams;
|
||||
const itemId = params.itemId || '';
|
||||
const [itemInfo, setItemInfo] = useState<ItemInfo | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 품목 정보 조회
|
||||
const itemInfo = itemId ? await getItemInfo(itemId) : null;
|
||||
useEffect(() => {
|
||||
if (!itemId) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!itemInfo && itemId) {
|
||||
getItemInfo(itemId)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
setItemInfo(result);
|
||||
} else {
|
||||
setError('품목 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('품목 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [itemId]);
|
||||
|
||||
// 단가 등록 핸들러
|
||||
const handleSave = async (data: PricingData) => {
|
||||
const result = await createPricing(data);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '단가 등록에 실패했습니다.');
|
||||
}
|
||||
console.log('[CreatePricingPage] 단가 등록 성공:', result.data);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4">
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-xl font-semibold mb-2">품목 정보를 찾을 수 없습니다</h2>
|
||||
<p className="text-muted-foreground">
|
||||
올바른 품목 정보로 다시 시도해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 품목 정보 없이 접근한 경우 (목록에서 바로 등록)
|
||||
if (!itemInfo) {
|
||||
// 품목 정보 없이 접근한 경우
|
||||
if (!itemId) {
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4">
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-xl font-semibold mb-2">품목을 선택해주세요</h2>
|
||||
<p className="text-muted-foreground">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
단가 목록에서 품목을 선택한 후 등록해주세요.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 서버 액션: 단가 등록
|
||||
// item_type_code는 data.itemType에서 자동으로 가져옴
|
||||
async function handleSave(data: PricingData) {
|
||||
'use server';
|
||||
|
||||
const result = await createPricing(data);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '단가 등록에 실패했습니다.');
|
||||
}
|
||||
|
||||
console.log('[CreatePricingPage] 단가 등록 성공:', result.data);
|
||||
if (error || !itemInfo) {
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4">
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-xl font-semibold mb-2">{error || '품목 정보를 찾을 수 없습니다'}</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
올바른 품목 정보로 다시 시도해주세요.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -73,4 +107,4 @@ export default async function CreatePricingPage({ searchParams }: CreatePricingP
|
||||
onSave={handleSave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 단가 목록 페이지
|
||||
* 단가 목록 페이지 (Client Component)
|
||||
*
|
||||
* 경로: /sales/pricing-management
|
||||
* API:
|
||||
@@ -10,302 +12,29 @@
|
||||
* 품목 목록 + 단가 목록 → 병합 → 품목별 단가 현황 표시
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PricingListClient } from '@/components/pricing';
|
||||
import type { PricingListItem, PricingStatus } from '@/components/pricing';
|
||||
import { cookies } from 'next/headers';
|
||||
import { getPricingListData, type PricingListItem } from '@/components/pricing/actions';
|
||||
|
||||
// ============================================
|
||||
// API 응답 타입 정의
|
||||
// ============================================
|
||||
export default function PricingManagementPage() {
|
||||
const [data, setData] = useState<PricingListItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// 품목 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;
|
||||
}
|
||||
useEffect(() => {
|
||||
getPricingListData()
|
||||
.then(result => {
|
||||
setData(result);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
interface ItemsApiResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
current_page: number;
|
||||
data: ItemApiData[];
|
||||
total: number;
|
||||
per_page: number;
|
||||
last_page: number;
|
||||
};
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 단가 API 응답 타입 (GET /api/v1/pricing)
|
||||
interface PriceApiItem {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
item_type_code: string; // FG, PT, SM, RM, CS (items.item_type과 동일)
|
||||
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;
|
||||
client_group?: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
product?: {
|
||||
id: number;
|
||||
product_code: string;
|
||||
product_name: string;
|
||||
specification: string | null;
|
||||
unit: string;
|
||||
product_type: string;
|
||||
};
|
||||
material?: {
|
||||
id: number;
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
specification: string | null;
|
||||
unit: string;
|
||||
product_type: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PricingApiResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
current_page: number;
|
||||
data: PriceApiItem[];
|
||||
total: number;
|
||||
per_page: number;
|
||||
last_page: number;
|
||||
};
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
// API 헤더 생성
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// 품목 유형 매핑 (type_code → 프론트엔드 ItemType)
|
||||
function mapItemType(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 mapStatus(apiStatus: string, isFinal: boolean): PricingStatus | 'not_registered' {
|
||||
if (isFinal) return 'finalized';
|
||||
switch (apiStatus) {
|
||||
case 'draft': return 'draft';
|
||||
case 'active': return 'active';
|
||||
case 'finalized': return 'finalized';
|
||||
default: return 'draft';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 호출 함수
|
||||
// ============================================
|
||||
|
||||
// 품목 목록 조회
|
||||
async function getItemsList(): Promise<ItemApiData[]> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?group_id=1&size=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PricingPage] Items API Error:', response.status, response.statusText);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ItemsApiResponse = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[PricingPage] No items data in response');
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.data.data;
|
||||
} catch (error) {
|
||||
console.error('[PricingPage] Items fetch error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 단가 목록 조회
|
||||
async function getPricingList(): Promise<PriceApiItem[]> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing?size=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PricingPage] Pricing API Error:', response.status, response.statusText);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: PricingApiResponse = await response.json();
|
||||
console.log('[PricingPage] Pricing API Response count:', result.data?.data?.length || 0);
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[PricingPage] No pricing data in response');
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.data.data;
|
||||
} catch (error) {
|
||||
console.error('[PricingPage] Pricing fetch error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 데이터 병합 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 품목 목록 + 단가 목록 병합
|
||||
*
|
||||
* - 품목 목록을 기준으로 순회
|
||||
* - 각 품목에 해당하는 단가 정보를 매핑 (item_type + item_id로 매칭)
|
||||
* - 단가 미등록 품목은 'not_registered' 상태로 표시
|
||||
*/
|
||||
function mergeItemsWithPricing(
|
||||
items: ItemApiData[],
|
||||
pricings: PriceApiItem[]
|
||||
): PricingListItem[] {
|
||||
// 단가 정보를 빠르게 찾기 위한 Map 생성
|
||||
// key: "{item_type}_{item_id}" (예: "FG_123", "PT_456")
|
||||
const pricingMap = new Map<string, PriceApiItem>();
|
||||
|
||||
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: mapItemType(item.item_type),
|
||||
specification: undefined, // items API에서는 specification 미제공
|
||||
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: mapStatus(pricing.status, pricing.is_final),
|
||||
currentRevision: 0,
|
||||
isFinal: pricing.is_final,
|
||||
itemTypeCode: item.item_type, // FG, PT, SM, RM, CS (단가 등록 시 필요)
|
||||
};
|
||||
} else {
|
||||
// 단가 미등록 품목
|
||||
return {
|
||||
id: `item_${item.id}`, // 임시 ID (단가 ID가 없으므로)
|
||||
itemId: String(item.id),
|
||||
itemCode: item.code,
|
||||
itemName: item.name,
|
||||
itemType: mapItemType(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, // FG, PT, SM, RM, CS (단가 등록 시 필요)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 페이지 컴포넌트
|
||||
// ============================================
|
||||
|
||||
export default async function PricingManagementPage() {
|
||||
// 품목 목록과 단가 목록을 병렬로 조회
|
||||
const [items, pricings] = await Promise.all([
|
||||
getItemsList(),
|
||||
getPricingList(),
|
||||
]);
|
||||
|
||||
console.log('[PricingPage] Items count:', items.length);
|
||||
console.log('[PricingPage] Pricings count:', pricings.length);
|
||||
|
||||
// 데이터 병합
|
||||
const mergedData = mergeItemsWithPricing(items, pricings);
|
||||
console.log('[PricingPage] Merged data count:', mergedData.length);
|
||||
|
||||
return (
|
||||
<PricingListClient initialData={mergedData} />
|
||||
);
|
||||
return <PricingListClient initialData={data} />;
|
||||
}
|
||||
@@ -1,20 +1,48 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 견적관리 페이지 (Server Component)
|
||||
* 견적관리 페이지 (Client Component)
|
||||
*
|
||||
* 초기 데이터를 서버에서 fetch하여 Client Component에 전달
|
||||
* 초기 데이터를 useEffect에서 fetch하여 Client Component에 전달
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { QuoteManagementClient } from '@/components/quotes/QuoteManagementClient';
|
||||
import { getQuotes } from '@/components/quotes/actions';
|
||||
|
||||
export default async function QuoteManagementPage() {
|
||||
// 서버에서 초기 데이터 조회
|
||||
const result = await getQuotes({ perPage: 100 });
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 100,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export default function QuoteManagementPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getQuotes>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getQuotes({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QuoteManagementClient
|
||||
initialData={result.data}
|
||||
initialPagination={result.pagination}
|
||||
initialData={data}
|
||||
initialPagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AccountInfoClient } from '@/components/settings/AccountInfoManagement';
|
||||
import { getAccountInfo } from '@/components/settings/AccountInfoManagement/actions';
|
||||
import type { AccountInfo, TermsAgreement, MarketingConsent } from '@/components/settings/AccountInfoManagement/types';
|
||||
|
||||
export default async function AccountInfoPage() {
|
||||
const result = await getAccountInfo();
|
||||
const DEFAULT_ACCOUNT_INFO: AccountInfo = {
|
||||
id: '',
|
||||
email: '',
|
||||
profileImage: undefined,
|
||||
role: '',
|
||||
status: 'active',
|
||||
isTenantMaster: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
};
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
// 실패 시 빈 데이터로 렌더링 (클라이언트에서 에러 처리)
|
||||
const DEFAULT_MARKETING_CONSENT: MarketingConsent = {
|
||||
email: { agreed: false },
|
||||
sms: { agreed: false },
|
||||
};
|
||||
|
||||
export default function AccountInfoPage() {
|
||||
const [accountInfo, setAccountInfo] = useState<AccountInfo>(DEFAULT_ACCOUNT_INFO);
|
||||
const [termsAgreements, setTermsAgreements] = useState<TermsAgreement[]>([]);
|
||||
const [marketingConsent, setMarketingConsent] = useState<MarketingConsent>(DEFAULT_MARKETING_CONSENT);
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getAccountInfo()
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setAccountInfo(result.data.accountInfo);
|
||||
setTermsAgreements(result.data.termsAgreements);
|
||||
setMarketingConsent(result.data.marketingConsent);
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<AccountInfoClient
|
||||
initialAccountInfo={{
|
||||
id: '',
|
||||
email: '',
|
||||
profileImage: undefined,
|
||||
role: '',
|
||||
status: 'active',
|
||||
isTenantMaster: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
}}
|
||||
initialTermsAgreements={[]}
|
||||
initialMarketingConsent={{
|
||||
email: { agreed: false },
|
||||
sms: { agreed: false },
|
||||
}}
|
||||
error={result.error}
|
||||
/>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountInfoClient
|
||||
initialAccountInfo={result.data.accountInfo}
|
||||
initialTermsAgreements={result.data.termsAgreements}
|
||||
initialMarketingConsent={result.data.marketingConsent}
|
||||
initialAccountInfo={accountInfo}
|
||||
initialTermsAgreements={termsAgreements}
|
||||
initialMarketingConsent={marketingConsent}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { NotificationSettingsManagement } from '@/components/settings/NotificationSettings';
|
||||
import { getNotificationSettings } from '@/components/settings/NotificationSettings/actions';
|
||||
import { DEFAULT_NOTIFICATION_SETTINGS } from '@/components/settings/NotificationSettings/types';
|
||||
import type { NotificationSettings } from '@/components/settings/NotificationSettings/types';
|
||||
|
||||
export default async function NotificationSettingsPage() {
|
||||
const result = await getNotificationSettings();
|
||||
export default function NotificationSettingsPage() {
|
||||
const [data, setData] = useState<NotificationSettings>(DEFAULT_NOTIFICATION_SETTINGS);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return <NotificationSettingsManagement initialData={result.data} />;
|
||||
useEffect(() => {
|
||||
getNotificationSettings()
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <NotificationSettingsManagement initialData={data} />;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { PermissionDetailClient } from '@/components/settings/PermissionManagement/PermissionDetailClient';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function PermissionDetailPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
export default function PermissionDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
return <PermissionDetailClient permissionId={id} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PopupList } from '@/components/settings/PopupManagement';
|
||||
import { getPopups } from '@/components/settings/PopupManagement/actions';
|
||||
import type { Popup } from '@/components/settings/PopupManagement/types';
|
||||
|
||||
export default async function PopupManagementPage() {
|
||||
const popups = await getPopups({ size: 100 });
|
||||
export default function PopupManagementPage() {
|
||||
const [data, setData] = useState<Popup[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return <PopupList initialData={popups} />;
|
||||
}
|
||||
useEffect(() => {
|
||||
getPopups({ size: 100 })
|
||||
.then(result => {
|
||||
setData(result);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <PopupList initialData={data} />;
|
||||
}
|
||||
@@ -1,8 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SubscriptionManagement } from '@/components/settings/SubscriptionManagement';
|
||||
import { getSubscriptionData } from '@/components/settings/SubscriptionManagement/actions';
|
||||
|
||||
export default async function SubscriptionPage() {
|
||||
const result = await getSubscriptionData();
|
||||
export default function SubscriptionPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getSubscriptionData>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return <SubscriptionManagement initialData={result.data} />;
|
||||
useEffect(() => {
|
||||
getSubscriptionData()
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SubscriptionManagement initialData={data} />;
|
||||
}
|
||||
Reference in New Issue
Block a user