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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,4 @@ dist-ssr

# TanStack Router auto-generated
.tanstack/
.tanstack/tmp.tanstack/
*.routeTree.gen.ts
.tanstack/tmp
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@iconify/react": "^6.0.2",
"@tanstack/react-query": "^5.90.16",
"@tanstack/react-router": "^1.145.7",
Expand All @@ -20,6 +21,7 @@
"react-hook-form": "^7.71.0",
"react-mobile-picker": "^1.2.0",
"tailwind-merge": "^3.4.0",
"zod": "^3.23.8",
"zustand": "^5.0.9"
},
"devDependencies": {
Expand Down
700 changes: 358 additions & 342 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/assets/icon/exist-suggest.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 36 additions & 7 deletions src/components/common/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { Link } from "@tanstack/react-router";
import MiniLogo from "../../assets/logo/mini-logo.svg";

type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "action";
type ButtonSize = "sm" | "md" | "lg" | "action";
Expand All @@ -8,6 +10,8 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
size?: ButtonSize;
fullWidth?: boolean;
active?: boolean;
withLogo?: boolean;
to?: string;
children: ReactNode;
}

Expand All @@ -24,26 +28,28 @@ const actionActiveStyles = "bg-core-1 text-white hover:brightness-95";
const sizeStyles: Record<ButtonSize, string> = {
sm: "h-10 px-4 text-callout2 rounded-lg",
md: "h-12 px-5 text-title2 rounded-xl",
lg: "h-14 px-6 text-title7 rounded-2xl",
action: "h-[44px] px-4 py-[13px] gap-[10px] text-callout1 rounded-xl",
lg: "h-12 px-6 text-title7 rounded-2xl",
action: "h-11 px-4 py-3 gap-2.5 text-callout1 rounded-xl",
};

const disabledStyles = "bg-text-gray5 text-text-gray4 cursor-not-allowed hover:brightness-100";

function Button({
variant = "primary",
size = "lg",
size = "action",
fullWidth = false,
disabled = false,
active = true,
withLogo = false,
to,
className = "",
children,
...props
}: ButtonProps) {
const baseStyles = "font-semibold transition-all active:scale-[0.98] flex items-center justify-center cursor-pointer";
const widthStyle = fullWidth ? "w-full" : "";
const logoStyles = withLogo ? "gap-1.5 shadow-[0_10px_24px_rgba(91,99,255,0.28)]" : "";

// action variant의 경우 active 상태에 따라 스타일 변경
const getVariantStyle = () => {
if (variant === "action") {
return active ? actionActiveStyles : variantStyles.action;
Expand All @@ -52,12 +58,35 @@ function Button({
};

const combinedStyles = disabled
? `${baseStyles} ${sizeStyles[size]} ${disabledStyles} ${widthStyle} ${className}`
: `${baseStyles} ${sizeStyles[size]} ${getVariantStyle()} ${widthStyle} ${className}`;
? `${baseStyles} ${sizeStyles[size]} ${disabledStyles} ${widthStyle} ${logoStyles} ${className}`
: `${baseStyles} ${sizeStyles[size]} ${getVariantStyle()} ${widthStyle} ${logoStyles} ${className}`;

const content = (
<>
{withLogo && (
<img
src={MiniLogo}
alt=""
className="w-[26px] h-auto select-none"
draggable={false}
/>
)}
{children}
</>
);

// to가 있으면 Link로 렌더링
if (to && !disabled) {
return (
<Link to={to} className={combinedStyles}>
{content}
</Link>
);
}

return (
<button className={combinedStyles} disabled={disabled} {...props}>
{children}
{content}
</button>
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/FilterBottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ export default function FilterBottomSheet({ isOpen, onClose, children, className
<div
onAnimationEnd={handleAnimationEnd}
className={cn(
"relative w-full max-w-[375px] bg-white rounded-t-2xl",
"relative w-full max-w-[375px] bg-white rounded-t-3xl",
"flex flex-col",
animateClass,
className || "h-[60%]"
)}>

{/* 콘텐츠 */}
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto scrollbar-hide">
{children}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/RealmatchHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function RealMatchHeader({

// 가능하면 라우터 상위로, 실패하면 브라우저 히스토리
try {
navigate({ to: "/matching-test/matching-test/step3" });
navigate({ to: "/matching/test/step3" });
} catch {
window.history.back();
}
Expand Down
44 changes: 44 additions & 0 deletions src/components/form/DateField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
interface DateFieldProps {
placeholder: string;
value?: string;
onClick: () => void;
}

// 날짜를 "YYYY년 M월 D일" 형식으로 변환하는 함수
function formatDateToKorean(dateString: string): string {
if (!dateString) return "";

try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return dateString;

const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();

return `${year}년 ${month}월 ${day}일`;
} catch {
return dateString;
}
}

export default function DateField({
placeholder,
value,
onClick,
}: DateFieldProps) {
const displayValue = value ? formatDateToKorean(value) : placeholder;

return (
<button
onClick={onClick}
className="flex items-center flex-1 h-[34px] px-4 gap-[10px] rounded-md border border-core-2 bg-white/80"
>
<span
className={`text-title3 ${value ? "text-text-black" : "text-text-gray3"}`}
>
{displayValue}
</span>
</button>
);
}
26 changes: 26 additions & 0 deletions src/components/form/FeeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
interface FeeInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
unit?: string;
}

export default function FeeInput({
value,
onChange,
placeholder = "",
unit = "원",
}: FeeInputProps) {
return (
<div className="flex items-center flex-1 h-[34px] px-4 gap-[10px] rounded-md border border-core-2 bg-white/80">
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value.replace(/[^0-9]/g, ""))}
className="flex-1 text-title3 text-text-black placeholder:text-text-gray3 focus:outline-none bg-transparent text-right"
placeholder={placeholder}
/>
<span className="text-title3 text-text-black shrink-0">{unit}</span>
</div>
);
}
27 changes: 27 additions & 0 deletions src/components/form/SelectField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
interface SelectFieldProps {
placeholder: string;
value?: string;
onClick: () => void;
}

export default function SelectField({
placeholder,
value,
onClick,
}: SelectFieldProps) {
return (
<button
onClick={onClick}
className="flex items-center justify-between w-full h-[34px] pl-4 pr-1 gap-[10px] rounded-md border border-core-2 bg-white/80"
>
<span
className={`text-title3 ${value ? "text-text-black" : "text-text-gray3"}`}
>
{value || placeholder}
</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M7 18L12.5 12L7 6" stroke="#9B9BA1" strokeWidth="1.5"/>
</svg>
</button>
);
}
41 changes: 41 additions & 0 deletions src/components/form/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useRef, useEffect } from "react";

interface TextAreaProps {
placeholder: string;
maxLength: number;
value: string;
onChange: (value: string) => void;
}

export default function TextArea({
placeholder,
maxLength,
value,
onChange,
}: TextAreaProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);

useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
}, [value]);

return (
<div className="flex items-center w-full py-[10px] px-4 gap-[10px] rounded-md border border-core-2 bg-white/80">
<textarea
ref={textareaRef}
rows={1}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value.slice(0, maxLength))}
className="flex-1 bg-transparent text-title3 text-text-black placeholder:text-text-gray3 focus:outline-none resize-none overflow-hidden leading-[20px]"
/>
<span className="text-callout1 text-text-gray3 shrink-0">
{value.length}/{maxLength}
</span>
</div>
);
}
28 changes: 28 additions & 0 deletions src/components/form/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
interface TextInputProps {
placeholder: string;
maxLength: number;
value: string;
onChange: (value: string) => void;
}

export default function TextInput({
placeholder,
maxLength,
value,
onChange,
}: TextInputProps) {
return (
<div className="flex items-center w-full h-[34px] px-4 gap-[10px] rounded-md border border-core-2 bg-white/80">
<input
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value.slice(0, maxLength))}
className="flex-1 bg-transparent text-title3 text-text-black placeholder:text-text-gray3 focus:outline-none"
/>
<span className="text-callout1 text-text-gray3 shrink-0">
{value.length}/{maxLength}
</span>
</div>
);
}
5 changes: 5 additions & 0 deletions src/components/form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as TextInput } from "./TextInput";
export { default as TextArea } from "./TextArea";
export { default as SelectField } from "./SelectField";
export { default as DateField } from "./DateField";
export { default as FeeInput } from "./FeeInput";
6 changes: 6 additions & 0 deletions src/data/existing-campaigns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// 기존 캠페인 목록 (더미 데이터)
export const existingCampaigns = [
{ id: 1, name: "글로우 쿠션 신제품 론칭 리뷰" },
{ id: 2, name: "글로우 선크림' 체험단 모집" },
{ id: 3, name: "글로우 세럼' 신제품 론팅 리뷰" },
];
22 changes: 22 additions & 0 deletions src/hooks/useHideBottomTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useContext, useLayoutEffect } from "react";
import { LayoutContext } from "../routes/_main/layout-context";

/**
* 바텀탭을 숨기는 커스텀 훅
* 바텀시트가 열릴 때 바텀탭을 숨기고, 닫히거나 컴포넌트가 언마운트되면 복원
*
* @param hide - true면 바텀탭 숨김, false면 표시
*/
export function useHideBottomTab(hide: boolean) {
const layout = useContext(LayoutContext);

useLayoutEffect(() => {
if (!layout) return;

layout.setHideBottomTab(hide);

return () => {
layout.setHideBottomTab(false);
};
}, [hide, layout]);
}
Loading