Skip to content

Conversation

@minimo-9
Copy link
Collaborator

📌 변경 사항 개요

내 가게 정보 등록/편집 (사장님)(/owner/store/edit) 구현 완료

📝 상세 내용

초기 데이터 유무에 따른 상태 분기

const [edit, setEdit] = useState<StoreEditForm>(
    initialData
      ? {
          name: initialData.name,
          category: initialData.category,
          address1: initialData.address1,
          address2: initialData.address2,
          description: initialData.description,
          originalHourlyPay: initialData.originalHourlyPay,
          imageUrl: initialData.imageUrl,
        }
      : {
          name: '',
          category: '',
          address1: '',
          address2: '',
          description: '',
          originalHourlyPay: 0,
          imageUrl: '',
        },
  );

아무 값이 없으면 등록 / 값이 있으면 수정 페이지를 보여줍니다.

예외 상황 처리

  • 로그아웃 시
  useEffect(() => {
    const userId = localStorage.getItem('userId');

    if (!userId) {
      setModalType('auth');
      setModalContent('로그인이 필요합니다.');
      setIsModalOpen(true);
      return;
    }
  }, [isLoggedIn]);

'로그인이 필요합니다.'라는 모달을 보여준 뒤 로그인 페이지로 이동시킵니다.

  • 입력 값이 없는 경우
    for (const { key, label } of requiredFields) {
      const value = edit[key as keyof StoreEditForm];

      const isEmpty = typeof value === 'string' ? value.trim() === '' : !value;

      if (isEmpty) {
        setModalType('warning');
        setModalContent(`${label} 내용을 추가해 주세요.`);
        setIsModalOpen(true);
        return;
      }
    }

각 태그 이름을 보여주고 '내용을 추가해 주세요' 라는 모달을 보여줍니다.

모달 버튼 타입

type ModalType = 'success' | 'warning' | 'auth';

  const handleModalClose = () => {
    setIsModalOpen(false);

    switch (modalType) {
      case 'success':
        navigate('/owner/store');
        break;
      case 'auth':
        navigate('/login');
        break;
      case 'warning':
      default:
        break;
    }
  };

success는 가게 상세 페이지 이동, auth는 로그인 페이지 이동, warning은 단순 확인으로 동작합니다.

imageApi에 백엔드 에러메세지가 없어서 수정 / S3 업로드에 api로 잘못 요청한 부분 axios로 변경했습니다.

🔗 관련 이슈

🖼️ 스크린샷(선택사항)

  • 반응형
    image
    image

  • 빈 값일 때 모달
    image

  • 전체 입력
    image

  • 등록 완료
    image

  • 수정 페이지
    image

  • 수정 완료
    image

  • 수정된 모습
    image

💡 참고 사항

@minimo-9 minimo-9 requested review from a user, Moon-ju-young and Yun-Jinwoo June 19, 2025 16:01
@minimo-9 minimo-9 self-assigned this Jun 19, 2025
@minimo-9 minimo-9 added the ✨ 기능 추가/구현 새로운 기능을 추가하거나 구현했어요! label Jun 19, 2025
@minimo-9 minimo-9 linked an issue Jun 19, 2025 that may be closed by this pull request
1 task
Copy link
Collaborator

@Yun-Jinwoo Yun-Jinwoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

시급을 잘못 입력한 경우에 나오는 모달의 텍스트가 너무 길어서 그런지 좀 부자연스럽게 보이네요..
이를 해결하려면 모달 컴포넌트를 수정하거나 아니면 해당 오류가 나는 상황에 다른 메시지로 모달을 보여주는 방법이 있을 것 같습니다!

구현하느라 고생 많으셨습니다~ 👍👍

Comment on lines 40 to 60
const [edit, setEdit] = useState<StoreEditForm>(
initialData
? {
name: initialData.name,
category: initialData.category,
address1: initialData.address1,
address2: initialData.address2,
description: initialData.description,
originalHourlyPay: initialData.originalHourlyPay,
imageUrl: initialData.imageUrl,
}
: {
name: '',
category: '',
address1: '',
address2: '',
description: '',
originalHourlyPay: 0,
imageUrl: '',
},
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [edit, setEdit] = useState<StoreEditForm>(
initialData
? {
name: initialData.name,
category: initialData.category,
address1: initialData.address1,
address2: initialData.address2,
description: initialData.description,
originalHourlyPay: initialData.originalHourlyPay,
imageUrl: initialData.imageUrl,
}
: {
name: '',
category: '',
address1: '',
address2: '',
description: '',
originalHourlyPay: 0,
imageUrl: '',
},
);
const [edit, setEdit] = useState<StoreEditForm>({
name: initialData?.name ?? '',
category: initialData?.category ?? '',
address1: initialData?.address1 ?? '',
address2: initialData?.address2 ?? '',
description: initialData?.description ?? '',
originalHourlyPay: initialData?.originalHourlyPay ?? 0,
imageUrl: initialData?.imageUrl ?? '',
});

이게 좀더 보기 쉬울것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 그렇네요 감사합니다! 변경할게요

interface DropdownProps<T extends string> {
options: readonly T[];
selected: T;
selected: T | '';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에고 그 주영님께서 null로 사용하자는 피드백 주셔서 제가 null로 수정했는데 ㅠㅠ 혹시 드롭다운 요소 초기값을 null로 변경 가능하실까요?
좀 복잡하면 그냥 제가 ''로 되돌리겠습니다! 크게 상관은 없는것 같아서!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇.. 그렇군요 ㅠ

Comment on lines 283 to 289
<textarea
id="description"
value={edit.description}
onChange={handleChange('description')}
placeholder="입력"
className="h-153 w-full rounded-[5px] border border-gray-30 bg-white px-20 py-16 text-body1/26 font-regular text-black"
/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<textarea
id="description"
value={edit.description}
onChange={handleChange('description')}
placeholder="입력"
className="h-153 w-full rounded-[5px] border border-gray-30 bg-white px-20 py-16 text-body1/26 font-regular text-black"
/>
<textarea
id="description"
value={edit.description}
onChange={handleChange('description')}
placeholder="입력"
className="h-153 w-full resize-none rounded-[5px] border border-gray-30 bg-white px-20 py-16 text-body1/26 font-regular text-black"
/>

image
이걸 숨기려면 class에 resize-none을 추가해주시면 됩니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 까먹고 있었네요 감사합니다!

</div>
</div>

<div className="mb-20 flex flex-col gap-20 md:mb-24 md:gap-24">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보통 input을 포함하고 해당 값들을 전달하는 버튼이 필요한 부분은 form으로 감싸주는게 좋습니다!
또한 form의 onSubmit 속성을 활용하면 enter를 눌러서 등록버튼을 클릭할 수도 있습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 알겠습니다! 변경해보겠습니다!


<div className="mb-20 flex flex-col gap-20 md:mb-24 md:gap-24">
<div className="flex flex-col gap-20 md:flex-row md:gap-20">
<div className="flex flex-col gap-8 md:max-w-472 md:basis-1/2">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분 저도 고민했던 부분인데 grid로 하니까 편하더라구요! 이건 그냥 참고만 하세요! 방법은 여러가지니 ㅎㅎ

<div className="grid grid-cols-1 md:grid-cols-2">

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 그리드도 좋은 방법이네요 ㅎㅎ

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구현하시느라 고생하셨습니다!

Copy link
Contributor

@Moon-ju-young Moon-ju-young left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지에 너무 중첩된 div 태그들이 많아요...!! 그리고 통일성도 없는 것 같아요 (어디는 간격이 gap이고 어디는 margin 이고...) flexbox로도 가능은 하지만 grid로 작성하는 편이 훨씬 깔끔하고 div 태그를 줄일 수 있을 것 같습니다

Comment on lines 1 to 17
import { useLocation, useNavigate } from 'react-router-dom';
import Input from '@/components/common/Input';
import Dropdown from '@/components/common/Dropdown';
import { ADDRESS_OPTIONS, CATEGORY_OPTIONS } from '@/constants/dropdownOptions';
import Close from '@/assets/icons/close.svg';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import {
postShop,
putShop,
type ShopItem,
type ShopRequest,
} from '@/api/shopApi';
import ImageInput from '@/components/common/ImageInput';
import Button from '@/components/common/Button';
import Modal from '@/components/common/Modal';
import { getPresignedUrl, uploadImageToS3 } from '@/api/imageApi';
import { AuthContext } from '@/context/AuthContext';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 import 문을 비슷한 요소끼리 정렬해 놓으면 좀 더 이해하기 편할 것 같습니다~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 정리 해두겠습니다!

Comment on lines 33 to 34
const location = useLocation();
const initialData = location.state as InitialData;
Copy link
Contributor

@Moon-ju-young Moon-ju-young Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 아마 location.state를 통해 구현하신 걸 보면 navigate에서 데이터를 넘겨주는 것이지 않나 싶은데 이렇게 될 경우엔 주소 직접 치고 들어올 때는 데이터가 있는데 없다고 인식하는 경우가 있지 않을까 싶네요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 그래서 api에서 가져오는 방식으로 수정했습니다!

Comment on lines 40 to 60
const [edit, setEdit] = useState<StoreEditForm>(
initialData
? {
name: initialData.name,
category: initialData.category,
address1: initialData.address1,
address2: initialData.address2,
description: initialData.description,
originalHourlyPay: initialData.originalHourlyPay,
imageUrl: initialData.imageUrl,
}
: {
name: '',
category: '',
address1: '',
address2: '',
description: '',
originalHourlyPay: 0,
imageUrl: '',
},
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [edit, setEdit] = useState<StoreEditForm>(
initialData
? {
name: initialData.name,
category: initialData.category,
address1: initialData.address1,
address2: initialData.address2,
description: initialData.description,
originalHourlyPay: initialData.originalHourlyPay,
imageUrl: initialData.imageUrl,
}
: {
name: '',
category: '',
address1: '',
address2: '',
description: '',
originalHourlyPay: 0,
imageUrl: '',
},
);
const [edit, setEdit] = useState<StoreEditForm>(
initialData ?? {
name: '',
category: '',
address1: '',
address2: '',
description: '',
originalHourlyPay: 0,
imageUrl: '',
},
);

💬 이런 방식도 가능할 것 같습니다~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 좋습니다! 변경할게요!

Comment on lines 103 to 108
const formatNumber = useMemo(
() => (value: string) => {
return Number(value).toLocaleString();
},
[],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 여기서 useCallback이 아니라 useMemo를 쓰신 이유가 있으실까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 제가 따로 따로 작업하다보니 다른 부분이랑 통일을 못했네요! 변경해두겠습니다!

{ key: 'address2', label: '상세 주소' },
{ key: 'originalHourlyPay', label: '기본 시급' },
{ key: 'imageUrl', label: '가게 이미지' },
{ key: 'description', label: '가게 설명' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ 가게 설명도 필수인가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필수 부분은 아닌걸로 아는데 있는게 좋을 것 같아 넣었습니다! 요건 빼도록 하겠습니다!

Comment on lines 186 to 187
case 'warning':
default:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case 'warning':
default:
default:

💬 간단하게 이렇게 작성해도 될 듯 합니다~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경했습니다!

Comment on lines 204 to 227
<div className="mb-20 flex flex-col gap-20 md:mb-24 md:flex-row md:gap-20">
<div className="md:max-w-472 md:basis-1/2">
<Input
label="가게 이름*"
value={edit.name}
onChange={handleChange('name')}
/>
</div>
<div className="flex flex-col gap-8 md:max-w-472 md:basis-1/2">
<label className="text-body1/26 font-regular text-black">
분류*
</label>
<Dropdown<Category>
options={CATEGORY_OPTIONS}
selected={edit.category}
setSelect={(value) =>
setEdit((prev) => ({ ...prev, category: value as Category }))
}
variant="form"
/>
</div>
</div>

<div className="mb-20 flex flex-col gap-20 md:mb-24 md:gap-24">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ input 에 대해서 굳이 div 태그를 두 부분으로 분리하신 이유가 있으실까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

드롭다운 부분에 따로 라벨 props가 없어서 div 감싸는 걸 먼저 작업 후에 태블릿,데스크탑 사이즈를 작업하다보니 두 개로 나눈 것 같습니다! 요건 수정하겠습니다

unit="원"
/>
</div>
<div className="hidden md:block md:max-w-472 md:basis-1/2"></div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ 이 요소는 왜 있는 걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flex로 작업을 하다보니 태블릿에서 기본 시급 input 사이즈가 다른 사이즈들 보다 먼저 길어져 있어 맞추기 위해 넣었습니다
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다~ grid를 활용한다면 빈 div 태그 없이 처리 할 수도 있을 것 같네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 그리드로 바꿔도 좋을 것 같습니다!

value={
edit.originalHourlyPay === 0
? ''
: formatNumber(String(edit.originalHourlyPay))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 formatNumber 가 한 번 밖에 쓰이지 않는 것 같아 굳이 함수로 분리하지 않아도 될 것 같네요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 알겠습니다!


const isEditMode =
initialData !== undefined && typeof initialData?.id === 'string';
const shopId = localStorage.getItem('shopId');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ 저희 로컬스토리지에 shopId가 있나요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이건 제가 편집 모드 들어가기 위해서 조회 부분에서 생성해서 가져오는 식으로 해서 있었습니다.

Comment on lines 68 to 76
setEdit({
name: shopInfo.item.name ?? '',
category: shopInfo.item.category ?? null,
address1: shopInfo.item.address1 ?? null,
address2: shopInfo.item.address2 ?? '',
description: shopInfo.item.description ?? '',
originalHourlyPay: shopInfo.item.originalHourlyPay ?? 0,
imageUrl: shopInfo.item.imageUrl ?? '',
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
setEdit({
name: shopInfo.item.name ?? '',
category: shopInfo.item.category ?? null,
address1: shopInfo.item.address1 ?? null,
address2: shopInfo.item.address2 ?? '',
description: shopInfo.item.description ?? '',
originalHourlyPay: shopInfo.item.originalHourlyPay ?? 0,
imageUrl: shopInfo.item.imageUrl ?? '',
});
setEdit(shopInfo.item);

💬 이렇게 줄일 수 있을 것 같아요~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 좋네요! 바꾸겠습니다

return;
}

if (isEditMode && !shopId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isEditModeshopId에 의해 결정되는데 둘을 같이 체크하는 이유를 모르겠습니다! 이렇게 된다면 항상 거짓이 아닌가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 메세지 끄려고 만들었는데 중복이였네요! 삭제하겠습니다!

edit.originalHourlyPay === 0
? ''
: formatNumber(String(edit.originalHourlyPay))
: Number(edit.originalHourlyPay).toLocaleString()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
: Number(edit.originalHourlyPay).toLocaleString()
: edit.originalHourlyPay.toLocaleString()

💬 originalHourlyPay가 원래 number형이라 이대로 써도 될 것 같습니다~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 알겠습니다!

<div className="mb-24 flex items-center justify-between md:mb-32">
<div className="flex flex-col gap-24 px-12 pt-40 pb-80 md:gap-32 md:px-32 md:pb-60 lg:mx-auto lg:max-w-964 lg:px-0">
<div className="flex items-center justify-between">
<h2 className="text-h3/25 font-bold text-black md:text-h1">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<h2 className="text-h3/25 font-bold text-black md:text-h1">
<h2 className="text-h3/25 font-bold text-black md:text-h1/24">

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h1/24 이부분은 28인것 같습니다! 100%로 나온 부분이라

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오우 오타가 났네요 24가 아니라 34나 35가 맞을 것 같습니다!

};

return (
<div className="min-h-screen bg-gray-5">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗ 이 부분은 진우님께도 피드백 드린 부분인데 Nav 컴포넌트가 페이지 바깥에 있어서 h-screen 같은 스타일을 사용하면 실제 화면 크기보다 높이가 더 높아져 불필요한 스크롤이 생겨버립니다! 수정 부탁드려요~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 수정했습니다!

@minimo-9 minimo-9 merged commit aa19577 into develop Jun 20, 2025
2 checks passed
@minimo-9 minimo-9 deleted the feat/70-store-edit branch June 20, 2025 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ 기능 추가/구현 새로운 기능을 추가하거나 구현했어요!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 가게 정보 등록 페이지 구현

4 participants