Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 43 additions & 27 deletions src/api/employer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,65 @@ 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 postShop(body: Omit<RegisterFormData, 'image'>) {
const accessToken = localStorage.getItem('thejulge-token');
const { data } = await axios.post('/shops', body, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return data;
}

export async function getShop(shopId: string) {
const { data } = await axios.get(`/shops/${shopId}`);
return data;
}

export async function putShop(shopId: string, body: RegisterFormData) {
const accessToken = localStorage.getItem('thejulge-token');
const { data } = await axios.put(`/shops/${shopId}`, body, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return data;
}

export async function postPresignedUrl(imageName: string) {
const { data } = await axios.post('/images', { name: imageName });
// console.log(data);
export async function getNotice(shopId: string) {
const { data } = await axios.get(`/shops/${shopId}/notices`);
return data;
}

///////////////////////////////////////////////////////////////////////////////

export async function postPresignedUrl(imageUrl: string) {
const accessToken = localStorage.getItem('thejulge-token');
const { data } = await axios.post(
'/images',
{ name: imageUrl },
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
return data.item.url;
}

export async function uploadImage(presignedUrl: string, file: File) {
const result = await originAxios.put(presignedUrl, file);
try {
await originAxios.put(presignedUrl, file);
} catch (error) {
alert(error);
}
}

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;
return result;
}
21 changes: 19 additions & 2 deletions src/components/features/my-shop/registerImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import Image from 'next/image';
import { ChangeEvent } from 'react';

interface Props {
mode: string;
preview: string | null;
handleImageChange: (e: ChangeEvent<HTMLInputElement>) => void;
}

const RegisterImage = ({ preview, handleImageChange }: Props) => {
const RegisterImage = ({ mode, preview, handleImageChange }: Props) => {
return (
<>
<div className='flex flex-col gap-1'>
Expand All @@ -17,7 +18,23 @@ const RegisterImage = ({ preview, handleImageChange }: Props) => {
</div>
<label className='relative flex h-[200px] w-full cursor-pointer flex-col items-center justify-center overflow-hidden rounded-xl border border-gray-300 tablet:h-[276px] tablet:w-[483px]'>
{preview ? (
<Image src={preview} alt='미리보기' fill />
<>
<Image src={preview} alt='미리보기' fill />
{mode === 'edit' && (
<>
<Image src={preview} alt='미리보기' fill />
<div className='z-1 absolute inset-0 flex h-full w-full flex-col items-center justify-center gap-1 bg-black opacity-70 transition hover:opacity-50'>
<Icon
iconName='camera'
iconSize='lg'
ariaLabel='카메라 아이콘'
className='bg-gray-400'
/>
<p className='text-gray-400'>이미지 변경하기</p>
</div>
</>
)}
</>
) : (
<>
<Icon
Expand Down
4 changes: 3 additions & 1 deletion src/components/features/my-shop/registerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Modal } from '@/components/ui';
import { useRouter } from 'next/router';

interface Props {
mode: string;
openWarning: boolean;
setOpenWarning: (value: boolean) => void;
openCancel: boolean;
Expand All @@ -11,6 +12,7 @@ interface Props {
}

const RegisterModal = ({
mode,
openWarning,
setOpenWarning,
openCancel,
Expand Down Expand Up @@ -46,7 +48,7 @@ const RegisterModal = ({
open={openConfirm}
onClose={() => setOepnConfirm(false)}
variant='success'
title='등록이 완료되었습니다.'
title={mode === 'edit' ? '수정이 완료되었습니다.' : '등록이 완료되었습니다.'}
primaryText='확인'
onPrimary={() => router.push('/my-shop')}
/>
Expand Down
122 changes: 122 additions & 0 deletions src/components/features/my-shop/shopForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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 } from '@/components/layout';
import { Button, Icon } from '@/components/ui';
import RegisterFormData from '@/types/myShop';
import { ChangeEvent, useEffect, useState } from 'react';

interface ShopFromProps {
mode: 'register' | 'edit';
initialData?: RegisterFormData | null;
onSubmit: (data: RegisterFormData) => Promise<void>;
}

const ShopForm = ({ mode, initialData, onSubmit }: ShopFromProps) => {
const [formData, setFormData] = useState<RegisterFormData>(
initialData ?? {
name: '',
category: undefined,
address1: undefined,
address2: '',
originalHourlyPay: '',
description: '',
image: null,
imageUrl: '',
}
);

useEffect(() => {
if (initialData) {
setFormData(initialData);
if (initialData.imageUrl) {
setPreview(initialData.imageUrl);
}
}
}, [initialData]);

const [preview, setPreview] = useState<string | null>(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<HTMLInputElement>) => {
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 = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setFormData(prev => ({ ...prev, image: file }));
setPreview(URL.createObjectURL(file));
};

const validateForm = () => {
if (mode === 'register')
return (
!formData.name ||
!formData.category ||
!formData.address1 ||
!formData.address2 ||
!formData.originalHourlyPay ||
!formData.image ||
!formData.imageUrl
);
return false;
};

const handleSubmit = async () => {
if (validateForm()) {
setOpenWarning(true);
return;
}
setOepnConfirm(true);
await onSubmit(formData);
};

return (
<>
<div className='h-auto bg-gray-50'>
<Container as='section' className='flex flex-col gap-6 pb-20'>
<div className='mt-6 flex items-center justify-between'>
<h1 className='text-heading-l font-bold'>
{mode === 'register' ? '가게 등록' : '가게 편집'}
</h1>
<button
onClick={() => setOpenCancel(true)}
className='flex cursor-pointer items-center justify-center'
>
<Icon iconName='close' iconSize='lg' ariaLabel='닫기' />
</button>
</div>
<RegisterName formData={formData} handleChange={handleChange} />
<RegisterAddress formData={formData} handleChange={handleChange} />
<RegisterWage formData={formData} handleWageChange={handleWageChange} />
<RegisterImage mode={mode} preview={preview} handleImageChange={handleImageChange} />
<RegisterDescription formData={formData} handleChange={handleChange} />
<Button onClick={handleSubmit}>{mode === 'register' ? '등록하기' : '완료하기'}</Button>
<RegisterModal
mode={mode}
openWarning={openWarning}
setOpenWarning={setOpenWarning}
openCancel={openCancel}
setOpenCancel={setOpenCancel}
openConfirm={openConfirm}
setOepnConfirm={setOepnConfirm}
/>
</Container>
</div>
</>
);
};
export default ShopForm;
1 change: 1 addition & 0 deletions src/components/ui/card/cardImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const CardImage = ({ variant, src, alt, className, children }: CardImageProps) =
)}
>
<Image
//src={src ?? FALLBACK_SRC}
src={isValidSrc ? imgSrc : FALLBACK_SRC}
alt={`${alt} 가게 이미지`}
fill
Expand Down
2 changes: 1 addition & 1 deletion src/context/authProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const AuthContext = createContext<AuthContextValue | null>(null);
const TOKEN_KEY = 'thejulge_token';
const USER_ID_KEY = 'thejulge_user_id';
const EXPIRES_KEY = 'thejulge_expires_at'; // 만료시간 저장 키
const EXP_TIME = 10 * 60 * 1000; // 만료 유지시간 10분
const EXP_TIME = 1000 * 60 * 1000; // 만료 유지시간 10분

// 브라우저에서만 동작하도록 가드된 유틸
const setStorage = (key: string, value: string) => {
Expand Down
54 changes: 52 additions & 2 deletions src/pages/my-shop/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,59 @@
const Edit = () => {
import { getShop, postPresignedUrl, putShop, uploadImage } from '@/api/employer';
import ShopForm from '@/components/features/my-shop/shopForm';
import { Header, Wrapper } from '@/components/layout';
import useAuth from '@/hooks/useAuth';
import { NextPageWithLayout } from '@/pages/_app';
import RegisterFormData from '@/types/myShop';
import { useEffect, useState } from 'react';

const Edit: NextPageWithLayout = () => {
const { user } = useAuth();
const [editData, setEditData] = useState<RegisterFormData | null>(null);

useEffect(() => {
const fetchShop = async () => {
if (user?.shop) {
const res = await getShop(user.shop.item.id);
setEditData(res.item);
}
};
fetchShop();
}, [user]);

const handleEdit = async (editData: RegisterFormData) => {
if (!user?.shop) return;
let imageUrl = editData.imageUrl ?? '';
if (editData.image) {
const presignedUrl = await postPresignedUrl(editData.image.name);
await uploadImage(presignedUrl, editData.image);
try {
const url = new URL(presignedUrl);
const shortUrl = url.origin + url.pathname;
imageUrl = shortUrl;
} catch (error) {
alert(error);
}
}
// 🟣 PUT 요청
const { originalHourlyPay, ...shopData } = editData;
const numericPay =
typeof originalHourlyPay === 'string'
? Number(originalHourlyPay.replace(/,/g, ''))
: originalHourlyPay;
await putShop(user.shop.item.id, { ...shopData, originalHourlyPay: numericPay, imageUrl });
};
return (
<>
<div>수정 페이지</div>
<ShopForm mode='edit' initialData={editData} onSubmit={handleEdit} />
</>
);
};

Edit.getLayout = page => (
<Wrapper>
<Header />
<main>{page}</main>
</Wrapper>
);

export default Edit;
10 changes: 6 additions & 4 deletions src/pages/my-shop/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ import Link from 'next/link';
import { useEffect, useState } from 'react';

const Myshop = () => {
const { isLogin, user, role } = useAuth();
const { user } = 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);
Expand All @@ -37,7 +35,11 @@ const Myshop = () => {
>
편집하기
</Button>
<Button as={Link} href='/' className='h-[38px] flex-1 tablet:h-12'>
<Button
as={Link}
href={`/employer/notices/${user.shop.item.id}`}
className='h-[38px] flex-1 tablet:h-12'
>
공고 등록하기
</Button>
</div>
Expand Down
Loading