fix(WEB): 프로필 이미지 업로드 및 회사 로고 기능 수정
AccountInfoManagement: - toAbsoluteUrl() 함수 추가 (상대경로 → 절대 URL 변환) - getAccountInfo()에서 /api/v1/profiles/me 조회 추가 (이미지 새로고침 후 유지) - uploadProfileImage() 구현 (2단계: 파일 업로드 → 프로필 업데이트) - updateAgreements() 구현 (약관 동의 수정) - withdrawAccount()에 password 파라미터 추가 CompanyInfoManagement: - toAbsoluteUrl() 함수 추가 (로고 이미지 경로 변환) fetch-wrapper: - FormData 전송 시 Content-Type 헤더 제외 (브라우저 자동 설정)
This commit is contained in:
@@ -3,6 +3,24 @@
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { AccountInfo, TermsAgreement, MarketingConsent } from './types';
|
||||
|
||||
/**
|
||||
* 상대 경로를 절대 URL로 변환
|
||||
* /storage/... 또는 1/temp/... → https://api.example.com/storage/tenants/...
|
||||
*/
|
||||
function toAbsoluteUrl(path: string | undefined): string | undefined {
|
||||
if (!path) return undefined;
|
||||
// 이미 절대 URL이면 그대로 반환
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
}
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
// /storage/로 시작하면 그대로, 아니면 /storage/tenants/ 붙이기
|
||||
if (path.startsWith('/storage/')) {
|
||||
return `${apiUrl}${path}`;
|
||||
}
|
||||
return `${apiUrl}/storage/tenants/${path}`;
|
||||
}
|
||||
|
||||
// ===== 계정 정보 조회 =====
|
||||
export async function getAccountInfo(): Promise<{
|
||||
success: boolean;
|
||||
@@ -15,6 +33,7 @@ export async function getAccountInfo(): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
// 1. 사용자 기본 정보 조회
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/users/me`,
|
||||
{
|
||||
@@ -48,13 +67,35 @@ export async function getAccountInfo(): Promise<{
|
||||
|
||||
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: user.profile_image || undefined,
|
||||
profileImage, // 프로필 API에서 가져온 이미지
|
||||
role: user.role?.name || user.role || '',
|
||||
status: user.status || 'active',
|
||||
isTenantMaster: user.is_tenant_master || false,
|
||||
@@ -78,7 +119,7 @@ export async function getAccountInfo(): Promise<{
|
||||
}
|
||||
|
||||
// ===== 계정 탈퇴 =====
|
||||
export async function withdrawAccount(): Promise<{
|
||||
export async function withdrawAccount(password: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
@@ -88,7 +129,7 @@ export async function withdrawAccount(): Promise<{
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/users/withdraw`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
body: JSON.stringify({ password }),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -174,3 +215,171 @@ export async function suspendTenant(): Promise<{
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 약관 동의 수정 =====
|
||||
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) {
|
||||
console.error('[AccountInfoActions] updateAgreements error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 프로필 이미지 업로드 =====
|
||||
export async function uploadProfileImage(formData: FormData): Promise<{
|
||||
success: boolean;
|
||||
data?: { imageUrl: string };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
console.log('[uploadProfileImage] Starting upload...');
|
||||
|
||||
// 1. 먼저 파일 업로드 (일반 파일 업로드 엔드포인트 사용)
|
||||
const { response: uploadResponse, error: uploadError } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/files/upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
console.log('[uploadProfileImage] Upload response status:', uploadResponse?.status);
|
||||
|
||||
if (uploadError) {
|
||||
console.error('[uploadProfileImage] Upload error:', uploadError);
|
||||
return {
|
||||
success: false,
|
||||
error: uploadError.message,
|
||||
__authError: uploadError.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('[AccountInfoActions] uploadProfileImage error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user