- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
'use server';
|
|
|
|
|
|
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;
|
|
company_name: string;
|
|
code?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
address?: string;
|
|
business_num?: string;
|
|
corp_reg_no?: string;
|
|
ceo_name?: string;
|
|
homepage?: string;
|
|
fax?: string;
|
|
logo?: string;
|
|
options?: {
|
|
business_type?: string;
|
|
business_category?: string;
|
|
zip_code?: string;
|
|
address_detail?: string;
|
|
tax_invoice_email?: string;
|
|
manager_name?: string;
|
|
payment_bank?: string;
|
|
payment_account?: string;
|
|
payment_account_holder?: string;
|
|
payment_day?: string;
|
|
};
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
}
|
|
|
|
// ===== 테넌트 정보 조회 =====
|
|
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<ActionResult<CompanyFormData & { tenantId: number }>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/tenants`,
|
|
method: 'PUT',
|
|
body: transformFrontendToApi(tenantId, data),
|
|
transform: (d: TenantApiData) => transformApiToFrontend(d),
|
|
errorMessage: '회사 정보 수정에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 회사 로고 업로드 =====
|
|
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;
|
|
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
|
return `${apiUrl}${path}`;
|
|
}
|
|
|
|
function transformApiToFrontend(apiData: TenantApiData): CompanyFormData & { tenantId: number } {
|
|
const opts = apiData.options || {};
|
|
return {
|
|
tenantId: apiData.id,
|
|
companyName: apiData.company_name || '',
|
|
representativeName: apiData.ceo_name || '',
|
|
email: apiData.email || '',
|
|
managerPhone: apiData.phone || '',
|
|
businessNumber: apiData.business_num || '',
|
|
address: apiData.address || '',
|
|
companyLogo: toAbsoluteUrl(apiData.logo),
|
|
businessType: opts.business_type || '',
|
|
businessCategory: opts.business_category || '',
|
|
zipCode: opts.zip_code || '',
|
|
addressDetail: opts.address_detail || '',
|
|
taxInvoiceEmail: opts.tax_invoice_email || '',
|
|
managerName: opts.manager_name || '',
|
|
businessLicense: undefined,
|
|
paymentBank: opts.payment_bank || '',
|
|
paymentAccount: opts.payment_account || '',
|
|
paymentAccountHolder: opts.payment_account_holder || '',
|
|
paymentDay: opts.payment_day || '',
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
return {
|
|
tenant_id: tenantId,
|
|
company_name: data.companyName,
|
|
ceo_name: data.representativeName,
|
|
email: data.email,
|
|
phone: data.managerPhone,
|
|
business_num: data.businessNumber,
|
|
logo: logoPath,
|
|
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,
|
|
},
|
|
};
|
|
} |