From fdf89757b5aef602cd33e712b6517b7106a873e0 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 18:21:26 +0900 Subject: [PATCH 01/41] feat : install `cva` to implement shared `Button` component --- package.json | 1 + pnpm-lock.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/package.json b/package.json index 89408e2..2857718 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@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", "prettier": "^3.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5dd208..7316080 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ 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 @@ -808,6 +811,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'} @@ -2153,6 +2159,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: From 541088e3cd9142b43ec2907ed3fd05760c1cdf82 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 18:21:38 +0900 Subject: [PATCH 02/41] feat : implement `Button` component --- src/shared/ui/Button.tsx | 46 ++++++++++++++++++++++++++++++++++++++++ src/shared/ui/index.ts | 1 + 2 files changed, 47 insertions(+) create mode 100644 src/shared/ui/Button.tsx diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx new file mode 100644 index 0000000..7856539 --- /dev/null +++ b/src/shared/ui/Button.tsx @@ -0,0 +1,46 @@ +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', + }, + size: { + md: 'w-full', + lg: 'w-full h-14 text-xl', + }, + }, + defaultVariants: { + intent: 'primary', + size: 'md', + }, + }, +); + +export default function Button({ + intent, + size, + children, + className, + ...props +}: ButtonsProps) { + return ( + + ); +} diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index d026ed2..8b6b887 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,2 +1,3 @@ export * from './AppBar'; export { default as Dock } from './Dock'; +export { default as Button } from './Button'; From bfbf97c53a12198a00db9713f3f543f23f9f136c Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 18:22:01 +0900 Subject: [PATCH 03/41] feat : add image asset of phone call complete page --- src/assets/images/call-complete.png | Bin 0 -> 36849 bytes src/assets/images/index.ts | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 src/assets/images/call-complete.png diff --git a/src/assets/images/call-complete.png b/src/assets/images/call-complete.png new file mode 100644 index 0000000000000000000000000000000000000000..097b5461ad009c16163222ce70e2ecf5de2ae682 GIT binary patch literal 36849 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 Date: Sat, 28 Jun 2025 18:22:22 +0900 Subject: [PATCH 04/41] feat : implement `PhoneCompleteScreen` --- src/app/stackflow/Stack.tsx | 2 ++ .../phone-complete/PhoneCompleteScreen.tsx | 30 +++++++++++++++++++ src/screen/phone-complete/index.ts | 1 + src/shared/constants/path.ts | 1 + 4 files changed, 34 insertions(+) create mode 100644 src/screen/phone-complete/PhoneCompleteScreen.tsx create mode 100644 src/screen/phone-complete/index.ts diff --git a/src/app/stackflow/Stack.tsx b/src/app/stackflow/Stack.tsx index efc1388..9bbceac 100644 --- a/src/app/stackflow/Stack.tsx +++ b/src/app/stackflow/Stack.tsx @@ -1,5 +1,6 @@ import { HomeScreen } from '@/screen/home/ui'; import { JoinScreen } from '@/screen/join/ui'; +import { PhoneCompleteScreen } from '@/screen/phone-complete'; import { basicUIPlugin } from '@stackflow/plugin-basic-ui'; import { basicRendererPlugin } from '@stackflow/plugin-renderer-basic'; import { stackflow } from '@stackflow/react'; @@ -9,6 +10,7 @@ export const { Stack, useFlow } = stackflow({ activities: { JoinScreen, HomeScreen, + PhoneCompleteScreen, }, plugins: [ basicRendererPlugin(), diff --git a/src/screen/phone-complete/PhoneCompleteScreen.tsx b/src/screen/phone-complete/PhoneCompleteScreen.tsx new file mode 100644 index 0000000..8465db1 --- /dev/null +++ b/src/screen/phone-complete/PhoneCompleteScreen.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/phone-complete/index.ts b/src/screen/phone-complete/index.ts new file mode 100644 index 0000000..62ecd0c --- /dev/null +++ b/src/screen/phone-complete/index.ts @@ -0,0 +1 @@ +export { default as PhoneCompleteScreen } from './PhoneCompleteScreen'; diff --git a/src/shared/constants/path.ts b/src/shared/constants/path.ts index f3528f4..68b49fd 100644 --- a/src/shared/constants/path.ts +++ b/src/shared/constants/path.ts @@ -1,4 +1,5 @@ export const PATH = { HOME: 'HomeScreen', JOIN: 'JoinScreen', + PHONE_COMPLETE: 'PhoneCompleteScreen', } as const; From 356088c4e1c6e7cb69a8f2bd76e7f6aa45384b2b Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 18:22:43 +0900 Subject: [PATCH 05/41] feat : add conditional line to render `Dock` component --- src/shared/ui/Dock.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/shared/ui/Dock.tsx b/src/shared/ui/Dock.tsx index 75af86b..649cc58 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.PHONE_COMPLETE; + return ( <> - {DOCK_ITEMS.length > 0 && ( + {render && (
Date: Sat, 28 Jun 2025 18:22:59 +0900 Subject: [PATCH 06/41] refactor : make all buttons use `Button` component --- src/widgets/home/ui/HomeButton.tsx | 10 +++++----- src/widgets/home/ui/HomeContainer.tsx | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/widgets/home/ui/HomeButton.tsx b/src/widgets/home/ui/HomeButton.tsx index e7bc4f7..ad63391 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, @@ -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..48de680 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 (
@@ -20,6 +24,9 @@ export default function HomeContainer() { description="전화가 연결되면, 그냥 평소처럼 이야기하듯 말해주세요.
AI가 알아듣고 필요한 농기계 신청을 도와줍니다." buttonLabel="말로 신청하기" isAtag + onClick={() => { + push(PATH.PHONE_COMPLETE, {}); + }} /> Date: Sat, 28 Jun 2025 18:27:27 +0900 Subject: [PATCH 07/41] feat : implement appbar with iconbutton at `HomeScreen` --- src/assets/icons/icon-bell.svg | 4 ++++ src/assets/icons/icon-help.svg | 4 ++++ src/assets/icons/index.ts | 4 ++++ src/screen/home/ui/HomeScreen.tsx | 4 ++-- src/shared/ui/AppBar.tsx | 27 +++++++++++++++++++++++---- 5 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 src/assets/icons/icon-bell.svg create mode 100644 src/assets/icons/icon-help.svg 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-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/index.ts b/src/assets/icons/index.ts index a498b9c..54817f9 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -10,6 +10,8 @@ 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'; export { Logo, @@ -24,4 +26,6 @@ export { HomeSelectedIcon, UserSelectedIcon, ReservationSelectedIcon, + BellIcon, + HelpIcon, }; 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/shared/ui/AppBar.tsx b/src/shared/ui/AppBar.tsx index e2340b9..2e3d96e 100644 --- a/src/shared/ui/AppBar.tsx +++ b/src/shared/ui/AppBar.tsx @@ -1,9 +1,28 @@ +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: () => ( +
+ + +
+ ), +}); From ca20187b8500222401c08d999f2ff67fa1dfd76b Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 19:21:20 +0900 Subject: [PATCH 08/41] feat : add `PhotoUploadScreen` --- src/app/stackflow/Stack.tsx | 4 +++- src/screen/photo-upload/ui/PhotoUploadScreen.tsx | 11 +++++++++++ src/screen/photo-upload/ui/index.ts | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/screen/photo-upload/ui/PhotoUploadScreen.tsx create mode 100644 src/screen/photo-upload/ui/index.ts diff --git a/src/app/stackflow/Stack.tsx b/src/app/stackflow/Stack.tsx index 9bbceac..e7bfed2 100644 --- a/src/app/stackflow/Stack.tsx +++ b/src/app/stackflow/Stack.tsx @@ -1,6 +1,7 @@ import { HomeScreen } from '@/screen/home/ui'; import { JoinScreen } from '@/screen/join/ui'; import { PhoneCompleteScreen } from '@/screen/phone-complete'; +import { PhotoUploadScreen } from '@/screen/photo-upload/ui'; import { basicUIPlugin } from '@stackflow/plugin-basic-ui'; import { basicRendererPlugin } from '@stackflow/plugin-renderer-basic'; import { stackflow } from '@stackflow/react'; @@ -11,6 +12,7 @@ export const { Stack, useFlow } = stackflow({ JoinScreen, HomeScreen, PhoneCompleteScreen, + PhotoUploadScreen, }, plugins: [ basicRendererPlugin(), @@ -18,5 +20,5 @@ export const { Stack, useFlow } = stackflow({ theme: 'cupertino', }), ], - initialActivity: () => 'HomeScreen', + initialActivity: () => 'JoinScreen', }); 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'; From 7221c5b2891249e9ee724347f9ffb14478c271df Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 19:23:10 +0900 Subject: [PATCH 09/41] refactor : change layer and add mode of image hook to reuse --- src/shared/hooks/index.ts | 1 + .../useIdImage.ts => shared/hooks/useImageUpload.ts} | 10 ++++++---- src/widgets/join/model/hooks/index.ts | 1 - src/widgets/join/model/index.ts | 1 - 4 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 src/shared/hooks/index.ts rename src/{widgets/join/model/hooks/useIdImage.ts => shared/hooks/useImageUpload.ts} (74%) delete mode 100644 src/widgets/join/model/hooks/index.ts delete mode 100644 src/widgets/join/model/index.ts 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 74% rename from src/widgets/join/model/hooks/useIdImage.ts rename to src/shared/hooks/useImageUpload.ts index 6f4510b..d4c3f7f 100644 --- a/src/widgets/join/model/hooks/useIdImage.ts +++ b/src/shared/hooks/useImageUpload.ts @@ -1,7 +1,7 @@ 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); @@ -13,9 +13,11 @@ 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(''); } 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'; From d62b6c3eb7f5d01d6942e499e9ea3fa6c15ac239 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 19:23:38 +0900 Subject: [PATCH 10/41] feat : implement `PhotoUploadContainer` --- src/shared/constants/path.ts | 1 + .../photo-upload/ui/PhotoUploadContainer.tsx | 40 +++++++++++++++++++ src/widgets/photo-upload/ui/index.ts | 1 + 3 files changed, 42 insertions(+) create mode 100644 src/widgets/photo-upload/ui/PhotoUploadContainer.tsx create mode 100644 src/widgets/photo-upload/ui/index.ts diff --git a/src/shared/constants/path.ts b/src/shared/constants/path.ts index 68b49fd..2364e52 100644 --- a/src/shared/constants/path.ts +++ b/src/shared/constants/path.ts @@ -2,4 +2,5 @@ export const PATH = { HOME: 'HomeScreen', JOIN: 'JoinScreen', PHONE_COMPLETE: 'PhoneCompleteScreen', + PHOTO_UPLOAD: 'PhotoUploadScreen', } as const; diff --git a/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx b/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx new file mode 100644 index 0000000..edfcdb8 --- /dev/null +++ b/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx @@ -0,0 +1,40 @@ +import { CameraIcon } from '@/assets/icons'; +import { useImageUpload } from '@/shared/hooks'; +import { Button } from '@/shared/ui'; + +export default function PhotoUploadContainer() { + const { handleImageInputChange, image } = useImageUpload(true); + + 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'; From 0d29602c7757f71420c13496f219d7c05cdbfdc4 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 19:27:36 +0900 Subject: [PATCH 11/41] refactor : change import of hook --- src/widgets/join/ui/JoinContainer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/join/ui/JoinContainer.tsx b/src/widgets/join/ui/JoinContainer.tsx index c7e0d2d..a39d13d 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 } = useImageUpload(); return (
From ae4d7cbce815f00f46332a09ef6db180b31d7151 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 19:27:56 +0900 Subject: [PATCH 12/41] feat : add style of disabled intent --- src/shared/ui/Button.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx index 7856539..df29deb 100644 --- a/src/shared/ui/Button.tsx +++ b/src/shared/ui/Button.tsx @@ -15,6 +15,7 @@ const ButtonVariants = cva( 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', From 5f5422fe810e1bd0a334ddb932589ae037390e0d Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 19:28:03 +0900 Subject: [PATCH 13/41] feat : add normal appbar --- src/shared/ui/AppBar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/ui/AppBar.tsx b/src/shared/ui/AppBar.tsx index 2e3d96e..7dbe636 100644 --- a/src/shared/ui/AppBar.tsx +++ b/src/shared/ui/AppBar.tsx @@ -26,3 +26,5 @@ export const HomeAppBar = (
), }); + +export const NormalAppBar = (title: string) => ({ title: title, ...baseStyle }); From dbfc9d93adb3ab950987ddb83cdffa0837f13c38 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 19:28:22 +0900 Subject: [PATCH 14/41] feat : add render conditional logic --- src/shared/ui/Dock.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/ui/Dock.tsx b/src/shared/ui/Dock.tsx index 649cc58..fc64808 100644 --- a/src/shared/ui/Dock.tsx +++ b/src/shared/ui/Dock.tsx @@ -23,7 +23,8 @@ export default function Dock(isLoading: DockProps) { .map(i => i.name) .pop() as PathItem; - const render = current !== PATH.PHONE_COMPLETE; + const render = + current !== PATH.PHONE_COMPLETE && current !== PATH.PHOTO_UPLOAD; return ( <> From 81bcfdeaa47eaf03ba2ea969ac69d7eee0e27c5c Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 19:29:01 +0900 Subject: [PATCH 15/41] design : change text --- src/widgets/home/ui/HomeContainer.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/widgets/home/ui/HomeContainer.tsx b/src/widgets/home/ui/HomeContainer.tsx index 48de680..5d09e7d 100644 --- a/src/widgets/home/ui/HomeContainer.tsx +++ b/src/widgets/home/ui/HomeContainer.tsx @@ -20,9 +20,9 @@ export default function HomeContainer() {
{ push(PATH.PHONE_COMPLETE, {}); @@ -33,6 +33,9 @@ export default function HomeContainer() { label="사진으로 신청하기" description="피해 현장 사진을 찍어 올리면
AI가 현장을 파악하고 지원을 도와줍니다." buttonLabel="사진 업로드" + onClick={() => { + push(PATH.PHOTO_UPLOAD, {}); + }} /> Date: Sat, 28 Jun 2025 20:26:05 +0900 Subject: [PATCH 16/41] design : adjust button size and text --- src/screen/phone-complete/PhoneCompleteScreen.tsx | 2 +- src/shared/ui/Button.tsx | 4 ++-- src/widgets/home/ui/HomeButton.tsx | 2 +- src/widgets/home/ui/HomeContainer.tsx | 5 ++++- src/widgets/photo-upload/ui/PhotoUploadContainer.tsx | 6 +++++- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/screen/phone-complete/PhoneCompleteScreen.tsx b/src/screen/phone-complete/PhoneCompleteScreen.tsx index 8465db1..ba71b57 100644 --- a/src/screen/phone-complete/PhoneCompleteScreen.tsx +++ b/src/screen/phone-complete/PhoneCompleteScreen.tsx @@ -18,7 +18,7 @@ export default function PhoneCompleteScreen() { AI가 계약서를 자동으로 작성하였으며
예약 현황에서 언제든지 확인하실 수 있습니다

-
diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx index df29deb..3cfe13c 100644 --- a/src/shared/ui/Button.tsx +++ b/src/shared/ui/Button.tsx @@ -18,8 +18,8 @@ const ButtonVariants = cva( disabled: 'bg-[#D8D8D8] text-white', }, size: { - md: 'w-full', - lg: 'w-full h-14 text-xl', + md: 'w-full py-3', + lg: 'w-full py-5 text-xl', }, }, defaultVariants: { diff --git a/src/widgets/home/ui/HomeButton.tsx b/src/widgets/home/ui/HomeButton.tsx index ad63391..5fa348c 100644 --- a/src/widgets/home/ui/HomeButton.tsx +++ b/src/widgets/home/ui/HomeButton.tsx @@ -19,7 +19,7 @@ export const HomeButton = ({

{label}

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

+

{line}

))} diff --git a/src/widgets/home/ui/HomeContainer.tsx b/src/widgets/home/ui/HomeContainer.tsx index 5d09e7d..beb6dad 100644 --- a/src/widgets/home/ui/HomeContainer.tsx +++ b/src/widgets/home/ui/HomeContainer.tsx @@ -21,7 +21,7 @@ export default function HomeContainer() { { @@ -42,6 +42,9 @@ export default function HomeContainer() { label="직접 입력해서 신청하기" description="AI 추천이 아닌 내가 원하는 방식대로
농기계 대여소 날짜를 직접 선택해 신청하세요." buttonLabel="직접 입력하기" + onClick={() => { + push(PATH.FORM, {}); + }} />
diff --git a/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx b/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx index edfcdb8..7056114 100644 --- a/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx +++ b/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx @@ -31,7 +31,11 @@ export default function PhotoUploadContainer() {
-
From 97c028a3cb2c3120fefbacaff6a40cf1d00f317f Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:04:45 +0900 Subject: [PATCH 17/41] design : add letter spacing --- src/app/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 (
-
+
From e598bd5025eafb7a55be2eb43bbf45cb44a924db Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:05:33 +0900 Subject: [PATCH 18/41] feat : add search icon --- src/assets/icons/icon-search.svg | 4 ++++ src/assets/icons/index.ts | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 src/assets/icons/icon-search.svg 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/index.ts b/src/assets/icons/index.ts index 54817f9..fe0a843 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -12,6 +12,7 @@ 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'; export { Logo, @@ -28,4 +29,5 @@ export { ReservationSelectedIcon, BellIcon, HelpIcon, + SearchIcon, }; From 6647ff8d5a210afeefd587ba4d7ca81e7a0288f0 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:21:27 +0900 Subject: [PATCH 19/41] feat : implement `FormScreen` --- src/screen/form/ui/FormScreen.tsx | 17 +++++++++ src/screen/form/ui/index.ts | 1 + src/widgets/form/ui/FormContainer.tsx | 51 +++++++++++++++++++++++++++ src/widgets/form/ui/FormInput.tsx | 37 +++++++++++++++++++ src/widgets/form/ui/index.ts | 1 + 5 files changed, 107 insertions(+) create mode 100644 src/screen/form/ui/FormScreen.tsx create mode 100644 src/screen/form/ui/index.ts create mode 100644 src/widgets/form/ui/FormContainer.tsx create mode 100644 src/widgets/form/ui/FormInput.tsx create mode 100644 src/widgets/form/ui/index.ts 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/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'; From c23afe2ba2ead2719ac178c12053ee4f75adc6f3 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:22:00 +0900 Subject: [PATCH 20/41] feat : change style of normal appbar and add conditional to dock --- src/shared/ui/AppBar.tsx | 10 +++++++++- src/shared/ui/Dock.tsx | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/shared/ui/AppBar.tsx b/src/shared/ui/AppBar.tsx index 7dbe636..2ff47fb 100644 --- a/src/shared/ui/AppBar.tsx +++ b/src/shared/ui/AppBar.tsx @@ -27,4 +27,12 @@ export const HomeAppBar = ( ), }); -export const NormalAppBar = (title: string) => ({ title: title, ...baseStyle }); +export const NormalAppBar = (title: string) => ({ + renderLeft: () => { + if (title) { + return {title}; + } + return null; + }, + ...baseStyle, +}); diff --git a/src/shared/ui/Dock.tsx b/src/shared/ui/Dock.tsx index fc64808..79ecacf 100644 --- a/src/shared/ui/Dock.tsx +++ b/src/shared/ui/Dock.tsx @@ -24,7 +24,9 @@ export default function Dock(isLoading: DockProps) { .pop() as PathItem; const render = - current !== PATH.PHONE_COMPLETE && current !== PATH.PHOTO_UPLOAD; + current !== PATH.PHONE_COMPLETE && + current !== PATH.PHOTO_UPLOAD && + current !== PATH.FORM; return ( <> From 22bc782970cbc19ecc7bb4c9a3e0acec8655245c Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:22:15 +0900 Subject: [PATCH 21/41] feat : define the type of `Reservation` --- src/shared/types/index.ts | 1 + src/shared/types/reservation.ts | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 src/shared/types/reservation.ts diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 05a1f8b..13723f6 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,2 +1,3 @@ export * from './dock'; export * from './path'; +export * from './reservation'; 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; +}; From 8f045cf3a58a673bdc4b3a7eb1da6cdf0fc393dc Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:24:29 +0900 Subject: [PATCH 22/41] feat : add path of `FormScreen` --- src/shared/constants/path.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/constants/path.ts b/src/shared/constants/path.ts index 2364e52..d17eb91 100644 --- a/src/shared/constants/path.ts +++ b/src/shared/constants/path.ts @@ -3,4 +3,5 @@ export const PATH = { JOIN: 'JoinScreen', PHONE_COMPLETE: 'PhoneCompleteScreen', PHOTO_UPLOAD: 'PhotoUploadScreen', + FORM: 'FormScreen', } as const; From 9256a7f9c0cd74da52fb3e125901c135ad943f92 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:26:21 +0900 Subject: [PATCH 23/41] rename : change `PhoneCompleteScreen` into `CompleteScreen` --- src/app/stackflow/Stack.tsx | 6 ++++-- .../PhoneCompleteScreen.tsx => complete/CompleteScreen.tsx} | 2 +- src/screen/complete/index.ts | 1 + src/screen/phone-complete/index.ts | 1 - src/shared/constants/path.ts | 2 +- src/shared/ui/Dock.tsx | 2 +- src/widgets/home/ui/HomeContainer.tsx | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) rename src/screen/{phone-complete/PhoneCompleteScreen.tsx => complete/CompleteScreen.tsx} (91%) create mode 100644 src/screen/complete/index.ts delete mode 100644 src/screen/phone-complete/index.ts diff --git a/src/app/stackflow/Stack.tsx b/src/app/stackflow/Stack.tsx index e7bfed2..0c29752 100644 --- a/src/app/stackflow/Stack.tsx +++ b/src/app/stackflow/Stack.tsx @@ -1,6 +1,7 @@ +import { CompleteScreen } from '@/screen/complete'; +import { FormScreen } from '@/screen/form/ui'; import { HomeScreen } from '@/screen/home/ui'; import { JoinScreen } from '@/screen/join/ui'; -import { PhoneCompleteScreen } from '@/screen/phone-complete'; import { PhotoUploadScreen } from '@/screen/photo-upload/ui'; import { basicUIPlugin } from '@stackflow/plugin-basic-ui'; import { basicRendererPlugin } from '@stackflow/plugin-renderer-basic'; @@ -11,8 +12,9 @@ export const { Stack, useFlow } = stackflow({ activities: { JoinScreen, HomeScreen, - PhoneCompleteScreen, PhotoUploadScreen, + FormScreen, + CompleteScreen, }, plugins: [ basicRendererPlugin(), diff --git a/src/screen/phone-complete/PhoneCompleteScreen.tsx b/src/screen/complete/CompleteScreen.tsx similarity index 91% rename from src/screen/phone-complete/PhoneCompleteScreen.tsx rename to src/screen/complete/CompleteScreen.tsx index ba71b57..eed4188 100644 --- a/src/screen/phone-complete/PhoneCompleteScreen.tsx +++ b/src/screen/complete/CompleteScreen.tsx @@ -13,7 +13,7 @@ export default function PhoneCompleteScreen() {
-

신청이 완료되었습니다

+

신청이 완료되었습니다

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/phone-complete/index.ts b/src/screen/phone-complete/index.ts deleted file mode 100644 index 62ecd0c..0000000 --- a/src/screen/phone-complete/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PhoneCompleteScreen } from './PhoneCompleteScreen'; diff --git a/src/shared/constants/path.ts b/src/shared/constants/path.ts index d17eb91..8de1b04 100644 --- a/src/shared/constants/path.ts +++ b/src/shared/constants/path.ts @@ -1,7 +1,7 @@ export const PATH = { HOME: 'HomeScreen', JOIN: 'JoinScreen', - PHONE_COMPLETE: 'PhoneCompleteScreen', + COMPLETE: 'CompleteScreen', PHOTO_UPLOAD: 'PhotoUploadScreen', FORM: 'FormScreen', } as const; diff --git a/src/shared/ui/Dock.tsx b/src/shared/ui/Dock.tsx index 79ecacf..02f028f 100644 --- a/src/shared/ui/Dock.tsx +++ b/src/shared/ui/Dock.tsx @@ -24,7 +24,7 @@ export default function Dock(isLoading: DockProps) { .pop() as PathItem; const render = - current !== PATH.PHONE_COMPLETE && + current !== PATH.COMPLETE && current !== PATH.PHOTO_UPLOAD && current !== PATH.FORM; diff --git a/src/widgets/home/ui/HomeContainer.tsx b/src/widgets/home/ui/HomeContainer.tsx index beb6dad..0cc2fb4 100644 --- a/src/widgets/home/ui/HomeContainer.tsx +++ b/src/widgets/home/ui/HomeContainer.tsx @@ -25,7 +25,7 @@ export default function HomeContainer() { buttonLabel="전화로 신청하기" isAtag onClick={() => { - push(PATH.PHONE_COMPLETE, {}); + push(PATH.COMPLETE, {}); }} /> Date: Sat, 28 Jun 2025 22:34:23 +0900 Subject: [PATCH 24/41] feat : define `User` type --- src/shared/types/index.ts | 1 + src/shared/types/user.ts | 1 + 2 files changed, 2 insertions(+) create mode 100644 src/shared/types/user.ts diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 13723f6..5c182c5 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,3 +1,4 @@ export * from './dock'; export * from './path'; export * from './reservation'; +export * from './user'; 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 }; From c52004f1e062734cccb27c5d4dcc3e4843b8ed45 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:35:20 +0900 Subject: [PATCH 25/41] feat : implement shared util function session --- src/shared/utils/index.ts | 1 + src/shared/utils/session.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/shared/utils/session.ts 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); +}; From 4563323064232a844b13e994521f85c1095ec6ac Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 22:35:42 +0900 Subject: [PATCH 26/41] feat : store user information on success --- src/widgets/join/api/join.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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: () => { From 84d1b36d2a45e71867141ba94f96f83b0e67a016 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 23:13:30 +0900 Subject: [PATCH 27/41] feat : add favicon --- favicon/favicon.ico | Bin 0 -> 15406 bytes index.html | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 favicon/favicon.ico diff --git a/favicon/favicon.ico b/favicon/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b9bcc5bb755e206e09a54db3bd3396e4f5e7c117 GIT binary patch literal 15406 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

- + 농기구온 From 722a4ba0f4c9d06c11134f78942cefe186510e90 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 23:20:47 +0900 Subject: [PATCH 28/41] chore : install framer to implement animation --- package.json | 1 + pnpm-lock.yaml | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/package.json b/package.json index 2857718..d53ab45 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "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 7316080..dc13b00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: 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 @@ -1028,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} @@ -1264,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==} @@ -1472,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'} @@ -2392,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 @@ -2581,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: {} @@ -2727,6 +2768,8 @@ snapshots: dependencies: typescript: 5.8.3 + tslib@2.8.1: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 From d4bca8401048f024cc2517061eb830e71b9f6b7f Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 23:21:28 +0900 Subject: [PATCH 29/41] feat : add loading animation component with multiple loader images --- src/assets/images/background-loading.png | Bin 0 -> 12035 bytes src/assets/images/index.ts | 15 ++++++++- src/assets/images/loader-1.png | Bin 0 -> 690 bytes src/assets/images/loader-2.png | Bin 0 -> 651 bytes src/assets/images/loader-3.png | Bin 0 -> 682 bytes src/assets/images/loader-4.png | Bin 0 -> 688 bytes src/shared/ui/Loader.tsx | 41 +++++++++++++++++++++++ src/shared/ui/index.ts | 1 + 8 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/assets/images/background-loading.png create mode 100644 src/assets/images/loader-1.png create mode 100644 src/assets/images/loader-2.png create mode 100644 src/assets/images/loader-3.png create mode 100644 src/assets/images/loader-4.png create mode 100644 src/shared/ui/Loader.tsx diff --git a/src/assets/images/background-loading.png b/src/assets/images/background-loading.png new file mode 100644 index 0000000000000000000000000000000000000000..68168e519d15c437aad915d555b12cc945ad5256 GIT binary patch literal 12035 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts index 97ac568..cbe0535 100644 --- a/src/assets/images/index.ts +++ b/src/assets/images/index.ts @@ -1,4 +1,17 @@ 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, PhoneCompleteImage }; +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 0000000000000000000000000000000000000000..771f7fbe219c98416f165e769959d5f6d3c30c37 GIT binary patch literal 690 zcmV;j0!{siP)|y$X%JMN359tA#1mjT zNG4kLPPQirvSI~~w7*7Yo&J2@)$Q#r;g&p$ti4FlCc|C%*#@I2$L4%`Mudcc1Sx$p z2J!1*9OD*HnTro1+dM-$0Mo%GG3%0u==cuk`FI#7>)y1MKqu@R=^ecc3q)c zH(k&l?G7%0_wrba&C!-;KI%LOf1REr)1nItEw~@;Mw_zQN&&>{2!xO^%u-xHV5uFsjg8_tmchzqC)} z@B+0#JNIPad_74f%QK6?)5r!!Tr0x>^)hP^g^TZT>0%iiqJD7>A9EK!yangh`wm5j z6CV6 zpcdXXtRwcVqUly~+03{~ng+^o0%uT+2qfhKC7Xp}45#=E#h5Ll+k!sb>W{iVp)}mg zvl)5?rFel2gE5rii>AqO^1JgeY{_s3%2@6s4Z;x=2JL0p>8yCa7ygl94~n=_7oQ#{ z6K$@}?ak~*-AY>@MdW5P(#UyCO18kl_FI2sCH!R7Rb3P|Fy)*4*y)8oU>R0jHd>Y%qTjw;e_@hree$oQ_=wI# z&z{J{`FoK4KCh`RUMIfqB(!qbN4Ls*M1yKPo`+a2cF;YXs$6rco6^C#d;eib;^Jd< zv2<|mo+%v{DA(Uvh?zaCiFjNVx+^JWlf#@G(a04=dkgZa((Rm+AaB5^8QKP9Iy4T5)HI+T`0WD9_|t5JSwPvS1})73 zYUV*plYp8UsnR0$0A=h|?x_qqcrmGD43MUKaoAt1F$1XinZ0-6N}|dHAQ5R|b5z&% z5S@#ryK!ht@pb1QA(z}^To=|wbp%M$9%%6OeVE+Tv%L_DW0LWo_8ca4iOMG351_(; l6e5*F*-y#wrR)9;aR&HtvV_FQOVj`W002ovPDHLkV1n2GDlY&4 literal 0 HcmV?d00001 diff --git a/src/assets/images/loader-3.png b/src/assets/images/loader-3.png new file mode 100644 index 0000000000000000000000000000000000000000..1df1da9aa3825fdd5896c3814c80f7a0ad802523 GIT binary patch literal 682 zcmV;b0#*HqP)4I!VMTYQbq1EL!%quoB)^k zm}s+4vJ=OnD0arN_U9St>FG)SYj@w*5+2C=(DyGBtP@K^FV`Wc>A8j_LyI)t3H{nR zk~V~P9z#qXa|v|9E|7F26Sh+B71-)+wzXLoE3()ug#x$3&9DrsO_M7ybE*6ifsc}T zl2>^lawM*28h~djQnwlr=eXtK)))URNPfT&*8<;#ex22|TCBqmJXPqxm^epj!LjxJ z!5}<7bYPUs#Ueh8!fsUpgRwV(lcfCFq6pUCGZ=+-O>&2z4^X7$N94BxD zyNpyI*%e3_vL0esMd*Yj!2nhf#9U#!7Gy@0F1K2}#(!8FZZ!T5f`K<)%}^i4P|HQ` zoC>Tm_F#;spil0c3fv1K9ct`YkqAFk)6kW#XOb$R3k!p`#@@twj>@EPd^kLP+4Rn| z*hHBS1z&?Gnp{?<@BKX-9&fzy*0lHvr9l0I-RSeJn!IM)VdH4kc^Qqn_**b|K3ku= zn$Z~8q7_boL=@?NzRtZ@^kA#!>+mCOJSwSM?@aMkiXPRN{Ik*YPV_TeGHJOp4Pm6Y z$B*AXMBSohg#sNaGJetYe(M|8XlYq5>Q{-j_IGc3>zysJ04_yBf-aF}_GQ7nj9Ltd z3e#CDn;7PO?18uqf4ZmEZL7YdF3{bfjKjc*$ri$~ZpsGgrg%TN-&%QG0oO{VJ+aoN Q4*&oF07*qoM6N<$f=hZcoB#j- literal 0 HcmV?d00001 diff --git a/src/assets/images/loader-4.png b/src/assets/images/loader-4.png new file mode 100644 index 0000000000000000000000000000000000000000..c6ab41ea03cb2316b4de7afdae4a003d8a787385 GIT binary patch literal 688 zcmV;h0#E&kP)6}S)B@j0z~>{?p7mEu$KETcKrTynJR$-v)%58zEeD_ z69}9$%C?IKjRJF57gkg$P&myfpNnb*ViA9pCmI&&Ss3PyUJHUh8MRak#1`nQ7KrzG zRk=WN?gic!5tx!*QV3+d0&z1m_=rg>mQRe$Xhd4E6q|<8YeBj2vt+Oz|3u=DCNTdF zf{`~~W-Jd$8L(U=X)ds7XpfX3kq1e0f#dtA9xF;{qzH>lc9JI3fHW-Hn0tlwghr7$ zK1_Z+?s~tG?NXgE2k)msb&ct|?}y3p+B0tj*(X#BJUrNn-kmEcYqlA-kL0FLOGM%` zAA-@ti{+)K8O(uoUg8$W%o5n_l-5;s?Cttke#W<}yCZjNe^zaysw?K?x3#W!f}hc% zMej6BDK6-5KE93yRm~~|3J@7T>Uv)>;tI^&qJ_Z2CiD9K*42S^rW^1)^j)?zK!B%> zlZJbfzG5*%{D-n@l?5ticbk)!q3>rI-gHB&7{s+?U5IujV#WvI7*T-{+!W9IH^g6u W=CcdKeZ-9b0000 + {images.map((src, i) => ( + + {`loader-${i}`} + + ))} + + ); +} diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 8b6b887..5010953 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,3 +1,4 @@ export * from './AppBar'; export { default as Dock } from './Dock'; export { default as Button } from './Button'; +export { default as Loader } from './Loader'; From b323a504bf5d68e91bbe62129d4a422982b9a393 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sat, 28 Jun 2025 23:21:33 +0900 Subject: [PATCH 30/41] feat : add PhotoLoadingScreen and update stackflow configuration --- src/app/stackflow/Stack.tsx | 2 ++ .../photo-loading/ui/PhotoLoadingScreen.tsx | 18 ++++++++++++++++++ src/screen/photo-loading/ui/index.ts | 1 + src/shared/constants/path.ts | 1 + 4 files changed, 22 insertions(+) create mode 100644 src/screen/photo-loading/ui/PhotoLoadingScreen.tsx create mode 100644 src/screen/photo-loading/ui/index.ts diff --git a/src/app/stackflow/Stack.tsx b/src/app/stackflow/Stack.tsx index 0c29752..cc34853 100644 --- a/src/app/stackflow/Stack.tsx +++ b/src/app/stackflow/Stack.tsx @@ -2,6 +2,7 @@ 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 { PhotoUploadScreen } from '@/screen/photo-upload/ui'; import { basicUIPlugin } from '@stackflow/plugin-basic-ui'; import { basicRendererPlugin } from '@stackflow/plugin-renderer-basic'; @@ -15,6 +16,7 @@ export const { Stack, useFlow } = stackflow({ PhotoUploadScreen, FormScreen, CompleteScreen, + PhotoLoadingScreen, }, plugins: [ basicRendererPlugin(), diff --git a/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx b/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx new file mode 100644 index 0000000..36ee217 --- /dev/null +++ b/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx @@ -0,0 +1,18 @@ +import { LoadingBackground } from '@/assets/images'; +import { Loader } from '@/shared/ui'; +import { AppScreen } from '@stackflow/plugin-basic-ui'; + +export default function PhotoUploadScreen() { + return ( + +

+
+ +

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

+
+
+ + ); +} 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/shared/constants/path.ts b/src/shared/constants/path.ts index 8de1b04..9bf230a 100644 --- a/src/shared/constants/path.ts +++ b/src/shared/constants/path.ts @@ -3,5 +3,6 @@ export const PATH = { JOIN: 'JoinScreen', COMPLETE: 'CompleteScreen', PHOTO_UPLOAD: 'PhotoUploadScreen', + PHOTO_LOADING: 'PhotoLoadingScreen', FORM: 'FormScreen', } as const; From 992dd12d73885ebfc192b7b57ec2b067be28beee Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 00:55:12 +0900 Subject: [PATCH 31/41] feat : add icons for `ToolButton` --- src/assets/icons/icon-checked.svg | 4 ++++ src/assets/icons/icon-unchecked.svg | 4 ++++ src/assets/icons/index.ts | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 src/assets/icons/icon-checked.svg create mode 100644 src/assets/icons/icon-unchecked.svg 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-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 fe0a843..bd52db4 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -13,6 +13,8 @@ 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, @@ -30,4 +32,6 @@ export { BellIcon, HelpIcon, SearchIcon, + CheckIcon, + UnCheckedIcon, }; From 3ca120ea53f5460d2b3d686c8ee0673efba195e1 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:00:09 +0900 Subject: [PATCH 32/41] fix : change baseurl --- src/shared/api/axios.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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( From 2a64dec50e2df285a75427f118b42f7b244984a6 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:00:52 +0900 Subject: [PATCH 33/41] feat : add endpoint and get recommended tools from server --- src/shared/api/request.ts | 3 ++- src/widgets/photo-upload/api/index.ts | 1 + src/widgets/photo-upload/api/photo.ts | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/widgets/photo-upload/api/index.ts create mode 100644 src/widgets/photo-upload/api/photo.ts 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/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 }); + }, + }); +}; From 8e4d878fde2da936f74843329004d9e7a4842d93 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:01:15 +0900 Subject: [PATCH 34/41] feat : add `Tool` type definition and export in types --- src/shared/types/index.ts | 1 + src/shared/types/tools.ts | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 src/shared/types/tools.ts diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 5c182c5..b7a5106 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -2,3 +2,4 @@ export * from './dock'; export * from './path'; export * from './reservation'; export * from './user'; +export * from './tools'; 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; +}; From 5899211b31e879128c870a674c6622f5326dfc81 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:01:45 +0900 Subject: [PATCH 35/41] feat : add `isPending` status at `useImageUpload` to get status --- src/shared/hooks/useImageUpload.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/hooks/useImageUpload.ts b/src/shared/hooks/useImageUpload.ts index d4c3f7f..c66c84c 100644 --- a/src/shared/hooks/useImageUpload.ts +++ b/src/shared/hooks/useImageUpload.ts @@ -3,7 +3,7 @@ import { useSubmitIdCard } from '@/widgets/join/api'; 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]; @@ -22,5 +22,5 @@ export default function useImageUpload(isPhotoUpload?: boolean) { setImage(''); } }; - return { image, handleImageInputChange, setImage }; + return { image, handleImageInputChange, setImage, isPending }; } From 0b52eddb75e1bd7201f83b068dd1646aa8152337 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:02:04 +0900 Subject: [PATCH 36/41] feat : implement `PhotoResultScreen` --- .../photo-result/ui/PhotoResultScreen.tsx | 19 +++++++++++++++++++ src/screen/photo-result/ui/index.ts | 1 + src/shared/constants/path.ts | 1 + 3 files changed, 21 insertions(+) create mode 100644 src/screen/photo-result/ui/PhotoResultScreen.tsx create mode 100644 src/screen/photo-result/ui/index.ts 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/shared/constants/path.ts b/src/shared/constants/path.ts index 9bf230a..bae17f3 100644 --- a/src/shared/constants/path.ts +++ b/src/shared/constants/path.ts @@ -4,5 +4,6 @@ export const PATH = { COMPLETE: 'CompleteScreen', PHOTO_UPLOAD: 'PhotoUploadScreen', PHOTO_LOADING: 'PhotoLoadingScreen', + PHOTO_RESULT: 'PhotoResultScreen', FORM: 'FormScreen', } as const; From 34eabb49f4811dc1ebd83b635734196976a0d370 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:06:40 +0900 Subject: [PATCH 37/41] feat : implement `PhotoResultContainer` and `ToolButton` show tools --- src/shared/ui/ToolButton.tsx | 40 +++++++++++++++ src/shared/ui/index.ts | 1 + .../photo-result/ui/PhotoResultContainer.tsx | 49 +++++++++++++++++++ src/widgets/photo-result/ui/index.ts | 1 + 4 files changed, 91 insertions(+) create mode 100644 src/shared/ui/ToolButton.tsx create mode 100644 src/widgets/photo-result/ui/PhotoResultContainer.tsx create mode 100644 src/widgets/photo-result/ui/index.ts 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 5010953..383cf2b 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -2,3 +2,4 @@ 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/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'; From 10297bd8fa74786b35eb4ff948a4210e5866e948 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:07:55 +0900 Subject: [PATCH 38/41] feat : add `bgImage` option at `NormalAppBar` and edit rendering option of `Dock` component --- src/shared/ui/AppBar.tsx | 3 ++- src/shared/ui/Dock.tsx | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/shared/ui/AppBar.tsx b/src/shared/ui/AppBar.tsx index 2ff47fb..27c8510 100644 --- a/src/shared/ui/AppBar.tsx +++ b/src/shared/ui/AppBar.tsx @@ -27,7 +27,8 @@ export const HomeAppBar = ( ), }); -export const NormalAppBar = (title: string) => ({ +export const NormalAppBar = (title: string, bgImage?: string) => ({ + backgroundImage: `url(${bgImage})`, renderLeft: () => { if (title) { return {title}; diff --git a/src/shared/ui/Dock.tsx b/src/shared/ui/Dock.tsx index 02f028f..581f2d2 100644 --- a/src/shared/ui/Dock.tsx +++ b/src/shared/ui/Dock.tsx @@ -23,10 +23,7 @@ export default function Dock(isLoading: DockProps) { .map(i => i.name) .pop() as PathItem; - const render = - current !== PATH.COMPLETE && - current !== PATH.PHOTO_UPLOAD && - current !== PATH.FORM; + const render = current === PATH.HOME; return ( <> From aad309e744529657ed4512963d8bc3dcf54c4bc2 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:08:28 +0900 Subject: [PATCH 39/41] feat : add `base64ToFile` utility function to convert base64 strings to File objects --- src/shared/utils/string.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 }); +}; From 2a023018fcdddac293ed8bdaa65c9648c6f30d7b Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:08:46 +0900 Subject: [PATCH 40/41] feat : add text indicating fetching status --- src/widgets/join/ui/JoinContainer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widgets/join/ui/JoinContainer.tsx b/src/widgets/join/ui/JoinContainer.tsx index a39d13d..1d0715e 100644 --- a/src/widgets/join/ui/JoinContainer.tsx +++ b/src/widgets/join/ui/JoinContainer.tsx @@ -2,7 +2,7 @@ import { CameraIcon, Logo } from '@/assets/icons'; import { useImageUpload } from '@/shared/hooks'; export default function JoinContainer() { - const { handleImageInputChange, image } = useImageUpload(); + const { handleImageInputChange, image, isPending } = useImageUpload(); return (
@@ -34,6 +34,7 @@ export default function JoinContainer() { )}
+ {isPending &&
사진 확인 완료! 가입중..
}
); From e29a7133bd89f90b70851813dba4350627019c42 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 29 Jun 2025 01:09:30 +0900 Subject: [PATCH 41/41] feat : add screen at stack and complete photo functionality --- src/app/stackflow/Stack.tsx | 8 +++++++- .../photo-loading/ui/PhotoLoadingScreen.tsx | 20 +++++++++++++++++-- .../photo-upload/ui/PhotoUploadContainer.tsx | 15 ++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/app/stackflow/Stack.tsx b/src/app/stackflow/Stack.tsx index cc34853..4e021a2 100644 --- a/src/app/stackflow/Stack.tsx +++ b/src/app/stackflow/Stack.tsx @@ -3,7 +3,9 @@ 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'; @@ -17,6 +19,7 @@ export const { Stack, useFlow } = stackflow({ FormScreen, CompleteScreen, PhotoLoadingScreen, + PhotoResultScreen, }, plugins: [ basicRendererPlugin(), @@ -24,5 +27,8 @@ export const { Stack, useFlow } = stackflow({ theme: 'cupertino', }), ], - initialActivity: () => 'JoinScreen', + initialActivity: () => { + if (fetchSessionData('userInfo')) return 'HomeScreen'; + return 'JoinScreen'; + }, }); diff --git a/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx b/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx index 36ee217..e801900 100644 --- a/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx +++ b/src/screen/photo-loading/ui/PhotoLoadingScreen.tsx @@ -1,8 +1,22 @@ 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 + }, []); -export default function PhotoUploadScreen() { return (
@@ -15,4 +29,6 @@ export default function PhotoUploadScreen() {
); -} +}; + +export default PhotoLoadingScreen; diff --git a/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx b/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx index 7056114..810de58 100644 --- a/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx +++ b/src/widgets/photo-upload/ui/PhotoUploadContainer.tsx @@ -1,9 +1,22 @@ 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 (
@@ -32,9 +45,11 @@ export default function PhotoUploadContainer() {