diff --git a/package.json b/package.json index c51d566..9209ddf 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,11 @@ "autoprefixer": "^10.4.19", "class-variance-authority": "^0.7.0", "next": "14.2.5", + "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", "react-hls-player": "^3.0.7", + "sonner": "^1.5.0", "tailwind-scrollbar-hide": "^1.1.7", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.2.0", diff --git a/public/background.png b/public/assets/background.png similarity index 100% rename from public/background.png rename to public/assets/background.png diff --git a/public/logo.svg b/public/assets/logo.svg similarity index 100% rename from public/logo.svg rename to public/assets/logo.svg diff --git a/public/snow.png b/public/assets/snow.png similarity index 100% rename from public/snow.png rename to public/assets/snow.png diff --git a/public/blind_01.png b/public/blinds/blind_01.png similarity index 100% rename from public/blind_01.png rename to public/blinds/blind_01.png diff --git a/public/blind_02-1.png b/public/blinds/blind_02-1.png similarity index 100% rename from public/blind_02-1.png rename to public/blinds/blind_02-1.png diff --git a/public/blind_02.png b/public/blinds/blind_02.png similarity index 100% rename from public/blind_02.png rename to public/blinds/blind_02.png diff --git a/public/blind_03-1.png b/public/blinds/blind_03-1.png similarity index 100% rename from public/blind_03-1.png rename to public/blinds/blind_03-1.png diff --git a/public/blind_03.png b/public/blinds/blind_03.png similarity index 100% rename from public/blind_03.png rename to public/blinds/blind_03.png diff --git a/public/blind_04-1.png b/public/blinds/blind_04-1.png similarity index 100% rename from public/blind_04-1.png rename to public/blinds/blind_04-1.png diff --git a/public/blind_04.png b/public/blinds/blind_04.png similarity index 100% rename from public/blind_04.png rename to public/blinds/blind_04.png diff --git a/public/android.png b/public/downloads/android.png similarity index 100% rename from public/android.png rename to public/downloads/android.png diff --git a/public/ios.png b/public/downloads/ios.png similarity index 100% rename from public/ios.png rename to public/downloads/ios.png diff --git a/public/shares/share_01.png b/public/shares/share_01.png new file mode 100644 index 0000000..a76059e Binary files /dev/null and b/public/shares/share_01.png differ diff --git a/public/shares/share_02.png b/public/shares/share_02.png new file mode 100644 index 0000000..ad4a627 Binary files /dev/null and b/public/shares/share_02.png differ diff --git a/public/shares/share_03.png b/public/shares/share_03.png new file mode 100644 index 0000000..c241383 Binary files /dev/null and b/public/shares/share_03.png differ diff --git a/public/shares/share_04.png b/public/shares/share_04.png new file mode 100644 index 0000000..eae9480 Binary files /dev/null and b/public/shares/share_04.png differ diff --git a/src/app/(web)/layout.tsx b/src/app/(web)/layout.tsx index 2466710..1d3387a 100644 --- a/src/app/(web)/layout.tsx +++ b/src/app/(web)/layout.tsx @@ -5,7 +5,9 @@ import { cn } from '@/shared/lib'; export default function Layout({ children }: { children: React.ReactNode }) { return (
-
+
{children}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e3eb500..d04a815 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import '@/app/globals.css'; import type { Metadata } from 'next'; +import { Toaster } from '@/shared/ui/toaster'; import Providers from './_providers'; export const metadata: Metadata = { @@ -13,6 +14,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {children} + ); diff --git a/src/widgets/discovery-detail/ui/app-download-dialog.tsx b/src/features/discovery-detail/ui/app-download-dialog.tsx similarity index 79% rename from src/widgets/discovery-detail/ui/app-download-dialog.tsx rename to src/features/discovery-detail/ui/app-download-dialog.tsx index fffc592..d27d064 100644 --- a/src/widgets/discovery-detail/ui/app-download-dialog.tsx +++ b/src/features/discovery-detail/ui/app-download-dialog.tsx @@ -1,8 +1,8 @@ import Image from 'next/image'; -import android from '@public/android.png'; -import ios from '@public/ios.png'; -import logo from '@public/logo.svg'; -import snow from '@public/snow.png'; +import logo from '@public/assets/logo.svg'; +import snow from '@public/assets/snow.png'; +import android from '@public/downloads/android.png'; +import ios from '@public/downloads/ios.png'; import { CloseIcon } from '@/shared/icons'; import { cn } from '@/shared/lib'; @@ -22,13 +22,11 @@ const AppDownloadDialog = ({ className, onClose }: AppDownloadDialogProps) => { snow logo -

+

{'지금 앱 다운로드하고\n블라인드된 정보를\n매일 확인해보세요'}

diff --git a/src/features/discovery-detail/ui/share-dialog.tsx b/src/features/discovery-detail/ui/share-dialog.tsx new file mode 100644 index 0000000..a61408a --- /dev/null +++ b/src/features/discovery-detail/ui/share-dialog.tsx @@ -0,0 +1,94 @@ +import Image from 'next/image'; +import { useCallback } from 'react'; +import { toast } from 'sonner'; +import logo from '@public/assets/logo.svg'; +import snow from '@public/assets/snow.png'; +import share1 from '@public/shares/share_01.png'; +import share2 from '@public/shares/share_02.png'; +import share3 from '@public/shares/share_03.png'; +import share4 from '@public/shares/share_04.png'; +import { CloseIcon } from '@/shared/icons'; +import { cn, getPostposition } from '@/shared/lib'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from '@/shared/ui/dialog'; + +interface ShareDialogProps { + trigger: React.ReactNode; + name?: string; +} + +const ShareDialog = ({ trigger, name }: ShareDialogProps) => { + const handleCopyUrl = useCallback(() => { + const currentUrl = window.location.href; + navigator.clipboard + .writeText(currentUrl) + .then(() => { + toast('클립보드에 복사되었습니다'); + }) + .catch((error) => { + console.error('클립보드 복사 실패!', error); + }); + }, []); + + return ( + + {trigger} + + + + + snow + + + + logo +
+

{getPostposition(name || '')}

+

공유해보세요!

+
+
+
+ +

카카오톡

+
+
+ +

인스타그램

+
+
+ +

스레드

+
+
+ +

링크 공유

+
+
+
+
+ ); +}; + +export default ShareDialog; diff --git a/src/widgets/discovery-detail/ui/vote-dialog.tsx b/src/features/discovery-detail/ui/vote-dialog.tsx similarity index 100% rename from src/widgets/discovery-detail/ui/vote-dialog.tsx rename to src/features/discovery-detail/ui/vote-dialog.tsx diff --git a/src/pages/discovery-detail/ui/discovery-detail-page.tsx b/src/pages/discovery-detail/ui/discovery-detail-page.tsx index f93e8be..5f62f29 100644 --- a/src/pages/discovery-detail/ui/discovery-detail-page.tsx +++ b/src/pages/discovery-detail/ui/discovery-detail-page.tsx @@ -3,12 +3,12 @@ import Image from 'next/image'; import { useState } from 'react'; import { useCallback } from 'react'; -import blind1 from '@public/blind_01.png'; +import blind1 from '@public/blinds/blind_01.png'; import { DiscoveryContentTabList } from '@/widgets/discovery-detail/model/constants'; -import AppDownloadDialog from '@/widgets/discovery-detail/ui/app-download-dialog'; import DiscoverySummary from '@/widgets/discovery-detail/ui/discovery-summary'; import { Header } from '@/widgets/header/ui'; import { WebcamMap, WebcamSlopList } from '@/widgets/webcam/ui'; +import AppDownloadDialog from '@/features/discovery-detail/ui/app-download-dialog'; import useMapPinch from '@/features/slop/hooks/useMapPinch'; import calculateWebcamPosition from '@/features/slop/lib/calculateWebcamPosition'; import { DiscoveryData } from '@/entities/discovery'; @@ -49,7 +49,7 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: number } }) => { return (
-
+
    {DiscoveryContentTabList.map((tab) => ( @@ -99,8 +99,8 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: number } }) => {

    주간 예보

    {showAppDownloadDialog && ( @@ -118,16 +118,16 @@ const DiscoveryDetailPage = ({ params }: { params: { resortId: number } }) => {

    인기 시간대

    슬로프 운행 현황

    {showAppDownloadDialog && ( diff --git a/src/shared/lib/getPostposition.ts b/src/shared/lib/getPostposition.ts new file mode 100644 index 0000000..ab4bccb --- /dev/null +++ b/src/shared/lib/getPostposition.ts @@ -0,0 +1,12 @@ +const getPostposition = (name: string) => { + const charCode = name.charCodeAt(name.length - 1); + + const consonantCode = (charCode - 44032) % 28; + + if (consonantCode === 0) { + return `${name}를`; + } + return `${name}을`; +}; + +export default getPostposition; diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index d4b67fe..77f411e 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -1,3 +1,4 @@ export { default as cn } from '@/shared/lib/cn'; export { default as getBoundedPositions } from '@/shared/lib/getBoundedPositions'; +export { default as getPostposition } from '@/shared/lib/getPostposition'; export type { ExcludeArray } from '@/shared/lib/util.d.ts'; diff --git a/src/shared/ui/toaster.tsx b/src/shared/ui/toaster.tsx new file mode 100644 index 0000000..584f52a --- /dev/null +++ b/src/shared/ui/toaster.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Toaster as Sonner } from 'sonner'; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/src/widgets/discovery-detail/ui/discovery-summary.tsx b/src/widgets/discovery-detail/ui/discovery-summary.tsx index 313b586..3ad492d 100644 --- a/src/widgets/discovery-detail/ui/discovery-summary.tsx +++ b/src/widgets/discovery-detail/ui/discovery-summary.tsx @@ -1,12 +1,12 @@ import { useCallback } from 'react'; import WeatherIcon from '@/features/discovery/ui/weather-icon'; +import VoteDialog from '@/features/discovery-detail/ui/vote-dialog'; import type { Discovery } from '@/entities/discovery'; import { BusIcon, LiftIcon, VoteIcon } from '@/shared/icons'; import { cn } from '@/shared/lib'; import Card from '@/shared/ui/card'; import { DiscoverySummaryActionList } from '../model/constants'; import DiscoverySummaryAction from './discovery-summary-action'; -import VoteDialog from './vote-dialog'; const DiscoverySummary = ({ name, slope, weather }: Discovery) => { const handleAction = useCallback( diff --git a/src/widgets/header/ui/header.tsx b/src/widgets/header/ui/header.tsx index 707e9d5..e34062b 100644 --- a/src/widgets/header/ui/header.tsx +++ b/src/widgets/header/ui/header.tsx @@ -1,13 +1,15 @@ import { useRouter } from 'next/navigation'; +import ShareDialog from '@/features/discovery-detail/ui/share-dialog'; import { ChevronLeftIcon, ShareIcon } from '@/shared/icons'; import { cn } from '@/shared/lib'; interface HeaderProps { + resortName?: string; hasBackButton?: boolean; hasShareButton?: boolean; } -const Header = ({ hasBackButton, hasShareButton }: HeaderProps) => { +const Header = ({ resortName, hasBackButton, hasShareButton }: HeaderProps) => { const router = useRouter(); return ( @@ -24,9 +26,14 @@ const Header = ({ hasBackButton, hasShareButton }: HeaderProps) => { WeSki {hasShareButton && ( - + + +
    + } + name={resortName} + /> )}
    ); diff --git a/yarn.lock b/yarn.lock index 999073c..0b35a83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2337,6 +2337,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +next-themes@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a" + integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w== + next@14.2.5: version "14.2.5" resolved "https://registry.yarnpkg.com/next/-/next-14.2.5.tgz#afe4022bb0b752962e2205836587a289270efbea" @@ -2878,6 +2883,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +sonner@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.5.0.tgz#af359f817063318415326b33aab54c5d17c747b7" + integrity sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA== + source-map-js@^1.0.2, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"