Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/images/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ body {
.BG-blue-disabled {
@apply cursor-not-allowed bg-[#9FA6B2];
}

.BG-violet {
@apply bg-[#228DFF];
}
.Text-black {
@apply text-[#333236] dark:text-[#FFFFFF];
}
Expand Down
6 changes: 4 additions & 2 deletions src/app/shared/components/common/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import Image from 'next/image'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'

import { useModalStore } from '@/app/shared/store/useModalStore'

import CreateDashboardButton from './CreateDashboardButton'
import DashboardItem from './DashboardItem'

export default function Sidebar(): JSX.Element {
const pathname = usePathname()
const router = useRouter()
const { openCreateDashboardModal } = useModalStore()

// TODO: 목데이터 - API 연동시 삭제예정
const mockDashboards = [
Expand Down Expand Up @@ -65,8 +68,7 @@ export default function Sidebar(): JSX.Element {
}

const handleCreateDashboard = () => {
// TODO: 대시보드 생성 모달 열기
console.log('대시보드 생성 모달 열기임')
openCreateDashboardModal()
}
return (
<aside className="BG-white Border-section fixed left-0 top-0 h-screen w-300 overflow-y-auto">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use client'

import Image from 'next/image'
import { useRouter } from 'next/navigation'
import React, { useEffect, useState } from 'react'

import api from '@/app/shared/lib/axios'
import { useModalStore } from '@/app/shared/store/useModalStore'
import { CreateDashboardRequest } from '@/app/shared/types/dashboard'

const DASHBOARD_COLORS = ['#10B981', '#8B5CF6', '#F59E0B', '#3B82F6', '#EC4899']

export default function CreateDashboardModal() {
const router = useRouter()
const { createDashboardModalOpen, closeCreateDashboardModal } =
useModalStore()

const [formData, setFormData] = useState<CreateDashboardRequest>({
title: '',
color: DASHBOARD_COLORS[0],
})

const [isSubmitting, setIsSubmitting] = useState(false)

useEffect(() => {
if (!createDashboardModalOpen) {
setFormData({ title: '', color: DASHBOARD_COLORS[0] })
setIsSubmitting(false)
}
}, [createDashboardModalOpen])

if (!createDashboardModalOpen) {
return null
}

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()

if (!formData.title || !formData.color) {
return
}
try {
setIsSubmitting(true)

const response = await api.post(`/dashboards`, formData)

const data = response.data

// 성공 시 대시보드 상세 페이지로 이동
router.push(`/dashboard/${data.id}`)
closeCreateDashboardModal()
} catch (error) {
console.error('대시보드 생성 오류:', error)
} finally {
setIsSubmitting(false)
}
}

// 입력값 변경 처리
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData((prev) => ({
...prev,
[name]: value,
}))
}

// 색상 선택 처리
const handleColorSelect = (color: string) => {
setFormData((prev) => ({ ...prev, color }))
}

// 모달 외부 클릭 시 닫기
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.target이랑 e.currentTarget이 각각 뭘 가리키고 있는건가요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zustand를 사용한 이유는 모달 관련 로직을 한곳에서 관리하기 쉽다고 생각했고, 추후에 다른 모달(컬럼 모달)도 비슷한 패턴으로 추가하기 쉽다고 판단해서 사용했습니다.

현재 구조에서 SidebarCreateDashboardModal이 각각 독립적으로 useModalStore를 사용해서 상태를 공유하고 있는데, 이렇게 이용하면 컴포넌트 간 결합도를 낮출 수 있기 때문입니다. 만약 useState를 사용했다면 상위 컴포넌트에서 모달 상태를 관리하고 props로 전달해서 사용했었어야 했습니다.

e.target은 실제로 클릭된 요소(이벤트가 발생한 요소)이고,
e.currentTarget은 이벤트 리스너가 등록된 요소(해당 코드에서는 모달 백드롭 div)를 가리킵니다.

모달 외부/배경 클릭 시 e.target === e.currentTarget (백드롭 자체 클릭) -> 모달이 닫힘.
모달 내부 클릭 시 e.target !== e.currentTarget (자식 요소 클릭, 이벤트 버블링) -> 모달 유지

이 방식으로 모달 외부 클릭 시에만 모달을 닫고, 모달 내부 클릭 시에는 닫히지 않도록 구현했습니다.

closeCreateDashboardModal()
}
}

return (
// 모달 백드롭
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
onClick={handleBackdropClick}
>
{/* 모달 컨테이너 */}
<div className="BG-white h-344 w-584 rounded-16 p-32">
<h2 className="Text-black mb-24 text-24 font-bold">새로운 대시보드</h2>

<form onSubmit={handleSubmit}>
{/* 제목 입력 */}
<div className="mb-24">
<label htmlFor="title" className="Text-black mb-8 block text-18">
대시보드 이름
</label>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="대시보드 이름을 입력해주세요."
className="Border-section w-full rounded-8 px-12 py-10 text-16 outline-none"
required
/>
</div>

{/* 색상 선택 */}
<div className="mb-32">
<div className="flex gap-8">
{DASHBOARD_COLORS.map((color) => (
<button
key={color}
type="button"
onClick={() => handleColorSelect(color)}
className="relative flex size-30 items-center justify-center rounded-full"
style={{ backgroundColor: color }}
aria-label={`색상 ${color}`}
>
{/* 선택된 색상 체크 */}
{formData.color === color && (
<div className="relative size-24 items-center justify-center">
<Image
src="/images/check.svg"
alt="check"
fill
className="object-contain"
/>
</div>
)}
</button>
))}
</div>
</div>

{/* 하단 버튼 */}
<div className="flex justify-end gap-10">
<button
type="button"
onClick={closeCreateDashboardModal}
className="Border-btn Text-black h-54 w-256 rounded-8 px-16 py-10 text-16 font-semibold"
>
취소
</button>
<button
type="submit"
disabled={!formData.title || !formData.color || isSubmitting}
className={`BG-violet h-54 w-256 rounded-8 px-16 py-10 text-16 font-semibold text-white transition-opacity ${
!formData.title || !formData.color || isSubmitting
? 'cursor-not-allowed opacity-50'
: 'hover:opacity-90'
}`}
>
생성
</button>
</div>
</form>
</div>
</div>
)
}
3 changes: 2 additions & 1 deletion src/app/shared/lib/axios.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from 'axios'
import { useAuthStore } from '@/app/features/auth/store/useAuthStore'

import { AUTH_ENDPOINT } from '@/app/features/auth/api/authEndpoint'
import { useAuthStore } from '@/app/features/auth/store/useAuthStore'

const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
Expand Down
Empty file removed src/app/shared/store/.gitkeep
Empty file.
14 changes: 14 additions & 0 deletions src/app/shared/store/useModalStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { create } from 'zustand'

import { ModalState } from '../types/dashboard'

export const useModalStore = create<ModalState>((set) => ({
// 초기 상태
createDashboardModalOpen: false,

// 모달 열기
openCreateDashboardModal: () => set({ createDashboardModalOpen: true }),

// 모달 닫기
closeCreateDashboardModal: () => set({ createDashboardModalOpen: false }),
}))
Comment on lines +1 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zustand로 모달 상태 관리 해주셨군용!!
참고해서 공통 모달 컴포넌트 작성해보도록 하겠습니다~

7 changes: 7 additions & 0 deletions src/app/shared/types/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@ export interface CreateDashboardRequest {
title: string
color: string
}

// 대시보드 생성 모달
export interface ModalState {
createDashboardModalOpen: boolean
openCreateDashboardModal: () => void
closeCreateDashboardModal: () => void
}
19 changes: 17 additions & 2 deletions src/app/tester/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
'use client'

import Image from 'next/image'

import Header from '@/app/shared/components/common/header/Header'
import Sidebar from '@/app/shared/components/common/sidebar/Sidebar'
import ThemeToggle from '@/app/shared/components/ThemeToggle'

import CreateDashboardModal from '../shared/components/common/sidebar/modal/CreateDashboardModal'
import { useModalStore } from '../shared/store/useModalStore'

//<초기 설정 안내>

// 이미지 파일에 접근할 때: /images/파일명
Expand All @@ -18,20 +23,30 @@ import ThemeToggle from '@/app/shared/components/ThemeToggle'
// globals.css에 작성한 커스텀 유틸 클래스(@apply) 참고해서, 클래스명 가져다 사용하거나 직접 커스텀

export default function Home() {
const { openCreateDashboardModal } = useModalStore()

return (
<>
<Header />
<div className="flex">
{/* 사이드바 */}
<Sidebar />

{/* 메인 콘텐츠 영역 */}
{/* 메인 콘텐츠 영역 */}
<div className="ml-300 p-20">
{/* 헤더 영역 */}
<div className="mb-24">
<h1 className="mb-16 text-24 font-bold">Sidebar 테스트 페이지</h1>
<p className="Text-gray mb-20">왼쪽에 사이드바 만들어보자잇!</p>
<ThemeToggle />
{/* 모달 테스트 버튼 - 이 부분을 추가! */}
<button
onClick={openCreateDashboardModal}
className="BG-blue mt-12 rounded-6 px-16 py-10 text-white"
>
대시보드 생성 모달 테스트
</button>
{/* 모달 버튼 컴포넌트 추가 - 이 부분도 추가! */}
<CreateDashboardModal />
</div>

{/* 기존 테스트 요소들 */}
Expand Down