From 250213bf459b2390d0f66e873adb1a04e06af7dc Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 14:35:30 +0900 Subject: [PATCH 01/14] =?UTF-8?q?#160=20[DEL]=20console=20log=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/queries/useRecruitingPosts.ts | 1 - src/libs/api/recruitingPosts.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/hooks/queries/useRecruitingPosts.ts b/src/hooks/queries/useRecruitingPosts.ts index 26d74640..963244b0 100644 --- a/src/hooks/queries/useRecruitingPosts.ts +++ b/src/hooks/queries/useRecruitingPosts.ts @@ -22,7 +22,6 @@ export const useRecruitingPosts = (filters: Filters, pageNumber: number, pageSiz size: pageSize, sort: [''], }); - console.log(result); return result; }, }); diff --git a/src/libs/api/recruitingPosts.ts b/src/libs/api/recruitingPosts.ts index 4a4a1e50..8c221a63 100644 --- a/src/libs/api/recruitingPosts.ts +++ b/src/libs/api/recruitingPosts.ts @@ -12,7 +12,6 @@ interface GetRecruitingPostsParams { } export const getRecruitingPosts = async (params: GetRecruitingPostsParams) => { - console.log(params); const response = await axios.get<{ result: PagedProjects }>( `${process.env.NEXT_PUBLIC_API_BASE_URL}/recruiting-posts`, { From 045102b1ea3b8b74f5d07c305cc0a238b480c007 Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 16:09:50 +0900 Subject: [PATCH 02/14] =?UTF-8?q?#160=20[FEAT]=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/project.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/project.ts b/src/types/project.ts index 713a5f1c..5f33ade8 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -16,6 +16,7 @@ export type Project = { content: string; title: string; recruitingPositions: RecruitingPosition[]; + imageKeys?: string[]; }; export type CreateProject = Project & { @@ -32,6 +33,7 @@ export type ResponseProject = Project & { alreadyApplied: boolean; writer: boolean; profileImageUrl: string; + imageUrls: string[]; }; export type DeleteProject = { From da05e8a44ccf904e5f5ae18351656cffae089969 Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 16:10:39 +0900 Subject: [PATCH 03/14] =?UTF-8?q?#160=20[CHORE]=20=ED=83=9C=EB=B8=94?= =?UTF-8?q?=EB=A6=BF=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=202=EC=97=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/project/_components/ProjectList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(main)/project/_components/ProjectList.tsx b/src/app/(main)/project/_components/ProjectList.tsx index e4885de3..a490d0a8 100644 --- a/src/app/(main)/project/_components/ProjectList.tsx +++ b/src/app/(main)/project/_components/ProjectList.tsx @@ -107,7 +107,7 @@ const ProjectListPage = () => {
-
+
{tabletCards.length > 0 ? tabletCards.map((card: ResponseProject) => ( From dd102ad3e6a02fd7c27330a0c22bc0ed01483af3 Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 16:12:25 +0900 Subject: [PATCH 04/14] =?UTF-8?q?#160=20[FEAT]=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/libs/api/image.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/libs/api/image.ts b/src/libs/api/image.ts index abf97c3e..b3897603 100644 --- a/src/libs/api/image.ts +++ b/src/libs/api/image.ts @@ -50,3 +50,41 @@ export const uploadImage = async (file: File): Promise => { return null; } }; + +// 게시글 이미지용 presigned URL 여러 장 한번에 발급 +export async function getPostImagePresignedUrls( + fileNames: string[], +): Promise { + try { + const params = new URLSearchParams(); + fileNames.forEach((name) => params.append('fileNames', name)); + + const { data } = await api.post>( + `/post-images/presigned-url?${params.toString()}`, + ); + + if (!data.isSuccess) { + throw new Error(data.message || 'Failed to get post image presigned URLs'); + } + + return data.result; + } catch (error) { + console.error('getPostImagePresignedUrls Error:', error); + throw error; + } +} + +// 게시글 이미지 여러 장 S3 업로드 → objectKey[] 반환 +export async function uploadPostImages(files: File[]): Promise { + const presignedList = await getPostImagePresignedUrls(files.map((f) => f.name)); + + const results = await Promise.all( + presignedList.map(async ({ preSignedUrl, objectKey }, i) => { + const success = await uploadToS3(preSignedUrl, files[i]); + if (!success) throw new Error(`S3 upload failed: ${files[i].name}`); + return objectKey; + }), + ); + + return results; +} From 80581db928900fbb6c0f1eb046f5b769546a809b Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 16:15:07 +0900 Subject: [PATCH 05/14] =?UTF-8?q?#160=20[FEAT]=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=EC=97=AD=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/recruit/editor/PostImage.tsx | 200 ++++++++++++++++++ src/components/recruit/editor/TextContent.tsx | 2 + 2 files changed, 202 insertions(+) create mode 100644 src/components/recruit/editor/PostImage.tsx diff --git a/src/components/recruit/editor/PostImage.tsx b/src/components/recruit/editor/PostImage.tsx new file mode 100644 index 00000000..26219efd --- /dev/null +++ b/src/components/recruit/editor/PostImage.tsx @@ -0,0 +1,200 @@ +'use client'; + +import { useRef, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import Image from 'next/image'; +import { RecruitFormType } from '@/libs/schemas/projectSchema'; +import { uploadPostImages } from '@/libs/api/image'; + +const MAX_IMAGE_COUNT = 2; + +type ImageItem = { + objectKey: string; + previewUrl: string; +}; + +export default function PostImage() { + const { setValue } = useFormContext(); + const [images, setImages] = useState([]); + const [isUploading, setIsUploading] = useState(false); + const fileInputRef = useRef(null); + + const syncImageKeys = (updated: ImageItem[]) => { + setValue( + 'imageKeys', + updated.map((img) => img.objectKey), + { shouldDirty: true }, + ); + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + const files = Array.from(e.target.files ?? []); + if (!files.length) return; + + const remaining = MAX_IMAGE_COUNT - images.length; + + if (files.length > remaining) { + alert(`이미지는 최대 ${MAX_IMAGE_COUNT}장까지 첨부할 수 있어요.`); + } + + const filesToUpload = files.slice(0, remaining); + if (!filesToUpload.length) return; + + setIsUploading(true); + try { + const objectKeys = await uploadPostImages(filesToUpload); + const newItems: ImageItem[] = filesToUpload.map((file, i) => ({ + objectKey: objectKeys[i], + previewUrl: URL.createObjectURL(file), + })); + + const updated = [...images, ...newItems]; + setImages(updated); + syncImageKeys(updated); + } catch (err) { + console.error('이미지 업로드 실패:', err); + alert('이미지 업로드에 실패했어요. 다시 시도해주세요.'); + } finally { + setIsUploading(false); + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }; + + const handleDeleteImage = (index: number) => { + const confirmed = window.confirm('이미지를 삭제할까요?'); + if (!confirmed) return; + + URL.revokeObjectURL(images[index].previewUrl); + + const updated = images.filter((_, i) => i !== index); + setImages(updated); + syncImageKeys(updated); + }; + + const canAddMore = images.length < MAX_IMAGE_COUNT; + + return ( +
+ {/* 이미지 없을 때: 카메라 버튼 */} + {images.length === 0 && ( + + )} + + {/* 이미지 미리보기 */} + {images.length > 0 && ( +
+ {images.map((img, index) => ( +
+ {`첨부 + +
+ ))} + + {/* 이미지 1장일 때 추가 버튼 */} + {canAddMore && ( + + )} +
+ )} + + +
+ ); +} + +function CameraIcon() { + return ( + + + + + ); +} + +function CloseIcon() { + return ( + + + + + ); +} + +function PlusIcon() { + return ( + + + + + ); +} diff --git a/src/components/recruit/editor/TextContent.tsx b/src/components/recruit/editor/TextContent.tsx index 4d3494db..fd3fb337 100644 --- a/src/components/recruit/editor/TextContent.tsx +++ b/src/components/recruit/editor/TextContent.tsx @@ -9,6 +9,7 @@ import { Control, useController } from 'react-hook-form'; import { RecruitFormType } from '@/libs/schemas/projectSchema'; import { TableBubbleMenu } from './table/TableBubbleMenu'; import { TableHandles } from './table/TableHandles'; +import PostImage from './PostImage'; type Props = { control: Control; @@ -87,6 +88,7 @@ const TextContent = ({ control, name = 'content' }: Props) => {

{textLength}

+
From 9f68d4b3872e762201a3d8dc51df4b28231f43b7 Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 16:16:10 +0900 Subject: [PATCH 06/14] =?UTF-8?q?#160=20[FEAT]=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=9E=91=EC=84=B1=20=ED=8F=BC=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/recruit/RecruitForm.tsx | 90 ++++++++++++------------ src/components/recruit/useRecruitForm.ts | 10 ++- src/libs/schemas/projectSchema.ts | 1 + 3 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/components/recruit/RecruitForm.tsx b/src/components/recruit/RecruitForm.tsx index d42e9df5..62ea9f56 100644 --- a/src/components/recruit/RecruitForm.tsx +++ b/src/components/recruit/RecruitForm.tsx @@ -13,6 +13,7 @@ import Button from '@/components/common/Button'; import { Project } from '@/types/project'; import { useRecruitForm } from './useRecruitForm'; import { RecruitFormType } from '@/libs/schemas/projectSchema'; +import { FormProvider } from 'react-hook-form'; type RecruitFormProps = { mode?: 'create' | 'edit'; @@ -31,15 +32,7 @@ const RecruitForm = ({ onNext, initialFormData, }: RecruitFormProps) => { - const { - handleSubmit, - control, - watch, - reset, - formState: { isValid, errors }, - onSubmit, - onError, - } = useRecruitForm({ + const { formMethods, onSubmit, onError } = useRecruitForm({ mode, initialData, postId, @@ -47,6 +40,13 @@ const RecruitForm = ({ onNext, initialFormData, }); + const { + handleSubmit, + control, + watch, + reset, + formState: { isValid, errors }, + } = formMethods; useEffect(() => { if (initialFormData && mode === 'create' && !showProfileList) { @@ -70,43 +70,45 @@ const RecruitForm = ({ }; return ( -
-
- -
- {/* 모집분야/인원 */} - - {/* 진행방법 */} - - {/* 프로젝트 기간 및 연락처 */} -
- - - - + + +
+ +
+ {/* 모집분야/인원 */} + + {/* 진행방법 */} + + {/* 프로젝트 기간 및 연락처 */} +
+ + + + +
+ + {mode === 'create' && showProfileList && } +
+
+
- - {mode === 'create' && showProfileList && } -
-
-
- + + ); }; diff --git a/src/components/recruit/useRecruitForm.ts b/src/components/recruit/useRecruitForm.ts index fb140b1e..723aea5e 100644 --- a/src/components/recruit/useRecruitForm.ts +++ b/src/components/recruit/useRecruitForm.ts @@ -52,7 +52,7 @@ export const useRecruitForm = ({ const formMethods = useForm({ mode: 'onChange', reValidateMode: 'onChange', - resolver: zodResolver(getSchema()), + resolver: zodResolver(getSchema()) as any, defaultValues: mode === 'edit' && initialData ? { @@ -64,6 +64,8 @@ export const useRecruitForm = ({ deadline: convertDateFormat(initialData.deadline), contactWay: initialData.contactWay, content: initialData.content, + // TODO: 서버 확인 및 objectKey 전달 방식 확인 + imageKeys: [], } : initialFormData || { title: '', @@ -75,6 +77,7 @@ export const useRecruitForm = ({ contactWay: '', content: '', profileId: undefined, + imageKeys: [], // create 모드 초기값 }, }); @@ -91,6 +94,8 @@ export const useRecruitForm = ({ contactWay: formData.contactWay, content: formData.content, status: initialData?.status || 'OPEN', + // TODO: 서버 확인 후 imageKeys 전달 방식 결정 + imageKeys: formData.imageKeys ?? [], // 폼에서 관리된 imageKeys 전달 }; updateProject( @@ -112,6 +117,7 @@ export const useRecruitForm = ({ ...formData, profileId: formData.profileId, status: 'OPEN', + imageKeys: formData.imageKeys ?? [], }; createProject(projectData, { @@ -146,7 +152,7 @@ export const useRecruitForm = ({ }; return { - ...formMethods, + formMethods, onSubmit, onError, }; diff --git a/src/libs/schemas/projectSchema.ts b/src/libs/schemas/projectSchema.ts index bf834a48..74303ecd 100644 --- a/src/libs/schemas/projectSchema.ts +++ b/src/libs/schemas/projectSchema.ts @@ -49,6 +49,7 @@ export const recruitFormSchema = z.object({ { message: '최소 50자 이상 작성해주세요.' }, ), profileId: z.number().min(1).optional(), + imageKeys: z.array(z.string()).default([]), }); export type RecruitFormType = z.infer; From 891d42f22419a03347173f9fb0c023baec51ac3d Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 16:25:06 +0900 Subject: [PATCH 07/14] =?UTF-8?q?#160=20[CHORE]=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/project.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/project.ts b/src/types/project.ts index 5f33ade8..4c3923a7 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -33,7 +33,12 @@ export type ResponseProject = Project & { alreadyApplied: boolean; writer: boolean; profileImageUrl: string; - imageUrls: string[]; + images: [ + { + imageUrl: 'string'; + objectKey: 'string'; + }, + ]; }; export type DeleteProject = { From 8b99361c8054b432f122860b0447193ae73b8530 Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 16:25:36 +0900 Subject: [PATCH 08/14] =?UTF-8?q?#160=20[FEAT]=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=97=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/[id]/_components/ProjectImage.tsx | 21 +++++++++++++++++++ .../project/[id]/_components/ProjectInfo.tsx | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 src/app/(main)/project/[id]/_components/ProjectImage.tsx diff --git a/src/app/(main)/project/[id]/_components/ProjectImage.tsx b/src/app/(main)/project/[id]/_components/ProjectImage.tsx new file mode 100644 index 00000000..67c2697a --- /dev/null +++ b/src/app/(main)/project/[id]/_components/ProjectImage.tsx @@ -0,0 +1,21 @@ +import { ResponseProject } from '@/types/project'; +import Image from 'next/image'; + +const ProjectImage = ({ images }: ResponseProject) => { + return ( +
+ {images.map((img, index) => ( + {`Project + ))} +
+ ); +}; + +export default ProjectImage; diff --git a/src/app/(main)/project/[id]/_components/ProjectInfo.tsx b/src/app/(main)/project/[id]/_components/ProjectInfo.tsx index e11e10dd..7cf55a2b 100644 --- a/src/app/(main)/project/[id]/_components/ProjectInfo.tsx +++ b/src/app/(main)/project/[id]/_components/ProjectInfo.tsx @@ -5,6 +5,7 @@ import ProjectTitle from './ProjectTitle'; import InfoCard from './InfoCard'; import Profile1 from '@/components/profile/Profile1'; import { useGetProject } from '@/hooks/queries/useProject'; +import ProjectImage from './ProjectImage'; const ProjectInfo = ({ id }: { id: string }) => { const { data } = useGetProject( @@ -29,6 +30,7 @@ const ProjectInfo = ({ id }: { id: string }) => {
+
From 64f1c373b9eb0177b8413f8caf50b5020d0304c9 Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Mon, 9 Mar 2026 16:58:59 +0900 Subject: [PATCH 09/14] =?UTF-8?q?#160=20[CHORE]=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/project.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types/project.ts b/src/types/project.ts index 4c3923a7..b698e07c 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -6,6 +6,11 @@ export type RecruitingPosition = { count: number; }; +export type ProjectImage = { + imageUrl: string; + objectKey: string; +}; + export type Project = { progressWay: ProgressWayType; contactWay: string; @@ -17,6 +22,7 @@ export type Project = { title: string; recruitingPositions: RecruitingPosition[]; imageKeys?: string[]; + images?: ProjectImage[]; }; export type CreateProject = Project & { @@ -33,12 +39,6 @@ export type ResponseProject = Project & { alreadyApplied: boolean; writer: boolean; profileImageUrl: string; - images: [ - { - imageUrl: 'string'; - objectKey: 'string'; - }, - ]; }; export type DeleteProject = { From 00537bf2f83712a435dd990709f7bd02b3830268 Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Tue, 24 Mar 2026 04:01:55 +0900 Subject: [PATCH 10/14] =?UTF-8?q?#160=20[CHORE]=20=EB=8D=B0=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=ED=86=B1=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(main)/teampsylog/_components/BottomComment.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/app/(main)/teampsylog/_components/BottomComment.tsx b/src/app/(main)/teampsylog/_components/BottomComment.tsx index a8919071..30c05628 100644 --- a/src/app/(main)/teampsylog/_components/BottomComment.tsx +++ b/src/app/(main)/teampsylog/_components/BottomComment.tsx @@ -27,23 +27,19 @@ const BottomComment = ({ isOpen, onClose, children }: BottomSheetProps) => { return ( <>
{/* 바텀 시트 */}
{ ? 'translateY(0)' : 'translateY(100%)', transition: isDragging ? 'none' : 'transform 0.3s ease-out', - visibility: shouldShow ? 'visible' : 'hidden', pointerEvents: shouldShow ? 'auto' : 'none', + overscrollBehavior: 'contain', }} >
Date: Sun, 29 Mar 2026 12:47:23 +0900 Subject: [PATCH 11/14] =?UTF-8?q?#160=20[FEAT]=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/[id]/_components/ProjectImage.tsx | 2 +- src/components/recruit/RecruitForm.tsx | 2 +- src/components/recruit/editor/PostImage.tsx | 16 +++++++++++++--- src/components/recruit/editor/TextContent.tsx | 6 ++++-- src/components/recruit/useRecruitForm.ts | 2 +- src/libs/api/image.ts | 1 + 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/app/(main)/project/[id]/_components/ProjectImage.tsx b/src/app/(main)/project/[id]/_components/ProjectImage.tsx index 67c2697a..ed7beb12 100644 --- a/src/app/(main)/project/[id]/_components/ProjectImage.tsx +++ b/src/app/(main)/project/[id]/_components/ProjectImage.tsx @@ -4,7 +4,7 @@ import Image from 'next/image'; const ProjectImage = ({ images }: ResponseProject) => { return (
- {images.map((img, index) => ( + {images?.map((img, index) => (
- + {mode === 'create' && showProfileList && }
diff --git a/src/components/recruit/editor/PostImage.tsx b/src/components/recruit/editor/PostImage.tsx index 26219efd..d748087f 100644 --- a/src/components/recruit/editor/PostImage.tsx +++ b/src/components/recruit/editor/PostImage.tsx @@ -5,6 +5,7 @@ import { useFormContext } from 'react-hook-form'; import Image from 'next/image'; import { RecruitFormType } from '@/libs/schemas/projectSchema'; import { uploadPostImages } from '@/libs/api/image'; +import { ProjectImage } from '@/types/project'; const MAX_IMAGE_COUNT = 2; @@ -13,9 +14,18 @@ type ImageItem = { previewUrl: string; }; -export default function PostImage() { +type PostImageProps = { + initialImages?: ProjectImage[]; +}; + +export default function PostImage({ initialImages }: PostImageProps) { const { setValue } = useFormContext(); - const [images, setImages] = useState([]); + const [images, setImages] = useState(() => + (initialImages ?? []).map((img) => ({ + objectKey: img.objectKey, + previewUrl: img.imageUrl, // 서버 URL을 previewUrl로 + })), + ); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); @@ -81,7 +91,7 @@ export default function PostImage() { type="button" onClick={() => fileInputRef.current?.click()} disabled={isUploading} - className="flex w-fit items-center gap-1.5 rounded-lg border border-gray-400 px-3 py-2 text-sm text-gray-500 transition-colors hover:bg-gray-50 disabled:opacity-50" + className="flex w-fit cursor-pointer items-center gap-1.5 rounded-lg border border-gray-400 px-3 py-2 text-sm text-gray-500 transition-colors hover:bg-gray-50 disabled:opacity-50" > {isUploading ? '업로드 중...' : '사진'} diff --git a/src/components/recruit/editor/TextContent.tsx b/src/components/recruit/editor/TextContent.tsx index fd3fb337..0ccab893 100644 --- a/src/components/recruit/editor/TextContent.tsx +++ b/src/components/recruit/editor/TextContent.tsx @@ -10,13 +10,15 @@ import { RecruitFormType } from '@/libs/schemas/projectSchema'; import { TableBubbleMenu } from './table/TableBubbleMenu'; import { TableHandles } from './table/TableHandles'; import PostImage from './PostImage'; +import { ProjectImage } from '@/types/project'; type Props = { control: Control; name?: 'content'; + initialImages?: ProjectImage[]; }; -const TextContent = ({ control, name = 'content' }: Props) => { +const TextContent = ({ control, name = 'content', initialImages }: Props) => { const { field } = useController({ name, control }); const value = field.value ?? ''; const onChange = field.onChange as (html: string) => void; @@ -88,7 +90,7 @@ const TextContent = ({ control, name = 'content' }: Props) => {

{textLength}

- +
diff --git a/src/components/recruit/useRecruitForm.ts b/src/components/recruit/useRecruitForm.ts index 723aea5e..73e1bf98 100644 --- a/src/components/recruit/useRecruitForm.ts +++ b/src/components/recruit/useRecruitForm.ts @@ -65,7 +65,7 @@ export const useRecruitForm = ({ contactWay: initialData.contactWay, content: initialData.content, // TODO: 서버 확인 및 objectKey 전달 방식 확인 - imageKeys: [], + imageKeys: initialData?.images?.map((img) => img.objectKey) || [], } : initialFormData || { title: '', diff --git a/src/libs/api/image.ts b/src/libs/api/image.ts index b3897603..a7092e9e 100644 --- a/src/libs/api/image.ts +++ b/src/libs/api/image.ts @@ -8,6 +8,7 @@ export const uploadToS3 = async (presignedUrl: string, file: File): Promise Date: Sun, 29 Mar 2026 12:55:19 +0900 Subject: [PATCH 12/14] =?UTF-8?q?#160=20[CHORE]=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B0=B0=EC=97=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/project/[id]/_components/ProjectImage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/(main)/project/[id]/_components/ProjectImage.tsx b/src/app/(main)/project/[id]/_components/ProjectImage.tsx index ed7beb12..e569778e 100644 --- a/src/app/(main)/project/[id]/_components/ProjectImage.tsx +++ b/src/app/(main)/project/[id]/_components/ProjectImage.tsx @@ -2,9 +2,10 @@ import { ResponseProject } from '@/types/project'; import Image from 'next/image'; const ProjectImage = ({ images }: ResponseProject) => { + if (!images || images.length === 0) return null; return ( -
- {images?.map((img, index) => ( +
+ {images.map((img, index) => ( Date: Sun, 29 Mar 2026 12:55:42 +0900 Subject: [PATCH 13/14] =?UTF-8?q?#160=20[FEAT]=20=ED=8A=9C=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=96=BC=20'=EB=8B=A4=EC=8B=9C=EB=B3=B4=EC=A7=80?= =?UTF-8?q?=EC=95=8A=EA=B8=B0'=20=EC=9E=84=EC=8B=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/teampsylog/_components/KeywordGuideOverlay.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(main)/teampsylog/_components/KeywordGuideOverlay.tsx b/src/app/(main)/teampsylog/_components/KeywordGuideOverlay.tsx index 22a5970c..848b34a8 100644 --- a/src/app/(main)/teampsylog/_components/KeywordGuideOverlay.tsx +++ b/src/app/(main)/teampsylog/_components/KeywordGuideOverlay.tsx @@ -63,7 +63,7 @@ const KeywordGuideOverlay = ({ onClose }: Props) => { - */}
{/* mobile */} From e9b58140244a344ce8c51bad8b4ac162b09c2acc Mon Sep 17 00:00:00 2001 From: sunhwaaRj Date: Sun, 29 Mar 2026 13:37:40 +0900 Subject: [PATCH 14/14] =?UTF-8?q?#160=20[CHORE]=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(main)/project/_components/ProjectList.tsx | 4 +++- src/components/recruit/useRecruitForm.ts | 3 +-- src/libs/api/image.ts | 16 +++++++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/app/(main)/project/_components/ProjectList.tsx b/src/app/(main)/project/_components/ProjectList.tsx index a490d0a8..a7e62ea6 100644 --- a/src/app/(main)/project/_components/ProjectList.tsx +++ b/src/app/(main)/project/_components/ProjectList.tsx @@ -128,7 +128,9 @@ const ProjectListPage = () => { /> )) : !isLoading && ( -

프로젝트가 없습니다.

+

+ 프로젝트가 없습니다. +

)}
diff --git a/src/components/recruit/useRecruitForm.ts b/src/components/recruit/useRecruitForm.ts index 73e1bf98..06248013 100644 --- a/src/components/recruit/useRecruitForm.ts +++ b/src/components/recruit/useRecruitForm.ts @@ -94,8 +94,7 @@ export const useRecruitForm = ({ contactWay: formData.contactWay, content: formData.content, status: initialData?.status || 'OPEN', - // TODO: 서버 확인 후 imageKeys 전달 방식 결정 - imageKeys: formData.imageKeys ?? [], // 폼에서 관리된 imageKeys 전달 + imageKeys: initialData?.images?.map((img) => img.objectKey) ?? initialData?.imageKeys ?? [], }; updateProject( diff --git a/src/libs/api/image.ts b/src/libs/api/image.ts index a7092e9e..779d7113 100644 --- a/src/libs/api/image.ts +++ b/src/libs/api/image.ts @@ -8,7 +8,6 @@ export const uploadToS3 = async (presignedUrl: string, file: File): Promise { const presignedList = await getPostImagePresignedUrls(files.map((f) => f.name)); + if (presignedList.length !== files.length) { + throw new Error( + `Presigned URL count mismatch: expected ${files.length}, got ${presignedList.length}`, + ); + } + const results = await Promise.all( - presignedList.map(async ({ preSignedUrl, objectKey }, i) => { - const success = await uploadToS3(preSignedUrl, files[i]); - if (!success) throw new Error(`S3 upload failed: ${files[i].name}`); - return objectKey; + files.map(async (file, i) => { + const target = presignedList[i]; + const success = await uploadToS3(target.preSignedUrl, file); + if (!success) throw new Error(`S3 upload failed: ${file.name}`); + return target.objectKey; }), );