diff --git a/package-lock.json b/package-lock.json
index be1e63d..d0db919 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"next": "14.2.32",
"react": "^18",
"react-dom": "^18",
+ "react-responsive": "^10.0.1",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
@@ -5782,6 +5783,12 @@
}
}
},
+ "node_modules/css-mediaquery": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+ "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==",
+ "license": "BSD"
+ },
"node_modules/css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
@@ -8006,6 +8013,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/hyphenate-style-name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
+ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/icss-utils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
@@ -9064,6 +9077,15 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/matchmediaquery": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz",
+ "integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-mediaquery": "^0.1.2"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -9478,7 +9500,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -10562,7 +10583,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -10759,7 +10779,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/react-refresh": {
@@ -10772,6 +10791,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-responsive": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.1.tgz",
+ "integrity": "sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==",
+ "license": "MIT",
+ "dependencies": {
+ "hyphenate-style-name": "^1.0.0",
+ "matchmediaquery": "^0.4.2",
+ "prop-types": "^15.6.1",
+ "shallow-equal": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -11513,6 +11550,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/shallow-equal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz",
+ "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/package.json b/package.json
index 8aa54ab..face2b9 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"next": "14.2.32",
"react": "^18",
"react-dom": "^18",
+ "react-responsive": "^10.0.1",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
diff --git a/src/api/employer.ts b/src/api/employer.ts
new file mode 100644
index 0000000..954fdc9
--- /dev/null
+++ b/src/api/employer.ts
@@ -0,0 +1,50 @@
+import axios from '@/lib/axios';
+import RegisterFormData from '@/types/myShop';
+import { default as originAxios } from 'axios';
+
+export async function postShop(body: RegisterFormData) {
+ const { address1, address2, category, description, name, originalHourlyPay, image } = body;
+
+ const imageUrl = image
+ ? `https://bootcamp-project-api.s3.ap-northeast-2.amazonaws.com/${image.name}`
+ : '';
+
+ const tmpBody = {
+ address1,
+ address2,
+ category,
+ description,
+ name,
+ originalHourlyPay,
+ imageUrl,
+ };
+ const { data } = await axios.post('/shops', tmpBody);
+}
+
+export async function postPresignedUrl(imageName: string) {
+ const { data } = await axios.post('/images', { name: imageName });
+ console.log(data);
+ return data.item.url;
+}
+
+export async function uploadImage(presignedUrl: string, file: File) {
+ const result = await originAxios.put(presignedUrl, file);
+}
+
+export async function getPresignedUrl(presignedUrl: string) {
+ // 1. URL 객체 생성
+ const url = new URL(presignedUrl);
+
+ // 2. 쿼리 파라미터를 제거 (URL 객체의 search 속성을 비움)
+ url.search = '';
+
+ // 3. 쿼리 파라미터가 제거된 새 URL 문자열을 얻습니다.
+ const baseUrl = url.toString();
+
+ const result = await originAxios.get(baseUrl);
+}
+
+export async function getShop(shopId: string) {
+ const { data } = await axios.get(`/shops/${shopId}`);
+ return data;
+}
diff --git a/src/components/features/my-shop/registerAddress.tsx b/src/components/features/my-shop/registerAddress.tsx
new file mode 100644
index 0000000..0bc7cbb
--- /dev/null
+++ b/src/components/features/my-shop/registerAddress.tsx
@@ -0,0 +1,43 @@
+import { Dropdown, Input } from '@/components/ui';
+import { ADDRESS_CODE } from '@/constants/dropdown';
+import RegisterFormData from '@/types/myShop';
+
+interface Props {
+ formData: RegisterFormData;
+ handleChange: (key: keyof RegisterFormData, value: string) => void;
+}
+
+const RegisterAddress = ({ formData, handleChange }: Props) => {
+ return (
+ <>
+
+
+
+ 주소
+ *
+
+
handleChange('address1', val)}
+ className='w-full'
+ />
+
+
+ handleChange('address2', e.target.value)}
+ placeholder='입력'
+ />
+
+
+ >
+ );
+};
+
+export default RegisterAddress;
diff --git a/src/components/features/my-shop/registerDescription.tsx b/src/components/features/my-shop/registerDescription.tsx
new file mode 100644
index 0000000..4e820a0
--- /dev/null
+++ b/src/components/features/my-shop/registerDescription.tsx
@@ -0,0 +1,24 @@
+import RegisterFormData from '@/types/myShop';
+
+interface Props {
+ formData: RegisterFormData;
+ handleChange: (key: keyof RegisterFormData, value: string) => void;
+}
+
+const RegisterDescription = ({ formData, handleChange }: Props) => {
+ return (
+ <>
+
+ 가게 설명
+
+
+ >
+ );
+};
+
+export default RegisterDescription;
diff --git a/src/components/features/my-shop/registerImage.tsx b/src/components/features/my-shop/registerImage.tsx
new file mode 100644
index 0000000..c9a2a47
--- /dev/null
+++ b/src/components/features/my-shop/registerImage.tsx
@@ -0,0 +1,39 @@
+import { Icon } from '@/components/ui';
+import Image from 'next/image';
+import { ChangeEvent } from 'react';
+
+interface Props {
+ preview: string | null;
+ handleImageChange: (e: ChangeEvent) => void;
+}
+
+const RegisterImage = ({ preview, handleImageChange }: Props) => {
+ return (
+ <>
+
+
+ 가게 이미지
+ *
+
+
+
+ >
+ );
+};
+
+export default RegisterImage;
diff --git a/src/components/features/my-shop/registerModal.tsx b/src/components/features/my-shop/registerModal.tsx
new file mode 100644
index 0000000..b1c5536
--- /dev/null
+++ b/src/components/features/my-shop/registerModal.tsx
@@ -0,0 +1,57 @@
+import { Modal } from '@/components/ui';
+import { useRouter } from 'next/router';
+
+interface Props {
+ openWarning: boolean;
+ setOpenWarning: (value: boolean) => void;
+ openCancel: boolean;
+ setOpenCancel: (value: boolean) => void;
+ openConfirm: boolean;
+ setOepnConfirm: (value: boolean) => void;
+}
+
+const RegisterModal = ({
+ openWarning,
+ setOpenWarning,
+ openCancel,
+ setOpenCancel,
+ openConfirm,
+ setOepnConfirm,
+}: Props) => {
+ const router = useRouter();
+ return (
+ <>
+ setOpenWarning(false)}
+ variant='warning'
+ title='필수 항목을 작성해주세요.'
+ primaryText='확인'
+ onPrimary={() => setOpenWarning(false)}
+ />
+ setOpenCancel(false)}
+ variant='warning'
+ title='취소하시겠습니까?'
+ primaryText='아니요'
+ secondaryText='예'
+ onSecondary={() => {
+ setOpenCancel(false);
+ router.push('/my-shop');
+ }}
+ onPrimary={() => setOpenCancel(false)}
+ />
+ setOepnConfirm(false)}
+ variant='success'
+ title='등록이 완료되었습니다.'
+ primaryText='확인'
+ onPrimary={() => router.push('/my-shop')}
+ />
+ >
+ );
+};
+
+export default RegisterModal;
diff --git a/src/components/features/my-shop/registerName.tsx b/src/components/features/my-shop/registerName.tsx
new file mode 100644
index 0000000..93e0d01
--- /dev/null
+++ b/src/components/features/my-shop/registerName.tsx
@@ -0,0 +1,43 @@
+import { Dropdown, Input } from '@/components/ui';
+import { CATEGORY_CODE } from '@/constants/dropdown';
+import RegisterFormData from '@/types/myShop';
+
+interface Props {
+ formData: RegisterFormData;
+ handleChange: (key: keyof RegisterFormData, value: string) => void;
+}
+
+const RegisterName = ({ formData, handleChange }: Props) => {
+ return (
+ <>
+
+
+ handleChange('name', e.target.value)}
+ placeholder='입력'
+ />
+
+
+
+ 분류
+ *
+
+
handleChange('category', val)}
+ className='w-full'
+ />
+
+
+ >
+ );
+};
+
+export default RegisterName;
diff --git a/src/components/features/my-shop/registerWage.tsx b/src/components/features/my-shop/registerWage.tsx
new file mode 100644
index 0000000..3e4ead3
--- /dev/null
+++ b/src/components/features/my-shop/registerWage.tsx
@@ -0,0 +1,30 @@
+import { Input } from '@/components/ui';
+import RegisterFormData from '@/types/myShop';
+import { ChangeEvent } from 'react';
+
+interface Props {
+ formData: RegisterFormData;
+ handleWageChange: (e: ChangeEvent) => void;
+}
+
+const RegisterWage = ({ formData, handleWageChange }: Props) => {
+ return (
+ <>
+
+ >
+ );
+};
+
+export default RegisterWage;
diff --git a/src/components/ui/pagination/pagination.tsx b/src/components/ui/pagination/pagination.tsx
index 6e7b4b5..9aba82c 100644
--- a/src/components/ui/pagination/pagination.tsx
+++ b/src/components/ui/pagination/pagination.tsx
@@ -1,8 +1,8 @@
import { Icon } from '@/components/ui';
import { cn } from '@/lib/utils/cn';
import { useEffect, useState } from 'react';
+import { useMediaQuery } from 'react-responsive';
-const PAGE_GROUP_SIZE = 7;
const BUTTON_ALIGN = 'flex items-center justify-center shrink-0';
interface PaginationProps {
total: number; // 전체 개수 (count)
@@ -15,35 +15,54 @@ interface PaginationProps {
/* {
+ const isDesktop = useMediaQuery({ minWidth: 1028 });
+ const isTablet = useMediaQuery({ minWidth: 744, maxWidth: 1027 });
+ const [pageGroupSize, setPageGroupSize] = useState(7);
const [pageGroup, setPageGroup] = useState(0);
const totalPages = total ? Math.ceil(total / limit) : 0;
const currentPage = Math.floor(offset / limit) + 1; // offset → page 변환
useEffect(() => {
- const newGroup = Math.floor((currentPage - 1) / PAGE_GROUP_SIZE);
+ if (isDesktop) setPageGroupSize(10);
+ else if (isTablet) setPageGroupSize(7);
+ else setPageGroupSize(5);
+ }, [isDesktop, isTablet]);
+
+ useEffect(() => {
+ const newGroup = Math.floor((currentPage - 1) / pageGroupSize);
setPageGroup(newGroup);
- }, [currentPage]);
+ }, [currentPage, pageGroupSize]);
if (totalPages < 1) return null;
- const startPage = pageGroup * PAGE_GROUP_SIZE + 1;
- const endPage = Math.min(startPage + PAGE_GROUP_SIZE - 1, totalPages);
+ const startPage = pageGroup * pageGroupSize + 1;
+ const endPage = Math.min(startPage + pageGroupSize - 1, totalPages);
const pageNumbers = Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
const isPrevDisabled = pageGroup === 0;
- const isNextDisabled = (pageGroup + 1) * PAGE_GROUP_SIZE >= totalPages;
+ const isNextDisabled = (pageGroup + 1) * pageGroupSize >= totalPages;
/* 이전 그룹으로 이동 */
const handlePrevPage = () => {
if (!isPrevDisabled) {
+ const prevGroup = pageGroup - 1;
+ const prevStartPage = prevGroup * pageGroupSize + 1;
setPageGroup(prev => Math.max(prev - 1, 0));
+
+ const newOffset = (prevStartPage - 1) * limit;
+ onPageChange(newOffset);
}
};
/* 다음 그룹으로 이동 */
const handleNextPage = () => {
if (!isNextDisabled) {
- setPageGroup(prev => ((prev + 1) * PAGE_GROUP_SIZE < totalPages ? prev + 1 : prev));
+ const nextGroup = pageGroup + 1;
+ const nextStartPage = nextGroup * pageGroupSize + 1;
+ setPageGroup(prev => ((prev + 1) * pageGroupSize < totalPages ? prev + 1 : prev));
+
+ const newOffset = (nextStartPage - 1) * limit;
+ onPageChange(newOffset);
}
};
diff --git a/src/context/authProvider.tsx b/src/context/authProvider.tsx
index 3b7b06b..1e4e87b 100644
--- a/src/context/authProvider.tsx
+++ b/src/context/authProvider.tsx
@@ -16,7 +16,7 @@ type AuthContextValue = {
login: (credentials: LoginRequest) => Promise;
logout: () => void;
signup: (data: UserRequest) => Promise;
- getUser: () => Promise;
+ getUser: (userId: string) => Promise;
updateUser: (patch: Partial) => Promise;
};
@@ -52,6 +52,7 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
const storedUserId = getStorage(USER_ID_KEY);
if (storedToken) setToken(storedToken);
if (storedUserId) setUserId(storedUserId);
+ if (storedUserId) getUser(storedUserId);
}, []);
// 로그인: /token → 토큰/사용자 ID 저장 → /users/{id}로 내 정보 동기화
@@ -84,11 +85,14 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
}, []);
// 내 정보 재조회
- const getUser = useCallback(async () => {
- if (!userId) throw new Error('로그인이 필요합니다');
- const me = await apiGetUser(userId);
- setUser(me);
- }, [userId]);
+ const getUser = useCallback(
+ async (userId: string) => {
+ if (!userId) throw new Error('로그인이 필요합니다');
+ const me = await apiGetUser(userId);
+ setUser(me);
+ },
+ [userId]
+ );
// 내 정보 수정
const updateUser = useCallback(
diff --git a/src/pages/my-shop/edit.tsx b/src/pages/my-shop/edit.tsx
new file mode 100644
index 0000000..0772ebb
--- /dev/null
+++ b/src/pages/my-shop/edit.tsx
@@ -0,0 +1,9 @@
+const Edit = () => {
+ return (
+ <>
+ 수정 페이지
+ >
+ );
+};
+
+export default Edit;
diff --git a/src/pages/my-shop/index.tsx b/src/pages/my-shop/index.tsx
new file mode 100644
index 0000000..e477f9d
--- /dev/null
+++ b/src/pages/my-shop/index.tsx
@@ -0,0 +1,64 @@
+import { getShop } from '@/api/employer';
+import { Frame } from '@/components/layout';
+import { Button, Notice } from '@/components/ui';
+import useAuth from '@/hooks/useAuth';
+import Link from 'next/link';
+import { useEffect, useState } from 'react';
+
+const Myshop = () => {
+ const { isLogin, user, role } = useAuth();
+ const [shopData, setShopData] = useState({});
+ // console.log('user :', user);
+
+ useEffect(() => {
+ const get = async () => {
+ if (user?.shop) {
+ const res = await getShop(user.shop.item.id);
+ // console.log('shop:', res);
+ const { description, ...rest } = res.item;
+ const formattedShopData = { ...rest, shopDescription: description };
+ setShopData(formattedShopData);
+ }
+ };
+ get();
+ }, [user]);
+
+ return (
+ <>
+ {user?.shop ? (
+ <>
+
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export default Myshop;
diff --git a/src/pages/my-shop/register.tsx b/src/pages/my-shop/register.tsx
new file mode 100644
index 0000000..3c7d751
--- /dev/null
+++ b/src/pages/my-shop/register.tsx
@@ -0,0 +1,114 @@
+import { postPresignedUrl, postShop, uploadImage } from '@/api/employer';
+import RegisterAddress from '@/components/features/my-shop/registerAddress';
+import RegisterDescription from '@/components/features/my-shop/registerDescription';
+import RegisterImage from '@/components/features/my-shop/registerImage';
+import RegisterModal from '@/components/features/my-shop/registerModal';
+import RegisterName from '@/components/features/my-shop/registerName';
+import RegisterWage from '@/components/features/my-shop/registerWage';
+import { Container, Header, Wrapper } from '@/components/layout';
+import { Button, Icon } from '@/components/ui';
+import { NextPageWithLayout } from '@/pages/_app';
+import RegisterFormData from '@/types/myShop';
+import { ChangeEvent, useState } from 'react';
+
+const Register: NextPageWithLayout = () => {
+ const [formData, setFormData] = useState({
+ name: '',
+ category: undefined,
+ address1: undefined,
+ address2: '',
+ originalHourlyPay: '',
+ description: '',
+ image: null,
+ });
+
+ const [preview, setPreview] = useState(null);
+ const [openWarning, setOpenWarning] = useState(false);
+ const [openCancel, setOpenCancel] = useState(false);
+ const [openConfirm, setOepnConfirm] = useState(false);
+
+ const handleChange = (key: keyof RegisterFormData, value: string) => {
+ setFormData(prev => ({ ...prev, [key]: value }));
+ };
+
+ const handleWageChange = (e: ChangeEvent) => {
+ const raw = e.target.value.replace(/,/g, '');
+ if (!/^\d*$/.test(raw)) return;
+ const formatted = raw ? Number(raw).toLocaleString() : '';
+ setFormData(prev => ({ ...prev, originalHourlyPay: formatted }));
+ };
+
+ const handleImageChange = (e: ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ setFormData(prev => ({ ...prev, image: file }));
+ setPreview(URL.createObjectURL(file));
+ };
+
+ const validateForm = () => {
+ return (
+ !formData.name ||
+ !formData.category ||
+ !formData.address1 ||
+ !formData.address2 ||
+ !formData.originalHourlyPay ||
+ !formData.image
+ );
+ };
+
+ const handleSubmit = async () => {
+ if (validateForm()) {
+ setOpenWarning(true);
+ return;
+ }
+ setOepnConfirm(true);
+ // console.log('모든 값 작성 완료', formData);
+ if (formData.image) {
+ const presignedUrl = await postPresignedUrl(formData.image.name);
+ await uploadImage(presignedUrl, formData.image);
+
+ await postShop(formData);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
가게 정보
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+Register.getLayout = page => (
+
+
+ {page}
+
+);
+
+export default Register;
diff --git a/src/pages/testAuth.tsx b/src/pages/testAuth.tsx
index 56ecd28..c63f9e8 100644
--- a/src/pages/testAuth.tsx
+++ b/src/pages/testAuth.tsx
@@ -1,10 +1,11 @@
import useAuth from '@/hooks/useAuth';
import axios from '@/lib/axios';
+import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
export default function TestAuthPage() {
const { isLogin, user, getUser, logout, login } = useAuth();
-
+ const router = useRouter();
// 하이드레이션 에러 방지: 마운트 이후에만 localStorage 값을 렌더
const [mounted, setMounted] = useState(false);
const [lsToken, setLsToken] = useState('');
@@ -74,7 +75,7 @@ export default function TestAuthPage() {
setLoading(true);
setMsg('');
try {
- await getUser();
+ // await getUser();
setMsg('getUser() 호출 성공: user 상태 갱신');
} catch (e: unknown) {
setMsg('getUser() 실패: ' + pickErrorMessage(e));
@@ -125,6 +126,7 @@ export default function TestAuthPage() {
return (
+
Test Auth (임시 테스트 전용)
diff --git a/src/types/myShop.ts b/src/types/myShop.ts
new file mode 100644
index 0000000..57e07bd
--- /dev/null
+++ b/src/types/myShop.ts
@@ -0,0 +1,11 @@
+interface RegisterFormData {
+ name: string;
+ category?: string;
+ address1?: string;
+ address2: string;
+ originalHourlyPay: number | string;
+ description?: string;
+ image?: File | null;
+}
+
+export default RegisterFormData;