- middleware CSP: *.kakao.com, *.kakaocdn.net 추가 (다음 주소찾기 차단 해결) - frame-src에 'self' 추가 - 공지 팝업 이미지 중앙정렬 ([&_img]:mx-auto) - HR 사원관리, 결재, 품목, 생산 등 다수 개선 - API 에러 핸들링 및 JSON 파싱 안정화
157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
'use server';
|
|
|
|
|
|
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로 변환
|
|
* R2 전환 후: /api/proxy/files/{id}/view 사용
|
|
* 레거시 경로는 그대로 반환 (표시 불가할 수 있음)
|
|
*/
|
|
function toAbsoluteUrl(path: string | undefined): string | undefined {
|
|
if (!path) return undefined;
|
|
// 이미 절대 URL이면 그대로 반환
|
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
return path;
|
|
}
|
|
// 프록시 경로면 그대로 반환
|
|
if (path.startsWith('/api/proxy/')) {
|
|
return path;
|
|
}
|
|
// R2 전환 후 /storage/ 직접 접근 불가 — 경로만 보존
|
|
return path;
|
|
}
|
|
|
|
// ===== 계정 정보 조회 =====
|
|
export async function getAccountInfo(): Promise<{
|
|
success: boolean;
|
|
data?: {
|
|
accountInfo: AccountInfo;
|
|
termsAgreements: TermsAgreement[];
|
|
marketingConsent: MarketingConsent;
|
|
};
|
|
error?: string;
|
|
__authError?: boolean;
|
|
}> {
|
|
// 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 };
|
|
|
|
const user = userResult.data;
|
|
|
|
// 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<ActionResult> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/users/withdraw`,
|
|
method: 'POST',
|
|
body: { password },
|
|
errorMessage: '계정 탈퇴에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 테넌트 사용 중지 =====
|
|
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<ActionResult> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/account/agreements`,
|
|
method: 'PUT',
|
|
body: { agreements },
|
|
errorMessage: '약관 동의 수정에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 프로필 이미지 업로드 =====
|
|
export async function uploadProfileImage(formData: FormData): Promise<{
|
|
success: boolean;
|
|
data?: { imageUrl: string };
|
|
error?: string;
|
|
__authError?: boolean;
|
|
}> {
|
|
// 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 };
|
|
|
|
const uploadedPath = uploadResult.data.file_path || uploadResult.data.path || uploadResult.data.url;
|
|
if (!uploadedPath) return { success: false, error: '업로드된 파일 경로를 가져올 수 없습니다.' };
|
|
|
|
// 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 };
|
|
|
|
// R2 전환: file_id 기반 프록시 경로 사용
|
|
const fileId = uploadResult.data.id;
|
|
const viewUrl = fileId
|
|
? `/api/proxy/files/${fileId}/view`
|
|
: uploadedPath;
|
|
|
|
return {
|
|
success: true,
|
|
data: { imageUrl: viewUrl },
|
|
};
|
|
}
|