- SearchableSelectionModal<T> 제네릭 컴포넌트 추출 (organisms) - 검색 모달 5개 리팩토링: SupplierSearch, QuotationSelect, SalesOrderSelect, OrderSelect, ItemSearch - shared-lookups API 유틸 추가 (거래처/품목/수주 등 공통 조회) - create-crud-service 확장 (lookup, search 메서드) - actions.ts 20+개 파일 lookup 패턴 통일 - 공통 페이지 패턴 가이드 문서 추가 - CLAUDE.md Common Component Usage Rules 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.9 KiB
TypeScript
161 lines
5.9 KiB
TypeScript
'use server';
|
|
|
|
|
|
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
|
import type { PaginatedApiResponse } from '@/lib/api/types';
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
import type { Account, AccountFormData, AccountStatus } from './types';
|
|
import { BANK_LABELS } from './types';
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
|
|
|
// ===== API 응답 타입 =====
|
|
interface BankAccountApiData {
|
|
id: number;
|
|
bank_code: string;
|
|
bank_name: string;
|
|
account_number: string;
|
|
account_holder: string;
|
|
account_name: string;
|
|
status: AccountStatus;
|
|
is_primary: boolean;
|
|
assigned_user_id?: number;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
}
|
|
|
|
type BankAccountPaginatedResponse = PaginatedApiResponse<BankAccountApiData>;
|
|
|
|
// ===== 데이터 변환 =====
|
|
function transformApiToFrontend(apiData: BankAccountApiData): Account {
|
|
return {
|
|
id: apiData.id,
|
|
bankCode: apiData.bank_code,
|
|
bankName: apiData.bank_name || BANK_LABELS[apiData.bank_code] || apiData.bank_code,
|
|
accountNumber: apiData.account_number,
|
|
accountName: apiData.account_name,
|
|
accountHolder: apiData.account_holder,
|
|
status: apiData.status,
|
|
isPrimary: apiData.is_primary,
|
|
assignedUserId: apiData.assigned_user_id,
|
|
createdAt: apiData.created_at || '',
|
|
updatedAt: apiData.updated_at || '',
|
|
};
|
|
}
|
|
|
|
function transformFrontendToApi(data: Partial<AccountFormData>): Record<string, unknown> {
|
|
return {
|
|
bank_code: data.bankCode,
|
|
bank_name: data.bankName || BANK_LABELS[data.bankCode || ''] || data.bankCode,
|
|
account_number: data.accountNumber,
|
|
account_holder: data.accountHolder,
|
|
account_name: data.accountName,
|
|
status: data.status,
|
|
};
|
|
}
|
|
|
|
// ===== 계좌 목록 조회 =====
|
|
export async function getBankAccounts(params?: {
|
|
page?: number; perPage?: number; search?: string;
|
|
}): Promise<{
|
|
success: boolean; data?: Account[]; meta?: { currentPage: number; lastPage: number; perPage: number; total: number };
|
|
error?: string; __authError?: boolean;
|
|
}> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page) searchParams.set('page', params.page.toString());
|
|
if (params?.perPage) searchParams.set('per_page', params.perPage.toString());
|
|
if (params?.search) searchParams.set('search', params.search);
|
|
const queryString = searchParams.toString();
|
|
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/api/v1/bank-accounts${queryString ? `?${queryString}` : ''}`,
|
|
transform: (data: BankAccountPaginatedResponse) => ({
|
|
accounts: (data?.data || []).map(transformApiToFrontend),
|
|
meta: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
|
}),
|
|
errorMessage: '계좌 목록 조회에 실패했습니다.',
|
|
});
|
|
return { success: result.success, data: result.data?.accounts, meta: result.data?.meta, error: result.error, __authError: result.__authError };
|
|
}
|
|
|
|
// ===== 계좌 상세 조회 =====
|
|
export async function getBankAccount(id: number): Promise<ActionResult<Account>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/bank-accounts/${id}`,
|
|
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
|
|
errorMessage: '계좌 조회에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 계좌 생성 =====
|
|
export async function createBankAccount(data: AccountFormData): Promise<ActionResult<Account>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/bank-accounts`,
|
|
method: 'POST',
|
|
body: transformFrontendToApi(data),
|
|
transform: (d: BankAccountApiData) => transformApiToFrontend(d),
|
|
errorMessage: '계좌 등록에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 계좌 수정 =====
|
|
export async function updateBankAccount(id: number, data: Partial<AccountFormData>): Promise<ActionResult<Account>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/bank-accounts/${id}`,
|
|
method: 'PUT',
|
|
body: transformFrontendToApi(data),
|
|
transform: (d: BankAccountApiData) => transformApiToFrontend(d),
|
|
errorMessage: '계좌 수정에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 계좌 삭제 =====
|
|
export async function deleteBankAccount(id: number): Promise<ActionResult> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/bank-accounts/${id}`,
|
|
method: 'DELETE',
|
|
errorMessage: '계좌 삭제에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 계좌 상태 토글 =====
|
|
export async function toggleBankAccountStatus(id: number): Promise<ActionResult<Account>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/bank-accounts/${id}/toggle`,
|
|
method: 'PATCH',
|
|
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
|
|
errorMessage: '상태 변경에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 대표 계좌 설정 =====
|
|
export async function setPrimaryBankAccount(id: number): Promise<ActionResult<Account>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/bank-accounts/${id}/set-primary`,
|
|
method: 'PATCH',
|
|
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
|
|
errorMessage: '대표 계좌 설정에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 다중 삭제 =====
|
|
export async function deleteBankAccounts(ids: number[]): Promise<{
|
|
success: boolean; deletedCount?: number; error?: string;
|
|
}> {
|
|
try {
|
|
const results = await Promise.all(ids.map(id => deleteBankAccount(id)));
|
|
const successCount = results.filter(r => r.success).length;
|
|
const failedCount = results.filter(r => !r.success).length;
|
|
|
|
if (failedCount > 0 && successCount === 0) {
|
|
return { success: false, error: '계좌 삭제에 실패했습니다.' };
|
|
}
|
|
if (failedCount > 0) {
|
|
return { success: true, deletedCount: successCount, error: `${failedCount}개의 계좌 삭제에 실패했습니다.` };
|
|
}
|
|
return { success: true, deletedCount: successCount };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
} |