From 016c95b5d1c9a85cdbde597ce684c4f8d9fcf40c Mon Sep 17 00:00:00 2001 From: YuminPark Date: Tue, 30 Jun 2026 13:53:51 +0900 Subject: [PATCH 01/13] =?UTF-8?q?#40=20[REFACTOR]=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=84=B9=EC=85=98=20UI=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/write/config/write.ts | 6 +-- src/shared/ui/Radio.tsx | 9 +++-- .../instructor/write/ui/CategorySection.tsx | 38 ++++++++++++------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/features/instructor/write/config/write.ts b/src/features/instructor/write/config/write.ts index ba04ac8..29c11c0 100644 --- a/src/features/instructor/write/config/write.ts +++ b/src/features/instructor/write/config/write.ts @@ -18,9 +18,9 @@ export const WRITE_STEPS: { STEP1 ========================= */ export const CATEGORIES = [ - { label: "유인물", items: ["교재 외지/내지"] }, - { label: "홍보물", items: [] }, - { label: "퍼스널 브랜딩", items: [] }, + { label: "유인물", items: ["교재 외지/내지", "대봉투"] }, + { label: "홍보물", items: ["포스터", "배너", "옥외 광고물", "SNS 카드뉴스"] }, + { label: "퍼스널 브랜딩", items: ["포토카드", "스티커", "키링", "명함", "로고"] }, ]; export const SIZE_OPTIONS = [ diff --git a/src/shared/ui/Radio.tsx b/src/shared/ui/Radio.tsx index 1900661..932cc8b 100644 --- a/src/shared/ui/Radio.tsx +++ b/src/shared/ui/Radio.tsx @@ -2,13 +2,14 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react"; type RadioProps = Omit, "className" | "type"> & { children?: ReactNode; + labelClassName?: string; }; -const Radio = ({ children, disabled, ...props }: RadioProps) => { +const Radio = ({ children, disabled, labelClassName, ...props }: RadioProps) => { return ( ); diff --git a/src/widgets/instructor/write/ui/CategorySection.tsx b/src/widgets/instructor/write/ui/CategorySection.tsx index 4fcb8a8..9e82af2 100644 --- a/src/widgets/instructor/write/ui/CategorySection.tsx +++ b/src/widgets/instructor/write/ui/CategorySection.tsx @@ -8,7 +8,7 @@ import Radio from "@/shared/ui/Radio"; const CategorySection = () => { const { selectedCategory, setSelectedCategory } = useWriteFormStore(); - const [openIndex, setOpenIndex] = useState(null); + const [openIndex, setOpenIndex] = useState(0); const handleCategoryClick = (index: number) => { if (CATEGORIES[index].items.length === 0) return; @@ -25,7 +25,9 @@ const CategorySection = () => { : null; return ( -
+

카테고리

@@ -38,7 +40,9 @@ const CategorySection = () => {

원하는 작업물의 종류를 선택해주세요

-
+
{CATEGORIES.map((category, index) => ( { className={`grid transition-all duration-300 ease-in-out ${openIndex !== null ? "grid-rows-[1fr] pt-4" : "grid-rows-[0fr] pt-0"}`} >
-
-
+
{openIndex !== null && CATEGORIES[openIndex].items.map(item => ( - setSelectedCategory({ categoryIndex: openIndex, item })} - > - {item} - +
+ setSelectedCategory({ categoryIndex: openIndex, item })} + > + {item} + +
))}
From a4ec7e4e05a8d957b211801f58c9091132114300 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Tue, 30 Jun 2026 14:09:11 +0900 Subject: [PATCH 02/13] =?UTF-8?q?#40=20[MOD]=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EB=B3=B4=EB=8D=94=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A6=88=20=EC=84=A0=ED=83=9D=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instructor/write/model/writeFormStore.ts | 2 +- .../instructor/write/ui/AttachFileSection.tsx | 2 +- .../write/ui/BasicInfoTypingSection.tsx | 2 +- .../instructor/write/ui/CategorySection.tsx | 2 +- .../instructor/write/ui/ColorChooseSection.tsx | 2 +- .../instructor/write/ui/DeadlineChooseSection.tsx | 2 +- .../instructor/write/ui/DesignConceptSection.tsx | 2 +- .../write/ui/NecessaryPageChooseSection.tsx | 2 +- .../instructor/write/ui/PlanChooseSection.tsx | 2 +- .../instructor/write/ui/ReferenceSection.tsx | 2 +- src/widgets/instructor/write/ui/SizeSection.tsx | 15 +++++++++++++-- 11 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/features/instructor/write/model/writeFormStore.ts b/src/features/instructor/write/model/writeFormStore.ts index aa5b119..7114b9d 100644 --- a/src/features/instructor/write/model/writeFormStore.ts +++ b/src/features/instructor/write/model/writeFormStore.ts @@ -79,7 +79,7 @@ const initialState = { selectedSize: null, selectedKeywords: [], additionalConcept: "", - colorMode: "custom" as ColorMode, + colorMode: "designer" as ColorMode, colors: [null, null, null] as (RgbaColor | null)[], mainColorIndex: 0, basicInfo: { 교재명: "", 강사명: "", 과목명: "" }, diff --git a/src/widgets/instructor/write/ui/AttachFileSection.tsx b/src/widgets/instructor/write/ui/AttachFileSection.tsx index 1699e2f..24800ce 100644 --- a/src/widgets/instructor/write/ui/AttachFileSection.tsx +++ b/src/widgets/instructor/write/ui/AttachFileSection.tsx @@ -15,7 +15,7 @@ const AttachFileSection = () => { ); return ( -
+
0 ? "gap-7" : "gap-6"}`}>
diff --git a/src/widgets/instructor/write/ui/BasicInfoTypingSection.tsx b/src/widgets/instructor/write/ui/BasicInfoTypingSection.tsx index a9c401a..01cfc16 100644 --- a/src/widgets/instructor/write/ui/BasicInfoTypingSection.tsx +++ b/src/widgets/instructor/write/ui/BasicInfoTypingSection.tsx @@ -11,7 +11,7 @@ const BasicInfoTypingSection = () => { }; return ( -
+

기본정보 작성하기

원하는 작업물의 종류를 선택해주세요

diff --git a/src/widgets/instructor/write/ui/CategorySection.tsx b/src/widgets/instructor/write/ui/CategorySection.tsx index 9e82af2..8343e3c 100644 --- a/src/widgets/instructor/write/ui/CategorySection.tsx +++ b/src/widgets/instructor/write/ui/CategorySection.tsx @@ -26,7 +26,7 @@ const CategorySection = () => { return (
diff --git a/src/widgets/instructor/write/ui/ColorChooseSection.tsx b/src/widgets/instructor/write/ui/ColorChooseSection.tsx index b9be509..7503e80 100644 --- a/src/widgets/instructor/write/ui/ColorChooseSection.tsx +++ b/src/widgets/instructor/write/ui/ColorChooseSection.tsx @@ -45,7 +45,7 @@ const ColorChooseSection = () => { ref={sectionRef} className={cn( "rounded-12 flex flex-col gap-8 border bg-white p-6", - isFocused ? "border-purple-40" : "border-transparent", + isFocused ? "border-gray-40" : "border-transparent", )} >
diff --git a/src/widgets/instructor/write/ui/DeadlineChooseSection.tsx b/src/widgets/instructor/write/ui/DeadlineChooseSection.tsx index ffcea58..a77a2c2 100644 --- a/src/widgets/instructor/write/ui/DeadlineChooseSection.tsx +++ b/src/widgets/instructor/write/ui/DeadlineChooseSection.tsx @@ -43,7 +43,7 @@ const DeadlineChooseSection = () => { }; return ( -
+

마감 기한 선택

시안을 수령할 날짜를 선택해주세요

diff --git a/src/widgets/instructor/write/ui/DesignConceptSection.tsx b/src/widgets/instructor/write/ui/DesignConceptSection.tsx index 11e635d..c47b8e9 100644 --- a/src/widgets/instructor/write/ui/DesignConceptSection.tsx +++ b/src/widgets/instructor/write/ui/DesignConceptSection.tsx @@ -26,7 +26,7 @@ const DesignConceptSection = () => { }; return ( -
+

디자인 컨셉

diff --git a/src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx b/src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx index 3ac6edd..2d6b255 100644 --- a/src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx +++ b/src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx @@ -19,7 +19,7 @@ const NecessaryPageChooseSection = () => { const hasSelected = selectedPages.length > 0; return ( -
+

필수 페이지 선택하기

diff --git a/src/widgets/instructor/write/ui/PlanChooseSection.tsx b/src/widgets/instructor/write/ui/PlanChooseSection.tsx index 446ca28..036b473 100644 --- a/src/widgets/instructor/write/ui/PlanChooseSection.tsx +++ b/src/widgets/instructor/write/ui/PlanChooseSection.tsx @@ -18,7 +18,7 @@ const PlanChooseSection = () => { }, []); return ( -
+

플랜 선택

diff --git a/src/widgets/instructor/write/ui/ReferenceSection.tsx b/src/widgets/instructor/write/ui/ReferenceSection.tsx index 2b0c703..53aafbd 100644 --- a/src/widgets/instructor/write/ui/ReferenceSection.tsx +++ b/src/widgets/instructor/write/ui/ReferenceSection.tsx @@ -15,7 +15,7 @@ const ReferenceSection = () => { ); return ( -
+
0 ? "gap-7" : "gap-6"}`}>
diff --git a/src/widgets/instructor/write/ui/SizeSection.tsx b/src/widgets/instructor/write/ui/SizeSection.tsx index a333890..3a5b124 100644 --- a/src/widgets/instructor/write/ui/SizeSection.tsx +++ b/src/widgets/instructor/write/ui/SizeSection.tsx @@ -10,10 +10,21 @@ import { const SizeSection = () => { const { selectedCategory, selectedSize, setSelectedSize } = useWriteFormStore(); + const selectedSizeLabel = selectedSize + ? SIZE_OPTIONS.find(option => option.id === selectedSize)?.size + : null; + return ( -
+
-

사이즈

+
+

사이즈

+ {selectedSizeLabel && ( +
+ {selectedSizeLabel} +
+ )} +
{selectedCategory && (

진행할 작업물의 사이즈를 선택해주세요

)} From d2ab189e3e5a05a7d6ba2f2919a7b7ecb5006d36 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Tue, 30 Jun 2026 17:40:36 +0900 Subject: [PATCH 03/13] =?UTF-8?q?#40=20[REFACTOR]=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=BB=A8=EC=85=89=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B0=9C=ED=8E=B8=20?= =?UTF-8?q?=EB=B0=8F=20Chip=20long=20=EB=B0=B0=EB=A6=AC=EC=96=B8=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/write/config/write.ts | 42 +++++++++++-------- .../write/ui/ConceptKeywordCard.tsx | 6 ++- src/shared/ui/Chip.tsx | 28 ++++++++++++- .../instructor/write/ui/CategorySection.tsx | 2 +- .../write/ui/DesignConceptSection.tsx | 26 ++---------- 5 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/features/instructor/write/config/write.ts b/src/features/instructor/write/config/write.ts index 29c11c0..78672e3 100644 --- a/src/features/instructor/write/config/write.ts +++ b/src/features/instructor/write/config/write.ts @@ -52,14 +52,14 @@ export const SIZE_OPTIONS = [ ]; export const CONCEPT_CATEGORIES = [ - { title: "밝은", keywords: ["귀여운", "경쾌한", "맑은"] }, - { title: "부드러운", keywords: ["내츄럴한", "은은한", "온화한"] }, - { title: "고급스러운", keywords: ["우아한", "고상한", "모던한"] }, - { title: "강렬한", keywords: ["화려한", "다이나믹한"] }, - { title: "단정한", keywords: ["점잖은"] }, + { title: "질감", keywords: ["입체감 있는", "평면적인", "거친", "매끈한"] }, + { title: "레이아웃", keywords: ["정돈된", "역동적인", "여백이 많은", "꽉 찬"] }, + { title: "형태", keywords: ["둥근", "각진", "지유로운", "기하학적인"] }, + { title: "색감", keywords: ["화려한", "차분한", "밝은", "어두운"] }, + { title: "무드", keywords: ["귀여운", "시크한", "감성적인", "전문적인"] }, ]; -export const MAX_CONCEPT_SELECT = 2; +export const MAX_CONCEPT_SELECT = 5; /* ========================= STEP2 @@ -123,18 +123,26 @@ export const SIZE_DISPLAY_MAP: Record = { }; export const KEYWORD_API_MAP: Record = { - 귀여운: "CUTE", - 경쾌한: "LIGHT", - 맑은: "CLEAR", - 내츄럴한: "NATURAL", - 은은한: "SUBTLE", - 온화한: "WARM", - 우아한: "ELEGANT", - 고상한: "REFINED", - 모던한: "MODERN", + "입체감 있는": "DIMENSIONAL", + 평면적인: "LIGHT", + 거친: "ROUGH", + 매끈한: "SMOOTH", + 정돈된: "ORDERLY", + 역동적인: "DYNAMIC", + "여백이 많은": "SPACIOUS", + "꽉 찬": "DENSE", + 둥근: "ROUND", + 각진: "ANGULAR", + 자유로운: "FREEFORM", + 기하학적인: "GEOMETRIC", 화려한: "VIVID", - 다이나믹한: "DYNAMIC", - 점잖은: "CALM", + 차분한: "MUTED", + 밝은: "BRIGHT", + 어두운: "DARK", + 귀여운: "CUTE", + 시크한: "CHIC", + 감성적인: "EMOTIONAL", + 전문적인: "PROFESSIONAL", }; export type PageType = diff --git a/src/features/instructor/write/ui/ConceptKeywordCard.tsx b/src/features/instructor/write/ui/ConceptKeywordCard.tsx index 8ccd202..2996926 100644 --- a/src/features/instructor/write/ui/ConceptKeywordCard.tsx +++ b/src/features/instructor/write/ui/ConceptKeywordCard.tsx @@ -14,15 +14,17 @@ const ConceptKeywordCard = ({ onSelect, }: ConceptKeywordCardProps) => { return ( -
+

{title}

-
+
{keywords.map(keyword => ( onSelect(keyword)} + variant="long" + className="w-35" /> ))}
diff --git a/src/shared/ui/Chip.tsx b/src/shared/ui/Chip.tsx index 5bf8fc7..bc94c8e 100644 --- a/src/shared/ui/Chip.tsx +++ b/src/shared/ui/Chip.tsx @@ -1,17 +1,19 @@ import { CloseIcon } from "@/shared/assets/icons"; import { cn } from "@/shared/lib/utils/cn"; -type ChipVariant = "selectable" | "removable"; +type ChipVariant = "selectable" | "removable" | "long"; interface ChipProps { label: string; isSelected?: boolean; isHoverPreview?: boolean; disableHover?: boolean; + disabled?: boolean; onRemove?: () => void; onClick?: () => void; removeAriaLabel?: string; variant?: ChipVariant; + className?: string; } const Chip = ({ @@ -19,10 +21,12 @@ const Chip = ({ isSelected = false, isHoverPreview = false, disableHover = false, + disabled = false, onRemove, onClick, removeAriaLabel, variant = "selectable", + className, }: ChipProps) => { if (variant === "removable") { const iconWrapperStyles = "inline-flex size-4 shrink-0 items-center justify-center"; @@ -42,6 +46,28 @@ const Chip = ({ ); } + if (variant === "long") { + return ( +
+ {label} +
+ ); + } + const baseStyles = cn( "group rounded-100 inline-flex h-[34px] items-center justify-center border px-3 py-[6px] transition-colors duration-150", disableHover ? "cursor-default" : "cursor-pointer", diff --git a/src/widgets/instructor/write/ui/CategorySection.tsx b/src/widgets/instructor/write/ui/CategorySection.tsx index 8343e3c..d70fdd2 100644 --- a/src/widgets/instructor/write/ui/CategorySection.tsx +++ b/src/widgets/instructor/write/ui/CategorySection.tsx @@ -26,7 +26,7 @@ const CategorySection = () => { return (
diff --git a/src/widgets/instructor/write/ui/DesignConceptSection.tsx b/src/widgets/instructor/write/ui/DesignConceptSection.tsx index c47b8e9..e32cb20 100644 --- a/src/widgets/instructor/write/ui/DesignConceptSection.tsx +++ b/src/widgets/instructor/write/ui/DesignConceptSection.tsx @@ -29,30 +29,10 @@ const DesignConceptSection = () => {

디자인 컨셉

-

- 원하는 컨셉의 태그를 두가지 선택하거나 직접 작성해주세요 -

+

최대 5개까지 자유롭게 선택할 수 있어요

-
- 작업물이 -
- {Array.from({ length: MAX_CONCEPT_SELECT }).map((_, i) => { - const keyword = selectedKeywords[i]; - return keyword != null ? ( - handleRemove(keyword)} - /> - ) : ( -
- ); - })} -
- 컨셉으로 되면 좋겠어요 -
-
+ +
{CONCEPT_CATEGORIES.map(({ title, keywords }) => ( Date: Tue, 30 Jun 2026 18:30:03 +0900 Subject: [PATCH 04/13] =?UTF-8?q?#40=20[FEAT]=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=BB=A8=EC=85=89=20=EC=B6=94=EA=B0=80=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EC=B0=BD=20=EB=B0=8F=20=EC=84=A0=ED=83=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EC=A0=9C=ED=95=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 8 +++ src/app/instructor/write/layout.tsx | 2 +- src/features/instructor/write/config/write.ts | 2 +- .../instructor/write/ui/ConceptLimitToast.tsx | 23 ++++++++ .../instructor/write/ui/ConceptResult.tsx | 26 +++++++++ src/shared/ui/Sidebar.tsx | 22 +++++++- .../write/ui/DesignConceptSection.tsx | 56 ++++++++++++++++--- 7 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 src/features/instructor/write/ui/ConceptLimitToast.tsx create mode 100644 src/features/instructor/write/ui/ConceptResult.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 74e875b..8ca61d1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -177,6 +177,8 @@ Shadow Tokens ========================= */ --shadow-dropdown: 0 0 20px 4px rgba(0, 0, 0, 0.05); + /* UI/안내바 */ + --shadow-banner: 0 8px 20px 4px rgba(0, 0, 0, 0.08); /* ========================= Blur Tokens @@ -184,6 +186,12 @@ --blur-hover: 1.7px; --blur-button: 2px; + /* ========================= + Z-Index Tokens + ========================= */ + --z-index-header: 10; + --z-index-toast: 60; + /* ========================= Radius Tokens ========================= */ diff --git a/src/app/instructor/write/layout.tsx b/src/app/instructor/write/layout.tsx index eaa40bb..7a5b867 100644 --- a/src/app/instructor/write/layout.tsx +++ b/src/app/instructor/write/layout.tsx @@ -15,7 +15,7 @@ const WriteLayout = ({ children }: { children: ReactNode }) => { return (
-
+
{children} diff --git a/src/features/instructor/write/config/write.ts b/src/features/instructor/write/config/write.ts index 78672e3..7b26e66 100644 --- a/src/features/instructor/write/config/write.ts +++ b/src/features/instructor/write/config/write.ts @@ -54,7 +54,7 @@ export const SIZE_OPTIONS = [ export const CONCEPT_CATEGORIES = [ { title: "질감", keywords: ["입체감 있는", "평면적인", "거친", "매끈한"] }, { title: "레이아웃", keywords: ["정돈된", "역동적인", "여백이 많은", "꽉 찬"] }, - { title: "형태", keywords: ["둥근", "각진", "지유로운", "기하학적인"] }, + { title: "형태", keywords: ["둥근", "각진", "자유로운", "기하학적인"] }, { title: "색감", keywords: ["화려한", "차분한", "밝은", "어두운"] }, { title: "무드", keywords: ["귀여운", "시크한", "감성적인", "전문적인"] }, ]; diff --git a/src/features/instructor/write/ui/ConceptLimitToast.tsx b/src/features/instructor/write/ui/ConceptLimitToast.tsx new file mode 100644 index 0000000..682970a --- /dev/null +++ b/src/features/instructor/write/ui/ConceptLimitToast.tsx @@ -0,0 +1,23 @@ +import { ExclamationMarkCircleIcon } from "@/shared/assets/icons"; +import { cn } from "@/shared/lib/utils/cn"; + +interface ConceptLimitToastProps { + message: string; + show: boolean; +} + +const ConceptLimitToast = ({ message, show }: ConceptLimitToastProps) => { + return ( +
+ + {message} +
+ ); +}; + +export default ConceptLimitToast; diff --git a/src/features/instructor/write/ui/ConceptResult.tsx b/src/features/instructor/write/ui/ConceptResult.tsx new file mode 100644 index 0000000..19e27a8 --- /dev/null +++ b/src/features/instructor/write/ui/ConceptResult.tsx @@ -0,0 +1,26 @@ +import Chip from "@/shared/ui/Chip"; + +interface ConceptResultProps { + selectedKeywords: string[]; + onRemove: (keyword: string) => void; +} + +const ConceptResult = ({ selectedKeywords, onRemove }: ConceptResultProps) => { + return ( +
+

선택한 컨셉

+
+ {selectedKeywords.map(keyword => ( + onRemove(keyword)} + /> + ))} +
+
+ ); +}; + +export default ConceptResult; diff --git a/src/shared/ui/Sidebar.tsx b/src/shared/ui/Sidebar.tsx index 2078947..ee6c296 100644 --- a/src/shared/ui/Sidebar.tsx +++ b/src/shared/ui/Sidebar.tsx @@ -1,4 +1,6 @@ -import { ReactNode } from "react"; +"use client"; + +import { ReactNode, useEffect, useRef } from "react"; interface SidebarProps { children: ReactNode; @@ -6,8 +8,24 @@ interface SidebarProps { } const Sidebar = ({ children, bottom }: SidebarProps) => { + const asideRef = useRef(null); + + useEffect(() => { + const node = asideRef.current; + if (!node) return; + + const updateWidth = () => { + document.documentElement.style.setProperty("--sidebar-w", `${node.offsetWidth}px`); + }; + + updateWidth(); + const observer = new ResizeObserver(updateWidth); + observer.observe(node); + return () => observer.disconnect(); + }, []); + return ( -

- +
{uploadedFiles.length > 0 && ( @@ -55,6 +82,26 @@ const AttachFileSection = () => {

+ setIsInvalidFileModalOpen(false)} + onClose={() => setIsInvalidFileModalOpen(false)} + /> + setIsFileCountExceededModalOpen(false)} + onClose={() => setIsFileCountExceededModalOpen(false)} + />
); }; diff --git a/src/widgets/instructor/write/ui/ReferenceSection.tsx b/src/widgets/instructor/write/ui/ReferenceSection.tsx index dce6f34..1d565e8 100644 --- a/src/widgets/instructor/write/ui/ReferenceSection.tsx +++ b/src/widgets/instructor/write/ui/ReferenceSection.tsx @@ -1,10 +1,16 @@ "use client"; +import { useState } from "react"; + import { useWriteFormStore } from "@/features/instructor/write"; import { useUploadedFiles } from "@/shared/lib/hooks/useUploadedFiles"; +import { isAllowedFileType, MAX_FILE_SIZE_BYTES } from "@/shared/lib/utils/file"; import FileDragAndDrop from "@/shared/ui/FileDragAndDrop"; import FileUpload from "@/shared/ui/FileUpload"; import TextField from "@/shared/ui/input/TextField"; +import Modal from "@/shared/ui/modal/Modal"; + +const MAX_FILE_COUNT = 3; const ReferenceSection = () => { const { referenceFiles, setReferenceFiles, referenceDescription, setReferenceDescription } = @@ -13,6 +19,27 @@ const ReferenceSection = () => { referenceFiles, setReferenceFiles, ); + const [isInvalidFileModalOpen, setIsInvalidFileModalOpen] = useState(false); + const [isFileCountExceededModalOpen, setIsFileCountExceededModalOpen] = useState(false); + + const handleValidatedFilesAdded = (files: File[]) => { + const validFiles = files.filter( + file => isAllowedFileType(file, [".png"]) && file.size <= MAX_FILE_SIZE_BYTES, + ); + + if (validFiles.length < files.length) { + setIsInvalidFileModalOpen(true); + } + + if (validFiles.length === 0) return; + + if (uploadedFiles.length + validFiles.length > MAX_FILE_COUNT) { + setIsFileCountExceededModalOpen(true); + return; + } + + handleFilesAdded(validFiles); + }; return (
@@ -24,7 +51,7 @@ const ReferenceSection = () => { 디자이너가 참고하길 원하는 스타일이 있다면 레퍼런스 파일을 첨부해주세요.

- +
{uploadedFiles.length > 0 && ( @@ -55,6 +82,26 @@ const ReferenceSection = () => {
+ setIsInvalidFileModalOpen(false)} + onClose={() => setIsInvalidFileModalOpen(false)} + /> + setIsFileCountExceededModalOpen(false)} + onClose={() => setIsFileCountExceededModalOpen(false)} + />
); }; From 29bad6190185275dbeca037b680a511b685075d6 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Tue, 30 Jun 2026 21:51:15 +0900 Subject: [PATCH 08/13] =?UTF-8?q?#40=20[FEAT]=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EC=84=A0=ED=83=9D=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=97=86=EC=9D=B4=20=EB=B0=94=EB=A1=9C=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=ED=99=95=EC=A0=95=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=EB=9E=99=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/lib/utils/dropdown.ts | 11 +++++++++ src/shared/ui/dropdown/DateDropdownMenu.tsx | 25 +++++++++++++++++++-- src/shared/ui/dropdown/WheelColumn.tsx | 15 +++++++++++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/shared/lib/utils/dropdown.ts b/src/shared/lib/utils/dropdown.ts index 5ccdc3d..e161f59 100644 --- a/src/shared/lib/utils/dropdown.ts +++ b/src/shared/lib/utils/dropdown.ts @@ -5,6 +5,17 @@ export const easeOutCubic = (progress: number) => 1 - (1 - progress) ** 3; export const getDaysInMonth = (year: number, monthIndex: number) => new Date(year, monthIndex + 1, 0).getDate(); +export const getDateForIndices = ( + baseYear: number, + yearIndex: number, + monthIndex: number, + dayIndex: number, +) => { + const year = baseYear + yearIndex; + const daysInMonth = getDaysInMonth(year, monthIndex); + return new Date(year, monthIndex, Math.min(dayIndex, daysInMonth - 1) + 1); +}; + export const getScrollTopForIndex = (index: number, selectedIndex: number) => index * STEP + (index > selectedIndex ? SELECTED_EXTRA : 0); diff --git a/src/shared/ui/dropdown/DateDropdownMenu.tsx b/src/shared/ui/dropdown/DateDropdownMenu.tsx index f7191a1..fdf0e42 100644 --- a/src/shared/ui/dropdown/DateDropdownMenu.tsx +++ b/src/shared/ui/dropdown/DateDropdownMenu.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from "react"; import { PAD_TOP, YEAR_RANGE } from "@/shared/config/dropdown"; import { cn } from "@/shared/lib/utils/cn"; -import { getDaysInMonth } from "@/shared/lib/utils/dropdown"; +import { getDateForIndices, getDaysInMonth } from "@/shared/lib/utils/dropdown"; import WheelColumn from "@/shared/ui/dropdown/WheelColumn"; interface DropdownMenuProps { @@ -43,6 +43,11 @@ const DateDropdownMenu = ({ const selectedDate = new Date(selectedYear, monthIndex, safeDayIndex + 1); const isInvalid = minDate != null && selectedDate <= minDate; + const confirmDate = (date: Date) => { + if (minDate != null && date <= minDate) return; + onConfirm?.(date); + }; + const handleYearSelect = useCallback( (nextYearIndex: number) => { setYearIndex(nextYearIndex); @@ -78,17 +83,33 @@ const DateDropdownMenu = ({
- + { + handleYearSelect(index); + confirmDate(getDateForIndices(baseYear, index, monthIndex, safeDayIndex)); + }} + /> { + handleMonthSelect(index); + confirmDate(getDateForIndices(baseYear, yearIndex, index, safeDayIndex)); + }} itemClassName="w-7.5" /> { + setDayIndex(index); + confirmDate(getDateForIndices(baseYear, yearIndex, monthIndex, index)); + }} itemClassName="w-8.5" />
diff --git a/src/shared/ui/dropdown/WheelColumn.tsx b/src/shared/ui/dropdown/WheelColumn.tsx index 3f94c1a..8c66abd 100644 --- a/src/shared/ui/dropdown/WheelColumn.tsx +++ b/src/shared/ui/dropdown/WheelColumn.tsx @@ -8,10 +8,17 @@ interface WheelColumnProps { items: string[]; selectedIndex: number; onSelect: (index: number) => void; + onItemClick?: (index: number) => void; itemClassName?: string; } -const WheelColumn = ({ items, selectedIndex, onSelect, itemClassName = "" }: WheelColumnProps) => { +const WheelColumn = ({ + items, + selectedIndex, + onSelect, + onItemClick, + itemClassName = "", +}: WheelColumnProps) => { const { scrollRef, handleScroll, handleWheel } = useWheelColumn({ items, selectedIndex, @@ -34,8 +41,12 @@ const WheelColumn = ({ items, selectedIndex, onSelect, itemClassName = "" }: Whe style={{ marginBottom: index === items.length - 1 ? 0 : ITEM_GAP, }} + onClick={() => { + if (index !== selectedIndex) onSelect(index); + onItemClick?.(index); + }} className={cn( - "flex snap-start snap-always items-center justify-end", + "flex cursor-pointer snap-start snap-always items-center justify-end", index === selectedIndex ? "h-9.5" : "h-5.5", )} > From 74e762ac0075d359dabe6f71c59010902311e444 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Tue, 30 Jun 2026 21:53:02 +0900 Subject: [PATCH 09/13] =?UTF-8?q?#40=20[CHORE]=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=9D=84=EC=96=B4=EC=93=B0=EA=B8=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/instructor/write/ui/PlanChooseSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/instructor/write/ui/PlanChooseSection.tsx b/src/widgets/instructor/write/ui/PlanChooseSection.tsx index 036b473..1522e0e 100644 --- a/src/widgets/instructor/write/ui/PlanChooseSection.tsx +++ b/src/widgets/instructor/write/ui/PlanChooseSection.tsx @@ -22,7 +22,7 @@ const PlanChooseSection = () => {

플랜 선택

- 작업을 진행할 디자이너의 인원수를 선택해주세요 + 작업을 진행할 디자이너의 인원수를 선택해 주세요.

From cedfb388341b40000ec6ce8bbac182d26ecbc3d7 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Tue, 30 Jun 2026 23:47:19 +0900 Subject: [PATCH 10/13] =?UTF-8?q?#40=20[FEAT]=20=EA=B2=B0=EC=A0=9C?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=9C=84=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../write/ui/PaymentModal/Step1.tsx | 40 ++++++++++++------- .../ui/Toast.tsx} | 10 +++-- .../write/ui/DesignConceptSection.tsx | 8 +++- 3 files changed, 38 insertions(+), 20 deletions(-) rename src/{features/instructor/write/ui/ConceptLimitToast.tsx => shared/ui/Toast.tsx} (62%) diff --git a/src/features/instructor/write/ui/PaymentModal/Step1.tsx b/src/features/instructor/write/ui/PaymentModal/Step1.tsx index 4230f20..40a3b5a 100644 --- a/src/features/instructor/write/ui/PaymentModal/Step1.tsx +++ b/src/features/instructor/write/ui/PaymentModal/Step1.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { PLAN_LABEL_MAP, @@ -10,6 +10,7 @@ import { useWriteFormStore } from "@/features/instructor/write/model/writeFormSt import { ArrowDownIcon, CheckboxFillIcon, CheckboxGrayIcon } from "@/shared/assets/icons"; import Button from "@/shared/ui/Button"; import Chip from "@/shared/ui/Chip"; +import Toast from "@/shared/ui/Toast"; /* ───────────────────────────────────────────── InfoRow @@ -123,6 +124,15 @@ const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: st setIsTermsAgreed, } = useWriteFormStore(); + const [dismissedMessage, setDismissedMessage] = useState(null); + const showError = !!errorMessage && errorMessage !== dismissedMessage; + + useEffect(() => { + if (!errorMessage || errorMessage === dismissedMessage) return; + const timeout = setTimeout(() => setDismissedMessage(errorMessage), 2500); + return () => clearTimeout(timeout); + }, [errorMessage, dismissedMessage]); + return ( <>
@@ -166,25 +176,27 @@ const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: st
-
+

최종 금액

{selectedPlan ? `${selectedPlan.price.toLocaleString("ko-KR")}원` : "-"}

- {errorMessage && ( -

- {errorMessage} -

- )} - +
+ + +
); diff --git a/src/features/instructor/write/ui/ConceptLimitToast.tsx b/src/shared/ui/Toast.tsx similarity index 62% rename from src/features/instructor/write/ui/ConceptLimitToast.tsx rename to src/shared/ui/Toast.tsx index 682970a..59d4ff2 100644 --- a/src/features/instructor/write/ui/ConceptLimitToast.tsx +++ b/src/shared/ui/Toast.tsx @@ -1,17 +1,19 @@ import { ExclamationMarkCircleIcon } from "@/shared/assets/icons"; import { cn } from "@/shared/lib/utils/cn"; -interface ConceptLimitToastProps { +interface ToastProps { message: string; show: boolean; + className?: string; } -const ConceptLimitToast = ({ message, show }: ConceptLimitToastProps) => { +const Toast = ({ message, show, className }: ToastProps) => { return (
@@ -20,4 +22,4 @@ const ConceptLimitToast = ({ message, show }: ConceptLimitToastProps) => { ); }; -export default ConceptLimitToast; +export default Toast; diff --git a/src/widgets/instructor/write/ui/DesignConceptSection.tsx b/src/widgets/instructor/write/ui/DesignConceptSection.tsx index b203fd6..4e00c68 100644 --- a/src/widgets/instructor/write/ui/DesignConceptSection.tsx +++ b/src/widgets/instructor/write/ui/DesignConceptSection.tsx @@ -8,10 +8,10 @@ import { MAX_CONCEPT_SELECT, useWriteFormStore, } from "@/features/instructor/write"; -import ConceptLimitToast from "@/features/instructor/write/ui/ConceptLimitToast"; import ConceptResult from "@/features/instructor/write/ui/ConceptResult"; import { ArrowDownIcon, ExclamationMarkCircleIcon } from "@/shared/assets/icons"; import TextField from "@/shared/ui/input/TextField"; +import Toast from "@/shared/ui/Toast"; const LIMIT_TOAST_MESSAGE = "컨셉은 5개까지 선택할 수 있습니다. 추가적인 내용은 하단 토글을 열어 작성해주세요."; @@ -47,7 +47,11 @@ const DesignConceptSection = () => { return (
- +

디자인 컨셉

최대 5개까지 자유롭게 선택할 수 있어요

From bbcdfe4021a89a223e129e6f88db020fcee008cc Mon Sep 17 00:00:00 2001 From: YuminPark Date: Tue, 30 Jun 2026 23:49:51 +0900 Subject: [PATCH 11/13] =?UTF-8?q?#40=20[CHORE]=20FAQ=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EB=A7=81=ED=81=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/ui/Header.tsx b/src/shared/ui/Header.tsx index d0976dc..08f5337 100644 --- a/src/shared/ui/Header.tsx +++ b/src/shared/ui/Header.tsx @@ -92,7 +92,7 @@ const Header = () => { 1:1 문의하기 From d15fa89fb5f2bc97c9dcac2b7647dd79a0a163ff Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 01:22:35 +0900 Subject: [PATCH 12/13] =?UTF-8?q?#40=20[CHORE]=20disabled=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=BB=A4=EC=84=9C=20default=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Chip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/ui/Chip.tsx b/src/shared/ui/Chip.tsx index bc94c8e..048c29d 100644 --- a/src/shared/ui/Chip.tsx +++ b/src/shared/ui/Chip.tsx @@ -52,7 +52,7 @@ const Chip = ({ className={cn( "rounded-100 border-gray-20 text-body2-m text-gray-70 flex cursor-pointer items-center justify-start border px-3 py-1.5 transition-colors duration-150", disabled - ? "bg-gray-5 cursor-not-allowed" + ? "bg-gray-5 cursor-default" : isSelected ? "border-main-main bg-purple-10 text-body2-sb text-main-main" : "hover:border-gray-60", From b47b1406e6a45e2885368d917d5aaa06e981cc02 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 01:29:54 +0900 Subject: [PATCH 13/13] =?UTF-8?q?#40=20[FIX]=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=82=AC=EB=9D=BC=EC=A7=80?= =?UTF-8?q?=EB=A9=B4=20=EA=B2=B0=EC=A0=9C=ED=95=98=EA=B8=B0=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=9E=AC=ED=99=9C=EC=84=B1=ED=99=94=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../write/ui/PaymentModal/Step1.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/features/instructor/write/ui/PaymentModal/Step1.tsx b/src/features/instructor/write/ui/PaymentModal/Step1.tsx index 40a3b5a..6c2591c 100644 --- a/src/features/instructor/write/ui/PaymentModal/Step1.tsx +++ b/src/features/instructor/write/ui/PaymentModal/Step1.tsx @@ -124,14 +124,19 @@ const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: st setIsTermsAgreed, } = useWriteFormStore(); - const [dismissedMessage, setDismissedMessage] = useState(null); - const showError = !!errorMessage && errorMessage !== dismissedMessage; + const [prevErrorMessage, setPrevErrorMessage] = useState(errorMessage); + const [autoHide, setAutoHide] = useState(false); + if (errorMessage !== prevErrorMessage) { + setPrevErrorMessage(errorMessage); + setAutoHide(false); + } + const showError = !!errorMessage && !autoHide; useEffect(() => { - if (!errorMessage || errorMessage === dismissedMessage) return; - const timeout = setTimeout(() => setDismissedMessage(errorMessage), 2500); + if (!showError) return; + const timeout = setTimeout(() => setAutoHide(true), 2500); return () => clearTimeout(timeout); - }, [errorMessage, dismissedMessage]); + }, [showError]); return ( <> @@ -190,8 +195,8 @@ const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: st className="absolute -top-4 left-1/2 w-fit shrink-0 -translate-x-1/2 -translate-y-full whitespace-nowrap" />