diff --git a/next.config.ts b/next.config.ts
index 996fbd9..0ecd118 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,13 +1,14 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
+
// TurboPack 설정
experimental: {
turbo: {
rules: {
- '.svg': {
+ '*.svg': {
loaders: ['@svgr/webpack'],
- as: '.js',
+ as: '*.js',
},
},
},
@@ -20,11 +21,11 @@ const nextConfig: NextConfig = {
config.module.rules.push(
{
...fileLoaderRule,
- test: /.svg$/i,
+ test: /\.svg$/i,
resourceQuery: /url/,
},
{
- test: /.svg$/i,
+ test: /\.svg$/i,
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
use: [
@@ -38,7 +39,7 @@ const nextConfig: NextConfig = {
],
}
);
- fileLoaderRule.exclude = /.svg$/i;
+ fileLoaderRule.exclude = /\.svg$/i;
return config;
},
};
diff --git a/package-lock.json b/package-lock.json
index a2e3b32..0b1c61b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,13 @@
"version": "0.1.0",
"license": "ISC",
"dependencies": {
+ "class-variance-authority": "^0.7.1",
"gsap": "^3.13.0",
+ "lottie-react": "^2.4.1",
"next": "15.5.3",
"react": "19.1.0",
- "react-dom": "19.1.0"
+ "react-dom": "19.1.0",
+ "react-hot-toast": "^2.6.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -4521,6 +4524,18 @@
"node": ">=18"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@@ -4564,7 +4579,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4786,7 +4800,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -6065,6 +6078,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/goober": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
+ "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -7251,6 +7273,25 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lottie-react": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/lottie-react/-/lottie-react-2.4.1.tgz",
+ "integrity": "sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==",
+ "license": "MIT",
+ "dependencies": {
+ "lottie-web": "^5.10.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/lottie-web": {
+ "version": "5.13.0",
+ "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz",
+ "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==",
+ "license": "MIT"
+ },
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -8030,6 +8071,23 @@
"react": "^19.1.0"
}
},
+ "node_modules/react-hot-toast": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
+ "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.3",
+ "goober": "^2.1.16"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/package.json b/package.json
index cc5c7a0..3458d02 100644
--- a/package.json
+++ b/package.json
@@ -18,10 +18,13 @@
]
},
"dependencies": {
+ "class-variance-authority": "^0.7.1",
"gsap": "^3.13.0",
+ "lottie-react": "^2.4.1",
"next": "15.5.3",
"react": "19.1.0",
- "react-dom": "19.1.0"
+ "react-dom": "19.1.0",
+ "react-hot-toast": "^2.6.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/public/file.svg b/public/file.svg
deleted file mode 100644
index 004145c..0000000
--- a/public/file.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/globe.svg b/public/globe.svg
deleted file mode 100644
index 567f17b..0000000
--- a/public/globe.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/logo.svg b/public/logo.svg
new file mode 100644
index 0000000..e664442
--- /dev/null
+++ b/public/logo.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/logoDark.svg b/public/logoDark.svg
new file mode 100644
index 0000000..709a21b
--- /dev/null
+++ b/public/logoDark.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28..0000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index 7705396..0000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/window.svg b/public/window.svg
deleted file mode 100644
index b2b2a44..0000000
--- a/public/window.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/@store/store.ts b/src/@store/store.ts
deleted file mode 100644
index b9b9f5c..0000000
--- a/src/@store/store.ts
+++ /dev/null
@@ -1 +0,0 @@
-// zustand store
diff --git a/src/api/test.tsx b/src/api/test.tsx
deleted file mode 100644
index 5281734..0000000
--- a/src/api/test.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-function test() {
- return
test
;
-}
-export default test;
diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx
index 804c93f..84390d4 100644
--- a/src/app/community/page.tsx
+++ b/src/app/community/page.tsx
@@ -1,4 +1,37 @@
-function page() {
- return page
;
+import CommunityFilter from '@/shared/components/community/CommunityFilter';
+import CommunityHeader from '@/shared/components/community/CommunityHeader';
+import CommunityTab from '@/shared/components/community/CommunityTab';
+import PostCard from '@/shared/components/community/PostCard';
+import WriteBtn from '@/shared/components/community/WriteBtn';
+
+function Page() {
+ return (
+
+
+
+ );
}
-export default page;
+export default Page;
diff --git a/src/app/design-system/page.tsx b/src/app/design-system/page.tsx
new file mode 100644
index 0000000..ad3e5cb
--- /dev/null
+++ b/src/app/design-system/page.tsx
@@ -0,0 +1,164 @@
+'use client';
+
+import Button from '@/shared/components/button/Button';
+import TextButton from '@/shared/components/button/TextButton';
+import Input from '@/shared/components/InputBox/Input';
+import { useState } from 'react';
+import { customToast } from '@/shared/components/toast/CustomToastUtils';
+import ModalLayout from '@/shared/components/modalPop/ModalLayout';
+import ConfirmPop from '@/shared/components/modalPop/ConfirmPop';
+import SelectBox from '@/shared/components/InputBox/SelectBox';
+import LikeBtn from '@/shared/components/like/LikeBtn';
+import Share from '@/shared/components/share/Share';
+import Keep from '@/shared/components/keep/Keep';
+import Spinner from '@/shared/components/spinner/Spinner';
+
+function Page() {
+ const [isModalOpen, setModalOpen] = useState(false);
+ const [isConfirmOpen, setConfirmOpen] = useState(false);
+
+ return (
+
+ {/* 페이지 제목 */}
+
Design System
+
+ {/* Form 영역 */}
+
+
+ {/* Button */}
+
+
Button
+
+
+
Button
+
+
+
+
+
+
+
+ 텍스트 버튼
+ 텍스트 버튼
+
+
+
+ {/* popup */}
+
+
popup
+
+
+
toast
+
+
+
+
+
+
+
+
+
+
+
popup
+ {/* 모달 열기 버튼 */}
+
+
+
+
setModalOpen(false)}
+ title="제목"
+ description="설명"
+ buttons={
+ <>
+
+ >
+ }
+ >
+ 모달팝업 내용
+
+
+
setConfirmOpen(false)}
+ title="Confirm제목"
+ description="설명"
+ />
+
+
+
+
+
Icons
+
+
like
+
+
+
+
Share
+
+
+
+
+
keep
+
+
+
+
+
+
Spinner
+
+
+
+ );
+}
+export default Page;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9cc8854..16084e3 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,9 @@
import type { Metadata } from 'next';
-import '../styles/global.css';
-
+import '@/shared/styles/global.css';
+import { Toaster } from 'react-hot-toast';
+import Header from '@/shared/components/header/Header';
+import FooterWrapper from '@/shared/components/footer/FooterWrapper';
+import ScrollTopBtnWrapper from '@/shared/components/scrollTop/ScrollTopBtnWrapper';
export const metadata: Metadata = {
title: 'SSOUL',
description: '칵테일을 좋아하는 사람들을 위한 서비스',
@@ -13,7 +16,28 @@ export default function RootLayout({
}>) {
return (
- {children}
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/app/login/SocialLogin.tsx b/src/app/login/SocialLogin.tsx
new file mode 100644
index 0000000..1d60cbf
--- /dev/null
+++ b/src/app/login/SocialLogin.tsx
@@ -0,0 +1,56 @@
+'use client';
+
+import Naver from '@/shared/assets/icons/naver.svg';
+import Kakao from '@/shared/assets/icons/kakao.svg';
+import Google from '@/shared/assets/icons/google.svg';
+import tw from '@/shared/utills/tw';
+import { useAuthStore } from '@/shared/@store/auth';
+
+function SocialLogin() {
+ const { loginWithProvider } = useAuthStore();
+
+ const socialButtons = [
+ {
+ id: 'naver',
+ label: 'Naver 로그인',
+ icon: ,
+ style: 'bg-[#00C73C]',
+ },
+ {
+ id: 'kakao',
+ label: 'Kakao 로그인',
+ icon: ,
+ style: 'bg-[#FEE500] text-primary',
+ },
+ {
+ id: 'google',
+ label: 'Google 로그인',
+ icon: ,
+ style: 'bg-[#EBEBEB] text-primary',
+ },
+ ];
+
+ // TODO: 백엔드 연동 로직 구현 필요
+ const handleLogin = (id: string) => {
+ loginWithProvider(id as 'naver' | 'kakao' | 'google');
+ };
+
+ return (
+ <>
+
+ {socialButtons.map(({ id, label, icon, style }) => (
+
+ ))}
+
+ >
+ );
+}
+export default SocialLogin;
diff --git a/src/app/login/first-user/page.tsx b/src/app/login/first-user/page.tsx
new file mode 100644
index 0000000..7f902a4
--- /dev/null
+++ b/src/app/login/first-user/page.tsx
@@ -0,0 +1,7 @@
+import LoginRedirectHandler from '@/shared/components/auth/LoginRedirectHandler';
+
+function Page() {
+ return ;
+}
+
+export default Page;
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
new file mode 100644
index 0000000..d1e4fb8
--- /dev/null
+++ b/src/app/login/page.tsx
@@ -0,0 +1,36 @@
+import Image from 'next/image';
+import type { Metadata } from 'next';
+import loginBg from '@/shared/assets/images/login_bg.webp';
+import SocialLogin from './SocialLogin';
+
+export const metadata: Metadata = {
+ title: 'SSOUL | 로그인',
+ description: '칵테일을 좋아하는 사람들을 위한 서비스 로그인 페이지',
+};
+
+function Page() {
+ return (
+
+
+
+
+
+
+
로그인
+
3초 로그인으로 SSOUL을 가볍게 즐겨보세요🍸
+
+
+
+
+ );
+}
+export default Page;
diff --git a/src/app/login/success/page.tsx b/src/app/login/success/page.tsx
new file mode 100644
index 0000000..15766eb
--- /dev/null
+++ b/src/app/login/success/page.tsx
@@ -0,0 +1,6 @@
+import LoginRedirectHandler from '@/shared/components/auth/LoginRedirectHandler';
+
+function Page() {
+ return ;
+}
+export default Page;
diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx
index 804c93f..3b9d3ac 100644
--- a/src/app/mypage/page.tsx
+++ b/src/app/mypage/page.tsx
@@ -1,4 +1,4 @@
-function page() {
- return page
;
+function Page() {
+ return mypage
;
}
-export default page;
+export default Page;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 54fd584..14d51b2 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,9 +1,7 @@
export default function Home() {
return (
-
-
이게 기본
-
this is Serif
-
테스트입니다
+
+
메인페이지
);
}
diff --git a/src/app/recipe/[id]/page.tsx b/src/app/recipe/[id]/page.tsx
new file mode 100644
index 0000000..9d89ac4
--- /dev/null
+++ b/src/app/recipe/[id]/page.tsx
@@ -0,0 +1,10 @@
+import StarBg from '@/shared/components/starBg/StarBg';
+
+function page() {
+ return (
+
+
+
+ );
+}
+export default page;
diff --git a/src/app/recipe/components/Accordion.tsx b/src/app/recipe/components/Accordion.tsx
new file mode 100644
index 0000000..aff8c14
--- /dev/null
+++ b/src/app/recipe/components/Accordion.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import SelectBox from '@/shared/components/InputBox/SelectBox';
+
+const selectOption = [
+ {
+ id: 'abv',
+ option: ['', '약한 도수', '가벼운 도수', '중간 도수', '센 도수', '매우 센 도수'],
+ title: '도수',
+ },
+ {
+ id: 'base',
+ option: ['', '위스키', '진', '럼', '보드카', '데킬라', '리큐르'],
+ title: '베이스',
+ },
+ {
+ id: 'glass',
+ option: ['', '클래식', '롱', '슈터', '숏'],
+ title: '글라스',
+ },
+];
+
+function Accordion() {
+ return (
+
+ {selectOption.map(({ id, option, title }) => {
+ return (
+ -
+
+
+ );
+ })}
+
+ );
+}
+export default Accordion;
diff --git a/src/app/recipe/page.tsx b/src/app/recipe/page.tsx
index 804c93f..9f5e417 100644
--- a/src/app/recipe/page.tsx
+++ b/src/app/recipe/page.tsx
@@ -1,4 +1,47 @@
-function page() {
- return
page
;
+import PageHeader from '@/shared/components/pageHeader/PageHeader';
+import { Metadata } from 'next';
+import Glass from '@/shared/assets/images/recipe_page_header.webp';
+import SelectBox from '@/shared/components/InputBox/SelectBox';
+import Input from '@/shared/components/InputBox/Input';
+import CocktailList from '@/shared/components/recipePage/cocktailList/CocktailList';
+import Accordion from './components/Accordion';
+
+export const metadata: Metadata = {
+ title: 'SSOUL | 칵테일레시피',
+ description: '칵테일 레시피가 궁금하신 분들을 위한 레시피 페이지',
+};
+
+function Page() {
+ return (
+
+ );
}
-export default page;
+export default Page;
diff --git a/src/app/recommend/components/ChatCocktailCard.tsx b/src/app/recommend/components/ChatCocktailCard.tsx
new file mode 100644
index 0000000..19b6c4a
--- /dev/null
+++ b/src/app/recommend/components/ChatCocktailCard.tsx
@@ -0,0 +1,23 @@
+import Image from 'next/image';
+import Dummy from '@/shared/assets/images/dummy/exampleCocktail.png';
+import Link from 'next/link';
+import Keep from '@/shared/components/keep/Keep';
+
+function ChatCocktailCard() {
+ return (
+
+
+
+
+
+
+
+ {'진피즈'}
+ + 상세보기
+
+
+
+
+ );
+}
+export default ChatCocktailCard;
diff --git a/src/app/recommend/components/ChatForm.tsx b/src/app/recommend/components/ChatForm.tsx
new file mode 100644
index 0000000..78e6a9a
--- /dev/null
+++ b/src/app/recommend/components/ChatForm.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import Send from '@/shared/assets/icons/send_36.svg';
+import { keyDown } from '@/shared/utills/keyDown';
+import { useState } from 'react';
+
+function ChatForm() {
+ const handleInput = (e: React.FormEvent
) => {
+ const target = e.currentTarget;
+
+ if (target.value == '') {
+ target.style.height = '';
+ }
+ target.style.height = `${target.scrollHeight}px`;
+ };
+
+ return (
+
+ );
+}
+export default ChatForm;
diff --git a/src/app/recommend/components/ChatRadio.tsx b/src/app/recommend/components/ChatRadio.tsx
new file mode 100644
index 0000000..c42de30
--- /dev/null
+++ b/src/app/recommend/components/ChatRadio.tsx
@@ -0,0 +1,42 @@
+interface Option {
+ label: string;
+ value: string;
+}
+
+interface RadioGroupProps {
+ options: Option[];
+ value: string;
+ onChange: (value: string) => void;
+}
+
+function ChatRadio({ options, value, onChange }: RadioGroupProps) {
+ return (
+
+ {options.map((opt) => (
+
+ ))}
+
+ );
+}
+export default ChatRadio;
diff --git a/src/app/recommend/components/MyChat.tsx b/src/app/recommend/components/MyChat.tsx
new file mode 100644
index 0000000..1d56a31
--- /dev/null
+++ b/src/app/recommend/components/MyChat.tsx
@@ -0,0 +1,39 @@
+interface Message {
+ id: string;
+ sender: 'ssury' | 'user';
+ text: string;
+ type?: 'radio' | 'text';
+}
+
+// 메시지 (연속 메시지)
+const messages: Message[] = [
+ {
+ id: '1',
+ sender: 'user',
+ text: '냥냥냥글자가길어지면 어케될까요 너무너무너무 궁금해요 하하하하하하하하하하하하하하하하하하ㅏ',
+ },
+ { id: '2', sender: 'user', text: '배고파요' },
+];
+
+function MyChat() {
+ return (
+
+
+
+ {/* 메시지 그룹 */}
+
+ {messages.map((msg) => (
+
+ ))}
+
+
+ );
+}
+export default MyChat;
diff --git a/src/app/recommend/components/SsuryChat.tsx b/src/app/recommend/components/SsuryChat.tsx
new file mode 100644
index 0000000..fa4b620
--- /dev/null
+++ b/src/app/recommend/components/SsuryChat.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import Ssury from '@/shared/assets/ssury/ssury_shaker.webp';
+import Image from 'next/image';
+import { useState } from 'react';
+import ChatRadio from './ChatRadio';
+import ChatCocktailCard from './ChatCocktailCard';
+
+interface Message {
+ id: string;
+ text?: string;
+ type?: 'radio' | 'text' | 'recommend';
+}
+
+function SsuryChat() {
+ const [selected, setSelected] = useState('option1');
+
+ // radio 옵션
+ const options = [
+ { label: '옵션 1', value: 'option1' },
+ { label: '옵션 2', value: 'option2' },
+ { label: '옵션 3', value: 'option3' },
+ ];
+
+ // 메시지 (연속 메시지)
+ const messages: Message[] = [
+ {
+ id: '1',
+ text: '안녕하세요, 바텐더 쑤리에요. \n 취향에 맞는 칵테일을 추천해드릴게요!',
+ },
+ {
+ id: '2',
+ text: '어떤 유형으로 찾아드릴까요?',
+ type: 'radio',
+ },
+ {
+ id: '3',
+ type: 'recommend',
+ },
+ ];
+
+ return (
+
+
+
+ {/* 메시지 그룹 */}
+
+ {messages.map((msg) => (
+
+ {msg.type === 'recommend' ? (
+
+ ) : (
+
+ {msg.text &&
{msg.text}
}
+
+ {/* radio */}
+ {msg.type === 'radio' && (
+
+ )}
+
+ )}
+
+ ))}
+
+
+ );
+}
+export default SsuryChat;
diff --git a/src/app/recommend/page.tsx b/src/app/recommend/page.tsx
index 48689b8..e8575e9 100644
--- a/src/app/recommend/page.tsx
+++ b/src/app/recommend/page.tsx
@@ -1,9 +1,23 @@
-function page() {
+import Bg from '@/shared/assets/images/recommend_bg.webp';
+import ChatForm from './components/ChatForm';
+import SsuryChat from './components/SsuryChat';
+import MyChat from './components/MyChat';
+
+function Page() {
return (
-
- page
-
test
+
);
}
-export default page;
+export default Page;
diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/components/index.tsx b/src/components/index.tsx
deleted file mode 100644
index defae75..0000000
--- a/src/components/index.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-function index() {
- return
;
-}
-export default index;
diff --git a/src/shared/@store/accordionStore.ts b/src/shared/@store/accordionStore.ts
new file mode 100644
index 0000000..5739eb9
--- /dev/null
+++ b/src/shared/@store/accordionStore.ts
@@ -0,0 +1,32 @@
+// zustand
+import { create } from 'zustand';
+
+// select박스 아코디언 메뉴
+export type ID = string | number;
+
+type AccordionState = {
+ openByGroup: Record
;
+};
+
+type AccordionAction = {
+ setOpen: (group: string, id: ID | null) => void;
+ toggle: (group: string, id: ID) => void;
+ closeGroup: (group: string) => void;
+};
+
+type Accordion = AccordionState & AccordionAction;
+
+export const useAccordionStore = create((set) => ({
+ openByGroup: {},
+ setOpen: (group, id) => set((s) => ({ openByGroup: { ...s.openByGroup, [group]: id } })),
+
+ // 같은 id가 이미 열려있으면 닫고 id 교체
+ toggle: (group, id) =>
+ set((s) => {
+ const cur = s.openByGroup[group] ?? null;
+ return { openByGroup: { ...s.openByGroup, [group]: cur === id ? null : id } };
+ }),
+
+ // 선택 후 닫기
+ closeGroup: (group) => set((s) => ({ openByGroup: { ...s.openByGroup, [group]: null } })),
+}));
diff --git a/src/shared/@store/auth.ts b/src/shared/@store/auth.ts
new file mode 100644
index 0000000..502da2a
--- /dev/null
+++ b/src/shared/@store/auth.ts
@@ -0,0 +1,83 @@
+import { create } from 'zustand';
+import { customToast } from '../components/toast/CustomToastUtils';
+
+interface User {
+ id: string;
+ email: string;
+ nickname: string;
+ isFirstLogin: boolean;
+ abv_degree?: number;
+ provider?: 'naver' | 'kakao' | 'google';
+}
+
+interface AuthState {
+ user: User | null;
+ accessToken: string | null;
+ isLoggedIn: boolean;
+ setUser: (user: User, token: string) => void;
+ logout: () => Promise;
+ loginWithProvider: (provider: User['provider']) => void;
+
+ updateUser: () => Promise;
+}
+
+export const useAuthStore = create((set) => ({
+ user: null,
+ accessToken: null,
+ isLoggedIn: false,
+
+ loginWithProvider: (provider) => {
+ window.location.href = `http://localhost:8080/oauth2/authorization/${provider}`;
+ },
+
+ setUser: (user, token) => {
+ const updatedUser = { ...user, abv_degree: 5.0 };
+ set({ user: updatedUser, accessToken: token, isLoggedIn: true });
+
+ customToast.success(`${updatedUser.nickname}님, 로그인 성공 🎉`);
+ },
+
+ logout: async () => {
+ try {
+ await fetch('http://localhost:8080/user/auth/logout', {
+ method: 'POST',
+ credentials: 'include',
+ });
+
+ customToast.success('로그아웃 되었습니다.');
+ set({ user: null, accessToken: null, isLoggedIn: false });
+ } catch (err) {
+ customToast.error('로그아웃 실패❌ \n 다시 시도해주세요.');
+ console.error('로그아웃 실패', err);
+ }
+ },
+
+ updateUser: async () => {
+ try {
+ const res = await fetch('http://localhost:8080/user/auth/refresh', {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ if (!res.ok) throw new Error('토큰 갱신 실패');
+ const data = await res.json();
+
+ console.log('updateUser response:', data);
+ const userInfo = data?.data?.user;
+ const accessToken = data?.data?.accessToken;
+
+ if (userInfo && accessToken) {
+ set({ user: userInfo, accessToken, isLoggedIn: true });
+ console.log('토큰 및 유저 정보 갱신 완료:', userInfo);
+ return userInfo;
+ }
+
+ return null;
+ } catch (err) {
+ console.error('updateUser 실패', err);
+ set({ accessToken: null, user: null, isLoggedIn: false });
+ return null;
+ }
+ },
+}));
diff --git a/src/shared/@store/modal.ts b/src/shared/@store/modal.ts
new file mode 100644
index 0000000..748ba24
--- /dev/null
+++ b/src/shared/@store/modal.ts
@@ -0,0 +1,33 @@
+import { create } from 'zustand';
+
+interface WelcomeModalData {
+ open: boolean;
+ nickname: string;
+}
+
+interface LogoutConfirmModalData {
+ open: boolean;
+}
+
+interface ModalStore {
+ welcomeModal: WelcomeModalData;
+ logoutConfirmModal: LogoutConfirmModalData;
+
+ openWelcomeModal: (nickname: string) => void;
+ closeWelcomeModal: () => void;
+
+ openLogoutConfirmModal: () => void;
+ closeLogoutConfirmModal: () => void;
+}
+
+export const useModalStore = create((set) => ({
+ welcomeModal: { open: false, nickname: '' },
+ logoutConfirmModal: { open: false },
+
+ openWelcomeModal: (nickname: string) => set({ welcomeModal: { open: true, nickname } }),
+
+ closeWelcomeModal: () => set({ welcomeModal: { open: false, nickname: '' } }),
+
+ openLogoutConfirmModal: () => set({ logoutConfirmModal: { open: true } }),
+ closeLogoutConfirmModal: () => set({ logoutConfirmModal: { open: false } }),
+}));
diff --git a/src/@types/index.ts b/src/shared/@types/index.ts
similarity index 100%
rename from src/@types/index.ts
rename to src/shared/@types/index.ts
diff --git a/src/shared/assets/icons/arrow_up_24.svg b/src/shared/assets/icons/arrow_up_24.svg
new file mode 100644
index 0000000..24da3d0
--- /dev/null
+++ b/src/shared/assets/icons/arrow_up_24.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/bell_24.svg b/src/shared/assets/icons/bell_24.svg
new file mode 100644
index 0000000..ab560cb
--- /dev/null
+++ b/src/shared/assets/icons/bell_24.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/shared/assets/icons/close_20.svg b/src/shared/assets/icons/close_20.svg
new file mode 100644
index 0000000..844b38d
--- /dev/null
+++ b/src/shared/assets/icons/close_20.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/assets/icons/close_32.svg b/src/shared/assets/icons/close_32.svg
new file mode 100644
index 0000000..21f3782
--- /dev/null
+++ b/src/shared/assets/icons/close_32.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/assets/icons/comment_28.svg b/src/shared/assets/icons/comment_28.svg
new file mode 100644
index 0000000..480514e
--- /dev/null
+++ b/src/shared/assets/icons/comment_28.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/shared/assets/icons/edit_28.svg b/src/shared/assets/icons/edit_28.svg
new file mode 100644
index 0000000..c7ded30
--- /dev/null
+++ b/src/shared/assets/icons/edit_28.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/assets/icons/error_toast_32.svg b/src/shared/assets/icons/error_toast_32.svg
new file mode 100644
index 0000000..c02f4da
--- /dev/null
+++ b/src/shared/assets/icons/error_toast_32.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/shared/assets/icons/github_32.svg b/src/shared/assets/icons/github_32.svg
new file mode 100644
index 0000000..888f688
--- /dev/null
+++ b/src/shared/assets/icons/github_32.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/google.svg b/src/shared/assets/icons/google.svg
new file mode 100644
index 0000000..0136eee
--- /dev/null
+++ b/src/shared/assets/icons/google.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/shared/assets/icons/help_24.svg b/src/shared/assets/icons/help_24.svg
new file mode 100644
index 0000000..44b0045
--- /dev/null
+++ b/src/shared/assets/icons/help_24.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/imageBox_24.svg b/src/shared/assets/icons/imageBox_24.svg
new file mode 100644
index 0000000..8844498
--- /dev/null
+++ b/src/shared/assets/icons/imageBox_24.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/shared/assets/icons/imageBox_fill_24.svg b/src/shared/assets/icons/imageBox_fill_24.svg
new file mode 100644
index 0000000..12398a8
--- /dev/null
+++ b/src/shared/assets/icons/imageBox_fill_24.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/shared/assets/icons/info_toast_32.svg b/src/shared/assets/icons/info_toast_32.svg
new file mode 100644
index 0000000..127540a
--- /dev/null
+++ b/src/shared/assets/icons/info_toast_32.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/shared/assets/icons/kakao.svg b/src/shared/assets/icons/kakao.svg
new file mode 100644
index 0000000..f1ec235
--- /dev/null
+++ b/src/shared/assets/icons/kakao.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/shared/assets/icons/keep_36.svg b/src/shared/assets/icons/keep_36.svg
new file mode 100644
index 0000000..a24cacc
--- /dev/null
+++ b/src/shared/assets/icons/keep_36.svg
@@ -0,0 +1,22 @@
+
diff --git a/src/shared/assets/icons/keep_active_36.svg b/src/shared/assets/icons/keep_active_36.svg
new file mode 100644
index 0000000..1045102
--- /dev/null
+++ b/src/shared/assets/icons/keep_active_36.svg
@@ -0,0 +1,23 @@
+
diff --git a/src/shared/assets/icons/like_28.svg b/src/shared/assets/icons/like_28.svg
new file mode 100644
index 0000000..48b8dc8
--- /dev/null
+++ b/src/shared/assets/icons/like_28.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/like_active_28.svg b/src/shared/assets/icons/like_active_28.svg
new file mode 100644
index 0000000..a64f7f3
--- /dev/null
+++ b/src/shared/assets/icons/like_active_28.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/link_36.svg b/src/shared/assets/icons/link_36.svg
new file mode 100644
index 0000000..6d1588c
--- /dev/null
+++ b/src/shared/assets/icons/link_36.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/assets/icons/menu_32.svg b/src/shared/assets/icons/menu_32.svg
new file mode 100644
index 0000000..c26bb9d
--- /dev/null
+++ b/src/shared/assets/icons/menu_32.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/shared/assets/icons/naver.svg b/src/shared/assets/icons/naver.svg
new file mode 100644
index 0000000..80c3146
--- /dev/null
+++ b/src/shared/assets/icons/naver.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/shared/assets/icons/search_32.svg b/src/shared/assets/icons/search_32.svg
new file mode 100644
index 0000000..73104e9
--- /dev/null
+++ b/src/shared/assets/icons/search_32.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/assets/icons/selectDown_24.svg b/src/shared/assets/icons/selectDown_24.svg
new file mode 100644
index 0000000..494eb37
--- /dev/null
+++ b/src/shared/assets/icons/selectDown_24.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/send_36.svg b/src/shared/assets/icons/send_36.svg
new file mode 100644
index 0000000..ae67e42
--- /dev/null
+++ b/src/shared/assets/icons/send_36.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/share_28.svg b/src/shared/assets/icons/share_28.svg
new file mode 100644
index 0000000..be5e12e
--- /dev/null
+++ b/src/shared/assets/icons/share_28.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/sign_in_24.svg b/src/shared/assets/icons/sign_in_24.svg
new file mode 100644
index 0000000..3c0d540
--- /dev/null
+++ b/src/shared/assets/icons/sign_in_24.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/assets/icons/sign_out_24.svg b/src/shared/assets/icons/sign_out_24.svg
new file mode 100644
index 0000000..3cc2b7d
--- /dev/null
+++ b/src/shared/assets/icons/sign_out_24.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/assets/icons/success_toast_32.svg b/src/shared/assets/icons/success_toast_32.svg
new file mode 100644
index 0000000..d0d718c
--- /dev/null
+++ b/src/shared/assets/icons/success_toast_32.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/shared/assets/icons/tag_24.svg b/src/shared/assets/icons/tag_24.svg
new file mode 100644
index 0000000..32e4a54
--- /dev/null
+++ b/src/shared/assets/icons/tag_24.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/user_24.svg b/src/shared/assets/icons/user_24.svg
new file mode 100644
index 0000000..92260b0
--- /dev/null
+++ b/src/shared/assets/icons/user_24.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/assets/images/community_page_header.webp b/src/shared/assets/images/community_page_header.webp
new file mode 100644
index 0000000..4ef858a
Binary files /dev/null and b/src/shared/assets/images/community_page_header.webp differ
diff --git a/src/shared/assets/images/dummy/exampleCocktail.png b/src/shared/assets/images/dummy/exampleCocktail.png
new file mode 100644
index 0000000..a8cb859
Binary files /dev/null and b/src/shared/assets/images/dummy/exampleCocktail.png differ
diff --git a/src/shared/assets/images/login_bg.webp b/src/shared/assets/images/login_bg.webp
new file mode 100644
index 0000000..27723e7
Binary files /dev/null and b/src/shared/assets/images/login_bg.webp differ
diff --git a/src/shared/assets/images/prepost_img.webp b/src/shared/assets/images/prepost_img.webp
new file mode 100644
index 0000000..7c659d7
Binary files /dev/null and b/src/shared/assets/images/prepost_img.webp differ
diff --git a/src/shared/assets/images/recipe_page_header.webp b/src/shared/assets/images/recipe_page_header.webp
new file mode 100644
index 0000000..1bdbf4f
Binary files /dev/null and b/src/shared/assets/images/recipe_page_header.webp differ
diff --git a/src/shared/assets/images/recommend_bg.webp b/src/shared/assets/images/recommend_bg.webp
new file mode 100644
index 0000000..0f7617b
Binary files /dev/null and b/src/shared/assets/images/recommend_bg.webp differ
diff --git a/src/shared/assets/images/star_bg.webp b/src/shared/assets/images/star_bg.webp
new file mode 100644
index 0000000..7e23d28
Binary files /dev/null and b/src/shared/assets/images/star_bg.webp differ
diff --git a/src/shared/assets/lottie/loading.json b/src/shared/assets/lottie/loading.json
new file mode 100644
index 0000000..e6ffbaa
--- /dev/null
+++ b/src/shared/assets/lottie/loading.json
@@ -0,0 +1 @@
+{"v":"5.7.8","fr":30,"ip":0,"op":121,"w":1080,"h":1080,"nm":"3GWS5M 22","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-122,333],[78,333]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.925490255917,0.117647066303,0.141176470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":28,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[0]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":86,"s":[0]},{"t":101,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5,"op":185,"st":5,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 3","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[517.547,873.309,0],"ix":2,"l":2},"a":{"a":0,"k":[-22.453,333.309,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-119.516,325.931],[74.609,325.931],[74.609,340.686],[-119.516,340.686],[-119.516,325.931]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.674509823322,0.800000011921,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":5,"op":185,"st":5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-10,4.5],[-5.5,27.5],[0,21],[0,0],[6,46],[-4.5,40.5],[0,0],[0,0],[0,0],[0,-30],[14,-27],[0,0],[-6,-17],[-39,-7],[0,0]],"o":[[0,0],[10,-4.5],[3.661,-18.306],[0,-22.522],[0,0],[-2.407,-18.452],[4.5,-40.5],[0,0],[0,0],[0,0],[0,30],[-18.717,36.096],[0,0],[13,14],[20.67,3.71],[0,0]],"v":[[-122.5,329],[-62.5,322.5],[-39.5,293.5],[-39.5,239.5],[-41.5,173],[-110.5,112.5],[-107.5,-16.5],[-96,-181],[-14,-180.5],[52,-180],[64,1],[59.5,126],[-3.5,173.5],[-1.5,308.5],[47,330],[78,328]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.925490255917,0.117647066303,0.141176470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":28,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":81,"s":[0]},{"t":101,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Layer 2","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[517.539,612.7,0],"ix":2,"l":2},"a":{"a":0,"k":[-22.461,72.7,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[13.044,22.155],[-1.092,18.064],[0,0],[0,0],[-46.383,0],[0,0],[0,0],[9.179,-15.586],[24.603,-7.157],[0,0],[-18.75,0.229],[0,0],[0,0],[0,0],[9.863,12.032],[0,0],[0,0],[0,0],[-12.227,20.771],[0.846,14.215],[0,0],[43.187,0],[0,0],[-7.207,-12.259],[-23.705,-3.88],[0,0],[0,0],[0,0],[14.347,-4.451],[0,0],[0,0],[0,0],[-13.875,13.386]],"o":[[0,0],[-24.586,-7.157],[-9.162,-15.586],[0,0],[0,0],[46.366,0],[0,0],[0,0],[1.093,18.064],[-13.025,22.155],[0,0],[13.892,13.386],[0,0],[0,0],[0,0],[-14.364,-4.451],[0,0],[0,0],[0,0],[23.721,-3.88],[7.222,-12.259],[0,0],[-43.204,0],[0,0],[-0.863,14.215],[12.227,20.771],[0,0],[0,0],[0,0],[-9.88,12.032],[0,0],[0,0],[0,0],[18.733,0.229],[0,0]],"v":[[-50.682,302.911],[-50.682,182.234],[-109.961,136.666],[-122.4,85.36],[-105.494,-190.591],[-92.011,-190.591],[47.105,-190.591],[60.588,-190.591],[77.478,85.36],[65.038,136.666],[5.759,182.234],[5.759,302.911],[60.083,321.432],[74.609,321.252],[74.609,335.991],[24.868,335.991],[-12.483,311.731],[-13.625,310.313],[-13.625,166.501],[-9.403,165.8],[48.312,126.835],[58.127,86.534],[42.329,-171.189],[-87.25,-171.189],[-103.033,86.534],[-93.233,126.835],[-35.537,165.8],[-31.281,166.501],[-31.281,310.313],[-32.423,311.731],[-69.774,335.991],[-119.516,335.991],[-119.516,321.252],[-105.005,321.432],[-50.682,302.911]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.674509823322,0.800000011921,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Layer 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.158],"y":[0]},"t":40,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[100]},{"t":85,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.92,"y":0.997},"o":{"x":0.553,"y":0},"t":35,"s":[435.215,214.417,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.391,"y":0.951},"o":{"x":0.079,"y":0.008},"t":45,"s":[435.215,369.417,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.847,"y":1},"o":{"x":0.573,"y":0.122},"t":50,"s":[435.215,337.417,0],"to":[0,0,0],"ti":[0,0,0]},{"t":55,"s":[435.215,349.417,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-104.785,-190.583,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-7.973,45.274],[0,0],[35.509,-4.648],[0,0]],"o":[[46.269,-4.907],[0,0],[-7.531,34.513],[0,0],[0,0]],"v":[[-93.691,-87.049],[-2.244,-172.428],[-22.054,-172.428],[-93.691,-106.628],[-93.691,-87.049]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.92549020052,0.109803922474,0.141176477075,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[46.757,0],[0,46.774],[-46.775,0],[0,-46.775]],"o":[[-46.775,0],[0,-46.775],[46.757,0],[0,46.774]],"v":[[-104.776,-105.895],[-189.473,-190.591],[-104.776,-275.27],[-20.098,-190.591]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[57.501,0],[0,-57.503],[-57.52,0],[0,57.502]],"o":[[-57.52,0],[0,57.502],[57.501,0],[0,-57.503]],"v":[[-104.776,-294.705],[-208.907,-190.591],[-104.776,-86.461],[-0.663,-190.591]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.956862747669,0.505882382393,0.1254902035,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Layer 4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.159],"y":[0]},"t":27,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[100]},{"t":85,"s":[0]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.837],"y":[0.989]},"o":{"x":[0.544],"y":[0]},"t":22,"s":[33]},{"t":37,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.837,"y":0.984},"o":{"x":0.544,"y":0},"t":22,"s":[874.764,60.517,0],"to":[-36.333,16,0],"ti":[38.167,-12.333,0]},{"i":{"x":0.208,"y":0.811},"o":{"x":0.07,"y":0.017},"t":37,"s":[656.764,156.517,0],"to":[-38.167,12.333,0],"ti":[0,0,0]},{"i":{"x":0.893,"y":1},"o":{"x":0.78,"y":0.344},"t":44,"s":[645.764,134.517,0],"to":[0,0,0],"ti":[-1.833,-3.667,0]},{"t":51,"s":[656.764,156.517,0]}],"ix":2,"l":2},"a":{"a":0,"k":[116.764,-383.483,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[9.897,-39.895],[0,0],[-6.586,-1.287],[0,0],[-27.847,-0.831],[0,0],[0,0]],"o":[[0,0],[-37.79,-1.125],[0,0],[6.032,3.506],[0,0],[7.499,-30.161],[0,0],[0,0],[0,0]],"v":[[208.198,-398.328],[153.451,-399.943],[66.831,-345.864],[-58.67,158.414],[-39.597,165.033],[86.282,-341.104],[152.767,-379.922],[207.514,-378.325],[208.198,-398.328]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.776470601559,0.494117647409,0.705882370472,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/src/shared/assets/ssury/ssurl_level3.webp b/src/shared/assets/ssury/ssurl_level3.webp
new file mode 100644
index 0000000..a53fae8
Binary files /dev/null and b/src/shared/assets/ssury/ssurl_level3.webp differ
diff --git a/src/shared/assets/ssury/ssury_bell.webp b/src/shared/assets/ssury/ssury_bell.webp
new file mode 100644
index 0000000..eae467a
Binary files /dev/null and b/src/shared/assets/ssury/ssury_bell.webp differ
diff --git a/src/shared/assets/ssury/ssury_drink.webp b/src/shared/assets/ssury/ssury_drink.webp
new file mode 100644
index 0000000..0f550e7
Binary files /dev/null and b/src/shared/assets/ssury/ssury_drink.webp differ
diff --git a/src/shared/assets/ssury/ssury_jump.webp b/src/shared/assets/ssury/ssury_jump.webp
new file mode 100644
index 0000000..e1af31d
Binary files /dev/null and b/src/shared/assets/ssury/ssury_jump.webp differ
diff --git a/src/shared/assets/ssury/ssury_level1.webp b/src/shared/assets/ssury/ssury_level1.webp
new file mode 100644
index 0000000..63ed28b
Binary files /dev/null and b/src/shared/assets/ssury/ssury_level1.webp differ
diff --git a/src/shared/assets/ssury/ssury_level2.webp b/src/shared/assets/ssury/ssury_level2.webp
new file mode 100644
index 0000000..c9aa2e3
Binary files /dev/null and b/src/shared/assets/ssury/ssury_level2.webp differ
diff --git a/src/shared/assets/ssury/ssury_level4.webp b/src/shared/assets/ssury/ssury_level4.webp
new file mode 100644
index 0000000..f88e697
Binary files /dev/null and b/src/shared/assets/ssury/ssury_level4.webp differ
diff --git a/src/shared/assets/ssury/ssury_level5.webp b/src/shared/assets/ssury/ssury_level5.webp
new file mode 100644
index 0000000..1306011
Binary files /dev/null and b/src/shared/assets/ssury/ssury_level5.webp differ
diff --git a/src/shared/assets/ssury/ssury_level6.webp b/src/shared/assets/ssury/ssury_level6.webp
new file mode 100644
index 0000000..0ed79b8
Binary files /dev/null and b/src/shared/assets/ssury/ssury_level6.webp differ
diff --git a/src/shared/assets/ssury/ssury_make.webp b/src/shared/assets/ssury/ssury_make.webp
new file mode 100644
index 0000000..ec905c3
Binary files /dev/null and b/src/shared/assets/ssury/ssury_make.webp differ
diff --git a/src/shared/assets/ssury/ssury_question.webp b/src/shared/assets/ssury/ssury_question.webp
new file mode 100644
index 0000000..d9ad76d
Binary files /dev/null and b/src/shared/assets/ssury/ssury_question.webp differ
diff --git a/src/shared/assets/ssury/ssury_shaker.webp b/src/shared/assets/ssury/ssury_shaker.webp
new file mode 100644
index 0000000..7255d19
Binary files /dev/null and b/src/shared/assets/ssury/ssury_shaker.webp differ
diff --git a/src/shared/components/InputBox/Input.tsx b/src/shared/components/InputBox/Input.tsx
new file mode 100644
index 0000000..399e506
--- /dev/null
+++ b/src/shared/components/InputBox/Input.tsx
@@ -0,0 +1,80 @@
+import tw from '@/shared/utills/tw';
+import { cva } from 'class-variance-authority';
+import { HTMLInputTypeAttribute, Ref } from 'react';
+import Search from '@/shared/assets/icons/search_32.svg';
+import Button from '../button/Button';
+// select나올떄 자연스러운 처리 화살표 로테이트 [x]
+// 인풋 타입받을 수 있게 수정 [x]
+// 인풋접근성 라벨이 중요함 라벨 을 div에 묶어서 하거나 label로 인풋감싸거나 div로 묶고 같은 선상에두게 [x]
+// div안에 라벨이랑감싸기 [x]
+// 텍스트 에어리어 버전도 만들기
+// 인풋 잘림 = 라인height 인풋 높이랑 맞춰두기 [x]
+
+interface Props {
+ placeholder: string;
+ type?: HTMLInputTypeAttribute;
+ ref?: Ref;
+ size?: 'default' | 'lg';
+ variant?: 'default' | 'search' | 'comment';
+ className?: string;
+ onChange?: () => void;
+ id: string;
+}
+
+export const InputClass = cva(
+ `px-4 py-1 w-80 rounded-lg bg-white text-primary flex items-center gap-2 placeholder:text-gray-dark`,
+ {
+ variants: {
+ size: {
+ default: 'h-10',
+ lg: 'h-13',
+ },
+ },
+ defaultVariants: {
+ size: 'default',
+ },
+ }
+);
+
+function Input({
+ placeholder,
+ type,
+ ref,
+ size,
+ variant = 'default',
+ className,
+ id,
+ onChange,
+ ...rest
+}: Props) {
+ return (
+
+
+ {variant === 'search' ? (
+
+ ) : variant === 'comment' ? (
+
+ ) : null}
+
+ );
+}
+export default Input;
diff --git a/src/shared/components/InputBox/SelectBox.tsx b/src/shared/components/InputBox/SelectBox.tsx
new file mode 100644
index 0000000..dfeb341
--- /dev/null
+++ b/src/shared/components/InputBox/SelectBox.tsx
@@ -0,0 +1,97 @@
+'use client';
+import { Ref, useMemo, useState } from 'react';
+import Down from '@/shared/assets/icons/selectDown_24.svg';
+import { ID, useAccordionStore } from '@/shared/@store/accordionStore';
+import { useShallow } from 'zustand/shallow';
+
+interface Props {
+ id?: ID;
+ groupKey?: string;
+ ref?: Ref;
+ option: string[];
+ title: string;
+ onChange?: (value: string) => void;
+}
+
+// groupKey를 Props로 내릴경우 == 아코디언 없는 경우 == select박스
+function SelectBox({ id, groupKey, ref, option, title, onChange }: Props) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [select, setSelect] = useState('');
+
+ const ingroup = !!groupKey;
+ // groupkey일 경우 전달받은 ID로 식별 아닐경우 title로 식별
+ const keyId = useMemo(() => id ?? title, [id, title]);
+
+ const { openId, toggleGroup, closeGroup } = useAccordionStore(
+ useShallow((s) => ({
+ openId: ingroup ? (s.openByGroup[groupKey] ?? null) : null,
+ toggleGroup: s.toggle,
+ closeGroup: s.closeGroup,
+ }))
+ );
+
+ //groupkey가 있을 떄와 없을때로 구분해서 state혹은 store로 관리
+ const localOpen = ingroup ? openId === keyId : isOpen;
+
+ const toggle = () => {
+ if (ingroup) toggleGroup(groupKey, keyId);
+ else setIsOpen((v) => !v);
+ };
+
+ const close = () => {
+ if (ingroup) closeGroup(groupKey);
+ else setIsOpen(false);
+ };
+
+ const handleChoose = (v: string) => {
+ const value = v || title;
+ setSelect(value);
+ onChange?.(value);
+ close();
+ };
+
+ return (
+
+
+
+
+ {option.map((v, i) => (
+ - handleChoose(v)}
+ aria-selected={v === select}
+ >
+ {v || title}
+
+ ))}
+
+
+ );
+}
+export default SelectBox;
diff --git a/src/shared/components/auth/LoginConfirm.tsx b/src/shared/components/auth/LoginConfirm.tsx
new file mode 100644
index 0000000..efed6f9
--- /dev/null
+++ b/src/shared/components/auth/LoginConfirm.tsx
@@ -0,0 +1,4 @@
+function LoginConfirm() {
+ return LoginConfirm
;
+}
+export default LoginConfirm;
diff --git a/src/shared/components/auth/LoginRedirectHandler.tsx b/src/shared/components/auth/LoginRedirectHandler.tsx
new file mode 100644
index 0000000..4077b54
--- /dev/null
+++ b/src/shared/components/auth/LoginRedirectHandler.tsx
@@ -0,0 +1,55 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { usePathname, useRouter } from 'next/navigation';
+import { useAuthStore } from '@/shared/@store/auth';
+import { useModalStore } from '@/shared/@store/modal';
+import { customToast } from '@/shared/components/toast/CustomToastUtils';
+import Spinner from '../spinner/Spinner';
+
+function LoginRedirectHandler() {
+ const pathname = usePathname();
+ const router = useRouter();
+ const { user, updateUser } = useAuthStore();
+ const { openWelcomeModal } = useModalStore();
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!user && loading) {
+ updateUser()
+ .then((fetchedUser) => {
+ if (!fetchedUser) {
+ router.replace('/login');
+ }
+ })
+ .catch(() => {
+ router.replace('/login');
+ })
+ .finally(() => setLoading(false));
+ }
+ }, [user, loading, updateUser, router]);
+
+ useEffect(() => {
+ if (!user || loading) return;
+
+ const preLoginPath = sessionStorage.getItem('preLoginPath') || '/';
+
+ if (pathname.startsWith('/login/first-user')) {
+ openWelcomeModal(user.nickname);
+ } else if (pathname.startsWith('/login/success')) {
+ customToast.success(`${user.nickname}님 \n 로그인 성공 🎉`);
+ router.replace(preLoginPath);
+ }
+ }, [pathname, user, router, openWelcomeModal, loading]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return null;
+}
+export default LoginRedirectHandler;
diff --git a/src/shared/components/auth/Welcome.tsx b/src/shared/components/auth/Welcome.tsx
new file mode 100644
index 0000000..b479480
--- /dev/null
+++ b/src/shared/components/auth/Welcome.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import Image from 'next/image';
+
+import Button from '@/shared/components/button/Button';
+import ModalLayout from '@/shared/components/modalPop/ModalLayout';
+import Ssury from '@/shared/assets/ssury/ssury_jump.webp';
+import { useRouter } from 'next/navigation';
+import { useModalStore } from '@/shared/@store/modal';
+import { useAuthStore } from '@/shared/@store/auth';
+
+function Welcome() {
+ const router = useRouter();
+ const { user } = useAuthStore();
+ const { welcomeModal, closeWelcomeModal } = useModalStore();
+
+ if (!welcomeModal.open || !user) return null;
+
+ return (
+
+
+
+ >
+ }
+ >
+
+
+ );
+}
+export default Welcome;
diff --git a/src/shared/components/button/Button.tsx b/src/shared/components/button/Button.tsx
new file mode 100644
index 0000000..9acbfc8
--- /dev/null
+++ b/src/shared/components/button/Button.tsx
@@ -0,0 +1,63 @@
+import tw from '@/shared/utills/tw';
+import { cva } from 'class-variance-authority';
+import { ButtonHTMLAttributes, Ref } from 'react';
+
+interface Props extends ButtonHTMLAttributes {
+ size?: 'default' | 'sm' | 'auto';
+ color?: 'default' | 'purple';
+ ref?: Ref;
+ disable?: boolean;
+ type?: 'submit' | 'button';
+ children?: React.ReactNode;
+ className?: string;
+ onClick?: () => void;
+}
+
+export const ButtonClass = cva(
+ `
+ py-1 px-2 rounded-lg text-base flex-center text-primary duration-300 disabled:bg-gray disabled:cursor-not-allowed disabled:text-primary enabled:hover:inset-shadow-black
+ `,
+ {
+ variants: {
+ color: {
+ default: 'bg-secondary text-primary',
+ purple: 'bg-tertiary text-secondary',
+ },
+ size: {
+ default: 'h-10 min-w-25',
+ sm: 'h-8 min-w-20',
+ auto: 'w-auto',
+ },
+ },
+ defaultVariants: {
+ color: 'default',
+ size: 'default',
+ },
+ }
+);
+
+function Button({
+ size,
+ type = 'button',
+ color,
+ children,
+ className,
+ ref,
+ disabled,
+ onClick,
+ ...rest
+}: Props) {
+ return (
+
+ );
+}
+export default Button;
diff --git a/src/shared/components/button/TextButton.tsx b/src/shared/components/button/TextButton.tsx
new file mode 100644
index 0000000..acdee1f
--- /dev/null
+++ b/src/shared/components/button/TextButton.tsx
@@ -0,0 +1,31 @@
+import { Ref } from 'react';
+
+interface Props {
+ size?: 'default' | 'sm';
+ type?: 'button' | 'submit';
+ ref?: Ref;
+ children: React.ReactNode;
+ className?: string;
+}
+
+const SIZE = {
+ default: 'text-base flex flex-col justfy-center',
+ sm: 'text-sm flex flex-col justfy-center',
+};
+
+function TextButton({
+ type = 'button',
+ children,
+ className,
+ ref,
+ size = 'default',
+ ...rest
+}: Props) {
+ return (
+
+ );
+}
+export default TextButton;
diff --git a/src/shared/components/community/CommunityFilter.tsx b/src/shared/components/community/CommunityFilter.tsx
new file mode 100644
index 0000000..c8deedd
--- /dev/null
+++ b/src/shared/components/community/CommunityFilter.tsx
@@ -0,0 +1,16 @@
+'use client';
+
+import SelectBox from '../InputBox/SelectBox';
+function CommunityFilter() {
+ return (
+
+ );
+}
+
+export default CommunityFilter;
diff --git a/src/shared/components/community/CommunityHeader.tsx b/src/shared/components/community/CommunityHeader.tsx
new file mode 100644
index 0000000..956376d
--- /dev/null
+++ b/src/shared/components/community/CommunityHeader.tsx
@@ -0,0 +1,12 @@
+import PageHeader from '../pageHeader/PageHeader';
+import headerImg from '@/shared/assets/images/community_page_header.webp';
+
+function CommunityHeader() {
+ return (
+
+ );
+}
+
+export default CommunityHeader;
diff --git a/src/shared/components/community/CommunityTab.tsx b/src/shared/components/community/CommunityTab.tsx
new file mode 100644
index 0000000..16fdf2a
--- /dev/null
+++ b/src/shared/components/community/CommunityTab.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import tw from '@/shared/utills/tw';
+import { useState } from 'react';
+
+const tabItem = [
+ { title: '전체' },
+ { title: '레시피' },
+ { title: '팁' },
+ { title: '질문' },
+ { title: '자유' },
+];
+
+function CommunityTab() {
+ const [selectedIdx, setSelectedIdx] = useState(0);
+
+ return (
+
+
+
+ {tabItem.map(({ title }, idx) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default CommunityTab;
diff --git a/src/shared/components/community/PostCard.tsx b/src/shared/components/community/PostCard.tsx
new file mode 100644
index 0000000..cc672e8
--- /dev/null
+++ b/src/shared/components/community/PostCard.tsx
@@ -0,0 +1,44 @@
+import Image from 'next/image';
+import prePost from '@/shared/assets/images/prepost_img.webp';
+import PostLabel from './PostLabel';
+
+function PostCard({ label }: { label: string }) {
+ return (
+
+
+
+
+
+
칵테일 만들 때 준비물
+
+
칵테일 처음 만들어 보는데 랄랄
+
가나다라마바사아자차카파타하
+
+
+ - 실버븬
+ - |
+ - 3분 전
+ - |
+ - 조회 3
+ - |
+ - 댓글 3
+
+
+
+
+
+
+
+ );
+}
+
+export default PostCard;
diff --git a/src/shared/components/community/PostLabel.tsx b/src/shared/components/community/PostLabel.tsx
new file mode 100644
index 0000000..c838e52
--- /dev/null
+++ b/src/shared/components/community/PostLabel.tsx
@@ -0,0 +1,24 @@
+function PostLabel({ title }: { title: string }) {
+ return (
+
+ {title}
+
+ );
+}
+
+export default PostLabel;
diff --git a/src/shared/components/community/WriteBtn.tsx b/src/shared/components/community/WriteBtn.tsx
new file mode 100644
index 0000000..26befbd
--- /dev/null
+++ b/src/shared/components/community/WriteBtn.tsx
@@ -0,0 +1,12 @@
+import Write from '@/shared/assets/icons/edit_28.svg';
+
+function WriteBtn() {
+ return (
+
+ );
+}
+
+export default WriteBtn;
diff --git a/src/shared/components/footer/Footer.tsx b/src/shared/components/footer/Footer.tsx
new file mode 100644
index 0000000..b1d533b
--- /dev/null
+++ b/src/shared/components/footer/Footer.tsx
@@ -0,0 +1,18 @@
+import Github from './Github';
+
+function Footer() {
+ return (
+
+ );
+}
+
+export default Footer;
diff --git a/src/shared/components/footer/FooterWrapper.tsx b/src/shared/components/footer/FooterWrapper.tsx
new file mode 100644
index 0000000..4a05dc0
--- /dev/null
+++ b/src/shared/components/footer/FooterWrapper.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+import Footer from './Footer';
+
+export default function FooterWrapper() {
+ const pathname = usePathname();
+ const showFooter = pathname !== '/recommend';
+
+ return showFooter ? : null;
+}
diff --git a/src/shared/components/footer/Github.tsx b/src/shared/components/footer/Github.tsx
new file mode 100644
index 0000000..6e981df
--- /dev/null
+++ b/src/shared/components/footer/Github.tsx
@@ -0,0 +1,20 @@
+import Link from 'next/link';
+import Icon from '@/shared/assets/icons/github_32.svg';
+
+function Github() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default Github;
diff --git a/src/shared/components/header/DropdownMenu.tsx b/src/shared/components/header/DropdownMenu.tsx
new file mode 100644
index 0000000..a64a21a
--- /dev/null
+++ b/src/shared/components/header/DropdownMenu.tsx
@@ -0,0 +1,146 @@
+import { navItem } from '@/shared/utills/navigation';
+import Image from 'next/image';
+import Close from '@/shared/assets/icons/close_32.svg';
+import User from '@/shared/assets/icons/user_24.svg';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { useEffect, useRef } from 'react';
+import gsap from 'gsap';
+import { useAuthStore } from '@/shared/@store/auth';
+
+interface Props {
+ isClicked: boolean;
+ setIsClicked: (state: boolean) => void;
+}
+
+function DropdownMenu({ isClicked, setIsClicked }: Props) {
+ const pathname = usePathname();
+ const menuRef = useRef(null);
+ const textRef = useRef<(HTMLSpanElement | null)[]>([]);
+
+ const { isLoggedIn, logout } = useAuthStore();
+
+ useEffect(() => {
+ if (!menuRef.current) return;
+
+ if (isClicked) {
+ gsap.fromTo(
+ menuRef.current,
+ {
+ x: -200,
+ opacity: 0,
+ },
+ {
+ x: 0,
+ opacity: 1,
+ duration: 0.8,
+ ease: 'expo.inOut',
+ }
+ );
+ }
+ }, [isClicked]);
+
+ const handleMouseEnter = (index: number) => {
+ const el = textRef.current[index];
+ if (!el) return;
+ gsap.to(el, {
+ y: -5,
+ duration: 0.3,
+ ease: 'power1.out',
+ });
+ };
+
+ const handleMouseLeave = (index: number) => {
+ const el = textRef.current[index];
+ if (!el) return;
+ gsap.to(el, {
+ y: 0,
+ duration: 0.3,
+ ease: 'power1.out',
+ });
+ };
+
+ return (
+
+ );
+}
+
+export default DropdownMenu;
diff --git a/src/shared/components/header/HamburgerMenu.tsx b/src/shared/components/header/HamburgerMenu.tsx
new file mode 100644
index 0000000..aa4d950
--- /dev/null
+++ b/src/shared/components/header/HamburgerMenu.tsx
@@ -0,0 +1,30 @@
+import Menu from '@/shared/assets/icons/menu_32.svg';
+import { useState } from 'react';
+import DropdownMenu from './DropdownMenu';
+
+function HamburgerMenu() {
+ const [isClicked, setIsClicked] = useState(false);
+
+ const handleClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsClicked(true);
+ };
+
+ return (
+ <>
+
+ {isClicked && }
+ >
+ );
+}
+
+export default HamburgerMenu;
diff --git a/src/shared/components/header/Header.tsx b/src/shared/components/header/Header.tsx
new file mode 100644
index 0000000..dbad4da
--- /dev/null
+++ b/src/shared/components/header/Header.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+import HeaderBtn from './HeaderBtn';
+import HeaderLogo from './HeaderLogo';
+import NavItem from './NavItem';
+import HamburgerMenu from './HamburgerMenu';
+import { useEffect, useRef, useState } from 'react';
+import tw from '@/shared/utills/tw';
+
+function Header() {
+ const pathname = usePathname();
+
+ const [showShadow, setShowShadow] = useState(true);
+
+ const [lastScrollTop, setLastScrollTop] = useState(0); // 마지막 스크롤 위치 저장
+ const headerRef = useRef(null);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const currentScrollTop = window.scrollY;
+ // console.log(currentScrollTop, lastScrollTop);
+
+ if (Math.abs(currentScrollTop - lastScrollTop) < -5) return;
+
+ if (currentScrollTop > lastScrollTop) {
+ // 유저가 아래로 스크롤 -> 헤더 숨기기
+ if (headerRef.current) {
+ headerRef.current.style.top = '-60px';
+ setShowShadow(false);
+ }
+ } else {
+ // 유저가 위로 스크롤 -> 헤더 다시 보이기
+ if (headerRef.current) {
+ headerRef.current.style.top = '0';
+ setShowShadow(true);
+ }
+ }
+
+ setLastScrollTop(currentScrollTop); // 마지막 위치 갱신
+ };
+
+ window.addEventListener('scroll', handleScroll);
+
+ // 클린업 함수
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, [lastScrollTop]);
+
+ return (
+
+ );
+}
+
+export default Header;
diff --git a/src/shared/components/header/HeaderBtn.tsx b/src/shared/components/header/HeaderBtn.tsx
new file mode 100644
index 0000000..ad2c91d
--- /dev/null
+++ b/src/shared/components/header/HeaderBtn.tsx
@@ -0,0 +1,69 @@
+import Bell from '@/shared/assets/icons/bell_24.svg';
+import User from '@/shared/assets/icons/user_24.svg';
+import SignOut from '@/shared/assets/icons/sign_out_24.svg';
+import SignIn from '@/shared/assets/icons/sign_in_24.svg';
+import { useRouter } from 'next/navigation';
+import tw from '@/shared/utills/tw';
+import { useAuthStore } from '@/shared/@store/auth';
+
+type RouterType = ReturnType;
+
+function HeaderBtn({ pathname }: { pathname: string }) {
+ const { isLoggedIn, logout } = useAuthStore();
+
+ const router = useRouter();
+ const headerBtn = [
+ ...(isLoggedIn
+ ? [
+ {
+ icon: Bell,
+ label: '알림',
+ onClick: () => {},
+ },
+ {
+ icon: User,
+ label: '마이 페이지',
+ className: pathname === '/mypage' ? 'text-tertiary' : 'text-current',
+ onClick: (router: RouterType) => router.push('/mypage'),
+ },
+ {
+ icon: SignOut,
+ label: '로그아웃',
+ onClick: async () => {
+ await logout();
+ },
+ },
+ ]
+ : [
+ {
+ icon: SignIn,
+ label: '로그인',
+ className: `${pathname === '/login' ? 'text-tertiary' : ''}`,
+ onClick: () => {
+ sessionStorage.setItem('preLoginPath', window.location.pathname);
+ router.push('/login');
+ },
+ },
+ ]),
+ ];
+
+ return (
+
+ {headerBtn.map(({ icon: Icon, label, onClick, className }) => (
+
+ ))}
+
+ );
+}
+
+export default HeaderBtn;
diff --git a/src/shared/components/header/HeaderLogo.tsx b/src/shared/components/header/HeaderLogo.tsx
new file mode 100644
index 0000000..4d2d06c
--- /dev/null
+++ b/src/shared/components/header/HeaderLogo.tsx
@@ -0,0 +1,20 @@
+import Link from 'next/link';
+import Image from 'next/image';
+
+function HeaderLogo() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default HeaderLogo;
diff --git a/src/shared/components/header/NavItem.tsx b/src/shared/components/header/NavItem.tsx
new file mode 100644
index 0000000..570871f
--- /dev/null
+++ b/src/shared/components/header/NavItem.tsx
@@ -0,0 +1,30 @@
+import { navItem } from '@/shared/utills/navigation';
+import tw from '@/shared/utills/tw';
+import Link from 'next/link';
+
+interface Props {
+ pathname: string;
+ className?: string;
+}
+
+function NavItem({ pathname, className }: Props) {
+ return (
+
+ );
+}
+
+export default NavItem;
diff --git a/src/shared/components/keep/Keep.tsx b/src/shared/components/keep/Keep.tsx
new file mode 100644
index 0000000..fb7fd18
--- /dev/null
+++ b/src/shared/components/keep/Keep.tsx
@@ -0,0 +1,40 @@
+import KeepIcon from '@/shared/assets/icons/keep_36.svg';
+import KeepIconActive from '@/shared/assets/icons/keep_active_36.svg';
+import { useState } from 'react';
+
+interface Props {
+ className?: string;
+}
+
+function Keep({ className }: Props) {
+ const [isClick, setIsClick] = useState(false);
+ const handleClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsClick(!isClick);
+ };
+
+ return (
+
+ );
+}
+export default Keep;
diff --git a/src/shared/components/like/LikeBtn.tsx b/src/shared/components/like/LikeBtn.tsx
new file mode 100644
index 0000000..48cbf5c
--- /dev/null
+++ b/src/shared/components/like/LikeBtn.tsx
@@ -0,0 +1,26 @@
+import LikeIcon from '@/shared/assets/icons/like_28.svg';
+import { useState } from 'react';
+
+function LikeBtn() {
+ const [isClick, setIsClick] = useState(false);
+
+ const handleClick = () => {
+ setIsClick(!isClick);
+ };
+ return (
+
+ );
+}
+export default LikeBtn;
diff --git a/src/shared/components/modalPop/ConfirmPop.tsx b/src/shared/components/modalPop/ConfirmPop.tsx
new file mode 100644
index 0000000..faf011d
--- /dev/null
+++ b/src/shared/components/modalPop/ConfirmPop.tsx
@@ -0,0 +1,36 @@
+import Button from '../button/Button';
+import ModalLayout from './ModalLayout';
+
+interface Props {
+ ref?: React.Ref;
+ open: boolean;
+ onClose: () => void;
+ title?: string;
+ description?: React.ReactNode;
+ children?: React.ReactNode;
+}
+function ConfirmPop({ ref, open, onClose, title, description, children }: Props) {
+ return (
+
+
+
+ >
+ }
+ >
+ {children}
+
+ );
+}
+export default ConfirmPop;
diff --git a/src/shared/components/modalPop/ModalLayout.tsx b/src/shared/components/modalPop/ModalLayout.tsx
new file mode 100644
index 0000000..c522756
--- /dev/null
+++ b/src/shared/components/modalPop/ModalLayout.tsx
@@ -0,0 +1,110 @@
+'use client';
+
+import Close from '@/shared/assets/icons/close_32.svg';
+import Portal from './Portal';
+import tw from '@/shared/utills/tw';
+import { useEffect } from 'react';
+
+interface Props {
+ ref?: React.Ref;
+ size?: 'sm' | 'md';
+ open: boolean;
+ onClose: () => void;
+ title?: string;
+ description?: React.ReactNode;
+ children?: React.ReactNode;
+ buttons?: React.ReactNode;
+}
+
+function ModalLayout({
+ ref,
+ size = 'md',
+ open,
+ onClose,
+ title,
+ description,
+ children,
+ buttons,
+}: Props) {
+ // ESC키 모달 닫기
+ useEffect(() => {
+ if (!open) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose();
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [open, onClose]);
+
+ // 팝업 열릴 시 scroll 막기
+ useEffect(() => {
+ if (open) document.body.style.overflow = 'hidden';
+ else document.body.style.overflow = '';
+
+ return () => {
+ document.body.style.overflow = '';
+ };
+ }, [open]);
+
+ if (!open) return null;
+
+ return (
+
+
+
+
e.stopPropagation()}
+ >
+
+ {title && (
+
+ {title}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {children &&
{children}
}
+
+ {buttons && (
+
+ {buttons}
+
+ )}
+
+
+
+
+ );
+}
+export default ModalLayout;
diff --git a/src/shared/components/modalPop/Portal.tsx b/src/shared/components/modalPop/Portal.tsx
new file mode 100644
index 0000000..c8ad8e6
--- /dev/null
+++ b/src/shared/components/modalPop/Portal.tsx
@@ -0,0 +1,19 @@
+'use client';
+import { ReactNode, useEffect, useState } from 'react';
+import { createPortal } from 'react-dom';
+
+interface Props {
+ children: ReactNode;
+}
+
+export default function Portal({ children }: Props) {
+ const [target, setTarget] = useState(null);
+
+ useEffect(() => {
+ const el = document.getElementById('modal-root');
+ setTarget(el);
+ }, []);
+
+ if (!target) return null;
+ return createPortal(children, target);
+}
diff --git a/src/shared/components/pageHeader/PageHeader.tsx b/src/shared/components/pageHeader/PageHeader.tsx
new file mode 100644
index 0000000..eb0a654
--- /dev/null
+++ b/src/shared/components/pageHeader/PageHeader.tsx
@@ -0,0 +1,27 @@
+import Image, { StaticImageData } from 'next/image';
+import StarBg from '../starBg/StarBg';
+
+interface Props {
+ src: StaticImageData;
+ title: string;
+ description: string;
+}
+
+function PageHeader({ src, title, description }: Props) {
+ return (
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+ );
+}
+export default PageHeader;
diff --git a/src/shared/components/recipePage/cocktailCard/CocktailCard.tsx b/src/shared/components/recipePage/cocktailCard/CocktailCard.tsx
new file mode 100644
index 0000000..d362703
--- /dev/null
+++ b/src/shared/components/recipePage/cocktailCard/CocktailCard.tsx
@@ -0,0 +1,29 @@
+import { StaticImageData } from 'next/image';
+import Image from 'next/image';
+import Img from '@/shared/assets/images/dummy/exampleCocktail.png';
+import Keep from '@/shared/components/keep/Keep';
+
+interface Props {
+ src?: StaticImageData;
+ name?: string;
+ nameKo?: string;
+}
+
+function CocktailCard({ src, name, nameKo }: Props) {
+ return (
+
+
+
+
Old Fassioned
+
올드 패션드
+
+
+ );
+}
+export default CocktailCard;
diff --git a/src/shared/components/recipePage/cocktailList/CocktailList.tsx b/src/shared/components/recipePage/cocktailList/CocktailList.tsx
new file mode 100644
index 0000000..2c5ede9
--- /dev/null
+++ b/src/shared/components/recipePage/cocktailList/CocktailList.tsx
@@ -0,0 +1,34 @@
+'use client';
+import CocktailCard from '../cocktailCard/CocktailCard';
+
+function CocktailList() {
+ // const [data,setData] = useState([])
+ // useEffect(() => {
+ // fetch('http://localhost:8080/api/cocktails')
+ // .then((res) => res.json())
+ // .then((data) => {
+ // console.log(data.data)
+ // setData(data.data)
+ // });
+ // }, []);
+
+ return (
+
+ {/* {
+ data.map(({ cocktailImgUrl, cocktailId, cocktailName }) => (
+ -
+
+
+ ))
+ } */}
+
+
+
+
+
+
+
+
+ );
+}
+export default CocktailList;
diff --git a/src/shared/components/scrollTop/ScrollTopBtn.tsx b/src/shared/components/scrollTop/ScrollTopBtn.tsx
new file mode 100644
index 0000000..5ad671f
--- /dev/null
+++ b/src/shared/components/scrollTop/ScrollTopBtn.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+import Arrow from '@/shared/assets/icons/arrow_up_24.svg';
+import { throttle } from '@/shared/utills/throttle';
+import { useEffect, useRef, useState } from 'react';
+
+function ScrollTopBtn() {
+ const animationRef = useRef(null);
+ const [isVisible, setIsVisible] = useState(false);
+
+ // scrollTop 버튼 클릭 시
+ const scrollToTop = () => {
+ const currentPosition = document.documentElement.scrollTop || document.body.scrollTop;
+ if (currentPosition > 0) {
+ animationRef.current = requestAnimationFrame(scrollToTop);
+ window.scrollTo(0, currentPosition - currentPosition / 8);
+ }
+ };
+
+ // 사용자 스크롤 시 애니메이션 취소
+ useEffect(() => {
+ const cancelScroll = () => {
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current);
+ animationRef.current = null;
+ }
+ };
+
+ const handleScroll = throttle(() => {
+ const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
+ setIsVisible(currentScroll > 30);
+ }, 100);
+
+ window.addEventListener('scroll', handleScroll, { passive: true });
+ window.addEventListener('wheel', cancelScroll, { passive: true });
+ window.addEventListener('touchstart', cancelScroll, { passive: true });
+
+ return () => {
+ window.removeEventListener('scroll', handleScroll);
+ window.removeEventListener('wheel', cancelScroll);
+ window.removeEventListener('touchstart', cancelScroll);
+ };
+ }, []);
+
+ if (!isVisible) return null;
+ return (
+
+ );
+}
+export default ScrollTopBtn;
diff --git a/src/shared/components/scrollTop/ScrollTopBtnWrapper.tsx b/src/shared/components/scrollTop/ScrollTopBtnWrapper.tsx
new file mode 100644
index 0000000..16b543f
--- /dev/null
+++ b/src/shared/components/scrollTop/ScrollTopBtnWrapper.tsx
@@ -0,0 +1,12 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+import ScrollTopBtn from './ScrollTopBtn';
+
+function ScrollTopBtnWrapper() {
+ const pathname = usePathname();
+ const showScroll = pathname !== '/recommend';
+
+ return showScroll ? : null;
+}
+export default ScrollTopBtnWrapper;
diff --git a/src/shared/components/share/Share.tsx b/src/shared/components/share/Share.tsx
new file mode 100644
index 0000000..f1f292f
--- /dev/null
+++ b/src/shared/components/share/Share.tsx
@@ -0,0 +1,32 @@
+import ShareBtn from '@/shared/assets/icons/share_28.svg';
+
+interface Props {
+ onClick?: () => void;
+ variants?: 'default' | 'community';
+ title?: string;
+ content?: string;
+}
+
+function Share({ onClick, variants = 'default', title, content }: Props) {
+ // title과 content는 추후 API가 들어오면 사용예정 API가 들어오면 타입 옵셔널 체크 해제헤 주세요
+
+ return (
+
+ );
+}
+export default Share;
diff --git a/src/shared/components/spinner/Spinner.tsx b/src/shared/components/spinner/Spinner.tsx
new file mode 100644
index 0000000..248d622
--- /dev/null
+++ b/src/shared/components/spinner/Spinner.tsx
@@ -0,0 +1,21 @@
+import Lottie from 'lottie-react';
+import spinner from '@/shared/assets/lottie/loading.json';
+
+function Spinner() {
+ const style = {
+ width: 130,
+ height: 130,
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default Spinner;
diff --git a/src/shared/components/starBg/StarBg.tsx b/src/shared/components/starBg/StarBg.tsx
new file mode 100644
index 0000000..e79087c
--- /dev/null
+++ b/src/shared/components/starBg/StarBg.tsx
@@ -0,0 +1,20 @@
+import Star from '@/shared/assets/images/star_bg.webp';
+
+interface Props {
+ className: string;
+ children?: React.ReactNode;
+}
+
+function StarBg({ className, children }: Props) {
+ return (
+
+ {children}
+
+ );
+}
+export default StarBg;
diff --git a/src/shared/components/toast/CustomToast.tsx b/src/shared/components/toast/CustomToast.tsx
new file mode 100644
index 0000000..ab57f31
--- /dev/null
+++ b/src/shared/components/toast/CustomToast.tsx
@@ -0,0 +1,34 @@
+import Close from '@/shared/assets/icons/close_20.svg';
+import InfoSvg from '@/shared/assets/icons/info_toast_32.svg';
+import SuccesSvg from '@/shared/assets/icons/success_toast_32.svg';
+import ErrorSvg from '@/shared/assets/icons/error_toast_32.svg';
+
+interface Props {
+ type: 'success' | 'error' | 'info';
+ message: string;
+ onClose?: () => void;
+}
+
+function CustomToast({ type, message, onClose }: Props) {
+ return (
+
+
+ {type === 'info' && }
+ {type === 'success' && }
+ {type === 'error' && }
+
+
+
{message}
+
+
+ );
+}
+
+export default CustomToast;
diff --git a/src/shared/components/toast/CustomToastUtils.tsx b/src/shared/components/toast/CustomToastUtils.tsx
new file mode 100644
index 0000000..5da1b3c
--- /dev/null
+++ b/src/shared/components/toast/CustomToastUtils.tsx
@@ -0,0 +1,17 @@
+import toast from 'react-hot-toast';
+import CustomToast from './CustomToast';
+
+export const customToast = {
+ success: (message: string) => {
+ toast.dismiss();
+ toast( toast.dismiss()} />);
+ },
+ info: (message: string) => {
+ toast.dismiss();
+ toast( toast.dismiss()} />);
+ },
+ error: (message: string) => {
+ toast.dismiss();
+ toast( toast.dismiss()} />);
+ },
+};
diff --git a/src/hook/index.ts b/src/shared/hook/index.ts
similarity index 100%
rename from src/hook/index.ts
rename to src/shared/hook/index.ts
diff --git a/src/lib/index.ts b/src/shared/lib/index.ts
similarity index 100%
rename from src/lib/index.ts
rename to src/shared/lib/index.ts
diff --git a/src/shared/styles/_base.css b/src/shared/styles/_base.css
new file mode 100644
index 0000000..3748732
--- /dev/null
+++ b/src/shared/styles/_base.css
@@ -0,0 +1,9 @@
+@layer base {
+ button {
+ cursor: pointer;
+ }
+
+ select{
+ appearance:none
+ }
+}
diff --git a/src/styles/_components.css b/src/shared/styles/_components.css
similarity index 100%
rename from src/styles/_components.css
rename to src/shared/styles/_components.css
diff --git a/src/shared/styles/_theme.css b/src/shared/styles/_theme.css
new file mode 100644
index 0000000..85be9a5
--- /dev/null
+++ b/src/shared/styles/_theme.css
@@ -0,0 +1,22 @@
+@theme {
+ /* breakpoint */
+ /* mo 0 ~ 767 */
+ /* tablet 768 ~ 1023 */
+ /* pc 1024~ */
+
+ --color-primary: #1a1a1a;
+ --color-secondary: #ffffe6;
+ --color-tertiary: #81689d;
+ --color-gray-dark: #636363;
+ --color-gray: #949494;
+ --color-gray-light: #e3e3e3;
+ --color-white: #ffffff;
+ --color-black: #0f172a;
+ --color-bg-pop: #3d3d3d;
+ --color-navy: #1f2544;
+ --font-serif: 'Hahmlet', serif;
+
+ --inset-shadow-black: inset 0 0px 6px rgba(0, 0, 0, 0.6);
+
+ --shadow-header: 0 6px 17px 0 rgba(255, 210, 187, 0.3);
+}
diff --git a/src/shared/styles/_utilities.css b/src/shared/styles/_utilities.css
new file mode 100644
index 0000000..7846bf0
--- /dev/null
+++ b/src/shared/styles/_utilities.css
@@ -0,0 +1,42 @@
+@layer utilities {
+ .flex-center {
+ @apply flex justify-center items-center;
+ }
+
+ /*
+ 모든 페이지는 div 한번 감싸야 함
+ - flex-1 적용 → 상위 main/flex 구조에서 남은 화면 높이 채우기
+ - max-width 설정 → 페이지별 디자인 가이드에 맞춰서 (ex: 824, 1024, 1224, full)
+ - padding 적용 → 좌우 여백 통일 (ex: px-4)
+ - 중앙 정렬 유지 → mx-auto
+
+
+ * max-width
+ 824 → 로그인, 커뮤니티 상세/글쓰기
+ 1024 → 레시피 상세, 챗봇, 커뮤니티리스트, 마이페이지
+ 1224 → 레시피 리스트
+ full → 메인
+
+ * 824 레이아웃 예시
+ ex)
+ */
+
+ /* 페이지 레이아웃 기본 값 */
+ .page-layout {
+ @apply flex-1 w-full px-3 mx-auto;
+ }
+
+ /* max-width 별 */
+ .max-w-824 {
+ @apply max-w-[51.5rem];
+ }
+ .max-w-1024 {
+ @apply max-w-[64rem];
+ }
+ .max-w-1224 {
+ @apply max-w-[76.5rem];
+ }
+ .max-w-full {
+ @apply max-w-full;
+ }
+}
diff --git a/src/styles/global.css b/src/shared/styles/global.css
similarity index 53%
rename from src/styles/global.css
rename to src/shared/styles/global.css
index d28fcaf..b4a696b 100644
--- a/src/styles/global.css
+++ b/src/shared/styles/global.css
@@ -1,6 +1,6 @@
@import url('https://hangeul.pstatic.net/hangeul_static/css/nanum-square-neo.css');
@import url('https://fonts.googleapis.com/css2?family=Hahmlet:wght@100..900&display=swap');
-@import "tailwindcss";
+@import 'tailwindcss';
@import './_base.css';
@import './_theme.css';
@@ -8,9 +8,22 @@
@import './_components.css';
:root {
- font-family:
- 'NanumSquareNeo',sans-serif;
+ font-family: 'NanumSquareNeo', sans-serif;
line-height: 1.5;
font-weight: 500;
+ color: #fff;
+ background-color: var(--color-primary);
}
+.scroll-up {
+ transform: translateY(0);
+}
+
+.scroll-down {
+ transform: translateY(-100%);
+}
+
+.no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
diff --git a/src/utills/debounce.ts b/src/shared/utills/debounce.ts
similarity index 100%
rename from src/utills/debounce.ts
rename to src/shared/utills/debounce.ts
diff --git a/src/shared/utills/keyDown.ts b/src/shared/utills/keyDown.ts
new file mode 100644
index 0000000..7dc6e48
--- /dev/null
+++ b/src/shared/utills/keyDown.ts
@@ -0,0 +1,8 @@
+export const keyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ if (e.nativeEvent.isComposing) {
+ return;
+ }
+ }
+};
diff --git a/src/shared/utills/navigation.ts b/src/shared/utills/navigation.ts
new file mode 100644
index 0000000..226461b
--- /dev/null
+++ b/src/shared/utills/navigation.ts
@@ -0,0 +1,5 @@
+export const navItem = [
+ { href: '/recipe', label: '칵테일 찾기' },
+ { href: '/recommend', label: '취향추천받기' },
+ { href: '/community', label: '커뮤니티' },
+];
diff --git a/src/utills/throttle.ts b/src/shared/utills/throttle.ts
similarity index 100%
rename from src/utills/throttle.ts
rename to src/shared/utills/throttle.ts
diff --git a/src/utills/tw.ts b/src/shared/utills/tw.ts
similarity index 100%
rename from src/utills/tw.ts
rename to src/shared/utills/tw.ts
diff --git a/src/styles/_base.css b/src/styles/_base.css
deleted file mode 100644
index 97fca66..0000000
--- a/src/styles/_base.css
+++ /dev/null
@@ -1,2 +0,0 @@
-@layer base;
-
diff --git a/src/styles/_theme.css b/src/styles/_theme.css
deleted file mode 100644
index 6409a85..0000000
--- a/src/styles/_theme.css
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-@layer theme;
-
-@theme {
- /* primary */
- --color-primary: #1a1a1a;
-
- /* secondary */
- --color-secondary:#FFFFE6;
-
- /* tertiary */
- --color-tertiary:#81689D;
-
- /* gray-dark */
- --color-graydark:#636363;
-
- /* gray */
- --color-gray:#949494;
-
- /* gray-light */
- --color-graylight: #e3e3e3;
-
- /* white */
- --color-white:#ffffff;
-
- /* black */
- --color-black:#0f172a;
-
- /* bg-pop */
- --color-bgpop:#3d3d3d;
-
- /* font-serif */
- --font-serif: "Hahmlet", serif;
-
- /* text-xsm */
- --text-xsm: 12px;
-}
diff --git a/src/styles/_utilities.css b/src/styles/_utilities.css
deleted file mode 100644
index e132857..0000000
--- a/src/styles/_utilities.css
+++ /dev/null
@@ -1,5 +0,0 @@
-@layer utilities{
-.flex-center {
- @apply flex justify-center items-center;
- }
-}
diff --git a/src/utills/scrollToTop.ts b/src/utills/scrollToTop.ts
deleted file mode 100644
index f960a4a..0000000
--- a/src/utills/scrollToTop.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function throttle void>(func: F, delay: number) {
- let lastCall = 0;
- return function (...args: Parameters) {
- const now = Date.now();
- if (now - lastCall >= delay) {
- lastCall = now;
- func(...args);
- }
- };
-}
diff --git a/src/utills/test.ts b/src/utills/test.ts
deleted file mode 100644
index 4f511a8..0000000
--- a/src/utills/test.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-// 테스트 입니다
-
-export const test = () => {
- console.log('test');
-};
-
-// 수정입니다.
-// 수정입니다.2
-// 수정입니다.3
-// 수정입니다.4