Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 3 additions & 22 deletions app/profile-image/_components/TermsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,6 @@ interface TermsDrawerProps {

type ViewMode = "list" | "terms" | "privacy";

const DEFAULT_PROFILE_IDS = new Set([
"dog",
"cat",
"dinosaur",
"otter",
"bear",
"fox",
"penguin",
"wolf",
"rabbit",
"snake",
"horse",
"frog",
]);

const toDefaultProfileImageKey = (profileId?: string) => {
const rawId = profileId || "bear";
if (rawId.startsWith("default_")) return rawId;
return DEFAULT_PROFILE_IDS.has(rawId) ? `default_${rawId}` : rawId;
};

const TermsDrawer = ({ children }: TermsDrawerProps) => {
const router = useRouter();
const { profile, clearProfile } = useProfile();
Expand Down Expand Up @@ -116,11 +95,13 @@ const TermsDrawer = ({ children }: TermsDrawerProps) => {
if (!allAgreed || isSubmitting) return;

try {
let finalImageUrl = toDefaultProfileImageKey(profile.profileImageUrl);
let finalImageUrl: string;

// 1. 커스텀 이미지가 있으면 먼저 업로드 (Key 획득)
if (profile.profileImageFile) {
finalImageUrl = await uploadImage(profile.profileImageFile);
} else {
finalImageUrl = `default_${profile.profileImageUrl || "bear"}`;
}

// 2. 최종 데이터 객체 생성 (백엔드 양식 준수)
Expand Down
43 changes: 32 additions & 11 deletions hooks/useProfileSignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,47 @@ interface SignUpResponse {
* 1단계: Presigned URL을 받아오고 S3에 업로드하는 함수
*/
const uploadImage = async (file: File): Promise<string> => {
// 백엔드 명세: GET /api/members/files/presigned/profiles
const { data: response } = await api.get<PresignedUrlResponse>("/api/members/files/presigned/profiles");
let presignedUrl: string;
let imageKey: string;

const { presignedUrl, imageKey } = response.data;
// 1단계: Presigned URL 발급
try {
const { data: response } = await api.get<PresignedUrlResponse>(
"/api/members/files/presigned/profiles",
{ params: { filename: file.name } },
);
presignedUrl = response.data.presignedUrl;
imageKey = response.data.imageKey;
} catch (error) {
console.error("❌ Presigned URL을 받아오지 못했습니다.", error);
throw error;
}

// S3에 직접 업로드 (Axios 인스턴스 대신 표준 axios 사용 - baseURL 무시를 위해)
await axios.put(presignedUrl, file, {
headers: {
"Content-Type": file.type,
},
});
// 2단계: S3에 직접 업로드 (Axios 인스턴스 대신 표준 axios 사용 - baseURL 무시를 위해)
try {
await axios.put(presignedUrl, file, {
headers: {
"Content-Type": file.type,
},
});
Comment on lines +33 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Serialized file upload crash 🐞 Bug ⛯ Reliability

TermsDrawer가 profile.profileImageFile을 truthy 체크만으로 uploadImage에 전달하는데, ProfileProvider가 프로필 전체를
JSON.stringify로 localStorage에 저장/로드하므로 새로고침 후 profileImageFile이 실제 File이 아닌 plain object가 되어
file.name/file.type 접근이 깨져 presigned 발급 또는 S3 업로드가 런타임 실패합니다.
Agent Prompt
### Issue description
`profileImageFile`(File 객체)가 ProfileProvider에서 localStorage에 JSON으로 저장/복원되면서 실제 `File` 형태가 보존되지 않습니다. 이후 `TermsDrawer.handleComplete()`에서 truthy 체크만으로 `uploadImage()`에 전달되고, 이번 PR에서 `uploadImage()`가 `file.name`을 쿼리 파라미터로 사용하게 되어 새로고침/재방문 시 런타임 실패가 발생할 수 있습니다.

### Issue Context
- 프로필 상태는 localStorage에 그대로 저장됩니다.
- File 객체는 JSON 직렬화/역직렬화로 원형이 보존되지 않습니다.
- `uploadImage()`는 `file.name`/`file.type`에 의존합니다.

### Fix Focus Areas
- providers/profile-provider.tsx[22-46]
- app/profile-image/_components/ScreenProfileImage.tsx[49-57]
- app/profile-image/_components/TermsDrawer.tsx[94-105]
- hooks/useProfileSignUp.ts[27-50]

### Expected changes
1) localStorage에 저장할 프로필 데이터에서 `profileImageFile`을 제외(저장 직전 삭제)하거나, 로드 직후 `profileImageFile`을 항상 `undefined`로 정리.
2) `TermsDrawer`에서 업로드 호출 전에 `profile.profileImageFile instanceof File` 체크. 아니라면 `updateProfile({ profileImageFile: undefined })`(provider 접근 가능 시)로 정리하고 에러 메시지/재선택 유도.
3) 가능하면 `useProfile` 상태를 `persisted fields`와 `ephemeral fields(File/preview)`로 분리.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

} catch (error) {
console.error("❌ 이미지 업로드에 실패했습니다.", error);
throw error;
}

return imageKey;
};

/**
* 2단계: 최종 프로필 데이터를 전송하는 함수
*/
const signUpProfile = async (payload: ProfileSubmitData): Promise<SignUpResponse> => {
const { data } = await api.post<SignUpResponse>("/api/auth/signup/profile", payload);
const signUpProfile = async (
payload: ProfileSubmitData,
): Promise<SignUpResponse> => {
const { data } = await api.post<SignUpResponse>(
"/api/auth/signup/profile",
payload,
);
return data;
};

Expand Down
Loading