diff --git a/favicon/favicon.ico b/favicon/favicon.ico new file mode 100644 index 0000000..b9bcc5b Binary files /dev/null and b/favicon/favicon.ico differ diff --git a/index.html b/index.html index f52af8c..11d1d16 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + 농기구온 diff --git a/package.json b/package.json index 89408e2..d53ab45 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "@tailwindcss/vite": "^4.1.11", "@tanstack/react-query": "^5.81.5", "axios": "^1.10.0", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "daisyui": "^5.0.43", + "framer-motion": "^12.19.2", "prettier": "^3.6.2", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5dd208..dc13b00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,12 +29,18 @@ importers: axios: specifier: ^1.10.0 version: 1.10.0 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 clsx: specifier: ^2.1.1 version: 2.1.1 daisyui: specifier: ^5.0.43 version: 5.0.43 + framer-motion: + specifier: ^12.19.2 + version: 12.19.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -808,6 +814,9 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1022,6 +1031,20 @@ packages: resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} engines: {node: '>= 6'} + framer-motion@12.19.2: + resolution: {integrity: sha512-0cWMLkYr+i0emeXC4hkLF+5aYpzo32nRdQ0D/5DI460B3O7biQ3l2BpDzIGsAHYuZ0fpBP0DC8XBkVf6RPAlZw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1258,6 +1281,12 @@ packages: modern-ahocorasick@1.1.0: resolution: {integrity: sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==} + motion-dom@12.19.0: + resolution: {integrity: sha512-m96uqq8VbwxFLU0mtmlsIVe8NGGSdpBvBSHbnnOJQxniPaabvVdGgxSamhuDwBsRhwX7xPxdICgVJlOpzn/5bw==} + + motion-utils@12.19.0: + resolution: {integrity: sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1466,6 +1495,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2153,6 +2185,10 @@ snapshots: chownr@3.0.0: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + clsx@2.1.1: {} color-convert@2.0.1: @@ -2382,6 +2418,15 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + framer-motion@12.19.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + motion-dom: 12.19.0 + motion-utils: 12.19.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + fsevents@2.3.3: optional: true @@ -2571,6 +2616,12 @@ snapshots: modern-ahocorasick@1.1.0: {} + motion-dom@12.19.0: + dependencies: + motion-utils: 12.19.0 + + motion-utils@12.19.0: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -2717,6 +2768,8 @@ snapshots: dependencies: typescript: 5.8.3 + tslib@2.8.1: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/src/app/App.tsx b/src/app/App.tsx index 8e3a4f2..502d2bb 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,9 +1,9 @@ -import { Stack } from "./stackflow"; +import { Stack } from './stackflow'; export default function App() { return (
-
+
diff --git a/src/app/stackflow/Stack.tsx b/src/app/stackflow/Stack.tsx index efc1388..4e021a2 100644 --- a/src/app/stackflow/Stack.tsx +++ b/src/app/stackflow/Stack.tsx @@ -1,5 +1,11 @@ +import { CompleteScreen } from '@/screen/complete'; +import { FormScreen } from '@/screen/form/ui'; import { HomeScreen } from '@/screen/home/ui'; import { JoinScreen } from '@/screen/join/ui'; +import { PhotoLoadingScreen } from '@/screen/photo-loading/ui'; +import { PhotoResultScreen } from '@/screen/photo-result/ui'; +import { PhotoUploadScreen } from '@/screen/photo-upload/ui'; +import { fetchSessionData } from '@/shared/utils'; import { basicUIPlugin } from '@stackflow/plugin-basic-ui'; import { basicRendererPlugin } from '@stackflow/plugin-renderer-basic'; import { stackflow } from '@stackflow/react'; @@ -9,6 +15,11 @@ export const { Stack, useFlow } = stackflow({ activities: { JoinScreen, HomeScreen, + PhotoUploadScreen, + FormScreen, + CompleteScreen, + PhotoLoadingScreen, + PhotoResultScreen, }, plugins: [ basicRendererPlugin(), @@ -16,5 +27,8 @@ export const { Stack, useFlow } = stackflow({ theme: 'cupertino', }), ], - initialActivity: () => 'HomeScreen', + initialActivity: () => { + if (fetchSessionData('userInfo')) return 'HomeScreen'; + return 'JoinScreen'; + }, }); diff --git a/src/assets/icons/icon-bell.svg b/src/assets/icons/icon-bell.svg new file mode 100644 index 0000000..8a8b46b --- /dev/null +++ b/src/assets/icons/icon-bell.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/icon-checked.svg b/src/assets/icons/icon-checked.svg new file mode 100644 index 0000000..9c72c06 --- /dev/null +++ b/src/assets/icons/icon-checked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/icon-help.svg b/src/assets/icons/icon-help.svg new file mode 100644 index 0000000..eb8b2b3 --- /dev/null +++ b/src/assets/icons/icon-help.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/icon-search.svg b/src/assets/icons/icon-search.svg new file mode 100644 index 0000000..9630155 --- /dev/null +++ b/src/assets/icons/icon-search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/icon-unchecked.svg b/src/assets/icons/icon-unchecked.svg new file mode 100644 index 0000000..052f782 --- /dev/null +++ b/src/assets/icons/icon-unchecked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index a498b9c..bd52db4 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -10,6 +10,11 @@ import ReservationIcon from './icon-reservation.svg'; import HomeSelectedIcon from './icon-home-selected.svg'; import UserSelectedIcon from './icon-user-selected.svg'; import ReservationSelectedIcon from './icon-reservation-selected.svg'; +import BellIcon from './icon-bell.svg'; +import HelpIcon from './icon-help.svg'; +import SearchIcon from './icon-search.svg'; +import CheckIcon from './icon-checked.svg'; +import UnCheckedIcon from './icon-unchecked.svg'; export { Logo, @@ -24,4 +29,9 @@ export { HomeSelectedIcon, UserSelectedIcon, ReservationSelectedIcon, + BellIcon, + HelpIcon, + SearchIcon, + CheckIcon, + UnCheckedIcon, }; diff --git a/src/assets/images/background-loading.png b/src/assets/images/background-loading.png new file mode 100644 index 0000000..68168e5 Binary files /dev/null and b/src/assets/images/background-loading.png differ diff --git a/src/assets/images/call-complete.png b/src/assets/images/call-complete.png new file mode 100644 index 0000000..097b546 Binary files /dev/null and b/src/assets/images/call-complete.png differ diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts index 501e8ea..cbe0535 100644 --- a/src/assets/images/index.ts +++ b/src/assets/images/index.ts @@ -1,3 +1,17 @@ -import BackgroundImage from "./background.png"; +import BackgroundImage from './background.png'; +import LoadingBackground from './background-loading.png'; +import PhoneCompleteImage from './call-complete.png'; +import Loader1 from './loader-1.png'; +import Loader2 from './loader-2.png'; +import Loader3 from './loader-3.png'; +import Loader4 from './loader-4.png'; -export { BackgroundImage }; +export { + BackgroundImage, + LoadingBackground, + Loader1, + Loader2, + Loader3, + Loader4, + PhoneCompleteImage, +}; diff --git a/src/assets/images/loader-1.png b/src/assets/images/loader-1.png new file mode 100644 index 0000000..771f7fb Binary files /dev/null and b/src/assets/images/loader-1.png differ diff --git a/src/assets/images/loader-2.png b/src/assets/images/loader-2.png new file mode 100644 index 0000000..0b08b1b Binary files /dev/null and b/src/assets/images/loader-2.png differ diff --git a/src/assets/images/loader-3.png b/src/assets/images/loader-3.png new file mode 100644 index 0000000..1df1da9 Binary files /dev/null and b/src/assets/images/loader-3.png differ diff --git a/src/assets/images/loader-4.png b/src/assets/images/loader-4.png new file mode 100644 index 0000000..c6ab41e Binary files /dev/null and b/src/assets/images/loader-4.png differ diff --git a/src/screen/complete/CompleteScreen.tsx b/src/screen/complete/CompleteScreen.tsx new file mode 100644 index 0000000..eed4188 --- /dev/null +++ b/src/screen/complete/CompleteScreen.tsx @@ -0,0 +1,30 @@ +import { AppScreen } from '@stackflow/plugin-basic-ui'; +import { Dock } from '@/shared/ui'; +import { PhoneCompleteImage } from '@/assets/images'; +import Button from '@/shared/ui/Button'; +import { useFlow } from '@/app/stackflow'; + +export default function PhoneCompleteScreen() { + const { pop } = useFlow(); + + return ( + <> + +
+
+ +

신청이 완료되었습니다

+

+ AI가 계약서를 자동으로 작성하였으며
+ 예약 현황에서 언제든지 확인하실 수 있습니다 +

+ +
+
+
+ + + ); +} diff --git a/src/screen/complete/index.ts b/src/screen/complete/index.ts new file mode 100644 index 0000000..687ccfc --- /dev/null +++ b/src/screen/complete/index.ts @@ -0,0 +1 @@ +export { default as CompleteScreen } from './CompleteScreen'; diff --git a/src/screen/form/ui/FormScreen.tsx b/src/screen/form/ui/FormScreen.tsx new file mode 100644 index 0000000..26c9dab --- /dev/null +++ b/src/screen/form/ui/FormScreen.tsx @@ -0,0 +1,17 @@ +import { NormalAppBar } from '@/shared/ui'; +import { FormContainer } from '@/widgets/form/ui'; +import { AppScreen } from '@stackflow/plugin-basic-ui'; + +export default function PhotoUploadScreen() { + return ( + + + + ); +} diff --git a/src/screen/form/ui/index.ts b/src/screen/form/ui/index.ts new file mode 100644 index 0000000..46e2958 --- /dev/null +++ b/src/screen/form/ui/index.ts @@ -0,0 +1 @@ +export { default as FormScreen } from './FormScreen'; diff --git a/src/screen/home/ui/HomeScreen.tsx b/src/screen/home/ui/HomeScreen.tsx index ad9c054..2ffb39f 100644 --- a/src/screen/home/ui/HomeScreen.tsx +++ b/src/screen/home/ui/HomeScreen.tsx @@ -1,6 +1,6 @@ import { AppScreen } from '@stackflow/plugin-basic-ui'; import { BackgroundImage } from '@/assets/images'; -import { BasicAppBar, Dock } from '@/shared/ui'; +import { HomeAppBar, Dock } from '@/shared/ui'; import { HomeContainer } from '@/widgets/home/ui'; export default function HomeScreen() { @@ -9,7 +9,7 @@ export default function HomeScreen() { diff --git a/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx b/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx new file mode 100644 index 0000000..e801900 --- /dev/null +++ b/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx @@ -0,0 +1,34 @@ +import { LoadingBackground } from '@/assets/images'; +import { Loader } from '@/shared/ui'; +import { useSubmitPhoto } from '@/widgets/photo-upload/api'; +import { AppScreen } from '@stackflow/plugin-basic-ui'; +import type { ActivityComponentType } from '@stackflow/react'; +import { useEffect } from 'react'; + +const PhotoLoadingScreen: ActivityComponentType<{ data: FormData }> = ({ + params, +}: { + params: { data: FormData }; +}) => { + const { mutate: submitPhoto } = useSubmitPhoto(); + + useEffect(() => { + submitPhoto(params.data); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + +
+
+ +

+ AI가 사진을 분석하고 있어요... +

+
+
+
+ ); +}; + +export default PhotoLoadingScreen; diff --git a/src/screen/photo-loading/ui/index.ts b/src/screen/photo-loading/ui/index.ts new file mode 100644 index 0000000..0e4f12e --- /dev/null +++ b/src/screen/photo-loading/ui/index.ts @@ -0,0 +1 @@ +export { default as PhotoLoadingScreen } from './PhotoLoadingScreen'; diff --git a/src/screen/photo-result/ui/PhotoResultScreen.tsx b/src/screen/photo-result/ui/PhotoResultScreen.tsx new file mode 100644 index 0000000..e855f82 --- /dev/null +++ b/src/screen/photo-result/ui/PhotoResultScreen.tsx @@ -0,0 +1,19 @@ +import type { Tool } from '@/shared/types'; +import { NormalAppBar } from '@/shared/ui'; +import { AppScreen } from '@stackflow/plugin-basic-ui'; +import type { ActivityComponentType } from '@stackflow/react'; +import { PhotoResultContainer } from '@/widgets/photo-result/ui'; + +const PhotoLoadingScreen: ActivityComponentType<{ result: Tool[] }> = ({ + params, +}: { + params: { result: Tool[] }; +}) => { + return ( + + + + ); +}; + +export default PhotoLoadingScreen; diff --git a/src/screen/photo-result/ui/index.ts b/src/screen/photo-result/ui/index.ts new file mode 100644 index 0000000..11ce858 --- /dev/null +++ b/src/screen/photo-result/ui/index.ts @@ -0,0 +1 @@ +export { default as PhotoResultScreen } from './PhotoResultScreen'; diff --git a/src/screen/photo-upload/ui/PhotoUploadScreen.tsx b/src/screen/photo-upload/ui/PhotoUploadScreen.tsx new file mode 100644 index 0000000..30d8cae --- /dev/null +++ b/src/screen/photo-upload/ui/PhotoUploadScreen.tsx @@ -0,0 +1,11 @@ +import { NormalAppBar } from '@/shared/ui'; +import { PhotoUploadContainer } from '@/widgets/photo-upload/ui'; +import { AppScreen } from '@stackflow/plugin-basic-ui'; + +export default function PhotoUploadScreen() { + return ( + + + + ); +} diff --git a/src/screen/photo-upload/ui/index.ts b/src/screen/photo-upload/ui/index.ts new file mode 100644 index 0000000..f16c77c --- /dev/null +++ b/src/screen/photo-upload/ui/index.ts @@ -0,0 +1 @@ +export { default as PhotoUploadScreen } from './PhotoUploadScreen'; diff --git a/src/shared/api/axios.ts b/src/shared/api/axios.ts index c6d7c96..d1827e1 100644 --- a/src/shared/api/axios.ts +++ b/src/shared/api/axios.ts @@ -13,7 +13,7 @@ interface GetRequestParams { } const instance = axios.create({ - baseURL: 'https://usfarmtools.com/api', + baseURL: 'https://usfarmtools.com', }); export async function get( diff --git a/src/shared/api/request.ts b/src/shared/api/request.ts index 0d9efcd..3440d87 100644 --- a/src/shared/api/request.ts +++ b/src/shared/api/request.ts @@ -1,3 +1,4 @@ export const REQUEST = { - JOIN: '/ocr/idcard', + JOIN: '/api/ocr/idcard', + PHOTO_UPLOAD: '/chat/image', }; diff --git a/src/shared/constants/path.ts b/src/shared/constants/path.ts index f3528f4..bae17f3 100644 --- a/src/shared/constants/path.ts +++ b/src/shared/constants/path.ts @@ -1,4 +1,9 @@ export const PATH = { HOME: 'HomeScreen', JOIN: 'JoinScreen', + COMPLETE: 'CompleteScreen', + PHOTO_UPLOAD: 'PhotoUploadScreen', + PHOTO_LOADING: 'PhotoLoadingScreen', + PHOTO_RESULT: 'PhotoResultScreen', + FORM: 'FormScreen', } as const; diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts new file mode 100644 index 0000000..e9dd277 --- /dev/null +++ b/src/shared/hooks/index.ts @@ -0,0 +1 @@ +export { default as useImageUpload } from './useImageUpload'; diff --git a/src/widgets/join/model/hooks/useIdImage.ts b/src/shared/hooks/useImageUpload.ts similarity index 58% rename from src/widgets/join/model/hooks/useIdImage.ts rename to src/shared/hooks/useImageUpload.ts index 6f4510b..c66c84c 100644 --- a/src/widgets/join/model/hooks/useIdImage.ts +++ b/src/shared/hooks/useImageUpload.ts @@ -1,9 +1,9 @@ import { useState, type ChangeEvent } from 'react'; import { useSubmitIdCard } from '@/widgets/join/api'; -export default function useIdImage() { +export default function useImageUpload(isPhotoUpload?: boolean) { const [image, setImage] = useState(null); - const { mutate: submitIdCard } = useSubmitIdCard(setImage); + const { mutate: submitIdCard, isPending } = useSubmitIdCard(setImage); const handleImageInputChange = (e: ChangeEvent) => { const file = e.target.files?.[0]; @@ -13,12 +13,14 @@ export default function useIdImage() { setImage(reader.result as string); }; reader.readAsDataURL(file); - const formData = new FormData(); - formData.append('image', file); - submitIdCard(formData); + if (!isPhotoUpload) { + const formData = new FormData(); + formData.append('image', file); + submitIdCard(formData); + } } else { setImage(''); } }; - return { image, handleImageInputChange, setImage }; + return { image, handleImageInputChange, setImage, isPending }; } diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 05a1f8b..b7a5106 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,2 +1,5 @@ export * from './dock'; export * from './path'; +export * from './reservation'; +export * from './user'; +export * from './tools'; diff --git a/src/shared/types/reservation.ts b/src/shared/types/reservation.ts new file mode 100644 index 0000000..399091a --- /dev/null +++ b/src/shared/types/reservation.ts @@ -0,0 +1,7 @@ +export type Reservation = { + tool: string; + startDate: string; + endDate: string; + location: string; + userName: string; +}; diff --git a/src/shared/types/tools.ts b/src/shared/types/tools.ts new file mode 100644 index 0000000..888aec4 --- /dev/null +++ b/src/shared/types/tools.ts @@ -0,0 +1,8 @@ +export type Tool = { + id: number; + toolType: string; + quantity: number; + description: string; + image: string; + price: number; +}; diff --git a/src/shared/types/user.ts b/src/shared/types/user.ts new file mode 100644 index 0000000..3fe2e62 --- /dev/null +++ b/src/shared/types/user.ts @@ -0,0 +1 @@ +export type User = { jumin: string; name: string }; diff --git a/src/shared/ui/AppBar.tsx b/src/shared/ui/AppBar.tsx index e2340b9..27c8510 100644 --- a/src/shared/ui/AppBar.tsx +++ b/src/shared/ui/AppBar.tsx @@ -1,9 +1,39 @@ +import { BellIcon, HelpIcon } from '@/assets/icons'; import { BackgroundImage } from '@/assets/images'; -const baseStyle = { height: '58px', border: false }; +const baseStyle = { height: '62px', border: false }; -export const BasicAppBar = { +export const HomeAppBar = ( + handleBellClick?: () => void, + handleHelpClick?: () => void, +) => ({ ...baseStyle, backgroundImage: `url(${BackgroundImage})`, - renderRight: () => <>, -}; + renderRight: () => ( +
+ + +
+ ), +}); + +export const NormalAppBar = (title: string, bgImage?: string) => ({ + backgroundImage: `url(${bgImage})`, + renderLeft: () => { + if (title) { + return {title}; + } + return null; + }, + ...baseStyle, +}); diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx new file mode 100644 index 0000000..3cfe13c --- /dev/null +++ b/src/shared/ui/Button.tsx @@ -0,0 +1,47 @@ +import type { ButtonHTMLAttributes } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/shared/utils'; + +interface ButtonsProps + extends ButtonHTMLAttributes, + VariantProps { + children?: React.ReactNode; +} + +const ButtonVariants = cva( + 'flex cursor-pointer items-center justify-center rounded-full py-3 font-semibold focus:outline-none', + { + variants: { + intent: { + home: 'border-m text-m hover:bg-m-hover active:bg-m-hover border-[1px]', + primary: 'bg-m text-white', + disabled: 'bg-[#D8D8D8] text-white', + }, + size: { + md: 'w-full py-3', + lg: 'w-full py-5 text-xl', + }, + }, + defaultVariants: { + intent: 'primary', + size: 'md', + }, + }, +); + +export default function Button({ + intent, + size, + children, + className, + ...props +}: ButtonsProps) { + return ( + + ); +} diff --git a/src/shared/ui/Dock.tsx b/src/shared/ui/Dock.tsx index 75af86b..581f2d2 100644 --- a/src/shared/ui/Dock.tsx +++ b/src/shared/ui/Dock.tsx @@ -2,7 +2,7 @@ import { useStack } from '@stackflow/react'; import { useFlow } from '@/app/stackflow'; -import { DOCK, DOCK_ITEMS } from '@/shared/constants'; +import { DOCK, DOCK_ITEMS, PATH } from '@/shared/constants'; import type { DockItem, PathItem } from '../types'; import { cn } from '../utils'; @@ -23,9 +23,11 @@ export default function Dock(isLoading: DockProps) { .map(i => i.name) .pop() as PathItem; + const render = current === PATH.HOME; + return ( <> - {DOCK_ITEMS.length > 0 && ( + {render && (
+ {images.map((src, i) => ( + + {`loader-${i}`} + + ))} + + ); +} diff --git a/src/shared/ui/ToolButton.tsx b/src/shared/ui/ToolButton.tsx new file mode 100644 index 0000000..6bef11e --- /dev/null +++ b/src/shared/ui/ToolButton.tsx @@ -0,0 +1,40 @@ +import { CheckIcon, UnCheckedIcon } from '@/assets/icons'; +import type { ButtonHTMLAttributes } from 'react'; +import { cn } from '../utils'; + +interface ToolButtonProps extends ButtonHTMLAttributes { + image: string; + toolType: string; + description: string; + selected: boolean; +} + +export default function ToolButton({ + image, + toolType, + description, + selected, + ...rest +}: ToolButtonProps) { + return ( + + ); +} diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index d026ed2..383cf2b 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,2 +1,5 @@ export * from './AppBar'; export { default as Dock } from './Dock'; +export { default as Button } from './Button'; +export { default as Loader } from './Loader'; +export { default as ToolButton } from './ToolButton'; diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index 57f9f48..2573643 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -1 +1,2 @@ export * from './string'; +export * from './session'; diff --git a/src/shared/utils/session.ts b/src/shared/utils/session.ts new file mode 100644 index 0000000..94f1bb8 --- /dev/null +++ b/src/shared/utils/session.ts @@ -0,0 +1,16 @@ +export const fetchSessionData = (key: string): T | null => { + const stored = sessionStorage.getItem(key); + if (stored) { + const parsed = JSON.parse(stored); + return parsed; + } + return null; +}; + +export const setSessionData = (key: string, data: T) => { + sessionStorage.setItem(key, JSON.stringify(data)); +}; + +export const removeSessionData = (key: string) => { + sessionStorage.removeItem(key); +}; diff --git a/src/shared/utils/string.ts b/src/shared/utils/string.ts index 33e3cfe..3b1b089 100644 --- a/src/shared/utils/string.ts +++ b/src/shared/utils/string.ts @@ -8,3 +8,20 @@ export function cn(...inputs: ClassValue[]) { export function getPath(base: string, path: string) { return `${base}/${path}`; } + +export const base64ToFile = ( + base64String: string, + filename: string = 'image.jpg', +): File => { + const arr = base64String.split(','); + const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg'; + const bstr = atob(arr[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + return new File([u8arr], filename, { type: mime }); +}; diff --git a/src/widgets/form/ui/FormContainer.tsx b/src/widgets/form/ui/FormContainer.tsx new file mode 100644 index 0000000..6c66538 --- /dev/null +++ b/src/widgets/form/ui/FormContainer.tsx @@ -0,0 +1,51 @@ +import { Button } from '@/shared/ui'; +import FormInput from './FormInput'; + +export default function FormContainer() { + const formItems = [ + { label: '농기계', type: 'search', onClick: () => {} }, + { label: '대여소', type: 'text' }, + { label: '대여일자', type: 'date' }, + { label: '반납일자', type: 'date' }, + ]; + + return ( +
+ {formItems.map(item => ( + + ))} + +
+
+

계약서는 AI가 자동으로 작성해줍니다

+

· 예약 현황에서 확인 가능

+

임대영업소 운영 시간: 평일 09:00 ~ 18:00

+
+ +
+
+
+ ); +} + +const FormItem = ({ + label, + type, +}: { + label: string; + type: string; + onClick?: () => void; +}) => ( +
+ + {label} + + {type === 'search' ? : } +
+); diff --git a/src/widgets/form/ui/FormInput.tsx b/src/widgets/form/ui/FormInput.tsx new file mode 100644 index 0000000..b27391b --- /dev/null +++ b/src/widgets/form/ui/FormInput.tsx @@ -0,0 +1,37 @@ +import { SearchIcon } from '@/assets/icons'; +import type { InputHTMLAttributes } from 'react'; + +interface FormInputProps extends InputHTMLAttributes { + isSearch?: boolean; + onClick?: () => void; +} + +export default function FormInput({ + isSearch, + onClick = () => {}, + ...rest +}: FormInputProps) { + const renderInput = () => { + if (isSearch) { + return ( + + ); + } else { + return ( + + ); + } + }; + return renderInput(); +} diff --git a/src/widgets/form/ui/index.ts b/src/widgets/form/ui/index.ts new file mode 100644 index 0000000..b7a14c5 --- /dev/null +++ b/src/widgets/form/ui/index.ts @@ -0,0 +1 @@ +export { default as FormContainer } from './FormContainer'; diff --git a/src/widgets/home/ui/HomeButton.tsx b/src/widgets/home/ui/HomeButton.tsx index e7bc4f7..5fa348c 100644 --- a/src/widgets/home/ui/HomeButton.tsx +++ b/src/widgets/home/ui/HomeButton.tsx @@ -1,3 +1,5 @@ +import { Button } from '@/shared/ui'; + export const HomeButton = ({ icon, label, @@ -17,7 +19,7 @@ export const HomeButton = ({

{label}

{description.split('
').map((line, index) => ( -

+

{line}

))} @@ -25,18 +27,16 @@ export const HomeButton = ({ {buttonLabel} ) : ( - + )}
); diff --git a/src/widgets/home/ui/HomeContainer.tsx b/src/widgets/home/ui/HomeContainer.tsx index 8875352..0cc2fb4 100644 --- a/src/widgets/home/ui/HomeContainer.tsx +++ b/src/widgets/home/ui/HomeContainer.tsx @@ -1,3 +1,4 @@ +import { useFlow } from '@/app/stackflow'; import { HomeButton } from './HomeButton'; import { CameraSolidIcon, @@ -5,8 +6,11 @@ import { PhoneIcon, TractorBlackIcon, } from '@/assets/icons'; +import { PATH } from '@/shared/constants'; export default function HomeContainer() { + const { push } = useFlow(); + return (
@@ -16,22 +20,31 @@ export default function HomeContainer() {
{ + push(PATH.COMPLETE, {}); + }} /> { + push(PATH.PHOTO_UPLOAD, {}); + }} /> { + push(PATH.FORM, {}); + }} />
diff --git a/src/widgets/join/api/join.ts b/src/widgets/join/api/join.ts index 4e46635..4274001 100644 --- a/src/widgets/join/api/join.ts +++ b/src/widgets/join/api/join.ts @@ -1,6 +1,7 @@ import { useFlow } from '@/app/stackflow'; import { post, REQUEST } from '@/shared/api'; import { PATH } from '@/shared/constants'; +import { setSessionData } from '@/shared/utils'; import { useMutation } from '@tanstack/react-query'; import type { Dispatch, SetStateAction } from 'react'; @@ -19,7 +20,8 @@ export const useSubmitIdCard = ( return useMutation({ mutationFn: submitIdCard, - onSuccess: () => { + onSuccess: data => { + setSessionData('userInfo', data); replace(PATH.HOME, {}); }, onError: () => { diff --git a/src/widgets/join/model/hooks/index.ts b/src/widgets/join/model/hooks/index.ts deleted file mode 100644 index 47d62ec..0000000 --- a/src/widgets/join/model/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as useIdImage } from './useIdImage'; diff --git a/src/widgets/join/model/index.ts b/src/widgets/join/model/index.ts deleted file mode 100644 index 4cc90d0..0000000 --- a/src/widgets/join/model/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './hooks'; diff --git a/src/widgets/join/ui/JoinContainer.tsx b/src/widgets/join/ui/JoinContainer.tsx index c7e0d2d..1d0715e 100644 --- a/src/widgets/join/ui/JoinContainer.tsx +++ b/src/widgets/join/ui/JoinContainer.tsx @@ -1,8 +1,8 @@ import { CameraIcon, Logo } from '@/assets/icons'; -import { useIdImage } from '../model'; +import { useImageUpload } from '@/shared/hooks'; export default function JoinContainer() { - const { handleImageInputChange, image } = useIdImage(); + const { handleImageInputChange, image, isPending } = useImageUpload(); return (
@@ -34,6 +34,7 @@ export default function JoinContainer() { )}
+ {isPending &&
사진 확인 완료! 가입중..
}
); diff --git a/src/widgets/photo-result/ui/PhotoResultContainer.tsx b/src/widgets/photo-result/ui/PhotoResultContainer.tsx new file mode 100644 index 0000000..5b53dc1 --- /dev/null +++ b/src/widgets/photo-result/ui/PhotoResultContainer.tsx @@ -0,0 +1,49 @@ +import { useFlow } from '@/app/stackflow'; +import { PATH } from '@/shared/constants'; +import type { Tool } from '@/shared/types'; +import { Button, ToolButton } from '@/shared/ui'; +import { useState } from 'react'; + +export default function PhotoResultContainer({ tools }: { tools: Tool[] }) { + const { replace } = useFlow(); + const [selected, setSelected] = useState([]); + + return ( +
+
+

+ 상황에 맞는 농기계를 골라봤어요 +

+

필요한 장비를 선택해 예약을 진행해 주세요

+
+
+ {tools.map(({ id, image, toolType, description }) => ( + + setSelected(prev => + prev.includes(id) ? prev.filter(t => t !== id) : [...prev, id], + ) + } + /> + ))} +
+
+ +
+
+ ); +} diff --git a/src/widgets/photo-result/ui/index.ts b/src/widgets/photo-result/ui/index.ts new file mode 100644 index 0000000..cff04f1 --- /dev/null +++ b/src/widgets/photo-result/ui/index.ts @@ -0,0 +1 @@ +export { default as PhotoResultContainer } from './PhotoResultContainer'; diff --git a/src/widgets/photo-upload/api/index.ts b/src/widgets/photo-upload/api/index.ts new file mode 100644 index 0000000..6985ad4 --- /dev/null +++ b/src/widgets/photo-upload/api/index.ts @@ -0,0 +1 @@ +export * from './photo'; diff --git a/src/widgets/photo-upload/api/photo.ts b/src/widgets/photo-upload/api/photo.ts new file mode 100644 index 0000000..04741a1 --- /dev/null +++ b/src/widgets/photo-upload/api/photo.ts @@ -0,0 +1,24 @@ +import { useFlow } from '@/app/stackflow'; +import { post, REQUEST } from '@/shared/api'; +import { PATH } from '@/shared/constants'; +import type { Tool } from '@/shared/types'; +import { useMutation } from '@tanstack/react-query'; + +const submitPhoto = async (data: FormData) => { + const response = await post({ + request: REQUEST.PHOTO_UPLOAD, + data: data, + }); + return response.data; +}; + +export const useSubmitPhoto = () => { + const { replace } = useFlow(); + + return useMutation({ + mutationFn: submitPhoto, + onSuccess: data => { + replace(PATH.PHOTO_RESULT, { result: data }); + }, + }); +}; diff --git a/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx b/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx new file mode 100644 index 0000000..810de58 --- /dev/null +++ b/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx @@ -0,0 +1,59 @@ +import { CameraIcon } from '@/assets/icons'; +import { useImageUpload } from '@/shared/hooks'; +import { Button } from '@/shared/ui'; +import { base64ToFile } from '@/shared/utils'; +import { useFlow } from '@/app/stackflow'; +import { PATH } from '@/shared/constants'; + +export default function PhotoUploadContainer() { + const { handleImageInputChange, image } = useImageUpload(true); + const { replace } = useFlow(); + + const handleSubmitPhoto = () => { + if (image) { + const file = base64ToFile(image); + const formData = new FormData(); + formData.append('image', file); + replace(PATH.PHOTO_LOADING, { data: formData }); + } + }; + + return ( +
+

피해 현장 사진을 올려주세요

+

+ AI가 사진을 보고 필요한 농기계를 추천해 드립니다 +

+ +
+ +
+
+ ); +} diff --git a/src/widgets/photo-upload/ui/index.ts b/src/widgets/photo-upload/ui/index.ts new file mode 100644 index 0000000..a6a9726 --- /dev/null +++ b/src/widgets/photo-upload/ui/index.ts @@ -0,0 +1 @@ +export { default as PhotoUploadContainer } from './PhotoUploadContainer';