Skip to content

Commit

Permalink
feat: fsd 마이그레이션
Browse files Browse the repository at this point in the history
* refactor: fsd로 마이그레이션

* refactor: 전반적인 코드 분리

* chore: typescript lint 추가

* chore: fsd eslint 추가

* chore: tanstack query 세팅
  • Loading branch information
Yoon-Hae-Min authored Aug 7, 2024
1 parent f0dd33a commit 4df3a70
Show file tree
Hide file tree
Showing 36 changed files with 539 additions and 526 deletions.
12 changes: 7 additions & 5 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"extends": ["next", "prettier"],
"plugins": ["unused-imports", "simple-import-sort"],
"parser": "@typescript-eslint/parser",
"extends": ["next", "prettier", "plugin:@typescript-eslint/recommended", "@feature-sliced"],
"plugins": ["@typescript-eslint", "unused-imports"],

"rules": {
"unused-imports/no-unused-imports": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
"unused-imports/no-unused-imports": "error", // 사용하지 않는 import 제거
"@typescript-eslint/consistent-type-imports": "error", // type 가져오기 경우 import type { Type } from 'module' 사용
"@typescript-eslint/no-unused-expressions": "off", // 사용하지 않는 표현식 허용
"import/no-internal-modules": "off" // 내부 모듈 사용 허용 (fsd index 파일을 사용하기 위함)
}
}
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"dependencies": {
"@radix-ui/react-popover": "^1.1.1",
"@react-spring/web": "^9.7.4",
"@tanstack/react-query": "^5.51.21",
"@tanstack/react-query-devtools": "^5.51.21",
"@use-gesture/react": "^10.3.1",
"autoprefixer": "^10.4.19",
"class-variance-authority": "^0.7.0",
Expand All @@ -23,14 +25,18 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@feature-sliced/eslint-config": "^0.1.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"clsx": "^2.1.1",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-boundaries": "^4.2.2",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-tailwindcss": "^3.17.4",
"eslint-plugin-unused-imports": "^4.0.0",
"postcss": "^8",
Expand Down
19 changes: 19 additions & 0 deletions src/app/_providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { PropsWithChildren } from 'react';
import React from 'react';

const Providers = ({ children }: PropsWithChildren) => {
const queryClient = new QueryClient();

return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};

export default Providers;
7 changes: 5 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import './globals.css';
import '@/app/globals.css';

import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import Providers from './_providers';

const inter = Inter({ subsets: ['latin'] });

Expand All @@ -13,7 +14,9 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}
22 changes: 1 addition & 21 deletions src/app/mobile/congestion/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1 @@
import Image from 'next/image';

import { cn } from '@/lib/utils';

const Page = () => {
return (
<div className={cn('h-[calc(100%-154px)] overflow-scroll scrollbar-hide')}>
{Array.from({ length: 4 }).map((_, index) => (
<Image
key={index}
src={`/congestion/mock${(index % 3) + 1}.png`}
alt={`${index}`}
width={376}
height={461}
/>
))}
</div>
);
};

export default Page;
export { default } from '@/pages/congestion/ui/congestion-page';
5 changes: 2 additions & 3 deletions src/app/mobile/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import NavBar from '@/components/ui/nav-bar';
import StatusBar from '@/components/ui/status-bar';
import { cn } from '@/lib/utils';
import { NavBar, StatusBar } from '@/widgets/header/ui';
import { cn } from '@/shared/lib';

export default function Layout({ children }: { children: React.ReactNode }) {
return (
Expand Down
162 changes: 1 addition & 161 deletions src/app/mobile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,161 +1 @@
'use client';

import { animated, useSpring } from '@react-spring/web';
import { createUseGesture, dragAction, pinchAction } from '@use-gesture/react';
import Image from 'next/image';
import React from 'react';

import Summary from '@/components/ui/main/summary';
import { cn } from '@/lib/utils';

import { ResortList, Spot } from './data';

const useGesture = createUseGesture([pinchAction, dragAction]);

const Page = () => {
const [selectedTab, setSelectedTab] = React.useState(ResortList[0]);
const [selectedSpot, setSelectedSpot] = React.useState<Spot | null>(null);
const [style, api] = useSpring(() => ({ scale: 1, x: 0, y: 0 }));
const ref = React.useRef<HTMLDivElement>(null);

useGesture(
{
onPinch: ({ origin: [ox, oy], first, movement: [ms], offset: [s], memo }) => {
if (first) {
const { width, height, x, y } = ref.current!.getBoundingClientRect();
const tx = ox - (x + width / 2);
const ty = oy - (y + height / 2);
memo = [style.x.get(), style.y.get(), tx, ty];
}

const x = memo[0] - (ms - 1) * memo[2];
const y = memo[1] - (ms - 1) * memo[3];
api.start({ scale: s, x, y });
return memo;
},
onPinchEnd: () => {
if (style.scale.get() < 1) {
api.start({ scale: 1, x: 0, y: 0 });
}
},
onDrag: ({ pinching, cancel, offset: [x, y] }) => {
if (pinching) return cancel();
api.start({ x, y });
},
onDragEnd: () => {
const [boundedX, boundedY] = getBoundedPositions(
style.x.get(),
style.y.get(),
style.scale.get()
);
api.start({ x: boundedX, y: boundedY });
},
},
{
target: ref,
drag: { from: () => [style.x.get(), style.y.get()] },
pinch: { scaleBounds: { min: 1, max: 6 }, rubberband: true },
}
);

const getBoundedPositions = (x: number, y: number, scale: number): [number, number] => {
const CONTAINER = { width: 376, height: 200 };
const IMAGE = { width: 376, height: 357 };
const OFFSET_Y = IMAGE.height - CONTAINER.height;

const scaledSize = {
width: IMAGE.width * scale,
height: IMAGE.height * scale,
};

const bounds = {
x: {
min: (CONTAINER.width - scaledSize.width) / 2,
max: -(CONTAINER.width - scaledSize.width) / 2,
},
y: {
min: -((scaledSize.height - CONTAINER.height + OFFSET_Y) / 2),
max: Math.max((scaledSize.height - CONTAINER.height - OFFSET_Y) / 2, 0),
},
};

const boundedPosition = {
x: Math.max(bounds.x.min, Math.min(bounds.x.max, x)),
y: Math.max(bounds.y.min, Math.min(bounds.y.max, y)),
};

return [boundedPosition.x, boundedPosition.y];
};

return (
<div className={cn('size-full')}>
<div className={cn('mb-1 flex w-full overflow-scroll scrollbar-hide')}>
{ResortList.map((tab) => (
<div
key={tab.name}
className={cn(
'flex shrink-0 cursor-pointer items-center justify-center border-b-4 p-3 pb-2 font-bold',
selectedTab.name === tab.name ? 'border-black' : 'border-white opacity-20'
)}
onClick={() => {
setSelectedTab(tab);
setSelectedSpot(null);
}}
>
{tab.name}
</div>
))}
</div>
<Summary {...ResortList.find((tab) => tab.name === selectedTab.name)!} />
<div className={cn('relative h-[200px] w-full overflow-hidden')}>
<animated.div
ref={ref}
style={{
touchAction: 'none',
display: 'inline-block',
...style,
}}
>
<Image
className={cn('object-cover')}
width={376}
height={357}
src={`/map/${selectedTab.map}`}
alt={`${selectedTab.name}`}
/>
<div
className={cn('absolute left-0 top-3 h-2 w-3 bg-black opacity-20')}
onClick={() => console.log('hi')}
/>
</animated.div>
{selectedSpot && (
<div className={cn('absolute left-0 top-0 size-full')}>
<video src={`/video/${selectedTab.tag}/${selectedSpot.tag}.mov`} muted autoPlay loop />
</div>
)}
</div>
<div className={cn('flex h-[296px] flex-col gap-0.5 overflow-scroll scrollbar-hide')}>
{selectedTab.spots?.map((spot) => (
<div
key={spot.name}
className={cn(
'flex h-20 w-full items-center justify-between bg-gray-100 p-6 font-bold',
spot.isAvailable ? 'cursor-pointer' : 'cursor-not-allowed opacity-20'
)}
onClick={() => {
spot.isAvailable && setSelectedSpot(spot);
}}
>
<p className={cn('text-lg')}>{spot.name}</p>
<div className={cn('flex items-center gap-3')}>
<p className={cn('text-sm')}>{spot.level}</p>
<p className={cn('text-lg')}>헤라1,2</p>
</div>
</div>
))}
</div>
</div>
);
};

export default Page;
export { default } from '@/pages/webcam/ui/web-cam-map-page';
73 changes: 1 addition & 72 deletions src/app/mobile/weather/page.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1 @@
import CloudIcon from '@/components/icons/cloud';
import SunIcon from '@/components/icons/sun';
import { cn } from '@/lib/utils';

const WEATHER_DATA = [
{
time: '오전 8시',
icon: <SunIcon />,
temperature: '22°',
humidity: '20%',
},
{
time: '오전 11시',
icon: <CloudIcon />,
temperature: '24°',
humidity: '30%',
},
{
time: '오후 2시',
icon: <SunIcon />,
temperature: '26°',
humidity: '40%',
},
{
time: '오후 5시',
icon: <CloudIcon />,
temperature: '25°',
humidity: '30%',
},
];

const Page = () => {
return (
<div className={cn('flex h-[650px] flex-col items-center gap-3 overflow-auto')}>
{Array.from({ length: 3 }).map((_, index) => (
<WeatherCard key={index} />
))}
</div>
);
};

export default Page;

const WeatherCard = () => {
return (
<div className={cn('h-[461px] w-[335px] rounded-lg bg-gray-100 p-6')}>
<div className={cn('mb-[130px] flex items-center justify-between')}>
<h4 className={cn('font-bold')}>하이원 스키장</h4>
<div className={cn('rounded-lg bg-white px-3 py-1 text-sm font-bold')}>설질 GOOD 56%</div>
</div>

<p className={cn('text-4xl font-semibold')}>25°</p>
<p className={cn('mb-4 text-sm text-gray-600')}>흐리고 비</p>

<p className={cn('text-lg font-semibold')}>흐리고 비오는 날씨가 계속 돼요</p>
<p className={cn('mb-4 text-sm')}>최고:28° 최저 24° 미세먼지 좋음</p>

<hr className={cn('mb-4')} />

<ul className={cn('flex gap-5')}>
{WEATHER_DATA.map((data) => (
<li key={data.time} className={cn('flex flex-col items-center')}>
<div className={cn('mb-1 text-sm font-medium')}>{data.time}</div>
{data.icon}
<p className={cn('mt-1')}>{data.temperature}</p>
<p className={cn('text-xs')}>{data.humidity}</p>
</li>
))}
</ul>
</div>
);
};
export { default } from '@/pages/weather/ui/weather-page';
11 changes: 0 additions & 11 deletions src/app/page.tsx

This file was deleted.

26 changes: 0 additions & 26 deletions src/components/camera-button.tsx

This file was deleted.

Loading

0 comments on commit 4df3a70

Please sign in to comment.