diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 2f0176e..5f0d6f5 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -11,15 +11,15 @@ export default function LoginPage() { if(localStorage.getItem('googleLoginToken')) { localStorage.removeItem('googleLoginToken'); } + window.location.href = googleAuthUrl; }; return (
- - -
+
{/* 로고 */}
MUKPIC diff --git a/app/(auth)/login/withGoogle/success/page.tsx b/app/(auth)/login/withGoogle/success/page.tsx index 70e4b88..e898592 100644 --- a/app/(auth)/login/withGoogle/success/page.tsx +++ b/app/(auth)/login/withGoogle/success/page.tsx @@ -4,6 +4,7 @@ import { useEffect } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import axios from "axios"; import { Suspense } from "react"; +import { addUserKey, createAuthCookie } from "@/app/components/auth/authFunctions"; function GoogleLoginContent() { const searchParams = useSearchParams(); @@ -32,19 +33,25 @@ function GoogleLoginContent() { if (accessToken) { // 로컬 스토리지에 토큰 저장 localStorage.setItem("Authorization", accessToken); - console.log("Access token stored successfully:", accessToken); + + // 미들웨어를 위한 쿠키 설정 + createAuthCookie(accessToken); + + //userKey 저장 + const userKey = response.data.userKey; + addUserKey(userKey); // 메인 페이지로 리디렉션 router.push("/"); } else { console.error("Authorization token is missing in the response headers."); - alert("로그인 토큰을 가져오지 못했습니다. 다시 시도해주세요."); + alert("login error please try again"); router.push("/login"); } } } catch (error) { console.error("Error fetching tokens:", error); - alert("로그인 중 오류가 발생했습니다. 다시 시도해주세요."); + alert("login error please try again"); router.push("/login"); } }; diff --git a/app/(auth)/signup/google/page.tsx b/app/(auth)/signup/google/page.tsx index b2480ca..5109135 100644 --- a/app/(auth)/signup/google/page.tsx +++ b/app/(auth)/signup/google/page.tsx @@ -3,10 +3,12 @@ import { useEffect, Suspense } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import axios from "axios"; +import { useSignupStore } from "@/app/types/signupStore"; function SignUpContent() { const searchParams = useSearchParams(); const router = useRouter(); + const setEmail = useSignupStore((state) => state.setemail); useEffect(() => { const email = searchParams.get("email"); @@ -25,6 +27,7 @@ function SignUpContent() { ); if (response.status === 200) { + setEmail(email); const accessToken = response.headers["authorization"]; if (accessToken) { @@ -47,7 +50,7 @@ function SignUpContent() { }; fetchTokens(); - }, [searchParams, router]); + }, [searchParams, router, setEmail]); return (
diff --git a/app/(auth)/signup/layout.tsx b/app/(auth)/signup/layout.tsx index ddfb3bd..fd5efdc 100644 --- a/app/(auth)/signup/layout.tsx +++ b/app/(auth)/signup/layout.tsx @@ -1,5 +1,6 @@ 'use client'; import "@/app/globals.css"; +import '@/app/(css)/auth.css'; import { ReactNode, useEffect } from "react"; import { SvgButtonForNav } from "@/app/components/button"; import TopNav from "@/app/components/TopNav"; @@ -27,7 +28,7 @@ export default function SignupLayout({ children }: LayoutProps) { leftButton={ { router.back(); }}> - + @@ -41,7 +42,7 @@ export default function SignupLayout({ children }: LayoutProps) { onClick={() => { router.push('/login');}} > - + @@ -52,7 +53,7 @@ export default function SignupLayout({ children }: LayoutProps) { } /> -
+
{children}
diff --git a/app/(css)/auth.css b/app/(css)/auth.css index c19522e..19a708b 100644 --- a/app/(css)/auth.css +++ b/app/(css)/auth.css @@ -11,6 +11,7 @@ input { line-height: 1.375rem; /* 137.5% */ letter-spacing: -0.01875rem; background: #f1f3f6; + width:100%; } input:-webkit-autofill { @@ -75,7 +76,7 @@ input:focus::placeholder { height: 3.375rem; flex-shrink: 0; border-radius: 6.25rem; - padding: 1.25rem; + padding: 0.75rem 1.25rem; background: #f1f3f6; color: #0b0b0b; font-feature-settings: "liga" off, "clig" off; @@ -143,7 +144,7 @@ input.auth-placeholder::placeholder { .dropdown-item { display: flex; align-items: center; /* 세로 방향 가운데 정렬 */ - justify-content: flex-start; /* 가로 방향 왼쪽 정렬 */ + justify-content: space-between; /* 가로 방향 왼쪽 정렬 */ padding-left: 1.53rem; width: 100%; height: 3.125rem; @@ -184,7 +185,7 @@ input.auth-placeholder::placeholder { } .dropdown-badge-green { background: #cff7ca; - padding-right: 0.5rem; + padding-right: 1rem; } .dropdown-badge-red { background: #ffc4b3; @@ -280,12 +281,11 @@ input.auth-placeholder::placeholder { } .validate-error-text { - padding-left:1.25rem; + padding-left: 1.25rem; color: #fa0c38; - font-size: 0.75rem; - font-weight: 500; - line-height: 0.875rem; /* 116.667% */ - letter-spacing: -0.01875rem; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; text-align: left; } @@ -294,6 +294,7 @@ input.auth-placeholder::placeholder { font-size: 0.875rem; font-style: normal; font-weight: 400; + text-align: right; } input[type="checkbox"] { @@ -333,7 +334,7 @@ input[type="checkbox"]:active { } .dropdown-checkbox { - margin-right: 1.5rem !important; + padding-right:1rem; } .camera-circle { @@ -342,7 +343,7 @@ input[type="checkbox"]:active { flex-shrink: 0; position: absolute; right: 0rem; - bottom:0.25rem; + bottom: 0.25rem; overflow: visible; } .camera-icon { @@ -350,5 +351,33 @@ input[type="checkbox"]:active { height: 1rem; flex-shrink: 0; overflow: visible; - transform: translate(-50%, -50%); ; + transform: translate(-50%, -50%); +} + +.label-text { + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: 1.375rem; /* 137.5% */ + letter-spacing: -0.01875rem; +} + +.terms-h2 { + font-size: 1.25rem; + font-weight: 700; + font-style: initial; +} +.terms-wrapper { + color: black; + text-align: left; + padding-top: 1rem; + padding-bottom: 1rem; +} +.privacy-policy { + max-height: 0; + overflow-y: auto; + transition: height 0.5s ease-in-out; +} +.privacy-policy.open { + max-height: 25rem; } diff --git a/app/(css)/community.css b/app/(css)/community.css index 1a9aa09..6b283bd 100644 --- a/app/(css)/community.css +++ b/app/(css)/community.css @@ -33,8 +33,8 @@ height: 100%; background-color: #f1f3f6; padding: 0; - margin-top: 0.1rem; - margin-bottom: 0.1rem; + /* margin-top: 0.1rem; */ + margin-bottom: 0.2rem; gap: 0.5rem; } .post-component-wrapper-detail { @@ -47,7 +47,7 @@ flex-direction: column; height: 100%; background-color: white; - padding : 0.75rem 3% 1.12rem 3%; + padding: 0.75rem 3% 1.12rem 3%; } .post-profile-wrapper { @@ -69,10 +69,11 @@ .post-img-wrapper { width: 100%; - height: auto; + height: 30rem; position: relative; flex-shrink: 0; align-self: stretch; + margin-bottom : 1rem; } img { @@ -95,6 +96,22 @@ img { background: #0a0c10; box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1); } +.view-ai-research-button-carousel { + display: flex; + width: 9.3125rem; + height: 2.25rem; + padding: 0.625rem 0.5rem 0.625rem 0.75rem; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5rem; + position: absolute; + left: 0.75rem; + bottom: 0.75rem; + border-radius: 6.1875rem; + background: #0a0c10; + box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1); +} .view-ai-research-button-text { display: inline-flex; @@ -182,10 +199,16 @@ img { justify-content: center; align-items: flex-start; } +.post-contents-wrapper-row-detail { + display: flex; + width: 100%; + justify-content: center; + align-items: flex-start; +} .post-contents-left { display: flex; - width: 18.875rem; + width: 90%; height: 4.9375rem; flex-direction: column; align-items: flex-start; @@ -194,7 +217,7 @@ img { } .post-contents-right { display: flex; - width: 2.25rem; + width: 10%; height: 4.9375rem; flex-direction: column; align-items: center; @@ -289,9 +312,10 @@ img { } .food-category-dropdown-list { - max-width: 6.375rem; - width: 100%; - max-height: 15.625rem; + position: absolute; + max-width: 10rem; + z-index: 25; + max-height: 17rem; overflow-y: auto; border-radius: 1rem 1rem 1rem 1rem; border-top: 0.5px solid #000; @@ -301,10 +325,13 @@ img { } .food-category-dropdown-item { + overflow-y: auto; + z-index: 25; display: flex; align-items: center; /* 세로 방향 가운데 정렬 */ justify-content: flex-start; /* 가로 방향 왼쪽 정렬 */ - padding-left: 1.53rem; + padding-left: 1rem; + padding-right: 1rem; width: 100%; height: 2.25rem; border-bottom: 0.5px solid #000; @@ -430,4 +457,55 @@ textarea { line-height: 1.375rem; /* 137.5% */ letter-spacing: -0.01875rem; min-height: 9.625rem; + white-space: normal; /* 텍스트 줄바꿈 활성화 */ + word-wrap: break-word; /* 단어가 길 경우 줄바꿈 */ + overflow-wrap: break-word; /* 긴 단어가 넘치지 않도록 처리 */ + word-break: break-word; /* 긴 단어를 끊어 줄바꿈 */ +} + +.category-scroll-container { + width: 100%; /* 부모 요소의 가로 크기 */ + overflow-x: auto; /* 가로 스크롤을 가능하게 */ + white-space: nowrap; /* 자식 요소들이 가로로 나열되도록 설정 */ +} + +/* 버튼들 - 기본적으로 가로로 나열될 수 있도록 설정 */ +.category-scroll-container button { + display: inline-block; /* 버튼들이 가로로 나열되도록 */ + margin-right: 10px; /* 버튼들 간의 간격 */ +} + +.carousel-button-next { + background: transparent; /* 버튼 배경 투명 */ + border: none; /* 버튼의 테두리 제거 */ + padding: 0; /* 내부 패딩 제거 */ + display: flex; /* flexbox로 아이콘을 가운데 배치 */ + align-items: center; /* 세로 가운데 정렬 */ + justify-content: center; /* 가로 가운데 정렬 */ + position: absolute; + right: 1rem; + bottom: 50%; + transform: rotate(180deg); +} + +.carousel-button-prev { + background: transparent; /* 버튼 배경 투명 */ + border: none; /* 버튼의 테두리 제거 */ + padding: 0; /* 내부 패딩 제거 */ + display: flex; /* flexbox로 아이콘을 가운데 배치 */ + align-items: center; /* 세로 가운데 정렬 */ + justify-content: center; /* 가로 가운데 정렬 */ + position: absolute; + left: 1rem; + bottom: 50%; +} + +.content-detail { + border-top: 1px solid #d0d7e1; +} + +/* 상세 페이지 본문 */ +.post-detail-content-wrapper{ +border-bottom: 1px solid #d0d7e1; +padding-bottom: 1.25rem; } diff --git a/app/(pages)/(main)/layout.tsx b/app/(pages)/(main)/layout.tsx index e2e4aad..3042183 100644 --- a/app/(pages)/(main)/layout.tsx +++ b/app/(pages)/(main)/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next"; import "@/app/globals.css"; import TopNav from "@/app/components/TopNav"; import { ReactNode } from "react"; -import { SvgButtonForNav, TextLogoButtonForNav } from "@/app/components/button"; +import { AlertButtonForNav, TextLogoButtonForNav } from "@/app/components/button"; import { AddBotNav } from "@/app/components/BotNav"; @@ -16,6 +16,8 @@ type LayoutProps = { }; export default function Layout({ children }: LayoutProps) { + // 현석이 버튼 수정함 + return (
@@ -23,10 +25,10 @@ export default function Layout({ children }: LayoutProps) { {/* 상단 바 */} MUKPIC} - rightButton={ + rightButton={ - - + + @@ -34,16 +36,18 @@ export default function Layout({ children }: LayoutProps) { - } + } /> {/* 메인 */} -
+
{/* 메인 페이지 내용 */} {children} - + {/* 하단 네비게이션 */} +
+ +
- {/* 하단 네비게이션 */} - +
); } diff --git a/app/(pages)/community/[communityId]/modify/page.tsx b/app/(pages)/community/[communityId]/modify/page.tsx index 955b173..bc6e009 100644 --- a/app/(pages)/community/[communityId]/modify/page.tsx +++ b/app/(pages)/community/[communityId]/modify/page.tsx @@ -1,18 +1,22 @@ 'use client'; import { SvgButtonForNav } from "@/app/components/button"; -import { Write } from "@/app/components/community/postComponents"; +import { AddImageUrl, CategorySelectDropdown } from "@/app/components/community/postComponents"; import TopNav from "@/app/components/TopNav"; import { usePostStore } from "@/app/types/postStore"; +import { useUpdateImageStore } from "@/app/types/updateImgStore"; import axios from "axios"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; + + export default function BoardDetail() { const router = useRouter(); // 쿼리 파라미터 추출 const pathname = usePathname() as string; const [communityId, setCommunityId] = useState(null); - + const categoryList: string[] = ['Rice', 'Noodle', 'Soup', 'Dessert', 'ETC', 'Streetfood', 'Kimchi']; // 드롭다운 옵션 + const maxLength = 300; // 최대 글자 수 // 파라미터에서 communityId 추출 useEffect(() => { if (pathname) { @@ -25,69 +29,83 @@ export default function BoardDetail() { // zustand 사용해서 받아 온 값을 상태에 저장해준다음 수정할 때, 페이지에 보여줄 때 사용 - const title = usePostStore(state => state.title); - const content = usePostStore(state => state.content); - const images = usePostStore(state => state.images); - const category = usePostStore(state => state.category); - const imageUrl = usePostStore(state => state.imageUrl); - const setTitle = usePostStore(state => state.setTitle); - const setContent = usePostStore(state => state.setContent); - // const setImages = usePostStore(state => state.setImages); - const setCategory = usePostStore(state => state.setCategory); - const setImageUrl = usePostStore(state => state.setImageUrl); - - - // 타입 정의 - type CommunityPost = { - communityKey: string; - title: string; - content: string; - imageUrls: string[]; - likeCount: number; - }; - - // 상태 정의 - const [post, setPost] = useState(null); + const [initTitle, setInitTile] = useState(''); // 수정 전 타이틀 + const [initContent, setInitContent] = useState(''); // 수정 전 내용 + const [initCategory, setInitCategory] = useState(''); // 수정 전 카테고리 + const [title, setTitle] = useState(''); // 수정 후 타이틀 + const [content, setContent] = useState(''); // 수정 후 내용 + const [category, setCategory] = useState(''); // 수정 후 카테고리 + + //이미지 url만 다른 컴포넌트로 처리해주므로 useStore 사용 + const setImageUrls = usePostStore((state) => state.setImageUrls); + const updateImageUrls = useUpdateImageStore((state) => state.updateImageUrls); + const setUpdateImageUrls = useUpdateImageStore((state) => state.setUpdateImageUrls); + const [loading, setLoading] = useState(true); - // 수정 버튼 클릭 시 + interface UpdateData { + title?: string; + content?: string; + imageUrl?: string[]; // 혹은 File[] + category?: string; + } + function UpDateHandler() { - if (title && content && images && category) { - // 이미지 등록 - axios({ - method: 'post', - url: `${process.env.NEXT_PUBLIC_ROOT_API}/images/upload`, - data: { - file: images, - type: 'COMMUNITY' - } - }).then((response) => { - axios({ - method: 'patch', - url: `${process.env.NEXT_PUBLIC_ROOT_API}/community/${communityId}`, - data: { - communityKey: communityId, - title: title, - content: content, - // 필요하면 카테고리 추가 - imageUrls: [...imageUrl, ...response.data], - likecount: post?.likeCount - }, - headers: { - Authorization: `${localStorage.getItem('Authorization')}` - } - }).then((response) => { - if (response.status === 200) { - alert('Item successfully modified.'); - router.push(`/community/${communityId}`); - } - }).catch((error) => { - console.error('게시글 수정 api 에러: ', error); - }) - }).catch((error) => { - console.error('게시글 이미지 등록 api 에러: ', error); - }) + const updateData: UpdateData = {}; + if ((title === '' || content === '' || category === '')) { + const missingFields = []; + + if (category === '') { + missingFields.push('category'); + } + if (title === '') { + missingFields.push('title'); + } + if (content === '') { + missingFields.push('content'); + } + + if (missingFields.length > 0) { + const message = `Please fill in the following fields - ${missingFields.join(', ')}`; + alert(message); + return; + } } + + if (title !== initTitle) { + updateData.title = title; + } + if (content !== initContent) { + updateData.content = content; + } + if (category !== initCategory) { + updateData.category = category.toUpperCase(); + } + if (updateImageUrls.length > 0) { + updateData.imageUrl = updateImageUrls; + } + + + + console.log('수정할 데이터:', updateData); + axios({ + method: 'patch', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/community/${communityId}`, + data: updateData, + headers: { + Authorization: `${localStorage.getItem('Authorization')}` + } + }).then((response) => { + if (response.status == 200) { + alert('Item successfully modified.'); + setImageUrls([]); // 이미지 url 초기화 + setUpdateImageUrls([]); // 이미지 url 초기화 + router.push(`/community/${communityId}`); + } + }).catch((error) => { + console.error('게시글 수정 api 에러: ', error); + + }) } @@ -96,26 +114,34 @@ export default function BoardDetail() { useEffect(() => { if (communityId) { - console.log('use effect 작동 체크 communityId: ', communityId); axios.get(`${process.env.NEXT_PUBLIC_ROOT_API}/community/${communityId}`, - { - headers: - { + { + headers: + { Authorization: `${localStorage.getItem('Authorization')}` - } + } } ) .then((response) => { - if(response.status === 200) { - setPost(response.data); - // 정보 불러오면서 상태 업데이트 - setTitle(response.data.title); - setContent(response.data.content); - setImageUrl(response.data.imageUrls); - setCategory(''); // 카테고리 정보는 없음 - setLoading(false); + if (response.status === 200) { + + + // 유저 키 비교해서 접근 허용/비허용 코드드 + // const userKeyFromLocalStorage = localStorage.getItem('userKey'); + // if (response.data.userKey !== userKeyFromLocalStorage) { + // alert('You do not have permission to view this.'); + // router.push(`/community/${communityId}`); + // } + setInitTile(response.data.title); + setInitContent(response.data.content); + setInitCategory(response.data.category); + setImageUrls(response.data.imageUrls); + setTitle(response.data.title); + setContent(response.data.content); + setCategory(response.data.category); + setLoading(false); } - if(response.status === 401) { + if (response.status === 401) { alert('You do not have permission to view this.'); router.push('/community'); } @@ -127,8 +153,19 @@ export default function BoardDetail() { }); } - }, [communityId, setTitle, setCategory, setContent, setImageUrl, router]); // communityId가 변경될 때마다 호출 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [communityId, router]); // communityId가 변경될 때마다 호출 + + const contentshandleChange = (e: React.ChangeEvent) => { + if (e?.target.value.length <= maxLength) { + setContent(e.target.value); + } + } + + const titlehandleChange = (e: React.ChangeEvent) => { + setTitle(e.target.value); + } // 로딩 상태와 에러 처리 if (loading) return
Loading...
; @@ -136,9 +173,10 @@ export default function BoardDetail() { <> + { router.back(); }}> - + @@ -156,7 +194,46 @@ export default function BoardDetail() { } />
- +
+ {/* 카테고리 드롭다운 */} + setCategory(item)} // 선택된 항목 category로로 콜백 + /> + {/* 타이틀 */} + + {/* 내용 입력 */} + + + {/* 이미지 추가 */} + +
diff --git a/app/(pages)/community/[communityId]/page.tsx b/app/(pages)/community/[communityId]/page.tsx index 7dca86f..c4e0a74 100644 --- a/app/(pages)/community/[communityId]/page.tsx +++ b/app/(pages)/community/[communityId]/page.tsx @@ -1,6 +1,6 @@ 'use client'; import { SvgButtonForNav } from '@/app/components/button'; -import { PostContent } from '@/app/components/community/communityComponents'; +import { DetailPostContent } from '@/app/components/community/communityComponents'; import TopNav from '@/app/components/TopNav'; import axios from 'axios'; import { usePathname, useRouter } from 'next/navigation'; @@ -14,6 +14,13 @@ export default function BoardDetail() { const router = useRouter(); const pathname = usePathname() as string; const [communityId, setCommunityId] = useState(null); + const [currentIndex, setCurrentIndex] = useState(0); + const [imageUrls, setImageUrls] = useState([]); + const [userKey, setUserKey] = useState(-5); + const [rightButtonVisible, setRightButtonVisible] = useState(false); + + + const DropdownForNav = () => { const [isOpen, setIsOpen] = useState(false); @@ -45,7 +52,7 @@ export default function BoardDetail() {
{isOpen && ( -
+
  • { @@ -101,7 +114,7 @@ export default function BoardDetail() { } function DeleteHandler() { - const isConfirmed = window.confirm('정말 삭제하시겠습니까?'); + const isConfirmed = window.confirm('Are you sure you want to delete it?'); if (isConfirmed) { axios({ method: 'delete', @@ -110,13 +123,12 @@ export default function BoardDetail() { Authorization: `${localStorage.getItem('Authorization')}` } }).then((response) => { - if (response.status === 200) { - alert('Item successfully deleted.'); - router.push('/community'); - } if (response.status === 401) { alert('You do not have permission to delete this.') } + alert('Item successfully deleted.'); + router.push('/community'); + }).catch((error) => { console.error('게시글 삭제 api 에러: ', error); }) @@ -135,6 +147,8 @@ export default function BoardDetail() { }) .then((response) => { setPost(response.data); + setImageUrls(response.data.imageUrls); + setUserKey(response.data.userKey); setLoading(false); }) .catch((error) => { @@ -142,16 +156,59 @@ export default function BoardDetail() { }); }, [communityId]); // communityId가 변경될 때마다 호출 + + const [isAnimating, setIsAnimating] = useState(false); + + const handlePrev = () => { + if (isAnimating) return; + setIsAnimating(true); + + const prevIndex = (currentIndex - 1 + imageUrls.length) % imageUrls.length; // 순환 처리 + setTimeout(() => { + setCurrentIndex(prevIndex); + setIsAnimating(false); + }, 300); + }; + // 다음 이미지로 이동 + const handleNext = () => { + if (isAnimating) return; // 애니메이션 중에는 버튼 막기 + setIsAnimating(true); + + const nextIndex = (currentIndex + 1) % imageUrls.length; // 순환 처리 + setTimeout(() => { + setCurrentIndex(nextIndex); + setIsAnimating(false); + }, 300); // 슬라이드 애니메이션 시간 + }; + + useEffect(() => { + if (imageUrls.length < 0 && currentIndex >= imageUrls.length) { + setCurrentIndex(0); + } + }, [imageUrls, currentIndex, setCurrentIndex]); + + //유저 키 비교해서 버튼 활성화 + useEffect(() => { + const getUserKey = localStorage.getItem('userKey'); + if (Number(userKey) === Number(getUserKey)) { + setRightButtonVisible(true); + } + else { + setRightButtonVisible(false); + } + }, [userKey]); + // 로딩 상태와 에러 처리 if (loading) return
    Loading...
    ; return ( <> -
    +
    + leftButton={ { location.href = '/community' }}> - + @@ -163,15 +220,14 @@ export default function BoardDetail() { } // 임시로 넣은 아이콘 - rightButton={} + rightButton={rightButtonVisible ? : null} /> - - {/* 메인 */} - -
    - {post && } -
    - {post?.content} +
    +
    + {post && }
    diff --git a/app/(pages)/community/layout.tsx b/app/(pages)/community/layout.tsx index 906e7be..1527f08 100644 --- a/app/(pages)/community/layout.tsx +++ b/app/(pages)/community/layout.tsx @@ -13,7 +13,7 @@ type LayoutProps = { export default function Layout({ children }: LayoutProps) { return ( -
    +
    {children}
    ); diff --git a/app/(pages)/community/page.tsx b/app/(pages)/community/page.tsx index f151154..d986464 100644 --- a/app/(pages)/community/page.tsx +++ b/app/(pages)/community/page.tsx @@ -2,35 +2,43 @@ import { PostComponents } from "@/app/components/community/communityComponents"; import '@/app/(css)/community.css'; import TopNav from "@/app/components/TopNav"; -import { TextAndIconButton, TextLogoButtonForNav } from "@/app/components/button"; -// import { useRouter } from "next/navigation"; +import { CommunityButtonForNav, TextAndIconButton } from "@/app/components/button"; +import { AddBotNav } from "@/app/components/BotNav"; export default function BoardMain() { - // const router = useRouter(); - + const postButtonHandler = () => { location.href = '/community/post'; } + return ( <> - Community} - rightButton={ - - - } - onclick={postButtonHandler} - > - Post - } - /> -
    - -
    +
    + + + } + rightButton={ + + + } + onclick={postButtonHandler} + > + Post + } + /> +
    + +
    +
    + ); } \ No newline at end of file diff --git a/app/(pages)/community/post/page.tsx b/app/(pages)/community/post/page.tsx index 765ecdc..f19482b 100644 --- a/app/(pages)/community/post/page.tsx +++ b/app/(pages)/community/post/page.tsx @@ -3,6 +3,7 @@ import { SvgButtonForNav } from "@/app/components/button"; import { Write } from "@/app/components/community/postComponents"; import TopNav from "@/app/components/TopNav"; import { usePostStore } from "@/app/types/postStore"; +import { useUpdateImageStore } from "@/app/types/updateImgStore"; import axios from "axios"; import { useRouter } from "next/navigation"; @@ -10,71 +11,59 @@ export default function Post() { const router = useRouter(); const title = usePostStore((state) => state.title); const content = usePostStore((state) => state.content); - const images = usePostStore((state) => state.images); const category = usePostStore((state) => state.category); + const updateImageUrls = useUpdateImageStore((state) => state.updateImageUrls); const UploadHandler = () => { - console.log('페이지 쪽 상태관리 images', images); // 이미지 등록 포스트 요청 - if (images.length !== 0 && category !== '' && title !== '' && content !== '') { - // 이미지 업로드 요청을 각각 보내는 배열 - const uploadPromises = images.map((image) => { - const uploadFormData = new FormData(); - uploadFormData.append("file", image); - uploadFormData.append("type", 'COMMUNITY'); - - return axios({ - method: 'post', - url: `${process.env.NEXT_PUBLIC_ROOT_API}/images/upload`, - data: uploadFormData - }).then((response) => { - return response.data; // 서버에서 반환한 이미지 URL 배열 - }); + if (updateImageUrls.length !== 0 && category !== '' && title !== '' && content !== '') { + // 게시글 등록 API 호출 + axios({ + method: 'post', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/community`, + headers: { + Authorization: `${localStorage.getItem('Authorization')}`, + }, + data: { + title: title, + content: content, + imageUrl: updateImageUrls, + category: category, + } + }).then((response) => { + if (response.status === 200) { + alert('Upload Success'); + location.href = '/community'; + } + }).catch((error) => { + alert('Upload Error'); + console.log('post upload error', error); + router.push('/community'); }); + } + - // 모든 이미지 업로드가 완료된 후 커뮤니티 게시글 등록 - Promise.all(uploadPromises) - .then((uploadedImageUrlsArrays) => { - // uploadedImageUrlsArrays는 배열 안에 배열이 있을 수 있으므로, 이를 평평하게(flatten) 만듭니다 - const uploadedImageUrls = [].concat(...uploadedImageUrlsArrays); - console.log('업로드된 이미지 URLs:', uploadedImageUrls); + else { + const missingFields = []; - const formData = { title, content, uploadedImageUrls, category }; - console.log('formData:', formData); + if (updateImageUrls.length === 0) { + missingFields.push('image'); + } + if (category === '') { + missingFields.push('category'); + } + if (title === '') { + missingFields.push('title'); + } + if (content === '') { + missingFields.push('content'); + } - // 게시글 등록 API 호출 - axios({ - method: 'post', - url: `${process.env.NEXT_PUBLIC_ROOT_API}/community`, - headers: { - Authorization: `${localStorage.getItem('Authorization')}`, - }, - data: { - title: title, - content: content, - imageUrl: uploadedImageUrls, // 평평하게 만든 이미지 URL 배열 - category: category, - } - }).then((response) => { - if (response.status === 200) { - alert('Upload Success'); - location.href = '/community'; - } - }).catch((error) => { - alert('Upload Error'); - console.log('post upload error', error); - router.push('/community'); - }); - }) - .catch((error) => { - alert('Image Upload Error'); - console.log('image upload error', error); - router.push('/community'); - }); - } else { - // 필수 항목이 누락되었을 경우 - alert('Please fill in all the blanks'); + if (missingFields.length > 0) { + const message = `Please fill in the following fields - ${missingFields.join(', ')}`; + alert(message); + } } }; @@ -82,9 +71,16 @@ export default function Post() { <> + { + if (window.history.length > 1) { + router.back(); + } else { + router.push('/community'); // 기본 페이지로 이동 + } + }} > - + @@ -102,7 +98,7 @@ export default function Post() { } />
    - +
    diff --git a/app/(pages)/info/layout.tsx b/app/(pages)/info/layout.tsx index 7dda7ca..7180188 100644 --- a/app/(pages)/info/layout.tsx +++ b/app/(pages)/info/layout.tsx @@ -2,51 +2,26 @@ import type { Metadata } from "next"; import "@/app/globals.css"; import TopNav from "@/app/components/TopNav"; import { ReactNode } from "react"; -import { SearchButtonForNav, TextButtonForNav } from "@/app/components/button"; +import { MainButtonForNav } from "@/app/components/button"; +import TextButtonForNavWrapper from "@/app/components/TextButtonForNavWrapper"; export const metadata: Metadata = { title: "MukPic-Info", description: "Info Page", }; -//기본 레이아웃 -/* -구조 - body[main{div class="root-container"(header,본문,footer)}] -일단 스타일 설정은 body 와 main은 비워두고 root-container에만 해놓았음 -*/ - type LayoutProps = { children: ReactNode; }; - export default function Layout({ children }: LayoutProps) { - return ( - - /* 메인 */ - < div className="root-wrapper" > - - - } - rightButton={ - - - } - >Post} - /> -
    - {/* 상단 내비게이션 필요한 버튼 넣어서 사용용 */} - - {/* 메인 페이지 내용 */} - {children} - - - {/* 하단 네비게이션 */} -
    - - - ); + return ( +
    + } + rightButton={} + /> +
    {children}
    +
    + ); } diff --git a/app/(pages)/info/page.tsx b/app/(pages)/info/page.tsx index 0a7ef45..7ec4c5a 100644 --- a/app/(pages)/info/page.tsx +++ b/app/(pages)/info/page.tsx @@ -63,14 +63,6 @@ function InfoPageContent() { router.push("/"); }; - const handlePostClick = () => { - if (imageUrl) { - router.push(`/community/post`); - } else { - setError("Error: Unable to proceed without image URL."); - } - }; - if (!imageUrl) { return (
    @@ -88,8 +80,20 @@ function InfoPageContent() { )} {!loading && error && ( -
    -

    Error: {error}

    +
    + {/* 에러 메시지 디자인 */} +
    +

    Oops! Something went wrong.

    +

    {error}

    +
    + + {/* "Go Back" 버튼 디자인 */} +
    )} @@ -145,17 +149,11 @@ function InfoPageContent() {
    -
    )} diff --git a/app/(pages)/keyword/layout.tsx b/app/(pages)/keyword/layout.tsx index 1ef8695..fdaab9c 100644 --- a/app/(pages)/keyword/layout.tsx +++ b/app/(pages)/keyword/layout.tsx @@ -2,49 +2,36 @@ import type { Metadata } from "next"; import "@/app/globals.css"; import TopNav from "@/app/components/TopNav"; import { ReactNode } from "react"; -import { SearchButtonForNav, TextButtonForNav } from "@/app/components/button"; - - +import { MainButtonForNav } from "@/app/components/button"; export const metadata: Metadata = { - title: "MukPic-Keyword", - description: "MukPic Keyword Page", + title: "MukPic-Keyword", + description: "MukPic Keyword Page", }; -//기본 레이아웃 +// 기본 레이아웃 /* 구조 - body[main{div class="root-container"(header,본문,footer)}] 일단 스타일 설정은 body 와 main은 비워두고 root-container에만 해놓았음 */ type LayoutProps = { - children: ReactNode; + children: ReactNode; }; - export default function Layout({ children }: LayoutProps) { - return ( - - /* 메인 */ -
    - - - } - rightButton={ - - - } - >Post} - /> -
    - {/* 상단 내비게이션 필요한 버튼 넣어서 사용용 */} - {/* 메인 페이지 내용 */} - {children} - {/* 하단 네비게이션 */} -
    -
    - ); + return ( + /* 메인 */ +
    + } + /> +
    + {/* 상단 내비게이션 필요한 버튼 넣어서 사용 */} + {/* 메인 페이지 내용 */} + {children} + {/* 하단 네비게이션 */} +
    +
    + ); } diff --git a/app/(pages)/keyword/page.tsx b/app/(pages)/keyword/page.tsx index e5042c8..c63e9ab 100644 --- a/app/(pages)/keyword/page.tsx +++ b/app/(pages)/keyword/page.tsx @@ -144,7 +144,7 @@ function KeywordPageContent() {
    -
    - - - - + {/* 하단 네비게이션 */} + + +
    + ); } diff --git a/app/(pages)/myPage/page.tsx b/app/(pages)/myPage/page.tsx index 287bc7e..a13081d 100644 --- a/app/(pages)/myPage/page.tsx +++ b/app/(pages)/myPage/page.tsx @@ -1,395 +1,6 @@ -'use client'; +import React from "react"; +import MyPage from "@/app/components/myPage/myPageComponent"; -import React, { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import styled, { createGlobalStyle } from 'styled-components'; -import { AddBotNav } from "@/app/components/BotNav"; - -const GlobalStyle = createGlobalStyle` - html, body { - background-color: #ffffff; - width: 100%; - height: 100%; - } -`; - -interface UserInfoData { - image: string; - userName: string; - nationality: string; - religion: string; - allergies: string[]; - chronicDiseases: string[]; - dietaryPreferences: string[]; -} - -interface PostData { - communityKey: string; - title: string; - content: string; - imageUrls: string[]; -} - -const MyPage = () => { - const [userInfo, setUserInfo] = useState(null); - const [activeTab, setActiveTab] = useState<'liked' | 'myPost'>('liked'); - const [postData, setPostData] = useState([]); - const router = useRouter(); - - // 토큰 가져오기 함수 - const getAuthToken = () => { - const token = localStorage.getItem('Authorization'); - if (!token) { - console.error('Authorization token not found'); - } - return token; - }; - - // 사용자 정보 가져오기 - useEffect(() => { - const fetchUserInfo = async () => { - const token = getAuthToken(); - if (!token) return; - - try { - const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/myinfo`, { - method: 'GET', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - Authorization: `${token}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - setUserInfo(data); - } else { - console.error('Failed to fetch user info:', response.status); - } - } catch (error) { - console.error('Error fetching user info:', error); - } - }; - - fetchUserInfo(); - }, []); - - // 게시글 정보 가져오기 - useEffect(() => { - const fetchPostData = async () => { - const token = getAuthToken(); - if (!token) return; - - const endpoint = - activeTab === 'liked' - ? `${process.env.NEXT_PUBLIC_ROOT_API}/community/likedCommunities?page=0&size=5` - : `${process.env.NEXT_PUBLIC_ROOT_API}/community/myCommunities?page=0&size=5`; - - try { - const response = await fetch(endpoint, { - method: 'GET', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - Authorization: `${token}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - setPostData(data.content || []); - } else { - console.error('Failed to fetch posts:', response.status); - } - } catch (error) { - console.error('Error fetching posts:', error); - } - }; - - fetchPostData(); - }, [activeTab]); - - return ( - <> - - - - My Page - router.push('/settings')}> - - - - - - - {/* 사용자 프로필 */} - - - - {userInfo?.userName || 'Loading...'} - {userInfo?.nationality || 'Loading...'} - - {userInfo?.religion && {userInfo.religion}} - - - {userInfo?.allergies?.map((tag, index) => ( - {tag} - ))} - - - {userInfo?.chronicDiseases?.map((tag, index) => ( - {tag} - ))} - - - {userInfo?.dietaryPreferences?.map((tag, index) => ( - {tag} - ))} - - - - - {/* 탭 */} - - setActiveTab('liked')}> - Liked - - setActiveTab('myPost')}> - My Post - - - - {/* 게시글 목록 */} - - {postData.map((post) => ( - - - - {post.title} - {post.content} - - - ))} - - - - - - ); -}; - -export default MyPage; - - -const Container = styled.div` - margin: 0 auto; - background-color: #ffffff; - display: flex; - flex-direction: column; - width: 24.375rem; - height: 52.75rem; - padding-top: 3.5rem; -`; - -const CustomHeader = styled.header` - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px; - background-color: #ffffff; - border-bottom: 1px solid #e5e5e5; - position: fixed; - top: 0; - z-index: 10; - width: 24.375rem; - height: 3.5rem; - flex-shrink: 0; -`; - -const Title = styled.h1` - font-size: 18px; - font-weight: bold; - margin: 0 auto; - color: var(--Gray-gray-900, #0B0B0B); - text-align: center; - font-feature-settings: 'liga' off, 'clig' off; - font-family: SUIT; - font-size: 1.5rem; - font-style: normal; - font-weight: 600; - line-height: normal; -`; - -const SettingsIcon = styled.div` - cursor: pointer; - position: absolute; - right: 16px; -`; - -const ProfileSection = styled.div` - display: flex; - align-items: flex-start; - background-color: #FFF; - border-bottom: 1px solid #E0E5EB; - width: 24.375rem; - padding: 16px; - box-sizing: border-box; - flex-wrap: wrap; -`; - -const Avatar = styled.div` - width: 6.25rem; - height: 6.25rem; - flex-shrink: 0; - border-radius: 50%; - background-color: #ccc; - background-size: cover; - background-position: center; - margin-right: 16px; -`; - -const UserInfo = styled.div` - flex: 1; - display: flex; - flex-direction: column; - gap: 6px; -`; - -const Username = styled.div` - color: var(--Gray-gray-900, #0B0B0B); - font-feature-settings: 'liga' off, 'clig' off; - font-family: SUIT; - font-size: 1.25rem; - font-style: normal; - font-weight: 700; - line-height: normal; -`; - -const Location = styled.div` - color: #6A7784; - font-feature-settings: 'liga' off, 'clig' off; - font-family: SUIT; - font-size: 0.75rem; - font-style: normal; - font-weight: 500; - line-height: normal; -`; - -const Tags = styled.div` - display: inline-flex; - flex-wrap: wrap; /* 태그가 많으면 줄 바꿈 */ - gap: 0.25rem; - margin-top: 0.38px; - padding: 0.38px 0.25px; - align-items: center; - color: #6A7784; - font-feature-settings: 'liga' off, 'clig' off; - font-family: SUIT; - font-size: 0.75rem; - font-style: normal; - font-weight: 500; - line-height: normal; - -`; - -const Tag = styled.div<{ color: string }>` - background-color: ${(props) => props.color}; - padding: 0.125rem 0.75rem; - border-radius: 3.125rem; - border: 1px solid #1E252F; - color: #1E252F; - font-feature-settings: 'liga' off, 'clig' off; - font-family: SUIT; - font-size: 0.6875rem; - font-style: normal; - font-weight: 500; - line-height: normal; -`; - -const TabBar = styled.div` - display: flex; - justify-content: space-around; - background-color: #fff; - padding: 1.5rem 0 1rem 1.5rem; - font-weight: bold; - border-bottom: 1px solid #ccc; - width: 24.375rem; - height: 3.75rem; - flex-shrink: 0; -`; - -const Tab = styled.div<{ isActive: boolean }>` - cursor: pointer; - color: ${(props) => (props.isActive ? 'blue' : '#000')}; - border-bottom: ${(props) => (props.isActive ? '2px solid black' : 'none')}; - color: #0A0C10; - font-feature-settings: 'liga' off, 'clig' off; - font-family: SUIT; - font-size: 1rem; - font-style: normal; - font-weight: 700; - line-height: normal; - -`; - - -const PostsGrid = styled.div` - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 0.75px; - padding: 16px; - width: 24.375rem; - height: 32.8125rem; - flex-shrink: 0; -`; - -const PostCard = styled.div` - border: 1px solid #ddd; - border-radius: 8px; - overflow: hidden; -`; - -const PostImage = styled.img` - width: 100%; - height: 100%; - flex-shrink: 0; - border-radius: 0.5rem; -`; - -// const TestImage = styled.img` -// width: 100%; -// height: 50%; -// flex-shrink: 0; -// border-radius: 0.5rem; -// `; - -const PostTitle = styled.h3` - font-size: 14px; - margin: 8px; -`; - -const PostDescription = styled.p` - font-size: 12px; - margin: 0 8px 8px; -`; - -const PostContent = styled.div` - padding: 8px; -`; - -// const PostDetails = styled.div` -// display: flex; -// justify-content: space-between; -// margin-top: 8px; -// `; - -// const Comments = styled.div` -// font-size: 12px; -// color: #555; -// `; - -// const Likes = styled.div` -// font-size: 12px; -// color: #555; -// `; +export default function Page() { + return ; +} \ No newline at end of file diff --git a/app/(pages)/settings/account/change-password/layout.tsx b/app/(pages)/settings/account/change-password/layout.tsx new file mode 100644 index 0000000..f7223db --- /dev/null +++ b/app/(pages)/settings/account/change-password/layout.tsx @@ -0,0 +1,57 @@ +'use client'; +import "@/app/globals.css"; +import { ReactNode, useEffect } from "react"; +import { SvgButtonForNav } from "@/app/components/button"; +import TopNav from "@/app/components/TopNav"; +import { useRouter } from "next/navigation"; + +type LayoutProps = { + children: ReactNode; +}; + +export default function EditProfileLayout({ children }: LayoutProps) { + const router = useRouter(); + useEffect(() => { + document.title = "Edit-Profile"; + }, []); + + return ( +
    + { + router.back(); + }} + > + + + + + + + + + + + + } + /> +
    + Change Password +
    +
    + {children} +
    +
    + ); +} diff --git a/app/(pages)/settings/account/change-password/page.tsx b/app/(pages)/settings/account/change-password/page.tsx new file mode 100644 index 0000000..dcd7a3a --- /dev/null +++ b/app/(pages)/settings/account/change-password/page.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import ChangePasswordPage from "@/app/components/setting/changePassword"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/app/(pages)/settings/account/edit-healthInfo/layout.tsx b/app/(pages)/settings/account/edit-healthInfo/layout.tsx new file mode 100644 index 0000000..c1e3f8b --- /dev/null +++ b/app/(pages)/settings/account/edit-healthInfo/layout.tsx @@ -0,0 +1,49 @@ +'use client'; +import "@/app/globals.css"; +import '@/app/(css)/auth.css'; +import { ReactNode, useEffect } from "react"; +import { SvgButtonForNav } from "@/app/components/button"; +import TopNav from "@/app/components/TopNav"; +import { useRouter } from "next/navigation"; + + + + +type LayoutProps = { + children: ReactNode; +}; + +export default function SignupLayout({ children }: LayoutProps) { + const router = useRouter(); + useEffect(() => { + document.title = "Mukpic-Signup"; // 원하는 타이틀로 변경 + }, []); + + return ( + + + +
    + { router.back(); }}> + + + + + + + + + + + } + /> +
    + {children} +
    +
    + + + ); +} diff --git a/app/(pages)/settings/account/edit-healthInfo/page.tsx b/app/(pages)/settings/account/edit-healthInfo/page.tsx new file mode 100644 index 0000000..a7d5ce6 --- /dev/null +++ b/app/(pages)/settings/account/edit-healthInfo/page.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import EditHealthPage from "@/app/components/setting/edit-healthInfo"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/app/(pages)/settings/account/edit-preference/layout.tsx b/app/(pages)/settings/account/edit-preference/layout.tsx new file mode 100644 index 0000000..c1e3f8b --- /dev/null +++ b/app/(pages)/settings/account/edit-preference/layout.tsx @@ -0,0 +1,49 @@ +'use client'; +import "@/app/globals.css"; +import '@/app/(css)/auth.css'; +import { ReactNode, useEffect } from "react"; +import { SvgButtonForNav } from "@/app/components/button"; +import TopNav from "@/app/components/TopNav"; +import { useRouter } from "next/navigation"; + + + + +type LayoutProps = { + children: ReactNode; +}; + +export default function SignupLayout({ children }: LayoutProps) { + const router = useRouter(); + useEffect(() => { + document.title = "Mukpic-Signup"; // 원하는 타이틀로 변경 + }, []); + + return ( + + + +
    + { router.back(); }}> + + + + + + + + + + + } + /> +
    + {children} +
    +
    + + + ); +} diff --git a/app/(pages)/settings/account/edit-preference/page.tsx b/app/(pages)/settings/account/edit-preference/page.tsx new file mode 100644 index 0000000..a9b0d65 --- /dev/null +++ b/app/(pages)/settings/account/edit-preference/page.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import EditPreferencePage from "@/app/components/setting/edit-preferenceComponent"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/app/(pages)/settings/account/edit-profile/layout.tsx b/app/(pages)/settings/account/edit-profile/layout.tsx new file mode 100644 index 0000000..18a25f4 --- /dev/null +++ b/app/(pages)/settings/account/edit-profile/layout.tsx @@ -0,0 +1,49 @@ +'use client'; +import "@/app/globals.css"; +import '@/app/(css)/auth.css'; +import { ReactNode, useEffect } from "react"; +import { SvgButtonForNav } from "@/app/components/button"; +import TopNav from "@/app/components/TopNav"; +import { useRouter } from "next/navigation"; + + + + +type LayoutProps = { + children: ReactNode; +}; + +export default function SignupLayout({ children }: LayoutProps) { + const router = useRouter(); + useEffect(() => { + document.title = "Edit-profile"; // 원하는 타이틀로 변경 + }, []); + + return ( + + + +
    + { router.back(); }}> + + + + + + + + + + + } + /> +
    + {children} +
    +
    + + + ); +} diff --git a/app/(pages)/settings/account/edit-profile/page.tsx b/app/(pages)/settings/account/edit-profile/page.tsx new file mode 100644 index 0000000..854a77c --- /dev/null +++ b/app/(pages)/settings/account/edit-profile/page.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import EditProfilePage from "@/app/components/setting/edit-profile"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/app/(pages)/settings/account/logout/layout.tsx b/app/(pages)/settings/account/logout/layout.tsx new file mode 100644 index 0000000..fac70c3 --- /dev/null +++ b/app/(pages)/settings/account/logout/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; +import "@/app/globals.css"; +import { ReactNode } from "react"; + + + +export const metadata: Metadata = { + title: "MukPic-Settings", + description: "settings page", +}; + +//기본 레이아웃 +/* +구조 - body[main{div class="root-container"(header,본문,footer)}] +일단 스타일 설정은 body 와 main은 비워두고 root-container에만 해놓았음 +*/ + +type LayoutProps = { + children: ReactNode; +}; + + + +export default function Layout({ children }: LayoutProps) { + return ( + +
    + {/* 상단 내비게이션 필요한 버튼 넣어서 사용용 */} + + {/* 메인 페이지 내용 */} + {children} + {/* 하단 네비게이션 */} +
    + + ); +} diff --git a/app/(pages)/settings/account/logout/page.tsx b/app/(pages)/settings/account/logout/page.tsx new file mode 100644 index 0000000..0b0782e --- /dev/null +++ b/app/(pages)/settings/account/logout/page.tsx @@ -0,0 +1,72 @@ +'use client'; + +import React, { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +const LogoutPage = () => { + const router = useRouter(); + + const getAuthToken = () => { + const token = localStorage.getItem('Authorization'); + if (!token) { + console.error('Authorization token not found'); + } + return token; + }; + + useEffect(() => { + const handleLogout = async () => { + const token = getAuthToken(); + if (!token) return; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/auth/logout`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + }); + + if (response.ok) { + localStorage.removeItem('Authorization'); + + // 현석 추가 코드 (미들웨어를 위한 쿠키 삭제) + localStorage.removeItem('userKey'); + const cookieName = 'authCookie'; + const cookies = document.cookie.split(';').find(cookie => cookie.startsWith(`${cookieName}=`)); + if (cookies) { + // authCookie 삭제 + document.cookie = `${cookieName}=; max-age=0; path=/; secure; SameSite=Strict`; + } + + // 로그인 페이지로 리다이렉트 + router.push('/login'); + } else { + console.error('Failed to logout:', response.status); + } + } catch (error) { + console.error('Error during logout:', error); + } + }; + + handleLogout(); + }, [router]); + + return ( +
    +

    Logging out...

    +
    + ); +}; + +export default LogoutPage; diff --git a/app/(pages)/settings/account/withdraw/layout.tsx b/app/(pages)/settings/account/withdraw/layout.tsx new file mode 100644 index 0000000..0116b14 --- /dev/null +++ b/app/(pages)/settings/account/withdraw/layout.tsx @@ -0,0 +1,57 @@ +'use client'; +import "@/app/globals.css"; +import { ReactNode, useEffect } from "react"; +import { SvgButtonForNav } from "@/app/components/button"; +import TopNav from "@/app/components/TopNav"; +import { useRouter } from "next/navigation"; + +type LayoutProps = { + children: ReactNode; +}; + +export default function EditProfileLayout({ children }: LayoutProps) { + const router = useRouter(); + useEffect(() => { + document.title = "Edit-Profile"; + }, []); + + return ( +
    + { + router.back(); + }} + > + + + + + + + + + + + + } + /> +
    + Delete Account +
    +
    + {children} +
    +
    + ); +} diff --git a/app/(pages)/settings/account/withdraw/page.tsx b/app/(pages)/settings/account/withdraw/page.tsx new file mode 100644 index 0000000..aa41edd --- /dev/null +++ b/app/(pages)/settings/account/withdraw/page.tsx @@ -0,0 +1,5 @@ +import AccountDeletion from "@/app/components/setting/AccountDeletion"; + +export default function AccountDeletionPage() { + return ; +} diff --git a/app/(pages)/settings/layout.tsx b/app/(pages)/settings/layout.tsx index fac70c3..b873444 100644 --- a/app/(pages)/settings/layout.tsx +++ b/app/(pages)/settings/layout.tsx @@ -1,12 +1,11 @@ import type { Metadata } from "next"; -import "@/app/globals.css"; import { ReactNode } from "react"; export const metadata: Metadata = { - title: "MukPic-Settings", - description: "settings page", + title: "MukPic-myPage", + description: "myPage", }; //기본 레이아웃 @@ -24,13 +23,20 @@ type LayoutProps = { export default function Layout({ children }: LayoutProps) { return ( -
    - {/* 상단 내비게이션 필요한 버튼 넣어서 사용용 */} - {/* 메인 페이지 내용 */} - {children} +
    +
    + {/* 상단 내비게이션 필요한 버튼 넣어서 사용용 */} + + {/* 메인 페이지 내용 */} + {children} + + +
    {/* 하단 네비게이션 */} -
    + +
    + ); } diff --git a/app/(pages)/settings/page.tsx b/app/(pages)/settings/page.tsx index 7d1b0c6..defbbb2 100644 --- a/app/(pages)/settings/page.tsx +++ b/app/(pages)/settings/page.tsx @@ -1,11 +1,145 @@ +'use client'; + import React from 'react'; +import styled from 'styled-components'; +import { useRouter } from 'next/navigation'; + const SettingsPage = () => { + const router = useRouter(); + + const menuData = [ + { + category: 'Account', + items: [ + { label: 'Edit profile', path: '/account/edit-profile' }, + { label: 'Edit preference', path: '/account/edit-preference' }, + { label: 'Edit healthInfo', path: '/account/edit-healthInfo' }, + { label: 'Change password', path: '/account/change-password' }, + ], + }, + { + category: 'Terms And Conditions', + items: [ + { label: 'Privacy policy', path: '/terms/privacy-policy' }, + { label: 'Terms of Service', path: '/terms/terms-of-service' }, + ], + }, + { + category: 'Management', + items: [ + { label: 'Logout', path: '/account/logout' }, + { label: 'Withdraw membership', path: '/account/withdraw' }, + ], + }, + ]; + + const handleMenuClick = (path: string) => { + router.push(`/settings/${path}`); + }; + return ( -
    -

    설정 페이지

    -
    + <> + +
    + router.push('/myPage')}>< + Settings +
    + + {menuData.map((section, index) => ( + + {section.category} + {section.items.map((item, label) => ( + handleMenuClick(item.path)}> + {item.label} + > + + ))} + + ))} + +
    + ); }; export default SettingsPage; + +const Container = styled.div` + margin: 0 auto; + background-color: #ffffff; + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + min-height: 300px; /* 최소 높이 설정 */ +`; + +const Header = styled.header` + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + background-color: #ffffff; + border-bottom: 1px solid #e5e5e5; + position: sticky; + top: 0; + z-index: 10; +`; + +const BackButton = styled.div` + font-size: 1.25rem; + cursor: pointer; +`; + +const Title = styled.h1` + font-size: 18px; + font-weight: bold; + margin: 0 auto; + color: var(--Gray-gray-900, #0B0B0B); + text-align: center; + font-feature-settings: 'liga' off, 'clig' off; + font-family: SUIT; + font-size: 1.5rem; + font-style: normal; + font-weight: 600; + line-height: normal; +`; + +const MenuList = styled.div` + margin-top: 16px; + display: flex; + flex-direction: column; +`; + +const MenuSection = styled.div` + margin-bottom: 24px; +`; + +const CategoryTitle = styled.div` + font-size: 0.875rem; + color: #6A7784; + margin: 0 16px 8px; +`; + +const MenuItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + cursor: pointer; + border-top: 1px solid #e5e5e5; + + &:hover { + background-color: #f9f9f9; + } + + &:last-child { + border-bottom: 1px solid #e5e5e5; + } +`; + +const ArrowIcon = styled.div` + font-size: 1rem; + color: #1e252f; +`; diff --git a/app/(pages)/settings/terms/privacy-policy/layout.tsx b/app/(pages)/settings/terms/privacy-policy/layout.tsx new file mode 100644 index 0000000..6a42f51 --- /dev/null +++ b/app/(pages)/settings/terms/privacy-policy/layout.tsx @@ -0,0 +1,57 @@ +'use client'; +import "@/app/globals.css"; +import { ReactNode, useEffect } from "react"; +import { SvgButtonForNav } from "@/app/components/button"; +import TopNav from "@/app/components/TopNav"; +import { useRouter } from "next/navigation"; + +type LayoutProps = { + children: ReactNode; +}; + +export default function EditProfileLayout({ children }: LayoutProps) { + const router = useRouter(); + useEffect(() => { + document.title = "Edit-Profile"; + }, []); + + return ( +
    + { + router.push('/settings'); + }} + > + + + + + + + + + + + + } + /> +
    + PrivacyPolicy +
    +
    + {children} +
    +
    + ); +} diff --git a/app/(pages)/settings/terms/privacy-policy/page.tsx b/app/(pages)/settings/terms/privacy-policy/page.tsx new file mode 100644 index 0000000..6619e96 --- /dev/null +++ b/app/(pages)/settings/terms/privacy-policy/page.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import policyData from "@/app/components/auth/Policy.json"; + +const PrivacyPolicy = () => { + const { privacyPolicy } = policyData; + + return ( +
    + {privacyPolicy.sections.map((section, index) => ( +
    +

    {section.heading}

    +

    {section.content}

    + + {section.list && ( +
      + {section.list.map((item, idx) => ( +
    • {item}
    • + ))} +
    + )} + + {section.footer &&

    {section.footer}

    } +
    + ))} +
    + ); +}; + +export default PrivacyPolicy; diff --git a/app/(pages)/settings/terms/terms-of-service/layout.tsx b/app/(pages)/settings/terms/terms-of-service/layout.tsx new file mode 100644 index 0000000..f65d99e --- /dev/null +++ b/app/(pages)/settings/terms/terms-of-service/layout.tsx @@ -0,0 +1,57 @@ +'use client'; +import "@/app/globals.css"; +import { ReactNode, useEffect } from "react"; +import { SvgButtonForNav } from "@/app/components/button"; +import TopNav from "@/app/components/TopNav"; +import { useRouter } from "next/navigation"; + +type LayoutProps = { + children: ReactNode; +}; + +export default function EditProfileLayout({ children }: LayoutProps) { + const router = useRouter(); + useEffect(() => { + document.title = "Edit-Profile"; + }, []); + + return ( +
    + { + router.push('/settings'); + }} + > + + + + + + + + + + + + } + /> +
    + Terms of Service +
    +
    + {children} +
    +
    + ); +} diff --git a/app/(pages)/settings/terms/terms-of-service/page.tsx b/app/(pages)/settings/terms/terms-of-service/page.tsx new file mode 100644 index 0000000..0b8f74b --- /dev/null +++ b/app/(pages)/settings/terms/terms-of-service/page.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import policyData from "@/app/components/auth/Policy.json"; + +const TermsOfService = () => { + const { termsOfService } = policyData; + + return ( +
    + {termsOfService.sections.map((section, index) => ( +
    +

    {section.heading}

    +

    {section.content}

    + + {section.list && ( +
      + {section.list.map((item, idx) => ( +
    • {item}
    • + ))} +
    + )} +{/* + {section.footer &&

    {section.footer}

    } */} +
    + ))} +
    + ); +}; + +export default TermsOfService; diff --git a/app/axios.ts b/app/axios.ts index c59a21a..0d0f702 100644 --- a/app/axios.ts +++ b/app/axios.ts @@ -1,13 +1,36 @@ import axios from "axios"; - -const instance = axios.create({ - baseURL: process.env.NEXT_PUBLIC_ROOT_API, - headers: { - 'Content-Type': 'application/json', - }, +const axiosInstance = axios.create({ + baseURL: `${process.env.NEXT_PUBLIC_ROOT_API}/v1`, // 기본 API 엔드포인트 설정 + timeout: 5000, // 타임아웃 설정 (선택 사항) + headers: { + "Content-Type": "application/json", + }, }); -export default instance; +// 요청 인터셉터 추가 +axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); // 예제: 로컬스토리지에서 토큰 가져오기 + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); +// 응답 인터셉터 추가 +axiosInstance.interceptors.response.use( + (response) => response, // 정상 응답 그대로 반환 + (error) => { + if (error.response?.status === 401) { + console.error("401 Unauthorized - 로그인 필요"); + // 로그아웃 처리 및 리다이렉트 예제 + // window.location.href = '/login'; + } + return Promise.reject(error); + } +); +export default axiosInstance; diff --git a/app/components/BotNav.tsx b/app/components/BotNav.tsx index 78bc079..57496fe 100644 --- a/app/components/BotNav.tsx +++ b/app/components/BotNav.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ReactNode, useState } from "react"; -import { useRouter } from "next/navigation"; +import { ReactNode } from "react"; +import { useRouter, usePathname } from "next/navigation"; type BotNavProps = { searchButton: ReactNode; @@ -9,15 +9,12 @@ type BotNavProps = { }; export function BotNav({ searchButton, communityButton, mypageButton }: BotNavProps) { - return ( - <> -
    - {searchButton} - {communityButton} - {mypageButton} -
    - +
    + {searchButton} + {communityButton} + {mypageButton} +
    ); } @@ -26,84 +23,91 @@ type BotNavButtonProps = { text?: string; isActive: boolean; onClick: () => void; -} +}; export function BotNavButton({ Image, text, isActive, onClick }: BotNavButtonProps) { return ( - ); } export function AddBotNav() { - const [activeBotNavButton, setActiveBotNavButton] = useState('search'); const router = useRouter(); + const pathname = usePathname(); - const handleButtonClick = (buttonName: string, route: string) => { - setActiveBotNavButton(buttonName); // 상태 업데이트 + const handleButtonClick = (route: string) => { router.push(route); // 페이지 이동 }; return ( handleButtonClick("search", "/search")} - isActive={activeBotNavButton === 'search'} - Image={ - - - - - - - - - - - } />} - - communityButton= - { handleButtonClick('community', "/community")} - isActive={activeBotNavButton === 'community'} - Image={ - - - - } - />} - - mypageButton= - { handleButtonClick('myPage', '/myPage')} - isActive={activeBotNavButton === 'mypage'} - Image={ - - - - - - - - - - - - } - />} - > - - - + searchButton={ + handleButtonClick("/")} + isActive={pathname === "/"} + Image={ + + + + } + /> + } + communityButton={ + location.href = "/community"} //현석 수정함 + isActive={pathname === "/community"} + Image={ + + + + } + /> + } + mypageButton={ + handleButtonClick("/myPage")} + isActive={pathname === "/myPage"} + Image={ + + + + } + /> + } + /> ); } diff --git a/app/components/TempMainPage.tsx b/app/components/TempMainPage.tsx index 7be7b43..010493c 100644 --- a/app/components/TempMainPage.tsx +++ b/app/components/TempMainPage.tsx @@ -3,6 +3,8 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import Image from "next/image"; +import { useUpdateImageStore } from "@/app/types/updateImgStore"; +import { usePostStore } from "@/app/types/postStore"; export default function MainPage() { const [loading, setLoading] = useState(false); @@ -10,65 +12,23 @@ export default function MainPage() { const [selectedCategory, setSelectedCategory] = useState("Top picks"); const router = useRouter(); - const categories = ["Top picks", "Bap", "Myeon", "Snacks", "Cafe"]; + // 현석 추가 작성 + const setImageUrls = usePostStore((state) => state.setImageUrls); + const setUpdateImageUrls = useUpdateImageStore((state) => state.setUpdateImageUrls); + + const categories = ["Top picks", "Rice", "Noodle", "Snacks", "Cafe"]; const foodData: Record = { - "Top picks": [ - "Dakgalbi", - "Fried Chicken", - "Bibimbap with Extra Veggies", - "Tteokbokki", - "Bulgogi", - "Kimchi Stew", - "Samgyeopsal", - "Japchae", - ], - Bap: [ - "Kimchi Fried Rice", - "Bibimbap", - "Bulgogi Rice", - "Gukbap", - "Soy Sauce Egg Rice", - "Mushroom Rice Bowl", - "Omurice", - "Gimbap", - ], - Myeon: [ - "Jajangmyeon", - "Kalguksu", - "Jjamppong", - "Naengmyeon", - "Udon", - "Spicy Cold Noodles", - "Bulgogi Noodles", - "Soy Sauce Noodles", - ], - Snacks: [ - "Hotteok", - "Tteokbokki", - "Corn Dog", - "Gimbap", - "Fish Cake Skewers", - "Sweet Potato Fries", - "Rice Cakes", - "Manduguk", - ], - Cafe: [ - "Espresso", - "Cappuccino", - "Bingsu", - "Macaron", - "Iced Americano", - "Matcha Latte", - "Affogato", - "Cheesecake", - ], + "Top picks": ["Dakgalbi", "Fried Chicken", "Bibimbap", "Tteokbokki", "Bulgogi", "Kimchi Stew", "Samgyeopsal", "Japchae"], + Rice: ["Kimchi Fried Rice", "Bibimbap", "Bulgogi Rice", "Gukbap", "Soy Sauce Egg Rice", "Albap", "Omurice", "Gimbap"], + Noodle: ["Jajangmyeon", "Kalguksu", "Jjamppong", "Naengmyeon", "Mak Guksu", "Spicy Cold Noodles", "Bulgogi Noodles", "Soybean Noodle"], + Snacks: ["Hotteok", "Tteokbokki", "Corn Dog", "Injeolmi", "Fish Cake Skewers", "Sweet Potato Fries", "Songpyeon", "Gamja Jeon"], + Cafe: ["Espresso", "Cappuccino", "Bingsu", "Macaron", "Iced Americano", "Matcha Latte", "Affogato", "Cheesecake"], }; const foodImages: Record = { Dakgalbi: "/images/Dakgalbi.jpg", "Fried Chicken": "/images/Fried Chicken.jpg", - "Bibimbap with Extra Veggies": "/images/Bibimbap.jpg", Tteokbokki: "/images/Tteokbokki.jpeg", Bulgogi: "/images/Bulgogi.jpg", "Kimchi Stew": "/images/kimchi Stew.jpg", @@ -79,23 +39,24 @@ export default function MainPage() { "Bulgogi Rice": "/images/Bulgogi Rice.jpg", Gukbap: "/images/Gukbap.jpg", "Soy Sauce Egg Rice": "/images/Soy Sauce Egg Rice.jpg", - "Mushroom Rice Bowl": "/images/Mushroom Rice.jpeg", + Albap: "/images/Albap.jpg", Omurice: "/images/Omurice.jpg", Gimbap: "/images/Gimbap.jpg", Jajangmyeon: "/images/Jajangmyeon.jpeg", Kalguksu: "/images/Kalguksu.jpeg", Jjamppong: "/images/Jjamppong.jpeg", Naengmyeon: "/images/Naengmyeon.jpg", - Udon: "/images/Udon.jpg", + "Mak Guksu": "/images/Mak Guksu.jpg", "Spicy Cold Noodles": "/images/Spicy Cold Noodles.jpg", "Bulgogi Noodles": "/images/Bulgogi Noodles.jpg", - "Soy Sauce Noodles": "/images/Soy Sauce Noodles.jpg", + "Soybean Noodle": "/images/Soybean Noodle.jpg", Hotteok: "/images/Hotteok.jpg", "Corn Dog": "/images/Corn Dog.jpg", "Fish Cake Skewers": "/images/Fish Cake Skewers.jpg", "Sweet Potato Fries": "/images/Sweet Potato Fries.jpeg", - "Rice Cakes": "/images/Rice Cakes.jpg", - Manduguk: "/images/Manduguk.jpg", + "Songpyeon": "/images/Songpyeon.jpg", + "Injeolmi": "/images/Injeolmi.jpg", + "Gamja Jeon": "/images/Gamja Jeon.jpg", Espresso: "/images/Espresso.jpg", Cappuccino: "/images/Cappuccino.jpg", Bingsu: "/images/Bingsu.jpg", @@ -106,6 +67,10 @@ export default function MainPage() { Cheesecake: "/images/Cheesecake.jpg", }; + const handleFoodClick = (food: string) => { + router.push(`/keyword?query=${encodeURIComponent(food)}`); + }; + useEffect(() => { const token = localStorage.getItem("Authorization"); @@ -160,6 +125,11 @@ export default function MainPage() { const uploadedUrls: string[] = await uploadResponse.json(); const imageUrl = uploadedUrls[0]; + + // 현석 추가 작성 + setUpdateImageUrls(uploadedUrls); + setImageUrls(uploadedUrls); + router.push(`/info?imageUrl=${encodeURIComponent(imageUrl)}`); } catch (error: unknown) { if (error instanceof Error) { @@ -181,42 +151,129 @@ export default function MainPage() { }; return ( -
    -
    -
    -

    Discover by Photo

    +
    + {/* 검색바 */} +
    +
    +
    + {/* 돋보기 아이콘 */} +
    + + + +
    + + {/* 검색 입력란 */} + { + if (event.key === "Enter") { + const query = (event.target as HTMLInputElement).value.trim(); + if (query) { + router.push(`/keyword?query=${encodeURIComponent(query)}`); + } + } + }} + /> +
    +
    +
    + +
    +
    + {/* 텍스트를 좌측 상단으로 이동 */} +
    +

    Discover

    +

    by Photo

    +
    + {/* 버튼을 하단 중앙으로 이동 */}
    -
    -

    Create a Post

    +
    +
    +

    Create

    +

    a Post

    +
    + {/* 카테고리 */}
    -
    -

    Categories

    -
    {categories.map((category, idx) => (
    -
    -
    - {foodData[selectedCategory]?.map((food, idx) => ( -
    -
    - {food} -
    -

    - {food} -

    + {/* 음식 리스트 */} +
    + {foodData[selectedCategory]?.map((food, idx) => ( +
    handleFoodClick(food)} + > +
    + {food}
    - ))} -
    +

    + {food} +

    +
    + ))}
    {error && ( @@ -265,4 +313,4 @@ export default function MainPage() { )}
    ); -} +} \ No newline at end of file diff --git a/app/components/TextButtonForNavWrapper.tsx b/app/components/TextButtonForNavWrapper.tsx new file mode 100644 index 0000000..be8b59c --- /dev/null +++ b/app/components/TextButtonForNavWrapper.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { TextAndIconButton } from "@/app/components/button"; +import { useRouter } from "next/navigation"; + +export default function TextButtonForNavWrapper() { + const router = useRouter(); + + const handleNavigateToPost = () => { + router.push("/community/post"); + }; + //현석 0204 수정 + return ( + + + + } + onclick={handleNavigateToPost} + > + Post + + ); +} diff --git a/app/components/auth/Policy.json b/app/components/auth/Policy.json new file mode 100644 index 0000000..6291d5d --- /dev/null +++ b/app/components/auth/Policy.json @@ -0,0 +1,92 @@ +{ + "privacyPolicy": { + "title": "Privacy Policy", + "sections": [ + { + "heading": "Introduction", + "content": "Our Privacy Policy outlines how we collect, use, and protect your personal information when you use our website and services. By accessing or using our platform, you agree to the terms outlined in this policy." + }, + { + "heading": "Information We Collect", + "content": "We may collect the following types of personal information:", + "list": [ + "Personal Identification Information: This includes your name, email address, phone number, and any other information you provide.", + "Health and Dietary Information: This includes allergies, dietary restrictions, and food preferences that you voluntarily share to enhance your experience on the platform.", + "Religious Preferences: If relevant, you may provide information about your religious dietary practices.", + "User Activity Data: This includes data on your usage of the platform, interactions with content, and communication with other users." + ] + }, + { + "heading": "How We Use Your Information", + "content": "We use the information we collect for the following purposes:", + "list": [ + "To improve the service and personalize your experience.", + "To communicate with you about updates, promotions, or changes to the platform.", + "To ensure that content and recommendations are appropriate for your dietary restrictions, allergies, and preferences.", + "To maintain the security and functionality of the platform." + ] + }, + { + "heading": "How We Protect Your Information", + "content": "We implement a variety of security measures to protect your personal information, including encryption, firewalls, and access controls. We also ensure that only authorized personnel have access to your data." + }, + { + "heading": "Sharing Your Information", + "content": "We do not sell, trade, or rent your personal information to third parties. However, we may share your information with trusted partners or service providers who assist in delivering the service, under strict confidentiality agreements." + }, + { + "heading": "Your Rights", + "content": "You have the right to:", + "list": [ + "Access, update, or delete your personal information.", + "Request that we limit the processing of your data.", + "Opt-out of marketing communications." + ], + "footer": "For more information on your rights and how to exercise them, please contact us at [your contact information]." + } + ] + }, + "termsOfService": { + "title": "Terms of Service", + "sections": [ + { + "heading": "Introduction", + "content": "These Terms of Service govern your use of our website and services. By accessing or using our platform, you agree to comply with these terms and all applicable laws." + }, + { + "heading": "User Obligations", + "content": "As a user, you agree to:", + "list": [ + "Provide accurate and up-to-date information when creating your account.", + "Respect other users' privacy and avoid posting offensive, inappropriate, or harmful content.", + "Not engage in any activity that could harm the platform or violate its security." + ] + }, + { + "heading": "Content and Intellectual Property", + "content": "All content on the platform, including text, images, and software, is the property of the service or its licensors and is protected by intellectual property laws. You are granted a limited, non-exclusive license to use the service, but you may not copy, distribute, or modify the content without permission." + }, + { + "heading": "Prohibited Activities", + "content": "You are prohibited from:", + "list": [ + "Engaging in spamming, harassment, or abusive behavior.", + "Using the service for any unlawful or fraudulent activity.", + "Posting content that infringes on the rights of others, including intellectual property rights." + ] + }, + { + "heading": "Disclaimers and Limitation of Liability", + "content": "We make no representations or warranties regarding the availability, accuracy, or reliability of the platform. The platform is provided \"as is,\" and we disclaim all liability for any damages arising from your use of the service." + }, + { + "heading": "Termination", + "content": "We reserve the right to suspend or terminate your access to the platform if you violate these Terms of Service. You may also terminate your account at any time by contacting us." + }, + { + "heading": "Changes to Terms", + "content": "We may update these Terms of Service at any time. Users will be notified of significant changes, and the latest version will always be available on the platform." + } + ] + } +} \ No newline at end of file diff --git a/app/components/auth/authFunctions.tsx b/app/components/auth/authFunctions.tsx new file mode 100644 index 0000000..da6629b --- /dev/null +++ b/app/components/auth/authFunctions.tsx @@ -0,0 +1,12 @@ + +export function addUserKey(userKey: string) { + localStorage.setItem('userKey', userKey); +} +export function removeUserKey() { + localStorage.removeItem('userKey'); +} + +export function createAuthCookie(Authorization: string) { + const maxAge = 60 * 24 * 60 * 60; // 60일일(초 단위) + document.cookie = `authCookie=${Authorization}; max-age=${maxAge}; path=/; secure; SameSite=Strict`; +} \ No newline at end of file diff --git a/app/components/auth/googleSignupComponents.tsx b/app/components/auth/googleSignupComponents.tsx index 464c566..567ad3f 100644 --- a/app/components/auth/googleSignupComponents.tsx +++ b/app/components/auth/googleSignupComponents.tsx @@ -7,6 +7,7 @@ import axios from "axios"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; +import { addUserKey, createAuthCookie } from "./authFunctions"; type Props = { @@ -39,7 +40,7 @@ function FormatStringArray(input: string[]): string[] { export function DropDownIcon() { return ( - + @@ -195,7 +196,7 @@ export function GoogleSignupStep3() { data: formData, }).then(function (response) { if (response.status === 200) { - setImage(response.data); + setImage(response.data[0]); router.push('/signup/google/step4'); } }).catch(function () { @@ -291,7 +292,7 @@ export function GoogleSignupStep3() { viewBox="-4 -4 24 24" fill="none" > - + @@ -349,9 +350,10 @@ type DropdownProps = { buttonName: string; isMultiSelect?: boolean; onSelect: (selected: string | string[]) => void; + buttonColor?: string; } -export function Dropdown({ options, buttonName, isMultiSelect, onSelect }: DropdownProps) { +export function Dropdown({ options, buttonName, isMultiSelect, onSelect, buttonColor }: DropdownProps) { const [isOpen, setIsOpen] = useState(false); // 드롭다운 열림 상태 const [selectedItems, setSelectedItems] = useState([]); // 선택된 항목 리스트 const [selectedItem, setSelectedItem] = useState(null); // 단일 선택용 @@ -429,15 +431,16 @@ export function Dropdown({ options, buttonName, isMultiSelect, onSelect }: Dropd className="dropdown-item flex" onClick={() => selectItem(option)} > - {option} + {option} {/* 여기에 체크박스 추가 */} {isMultiSelect ? ( - selectItem(option)} - className="dropdown-checkbox" /> - ) : + + selectItem(option)} + /> + ) : ( null ) @@ -455,6 +458,7 @@ export function Dropdown({ options, buttonName, isMultiSelect, onSelect }: Dropd key={item} className="dropdown-badge dropdown-badge-green" onClick={() => removeBadge(item)} + style={{ backgroundColor: buttonColor }} > {item} × @@ -464,6 +468,7 @@ export function Dropdown({ options, buttonName, isMultiSelect, onSelect }: Dropd key={selectedItem} className="dropdown-badge dropdown-badge-green" onClick={() => removeBadge(selectedItem)} + style={{ backgroundColor: buttonColor }} > {selectedItem} × @@ -717,6 +722,7 @@ export function GoogleSignupStep4() { buttonName="Select Your Country" isMultiSelect={false} onSelect={(selected) => setSelectedCountry(selected as string | null)} + buttonColor='#E0E4EB' />
    @@ -741,6 +747,7 @@ export function GoogleSignupStep4() { buttonName="Select Your Chronic Disease" isMultiSelect={true} onSelect={(selected) => setSelectedChronicDisease(selected as string[])} + buttonColor="#FFC4B3" />
    -
    -

    ETC

    -
    +

    ETC

    {etcAllergieList.map((allergie) => ( + + ))}
    diff --git a/app/components/auth/loginComponents.tsx b/app/components/auth/loginComponents.tsx index a4da403..ec03055 100644 --- a/app/components/auth/loginComponents.tsx +++ b/app/components/auth/loginComponents.tsx @@ -2,10 +2,11 @@ import { userLoginStore } from "@/app/types/loginStore"; import axios from 'axios'; import React, { useEffect, useState } from "react"; -import Link from "next/link"; +//import Link from "next/link"; import '@/app/globals.css' import '@/app/(css)/auth.css' import { useRouter } from "next/navigation"; +import { addUserKey, createAuthCookie } from "@/app/components/auth/authFunctions"; @@ -53,15 +54,23 @@ export default function LoginForm() { }) .then(function (response) { if (response.status === 200) { - localStorage.setItem('Authorization', response.headers['authorization']); + const Authorization = response.headers['authorization']; + localStorage.setItem('Authorization', Authorization); + + // 미들웨어를 위한 쿠키 설정 + createAuthCookie(Authorization); + + //userKey 저장 필요 + const userKey = response.data.userKey; + addUserKey(userKey); + - // 로그인 성공시 메인페이지로 router.push('/'); } }) .catch(function () { setErrorMessage('Id or Password is incorrect'); - }) + }) } }; @@ -87,8 +96,8 @@ export default function LoginForm() { value={password} className='auth-placeholder grow text-left' /> - {errorMessage &&
    {errorMessage}
    } - +
    + {errorMessage && + {errorMessage} + } + +
    +
    + {isPrivacyOpen &&
    +
    + {termsData.privacyPolicy.sections.map((section, index) => ( +
    +
    +

    {section.heading}

    +
    +

    {section.content}

    + {section.list && ( +
      + {section.list.map((item, listIndex) => ( +
    • - {item}
    • + ))} +
    + )} + {section.footer &&

    {section.footer}

    } +
    + ))} +
    +
    }
+ {isTermsOpen &&
+
+ {termsData.termsOfService.sections.map((section, index) => ( +
+
+

{section.heading}

+
+

{section.content}

+ {section.list && ( +
    + {section.list.map((item, listIndex) => ( +
  • - {item}
  • + ))} +
+ )} +
+ ))} +
+
}
- {sendMessageVisiblity && (
+ )} + {emailVerifyInputVisible && ( )} + {isTimerActive && ( +
+ Verification code expires in: {Math.floor(timer / 60)}:{timer % 60 < 10 ? "0" : ""} + {timer % 60} minutes +
+ )}
{emailVerifyMessage && }
+ {/* 인증 메시지 처리 넣어줘야함 */}
- {errors?.userId && } + {userIdValidationMessages.map((message, index) => ( + + {message} + + ))} + + {idDuplicateMessage && } + color={messageColor}>}
- {errors?.password && } + {passwordValidationMessages.map((message, index) => ( + + {message} + + ))}
@@ -784,6 +1011,7 @@ export function Dropdown({ options, buttonName, isMultiSelect, onSelect }: Dropd key={selectedItem} className="dropdown-badge dropdown-badge-green" onClick={() => removeBadge(selectedItem)} + style={{ backgroundColor: buttonColor }} > {selectedItem} × @@ -1003,9 +1231,9 @@ export function SignupStep4() { ]; const religionList: string[] = ["Atheism", "Christianity", "Buddhism", "Catholicism", "Islam", "Hinduism"]; - const dietaryPreferences: string[] = ["No food to cover", 'Halal', 'Kosher', "Vegetarian", "Vegan", + const dietaryPreferences: string[] = ['Halal', 'Kosher', "Vegetarian", "Vegan", "Pescatarian", "Low Spice tolerance", "No Alcohol", 'Gluten Free', 'Lactose Free', 'Low Carb']; - const chronicDiseaseList: string[] = ['No Disease', 'Cancer', 'Diabetes', 'Osteoporosis', 'Heart Disease']; + const chronicDiseaseList: string[] = ['Cancer', 'Diabetes', 'Osteoporosis', 'Heart Disease']; const isFormValid = selectedCountry && selectedReligions; @@ -1023,7 +1251,6 @@ export function SignupStep4() { setDietaryPreferences(FormatStringArray(selectedDietaryPreferences)); // 식습관 설정 setChronicDiseaseTypes(FormatStringArray(selectedChronicDisease)); // 만성질환 설정 - console.log(selectedCountry, selectedReligions, selectedDietaryPreferences, selectedChronicDisease); router.push('/signup/step5'); } @@ -1037,6 +1264,7 @@ export function SignupStep4() { buttonName="Select Your Country" isMultiSelect={false} onSelect={(selected) => setSelectedCountry(selected as string | null)} + buttonColor='#E0E4EB' />
@@ -1061,6 +1289,7 @@ export function SignupStep4() { buttonName="Select Your Chronic Disease" isMultiSelect={true} onSelect={(selected) => setSelectedChronicDisease(selected as string[])} + buttonColor="#FFC4B3" />
-

Meat & Dairy

+

Meat & Dairy

{meatAllergieList.map((allergie) => (
-
-

ETC

-
+

ETC

{etcAllergieList.map((allergie) => ( + ) +} + export function SearchButtonForNav() { @@ -100,7 +115,7 @@ export function SearchButtonForNav() {
+ +
+ + ) +} + +export function MainButtonForNav() { + const router = useRouter(); + + const handleClick = () => { + router.push("/"); // 메인 페이지로 이동 + }; + + return ( +
+ + { + Mukpic + } +
+ ) +} + + type TextLogoButtonForNavProps = { children: string; className?: string; + onclick?: () => void; } -export function TextLogoButtonForNav({ children, className }: TextLogoButtonForNavProps) { +export function TextLogoButtonForNav({ children, className, onclick }: TextLogoButtonForNavProps) { return ( - - ) - + ); } type ViewAiResearchButtonProps = { icon?: ReactNode; diff --git a/app/components/community/communityComponents.tsx b/app/components/community/communityComponents.tsx index 87e350a..05a96ed 100644 --- a/app/components/community/communityComponents.tsx +++ b/app/components/community/communityComponents.tsx @@ -4,13 +4,24 @@ import "@/app/globals.css"; import "@/app/(css)/community.css"; import axios from "axios"; import Image from "next/image"; +import { addHours, formatDistanceToNow, parseISO } from "date-fns"; +import { CategorySelectDropdown } from "./postComponents"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { Pagination } from "swiper/modules"; +import 'swiper/css/bundle'; -interface CommunityPost { +export interface CommunityPost { communityKey: number; title: string; content: string; - imageUrls: string[]; // 이미지 URL 배열 + imageUrls: string[]; likeCount: number; + profileImage: string; + userName: string; + createdAt: string; + updatedAt: string; + category: string; + liked: boolean; } interface Pageable { @@ -34,7 +45,7 @@ interface CommunityApiResponse { totalElements: number; last: boolean; // 마지막 페이지 여부 size: number; // 요청당 글 개수 - number: number; // 현재 페이지 번호 + number: number; // 현재 페이지 번호 sort: Sort; // 정렬 정보 numberOfElements: number; // 현재 페이지에서 가져온 게시글 수 first: boolean; // 첫 페이지 여부 @@ -65,7 +76,7 @@ export function ViewAiResearchButton() { return ( ) +} +function ConvertToTitleCase(str: string) { + if (str === 'ETC') return 'ETC'; + if (str === 'ALL') return 'ALL'; + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +} + +export function ViewAiResearchButtonForCarousel() { + return ( + + ) } export function FoodCategoryBadge({ children }: FoodCategoryBadgeProps) { @@ -88,6 +122,70 @@ export function FoodCategoryBadge({ children }: FoodCategoryBadgeProps) { ); } +type CommunityImageProps = { + imageUrls: string[]; + handleImageLoad: () => void; + imageLoaded?: boolean; + +} + +export function CommunityImage({ imageUrls, handleImageLoad }: CommunityImageProps) { + + return ( +
+ img_error + {/* objectfit - cover 할지 contain할지 정해야함 */} + + {/* {imageLoaded && } */} +
+ ); +} + +type CommunityImageCarouselProps = { + imageUrls: string[]; + handleImageLoad: () => void; +} + +const CommunityImageCarousel: React.FC = ({ + imageUrls, + handleImageLoad, +}) => { + return ( + + {imageUrls.map((url, index) => ( + + {/* 이미지 */} + {`Slide + + ))} + + + ); +}; + + + + + // 마이데이터, 게시판에 게시글 리스트 보여주기용 컴포넌트 @@ -96,6 +194,10 @@ export function PostComponents() { const [posts, setPosts] = useState([]); const [page, setPage] = useState(0); const [isLast, setIsLast] = useState(false); + const [category, setCategory] = useState('All'); + const categoryList = ['All', 'Rice', 'Noodle', 'Soup', 'Dessert', 'Streetfood', 'Kimchi', 'ETC']; + const [sortBy, setSortBy] = useState('Latest'); + const sortByList = ['Latest', 'Likes']; // 감지할 마지막 요소 Ref const observerRef = useRef(null); @@ -103,13 +205,15 @@ export function PostComponents() { const fetchPosts = useCallback(() => { if (isLast) return; // 이미 마지막 페이지면 요청하지 않음 + const categoryUpper = category.toUpperCase(); + const sortByLower = sortBy.toLowerCase(); axios .get(`${process.env.NEXT_PUBLIC_ROOT_API}/community`, { params: { - category: "RICE", - sortBy: "latest", + category: categoryUpper, + sortBy: sortByLower, page, - size: 3, + size: 2, }, headers: { Authorization: `${localStorage.getItem('Authorization')}` @@ -124,10 +228,23 @@ export function PostComponents() { } }) .catch((error) => { - console.error("데이터를 가져오는 중 오류가 발생했습니다: catch", error); + console.error("error", error); }); - }, [isLast, page]); // isLast와 page 상태만 의존성으로 사용 + }, [isLast, page, category, sortBy]); // isLast와 page 상태만 의존성으로 사용 + + const handleCategoryChange = (newCategory: string) => { + setCategory(newCategory); // 카테고리 변경 + setPosts([]); // 게시글 초기화 + setPage(0); // 페이지 초기화 + setIsLast(false); // 마지막 페이지 여부 초기화 + } + const handleSortByChange = (newSortBy: string) => { + setSortBy(newSortBy); // 정렬 변경 + setPosts([]); // 게시글 초기화 + setPage(0); // 페이지 초기화 + setIsLast(false); // 마지막 페이지 여부 초기화 + } // 초기 데이터 로드 useEffect(() => { if (page === 0) { @@ -147,11 +264,9 @@ export function PostComponents() { // Intersection Observer 설정 (마지막 요소 감지해서 스크롤 시 추가 데이터) - // 일단 지금은 처음에는 2번 실행됨.. useEffect(() => { if (isLast) return; // 마지막 페이지면 Intersection Observer 설정하지 않음 - console.log('useEffect observer실행행'); const observer = new IntersectionObserver( (entries) => { // 마지막 게시글이 반 정도 보이면 페이지 증가 @@ -181,19 +296,62 @@ export function PostComponents() { }, [isLast, fetchNextPagePosts]); + return ( -
- {posts.map((post) => ( - - ))} - {isLast &&

no more post

} -
-
+ <> +
+
+ + +
+ {posts.length === 0 ? ( + null + ) : ( + + posts.map((post) => ( +
+ {/* 첫 번째 요소에만 CategorySelectDropdown 추가 */} + + +
+ )) + + )} +
+
+
+ {isLast && no more post} +
+ + ); } +type CommunityPostProps = { + post: CommunityPost; + useManyImage: boolean; + currentIndex?: number; + handleNext?: () => void; + handlePrev?: () => void; + +} -export function PostContent({ post }: { post: CommunityPost }) { +export function PostContent({ post, useManyImage }: CommunityPostProps) { const [imageLoaded, setImageLoaded] = useState(false); + const [like, setLike] = useState(post.liked); + const [likeCount, setLikeCount] = useState(post.likeCount); + const imageUrls: string[] = post.imageUrls; const handleImageLoad = () => { setImageLoaded(true); // 이미지가 정상적으로 로드되었음을 확인 @@ -201,7 +359,6 @@ export function PostContent({ post }: { post: CommunityPost }) { const DetailPostHandler = () => { - console.log('게시글 상세보기 페이지로 이동'); //뒤로가기 시 제대로 작동 안하는 것 때문에 임시로 이렇게 해놓음 location.href = `/community/${post.communityKey}`; } @@ -209,93 +366,313 @@ export function PostContent({ post }: { post: CommunityPost }) { // 일단 좋아요 요청 보내는것만. const likeHandler = (event: React.MouseEvent) => { event.stopPropagation(); //부모요소 이벤트 방지(div 클릭시 상세 페이지로 이동하는 것 방지) - axios({ - method: 'post', - url: `${process.env.NEXT_PUBLIC_ROOT_API}/community/like/${post.communityKey}/likes`, - headers: { - Authorization: `${localStorage.getItem('Authorization')}` - } - }).then((response) => { - if (response.status === 200) { - console.log('좋아요 성공'); - } - }).catch((error) => { - console.log('좋아요 실패', error); - }) - - // 좋아요 취소 요청 - // axios({ - // method: 'delete', - // url: `${process.env.NEXT_PUBLIC_ROOT_API}/community/like/${post.communityKey}/likes`, - // headers: { - // Authorization: `${localStorage.getItem('Authorization')}` - // } - // }).then((response) => { - // if(response.status === 200){ - // console.log('좋아요 취소 성공'); - // } - // }).catch((error) => { - // console.log('좋아요 취소소 실패', error); - // }) + + if (like) { + axios({ + method: 'delete', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/community/${post.communityKey}/likes`, + headers: { + Authorization: `${localStorage.getItem('Authorization')}` + } + }).then((response) => { + if (response.status === 200) { + setLike(false); + setLikeCount(likeCount - 1); + } + }).catch((error) => { + console.log('error', error); + }) + } + if (!like) { + axios({ + method: 'post', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/community/${post.communityKey}/likes`, + headers: { + Authorization: `${localStorage.getItem('Authorization')}` + } + }).then((response) => { + if (response.status === 200) { + setLike(true); + setLikeCount(likeCount + 1); + } + }).catch((error) => { + console.log('error', error); + }) + } + + } + + function TimeAgo({ timestamp }: { timestamp: string }) { + const parsedDate = parseISO(timestamp); + // 시간대 보정 (UTC 기준에서 ±9시간 조정) + const adjustedDate = addHours(parsedDate, +9); // UTC+9를 위해 +9 (UTC-9면 -9로 설정) + const timeAgo = formatDistanceToNow(adjustedDate, { addSuffix: true }); + const createTime = timeAgo.replace(/^about\s/, ''); + return createTime; } return (
+ onClick={useManyImage ? undefined : DetailPostHandler}> {/* 프로필 부분 */} -
- - - - UserName +
+
+ 미리보기 +
+ {post.userName}
{/* 이미지 부분 */} -
- img_error - {imageLoaded && } -
+ {useManyImage ? + : + + }
{/* 음식 카테고리 뱃지 입력받아서 넣기 */}
- snacks + + {ConvertToTitleCase(post.category)} +
{/* 컨텐츠 제목 */}
{post.title}
- + {/* 원래는 댓글 수 작성한 공간 */} + {/* - {/* 댓글 수 */} - 12 + 12 */} - + {/* - + */} {/* 몇분 전 등록했는지 등록시간 - 현재시간 */} - 9 days ago +
{/* 좋아요 수 */} - - - - + {like ? + + + + + + + : + + + + + + + } + + {likeCount} +
+
+ {/* 내용 부분 */} +
+ ) +} + +export function DetailPostContent({ post, useManyImage }: CommunityPostProps) { + const [imageLoaded, setImageLoaded] = useState(false); + const [like, setLike] = useState(post.liked); + const [likeCount, setLikeCount] = useState(post.likeCount); + const imageUrls: string[] = post.imageUrls; + + const handleImageLoad = () => { + setImageLoaded(true); // 이미지가 정상적으로 로드되었음을 확인 + }; + + + // 일단 좋아요 요청 보내는것만. + const likeHandler = (event: React.MouseEvent) => { + event.stopPropagation(); //부모요소 이벤트 방지(div 클릭시 상세 페이지로 이동하는 것 방지) + + if (like) { + axios({ + method: 'delete', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/community/${post.communityKey}/likes`, + headers: { + Authorization: `${localStorage.getItem('Authorization')}` + } + }).then((response) => { + if (response.status === 200) { + setLike(false); + setLikeCount(likeCount - 1); + } + }).catch((error) => { + console.log('error', error); + }) + } + if (!like) { + axios({ + method: 'post', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/community/${post.communityKey}/likes`, + headers: { + Authorization: `${localStorage.getItem('Authorization')}` + } + }).then((response) => { + if (response.status === 200) { + setLike(true); + setLikeCount(likeCount + 1); + } + }).catch((error) => { + console.log('error', error); + }) + } + + } + + function TimeAgo({ timestamp }: { timestamp: string }) { + const parsedDate = parseISO(timestamp); + // 시간대 보정 (UTC 기준에서 ±9시간 조정) + const adjustedDate = addHours(parsedDate, +9); // UTC+9를 위해 +9 (UTC-9면 -9로 설정) + const timeAgo = formatDistanceToNow(adjustedDate, { addSuffix: true }); + const createTime = timeAgo.replace(/^about\s/, ''); + return createTime; + } + + return ( +
+ {/* 프로필 부분 */} +
+
+ preview +
+ {post.userName} +
+ {/* 이미지 부분 */} + {useManyImage ? + : + + } +
+ {post.title} +
+
+ {post?.content} +
+ +
+
+ {/* 음식 카테고리 뱃지 입력받아서 넣기 */} +
+
+ + {ConvertToTitleCase(post.category)} + +
+ {/* 컨텐츠 제목 */} +
+ {/* 원래는 댓글 수 작성한 공간 */} + {/* + - - {post.likeCount} + 12 */} + + {/* + + */} + + {/* 몇분 전 등록했는지 등록시간 - 현재시간 */} + +
+
+
+ {/* 좋아요 수 */} + {like ? + + + + + + + : + + + + + + + } + + {likeCount} +
{/* 내용 부분 */} diff --git a/app/components/community/postComponents.tsx b/app/components/community/postComponents.tsx index 0ce9219..1be8ce6 100644 --- a/app/components/community/postComponents.tsx +++ b/app/components/community/postComponents.tsx @@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'react'; import '@/app/(css)/auth.css'; import { usePostStore } from '@/app/types/postStore'; import Image from 'next/image'; +import axios from 'axios'; +import { useUpdateImageStore } from '@/app/types/updateImgStore'; const DropDownIcon = () => { return ( @@ -23,12 +25,14 @@ type CategorySelectDropdownProps = { defaultItem?: string; // 기본 선택 항목 options: string[]; // 드롭다운 옵션 목록 onSelect: (item: string) => void; // 선택된 항목 전달 콜백 + value?: string; }; export function CategorySelectDropdown({ defaultItem, options, onSelect, + value, }: CategorySelectDropdownProps) { const [isOpen, setIsOpen] = useState(false); // 드롭다운 열림 상태 const [selectedItem, setSelectedItem] = useState(defaultItem ? defaultItem : ''); // 기본 선택 항목 @@ -65,7 +69,7 @@ export function CategorySelectDropdown({ onClick={toggleDropdown} className="category-select-button" > - {selectedItem} {/* 버튼에 현재 선택된 항목 표시 */} + {value ? value : selectedItem} {/* 버튼에 현재 선택된 항목 표시 */} {!isOpen ? : } {isOpen && ( @@ -88,36 +92,81 @@ export function CategorySelectDropdown({ ); } -export function AddImage() { +export function AddImageUrl() { + + const imageUrl = usePostStore((state) => state.imageUrls); + const setImageUrl = usePostStore((state) => state.setImageUrls); + const updateImageUrls = useUpdateImageStore((state) => state.updateImageUrls); + const setUpdateImageUrls = useUpdateImageStore((state) => state.setUpdateImageUrls); - const images = usePostStore((state) => state.images); - const setImages = usePostStore((state) => state.setImages); - const imageUrl = usePostStore((state) => state.imageUrl); - const setImageUrl = usePostStore((state) => state.setImageUrl); - // 이미지 추가 핸들러 const handleImageUpload = (event: React.ChangeEvent) => { const files = event.target.files; - if (files) { - setImages([...images, ...Array.from(files)]); // 기존 이미지에 새 이미지를 추가 - } - }; + if (!files) return; - // 이미지 삭제 핸들러 - const removeImage = (index: number) => { - if (index < imageUrl.length) { - setImageUrl(imageUrl.filter((_, i) => i !== index)); // imageUrls에서 삭제 - } else { - setImages(images.filter((_, i) => i !== (index - imageUrl.length))); // images에서 삭제 - } - }; + // 파일 유형 및 크기 제한 + const allowedTypes = ["image/jpg", "image/jpeg", "image/JPG", "image/JPEG", "image/png", "image/PNG"]; + const maxSize = 10 * 1024 * 1024; // 10MB + + // 파일을 배열로 변환하여 순차적으로 검사 + Array.from(files).forEach((file) => { + if (!allowedTypes.includes(file.type)) { + alert('jpg, jpeg, png 파일만 업로드 가능합니다.'); + event.target.value = ''; // 파일 선택 초기화 + return; + } + if (file.size > maxSize) { + alert('10MB 이하의 파일만 업로드 가능합니다.'); + event.target.value = ''; // 파일 선택 초기화 + return; + } + + const formData = new FormData(); + formData.append('file', file); + formData.append('type', 'COMMUNITY'); + // 파일 업로드 요청 보내기 + axios({ + method: 'post', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/images/upload`, + headers: { + Authorization: `${localStorage.getItem('Authorization')}`, + }, + data: formData, + }).then((response) => { + if (response.status === 200) { + // 성공적으로 업로드된 이미지 URL을 상태에 추가 + setImageUrl([...imageUrl, ...response.data]); + setUpdateImageUrls([...updateImageUrls, ...response.data]); + } + }).catch((error) => { + console.log('upload error', error); + }); + }); + }; - // 이미지 상태 확인용용 - // useEffect(() => { - // console.log('현재 이미지 상태 : ', images); - // }, [images]); // images 상태가 변경될 때마다 실행 + const removeImageModify = (index: number) => { + setImageUrl(imageUrl.filter((_, i) => i !== index)); // imageUrls에서 삭제 + axios({ + method: 'delete', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/images/delete`, + headers: { + Authorization: `${localStorage.getItem('Authorization')}`, + }, + params: { + imageUrl: imageUrl[index], + } + }).then((response) => { + + if (response.status === 204) { + setImageUrl(imageUrl.filter((_, i) => i !== index)); // imageUrls에서 삭제 + setUpdateImageUrls(updateImageUrls.filter((url) => url !== imageUrl[index])); + } + }).catch((error) => { + console.log('delete error', error); + }) + } return (
@@ -145,7 +194,6 @@ export function AddImage() { Add image
- {/* 이미지 미리보기 - imageUrls */} {imageUrl.length > 0 && ( <> @@ -159,53 +207,212 @@ export function AddImage() { width={400} height={300} /> - -
- ))} - - )} - - {/* 이미지 미리보기 - images (로컬 파일) */} - {images.length > 0 && ( - <> - {images.map((image, index) => ( -
- {`preview-${index}`} -
))} )}
- ); } +// export function AddImage() { + +// const images = usePostStore((state) => state.images); +// const setImages = usePostStore((state) => state.setImages); +// const imageUrl = usePostStore((state) => state.imageUrls); + + +// // 이미지 추가 핸들러 +// const handleImageUpload = (event: React.ChangeEvent) => { +// const files = event.target.files; +// if (!files) return; + +// // 파일 유형 및 크기 제한 +// const allowedTypes = ["image/jpg", "image/jpeg", "image/JPG", "image/JPEG", "image/png", "image/PNG"]; +// const maxSize = 10 * 1024 * 1024; // 10MB + +// // 파일을 배열로 변환하여 순차적으로 검사 +// Array.from(files).forEach((file) => { +// if (!allowedTypes.includes(file.type)) { +// alert('jpg, jpeg, png 파일만 업로드 가능합니다.'); +// event.target.value = ''; // 파일 선택 초기화 +// return; +// } + +// if (file.size > maxSize) { +// alert('10MB 이하의 파일만 업로드 가능합니다.'); +// event.target.value = ''; // 파일 선택 초기화 +// return; +// } +// }); + +// // 유효한 파일들을 이미지 배열에 추가 + +// setImages([...images, ...Array.from(files)]); +// }; + + +// // 이미지 삭제 핸들러 +// const removeImage = (index: number) => { +// setImages(images.filter((_, i) => i !== (index - imageUrl.length))); // images에서 삭제 +// }; + + + + + +// // 이미지 상태 확인용용 +// // useEffect(() => { +// // console.log('현재 이미지 상태 : ', images); +// // }, [images]); // images 상태가 변경될 때마다 실행 + +// return ( +//
+//
+// {/* 업로드 버튼 */} +// +//
+ +// {/* 이미지 미리보기 - images (로컬 파일) */} +// {images.length > 0 && ( +// <> +// {images.map((image, index) => ( +//
+// {`preview-${index}`} +// +//
+// ))} +// +// )} +//
+ +// ); +// } + export function Write() { - const categoryList: string[] = ["Rice", "Noodle"]; // 드롭다운 옵션 + const categoryList: string[] = ['Rice', 'Noodle', 'Soup', 'Dessert', 'ETC', 'Streetfood', 'Kimchi']; // 드롭다운 옵션 const setCategory = usePostStore((state) => state.setCategory); const title = usePostStore((state) => state.title); const setTitle = usePostStore((state) => state.setTitle); const contents = usePostStore((state) => state.content); const setContents = usePostStore((state) => state.setContent); + const titleMaxLength = 30; + // 내용 입력 최대 글자 수 + const contentMaxLength = 300; + + const contentshandleChange = (e: React.ChangeEvent) => { + if (e?.target.value.length <= contentMaxLength) { + setContents(e.target.value); + } + } + + const titlehandleChange = (e: React.ChangeEvent) => { + setTitle(e.target.value); + } + + + + return ( +
+ {/* 카테고리 드롭다운 */} + setCategory(item.toUpperCase())} // 선택된 항목 category로로 콜백 + /> + {/* 타이틀 */} + + {/* 내용 입력 */} + + + {/* 이미지 추가 */} + +
+ ); +} + +type ModifyProps = { + initTitle: string; + initContent: string; + onDataChange: (data: { title: string, content: string }) => void; +}; + + + +export function Modify({ initTitle, initContent, onDataChange }: ModifyProps) { + + const categoryList: string[] = ['Rice', 'Noodle', 'Soup', 'Dessert', 'ETC', 'Streetfood', 'Kimchi']; // 드롭다운 옵션 + const setCategory = usePostStore((state) => state.setCategory); + const [title, setTitle] = useState(initTitle || ''); + const [content, setContent] = useState(initContent || ''); + // 내용 입력 최대 글자 수 const maxLength = 300; const contentshandleChange = (e: React.ChangeEvent) => { if (e?.target.value.length <= maxLength) { - setContents(e.target.value); + setContent(e.target.value); } } @@ -213,6 +420,9 @@ export function Write() { setTitle(e.target.value); } + useEffect(() => { + onDataChange({ title, content }); + }, [title, content, onDataChange]); return ( @@ -238,7 +448,7 @@ export function Write() { {/* 내용 입력 */} {/* 이미지 추가 */} - +
); } \ No newline at end of file diff --git a/app/components/myPage/myPageComponent.tsx b/app/components/myPage/myPageComponent.tsx new file mode 100644 index 0000000..676b24a --- /dev/null +++ b/app/components/myPage/myPageComponent.tsx @@ -0,0 +1,420 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import styled from 'styled-components'; +import { PostContent as CommunityPostContent, CommunityPost } from '@/app/components/community/communityComponents'; + + +interface UserInfoData { + image: string; + userName: string; + nationality: string; + religion: string; + allergies: string[]; + chronicDiseases: string[]; + dietaryPreferences: string[]; +} + + + +const MyPage = () => { + const [userInfo, setUserInfo] = useState(null); + const [activeTab, setActiveTab] = useState<'liked' | 'myPost'>('liked'); + const [postData, setPostData] = useState([]); + const router = useRouter(); + + // 토큰 가져오기 함수 + const getAuthToken = () => { + const token = localStorage.getItem('Authorization'); + if (!token) { + console.error('Authorization token not found'); + } + return token; + }; + + // 사용자 정보 가져오기 + useEffect(() => { + const fetchUserInfo = async () => { + const token = getAuthToken(); + if (!token) return; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/myinfo`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + setUserInfo(data); + } else { + console.error('Failed to fetch user info:', response.status); + } + } catch (error) { + console.error('Error fetching user info:', error); + } + }; + + fetchUserInfo(); + }, []); + + // 게시글 정보 가져오기 + useEffect(() => { + const fetchPostData = async () => { + const token = getAuthToken(); + if (!token) return; + + const endpoint = + activeTab === 'liked' + ? `${process.env.NEXT_PUBLIC_ROOT_API}/community/likedCommunities` + : `${process.env.NEXT_PUBLIC_ROOT_API}/community/myCommunities`; + + try { + const response = await fetch(endpoint, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + const formattedPosts: CommunityPost[] = data.content.map((post: CommunityPost) => ({ + communityKey: post.communityKey, // number 그대로 유지 + title: post.title, + content: post.content, + imageUrls: post.imageUrls || [], + likeCount: post.likeCount || 0, + profileImage: post.profileImage || '', + userName: post.userName || '', + createdAt: post.createdAt, + updatedAt: post.updatedAt, + category: post.category || 'ALL', + liked: post.liked || false, + })); + + setPostData(formattedPosts); + } else { + console.error('Failed to fetch posts:', response.status); + } + } catch (error) { + console.error('Error fetching posts:', error); + } + }; + + fetchPostData(); + }, [activeTab]); + + return ( + <> + + + router.back()}> + + + + + My Page + router.push('/settings')}> + + + + + + + {/* 사용자 프로필 */} + + + + {userInfo?.userName || 'Loading...'} + {userInfo?.nationality || 'Loading...'} + + {userInfo?.religion && {userInfo.religion}} + + + {userInfo?.allergies?.map((tag, index) => ( + {tag} + ))} + + + {userInfo?.dietaryPreferences?.map((tag, index) => ( + {tag} + ))} + + + {userInfo?.chronicDiseases?.map((tag, index) => ( + {tag} + ))} + + + + + {/* 탭 */} + + setActiveTab('liked')}> + Liked + + setActiveTab('myPost')}> + My Post + + + + {/* 게시글 목록 */} + + {postData.length > 0 ? ( + activeTab === 'liked' ? ( + + {postData.map((post) => ( + + + {post.title} + {post.content} + + ))} + + ) : ( + postData.map((post) => ( + + )) + ) + ) : ( + No posts yet. + )} + + + + + ); +}; + +export default MyPage; + + +const Container = styled.div` + position: relative; + margin: 0 auto; + background-color: #ffffff; + display: flex; + flex-direction: column; + width: 100%; + //height: 52.75rem; + min-height: 300px; /* 최소 높이 설정 */ + justify-content: flex-start; /* 게시글이 없을 때도 상단 유지 */ + align-items: center; + padding-top: 3.5rem; +`; + +const CustomHeader = styled.header` + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background-color: #ffffff; + border-bottom: 1px solid #e5e5e5; + position: absolute; + top: 0; + left: 0; + right: 0; + width: 100%; + max-width: 560px; + margin: 0 auto; + height: 3.5rem; + flex-shrink: 0; +`; + +const BackIcon = styled.div` + cursor: pointer; + position: absolute; + left: 16px; + svg { + width: 28px; + height: 28px; + fill: #1E252F; + } +`; + +const Title = styled.h1` + font-size: 18px; + font-weight: bold; + margin: 0 auto; + color: var(--Gray-gray-900, #0B0B0B); + text-align: center; + font-feature-settings: 'liga' off, 'clig' off; + font-family: SUIT; + font-size: 1.5rem; + font-style: normal; + font-weight: 600; + line-height: normal; +`; + +const SettingsIcon = styled.div` + cursor: pointer; + position: absolute; + right: 16px; +`; + +const ProfileSection = styled.div` + display: flex; + align-items: flex-start; + background-color: #FFF; + border-bottom: 1px solid #E0E5EB; + width: 100%; + padding: 16px; + box-sizing: border-box; + flex-wrap: wrap; +`; + +const Avatar = styled.div` + width: 6.25rem; + height: 6.25rem; + flex-shrink: 0; + border-radius: 50%; + background-color: #ccc; + background-size: cover; + background-position: center; + margin-right: 16px; +`; + +const UserInfo = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; +`; + +const Username = styled.div` + color: var(--Gray-gray-900, #0B0B0B); + font-feature-settings: 'liga' off, 'clig' off; + font-family: SUIT; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; +`; + +const Location = styled.div` + color: #6A7784; + font-feature-settings: 'liga' off, 'clig' off; + font-family: SUIT; + font-size: 0.75rem; + font-style: normal; + font-weight: 500; + line-height: normal; +`; + +const Tags = styled.div` + display: inline-flex; + flex-wrap: wrap; /* 태그가 많으면 줄 바꿈 */ + gap: 0.25rem; + margin-top: 0.38px; + padding: 0.38px 0.25px; + align-items: center; + color: #6A7784; + font-feature-settings: 'liga' off, 'clig' off; + font-family: SUIT; + font-size: 0.75rem; + font-style: normal; + font-weight: 500; + line-height: normal; + +`; + +const Tag = styled.div<{ color: string }>` + background-color: ${(props) => props.color}; + padding: 0.125rem 0.75rem; + border-radius: 3.125rem; + border: 1px solid #1E252F; + color: #1E252F; + font-feature-settings: 'liga' off, 'clig' off; + font-family: SUIT; + font-size: 0.6875rem; + font-style: normal; + font-weight: 500; + line-height: normal; +`; + +const TabBar = styled.div` + display: flex; + justify-content: space-around; + background-color: #fff; + padding: 1.5rem 0 1rem 1.5rem; + font-weight: bold; + border-bottom: 1px solid #ccc; + width: 100%; + height: 3.75rem; + flex-shrink: 0; +`; + +const Tab = styled.div<{ isActive: boolean }>` + cursor: pointer; + color: ${(props) => (props.isActive ? 'blue' : '#000')}; + border-bottom: ${(props) => (props.isActive ? '2px solid black' : 'none')}; + color: #0A0C10; + font-feature-settings: 'liga' off, 'clig' off; + font-family: SUIT; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + +`; + +const PostContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + flex-grow: 1; + min-height: 52.75rem; + justify-content: flex-start; +`; + +const PostsGrid = styled.div` + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75px; + padding: 16px; + width: 100%; + flex-shrink: 0; +`; + +const PostCard = styled.div` + overflow: hidden; + height: 200px; /* 일정한 높이를 설정 */ +`; + + + +const PostImage = styled.img` + width: 97.7%; + height: 97.7%; + flex-shrink: 0; + border-radius: 0.5rem; +`; + + +const PostTitle = styled.h3` + font-size: 14px; + margin: 8px; +`; + +const PostDescription = styled.p` + font-size: 12px; + margin: 0 8px 8px; +`; + + + + +const NoPostsMessage = styled.p` + text-align: center; + color: #666; + margin-top: 20px; /* 기본 여백 추가 */ +`; \ No newline at end of file diff --git a/app/components/setting/AccountDeletion.tsx b/app/components/setting/AccountDeletion.tsx new file mode 100644 index 0000000..adeade3 --- /dev/null +++ b/app/components/setting/AccountDeletion.tsx @@ -0,0 +1,77 @@ +'use client'; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; + +const AccountDeletion = () => { + const router = useRouter(); + const [error, setError] = useState(null); + + const getAuthToken = () => { + const token = localStorage.getItem("Authorization"); + if (!token) { + console.error("Authorization token not found"); + return null; + } + return token; + }; + + const deleteCookies = () => { + document.cookie.split(";").forEach((cookie) => { + const [name] = cookie.split("="); + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + }); + }; + + const handleDeleteAccount = async () => { + const token = getAuthToken(); + if (!token) return; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/deactivate`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + Authorization: `${token}`, + }, + }); + + if (response.ok) { + alert("Your account has been successfully deleted."); + localStorage.removeItem("Authorization"); // 토큰 삭제 + // 현석 추가 + localStorage.removeItem("userKey"); // 유저정보 삭제 + deleteCookies(); //쿠키삭제 + + router.push("/login"); + } else { + console.error("Failed to delete account:", response.status); + setError("Account deletion failed. Please try again."); + } + } catch (error) { + console.error("Error deleting account:", error); + setError("An error occurred. Please try again."); + } + }; + + return ( +
+

+ Once you delete your account, all your data will be permanently erased. + This action cannot be undone. +

+ + {error &&

{error}

} + + +
+ ); +}; + +export default AccountDeletion; diff --git a/app/components/setting/changePassword.tsx b/app/components/setting/changePassword.tsx new file mode 100644 index 0000000..3f5b8c4 --- /dev/null +++ b/app/components/setting/changePassword.tsx @@ -0,0 +1,128 @@ +'use client'; + +import {userSchema } from "@/schemas/auth"; +import { UseVaildate } from "@/app/hooks/useVaildate"; +import { UserValidateError } from "@/app/types/signupValidate"; +import React, { useEffect, useState } from "react"; +import { useSignupStore } from "@/app/types/signupStore"; +import { useRouter } from "next/navigation"; +import { ValidateSpan } from "@/app/components/auth/signupComponents"; + + + + +const ChangePassword = () => { + const { errors, validateField } = UseVaildate(userSchema); + const [isButtonDisabled, setIsButtonDisabled] = useState(true); + const [passwordVisible, setPasswordVisible] = useState(false); + + const password = useSignupStore(state => state.password); + const setpassword = useSignupStore(state => state.setpassword); + + const router = useRouter(); + + const getAuthToken = () => { + const token = localStorage.getItem("Authorization"); + if (!token) { + console.error("Authorization token not found"); + } + return token; +}; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + validateField(name, value); + setpassword(value); // password만 업데이트 + + }; + + + useEffect(() => { + const isFormValid = password.trim(); + const hasErrors = !!errors?.userId || !!errors?.password; + + setIsButtonDisabled(hasErrors || !isFormValid); + }, [errors, password]); + + + const PasswordToggle = () => { + setPasswordVisible(!passwordVisible); + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const token = getAuthToken(); + if (!token) return; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/editPassword`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + Authorization: `${token}`, + }, + body: JSON.stringify({ password }), + }); + + if (response.ok) { + alert("Password has been successfully updated."); + router.push("/settings"); + } else { + console.error("Failed to update password:", response.status); + alert("Failed to update password."); + } + } catch (error) { + console.error("Error updating password:", error); + alert("An error occurred. Please try again."); + } + }; + + + + + return ( + + + +
+ {errors?.password && } +
+ + + + + ); +} + +export default ChangePassword; \ No newline at end of file diff --git a/app/components/setting/edit-healthInfo.tsx b/app/components/setting/edit-healthInfo.tsx new file mode 100644 index 0000000..576dcf6 --- /dev/null +++ b/app/components/setting/edit-healthInfo.tsx @@ -0,0 +1,310 @@ +'use client'; + +import React, { useEffect, useRef, useState } from "react"; +import { useRouter } from "next/navigation"; + + +interface UserInfoData { + allergies: string[]; +} + + + + +const EditHealthInfo = () => { + const [userInfo, setUserInfo] = useState(null); + const [selectedAllergies, setSelectedAllergies] = useState([]); // 알레르기 선택 상태 + const [AllergiesSearch, setAllergiesSearch] = useState(''); // 검색어 상태 + const [showDropdown, setShowDropdown] = useState(false); // 드롭다운 표시 여부 + const inputRef = useRef(null); + + const seaFoodAllergieList = ['Fish', 'Crab', 'Shrimp', 'Squid', 'Abalone', 'Mussel', 'Oyster', 'Shellfish']; + const fruitAllergieList = ['Peach', 'Tomato']; + const nutsAllergieList = ['Buck wheat', 'Wheat', 'Walnut', 'Pine nut', 'Peanut', 'Soybean']; + const meatAllergieList = ['Pork', 'Eggs', 'Milk', 'Chicken', 'Beef']; + const etcAllergieList = ['Sulfurous']; + + const router = useRouter(); + + const getAuthToken = () => { + const token = localStorage.getItem("Authorization"); + if (!token) { + console.error("Authorization token not found"); + } + return token; + }; + + // 사용자 정보 가져오기 + useEffect(() => { + const fetchUserInfo = async () => { + const token = getAuthToken(); + if (!token) return; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/myinfo`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + setUserInfo(data); + setSelectedAllergies(data.allergies || []); + + } else { + console.error('Failed to fetch user info:', response.status); + } + } catch (error) { + console.error('Error fetching user info:', error); + } + }; + + fetchUserInfo(); + }, [router]); + + useEffect(() => { + if (userInfo && userInfo.allergies) { + // 모든 알레르기 값을 대문자로 변환 + setSelectedAllergies(userInfo.allergies.map((allergy) => allergy.toUpperCase())); + } + }, [userInfo]); + + // 모든 알레르기 리스트 합치기기 + const allAllergies = [...seaFoodAllergieList, ...fruitAllergieList, ...nutsAllergieList, ...meatAllergieList, ...etcAllergieList]; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const token = getAuthToken(); + if (!token) return; + + try { + const updatedData: { allergyTypes?: string[] } = {}; + + // 알레르기 변경 확인 + if (JSON.stringify(selectedAllergies) !== JSON.stringify(userInfo?.allergies)) { + updatedData.allergyTypes = selectedAllergies.map((allergy) => allergy.toUpperCase()); + } + + // 변경 사항이 있을 경우에만 PATCH 요청 + if (Object.keys(updatedData).length > 0) { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/editUserInfo`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + Authorization: `${token}`, + }, + body: JSON.stringify(updatedData), + }); + + if (response.ok) { + const data = await response.json(); + setUserInfo(data); + alert("User information has been modified."); + console.log(updatedData); + router.push("/settings"); + } else { + console.error("Failed to update user info:", response.status); + alert("Failed to update user info."); + } + } else { + alert("No changes detected."); + } + } catch (error) { + console.error("Failed to update profile:", error); + alert("Failed to update profile. Please try again."); + } +}; + + // 알레르기 선택 토글 + const toggleAllergy = (allergy: string) => { + setSelectedAllergies(prev => + prev.includes(allergy) ? prev.filter(item => item !== allergy) : [...prev, allergy] + ); + }; + + // 검색에 입력시 상태 업데이트트 + const AllergiesSearchChange = (e: React.ChangeEvent) => { + setAllergiesSearch(e.target.value); + }; + + + //검색창 외 클릭시 드롭다운 숨김 + const handleClickOutside = (e: MouseEvent) => { + if (inputRef.current && e.target instanceof Node && !inputRef.current.contains(e.target)) { + setShowDropdown(false); + } + }; + + + // 검색어에 따라 필터링된 알레르기 리스트 + + useEffect(() => { + document.addEventListener("click", handleClickOutside); + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, []); + + const filteredAllergies = AllergiesSearch + ? allAllergies.filter(allergy => allergy.toLowerCase().includes(AllergiesSearch.toLowerCase())) + : allAllergies; + + return ( +
+

Please edit your allergy

+ + +
+ + {showDropdown && filteredAllergies.length > 0 ? ( +
+
    + {filteredAllergies.map((allergy) => ( +
  • { + toggleAllergy(allergy.toUpperCase()); + }} + > + {allergy} +
  • + ))} +
+
+ ) : null} +
+ + {/* 버튼들을 나열 */} +
+
+

Fruits

+
+
+ {fruitAllergieList.map((allergy) => ( + + ))} +
+
+
+

Sea Food

+
+ {seaFoodAllergieList.map((allergie) => ( + + ))} +
+
+
+

Nuts & Seeds

+
+ {nutsAllergieList.map((allergie) => ( + + ))} +
+
+
+

Meat & Dairy

+
+ {meatAllergieList.map((allergie) => ( + + ))} +
+
+
+
+

ETC

+
+
+ {etcAllergieList.map((allergie) => ( + + ))} +
+
+ +
+ ); +} + +export default EditHealthInfo; \ No newline at end of file diff --git a/app/components/setting/edit-preferenceComponent.tsx b/app/components/setting/edit-preferenceComponent.tsx new file mode 100644 index 0000000..23fdf06 --- /dev/null +++ b/app/components/setting/edit-preferenceComponent.tsx @@ -0,0 +1,450 @@ +'use client'; + +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Dropdown } from "@/app/components/auth/signupComponents"; + + +interface UserInfoData { + nationality: string; + religion: string; + allergies: string[]; + chronicDiseases: string[]; + chronicDiseaseTypes: string[]; + dietaryPreferences: string[]; + } + + const convertToBackendFormat = (value: string | string[] | null): string | string[] | undefined => { + if (!value) return undefined; // null이면 undefined로 변환 + if (Array.isArray(value)) { + return value.map(item => item.replace(/\s+/g, "_")); + } + return value.replace(/\s+/g, "_"); +}; + +const EditPreference = () => { + const [userInfo, setUserInfo] = useState(null); + + const [selectedCountry, setSelectedCountry] = useState(null); // 지역 선택 상태 + const [selectedReligions, setSelectedReligions] = useState(null); // 종교 선택 상태 + const [selectedDietaryPreferences, setSelectedDietaryPreferences] = useState([]); // 식습관 선택 상태 + const [selectedChronicDisease, setSelectedChronicDisease] = useState([]); // 만성질환 선택 상태 + const router = useRouter(); + + + const countryList: string[] = [ + "Afghanistan", + "Albania", + "Algeria", + "Andorra", + "Angola", + "Antigua and Barbuda", + "Argentina", + "Armenia", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Bangladesh", + "Barbados", + "Belarus", + "Belgium", + "Belize", + "Benin", + "Bhutan", + "Bolivia", + "Bosnia and Herzegovina", + "Botswana", + "Brazil", + "Brunei", + "Bulgaria", + "Burkina Faso", + "Burundi", + "Cabo Verde", + "Cambodia", + "Cameroon", + "Canada", + "Central African Republic", + "Chad", + "Chile", + "China", + "Colombia", + "Comoros", + "Congo (Congo-Brazzaville)", + "Costa Rica", + "Croatia", + "Cuba", + "Cyprus", + "Czechia (Czech Republic)", + "Denmark", + "Djibouti", + "Dominica", + "Dominican Republic", + "Ecuador", + "Egypt", + "El Salvador", + "Equatorial Guinea", + "Eritrea", + "Estonia", + "Eswatini (fmr. Swaziland)", + "Ethiopia", + "Fiji", + "Finland", + "France", + "Gabon", + "Gambia", + "Georgia", + "Germany", + "Ghana", + "Greece", + "Grenada", + "Guatemala", + "Guinea", + "Guinea-Bissau", + "Guyana", + "Haiti", + "Holy See", + "Honduras", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iran", + "Iraq", + "Ireland", + "Israel", + "Italy", + "Jamaica", + "Japan", + "Jordan", + "Kazakhstan", + "Kenya", + "Kiribati", + "Korea (North)", + "Korea (South)", + "Kosovo", + "Kuwait", + "Kyrgyzstan", + "Laos", + "Latvia", + "Lebanon", + "Lesotho", + "Liberia", + "Libya", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Marshall Islands", + "Mauritania", + "Mauritius", + "Mexico", + "Micronesia", + "Moldova", + "Monaco", + "Mongolia", + "Montenegro", + "Morocco", + "Mozambique", + "Myanmar (Burma)", + "Namibia", + "Nauru", + "Nepal", + "Netherlands", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "North Macedonia", + "Norway", + "Oman", + "Pakistan", + "Palau", + "Palestine State", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Poland", + "Portugal", + "Qatar", + "Romania", + "Russia", + "Rwanda", + "Saint Kitts and Nevis", + "Saint Lucia", + "Saint Vincent and the Grenadines", + "Samoa", + "San Marino", + "Sao Tome and Principe", + "Saudi Arabia", + "Senegal", + "Serbia", + "Seychelles", + "Sierra Leone", + "Singapore", + "Slovakia", + "Slovenia", + "Solomon Islands", + "Somalia", + "South Africa", + "South Sudan", + "Spain", + "Sri Lanka", + "Sudan", + "Suriname", + "Sweden", + "Switzerland", + "Syria", + "Tajikistan", + "Tanzania", + "Thailand", + "Timor-Leste", + "Togo", + "Tonga", + "Trinidad and Tobago", + "Tunisia", + "Turkey", + "Turkmenistan", + "Tuvalu", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United Kingdom", + "United States of America", + "Uruguay", + "Uzbekistan", + "Vanuatu", + "Venezuela", + "Vietnam", + "Yemen", + "Zambia", + "Zimbabwe" + ]; + const religionList: string[] = ["Atheism", "Christianity", "Buddhism", "Catholicism", "Islam", + "Hinduism"]; + const dietaryPreferences: string[] = ["No food to cover", 'Halal', 'Kosher', "Vegetarian", "Vegan", + "Pescatarian", "Low Spice tolerance", "No Alcohol", 'Gluten Free', 'Lactose Free', 'Low Carb']; + const chronicDiseaseList: string[] = ['No Disease', 'Cancer', 'Diabetes', 'Osteoporosis', 'Heart Disease']; + //const isFormValid = selectedCountry && selectedReligions; + + const handleSelectCountry = (selected: string | string[]) => { + if (typeof selected === "string") { + setSelectedCountry(selected); + } + }; + + const handleSelectReligion = (selected: string | string[]) => { + if (typeof selected === "string") { + setSelectedReligions(selected); + } + }; + + const handleSelectDietaryPreferences = (selected: string | string[]) => { + if (Array.isArray(selected)) { + setSelectedDietaryPreferences([...new Set(selected)]); + } + }; + + const handleSelectChronicDisease = (selected: string | string[]) => { + if (Array.isArray(selected)) { + setSelectedChronicDisease([...new Set(selected)]); + } + }; + + + const getAuthToken = () => { + const token = localStorage.getItem("Authorization"); + if (!token) { + console.error("Authorization token not found"); + } + return token; + }; + + // 사용자 정보 가져오기 + useEffect(() => { + const fetchUserInfo = async () => { + const token = getAuthToken(); + if (!token) return; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/myinfo`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + setUserInfo(data); + // userInfo 값으로 selected* 상태 초기화 + setSelectedCountry(data.nationality || null); + setSelectedReligions(data.religion || null); + setSelectedDietaryPreferences(data.dietaryPreferences || []); + setSelectedChronicDisease(data.chronicDiseases || []); + + } else { + console.error('Failed to fetch user info:', response.status); + } + } catch (error) { + console.error('Error fetching user info:', error); + } + }; + + fetchUserInfo(); + }, [router]); + + + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const token = getAuthToken(); + if (!token) return; + + try { + const updatedData: Partial = {}; + + if (selectedCountry && selectedCountry !== userInfo?.nationality) { + updatedData.nationality = convertToBackendFormat(selectedCountry) as string; + } + if (selectedReligions && selectedReligions !== userInfo?.religion) { + updatedData.religion = convertToBackendFormat(selectedReligions) as string; + } + if (JSON.stringify(selectedDietaryPreferences) !== JSON.stringify(userInfo?.dietaryPreferences)) { + updatedData.dietaryPreferences = convertToBackendFormat(selectedDietaryPreferences) as string []; + } + if (JSON.stringify(selectedChronicDisease) !== JSON.stringify(userInfo?.chronicDiseases)) { + updatedData.chronicDiseaseTypes = convertToBackendFormat(selectedChronicDisease) as string []; + } + + if (Object.keys(updatedData).length > 0) { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/editUserInfo`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + Authorization: `${token}`, + }, + body: JSON.stringify(updatedData), + }); + + if (response.ok) { + const data = await response.json(); + setUserInfo(data); + alert("User information has been modified."); + router.push("/settings"); + } else { + console.error("Failed to update user info:", response.status); + } + } else { + alert("No changes detected."); + } + } catch (error) { + console.error("Failed to update profile:", error); + alert("Failed to update profile. Please try again."); + } + }; + + + return ( + +
+ {/* 국가 (Nationality) */} +
+ + {/* 선택된 값 표시 */} + {selectedCountry && selectedCountry === userInfo?.nationality && ( +
+ + {selectedCountry} + +
+ )} +
+ + {/* 종교 (Religion) */} +
+ + {/* 선택된 값 표시 */} + {selectedReligions && selectedReligions === userInfo?.religion && ( +
+ + {selectedReligions} + +
+ )} +
+ + {/* 식습관 (Dietary Preferences) */} +
+ + {/* 선택된 값 표시 */} + {selectedDietaryPreferences.length > 0 &&JSON.stringify(selectedDietaryPreferences) === JSON.stringify(userInfo?.dietaryPreferences) && ( +
+ {selectedDietaryPreferences.map((item) => ( + + {item} + + ))} +
+ )} +
+ + {/* 만성질환 (Chronic Diseases) */} +
+ + {/* 선택된 값 표시 */} + {selectedChronicDisease.length > 0 &&JSON.stringify(selectedChronicDisease) === JSON.stringify(userInfo?.chronicDiseases) && ( +
{selectedChronicDisease.map((item) => ( + + {item} + + ))}
+ )} +
+ + {/* 저장 버튼 */} + +
+ +); +} + +export default EditPreference; \ No newline at end of file diff --git a/app/components/setting/edit-profile.tsx b/app/components/setting/edit-profile.tsx new file mode 100644 index 0000000..ca2e6f5 --- /dev/null +++ b/app/components/setting/edit-profile.tsx @@ -0,0 +1,449 @@ +'use client'; +import { userNameSchema } from "@/schemas/auth"; +import { UseVaildate } from "@/app/hooks/useVaildate"; +import { UserNameValidateError} from "@/app/types/signupValidate"; +import React, { useCallback, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import styled from 'styled-components'; +import axios from "axios"; +import Image from "next/image"; + + + +interface UserInfoData { + image: string; + userName: string; + email: string; +} + + + +const EditProfile = () => { + const { errors } = UseVaildate(userNameSchema); + const [userInfo, setUserInfo] = useState(null); + const [profileImage] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [newUserName, setNewUserName] = useState(""); + const [uploadedImageUrl, setUploadedImageUrl] = useState(null); + const [canUseUserName, setCanUseUserName] = useState(false); + const [userNameDuplicateMessage, setUserNameDuplicateMessage] = useState(""); + const [messageColor, setMessageColor] = useState("text-gray-500"); + const [isSaveDisabled, setIsSaveDisabled] = useState(true); + const [checkButtonDisabled, setCheckButtonDisabled] = useState(true); + const router = useRouter(); + + const getAuthToken = useCallback(() => { + const token = localStorage.getItem("Authorization"); + if (!token) { + alert("Login is required."); + router.push("/login"); + return null; + } + return token; + }, [router]); + + useEffect(() => { + const fetchUserInfo = async () => { + const token = getAuthToken(); + if (!token) return; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/myinfo`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + setUserInfo(data); + setNewUserName(data.userName); + setUploadedImageUrl(data.image) + } else { + console.error("Failed to fetch user info:", response.status); + } + } catch (error) { + console.error("Error fetching user info:", error); + } + }; + + fetchUserInfo(); + }, [getAuthToken]); + + const CheckUserNameDuplicateHandler = async () => { + axios({ + method: 'get', + url: `${process.env.NEXT_PUBLIC_ROOT_API}/users/checkUserName`, + params: { + userName: newUserName, + email: userInfo?.email + }, + responseType: 'json' + }).then(function (response) { + //이미 있는 경우 true 반환 + if (response.data === true) { + setCanUseUserName(false); + setUserNameDuplicateMessage('This userName is already exist'); + setMessageColor('text-red-500'); + } + else { + setCanUseUserName(true); + setUserNameDuplicateMessage('This userName is available'); + setMessageColor('text-green-500'); + } + }).catch(function (error) { + console.log(error); + }); +} + + // 닉네임 변경 시 상태 초기화 및 Check 버튼 활성화 +const handleUserNameChange = (e: React.ChangeEvent) => { + const value = e.target.value.trim(); // 입력값의 앞뒤 공백 제거 + setNewUserName(value); + + // 닉네임 중복 확인 상태 초기화 + setCanUseUserName(false); // 중복 확인 필요 + setUserNameDuplicateMessage(""); // 메시지 초기화 + + // Check 버튼 활성화 (닉네임이 비어있지 않으면 활성화) + setCheckButtonDisabled(value === ""); // 공백이면 비활성화 +}; + +// 버튼 활성화 상태 관리 +useEffect(() => { + const hasImageChanged = profileImage !== null; // 새 이미지가 선택되었는지 확인 + const hasUserNameChanged = newUserName !== userInfo?.userName; // 닉네임 변경 여부 확인 + + const hasChanges = hasImageChanged || hasUserNameChanged; // 변경 사항이 있는지 확인 + + setIsSaveDisabled( + !hasChanges || // 변경 사항이 없으면 비활성화 + !!errors?.userName || // 닉네임 유효성 검사 오류가 있으면 비활성화 + (!canUseUserName && hasUserNameChanged) // 닉네임 중복 확인 실패 시 비활성화 + ); +}, [newUserName, profileImage, canUseUserName, userInfo, errors]); + + + + +const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const allowedTypes = ["image/jpg", "image/jpeg", "image/png"]; + if (!allowedTypes.includes(file.type)) { + alert("jpg, jpeg, png 파일만 업로드 가능합니다."); + return; + } + + const maxSize = 10 * 1024 * 1024; + if (file.size > maxSize) { + alert("10MB 이하의 파일만 업로드 가능합니다."); + return; + } + + // 로컬 미리보기 URL 생성 + const preview = URL.createObjectURL(file); + setPreviewUrl(preview); + + const token = getAuthToken(); + if (!token) return; + + try { + const formData = new FormData(); + formData.append("file", file); + formData.append("type", "PROFILE"); + + const response = await axios.post( + `${process.env.NEXT_PUBLIC_ROOT_API}/images/upload`, + formData, + { + headers: { Authorization: `${token}` }, + } + ); + + const imageUrl = response.data[0]; + setUploadedImageUrl(imageUrl); + alert("이미지 업로드가 완료되었습니다."); + + } catch (error) { + console.error("이미지 업로드 실패:", error); + alert("이미지 업로드에 실패했습니다. 다시 시도해주세요."); + } +}; + + +useEffect(() => { + const hasImageChanged = uploadedImageUrl !== userInfo?.image; + const hasUserNameChanged = newUserName !== userInfo?.userName; + + const hasChanges = hasImageChanged || hasUserNameChanged; // 변경 사항 여부 확인 + + setIsSaveDisabled( + !hasChanges || // 변경 사항 없으면 비활성화 + !!errors?.userName || // 닉네임 유효성 오류 + (!canUseUserName && hasUserNameChanged) // 닉네임 중복 확인 실패 시 비활성화 + ); +}, [newUserName, uploadedImageUrl, canUseUserName, userInfo, errors]); + +const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const token = getAuthToken(); + if (!token) return; + + try { + const updatedData: Partial = {}; + + if (newUserName && newUserName !== userInfo?.userName) { + updatedData.userName = newUserName; + } + if (uploadedImageUrl && uploadedImageUrl !== userInfo?.image) { + updatedData.image = uploadedImageUrl; + } + + if (Object.keys(updatedData).length > 0) { + const response = await fetch(`${process.env.NEXT_PUBLIC_ROOT_API}/users/editUserInfo`, { + method: 'PATCH', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + body: JSON.stringify(updatedData) + + }); + + if (response.ok) { + const data = await response.json(); + setUserInfo(data); + alert("User information has been modified."); + router.push("/settings"); + } else { + console.error('Failed to update user info:', response.status); + } + } + } catch (error) { + console.error("Failed to update profile:", error); + alert("Failed to update profile. Please try again."); + } +}; + + + return ( + + + {/* 프로필 이미지 업로드 */} + + + + + {/* 닉네임 변경 */} + +
+ + +
+ {userNameDuplicateMessage} +
+ + {/* 저장 버튼 */} + + +
+ ); +}; + +export default EditProfile; + +const StyledForm = styled.form` + margin: 0 auto; + background-color: #ffffff; + display: flex; + flex-direction: column; + width: 100% + min-height: 300px; /* 최소 높이 설정 */ + justify-content: flex-start; /* 게시글이 없을 때도 상단 유지 */ + padding-top: 3.5rem; +`; + +const Label = styled.label` + display: flex; + align-items: center; + gap: 1rem; +`; + +const CameraIcon = styled.div` + position: absolute; + bottom: 0.5rem; /* 프로필 이미지 아래쪽에 고정 */ + right: 0.5rem; /* 프로필 이미지 오른쪽에 고정 */ + z-index: 10; /* 아이콘이 항상 보이게 */ + +`; + +const ProfileImageContainer = styled.div` + width: 6.25rem; + height: 6.25rem; + flex-shrink: 0; + border-radius: 50%; + background-color: #F1F3F6; + position: relative; + cursor: pointer; + overflow: hidden; + display: flex; /* 프로필 이미지를 가운데 정렬 */ + align-items: center; + justify-content: center; + padding-bottom : 3.5 rem; +`; + +const Text = styled.div` +color: #758595; +font-feature-settings: 'liga' off, 'clig' off; +font-family: SUIT; +font-size: 1rem; +font-style: normal; +font-weight: 500; +line-height: 1.375rem; /* 137.5% */ +letter-spacing: -0.01875rem; +`; + +const DefaultIcon = styled.div` + width: 2rem; + height: 2rem; + background-color: #F1F3F6; + border-radius: 50%; +`; + +const UserNameSection = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 15rem; + padding-top: 1.5rem; + width: 22.375rem; + height: 3.375rem; + flex-shrink: 0; + border-radius: 6.25rem; + + + .input-group { + + display: flex; + align-items: center; + background-color:#F1F3F6; + border-radius: 30px; + padding: 0.5rem; + } + + input { + flex-grow: 1; + padding: 0.75rem 1rem ; + border: none; + border-radius: 30px; + font-size: 0.875rem; + color: #6b7280; + background-color: transparent; + + &:focus { + outline: none; + } + + ::placeholder { + color: #9ca3af; + } + } + + button { + padding: 0.5rem 1rem; + background-color:#1E252F; + color: #FFF; + border: none; + border-radius: 30px; + font-size: 0.875rem; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + + + &:disabled { + background-color: #f3f4f6; + color: #9ca3af; + cursor: not-allowed; + } + } +`; + +const Message = styled.p` + font-size: 0.75rem; + color: ${(props) => props.color || "#6b7280"}; + margin-top: 0.5rem; +`; + diff --git a/app/globals.css b/app/globals.css index 1aa2809..0c70799 100644 --- a/app/globals.css +++ b/app/globals.css @@ -12,6 +12,7 @@ button:disabled { } .root-wrapper { + width: 100%; display: flex; flex-direction: column; min-height: 100vh; @@ -19,7 +20,12 @@ button:disabled { justify-content: center; align-items: center; margin: 0 auto; - max-width: 390; /* 확인용으로 일단 390px */ +} + +@media only screen and (min-width: 768px) { + .root-wrapper { + max-width:390px; + } } .main-container { @@ -27,7 +33,7 @@ button:disabled { flex-direction: column; flex-grow: 1; background-color: white; - width: 94%; + width: 100%; justify-content: center; margin: 0 auto; padding: 0; @@ -42,6 +48,7 @@ button:disabled { /* 웹뷰 및 모바일 최적화 */ @media { body { + width: 100%; font-size: 16px; font-family: "SUIT", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; @@ -141,12 +148,88 @@ button:disabled { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 기존 shadow-md */ } +/* 검색바와 Discover by Photo 간격 조정 */ +.mukpic-main-container section:nth-child(1) { + margin-bottom: 3rem; /* 기존 간격보다 조금 더 넓게 설정 */ +} + +/* Section 간 간격 조정 */ +.mukpic-main-container section { + margin-bottom: 2.5rem; /* 전체 섹션 간 간격을 일정하게 설정 */ +} + +/* 메인 컨테이너 하단 여백 줄이기 */ .mukpic-main-container { - max-width: 24.375rem; /* 페이지 전체 넓이로 확장 */ - width: 100%; /* 부모 요소에 맞게 조정 */ - padding: 16px; /* 여백 추가 */ - background-color: #ffffff; /* 흰색 배경 */ + width: 100%; + max-width: 24.375rem; /* 기존 480px에서 420px로 줄여 여백 감소 */ margin: 0 auto; /* 중앙 정렬 */ - border-radius: 8px; /* 둥근 모서리 */ - box-shadow: none; /* 그림자 제거 */ + padding: 8px; /* 내부 여백 */ + background-color: white; /* 흰 배경 */ + + /* 현석 수정 0204 */ + border-radius: 8px; /* 모서리 둥글기 */ + /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 기존 shadow-md */ +} + +/* TempMainPage 전용 스타일 */ +.mukpic-main-container .temp-page-button { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1rem; + font-weight: 600; + transition: all 0.3s ease; +} + +.mukpic-main-container .temp-page-button:hover { + opacity: 0.9; +} + +/* Discover by Photo 및 Create a Post 스타일 */ +.mukpic-main-container .grid-cols-2 > div { + border-radius: 1rem; + transition: transform 0.3s ease; +} + +.mukpic-main-container .grid-cols-2 > div:hover { + transform: translateY(-4px); +} + +/* 모바일(핸드폰) 기본 스타일 - 화면 크기가 768px 미만 */ +@media only screen and (max-width: 767px) { + .root-wrapper { + max-width: 100%; /* 핸드폰에서 화면 전체 너비 사용 */ + padding: 8px; /* 기본 여백 줄이기 */ + margin: 0 auto; + } + + .mukpic-main-container { + max-width: 100%; /* 핸드폰 화면에 맞게 컨테이너 확장 */ + padding: 8px; /* 내부 여백 축소 */ + margin: 0 auto; + } + + .info-main-container { + max-width: 100%; /* 화면 너비에 맞게 확장 */ + padding: 8px; /* 여백 축소 */ + margin: 0 auto; + } } + +/* 태블릿 화면 - 768px 이상 1024px 이하 */ +@media only screen and (min-width: 768px) and (max-width: 1024px) { + .root-wrapper { + max-width: 560px; /* 태블릿에 적합한 너비 조정 */ + padding: 16px; /* 기본 여백 유지 */ + } + + .mukpic-main-container { + max-width: 560px; /* 태블릿에서 적절한 너비 */ + padding: 8px; /* 여백 유지 */ + } + + .info-main-container { + max-width: 560px; /* 태블릿에서 적절한 너비 */ + padding: 16px; /* 여백 유지 */ + } +} \ No newline at end of file diff --git a/app/hooks/axios.ts b/app/hooks/axios.ts new file mode 100644 index 0000000..0d0f702 --- /dev/null +++ b/app/hooks/axios.ts @@ -0,0 +1,36 @@ +import axios from "axios"; + +const axiosInstance = axios.create({ + baseURL: `${process.env.NEXT_PUBLIC_ROOT_API}/v1`, // 기본 API 엔드포인트 설정 + timeout: 5000, // 타임아웃 설정 (선택 사항) + headers: { + "Content-Type": "application/json", + }, +}); + +// 요청 인터셉터 추가 +axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); // 예제: 로컬스토리지에서 토큰 가져오기 + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +// 응답 인터셉터 추가 +axiosInstance.interceptors.response.use( + (response) => response, // 정상 응답 그대로 반환 + (error) => { + if (error.response?.status === 401) { + console.error("401 Unauthorized - 로그인 필요"); + // 로그아웃 처리 및 리다이렉트 예제 + // window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +export default axiosInstance; diff --git a/app/types/postStore.ts b/app/types/postStore.ts index 3cdc286..ca95f4a 100644 --- a/app/types/postStore.ts +++ b/app/types/postStore.ts @@ -1,29 +1,55 @@ import { create } from "zustand"; interface PostState { + communityKey: number; title: string; content: string; - images: File[]; + imageUrls: string[]; // imageUrls로 통일 + likeCount: number; + profileImage: string; + userName: string; + createdAt: string; + updatedAt: string; category: string; - imageUrl: string[]; + liked: boolean; + images: File[]; setTitle: (title: string) => void; setContent: (content: string) => void; - setImages: (images: File[]) => void; - setImageUrl: (imageUrl: string[]) => void; + setImageUrls: (imageUrls: string[]) => void; + setLikeCount: (likeCount: number) => void; + setProfileImage: (profileImage: string) => void; + setUserName: (userName: string) => void; + setCreatedAt: (createdAt: string) => void; + setUpdatedAt: (updatedAt: string) => void; setCategory: (category: string) => void; + setLiked: (liked: boolean) => void; + setImages: (images: File[]) => void; } export const usePostStore = create((set) => ({ + communityKey: 0, title: "", content: "", - images: [], + imageUrls: [], // imageUrls로 일관성 있게 사용 + likeCount: 0, + profileImage: "", + userName: "", + createdAt: "", + updatedAt: "", category: "", - imageUrl: [], + liked: false, + images: [], setTitle: (title: string) => set({ title }), setContent: (content: string) => set({ content }), + setImageUrls: (imageUrls: string[]) => set({ imageUrls }), // imageUrls로 변경 + setLikeCount: (likeCount: number) => set({ likeCount }), + setProfileImage: (profileImage: string) => set({ profileImage }), + setUserName: (userName: string) => set({ userName }), + setCreatedAt: (createdAt: string) => set({ createdAt }), + setUpdatedAt: (updatedAt: string) => set({ updatedAt }), + setCategory: (category: string) => set({ category }), // 중복된 setCategory 제거 + setLiked: (liked: boolean) => set({ liked }), setImages: (images: File[]) => set({ images }), - setCategory: (category: string) => set({ category }), - setImageUrl: (imageUrl: string[]) => set({ imageUrl }), })); diff --git a/app/types/updateImgStore.ts b/app/types/updateImgStore.ts new file mode 100644 index 0000000..6cc8274 --- /dev/null +++ b/app/types/updateImgStore.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface updateImageStore { + updateImageUrls: string[]; + setUpdateImageUrls: (imageUrls: string[]) => void; +} + +export const useUpdateImageStore = create((set) => ({ + updateImageUrls: [], + setUpdateImageUrls: (updateImageUrls) => set({ updateImageUrls }), +})); diff --git a/middleware.ts b/middleware.ts index 1736bbf..31ef216 100644 --- a/middleware.ts +++ b/middleware.ts @@ -2,21 +2,36 @@ import { NextRequest, NextResponse } from "next/server"; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; - // const token = request.cookies.has("authToken"); + console.log(pathname); + // const token = request.cookies.get("authCookie"); - //로그인이 필요한 페이지로 리다이렉트 개발중이라 임시로 주석처리 + // // OAuth 콜백 URL은 미들웨어에서 제외 + // if (pathname.startsWith("/auth/google/callback")) { + // return NextResponse.next(); + // } - // if (!token) { + // // 토큰이 없을 경우 로그인과 회원가입 페이지 외의 페이지에는 접근할 수 없음 + // if ( + // !token && + // !pathname.startsWith("/login") && + // !pathname.startsWith("/signup") + // ) { // const loginUrl = new URL("/login", request.url); // 로그인 페이지 URL 생성 - // loginUrl.searchParams.set("redirect", pathname); // 리다이렉트 후 돌아올 경로 추가 - // console.log("요청 거부됨", loginUrl); // return NextResponse.redirect(loginUrl); // } - console.log("요청 허용됨", pathname); + // // 토큰이 있을 경우 login과 signup 페이지로 접근할 수 없게 함 + // if ( + // token && + // (pathname.startsWith("/login") || pathname.startsWith("/signup")) + // ) { + // return NextResponse.redirect(new URL("/", request.url)); // 메인 페이지로 리디렉션 + // } + return NextResponse.next(); } export const config = { - matcher:[ '/((?!_next/static|_next/image|favicon.ico|login|signup).*)',], + matcher: ["/((?!_next/static|_next/image|favicon.ico|auth/google/callback).*)", + ], }; diff --git a/package-lock.json b/package-lock.json index 2d9abae..defd995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,14 @@ "dependencies": { "@types/classnames": "^2.3.0", "axios": "^1.7.9", + "date-fns": "^4.1.0", "js-cookie": "^3.0.5", "next": "^15.1.4", "react": "^18.3.1", "react-dom": "^18.3.1", "run": "^1.5.0", "styled-components": "^6.1.14", + "swiper": "^11.2.1", "zod": "^3.24.1", "zustand": "^5.0.3" }, @@ -121,13 +123,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -136,9 +138,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -173,9 +175,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", + "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", "dev": true, "license": "MIT", "engines": { @@ -183,9 +185,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -193,12 +195,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { @@ -704,9 +707,9 @@ } }, "node_modules/@next/env": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz", - "integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz", + "integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -720,9 +723,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz", - "integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz", + "integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==", "cpu": [ "arm64" ], @@ -736,9 +739,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz", - "integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz", + "integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==", "cpu": [ "x64" ], @@ -752,9 +755,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz", - "integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz", + "integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==", "cpu": [ "arm64" ], @@ -768,9 +771,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz", - "integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz", + "integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==", "cpu": [ "arm64" ], @@ -784,9 +787,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz", - "integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz", + "integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==", "cpu": [ "x64" ], @@ -800,9 +803,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz", - "integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz", + "integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==", "cpu": [ "x64" ], @@ -816,9 +819,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz", - "integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz", + "integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==", "cpu": [ "arm64" ], @@ -832,9 +835,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz", - "integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz", + "integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==", "cpu": [ "x64" ], @@ -914,9 +917,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", - "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.5.tgz", + "integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==", "dev": true, "license": "MIT" }, @@ -977,9 +980,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz", - "integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==", + "version": "20.17.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.16.tgz", + "integrity": "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==", "dev": true, "license": "MIT", "dependencies": { @@ -987,9 +990,9 @@ } }, "node_modules/@types/react": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.2.tgz", - "integrity": "sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg==", + "version": "19.0.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", + "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -997,9 +1000,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz", - "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==", + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", + "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1025,21 +1028,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", - "integrity": "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz", + "integrity": "sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/type-utils": "8.18.2", - "@typescript-eslint/utils": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/type-utils": "8.22.0", + "@typescript-eslint/utils": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1055,16 +1058,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", - "integrity": "sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.22.0.tgz", + "integrity": "sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "debug": "^4.3.4" }, "engines": { @@ -1080,14 +1083,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz", - "integrity": "sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz", + "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2" + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1098,16 +1101,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz", - "integrity": "sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz", + "integrity": "sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/utils": "8.18.2", + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/utils": "8.22.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1122,9 +1125,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.2.tgz", - "integrity": "sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", + "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", "dev": true, "license": "MIT", "engines": { @@ -1136,20 +1139,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz", - "integrity": "sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz", + "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1173,9 +1176,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -1183,7 +1186,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -1219,16 +1222,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", - "integrity": "sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz", + "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2" + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1243,13 +1246,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz", - "integrity": "sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz", + "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/types": "8.22.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1538,6 +1541,16 @@ "dev": true, "license": "MIT" }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1672,9 +1685,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -1795,9 +1808,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", "funding": [ { "type": "opencollective", @@ -2110,6 +2123,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2240,9 +2263,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.76", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", - "integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==", + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", "dev": true, "license": "ISC" }, @@ -2268,9 +2291,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.7.tgz", - "integrity": "sha512-OygGC8kIcDhXX+6yAZRGLqwi2CmEXCbLQixeGUgYeR+Qwlppqmo7DIDr8XibtEBZp+fJcoYpoatp5qwLMEdcqQ==", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, "license": "MIT", "dependencies": { @@ -2285,10 +2308,11 @@ "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -2309,9 +2333,12 @@ "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", + "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -2378,9 +2405,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -2391,15 +2418,16 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -2457,19 +2485,19 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", + "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", + "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/js": "9.19.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", @@ -2603,9 +2631,9 @@ } }, "node_modules/eslint-import-resolver-typescript/node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -2613,7 +2641,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -2745,9 +2773,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", - "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==", + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2971,9 +2999,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", "dev": true, "license": "ISC", "dependencies": { @@ -3065,13 +3093,19 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", + "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/foreground-child": { @@ -3176,22 +3210,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3200,6 +3234,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -3219,9 +3267,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, "license": "MIT", "dependencies": { @@ -3530,13 +3578,17 @@ "optional": true }, "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3702,13 +3754,16 @@ } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3930,17 +3985,17 @@ "license": "ISC" }, "node_modules/iterator.prototype": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", - "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", "has-symbols": "^1.1.0", - "reflect.getprototypeof": "^1.0.8", "set-function-name": "^2.0.2" }, "engines": { @@ -4289,12 +4344,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.1.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz", - "integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==", + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz", + "integrity": "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==", "license": "MIT", "dependencies": { - "@next/env": "15.1.4", + "@next/env": "15.1.6", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -4309,14 +4364,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.4", - "@next/swc-darwin-x64": "15.1.4", - "@next/swc-linux-arm64-gnu": "15.1.4", - "@next/swc-linux-arm64-musl": "15.1.4", - "@next/swc-linux-x64-gnu": "15.1.4", - "@next/swc-linux-x64-musl": "15.1.4", - "@next/swc-win32-arm64-msvc": "15.1.4", - "@next/swc-win32-x64-msvc": "15.1.4", + "@next/swc-darwin-arm64": "15.1.6", + "@next/swc-darwin-x64": "15.1.6", + "@next/swc-linux-arm64-gnu": "15.1.6", + "@next/swc-linux-arm64-musl": "15.1.6", + "@next/swc-linux-x64-gnu": "15.1.6", + "@next/swc-linux-x64-musl": "15.1.6", + "@next/swc-win32-arm64-msvc": "15.1.6", + "@next/swc-win32-x64-msvc": "15.1.6", "sharp": "^0.33.5" }, "peerDependencies": { @@ -4547,6 +4602,24 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4693,9 +4766,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "dev": true, "funding": [ { @@ -4713,7 +4786,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4956,19 +5029,19 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", - "integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "dunder-proto": "^1.0.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" }, "engines": { @@ -4979,15 +5052,17 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "set-function-name": "^2.0.2" }, "engines": { @@ -5107,6 +5182,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -5135,9 +5227,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", "devOptional": true, "license": "ISC", "bin": { @@ -5181,6 +5273,21 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -5753,6 +5860,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swiper": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.2.tgz", + "integrity": "sha512-FmAN6zACpVUbd/1prO9xQ9gKo9cc6RE2UKU/z4oXtS8fNyX4sdOW/HHT/e444WucLJs0jeMId6WjdWM2Lrs8zA==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", @@ -5792,9 +5918,9 @@ } }, "node_modules/tailwindcss/node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -5802,7 +5928,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -5868,16 +5994,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-interface-checker": { @@ -5998,9 +6124,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6038,9 +6164,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "dev": true, "funding": [ { @@ -6059,7 +6185,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6295,9 +6421,9 @@ } }, "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 8b1fe00..0f242e3 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,14 @@ "dependencies": { "@types/classnames": "^2.3.0", "axios": "^1.7.9", + "date-fns": "^4.1.0", "js-cookie": "^3.0.5", "next": "^15.1.4", "react": "^18.3.1", "react-dom": "^18.3.1", "run": "^1.5.0", "styled-components": "^6.1.14", + "swiper": "^11.2.1", "zod": "^3.24.1", "zustand": "^5.0.3" }, diff --git a/public/images/Albap.jpg b/public/images/Albap.jpg new file mode 100644 index 0000000..527fcd1 Binary files /dev/null and b/public/images/Albap.jpg differ diff --git a/public/images/Gamja Jeon.jpg b/public/images/Gamja Jeon.jpg new file mode 100644 index 0000000..5954d63 Binary files /dev/null and b/public/images/Gamja Jeon.jpg differ diff --git a/public/images/Injeolmi.jpg b/public/images/Injeolmi.jpg new file mode 100644 index 0000000..a9520f2 Binary files /dev/null and b/public/images/Injeolmi.jpg differ diff --git a/public/images/Mak Guksu.jpg b/public/images/Mak Guksu.jpg new file mode 100644 index 0000000..8159617 Binary files /dev/null and b/public/images/Mak Guksu.jpg differ diff --git a/public/images/Manduguk.jpg b/public/images/Manduguk.jpg deleted file mode 100644 index fed7a32..0000000 Binary files a/public/images/Manduguk.jpg and /dev/null differ diff --git a/public/images/Mushroom Rice.jpeg b/public/images/Mushroom Rice.jpeg deleted file mode 100644 index f59fa8f..0000000 Binary files a/public/images/Mushroom Rice.jpeg and /dev/null differ diff --git a/public/images/Rice Cakes.jpg b/public/images/Rice Cakes.jpg deleted file mode 100644 index a093800..0000000 Binary files a/public/images/Rice Cakes.jpg and /dev/null differ diff --git a/public/images/Songpyeon.jpg b/public/images/Songpyeon.jpg new file mode 100644 index 0000000..79abb74 Binary files /dev/null and b/public/images/Songpyeon.jpg differ diff --git a/public/images/Soy Sauce Noodles.jpg b/public/images/Soy Sauce Noodles.jpg deleted file mode 100644 index f867b51..0000000 Binary files a/public/images/Soy Sauce Noodles.jpg and /dev/null differ diff --git a/public/images/Soybean Noodle.jpg b/public/images/Soybean Noodle.jpg new file mode 100644 index 0000000..48e5dec Binary files /dev/null and b/public/images/Soybean Noodle.jpg differ diff --git a/public/images/Udon.jpg b/public/images/Udon.jpg deleted file mode 100644 index 0b8babf..0000000 Binary files a/public/images/Udon.jpg and /dev/null differ diff --git a/schemas/auth.ts b/schemas/auth.ts index 477a35d..180565d 100644 --- a/schemas/auth.ts +++ b/schemas/auth.ts @@ -38,7 +38,7 @@ export const userNameSchema = z.object({ userName: z .string() .min(2, { message: "User name must be at least 2 characters." }) - .max(10, { message: "User name must be at most 20 characters." }) + .max(12, { message: "User name must be at most 12 characters." }) .refine((val) => !/^\d/.test(val), { message: "User name cannot start with a number.", })