Skip to content

Commit 37dca81

Browse files
authored
Fix: 이미지 관련 문의사항 해결 (#204)
* fix: 모달 닫힘 경고 팝업 뜨도록 수정 및 esc props 전달 * Squashed commit of the following: commit c07799a Merge: 0b2fa2b 6d6f73e Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:58:43 2025 +0900 Merge branch 'develop' of https://github.com/YAPP-Github/26th-Web-Team-3-FE into qa/additional/#199 commit 0b2fa2b Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:43:48 2025 +0900 style: grid-layout 모바일 view 수정 commit 278afe5 Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:24:26 2025 +0900 fix: 편지 담기 reset 추가 commit 6d6f73e Author: beom <74394824+seung365@users.noreply.github.com> Date: Sat Aug 23 00:05:59 2025 +0900 fix: 모달 닫힘 경고 팝업 뜨도록 수정 및 esc props 전달 (#198) * fix: 모바일에서 간헐적으로 이미지 업로드 안되는 현상 수정 * feat: objectKey를 선택적 속성으로 변경 * feat: 사진 업로드 로직 변경 * chore: 줄간격 추가 * chore: type 충돌 해결 * chore: 코드 리뷰 반영
1 parent 5c19df6 commit 37dca81

7 files changed

Lines changed: 145 additions & 109 deletions

File tree

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,77 @@
11
import { useFileUpload } from "@/shared/api/mutations/file";
2-
import { useEffect, useState, useCallback } from "react";
2+
import type { ChangeEvent } from "react";
3+
import { useEffect, useState } from "react";
34

4-
interface useImageUploadProps {
5-
onObjectKeyChange: (value: string) => void;
6-
}
7-
8-
export const useImageUpload = ({ onObjectKeyChange }: useImageUploadProps) => {
5+
export const useImageUpload = () => {
96
const fileUploadMutation = useFileUpload();
10-
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
7+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
8+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
119

1210
useEffect(() => {
1311
return () => {
14-
if (uploadedImageUrl) {
15-
URL.revokeObjectURL(uploadedImageUrl);
12+
if (previewUrl) {
13+
URL.revokeObjectURL(previewUrl);
1614
}
1715
};
18-
}, [uploadedImageUrl]);
16+
}, [previewUrl]);
1917

20-
const handleImageUpload = async (): Promise<void> => {
21-
const input = document.createElement("input");
22-
input.type = "file";
23-
input.accept = "image/*";
18+
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
19+
const file = e.target.files?.[0];
20+
if (!file) {
21+
return;
22+
}
2423

25-
return new Promise((resolve, reject) => {
26-
input.onchange = async (e) => {
27-
const file = (e.target as HTMLInputElement).files?.[0];
28-
if (!file) {
29-
resolve();
30-
return;
31-
}
24+
const extension = file.name.split(".").pop();
25+
if (!extension) {
26+
alert("파일 확장자를 찾을 수 없습니다.");
27+
return;
28+
}
3229

33-
try {
34-
const fileName = "LETTER";
35-
const extension = file.name.split(".").pop();
30+
if (previewUrl) {
31+
URL.revokeObjectURL(previewUrl);
32+
}
3633

37-
if (!extension) {
38-
throw new Error("파일 확장자를 찾을 수 없습니다.");
39-
}
34+
const objectUrl = URL.createObjectURL(file);
4035

41-
const uploadedObjectKey = await fileUploadMutation.mutateAsync({
42-
fileName,
43-
extension,
44-
file,
45-
});
36+
setSelectedFile(file);
37+
setPreviewUrl(objectUrl);
38+
};
4639

47-
setUploadedImageUrl(URL.createObjectURL(file));
48-
onObjectKeyChange(uploadedObjectKey);
49-
resolve();
50-
} catch (error) {
51-
console.error("이미지 업로드 실패:", error);
52-
reject(error);
53-
}
54-
};
40+
const uploadFile = async (): Promise<string> => {
41+
if (!selectedFile) {
42+
throw new Error("선택된 파일이 없습니다.");
43+
}
5544

56-
input.click();
45+
const fileName = "LETTER";
46+
const extension = selectedFile.name.split(".").pop();
47+
48+
if (!extension) {
49+
throw new Error("파일 확장자를 찾을 수 없습니다.");
50+
}
51+
52+
const uploadedObjectKey = await fileUploadMutation.mutateAsync({
53+
fileName,
54+
extension,
55+
file: selectedFile,
5756
});
57+
58+
return uploadedObjectKey;
5859
};
5960

60-
const removeImage = useCallback(() => {
61-
if (uploadedImageUrl) {
62-
URL.revokeObjectURL(uploadedImageUrl);
61+
const removeImage = () => {
62+
if (previewUrl) {
63+
URL.revokeObjectURL(previewUrl);
6364
}
64-
setUploadedImageUrl(null);
65-
onObjectKeyChange("");
66-
}, [uploadedImageUrl, onObjectKeyChange]);
65+
setPreviewUrl(null);
66+
setSelectedFile(null);
67+
};
6768

6869
return {
69-
uploadedImageUrl,
70-
handleImageUpload,
70+
previewUrl,
71+
handleFileChange,
7172
removeImage,
73+
uploadFile,
74+
hasFile: !!selectedFile,
7275
isUploading: fileUploadMutation.isPending,
7376
};
7477
};

app/(sub)/capsule-detail/_components/write-modal/index.tsx

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,11 @@ const WriteModal = ({
3636
const [isWarningOpen, setIsWarningOpen] = useState(false);
3737
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
3838

39-
const {
40-
handleSubmit,
41-
setValue,
42-
getValues,
43-
control,
44-
reset,
45-
formState: { errors },
46-
} = useForm<WriteLetterReq>({
39+
const { handleSubmit, getValues, control, reset } = useForm<WriteLetterReq>({
4740
defaultValues: {
4841
capsuleId: capsuleData.result.id.toString(),
4942
content: "",
5043
from: "",
51-
objectKey: "",
5244
},
5345
});
5446

@@ -62,10 +54,14 @@ const WriteModal = ({
6254
control,
6355
});
6456

65-
const { uploadedImageUrl, handleImageUpload, removeImage, isUploading } =
66-
useImageUpload({
67-
onObjectKeyChange: (value) => setValue("objectKey", value),
68-
});
57+
const {
58+
previewUrl,
59+
handleFileChange,
60+
removeImage,
61+
uploadFile,
62+
hasFile,
63+
isUploading,
64+
} = useImageUpload();
6965

7066
const onSubmit = (data: WriteLetterReq) => {
7167
if (!data.content?.trim()) {
@@ -75,42 +71,63 @@ const WriteModal = ({
7571
setIsConfirmOpen(true);
7672
};
7773

78-
const handleConfirm = (data: WriteLetterReq) => {
79-
if (isPending) return;
80-
writeLetterMutate(data, {
81-
onSuccess: () => {
82-
setIsConfirmOpen(false);
83-
reset({
84-
capsuleId: capsuleData.result.id.toString(),
85-
content: "",
86-
from: "",
87-
objectKey: "",
88-
});
89-
if (uploadedImageUrl) {
90-
removeImage();
91-
}
92-
onClose();
93-
onSuccess();
94-
},
95-
onError: (error) => {
96-
setIsConfirmOpen(false);
97-
console.error("편지 제출 실패:", error);
98-
},
99-
});
74+
const handleConfirm = async (data: WriteLetterReq) => {
75+
if (isPending || isUploading) return;
76+
77+
try {
78+
const submitData = { ...data };
79+
80+
if (hasFile) {
81+
const objectKey = await uploadFile();
82+
submitData.objectKey = objectKey;
83+
} else {
84+
delete submitData.objectKey;
85+
}
86+
87+
writeLetterMutate(submitData, {
88+
onSuccess: () => {
89+
setIsConfirmOpen(false);
90+
reset({
91+
capsuleId: capsuleData.result.id.toString(),
92+
content: "",
93+
from: "",
94+
objectKey: undefined,
95+
});
96+
if (previewUrl) {
97+
removeImage();
98+
}
99+
onClose();
100+
onSuccess();
101+
},
102+
onError: (error) => {
103+
setIsConfirmOpen(false);
104+
console.error("편지 제출 실패:", error);
105+
},
106+
});
107+
} catch (error) {
108+
setIsConfirmOpen(false);
109+
console.error("파일 업로드 실패:", error);
110+
alert(
111+
`업로드 실패: ${
112+
error instanceof Error ? error.message : "알 수 없는 오류"
113+
}`,
114+
);
115+
}
100116
};
101117

102118
const handleCloseWithWarning = (e: React.MouseEvent) => {
103119
e.stopPropagation();
104120

105121
const hasContent =
106-
contentField.value?.trim() || fromField.value?.trim() || uploadedImageUrl;
122+
contentField.value?.trim() || fromField.value?.trim() || previewUrl;
107123

108124
if (hasContent) {
109125
setIsWarningOpen(true);
110126
} else {
111127
onClose();
112128
}
113129
};
130+
114131
return (
115132
<>
116133
<Modal
@@ -174,7 +191,7 @@ const WriteModal = ({
174191
</span>
175192
</div>
176193

177-
{uploadedImageUrl ? (
194+
{previewUrl ? (
178195
<div className={styles.imagePreviewContainer}>
179196
<button
180197
type="button"
@@ -184,7 +201,7 @@ const WriteModal = ({
184201
<Close />
185202
</button>
186203
<Image
187-
src={uploadedImageUrl}
204+
src={previewUrl}
188205
alt="업로드된 이미지"
189206
width={80}
190207
height={80}
@@ -193,20 +210,21 @@ const WriteModal = ({
193210
</div>
194211
) : (
195212
<div className={styles.imageAddButtonContainer}>
196-
<button
197-
type="button"
198-
className={styles.imageAddButton}
199-
onClick={handleImageUpload}
200-
disabled={isUploading}
201-
aria-label="이미지 추가"
202-
>
213+
<label className={styles.imageAddButton}>
214+
<input
215+
type="file"
216+
accept="image/*"
217+
onChange={handleFileChange}
218+
disabled={isUploading}
219+
style={{ display: "none" }}
220+
/>
203221
<div className={styles.plusIconWrapper}>
204222
<Plus className={styles.plusIcon} />
205223
</div>
206224
<span className={styles.imageCaption}>
207225
{isUploading ? "업로드 중..." : "이미지 추가"}
208226
</span>
209-
</button>
227+
</label>
210228
</div>
211229
)}
212230
</div>
@@ -234,6 +252,7 @@ const WriteModal = ({
234252
</form>
235253
{isConfirmOpen && (
236254
<PopupConfirmLetter
255+
isLoading={isPending || isUploading}
237256
openDate={formatOpenDateString(capsuleData.result.openAt)}
238257
isOpen={isConfirmOpen}
239258
close={() => setIsConfirmOpen(false)}
@@ -260,9 +279,9 @@ const WriteModal = ({
260279
capsuleId: capsuleData.result.id.toString(),
261280
content: "",
262281
from: "",
263-
objectKey: "",
282+
objectKey: undefined,
264283
});
265-
if (uploadedImageUrl) {
284+
if (previewUrl) {
266285
removeImage();
267286
}
268287
onClose();

app/(sub)/capsule-detail/_components/write-modal/write-modal.css.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ export const senderTitle = style({
175175
marginLeft: "0.6rem",
176176
});
177177

178+
export const imageInput = style({
179+
display: "none",
180+
});
181+
178182
export const textareaContainer = style({
179183
position: "relative",
180184
height: "38.8rem",

shared/api/mutations/file.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,5 @@ export const useFileUpload = () => {
3030

3131
return objectKey;
3232
},
33-
onSuccess: (data) => {
34-
console.log("File uploaded successfully:", data);
35-
},
36-
onError: (error) => {
37-
console.error("File upload failed:", error);
38-
},
3933
});
4034
};

shared/types/api/letter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ export interface LetterListRes {
3232
export interface WriteLetterReq {
3333
capsuleId: string;
3434
content: string;
35-
objectKey: string;
35+
objectKey?: string;
3636
from: string;
3737
}

shared/ui/popup/popup-confirm-letter/index.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ import Lettie from "@/shared/assets/character/lettie_animate.png";
22
import Popup from "@/shared/ui/popup";
33

44
import Image from "next/image";
5+
import { PulseLoader } from "react-spinners";
56
import * as styles from "./popup-confirm-letter.css";
67

78
interface PopupConfirmLetterProps {
9+
isLoading: boolean;
810
openDate: string;
911
isOpen: boolean;
1012
close: () => void;
1113
onConfirm: () => void;
1214
}
1315

1416
const PopupConfirmLetter = ({
17+
isLoading,
1518
openDate,
1619
isOpen,
1720
close,
@@ -28,10 +31,18 @@ const PopupConfirmLetter = ({
2831
</Popup.Content>
2932
<Image src={Lettie} alt="apng" width={200} height={200} />
3033
<Popup.Actions>
31-
<Popup.Button onClick={close} className={styles.content}>계속 쓰기</Popup.Button>
32-
<Popup.Button className={styles.putButton} onClick={onConfirm}>
33-
편지 담기
34-
</Popup.Button>
34+
{isLoading ? (
35+
<PulseLoader color="white" size={10} />
36+
) : (
37+
<>
38+
<Popup.Button onClick={close} className={styles.content}>
39+
계속 쓰기
40+
</Popup.Button>
41+
<Popup.Button className={styles.putButton} onClick={onConfirm}>
42+
편지 담기
43+
</Popup.Button>
44+
</>
45+
)}
3546
</Popup.Actions>
3647
</Popup>
3748
);

0 commit comments

Comments
 (0)