-
-
품목 정보를 찾을 수 없습니다
-
- 올바른 품목 정보로 다시 시도해주세요.
-
-
+
);
}
- // 품목 정보 없이 접근한 경우 (목록에서 바로 등록)
- 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 (