Repository: team-da-sh/dash-client
Pull request #439: [Refactor] SP2 클래스 등록 페이지 QA 반영
Author: login: KIMGEONHWI
Labels: name: 🪄 Refactor
description: 코드 리팩토링
name: 건휘
description: Pull it back, that siren
Created: 2025-05-20T09:57:24Z | Merged: 2025-05-22T14:48:16Z

## 📌 Related Issues
- close #429 

## ✅ 체크 리스트 
- [x] PR 제목의 형식을 잘 작성했나요? e.g. [Feat] PR 템플릿 작성
- [x] 빌드가 성공했나요? (pnpm build)
- [x] 리뷰어와 라벨을 지정했나요?

## 📄 Tasks
- 클래스 등록 페이지 디자인 QA 반영
-  "완료" 버튼 활성화 조건 올바르게 수정
- 모든 필드 리액트 훅폼 적용
- 일정 등록 로직 기획에 맞추어 수정

## ⭐ PR Point
변동 사항이 많기 때문에, 커밋 메세지를 참고 하는것을 추천드립니다. 또한, 제가 작업한 브랜치에서 작업한 결과물을 확인하시면 훨씬 이해하는데 도움이 될겁니다.

작업 내용 중 가장 리소스가 높았던 것은 "일정 등록"에 대한 부분입니다.

<기획 요구사항>
1. 일정 등록  이전 회차와 시간이 중복되지 않도록 하기
2. 일정 등록 1회차 등록 이후 2회차가 1회차보다 전 시간대로 등록이 되지 못하도록 하기

해당 부분을 구현하기 위해서 로직도 매우 복잡하고, 고려해야되는 부분이 많았습니다.

그래서 첫번째로 1회차에 등록한 날짜 이전 날짜는 모두 선택하지 못하도록 하였습니다. (아래 영상 참고)

https://github.com/user-attachments/assets/7950f7df-a990-45a5-a29d-ce733ce569ad

2회차 등록에서는 1회차에서 등록한 날짜(24일)의 이전 날짜는 모두 선택하지 못하도록하였습니다. 하지만, 여기서 또다른 문제점은 이전 회차에서 등록한 날짜와 동일한 날짜를 2회차로 다시 등록하려고 할 때 중복된 시간 혹은 이전 시간을 등록할시 막는 로직이 없었기 때문에  해당 부분에서 고민이 많았습니다. 그래서 이전 회차에 등록한 날짜와 동일한 날짜로 다음회차를 등록하려고 할시에 이전 회차 시간을 상단에 텍스트로 알려주고 이전 회차 시간으로 TimeController를 움직이는 버튼을 disabled처리하였습니다. (아래 사진 참고)

![스크린샷 2025-05-21 오전 2 35 11](https://github.com/user-attachments/assets/488c2897-9067-4656-aba8-2307df04ba85)

그렇기 때문에 이전 회차에 등록한 시간의 종료시간도 계산해야되고, 익일로 넘어갔을 때의 계산도 따로 해주어야하다보니 로직이 많이 복잡해졌습니다. 그래서 time 관련한 로직들은 최대한 timeutils에 분리하여 사용하도록 하였습니다.




## 📷 Screenshot
> 모든 필드 리액트 훅폼 적용(에러 메세지)


https://github.com/user-attachments/assets/78e43258-1a84-4e19-8ed8-8e476db22a1c

참고로, 해당 에러메세지는 초기화면에 뜨는 것이 아니라 input의 경우에는 텍스트를 썼다가 모두 지웠을시, 버튼 혹은 이미지의 경우 등록 후 취소했을 경우에만 에러메세지가 뜹니다.





## 🔔 ETC


Linked issue #429: [Refactor] SP2 클래스 등록 페이지 QA 반영
## 🛠 Task Description
<!-- 여기에 작업에 대한 설명을 작성 -->
클래스 등록 페이지 QA 반영 작업

## 📝 TO-DO
- [ ] 클래스 등록 페이지 QA 반영

## 🔔ETC


Diff:
diff --git a/src/pages/editProfiles/components/FormField/FormField.tsx b/src/pages/editProfiles/components/FormField/FormField.tsx
index 2e6d0c7a3..945494816 100644
--- a/src/pages/editProfiles/components/FormField/FormField.tsx
+++ b/src/pages/editProfiles/components/FormField/FormField.tsx
@@ -41,7 +41,6 @@ const FormField = ({
         isError={!!error}
         maxLength={name === 'phoneNumber' ? MAX_PHONENUMBER_LENGTH : MAX_NAME_LENGTH}
         readOnly={readOnly}
-        isFocused={isFocused}
         onFocus={onFocus}
         onBlur={onBlur}
       />
diff --git a/src/pages/instructor/classRegister/ClassRegister.tsx b/src/pages/instructor/classRegister/ClassRegister.tsx
index 4d8894a55..9bad50be5 100644
--- a/src/pages/instructor/classRegister/ClassRegister.tsx
+++ b/src/pages/instructor/classRegister/ClassRegister.tsx
@@ -1,7 +1,7 @@
 import { zodResolver } from '@hookform/resolvers/zod';
 import { useQueryClient } from '@tanstack/react-query';
-import { useEffect } from 'react';
-import { useController, useForm } from 'react-hook-form';
+import type { FormEvent } from 'react';
+import { useController, useForm, FormProvider } from 'react-hook-form';
 import { useNavigate } from 'react-router-dom';
 import { useGetLocationList, usePostClassRegisterInfo } from '@/pages/instructor/classRegister/apis/queries';
 import * as styles from '@/pages/instructor/classRegister/classRegister.css';
@@ -19,6 +19,7 @@ import ClassSchedule from '@/pages/instructor/classRegister/components/ClassSche
 import { useClassRegisterForm } from '@/pages/instructor/classRegister/hooks/useClassRegisterForm';
 import { classRegisterSchema } from '@/pages/instructor/classRegister/schema/classRegisterSchema';
 import type { ClassRegisterInfoTypes } from '@/pages/instructor/classRegister/types/api';
+import type { LocationTypes } from '@/pages/instructor/classRegister/types/index';
 import { ROUTES_CONFIG } from '@/routes/routesConfig';
 import BoxButton from '@/shared/components/BoxButton/BoxButton';
 import { genreEngMapping, levelEngMapping } from '@/shared/constants';
@@ -33,13 +34,7 @@ const ClassRegister = () => {
   const { mutate: classRegisterMutate } = usePostClassRegisterInfo();
   const { isBottomSheetOpen, openBottomSheet, closeBottomSheet } = useBottomSheet();
 
-  const {
-    register,
-    watch,
-    setValue,
-    control,
-    formState: { errors },
-  } = useForm({
+  const methods = useForm({
     resolver: zodResolver(classRegisterSchema),
     mode: 'onChange',
     defaultValues: {
@@ -55,6 +50,8 @@ const ClassRegister = () => {
     },
   });
 
+  const { register, watch, setValue, control } = methods;
+
   const {
     className,
     detail,
@@ -71,11 +68,7 @@ const ClassRegister = () => {
     control,
   });
 
-  useEffect(() => {
-    console.log('imageUrls', imageUrls);
-  }, [imageUrls]);
-
-  const handleTextAreaHeight = (e: React.FormEvent<HTMLTextAreaElement>) => {
+  const handleTextAreaHeight = (e: FormEvent<HTMLTextAreaElement>) => {
     const textArea = e.target as HTMLTextAreaElement;
 
     if (textArea) {
@@ -99,37 +92,73 @@ const ClassRegister = () => {
   };
 
   const {
-    // imageUrls,
-
     selectedTime,
     times,
     hour,
     minute,
     ampm,
     startDate,
-
     isUndecidedLocation,
     defaultPlace,
     selectedLocation,
-
     setImageUrls,
-
     setStartDate,
     setHour,
     setMinute,
     setAmpm,
     setSelectedTime,
-    handleAddTime,
-    handleRemoveTime,
-
+    handleAddTime: originalHandleAddTime,
+    handleRemoveTime: originalHandleRemoveTime,
     handleNoneLocationCheck,
     handleDefaultPlace,
     setSelectedLocation,
-
-    // isButtonActive,
+    isButtonActive,
   } = useClassRegisterForm();
 
-  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
+  const handleAddTime = () => {
+    const newTimes = originalHandleAddTime();
+    setValue('times', newTimes, { shouldValidate: true });
+  };
+
+  const handleRemoveTime = (idx: number) => {
+    const newTimes = originalHandleRemoveTime(idx);
+    setValue('times', newTimes, { shouldValidate: true });
+  };
+
+  const initTimeAndOpenBottomSheet = () => {
+    if (startDate) {
+      let latestEndTime: Date | null = null;
+
+      for (const time of times) {
+        const existingEndTime = new Date(time.endTime);
+
+        const endTimeDate = existingEndTime.toDateString();
+        const selectedDateObj = new Date(startDate);
+        const selectedDateStr = selectedDateObj.toDateString();
+
+        if (endTimeDate === selectedDateStr) {
+          if (latestEndTime === null || existingEndTime > latestEndTime) {
+            latestEndTime = existingEndTime;
+          }
+        }
+      }
+
+      if (latestEndTime) {
+        const endHour = latestEndTime.getHours();
+        const endMinute = latestEndTime.getMinutes();
+        const endAmpm = endHour >= 12 ? 'PM' : 'AM';
+        const display12Hour = endHour > 12 ? endHour - 12 : endHour === 0 ? 12 : endHour;
+
+        setHour(display12Hour);
+        setMinute(endMinute);
+        setAmpm(endAmpm);
+      }
+    }
+
+    openBottomSheet();
+  };
+
+  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
     e.preventDefault();
 
     if (selectedGenre && selectedLevel) {
@@ -168,9 +197,11 @@ const ClassRegister = () => {
 
   const handleImageUploadSuccess = (url: string) => {
     field.onChange(url);
+    setImageUrls({ imageUrls: url });
   };
 
   const handleDeleteUrl = () => {
+    field.onChange('');
     setImageUrls({ imageUrls: '' });
   };
 
@@ -183,43 +214,76 @@ const ClassRegister = () => {
 
   const { data: locationList } = useGetLocationList(debouncedSearchValue);
 
+  const handleRemoveLocation = () => {
+    setSelectedLocation(null);
+    setValue('selectedLocation', null, { shouldValidate: true });
+  };
+
+  const handleSelectLocation = (location: LocationTypes | null) => {
+    setSelectedLocation(location);
+    setValue('selectedLocation', location, { shouldValidate: true });
+  };
+
   return (
     <>
-      <form onSubmit={handleSubmit}>
-        <div className={styles.containerStyle}>
-          <ClassName className={className} register={register} error={errors.className} />
-          <ClassDescription register={register} error={errors.detail} handleTextAreaHeight={handleTextAreaHeight} />
-          <ClassRepresentImage
-            imgFile={imgFile}
-            previewImg={previewImg}
-            imgRef={imgRef}
-            handleUploaderClick={handleUploaderClick}
-            uploadImgFile={uploadImgFile}
-            deleteImgFile={deleteImgFile}
-          />
-          <ClassGenre selectedGenre={selectedGenre} toggleCategory={toggleCategory} />
-          <ClassLevel selectedLevel={selectedLevel} toggleLevel={toggleLevel} />
-          <ClassRecommend register={register} handleTextAreaHeight={handleTextAreaHeight} />
-          <ClassSchedule openBottomSheet={openBottomSheet} times={times} handleRemoveTime={handleRemoveTime} />
-          <ClassPersonnel maxReservationCount={maxReservationCount} register={register} />
-          <ClassPlace
-            register={register}
-            isUndecidedLocation={isUndecidedLocation}
-            handleHasLocation={handleNoneLocationCheck}
-            defaultPlace={defaultPlace}
-            handleDefaultPlace={handleDefaultPlace}
-            selectedLocation={selectedLocation}
-            setSelectedLocation={setSelectedLocation}
-            locationList={locationList}
-          />
-          <ClassAmount price={price} register={register} />
-        </div>
-        <div className={styles.buttonContainerStyle}>
-          <BoxButton type="submit" disabled={false}>
-            완료
-          </BoxButton>
-        </div>
-      </form>
+      <FormProvider {...methods}>
+        <form onSubmit={handleSubmit}>
+          <div className={styles.containerStyle}>
+            <ClassName className={className} register={register} />
+            <ClassDescription register={register} detail={detail} handleTextAreaHeight={handleTextAreaHeight} />
+            <ClassRepresentImage
+              imgFile={imgFile}
+              previewImg={previewImg}
+              imgRef={imgRef}
+              handleUploaderClick={handleUploaderClick}
+              uploadImgFile={uploadImgFile}
+              deleteImgFile={deleteImgFile}
+            />
+            <ClassGenre selectedGenre={selectedGenre} toggleCategory={toggleCategory} />
+            <ClassLevel selectedLevel={selectedLevel} toggleLevel={toggleLevel} />
+            <ClassRecommend
+              register={register}
+              recommendation={recommendation}
+              handleTextAreaHeight={handleTextAreaHeight}
+            />
+            <ClassSchedule
+              openBottomSheet={initTimeAndOpenBottomSheet}
+              times={times}
+              handleRemoveTime={handleRemoveTime}
+            />
+            <ClassPersonnel maxReservationCount={maxReservationCount} register={register} />
+            <ClassPlace
+              register={register}
+              isUndecidedLocation={isUndecidedLocation}
+              handleHasLocation={handleNoneLocationCheck}
+              defaultPlace={defaultPlace}
+              handleDefaultPlace={handleDefaultPlace}
+              selectedLocation={selectedLocation}
+              setSelectedLocation={handleSelectLocation}
+              handleRemoveLocation={handleRemoveLocation}
+              locationList={locationList}
+            />
+            <ClassAmount price={price} register={register} />
+          </div>
+          <div className={styles.buttonContainerStyle}>
+            <BoxButton
+              type="submit"
+              disabled={
+                !isButtonActive({
+                  className,
+                  detail,
+                  selectedGenre,
+                  selectedLevel,
+                  recommendation,
+                  maxReservationCount,
+                  price,
+                })
+              }>
+              완료
+            </BoxButton>
+          </div>
+        </form>
+      </FormProvider>
 
       {isBottomSheetOpen && (
         <ClassRegisterBottomSheet
@@ -235,6 +299,7 @@ const ClassRegister = () => {
           setSelectedTime={setSelectedTime}
           selectedTime={selectedTime}
           handleAddTime={handleAddTime}
+          times={times}
         />
       )}
     </>
diff --git a/src/pages/instructor/classRegister/components/ClassAmount/ClassAmount.tsx b/src/pages/instructor/classRegister/components/ClassAmount/ClassAmount.tsx
index 13453cf9a..158d1554a 100644
--- a/src/pages/instructor/classRegister/components/ClassAmount/ClassAmount.tsx
+++ b/src/pages/instructor/classRegister/components/ClassAmount/ClassAmount.tsx
@@ -1,4 +1,6 @@
-import type { UseFormRegister } from 'react-hook-form';
+import type { ChangeEvent } from 'react';
+import type { FieldError, UseFormRegister } from 'react-hook-form';
+import { useFormContext } from 'react-hook-form';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { CLASS_AMOUNT_SUBTITLE } from '@/pages/instructor/classRegister/constants/registerSectionText';
 import Input from '@/shared/components/Input/Input';
@@ -14,8 +16,12 @@ interface ClassAmountPropTypes {
 
 const ClassAmount = ({ price, register }: ClassAmountPropTypes) => {
   const { name, onChange, ref } = register('price');
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.price as FieldError | undefined;
 
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
     if (ONLY_NUMBER.test(e.target.value)) {
       onChange(e);
     }
@@ -24,14 +30,22 @@ const ClassAmount = ({ price, register }: ClassAmountPropTypes) => {
   return (
     <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 20, width: '100%' })}>
       <Description title="수강료" subTitle={CLASS_AMOUNT_SUBTITLE} />
-      <Input
-        name={name}
-        ref={ref}
-        placeholder="0"
-        value={price}
-        onChange={handleChange}
-        rightAddOn={<Text tag="b2_sb_long">원</Text>}
-      />
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 4 })}>
+        <Input
+          name={name}
+          ref={ref}
+          placeholder="0"
+          value={price}
+          onChange={handleChange}
+          rightAddOn={<Text tag="b2_sb_long">원</Text>}
+          isError={!!error}
+        />
+        {error && (
+          <Text tag="b3_r" color="alert3">
+            {error.message}
+          </Text>
+        )}
+      </div>
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassDescription/ClassDescription.tsx b/src/pages/instructor/classRegister/components/ClassDescription/ClassDescription.tsx
index 5ae2f081f..782e52558 100644
--- a/src/pages/instructor/classRegister/components/ClassDescription/ClassDescription.tsx
+++ b/src/pages/instructor/classRegister/components/ClassDescription/ClassDescription.tsx
@@ -1,6 +1,11 @@
-import React from 'react';
+import type { ChangeEvent } from 'react';
+import { useState } from 'react';
 import type { FieldError, UseFormRegister } from 'react-hook-form';
-import { textareaStyle } from '@/pages/instructor/classRegister/components/ClassDescription/classDescription.css';
+import { useFormContext } from 'react-hook-form';
+import {
+  textareaStyle,
+  textareaErrorStyle,
+} from '@/pages/instructor/classRegister/components/ClassDescription/classDescription.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { MAX_CLASS_DESCRIPTION_LENGTH } from '@/pages/instructor/classRegister/constants/formLimit';
 import {
@@ -8,15 +13,22 @@ import {
   CLASS_DESCRIPTION_SUBTITLE,
 } from '@/pages/instructor/classRegister/constants/registerSectionText';
 import type { ClassRegisterFormTypes } from '@/pages/instructor/classRegister/types/classRegisterForm';
+import Text from '@/shared/components/Text/Text';
 import { sprinkles } from '@/shared/styles/sprinkles.css';
 
 interface ClassDescriptionPropTypes {
   register: UseFormRegister<ClassRegisterFormTypes>;
-  error: FieldError | undefined;
-  handleTextAreaHeight: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
+  handleTextAreaHeight?: (event: ChangeEvent<HTMLTextAreaElement>) => void;
+  detail?: string;
 }
 
-const ClassDescription = ({ register, handleTextAreaHeight }: ClassDescriptionPropTypes) => {
+const ClassDescription = ({ register, handleTextAreaHeight, detail = '' }: ClassDescriptionPropTypes) => {
+  const [isInputFocused, setIsInputFocused] = useState(false);
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.detail as FieldError | undefined;
+
   return (
     <div
       className={sprinkles({
@@ -27,14 +39,26 @@ const ClassDescription = ({ register, handleTextAreaHeight }: ClassDescriptionPr
         mb: 40,
       })}>
       <Description title="클래스 설명" subTitle={CLASS_DESCRIPTION_SUBTITLE} />
-      <textarea
-        {...register('detail', {
-          onChange: handleTextAreaHeight,
-        })}
-        placeholder={CLASS_DESCRIPTION_PLACEHOLDER}
-        className={textareaStyle}
-        maxLength={MAX_CLASS_DESCRIPTION_LENGTH}
-      />
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 4 })}>
+        <textarea
+          {...register('detail', {
+            onChange: handleTextAreaHeight,
+          })}
+          placeholder={CLASS_DESCRIPTION_PLACEHOLDER}
+          className={error ? textareaErrorStyle : textareaStyle}
+          maxLength={MAX_CLASS_DESCRIPTION_LENGTH}
+          onFocus={() => setIsInputFocused(true)}
+          onBlur={() => setIsInputFocused(false)}
+        />
+        <div className={sprinkles({ display: 'flex', justifyContent: 'space-between' })}>
+          <Text tag="b3_r" color="alert3">
+            {error && error.message}
+          </Text>
+          <Text tag="c1_m" color={error ? 'alert3' : detail && isInputFocused ? 'main4' : 'gray9'}>
+            {detail?.length || 0} / {MAX_CLASS_DESCRIPTION_LENGTH}
+          </Text>
+        </div>
+      </div>
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassDescription/classDescription.css.ts b/src/pages/instructor/classRegister/components/ClassDescription/classDescription.css.ts
index 9f78beb55..d19385ab7 100644
--- a/src/pages/instructor/classRegister/components/ClassDescription/classDescription.css.ts
+++ b/src/pages/instructor/classRegister/components/ClassDescription/classDescription.css.ts
@@ -1,7 +1,6 @@
 import { style } from '@vanilla-extract/css';
 import { vars } from '@/shared/styles/theme.css';
 
-// 클래스 설명
 export const textareaStyle = style({
   width: '100%',
   padding: '1.6rem 1.8rem',
@@ -10,6 +9,7 @@ export const textareaStyle = style({
   resize: 'none',
   borderRadius: '4px',
   backgroundColor: vars.colors.gray01,
+  outline: 'none',
   ...vars.fonts.b2_sb_long,
 
   '::placeholder': {
@@ -20,3 +20,23 @@ export const textareaStyle = style({
     outline: `1px solid ${vars.colors.main04}`,
   },
 });
+
+export const textareaErrorStyle = style({
+  width: '100%',
+  padding: '1.6rem 1.8rem',
+  height: '9.8rem',
+  overflow: 'hidden',
+  resize: 'none',
+  borderRadius: '4px',
+  backgroundColor: vars.colors.gray01,
+  outline: `1px solid ${vars.colors.alert03}`,
+  ...vars.fonts.b2_sb_long,
+
+  '::placeholder': {
+    color: vars.colors.gray05,
+  },
+
+  ':focus': {
+    outline: `1px solid ${vars.colors.alert03}`,
+  },
+});
diff --git a/src/pages/instructor/classRegister/components/ClassGenre/ClassGenre.tsx b/src/pages/instructor/classRegister/components/ClassGenre/ClassGenre.tsx
index 0f86c6c0f..d37ccd346 100644
--- a/src/pages/instructor/classRegister/components/ClassGenre/ClassGenre.tsx
+++ b/src/pages/instructor/classRegister/components/ClassGenre/ClassGenre.tsx
@@ -1,7 +1,10 @@
+import type { FieldError } from 'react-hook-form';
+import { useFormContext } from 'react-hook-form';
 import { genreButtonContainerStyle } from '@/pages/instructor/classRegister/components/ClassGenre/classGenre.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { CLASS_GENRE_SUBTITLE } from '@/pages/instructor/classRegister/constants/registerSectionText';
 import GenreButton from '@/pages/search/components/TabContainer/TagSection/BottomSheet/GenreButton/GenreButton';
+import Text from '@/shared/components/Text/Text';
 import { GENRE_CATEGORY } from '@/shared/constants';
 import { sprinkles } from '@/shared/styles/sprinkles.css';
 
@@ -11,27 +14,43 @@ interface ClassGenrePropTypes {
 }
 
 const ClassGenre = ({ selectedGenre, toggleCategory }: ClassGenrePropTypes) => {
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.selectedGenre as FieldError | undefined;
+
   return (
     <div
       className={sprinkles({
         display: 'flex',
         flexDirection: 'column',
         width: '100%',
-        gap: 20,
         mb: 40,
       })}>
       <Description title="장르" subTitle={CLASS_GENRE_SUBTITLE} />
 
-      <div className={genreButtonContainerStyle}>
-        {GENRE_CATEGORY.flat().map((category, index) => (
-          <GenreButton
-            key={index}
-            category={category}
-            isSelected={selectedGenre === category}
-            onClick={() => toggleCategory(category)}
-          />
-        ))}
+      <div className={sprinkles({ mt: 20 })}>
+        <div className={genreButtonContainerStyle}>
+          {GENRE_CATEGORY.flatMap((categoryArray) =>
+            categoryArray.map((category, index) => (
+              <GenreButton
+                key={`${category}-${index}`}
+                category={category}
+                isSelected={selectedGenre === category}
+                onClick={() => toggleCategory(category)}
+              />
+            ))
+          )}
+        </div>
       </div>
+
+      {error && (
+        <div className={sprinkles({ mt: 4 })}>
+          <Text tag="b3_r" color="alert3">
+            {error.message}
+          </Text>
+        </div>
+      )}
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassLevel/ClassLevel.tsx b/src/pages/instructor/classRegister/components/ClassLevel/ClassLevel.tsx
index 55a2f3960..036a13da3 100644
--- a/src/pages/instructor/classRegister/components/ClassLevel/ClassLevel.tsx
+++ b/src/pages/instructor/classRegister/components/ClassLevel/ClassLevel.tsx
@@ -1,7 +1,9 @@
+import type { FieldError } from 'react-hook-form';
+import { useFormContext } from 'react-hook-form';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { CLASS_LEVEL_SUBTITLE } from '@/pages/instructor/classRegister/constants/registerSectionText';
-import Flex from '@/shared/components/Flex/Flex';
 import LevelButton from '@/shared/components/LevelButton/LevelButton';
+import Text from '@/shared/components/Text/Text';
 import { LEVEL } from '@/shared/constants';
 import { sprinkles } from '@/shared/styles/sprinkles.css';
 
@@ -11,26 +13,53 @@ interface ClassLevelPropTypes {
 }
 
 const ClassLevel = ({ selectedLevel, toggleLevel }: ClassLevelPropTypes) => {
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.selectedLevel as FieldError | undefined;
+
   return (
     <div
       className={sprinkles({
         display: 'flex',
         flexDirection: 'column',
         width: '100%',
-        gap: 20,
         mb: 40,
       })}>
       <Description title="난이도" subTitle={CLASS_LEVEL_SUBTITLE} />
-      <Flex direction="column" gap="0.8rem" width="100%">
-        {LEVEL.map((level) => (
-          <LevelButton
-            key={level.title}
-            level={level}
-            isSelected={selectedLevel === level.title}
-            onClick={() => toggleLevel(level.title)}
-          />
-        ))}
-      </Flex>
+
+      <div
+        className={sprinkles({
+          display: 'flex',
+          flexDirection: 'column',
+          width: '100%',
+          mt: 20,
+        })}>
+        <div
+          className={sprinkles({
+            display: 'flex',
+            flexDirection: 'column',
+            gap: 8,
+            width: '100%',
+          })}>
+          {LEVEL.map((level) => (
+            <LevelButton
+              key={level.title}
+              level={level}
+              isSelected={selectedLevel === level.title}
+              onClick={() => toggleLevel(level.title)}
+              isError={!!error}
+            />
+          ))}
+        </div>
+        {error && (
+          <div className={sprinkles({ mt: 4 })}>
+            <Text tag="b3_r" color="alert3">
+              {error.message}
+            </Text>
+          </div>
+        )}
+      </div>
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassName/ClassName.tsx b/src/pages/instructor/classRegister/components/ClassName/ClassName.tsx
index c4d6c5843..955232f38 100644
--- a/src/pages/instructor/classRegister/components/ClassName/ClassName.tsx
+++ b/src/pages/instructor/classRegister/components/ClassName/ClassName.tsx
@@ -1,4 +1,6 @@
+import { useState } from 'react';
 import type { FieldError, UseFormRegister } from 'react-hook-form';
+import { useFormContext } from 'react-hook-form';
 import { nameLengthStyle } from '@/pages/instructor/classRegister/components/ClassName/className.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { MAX_CLASS_NAME_LENGTH } from '@/pages/instructor/classRegister/constants/formLimit';
@@ -11,11 +13,19 @@ import { sprinkles } from '@/shared/styles/sprinkles.css';
 interface ClassNamePropTypes {
   className: string;
   register: UseFormRegister<ClassRegisterFormTypes>;
-  error: FieldError | undefined;
 }
 
-const ClassName = ({ register, error, className }: ClassNamePropTypes) => {
+const ClassName = ({ register, className }: ClassNamePropTypes) => {
   const { name, onChange, ref } = register('className');
+  const [isInputFocused, setIsInputFocused] = useState(false);
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.className as FieldError | undefined;
+
+  const handleFocusChange = (isFocused: boolean) => {
+    setIsInputFocused(isFocused);
+  };
 
   return (
     <div
@@ -35,12 +45,16 @@ const ClassName = ({ register, error, className }: ClassNamePropTypes) => {
           isError={!!error}
           placeholder="클래스명을 입력해 주세요"
           maxLength={MAX_CLASS_NAME_LENGTH}
+          onFocusChange={handleFocusChange}
         />
         <div className={sprinkles({ display: 'flex', justifyContent: 'space-between' })}>
           <Text tag="b3_r" color="alert3">
             {error && error.message}
           </Text>
-          <Text tag="c1_m" color={className ? 'gray9' : 'gray4'} className={nameLengthStyle}>
+          <Text
+            tag="c1_m"
+            color={error ? 'alert3' : className && isInputFocused ? 'main4' : 'gray9'}
+            className={nameLengthStyle}>
             {className.length} / {MAX_CLASS_NAME_LENGTH}
           </Text>
         </div>
diff --git a/src/pages/instructor/classRegister/components/ClassPersonnel/ClassPersonnel.tsx b/src/pages/instructor/classRegister/components/ClassPersonnel/ClassPersonnel.tsx
index 89a9cac44..74a347ecc 100644
--- a/src/pages/instructor/classRegister/components/ClassPersonnel/ClassPersonnel.tsx
+++ b/src/pages/instructor/classRegister/components/ClassPersonnel/ClassPersonnel.tsx
@@ -1,4 +1,5 @@
-import type { UseFormRegister } from 'react-hook-form';
+import type { FieldError, UseFormRegister } from 'react-hook-form';
+import { useFormContext } from 'react-hook-form';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { CLASS_PERSONNEL_SUBTITLE } from '@/pages/instructor/classRegister/constants/registerSectionText';
 import type { ClassRegisterFormTypes } from '@/pages/instructor/classRegister/types/classRegisterForm';
@@ -14,6 +15,10 @@ interface ClassPersonnelPropTypes {
 
 const ClassPersonnel = ({ register, maxReservationCount }: ClassPersonnelPropTypes) => {
   const { name, onChange, ref } = register('maxReservationCount');
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.maxReservationCount as FieldError | undefined;
 
   const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     if (ONLY_NUMBER.test(e.target.value)) {
@@ -31,14 +36,22 @@ const ClassPersonnel = ({ register, maxReservationCount }: ClassPersonnelPropTyp
         mb: 40,
       })}>
       <Description title="모집 인원" subTitle={CLASS_PERSONNEL_SUBTITLE} />
-      <Input
-        value={maxReservationCount}
-        name={name}
-        onChange={handleChange}
-        ref={ref}
-        placeholder="0"
-        rightAddOn={<Text tag="b2_sb_long">명</Text>}
-      />
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 4 })}>
+        <Input
+          value={maxReservationCount}
+          name={name}
+          onChange={handleChange}
+          ref={ref}
+          placeholder="0"
+          rightAddOn={<Text tag="b2_sb_long">명</Text>}
+          isError={!!error}
+        />
+        {error && (
+          <Text tag="b3_r" color="alert3">
+            {error.message}
+          </Text>
+        )}
+      </div>
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassPlace/ClassPlace.tsx b/src/pages/instructor/classRegister/components/ClassPlace/ClassPlace.tsx
index 8b471750c..4be306601 100644
--- a/src/pages/instructor/classRegister/components/ClassPlace/ClassPlace.tsx
+++ b/src/pages/instructor/classRegister/components/ClassPlace/ClassPlace.tsx
@@ -1,5 +1,6 @@
 import { type ChangeEvent } from 'react';
 import type { UseFormRegister } from 'react-hook-form';
+import { useFormContext } from 'react-hook-form';
 import * as styles from '@/pages/instructor/classRegister/components/ClassPlace/classPlace.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import {
@@ -25,6 +26,7 @@ interface ClassPlacePropTypes {
   selectedLocation: LocationTypes | null;
   locationList: LocationsData;
   setSelectedLocation: (location: LocationTypes | null) => void;
+  handleRemoveLocation: () => void;
 }
 
 const ClassPlace = ({
@@ -36,9 +38,14 @@ const ClassPlace = ({
   selectedLocation,
   locationList,
   setSelectedLocation,
+  handleRemoveLocation,
 }: ClassPlacePropTypes) => {
   const shouldShowEmptyMessage = Array.isArray(locationList?.locations) && locationList.locations.length === 0;
   const shouldShowLocationList = Array.isArray(locationList?.locations) && locationList.locations.length > 0;
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.selectedLocation as { message?: string } | undefined;
 
   const { onChange, ref, name } = register('detailedAddress');
 
@@ -48,74 +55,82 @@ const ClassPlace = ({
         display: 'flex',
         flexDirection: 'column',
         width: '100%',
-        gap: 20,
         mb: 40,
       })}>
-      <div className={sprinkles({ display: 'flex', width: '100%', justifyContent: 'space-between' })}>
-        <Description title="클래스 장소" subTitle={CLASS_PLACE_SUBTITLE} />
-        <div className={styles.checkboxContainerStyle}>
-          <Text tag="b3_m" color="gray8">
-            장소 미정
-          </Text>
-          {isUndecidedLocation ? (
-            <BtnCheck width={'2rem'} onClick={handleHasLocation} />
-          ) : (
-            <div className={styles.checkboxStyle} onClick={handleHasLocation} />
-          )}
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 20 })}>
+        <div className={sprinkles({ display: 'flex', width: '100%', justifyContent: 'space-between' })}>
+          <Description title="클래스 장소" subTitle={CLASS_PLACE_SUBTITLE} />
+          <div className={styles.checkboxContainerStyle}>
+            <Text tag="b3_m" color="gray8">
+              장소 미정
+            </Text>
+            {isUndecidedLocation ? (
+              <BtnCheck width={'2rem'} onClick={handleHasLocation} />
+            ) : (
+              <div className={styles.checkboxStyle} onClick={handleHasLocation} />
+            )}
+          </div>
         </div>
-      </div>
 
-      {!isUndecidedLocation && (
-        <div className={sprinkles({ display: 'flex', width: '100%', flexDirection: 'column', gap: 8 })}>
-          {selectedLocation ? (
-            <div className={styles.selectedLocationContainerStyle}>
-              <Text tag="b2_sb_long">{selectedLocation.location}</Text>
-              <IcXCircleGray0424 width={'2.4rem'} onClick={() => setSelectedLocation(null)} />
-            </div>
-          ) : (
-            <>
-              <Input
-                value={defaultPlace}
-                onChange={handleDefaultPlace}
-                placeholder={CLASS_DEFAULT_PLACE_PLACEHOLDER}
-                onKeyDown={(e) => {
-                  if (e.key === 'Enter') {
-                    e.preventDefault();
-                  }
-                }}
-                rightAddOn={<IcSearchGray width={'2.4rem'} />}
-              />
+        {!isUndecidedLocation && (
+          <div className={sprinkles({ display: 'flex', width: '100%', flexDirection: 'column', gap: 8 })}>
+            {selectedLocation ? (
+              <div className={styles.selectedLocationContainerStyle}>
+                <Text tag="b2_sb_long">{selectedLocation.location}</Text>
+                <IcXCircleGray0424 width={'2.4rem'} onClick={handleRemoveLocation} />
+              </div>
+            ) : (
+              <>
+                <Input
+                  value={defaultPlace}
+                  onChange={handleDefaultPlace}
+                  placeholder={CLASS_DEFAULT_PLACE_PLACEHOLDER}
+                  onKeyDown={(e) => {
+                    if (e.key === 'Enter') {
+                      e.preventDefault();
+                    }
+                  }}
+                  rightAddOn={<IcSearchGray width={'2.4rem'} />}
+                />
 
-              {shouldShowEmptyMessage && (
-                <div className={styles.locationEmptyContainerStyle}>해당 검색어에 대한 검색 결과가 없어요!</div>
-              )}
+                {shouldShowEmptyMessage && (
+                  <div className={styles.locationEmptyContainerStyle}>해당 검색어에 대한 검색 결과가 없어요!</div>
+                )}
 
-              {shouldShowLocationList && (
-                <div className={styles.locationListContainerStyle}>
-                  <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 10 })}>
-                    {locationList.locations.map((item, idx) => (
-                      <div
-                        key={idx}
-                        onClick={() => setSelectedLocation(item)}
-                        className={styles.locationItemContainerStyle}>
-                        {idx !== 0 && <div className={styles.dividerStyle} />}
-                        <div className={sprinkles({ display: 'flex', flexDirection: 'column', width: '100%' })}>
-                          <Text tag="b2_sb_long" color="gray10">
-                            {item.location}
-                          </Text>
-                          <Text tag="b3_sb" color="gray5">
-                            {item.streetAddress}
-                          </Text>
+                {shouldShowLocationList && (
+                  <div className={styles.locationListContainerStyle}>
+                    <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 10 })}>
+                      {locationList.locations.map((location, idx) => (
+                        <div
+                          key={idx}
+                          onClick={() => setSelectedLocation(location)}
+                          className={styles.locationItemContainerStyle}>
+                          {idx !== 0 && <div className={styles.dividerStyle} />}
+                          <div className={sprinkles({ display: 'flex', flexDirection: 'column', width: '100%' })}>
+                            <Text tag="b2_sb_long" color="gray10">
+                              {location.location}
+                            </Text>
+                            <Text tag="b3_sb" color="gray5">
+                              {location.streetAddress}
+                            </Text>
+                          </div>
                         </div>
-                      </div>
-                    ))}
+                      ))}
+                    </div>
                   </div>
-                </div>
-              )}
-            </>
-          )}
+                )}
+              </>
+            )}
 
-          <Input onChange={onChange} name={name} ref={ref} placeholder={CLASS_DETAIL_PLACE_PLACEHOLDER} />
+            <Input onChange={onChange} name={name} ref={ref} placeholder={CLASS_DETAIL_PLACE_PLACEHOLDER} />
+          </div>
+        )}
+      </div>
+      {error?.message && (
+        <div className={sprinkles({ mt: 4 })}>
+          <Text tag="b3_r" color="alert3">
+            {error.message}
+          </Text>
         </div>
       )}
     </div>
diff --git a/src/pages/instructor/classRegister/components/ClassRecommend/ClassRecommend.tsx b/src/pages/instructor/classRegister/components/ClassRecommend/ClassRecommend.tsx
index 055c59608..9c1539a46 100644
--- a/src/pages/instructor/classRegister/components/ClassRecommend/ClassRecommend.tsx
+++ b/src/pages/instructor/classRegister/components/ClassRecommend/ClassRecommend.tsx
@@ -1,21 +1,34 @@
-import React from 'react';
-import type { UseFormRegister } from 'react-hook-form';
-import { textareaStyle } from '@/pages/instructor/classRegister/components/ClassRecommend/classRecommend.css';
+import type { ChangeEvent } from 'react';
+import { useState } from 'react';
+import type { FieldError, UseFormRegister } from 'react-hook-form';
+import { useFormContext } from 'react-hook-form';
+import {
+  textareaStyle,
+  textareaErrorStyle,
+} from '@/pages/instructor/classRegister/components/ClassRecommend/classRecommend.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { MAX_RECOMMEND_LENGTH } from '@/pages/instructor/classRegister/constants/formLimit';
 import {
   CLASS_RECOMMEND_PLACEHOLDER,
   CLASS_RECOMMEND_SUBTITLE,
 } from '@/pages/instructor/classRegister/constants/registerSectionText';
+import Text from '@/shared/components/Text/Text';
 import { sprinkles } from '@/shared/styles/sprinkles.css';
 import type { ClassRegisterFormTypes } from '../../types/classRegisterForm';
 
 interface ClassRecommendPropTypes {
   register: UseFormRegister<ClassRegisterFormTypes>;
-  handleTextAreaHeight: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
+  handleTextAreaHeight?: (event: ChangeEvent<HTMLTextAreaElement>) => void;
+  recommendation?: string;
 }
 
-const ClassRecommend = ({ register, handleTextAreaHeight }: ClassRecommendPropTypes) => {
+const ClassRecommend = ({ register, handleTextAreaHeight, recommendation = '' }: ClassRecommendPropTypes) => {
+  const [isInputFocused, setIsInputFocused] = useState(false);
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.recommendation as FieldError | undefined;
+
   return (
     <div
       className={sprinkles({
@@ -26,14 +39,26 @@ const ClassRecommend = ({ register, handleTextAreaHeight }: ClassRecommendPropTy
         mb: 30,
       })}>
       <Description title="클래스 추천 대상" subTitle={CLASS_RECOMMEND_SUBTITLE} />
-      <textarea
-        {...register('recommendation', {
-          onChange: handleTextAreaHeight,
-        })}
-        placeholder={CLASS_RECOMMEND_PLACEHOLDER}
-        className={textareaStyle}
-        maxLength={MAX_RECOMMEND_LENGTH}
-      />
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 4 })}>
+        <textarea
+          {...register('recommendation', {
+            onChange: handleTextAreaHeight,
+          })}
+          placeholder={CLASS_RECOMMEND_PLACEHOLDER}
+          className={error ? textareaErrorStyle : textareaStyle}
+          maxLength={MAX_RECOMMEND_LENGTH}
+          onFocus={() => setIsInputFocused(true)}
+          onBlur={() => setIsInputFocused(false)}
+        />
+        <div className={sprinkles({ display: 'flex', justifyContent: 'space-between' })}>
+          <Text tag="b3_r" color="alert3">
+            {error && error.message}
+          </Text>
+          <Text tag="c1_m" color={error ? 'alert3' : recommendation && isInputFocused ? 'main4' : 'gray9'}>
+            {recommendation?.length || 0} / {MAX_RECOMMEND_LENGTH}
+          </Text>
+        </div>
+      </div>
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassRecommend/classRecommend.css.ts b/src/pages/instructor/classRegister/components/ClassRecommend/classRecommend.css.ts
index 9f78beb55..d19385ab7 100644
--- a/src/pages/instructor/classRegister/components/ClassRecommend/classRecommend.css.ts
+++ b/src/pages/instructor/classRegister/components/ClassRecommend/classRecommend.css.ts
@@ -1,7 +1,6 @@
 import { style } from '@vanilla-extract/css';
 import { vars } from '@/shared/styles/theme.css';
 
-// 클래스 설명
 export const textareaStyle = style({
   width: '100%',
   padding: '1.6rem 1.8rem',
@@ -10,6 +9,7 @@ export const textareaStyle = style({
   resize: 'none',
   borderRadius: '4px',
   backgroundColor: vars.colors.gray01,
+  outline: 'none',
   ...vars.fonts.b2_sb_long,
 
   '::placeholder': {
@@ -20,3 +20,23 @@ export const textareaStyle = style({
     outline: `1px solid ${vars.colors.main04}`,
   },
 });
+
+export const textareaErrorStyle = style({
+  width: '100%',
+  padding: '1.6rem 1.8rem',
+  height: '9.8rem',
+  overflow: 'hidden',
+  resize: 'none',
+  borderRadius: '4px',
+  backgroundColor: vars.colors.gray01,
+  outline: `1px solid ${vars.colors.alert03}`,
+  ...vars.fonts.b2_sb_long,
+
+  '::placeholder': {
+    color: vars.colors.gray05,
+  },
+
+  ':focus': {
+    outline: `1px solid ${vars.colors.alert03}`,
+  },
+});
diff --git a/src/pages/instructor/classRegister/components/ClassRepresentImage/ClassRepresentImage.tsx b/src/pages/instructor/classRegister/components/ClassRepresentImage/ClassRepresentImage.tsx
index 9f1a7dfd1..dfd7367bc 100644
--- a/src/pages/instructor/classRegister/components/ClassRepresentImage/ClassRepresentImage.tsx
+++ b/src/pages/instructor/classRegister/components/ClassRepresentImage/ClassRepresentImage.tsx
@@ -1,10 +1,11 @@
+import type { FieldError } from 'react-hook-form';
+import { useFormContext } from 'react-hook-form';
 import {
   closeIconStyle,
   inputStyle,
   previewImgStyle,
 } from '@/pages/instructor/classRegister/components/ClassRepresentImage/classRepresentImage.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
-import { MAX_CLASS_REPRESENT_IMAGE_LENGTH } from '@/pages/instructor/classRegister/constants/formLimit';
 import { CLASS_REPRESENT_IMAGE_SUBTITLE } from '@/pages/instructor/classRegister/constants/registerSectionText';
 import IcPlusGray0524 from '@/shared/assets/svg/IcPlusGray0524';
 import IcXCircleMain0324 from '@/shared/assets/svg/IcXCircleMain0324';
@@ -28,37 +29,48 @@ const ClassRepresentImage = ({
   uploadImgFile,
   deleteImgFile,
 }: ClassRepresentImagePropTypes) => {
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.imageUrls as FieldError | undefined;
+
   return (
-    <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 20, width: '100%', mb: 40 })}>
-      <Description title="클래스 대표 이미지" subTitle={CLASS_REPRESENT_IMAGE_SUBTITLE} />
-      <div
-        className={previewImgStyle({ hasImage: !!previewImg })}
-        style={previewImg ? { backgroundImage: `url(${previewImg})` } : {}}
-        onClick={handleUploaderClick}>
-        {!previewImg && (
-          <div className={sprinkles({ display: 'flex', flexDirection: 'column', alignItems: 'center' })}>
-            <IcPlusGray0524 width={'2.4rem'} />
-            <Text tag="c1_r" color="gray5">
-              1/{MAX_CLASS_REPRESENT_IMAGE_LENGTH}
-            </Text>
-          </div>
-        )}
+    <div className={sprinkles({ display: 'flex', flexDirection: 'column', width: '100%', mb: 40 })}>
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 20 })}>
+        <Description title="클래스 대표 이미지" subTitle={CLASS_REPRESENT_IMAGE_SUBTITLE} />
+        <div
+          className={previewImgStyle({ hasImage: !!previewImg })}
+          style={previewImg ? { backgroundImage: `url(${previewImg})` } : {}}
+          onClick={handleUploaderClick}>
+          {!previewImg && (
+            <div className={sprinkles({ display: 'flex', flexDirection: 'column', alignItems: 'center' })}>
+              <IcPlusGray0524 width={'2.4rem'} />
+            </div>
+          )}
 
-        {imgFile && (
-          <div className={closeIconStyle} onClick={deleteImgFile}>
-            <IcXCircleMain0324 width={'2.4rem'} />
-          </div>
-        )}
-      </div>
+          {imgFile && (
+            <div className={closeIconStyle} onClick={deleteImgFile}>
+              <IcXCircleMain0324 width={'2.4rem'} />
+            </div>
+          )}
+        </div>
 
-      <input
-        type="file"
-        accept="image/*"
-        id="profileImg"
-        className={inputStyle}
-        onChange={uploadImgFile}
-        ref={imgRef}
-      />
+        <input
+          type="file"
+          accept="image/*"
+          id="profileImg"
+          className={inputStyle}
+          onChange={uploadImgFile}
+          ref={imgRef}
+        />
+      </div>
+      {error && (
+        <div className={sprinkles({ mt: 4 })}>
+          <Text tag="b3_r" color="alert3">
+            {error.message}
+          </Text>
+        </div>
+      )}
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterBottomSheet/ClassRegisterBottomSheet.tsx b/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterBottomSheet/ClassRegisterBottomSheet.tsx
index 1c27ce210..45b4afd03 100644
--- a/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterBottomSheet/ClassRegisterBottomSheet.tsx
+++ b/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterBottomSheet/ClassRegisterBottomSheet.tsx
@@ -1,9 +1,9 @@
+import type { MouseEvent } from 'react';
 import * as styles from '@/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterBottomSheet/classRegisterBottomSheet.css';
 import ClassRegisterFunnel from '@/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/ClassRegisterFunnel';
-import { ROUTES_CONFIG } from '@/routes/routesConfig';
+import { useLocalFunnel } from '@/pages/instructor/classRegister/hooks/useLocalFunnel';
 import BoxButton from '@/shared/components/BoxButton/BoxButton';
 import { CLASS_REGISTER_TOTAL_STEP } from '@/shared/constants';
-import { useFunnel } from '@/shared/hooks/useFunnel';
 
 interface ClassRegisterBottomSheetPropTypes {
   onClose: () => void;
@@ -18,6 +18,7 @@ interface ClassRegisterBottomSheetPropTypes {
   setSelectedTime: (value: number | null) => void;
   selectedTime: number | null;
   handleAddTime: () => void;
+  times: { startTime: string; endTime: string; date: string; duration: number }[];
 }
 
 const ClassRegisterBottomSheet = ({
@@ -33,30 +34,39 @@ const ClassRegisterBottomSheet = ({
   setSelectedTime,
   selectedTime,
   handleAddTime,
+  times,
 }: ClassRegisterBottomSheetPropTypes) => {
-  const { Funnel, Step, currentStep, setStep } = useFunnel(
-    CLASS_REGISTER_TOTAL_STEP,
-    `${ROUTES_CONFIG.classRegister}`,
-    false // 완료 페이지 없음 (false)
-  );
+  const { Funnel, Step, currentStep, setStep } = useLocalFunnel(CLASS_REGISTER_TOTAL_STEP);
 
-  const handleSheetComplete = () => {
-    setStep(1);
-    onClose();
-    handleAddTime();
-    handleDateAndTimeReset();
-  };
+  document.body.style.overflow = 'hidden';
 
-  const handleDateAndTimeReset = () => {
+  const resetDateAndTime = () => {
     setStartDate('');
     setHour(12);
     setMinute(0);
     setAmpm('AM');
+    setSelectedTime(null);
+  };
+
+  const handleSheetComplete = () => {
+    setStep(-currentStep + 1);
+    document.body.style.overflow = '';
+    onClose();
+    handleAddTime();
+    resetDateAndTime();
+  };
+
+  const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
+    if (e.target === e.currentTarget) {
+      document.body.style.overflow = '';
+      onClose();
+      resetDateAndTime();
+    }
   };
 
   return (
-    <div className={styles.bottomSheetContainerStyle}>
-      <div className={styles.mainWrapperStyle}>
+    <div className={styles.bottomSheetContainerStyle} onClick={handleOverlayClick}>
+      <div className={styles.mainWrapperStyle} onClick={(e) => e.stopPropagation()}>
         <ClassRegisterFunnel
           Funnel={Funnel}
           Step={Step}
@@ -70,6 +80,7 @@ const ClassRegisterBottomSheet = ({
           setAmpm={setAmpm}
           setSelectedTime={setSelectedTime}
           selectedTime={selectedTime}
+          times={times}
         />
 
         <div className={styles.buttonWrapperStyle}>
diff --git a/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/ClassRegisterFunnel.tsx b/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/ClassRegisterFunnel.tsx
index b8cce157d..cb1ae492a 100644
--- a/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/ClassRegisterFunnel.tsx
+++ b/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/ClassRegisterFunnel.tsx
@@ -19,6 +19,7 @@ interface ClassRegisterBottomSheetProps {
   setAmpm: (value: string) => void;
   setSelectedTime: (value: number | null) => void;
   selectedTime: number | null;
+  times?: { startTime: string; endTime: string; date: string; duration: number }[];
 }
 
 const ClassRegisterFunnel = ({
@@ -34,12 +35,13 @@ const ClassRegisterFunnel = ({
   setAmpm,
   setSelectedTime,
   selectedTime,
+  times = [],
 }: ClassRegisterBottomSheetProps) => {
   return (
     <div className={funnelContainerStyle}>
       <Funnel>
         <Step name="1">
-          <DateStep startDate={startDate} setStartDate={setStartDate} />
+          <DateStep startDate={startDate} setStartDate={setStartDate} times={times} />
         </Step>
         <Step name="2">
           <TimeStep
@@ -51,6 +53,8 @@ const ClassRegisterFunnel = ({
             setAmpm={setAmpm}
             setSelectedTime={setSelectedTime}
             selectedTime={selectedTime}
+            times={times}
+            startDate={startDate}
           />
         </Step>
       </Funnel>
diff --git a/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/classRegisterFunnel.css.ts b/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/classRegisterFunnel.css.ts
index 9ebd1bcb4..283b389cd 100644
--- a/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/classRegisterFunnel.css.ts
+++ b/src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterFunnel/classRegisterFunnel.css.ts
@@ -2,5 +2,5 @@ import { style } from '@vanilla-extract/css';
 
 export const funnelContainerStyle = style({
   width: '100%',
-  padding: '6.4rem 2rem 15rem 2rem',
+  padding: '4.4rem 2rem 15rem 2rem',
 });
diff --git a/src/pages/instructor/classRegister/components/ClassSchedule/ClassSchedule.tsx b/src/pages/instructor/classRegister/components/ClassSchedule/ClassSchedule.tsx
index af5e2130f..f20113ce3 100644
--- a/src/pages/instructor/classRegister/components/ClassSchedule/ClassSchedule.tsx
+++ b/src/pages/instructor/classRegister/components/ClassSchedule/ClassSchedule.tsx
@@ -1,3 +1,4 @@
+import { useFormContext } from 'react-hook-form';
 import * as styles from '@/pages/instructor/classRegister/components/ClassSchedule/classSchedule.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { CLASS_SCHEDULE_SUBTITLE } from '@/pages/instructor/classRegister/constants/registerSectionText';
@@ -15,40 +16,53 @@ interface ClassSchedulePropTypes {
 }
 
 const ClassSchedule = ({ openBottomSheet, times, handleRemoveTime }: ClassSchedulePropTypes) => {
+  const {
+    formState: { errors },
+  } = useFormContext();
+  const error = errors.times as { message?: string } | undefined;
+
   return (
     <div
       className={sprinkles({
         display: 'flex',
         flexDirection: 'column',
-        gap: 20,
-        mb: 50,
         width: '100%',
+        mb: 50,
       })}>
-      <Description title="클래스 일정" subTitle={CLASS_SCHEDULE_SUBTITLE} />
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 20 })}>
+        <Description title="클래스 일정" subTitle={CLASS_SCHEDULE_SUBTITLE} />
 
-      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 8, width: '100%' })}>
-        {times.map((time, idx) => (
-          <div className={styles.scheduleItemContainerStyle}>
-            <div className={sprinkles({ display: 'flex', gap: 16, alignItems: 'center' })}>
-              <div className={styles.tagStyle}>{idx + 1}회차</div>
-              <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 4 })}>
-                <Text tag="b2_sb"> {formatDate(time.date)}</Text>
-                <Text tag="b3_m" color="gray7">
-                  {`${calculatePeriod(time.startTime, time.endTime).startTime} ~ ${
-                    calculatePeriod(time.startTime, time.endTime).formattedEndTime
-                  }`}{' '}
-                  (총 {time.duration} 시간)
-                </Text>
+        <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 8, width: '100%' })}>
+          {times.map((time, idx) => (
+            <div key={idx} className={styles.scheduleItemContainerStyle}>
+              <div className={sprinkles({ display: 'flex', gap: 16, alignItems: 'center' })}>
+                <div className={styles.tagStyle}>{idx + 1}회차</div>
+                <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 4 })}>
+                  <Text tag="b2_sb"> {formatDate(time.date)}</Text>
+                  <Text tag="b3_m" color="gray7">
+                    {`${calculatePeriod(time.startTime, time.endTime).startTime} ~ ${
+                      calculatePeriod(time.startTime, time.endTime).formattedEndTime
+                    }`}{' '}
+                    (총 {time.duration} 시간)
+                  </Text>
+                </div>
               </div>
+              <IcXCircleGray0424 width={'2.4rem'} onClick={() => handleRemoveTime(idx)} />
             </div>
-            <IcXCircleGray0424 width={'2.4rem'} onClick={() => handleRemoveTime(idx)} />
-          </div>
-        ))}
+          ))}
 
-        <div className={styles.addInputBoxStyle} onClick={openBottomSheet}>
-          <IcPlusGray0524 width={'2.4rem'} />
+          <div className={styles.addInputBoxStyle} onClick={openBottomSheet}>
+            <IcPlusGray0524 width={'2.4rem'} />
+          </div>
         </div>
       </div>
+      {error?.message && (
+        <div className={sprinkles({ mt: 4 })}>
+          <Text tag="b3_r" color="alert3">
+            {error.message}
+          </Text>
+        </div>
+      )}
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassSchedule/DateStep/DateStep.tsx b/src/pages/instructor/classRegister/components/ClassSchedule/DateStep/DateStep.tsx
index 9c9e02c1d..a78df1ecf 100644
--- a/src/pages/instructor/classRegister/components/ClassSchedule/DateStep/DateStep.tsx
+++ b/src/pages/instructor/classRegister/components/ClassSchedule/DateStep/DateStep.tsx
@@ -1,14 +1,36 @@
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
 import CalendarCustom from '@/pages/search/components/Calendar/Calendar';
+import { getDateWithoutTime } from '@/shared/utils/timeUtils';
 
 interface DateStepProps {
   startDate: string;
   setStartDate: (date: string) => void;
+  times?: { startTime: string; endTime: string; date: string; duration: number }[];
 }
 
-const DateStep = ({ startDate, setStartDate }: DateStepProps) => {
+const DateStep = ({ startDate, setStartDate, times = [] }: DateStepProps) => {
   const [selectedDate, setSelectedDate] = useState<string>(startDate);
 
+  // 최소 선택 가능 날짜 계산
+  const minSelectableDate = useMemo(() => {
+    // 오늘 날짜를 한 번만 계산
+    const today = getDateWithoutTime(new Date());
+
+    // 일정이 없으면 오늘 날짜 반환
+    if (times.length === 0) {
+      return today;
+    }
+
+    // 각 일정의 종료 날짜들 중 가장 늦은 날짜 찾기
+    const latestEndDate = times.reduce((latest, time) => {
+      const endDate = getDateWithoutTime(new Date(time.endTime));
+      return endDate > latest ? endDate : latest;
+    }, new Date(0));
+
+    // 오늘보다 이전 날짜면 오늘을 반환, 아니면 찾은 최신 날짜 반환
+    return latestEndDate < today ? today : latestEndDate;
+  }, [times]);
+
   const handleDateChange = (date: string) => {
     setSelectedDate(date);
     setStartDate(date);
@@ -16,7 +38,12 @@ const DateStep = ({ startDate, setStartDate }: DateStepProps) => {
 
   return (
     <div className={`custom-register-style`}>
-      <CalendarCustom startDate={selectedDate} setStartDate={handleDateChange} isSearch={false} />
+      <CalendarCustom
+        startDate={selectedDate}
+        setStartDate={handleDateChange}
+        isSearch={false}
+        minCustomDate={minSelectableDate}
+      />
     </div>
   );
 };
diff --git a/src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeControl/TimeControl.tsx b/src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeControl/TimeControl.tsx
index 1b8dea57d..ffbafe2da 100644
--- a/src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeControl/TimeControl.tsx
+++ b/src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeControl/TimeControl.tsx
@@ -9,10 +9,16 @@ interface TimeControlProps {
   value: number | string;
   onIncrease: () => void;
   onDecrease: () => void;
+  disableIncrease?: boolean;
+  disableDecrease?: boolean;
 }
-const TimeControl = ({ label, value, onIncrease, onDecrease }: TimeControlProps) => (
+const TimeControl = ({ label, value, onIncrease, onDecrease, disableIncrease, disableDecrease }: TimeControlProps) => (
   <Flex direction="column" width="100%" align="center" gap="0.4rem">
-    <button onClick={onIncrease} className={styles.buttonStyle}>
+    <button
+      onClick={onIncrease}
+      className={styles.buttonStyle}
+      disabled={disableIncrease}
+      style={disableIncrease ? { opacity: 0.5, cursor: 'not-allowed' } : undefined}>
       <IcArrowUpGray1032 width={'3.2rem'} />
     </button>
     <div className={styles.timeValueWrapperStyle}>
@@ -20,7 +26,11 @@ const TimeControl = ({ label, value, onIncrease, onDecrease }: TimeControlProps)
         {label === 'minute' ? (value === 0 ? '00' : '30') : value}
       </Head>
     </div>
-    <button onClick={onDecrease} className={styles.buttonStyle}>
+    <button
+      onClick={onDecrease}
+      className={styles.buttonStyle}
+      disabled={disableDecrease}
+      style={disableDecrease ? { opacity: 0.5, cursor: 'not-allowed' } : undefined}>
       <IcArrowDownGray1032 width={'3.2rem'} />
     </button>
   </Flex>
diff --git a/src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeStep.tsx b/src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeStep.tsx
index 0c611989c..56c05d654 100644
--- a/src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeStep.tsx
+++ b/src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeStep.tsx
@@ -1,9 +1,21 @@
+import { useMemo } from 'react';
 import TimeControl from '@/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeControl/TimeControl';
 import Flex from '@/shared/components/Flex/Flex';
 import Head from '@/shared/components/Head/Head';
 import Tag from '@/shared/components/Tag/Tag';
+import Text from '@/shared/components/Text/Text';
 import { TIME_RANGE } from '@/shared/constants';
-import { decreaseHour, decreaseMinute, increaseHour, increaseMinute, toggleAmpm } from '@/shared/utils/timeUtils';
+import {
+  decreaseHour,
+  decreaseMinute,
+  increaseHour,
+  increaseMinute,
+  toggleAmpm,
+  convertTo24HourFormat,
+  convertToMinutes,
+  isTimeOverlapping,
+  formatTimeDisplay,
+} from '@/shared/utils/timeUtils';
 
 interface TimeStepProps {
   hour: number;
@@ -14,6 +26,8 @@ interface TimeStepProps {
   setAmpm: (value: string) => void;
   setSelectedTime: (value: number | null) => void;
   selectedTime: number | null;
+  times?: { startTime: string; endTime: string; date: string; duration: number }[];
+  startDate: string;
 }
 
 const TimeStep = ({
@@ -25,51 +39,262 @@ const TimeStep = ({
   setAmpm,
   setSelectedTime,
   selectedTime,
+  times = [],
+  startDate,
 }: TimeStepProps) => {
+  // 선택된 날짜에 영향을 미치는 회차의 가장 늦은 종료 시간 찾기
+  const latestEndTime = useMemo(() => {
+    if (times.length === 0) return null;
+
+    // 선택된 날짜 정보를 반복문 밖에서 한 번만 계산
+    const selectedDateObj = new Date(startDate);
+    const selectedDateStr = selectedDateObj.toDateString();
+
+    let latest: Date | null = null;
+
+    // 모든 일정 검사
+    for (const time of times) {
+      const existingEndTime = new Date(time.endTime);
+      const endTimeDate = existingEndTime.toDateString();
+
+      // 종료 날짜가 선택된 날짜와 다르면 건너뜀
+      if (endTimeDate !== selectedDateStr) continue;
+
+      // 최신 종료 시간 업데이트
+      if (latest === null || existingEndTime > latest) {
+        latest = existingEndTime;
+      }
+    }
+
+    return latest;
+  }, [times, startDate]);
+
+  // 최소 선택 가능 시간 계산
+  const minTimeConstraints = useMemo(() => {
+    if (!latestEndTime) return null;
+
+    const minHour = latestEndTime.getHours();
+    const minMinute = latestEndTime.getMinutes();
+    const minAmpm = minHour >= 12 ? 'PM' : 'AM';
+    const display12Hour = minHour % 12 === 0 ? 12 : minHour % 12;
+
+    return {
+      hour: display12Hour,
+      minute: minMinute,
+      ampm: minAmpm,
+    };
+  }, [latestEndTime]);
+
+  // 현재 시간이 최소 제한 시간보다 이전인지 확인하고 필요시 조정
+  const checkAndAdjustTime = () => {
+    if (minTimeConstraints && latestEndTime !== null) {
+      // 시간을 분으로 변환하여 비교
+      const currentTotalMinutes = convertToMinutes(hour, minute, ampm);
+      const minTotalMinutes = convertToMinutes(
+        minTimeConstraints.hour,
+        minTimeConstraints.minute,
+        minTimeConstraints.ampm
+      );
+
+      // 현재 시간이 최소 시간보다 이전이면 최소 시간으로 설정
+      if (currentTotalMinutes < minTotalMinutes) {
+        setHour(minTimeConstraints.hour);
+        setMinute(minTimeConstraints.minute);
+        setAmpm(minTimeConstraints.ampm);
+        return true;
+      }
+    }
+    return false;
+  };
+
+  // 시간 제약 조건이 변경될 때 자동으로 시간 조정
+  // 주의: 이 방식은 렌더링마다 실행되지만 조건을 충족할 때만 상태를 변경함
+  if (minTimeConstraints && latestEndTime !== null) {
+    checkAndAdjustTime();
+  }
+
+  // 시간 겹침 확인
+  const hasTimeConflict = (hourValue: number, minuteValue: number, ampmValue: string, duration: number) => {
+    if (times.length === 0) return false;
+
+    // 선택된 시간을 24시간 형식으로 변환
+    const selectedHour = convertTo24HourFormat(hourValue, ampmValue);
+
+    // 새로운 일정의 시작 및 종료 시간 계산
+    const dateParts = startDate.split('-');
+    const year = parseInt(dateParts[0]);
+    const month = parseInt(dateParts[1]) - 1; // 월은 0부터 시작
+    const day = parseInt(dateParts[2]);
+
+    const newStartDate = new Date(year, month, day, selectedHour, minuteValue, 0, 0);
+    const newEndDate = new Date(newStartDate);
+    newEndDate.setHours(newEndDate.getHours() + duration);
+
+    // 모든 일정과 시간 충돌 검사 (날짜 제한 없이)
+    for (const existingTime of times) {
+      const existingStartTime = new Date(existingTime.startTime);
+      const existingEndTime = new Date(existingTime.endTime);
+
+      // timeUtils의 isTimeOverlapping 함수 사용
+      if (isTimeOverlapping(newStartDate, newEndDate, existingStartTime, existingEndTime)) {
+        return true;
+      }
+    }
+
+    // 가장 늦은 종료 시간보다 이전에 시작하는지 검사
+    // 이 부분은 같은 날짜의 일정 뿐만 아니라, 이전 일정의 종료 시간도 고려해야 함
+    for (const existingTime of times) {
+      const existingEndTime = new Date(existingTime.endTime);
+
+      // 새 일정 시작 시간이 기존 일정 종료 시간보다 이전이면서
+      // 새 일정 시작 날짜가 기존 일정 종료 날짜와 같거나 이후인 경우
+      if (
+        newStartDate.getTime() < existingEndTime.getTime() &&
+        newStartDate.toDateString() >= existingEndTime.toDateString()
+      ) {
+        return true;
+      }
+    }
+
+    return false;
+  };
+
+  // 시간이 최소 제한 시간보다 이전인지 확인
+  const isBeforeMinTime = (newHour: number, newMinute: number, newAmpm: string) => {
+    if (!minTimeConstraints || latestEndTime === null) return false;
+
+    // convertToMinutes 함수 사용하여 시간 비교
+    const selectedTotalMinutes = convertToMinutes(newHour, newMinute, newAmpm);
+    const minTotalMinutes = convertToMinutes(
+      minTimeConstraints.hour,
+      minTimeConstraints.minute,
+      minTimeConstraints.ampm
+    );
+
+    return selectedTotalMinutes < minTotalMinutes;
+  };
+
+  // 시간 감소가 최소 시간 제한을 넘는지 확인
+  const willHourDecreaseViolateMinTime = () => {
+    const newHour = decreaseHour(hour);
+    return isBeforeMinTime(newHour, minute, ampm);
+  };
+
+  // 분 감소가 최소 시간 제한을 넘는지 확인
+  const willMinuteDecreaseViolateMinTime = () => {
+    if (minute === 30) {
+      // 30분에서 0분으로 감소
+      return isBeforeMinTime(hour, 0, ampm);
+    } else if (minute === 0) {
+      // 0분에서 이전 시간 30분으로 감소
+      const newHour = decreaseHour(hour);
+      return isBeforeMinTime(newHour, 30, ampm);
+    }
+    return false; // 다른 케이스는 없지만 타입 안전성을 위해 추가
+  };
+
+  // AM/PM 전환이 최소 시간 제한을 넘는지 확인
+  const willAmpmToggleViolateMinTime = () => {
+    const newAmpm = toggleAmpm(ampm);
+    return isBeforeMinTime(hour, minute, newAmpm);
+  };
+
+  // 시간 증가 처리
+  const handleIncreaseHour = () => {
+    const newHour = increaseHour(hour);
+    setHour(newHour);
+  };
+
+  // 시간 감소 처리
+  const handleDecreaseHour = () => {
+    const newHour = decreaseHour(hour);
+    setHour(newHour);
+  };
+
+  // 분 증가 처리
+  const handleIncreaseMinute = () => {
+    const newMinute = increaseMinute({ currentMinute: minute, setHour });
+    setMinute(newMinute);
+  };
+
+  // 분 감소 처리
+  const handleDecreaseMinute = () => {
+    const newMinute = decreaseMinute({ currentMinute: minute, setHour });
+    setMinute(newMinute);
+  };
+
+  // AM/PM 전환 처리
+  const handleToggleAmpm = () => {
+    const newAmpm = toggleAmpm(ampm);
+    setAmpm(newAmpm);
+  };
+
   const handleTagClick = (id: number) => {
     setSelectedTime(selectedTime === id ? null : id);
   };
 
+  // 최소 가능 시간 표시 (formatTimeDisplay 함수 사용)
+  const minTimeDisplay = minTimeConstraints
+    ? formatTimeDisplay(minTimeConstraints.hour, minTimeConstraints.minute, minTimeConstraints.ampm)
+    : null;
+
   return (
     <Flex direction="column">
       <Head level="h2" tag="b1_sb">
         클래스 시작 시간
       </Head>
 
+      {latestEndTime !== null && minTimeDisplay && (
+        <Flex paddingTop="0.8rem">
+          <Text tag="b3_r" color="main3">
+            이전 회차 종료 시간({minTimeDisplay}) 이후로만 등록 가능합니다.
+          </Text>
+        </Flex>
+      )}
+
       <Flex justify="center" gap="1rem" paddingTop="1.6rem" paddingBottom="2.4rem" width="100%">
         <TimeControl
           label="hour"
           value={hour}
-          onIncrease={() => setHour(increaseHour(hour))}
-          onDecrease={() => setHour(decreaseHour(hour))}
+          onIncrease={handleIncreaseHour}
+          onDecrease={handleDecreaseHour}
+          disableDecrease={willHourDecreaseViolateMinTime()}
         />
         <TimeControl
           label="minute"
           value={minute}
-          onIncrease={() => setMinute(increaseMinute({ currentMinute: minute, setHour }))}
-          onDecrease={() => setMinute(decreaseMinute({ currentMinute: minute, setHour }))}
+          onIncrease={handleIncreaseMinute}
+          onDecrease={handleDecreaseMinute}
+          disableDecrease={willMinuteDecreaseViolateMinTime()}
         />
         <TimeControl
           label="ampm"
           value={ampm}
-          onIncrease={() => setAmpm(toggleAmpm(ampm))}
-          onDecrease={() => setAmpm(toggleAmpm(ampm))}
+          onIncrease={handleToggleAmpm}
+          onDecrease={handleToggleAmpm}
+          disableIncrease={willAmpmToggleViolateMinTime()}
+          disableDecrease={willAmpmToggleViolateMinTime()}
         />
       </Flex>
+
       <Head level="h2" tag="b1_sb">
         클래스 진행 시간
       </Head>
 
       <Flex wrap="wrap" gap="0.8rem" paddingTop="1.6rem">
-        {TIME_RANGE.map((time) => (
-          <Tag
-            key={time.id}
-            size="timeSelector"
-            isSelected={selectedTime === time.id}
-            onClick={() => handleTagClick(time.id)}>
-            {time.label}
-          </Tag>
-        ))}
+        {TIME_RANGE.map((time) => {
+          const timeConflict = hasTimeConflict(hour, minute, ampm, time.id);
+          return (
+            <Tag
+              key={time.id}
+              size="timeSelector"
+              isSelected={selectedTime === time.id}
+              onClick={() => !timeConflict && handleTagClick(time.id)}
+              style={timeConflict ? { opacity: 0.5, cursor: 'not-allowed' } : undefined}>
+              {time.label}
+            </Tag>
+          );
+        })}
       </Flex>
     </Flex>
   );
diff --git a/src/pages/instructor/classRegister/components/Description/index.tsx b/src/pages/instructor/classRegister/components/Description/index.tsx
index 25eb4409e..625332f02 100644
--- a/src/pages/instructor/classRegister/components/Description/index.tsx
+++ b/src/pages/instructor/classRegister/components/Description/index.tsx
@@ -10,7 +10,7 @@ interface DescriptionProps {
 
 const Description = ({ title, subTitle }: DescriptionProps) => {
   return (
-    <Flex direction="column" gap="0.8rem">
+    <Flex direction="column" gap="0.4rem">
       <Flex gap="0.8rem">
         <Head level="h2" tag="h5_sb">
           {title}
diff --git a/src/pages/instructor/classRegister/hooks/useClassRegisterForm.ts b/src/pages/instructor/classRegister/hooks/useClassRegisterForm.ts
index 50cf99cce..303ddff31 100644
--- a/src/pages/instructor/classRegister/hooks/useClassRegisterForm.ts
+++ b/src/pages/instructor/classRegister/hooks/useClassRegisterForm.ts
@@ -46,25 +46,47 @@ export const useClassRegisterForm = () => {
         duration: selectedTime,
       };
 
-      setTimes([...times, newTime]);
+      const newTimes = [...times, newTime];
+      setTimes(newTimes);
+      return newTimes;
     }
+    return times;
   };
 
   const handleRemoveTime = (index: number) => {
     const updatedTimes = times.filter((_, idx) => idx !== index);
     setTimes(updatedTimes);
+    return updatedTimes;
   };
 
   const handleDetailPlace = (e: ChangeEvent<HTMLInputElement>) => {
     setDetailPlace(e.target.value);
   };
 
-  const isButtonActive = () => {
-    if (isUndecidedLocation) {
-      return imageUrls.imageUrls.length > 0 && selectedTime && times.length > 0;
-    }
+  const isButtonActive = (externalFormFields?: {
+    className?: string;
+    detail?: string;
+    selectedGenre?: string;
+    selectedLevel?: string;
+    recommendation?: string;
+    maxReservationCount?: string;
+    price?: string;
+  }) => {
+    const isExternalFieldsValid = externalFormFields
+      ? !!externalFormFields.className &&
+        !!externalFormFields.detail &&
+        !!externalFormFields.selectedGenre &&
+        !!externalFormFields.selectedLevel &&
+        !!externalFormFields.recommendation &&
+        !!externalFormFields.maxReservationCount &&
+        !!externalFormFields.price
+      : true;
+
+    const isInternalFieldsValid = imageUrls.imageUrls.length > 0 && times.length > 0;
+
+    const isLocationValid = isUndecidedLocation || selectedLocation !== null;
 
-    return imageUrls.imageUrls.length > 0 && selectedTime && selectedLocation && times.length > 0;
+    return isExternalFieldsValid && isInternalFieldsValid && isLocationValid;
   };
 
   return {
diff --git a/src/pages/instructor/classRegister/hooks/useLocalFunnel.tsx b/src/pages/instructor/classRegister/hooks/useLocalFunnel.tsx
new file mode 100644
index 000000000..4d9d2de7d
--- /dev/null
+++ b/src/pages/instructor/classRegister/hooks/useLocalFunnel.tsx
@@ -0,0 +1,39 @@
+import type { ReactElement, ReactNode } from 'react';
+import { useCallback, useState } from 'react';
+
+export interface StepProps {
+  name: string;
+  children: ReactNode;
+}
+
+export interface FunnelProps {
+  children: ReactElement<StepProps>[];
+}
+
+export const useLocalFunnel = (totalSteps: number) => {
+  const [currentStep, setCurrentStep] = useState(1);
+
+  const setStep = (stepChange: number) => {
+    const newStep = currentStep + stepChange;
+
+    if (newStep < 1) {
+      return;
+    } else if (newStep > totalSteps) {
+      setCurrentStep(1);
+    } else {
+      setCurrentStep(newStep);
+    }
+  };
+
+  const Step = useCallback(({ children }: StepProps) => <>{children}</>, []);
+
+  const Funnel = useCallback(
+    ({ children }: FunnelProps) => {
+      const targetStep = children.find((childStep) => childStep.props.name === String(currentStep));
+      return <>{targetStep}</>;
+    },
+    [currentStep]
+  );
+
+  return { Funnel, Step, setStep, currentStep };
+};
diff --git a/src/pages/instructor/classRegister/schema/classRegisterSchema.ts b/src/pages/instructor/classRegister/schema/classRegisterSchema.ts
index bc8f59f60..395b1d97b 100644
--- a/src/pages/instructor/classRegister/schema/classRegisterSchema.ts
+++ b/src/pages/instructor/classRegister/schema/classRegisterSchema.ts
@@ -22,24 +22,29 @@ export const classRegisterSchema = z.object({
     .string()
     .min(MIN_CLASS_DESCRIPTION_LENGTH, '설명을 입력해주세요')
     .max(MAX_CLASS_DESCRIPTION_LENGTH, '설명은 최대 300자까지 입력 가능합니다'),
-  imageUrls: z.string().url('유효한 이미지 URL이 아닙니다'),
+  imageUrls: z.string().url('대표 이미지를 등록해주세요'),
   selectedGenre: z.string().min(MIN_CLASS_GENRE_LENGTH, '장르를 선택해주세요'),
   selectedLevel: z.string().min(MIN_CLASS_LEVEL_LENGTH, '레벨을 선택해주세요'),
   recommendation: z
     .string()
-    .min(MIN_RECOMMEND_LENGTH, '추천사항을 입력해주세요')
-    .max(MAX_RECOMMEND_LENGTH, '추천사항은 최대 200자까지 입력 가능합니다'),
-  maxReservationCount: z.string().regex(/^\d+$/, '숫자만 입력해주세요'),
-  price: z.string().regex(/^\d+$/, '숫자만 입력해주세요'),
+    .min(MIN_RECOMMEND_LENGTH, '추천 대상을 입력해주세요')
+    .max(MAX_RECOMMEND_LENGTH, '추천 대상은 최대 200자까지 입력 가능합니다'),
+  maxReservationCount: z.string().regex(/^\d+$/, '인원을 입력해주세요'),
+  price: z.string().regex(/^\d+$/, '수강료를 입력해주세요'),
   isUndecidedLocation: z.boolean(),
-  selectedLocation: z.union([
-    z.object({
-      location: z.string(),
-      streetAddress: z.string().nullable(),
-      oldStreetAddress: z.string().nullable(),
-    }),
-    z.null(),
-  ]),
+  selectedLocation: z
+    .union([
+      z.object({
+        location: z.string(),
+        streetAddress: z.string().nullable(),
+        oldStreetAddress: z.string().nullable(),
+      }),
+      z.null(),
+    ])
+    .refine((val) => {
+      if (val === null) return false;
+      return val.location !== '';
+    }, '장소를 입력해주세요'),
   detailedAddress: z.string().optional(),
   times: z
     .array(
@@ -48,5 +53,5 @@ export const classRegisterSchema = z.object({
         endTime: z.string(),
       })
     )
-    .min(MIN_TIMES_LENGTH, '수업 시간을 추가해주세요'),
+    .min(MIN_TIMES_LENGTH, '일정을 추가해주세요'),
 });
diff --git a/src/pages/search/components/Calendar/Calendar.tsx b/src/pages/search/components/Calendar/Calendar.tsx
index 680b8a79e..2c0354589 100644
--- a/src/pages/search/components/Calendar/Calendar.tsx
+++ b/src/pages/search/components/Calendar/Calendar.tsx
@@ -10,9 +10,17 @@ interface CalendarCustomPropTypes {
   setStartDate: (date: string) => void;
   setEndDate?: (date: string) => void;
   isSearch: boolean;
+  minCustomDate?: Date;
 }
 
-const CalendarCustom = ({ startDate, endDate, setStartDate, setEndDate, isSearch }: CalendarCustomPropTypes) => {
+const CalendarCustom = ({
+  startDate,
+  endDate,
+  setStartDate,
+  setEndDate,
+  isSearch,
+  minCustomDate,
+}: CalendarCustomPropTypes) => {
   const dateChangeHandler = (value: Value | Range<Value>) => {
     // 시작과 끝 날짜 모두 선택한 경우
     if (Array.isArray(value)) {
@@ -44,7 +52,7 @@ const CalendarCustom = ({ startDate, endDate, setStartDate, setEndDate, isSearch
   const today = new Date();
   today.setHours(0, 0, 0, 0);
 
-  const minDate = isSearch ? undefined : today;
+  const minDate = minCustomDate ? minCustomDate : isSearch ? undefined : today;
 
   const tileClassName = ({ date }: { date: Date }) => {
     if (minDate && date < minDate) {
@@ -65,7 +73,7 @@ const CalendarCustom = ({ startDate, endDate, setStartDate, setEndDate, isSearch
         defaultValue={
           startDate ? (endDate ? [new Date(startDate), new Date(endDate)] : new Date(startDate)) : undefined
         }
-        minDate={isSearch ? undefined : today}
+        minDate={minDate}
         tileClassName={tileClassName}
       />
     </div>
diff --git a/src/shared/components/Input/Input.tsx b/src/shared/components/Input/Input.tsx
index 67eace18b..fb616b576 100644
--- a/src/shared/components/Input/Input.tsx
+++ b/src/shared/components/Input/Input.tsx
@@ -1,19 +1,39 @@
 import clsx from 'clsx';
 import type { ForwardedRef, InputHTMLAttributes, ReactNode } from 'react';
-import { forwardRef } from 'react';
+import { forwardRef, useState } from 'react';
 import * as style from '@/shared/components/Input/input.css';
 
 interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
   isError?: boolean;
   rightAddOn?: ReactNode;
-  isFocused?: boolean;
+  onFocusChange?: (isFocused: boolean) => void;
 }
 
 const Input = (
-  { isError, className, value, rightAddOn, isFocused, ...props }: InputProps,
+  { isError, className, value, rightAddOn, onFocusChange, ...props }: InputProps,
   ref: ForwardedRef<HTMLInputElement>
 ) => {
-  const inputState = isError ? 'error' : isFocused ? 'focus' : undefined;
+  const [isFocused, setIsFocused] = useState(false);
+
+  const handleFocus = () => {
+    setIsFocused(true);
+    onFocusChange?.(true);
+  };
+
+  const handleBlur = () => {
+    setIsFocused(false);
+    onFocusChange?.(false);
+  };
+
+  const defineInputState = (isError?: boolean, isFocused?: boolean) => {
+    if (isError) {
+      return 'error';
+    } else if (isFocused) {
+      return 'focus';
+    }
+  };
+
+  const inputState = defineInputState(isError, isFocused);
 
   return (
     <div className={style.containerStyle({ defineInputState: inputState })}>
@@ -21,6 +41,8 @@ const Input = (
         ref={ref}
         type="text"
         className={clsx(className, style.inputStyle)}
+        onFocus={handleFocus}
+        onBlur={handleBlur}
         value={value}
         {...props}
         aria-invalid={isError ? 'true' : 'false'}
@@ -30,4 +52,4 @@ const Input = (
   );
 };
 
-export default forwardRef(Input);
+export default forwardRef(Input);
\ No newline at end of file
diff --git a/src/shared/components/LevelButton/LevelButton.tsx b/src/shared/components/LevelButton/LevelButton.tsx
index 20c6f37be..75723d497 100644
--- a/src/shared/components/LevelButton/LevelButton.tsx
+++ b/src/shared/components/LevelButton/LevelButton.tsx
@@ -14,12 +14,15 @@ interface LevelButtonProps {
   };
   isSelected: boolean | null;
   onClick: () => void;
+  isError?: boolean;
 }
 
-const LevelButton = ({ level, isSelected, onClick }: LevelButtonProps) => {
+const LevelButton = ({ level, isSelected, onClick, isError = false }: LevelButtonProps) => {
   return (
     <button
-      className={isSelected === null ? onboardingLevelButtonStyle : levelButtonStyle({ selected: isSelected })}
+      className={
+        isSelected === null ? onboardingLevelButtonStyle : levelButtonStyle({ selected: isSelected, error: isError })
+      }
       onClick={onClick}
       type="button">
       {level.icon}
diff --git a/src/shared/components/LevelButton/levelButton.css.ts b/src/shared/components/LevelButton/levelButton.css.ts
index df11d2951..cb6a20c7a 100644
--- a/src/shared/components/LevelButton/levelButton.css.ts
+++ b/src/shared/components/LevelButton/levelButton.css.ts
@@ -23,9 +23,15 @@ export const levelButtonStyle = recipe({
         backgroundColor: vars.colors.gray01,
       },
     },
+    error: {
+      false: {
+        border: 'none',
+      },
+    },
   },
   defaultVariants: {
     selected: false,
+    error: false,
   },
 });
 
diff --git a/src/shared/utils/timeUtils.ts b/src/shared/utils/timeUtils.ts
index cb03476f7..8c9aa18d5 100644
--- a/src/shared/utils/timeUtils.ts
+++ b/src/shared/utils/timeUtils.ts
@@ -41,6 +41,39 @@ export const decreaseMinute = ({
 // AM/PM 토글
 export const toggleAmpm = (currentAmpm: string): string => (currentAmpm === 'AM' ? 'PM' : 'AM');
 
+// 날짜에서 시간 정보를 제거 (00:00:00으로 설정)
+export const getDateWithoutTime = (date: Date): Date => {
+  const newDate = new Date(date);
+  newDate.setHours(0, 0, 0, 0);
+  return newDate;
+};
+
+// 12시간 형식에서 24시간 형식으로 변환
+export const convertTo24HourFormat = (hour: number, ampm: string): number => {
+  if (ampm === 'PM' && hour !== 12) {
+    return hour + 12;
+  } else if (ampm === 'AM' && hour === 12) {
+    return 0;
+  }
+  return hour;
+};
+
+// 시간과 분을 분 단위로 변환
+export const convertToMinutes = (hour: number, minute: number, ampm: string): number => {
+  const hour24 = convertTo24HourFormat(hour, ampm);
+  return hour24 * 60 + minute;
+};
+
+// 시간 간격(분)이 겹치는지 확인
+export const isTimeOverlapping = (startTime1: Date, endTime1: Date, startTime2: Date, endTime2: Date): boolean => {
+  return startTime1 < endTime2 && endTime1 > startTime2;
+};
+
+// 시간을 읽기 쉬운 형식으로 포맷팅
+export const formatTimeDisplay = (hour: number, minute: number, ampm: string): string => {
+  return `${hour}:${minute === 0 ? '00' : minute} ${ampm}`;
+};
+
 export const formatToISOString = (date: string, hour: number, minute: number, ampm: string, selectedTime: number) => {
   let adjustedHour = hour;
   if (ampm === 'PM' && hour !== 12) {


Reviews:
user: login: constantly-dev
body: 진짜 수고많으셨어요...! 상태 관리할 것도 많고, 시간 계산 로직이 너무 복잡해서 생각할 것이 많았을텐데 👍 
코멘트 몇개 남겼으니 확인해주세요!! 완벽하게 모든 로직을 다 이해는 못했는데... 이후에 리팩토링 같이 하면서 정리하면 좋을 것 같습니다 💯 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T06:22:28Z
user: login: rtttr1
body: 고생하셨습니다!! 디테일한 부분을 모두 챙기셨네요~~
코멘트 몇개 남겼으니 확인해주세요~~!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T10:21:59Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T10:52:54Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T11:46:34Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T12:10:17Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T12:11:09Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:41:05Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:42:20Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:43:27Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:45:28Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:47:35Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T13:50:39Z
user: login: KIMGEONHWI
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T14:11:20Z

Review comments:
diff_hunk: @@ -10,28 +11,39 @@ import type { ClassRegisterFormTypes } from '../../types/classRegisterForm';
 interface ClassAmountPropTypes {
   price: string;
   register: UseFormRegister<ClassRegisterFormTypes>;
+  error?: FieldError;
+  onPriceChange?: (e: ChangeEvent<HTMLInputElement>) => void;
 }
 
-const ClassAmount = ({ price, register }: ClassAmountPropTypes) => {
+const ClassAmount = ({ price, register, error, onPriceChange }: ClassAmountPropTypes) => {
   const { name, onChange, ref } = register('price');
 
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
     if (ONLY_NUMBER.test(e.target.value)) {
       onChange(e);
+      onPriceChange?.(e);
path: src/pages/instructor/classRegister/components/ClassAmount/ClassAmount.tsx
user: login: constantly-dev
body: 값을 추적하는 react hook form의 `onChange`는 따로 있는데 해당 `onPriceChange`는 `optional`인데 언제 사용하는 용도인가요? 

created_at: 2025-05-22T02:22:14Z
author_association: MEMBER
diff_hunk: @@ -27,14 +35,26 @@ const ClassDescription = ({ register, handleTextAreaHeight }: ClassDescriptionPr
         mb: 40,
       })}>
       <Description title="클래스 설명" subTitle={CLASS_DESCRIPTION_SUBTITLE} />
-      <textarea
-        {...register('detail', {
-          onChange: handleTextAreaHeight,
-        })}
-        placeholder={CLASS_DESCRIPTION_PLACEHOLDER}
-        className={textareaStyle}
-        maxLength={MAX_CLASS_DESCRIPTION_LENGTH}
-      />
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 4 })}>
+        <textarea
+          {...register('detail', {
+            onChange: handleTextAreaHeight,
+          })}
+          placeholder={CLASS_DESCRIPTION_PLACEHOLDER}
+          className={error ? textareaErrorStyle : textareaStyle}
+          maxLength={MAX_CLASS_DESCRIPTION_LENGTH}
path: src/pages/instructor/classRegister/components/ClassDescription/ClassDescription.tsx
user: login: constantly-dev
body: 상수화 너무 깔끔합니다~ 👍 
created_at: 2025-05-22T02:24:00Z
author_association: MEMBER
diff_hunk: @@ -27,14 +35,26 @@ const ClassDescription = ({ register, handleTextAreaHeight }: ClassDescriptionPr
         mb: 40,
       })}>
       <Description title="클래스 설명" subTitle={CLASS_DESCRIPTION_SUBTITLE} />
-      <textarea
-        {...register('detail', {
-          onChange: handleTextAreaHeight,
-        })}
-        placeholder={CLASS_DESCRIPTION_PLACEHOLDER}
-        className={textareaStyle}
-        maxLength={MAX_CLASS_DESCRIPTION_LENGTH}
-      />
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 4 })}>
+        <textarea
+          {...register('detail', {
+            onChange: handleTextAreaHeight,
+          })}
+          placeholder={CLASS_DESCRIPTION_PLACEHOLDER}
+          className={error ? textareaErrorStyle : textareaStyle}
+          maxLength={MAX_CLASS_DESCRIPTION_LENGTH}
+          onFocus={() => setIsInputFocused(true)}
+          onBlur={() => setIsInputFocused(false)}
+        />
+        <div className={sprinkles({ display: 'flex', justifyContent: 'space-between' })}>
+          <Text tag="b3_r" color="alert3">
+            {error && error.message}
+          </Text>
+          <Text tag="c1_m" color={detail && isInputFocused ? 'main4' : 'gray9'}>
+            {detail?.length || 0} / {MAX_CLASS_DESCRIPTION_LENGTH}
+          </Text>
path: src/pages/instructor/classRegister/components/ClassDescription/ClassDescription.tsx
user: login: constantly-dev
body: 사실 글자수 `text`도 에러와 `focus`에 따른 색상 변화가 필요합니다..!
![image](https://github.com/user-attachments/assets/7fed5445-64fb-4210-9b6b-005e67498c50)

이렇게 구현이 되어 있는데 사실 

![image](https://github.com/user-attachments/assets/0ea8467e-e81b-4f4d-a324-f3dfdfb107ff)

이렇게 구현이 되어야 합니다!

사실 그래서 이 부분이 그때 말한 input안에 글자수와 에러 메시지를 두고 안에서 focus/error 상태에 따라 스타일 분기를 두도록 하는 것이 좋겠다고 말씀드린 부분이었어요!!

그래서 디자인만 통일되면 `Input` 공컴에서만 구현해두고 `optional`로 쓰면 되니까...!!
일단 QA때는 제가 강사 등록에서 `IntroductionSection`에 구현해둔 상태 관리 참고해서 수정해주시고, 이후에 제가 `Input` 공통 컴포넌트 기/디 상의 후에 `Input` 공컴에 해당 상태 구현 다 하고 반영되어야 하는 부분 모두 수정해두겠습니다!

https://github.com/team-da-sh/dash-client/blob/develop/src/pages/instructorRegister/components/IntroductionSection/IntroductionSection.tsx

created_at: 2025-05-22T02:30:52Z
author_association: MEMBER
diff_hunk: @@ -52,11 +53,24 @@ const ClassRegisterBottomSheet = ({
     setHour(12);
     setMinute(0);
     setAmpm('AM');
+    setSelectedTime(null);
+  };
+
+  const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
+    if (e.target === e.currentTarget) {
+      document.body.style.overflow = '';
+      onClose();
+      setStartDate('');
+      setHour(12);
+      setMinute(0);
+      setAmpm('AM');
+      setSelectedTime(null);
+    }
path: src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterBottomSheet/ClassRegisterBottomSheet.tsx
user: login: constantly-dev
body: 해당 내부 로직을 `reset`과 같은 네이밍으로 따로 함수를 분리하는 것은 어떨까요? 가독성과 역할 분리 측면에서 더 좋을 것 같습니다!
created_at: 2025-05-22T02:34:57Z
author_association: MEMBER
diff_hunk: @@ -1,22 +1,47 @@
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
 import CalendarCustom from '@/pages/search/components/Calendar/Calendar';
+import { getDateWithoutTime } from '@/shared/utils/timeUtils';
 
 interface DateStepProps {
   startDate: string;
   setStartDate: (date: string) => void;
+  times?: { startTime: string; endTime: string; date: string; duration: number }[];
 }
 
-const DateStep = ({ startDate, setStartDate }: DateStepProps) => {
+const DateStep = ({ startDate, setStartDate, times = [] }: DateStepProps) => {
   const [selectedDate, setSelectedDate] = useState<string>(startDate);
 
+  // 최소 선택 가능 날짜 계산
+  const minSelectableDate = useMemo(() => {
+    // 일정이 없으면 오늘 날짜 반환
+    if (times.length === 0) {
+      return getDateWithoutTime(new Date());
+    }
+
+    // 각 일정의 종료 날짜들 중 가장 늦은 날짜 찾기
+    const latestEndDate = times.reduce((latest, time) => {
+      const endDate = getDateWithoutTime(new Date(time.endTime));
+      return endDate > latest ? endDate : latest;
+    }, new Date(0));
+
+    // 오늘보다 이전 날짜면 오늘을 반환, 아니면 찾은 최신 날짜 반환
+    const today = getDateWithoutTime(new Date());
+    return latestEndDate < today ? today : latestEndDate;
+  }, [times]);
path: src/pages/instructor/classRegister/components/ClassSchedule/DateStep/DateStep.tsx
user: login: constantly-dev
body: 위아래 쪽에서 반복적으로 `getDateWithoutTime(new Date());`를 쓰고 있으니 초반에서 `today`로 한번만 설정해서 재사용하는 것은 어떨까요?
created_at: 2025-05-22T04:12:00Z
author_association: MEMBER
diff_hunk: @@ -25,51 +39,265 @@ const TimeStep = ({
   setAmpm,
   setSelectedTime,
   selectedTime,
+  times = [],
+  startDate,
 }: TimeStepProps) => {
+  // 선택된 날짜에 영향을 미치는 회차의 가장 늦은 종료 시간 찾기
+  const latestEndTime = useMemo(() => {
+    if (times.length === 0) return null;
+
+    let latest: Date | null = null;
+
+    // 모든 일정 검사
+    for (const time of times) {
+      const existingEndTime = new Date(time.endTime);
+
+      // 종료 날짜가 현재 선택된 날짜와 같거나
+      // 시작 날짜는 이전이고 종료 날짜는 현재 선택된 날짜인 경우 (날짜를 넘어가는 경우)
+      const endTimeDate = existingEndTime.toDateString();
+      const selectedDateObj = new Date(startDate);
+      const selectedDateStr = selectedDateObj.toDateString();
+
+      // 다음 조건에 해당하는 회차의 종료 시간을 고려:
+      // 1. 종료 시간의 날짜가 현재 선택된 날짜와 같은 경우
+      // 2. 시작 날짜와 종료 날짜가 다른 경우(날짜를 넘어가는 경우),
+      //    종료 날짜가 현재 선택된 날짜와 같은 경우
+      if (endTimeDate === selectedDateStr) {
+        if (latest === null || existingEndTime > latest) {
+          latest = existingEndTime;
+        }
+      }
+    }
+
+    return latest;
+  }, [times, startDate]);
path: src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeStep.tsx
user: login: constantly-dev
body: ### 1️⃣
반복해야 할 time을 사용하는 `existingEndTime` 등을 제외하고 `startDate`같은 건 반복해도 항상 똑같은 값을 가지니 굳이 반복문 안에서 선언할 필요 없을 것 같아요!

```ts
const selectedDateObj = new Date(startDate);
const selectedDateStr = selectedDateObj.toDateString();
```

대표적으로 이 두 코드는 반복문 밖에 있어도 되지 않을까요??

### 2️⃣
그리고 
```ts
if (endTimeDate === selectedDateStr) {
  if (latest === null || existingEndTime > latest) {
    latest = existingEndTime;
  }
}
```

`if`를 중첩해도 되지만 `endTimeDate !== selectedDateStr` 이 두개가 다른 경우를 `early return` 해도 될 것 같아요! 
이건 진짜 건휘님이 보기에 가독성이 좋은 코드를 선택하는 것이 좋을 것 같습니다 👍 


### 3️⃣
해당 코드를 함수형, 선언적으로 고차함수를 이용할 수도 있을 것 같은데 for...of를 사용하신 이유가 있으신지 궁금합니다!
당연히 둘 중 뭐가 무조건 좋다는 없고 단순 해당 코드에서 for...of를 선택하신 이유가 궁금합니다!
created_at: 2025-05-22T04:34:45Z
author_association: MEMBER
diff_hunk: @@ -25,51 +39,265 @@ const TimeStep = ({
   setAmpm,
   setSelectedTime,
   selectedTime,
+  times = [],
+  startDate,
 }: TimeStepProps) => {
+  // 선택된 날짜에 영향을 미치는 회차의 가장 늦은 종료 시간 찾기
+  const latestEndTime = useMemo(() => {
+    if (times.length === 0) return null;
+
+    let latest: Date | null = null;
+
+    // 모든 일정 검사
+    for (const time of times) {
+      const existingEndTime = new Date(time.endTime);
+
+      // 종료 날짜가 현재 선택된 날짜와 같거나
+      // 시작 날짜는 이전이고 종료 날짜는 현재 선택된 날짜인 경우 (날짜를 넘어가는 경우)
+      const endTimeDate = existingEndTime.toDateString();
+      const selectedDateObj = new Date(startDate);
+      const selectedDateStr = selectedDateObj.toDateString();
+
+      // 다음 조건에 해당하는 회차의 종료 시간을 고려:
+      // 1. 종료 시간의 날짜가 현재 선택된 날짜와 같은 경우
+      // 2. 시작 날짜와 종료 날짜가 다른 경우(날짜를 넘어가는 경우),
+      //    종료 날짜가 현재 선택된 날짜와 같은 경우
+      if (endTimeDate === selectedDateStr) {
+        if (latest === null || existingEndTime > latest) {
+          latest = existingEndTime;
+        }
+      }
+    }
+
+    return latest;
+  }, [times, startDate]);
+
+  // 최소 선택 가능 시간 계산
+  const minTimeConstraints = useMemo(() => {
+    if (!latestEndTime) return null;
+
+    const minHour = latestEndTime.getHours();
+    const minMinute = latestEndTime.getMinutes();
+    const minAmpm = minHour >= 12 ? 'PM' : 'AM';
+    const display12Hour = minHour > 12 ? minHour - 12 : minHour === 0 ? 12 : minHour;
path: src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeStep.tsx
user: login: constantly-dev
body: ```ts
const display12Hour = minHour % 12 === 0 ? 12 : minHour % 12;
```

해당 로직 이렇게도 표현 가능할 거 같은데 어떤가요?
created_at: 2025-05-22T04:52:20Z
author_association: MEMBER
diff_hunk: @@ -41,6 +41,44 @@ export const decreaseMinute = ({
 // AM/PM 토글
 export const toggleAmpm = (currentAmpm: string): string => (currentAmpm === 'AM' ? 'PM' : 'AM');
 
+// 날짜에서 시간 정보를 제거 (00:00:00으로 설정)
+export const getDateWithoutTime = (date: Date): Date => {
+  const newDate = new Date(date);
+  newDate.setHours(0, 0, 0, 0);
+  return newDate;
+};
+
+// 12시간 형식에서 24시간 형식으로 변환
+export const convertTo24HourFormat = (hour: number, ampm: string): number => {
+  if (ampm === 'PM' && hour !== 12) {
+    return hour + 12;
+  } else if (ampm === 'AM' && hour === 12) {
+    return 0;
+  }
+  return hour;
+};
+
+// 시간과 분을 분 단위로 변환
+export const convertToMinutes = (hour: number, minute: number, ampm: string): number => {
+  const hour24 = convertTo24HourFormat(hour, ampm);
+  return hour24 * 60 + minute;
+};
+
+// 시간 간격(분)이 겹치는지 확인
+export const isTimeOverlapping = (startTime1: Date, endTime1: Date, startTime2: Date, endTime2: Date): boolean => {
+  return (
+    (startTime1 >= startTime2 && startTime1 < endTime2) ||
+    (endTime1 > startTime2 && endTime1 <= endTime2) ||
+    (startTime1 <= startTime2 && endTime1 >= endTime2) ||
+    (startTime2 <= startTime1 && endTime2 >= endTime1)
path: src/shared/utils/timeUtils.ts
user: login: constantly-dev
body: 요 조건이 맞기는 한데 저도 생각하는데 조금 시간이 걸렸던 것 같아요!
그래서 생각을 해보니 결국 `두 시간 간격(A,B)이 겹친다`는 것은 
```ts
A의 왼쪽 끝(startTime1)이 B의 오른쪽 끝(endTime2)보다 왼쪽에 있으면서, 
A의 오른쪽 끝(endTime1)이 B의 왼쪽 끝(startTime2)보다 오른쪽에 있을 때
```
가 결국 시간이 겹치는 때라고 생각을 했고 이를 식으로 표현하면
```ts
startTime1 < endTime2 && endTime1 > startTime2
```
이렇게 단순화 할 수 있을 것 같아요! 어떻게 생각하시나요??!

created_at: 2025-05-22T05:59:35Z
author_association: MEMBER
diff_hunk: @@ -73,15 +73,6 @@ const ClassRegister = () => {
 
   useEffect(() => {}, [imageUrls]);
path: src/pages/instructor/classRegister/ClassRegister.tsx
user: login: rtttr1
body: 이건 뭐죠?
created_at: 2025-05-22T07:21:29Z
author_association: COLLABORATOR
diff_hunk: @@ -196,11 +187,15 @@ const ClassRegister = () => {
             uploadImgFile={uploadImgFile}
             deleteImgFile={deleteImgFile}
           />
-          <ClassGenre selectedGenre={selectedGenre} toggleCategory={toggleCategory} />
-          <ClassLevel selectedLevel={selectedLevel} toggleLevel={toggleLevel} />
-          <ClassRecommend register={register} handleTextAreaHeight={handleTextAreaHeight} />
+          <ClassGenre selectedGenre={selectedGenre} toggleCategory={toggleCategory} error={errors.selectedGenre} />
+          <ClassLevel selectedLevel={selectedLevel} toggleLevel={toggleLevel} error={errors.selectedLevel} />
+          <ClassRecommend register={register} error={errors.recommendation} recommendation={recommendation} />
path: src/pages/instructor/classRegister/ClassRegister.tsx
user: login: rtttr1
body: 에러를 prop으로 안넘기고 각 하위 컴포넌트에서 받아서 사용하면 prop도 줄이고 에러에 대한 책임분리도 할 수 있을거 같은데 어떻게 생각하시나요?
인터페이스가 적으면 좋고 해당 컴포넌트에서는 에러를 몰라도 된다고 생각하여 남겨봐요!
created_at: 2025-05-22T07:26:01Z
author_association: COLLABORATOR
diff_hunk: @@ -1,37 +1,49 @@
+import type { FieldError } from 'react-hook-form';
 import { genreButtonContainerStyle } from '@/pages/instructor/classRegister/components/ClassGenre/classGenre.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { CLASS_GENRE_SUBTITLE } from '@/pages/instructor/classRegister/constants/registerSectionText';
 import GenreButton from '@/pages/search/components/TabContainer/TagSection/BottomSheet/GenreButton/GenreButton';
+import Text from '@/shared/components/Text/Text';
 import { GENRE_CATEGORY } from '@/shared/constants';
 import { sprinkles } from '@/shared/styles/sprinkles.css';
 
 interface ClassGenrePropTypes {
   selectedGenre: string | null;
   toggleCategory: (category: string) => void;
+  error?: FieldError;
 }
 
-const ClassGenre = ({ selectedGenre, toggleCategory }: ClassGenrePropTypes) => {
+const ClassGenre = ({ selectedGenre, toggleCategory, error }: ClassGenrePropTypes) => {
   return (
     <div
       className={sprinkles({
         display: 'flex',
         flexDirection: 'column',
         width: '100%',
-        gap: 20,
         mb: 40,
       })}>
       <Description title="장르" subTitle={CLASS_GENRE_SUBTITLE} />
 
-      <div className={genreButtonContainerStyle}>
-        {GENRE_CATEGORY.flat().map((category, index) => (
-          <GenreButton
-            key={index}
-            category={category}
-            isSelected={selectedGenre === category}
-            onClick={() => toggleCategory(category)}
-          />
-        ))}
+      <div className={sprinkles({ mt: 20 })}>
+        <div className={genreButtonContainerStyle}>
+          {GENRE_CATEGORY.flat().map((category, index) => (
path: src/pages/instructor/classRegister/components/ClassGenre/ClassGenre.tsx
user: login: rtttr1
body: 이거 flatMap 쓰면 flat된거로 map 한방에 돌릴 수 있어요!
created_at: 2025-05-22T07:28:39Z
author_association: COLLABORATOR
diff_hunk: @@ -48,74 +52,82 @@ const ClassPlace = ({
         display: 'flex',
         flexDirection: 'column',
         width: '100%',
-        gap: 20,
         mb: 40,
       })}>
-      <div className={sprinkles({ display: 'flex', width: '100%', justifyContent: 'space-between' })}>
-        <Description title="클래스 장소" subTitle={CLASS_PLACE_SUBTITLE} />
-        <div className={styles.checkboxContainerStyle}>
-          <Text tag="b3_m" color="gray8">
-            장소 미정
-          </Text>
-          {isUndecidedLocation ? (
-            <BtnCheck width={'2rem'} onClick={handleHasLocation} />
-          ) : (
-            <div className={styles.checkboxStyle} onClick={handleHasLocation} />
-          )}
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 20 })}>
+        <div className={sprinkles({ display: 'flex', width: '100%', justifyContent: 'space-between' })}>
+          <Description title="클래스 장소" subTitle={CLASS_PLACE_SUBTITLE} />
+          <div className={styles.checkboxContainerStyle}>
+            <Text tag="b3_m" color="gray8">
+              장소 미정
+            </Text>
+            {isUndecidedLocation ? (
+              <BtnCheck width={'2rem'} onClick={handleHasLocation} />
+            ) : (
+              <div className={styles.checkboxStyle} onClick={handleHasLocation} />
+            )}
+          </div>
         </div>
-      </div>
 
-      {!isUndecidedLocation && (
-        <div className={sprinkles({ display: 'flex', width: '100%', flexDirection: 'column', gap: 8 })}>
-          {selectedLocation ? (
-            <div className={styles.selectedLocationContainerStyle}>
-              <Text tag="b2_sb_long">{selectedLocation.location}</Text>
-              <IcXCircleGray0424 width={'2.4rem'} onClick={() => setSelectedLocation(null)} />
-            </div>
-          ) : (
-            <>
-              <Input
-                value={defaultPlace}
-                onChange={handleDefaultPlace}
-                placeholder={CLASS_DEFAULT_PLACE_PLACEHOLDER}
-                onKeyDown={(e) => {
-                  if (e.key === 'Enter') {
-                    e.preventDefault();
-                  }
-                }}
-                rightAddOn={<IcSearchGray width={'2.4rem'} />}
-              />
+        {!isUndecidedLocation && (
+          <div className={sprinkles({ display: 'flex', width: '100%', flexDirection: 'column', gap: 8 })}>
+            {selectedLocation ? (
+              <div className={styles.selectedLocationContainerStyle}>
+                <Text tag="b2_sb_long">{selectedLocation.location}</Text>
+                <IcXCircleGray0424 width={'2.4rem'} onClick={handleRemoveLocation} />
+              </div>
+            ) : (
+              <>
+                <Input
+                  value={defaultPlace}
+                  onChange={handleDefaultPlace}
+                  placeholder={CLASS_DEFAULT_PLACE_PLACEHOLDER}
+                  onKeyDown={(e) => {
+                    if (e.key === 'Enter') {
+                      e.preventDefault();
+                    }
+                  }}
+                  rightAddOn={<IcSearchGray width={'2.4rem'} />}
+                />
 
-              {shouldShowEmptyMessage && (
-                <div className={styles.locationEmptyContainerStyle}>해당 검색어에 대한 검색 결과가 없어요!</div>
-              )}
+                {shouldShowEmptyMessage && (
+                  <div className={styles.locationEmptyContainerStyle}>해당 검색어에 대한 검색 결과가 없어요!</div>
+                )}
 
-              {shouldShowLocationList && (
-                <div className={styles.locationListContainerStyle}>
-                  <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 10 })}>
-                    {locationList.locations.map((item, idx) => (
-                      <div
-                        key={idx}
-                        onClick={() => setSelectedLocation(item)}
-                        className={styles.locationItemContainerStyle}>
-                        {idx !== 0 && <div className={styles.dividerStyle} />}
-                        <div className={sprinkles({ display: 'flex', flexDirection: 'column', width: '100%' })}>
-                          <Text tag="b2_sb_long" color="gray10">
-                            {item.location}
-                          </Text>
-                          <Text tag="b3_sb" color="gray5">
-                            {item.streetAddress}
-                          </Text>
+                {shouldShowLocationList && (
+                  <div className={styles.locationListContainerStyle}>
+                    <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 10 })}>
+                      {locationList.locations.map((item, idx) => (
path: src/pages/instructor/classRegister/components/ClassPlace/ClassPlace.tsx
user: login: rtttr1
body: item 대신 location으로 네이밍하면 어떨까요~
created_at: 2025-05-22T07:39:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,39 @@
+import type { ReactElement, ReactNode } from 'react';
+import { useCallback, useState } from 'react';
+
+export interface StepProps {
+  name: string;
+  children: ReactNode;
+}
+
+export interface FunnelProps {
+  children: ReactElement<StepProps>[];
+}
+
+export const useLocalFunnel = (totalSteps: number) => {
+  const [currentStep, setCurrentStep] = useState(1);
+
+  const setStep = (stepChange: number) => {
+    const newStep = currentStep + stepChange;
+
+    if (newStep < 1) {
+      return;
+    } else if (newStep > totalSteps) {
+      setCurrentStep(1);
+    } else {
+      setCurrentStep(newStep);
+    }
+  };
+
+  const Step = useCallback(({ children }: StepProps) => <>{children}</>, []);
+
+  const Funnel = useCallback(
+    ({ children }: FunnelProps) => {
+      const targetStep = children.find((childStep) => childStep.props.name === String(currentStep));
+      return <>{targetStep}</>;
+    },
+    [currentStep]
+  );
+
+  return { Funnel, Step, setStep, currentStep };
+};
path: src/pages/instructor/classRegister/hooks/useLocalFunnel.tsx
user: login: rtttr1
body: 기존 퍼널을 안쓰신 이유가 무엇인가요??
created_at: 2025-05-22T10:21:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,39 @@
+import type { ReactElement, ReactNode } from 'react';
+import { useCallback, useState } from 'react';
+
+export interface StepProps {
+  name: string;
+  children: ReactNode;
+}
+
+export interface FunnelProps {
+  children: ReactElement<StepProps>[];
+}
+
+export const useLocalFunnel = (totalSteps: number) => {
+  const [currentStep, setCurrentStep] = useState(1);
+
+  const setStep = (stepChange: number) => {
+    const newStep = currentStep + stepChange;
+
+    if (newStep < 1) {
+      return;
+    } else if (newStep > totalSteps) {
+      setCurrentStep(1);
+    } else {
+      setCurrentStep(newStep);
+    }
+  };
+
+  const Step = useCallback(({ children }: StepProps) => <>{children}</>, []);
+
+  const Funnel = useCallback(
+    ({ children }: FunnelProps) => {
+      const targetStep = children.find((childStep) => childStep.props.name === String(currentStep));
+      return <>{targetStep}</>;
+    },
+    [currentStep]
+  );
+
+  return { Funnel, Step, setStep, currentStep };
+};
path: src/pages/instructor/classRegister/hooks/useLocalFunnel.tsx
user: login: KIMGEONHWI
body: > 기존 퍼널을 안쓰신 이유가 무엇인가요??

퍼널이 클래스 등록 페이지에서 "일정 등록" 부분에서만 사용이 됩니다. 그런데 퍼널 스탭이 변경될시에 뒷배경이 맨위로 스크롤되는 문제가 발생하였습니다. 해당 문제는 기존 `useFunnel`을 사용할시에 url 파라미터가 변경되면서 스크롤 위치가 처음으로 초기화 되고 있었기 때문이었어요. 그래서 기존 useFunnel 사용안하고 클래스등록에만 사용하는 별도의 useLocalFunnel이라는 커스텀 훅을 만들어서 url파라미터는 변경하는 로직 없도록 하였습니다! 온보딩페이지에서 퍼널 방식이 사라질 예정이라고 하여서 추후에는 하나로 통합해서 사용할 예정입니다.
created_at: 2025-05-22T10:52:53Z
author_association: COLLABORATOR
diff_hunk: @@ -48,74 +52,82 @@ const ClassPlace = ({
         display: 'flex',
         flexDirection: 'column',
         width: '100%',
-        gap: 20,
         mb: 40,
       })}>
-      <div className={sprinkles({ display: 'flex', width: '100%', justifyContent: 'space-between' })}>
-        <Description title="클래스 장소" subTitle={CLASS_PLACE_SUBTITLE} />
-        <div className={styles.checkboxContainerStyle}>
-          <Text tag="b3_m" color="gray8">
-            장소 미정
-          </Text>
-          {isUndecidedLocation ? (
-            <BtnCheck width={'2rem'} onClick={handleHasLocation} />
-          ) : (
-            <div className={styles.checkboxStyle} onClick={handleHasLocation} />
-          )}
+      <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 20 })}>
+        <div className={sprinkles({ display: 'flex', width: '100%', justifyContent: 'space-between' })}>
+          <Description title="클래스 장소" subTitle={CLASS_PLACE_SUBTITLE} />
+          <div className={styles.checkboxContainerStyle}>
+            <Text tag="b3_m" color="gray8">
+              장소 미정
+            </Text>
+            {isUndecidedLocation ? (
+              <BtnCheck width={'2rem'} onClick={handleHasLocation} />
+            ) : (
+              <div className={styles.checkboxStyle} onClick={handleHasLocation} />
+            )}
+          </div>
         </div>
-      </div>
 
-      {!isUndecidedLocation && (
-        <div className={sprinkles({ display: 'flex', width: '100%', flexDirection: 'column', gap: 8 })}>
-          {selectedLocation ? (
-            <div className={styles.selectedLocationContainerStyle}>
-              <Text tag="b2_sb_long">{selectedLocation.location}</Text>
-              <IcXCircleGray0424 width={'2.4rem'} onClick={() => setSelectedLocation(null)} />
-            </div>
-          ) : (
-            <>
-              <Input
-                value={defaultPlace}
-                onChange={handleDefaultPlace}
-                placeholder={CLASS_DEFAULT_PLACE_PLACEHOLDER}
-                onKeyDown={(e) => {
-                  if (e.key === 'Enter') {
-                    e.preventDefault();
-                  }
-                }}
-                rightAddOn={<IcSearchGray width={'2.4rem'} />}
-              />
+        {!isUndecidedLocation && (
+          <div className={sprinkles({ display: 'flex', width: '100%', flexDirection: 'column', gap: 8 })}>
+            {selectedLocation ? (
+              <div className={styles.selectedLocationContainerStyle}>
+                <Text tag="b2_sb_long">{selectedLocation.location}</Text>
+                <IcXCircleGray0424 width={'2.4rem'} onClick={handleRemoveLocation} />
+              </div>
+            ) : (
+              <>
+                <Input
+                  value={defaultPlace}
+                  onChange={handleDefaultPlace}
+                  placeholder={CLASS_DEFAULT_PLACE_PLACEHOLDER}
+                  onKeyDown={(e) => {
+                    if (e.key === 'Enter') {
+                      e.preventDefault();
+                    }
+                  }}
+                  rightAddOn={<IcSearchGray width={'2.4rem'} />}
+                />
 
-              {shouldShowEmptyMessage && (
-                <div className={styles.locationEmptyContainerStyle}>해당 검색어에 대한 검색 결과가 없어요!</div>
-              )}
+                {shouldShowEmptyMessage && (
+                  <div className={styles.locationEmptyContainerStyle}>해당 검색어에 대한 검색 결과가 없어요!</div>
+                )}
 
-              {shouldShowLocationList && (
-                <div className={styles.locationListContainerStyle}>
-                  <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 10 })}>
-                    {locationList.locations.map((item, idx) => (
-                      <div
-                        key={idx}
-                        onClick={() => setSelectedLocation(item)}
-                        className={styles.locationItemContainerStyle}>
-                        {idx !== 0 && <div className={styles.dividerStyle} />}
-                        <div className={sprinkles({ display: 'flex', flexDirection: 'column', width: '100%' })}>
-                          <Text tag="b2_sb_long" color="gray10">
-                            {item.location}
-                          </Text>
-                          <Text tag="b3_sb" color="gray5">
-                            {item.streetAddress}
-                          </Text>
+                {shouldShowLocationList && (
+                  <div className={styles.locationListContainerStyle}>
+                    <div className={sprinkles({ display: 'flex', flexDirection: 'column', gap: 10 })}>
+                      {locationList.locations.map((item, idx) => (
path: src/pages/instructor/classRegister/components/ClassPlace/ClassPlace.tsx
user: login: KIMGEONHWI
body: > item 대신 location으로 네이밍하면 어떨까요~

좋습니다! 반영하겠습니다
created_at: 2025-05-22T11:46:33Z
author_association: COLLABORATOR
diff_hunk: @@ -1,37 +1,49 @@
+import type { FieldError } from 'react-hook-form';
 import { genreButtonContainerStyle } from '@/pages/instructor/classRegister/components/ClassGenre/classGenre.css';
 import Description from '@/pages/instructor/classRegister/components/Description';
 import { CLASS_GENRE_SUBTITLE } from '@/pages/instructor/classRegister/constants/registerSectionText';
 import GenreButton from '@/pages/search/components/TabContainer/TagSection/BottomSheet/GenreButton/GenreButton';
+import Text from '@/shared/components/Text/Text';
 import { GENRE_CATEGORY } from '@/shared/constants';
 import { sprinkles } from '@/shared/styles/sprinkles.css';
 
 interface ClassGenrePropTypes {
   selectedGenre: string | null;
   toggleCategory: (category: string) => void;
+  error?: FieldError;
 }
 
-const ClassGenre = ({ selectedGenre, toggleCategory }: ClassGenrePropTypes) => {
+const ClassGenre = ({ selectedGenre, toggleCategory, error }: ClassGenrePropTypes) => {
   return (
     <div
       className={sprinkles({
         display: 'flex',
         flexDirection: 'column',
         width: '100%',
-        gap: 20,
         mb: 40,
       })}>
       <Description title="장르" subTitle={CLASS_GENRE_SUBTITLE} />
 
-      <div className={genreButtonContainerStyle}>
-        {GENRE_CATEGORY.flat().map((category, index) => (
-          <GenreButton
-            key={index}
-            category={category}
-            isSelected={selectedGenre === category}
-            onClick={() => toggleCategory(category)}
-          />
-        ))}
+      <div className={sprinkles({ mt: 20 })}>
+        <div className={genreButtonContainerStyle}>
+          {GENRE_CATEGORY.flat().map((category, index) => (
path: src/pages/instructor/classRegister/components/ClassGenre/ClassGenre.tsx
user: login: KIMGEONHWI
body: > 이거 flatMap 쓰면 flat된거로 map 한방에 돌릴 수 있어요!

그렇네요!! 반영하겠습니다
created_at: 2025-05-22T12:10:17Z
author_association: COLLABORATOR
diff_hunk: @@ -73,15 +73,6 @@ const ClassRegister = () => {
 
   useEffect(() => {}, [imageUrls]);
path: src/pages/instructor/classRegister/ClassRegister.tsx
user: login: KIMGEONHWI
body: > 이건 뭐죠?

불필요해서 최근 작업사항 보면 삭제 되어있을겁니다!
created_at: 2025-05-22T12:11:09Z
author_association: COLLABORATOR
diff_hunk: @@ -196,11 +187,15 @@ const ClassRegister = () => {
             uploadImgFile={uploadImgFile}
             deleteImgFile={deleteImgFile}
           />
-          <ClassGenre selectedGenre={selectedGenre} toggleCategory={toggleCategory} />
-          <ClassLevel selectedLevel={selectedLevel} toggleLevel={toggleLevel} />
-          <ClassRecommend register={register} handleTextAreaHeight={handleTextAreaHeight} />
+          <ClassGenre selectedGenre={selectedGenre} toggleCategory={toggleCategory} error={errors.selectedGenre} />
+          <ClassLevel selectedLevel={selectedLevel} toggleLevel={toggleLevel} error={errors.selectedLevel} />
+          <ClassRecommend register={register} error={errors.recommendation} recommendation={recommendation} />
path: src/pages/instructor/classRegister/ClassRegister.tsx
user: login: KIMGEONHWI
body: > 에러를 prop으로 안넘기고 각 하위 컴포넌트에서 받아서 사용하면 prop도 줄이고 에러에 대한 책임분리도 할 수 있을거 같은데 어떻게 생각하시나요? 인터페이스가 적으면 좋고 해당 컴포넌트에서는 에러를 몰라도 된다고 생각하여 남겨봐요!

의견감사합니다! 찾아보니 리액트 훅폼에서 `FormProvider`와 `useFormContext`을 제공해주고 있어서 해당 부분을 활용했습니다.
`FormProvider`를 사용하여 `form `컨텍스트를 모든 하위 컴포넌트에 제공했습니다.
그리고 각 컴포넌트에서 `useFormContext`를 사용하여 에러 상태에 직접 접근하도록 수정했습니다!
created_at: 2025-05-22T13:41:05Z
author_association: COLLABORATOR
diff_hunk: @@ -41,6 +41,44 @@ export const decreaseMinute = ({
 // AM/PM 토글
 export const toggleAmpm = (currentAmpm: string): string => (currentAmpm === 'AM' ? 'PM' : 'AM');
 
+// 날짜에서 시간 정보를 제거 (00:00:00으로 설정)
+export const getDateWithoutTime = (date: Date): Date => {
+  const newDate = new Date(date);
+  newDate.setHours(0, 0, 0, 0);
+  return newDate;
+};
+
+// 12시간 형식에서 24시간 형식으로 변환
+export const convertTo24HourFormat = (hour: number, ampm: string): number => {
+  if (ampm === 'PM' && hour !== 12) {
+    return hour + 12;
+  } else if (ampm === 'AM' && hour === 12) {
+    return 0;
+  }
+  return hour;
+};
+
+// 시간과 분을 분 단위로 변환
+export const convertToMinutes = (hour: number, minute: number, ampm: string): number => {
+  const hour24 = convertTo24HourFormat(hour, ampm);
+  return hour24 * 60 + minute;
+};
+
+// 시간 간격(분)이 겹치는지 확인
+export const isTimeOverlapping = (startTime1: Date, endTime1: Date, startTime2: Date, endTime2: Date): boolean => {
+  return (
+    (startTime1 >= startTime2 && startTime1 < endTime2) ||
+    (endTime1 > startTime2 && endTime1 <= endTime2) ||
+    (startTime1 <= startTime2 && endTime1 >= endTime2) ||
+    (startTime2 <= startTime1 && endTime2 >= endTime1)
path: src/shared/utils/timeUtils.ts
user: login: KIMGEONHWI
body: > 요 조건이 맞기는 한데 저도 생각하는데 조금 시간이 걸렸던 것 같아요! 그래서 생각을 해보니 결국 `두 시간 간격(A,B)이 겹친다`는 것은
> 
> ```ts
> A의 왼쪽 끝(startTime1)이 B의 오른쪽 끝(endTime2)보다 왼쪽에 있으면서, 
> A의 오른쪽 끝(endTime1)이 B의 왼쪽 끝(startTime2)보다 오른쪽에 있을 때
> ```
> 
> 가 결국 시간이 겹치는 때라고 생각을 했고 이를 식으로 표현하면
> 
> ```ts
> startTime1 < endTime2 && endTime1 > startTime2
> ```
> 
> 이렇게 단순화 할 수 있을 것 같아요! 어떻게 생각하시나요??!

오 그렇네요!! 좋은 의견 감사합니다.
created_at: 2025-05-22T13:42:20Z
author_association: COLLABORATOR
diff_hunk: @@ -25,51 +39,265 @@ const TimeStep = ({
   setAmpm,
   setSelectedTime,
   selectedTime,
+  times = [],
+  startDate,
 }: TimeStepProps) => {
+  // 선택된 날짜에 영향을 미치는 회차의 가장 늦은 종료 시간 찾기
+  const latestEndTime = useMemo(() => {
+    if (times.length === 0) return null;
+
+    let latest: Date | null = null;
+
+    // 모든 일정 검사
+    for (const time of times) {
+      const existingEndTime = new Date(time.endTime);
+
+      // 종료 날짜가 현재 선택된 날짜와 같거나
+      // 시작 날짜는 이전이고 종료 날짜는 현재 선택된 날짜인 경우 (날짜를 넘어가는 경우)
+      const endTimeDate = existingEndTime.toDateString();
+      const selectedDateObj = new Date(startDate);
+      const selectedDateStr = selectedDateObj.toDateString();
+
+      // 다음 조건에 해당하는 회차의 종료 시간을 고려:
+      // 1. 종료 시간의 날짜가 현재 선택된 날짜와 같은 경우
+      // 2. 시작 날짜와 종료 날짜가 다른 경우(날짜를 넘어가는 경우),
+      //    종료 날짜가 현재 선택된 날짜와 같은 경우
+      if (endTimeDate === selectedDateStr) {
+        if (latest === null || existingEndTime > latest) {
+          latest = existingEndTime;
+        }
+      }
+    }
+
+    return latest;
+  }, [times, startDate]);
+
+  // 최소 선택 가능 시간 계산
+  const minTimeConstraints = useMemo(() => {
+    if (!latestEndTime) return null;
+
+    const minHour = latestEndTime.getHours();
+    const minMinute = latestEndTime.getMinutes();
+    const minAmpm = minHour >= 12 ? 'PM' : 'AM';
+    const display12Hour = minHour > 12 ? minHour - 12 : minHour === 0 ? 12 : minHour;
path: src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeStep.tsx
user: login: KIMGEONHWI
body: > ```ts
> const display12Hour = minHour % 12 === 0 ? 12 : minHour % 12;
> ```
> 
> 해당 로직 이렇게도 표현 가능할 거 같은데 어떤가요?

굿입니다
created_at: 2025-05-22T13:43:27Z
author_association: COLLABORATOR
diff_hunk: @@ -1,22 +1,47 @@
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
 import CalendarCustom from '@/pages/search/components/Calendar/Calendar';
+import { getDateWithoutTime } from '@/shared/utils/timeUtils';
 
 interface DateStepProps {
   startDate: string;
   setStartDate: (date: string) => void;
+  times?: { startTime: string; endTime: string; date: string; duration: number }[];
 }
 
-const DateStep = ({ startDate, setStartDate }: DateStepProps) => {
+const DateStep = ({ startDate, setStartDate, times = [] }: DateStepProps) => {
   const [selectedDate, setSelectedDate] = useState<string>(startDate);
 
+  // 최소 선택 가능 날짜 계산
+  const minSelectableDate = useMemo(() => {
+    // 일정이 없으면 오늘 날짜 반환
+    if (times.length === 0) {
+      return getDateWithoutTime(new Date());
+    }
+
+    // 각 일정의 종료 날짜들 중 가장 늦은 날짜 찾기
+    const latestEndDate = times.reduce((latest, time) => {
+      const endDate = getDateWithoutTime(new Date(time.endTime));
+      return endDate > latest ? endDate : latest;
+    }, new Date(0));
+
+    // 오늘보다 이전 날짜면 오늘을 반환, 아니면 찾은 최신 날짜 반환
+    const today = getDateWithoutTime(new Date());
+    return latestEndDate < today ? today : latestEndDate;
+  }, [times]);
path: src/pages/instructor/classRegister/components/ClassSchedule/DateStep/DateStep.tsx
user: login: KIMGEONHWI
body: > 위아래 쪽에서 반복적으로 `getDateWithoutTime(new Date());`를 쓰고 있으니 초반에서 `today`로 한번만 설정해서 재사용하는 것은 어떨까요?

좋은 의견 감사합니다! 반영하겠습니다
created_at: 2025-05-22T13:45:28Z
author_association: COLLABORATOR
diff_hunk: @@ -10,28 +11,39 @@ import type { ClassRegisterFormTypes } from '../../types/classRegisterForm';
 interface ClassAmountPropTypes {
   price: string;
   register: UseFormRegister<ClassRegisterFormTypes>;
+  error?: FieldError;
+  onPriceChange?: (e: ChangeEvent<HTMLInputElement>) => void;
 }
 
-const ClassAmount = ({ price, register }: ClassAmountPropTypes) => {
+const ClassAmount = ({ price, register, error, onPriceChange }: ClassAmountPropTypes) => {
   const { name, onChange, ref } = register('price');
 
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
     if (ONLY_NUMBER.test(e.target.value)) {
       onChange(e);
+      onPriceChange?.(e);
path: src/pages/instructor/classRegister/components/ClassAmount/ClassAmount.tsx
user: login: KIMGEONHWI
body: > 값을 추적하는 react hook form의 `onChange`는 따로 있는데 해당 `onPriceChange`는 `optional`인데 언제 사용하는 용도인가요?

`react hook form` 적용하기 전에 사용했던 것입니다. 삭제를 깜빡했네요. 삭제하겠습니다.
created_at: 2025-05-22T13:47:35Z
author_association: COLLABORATOR
diff_hunk: @@ -52,11 +53,24 @@ const ClassRegisterBottomSheet = ({
     setHour(12);
     setMinute(0);
     setAmpm('AM');
+    setSelectedTime(null);
+  };
+
+  const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
+    if (e.target === e.currentTarget) {
+      document.body.style.overflow = '';
+      onClose();
+      setStartDate('');
+      setHour(12);
+      setMinute(0);
+      setAmpm('AM');
+      setSelectedTime(null);
+    }
path: src/pages/instructor/classRegister/components/ClassSchedule/ClassRegisterBottomSheet/ClassRegisterBottomSheet.tsx
user: login: KIMGEONHWI
body: > 해당 내부 로직을 `reset`과 같은 네이밍으로 따로 함수를 분리하는 것은 어떨까요? 가독성과 역할 분리 측면에서 더 좋을 것 같습니다!

좋은 의견 감사합니다! 반영하겠습니다.
created_at: 2025-05-22T13:50:39Z
author_association: COLLABORATOR
diff_hunk: @@ -25,51 +39,265 @@ const TimeStep = ({
   setAmpm,
   setSelectedTime,
   selectedTime,
+  times = [],
+  startDate,
 }: TimeStepProps) => {
+  // 선택된 날짜에 영향을 미치는 회차의 가장 늦은 종료 시간 찾기
+  const latestEndTime = useMemo(() => {
+    if (times.length === 0) return null;
+
+    let latest: Date | null = null;
+
+    // 모든 일정 검사
+    for (const time of times) {
+      const existingEndTime = new Date(time.endTime);
+
+      // 종료 날짜가 현재 선택된 날짜와 같거나
+      // 시작 날짜는 이전이고 종료 날짜는 현재 선택된 날짜인 경우 (날짜를 넘어가는 경우)
+      const endTimeDate = existingEndTime.toDateString();
+      const selectedDateObj = new Date(startDate);
+      const selectedDateStr = selectedDateObj.toDateString();
+
+      // 다음 조건에 해당하는 회차의 종료 시간을 고려:
+      // 1. 종료 시간의 날짜가 현재 선택된 날짜와 같은 경우
+      // 2. 시작 날짜와 종료 날짜가 다른 경우(날짜를 넘어가는 경우),
+      //    종료 날짜가 현재 선택된 날짜와 같은 경우
+      if (endTimeDate === selectedDateStr) {
+        if (latest === null || existingEndTime > latest) {
+          latest = existingEndTime;
+        }
+      }
+    }
+
+    return latest;
+  }, [times, startDate]);
path: src/pages/instructor/classRegister/components/ClassSchedule/TimeStep/TimeStep.tsx
user: login: KIMGEONHWI
body: > ### 1️⃣
> 반복해야 할 time을 사용하는 `existingEndTime` 등을 제외하고 `startDate`같은 건 반복해도 항상 똑같은 값을 가지니 굳이 반복문 안에서 선언할 필요 없을 것 같아요!
> 
> ```ts
> const selectedDateObj = new Date(startDate);
> const selectedDateStr = selectedDateObj.toDateString();
> ```
> 
> 대표적으로 이 두 코드는 반복문 밖에 있어도 되지 않을까요??
> 
> ### 2️⃣
> 그리고
> 
> ```ts
> if (endTimeDate === selectedDateStr) {
>   if (latest === null || existingEndTime > latest) {
>     latest = existingEndTime;
>   }
> }
> ```
> 
> `if`를 중첩해도 되지만 `endTimeDate !== selectedDateStr` 이 두개가 다른 경우를 `early return` 해도 될 것 같아요! 이건 진짜 건휘님이 보기에 가독성이 좋은 코드를 선택하는 것이 좋을 것 같습니다 👍
> 
> ### 3️⃣
> 해당 코드를 함수형, 선언적으로 고차함수를 이용할 수도 있을 것 같은데 for...of를 사용하신 이유가 있으신지 궁금합니다! 당연히 둘 중 뭐가 무조건 좋다는 없고 단순 해당 코드에서 for...of를 선택하신 이유가 궁금합니다!

좋은 의견 감사합니다! 1, 2번은 반영했습니다.
for...of를 사용한 이유는 성능 측면에서는 루프가 단순하므로 큰 차이는 없을 것이라고 판단하였고, 이와 같이 복잡한 조건이나 상태 변경이 있을 때 for 루프가 더 직관적이고 가독성이 높다고 판단하였습니다.
created_at: 2025-05-22T14:11:20Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-20T09:57:45Z
body: ✅ 빌드가 성공적으로 완료되었습니다 🛠️
author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-05-20T09:57:53Z
body: ## Deploying dash-client with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>daf891b</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://923683e9.dash-client.pages.dev'>https://923683e9.dash-client.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://refactor--429-class-register.dash-client.pages.dev'>https://refactor--429-class-register.dash-client.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/30f8fbf9324c453f380f6e3c29116567/pages/view/dash-client/923683e9-f8e1-4fb2-93cc-1025b97050f2)

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-20T09:58:32Z
body: ✅ Storybook 배포 완료! 🔗 https://67e4fd1fd2c7078dceec04a4-leidwlxovz.chromatic.com/
<!-- thollander/actions-comment-pull-request "439-storybook" -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T14:28:21Z
body: ✅ 빌드가 성공적으로 완료되었습니다 🛠️
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T14:47:13Z
body: ✅ 빌드가 성공적으로 완료되었습니다 🛠️
author_association: NONE