From ec0d97867f005332beacafa026f28c9a6b41147e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Wed, 11 Feb 2026 17:32:19 +0900 Subject: [PATCH] =?UTF-8?q?refactor(WEB):=20API=20=EC=9C=A0=ED=8B=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC,=20=EB=B6=88=ED=95=84=EC=9A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EA=B7=9C=EC=B9=99=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - executePaginatedAction, buildApiUrl 유틸 모듈 분리 - QuoteCalculationReport, demoStore, export.ts 불필요 코드 삭제 - CLAUDE.md에 Zod 스키마 검증 및 Server Action 공통 유틸 규칙 추가 - package.json 의존성 업데이트 Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 77 +++ ...025-02-10] frontend-improvement-roadmap.md | 1 + claudedocs/_index.md | 38 ++ package.json | 2 +- .../quotes/QuoteCalculationReport.tsx | 539 ------------------ src/components/quotes/index.ts | 1 - src/lib/api/execute-paginated-action.ts | 90 +++ src/lib/api/index.ts | 6 + src/lib/api/query-params.ts | 48 ++ src/lib/api/types.ts | 2 + src/lib/utils/export.ts | 44 -- src/stores/demoStore.ts | 39 -- 12 files changed, 263 insertions(+), 624 deletions(-) delete mode 100644 src/components/quotes/QuoteCalculationReport.tsx create mode 100644 src/lib/api/execute-paginated-action.ts create mode 100644 src/lib/api/query-params.ts delete mode 100644 src/stores/demoStore.ts diff --git a/CLAUDE.md b/CLAUDE.md index 5e76d64b..ce83bb35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -271,6 +271,83 @@ const [data, setData] = useState(() => { --- +## Zod 스키마 검증 (신규 코드 적용) +**Priority**: 🟡 + +### 적용 범위 +- **신규 폼**: Zod 스키마 필수 적용 +- **기존 폼**: 건드리지 않음 (정상 작동 중이면 마이그레이션 불필요) +- **API 응답**: 신규 서버 액션에서 선택적 적용 + +### 신규 폼 작성 패턴 +```typescript +import { z } from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +// 1. 스키마 정의 (타입 + 검증 한 번에) +const formSchema = z.object({ + itemName: z.string().min(1, '품목명을 입력하세요'), + quantity: z.number().min(1, '1 이상 입력하세요'), + status: z.enum(['active', 'inactive']), + memo: z.string().optional(), +}); + +// 2. 스키마에서 타입 추출 (별도 interface 정의 불필요) +type FormData = z.infer; + +// 3. useForm에 zodResolver 연결 +const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { itemName: '', quantity: 1, status: 'active' }, +}); +``` + +### 규칙 +- **스키마 위치**: 컴포넌트 파일 상단 또는 같은 디렉토리의 `schema.ts` +- **타입 추출**: `z.infer` 사용, 별도 `interface` 중복 정의 금지 +- **에러 메시지**: 한글로 작성 (사용자에게 직접 표시됨) +- **`as` 캐스트 지양**: Zod 스키마로 타입이 보장되므로 `as` 캐스트 불필요 + +### 사용하지 않는 경우 +- 기존 `rules={{ required: true }}` 패턴으로 작동 중인 폼 +- 단순 필드 1~2개짜리 인라인 폼 (오버엔지니어링) + +--- + +## Server Action 공통 유틸리티 (신규 코드 적용) +**Priority**: 🟡 + +### 신규 actions.ts 작성 시 필수 패턴: +- `buildApiUrl()` 사용 (직접 URLSearchParams 조립 금지) +- 페이지네이션 조회 → `executePaginatedAction()` 사용 +- 단건/목록 조회 → `executeServerAction()` 유지 +- `toPaginationMeta()` 직접 사용도 허용 + +```typescript +// ✅ 신규 코드 패턴 +import { buildApiUrl, executePaginatedAction } from '@/lib/api'; + +export async function getItems(params: SearchParams) { + return executePaginatedAction({ + url: buildApiUrl('/api/v1/items', { + search: params.search, + status: params.status !== 'all' ? params.status : undefined, + page: params.page, + per_page: params.perPage, + }), + transform: transformApiToFrontend, + errorMessage: '목록 조회에 실패했습니다.', + }); +} +``` + +### 기존 코드: 마이그레이션 없음 +- 잘 동작하는 기존 actions.ts는 수정하지 않음 +- 해당 파일을 수정할 일이 생길 때만 선택적으로 적용 + +--- + ## Common Component Usage Rules **Priority**: 🔴 diff --git a/claudedocs/[PLAN-2025-02-10] frontend-improvement-roadmap.md b/claudedocs/[PLAN-2025-02-10] frontend-improvement-roadmap.md index 14ac3fb4..2638651d 100644 --- a/claudedocs/[PLAN-2025-02-10] frontend-improvement-roadmap.md +++ b/claudedocs/[PLAN-2025-02-10] frontend-improvement-roadmap.md @@ -53,6 +53,7 @@ | 항목 | 상태 | 날짜 | |------|------|------| | Phase 1: 공통 훅 추출 (executeServerAction 등) | ✅ 완료 | 이전 세션 | +| 중복 코드 공통화 (buildApiUrl + executePaginatedAction) | ✅ 완료 | 2026-02-11 | | Phase 3: 공용 유틸 추출 (PaginatedApiResponse 등) | ✅ 완료 | 이전 세션 | | Phase 4: SearchableSelectionModal 공통화 | ✅ 완료 | 이전 세션 | | Phase 5: any 21건 + memo 3개 정리 | ✅ 완료 | 이전 세션 | diff --git a/claudedocs/_index.md b/claudedocs/_index.md index 814b866e..d5b91de1 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -149,6 +149,44 @@ export const remove = service.remove; **미전환 사유**: 84개 중 전환 가능 15~20개, 작업 2~4시간 대비 기능 변화 없음. 시간 대비 효율 낮음 +### Server Action 공통 유틸리티 — 신규 코드 적용 규칙 (2026-02-11) + +**결정**: 기존 actions.ts 마이그레이션 없음. **신규 actions.ts에만 `buildApiUrl` + `executePaginatedAction` 적용** + +**배경**: +- 89개 actions.ts 중 43개에서 동일한 URLSearchParams 조건부 `.set()` 패턴 반복 (326+ 건) +- 50+ 파일에서 `current_page → currentPage` 수동 변환 반복 +- `toPaginationMeta`가 `src/lib/api/types.ts`에 존재하나 import 0건 + +**생성된 유틸리티**: +1. `src/lib/api/query-params.ts` — `buildQueryParams()`, `buildApiUrl()`: URLSearchParams 보일러플레이트 제거 +2. `src/lib/api/execute-paginated-action.ts` — `executePaginatedAction()`: 페이지네이션 조회 패턴 통합 (내부에서 `toPaginationMeta` 사용) + +**효과**: +- 페이지네이션 조회 코드: ~20줄 → ~5줄 +- `DEFAULT_PAGINATION` 중앙화 (`execute-paginated-action.ts` 내부) +- `toPaginationMeta` 자동 활용 (직접 import 불필요) + +**미적용 사유**: 기존 89개 actions.ts는 정상 동작 중. 전면 전환 비용 >> 이득 + +### Zod 스키마 검증 — 신규 폼 적용 규칙 (2026-02-11) + +**결정**: 기존 폼은 건드리지 않음. **신규 폼에만 Zod + zodResolver 적용** + +**설치 상태**: `zod@^4.1.12`, `@hookform/resolvers@^5.2.2` — 이미 설치됨 + +**효과**: +1. 스키마 하나로 **타입 추론 + 런타임 검증** 동시 해결 (`z.infer`) +2. 별도 `interface` 중복 정의 불필요 +3. 신규 코드에서 `as` 캐스트 자연 감소 (D-2 개선 효과) + +**규칙**: +- 신규 폼 → `zodResolver(schema)` 사용 필수 (CLAUDE.md에 패턴 명시) +- 기존 `rules={{ required: true }}` 패턴 폼 → 마이그레이션 불필요 +- 단순 1~2 필드 인라인 폼 → Zod 불필요 (오버엔지니어링) + +**미적용 사유**: 기존 폼 수십 개를 전면 전환하는 비용 >> 이득. 신규 코드에서 점진적 확산 + --- ## 폴더 구조 diff --git a/package.json b/package.json index ee51be87..24c8b49b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint": "eslint", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", - "test:e2e:headed": "playwright test --headed" + "test:e2e:headed": "playwright test --h 1eaded" }, "dependencies": { "@capacitor/app": "^8.0.0", diff --git a/src/components/quotes/QuoteCalculationReport.tsx b/src/components/quotes/QuoteCalculationReport.tsx deleted file mode 100644 index 03abe929..00000000 --- a/src/components/quotes/QuoteCalculationReport.tsx +++ /dev/null @@ -1,539 +0,0 @@ -/** - * 견적 산출내역서 / 견적서 컴포넌트 - * - documentType="견적서": 간단한 견적서 - * - documentType="견적산출내역서": 상세 산출내역서 + 소요자재 내역 - */ - -import { QuoteFormData } from "./types"; -import type { BomMaterial } from "./types"; -import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types"; - -interface QuoteCalculationReportProps { - quote: QuoteFormData; - companyInfo?: CompanyFormData | null; - documentType?: "견적산출내역서" | "견적서"; - showDetailedBreakdown?: boolean; - showMaterialList?: boolean; -} - -export function QuoteCalculationReport({ - quote, - companyInfo, - documentType = "견적산출내역서", - showDetailedBreakdown = true, - showMaterialList = true -}: QuoteCalculationReportProps) { - const formatAmount = (amount: number | null | undefined) => { - if (amount == null) return '0'; - return Number(amount).toLocaleString('ko-KR'); - }; - - const formatDate = (dateStr: string) => { - if (!dateStr) return ''; - const date = new Date(dateStr); - return `${date.getFullYear()}년 ${String(date.getMonth() + 1).padStart(2, '0')}월 ${String(date.getDate()).padStart(2, '0')}일`; - }; - - // 총 금액 계산 (totalAmount > unitPrice * quantity > inspectionFee 우선순위) - const totalAmount = quote.items?.reduce((sum, item) => { - const itemTotal = item.totalAmount || - (item.unitPrice || 0) * (item.quantity || 1) || - (item.inspectionFee || 0) * (item.quantity || 1); - return sum + itemTotal; - }, 0) || 0; - - // 소요자재 내역 - BOM 자재 목록 (quote.bomMaterials)에서 가져옴 - // bomMaterials가 없으면 빈 배열 (BOM 계산 데이터 없음) - const materialItems = (quote.bomMaterials || []).map((material, index) => ({ - no: index + 1, - itemCode: material.itemCode || '-', - name: material.itemName || '-', - spec: material.specification || '-', - quantity: Math.floor(material.quantity || 1), - unit: material.unit || 'EA', - unitPrice: material.unitPrice || 0, - totalPrice: material.totalPrice || 0, - })); - - return ( - <> - - - {/* 문서 컴포넌트 */} -
- {/* 문서 헤더 */} -
-
- {documentType === "견적서" ? "견 적 서" : "견 적 산 출 내 역 서"} -
-
- 문서번호: {quote.id || '-'} | 작성일자: {formatDate(quote.registrationDate || '')} -
-
- - {/* 수요자 정보 */} -
-
수 요 자
-
- - - - - - - - - - - - - - - - - - - -
업체명{quote.clientName || '-'}
현장명{quote.siteName || '-'}담당자{quote.manager || '-'}
제품명{quote.items?.[0]?.productName || '-'}연락처{quote.contact || '-'}
-
-
- - {/* 공급자 정보 */} -
-
공 급 자
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
상호{companyInfo?.companyName || '-'}사업자등록번호{companyInfo?.businessNumber || '-'}
대표자{companyInfo?.representativeName || '-'}업태{companyInfo?.businessType || '-'}
종목{companyInfo?.businessCategory || '-'}
사업장주소{companyInfo?.address || '-'}
전화{companyInfo?.managerPhone || '-'}이메일{companyInfo?.email || '-'}
-
-
- - {/* 총 견적금액 */} -
-
총 견적금액
-
₩ {formatAmount(totalAmount)}
-
※ 부가가치세 별도
-
- - {/* 세부 산출내역서 */} - {showDetailedBreakdown && quote.items && quote.items.length > 0 && ( -
-
세 부 산 출 내 역
- - - - - - - - - - - - - - - {quote.items.map((item, index) => { - // 단가: unitPrice > inspectionFee 우선순위 - const unitPrice = item.unitPrice || item.inspectionFee || 0; - // 금액: totalAmount > unitPrice * quantity 우선순위 - const itemTotal = item.totalAmount || unitPrice * (item.quantity || 1); - return ( - - - - - - - - - - ); - })} - - - - - - - -
No.품목명규격수량단위단가금액
{index + 1}{item.productName}{`${item.openWidth}×${item.openHeight}mm`}{Math.floor(item.quantity || 0)}{item.unit || 'SET'}{formatAmount(unitPrice)}{formatAmount(itemTotal)}
공급가액 합계{formatAmount(totalAmount)}
-
- )} - - {/* 소요자재 내역 */} - {showMaterialList && documentType !== "견적서" && ( -
-
소 요 자 재 내 역
- - {/* 제품 정보 */} -
-
- - - - - - - - - - - - - - - - - - - - - -
제품구분{quote.items?.[0]?.productCategory === 'steel' ? '철재' : '스크린'}부호{quote.items?.[0]?.code || '-'}
오픈사이즈W {quote.items?.[0]?.openWidth || '-'} × H {quote.items?.[0]?.openHeight || '-'} (mm)제작사이즈W {Number(quote.items?.[0]?.openWidth || 0) + 100} × H {Number(quote.items?.[0]?.openHeight || 0) + 100} (mm)
수량{Math.floor(quote.items?.[0]?.quantity || 1)} {quote.items?.[0]?.unit || 'SET'}케이스2438 × 550 (mm)
-
-
- - {/* 자재 목록 테이블 */} - {materialItems.length > 0 ? ( - - - - - - - - - - - - - {materialItems.map((item, index) => ( - - - - - - - - - ))} - -
No.품목코드자재명규격수량단위
{index + 1}{item.itemCode}{item.name}{item.spec}{item.quantity}{item.unit}
- ) : ( -
- 소요자재 정보가 없습니다. (BOM 계산 데이터가 필요합니다) -
- )} -
- )} - - {/* 비고사항 */} - {quote.remarks && ( -
-
비 고 사 항
-
- {quote.remarks} -
-
- )} - - {/* 서명란 */} -
-
-
- 상기와 같이 견적합니다. -
-
-
-
{formatDate(quote.registrationDate || '')}
-
- 공급자: {companyInfo?.companyName || '-'} (인) -
-
-
-
- (인감
날인) -
-
-
-
-
- - {/* 하단 안내사항 */} -
-

【 유의사항 】

-

1. 본 견적서는 {formatDate(quote.registrationDate || '')} 기준으로 작성되었으며, 자재 가격 변동 시 조정될 수 있습니다.

-

2. 견적 유효기간은 발행일로부터 30일이며, 기간 경과 시 재견적이 필요합니다.

-

3. 제작 사양 및 수량 변경 시 견적 금액이 변동될 수 있습니다.

-

4. 현장 여건에 따라 추가 비용이 발생할 수 있습니다.

-

- 문의: {companyInfo?.managerName || quote.manager || '담당자'} | {companyInfo?.managerPhone || '-'} -

-
-
- - ); -} \ No newline at end of file diff --git a/src/components/quotes/index.ts b/src/components/quotes/index.ts index ec1986ae..8bba995c 100644 --- a/src/components/quotes/index.ts +++ b/src/components/quotes/index.ts @@ -8,7 +8,6 @@ export { QuoteManagementClient } from './QuoteManagementClient'; // 컴포넌트 export { QuoteDocument } from './QuoteDocument'; export { QuoteRegistration } from './QuoteRegistration'; -export { QuoteCalculationReport } from './QuoteCalculationReport'; export { PurchaseOrderDocument } from './PurchaseOrderDocument'; // 타입 diff --git a/src/lib/api/execute-paginated-action.ts b/src/lib/api/execute-paginated-action.ts new file mode 100644 index 00000000..a1359707 --- /dev/null +++ b/src/lib/api/execute-paginated-action.ts @@ -0,0 +1,90 @@ +/** + * 페이지네이션 조회 전용 Server Action 래퍼 + * + * executeServerAction + toPaginationMeta 조합을 통합하여 + * 50+ 파일에서 반복되는 15~25줄 패턴을 5~8줄로 줄입니다. + * + * 적용 범위: 신규 코드만 (기존 코드 마이그레이션 없음) + * + * @example + * ```typescript + * // Before: ~20줄 + * const result = await executeServerAction({ + * url: `${API_URL}/api/v1/bills?${queryString}`, + * transform: (data: BillPaginatedResponse) => ({ + * items: (data?.data || []).map(transformApiToFrontend), + * pagination: { currentPage: data?.current_page || 1, ... }, + * }), + * errorMessage: '어음 목록 조회에 실패했습니다.', + * }); + * return { success: result.success, data: result.data?.items || [], ... }; + * + * // After: ~5줄 + * return executePaginatedAction({ + * url: buildApiUrl('/api/v1/bills', params), + * transform: transformApiToFrontend, + * errorMessage: '어음 목록 조회에 실패했습니다.', + * }); + * ``` + */ + +import { executeServerAction } from './execute-server-action'; +import { toPaginationMeta, type PaginatedApiResponse, type PaginationMeta } from './types'; + +// ===== 반환 타입 ===== +export interface PaginatedActionResult { + success: boolean; + data: T[]; + pagination: PaginationMeta; + error?: string; + __authError?: boolean; +} + +// ===== 옵션 타입 ===== +interface PaginatedActionOptions { + /** API URL (전체 경로) */ + url: string; + /** 개별 아이템 변환 함수 (API 응답 아이템 → 프론트엔드 타입) */ + transform: (item: TApi) => TResult; + /** 실패 시 기본 에러 메시지 */ + errorMessage: string; +} + +const DEFAULT_PAGINATION: PaginationMeta = { + currentPage: 1, + lastPage: 1, + perPage: 20, + total: 0, +}; + +/** + * 페이지네이션 조회 Server Action 실행 + * + * executeServerAction으로 API 호출 → data 배열에 transform 적용 → toPaginationMeta 변환 + */ +export async function executePaginatedAction( + options: PaginatedActionOptions +): Promise> { + const { url, transform, errorMessage } = options; + + const result = await executeServerAction>({ + url, + errorMessage, + }); + + if (!result.success || !result.data) { + return { + success: result.success, + data: [], + pagination: DEFAULT_PAGINATION, + error: result.error, + __authError: result.__authError, + }; + } + + return { + success: true, + data: (result.data.data || []).map(transform), + pagination: toPaginationMeta(result.data), + }; +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index a916cdf4..2e8755f5 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -14,6 +14,12 @@ export { type SelectOption, } from './types'; +// 쿼리 파라미터 빌더 (신규 코드용) +export { buildQueryParams, buildApiUrl } from './query-params'; + +// 페이지네이션 조회 래퍼 (신규 코드용) +export { executePaginatedAction, type PaginatedActionResult } from './execute-paginated-action'; + // 공용 룩업 헬퍼 (거래처/계좌 조회) export { fetchVendorOptions, diff --git a/src/lib/api/query-params.ts b/src/lib/api/query-params.ts new file mode 100644 index 00000000..f0b8064a --- /dev/null +++ b/src/lib/api/query-params.ts @@ -0,0 +1,48 @@ +/** + * 조건부 쿼리 파라미터 빌더 + * + * 43개 actions.ts에서 반복되는 URLSearchParams 보일러플레이트를 제거합니다. + * - undefined/null/'' 자동 필터링 + * - boolean/number 자동 String 변환 + * + * 적용 범위: 신규 코드만 (기존 코드 마이그레이션 없음) + */ + +type ParamValue = string | number | boolean | undefined | null; + +/** + * 조건부 쿼리 파라미터를 URLSearchParams로 변환 + * undefined/null/'' 값은 자동으로 제외됩니다. + */ +export function buildQueryParams( + params: Record +): URLSearchParams { + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null || value === '') continue; + searchParams.set(key, String(value)); + } + return searchParams; +} + +/** + * API URL + 조건부 쿼리 파라미터를 결합한 전체 URL 생성 + * + * @example + * buildApiUrl('/api/v1/bills', { + * search: params.search, + * bill_type: params.billType !== 'all' ? params.billType : undefined, + * page: params.page, + * per_page: params.perPage, + * }) + * // → "https://api.example.com/api/v1/bills?search=test&page=1&per_page=20" + */ +export function buildApiUrl( + path: string, + params?: Record +): string { + const API_URL = process.env.NEXT_PUBLIC_API_URL; + if (!params) return `${API_URL}${path}`; + const qs = buildQueryParams(params).toString(); + return qs ? `${API_URL}${path}?${qs}` : `${API_URL}${path}`; +} diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index 16fc3a5a..1b7f23c2 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -23,6 +23,8 @@ export interface PaginationMeta { } // ===== 프론트엔드 페이지네이션 결과 ===== +// 신규 코드용: executePaginatedAction 외부에서 transform 결과를 직접 조합할 때 사용 +// (현재 직접 사용처 0건, 삭제 금지) export interface PaginatedResult { items: T[]; pagination: PaginationMeta; diff --git a/src/lib/utils/export.ts b/src/lib/utils/export.ts index 83fb0222..01785dfb 100644 --- a/src/lib/utils/export.ts +++ b/src/lib/utils/export.ts @@ -61,47 +61,3 @@ export function generateExportFilename( return `${prefix}_${dateStr}_${timeStr}.${extension}`; } -/** - * 엑셀 내보내기 API 호출을 위한 fetch 옵션 생성 - * - * @param token - 인증 토큰 - * @param params - 쿼리 파라미터 (선택) - * @returns fetch 옵션 객체 - */ -export function createExportFetchOptions( - token: string, - params?: Record -): RequestInit { - return { - method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'X-API-KEY': process.env.API_KEY || '', - }, - cache: 'no-store', - }; -} - -/** - * 쿼리 파라미터 문자열 생성 - * - * @param params - 쿼리 파라미터 객체 - * @returns URL 쿼리 문자열 (예: '?year=2025&month=1') - */ -export function buildExportQueryString( - params?: Record -): string { - if (!params) return ''; - - const searchParams = new URLSearchParams(); - - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null && value !== '') { - searchParams.set(key, String(value)); - } - }); - - const queryString = searchParams.toString(); - return queryString ? `?${queryString}` : ''; -} diff --git a/src/stores/demoStore.ts b/src/stores/demoStore.ts deleted file mode 100644 index 9e78fe81..00000000 --- a/src/stores/demoStore.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; - -export type UserRole = 'SystemAdmin' | 'Manager' | 'User' | 'Guest'; - -interface DemoState { - userRole: UserRole; - companyName: string; - userName: string; - setUserRole: (role: UserRole) => void; - setCompanyName: (name: string) => void; - setUserName: (name: string) => void; - resetDemo: () => void; -} - -const DEFAULT_STATE = { - userRole: 'Manager' as UserRole, - companyName: 'SAM 데모 회사', - userName: '홍길동', -}; - -export const useDemoStore = create()( - persist( - (set) => ({ - ...DEFAULT_STATE, - - setUserRole: (role: UserRole) => set({ userRole: role }), - - setCompanyName: (name: string) => set({ companyName: name }), - - setUserName: (name: string) => set({ userName: name }), - - resetDemo: () => set(DEFAULT_STATE), - }), - { - name: 'sam-demo', - } - ) -); \ No newline at end of file