refactor(WEB): 전체 actions.ts에 공통 API 유틸 적용

- buildApiUrl / executePaginatedAction 패턴으로 전환 (40+ actions 파일)
- 직접 URLSearchParams 조립 → buildApiUrl 유틸 사용
- 수동 페이지네이션 메타 변환 → executePaginatedAction 자동 처리
- HandoverReportDocumentModal, OrderDocumentModal 개선
- 급여관리 SalaryManagement 코드 개선
- CLAUDE.md Server Action 공통 유틸 규칙 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-12 20:59:59 +09:00
parent 31be9d4a25
commit cbb38d48b9
51 changed files with 1050 additions and 1405 deletions

View File

@@ -2,13 +2,12 @@
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
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;
@@ -61,14 +60,12 @@ export async function getBankAccounts(params?: {
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}` : ''}`,
url: buildApiUrl('/api/v1/bank-accounts', {
page: params?.page,
per_page: params?.perPage,
search: params?.search,
}),
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 },
@@ -81,7 +78,7 @@ export async function getBankAccounts(params?: {
// ===== 계좌 상세 조회 =====
export async function getBankAccount(id: number): Promise<ActionResult<Account>> {
return executeServerAction({
url: `${API_URL}/api/v1/bank-accounts/${id}`,
url: buildApiUrl(`/api/v1/bank-accounts/${id}`),
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
errorMessage: '계좌 조회에 실패했습니다.',
});
@@ -90,7 +87,7 @@ export async function getBankAccount(id: number): Promise<ActionResult<Account>>
// ===== 계좌 생성 =====
export async function createBankAccount(data: AccountFormData): Promise<ActionResult<Account>> {
return executeServerAction({
url: `${API_URL}/api/v1/bank-accounts`,
url: buildApiUrl('/api/v1/bank-accounts'),
method: 'POST',
body: transformFrontendToApi(data),
transform: (d: BankAccountApiData) => transformApiToFrontend(d),
@@ -101,7 +98,7 @@ export async function createBankAccount(data: AccountFormData): Promise<ActionRe
// ===== 계좌 수정 =====
export async function updateBankAccount(id: number, data: Partial<AccountFormData>): Promise<ActionResult<Account>> {
return executeServerAction({
url: `${API_URL}/api/v1/bank-accounts/${id}`,
url: buildApiUrl(`/api/v1/bank-accounts/${id}`),
method: 'PUT',
body: transformFrontendToApi(data),
transform: (d: BankAccountApiData) => transformApiToFrontend(d),
@@ -112,7 +109,7 @@ export async function updateBankAccount(id: number, data: Partial<AccountFormDat
// ===== 계좌 삭제 =====
export async function deleteBankAccount(id: number): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/bank-accounts/${id}`,
url: buildApiUrl(`/api/v1/bank-accounts/${id}`),
method: 'DELETE',
errorMessage: '계좌 삭제에 실패했습니다.',
});
@@ -121,7 +118,7 @@ export async function deleteBankAccount(id: number): Promise<ActionResult> {
// ===== 계좌 상태 토글 =====
export async function toggleBankAccountStatus(id: number): Promise<ActionResult<Account>> {
return executeServerAction({
url: `${API_URL}/api/v1/bank-accounts/${id}/toggle`,
url: buildApiUrl(`/api/v1/bank-accounts/${id}/toggle`),
method: 'PATCH',
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
errorMessage: '상태 변경에 실패했습니다.',
@@ -131,7 +128,7 @@ export async function toggleBankAccountStatus(id: number): Promise<ActionResult<
// ===== 대표 계좌 설정 =====
export async function setPrimaryBankAccount(id: number): Promise<ActionResult<Account>> {
return executeServerAction({
url: `${API_URL}/api/v1/bank-accounts/${id}/set-primary`,
url: buildApiUrl(`/api/v1/bank-accounts/${id}/set-primary`),
method: 'PATCH',
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
errorMessage: '대표 계좌 설정에 실패했습니다.',
@@ -158,4 +155,4 @@ export async function deleteBankAccounts(ids: number[]): Promise<{
if (isNextRedirectError(error)) throw error;
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
}

View File

@@ -2,15 +2,12 @@
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import type { PaginatedApiResponse } from '@/lib/api/types';
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
import { buildApiUrl } from '@/lib/api/query-params';
import type { PaymentApiData, PaymentHistory } from './types';
import { transformApiToFrontend } from './utils';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== API 응답 타입 =====
type PaymentPaginatedResponse = PaginatedApiResponse<PaymentApiData>;
interface PaymentStatementApiData {
statement_no: string;
issued_at: string;
@@ -37,40 +34,28 @@ interface StatementData {
total: number;
}
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
// ===== 결제 목록 조회 =====
export async function getPayments(params?: {
page?: number; perPage?: number; status?: string; startDate?: string; endDate?: string; search?: string;
}): Promise<{
success: boolean; data: PaymentHistory[]; pagination: FrontendPagination;
error?: string; __authError?: boolean;
}> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.append('page', String(params.page));
if (params?.perPage) searchParams.append('per_page', String(params.perPage));
if (params?.status) searchParams.append('status', params.status);
if (params?.startDate) searchParams.append('start_date', params.startDate);
if (params?.endDate) searchParams.append('end_date', params.endDate);
if (params?.search) searchParams.append('search', params.search);
const queryString = searchParams.toString();
const result = await executeServerAction({
url: `${API_URL}/api/v1/payments${queryString ? `?${queryString}` : ''}`,
transform: (data: PaymentPaginatedResponse) => ({
items: (data?.data || []).map(transformApiToFrontend),
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
}) {
return executePaginatedAction<PaymentApiData, PaymentHistory>({
url: buildApiUrl('/api/v1/payments', {
page: params?.page,
per_page: params?.perPage,
status: params?.status,
start_date: params?.startDate,
end_date: params?.endDate,
search: params?.search,
}),
transform: transformApiToFrontend,
errorMessage: '결제 내역을 불러오는데 실패했습니다.',
});
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error, __authError: result.__authError };
}
// ===== 결제 명세서 조회 =====
export async function getPaymentStatement(id: string): Promise<ActionResult<StatementData>> {
return executeServerAction({
url: `${API_URL}/api/v1/payments/${id}/statement`,
url: buildApiUrl(`/api/v1/payments/${id}/statement`),
transform: (data: PaymentStatementApiData): StatementData => ({
statementNo: data.statement_no,
issuedAt: data.issued_at,

View File

@@ -2,32 +2,29 @@
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
import { revalidatePath } from 'next/cache';
import type { Role, RoleStats, PermissionMatrix, MenuTreeItem, PaginatedResponse } from './types';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ========== Role CRUD ==========
export async function fetchRoles(params?: {
page?: number; size?: number; q?: string; is_hidden?: boolean;
}): Promise<ActionResult<PaginatedResponse<Role>>> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', params.page.toString());
if (params?.size) searchParams.set('per_page', params.size.toString());
if (params?.q) searchParams.set('q', params.q);
if (params?.is_hidden !== undefined) searchParams.set('is_hidden', params.is_hidden.toString());
const queryString = searchParams.toString();
return executeServerAction<PaginatedResponse<Role>>({
url: `${API_URL}/api/v1/roles${queryString ? `?${queryString}` : ''}`,
url: buildApiUrl('/api/v1/roles', {
page: params?.page,
per_page: params?.size,
q: params?.q,
is_hidden: params?.is_hidden,
}),
errorMessage: '역할 목록 조회에 실패했습니다.',
});
}
export async function fetchRole(id: number): Promise<ActionResult<Role>> {
return executeServerAction<Role>({
url: `${API_URL}/api/v1/roles/${id}`,
url: buildApiUrl(`/api/v1/roles/${id}`),
errorMessage: '역할 조회에 실패했습니다.',
});
}
@@ -36,7 +33,7 @@ export async function createRole(data: {
name: string; description?: string; is_hidden?: boolean;
}): Promise<ActionResult<Role>> {
const result = await executeServerAction<Role>({
url: `${API_URL}/api/v1/roles`,
url: buildApiUrl('/api/v1/roles'),
method: 'POST',
body: data,
errorMessage: '역할 생성에 실패했습니다.',
@@ -49,7 +46,7 @@ export async function updateRole(id: number, data: {
name?: string; description?: string; is_hidden?: boolean;
}): Promise<ActionResult<Role>> {
const result = await executeServerAction<Role>({
url: `${API_URL}/api/v1/roles/${id}`,
url: buildApiUrl(`/api/v1/roles/${id}`),
method: 'PATCH',
body: data,
errorMessage: '역할 수정에 실패했습니다.',
@@ -63,7 +60,7 @@ export async function updateRole(id: number, data: {
export async function deleteRole(id: number): Promise<ActionResult> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/roles/${id}`,
url: buildApiUrl(`/api/v1/roles/${id}`),
method: 'DELETE',
errorMessage: '역할 삭제에 실패했습니다.',
});
@@ -73,14 +70,14 @@ export async function deleteRole(id: number): Promise<ActionResult> {
export async function fetchRoleStats(): Promise<ActionResult<RoleStats>> {
return executeServerAction<RoleStats>({
url: `${API_URL}/api/v1/roles/stats`,
url: buildApiUrl('/api/v1/roles/stats'),
errorMessage: '역할 통계 조회에 실패했습니다.',
});
}
export async function fetchActiveRoles(): Promise<ActionResult<Role[]>> {
return executeServerAction<Role[]>({
url: `${API_URL}/api/v1/roles/active`,
url: buildApiUrl('/api/v1/roles/active'),
errorMessage: '활성 역할 목록 조회에 실패했습니다.',
});
}
@@ -91,14 +88,14 @@ export async function fetchPermissionMenus(): Promise<ActionResult<{
menus: MenuTreeItem[]; permission_types: string[];
}>> {
return executeServerAction({
url: `${API_URL}/api/v1/role-permissions/menus`,
url: buildApiUrl('/api/v1/role-permissions/menus'),
errorMessage: '메뉴 트리 조회에 실패했습니다.',
});
}
export async function fetchPermissionMatrix(roleId: number): Promise<ActionResult<PermissionMatrix>> {
return executeServerAction<PermissionMatrix>({
url: `${API_URL}/api/v1/roles/${roleId}/permissions/matrix`,
url: buildApiUrl(`/api/v1/roles/${roleId}/permissions/matrix`),
errorMessage: '권한 매트릭스 조회에 실패했습니다.',
});
}
@@ -107,7 +104,7 @@ export async function togglePermission(roleId: number, menuId: number, permissio
granted: boolean; propagated_to: number[];
}>> {
const result = await executeServerAction<{ granted: boolean; propagated_to: number[] }>({
url: `${API_URL}/api/v1/roles/${roleId}/permissions/toggle`,
url: buildApiUrl(`/api/v1/roles/${roleId}/permissions/toggle`),
method: 'POST',
body: { menu_id: menuId, permission_type: permissionType },
errorMessage: '권한 토글에 실패했습니다.',
@@ -118,7 +115,7 @@ export async function togglePermission(roleId: number, menuId: number, permissio
async function rolePermissionAction(roleId: number, action: string, errorMessage: string): Promise<ActionResult<{ count: number }>> {
const result = await executeServerAction<{ count: number }>({
url: `${API_URL}/api/v1/roles/${roleId}/permissions/${action}`,
url: buildApiUrl(`/api/v1/roles/${roleId}/permissions/${action}`),
method: 'POST',
errorMessage,
});
@@ -136,4 +133,4 @@ export async function denyAllPermissions(roleId: number): Promise<ActionResult<{
export async function resetPermissions(roleId: number): Promise<ActionResult<{ count: number }>> {
return rolePermissionAction(roleId, 'reset', '권한 초기화에 실패했습니다.');
}
}

View File

@@ -14,12 +14,11 @@
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
import type { PaginatedApiResponse } from '@/lib/api/types';
import type { Popup, PopupFormData } from './types';
import { transformApiToFrontend, transformFrontendToApi, type PopupApiData } from './utils';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ============================================
// API 함수
// ============================================
@@ -32,15 +31,12 @@ export async function getPopups(params?: {
size?: number;
status?: string;
}): Promise<Popup[]> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('size', String(params.size));
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/popups?${searchParams.toString()}`,
url: buildApiUrl('/api/v1/popups', {
page: params?.page,
size: params?.size,
status: params?.status !== 'all' ? params?.status : undefined,
}),
transform: (data: PaginatedApiResponse<PopupApiData>) => data.data.map(transformApiToFrontend),
errorMessage: '팝업 목록 조회에 실패했습니다.',
});
@@ -52,7 +48,7 @@ export async function getPopups(params?: {
*/
export async function getPopupById(id: string): Promise<Popup | null> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/popups/${id}`,
url: buildApiUrl(`/api/v1/popups/${id}`),
transform: (data: PopupApiData) => transformApiToFrontend(data),
errorMessage: '팝업 조회에 실패했습니다.',
});
@@ -66,7 +62,7 @@ export async function createPopup(
data: PopupFormData
): Promise<ActionResult<Popup>> {
return executeServerAction({
url: `${API_URL}/api/v1/popups`,
url: buildApiUrl('/api/v1/popups'),
method: 'POST',
body: transformFrontendToApi(data),
transform: (data: PopupApiData) => transformApiToFrontend(data),
@@ -82,7 +78,7 @@ export async function updatePopup(
data: PopupFormData
): Promise<ActionResult<Popup>> {
return executeServerAction({
url: `${API_URL}/api/v1/popups/${id}`,
url: buildApiUrl(`/api/v1/popups/${id}`),
method: 'PUT',
body: transformFrontendToApi(data),
transform: (data: PopupApiData) => transformApiToFrontend(data),
@@ -95,7 +91,7 @@ export async function updatePopup(
*/
export async function deletePopup(id: string): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/popups/${id}`,
url: buildApiUrl(`/api/v1/popups/${id}`),
method: 'DELETE',
errorMessage: '팝업 삭제에 실패했습니다.',
});