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
2 changes: 1 addition & 1 deletion src/components/common/CTA_Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const CTA_Button = ({
}: CTA_ButtonProps) => {
const router = useNavigate();
const baseClass =
"inline-flex items-center justify-center rounded-2xl border shadow-sm active:scale-[0.99]";
"inline-flex items-center justify-center rounded-2xl border shadow-sm active:scale-[0.99] gap-2";
const sizeClass: Record<CTA_ButtonSize, string> = {
xsmall: "h-[2.875rem] w-[7.5625rem] text-[0.875rem]",
small: "h-[3.5rem] w-[7.5625rem]",
Expand Down
16 changes: 6 additions & 10 deletions src/components/photoManage/DropBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,8 @@ export function DropBox({
onToggle,
onSelect,
}: DropBoxProps) {
//선택되면: 왼쪽(흰색)은 옵션 label
//미선택이면: 왼쪽(흰색)은 카테고리 title
const leftText = value ? value.label : category.title;

const leftTextClass = value ? "text-neutral-100" : "text-neutral-600";

// 선택되면: 오른쪽(회색)은 가격/배수(priceText)
// 미선택이면: placeholder
const rightText = value ? value.priceText : category.placeholder;

return (
Expand All @@ -37,10 +31,9 @@ export function DropBox({
aria-expanded={isOpen}
className="w-full"
>
{/* 상단 박스(첫번째 스크린샷) */}
<div className="border-neutral-850 flex h-12.75 w-full items-center justify-between gap-2.5 rounded-[0.625rem] border px-4 py-3">
<div className="flex flex-1 justify-between">
<p className={`${leftTextClass}`}>{leftText}</p>
<p className={leftTextClass}>{leftText}</p>
<p className="text-neutral-400">{rightText}</p>
</div>

Expand All @@ -53,7 +46,6 @@ export function DropBox({
</div>
</div>

{/* 펼쳐졌을 때 옵션 리스트 */}
{isOpen && (
<div className="border-neutral-850 mt-2 overflow-hidden rounded-[0.625rem] border">
<ul>
Expand All @@ -64,7 +56,11 @@ export function DropBox({
<li key={opt.value}>
<button
type="button"
onClick={() => onSelect(category.key, opt)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); //핵심: 바깥 토글 버튼 클릭 막기
onSelect(category.key, opt); //부모에서 setOpenKey(null)로 닫힘
}}
className={`flex w-full items-center justify-between px-4 py-3 text-left ${
selected ? "bg-neutral-850" : "bg-neutral-900"
}`}
Expand Down
145 changes: 145 additions & 0 deletions src/pages/photoManage/AddressDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type React from "react";
import { ActionButton, InputForm } from "@/components/auth";
import { CTA_Button } from "@/components/common";
import { DaumAddressSearch } from "@/components/photoManage/DaumAddressSearch";
import { useCreateAddress } from "@/hooks/member";
import { usePrintOrderStore } from "@/store/usePrintOrder.store";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router";

export function AddressDetailPage() {
const navigate = useNavigate();

const deliveryAddress = usePrintOrderStore((s) => s.deliveryAddress);
const setDeliveryAddress = usePrintOrderStore((s) => s.setDeliveryAddress);

const { mutate: addAddress, isPending } = useCreateAddress();

//주소 재검색 모달
const [isSearchOpen, setIsSearchOpen] = useState(false);

//입력값: 배송지명 / 상세주소
const [addressName, setAddressName] = useState("");
const [addressDetail, setAddressDetail] = useState(
deliveryAddress?.addressDetail ?? "",
);

//store(주소선택)에서 온 값이 바뀌면 상세주소는 유지
useEffect(() => {
const id = window.setTimeout(() => {
setAddressDetail(deliveryAddress?.addressDetail ?? "");
}, 0);

return () => window.clearTimeout(id);
}, [deliveryAddress?.addressDetail]);
Comment on lines +28 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

여기서 window.setTimeout(..., 0)을 사용하는 것은 불필요해 보입니다. useEffect는 렌더링 후에 실행되며, React가 상태 업데이트를 배치(batch) 처리합니다. setTimeout을 사용하면 컴포넌트의 동작을 예측하기 어렵게 만들고 디버깅을 복잡하게 할 수 있습니다. useEffect 내에서 직접 setAddressDetail을 호출하는 것이 더 명확합니다.

  useEffect(() => {
    setAddressDetail(deliveryAddress?.addressDetail ?? "");
  }, [deliveryAddress?.addressDetail]);


const zipcode = deliveryAddress?.zipcode ?? "";
const address = deliveryAddress?.address ?? "";

const canSubmit = useMemo(() => {
const nameOk = addressName.trim().length > 0;
const detailOk = addressDetail.trim().length > 0;
const baseOk = zipcode.trim().length > 0 && address.trim().length > 0;
return nameOk && detailOk && baseOk && !isPending;
}, [addressName, addressDetail, zipcode, address, isPending]);

const handleAddressFound = (data: { zipcode: string; address: string }) => {
//주소 다시 선택하면 store 갱신
setDeliveryAddress({
recipientName: "", // store 타입 유지용(지금 플로우에선 안 씀)
phone: "", // store 타입 유지용(지금 플로우에선 안 씀)
zipcode: data.zipcode,
address: data.address,
addressDetail, // 현재 입력중 상세주소 유지
});
Comment on lines +48 to +54
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

recipientNamephone에 빈 문자열을 할당하여 타입 호환성을 맞추는 것보다, DeliveryAddressRequest 타입 자체에서 해당 속성들을 선택적(optional)으로 만드는 것이 더 나은 접근 방식일 수 있습니다. 이렇게 하면 아직 값이 없는 상태를 더 명확하게 표현할 수 있고, 불필요한 플레이스홀더 값 사용을 피할 수 있습니다. 예를 들어, 타입을 다음과 같이 수정하는 것을 고려해볼 수 있습니다.

// In src/types/photomanage/printOrder.ts
export interface DeliveryAddressRequest {
  recipientName?: string;
  phone?: string;
  zipcode: string;
  address: string;
  addressDetail?: string;
}


setIsSearchOpen(false);
};

const handleSubmit = () => {
if (!canSubmit) return;

// 기본주소는 zustand에서 가져온 그대로 사용
addAddress(
{
addressName: addressName.trim(),
zipcode,
address,
addressDetail: addressDetail.trim(),
isDefault: false,
},
{
onSuccess: (res) => {
const newId = res.data.addressId;

// SelectAddressPage로 돌아가면서 상세 주소 정보 입력한 주소가 선택되게
navigate("../select-address", {
replace: true,
state: { selectedAddressId: newId },
});
},
},
);
};

return (
<div className="flex h-full flex-col">
<main className="flex flex-1 flex-col gap-10 py-10">
<section className="flex gap-[1.25rem]">
<InputForm
name="주소"
placeholder="주소를 선택해주세요"
size="medium"
value={address}
onChange={() => {}}
disabled={true}
/>
<ActionButton
type="button"
text="주소 찾기"
disabled={false}
onClick={() => setIsSearchOpen(true)}
/>
</section>

<InputForm
name="상세주소"
placeholder="상세주소를 입력해주세요"
size="large"
value={addressDetail}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAddressDetail(e.target.value)
}
/>

<InputForm
name="배송지명"
placeholder="배송지명을 입력해주세요 (예: 우리집, 회사)"
size="large"
value={addressName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAddressName(e.target.value)
}
/>
</main>

<footer className="border-neutral-850 sticky bottom-0 z-50 h-[var(--tabbar-height)] w-full max-w-6xl border-t bg-neutral-900">
<div className="flex h-full items-center">
<CTA_Button
text={isPending ? "저장 중..." : "입력 완료"}
size="xlarge"
color={canSubmit ? "orange" : "black"}
disabled={!canSubmit}
onClick={handleSubmit}
/>
</div>
</footer>

<DaumAddressSearch
open={isSearchOpen}
onClose={() => setIsSearchOpen(false)}
onComplete={handleAddressFound}
/>
</div>
);
}
46 changes: 42 additions & 4 deletions src/pages/photoManage/DetailInfoPage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,54 @@
import type React from "react";
import { InputForm } from "@/components/auth";
import { CTA_Button } from "@/components/common";
import { usePrintOrderStore } from "@/store/usePrintOrder.store";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router";

export function DetailInfoPage() {
const navigate = useNavigate();

const deliveryAddress = usePrintOrderStore((s) => s.deliveryAddress);
const setDeliveryAddress = usePrintOrderStore((s) => s.setDeliveryAddress);
const setSelectedOptions = usePrintOrderStore((s) => s.setSelectedOptions);

//store 값으로 초기화
const [recipientName, setRecipientName] = useState(
deliveryAddress?.recipientName ?? "",
);
const [phone, setPhone] = useState(deliveryAddress?.phone ?? "");

const [recipientName, setRecipientName] = useState("");
const [phone, setPhone] = useState("");
//뒤로 갔다가 다시 들어왔을 때(또는 주소 선택 페이지에서 address가 갱신됐을 때)
//로컬 state가 store를 따라가게 동기화
useEffect(() => {
const id = window.setTimeout(() => {
setRecipientName(deliveryAddress?.recipientName ?? "");
setPhone(deliveryAddress?.phone ?? "");
}, 0);

return () => window.clearTimeout(id);
}, [deliveryAddress?.recipientName, deliveryAddress?.phone]);
Comment on lines +23 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

window.setTimeout(..., 0)을 사용하여 상태 업데이트를 스케줄링할 필요가 없어 보입니다. useEffect 내에서 직접 상태 설정 함수(setRecipientName, setPhone)를 호출하는 것이 코드를 더 간단하고 예측 가능하게 만듭니다.

  useEffect(() => {
    setRecipientName(deliveryAddress?.recipientName ?? "");
    setPhone(deliveryAddress?.phone ?? "");
  }, [deliveryAddress?.recipientName, deliveryAddress?.phone]);


const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const digits = e.target.value.replace(/\D/g, "").slice(0, 11);
setPhone(digits);

if (!deliveryAddress) return;
setDeliveryAddress({
...deliveryAddress,
phone: digits,
});
};

const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const next = e.target.value;
setRecipientName(next);

if (!deliveryAddress) return;
setDeliveryAddress({
...deliveryAddress,
recipientName: next.trimStart(), //앞 공백만 제거
});
};

const isNextEnabled = useMemo(() => {
Expand All @@ -31,6 +65,9 @@ export function DetailInfoPage() {
recipientName: recipientName.trim(),
phone,
});

setSelectedOptions({});

navigate("/photoManage/print-option");
};

Expand All @@ -42,7 +79,7 @@ export function DetailInfoPage() {
placeholder="받는 사람의 이름을 입력해 주세요"
size="large"
value={recipientName}
onChange={(e) => setRecipientName(e.target.value)}
onChange={handleNameChange}
/>
<InputForm
name="휴대폰 번호"
Expand All @@ -52,6 +89,7 @@ export function DetailInfoPage() {
onChange={handlePhoneChange}
/>
</main>

<footer className="border-neutral-850 sticky bottom-0 z-50 h-[var(--tabbar-height)] w-full max-w-6xl border-t bg-neutral-900">
<div className="flex h-full items-center">
<CTA_Button
Expand Down
53 changes: 43 additions & 10 deletions src/pages/photoManage/PickUpMethodPage.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,69 @@
import { ShoeIcon, TruckIcon } from "@/assets/icon";
import { CTA_Button } from "@/components/common";
import { BigButton } from "@/components/photoManage/BigButton";
import { useAddressIdStore } from "@/store/useAddressId.store";
import { usePrintOrderStore } from "@/store/usePrintOrder.store";
import type { ReceiptMethod } from "@/types/photomanage/process";
import { useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router";

type PickUpMethod = "pickup" | "delivery";

const toReceiptMethod = (m: PickUpMethod): ReceiptMethod =>
m === "pickup" ? "PICKUP" : "DELIVERY";

const fromReceiptMethod = (
m: ReceiptMethod | null | undefined,
): PickUpMethod | null => {
if (m === "PICKUP") return "pickup";
if (m === "DELIVERY") return "delivery";
return null;
};

export function PickUpMethodPage() {
const navigate = useNavigate();

const setSelectedAddressId = useAddressIdStore((s) => s.setSelectedAddressId);

const receiptMethod = usePrintOrderStore((s) => s.receiptMethod);
const setReceiptMethod = usePrintOrderStore((s) => s.setReceiptMethod);
const setDeliveryAddress = usePrintOrderStore((s) => s.setDeliveryAddress);
const setSelectedOptions = usePrintOrderStore((s) => s.setSelectedOptions);

const [selectedMethod, setSelectedMethod] = useState<PickUpMethod | null>(
null,
fromReceiptMethod(receiptMethod),
);

useEffect(() => {
setSelectedMethod(fromReceiptMethod(receiptMethod));
}, [receiptMethod]);

const isNextEnabled = useMemo(
() => Boolean(selectedMethod),
[selectedMethod],
);

const isNextEnabled = selectedMethod;
const handlePick = (m: PickUpMethod) => {
setSelectedMethod(m);
//선택은 store에 유지
setReceiptMethod(toReceiptMethod(m));
};

const handleNext = () => {
if (!selectedMethod) return;
setReceiptMethod(toReceiptMethod(selectedMethod));

if (selectedMethod === "delivery") {
navigate("/photoManage/select-address");
} else {
setDeliveryAddress(null);
// 다음을 누르면 항상 주소 초기화(새로 입력 유도)
setDeliveryAddress(null);
setSelectedAddressId(null);

if (selectedMethod === "pickup") {
setSelectedOptions({});
navigate("/photoManage/print-option");
return;
}

// delivery
navigate("/photoManage/select-address");
};

return (
Expand All @@ -48,14 +81,14 @@ export function PickUpMethodPage() {
description="매장 방문 후 직접 수령"
icon={ShoeIcon}
isSelected={selectedMethod === "pickup"}
onClick={() => setSelectedMethod("pickup")}
onClick={() => handlePick("pickup")}
/>
<BigButton
title="배송"
description="배송비 3000원 추가"
icon={TruckIcon}
isSelected={selectedMethod === "delivery"}
onClick={() => setSelectedMethod("delivery")}
onClick={() => handlePick("delivery")}
/>
</main>

Expand Down
Loading