refactor(WEB): Server Action 공통화 및 보안 강화
- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { AccountInfo, TermsAgreement, MarketingConsent } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* 상대 경로를 절대 URL로 변환
|
||||
* /storage/... 또는 1/temp/... → https://api.example.com/storage/tenants/...
|
||||
@@ -34,243 +35,80 @@ export async function getAccountInfo(): Promise<{
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
// 1. 사용자 기본 정보 조회
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/users/me`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
// 1. 사용자 기본 정보 조회
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userResult = await executeServerAction<any>({
|
||||
url: `${API_URL}/api/v1/users/me`,
|
||||
errorMessage: '계정 정보를 불러올 수 없습니다.',
|
||||
});
|
||||
if (userResult.__authError) return { success: false, __authError: true };
|
||||
if (!userResult.success || !userResult.data) return { success: false, error: userResult.error };
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
const user = userResult.data;
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '계정 정보를 불러올 수 없습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '계정 정보 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const user = result.data;
|
||||
|
||||
// 2. 프로필 정보 조회 (프로필 이미지 포함)
|
||||
let profileImage: string | undefined;
|
||||
try {
|
||||
const { response: profileResponse } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/profiles/me`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
|
||||
if (profileResponse?.ok) {
|
||||
const profileResult = await profileResponse.json();
|
||||
if (profileResult.success && profileResult.data) {
|
||||
// profile_photo_path 필드에서 이미지 경로 가져오기
|
||||
profileImage = toAbsoluteUrl(profileResult.data.profile_photo_path);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 프로필 조회 실패해도 계속 진행
|
||||
console.warn('[getAccountInfo] Failed to fetch profile image');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
accountInfo: {
|
||||
id: user.id?.toString() || '',
|
||||
email: user.email || '',
|
||||
profileImage, // 프로필 API에서 가져온 이미지
|
||||
role: user.role?.name || user.role || '',
|
||||
status: user.status || 'active',
|
||||
isTenantMaster: user.is_tenant_master || false,
|
||||
createdAt: user.created_at || '',
|
||||
updatedAt: user.updated_at || '',
|
||||
},
|
||||
termsAgreements: user.terms_agreements || [],
|
||||
marketingConsent: user.marketing_consent || {
|
||||
email: { agreed: false },
|
||||
sms: { agreed: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[AccountInfoActions] getAccountInfo error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
// 2. 프로필 정보 조회 (프로필 이미지 포함 - 실패해도 계속 진행)
|
||||
let profileImage: string | undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const profileResult = await executeServerAction<any>({
|
||||
url: `${API_URL}/api/v1/profiles/me`,
|
||||
errorMessage: '프로필 조회 실패',
|
||||
});
|
||||
if (profileResult.success && profileResult.data) {
|
||||
profileImage = toAbsoluteUrl(profileResult.data.profile_photo_path);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
accountInfo: {
|
||||
id: user.id?.toString() || '',
|
||||
email: user.email || '',
|
||||
profileImage,
|
||||
role: user.role?.name || user.role || '',
|
||||
status: user.status || 'active',
|
||||
isTenantMaster: user.is_tenant_master || false,
|
||||
createdAt: user.created_at || '',
|
||||
updatedAt: user.updated_at || '',
|
||||
},
|
||||
termsAgreements: user.terms_agreements || [],
|
||||
marketingConsent: user.marketing_consent || {
|
||||
email: { agreed: false },
|
||||
sms: { agreed: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 계정 탈퇴 =====
|
||||
export async function withdrawAccount(password: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/users/withdraw`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '계정 탈퇴에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '계정 탈퇴에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[AccountInfoActions] withdrawAccount error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function withdrawAccount(password: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/users/withdraw`,
|
||||
method: 'POST',
|
||||
body: { password },
|
||||
errorMessage: '계정 탈퇴에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 테넌트 사용 중지 =====
|
||||
export async function suspendTenant(): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants/suspend`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '사용 중지에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '사용 중지에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[AccountInfoActions] suspendTenant error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function suspendTenant(): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/tenants/suspend`,
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '사용 중지에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 약관 동의 수정 =====
|
||||
export async function updateAgreements(
|
||||
agreements: Array<{ type: string; agreed: boolean }>
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/account/agreements`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ agreements }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '약관 동의 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '약관 동의 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[AccountInfoActions] updateAgreements error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/account/agreements`,
|
||||
method: 'PUT',
|
||||
body: { agreements },
|
||||
errorMessage: '약관 동의 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 프로필 이미지 업로드 =====
|
||||
@@ -280,113 +118,36 @@ export async function uploadProfileImage(formData: FormData): Promise<{
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
console.log('[uploadProfileImage] Starting upload...');
|
||||
// 1. 파일 업로드
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const uploadResult = await executeServerAction<any>({
|
||||
url: `${API_URL}/api/v1/files/upload`,
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
errorMessage: '파일 업로드에 실패했습니다.',
|
||||
});
|
||||
if (uploadResult.__authError) return { success: false, __authError: true };
|
||||
if (!uploadResult.success || !uploadResult.data) return { success: false, error: uploadResult.error };
|
||||
|
||||
// 1. 먼저 파일 업로드 (일반 파일 업로드 엔드포인트 사용)
|
||||
const { response: uploadResponse, error: uploadError } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/files/upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
const uploadedPath = uploadResult.data.file_path || uploadResult.data.path || uploadResult.data.url;
|
||||
if (!uploadedPath) return { success: false, error: '업로드된 파일 경로를 가져올 수 없습니다.' };
|
||||
|
||||
console.log('[uploadProfileImage] Upload response status:', uploadResponse?.status);
|
||||
// 2. 프로필 업데이트 (업로드된 파일 경로로)
|
||||
const updateResult = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/profiles/me`,
|
||||
method: 'PATCH',
|
||||
body: { profile_photo_path: uploadedPath },
|
||||
errorMessage: '프로필 업데이트에 실패했습니다.',
|
||||
});
|
||||
if (updateResult.__authError) return { success: false, __authError: true };
|
||||
if (!updateResult.success) return { success: false, error: updateResult.error };
|
||||
|
||||
if (uploadError) {
|
||||
console.error('[uploadProfileImage] Upload error:', uploadError);
|
||||
return {
|
||||
success: false,
|
||||
error: uploadError.message,
|
||||
__authError: uploadError.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
const storagePath = uploadedPath.startsWith('/storage/')
|
||||
? uploadedPath
|
||||
: `/storage/tenants/${uploadedPath}`;
|
||||
|
||||
if (!uploadResponse) {
|
||||
console.error('[uploadProfileImage] No upload response');
|
||||
return {
|
||||
success: false,
|
||||
error: '파일 업로드에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const uploadResult = await uploadResponse.json();
|
||||
console.log('[uploadProfileImage] Upload result:', JSON.stringify(uploadResult, null, 2));
|
||||
|
||||
if (!uploadResponse.ok || !uploadResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: uploadResult.message || '파일 업로드에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// 업로드된 파일 경로 추출 (API 응답: file_path 필드)
|
||||
const uploadedPath = uploadResult.data?.file_path || uploadResult.data?.path || uploadResult.data?.url;
|
||||
console.log('[uploadProfileImage] Uploaded path:', uploadedPath);
|
||||
|
||||
if (!uploadedPath) {
|
||||
console.error('[uploadProfileImage] No file path in response. Full data:', uploadResult.data);
|
||||
return {
|
||||
success: false,
|
||||
error: '업로드된 파일 경로를 가져올 수 없습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 프로필 업데이트 (업로드된 파일 경로로)
|
||||
console.log('[uploadProfileImage] Updating profile with path:', uploadedPath);
|
||||
const { response: updateResponse, error: updateError } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/profiles/me`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ profile_photo_path: uploadedPath }),
|
||||
}
|
||||
);
|
||||
|
||||
if (updateError) {
|
||||
console.error('[uploadProfileImage] Profile update error:', updateError);
|
||||
return {
|
||||
success: false,
|
||||
error: updateError.message,
|
||||
__authError: updateError.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!updateResponse) {
|
||||
console.error('[uploadProfileImage] No profile update response');
|
||||
return {
|
||||
success: false,
|
||||
error: '프로필 업데이트에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const updateResult = await updateResponse.json();
|
||||
console.log('[uploadProfileImage] Profile update result:', JSON.stringify(updateResult, null, 2));
|
||||
|
||||
if (!updateResponse.ok || !updateResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: updateResult.message || '프로필 업데이트에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// /storage/tenants/ 경로로 변환 (tenant disk 파일은 이 경로로 접근 가능)
|
||||
const storagePath = uploadedPath.startsWith('/storage/')
|
||||
? uploadedPath
|
||||
: `/storage/tenants/${uploadedPath}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
imageUrl: toAbsoluteUrl(storagePath) || '',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[AccountInfoActions] uploadProfileImage error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: { imageUrl: toAbsoluteUrl(storagePath) || '' },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
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;
|
||||
@@ -21,34 +23,15 @@ interface BankAccountApiData {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface PaginationMeta {
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface PaginatedData {
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
interface BankAccountPaginatedResponse {
|
||||
data: BankAccountApiData[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface ApiListResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data: PaginatedData;
|
||||
}
|
||||
|
||||
interface ApiSingleResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data: BankAccountApiData;
|
||||
}
|
||||
|
||||
// ===== 데이터 변환: API → Frontend =====
|
||||
// ===== 데이터 변환 =====
|
||||
function transformApiToFrontend(apiData: BankAccountApiData): Account {
|
||||
return {
|
||||
id: apiData.id,
|
||||
@@ -65,7 +48,6 @@ function transformApiToFrontend(apiData: BankAccountApiData): Account {
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 데이터 변환: Frontend → API =====
|
||||
function transformFrontendToApi(data: Partial<AccountFormData>): Record<string, unknown> {
|
||||
return {
|
||||
bank_code: data.bankCode,
|
||||
@@ -79,328 +61,91 @@ function transformFrontendToApi(data: Partial<AccountFormData>): Record<string,
|
||||
|
||||
// ===== 계좌 목록 조회 =====
|
||||
export async function getBankAccounts(params?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
search?: string;
|
||||
page?: number; perPage?: number; search?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: Account[];
|
||||
meta?: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
success: boolean; data?: Account[]; meta?: { currentPage: number; lastPage: number; perPage: number; total: number };
|
||||
error?: string; __authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
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();
|
||||
|
||||
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 url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?${searchParams.toString()}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiListResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '계좌 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const accounts = result.data.data.map(transformApiToFrontend);
|
||||
const meta: PaginationMeta = {
|
||||
current_page: result.data.current_page,
|
||||
last_page: result.data.last_page,
|
||||
per_page: result.data.per_page,
|
||||
total: result.data.total,
|
||||
};
|
||||
return { success: true, data: accounts, meta };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getBankAccounts] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
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<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '계좌 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const account = transformApiToFrontend(result.data);
|
||||
return { success: true, data: account };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getBankAccount] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
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<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '계좌 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const account = transformApiToFrontend(result.data);
|
||||
return { success: true, data: account };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[createBankAccount] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
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<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '계좌 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const account = transformApiToFrontend(result.data);
|
||||
return { success: true, data: account };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[updateBankAccount] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
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<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '계좌 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteBankAccount] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
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<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}/toggle`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const account = transformApiToFrontend(result.data);
|
||||
return { success: true, data: account };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[toggleBankAccountStatus] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
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<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}/set-primary`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '대표 계좌 설정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '대표 계좌 설정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const account = transformApiToFrontend(result.data);
|
||||
return { success: true, data: account };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[setPrimaryBankAccount] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
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;
|
||||
success: boolean; deletedCount?: number; error?: string;
|
||||
}> {
|
||||
try {
|
||||
const results = await Promise.all(ids.map(id => deleteBankAccount(id)));
|
||||
@@ -410,19 +155,12 @@ export async function deleteBankAccounts(ids: number[]): Promise<{
|
||||
if (failedCount > 0 && successCount === 0) {
|
||||
return { success: false, error: '계좌 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
if (failedCount > 0) {
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: successCount,
|
||||
error: `${failedCount}개의 계좌 삭제에 실패했습니다.`
|
||||
};
|
||||
return { success: true, deletedCount: successCount, error: `${failedCount}개의 계좌 삭제에 실패했습니다.` };
|
||||
}
|
||||
|
||||
return { success: true, deletedCount: successCount };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteBankAccounts] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
@@ -47,13 +46,6 @@ interface ApiDepartment {
|
||||
children?: ApiDepartment[];
|
||||
}
|
||||
|
||||
// API 응답 공통 타입
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ===== 데이터 변환 =====
|
||||
|
||||
/**
|
||||
@@ -75,151 +67,64 @@ function transformFromApi(data: ApiAttendanceSetting): AttendanceSettingFormData
|
||||
*/
|
||||
function transformToApi(data: Partial<AttendanceSettingFormData>): Record<string, unknown> {
|
||||
const apiData: Record<string, unknown> = {};
|
||||
|
||||
if (data.useGps !== undefined) apiData.use_gps = data.useGps;
|
||||
if (data.useAuto !== undefined) apiData.use_auto = data.useAuto;
|
||||
if (data.allowedRadius !== undefined) apiData.allowed_radius = data.allowedRadius;
|
||||
if (data.hqAddress !== undefined) apiData.hq_address = data.hqAddress;
|
||||
if (data.hqLatitude !== undefined) apiData.hq_latitude = data.hqLatitude;
|
||||
if (data.hqLongitude !== undefined) apiData.hq_longitude = data.hqLongitude;
|
||||
|
||||
return apiData;
|
||||
}
|
||||
|
||||
// ===== API 호출 =====
|
||||
|
||||
/**
|
||||
* 출퇴근 설정 조회
|
||||
*/
|
||||
export async function getAttendanceSetting(): Promise<ApiResponse<AttendanceSettingFormData>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/settings/attendance`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '출퇴근 설정 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '출퇴근 설정 조회 실패' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformFromApi(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('getAttendanceSetting error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '출퇴근 설정을 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 출퇴근 설정 수정
|
||||
*/
|
||||
export async function updateAttendanceSetting(
|
||||
data: Partial<AttendanceSettingFormData>
|
||||
): Promise<ApiResponse<AttendanceSettingFormData>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/settings/attendance`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(transformToApi(data)),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '출퇴근 설정 저장에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '출퇴근 설정 저장 실패' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformFromApi(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('updateAttendanceSetting error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '출퇴근 설정 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 트리 구조를 평탄화 (재귀)
|
||||
*/
|
||||
function flattenDepartmentTree(departments: ApiDepartment[], depth: number = 0): Department[] {
|
||||
const result: Department[] = [];
|
||||
|
||||
for (const dept of departments) {
|
||||
result.push({
|
||||
id: String(dept.id),
|
||||
name: dept.name,
|
||||
depth,
|
||||
});
|
||||
|
||||
result.push({ id: String(dept.id), name: dept.name, depth });
|
||||
if (dept.children && dept.children.length > 0) {
|
||||
result.push(...flattenDepartmentTree(dept.children, depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== API 호출 =====
|
||||
|
||||
/**
|
||||
* 출퇴근 설정 조회
|
||||
*/
|
||||
export async function getAttendanceSetting(): Promise<ActionResult<AttendanceSettingFormData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/settings/attendance`,
|
||||
transform: (data: ApiAttendanceSetting) => transformFromApi(data),
|
||||
errorMessage: '출퇴근 설정을 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 출퇴근 설정 수정
|
||||
*/
|
||||
export async function updateAttendanceSetting(
|
||||
data: Partial<AttendanceSettingFormData>
|
||||
): Promise<ActionResult<AttendanceSettingFormData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/settings/attendance`,
|
||||
method: 'PUT',
|
||||
body: transformToApi(data),
|
||||
transform: (data: ApiAttendanceSetting) => transformFromApi(data),
|
||||
errorMessage: '출퇴근 설정 저장에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 목록 조회 (트리 구조)
|
||||
*/
|
||||
export async function getDepartments(): Promise<ApiResponse<Department[]>> {
|
||||
try {
|
||||
// 트리 API 사용
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/departments/tree`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '부서 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '부서 목록 조회 실패' };
|
||||
}
|
||||
|
||||
// 트리를 평탄화하여 depth 포함된 배열로 변환
|
||||
const departments = flattenDepartmentTree(result.data || []);
|
||||
|
||||
return { success: true, data: departments };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('getDepartments error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '부서 목록을 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
export async function getDepartments(): Promise<ActionResult<Department[]>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/departments/tree`,
|
||||
transform: (data: ApiDepartment[]) => flattenDepartmentTree(data || []),
|
||||
errorMessage: '부서 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { CompanyFormData } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// API 응답 타입
|
||||
interface TenantApiData {
|
||||
id: number;
|
||||
@@ -35,138 +36,61 @@ interface TenantApiData {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 정보 조회
|
||||
*/
|
||||
export async function getCompanyInfo(): Promise<{
|
||||
success: boolean;
|
||||
data?: CompanyFormData & { tenantId: number };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '회사 정보 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '회사 정보 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const apiData: TenantApiData = result.data;
|
||||
const formData = transformApiToFrontend(apiData);
|
||||
|
||||
return { success: true, data: formData };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getCompanyInfo] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
// ===== 테넌트 정보 조회 =====
|
||||
export async function getCompanyInfo(): Promise<ActionResult<CompanyFormData & { tenantId: number }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/tenants`,
|
||||
transform: (data: TenantApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '회사 정보 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 정보 수정
|
||||
*/
|
||||
// ===== 테넌트 정보 수정 =====
|
||||
export async function updateCompanyInfo(
|
||||
tenantId: number,
|
||||
data: Partial<CompanyFormData>
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: CompanyFormData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(tenantId, data);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '회사 정보 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '회사 정보 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const updatedData = transformApiToFrontend(result.data);
|
||||
return { success: true, data: updatedData };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[updateCompanyInfo] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
): Promise<ActionResult<CompanyFormData & { tenantId: number }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/tenants`,
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(tenantId, data),
|
||||
transform: (d: TenantApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '회사 정보 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 상대 경로를 절대 URL로 변환
|
||||
* /storage/... → https://api.example.com/storage/...
|
||||
*/
|
||||
// ===== 회사 로고 업로드 =====
|
||||
export async function uploadCompanyLogo(formData: FormData): Promise<ActionResult<{ logoUrl: string }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/tenants/logo`,
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
transform: (data: { logo_url?: string }) => ({
|
||||
logoUrl: toAbsoluteUrl(data?.logo_url) || '',
|
||||
}),
|
||||
errorMessage: '로고 업로드에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 유틸리티 =====
|
||||
|
||||
function toAbsoluteUrl(path: string | undefined): string | undefined {
|
||||
if (!path) return undefined;
|
||||
// 이미 절대 URL이면 그대로 반환
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
}
|
||||
// 상대 경로면 API URL 붙이기
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
return `${apiUrl}${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 → Frontend 변환
|
||||
*
|
||||
* 기본 필드: company_name, ceo_name, email, phone, business_num, address
|
||||
* 확장 필드: options JSON에서 읽어옴
|
||||
*/
|
||||
function transformApiToFrontend(apiData: TenantApiData): CompanyFormData & { tenantId: number } {
|
||||
const opts = apiData.options || {};
|
||||
|
||||
return {
|
||||
// tenantId (API 응답의 id 필드)
|
||||
tenantId: apiData.id,
|
||||
// 기본 필드
|
||||
companyName: apiData.company_name || '',
|
||||
representativeName: apiData.ceo_name || '',
|
||||
email: apiData.email || '',
|
||||
managerPhone: apiData.phone || '',
|
||||
businessNumber: apiData.business_num || '',
|
||||
address: apiData.address || '',
|
||||
// 로고 URL (상대 경로 → 절대 URL 변환)
|
||||
companyLogo: toAbsoluteUrl(apiData.logo),
|
||||
businessType: opts.business_type || '',
|
||||
businessCategory: opts.business_category || '',
|
||||
@@ -182,25 +106,12 @@ function transformApiToFrontend(apiData: TenantApiData): CompanyFormData & { ten
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend → API 변환
|
||||
*
|
||||
* 기본 필드: tenant_id, company_name, ceo_name, email, phone, business_num, address
|
||||
* 확장 필드: options JSON에 저장 (businessType, businessCategory, taxInvoiceEmail 등)
|
||||
*/
|
||||
function transformFrontendToApi(
|
||||
tenantId: number,
|
||||
data: Partial<CompanyFormData>
|
||||
): Record<string, unknown> {
|
||||
// 로고 URL에서 상대 경로 추출 (API는 상대 경로 기대)
|
||||
function transformFrontendToApi(tenantId: number, data: Partial<CompanyFormData>): Record<string, unknown> {
|
||||
let logoPath: string | null = null;
|
||||
if (data.companyLogo && typeof data.companyLogo === 'string') {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
logoPath = data.companyLogo.startsWith(apiUrl)
|
||||
? data.companyLogo.replace(apiUrl, '')
|
||||
: data.companyLogo;
|
||||
logoPath = data.companyLogo.startsWith(apiUrl) ? data.companyLogo.replace(apiUrl, '') : data.companyLogo;
|
||||
}
|
||||
|
||||
return {
|
||||
tenant_id: tenantId,
|
||||
company_name: data.companyName,
|
||||
@@ -208,75 +119,14 @@ function transformFrontendToApi(
|
||||
email: data.email,
|
||||
phone: data.managerPhone,
|
||||
business_num: data.businessNumber,
|
||||
// 로고 (삭제 시 null)
|
||||
logo: logoPath,
|
||||
// address: 우편번호 + 주소 + 상세주소 결합
|
||||
address: [data.zipCode, data.address, data.addressDetail]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
// 확장 필드 (options JSON)
|
||||
address: [data.zipCode, data.address, data.addressDetail].filter(Boolean).join(' '),
|
||||
options: {
|
||||
business_type: data.businessType,
|
||||
business_category: data.businessCategory,
|
||||
zip_code: data.zipCode,
|
||||
address_detail: data.addressDetail,
|
||||
tax_invoice_email: data.taxInvoiceEmail,
|
||||
manager_name: data.managerName,
|
||||
payment_bank: data.paymentBank,
|
||||
payment_account: data.paymentAccount,
|
||||
payment_account_holder: data.paymentAccountHolder,
|
||||
payment_day: data.paymentDay,
|
||||
business_type: data.businessType, business_category: data.businessCategory,
|
||||
zip_code: data.zipCode, address_detail: data.addressDetail,
|
||||
tax_invoice_email: data.taxInvoiceEmail, manager_name: data.managerName,
|
||||
payment_bank: data.paymentBank, payment_account: data.paymentAccount,
|
||||
payment_account_holder: data.paymentAccountHolder, payment_day: data.paymentDay,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사 로고 업로드
|
||||
* @param formData - FormData (클라이언트에서 생성, 'logo' 키로 파일 포함)
|
||||
*/
|
||||
export async function uploadCompanyLogo(formData: FormData): Promise<{
|
||||
success: boolean;
|
||||
data?: { logoUrl: string };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants/logo`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
// FormData는 Content-Type을 자동으로 설정하므로 headers 제거
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '로고 업로드에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '로고 업로드에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
logoUrl: toAbsoluteUrl(result.data?.logo_url) || '',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[uploadCompanyLogo] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { LeavePolicySettings } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface LeavePolicyApi {
|
||||
id: number;
|
||||
@@ -40,7 +41,6 @@ function transformLeavePolicy(data: LeavePolicyApi): LeavePolicySettings {
|
||||
// ===== Frontend → API 변환 =====
|
||||
function transformToApi(data: Partial<LeavePolicySettings>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
if (data.standardType !== undefined) result.standard_type = data.standardType;
|
||||
if (data.fiscalStartMonth !== undefined) result.fiscal_start_month = data.fiscalStartMonth;
|
||||
if (data.fiscalStartDay !== undefined) result.fiscal_start_day = data.fiscalStartDay;
|
||||
@@ -50,119 +50,27 @@ function transformToApi(data: Partial<LeavePolicySettings>): Record<string, unkn
|
||||
if (data.carryOverEnabled !== undefined) result.carry_over_enabled = data.carryOverEnabled;
|
||||
if (data.carryOverMaxDays !== undefined) result.carry_over_max_days = data.carryOverMaxDays;
|
||||
if (data.carryOverExpiryMonths !== undefined) result.carry_over_expiry_months = data.carryOverExpiryMonths;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== 휴가 정책 조회 =====
|
||||
export async function getLeavePolicy(): Promise<{
|
||||
success: boolean;
|
||||
data?: LeavePolicySettings;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/leave-policy`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[LeavePolicyActions] GET error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 정책 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const transformedData = transformLeavePolicy(result.data);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformedData,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[LeavePolicyActions] getLeavePolicy error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function getLeavePolicy(): Promise<ActionResult<LeavePolicySettings>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/leave-policy`,
|
||||
transform: (data: LeavePolicyApi) => transformLeavePolicy(data),
|
||||
errorMessage: '휴가 정책 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 휴가 정책 업데이트 =====
|
||||
export async function updateLeavePolicy(data: Partial<LeavePolicySettings>): Promise<{
|
||||
success: boolean;
|
||||
data?: LeavePolicySettings;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/leave-policy`;
|
||||
const apiData = transformToApi(data);
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[LeavePolicyActions] PUT error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 정책 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const transformedData = transformLeavePolicy(result.data);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformedData,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[LeavePolicyActions] updateLeavePolicy error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function updateLeavePolicy(
|
||||
data: Partial<LeavePolicySettings>
|
||||
): Promise<ActionResult<LeavePolicySettings>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/leave-policy`,
|
||||
method: 'PUT',
|
||||
body: transformToApi(data),
|
||||
transform: (data: LeavePolicyApi) => transformLeavePolicy(data),
|
||||
errorMessage: '휴가 정책 저장에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,116 +1,40 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { NotificationSettings } from './types';
|
||||
import { DEFAULT_NOTIFICATION_SETTINGS } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 알림 설정 조회 =====
|
||||
export async function getNotificationSettings(): Promise<{
|
||||
success: boolean;
|
||||
data: NotificationSettings;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
success: boolean; data: NotificationSettings; error?: string; __authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/notifications`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/settings/notifications`,
|
||||
transform: (data: Record<string, unknown>) => transformApiToFrontend(data),
|
||||
errorMessage: '알림 설정 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: DEFAULT_NOTIFICATION_SETTINGS,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[NotificationActions] GET settings error:', response?.status);
|
||||
return {
|
||||
success: true,
|
||||
data: DEFAULT_NOTIFICATION_SETTINGS,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: DEFAULT_NOTIFICATION_SETTINGS,
|
||||
};
|
||||
}
|
||||
|
||||
// API → Frontend 변환
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[NotificationActions] getNotificationSettings error:', error);
|
||||
return {
|
||||
success: true,
|
||||
data: DEFAULT_NOTIFICATION_SETTINGS,
|
||||
};
|
||||
// 인증 에러는 전파, 그 외 에러는 기본값 반환 (설정 미존재 시 정상 동작)
|
||||
if (result.__authError) {
|
||||
return { success: false, data: DEFAULT_NOTIFICATION_SETTINGS, error: result.error, __authError: true };
|
||||
}
|
||||
if (!result.success || !result.data) {
|
||||
return { success: true, data: DEFAULT_NOTIFICATION_SETTINGS };
|
||||
}
|
||||
return { success: true, data: result.data };
|
||||
}
|
||||
|
||||
// ===== 알림 설정 저장 =====
|
||||
export async function saveNotificationSettings(
|
||||
settings: NotificationSettings
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(settings);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/notifications`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '알림 설정 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '알림 설정 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[NotificationActions] saveNotificationSettings error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function saveNotificationSettings(settings: NotificationSettings): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/settings/notifications`,
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(settings),
|
||||
errorMessage: '알림 설정 저장에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== API → Frontend 변환 (기본값과 병합) =====
|
||||
|
||||
@@ -1,246 +1,108 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaymentApiData, PaymentHistory } from './types';
|
||||
import { transformApiToFrontend } from './utils';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface PaymentPaginatedResponse {
|
||||
data: PaymentApiData[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface PaymentStatementApiData {
|
||||
statement_no: string;
|
||||
issued_at: string;
|
||||
payment: { id: number; amount: number; formatted_amount: string; payment_method: string; payment_method_label: string; transaction_id: string | null; status: string; status_label: string; paid_at: string | null; memo: string | null };
|
||||
subscription: { id: number; started_at: string | null; ended_at: string | null; status: string; status_label: string };
|
||||
plan: { id: number; name: string; code: string; price: number; billing_cycle: string; billing_cycle_label: string } | null;
|
||||
customer: { tenant_id: number; company_name: string; business_number: string | null; representative: string | null; address: string | null; email: string | null; phone: string | null };
|
||||
items: Array<{ description: string; quantity: number; unitPrice: number; amount: number }>;
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface StatementData {
|
||||
statementNo: string;
|
||||
issuedAt: string;
|
||||
payment: { id: number; amount: number; formattedAmount: string; paymentMethod: string; paymentMethodLabel: string; transactionId: string | null; status: string; statusLabel: string; paidAt: string | null; memo: string | null };
|
||||
subscription: { id: number; startedAt: string | null; endedAt: string | null; status: string; statusLabel: string };
|
||||
plan: { id: number; name: string; code: string; price: number; billingCycle: string; billingCycleLabel: string } | null;
|
||||
customer: { tenantId: number; companyName: string; businessNumber: string | null; representative: string | null; address: string | null; email: string | null; phone: string | null };
|
||||
items: Array<{ description: string; quantity: number; unitPrice: number; amount: number }>;
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
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;
|
||||
page?: number; perPage?: number; status?: string; startDate?: string; endDate?: string; search?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data: PaymentHistory[];
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
success: boolean; data: PaymentHistory[]; pagination: FrontendPagination;
|
||||
error?: string; __authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
// 쿼리 파라미터 생성
|
||||
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 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 queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/payments${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '결제 내역을 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '결제 내역을 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const payments = result.data.data.map(transformApiToFrontend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: payments,
|
||||
pagination: {
|
||||
currentPage: result.data.current_page,
|
||||
lastPage: result.data.last_page,
|
||||
perPage: result.data.per_page,
|
||||
total: result.data.total,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PaymentActions] getPayments error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
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 },
|
||||
}),
|
||||
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<{
|
||||
success: boolean;
|
||||
data?: {
|
||||
statementNo: string;
|
||||
issuedAt: string;
|
||||
payment: {
|
||||
id: number;
|
||||
amount: number;
|
||||
formattedAmount: string;
|
||||
paymentMethod: string;
|
||||
paymentMethodLabel: string;
|
||||
transactionId: string | null;
|
||||
status: string;
|
||||
statusLabel: string;
|
||||
paidAt: string | null;
|
||||
memo: string | null;
|
||||
};
|
||||
subscription: {
|
||||
id: number;
|
||||
startedAt: string | null;
|
||||
endedAt: string | null;
|
||||
status: string;
|
||||
statusLabel: string;
|
||||
};
|
||||
plan: {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
price: number;
|
||||
billingCycle: string;
|
||||
billingCycleLabel: string;
|
||||
} | null;
|
||||
customer: {
|
||||
tenantId: number;
|
||||
companyName: string;
|
||||
businessNumber: string | null;
|
||||
representative: string | null;
|
||||
address: string | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
};
|
||||
items: Array<{
|
||||
description: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
amount: number;
|
||||
}>;
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/payments/${id}/statement`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '명세서를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '명세서를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// snake_case → camelCase 변환
|
||||
const data = result.data;
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
statementNo: data.statement_no,
|
||||
issuedAt: data.issued_at,
|
||||
payment: {
|
||||
id: data.payment.id,
|
||||
amount: data.payment.amount,
|
||||
formattedAmount: data.payment.formatted_amount,
|
||||
paymentMethod: data.payment.payment_method,
|
||||
paymentMethodLabel: data.payment.payment_method_label,
|
||||
transactionId: data.payment.transaction_id,
|
||||
status: data.payment.status,
|
||||
statusLabel: data.payment.status_label,
|
||||
paidAt: data.payment.paid_at,
|
||||
memo: data.payment.memo,
|
||||
},
|
||||
subscription: {
|
||||
id: data.subscription.id,
|
||||
startedAt: data.subscription.started_at,
|
||||
endedAt: data.subscription.ended_at,
|
||||
status: data.subscription.status,
|
||||
statusLabel: data.subscription.status_label,
|
||||
},
|
||||
plan: data.plan ? {
|
||||
id: data.plan.id,
|
||||
name: data.plan.name,
|
||||
code: data.plan.code,
|
||||
price: data.plan.price,
|
||||
billingCycle: data.plan.billing_cycle,
|
||||
billingCycleLabel: data.plan.billing_cycle_label,
|
||||
} : null,
|
||||
customer: {
|
||||
tenantId: data.customer.tenant_id,
|
||||
companyName: data.customer.company_name,
|
||||
businessNumber: data.customer.business_number,
|
||||
representative: data.customer.representative,
|
||||
address: data.customer.address,
|
||||
email: data.customer.email,
|
||||
phone: data.customer.phone,
|
||||
},
|
||||
items: data.items,
|
||||
subtotal: data.subtotal,
|
||||
tax: data.tax,
|
||||
total: data.total,
|
||||
export async function getPaymentStatement(id: string): Promise<ActionResult<StatementData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/payments/${id}/statement`,
|
||||
transform: (data: PaymentStatementApiData): StatementData => ({
|
||||
statementNo: data.statement_no,
|
||||
issuedAt: data.issued_at,
|
||||
payment: {
|
||||
id: data.payment.id, amount: data.payment.amount, formattedAmount: data.payment.formatted_amount,
|
||||
paymentMethod: data.payment.payment_method, paymentMethodLabel: data.payment.payment_method_label,
|
||||
transactionId: data.payment.transaction_id, status: data.payment.status, statusLabel: data.payment.status_label,
|
||||
paidAt: data.payment.paid_at, memo: data.payment.memo,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PaymentActions] getPaymentStatement error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
subscription: {
|
||||
id: data.subscription.id, startedAt: data.subscription.started_at, endedAt: data.subscription.ended_at,
|
||||
status: data.subscription.status, statusLabel: data.subscription.status_label,
|
||||
},
|
||||
plan: data.plan ? {
|
||||
id: data.plan.id, name: data.plan.name, code: data.plan.code, price: data.plan.price,
|
||||
billingCycle: data.plan.billing_cycle, billingCycleLabel: data.plan.billing_cycle_label,
|
||||
} : null,
|
||||
customer: {
|
||||
tenantId: data.customer.tenant_id, companyName: data.customer.company_name,
|
||||
businessNumber: data.customer.business_number, representative: data.customer.representative,
|
||||
address: data.customer.address, email: data.customer.email, phone: data.customer.phone,
|
||||
},
|
||||
items: data.items,
|
||||
subtotal: data.subtotal,
|
||||
tax: data.tax,
|
||||
total: data.total,
|
||||
}),
|
||||
errorMessage: '명세서를 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,451 +1,139 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Role, RoleStats, PermissionMatrix, MenuTreeItem, ApiResponse, PaginatedResponse } from './types';
|
||||
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<ApiResponse<PaginatedResponse<Role>>> {
|
||||
try {
|
||||
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());
|
||||
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();
|
||||
|
||||
const url = `${API_URL}/api/v1/roles?${searchParams.toString()}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '역할 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '역할 목록 조회 실패' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to fetch roles:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '역할 목록 조회 실패' };
|
||||
}
|
||||
return executeServerAction<PaginatedResponse<Role>>({
|
||||
url: `${API_URL}/api/v1/roles${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '역할 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 상세 조회
|
||||
*/
|
||||
export async function fetchRole(id: number): Promise<ApiResponse<Role>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${id}`, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '역할 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '역할 조회 실패' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to fetch role:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '역할 조회 실패' };
|
||||
}
|
||||
export async function fetchRole(id: number): Promise<ActionResult<Role>> {
|
||||
return executeServerAction<Role>({
|
||||
url: `${API_URL}/api/v1/roles/${id}`,
|
||||
errorMessage: '역할 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 생성
|
||||
*/
|
||||
export async function createRole(data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
is_hidden?: boolean;
|
||||
}): Promise<ApiResponse<Role>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '역할 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '역할 생성 실패' };
|
||||
}
|
||||
|
||||
revalidatePath('/settings/permissions');
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to create role:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '역할 생성 실패' };
|
||||
}
|
||||
name: string; description?: string; is_hidden?: boolean;
|
||||
}): Promise<ActionResult<Role>> {
|
||||
const result = await executeServerAction<Role>({
|
||||
url: `${API_URL}/api/v1/roles`,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
errorMessage: '역할 생성에 실패했습니다.',
|
||||
});
|
||||
if (result.success) revalidatePath('/settings/permissions');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 수정
|
||||
*/
|
||||
export async function updateRole(
|
||||
id: number,
|
||||
data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
is_hidden?: boolean;
|
||||
}
|
||||
): Promise<ApiResponse<Role>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '역할 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '역할 수정 실패' };
|
||||
}
|
||||
|
||||
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}`,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
errorMessage: '역할 수정에 실패했습니다.',
|
||||
});
|
||||
if (result.success) {
|
||||
revalidatePath('/settings/permissions');
|
||||
revalidatePath(`/settings/permissions/${id}`);
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to update role:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '역할 수정 실패' };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 삭제
|
||||
*/
|
||||
export async function deleteRole(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '역할 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '역할 삭제 실패' };
|
||||
}
|
||||
|
||||
revalidatePath('/settings/permissions');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to delete role:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '역할 삭제 실패' };
|
||||
}
|
||||
export async function deleteRole(id: number): Promise<ActionResult> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/roles/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '역할 삭제에 실패했습니다.',
|
||||
});
|
||||
if (result.success) revalidatePath('/settings/permissions');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 통계 조회
|
||||
*/
|
||||
export async function fetchRoleStats(): Promise<ApiResponse<RoleStats>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/stats`, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '역할 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '역할 통계 조회 실패' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to fetch role stats:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '역할 통계 조회 실패' };
|
||||
}
|
||||
export async function fetchRoleStats(): Promise<ActionResult<RoleStats>> {
|
||||
return executeServerAction<RoleStats>({
|
||||
url: `${API_URL}/api/v1/roles/stats`,
|
||||
errorMessage: '역할 통계 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 역할 목록 (드롭다운용)
|
||||
*/
|
||||
export async function fetchActiveRoles(): Promise<ApiResponse<Role[]>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/active`, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '활성 역할 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '활성 역할 목록 조회 실패' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to fetch active roles:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '활성 역할 목록 조회 실패' };
|
||||
}
|
||||
export async function fetchActiveRoles(): Promise<ActionResult<Role[]>> {
|
||||
return executeServerAction<Role[]>({
|
||||
url: `${API_URL}/api/v1/roles/active`,
|
||||
errorMessage: '활성 역할 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Permission Matrix ==========
|
||||
|
||||
/**
|
||||
* 권한 매트릭스용 메뉴 트리 조회
|
||||
*/
|
||||
export async function fetchPermissionMenus(): Promise<ApiResponse<{
|
||||
menus: MenuTreeItem[];
|
||||
permission_types: string[];
|
||||
export async function fetchPermissionMenus(): Promise<ActionResult<{
|
||||
menus: MenuTreeItem[]; permission_types: string[];
|
||||
}>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/role-permissions/menus`, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '메뉴 트리 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '메뉴 트리 조회 실패' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to fetch permission menus:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '메뉴 트리 조회 실패' };
|
||||
}
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/role-permissions/menus`,
|
||||
errorMessage: '메뉴 트리 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할의 권한 매트릭스 조회
|
||||
*/
|
||||
export async function fetchPermissionMatrix(roleId: number): Promise<ApiResponse<PermissionMatrix>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/matrix`, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '권한 매트릭스 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '권한 매트릭스 조회 실패' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to fetch permission matrix:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '권한 매트릭스 조회 실패' };
|
||||
}
|
||||
export async function fetchPermissionMatrix(roleId: number): Promise<ActionResult<PermissionMatrix>> {
|
||||
return executeServerAction<PermissionMatrix>({
|
||||
url: `${API_URL}/api/v1/roles/${roleId}/permissions/matrix`,
|
||||
errorMessage: '권한 매트릭스 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 메뉴의 특정 권한 토글
|
||||
*/
|
||||
export async function togglePermission(
|
||||
roleId: number,
|
||||
menuId: number,
|
||||
permissionType: string
|
||||
): Promise<ApiResponse<{
|
||||
granted: boolean;
|
||||
propagated_to: number[];
|
||||
export async function togglePermission(roleId: number, menuId: number, permissionType: string): Promise<ActionResult<{
|
||||
granted: boolean; propagated_to: number[];
|
||||
}>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/toggle`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
menu_id: menuId,
|
||||
permission_type: permissionType,
|
||||
}),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '권한 토글에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '권한 토글 실패' };
|
||||
}
|
||||
|
||||
revalidatePath(`/settings/permissions/${roleId}`);
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to toggle permission:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '권한 토글 실패' };
|
||||
}
|
||||
const result = await executeServerAction<{ granted: boolean; propagated_to: number[] }>({
|
||||
url: `${API_URL}/api/v1/roles/${roleId}/permissions/toggle`,
|
||||
method: 'POST',
|
||||
body: { menu_id: menuId, permission_type: permissionType },
|
||||
errorMessage: '권한 토글에 실패했습니다.',
|
||||
});
|
||||
if (result.success) revalidatePath(`/settings/permissions/${roleId}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 권한 허용
|
||||
*/
|
||||
export async function allowAllPermissions(roleId: number): Promise<ApiResponse<{ count: number }>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/allow-all`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '전체 허용에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '전체 허용 실패' };
|
||||
}
|
||||
|
||||
revalidatePath(`/settings/permissions/${roleId}`);
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to allow all permissions:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '전체 허용 실패' };
|
||||
}
|
||||
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}`,
|
||||
method: 'POST',
|
||||
errorMessage,
|
||||
});
|
||||
if (result.success) revalidatePath(`/settings/permissions/${roleId}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 권한 거부
|
||||
*/
|
||||
export async function denyAllPermissions(roleId: number): Promise<ApiResponse<{ count: number }>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/deny-all`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '전체 거부에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '전체 거부 실패' };
|
||||
}
|
||||
|
||||
revalidatePath(`/settings/permissions/${roleId}`);
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to deny all permissions:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '전체 거부 실패' };
|
||||
}
|
||||
export async function allowAllPermissions(roleId: number): Promise<ActionResult<{ count: number }>> {
|
||||
return rolePermissionAction(roleId, 'allow-all', '전체 허용에 실패했습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 권한으로 초기화 (view만 허용)
|
||||
*/
|
||||
export async function resetPermissions(roleId: number): Promise<ApiResponse<{ count: number }>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/api/v1/roles/${roleId}/permissions/reset`, {
|
||||
method: 'POST',
|
||||
});
|
||||
export async function denyAllPermissions(roleId: number): Promise<ActionResult<{ count: number }>> {
|
||||
return rolePermissionAction(roleId, 'deny-all', '전체 거부에 실패했습니다.');
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '권한 초기화에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '권한 초기화 실패' };
|
||||
}
|
||||
|
||||
revalidatePath(`/settings/permissions/${roleId}`);
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to reset permissions:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '권한 초기화 실패' };
|
||||
}
|
||||
export async function resetPermissions(roleId: number): Promise<ActionResult<{ count: number }>> {
|
||||
return rolePermissionAction(roleId, 'reset', '권한 초기화에 실패했습니다.');
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { type Popup } from './types';
|
||||
import { sanitizeHTML } from '@/lib/sanitize';
|
||||
|
||||
interface PopupDetailProps {
|
||||
popup: Popup;
|
||||
@@ -91,7 +92,7 @@ export function PopupDetail({ popup, onEdit, onDelete }: PopupDetailProps) {
|
||||
<dd className="text-sm mt-1">
|
||||
<div
|
||||
className="border border-gray-200 rounded-md p-4 bg-gray-50 min-h-[100px] prose prose-sm max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: popup.content }}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeHTML(popup.content) }}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -13,24 +13,16 @@
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { Popup, PopupFormData } from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi, type PopupApiData } from './utils';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ============================================
|
||||
// API 응답 타입 정의
|
||||
// ============================================
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
|
||||
interface PaginatedResponse<T> {
|
||||
current_page: number;
|
||||
data: T[];
|
||||
@@ -39,6 +31,10 @@ interface PaginatedResponse<T> {
|
||||
last_page: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 팝업 목록 조회
|
||||
*/
|
||||
@@ -47,82 +43,31 @@ export async function getPopups(params?: {
|
||||
size?: number;
|
||||
status?: string;
|
||||
}): Promise<Popup[]> {
|
||||
try {
|
||||
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 url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups?${searchParams.toString()}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[PopupActions] GET list error:', error?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PopupActions] GET list error:', response.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<PopupApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[PopupActions] No data in response');
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.data.data.map(transformApiToFrontend);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PopupActions] getPopups error:', error);
|
||||
return [];
|
||||
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()}`,
|
||||
transform: (data: PaginatedResponse<PopupApiData>) => data.data.map(transformApiToFrontend),
|
||||
errorMessage: '팝업 목록 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 상세 조회
|
||||
*/
|
||||
export async function getPopupById(id: string): Promise<Popup | null> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[PopupActions] GET popup error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PopupActions] GET popup error:', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<PopupApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformApiToFrontend(result.data);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PopupActions] getPopupById error:', error);
|
||||
return null;
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups/${id}`,
|
||||
transform: (data: PopupApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '팝업 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,57 +75,14 @@ export async function getPopupById(id: string): Promise<Popup | null> {
|
||||
*/
|
||||
export async function createPopup(
|
||||
data: PopupFormData
|
||||
): Promise<{ success: boolean; data?: Popup; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[PopupActions] POST popup request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '팝업 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PopupActions] POST popup response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '팝업 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PopupActions] createPopup error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult<Popup>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups`,
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: PopupApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '팝업 등록에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,105 +91,25 @@ export async function createPopup(
|
||||
export async function updatePopup(
|
||||
id: string,
|
||||
data: PopupFormData
|
||||
): Promise<{ success: boolean; data?: Popup; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[PopupActions] PUT popup request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '팝업 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PopupActions] PUT popup response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '팝업 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PopupActions] updatePopup error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult<Popup>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups/${id}`,
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: PopupApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '팝업 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 팝업 삭제
|
||||
*/
|
||||
export async function deletePopup(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '팝업 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PopupActions] DELETE popup response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '팝업 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PopupActions] deletePopup error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function deletePopup(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '팝업 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,21 +119,13 @@ export async function deletePopups(ids: string[]): Promise<{ success: boolean; e
|
||||
try {
|
||||
const results = await Promise.all(ids.map(id => deletePopup(id)));
|
||||
const failed = results.filter(r => !r.success);
|
||||
|
||||
if (failed.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `${failed.length}개 팝업 삭제에 실패했습니다.`,
|
||||
};
|
||||
return { success: false, error: `${failed.length}개 팝업 삭제에 실패했습니다.` };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PopupActions] deletePopups error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { DetailConfig, FieldDefinition, SectionDefinition } from '@/compone
|
||||
import type { Popup, PopupFormData, PopupTarget, PopupStatus } from './types';
|
||||
import { RichTextEditor } from '@/components/board/RichTextEditor';
|
||||
import { createElement } from 'react';
|
||||
import { sanitizeHTML } from '@/lib/sanitize';
|
||||
|
||||
// ===== 대상 옵션 =====
|
||||
const TARGET_OPTIONS = [
|
||||
@@ -94,7 +95,7 @@ export const popupFields: FieldDefinition[] = [
|
||||
// View 모드: HTML 렌더링
|
||||
return createElement('div', {
|
||||
className: 'border border-gray-200 rounded-md p-4 bg-gray-50 min-h-[100px] prose prose-sm max-w-none',
|
||||
dangerouslySetInnerHTML: { __html: (value as string) || '' },
|
||||
dangerouslySetInnerHTML: { __html: sanitizeHTML((value as string) || '') },
|
||||
});
|
||||
}
|
||||
// Edit/Create 모드: RichTextEditor
|
||||
@@ -110,7 +111,7 @@ export const popupFields: FieldDefinition[] = [
|
||||
if (!value) return '-';
|
||||
return createElement('div', {
|
||||
className: 'border border-gray-200 rounded-md p-4 bg-gray-50 min-h-[100px] prose prose-sm max-w-none',
|
||||
dangerouslySetInnerHTML: { __html: value as string },
|
||||
dangerouslySetInnerHTML: { __html: sanitizeHTML(value as string) },
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { Rank } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface PositionApiData {
|
||||
id: number;
|
||||
@@ -17,12 +18,6 @@ interface PositionApiData {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// ===== 데이터 변환: API → Frontend =====
|
||||
function transformApiToFrontend(apiData: PositionApiData): Rank {
|
||||
return {
|
||||
@@ -39,55 +34,21 @@ function transformApiToFrontend(apiData: PositionApiData): Rank {
|
||||
export async function getRanks(params?: {
|
||||
is_active?: boolean;
|
||||
q?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: Rank[];
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('type', 'rank');
|
||||
|
||||
if (params?.is_active !== undefined) {
|
||||
searchParams.set('is_active', params.is_active.toString());
|
||||
}
|
||||
if (params?.q) {
|
||||
searchParams.set('q', params.q);
|
||||
}
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions?${searchParams.toString()}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '직급 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PositionApiData[]> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직급 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const ranks = result.data.map(transformApiToFrontend);
|
||||
return { success: true, data: ranks };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getRanks] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}): Promise<ActionResult<Rank[]>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('type', 'rank');
|
||||
if (params?.is_active !== undefined) {
|
||||
searchParams.set('is_active', params.is_active.toString());
|
||||
}
|
||||
if (params?.q) {
|
||||
searchParams.set('q', params.q);
|
||||
}
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions?${searchParams.toString()}`,
|
||||
transform: (data: PositionApiData[]) => data.map(transformApiToFrontend),
|
||||
errorMessage: '직급 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 직급 생성 =====
|
||||
@@ -95,51 +56,19 @@ export async function createRank(data: {
|
||||
name: string;
|
||||
sort_order?: number;
|
||||
is_active?: boolean;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: Rank;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
type: 'rank',
|
||||
name: data.name,
|
||||
sort_order: data.sort_order,
|
||||
is_active: data.is_active ?? true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '직급 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PositionApiData> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직급 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const rank = transformApiToFrontend(result.data);
|
||||
return { success: true, data: rank };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[createRank] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}): Promise<ActionResult<Rank>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
type: 'rank',
|
||||
name: data.name,
|
||||
sort_order: data.sort_order,
|
||||
is_active: data.is_active ?? true,
|
||||
},
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '직급 생성에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 직급 수정 =====
|
||||
@@ -150,127 +79,33 @@ export async function updateRank(
|
||||
sort_order?: number;
|
||||
is_active?: boolean;
|
||||
}
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: Rank;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '직급 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PositionApiData> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직급 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const rank = transformApiToFrontend(result.data);
|
||||
return { success: true, data: rank };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[updateRank] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
): Promise<ActionResult<Rank>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions/${id}`,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '직급 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 직급 삭제 =====
|
||||
export async function deleteRank(id: number): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '직급 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직급 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteRank] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
export async function deleteRank(id: number): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '직급 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 직급 순서 변경 =====
|
||||
export async function reorderRanks(
|
||||
items: { id: number; sort_order: number }[]
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/reorder`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ items }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '순서 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '순서 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[reorderRanks] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions/reorder`,
|
||||
method: 'PUT',
|
||||
body: { items },
|
||||
errorMessage: '순서 변경에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -2,236 +2,52 @@
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { SubscriptionApiData, UsageApiData, SubscriptionInfo } from './types';
|
||||
import { transformApiToFrontend } from './utils';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 현재 활성 구독 조회 =====
|
||||
export async function getCurrentSubscription(): Promise<{
|
||||
success: boolean;
|
||||
data: SubscriptionApiData | null;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/current`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '구독 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: result.message || '구독 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] getCurrentSubscription error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function getCurrentSubscription(): Promise<ActionResult<SubscriptionApiData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/current`,
|
||||
errorMessage: '구독 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 사용량 조회 =====
|
||||
export async function getUsage(): Promise<{
|
||||
success: boolean;
|
||||
data: UsageApiData | null;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/usage`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '사용량 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: result.message || '사용량 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] getUsage error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function getUsage(): Promise<ActionResult<UsageApiData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/usage`,
|
||||
errorMessage: '사용량 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 구독 취소 =====
|
||||
export async function cancelSubscription(
|
||||
id: number,
|
||||
reason?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/${id}/cancel`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '구독 취소에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '구독 취소에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] cancelSubscription error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/${id}/cancel`,
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
errorMessage: '구독 취소에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 데이터 내보내기 요청 =====
|
||||
export async function requestDataExport(
|
||||
exportType: string = 'all'
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: { id: number; status: string };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/export`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ export_type: exportType }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '내보내기 요청에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '내보내기 요청에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: result.data.id,
|
||||
status: result.data.status,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] requestDataExport error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult<{ id: number; status: string }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/export`,
|
||||
method: 'POST',
|
||||
body: { export_type: exportType },
|
||||
transform: (data: { id: number; status: string }) => ({ id: data.id, status: data.status }),
|
||||
errorMessage: '내보내기 요청에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 통합 데이터 조회 (현재 구독 + 사용량) =====
|
||||
@@ -255,21 +71,14 @@ export async function getSubscriptionData(): Promise<{
|
||||
}
|
||||
|
||||
const data = transformApiToFrontend(
|
||||
subscriptionResult.data,
|
||||
usageResult.data
|
||||
subscriptionResult.data ?? null,
|
||||
usageResult.data ?? null
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] getSubscriptionData error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, data: null, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { Title } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface PositionApiData {
|
||||
id: number;
|
||||
@@ -17,12 +18,6 @@ interface PositionApiData {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// ===== 데이터 변환: API → Frontend =====
|
||||
function transformApiToFrontend(apiData: PositionApiData): Title {
|
||||
return {
|
||||
@@ -39,55 +34,21 @@ function transformApiToFrontend(apiData: PositionApiData): Title {
|
||||
export async function getTitles(params?: {
|
||||
is_active?: boolean;
|
||||
q?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: Title[];
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('type', 'title');
|
||||
|
||||
if (params?.is_active !== undefined) {
|
||||
searchParams.set('is_active', params.is_active.toString());
|
||||
}
|
||||
if (params?.q) {
|
||||
searchParams.set('q', params.q);
|
||||
}
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions?${searchParams.toString()}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '직책 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PositionApiData[]> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직책 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const titles = result.data.map(transformApiToFrontend);
|
||||
return { success: true, data: titles };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getTitles] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}): Promise<ActionResult<Title[]>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('type', 'title');
|
||||
if (params?.is_active !== undefined) {
|
||||
searchParams.set('is_active', params.is_active.toString());
|
||||
}
|
||||
if (params?.q) {
|
||||
searchParams.set('q', params.q);
|
||||
}
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions?${searchParams.toString()}`,
|
||||
transform: (data: PositionApiData[]) => data.map(transformApiToFrontend),
|
||||
errorMessage: '직책 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 직책 생성 =====
|
||||
@@ -95,51 +56,19 @@ export async function createTitle(data: {
|
||||
name: string;
|
||||
sort_order?: number;
|
||||
is_active?: boolean;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: Title;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
type: 'title',
|
||||
name: data.name,
|
||||
sort_order: data.sort_order,
|
||||
is_active: data.is_active ?? true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '직책 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PositionApiData> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직책 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const title = transformApiToFrontend(result.data);
|
||||
return { success: true, data: title };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[createTitle] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}): Promise<ActionResult<Title>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
type: 'title',
|
||||
name: data.name,
|
||||
sort_order: data.sort_order,
|
||||
is_active: data.is_active ?? true,
|
||||
},
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '직책 생성에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 직책 수정 =====
|
||||
@@ -150,127 +79,33 @@ export async function updateTitle(
|
||||
sort_order?: number;
|
||||
is_active?: boolean;
|
||||
}
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: Title;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '직책 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PositionApiData> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직책 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const title = transformApiToFrontend(result.data);
|
||||
return { success: true, data: title };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[updateTitle] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
): Promise<ActionResult<Title>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions/${id}`,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '직책 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 직책 삭제 =====
|
||||
export async function deleteTitle(id: number): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '직책 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직책 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteTitle] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
export async function deleteTitle(id: number): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '직책 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 직책 순서 변경 =====
|
||||
export async function reorderTitles(
|
||||
items: { id: number; sort_order: number }[]
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/reorder`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ items }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '순서 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '순서 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[reorderTitles] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/positions/reorder`,
|
||||
method: 'PUT',
|
||||
body: { items },
|
||||
errorMessage: '순서 변경에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://sam.kr:8080';
|
||||
|
||||
@@ -49,7 +48,7 @@ function transformFromApi(data: ApiWorkSetting): WorkSettingFormData {
|
||||
return {
|
||||
workType: data.work_type,
|
||||
workDays: data.work_days || ['mon', 'tue', 'wed', 'thu', 'fri'],
|
||||
workStartTime: data.start_time?.substring(0, 5) || '09:00', // HH:mm:ss → HH:mm
|
||||
workStartTime: data.start_time?.substring(0, 5) || '09:00',
|
||||
workEndTime: data.end_time?.substring(0, 5) || '18:00',
|
||||
weeklyWorkHours: data.standard_hours,
|
||||
weeklyOvertimeHours: data.overtime_hours,
|
||||
@@ -65,10 +64,9 @@ function transformFromApi(data: ApiWorkSetting): WorkSettingFormData {
|
||||
*/
|
||||
function transformToApi(data: Partial<WorkSettingFormData>): Record<string, unknown> {
|
||||
const apiData: Record<string, unknown> = {};
|
||||
|
||||
if (data.workType !== undefined) apiData.work_type = data.workType;
|
||||
if (data.workDays !== undefined) apiData.work_days = data.workDays;
|
||||
if (data.workStartTime !== undefined) apiData.start_time = `${data.workStartTime}:00`; // HH:mm → HH:mm:ss
|
||||
if (data.workStartTime !== undefined) apiData.start_time = `${data.workStartTime}:00`;
|
||||
if (data.workEndTime !== undefined) apiData.end_time = `${data.workEndTime}:00`;
|
||||
if (data.weeklyWorkHours !== undefined) apiData.standard_hours = data.weeklyWorkHours;
|
||||
if (data.weeklyOvertimeHours !== undefined) apiData.overtime_hours = data.weeklyOvertimeHours;
|
||||
@@ -76,7 +74,6 @@ function transformToApi(data: Partial<WorkSettingFormData>): Record<string, unkn
|
||||
if (data.breakMinutes !== undefined) apiData.break_minutes = data.breakMinutes;
|
||||
if (data.breakStartTime !== undefined) apiData.break_start = `${data.breakStartTime}:00`;
|
||||
if (data.breakEndTime !== undefined) apiData.break_end = `${data.breakEndTime}:00`;
|
||||
|
||||
return apiData;
|
||||
}
|
||||
|
||||
@@ -85,48 +82,12 @@ function transformToApi(data: Partial<WorkSettingFormData>): Record<string, unkn
|
||||
/**
|
||||
* 근무 설정 조회
|
||||
*/
|
||||
export async function getWorkSetting(): Promise<{
|
||||
success: boolean;
|
||||
data?: WorkSettingFormData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_BASE_URL}/api/v1/settings/work`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
const errorData = await response?.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.message || `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformFromApi(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('getWorkSetting error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '근무 설정을 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
export async function getWorkSetting(): Promise<ActionResult<WorkSettingFormData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_BASE_URL}/api/v1/settings/work`,
|
||||
transform: (data: ApiWorkSetting) => transformFromApi(data),
|
||||
errorMessage: '근무 설정을 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,46 +95,12 @@ export async function getWorkSetting(): Promise<{
|
||||
*/
|
||||
export async function updateWorkSetting(
|
||||
data: Partial<WorkSettingFormData>
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: WorkSettingFormData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_BASE_URL}/api/v1/settings/work`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(transformToApi(data)),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
const errorData = await response?.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.message || `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformFromApi(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('updateWorkSetting error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '근무 설정 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult<WorkSettingFormData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_BASE_URL}/api/v1/settings/work`,
|
||||
method: 'PUT',
|
||||
body: transformToApi(data),
|
||||
transform: (data: ApiWorkSetting) => transformFromApi(data),
|
||||
errorMessage: '근무 설정 저장에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user