동작 원리 및 코드 내용 검색해보기
return new Intl.NumberFormat('ko-KR').format(price);
{Object.entries(post.tag).map(([key, value]) => ( <li key={key}>{value}</li> const code = searchParams.get('code');
useEffect(() => { // 인증 상태 변경 감지 const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => { if (event === 'PASSWORD_RECOVERY') { setShowResetForm(true); } });
// 컴포넌트 언마운트 시 리스너 정리 return () => { if (authListener && typeof (authListener as any).unsubscribe === 'function') { (authListener as any).unsubscribe(); } }; }, [supabase]);
프로필 이미지 업로드 문제
테이블처럼 버켓에도 정책 설정하여 오류 해결
하지만 업로드 이미지가 이상하게 보이는 버그 발생
-> 이미지 이름이 유저 고유 id 값으로 고정되므로 이미지 삭제 후 재업로드하고 불러오는 방식으로 해결
'use client' ;
import axios from 'axios' ;
import { useEffect , useState } from 'react' ;
import { useRouter } from 'next/navigation' ;
import { createClient } from '@/utils/supabase/client' ;
import { API_MYPAGE_PROFILE } from '@/utils/apiConstants' ;
import Image from 'next/image' ;
type Profile = {
id : string ;
name : string ;
email : string ;
avatar : string ;
};
const ProfileForm = ({ userId } : { userId : string }) => {
const [ profile , setProfile ] = useState < Profile | null >( null );
const [ nickname , setNickname ] = useState ( '' );
const [ email , setEmail ] = useState ( '' );
const [ imageUrl , setImageUrl ] = useState ( '' );
const router = useRouter ();
const supabase = createClient ();
const handleSubmit = async ( event : React . FormEvent ) => {
event . preventDefault ();
if ( profile ) {
await axios . put ( API_MYPAGE_PROFILE ( userId ), { id : profile . id , name : nickname , email , avatar : imageUrl });
router . back ();
}
};
const handleImageChange = async ( event : React . ChangeEvent < HTMLInputElement >) => {
const file = event . target . files ?.[ 0 ];
if ( file ) {
const fileExt = file . name . split ( '.' ). pop ();
const fileName = ` ${ userId } . ${ fileExt } ` ;
const filePath = `profile_images/ ${ fileName } ` ;
// 기존 파일 삭제
const existingImagePath = profile ?. avatar ?. split ( '/' ). pop ();
if ( existingImagePath && existingImagePath !== fileName ) {
await supabase . storage . from ( 'users' ). remove ([ `profile_images/ ${ existingImagePath } ` ]);
}
// 새로운 파일 업로드
const { error : uploadError } = await supabase . storage . from ( 'users' ). upload ( filePath , file , {
upsert : true
});
if ( uploadError ) {
console . error ( 'Failed to upload image:' , uploadError . message );
return ;
}
const { data : publicUrlData } = supabase . storage . from ( 'users' ). getPublicUrl ( filePath );
if ( publicUrlData ) {
setImageUrl ( publicUrlData . publicUrl );
await axios . put ( API_MYPAGE_PROFILE ( userId ), { id : userId , avatar : publicUrlData . publicUrl });
// 프로필 데이터를 다시 가져와서 상태를 업데이트
const response = await axios . get ( API_MYPAGE_PROFILE ( userId ));
const profileData = response . data ;
setProfile ( profileData );
setNickname ( profileData . name );
setEmail ( profileData . email );
setImageUrl ( publicUrlData . publicUrl );
} else {
console . error ( 'Failed to get public URL' );
}
}
};
useEffect (() => {
const fetchProfile = async () => {
const response = await axios . get ( API_MYPAGE_PROFILE ( userId ));
const profileData = response . data ;
setProfile ( profileData );
setNickname ( profileData . name );
setEmail ( profileData . email );
setImageUrl ( profileData . avatar );
};
fetchProfile ();
}, [ userId ]);
return (
< div >
< h1 > Edit Profile </ h1 >
{ profile ? (
< form onSubmit = { handleSubmit } >
< div >
{ imageUrl && (
< Image
src = { ` ${ imageUrl } ? ${new Date (). getTime () } ` }
alt = "Profile"
width = { 70 }
height = { 70 }
className = "rounded-full"
/>
) }
< input type = "file" onChange = { handleImageChange } />
</ div >
< input
type = "text"
value = { nickname }
onChange = { ( e ) => setNickname ( e . target . value ) }
placeholder = "Nickname"
className = "text-black"
/>
< input
type = "email"
value = { email }
onChange = { ( e ) => setEmail ( e . target . value ) }
placeholder = "Email"
className = "text-black"
/>
< button type = "submit" > Update Profile </ button >
</ form >
) : (
< div > Loading... </ div >
) }
</ div >
);
};
export default ProfileForm ;
이외에도 내가 작성한 게시글 리스트 확인 가능 및 상세보기 가능
내 투어 클릭 시 해당 상세페이지로 이동
'use client' ;
import axios from 'axios' ;
import { useQuery } from '@tanstack/react-query' ;
import React , { useState } from 'react' ;
import { useParams } from 'next/navigation' ;
import { API_MYPAGE_POST } from '@/utils/apiConstants' ;
import Link from 'next/link' ;
type Post = {
id : string ;
created_at : string ;
user_id : string ;
title : string ;
image : string ;
updated_at ?: string ;
};
export default function PostList () {
const params = useParams ();
const userId = Array . isArray ( params . id ) ? params . id [ 0 ] : params . id ;
const getPostsData = async () => {
try {
const response = await axios . get ( API_MYPAGE_POST ( userId ));
// 필터링 로직을 클라이언트에서 처리
const data : Post [] = response . data ;
return data . filter (( post ) => post . user_id === userId );
} catch ( error ) {
if ( axios . isAxiosError ( error )) {
throw new Error ( `HTTP error! status: ${ error . response ?. status } ` );
} else {
throw new Error ( 'An unknown error occurred' );
}
}
};
const { data , isLoading , error } = useQuery < Post []>({
queryKey : [ 'post' , userId ],
queryFn : getPostsData ,
enabled : !! userId
});
if ( isLoading ) return < div className = "flex h-screen items-center justify-center" > Loading... </ div > ;
if ( error ) {
return < div className = "flex h-screen items-center justify-center" > Error: { error . message } </ div > ;
}
if ( ! data || data . length === 0 ) {
return < div className = "flex h-screen items-center justify-center" > No posts found </ div > ;
}
return (
< div className = "max-w-[360px]" >
{ data . map (( post ) => (
< div key = { post . id } >
< p > { new Date ( post . created_at ). toLocaleString () } </ p >
< Link href = { `/detail/ ${ post . id } ` } >
< div className = "flex" >
< img className = "mb-[20px] mr-2 h-[76px] w-[76px]" src = { post . image } alt = { post . title } />
< p className = "overflow-hidden text-ellipsis whitespace-nowrap text-[14px] font-bold" > { post . title } </ p >
</ div >
</ Link >
</ div >
)) }
</ div >
);
}
회의 내용
마이페이지 내 투어에서 예약자 확인 구현해야함 / 예약자 정보 ( 일단 이메일 ,닉네임 ) / 작성 날짜 내 예약 -> 결제 날짜
오후 개발자 튜터님
apiConstants.ts 정리 방식 괜찮은 듯 번역18next 그거 쓰려면 영어/한국어 처음부터 같이 작업해야함 -> 나중에 하면 큰일남 -> 언어 파일을 만들어서 json형식으로 가져다 쓰는 것임
next에서는 axios도 괜찮지만 fetch가 조금 더 선호된다 하지만 axios가 편리한 기능도 더 있기때문에 나을 수 있다 공식문서가 fetch라서 정보가 많을 수 있다
저녁 개발자 튜터님
가결제면 mvp 괜찮을듯 기본부터 하고 어려운거해야지 -> 이게 실수다 중요한 기능이라면 도전적인 기능이라도 먼저 구현을 해내고 기본적인 로직을 완성하는게 낫다 그래야 나중에 로직이 크게 안바뀜 타임라인이 자세히 없는건 조금 아쉽다 HTML기본적인 태그들을 기준으로 공통적으로 쓰일 수 있는 컴포넌트는 ( p , title , h1 , button 등등 ) 디테일한 부분은 props로 전달하면 되니 아예 똑같은 부분은 미리 공통 컴포넌트로 만들어서 쓰면 이후 디자인 반영 시 훨씬 수월하게 적용 가능 할 것이다
pr방식은 큰 도움이 될 것이라 좋지만 작업시간을 고려해야해서 중요도를 나눠서 중요한것 위주로만 짚고 넘어가는게 좋을 듯 커밋 외에도 pr 제목 및 내용도 신경 써야함
주에1~2번정도는 방문 하실 예정
저녁 디자이너 멘토님
검색 시 지역명을 알고 검색 할 수 있는 외국인은 정말 드믈 것 같음 도 , 시 등 여러 지역들을 보여 줄 수 있는 리스트를 만드는게 필요 함 만약에 지역을 세분화 해서 선택지를 줄 수 있도록 긁어올 수 있는 정보를 찾아보고 없다면 타겟층을 바꿔야 할 듯 투어 리스트들도 촬영 명소나 전통음식 등 관심있는 외국인을 타겟으로 바꾸는 차선도 있을 듯 visitKOREA 같은 사이트를 알아보면 좋을듯 / where to eat Korea 등은 정보 구할 때 쓰일 검색어 오히려 영어로 검색해야 더 많이 나옴
검색 시 tag를 예시로 보여주는건 어떨지