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: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface TenantApiData {
|
||||
ceo_name?: string;
|
||||
homepage?: string;
|
||||
fax?: string;
|
||||
logo?: string;
|
||||
options?: {
|
||||
business_type?: string;
|
||||
business_category?: string;
|
||||
@@ -127,6 +128,21 @@ export async function updateCompanyInfo(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상대 경로를 절대 URL로 변환
|
||||
* /storage/... → https://api.example.com/storage/...
|
||||
*/
|
||||
function toAbsoluteUrl(path: string | undefined): string | undefined {
|
||||
if (!path) return undefined;
|
||||
// 이미 절대 URL이면 그대로 반환
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
}
|
||||
// 상대 경로면 API URL 붙이기
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
return `${apiUrl}${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 → Frontend 변환
|
||||
*
|
||||
@@ -146,8 +162,8 @@ function transformApiToFrontend(apiData: TenantApiData): CompanyFormData & { ten
|
||||
managerPhone: apiData.phone || '',
|
||||
businessNumber: apiData.business_num || '',
|
||||
address: apiData.address || '',
|
||||
// 확장 필드 (options에서 읽어옴)
|
||||
companyLogo: undefined,
|
||||
// 로고 URL (상대 경로 → 절대 URL 변환)
|
||||
companyLogo: toAbsoluteUrl(apiData.logo),
|
||||
businessType: opts.business_type || '',
|
||||
businessCategory: opts.business_category || '',
|
||||
zipCode: opts.zip_code || '',
|
||||
@@ -172,6 +188,15 @@ function transformFrontendToApi(
|
||||
tenantId: number,
|
||||
data: Partial<CompanyFormData>
|
||||
): Record<string, unknown> {
|
||||
// 로고 URL에서 상대 경로 추출 (API는 상대 경로 기대)
|
||||
let logoPath: string | null = null;
|
||||
if (data.companyLogo) {
|
||||
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,
|
||||
@@ -179,6 +204,8 @@ 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)
|
||||
@@ -197,4 +224,54 @@ function transformFrontendToApi(
|
||||
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) {
|
||||
console.error('[uploadCompanyLogo] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
||||
import { Building2, Plus, Save, Upload, X, Search, Loader2 } from 'lucide-react';
|
||||
import { Building2, Plus, Save, Upload, X, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -19,7 +19,7 @@ import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { AddCompanyDialog } from './AddCompanyDialog';
|
||||
import type { CompanyFormData } from './types';
|
||||
import { INITIAL_FORM_DATA, PAYMENT_DAY_OPTIONS } from './types';
|
||||
import { getCompanyInfo, updateCompanyInfo } from './actions';
|
||||
import { getCompanyInfo, updateCompanyInfo, uploadCompanyLogo } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function CompanyInfoManagement() {
|
||||
@@ -55,11 +55,18 @@ export function CompanyInfoManagement() {
|
||||
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||
const licenseInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 로고 파일명
|
||||
const [logoFileName, setLogoFileName] = useState<string>('');
|
||||
// 로고 업로드 상태
|
||||
const [isUploadingLogo, setIsUploadingLogo] = useState(false);
|
||||
// 로고 미리보기 URL (로컬 파일 미리보기용)
|
||||
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
|
||||
// 사업자등록증 파일명
|
||||
const [licenseFileName, setLicenseFileName] = useState<string>('');
|
||||
|
||||
// 로고 URL 계산 (서버 URL 또는 로컬 미리보기)
|
||||
const currentLogoUrl = logoPreviewUrl || (
|
||||
typeof formData.companyLogo === 'string' ? formData.companyLogo : null
|
||||
);
|
||||
|
||||
const handleChange = useCallback((field: keyof CompanyFormData, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
}, []);
|
||||
@@ -68,27 +75,58 @@ export function CompanyInfoManagement() {
|
||||
logoInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleLogoChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// 파일 크기 체크 (10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
alert('파일 크기는 10MB 이하여야 합니다.');
|
||||
return;
|
||||
}
|
||||
// 파일 타입 체크
|
||||
if (!['image/png', 'image/jpeg', 'image/gif'].includes(file.type)) {
|
||||
alert('PNG, JPEG, GIF 파일만 업로드 가능합니다.');
|
||||
return;
|
||||
}
|
||||
setFormData(prev => ({ ...prev, companyLogo: file }));
|
||||
setLogoFileName(file.name);
|
||||
if (!file) return;
|
||||
|
||||
// 파일 크기 체크 (5MB - API 제한에 맞춤)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error('파일 크기는 5MB 이하여야 합니다.');
|
||||
return;
|
||||
}
|
||||
// 파일 타입 체크
|
||||
if (!['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(file.type)) {
|
||||
toast.error('PNG, JPEG, GIF, WEBP 파일만 업로드 가능합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 이전 이미지 저장 (롤백용)
|
||||
const previousLogo = logoPreviewUrl;
|
||||
|
||||
// FileReader로 base64 미리보기 생성 (account-info 방식)
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setLogoPreviewUrl(event.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// FormData 생성 (Server Action에 전달)
|
||||
const formData = new FormData();
|
||||
formData.append('logo', file);
|
||||
|
||||
// 즉시 업로드
|
||||
setIsUploadingLogo(true);
|
||||
const result = await uploadCompanyLogo(formData);
|
||||
|
||||
if (result.success && result.data) {
|
||||
// 성공: 서버 URL도 저장 (다음 페이지 로드 시 사용)
|
||||
setFormData(prev => ({ ...prev, companyLogo: result.data!.logoUrl }));
|
||||
toast.success('로고가 업로드되었습니다.');
|
||||
} else {
|
||||
// 실패: 롤백
|
||||
setLogoPreviewUrl(previousLogo);
|
||||
toast.error(result.error || '로고 업로드에 실패했습니다.');
|
||||
}
|
||||
|
||||
setIsUploadingLogo(false);
|
||||
if (logoInputRef.current) {
|
||||
logoInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveLogo = () => {
|
||||
setLogoPreviewUrl(null);
|
||||
setFormData(prev => ({ ...prev, companyLogo: undefined }));
|
||||
setLogoFileName('');
|
||||
if (logoInputRef.current) {
|
||||
logoInputRef.current.value = '';
|
||||
}
|
||||
@@ -188,14 +226,22 @@ export function CompanyInfoManagement() {
|
||||
<div className="space-y-2">
|
||||
<Label>회사 로고</Label>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-[200px] h-[67px] border rounded-lg flex items-center justify-center bg-muted/50 overflow-hidden">
|
||||
{logoFileName ? (
|
||||
<span className="text-sm text-muted-foreground truncate px-2">
|
||||
{logoFileName}
|
||||
</span>
|
||||
<div className="relative w-[200px] h-[67px] border rounded-lg flex items-center justify-center bg-muted/50 overflow-hidden">
|
||||
{currentLogoUrl ? (
|
||||
<img
|
||||
src={currentLogoUrl}
|
||||
alt="회사 로고"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">IMG</span>
|
||||
)}
|
||||
{/* 업로드 중 오버레이 */}
|
||||
{isUploadingLogo && (
|
||||
<div className="absolute inset-0 bg-background/80 flex items-center justify-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isEditMode && (
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -204,11 +250,21 @@ export function CompanyInfoManagement() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleLogoUpload}
|
||||
disabled={isUploadingLogo}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
업로드
|
||||
{isUploadingLogo ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
업로드 중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
업로드
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{logoFileName && (
|
||||
{currentLogoUrl && !isUploadingLogo && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -223,12 +279,12 @@ export function CompanyInfoManagement() {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
750 X 250px, 10MB 이하의 PNG, JPEG, GIF
|
||||
750 X 250px, 5MB 이하의 PNG, JPEG, GIF, WEBP
|
||||
</p>
|
||||
<input
|
||||
ref={logoInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/gif"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
className="hidden"
|
||||
onChange={handleLogoChange}
|
||||
/>
|
||||
|
||||
@@ -83,7 +83,17 @@ export async function serverFetch(
|
||||
const cookieStore = await cookies();
|
||||
const refreshToken = cookieStore.get('refresh_token')?.value;
|
||||
|
||||
const headers = await getServerApiHeaders();
|
||||
const baseHeaders = await getServerApiHeaders() as Record<string, string>;
|
||||
|
||||
// FormData일 경우 Content-Type을 제외 (브라우저가 자동 설정)
|
||||
const isFormData = options?.body instanceof FormData;
|
||||
const headers: HeadersInit = isFormData
|
||||
? {
|
||||
Accept: baseHeaders.Accept,
|
||||
Authorization: baseHeaders.Authorization,
|
||||
'X-API-KEY': baseHeaders['X-API-KEY'],
|
||||
}
|
||||
: baseHeaders;
|
||||
|
||||
let response = await fetch(url, {
|
||||
...options,
|
||||
@@ -107,7 +117,15 @@ export async function serverFetch(
|
||||
await setNewTokenCookies(refreshResult);
|
||||
|
||||
// 새 토큰으로 원래 요청 재시도
|
||||
const newHeaders = await getServerApiHeaders(refreshResult.accessToken);
|
||||
const newBaseHeaders = await getServerApiHeaders(refreshResult.accessToken) as Record<string, string>;
|
||||
// FormData일 경우 Content-Type을 제외 (브라우저가 자동 설정)
|
||||
const newHeaders: HeadersInit = isFormData
|
||||
? {
|
||||
Accept: newBaseHeaders.Accept,
|
||||
Authorization: newBaseHeaders.Authorization,
|
||||
'X-API-KEY': newBaseHeaders['X-API-KEY'],
|
||||
}
|
||||
: newBaseHeaders;
|
||||
response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user