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 (
-
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) => {
-
+
{'지금 앱 다운로드하고\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 (
+
+ );
+};
+
+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"