diff --git a/.env b/.env index e8fb2ca..d74c18e 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ -REACT_APP_API_URL=https://e62e9111-4ca7-4984-93da-a6b562b71a83.mock.pstmn.io -REACT_APP_INICIS=imp31336811 \ No newline at end of file +# REACT_APP_API_URL=https://e62e9111-4ca7-4984-93da-a6b562b71a83.mock.pstmn.io +REACT_APP_INICIS=imp31336811 +REACT_APP_API_URL=https://jeontongju-dev.shop \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 91e2485..195043e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "off", { "functions": false, "classes": false } ], + "react/no-array-index-key": "off", "no-console": "off", "no-unused-vars": "off", "global-require": 0, diff --git a/.vscode/settings.json b/.vscode/settings.json index 692abfb..f84cf3e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,6 @@ "editor.formatOnPaste": false, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } } diff --git a/package.json b/package.json index 1b84149..1ff61e5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@types/react-router-dom": "^5.3.3", "antd": "^5.11.0", "axios": "^1.5.0", + "event-source-polyfill": "^1.0.31", "moment": "^2.29.4", "react": "^18.2.0", "react-daum-postcode": "^3.1.3", diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 9cd0dac..0000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/apis/notification/notificationAPIService.ts b/src/apis/notification/notificationAPIService.ts new file mode 100644 index 0000000..8f2c78e --- /dev/null +++ b/src/apis/notification/notificationAPIService.ts @@ -0,0 +1,24 @@ +import APIService from '../../libs/core/api/APIService'; +import { ReadAllNotiResponse, ReadNotiByNotiIdResponse } from './notificationAPIService.types'; + +const BASE_URL = `${process.env.REACT_APP_API_URL}/order-service/api`; + +class NotificationAPIService extends APIService { + constructor() { + super(); + this.setBaseUrl(BASE_URL); + } + + async readAllNoti() { + const { data } = await this.patch(`/notification-service/api/notifications`); + return data; + } + + async readNotiByNotiId(notificationId: number) { + const { data } = await this.patch( + `/notification-service/api/consumers/notifications/${notificationId}`, + ); + return data; + } +} +export const notiApi: NotificationAPIService = NotificationAPIService.shared(); diff --git a/src/apis/notification/notificationAPIService.types.ts b/src/apis/notification/notificationAPIService.types.ts new file mode 100644 index 0000000..83961b7 --- /dev/null +++ b/src/apis/notification/notificationAPIService.types.ts @@ -0,0 +1,11 @@ +interface ApiResponse { + code: number; + message: string; + detail?: string; + data: T; + failure?: string; +} + +export type ReadAllNotiResponse = ApiResponse; + +export type ReadNotiByNotiIdResponse = ApiResponse; diff --git a/src/apis/product/productAPIService.types.ts b/src/apis/product/productAPIService.types.ts index 4c7759e..8dd5f09 100644 --- a/src/apis/product/productAPIService.types.ts +++ b/src/apis/product/productAPIService.types.ts @@ -2,6 +2,7 @@ import { CONCEPT } from '../../constants/ProductType/ConceptType'; import { RAW_MATERIAL } from '../../constants/ProductType/MaterialType'; import { SNACK } from '../../constants/ProductType/SnackType'; import { categoryType } from '../../stores/MyInfo/MyInfoStore.types'; +import { Page } from '../search/searchAPIService.typs'; interface ApiResponse { code: number; @@ -51,6 +52,7 @@ export interface RegisterShortParams { shortsTitle: string; shortsDescription: string; shortsVideoUrl: string; + shortsPreviewUrl: string; shortsThumbnailImageUrl: string; productId?: string; // 보내면 상품에 등록, 안 보내면 주모 사이트로 연결 isActivate: boolean; @@ -88,7 +90,7 @@ export type DeleteProductResponse = ApiResponse; export type RegisterShortResponse = ApiResponse; -export type GetShortListResponse = ApiResponse; +export type GetShortListResponse = ApiResponse>; export type UpdateShortsResponse = ApiResponse; diff --git a/src/apis/storage/storageAPIService.ts b/src/apis/storage/storageAPIService.ts index aa3ccfb..50f7f29 100644 --- a/src/apis/storage/storageAPIService.ts +++ b/src/apis/storage/storageAPIService.ts @@ -1,5 +1,6 @@ +import { AxiosRequestConfig } from 'axios'; import APIService from '../../libs/core/api/APIService'; -import { UploadS3Response } from './storageAPIService.types'; +import { UploadS3Response, UploadShortsResponse } from './storageAPIService.types'; const BASE_URL = `${process.env.REACT_APP_API_URL}/storage-service/api`; @@ -13,6 +14,11 @@ class StorageAPIService extends APIService { const { data } = await this.post(`/file/${fileName}`); return data; } + + async uploadShorts(formData: FormData, config: AxiosRequestConfig) { + const { data } = await this.post(`/upload/shorts`, formData, config); + return data; + } } export const storageApi: StorageAPIService = StorageAPIService.shared(); diff --git a/src/apis/storage/storageAPIService.types.ts b/src/apis/storage/storageAPIService.types.ts index 5675946..9eea0a1 100644 --- a/src/apis/storage/storageAPIService.types.ts +++ b/src/apis/storage/storageAPIService.types.ts @@ -12,4 +12,10 @@ export interface UploadS3ResponseData { dataUrl: string; } +export interface UploadShortsResponseData { + dataUrl: string; + previewUrl: string; +} + export type UploadS3Response = ApiResponse; +export type UploadShortsResponse = ApiResponse; diff --git a/src/assets/images/fi-sr-bell.svg b/src/assets/images/fi-sr-bell.svg new file mode 100644 index 0000000..86589d5 --- /dev/null +++ b/src/assets/images/fi-sr-bell.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/fi-sr-new-bell.svg b/src/assets/images/fi-sr-new-bell.svg new file mode 100644 index 0000000..3e00380 --- /dev/null +++ b/src/assets/images/fi-sr-new-bell.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/components/Live/LiveContainer.hooks.tsx b/src/components/Live/LiveContainer.hooks.tsx index ff35954..ee02f71 100644 --- a/src/components/Live/LiveContainer.hooks.tsx +++ b/src/components/Live/LiveContainer.hooks.tsx @@ -10,14 +10,11 @@ export const useLiveModal = () => { }; const handleCancel = () => { - console.log('here'); setIsModalOpen(false); - console.log(isModalOpen); }; const handleOk = () => { navigate('/etc/live/register'); setIsModalOpen(false); - console.log(isModalOpen); }; return { isModalOpen, diff --git a/src/components/common/ImageUploader.tsx b/src/components/common/ImageUploader.tsx index ca1df58..97d3b17 100644 --- a/src/components/common/ImageUploader.tsx +++ b/src/components/common/ImageUploader.tsx @@ -58,7 +58,7 @@ const ImageUploader: React.FC = ({ imageUrl, setImageUrl onChange={handleChangeFile} style={{ display: 'none' }} /> - + ); }; @@ -66,6 +66,7 @@ const ImageUploader: React.FC = ({ imageUrl, setImageUrl export default ImageUploader; const StyledButton = styled.button` - width: 10rem; + width: 100%; height: 10rem; + background: none; `; diff --git a/src/components/common/Notification.tsx b/src/components/common/Notification.tsx new file mode 100644 index 0000000..b3538ef --- /dev/null +++ b/src/components/common/Notification.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react'; +import { EventSourcePolyfill, NativeEventSource } from 'event-source-polyfill'; +import styled from '@emotion/styled'; +import FiSrBellSVG from '../../assets/images/fi-sr-bell.svg'; +import NewFiSrBellSVG from '../../assets/images/fi-sr-new-bell.svg'; +import { Toast } from './Toast'; +import { notiApi } from '../../apis/notification/notificationAPIService'; + +const Notification = () => { + const [newNoti, setNewNoti] = useState([]); + const [notiOpen, setNotiOpen] = useState(false); + + useEffect(() => { + if (typeof window !== 'undefined') { + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) return; + + const EventSource = EventSourcePolyfill || NativeEventSource; + const eventSource = new EventSource( + 'https://jeontongju-dev.shop/notification-service/api/notifications/connect', + { + headers: { + Authorization: `Bearer ${accessToken}`, + Connection: 'keep-alive', + Accept: 'text/event-stream', + }, + heartbeatTimeout: 86400000, + withCredentials: true, + }, + ); + + eventSource.onopen = () => { + console.log('OPEN'); + + eventSource.removeEventListener('connect', () => { + console.log('remove connect'); + }); + eventSource.removeEventListener('happy', () => { + console.log('remove happy'); + }); + + eventSource.addEventListener('happy', (event: any) => { + console.log(event); + const currNoti = event.data; + console.log('HI'); + setNewNoti((prev) => [...prev, currNoti]); + }); + + eventSource.addEventListener('connect', (event: any) => { + console.log(event); + const { data: receivedConnectData } = event; + if (receivedConnectData === 'SSE 연결이 완료되었습니다.') { + console.log('SSE CONNECTED'); + } else { + console.log(event); + } + }); + }; + + /* eslint-disable consistent-return */ + return () => { + eventSource.close(); + console.log('SSE CLOSED'); + }; + } + }, []); + + const handleOpenNoti = () => { + setNotiOpen((prev: boolean) => !prev); + }; + + const handleAllRead = async () => { + try { + const data = await notiApi.readAllNoti(); + if (data.code === 200) { + Toast(true, '전체 읽음 처리에 성공했어요.'); + setNewNoti([]); + } + } catch (error) { + Toast(false, '전체 읽음 처리에 실패했어요'); + } + }; + + const handleReadByNotiId = async () => { + try { + const data = await notiApi.readNotiByNotiId(1); + if (data.code === 200) { + Toast(true, '전체 읽음 처리에 성공했어요.'); + setNewNoti([]); + } + } catch (error) { + Toast(false, '전체 읽음 처리에 실패했어요'); + } + }; + + return ( +
+ bell 0 ? NewFiSrBellSVG : FiSrBellSVG} + style={{ + cursor: 'pointer', + width: '2rem', + height: '2rem', + position: 'relative', + }} + onClick={handleOpenNoti} + role="presentation" + /> + + {notiOpen && + (newNoti.length === 0 ? ( + 알람 확인 완료 끝! + ) : ( + + + 전체 읽음 + + {newNoti.map((it, i: number) => ( + handleReadByNotiId()}> + {it} + + ))} + + ))} +
+ ); +}; + +export default Notification; + +const StyledAlarmBox = styled.div` + border: 1px solid #ccc; + padding: 1rem 2rem; + border-radius: 12px; + position: absolute; + z-index: 100; + background-color: white; + max-height: 10rem; + overflow-y: scroll; +`; + +const StyledAlarmDiv = styled.div` + padding: 0.5rem 0; +`; + +const StyledReadButton = styled.div` + text-align: right; + margin-bottom: 0.5rem; + cursor: pointer; +`; diff --git a/src/components/common/Table.tsx b/src/components/common/Table.tsx index 0cbb1ad..28452d2 100644 --- a/src/components/common/Table.tsx +++ b/src/components/common/Table.tsx @@ -1,15 +1,16 @@ import React from 'react'; import { Table as AntdTable } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; +import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; interface TableInterface { columns: ColumnsType; data: T[]; + pagination?: TablePaginationConfig | false; } -const Table = ({ data, columns }: TableInterface) => ( +const Table = ({ data, columns, pagination }: TableInterface) => (
- } dataSource={data} /> + } dataSource={data} pagination={pagination} />
); diff --git a/src/components/common/TopHeader.tsx b/src/components/common/TopHeader.tsx index 5ab9dd8..f51a98e 100644 --- a/src/components/common/TopHeader.tsx +++ b/src/components/common/TopHeader.tsx @@ -1,9 +1,9 @@ import { LogoutOutlined, SettingOutlined } from '@ant-design/icons'; -import { LiaWineBottleSolid } from 'react-icons/lia'; import { Avatar, Dropdown } from 'antd'; import { useNavigate } from 'react-router-dom'; import styled from '@emotion/styled'; import { useMyInfoStore } from '../../stores/MyInfo/MyInfoStore'; +import Notification from './Notification'; const TopHeader = () => { const navigate = useNavigate(); @@ -17,6 +17,7 @@ const TopHeader = () => { return ( +
{storeName} 주모님
{ key: 'setMyShopInfo', label: ( ), }, diff --git a/src/components/common/VideoUploader.tsx b/src/components/common/VideoUploader.tsx new file mode 100644 index 0000000..5242c50 --- /dev/null +++ b/src/components/common/VideoUploader.tsx @@ -0,0 +1,73 @@ +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import { Avatar, message, Upload } from 'antd'; +import styled from '@emotion/styled'; +import { AxiosRequestConfig } from 'axios'; +import { Toast } from './Toast'; +import { UploadShortsResponse, UploadShortsResponseData } from '../../apis/storage/storageAPIService.types'; + +interface VideoUploaderInterface { + videoUrl: string; + setVideoUrl: React.Dispatch>; + previewUrl: string; + setPreviewUrl: React.Dispatch>; +} + +const VideoUploader: React.FC = ({ videoUrl, setVideoUrl, previewUrl, setPreviewUrl }) => { + const inputRef = useRef(null); + + const uploadImgBtn = useCallback(() => { + inputRef.current?.click(); + }, []); + + const handleChangeFile = async (event: any) => { + event.preventDefault(); + const formData = new FormData(); + formData.append('shorts', event.target.files[0]); + const blob = new Blob([JSON.stringify(event.target.files[0])], { + type: 'application/json', + }); + formData.append('shorts', blob); + const reader = new FileReader(); + reader.readAsArrayBuffer(event.target.files[0]); + try { + console.log('HERE!'); + fetch('https://jeontongju-dev.shop/storage-service/api/upload/shorts', { + method: 'POST', + body: formData, + }) + .then((res) => { + return res.text(); + }) + .then((res) => { + setPreviewUrl(JSON.parse(res).data.previewUrl); + setVideoUrl(JSON.parse(res).data.dataUrl); + }) + .catch((err) => { + console.log(err); + }); + } catch (error) { + Toast(false, '동영상 업로드에 실패했어요'); + } + }; + + return ( + + + + ); +}; + +export default VideoUploader; + +const StyledButton = styled.button` + width: 100%; + height: 10rem; + background: none; +`; diff --git a/src/constants/SignUpFieldType.ts b/src/constants/SignUpFieldType.ts index a5891b2..65ae119 100644 --- a/src/constants/SignUpFieldType.ts +++ b/src/constants/SignUpFieldType.ts @@ -6,5 +6,6 @@ export type SignUpFieldType = { storeName: string; storeDescription: string; storePhoneNumber: string; + storeImageUrl: string; tel?: string; }; diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 5ab7ff9..f484238 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -47,8 +47,6 @@ const MainLayout = () => { const categoryData = await productApi.getCategory(); if (data.code === 200) { setCategory(categoryData.data); - } else { - console.error('category 조회 오류'); } } catch (error) { console.error(error); diff --git a/src/libs/core/api/APIService.ts b/src/libs/core/api/APIService.ts index 632f596..e24e1ac 100644 --- a/src/libs/core/api/APIService.ts +++ b/src/libs/core/api/APIService.ts @@ -131,6 +131,9 @@ axios.interceptors.response.use( } catch (err) { console.error(err); } + } else if (status === 401) { + localStorage.removeItem('accessToken'); + console.log('로그아웃'); } return config; }, diff --git a/src/libs/core/utils/index.ts b/src/libs/core/utils/index.ts index e512c93..3f540e1 100644 --- a/src/libs/core/utils/index.ts +++ b/src/libs/core/utils/index.ts @@ -5,7 +5,7 @@ const BASE_URL = process.env.REACT_APP_API_URL; const axiosApi = (baseURL: any) => { const instance = axios.create({ baseURL, - withCredentials: false, + withCredentials: true, headers: { 'Access-Control-Allow-Origin': `${process.env.REACT_APP_API_URL}`, }, diff --git a/src/pages/Cash/OrderList/OrderList.tsx b/src/pages/Cash/OrderList/OrderList.tsx index e19a6b8..88abedd 100644 --- a/src/pages/Cash/OrderList/OrderList.tsx +++ b/src/pages/Cash/OrderList/OrderList.tsx @@ -1,6 +1,7 @@ import { DatePicker, DatePickerProps, Form, Input, Select, Tooltip } from 'antd'; import styled from '@emotion/styled'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { useEffect } from 'react'; import Table from '../../../components/common/Table'; import { useOrderList } from './OrderList.hooks'; import { useMyInfoStore } from '../../../stores/MyInfo/MyInfoStore'; @@ -11,15 +12,20 @@ const OrderList = () => { const { data: orderData } = useGetMyOrderListQuery(); const { columns } = useOrderList(); const products = useMyInfoStore((state) => state.products); - const [setPage, isDeliveryCodeNull, setIsDeliveryCodeNull, setProductId, setSelectedDate] = useMyOrderListStore( - (state) => [ + const [page, setPage, isDeliveryCodeNull, setIsDeliveryCodeNull, setProductId, selectedDate, setSelectedDate] = + useMyOrderListStore((state) => [ + state.page, state.dispatchPage, state.isDeliveryCodeNull, state.dispatchIsDeliveryCodeNull, state.dispatchProductId, + state.selectedDate, state.dispatchSelectedDate, - ], - ); + ]); + + useEffect(() => { + setPage(1); + }, [selectedDate]); const onChange: DatePickerProps['onChange'] = (date, dateString) => { setSelectedDate(dateString.replaceAll('-', '')); @@ -48,7 +54,21 @@ const OrderList = () => { - {orderData ? :
로딩중
} + {orderData ? ( +
+ ) : ( +
로딩중
+ )} ); }; diff --git a/src/pages/Etc/Shorts/ShortsList/ShortsList.tsx b/src/pages/Etc/Shorts/ShortsList/ShortsList.tsx index 83f6c1a..5acc49b 100644 --- a/src/pages/Etc/Shorts/ShortsList/ShortsList.tsx +++ b/src/pages/Etc/Shorts/ShortsList/ShortsList.tsx @@ -16,7 +16,7 @@ const ShortsList = () => {
+ {productListData && ( +
+ )} { const emailRegex = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i; const navigate = useNavigate(); - // email 인증 눌렀을 때 중복이 아닌 경우에만 const [isValidEmail, setIsValidEmail] = useState(false); const [isValidEmailCode, setIsValidEmailCode] = useState(false); const [authCode, setAuthCode] = useState(''); @@ -81,7 +80,8 @@ export const useSignUp = () => { const checkRegisterDisabled = () => { if (password !== checkPassword) return 'disabled'; - if (!email || !password || !storeName || !storeDescription || !storePhoneNumber || !impUid) return 'disabled'; + if (!email || !password || !storeName || !storeDescription || !storePhoneNumber || !impUid || !storeImageUrl) + return 'disabled'; return 'positive'; }; @@ -152,5 +152,7 @@ export const useSignUp = () => { handleAdultValid, checkRegisterDisabled, isAbleToSendEmail, + storeImageUrl, + setStoreImageUrl, }; }; diff --git a/src/pages/SignUp/SignUp.tsx b/src/pages/SignUp/SignUp.tsx index 9c2eb1c..7d585eb 100644 --- a/src/pages/SignUp/SignUp.tsx +++ b/src/pages/SignUp/SignUp.tsx @@ -4,6 +4,7 @@ import { useSignUp } from './SignUp.hooks'; import { SignUpFieldType } from '../../constants/SignUpFieldType'; import Button from '../../components/common/Button'; import AdultValid from '../../assets/images/adultValid.png'; +import ImageUploader from '../../components/common/ImageUploader'; const SignUp = () => { const { @@ -29,6 +30,8 @@ const SignUp = () => { handleAdultValid, checkRegisterDisabled, isAbleToSendEmail, + storeImageUrl, + setStoreImageUrl, } = useSignUp(); return ( @@ -36,7 +39,7 @@ const SignUp = () => { name="basic" labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} - style={{ maxWidth: 600, width: '100%' }} + style={{ maxWidth: 600, width: '100%', display: 'flex', flexDirection: 'column' }} onFinish={onFinish} autoComplete="off" > @@ -162,6 +165,13 @@ const SignUp = () => { onChange={(e) => setStorePhoneNumber(e.target.value)} /> + + label="주모 대표 이미지" + name="storeImageUrl" + rules={[{ required: true, message: '주모 이미지를 입력해주세요.' }]} + > + +