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
55 changes: 36 additions & 19 deletions app/adminpage/dashboard/_components/OrderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,26 +143,43 @@ export default function OrderCard({ order }: OrderCardProps) {
</div>
</div>

{/* 티켓 수량 + 가격 */}
{/* 구매 수량 (조건부) + 가격 */}
<div className="flex gap-4">
<div className="flex items-start gap-2.5">
<Ticket size={15} className="mt-0.5 shrink-0 text-[#6b7094]" />
<div>
<p className="text-[11px] text-[#4a4e69]">티켓</p>
<div className="flex gap-2">
{order.matchingTicketQty > 0 && (
<span className="rounded-md bg-[#ff4d61]/10 px-1.5 py-0.5 text-xs font-medium text-[#ff4d61]">
매칭 {order.matchingTicketQty}
</span>
)}
{order.optionTicketQty > 0 && (
<span className="rounded-md bg-[#6366f1]/10 px-1.5 py-0.5 text-xs font-medium text-[#818cf8]">
옵션 {order.optionTicketQty}
</span>
)}
</div>
</div>
</div>
{(() => {
// STOMP 실시간 데이터는 productCode가 "UNKNOWN"이므로 수량 필드로 단품 여부 유추
const isMatchingSingle =
order.productCode === "MATCHING_TICKET_1" ||
(order.productCode === "UNKNOWN" &&
order.matchingTicketQty > 0 &&
order.optionTicketQty === 0);

const isOptionSingle =
order.productCode === "OPTION_TICKET_1" ||
(order.productCode === "UNKNOWN" &&
order.optionTicketQty > 0 &&
order.matchingTicketQty === 0);

if (isMatchingSingle || isOptionSingle) {
return (
<div className="flex items-start gap-2.5">
<Ticket
size={15}
className="mt-0.5 shrink-0 text-[#6b7094]"
/>
<div>
<p className="text-[11px] text-[#4a4e69]">구매 수량</p>
<p className="text-sm font-semibold text-white">
{isMatchingSingle
? order.matchingTicketQty
: order.optionTicketQty}
개
</p>
</div>
</div>
);
}
return null;
})()}

<div className="flex items-start gap-2.5">
<Coins size={15} className="mt-0.5 shrink-0 text-[#6b7094]" />
Expand Down
17 changes: 12 additions & 5 deletions app/profile-image/_components/ProfileImageSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,24 @@ const ProfileImageSelection = ({

if (targetFile.type.startsWith("image/")) {
onFileChange?.(targetFile);
const reader = new FileReader();
reader.onloadend = () => {
onCustomImageChange(reader.result as string);
};
reader.readAsDataURL(targetFile);

// 기존 브라우저 메모리에 캐싱된 Blob URL이 있다면 해제 (메모리 누수 방지)
if (customImage && customImage.startsWith("blob:")) {
URL.revokeObjectURL(customImage);
}

// FileReader 대신 createObjectURL을 사용하여 빠르고 가볍게 미리보기 생성
const objectUrl = URL.createObjectURL(targetFile);
Comment thread
dasosann marked this conversation as resolved.
onCustomImageChange(objectUrl);
}
} catch (error) {
console.error("Image processing failed:", error);
alert(
"이미지 처리 중 오류가 발생했습니다. 다른 이미지 형식을 사용해 주세요.",
);
} finally {
// 동일한 파일을 다시 선택해도 onChange가 트리거되도록 값 초기화
e.target.value = "";
}
};

Expand Down
6 changes: 3 additions & 3 deletions app/roulette/_components/Roulette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ const ROULETTE_CONFIG: Record<
imageSrc: "/roulette/special_roulette.png",
items: [
{ id: 1, label: "옵션권 2장" },
{ id: 2, label: "옵션권 5장" },
{ id: 3, label: "뽑기권 1장" },
{ id: 4, label: "풀세트" },
{ id: 2, label: "뽑기권 1장" },
{ id: 3, label: "풀세트" },
{ id: 4, label: "옵션권 5장" },
{ id: 5, label: "뽑기권 5장" },
{ id: 6, label: "뽑기권 10장" },
{ id: 7, label: "1만원권 상품권" },
Expand Down
68 changes: 29 additions & 39 deletions app/roulette/_components/RouletteResultModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,17 @@ const ITEM_IMAGE_MAP: Record<string, string> = {
"옵션권 1장": "/roulette/item/option_ticket_1.png",
"옵션권 2장": "/roulette/item/option_ticket_2.png",
"옵션권 5장": "/roulette/item/option_ticket_5.png",
꽝: "/roulette/item/miss.png", // TODO: 꽝 이미지 추가 필요
꽝: "/roulette/item/no-luck.png",
"뽑기권 1장": "/roulette/item/draw_ticket_1.png",
"뽑기권 5장": "/roulette/item/draw_ticket_5.png",
"뽑기권 10장": "/roulette/item/draw_ticket_10.png",
풀세트: "/roulette/item/full_set.png",
"1만원권 상품권": "/roulette/item/gift_card_10000.png", // TODO: 상품권 이미지 추가 필요
"2만원권 상품권": "/roulette/item/gift_card_20000.png", // TODO: 상품권 이미지 추가 필요
};

// 아이템 설명 텍스트
const ITEM_DESCRIPTION_MAP: Record<string, string> = {
"옵션권 1장": "매칭 시 옵션 1개를 선택할 수 있어요",
"옵션권 2장": "매칭 시 옵션 2개를 선택할 수 있어요",
"옵션권 5장": "매칭 시 옵션 5개를 선택할 수 있어요",
꽝: "아쉽지만 다음 기회에!",
"뽑기권 1장": "새로운 매칭 기회가 생겼어요",
"뽑기권 5장": "새로운 매칭 기회 5번이 생겼어요",
"뽑기권 10장": "새로운 매칭 기회 10번이 생겼어요",
풀세트: "뽑기권 1장 + 옵션권 3장 획득!",
"1만원권 상품권": "1만원 상품권이 지급됩니다",
"2만원권 상품권": "2만원 상품권이 지급됩니다",
"1만원권 상품권": "/roulette/item/gift_card_10000.png",
"2만원권 상품권": "/roulette/item/gift_card_20000.png",
};

// ==========================================
// 컨페티 조각 (좌/우)
// 컨페티 조각 (장식용)
// ==========================================
const ConfettiLeft = () => (
<div className="pointer-events-none absolute top-[59px] left-[25px] h-[153px] w-[70.87px]">
Expand Down Expand Up @@ -84,8 +70,7 @@ export default function RouletteResultModal({
}: RouletteResultModalProps) {
if (!item) return null;

const imageSrc = ITEM_IMAGE_MAP[item.label] ?? "/roulette/prizes/miss.png";
const description = ITEM_DESCRIPTION_MAP[item.label] ?? "";
const imageSrc = ITEM_IMAGE_MAP[item.label] ?? "/roulette/item/no-luck.png";

return (
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
Expand All @@ -95,19 +80,29 @@ export default function RouletteResultModal({
>
<DialogTitle className="sr-only">룰렛 당첨 결과</DialogTitle>
<DialogDescription className="sr-only">
당첨된 보상을 확인하세요.
당첨된 보상을 확인하세요
</DialogDescription>

{/* 찐 모달 본체 */}
{/* 모달 본체 */}
<div className="relative flex w-full max-w-[343px] flex-col items-center overflow-hidden rounded-2xl bg-white px-4 pt-10 pb-6 shadow-[0px_8px_8px_rgba(0,0,0,0.08),0px_0px_16px_rgba(0,0,0,0.1)]">
{/* 컨페티 (장식용이므로 얘네만 absolute 유지) */}
<ConfettiLeft />
<ConfettiRight />
{/* 컨페티 (장식이므로 부모는 absolute 아님) */}
{item.label !== "꽝" && (
<>
<ConfettiLeft />
<ConfettiRight />
</>
)}

{/* 상단 텍스트 영역 */}
<div className="flex w-full flex-col items-center justify-center">
<span className="typo-24-600 text-color-text-highlight">
축하해요!
<span
className={`typo-24-600 ${
item.label === "꽝"
? "text-color-gray-600"
: "text-color-text-highlight"
}`}
>
{item.label === "꽝" ? "아쉬워요!" : "축하해요!"}
</span>
</div>

Expand All @@ -129,27 +124,22 @@ export default function RouletteResultModal({
{/* 하단 텍스트 영역 */}
<div className="mt-4 flex w-full flex-col items-center gap-2">
<span className="typo-24-600 text-color-text-black">
{item.label}
{item.label === "꽝" ? "이번엔 꽝이에요" : `${item.label} 당첨`}
</span>
<span className="typo-14-500 text-color-gray-400">
{item.label === "꽝" ? description : "보상이 바로 지급되었어요."}
{item.label === "꽝"
? "내일 다시 도전해 보세요!"
: "보상이 바로 지급되었어요."}
</span>
</div>

{/* 하단 버튼 영역 */}
<div className="mt-6 flex w-full flex-col items-center gap-6">
<Button onClick={onClose}>확인</Button>
{item.label !== "꽝" && (
<button
type="button"
className="typo-14-500 text-color-gray-400 cursor-pointer text-center transition-opacity hover:opacity-80"
onClick={() => {
onClose();
// TODO: 마이페이지/아이템 확인 페이지로 이동 로직 추가
}}
>
내 아이템 확인하기
</button>
<span className="typo-14-500 text-color-gray-400 text-center select-none">
충전내역에서 확인할 수 있어요.
Comment thread
dasosann marked this conversation as resolved.
</span>
)}
</div>
</div>
Expand Down
77 changes: 59 additions & 18 deletions app/roulette/special/_components/SpecialRouletteChanceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,29 @@

import React from "react";
import Image from "next/image";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";

export interface SpecialRouletteChanceCardProps {
/** 현재 결제 누적 금액 (기본값: 1000) */
currentAmount?: number;
/** 다음 룰렛 기회까지의 목표/토탈 금액 (기본값: 3000) */
targetAmount?: number;
/** 상단 누적 결제 정보 텍스트 (옵션, 기본값: "누적 결제 {currentAmount}원") */
/** 오늘 이미 룰렛에 참여했는지 여부 */
isParticipated?: boolean;
/** 상단 누적 결제 정보 텍스트 (옵션) */
accumulatedText?: string;
/** 상단 추가 결제 안내 텍스트 (옵션, 기본값: "{남은금액}원 추가 결제 시 1회") */
/** 상단 추가 결제 안내 텍스트 (옵션) */
additionalText?: string;
/** 우측 하단 목표 금액 텍스트 (옵션, 기본값: "{targetAmount}원") */
/** 우측 하단 목표 금액 텍스트 (옵션) */
targetText?: string;
className?: string;
}

export default function SpecialRouletteChanceCard({
currentAmount = 1000,
targetAmount = 3000,
isParticipated = false,
accumulatedText,
additionalText,
targetText,
Expand All @@ -32,10 +36,28 @@ export default function SpecialRouletteChanceCard({
Math.max(0, Math.round((currentAmount / targetAmount) * 100)),
);

const displayAccumulated =
accumulatedText ?? `누적 결제 ${currentAmount.toLocaleString()}원`;
const displayAdditional =
additionalText ?? `${diff.toLocaleString()}원 추가 결제 시 1회`;
const isTargetReached = currentAmount >= targetAmount;

let displayAccumulated = "";
let displayAdditional = "";

if (isParticipated) {
displayAccumulated = accumulatedText ?? "오늘 참여 완료";
displayAdditional = additionalText ?? "내일 또 도전해보세요!";
} else {
displayAccumulated =
accumulatedText ??
(isTargetReached
? "오늘 1회 참여 가능"
: `누적 결제 ${currentAmount.toLocaleString()}원`);

displayAdditional =
additionalText ??
(isTargetReached
? `누적 결제 ${targetAmount.toLocaleString()}원 달성`
: `${diff.toLocaleString()}원 추가 결제 시 1회`);
}

const displayTarget = targetText ?? `${targetAmount.toLocaleString()}원`;

return (
Expand All @@ -45,23 +67,42 @@ export default function SpecialRouletteChanceCard({
className,
)}
>
{/* Chances container (상단 티켓 아이콘 + 결제 정보) */}
{/* Chances container (상단 아이콘 + 텍스트 정보) */}
<div className="flex h-10 w-full flex-row items-center gap-3">
{/* Ticket Image */}
{/* Icon (Ticket or Check) */}
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center">
<Image
src="/roulette/ticket.png"
alt="ticket"
width={40}
height={40}
priority
className="h-10 w-10 object-contain"
/>
{isParticipated ? (
<div
className="flex h-[28px] w-[28px] items-center justify-center rounded-full border-2 border-white"
style={{
background:
"linear-gradient(102.05deg, #F57DB2 18.65%, #FF8A9B 90.53%)",
boxShadow:
"0px 0px 8px rgba(0, 0, 0, 0.1), 0px 2px 4px rgba(0, 0, 0, 0.1)",
}}
>
<Check size={16} strokeWidth={3} className="text-white" />
</div>
) : (
<Image
src="/roulette/ticket.png"
alt="ticket"
width={40}
height={40}
priority
className="h-10 w-10 object-contain"
/>
)}
</div>

{/* Info container */}
<div className="flex flex-1 flex-col justify-center gap-1">
<span className="typo-12-600 leading-[14px] text-[#FF4D61]">
<span
className={cn(
"typo-12-600 leading-[14px]",
isParticipated ? "text-[#808080]" : "text-[#FF4D61]",
)}
>
{displayAccumulated}
</span>
<span className="typo-16-600 leading-[19px] text-[#1A1A1A]">
Expand Down
Binary file added public/roulette/item/no-luck.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/roulette/special_roulette.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading