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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,21 @@
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
========================= */
--blur-hover: 1.7px;
--blur-button: 2px;

/* =========================
Z-Index Tokens
========================= */
--z-index-header: 10;
--z-index-toast: 60;

/* =========================
Radius Tokens
========================= */
Expand Down
2 changes: 1 addition & 1 deletion src/app/instructor/(withSidebar)/write/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const WriteLayout = ({ children }: { children: ReactNode }) => {
return (
<div className="bg-gray-10 min-h-screen pt-16">
<div className="mx-auto w-235">
<div className="sticky top-0 z-10">
<div className="z-header sticky top-0">
<StepHeader />
</div>
{children}
Expand Down
48 changes: 28 additions & 20 deletions src/features/instructor/write/config/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -123,18 +123,26 @@ export const SIZE_DISPLAY_MAP: Record<string, string> = {
};

export const KEYWORD_API_MAP: Record<string, string> = {
귀여운: "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 =
Expand Down
2 changes: 1 addition & 1 deletion src/features/instructor/write/model/writeFormStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: { 교재명: "", 강사명: "", 과목명: "" },
Expand Down
17 changes: 7 additions & 10 deletions src/features/instructor/write/ui/ColorChooseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import { useState } from "react";
import type { RgbaColor } from "@/features/instructor/write/lib/color";
import { clamp, hexToRgb, toHex } from "@/features/instructor/write/lib/color";
import { cn } from "@/shared/lib/utils/cn";
import Radio from "@/shared/ui/Radio";

type ColorChooseCardProps = {
index: number;
isMain: boolean;
isSelected: boolean;
color: RgbaColor | null;
onRadioChange: () => void;
onCardClick: () => void;
onColorChange: (color: RgbaColor) => void;
};
Expand All @@ -24,7 +22,6 @@ const ColorChooseCard = ({
isMain,
isSelected,
color,
onRadioChange,
onCardClick,
onColorChange,
}: ColorChooseCardProps) => {
Expand Down Expand Up @@ -61,14 +58,14 @@ const ColorChooseCard = ({
)}
onClick={onCardClick}
>
<div className="relative" onClick={e => e.stopPropagation()}>
<Radio checked={isMain} name="main-color" value={String(index)} onChange={onRadioChange} />
{isMain && (
<span className="text-caption2-m text-main-main absolute top-full left-1/2 -translate-x-1/2 pt-0.5 whitespace-nowrap">
Main
</span>
<span
className={cn(
"text-caption2-m w-6.5 text-center whitespace-nowrap",
isMain ? "text-main-main" : "text-gray-60",
)}
</div>
>
{isMain ? "Main" : "Sub"}
</span>
<div
className={cn(
"rounded-8 ml-7 size-19.5",
Expand Down
6 changes: 4 additions & 2 deletions src/features/instructor/write/ui/ConceptKeywordCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ const ConceptKeywordCard = ({
onSelect,
}: ConceptKeywordCardProps) => {
return (
<div className="rounded-20 border-gray-20 flex h-51 w-30 flex-col gap-6 border bg-white px-4 pt-4 pb-3">
<div className="flex flex-col gap-4 bg-white">
<h1 className="text-gray-80 text-body2-sb">{title}</h1>
<div className="flex w-full flex-col items-start gap-2">
<div className="flex w-full flex-col gap-2">
{keywords.map(keyword => (
<Chip
key={keyword}
label={keyword}
isSelected={selectedKeywords.includes(keyword)}
onClick={() => onSelect(keyword)}
variant="long"
className="w-35"
/>
))}
</div>
Expand Down
26 changes: 26 additions & 0 deletions src/features/instructor/write/ui/ConceptResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Chip from "@/shared/ui/Chip";

interface ConceptResultProps {
selectedKeywords: string[];
onRemove: (keyword: string) => void;
}

const ConceptResult = ({ selectedKeywords, onRemove }: ConceptResultProps) => {
return (
<div className="rounded-12 bg-gray-5 flex h-12.5 flex-row gap-8 px-4 py-2">
<p className="text-gray-80 text-body1-m flex items-center">선택한 컨셉</p>
<div className="flex flex-row flex-wrap gap-2">
{selectedKeywords.map(keyword => (
<Chip
key={keyword}
label={keyword}
variant="removable"
onRemove={() => onRemove(keyword)}
/>
))}
</div>
</div>
Comment on lines +10 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## ConceptResult.tsx\n'
sed -n '1,220p' src/features/instructor/write/ui/ConceptResult.tsx

printf '\n## Chip component references\n'
rg -n "export .*Chip|function Chip|const Chip|<Chip" src -g '*.{ts,tsx}' -A 3 -B 3

printf '\n## h-12.5 / rounded-12 utility definitions if present\n'
rg -n "h-12\\.5|rounded-12|text-body1-m|bg-gray-5|text-gray-80" src -g '*.{ts,tsx,css,scss,less}' -A 2 -B 2

Repository: Ditda-Official/Ditda-Frontend

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Chip.tsx (relevant slices)\n'
sed -n '1,220p' src/shared/ui/Chip.tsx

printf '\n## ConceptResult usage and surrounding layout\n'
rg -n "ConceptResult|selectedKeywords" src/features/instructor/write src/widgets/instructor/write -g '*.{ts,tsx}' -A 5 -B 5

printf '\n## Nearby container widths in write flow\n'
rg -n "w-[0-9]|max-w-|min-w-|flex-wrap|overflow" src/features/instructor/write src/widgets/instructor/write -g '*.{ts,tsx}' -A 2 -B 2

Repository: Ditda-Official/Ditda-Frontend

Length of output: 33087


src/features/instructor/write/ui/ConceptResult.tsx: 고정 높이는 제거하는 편이 안전합니다.
selectedKeywords가 최대 5개라 좁은 폭에서는 칩이 2줄로 래핑될 수 있는데, 현재 h-12.5로 고정돼 있어 두 번째 줄이 박스 밖으로 밀려나 아래 영역과 겹칠 수 있습니다. min-h-12.5로 바꾸거나, 한 줄만 유지하려면 flex-nowrap + overflow-x-auto로 맞춰주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/instructor/write/ui/ConceptResult.tsx` around lines 10 - 22, In
ConceptResult, the container currently uses a fixed height that can clip wrapped
chips when selectedKeywords spans multiple rows. Update the wrapper in
ConceptResult.tsx to avoid a hard height by switching to a minimum height or, if
you want to keep a single row, enforce no wrapping with horizontal overflow;
keep the existing Chip rendering and onRemove behavior unchanged.

);
};

export default ConceptResult;
45 changes: 31 additions & 14 deletions src/features/instructor/write/ui/PaymentModal/Step1.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";

import {
PLAN_LABEL_MAP,
Expand All @@ -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
Expand Down Expand Up @@ -123,6 +124,20 @@ const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: st
setIsTermsAgreed,
} = useWriteFormStore();

const [prevErrorMessage, setPrevErrorMessage] = useState(errorMessage);
const [autoHide, setAutoHide] = useState(false);
if (errorMessage !== prevErrorMessage) {
setPrevErrorMessage(errorMessage);
setAutoHide(false);
}
const showError = !!errorMessage && !autoHide;

useEffect(() => {
if (!showError) return;
const timeout = setTimeout(() => setAutoHide(true), 2500);
return () => clearTimeout(timeout);
}, [showError]);

return (
<>
<div className="scrollbar-hide min-h-0 flex-1 overflow-y-auto pt-8">
Expand Down Expand Up @@ -166,25 +181,27 @@ const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: st
<TermsSection isTermsAgreed={isTermsAgreed} setIsTermsAgreed={setIsTermsAgreed} />
</div>
</div>
<div className="relative">
<div>
<div className="flex flex-row items-center justify-between pt-6 pb-8">
<h3 className="text-heading3-sb text-gray-70">최종 금액</h3>
<p className="text-gray-90 text-title2-sb">
{selectedPlan ? `${selectedPlan.price.toLocaleString("ko-KR")}원` : "-"}
</p>
</div>
{errorMessage && (
<p className="text-red-main text-caption2-m absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 whitespace-nowrap">
{errorMessage}
</p>
)}
<Button
variant={isTermsAgreed && !errorMessage ? "large_primary" : "large_disabled"}
disabled={!isTermsAgreed || !!errorMessage}
onClick={onNext}
>
결제하기
</Button>
<div className="relative">
<Toast
message={errorMessage ?? ""}
show={showError}
className="absolute -top-4 left-1/2 w-fit shrink-0 -translate-x-1/2 -translate-y-full whitespace-nowrap"
/>
<Button
variant={isTermsAgreed && !showError ? "large_primary" : "large_disabled"}
disabled={!isTermsAgreed || showError}
onClick={onNext}
>
결제하기
</Button>
</div>
</div>
</>
);
Expand Down
11 changes: 11 additions & 0 deletions src/shared/lib/utils/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
5 changes: 5 additions & 0 deletions src/shared/lib/utils/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export const formatFileSize = (bytes: number): string => {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
};

export const MAX_FILE_SIZE_BYTES = 30 * 1024 * 1024;

export const isAllowedFileType = (file: File, extensions: string[]) =>
extensions.some(extension => file.name.toLowerCase().endsWith(extension));
28 changes: 27 additions & 1 deletion src/shared/ui/Chip.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
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 = ({
label,
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";
Expand All @@ -42,6 +46,28 @@ const Chip = ({
);
}

if (variant === "long") {
return (
<div
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-default"
: isSelected
? "border-main-main bg-purple-10 text-body2-sb text-main-main"
: "hover:border-gray-60",
className,
)}
onClick={disabled ? undefined : onClick}
role={onClick != null ? "button" : undefined}
tabIndex={onClick != null && !disabled ? 0 : undefined}
aria-disabled={disabled}
>
<span>{label}</span>
</div>
);
}

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",
Expand Down
2 changes: 1 addition & 1 deletion src/shared/ui/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const Header = () => {
1:1 문의하기
</a>
<a
href="https://friendly-case-06a.notion.site/ditda-FAQ-388fb8159b3381929564d7e4e908a64f"
href="https://friendly-case-06a.notion.site/ditda-FAQ-38ffb8159b33800bb641f769dc9e2365"
target="_blank"
rel="noopener noreferrer"
>
Expand Down
Loading
Loading