diff --git a/claudedocs/[REF-2026-01-09] server-to-client-component-migration-checklist.md b/claudedocs/[REF-2026-01-09] server-to-client-component-migration-checklist.md new file mode 100644 index 00000000..073dc7e5 --- /dev/null +++ b/claudedocs/[REF-2026-01-09] server-to-client-component-migration-checklist.md @@ -0,0 +1,146 @@ +# Server Component → Client Component 마이그레이션 계획서 + +## 배경 +- **문제**: Server Component에서 API 호출 시 토큰 갱신(쿠키 수정)이 불가능 +- **원인**: Next.js 15에서 Server Component 렌더링 중 쿠키 수정 금지 +- **영향**: 토큰 만료 시 기본값 표시 → 데이터 덮어쓰기 위험 +- **결정**: 폐쇄형 사이트로 SEO 불필요, Client Component로 전환 + +## 변경 대상 (53개 페이지) + +### Settings (4개) +- [ ] settings/notification-settings/page.tsx +- [ ] settings/popup-management/page.tsx +- [ ] settings/permissions/[id]/page.tsx +- [ ] settings/account-info/page.tsx + +### Accounting (9개) +- [ ] accounting/vendors/page.tsx +- [ ] accounting/sales/page.tsx +- [ ] accounting/deposits/page.tsx +- [ ] accounting/bills/page.tsx +- [ ] accounting/withdrawals/page.tsx +- [ ] accounting/expected-expenses/page.tsx +- [ ] accounting/bad-debt-collection/page.tsx +- [ ] accounting/bad-debt-collection/[id]/page.tsx +- [ ] accounting/bad-debt-collection/[id]/edit/page.tsx + +### Sales (4개) +- [ ] sales/quote-management/page.tsx +- [ ] sales/pricing-management/page.tsx +- [ ] sales/pricing-management/[id]/edit/page.tsx +- [ ] sales/pricing-management/create/page.tsx + +### Production (3개) +- [ ] production/work-orders/[id]/page.tsx +- [ ] production/screen-production/page.tsx +- [ ] production/screen-production/[id]/page.tsx + +### Quality (1개) +- [ ] quality/inspections/[id]/page.tsx + +### Master Data (2개) +- [ ] master-data/process-management/[id]/page.tsx +- [ ] master-data/process-management/[id]/edit/page.tsx + +### Material (2개) +- [ ] material/stock-status/[id]/page.tsx +- [ ] material/receiving-management/[id]/page.tsx + +### Outbound (2개) +- [ ] outbound/shipments/[id]/page.tsx +- [ ] outbound/shipments/[id]/edit/page.tsx + +### Construction - Order (8개) +- [ ] construction/order/order-management/[id]/page.tsx +- [ ] construction/order/order-management/[id]/edit/page.tsx +- [ ] construction/order/site-management/[id]/page.tsx +- [ ] construction/order/site-management/[id]/edit/page.tsx +- [ ] construction/order/structure-review/[id]/page.tsx +- [ ] construction/order/structure-review/[id]/edit/page.tsx +- [ ] construction/order/base-info/items/[id]/page.tsx +- [ ] construction/order/base-info/pricing/[id]/page.tsx +- [ ] construction/order/base-info/pricing/[id]/edit/page.tsx +- [ ] construction/order/base-info/labor/[id]/page.tsx + +### Construction - Project/Bidding (8개) +- [ ] construction/project/bidding/[id]/page.tsx +- [ ] construction/project/bidding/[id]/edit/page.tsx +- [ ] construction/project/bidding/site-briefings/[id]/page.tsx +- [ ] construction/project/bidding/site-briefings/[id]/edit/page.tsx +- [ ] construction/project/bidding/estimates/[id]/page.tsx +- [ ] construction/project/bidding/estimates/[id]/edit/page.tsx +- [ ] construction/project/bidding/partners/[id]/page.tsx +- [ ] construction/project/bidding/partners/[id]/edit/page.tsx + +### Construction - Project/Contract (4개) +- [ ] construction/project/contract/[id]/page.tsx +- [ ] construction/project/contract/[id]/edit/page.tsx +- [ ] construction/project/contract/handover-report/[id]/page.tsx +- [ ] construction/project/contract/handover-report/[id]/edit/page.tsx + +### Others (4개) +- [ ] payment-history/page.tsx +- [ ] subscription/page.tsx +- [ ] dev/test-urls/page.tsx +- [ ] dev/construction-test-urls/page.tsx + +## 변환 패턴 + +### Before (Server Component) +```typescript +import { Component } from '@/components/...'; +import { getData } from '@/components/.../actions'; + +export default async function Page() { + const result = await getData(); + return ; +} +``` + +### After (Client Component) +```typescript +'use client'; + +import { useEffect, useState } from 'react'; +import { Component } from '@/components/...'; +import { getData } from '@/components/.../actions'; +import { DEFAULT_DATA } from '@/components/.../types'; + +export default function Page() { + const [data, setData] = useState(DEFAULT_DATA); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + getData() + .then(result => { + if (result.success) { + setData(result.data); + } + }) + .finally(() => setIsLoading(false)); + }, []); + + if (isLoading) { + return
로딩 중...
; + } + + return ; +} +``` + +## 추가 작업 + +### 1. RULES.md 업데이트 +- Client Component 사용 원칙 추가 +- SEO 불필요 폐쇄형 사이트 명시 + +### 2. fetch-wrapper.ts 정리 +- skipTokenRefresh 옵션 제거 (불필요해짐) + +### 3. actions.ts 정리 +- skipTokenRefresh 관련 코드 제거 + +## 진행 상태 +- 시작일: 2026-01-09 +- 현재 상태: 진행 중 diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx index 34a587ff..2d964ca9 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); - if (!badDebt) { - notFound(); + useEffect(() => { + getBadDebtById(id) + .then(result => { + if (result) { + setData(result); + } else { + setError('데이터를 찾을 수 없습니다.'); + } + }) + .catch(() => { + setError('데이터를 불러오는 중 오류가 발생했습니다.'); + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); } - return ; + if (error || !data) { + return ( +
+
{error || '데이터를 찾을 수 없습니다.'}
+ +
+ ); + } + + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx index 9c854537..282395bd 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); - if (!badDebt) { - notFound(); + useEffect(() => { + getBadDebtById(id) + .then(result => { + if (result) { + setData(result); + } else { + setError('데이터를 찾을 수 없습니다.'); + } + }) + .catch(() => { + setError('데이터를 불러오는 중 오류가 발생했습니다.'); + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); } - return ; + if (error || !data) { + return ( +
+
{error || '데이터를 찾을 수 없습니다.'}
+ +
+ ); + } + + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx index 99cff423..e38a1d7a 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx @@ -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>>([]); + const [summary, setSummary] = useState(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 ( +
+
로딩 중...
+
+ ); + } return ( ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/bills/page.tsx b/src/app/[locale]/(protected)/accounting/bills/page.tsx index 052cd949..8a196820 100644 --- a/src/app/[locale]/(protected)/accounting/bills/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bills/page.tsx @@ -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 { - 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([]); + 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 ( +
+
로딩 중...
+
); - - 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 ( ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/deposits/page.tsx b/src/app/[locale]/(protected)/accounting/deposits/page.tsx index e730fff5..231333bd 100644 --- a/src/app/[locale]/(protected)/accounting/deposits/page.tsx +++ b/src/app/[locale]/(protected)/accounting/deposits/page.tsx @@ -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>['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 ( +
+
로딩 중...
+
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx b/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx index f3125070..a9ae932e 100644 --- a/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx +++ b/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx @@ -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>['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 ( +
+
로딩 중...
+
+ ); + } return ( ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/sales/page.tsx b/src/app/[locale]/(protected)/accounting/sales/page.tsx index 6044332a..fd3f8d32 100644 --- a/src/app/[locale]/(protected)/accounting/sales/page.tsx +++ b/src/app/[locale]/(protected)/accounting/sales/page.tsx @@ -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>['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 ( +
+
로딩 중...
+
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/vendors/page.tsx b/src/app/[locale]/(protected)/accounting/vendors/page.tsx index b832e266..0267f640 100644 --- a/src/app/[locale]/(protected)/accounting/vendors/page.tsx +++ b/src/app/[locale]/(protected)/accounting/vendors/page.tsx @@ -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>['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 ( +
+
로딩 중...
+
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx b/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx index de7238bd..adb8f0e9 100644 --- a/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx +++ b/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx @@ -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>['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 ( +
+
로딩 중...
+
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/base-info/items/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/items/[id]/page.tsx index bb65ce7f..ed9f7553 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/items/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/items/[id]/page.tsx @@ -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 ; -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx index 173ab3b9..45e54d8e 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx @@ -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 ; diff --git a/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/edit/page.tsx index 9e8677e5..5298d713 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/edit/page.tsx @@ -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 ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/page.tsx index 35e3a13a..a5097a30 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/page.tsx @@ -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 ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/order-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/order/order-management/[id]/edit/page.tsx index c7965af1..6416e183 100644 --- a/src/app/[locale]/(protected)/construction/order/order-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/order-management/[id]/edit/page.tsx @@ -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>['data']>(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); } - return ; + if (error || !data) { + return ( +
+
{error || '주문 정보를 찾을 수 없습니다.'}
+ +
+ ); + } + + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx index 95f9d08a..e3f0018f 100644 --- a/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx @@ -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>['data']>(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); } - return ; + if (error || !data) { + return ( +
+
{error || '주문 정보를 찾을 수 없습니다.'}
+ +
+ ); + } + + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/site-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/order/site-management/[id]/edit/page.tsx index 2f7038ad..27e86534 100644 --- a/src/app/[locale]/(protected)/construction/order/site-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/site-management/[id]/edit/page.tsx @@ -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(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 ( +
+
로딩 중...
+
+ ); + } return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/site-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/site-management/[id]/page.tsx index efbf1981..0ba0ee0d 100644 --- a/src/app/[locale]/(protected)/construction/order/site-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/site-management/[id]/page.tsx @@ -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(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 ( +
+
로딩 중...
+
+ ); + } return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/structure-review/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/order/structure-review/[id]/edit/page.tsx index c30e82ce..c83f9720 100644 --- a/src/app/[locale]/(protected)/construction/order/structure-review/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/structure-review/[id]/edit/page.tsx @@ -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(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 ( +
+
로딩 중...
+
+ ); + } return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/structure-review/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/structure-review/[id]/page.tsx index bd234ce4..31d8886e 100644 --- a/src/app/[locale]/(protected)/construction/order/structure-review/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/structure-review/[id]/page.tsx @@ -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(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 ( +
+
로딩 중...
+
+ ); + } return ; -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/construction/project/bidding/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/[id]/edit/page.tsx index 6bd2c284..782b6382 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/[id]/edit/page.tsx @@ -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>['data']>(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + getBiddingDetail(id) + .then(result => { + setData(result.data); + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx index 5c370885..51cec9a3 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx @@ -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>['data']>(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + getBiddingDetail(id) + .then(result => { + setData(result.data); + }) + .finally(() => setIsLoading(false)); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx index 475bdc6e..311890aa 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx @@ -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 { +function getEstimateDetail(id: string): EstimateDetail { // TODO: 실제 API 연동 const mockData: EstimateDetail = { id, @@ -187,15 +190,30 @@ async function getEstimateDetail(id: string): Promise { 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(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const detail = getEstimateDetail(id); + setData(detail); + setIsLoading(false); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx index 9203692e..22385d31 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx @@ -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 { +function getEstimateDetail(id: string): EstimateDetail { // TODO: 실제 API 연동 const mockData: EstimateDetail = { id, @@ -187,15 +190,30 @@ async function getEstimateDetail(id: string): Promise { 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(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const detail = getEstimateDetail(id); + setData(detail); + setIsLoading(false); + }, [id]); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } return ( ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/edit/page.tsx index 2f64fccd..8f0bc930 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/edit/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx index 72fd696c..17f41dd4 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } return ( ); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/edit/page.tsx index aae936ed..19cb1f9b 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/edit/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx index dbcefdb5..2c8524c0 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } return ( ); } diff --git a/src/app/[locale]/(protected)/construction/project/contract/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/[id]/edit/page.tsx index a0e32370..7a1a6b89 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/[id]/edit/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx index b4ad89ca..654b0be4 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } return ( ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/edit/page.tsx index d472388a..cbb92c74 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/edit/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } return ( ); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx index 8292265d..03ad8289 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/construction-test-urls/actions.ts b/src/app/[locale]/(protected)/dev/construction-test-urls/actions.ts new file mode 100644 index 00000000..ae412ee4 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/construction-test-urls/actions.ts @@ -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 = { + '기본': '🏠', + '시스템': '💻', + '대시보드': '📊', +}; + +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' }; + } +} diff --git a/src/app/[locale]/(protected)/dev/construction-test-urls/page.tsx b/src/app/[locale]/(protected)/dev/construction-test-urls/page.tsx index 370cc150..35942381 100644 --- a/src/app/[locale]/(protected)/dev/construction-test-urls/page.tsx +++ b/src/app/[locale]/(protected)/dev/construction-test-urls/page.tsx @@ -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 = { - '기본': '🏠', - '시스템': '💻', - '대시보드': '📊', -}; +export default function TestUrlsPage() { + const [urlData, setUrlData] = useState([]); + 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 ( +
+
로딩 중...
+
); + } - 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 ; -} - -// 캐싱 비활성화 - 항상 최신 md 파일 읽기 -export const dynamic = 'force-dynamic'; -export const revalidate = 0; + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/test-urls/actions.ts b/src/app/[locale]/(protected)/dev/test-urls/actions.ts new file mode 100644 index 00000000..54f309c4 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/test-urls/actions.ts @@ -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 = { + '기본': '🏠', + '인사관리': '👥', + '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' }; + } +} diff --git a/src/app/[locale]/(protected)/dev/test-urls/page.tsx b/src/app/[locale]/(protected)/dev/test-urls/page.tsx index fbfcc7f8..73402243 100644 --- a/src/app/[locale]/(protected)/dev/test-urls/page.tsx +++ b/src/app/[locale]/(protected)/dev/test-urls/page.tsx @@ -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 = { - '기본': '🏠', - '인사관리': '👥', - '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([]); + 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 ( +
+
로딩 중...
+
+ ); } return ; -} - -// 캐싱 비활성화 - 항상 최신 md 파일 읽기 -export const dynamic = 'force-dynamic'; -export const revalidate = 0; \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx index e23b289d..24457f13 100644 --- a/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
공정 정보를 불러오는 중...
+
+ ); } - return ; -} - -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 ( +
+
{error || '공정을 찾을 수 없습니다.'}
+ +
+ ); } - return { - title: `${result.data.processName} - 공정 수정`, - description: `${result.data.processCode} 공정 수정`, - }; -} + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx index 42e983ae..8f5f70f9 100644 --- a/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
공정 정보를 불러오는 중...
+
+ ); } - return ( - }> - - - ); -} - -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 ( +
+
{error || '공정을 찾을 수 없습니다.'}
+ +
+ ); } - return { - title: `${result.data.processName} - 공정 상세`, - description: `${result.data.processCode} 공정 정보`, - }; -} + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/material/receiving-management/[id]/page.tsx b/src/app/[locale]/(protected)/material/receiving-management/[id]/page.tsx index 5bbc4740..0c65a80b 100644 --- a/src/app/[locale]/(protected)/material/receiving-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/material/receiving-management/[id]/page.tsx @@ -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 ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/material/stock-status/[id]/page.tsx b/src/app/[locale]/(protected)/material/stock-status/[id]/page.tsx index bf424c2e..cec31a7f 100644 --- a/src/app/[locale]/(protected)/material/stock-status/[id]/page.tsx +++ b/src/app/[locale]/(protected)/material/stock-status/[id]/page.tsx @@ -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 ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/outbound/shipments/[id]/edit/page.tsx b/src/app/[locale]/(protected)/outbound/shipments/[id]/edit/page.tsx index 345c614c..897df92e 100644 --- a/src/app/[locale]/(protected)/outbound/shipments/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/outbound/shipments/[id]/edit/page.tsx @@ -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 ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/outbound/shipments/[id]/page.tsx b/src/app/[locale]/(protected)/outbound/shipments/[id]/page.tsx index 2793fef4..c6961b5b 100644 --- a/src/app/[locale]/(protected)/outbound/shipments/[id]/page.tsx +++ b/src/app/[locale]/(protected)/outbound/shipments/[id]/page.tsx @@ -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 ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/payment-history/page.tsx b/src/app/[locale]/(protected)/payment-history/page.tsx index 5bb14a33..eef26c0e 100644 --- a/src/app/[locale]/(protected)/payment-history/page.tsx +++ b/src/app/[locale]/(protected)/payment-history/page.tsx @@ -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>['data']>(undefined); + const [pagination, setPagination] = useState>['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 ( +
+
로딩 중...
+
+ ); + } return ( ); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx b/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx index 74929ac1..a8bd08db 100644 --- a/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx @@ -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 { - // 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(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 ( +
+
로딩 중...
+
+ ); + } if (!item) { - notFound(); + return ( +
+
품목을 찾을 수 없습니다.
+ +
+ ); } return (
- 로딩 중...
}> - - + ); -} - -/** - * 메타데이터 설정 - */ -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} 품목 정보`, - }; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/production/screen-production/page.tsx b/src/app/[locale]/(protected)/production/screen-production/page.tsx index 5dcf0340..e9c54b24 100644 --- a/src/app/[locale]/(protected)/production/screen-production/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/page.tsx @@ -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 { - // API 연동 전 mock 데이터 반환 - // const items = await fetchItems(); - return mockItems; -} /** * 품목 목록 페이지 */ -export default async function ItemsPage() { - const items = await getItems(); - - return ( - 로딩 중...}> - - - ); -} - -/** - * 메타데이터 설정 - */ -export const metadata = { - title: '품목 관리', - description: '품목 목록 조회 및 관리', -}; \ No newline at end of file +export default function ItemsPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx b/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx index d47eb9c3..70fd23f8 100644 --- a/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx +++ b/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx @@ -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 ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/quality/inspections/[id]/page.tsx b/src/app/[locale]/(protected)/quality/inspections/[id]/page.tsx index cd446776..03d9a9d4 100644 --- a/src/app/[locale]/(protected)/quality/inspections/[id]/page.tsx +++ b/src/app/[locale]/(protected)/quality/inspections/[id]/page.tsx @@ -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 ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/sales/pricing-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/pricing-management/[id]/edit/page.tsx index e43b8425..90d2b4cd 100644 --- a/src/app/[locale]/(protected)/sales/pricing-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/sales/pricing-management/[id]/edit/page.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
로딩 중...
+
+ ); + } + + if (error || !data) { return (
-

단가 정보를 찾을 수 없습니다

-

+

{error || '단가 정보를 찾을 수 없습니다'}

+

올바른 단가 정보로 다시 시도해주세요.

+
); } - // 서버 액션: 단가 수정 - 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 ( ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx b/src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx index a382901b..304cc9d9 100644 --- a/src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx +++ b/src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( -
-
-

품목 정보를 찾을 수 없습니다

-

- 올바른 품목 정보로 다시 시도해주세요. -

-
+
+
로딩 중...
); } - // 품목 정보 없이 접근한 경우 (목록에서 바로 등록) - if (!itemInfo) { + // 품목 정보 없이 접근한 경우 + if (!itemId) { return (

품목을 선택해주세요

-

+

단가 목록에서 품목을 선택한 후 등록해주세요.

+
); } - // 서버 액션: 단가 등록 - // 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 ( +
+
+

{error || '품목 정보를 찾을 수 없습니다'}

+

+ 올바른 품목 정보로 다시 시도해주세요. +

+ +
+
+ ); } return ( @@ -73,4 +107,4 @@ export default async function CreatePricingPage({ searchParams }: CreatePricingP onSave={handleSave} /> ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/sales/pricing-management/page.tsx b/src/app/[locale]/(protected)/sales/pricing-management/page.tsx index 5b9ef67e..fa8f94e2 100644 --- a/src/app/[locale]/(protected)/sales/pricing-management/page.tsx +++ b/src/app/[locale]/(protected)/sales/pricing-management/page.tsx @@ -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([]); + 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 { - 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 { - 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 ( +
+
로딩 중...
+
); - - 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 { - 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(); - - 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 ( - - ); + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/sales/quote-management/page.tsx b/src/app/[locale]/(protected)/sales/quote-management/page.tsx index 6411c400..8e5b7259 100644 --- a/src/app/[locale]/(protected)/sales/quote-management/page.tsx +++ b/src/app/[locale]/(protected)/sales/quote-management/page.tsx @@ -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>['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 ( +
+
로딩 중...
+
+ ); + } return ( ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/account-info/page.tsx b/src/app/[locale]/(protected)/settings/account-info/page.tsx index 70f96533..98c960f2 100644 --- a/src/app/[locale]/(protected)/settings/account-info/page.tsx +++ b/src/app/[locale]/(protected)/settings/account-info/page.tsx @@ -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(DEFAULT_ACCOUNT_INFO); + const [termsAgreements, setTermsAgreements] = useState([]); + const [marketingConsent, setMarketingConsent] = useState(DEFAULT_MARKETING_CONSENT); + const [error, setError] = useState(); + 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 ( - +
+
로딩 중...
+
); } return ( ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/notification-settings/page.tsx b/src/app/[locale]/(protected)/settings/notification-settings/page.tsx index d1ab4306..56bc40a5 100644 --- a/src/app/[locale]/(protected)/settings/notification-settings/page.tsx +++ b/src/app/[locale]/(protected)/settings/notification-settings/page.tsx @@ -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(DEFAULT_NOTIFICATION_SETTINGS); + const [isLoading, setIsLoading] = useState(true); - return ; + useEffect(() => { + getNotificationSettings() + .then(result => { + if (result.success) { + setData(result.data); + } + }) + .finally(() => setIsLoading(false)); + }, []); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/permissions/[id]/page.tsx b/src/app/[locale]/(protected)/settings/permissions/[id]/page.tsx index e369015b..a761ad2f 100644 --- a/src/app/[locale]/(protected)/settings/permissions/[id]/page.tsx +++ b/src/app/[locale]/(protected)/settings/permissions/[id]/page.tsx @@ -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 ; -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/settings/popup-management/page.tsx b/src/app/[locale]/(protected)/settings/popup-management/page.tsx index 0f60417b..5f6454c2 100644 --- a/src/app/[locale]/(protected)/settings/popup-management/page.tsx +++ b/src/app/[locale]/(protected)/settings/popup-management/page.tsx @@ -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([]); + const [isLoading, setIsLoading] = useState(true); - return ; -} + useEffect(() => { + getPopups({ size: 100 }) + .then(result => { + setData(result); + }) + .finally(() => setIsLoading(false)); + }, []); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/subscription/page.tsx b/src/app/[locale]/(protected)/subscription/page.tsx index ee8baaf4..d3f4291f 100644 --- a/src/app/[locale]/(protected)/subscription/page.tsx +++ b/src/app/[locale]/(protected)/subscription/page.tsx @@ -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>['data']>(undefined); + const [isLoading, setIsLoading] = useState(true); - return ; + useEffect(() => { + getSubscriptionData() + .then(result => { + setData(result.data); + }) + .finally(() => setIsLoading(false)); + }, []); + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + return ; } \ No newline at end of file diff --git a/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx b/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx index d00673a8..4d67e1b0 100644 --- a/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx +++ b/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx @@ -198,7 +198,7 @@ export function DashboardSettingsDialog({ onClose(); }, [settings, onClose]); - // 커스텀 스위치 (ON/OFF 라벨 포함) + // 커스텀 스위치 (라이트 테마용) const ToggleSwitch = ({ checked, onCheckedChange, @@ -210,36 +210,20 @@ export function DashboardSettingsDialog({ type="button" onClick={() => onCheckedChange(!checked)} className={cn( - 'relative inline-flex h-7 w-14 items-center rounded-full transition-colors', - checked ? 'bg-cyan-500' : 'bg-gray-300' + 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors', + checked ? 'bg-blue-500' : 'bg-gray-300' )} > - ON - - - OFF - - ); - // 섹션 행 컴포넌트 + // 섹션 행 컴포넌트 (라이트 테마) const SectionRow = ({ label, checked, @@ -258,11 +242,16 @@ export function DashboardSettingsDialog({ children?: React.ReactNode; }) => ( -
+
{hasExpand && ( - )} - {label} + {label}
{children && ( - + {children} )} @@ -285,30 +274,30 @@ export function DashboardSettingsDialog({ return ( !open && handleCancel()}> - - - 항목 설정 + + + 항목 설정 -
+
{/* 오늘의 이슈 섹션 */} -
-
- 오늘의 이슈 +
+
+ 오늘의 이슈
{localSettings.todayIssue.enabled && ( -
+
{(Object.keys(TODAY_ISSUE_LABELS) as Array).map( (key) => (
- + {TODAY_ISSUE_LABELS[key]} - + {/* ■ 중소기업 판단 기준표 */}
- + 중소기업 판단 기준표
- - - + + + - - - + + + - - - + + + - - - + + +
조건기준충족 요건조건기준충족 요건
① 매출액업종별 상이업종별 기준 금액 이하① 매출액업종별 상이업종별 기준 금액 이하
② 자산총액5,000억원미만② 자산총액5,000억원미만
③ 독립성소유·경영대기업 계열 아님③ 독립성소유·경영대기업 계열 아님
@@ -451,20 +440,20 @@ export function DashboardSettingsDialog({ - - + + - - - - - - - - - + + + + + + + + +
업종 분류기준 매출액업종 분류기준 매출액
제조업1,500억원 이하
건설업1,000억원 이하
운수업1,000억원 이하
도매업1,000억원 이하
소매업600억원 이하
정보통신업600억원 이하
전문서비스업600억원 이하
숙박·음식점업400억원 이하
기타 서비스업400억원 이하
제조업1,500억원 이하
건설업1,000억원 이하
운수업1,000억원 이하
도매업1,000억원 이하
소매업600억원 이하
정보통신업600억원 이하
전문서비스업600억원 이하
숙박·음식점업400억원 이하
기타 서비스업400억원 이하
@@ -477,14 +466,14 @@ export function DashboardSettingsDialog({ - - + + - - + +
구분기준구분기준
5,000억원 미만직전 사업연도 말 자산총액5,000억원 미만직전 사업연도 말 자산총액
@@ -498,31 +487,31 @@ export function DashboardSettingsDialog({ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + +
구분내용판정구분내용판정
독립기업아래 항목에 모두 해당하지 않음충족독립기업아래 항목에 모두 해당하지 않음충족
기업집단 소속공정거래법상 상호출자제한 기업집단 소속미충족기업집단 소속공정거래법상 상호출자제한 기업집단 소속미충족
대기업 지분대기업이 발행주식 30% 이상 보유미충족대기업 지분대기업이 발행주식 30% 이상 보유미충족
관계기업 합산관계기업 포함 시 매출액·자산 기준 초과미충족관계기업 합산관계기업 포함 시 매출액·자산 기준 초과미충족
@@ -531,27 +520,27 @@ export function DashboardSettingsDialog({ {/* ■ 판정 결과 */}
- + 판정 결과
- - - + + + - - - + + + - - - + + +
판정조건접대비 기본한도판정조건접대비 기본한도
중소기업①②③ 모두 충족3,600만원중소기업①②③ 모두 충족3,600만원
일반법인①②③ 중 하나라도 미충족1,200만원일반법인①②③ 중 하나라도 미충족1,200만원
@@ -699,11 +688,18 @@ export function DashboardSettingsDialog({ />
- - - diff --git a/src/components/pricing/actions.ts b/src/components/pricing/actions.ts index ce120ca4..b84557b5 100644 --- a/src/components/pricing/actions.ts +++ b/src/components/pricing/actions.ts @@ -475,6 +475,186 @@ export async function finalizePricing(id: string): Promise<{ success: boolean; d } } +// ============================================ +// 품목 목록 + 단가 목록 병합 조회 +// ============================================ + +// 품목 API 응답 타입 (GET /api/v1/items) +interface ItemApiData { + id: number; + item_type: string; // FG, PT, SM, RM, CS (품목 유형) + code: string; + name: string; + unit: string; + category_id: number | null; + created_at: string; + deleted_at: string | null; +} + +// 단가 목록 조회용 타입 +interface PriceApiListItem { + id: number; + tenant_id: number; + item_type_code: string; + item_id: number; + client_group_id: number | null; + purchase_price: string | null; + processing_cost: string | null; + loss_rate: string | null; + margin_rate: string | null; + sales_price: string | null; + rounding_rule: 'round' | 'ceil' | 'floor'; + rounding_unit: number; + supplier: string | null; + effective_from: string; + effective_to: string | null; + status: 'draft' | 'active' | 'finalized'; + is_final: boolean; + finalized_at: string | null; + finalized_by: number | null; + note: string | null; + created_at: string; + updated_at: string; + deleted_at: string | null; +} + +// 목록 표시용 타입 +export interface PricingListItem { + id: string; + itemId: string; + itemCode: string; + itemName: string; + itemType: string; + specification?: string; + unit: string; + purchasePrice?: number; + processingCost?: number; + salesPrice?: number; + marginRate?: number; + effectiveDate?: string; + status: 'draft' | 'active' | 'finalized' | 'not_registered'; + currentRevision: number; + isFinal: boolean; + itemTypeCode: string; +} + +// 품목 유형 매핑 (type_code → 프론트엔드 ItemType) +function mapItemTypeForList(typeCode?: string): string { + switch (typeCode) { + case 'FG': return 'FG'; + case 'PT': return 'PT'; + case 'SM': return 'SM'; + case 'RM': return 'RM'; + case 'CS': return 'CS'; + default: return 'PT'; + } +} + +// API 상태 → 프론트엔드 상태 매핑 +function mapStatusForList(apiStatus: string, isFinal: boolean): 'draft' | 'active' | 'finalized' | 'not_registered' { + if (isFinal) return 'finalized'; + switch (apiStatus) { + case 'draft': return 'draft'; + case 'active': return 'active'; + case 'finalized': return 'finalized'; + default: return 'draft'; + } +} + +/** + * 단가 목록 데이터 조회 (품목 + 단가 병합) + */ +export async function getPricingListData(): Promise { + try { + // 품목 목록 조회 + const { response: itemsResponse, error: itemsError } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?group_id=1&size=100`, + { method: 'GET' } + ); + + if (itemsError || !itemsResponse) { + console.error('[PricingActions] Items fetch error:', itemsError?.message); + return []; + } + + const itemsResult = await itemsResponse.json(); + const items: ItemApiData[] = itemsResult.success ? (itemsResult.data?.data || []) : []; + + // 단가 목록 조회 + const { response: pricingResponse, error: pricingError } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing?size=100`, + { method: 'GET' } + ); + + if (pricingError || !pricingResponse) { + console.error('[PricingActions] Pricing fetch error:', pricingError?.message); + return []; + } + + const pricingResult = await pricingResponse.json(); + const pricings: PriceApiListItem[] = pricingResult.success ? (pricingResult.data?.data || []) : []; + + // 단가 정보를 빠르게 찾기 위한 Map 생성 + const pricingMap = new Map(); + for (const pricing of pricings) { + const key = `${pricing.item_type_code}_${pricing.item_id}`; + if (!pricingMap.has(key)) { + pricingMap.set(key, pricing); + } + } + + // 품목 목록을 기준으로 병합 + return items.map((item) => { + const key = `${item.item_type}_${item.id}`; + const pricing = pricingMap.get(key); + + if (pricing) { + return { + id: String(pricing.id), + itemId: String(item.id), + itemCode: item.code, + itemName: item.name, + itemType: mapItemTypeForList(item.item_type), + specification: undefined, + unit: item.unit || 'EA', + purchasePrice: pricing.purchase_price ? parseFloat(pricing.purchase_price) : undefined, + processingCost: pricing.processing_cost ? parseFloat(pricing.processing_cost) : undefined, + salesPrice: pricing.sales_price ? parseFloat(pricing.sales_price) : undefined, + marginRate: pricing.margin_rate ? parseFloat(pricing.margin_rate) : undefined, + effectiveDate: pricing.effective_from, + status: mapStatusForList(pricing.status, pricing.is_final), + currentRevision: 0, + isFinal: pricing.is_final, + itemTypeCode: item.item_type, + }; + } else { + return { + id: `item_${item.id}`, + itemId: String(item.id), + itemCode: item.code, + itemName: item.name, + itemType: mapItemTypeForList(item.item_type), + specification: undefined, + unit: item.unit || 'EA', + purchasePrice: undefined, + processingCost: undefined, + salesPrice: undefined, + marginRate: undefined, + effectiveDate: undefined, + status: 'not_registered' as const, + currentRevision: 0, + isFinal: false, + itemTypeCode: item.item_type, + }; + } + }); + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[PricingActions] getPricingListData error:', error); + return []; + } +} + /** * 단가 이력 조회 */ diff --git a/src/components/settings/NotificationSettings/ItemSettingsDialog.tsx b/src/components/settings/NotificationSettings/ItemSettingsDialog.tsx index 51f26267..5bd1c4ed 100644 --- a/src/components/settings/NotificationSettings/ItemSettingsDialog.tsx +++ b/src/components/settings/NotificationSettings/ItemSettingsDialog.tsx @@ -35,10 +35,10 @@ interface CategorySectionProps { function CategorySection({ title, enabled, onEnabledChange, children }: CategorySectionProps) { return ( -
+
{/* 카테고리 헤더 */} -
- {title} +
+ {title}
{/* 하위 항목 */} -
+
{children}
@@ -64,7 +64,7 @@ interface ItemRowProps { function ItemRow({ label, checked, onChange, disabled }: ItemRowProps) { return (
- {label} + {label} - + {/* 헤더 */} - +
- 항목 설정 + 항목 설정
@@ -315,17 +315,17 @@ export function ItemSettingsDialog({ isOpen, onClose, settings, onSave }: ItemSe
{/* 하단 버튼 */} -
+
diff --git a/src/lib/api/fetch-wrapper.ts b/src/lib/api/fetch-wrapper.ts index bf01cd4d..6242288a 100644 --- a/src/lib/api/fetch-wrapper.ts +++ b/src/lib/api/fetch-wrapper.ts @@ -107,7 +107,9 @@ export async function getServerApiHeaders(token?: string): Promise */ export async function serverFetch( url: string, - options?: RequestInit & { skipAuthCheck?: boolean } + options?: RequestInit & { + skipAuthCheck?: boolean; + } ): Promise<{ response: Response | null; error: ApiErrorResponse | null }> { try { const cookieStore = await cookies();