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
9 changes: 7 additions & 2 deletions app/components/form/SelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ interface SelectFieldProps {
placeholder: string;
value?: string;
onClick: () => void;
noTruncate?: boolean;
}

export default function SelectField({
placeholder,
value,
onClick,
noTruncate = false,
}: SelectFieldProps) {
// 12글자 이상이면 ...으로 표시 (noTruncate가 false일 때만)
const displayValue = !noTruncate && value && value.length > 12 ? `${value.slice(0, 12)}...` : value;

return (
<button
type="button"
Expand All @@ -18,10 +23,10 @@ export default function SelectField({
<span
className={`text-title3 ${value ? "text-text-black" : "text-text-gray3"}`}
>
{value || placeholder}
{displayValue || 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"/>
<path d="M7 18L12.5 12L7 6" stroke="#9B9BA1" strokeWidth="1.5" />
</svg>
</button>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LayoutContext } from "../../../layout-context";
import Button from "../../../../components/common/Button";
import { fetchSponsorProductDetail } from "../../api/api";
import LoadingSpinner from "../../../../components/common/LoadingSpinner";
import { useCampaignProposalStore } from "../../../../stores/campaign-proposal";

const INTERVAL = 3000;

Expand Down Expand Up @@ -152,6 +153,7 @@ export default function SponsorableDetailContent() {
const navigate = useNavigate();
const location = useLocation();
const [sp] = useSearchParams();
const setProposalData = useCampaignProposalStore((state) => state.setProposalData);

const layout = useContext(LayoutContext);
const state = (location.state ?? {}) as NavState;
Expand Down Expand Up @@ -404,16 +406,16 @@ export default function SponsorableDetailContent() {
variant="primary"
size="lg"
fullWidth
onClick={() =>
navigate("/matching/suggest/create", {
state: {
brandId: data?.brandId,
productId: data?.productId,
brandName: data?.brandName,
productName: data?.productName,
},
})
}
onClick={() => {
setProposalData({
brandId: data?.brandId ?? 0,
brandName: data?.brandName,
product: data?.productName,
productId: data?.productId,
domain: "BEAUTY",
});
navigate("/matching/suggest/create?type=new");
}}
>
{buttonText}
</Button>
Expand Down
8 changes: 4 additions & 4 deletions app/routes/business/components/MatchingTabSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { searchCollaborations, type CampaignCollaboration } from "../calendar/api/calendar";
import searchIcon from "../../../assets/search2.svg";
import closeIcon from "../../../assets/cancel.svg";
Expand Down Expand Up @@ -33,7 +33,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
const [isLoading, setIsLoading] = useState(false);

// 캠페인 검색 함수
const fetchCampaigns = async () => {
const fetchCampaigns = useCallback(async () => {
if (!keyword.trim()) return;
setIsLoading(true);
try {
Expand All @@ -47,7 +47,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
} finally {
setIsLoading(false);
}
};
}, [keyword, subTab]);

// 검색 상태에 따라 캠페인 검색
useEffect(() => {
Expand All @@ -56,7 +56,7 @@ export default function MatchingTabSection({ subTab, setSubTab, receivedCount, k
} else {
setCampaigns([]);
}
}, [isSearching, keyword, subTab]);
}, [isSearching, keyword, subTab, fetchCampaigns]);

if (isSearching) {
return (
Expand Down
2 changes: 1 addition & 1 deletion app/routes/campaign-detail/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export default function CampaignDetailRoute() {
return () => {
alive = false;
};
}, [resolvedBrandId, resolvedDomain]);
}, [resolvedBrandId, resolvedDomain, matchRateParam]);

if (!campaignId) {
return (
Expand Down
193 changes: 102 additions & 91 deletions app/routes/chat/resuggest/resuggest-content.tsx

Large diffs are not rendered by default.

106 changes: 88 additions & 18 deletions app/routes/home/components/BannerCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,41 +32,111 @@ export default function BannerCarousel({
category: CategoryKey;
}) {
const banners = category === "beauty" ? beautyBanners : fashionBanners;
const displayBanners = [banners[banners.length - 1], ...banners, banners[0]];

const [current, setCurrent] = useState(0);
const [current, setCurrent] = useState(1);
const [isDragging, setIsDragging] = useState(false);
const [isSilentJumping, setIsSilentJumping] = useState(false);
const [startX, setStartX] = useState(0);
const [dragOffset, setDragOffset] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);

const start = useCallback(() => {
timerRef.current = setInterval(() => {
setCurrent((prev) => (prev + 1) % banners.length);
}, INTERVAL);
}, [banners.length]);

const stop = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}, []);

const start = useCallback(() => {
stop();
timerRef.current = setInterval(() => {
setCurrent((prev) => prev + 1);
}, INTERVAL);
}, [stop]);

useEffect(() => {
start();
return stop;
}, [start, stop]);

const handleTransitionEnd = () => {
if (current === 0) {
setIsSilentJumping(true);
setCurrent(banners.length);
} else if (current === banners.length + 1) {
setIsSilentJumping(true);
setCurrent(1);
}
};

useEffect(() => {
if (isSilentJumping) {
const timeout = setTimeout(() => {
setIsSilentJumping(false);
}, 50);
return () => clearTimeout(timeout);
}
}, [isSilentJumping]);

const handleStart = (clientX: number) => {
stop();
setIsDragging(true);
setStartX(clientX);
setDragOffset(0);
};

const handleMove = (clientX: number) => {
if (!isDragging) return;
const offset = clientX - startX;
setDragOffset(offset);
};

const handleEnd = () => {
if (!isDragging) return;

const threshold = 50;
if (dragOffset > threshold) {
setCurrent((prev) => prev - 1);
} else if (dragOffset < -threshold) {
setCurrent((prev) => prev + 1);
}

setIsDragging(false);
setDragOffset(0);
start();
};

const activeDotIndex = (current - 1 + banners.length) % banners.length;

return (
<div className="-mx-5">
<div className="relative overflow-hidden">
<div
className="relative overflow-hidden cursor-grab active:cursor-grabbing touch-pan-y"
onMouseDown={(e) => handleStart(e.clientX)}
onMouseMove={(e) => handleMove(e.clientX)}
onMouseUp={handleEnd}
onMouseLeave={handleEnd}
onTouchStart={(e) => handleStart(e.touches[0].clientX)}
onTouchMove={(e) => handleMove(e.touches[0].clientX)}
onTouchEnd={handleEnd}
>
<div
className="flex transition-transform duration-500 ease-in-out"
style={{ transform: `translateX(-${current * 100}%)` }}
className={`flex ${isDragging || isSilentJumping ? "" : "transition-transform duration-500 ease-in-out"}`}
style={{
transform: `translateX(calc(-${current * 100}% + ${dragOffset}px))`,
}}
onTransitionEnd={handleTransitionEnd}
>
{banners.map((banner, i) => (
<div key={`${category}-${i}`} className="w-full shrink-0">
{displayBanners.map((banner, i) => (
<div
key={`${category}-${i}`}
className="w-full shrink-0 select-none"
>
<img
src={banner.src}
alt={banner.alt}
className="h-62.5 w-full object-cover"
className="h-62.5 w-full object-cover pointer-events-none"
/>
</div>
))}
Expand All @@ -77,14 +147,14 @@ export default function BannerCarousel({
<button
key={i}
type="button"
onClick={() => {
onClick={(e) => {
e.stopPropagation();
stop();
setCurrent(i);
setCurrent(i + 1);
start();
}}
className={`h-1.5 w-1.5 rounded-full transition-colors ${
i === current ? "bg-white" : "bg-white/50"
}`}
className={`h-1.5 w-1.5 rounded-full transition-colors ${i === activeDotIndex ? "bg-white" : "bg-white/50"
}`}
/>
))}
</div>
Expand Down
6 changes: 3 additions & 3 deletions app/routes/matching/components/ProposalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export default function ProposalModal({

<h3 className="text-callout3 text-text-black text-center">
{isConfirm
? (isSuggest ? "캠페인을 제안하시겠습니까?" : "지원하시겠습니까?")
: (isSuggest ? "제안하기 완료" : "지원 완료")}
? (isSuggest ? "제안하시겠습니까?" : "지원하시겠습니까?")
: (isSuggest ? "제안 완료" : "지원 완료")}
</h3>

{!isConfirm && (
Expand All @@ -72,7 +72,7 @@ export default function ProposalModal({
className="flex-[4] h-11 text-title7 rounded-xl"
onClick={onConfirm}
>
지원하기
{isSuggest ? "제안하기" : "지원하기"}
</Button>
</>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface SelectBottomSheetProps {
hasCustomInput?: boolean;
}

export default function SelectBottomSheet({
function SelectBottomSheetInner({
isOpen,
onClose,
title,
Expand Down Expand Up @@ -53,8 +53,6 @@ export default function SelectBottomSheet({
};

const handleClose = () => {
setSelected(selectedValues);
setCustomInput("");
onClose();
};

Expand Down Expand Up @@ -120,3 +118,8 @@ export default function SelectBottomSheet({
</FilterBottomSheet>
);
}

export default function SelectBottomSheet(props: SelectBottomSheetProps) {
// Reset component state when bottom sheet opens by changing key
return <SelectBottomSheetInner key={props.isOpen ? 'open' : 'closed'} {...props} />;
}
Loading