+ {/* hover 또는 터치 시 연필 아이콘 */}
+
{/* 이미지 미리보기 */}
diff --git a/src/app/features/mypage/hook/useUserQurey.ts b/src/app/features/mypage/hook/useUserQurey.ts
index 14e3a36..6d31194 100644
--- a/src/app/features/mypage/hook/useUserQurey.ts
+++ b/src/app/features/mypage/hook/useUserQurey.ts
@@ -1,10 +1,10 @@
import { useQuery } from '@tanstack/react-query'
-import { loadUser } from '../api/mypageApi'
+import { fetchUser } from '../api/mypageApi'
export function useUserQuery() {
return useQuery({
- queryKey: ['loadUser'],
- queryFn: loadUser,
+ queryKey: ['fetchUser'],
+ queryFn: fetchUser,
})
}
diff --git a/src/app/features/mypage/hook/useNewPasswordValidation.ts b/src/app/features/mypage/util/getNewPasswordValidation.ts
similarity index 82%
rename from src/app/features/mypage/hook/useNewPasswordValidation.ts
rename to src/app/features/mypage/util/getNewPasswordValidation.ts
index 8618626..6acbe48 100644
--- a/src/app/features/mypage/hook/useNewPasswordValidation.ts
+++ b/src/app/features/mypage/util/getNewPasswordValidation.ts
@@ -1,6 +1,6 @@
import { mypageValidation } from '../schemas/mypageValidation'
-export function useNewPasswordValidation(getPasswordValue: () => string) {
+export function getNewPasswordValidation(getPasswordValue: () => string) {
return {
...mypageValidation.password,
validate: (value: string) => {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6afe015..475bea7 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -7,10 +7,35 @@ import { Providers } from './providers'
import GlobalModalRenderer from './shared/components/common/GlobalModalRender'
export const metadata: Metadata = {
+ metadataBase: new URL('https://coplan.work'),
title: 'Coplan',
- description: 'Generated by create next app',
+ description: '팀워크를 더 쉽게 만드는 협업 툴',
icons: {
icon: '/favicon.ico',
+ shortcut: '/favicon-16x16.png',
+ apple: '/apple-touch-icon.png',
+ },
+ openGraph: {
+ title: 'Coplan - 팀워크를 더 쉽게',
+ description: '효율적인 협업을 위한 최고의 선택',
+ url: 'https://coplan.work',
+ siteName: 'Coplan',
+ images: [
+ {
+ url: '/banner.png', // 이건 네가 넣을 이미지
+ width: 1200,
+ height: 630,
+ alt: 'Coplan OG 이미지',
+ },
+ ],
+ type: 'website',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Coplan - 팀워크를 더 쉽게',
+ description: '프로젝트 관리를 위한 스마트한 협업툴',
+ images: ['/banner.png'],
+ creator: '@coplan_team',
},
}
@@ -20,7 +45,7 @@ export default function RootLayout({
children: React.ReactNode
}>) {
return (
-
+
{children}
diff --git a/src/app/loading.tsx b/src/app/loading.tsx
index 09367d7..d72ec70 100644
--- a/src/app/loading.tsx
+++ b/src/app/loading.tsx
@@ -1,3 +1,43 @@
+'use client'
+
+import { useMounted } from '@hooks/useMounted'
+import Image from 'next/image'
+import { useTheme } from 'next-themes'
+
export default function Loading() {
- return Loading...
+ const { theme, systemTheme } = useTheme()
+ const mounted = useMounted()
+ if (!mounted) return null
+
+ const currentTheme = theme === 'system' ? systemTheme : theme
+ const isDark = currentTheme === 'dark'
+
+ return (
+
+ )
}
diff --git a/src/app/mydashboard/api/dashboardApi.ts b/src/app/mydashboard/api/dashboardApi.ts
index d03b7ac..7222d6c 100644
--- a/src/app/mydashboard/api/dashboardApi.ts
+++ b/src/app/mydashboard/api/dashboardApi.ts
@@ -1,4 +1,5 @@
-import authHttpClient from '@/app/shared/lib/axios'
+import authHttpClient from '@api/axios'
+
import {
DashboardListResponse,
InvitationListResponse,
diff --git a/src/app/mydashboard/page.tsx b/src/app/mydashboard/page.tsx
index 0311ade..03b3776 100644
--- a/src/app/mydashboard/page.tsx
+++ b/src/app/mydashboard/page.tsx
@@ -13,17 +13,17 @@ export default function MyDashboardPage() {
{/* 메인 */}
-
+
{/* 헤더 */}
{/* 페이지 콘텐츠 */}
-
+
{/* 초대받은 대시보드 섹션 */}
-
-
+
+
초대받은 대시보드
diff --git a/src/app/mypage/layout.tsx b/src/app/mypage/layout.tsx
new file mode 100644
index 0000000..7ba9382
--- /dev/null
+++ b/src/app/mypage/layout.tsx
@@ -0,0 +1,20 @@
+import Header from '@components/common/header/Header'
+
+import Sidebar from '@/app/shared/components/common/sidebar/Sidebar'
+
+export default function MypageLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+ <>
+
+ {/* Sidebar의 반응형이 적용 될 경우 변경 예정 */}
+
+
+ {children}
+
+ >
+ )
+}
diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx
index 63f7a1b..1b381a6 100644
--- a/src/app/mypage/page.tsx
+++ b/src/app/mypage/page.tsx
@@ -1,58 +1,22 @@
'use client'
-import { useRouter } from 'next/navigation'
+import BackButton from '@components/common/BackButton/BackButton'
+import PasswordChangeForm from '@mypage/components/PasswordChangeForm'
+import ProfileEditForm from '@mypage/components/ProfileEditForm'
-import Header from '@/app/shared/components/common/header/Header'
-import Sidebar from '@/app/shared/components/common/sidebar/Sidebar'
-
-import PasswordChangeForm from '../features/mypage/components/PasswordChangeForm'
-import ProfileEditForm from '../features/mypage/components/ProfileEditForm'
export default function Mypage() {
- const router = useRouter()
return (
<>
-
-
-
-
-
- {/* 사이드바 */}
-
- {/* 메인 콘텐츠 영역 */}
-
- {/* 헤더 영역 */}
-
- {/* 임시 버튼 (교체 예정) */}
-
-
-
- {/* 닉네임 프로필 변경 */}
-
- {/* 비밀번호 변경 */}
-
-
-
+
+ {/* 헤더 영역 */}
+
+ {/* 뒤로 가기 버튼 */}
+
+ {/* 닉네임 프로필 변경 */}
+
+ {/* 비밀번호 변경 */}
+
+
>
)
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index 697bb30..a2815fb 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -1,3 +1,62 @@
+'use client'
+
+import { useMounted } from '@hooks/useMounted'
+import Image from 'next/image'
+import { useTheme } from 'next-themes'
+
export default function NotFound() {
- return
not found...
+ const { theme, systemTheme } = useTheme()
+ const mounted = useMounted()
+ if (!mounted) return null
+
+ const currentTheme = theme === 'system' ? systemTheme : theme
+ const isDark = currentTheme === 'dark'
+
+ return (
+
+
+
+
+
+ 404 Not Found
+
+
+ 요청하신 페이지를 찾을 수 없습니다.
+
+
+
+ )
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index d75e879..de2bde3 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,13 +1,9 @@
-import Footer from './features/landing/components/Footer'
-import Header from './features/landing/components/Header'
-import Main from './features/landing/components/Main'
+import Landing from './features/landing/components/Landing'
export default function Home() {
return (
<>
-
-
-
+
>
)
}
diff --git a/src/app/shared/lib/axios.ts b/src/app/shared/api/axios.ts
similarity index 100%
rename from src/app/shared/lib/axios.ts
rename to src/app/shared/api/axios.ts
diff --git a/src/app/shared/components/Input.tsx b/src/app/shared/components/Input.tsx
deleted file mode 100644
index 6256958..0000000
--- a/src/app/shared/components/Input.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-'use client'
-
-import Image from 'next/image'
-import { forwardRef, InputHTMLAttributes, useState } from 'react'
-
-import { cn } from '../lib/cn'
-
-interface CustomInputProps extends InputHTMLAttributes
{
- labelName: string
- name: string
- type?: React.HTMLInputTypeAttribute
- autoComplete?: string
- placeholder?: string
- hasError?: boolean
- errorMessage?: string
-}
-
-const Input = forwardRef(
- function Input(props, ref) {
- const {
- labelName,
- name,
- type = 'text',
- placeholder,
- hasError,
- errorMessage,
- autoComplete,
- ...rest
- } = props
-
- const [showPassword, setShowPassword] = useState(false)
- const isPassword = type === 'password'
- const inputType = isPassword && showPassword ? 'text' : type
-
- return (
-
-
-
- {isPassword ? (
-
-
-
-
- ) : (
-
- )}
-
- {hasError && errorMessage && (
-
{errorMessage}
- )}
-
- )
- },
-)
-
-Input.displayName = 'Input'
-
-export default Input
diff --git a/src/app/shared/components/Redirect.tsx b/src/app/shared/components/Redirect.tsx
index e9f3ff6..983246f 100644
--- a/src/app/shared/components/Redirect.tsx
+++ b/src/app/shared/components/Redirect.tsx
@@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from 'react'
import { useAuthStore } from '@/app/features/auth/store/useAuthStore'
+// 로그인 없이 접근 가능한 경로
const PUBLIC_ROUTES = ['/login', '/signup']
export default function Redirect({ children }: { children: React.ReactNode }) {
@@ -14,12 +15,16 @@ export default function Redirect({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const mounted = useMounted()
const { isLoggedIn } = useAuthStore()
- const [redirecting, setRedirecting] = useState(false) // 중복 호출 방지
- const prevPath = useRef(pathname) // 이전 경로 저장
+ const [redirecting, setRedirecting] = useState(false)
+ const prevPath = useRef(pathname)
const { data: firstDashboardId, isSuccess } = useFirstDashboardIdQuery()
- // pathname 바뀌면 redirecting 초기화
+ // ✅ 경로 파생값 선언 (중복 제거)
+ const isRoot = pathname === '/'
+ const isPublic = PUBLIC_ROUTES.includes(pathname)
+
+ // 경로 변경 시 redirecting 상태 초기화
useEffect(() => {
if (prevPath.current !== pathname) {
setRedirecting(false)
@@ -27,35 +32,38 @@ export default function Redirect({ children }: { children: React.ReactNode }) {
}
}, [pathname])
+ // 로그인 상태와 경로에 따른 리다이렉트 처리
useEffect(() => {
- if (!mounted || redirecting) return // 마운트가 되지 않았거나 리다이렉션 중이 아니면 return
+ if (!mounted || redirecting) return
- const isPublic = PUBLIC_ROUTES.includes(pathname) //로그인 없이 접근 가능한 공개 라우트
+ // 1. 비로그인 + 루트(/): 랜딩 페이지 접근 허용
+ if (!isLoggedIn && isRoot) return
- // 🔒 비로그인 상태에서 private 경로 접근 시 → /login
- if (!isLoggedIn && !isPublic && pathname !== '/') {
+ // 2. 비로그인 + 보호 경로: 로그인 페이지로 이동
+ if (!isLoggedIn && !isPublic && !isRoot) {
setRedirecting(true)
router.replace('/login')
return
}
- // 🔐 로그인 상태에서 루트 접근 시 → /dashboard/{id}
- if (isLoggedIn && pathname === '/') {
+ // 3. 로그인 + 루트(/): 대시보드 또는 마이대시보드로 이동
+ if (isLoggedIn && isRoot) {
+ if (!isSuccess) return
setRedirecting(true)
- if (isSuccess && firstDashboardId) {
- router.replace(`/dashboard/${firstDashboardId}`)
- } else if (isSuccess && !firstDashboardId) {
- router.replace('/mydashboard')
- }
+ router.replace(
+ firstDashboardId ? `/dashboard/${firstDashboardId}` : '/mydashboard',
+ )
return
}
- // 🔐 로그인 + 퍼블릭 경로 접근 시 → /mydashboard
+ // 4. 로그인 + 퍼블릭 경로: 마이대시보드로 이동
if (isLoggedIn && isPublic) {
setRedirecting(true)
router.replace('/mydashboard')
return
}
+
+ // 5. 나머지는 접근 허용
}, [
pathname,
isLoggedIn,
@@ -64,8 +72,13 @@ export default function Redirect({ children }: { children: React.ReactNode }) {
router,
isSuccess,
firstDashboardId,
+ isRoot,
+ isPublic,
])
- if (!mounted) return null
+ // 🔒 깜빡임 방지: 루트 경로만 예외로 즉시 렌더링 허용
+ if (!mounted && !isRoot) return null
+
+ // ✅ 최종 렌더링
return <>{children}>
}
diff --git a/src/app/shared/components/common/BackButton/BackButton.tsx b/src/app/shared/components/common/BackButton/BackButton.tsx
new file mode 100644
index 0000000..b76d45c
--- /dev/null
+++ b/src/app/shared/components/common/BackButton/BackButton.tsx
@@ -0,0 +1,23 @@
+'use client'
+
+import Image from 'next/image'
+import { useRouter } from 'next/navigation'
+
+export default function BackButton() {
+ const router = useRouter()
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/app/shared/components/common/Input/Input.tsx b/src/app/shared/components/common/Input/Input.tsx
new file mode 100644
index 0000000..560621f
--- /dev/null
+++ b/src/app/shared/components/common/Input/Input.tsx
@@ -0,0 +1,51 @@
+'use client'
+
+import { cn } from '@lib/cn'
+import { forwardRef } from 'react'
+
+import { InputProps } from '@/types/input.type'
+
+const Input = forwardRef(
+ function Input(props, ref) {
+ const {
+ labelName,
+ name,
+ type = 'text',
+ placeholder,
+ hasError,
+ errorMessage,
+ autoComplete,
+ ...rest
+ } = props
+
+ return (
+
+
+
+
+ {hasError && errorMessage && (
+
{errorMessage}
+ )}
+
+ )
+ },
+)
+
+Input.displayName = 'Input'
+
+export default Input
diff --git a/src/app/shared/components/common/Input/PasswordInput.tsx b/src/app/shared/components/common/Input/PasswordInput.tsx
new file mode 100644
index 0000000..5d90373
--- /dev/null
+++ b/src/app/shared/components/common/Input/PasswordInput.tsx
@@ -0,0 +1,71 @@
+'use client'
+
+import { cn } from '@lib/cn'
+import Image from 'next/image'
+import { forwardRef, useState } from 'react'
+
+import { InputProps } from '@/types/input.type'
+
+const PasswordInput = forwardRef(
+ function PasswordInput(props, ref) {
+ const {
+ labelName,
+ name,
+ type = 'password',
+ placeholder,
+ hasError,
+ errorMessage,
+ autoComplete,
+ ...rest
+ } = props
+
+ const [showPassword, setShowPassword] = useState(false)
+ const isPassword = type === 'password'
+ const inputType = isPassword && showPassword ? 'text' : 'password'
+
+ return (
+
+
+
+
+
+
+
+
+ {hasError && errorMessage && (
+
{errorMessage}
+ )}
+
+ )
+ },
+)
+
+PasswordInput.displayName = 'PasswordInput'
+
+export default PasswordInput
diff --git a/src/app/shared/components/common/header/UserDropdown.tsx b/src/app/shared/components/common/header/UserDropdown.tsx
index 85a54af..b0c2317 100644
--- a/src/app/shared/components/common/header/UserDropdown.tsx
+++ b/src/app/shared/components/common/header/UserDropdown.tsx
@@ -17,7 +17,11 @@ export default function UserDropdown() {
function handleLogout() {
logout()
- router.push('/login')
+ // ⚠️ 상태 변경(setIsPostLogout)이 반영되기 전에 페이지 이동하면 Redirect가 이상하게 동작함
+ // 따라서 router.push('/')는 이벤트 큐에 넣어 상태가 반영된 후 실행되도록 setTimeout 처리
+ setTimeout(() => {
+ router.push('/')
+ }, 0)
}
return (
diff --git a/src/app/shared/components/common/modal/CreateDashboardModal.tsx b/src/app/shared/components/common/modal/CreateDashboardModal.tsx
index 2b2616f..ae0a181 100644
--- a/src/app/shared/components/common/modal/CreateDashboardModal.tsx
+++ b/src/app/shared/components/common/modal/CreateDashboardModal.tsx
@@ -60,8 +60,8 @@ export default function CreateDashboardModal() {
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
onClick={handleBackdropClick}
>
-
-
+
+
새로운 대시보드
{/* 컬러 도트 */}
{/* 대시보드 제목 */}
- {dashboard.title}
+ {dashboard.title}
{/* 내가 만든 대시보드에 왕관 아이콘 */}
{dashboard.createdByMe && (
-
+
+