diff --git a/apps/customer/src/app/login/_components/KakaoLoginButton.tsx b/apps/customer/src/app/login/_components/KakaoLoginButton.tsx new file mode 100644 index 0000000..ca61d44 --- /dev/null +++ b/apps/customer/src/app/login/_components/KakaoLoginButton.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Icon } from "@compasser/design-system"; +import { LOGIN_TEXT } from "../_constants/login.constants"; + +interface KakaoLoginButtonProps { + onClick: () => void; +} + +export default function KakaoLoginButton({ + onClick, +}: KakaoLoginButtonProps) { + return ( + + ); +} \ No newline at end of file diff --git a/apps/customer/src/app/login/_components/LoginForm.tsx b/apps/customer/src/app/login/_components/LoginForm.tsx new file mode 100644 index 0000000..b76f96c --- /dev/null +++ b/apps/customer/src/app/login/_components/LoginForm.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Input, Button } from "@compasser/design-system"; +import type { LoginReqDTO } from "@compasser/api"; +import { LOGIN_TEXT } from "../_constants/login.constants"; + +interface LoginFormProps { + values: LoginReqDTO; + isPending: boolean; + onChangeEmail: (value: string) => void; + onChangePassword: (value: string) => void; + onSubmit: () => void; +} + +export default function LoginForm({ + values, + isPending, + onChangeEmail, + onChangePassword, + onSubmit, +}: LoginFormProps) { + return ( + <> +
+

+ {LOGIN_TEXT.emailLabel} +

+ onChangeEmail(e.target.value)} + /> +
+ +
+

+ {LOGIN_TEXT.passwordLabel} +

+ onChangePassword(e.target.value)} + /> +
+ +
+ +
+ + ); +} \ No newline at end of file diff --git a/apps/customer/src/app/login/_components/SignupLinkButton.tsx b/apps/customer/src/app/login/_components/SignupLinkButton.tsx new file mode 100644 index 0000000..7ceef5f --- /dev/null +++ b/apps/customer/src/app/login/_components/SignupLinkButton.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { LOGIN_TEXT } from "../_constants/login.constants"; + +interface SignupLinkButtonProps { + onClick: () => void; +} + +export default function SignupLinkButton({ + onClick, +}: SignupLinkButtonProps) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/customer/src/app/login/_constants/login.constants.ts b/apps/customer/src/app/login/_constants/login.constants.ts new file mode 100644 index 0000000..80aa76d --- /dev/null +++ b/apps/customer/src/app/login/_constants/login.constants.ts @@ -0,0 +1,11 @@ +export const LOGIN_TEXT = { + emailLabel: "이메일", + emailPlaceholder: "이메일을 입력해주세요", + passwordLabel: "비밀번호", + passwordPlaceholder: "비밀번호를 입력해주세요", + loginButton: "일반 로그인", + loginPendingButton: "로그인 중...", + kakaoLoginButton: "카카오 로그인", + signupButton: "회원가입", + emptyFieldMessage: "이메일과 비밀번호를 입력해주세요.", +} as const; \ No newline at end of file diff --git a/apps/customer/src/app/login/page.tsx b/apps/customer/src/app/login/page.tsx index 81b1b95..934d9d7 100644 --- a/apps/customer/src/app/login/page.tsx +++ b/apps/customer/src/app/login/page.tsx @@ -1,17 +1,70 @@ "use client"; +import { useState } from "react"; import { useRouter } from "next/navigation"; -import { Input, Button, Icon } from "@compasser/design-system"; +import type { LoginReqDTO } from "@compasser/api"; +import LoginForm from "./_components/LoginForm"; +import KakaoLoginButton from "./_components/KakaoLoginButton"; +import SignupLinkButton from "./_components/SignupLinkButton"; +import { LOGIN_TEXT } from "./_constants/login.constants"; +import { useLoginMutation } from "@/shared/queries/mutation/auth/useLoginMutation"; export default function LoginPage() { const router = useRouter(); + const loginMutation = useLoginMutation(); + + const [formValues, setFormValues] = useState({ + email: "", + password: "", + }); const handleMoveRoleSelect = () => { router.push("/role-select"); }; + const handleChangeEmail = (value: string) => { + setFormValues((prev) => ({ + ...prev, + email: value, + })); + }; + + const handleChangePassword = (value: string) => { + setFormValues((prev) => ({ + ...prev, + password: value, + })); + }; + const handleLogin = () => { - console.log("일반 로그인"); + const email = formValues.email.trim(); + const password = formValues.password.trim(); + + if (!email || !password) { + console.log(LOGIN_TEXT.emptyFieldMessage); + return; + } + + loginMutation.mutate( + { + email, + password, + }, + { + onSuccess: (response) => { + const accessToken = response.data.accessToken; + + if (typeof window !== "undefined") { + localStorage.setItem("accessToken", accessToken); + } + + router.push("/main"); + }, + onError: (error) => { + console.log("일반 로그인 실패", error); + }, + }, + ); }; const handleKakaoLogin = () => { @@ -22,57 +75,19 @@ export default function LoginPage() {
-
-

이메일

- -
- -
-

비밀번호

- -
- -
- -
+
- +
-
- -
+
diff --git a/apps/customer/src/app/signup/page.tsx b/apps/customer/src/app/signup/page.tsx index b3b6207..5d5b88f 100644 --- a/apps/customer/src/app/signup/page.tsx +++ b/apps/customer/src/app/signup/page.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { Input, Button } from "@compasser/design-system"; +import { useSignupMutation } from "@/shared/queries/mutation/auth/useSignupMutation"; export default function SignupPage() { const router = useRouter(); @@ -12,29 +13,52 @@ export default function SignupPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState(""); + const [submitError, setSubmitError] = useState(""); + + const { mutateAsync: signUp, isPending } = useSignupMutation(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const isEmailInvalid = - email.length > 0 && !emailRegex.test(email); + const isEmailInvalid = email.length > 0 && !emailRegex.test(email); const isPasswordMismatch = password.length > 0 && passwordConfirm.length > 0 && password !== passwordConfirm; - const handleSignup = () => { - if (isEmailInvalid || isPasswordMismatch) return; + const isFormInvalid = + !name.trim() || + !nickname.trim() || + !email.trim() || + !password.trim() || + !passwordConfirm.trim() || + isEmailInvalid || + isPasswordMismatch; + + const handleSignup = async () => { + if (isFormInvalid || isPending) return; + + setSubmitError(""); - console.log("회원가입", { - name, - nickname, - email, - password, - passwordConfirm, - }); + try { + await signUp({ + memberName: name, + nickname, + email, + password, + passwordConfirm, + }); - router.push("/main"); + router.push("/login"); + } catch (error) { + console.error("회원가입 실패", error); + + setSubmitError( + error instanceof Error + ? error.message + : "회원가입 중 오류가 발생했습니다.", + ); + } }; return ( @@ -96,6 +120,10 @@ export default function SignupPage() { errorMessage="비밀번호가 일치하지 않습니다." /> + + {submitError ? ( +

{submitError}

+ ) : null} @@ -106,8 +134,9 @@ export default function SignupPage() { kind="default" variant="primary" onClick={handleSignup} + disabled={isFormInvalid || isPending} > - 회원가입 + {isPending ? "회원가입 중..." : "회원가입"} diff --git a/apps/customer/src/shared/api/api.ts b/apps/customer/src/shared/api/api.ts new file mode 100644 index 0000000..f23758c --- /dev/null +++ b/apps/customer/src/shared/api/api.ts @@ -0,0 +1,39 @@ +"use client"; + +import { + createCompasserApi, + createAuthModule, + type TokenPair, + type TokenStore, +} from "@compasser/api"; + +const tokenStore: TokenStore = { + getAccessToken: () => + typeof window === "undefined" ? null : localStorage.getItem("accessToken"), + + getRefreshToken: () => + typeof window === "undefined" ? null : localStorage.getItem("refreshToken"), + + setTokens: ({ accessToken, refreshToken }: TokenPair) => { + if (typeof window === "undefined") return; + + localStorage.setItem("accessToken", accessToken); + + if (refreshToken) { + localStorage.setItem("refreshToken", refreshToken); + } + }, + + clearTokens: () => { + if (typeof window === "undefined") return; + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + }, +}; + +export const compasserApi = createCompasserApi({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL ?? "", + tokenStore, +}); + +export const authModule = createAuthModule(compasserApi); \ No newline at end of file diff --git a/apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts b/apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts new file mode 100644 index 0000000..a75cee9 --- /dev/null +++ b/apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts @@ -0,0 +1,10 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { authModule } from "@/shared/api/api"; + +export const useLoginMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(authModule.mutations.login(queryClient)); +}; \ No newline at end of file diff --git a/apps/customer/src/shared/queries/mutation/auth/useSignupMutation.ts b/apps/customer/src/shared/queries/mutation/auth/useSignupMutation.ts new file mode 100644 index 0000000..82ae4c9 --- /dev/null +++ b/apps/customer/src/shared/queries/mutation/auth/useSignupMutation.ts @@ -0,0 +1,10 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { authModule } from "@/shared/api/api"; + +export const useSignupMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(authModule.mutations.signUp(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx b/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx index cd2ffdc..d987eba 100644 --- a/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx +++ b/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx @@ -1,7 +1,13 @@ "use client"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; +import { Button, Header } from "@compasser/design-system"; +import type { + StoreUpdateReqDTO, + StoreLocationUpdateReqDTO, +} from "@compasser/api"; + import StoreNameField from "@/app/signup/register/_components/fields/StoreNameField"; import EmailField from "@/app/signup/register/_components/fields/EmailField"; import StoreAddressField from "@/app/signup/register/_components/fields/StoreAddressField"; @@ -12,130 +18,230 @@ import PhotoUploadSection from "@/app/signup/register/_components/sections/Photo import TagSection from "@/app/signup/register/_components/sections/TagSection"; import BusinessHoursModal from "@/app/signup/register/_components/modals/BusinessHoursModal"; import RandomBoxModal from "@/app/signup/register/_components/modals/RandomBoxModal"; -import { Button, Header } from "@compasser/design-system"; + +import { useMyStoreQuery } from "@/shared/queries/query/useMyStoreQuery"; +import { usePatchMyStoreMutation } from "@/shared/queries/mutation/auth/usePatchMyStoreMutation"; +import { usePatchMyStoreLocationMutation } from "@/shared/queries/mutation/auth/usePatchMyStoreLocationMutation"; +import { useRandomBoxListQuery } from "@/shared/queries/query/useRandomBoxListQuery"; +import { useCreateRandomBoxMutation } from "@/shared/queries/mutation/auth/useCreateRandomBoxMutation"; +import { useUpdateRandomBoxMutation } from "@/shared/queries/mutation/auth/useUpdateRandomBoxMutation"; +import { useDeleteRandomBoxMutation } from "@/shared/queries/mutation/auth/useDeleteRandomBoxMutation"; +import { useStoreImageQuery } from "@/shared/queries/query/useStoreImageQuery"; +import { useUploadStoreImageMutation } from "@/shared/queries/mutation/auth/useUploadStoreImageMutation"; +import { useRemoveStoreImageMutation } from "@/shared/queries/mutation/auth/useRemoveStoreImageMutation"; import { - businessHoursData, - dayLabelMap, - orderedDays, - initialRandomBoxes, - tagOptions, -} from "@/app/signup/register/_constants/register"; + parseBusinessHours, + EMPTY_BUSINESS_HOURS, +} from "@/app/signup/register/_utils/business-hours"; -import type { - AccountType, - RandomBoxItem, - RandomBoxFormValue, -} from "@/app/signup/register/_types/register"; +type FixedTag = "카페" | "베이커리" | "식당"; export default function StoreInfoEditPage() { const router = useRouter(); - const [selectedAccountType, setSelectedAccountType] = - useState(null); - const [selectedTags, setSelectedTags] = useState([]); - const [randomBoxes, setRandomBoxes] = - useState(initialRandomBoxes); - const [selectedRandomBoxIds, setSelectedRandomBoxIds] = useState([]); + const { data: myStore, isLoading: isMyStoreLoading } = useMyStoreQuery(); + const storeId = myStore?.storeId; + + const { data: randomBoxes = [] } = useRandomBoxListQuery(storeId); + const { data: storeImage } = useStoreImageQuery(); + + const patchMyStoreMutation = usePatchMyStoreMutation(); + const patchMyStoreLocationMutation = usePatchMyStoreLocationMutation(); + const createRandomBoxMutation = useCreateRandomBoxMutation(); + const updateRandomBoxMutation = useUpdateRandomBoxMutation(); + const deleteRandomBoxMutation = useDeleteRandomBoxMutation(); + const uploadStoreImageMutation = useUploadStoreImageMutation(); + const removeStoreImageMutation = useRemoveStoreImageMutation(); + + const [storeName, setStoreName] = useState(""); + const [storeEmail, setStoreEmail] = useState(""); + const [inputAddress, setInputAddress] = useState(""); + const [bankName, setBankName] = useState(""); + const [depositor, setDepositor] = useState(""); + const [bankAccount, setBankAccount] = useState(""); + const [businessHours, setBusinessHours] = useState(EMPTY_BUSINESS_HOURS); + + const tagOptions: FixedTag[] = ["카페", "베이커리", "식당"]; + const [selectedTag, setSelectedTag] = useState(""); + + const [selectedRandomBoxIds, setSelectedRandomBoxIds] = useState( + [], + ); const [isBusinessHoursModalOpen, setIsBusinessHoursModalOpen] = useState(false); const [isRandomBoxModalOpen, setIsRandomBoxModalOpen] = useState(false); const [photoFile, setPhotoFile] = useState(null); - const [photoPreviewUrl, setPhotoPreviewUrl] = useState(""); + const [photoPreviewUrl, setPhotoPreviewUrl] = useState(""); + const [imageRemoved, setImageRemoved] = useState(false); + + const mapStoreTagToLabel = (tag: string | undefined): FixedTag | "" => { + switch (tag) { + case "CAFE": + return "카페"; + case "BAKERY": + return "베이커리"; + case "RESTAURANT": + return "식당"; + default: + return ""; + } + }; + + useEffect(() => { + if (!myStore) return; + + setStoreName(myStore.storeName ?? ""); + setStoreEmail(""); + setInputAddress(myStore.inputAddress ?? ""); + setBusinessHours(parseBusinessHours(myStore.businessHours)); + setSelectedTag(mapStoreTagToLabel(myStore.tag)); + }, [myStore]); + + useEffect(() => { + if (!storeImage || imageRemoved) return; + setPhotoPreviewUrl(storeImage.imageUrl); + }, [storeImage, imageRemoved]); const businessHoursRows = useMemo(() => { + const dayLabelMap = { + mon: "월", + tue: "화", + wed: "수", + thu: "목", + fri: "금", + sat: "토", + sun: "일", + } as const; + + const orderedDays = [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun", + ] as const; + return orderedDays.map((day) => { - const value = businessHoursData[day]; - const formatted = value === "closed" ? "휴무" : value.replace("-", " ~ "); + const value = businessHours[day]; + const formatted = value === "closed" ? "휴무" : value || "-"; return { dayLabel: dayLabelMap[day], time: formatted, }; }); - }, []); - - const toggleTag = (tag: string) => { - setSelectedTags((prev) => - prev.includes(tag) - ? prev.filter((item) => item !== tag) - : [...prev, tag] - ); - }; + }, [businessHours]); const toggleRandomBoxSelection = (id: number) => { setSelectedRandomBoxIds((prev) => - prev.includes(id) - ? prev.filter((item) => item !== id) - : [...prev, id] + prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id], ); }; - const handleDeleteRandomBoxes = () => { - setRandomBoxes((prev) => - prev.filter((item) => !selectedRandomBoxIds.includes(item.id)) + const handleDeleteRandomBoxes = async () => { + if (!storeId || selectedRandomBoxIds.length === 0) return; + + await Promise.all( + selectedRandomBoxIds.map((boxId) => + deleteRandomBoxMutation.mutateAsync({ storeId, boxId }), + ), ); + setSelectedRandomBoxIds([]); }; - const handleOpenRandomBoxModal = () => { - setIsRandomBoxModalOpen(true); - }; + const handleSubmitRandomBox = async (form: { + boxName: string; + stock: number; + price: number; + buyLimit: number; + content: string; + boxId?: number; + }) => { + if (!storeId) return; - const handleCloseRandomBoxModal = () => { - setIsRandomBoxModalOpen(false); - }; + if (form.boxId) { + await updateRandomBoxMutation.mutateAsync({ + storeId, + boxId: form.boxId, + body: { + boxName: form.boxName, + stock: form.stock, + price: form.price, + buyLimit: form.buyLimit, + content: form.content, + }, + }); + return; + } - const handleSubmitRandomBox = (data: RandomBoxFormValue) => { - const nextId = - randomBoxes.length > 0 - ? Math.max(...randomBoxes.map((item) => item.id)) + 1 - : 1; - - setRandomBoxes((prev) => [ - ...prev, - { - id: nextId, - name: data.name, - quantity: data.quantity, - price: 0, - limit: data.limit, - pickupStartTime: data.pickupStartTime, - pickupEndTime: data.pickupEndTime, - description: data.description, + await createRandomBoxMutation.mutateAsync({ + storeId, + body: { + boxName: form.boxName, + stock: form.stock, + price: form.price, + buyLimit: form.buyLimit, + content: form.content, }, - ]); - }; - - const handleOpenBusinessHoursModal = () => { - setIsBusinessHoursModalOpen(true); + }); }; - const handleCloseBusinessHoursModal = () => { + const handleSubmitBusinessHours = (value: typeof businessHours) => { + setBusinessHours(value); setIsBusinessHoursModalOpen(false); }; - const handleSubmitBusinessHours = (data: any) => { - console.log("영업시간 수정", data); + const handleChangePhoto = (file: File) => { + setPhotoFile(file); + setImageRemoved(false); + setPhotoPreviewUrl(URL.createObjectURL(file)); }; - const handleSearchAddress = () => { - console.log("주소 검색"); + const handleRemovePhoto = async () => { + setPhotoFile(null); + setPhotoPreviewUrl(""); + setImageRemoved(true); + + if (storeImage) { + await removeStoreImageMutation.mutateAsync(undefined); + } }; - const handleChangePhoto = (file: File) => { - setPhotoFile(file); + const handleCompleteEdit = async () => { + if (!myStore) return; - const objectUrl = URL.createObjectURL(file); - setPhotoPreviewUrl(objectUrl); - }; + const storePayload: StoreUpdateReqDTO = { + storeName, + storeEmail, + bankName, + depositor, + bankAccount, + businessHours, + }; + + const locationPayload: StoreLocationUpdateReqDTO = { + inputAddress, + }; + + await patchMyStoreMutation.mutateAsync(storePayload); + await patchMyStoreLocationMutation.mutateAsync(locationPayload); + + if (photoFile) { + await uploadStoreImageMutation.mutateAsync(photoFile); + } - const handleCompleteEdit = () => { - console.log("수정 파일", photoFile); router.push("/mypage"); }; + if (isMyStoreLoading) { + return
불러오는 중...
; + } + return ( <>
@@ -148,17 +254,28 @@ export default function StoreInfoEditPage() {
- - - + + + {}} + /> + + setIsBusinessHoursModalOpen(true) + } /> setIsRandomBoxModalOpen(true)} />
@@ -198,13 +316,14 @@ export default function StoreInfoEditPage() { setIsBusinessHoursModalOpen(false)} + initialValue={businessHours} onSubmit={handleSubmitBusinessHours} /> setIsRandomBoxModalOpen(false)} onSubmit={handleSubmitRandomBox} /> diff --git a/apps/owner/src/app/signup/business/page.tsx b/apps/owner/src/app/signup/business/page.tsx index d970e5e..f9974b3 100644 --- a/apps/owner/src/app/signup/business/page.tsx +++ b/apps/owner/src/app/signup/business/page.tsx @@ -1,13 +1,90 @@ "use client"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { Input, Button } from "@compasser/design-system"; +import { useVerifyBusinessMutation } from "@/shared/queries/mutation/auth/useVerifyBusinessMutation"; +import { useOwnerSignupStore } from "@/shared/stores/ownerSignup.store"; +import { + normalizeBusinessNumber, + isValidBusinessNumberFormat, + isValidBusinessNumber, +} from "@/shared/utils/businessLicense"; export default function BusinessSignupPage() { const router = useRouter(); + const verifyMutation = useVerifyBusinessMutation(); + + const signupCompleted = useOwnerSignupStore((s) => s.signupCompleted); + const email = useOwnerSignupStore((s) => s.email); + const setBusinessCompleted = useOwnerSignupStore( + (s) => s.setBusinessCompleted, + ); + + const [value, setValue] = useState(""); + const [error, setError] = useState(""); + + useEffect(() => { + if (!signupCompleted) { + router.replace("/signup"); + } + }, [signupCompleted, router]); + + const handleChange = (v: string) => { + const normalized = normalizeBusinessNumber(v); + + if (normalized.length > 10) return; + + setValue(normalized); + + if (error) setError(""); + }; + + const validate = () => { + if (!value) return "사업자 번호를 입력해주세요."; + + if (!isValidBusinessNumberFormat(value)) + return "사업자 번호는 10자리 숫자입니다."; + + if (!isValidBusinessNumber(value)) + return "유효하지 않은 사업자 번호입니다."; + + return ""; + }; const handleNext = () => { - router.push("/signup/register"); + const message = validate(); + + if (message) { + setError(message); + return; + } + + if (!email) { + router.replace("/signup"); + return; + } + + verifyMutation.mutate( + { + businessLicenseNumber: value, + email, + }, + { + onSuccess: (res) => { + if (res.alreadyUpgraded) { + setError("이미 사업자 등록이 완료된 계정입니다."); + return; + } + + setBusinessCompleted(); + router.push("/signup/register"); + }, + onError: () => { + setError("사업자 번호 인증에 실패했습니다."); + }, + }, + ); }; return ( @@ -18,9 +95,15 @@ export default function BusinessSignupPage() {

사업자 등록번호를 입력해주세요!

+ handleChange(e.target.value)} + error={!!error} + errorMessage={error} />
@@ -30,11 +113,11 @@ export default function BusinessSignupPage() { diff --git a/apps/owner/src/app/signup/page.tsx b/apps/owner/src/app/signup/page.tsx index b3683b3..9d25c2d 100644 --- a/apps/owner/src/app/signup/page.tsx +++ b/apps/owner/src/app/signup/page.tsx @@ -1,13 +1,73 @@ "use client"; +import { useState } from "react"; import { Input, Button } from "@compasser/design-system"; import { useRouter } from "next/navigation"; +import type { JoinReqDTO } from "@compasser/api"; +import { useSignupMutation } from "@/shared/queries/mutation/auth/useSignupMutation"; +import { useOwnerSignupStore } from "@/shared/stores/ownerSignup.store"; export default function SignupPage() { const router = useRouter(); + const signupMutation = useSignupMutation(); + const setSignupCompleted = useOwnerSignupStore( + (state) => state.setSignupCompleted, + ); + + const [formValues, setFormValues] = useState({ + memberName: "", + nickname: "", + email: "", + password: "", + passwordConfirm: "", + }); + + const handleChange = + (key: keyof JoinReqDTO) => (e: React.ChangeEvent) => { + const value = e.target.value; + + setFormValues((prev) => ({ + ...prev, + [key]: value, + })); + }; const handleSignup = () => { - router.push("/signup/business"); + const memberName = formValues.memberName.trim(); + const nickname = formValues.nickname.trim(); + const email = formValues.email.trim(); + const password = formValues.password.trim(); + const passwordConfirm = formValues.passwordConfirm.trim(); + + if ( + !memberName || + !nickname || + !email || + !password || + !passwordConfirm + ) { + console.log("모든 항목을 입력해주세요."); + return; + } + + signupMutation.mutate( + { + memberName, + nickname, + email, + password, + passwordConfirm, + }, + { + onSuccess: () => { + setSignupCompleted(email); + router.push("/signup/business"); + }, + onError: (error) => { + console.log("회원가입 실패", error); + }, + }, + ); }; return ( @@ -16,27 +76,52 @@ export default function SignupPage() {

이름

- +

닉네임

- +

이메일

- +

비밀번호

- +

비밀번호 확인

- +
@@ -48,8 +133,9 @@ export default function SignupPage() { kind="default" variant="primary" onClick={handleSignup} + disabled={signupMutation.isPending} > - 회원가입 + {signupMutation.isPending ? "회원가입 중..." : "회원가입"} diff --git a/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx b/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx index 8a2cc1d..fc612e2 100644 --- a/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx +++ b/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx @@ -1,42 +1,127 @@ "use client"; +import { useMemo, useState } from "react"; import { Input, Tag } from "@compasser/design-system"; -import type { AccountType } from "../../_types/register"; +import { + filterBankNames, + normalizeAccountNumber, +} from "@/shared/utils/bank"; + +type AccountStep = "bank" | "holder" | "account"; interface AccountFieldProps { - selectedAccountType: AccountType | null; - onSelectAccountType: (type: AccountType) => void; + bankName: string; + depositor: string; + bankAccount: string; + onChangeBankName: (value: string) => void; + onChangeDepositor: (value: string) => void; + onChangeBankAccount: (value: string) => void; } export default function AccountField({ - selectedAccountType, - onSelectAccountType, + bankName, + depositor, + bankAccount, + onChangeBankName, + onChangeDepositor, + onChangeBankAccount, }: AccountFieldProps) { + const [step, setStep] = useState("bank"); + const [bankKeyword, setBankKeyword] = useState(bankName); + + const filteredBanks = useMemo(() => { + return filterBankNames(bankKeyword); + }, [bankKeyword]); + + const currentValue = + step === "bank" + ? bankName + : step === "holder" + ? depositor + : bankAccount; + + const currentPlaceholder = + step === "bank" + ? "은행명을 입력해주세요" + : step === "holder" + ? "예금주명을 입력해주세요" + : "계좌번호를 입력해주세요"; + + const handleChangeInput = (nextValue: string) => { + if (step === "bank") { + onChangeBankName(nextValue); + setBankKeyword(nextValue); + return; + } + + if (step === "holder") { + onChangeDepositor(nextValue); + return; + } + + onChangeBankAccount(normalizeAccountNumber(nextValue)); + }; + + const handleSelectBank = (bank: string) => { + onChangeBankName(bank); + setBankKeyword(bank); + }; + return (
-

계좌번호

+

계좌 정보

onSelectAccountType("bank")} + onClick={() => setStep("bank")} > 은행 onSelectAccountType("holder")} + onClick={() => setStep("holder")} > 예금주명 + + setStep("account")} + > + 계좌번호 +
- + handleChangeInput(e.target.value)} + /> + + {step === "bank" && bankKeyword.trim() !== "" && ( +
+ {filteredBanks.map((bank) => ( + handleSelectBank(bank)} + > + {bank} + + ))} +
+ )}
); } \ No newline at end of file diff --git a/apps/owner/src/app/signup/register/_components/fields/EmailField.tsx b/apps/owner/src/app/signup/register/_components/fields/EmailField.tsx index 13cd172..4be028b 100644 --- a/apps/owner/src/app/signup/register/_components/fields/EmailField.tsx +++ b/apps/owner/src/app/signup/register/_components/fields/EmailField.tsx @@ -2,11 +2,24 @@ import { Input } from "@compasser/design-system"; -export default function EmailField() { +interface EmailFieldProps { + value: string; + onChange: (value: string) => void; +} + +export default function EmailField({ + value, + onChange, +}: EmailFieldProps) { return (

이메일

- + onChange(e.target.value)} + />
); } \ No newline at end of file diff --git a/apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx b/apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx index cca21ce..4fecd15 100644 --- a/apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx +++ b/apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx @@ -3,10 +3,14 @@ import { Button, Input } from "@compasser/design-system"; interface StoreAddressFieldProps { + value: string; + onChange: (value: string) => void; onSearchAddress: () => void; } export default function StoreAddressField({ + value, + onChange, onSearchAddress, }: StoreAddressFieldProps) { return ( @@ -15,7 +19,12 @@ export default function StoreAddressField({
- + onChange(e.target.value)} + />
); })} @@ -167,8 +228,12 @@ export default function BusinessHoursModal({ updateSelectedDayField("openTime", value)} - onChangeEndTime={(value) => updateSelectedDayField("closeTime", value)} + onChangeStartTime={(value) => + updateSelectedDayField("openTime", value) + } + onChangeEndTime={(value) => + updateSelectedDayField("closeTime", value) + } className="ml-[5.2rem]" />
@@ -185,7 +250,7 @@ export default function BusinessHoursModal({ "flex items-center justify-center rounded-[10px] border px-[1.2rem] py-[1rem]", selectedDayValue.hasBreakTime === "yes" ? "border-primary-variant bg-primary-variant" - : "border-gray-300 bg-white" + : "border-gray-300 bg-white", )} > 유 @@ -207,7 +272,7 @@ export default function BusinessHoursModal({ "flex items-center justify-center rounded-[10px] border px-[1.2rem] py-[1rem]", selectedDayValue.hasBreakTime === "no" ? "border-primary-variant bg-primary-variant" - : "border-gray-300 bg-white" + : "border-gray-300 bg-white", )} > 무 diff --git a/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx b/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx index 0664c0d..454caca 100644 --- a/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx +++ b/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx @@ -2,41 +2,56 @@ import { useEffect, useState } from "react"; import { Button, Input, Modal, cn } from "@compasser/design-system"; -import TimeRangeField from "./TimeRangeField"; -import CountStepper from "./CountStepper"; -import type { RandomBoxFormValue } from "../../_types/register"; + +interface RandomBoxFormValue { + boxName: string; + stock: number; + price: number; + buyLimit: number; + content: string; + boxId?: number; +} interface RandomBoxModalProps { open: boolean; onClose: () => void; - onSubmit?: (data: RandomBoxFormValue) => void; + onSubmit?: (data: RandomBoxFormValue) => void | Promise; + initialValue?: RandomBoxFormValue | null; } const initialFormValue: RandomBoxFormValue = { - name: "", - quantity: 0, - limit: 0, - pickupStartTime: "00:00", - pickupEndTime: "00:00", - description: "", + boxName: "", + stock: 0, + price: 0, + buyLimit: 0, + content: "", }; export default function RandomBoxModal({ open, onClose, onSubmit, + initialValue, }: RandomBoxModalProps) { const [form, setForm] = useState(initialFormValue); useEffect(() => { if (!open) { setForm(initialFormValue); + return; + } + + if (initialValue) { + setForm(initialValue); + return; } - }, [open]); + + setForm(initialFormValue); + }, [open, initialValue]); const updateField = ( key: K, - value: RandomBoxFormValue[K] + value: RandomBoxFormValue[K], ) => { setForm((prev) => ({ ...prev, @@ -44,8 +59,13 @@ export default function RandomBoxModal({ })); }; - const handleSubmit = () => { - onSubmit?.(form); + const handleSubmit = async () => { + if (!form.boxName.trim()) return; + if (form.stock <= 0) return; + if (form.price < 0) return; + if (form.buyLimit <= 0) return; + + await onSubmit?.(form); onClose(); }; @@ -63,60 +83,68 @@ export default function RandomBoxModal({

랜덤박스 이름

updateField("name", event.target.value)} + value={form.boxName} + onChange={(event) => updateField("boxName", event.target.value)} className="w-[17rem]" containerClassName="border-gray-300" inputClassName="text-gray-600" /> -
+

총 수량

-
- updateField("quantity", value)} - min={0} - /> -
+ + updateField("stock", Number(event.target.value || 0)) + } + className="w-[17rem]" + containerClassName="border-gray-300" + inputClassName="text-gray-600" + />
-
-

구매 제한 개수

+
+

가격

-
- updateField("limit", value)} - min={0} - /> -
+ + updateField("price", Number(event.target.value || 0)) + } + className="w-[17rem]" + containerClassName="border-gray-300" + inputClassName="text-gray-600" + />
-
-

픽업 시간

- -
- updateField("pickupStartTime", value)} - onChangeEndTime={(value) => updateField("pickupEndTime", value)} - /> -
+
+

구매 제한 개수

+ + + updateField("buyLimit", Number(event.target.value || 0)) + } + className="w-[17rem]" + containerClassName="border-gray-300" + inputClassName="text-gray-600" + />

랜덤박스 설명란