diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.md b/.github/ISSUE_TEMPLATE/BUG-REPORT.md new file mode 100644 index 00000000..6122685d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: Bug +assignees: '' +--- + + +## Describe the bug +A clear and concise description of what the bug is. + +## To Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## Expected behavior +A clear and concise description of what you expected to happen. + +## Screenshot +If applicable, add screenshots to help explain your problem. + +## Environment + +**Desktop:** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone:** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] diff --git a/.github/workflows/LINT_CHECK.yml b/.github/workflows/LINT_CHECK.yml index 68972c97..4199a5a9 100644 --- a/.github/workflows/LINT_CHECK.yml +++ b/.github/workflows/LINT_CHECK.yml @@ -26,3 +26,5 @@ jobs: run: yarn - name: Check lint run: yarn lint + - name: Type Check + run: yarn typecheck \ No newline at end of file diff --git a/package.json b/package.json index b704d2b3..e638765a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "eject": "react-scripts eject", "lint": "yarn lint:eslint && yarn lint:stylelint", "lint:eslint": "eslint src/ --ext .tsx,.ts", - "lint:stylelint": "stylelint \"src/**/*.scss\" --config .stylelint.json" + "lint:stylelint": "stylelint \"src/**/*.scss\" --config .stylelint.json", + "typecheck": "tsc --project tsconfig.json" }, "eslintConfig": { "extends": [ diff --git a/src/api/shop/index.ts b/src/api/shop/index.ts index 23629969..f75656f4 100644 --- a/src/api/shop/index.ts +++ b/src/api/shop/index.ts @@ -3,6 +3,7 @@ import { MenuInfoRes } from 'model/shopInfo/menuCategory'; import { ShopListRes } from 'model/shopInfo/allShopInfo'; import { accessClient, client } from 'api'; import { OwnerShop } from 'model/shopInfo/ownerShop'; +import { NewMenu } from 'model/shopInfo/newMenu'; export const getMyShopList = async () => { const { data } = await accessClient.get('/owner/shops'); @@ -24,4 +25,6 @@ export const getShopList = async () => { return ShopListRes.parse(data); }; +export const addMenu = (shopId:number, param: NewMenu) => accessClient.post(`/owner/shops/${shopId}/menus`, param); + export const postShop = (data: OwnerShop) => accessClient.post('/owner/shops', data); diff --git a/src/api/uploadFile/Uploadfile.ts b/src/api/uploadFile/Uploadfile.ts new file mode 100644 index 00000000..0f757e0f --- /dev/null +++ b/src/api/uploadFile/Uploadfile.ts @@ -0,0 +1,9 @@ +import { accessClient } from 'api'; + +export const uploadFile = (formData: FormData) => accessClient.post('/OWNERS/upload/file', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, +}); + +export const uploadFiles = (formData: FormData) => accessClient.post('/OWNERS/upload/files', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, +}); diff --git a/src/assets/svg/addmenu/mobile-delete-new-image.svg b/src/assets/svg/addmenu/mobile-delete-new-image.svg new file mode 100644 index 00000000..f9943cc1 --- /dev/null +++ b/src/assets/svg/addmenu/mobile-delete-new-image.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/svg/addmenu/mobile-single-menu-check-selected.svg b/src/assets/svg/addmenu/mobile-single-menu-check-selected.svg new file mode 100644 index 00000000..c6e0e5b2 --- /dev/null +++ b/src/assets/svg/addmenu/mobile-single-menu-check-selected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/addmenu/mobile-single-menu-check.svg b/src/assets/svg/addmenu/mobile-single-menu-check.svg new file mode 100644 index 00000000..6dcbc03c --- /dev/null +++ b/src/assets/svg/addmenu/mobile-single-menu-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/addmenu/selected-check.svg b/src/assets/svg/addmenu/selected-check.svg new file mode 100644 index 00000000..d021fc02 --- /dev/null +++ b/src/assets/svg/addmenu/selected-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/addmenu/single-menu-check.svg b/src/assets/svg/addmenu/single-menu-check.svg new file mode 100644 index 00000000..ce49d8d0 --- /dev/null +++ b/src/assets/svg/addmenu/single-menu-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/model/File/index.ts b/src/model/File/index.ts new file mode 100644 index 00000000..91d5bc34 --- /dev/null +++ b/src/model/File/index.ts @@ -0,0 +1,13 @@ +import z from 'zod'; + +export const File = z.object({ + fileURL: z.string(), +}); + +export type File = z.infer; + +export const Files = z.object({ + fileURLs: z.array(File), +}); + +export type Files = z.infer; diff --git a/src/model/shopInfo/newMenu.ts b/src/model/shopInfo/newMenu.ts new file mode 100644 index 00000000..ab51af65 --- /dev/null +++ b/src/model/shopInfo/newMenu.ts @@ -0,0 +1,19 @@ +import z from 'zod'; + +export const OptionPrice = z.object({ + option: z.string(), + price: z.number(), +}); +export type OptionPrice = z.infer; + +export const NewMenu = z.object({ + category_ids: z.array(z.number()), + description: z.string().nullable(), + image_urls: z.array(z.string()), + is_single: z.boolean(), + name: z.string(), + option_prices: z.array(OptionPrice.nullable()), + single_price: z.number(), +}); + +export type NewMenu = z.infer; diff --git a/src/page/AddMenu/components/AddMenuImgModal/index.tsx b/src/page/AddMenu/components/AddMenuImgModal/index.tsx index 6ead02a9..2c66457c 100644 --- a/src/page/AddMenu/components/AddMenuImgModal/index.tsx +++ b/src/page/AddMenu/components/AddMenuImgModal/index.tsx @@ -1,53 +1,39 @@ +import React from 'react'; import { createPortal } from 'react-dom'; -import { ReactComponent as CancleIcon } from 'assets/svg/addmenu/mobile-cancle-icon.svg'; +import { ReactComponent as CancelIcon } from 'assets/svg/addmenu/mobile-cancle-icon.svg'; +import useMenuImageUpload from 'page/AddMenu/hook/useMenuImageUpload'; import styles from './AddMenuImgModal.module.scss'; interface AddMenuImgModalProps { isOpen: boolean; - onCancel: (event: React.MouseEvent | React.KeyboardEvent) => void; + closeModal: (event?: React.MouseEvent | React.KeyboardEvent) => void; } -export default function AddMenuImgModal({ isOpen, onCancel }: AddMenuImgModalProps) { +export default function AddMenuImgModal({ isOpen, closeModal }: AddMenuImgModalProps) { + const { imgRef, saveImgFile } = useMenuImageUpload(closeModal); + + const handleFileChange = () => { + saveImgFile(); + }; + + const triggerFileInput = () => { + imgRef.current?.click(); + }; + if (!isOpen) return null; return createPortal( -
-
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - role="presentation" - > - 이미지 추가 - 메뉴 사진을 추가 할 수 있습니다. + 메뉴 사진을 추가할 수 있습니다.
- - + + +
, diff --git a/src/page/AddMenu/components/GoMyShop/index.tsx b/src/page/AddMenu/components/GoMyShop/index.tsx index 1a85d98d..48287f91 100644 --- a/src/page/AddMenu/components/GoMyShop/index.tsx +++ b/src/page/AddMenu/components/GoMyShop/index.tsx @@ -5,9 +5,10 @@ import styles from './GoMyShopModal.module.scss'; interface CheckModalProps { isOpen: boolean; onCancel: (event: React.MouseEvent | React.KeyboardEvent) => void; + onConfirm: () => void; } -export default function GoMyShopModal({ isOpen, onCancel }: CheckModalProps) { +export default function GoMyShopModal({ isOpen, onCancel, onConfirm }: CheckModalProps) { if (!isOpen) return null; return createPortal(
내 상점으로 diff --git a/src/page/AddMenu/components/MenuCategory/MenuCategory.module.scss b/src/page/AddMenu/components/MenuCategory/MenuCategory.module.scss index 4ea0cd3a..05f8e14b 100644 --- a/src/page/AddMenu/components/MenuCategory/MenuCategory.module.scss +++ b/src/page/AddMenu/components/MenuCategory/MenuCategory.module.scss @@ -63,6 +63,18 @@ $button-text-color: #252525; } } +.category-text-container { + margin-top: 12px; + display: flex; + align-items: center; + gap: 15px; +} + +.category-text { + font-size: 20px; + font-weight: 500; +} + .mobile { &__container { width: 100%; @@ -120,4 +132,11 @@ $button-text-color: #252525; cursor: pointer; } } + + &__category-text-container { + margin-top: 12px; + display: flex; + align-items: center; + gap: 10px; + } } diff --git a/src/page/AddMenu/components/MenuCategory/index.tsx b/src/page/AddMenu/components/MenuCategory/index.tsx index 879cc48b..54fc93da 100644 --- a/src/page/AddMenu/components/MenuCategory/index.tsx +++ b/src/page/AddMenu/components/MenuCategory/index.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; import { ReactComponent as GearIcon } from 'assets/svg/mystore/gear.svg'; import useMediaQuery from 'utils/hooks/useMediaQuery'; +import useAddMenuStore from 'store/addMenu'; +import useMyShop from 'query/shop'; import cn from 'utils/ts/className'; import styles from './MenuCategory.module.scss'; @@ -8,84 +10,130 @@ interface MenuCategory { id: number; name: string; } +interface MenuCategoryProps { + isComplete: boolean; +} -export default function MenuCategory() { +export default function MenuCategory({ isComplete }:MenuCategoryProps) { const { isMobile } = useMediaQuery(); - const [menuCategories/* , setMenuCategories */] = useState([ - { id: 1, name: '이벤트 메뉴' }, - { id: 2, name: '대표 메뉴' }, - { id: 3, name: '사이드 메뉴' }, - { id: 4, name: '세트 메뉴' }, - ]); + const { shopData } = useMyShop(); + const { setCategoryIds } = useAddMenuStore(); const [selectedCategories, setSelectedCategories] = useState([]); const appendSelectCategory = (category: MenuCategory) => { - if (selectedCategories.some((alreadySelected) => alreadySelected.id === category.id)) { - setSelectedCategories( - selectedCategories.filter((alreadySelected) => alreadySelected.id !== category.id), - ); - } else { - setSelectedCategories([...selectedCategories, category]); - } + const newSelected = selectedCategories.some((c) => c.id === category.id) + ? selectedCategories.filter((c) => c.id !== category.id) + : [...selectedCategories, category]; + setSelectedCategories(newSelected); + setCategoryIds(newSelected.map((cat) => cat.id)); }; + return (
{isMobile ? (
-
-
-
- 메뉴 카테고리 + {isComplete ? ( + <> +
+
+
+ 메뉴 카테고리 +
+
+
+
+ {selectedCategories.map((category, index) => ( + +
+ {category.name} +
+ {index < selectedCategories.length - 1 && /} +
+ ))}
-
- (최대 선택 n개) + + ) : ( + <> +
+
+
+ 메뉴 카테고리 +
+
+ (최대 선택 n개) +
+
+
-
- -
-
- {menuCategories.map((category) => ( - - ))} -
+
+ {shopData && shopData.menu_categories.map((category) => ( + + ))} +
+ + )}
) : (
-
-
-
- 메뉴 카테고리 + {isComplete ? ( + <> +
+
+
+ 메뉴 카테고리 +
+
+
+
+ {selectedCategories.map((category, index) => ( + +
+ {category.name} +
+ {index < selectedCategories.length - 1 && /} +
+ ))} +
+ + ) : ( + <> +
+
+
+ 메뉴 카테고리 +
+
+ (최대 선택 n개) +
+
+
-
- (최대 선택 n개) +
+ {shopData && shopData.menu_categories.map((category) => ( + + ))}
-
- -
-
- {menuCategories.map((category) => ( - - ))} -
+ + )}
)}
diff --git a/src/page/AddMenu/components/MenuDetail/MenuDetail.module.scss b/src/page/AddMenu/components/MenuDetail/MenuDetail.module.scss index 25842ddd..69e52618 100644 --- a/src/page/AddMenu/components/MenuDetail/MenuDetail.module.scss +++ b/src/page/AddMenu/components/MenuDetail/MenuDetail.module.scss @@ -17,14 +17,20 @@ height: 153px; flex-shrink: 0; padding: 16px; - border: 0.5px solid var(--stroke, #858585); - color: var(--text-color, #252525); + border: 0.5px solid #858585; + color: #252525; font-size: 20px; font-weight: 500; resize: none; + &--complete { + margin-top: 24px; + font-size: 20px; + font-weight: 500; + } + &::placeholder { - color: var(--stroke, #858585); + color: #858585; font-size: 20px; font-weight: 400; } @@ -52,6 +58,12 @@ resize: none; box-sizing: border-box; + &--complete { + margin-top: 8px; + font-size: 15px; + font-weight: 400; + } + &::placeholder { color: #a1a1a1; font-size: 13px; diff --git a/src/page/AddMenu/components/MenuDetail/index.tsx b/src/page/AddMenu/components/MenuDetail/index.tsx index ccf778ee..305b9cd1 100644 --- a/src/page/AddMenu/components/MenuDetail/index.tsx +++ b/src/page/AddMenu/components/MenuDetail/index.tsx @@ -1,21 +1,45 @@ import useMediaQuery from 'utils/hooks/useMediaQuery'; - +import useAddMenuStore from 'store/addMenu'; import styles from './MenuDetail.module.scss'; -export default function MenuDetail() { +interface MenuCategoryProps { + isComplete:boolean; +} +export default function MenuDetail({ isComplete }:MenuCategoryProps) { const { isMobile } = useMediaQuery(); - + const { description, setDescription } = useAddMenuStore(); + const handleDetailChange = (e: React.ChangeEvent) => { + setDescription(e.target.value); + }; return (
{isMobile ? (
메뉴구성
-