Skip to content

Commit 96b0abb

Browse files
committed
feat: 전역 모달 시스템 구축
- Zustand를 활용한 `useModalStore` 전역 상태 관리 구현 (combine, devtools 미들웨어 적용) - 모달 렌더링을 담당하는 `GlobalModal` 컴포넌트 생성 및 RootLayout에 배치 - Header 및 Footer의 '피드백 남기기' 버튼을 전역 스토어와 연동
1 parent 6b45785 commit 96b0abb

6 files changed

Lines changed: 77 additions & 16 deletions

File tree

app/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Metadata } from 'next';
33
import localFont from 'next/font/local';
44
import Header from '../components/header';
55
import Footer from '../components/footer';
6+
import GlobalModal from '@/components/globalModal';
67

78
const pretendard = localFont({
89
src: [
@@ -34,7 +35,10 @@ export default function RootLayout({
3435
<html lang="ko" className={pretendard.variable}>
3536
<body className="flex min-h-screen flex-col">
3637
<Header />
37-
<main className="flex-1">{children}</main>
38+
<main className="flex-1">
39+
{children}
40+
<GlobalModal />
41+
</main>
3842
<Footer />
3943
</body>
4044
</html>
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export default function FeedbackModal({ isOpen, onClose }: FeedbackModalProps) {
2525

2626
const handleSubmit = () => {
2727
if (!isValid) return;
28-
2928
console.log('제출 데이터:', { score, reason });
30-
// TODO: API 호출 로직 추가
29+
30+
// TODO: API 호출
3131
onClose();
3232
};
3333

@@ -61,7 +61,7 @@ export default function FeedbackModal({ isOpen, onClose }: FeedbackModalProps) {
6161
<button
6262
key={num}
6363
onClick={() => setScore(num)}
64-
className={`hover:bg-blue-1 flex h-10 w-full items-center justify-center rounded border text-base font-normal transition-colors focus:outline-none ${score === num ? 'bg-blue-5 text-white' : 'border-gray-2 text-gray-7 bg-white'} `}
64+
className={`flex h-10 w-full items-center justify-center rounded border text-base font-normal focus:outline-none ${score === num ? 'bg-blue-5 text-white' : 'border-gray-2 text-gray-7 hover:bg-blue-1 bg-white'} `}
6565
>
6666
{num}
6767
</button>

components/footer.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
'use client';
2+
13
import Link from 'next/link';
24
import Image from 'next/image';
5+
import { useModalStore } from '@/store/useModalStore';
36

47
const ICON = ['threads', 'instagram'];
58

69
const Footer = () => {
10+
const { onOpen } = useModalStore();
11+
712
return (
813
<footer className="bg-gray-1 flex h-59 items-center md:h-35">
914
<div className="flex w-full flex-col items-start gap-5 px-5 md:flex-row md:justify-between md:gap-0">
@@ -22,11 +27,16 @@ const Footer = () => {
2227
<Link href="/" className="text-gray-7 text-[16px]">
2328
문의하기
2429
</Link>
25-
<Link href="/" className="text-gray-7 text-[16px]">
30+
<button
31+
type="button"
32+
onClick={() => onOpen('FEEDBACK')}
33+
className="text-gray-7 cursor-pointer text-[16px]"
34+
>
2635
피드백남기기
27-
</Link>
36+
</button>
2837
</div>
2938
</div>
39+
{/* 쓰레드, 인스타그램 아이콘 렌더링 */}
3040
<div className="text-gray-4 flex gap-3">
3141
{ICON.map((item, idx) => (
3242
<Link href={`https://www.${item}.com/`} key={idx}>

components/globalModal.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client';
2+
3+
import { useModalStore } from '@/store/useModalStore';
4+
import FeedbackModal from '@/components/feedback/feedbackModal';
5+
// import ShareModal from '@/components/ShareModal'; // 추후 제작 예정
6+
// import NudgeModal from '@/components/NudgeModal'; // 추후 제작 예정
7+
8+
export default function GlobalModal() {
9+
const { type, isOpen, onClose, props } = useModalStore();
10+
11+
// 닫혀있거나 타입이 없으면 모달 X
12+
if (!isOpen || !type) return null;
13+
14+
// 타입에 따라 다른 컴포넌트 렌더링.
15+
switch (type) {
16+
case 'FEEDBACK':
17+
return <FeedbackModal isOpen={isOpen} onClose={onClose} />;
18+
case 'SHARE':
19+
return;
20+
// return <ShareModal isOpen={isOpen} onClose={onClose} {...props} />;
21+
case 'NUDGE':
22+
return;
23+
// return <NudgeModal isOpen={isOpen} onClose={onClose} {...props} />;
24+
default:
25+
return null;
26+
}
27+
}

components/header.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
'use client';
22

3-
import { useState } from 'react';
43
import Link from 'next/link';
54
import Image from 'next/image';
6-
import FeedbackModal from './feedbackModal';
5+
import { useModalStore } from '@/store/useModalStore';
76

87
const Header = () => {
9-
// 3. 모달 상태 관리 (기본값: 닫힘 false)
10-
const [isFeedbackOpen, setIsFeedbackOpen] = useState(true);
8+
const { onOpen } = useModalStore();
119

1210
return (
1311
<header className="border-gray-1 sticky top-0 right-0 left-0 flex h-15 items-center justify-center border-b bg-white">
@@ -19,19 +17,15 @@ const Header = () => {
1917
<Link href="/" className="text-gray-5 p-2 text-[16px]">
2018
문의하기
2119
</Link>
22-
23-
{/* 4. 버튼 클릭 시 상태를 true로 변경 */}
2420
<button
25-
onClick={() => setIsFeedbackOpen(true)}
21+
type="button"
22+
onClick={() => onOpen('FEEDBACK')}
2623
className="text-gray-5 p-2 text-[16px] transition-colors hover:text-gray-900"
2724
>
2825
피드백남기기
2926
</button>
3027
</nav>
3128
</div>
32-
33-
{/* 5. 모달 렌더링 (Header 안에 두어도 Portal 덕분에 화면 위에 잘 뜹니다) */}
34-
<FeedbackModal isOpen={isFeedbackOpen} onClose={() => setIsFeedbackOpen(false)} />
3529
</header>
3630
);
3731
};

store/useModalStore.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { create } from 'zustand';
2+
import { combine, devtools } from 'zustand/middleware';
3+
4+
// 관리할 모달의 종류 정의 (피드백남기기, 링크 공유하기, 재촉하기)
5+
export type ModalType = 'FEEDBACK' | 'SHARE' | 'NUDGE';
6+
7+
export const useModalStore = create(
8+
devtools(
9+
combine(
10+
{
11+
type: null as ModalType | null,
12+
isOpen: false,
13+
props: null as any,
14+
},
15+
(set) => ({
16+
onOpen: (type: ModalType, props: any = null) => {
17+
set({ isOpen: true, type, props });
18+
},
19+
onClose: () => {
20+
set({ isOpen: false });
21+
},
22+
})
23+
),
24+
{ name: 'ModalStore' }
25+
)
26+
);

0 commit comments

Comments
 (0)