From 49af3f14c40063a7a4e435fd9f5c8d6167e40f87 Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Thu, 11 Dec 2025 23:35:18 +0900 Subject: [PATCH 1/5] =?UTF-8?q?CDP-222=20refactor=F0=9F=94=A8=20(annotatio?= =?UTF-8?q?n):=20annotation=20delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ForgotPasswordEmailStep.tsx | 11 ------ .../forgotPassword/ForgotPasswordForm.tsx | 4 --- .../ForgotPasswordResetStep.tsx | 10 ------ .../ForgotPasswordVerifyStep.tsx | 9 ----- src/components/auth/login/LoginForm.tsx | 34 ++++++------------- src/types/auth.ts | 4 +-- 6 files changed, 13 insertions(+), 59 deletions(-) diff --git a/src/components/auth/forgotPassword/ForgotPasswordEmailStep.tsx b/src/components/auth/forgotPassword/ForgotPasswordEmailStep.tsx index 0db64b8..f4bd658 100644 --- a/src/components/auth/forgotPassword/ForgotPasswordEmailStep.tsx +++ b/src/components/auth/forgotPassword/ForgotPasswordEmailStep.tsx @@ -13,32 +13,24 @@ import { Input } from "@/shared/input"; import type { StepFieldMeta } from "@/types/auth"; export function ForgotPasswordEmailStep({ fieldId, fieldName }: StepFieldMeta) { - // ✅ 1) 전역 스토어에서 이메일 값 + setter 가져오기 const { email, setEmail } = useForgotPasswordFormStore(); - // ✅ 2) 다음 스텝으로 이동하는 액션 const { goNext } = useForgotPasswordStepStore(); - // ✅ 3) 이메일 에러 메시지 (UI 전용 로컬 상태) const [emailError, setEmailError] = useState(""); - // ✅ 4) 공통 submit 훅으로 form 기본 동작 막기 + 콜백 실행 const handleSubmit = useAuthFormSubmit(() => { let hasError = false; - // 이메일 형식 검증 if (!isValidEmail(email)) { setEmailError("올바른 이메일을 입력해주세요."); hasError = true; } - // 하나라도 실패하면 이 스텝에 머무르기 if (hasError) return; - // 디버깅용 로그 (나중에 실제 API 요청으로 대체) console.log("📨 Forgot Password Email Step:", { email }); - // 검증 통과 시 다음 스텝으로 이동 goNext(); }); @@ -63,11 +55,8 @@ export function ForgotPasswordEmailStep({ fieldId, fieldName }: StepFieldMeta) { status="default" autoComplete="email" required - // 전역 스토어 값과 연결 value={email} - // 입력 시 전역 스토어 업데이트 onChange={(e) => setEmail(e.target.value)} - // 포커스 되면 에러 메시지 초기화 onFocus={() => setEmailError("")} className={cn(emailError && "border-[1.5px] border-[var(--color-danger-600)]")} /> diff --git a/src/components/auth/forgotPassword/ForgotPasswordForm.tsx b/src/components/auth/forgotPassword/ForgotPasswordForm.tsx index e8d45bd..b2c21f1 100644 --- a/src/components/auth/forgotPassword/ForgotPasswordForm.tsx +++ b/src/components/auth/forgotPassword/ForgotPasswordForm.tsx @@ -13,22 +13,18 @@ import { ForgotPasswordResetStep } from "./ForgotPasswordResetStep"; import { ForgotPasswordVerifyStep } from "./ForgotPasswordVerifyStep"; export function ForgotPasswordForm() { - // ✅ 1) 현재 스텝 이름 + 전환 방향 가져오기 const { step, direction } = useForgotPasswordStepStore(); const isEmailStep = step === "email"; const isVerifyStep = step === "verify"; const isResetStep = step === "reset"; - // ✅ 2) 현재 스텝이 몇 번째인지 계산 (1부터 시작) const currentStepIndex = FORGOT_PASSWORD_STEP_ORDER.indexOf(step); const currentStepNumber = currentStepIndex + 1; - // ✅ 3) 스텝별 필드 메타 (id / name) const { fieldId, fieldName } = FORGOT_PASSWORD_STEP_FIELD_META[step]; return ( - // 스텝이 바뀌면 슬라이드 전환
{ let hasError = false; - // 1) 비밀번호 규칙 검증 (길이 + 특수문자) if (!isValidPassword(newPassword)) { setPasswordError("8자리 이상, 특수문자를 포함해야 합니다."); hasError = true; } - // 2) 비밀번호 확인 일치 여부 검증 if (newPassword !== passwordConfirm) { setPasswordConfirmError("비밀번호가 일치하지 않습니다."); hasError = true; @@ -47,13 +40,10 @@ export function ForgotPasswordResetStep({ fieldId, fieldName }: StepFieldMeta) { if (hasError) return; - // 최종 payload 생성 (이메일 + 코드 + 새 비밀번호) const forgotPayload = { email, code, newPassword }; - // 디버깅용 출력 (나중에 Supabase 비밀번호 재설정 API로 교체) console.log("🔑 Forgot Password Reset Step:", forgotPayload); - // 전체 입력값 초기화 reset(); }); diff --git a/src/components/auth/forgotPassword/ForgotPasswordVerifyStep.tsx b/src/components/auth/forgotPassword/ForgotPasswordVerifyStep.tsx index fadd6a3..f9bd537 100644 --- a/src/components/auth/forgotPassword/ForgotPasswordVerifyStep.tsx +++ b/src/components/auth/forgotPassword/ForgotPasswordVerifyStep.tsx @@ -15,23 +15,17 @@ import { Input } from "@/shared/input"; import type { StepFieldMeta } from "@/types/auth"; export function ForgotPasswordVerifyStep({ fieldId, fieldName }: StepFieldMeta) { - // ✅ 1) 전역 스토어에서 code setter 가져오기 const { setCode } = useForgotPasswordFormStore(); - // ✅ 2) 스텝 이동 액션 const { goNext, goPrev } = useForgotPasswordStepStore(); - // ✅ 3) OTP 훅: 4자리 코드 입력 UX 관리 const { values, inputRefs, handleChange, handleKeyDown, codeValue } = useOtpCode(CODE_LENGTH); - // ✅ 4) 코드 에러 메시지 (로컬 상태) const [codeError, setCodeError] = useState(""); - // ✅ 5) submit 핸들러 (공통 훅 사용) const handleSubmit = useAuthFormSubmit(() => { let hasError = false; - // 숫자 4자리 검증 if (!isValidCode(codeValue)) { setCodeError("4자리 숫자 인증번호를 정확히 입력해주세요."); hasError = true; @@ -39,13 +33,10 @@ export function ForgotPasswordVerifyStep({ fieldId, fieldName }: StepFieldMeta) if (hasError) return; - // 전역 스토어에 최종 코드 저장 setCode(codeValue); - // 디버깅용 로그 console.log("✅ Forgot Password Verify Step:", { code: codeValue }); - // 다음 스텝(비밀번호 재설정)으로 이동 goNext(); }); diff --git a/src/components/auth/login/LoginForm.tsx b/src/components/auth/login/LoginForm.tsx index 8fbd95d..7d44b8e 100644 --- a/src/components/auth/login/LoginForm.tsx +++ b/src/components/auth/login/LoginForm.tsx @@ -16,42 +16,32 @@ import Link from "next/link"; import { useState } from "react"; export function LoginForm() { - // ✅ 1) 전역 스토어: 이메일/비밀번호 값 + 액션 const { email, password, setEmail, setPassword, reset } = useLoginFormStore(); - // ✅ 2) 로컬 에러 상태: UI 전용 const [emailError, setEmailError] = useState(""); const [passwordError, setPasswordError] = useState(""); - // ✅ 3) 비밀번호 토글 훅 const { inputType, iconName, ariaLabel, toggleVisibility } = usePasswordVisibility(false); - // ✅ 4) 공통 submit 훅으로 preventDefault 처리 const handleSubmit = useAuthFormSubmit(() => { let hasError = false; - // 이메일 검증 if (!isValidEmail(email)) { setEmailError("올바른 이메일을 입력해주세요."); hasError = true; } - // 비밀번호 검증 (8자리 이상 + 특수문자) if (!isValidPassword(password)) { setPasswordError("8자리 이상, 특수문자를 포함해야 합니다."); hasError = true; } if (hasError) { - return; // ❌ 에러가 하나라도 있으면 제출 중단 + return; } - // ✅ 5) 검증 통과 시: 로그인 데이터 콘솔 출력 - // (나중에 이 자리에서 Supabase Auth 요청으로 교체) - console.log("🟢 Login submit:", { email, password }); - // ✅ 6) 성공 후 인풋 값 초기화 reset(); }); @@ -88,12 +78,10 @@ export function LoginForm() { placeholder="example@gmail.com" autoComplete="email" required - value={email} // ✅ 전역 스토어의 email과 연결 - onChange={(event) => setEmail(event.target.value)} // ✅ setEmail으로 업데이트 - onFocus={() => setEmailError("")} // ✅ 포커스 시 에러 해제 - className={cn( - emailError && "border-[1.5px] border-[var(--color-danger-600)]", // ✅ 에러 시 테두리 강조 - )} + value={email} + onChange={(event) => setEmail(event.target.value)} + onFocus={() => setEmailError("")} + className={cn(emailError && "border-[1.5px] border-[var(--color-danger-600)]")} /> @@ -104,7 +92,7 @@ export function LoginForm() { 비밀번호 - {/* ✅ 비밀번호 에러 문구 */} + {/* 비밀번호 에러 문구 */} {passwordError && ( {passwordError} )} @@ -114,24 +102,24 @@ export function LoginForm() { setPassword(event.target.value)} - onFocus={() => setPasswordError("")} // ✅ 포커스 시 에러 해제 + onFocus={() => setPasswordError("")} className={cn( "w-full pr-10", passwordError && "border-[1.5px] border-[var(--color-danger-600)]", // ✅ 에러 테두리 )} /> - {/* ✅ 비밀번호 보기/숨기기 토글 버튼 */} + {/* 비밀번호 보기/숨기기 토글 버튼 */} - setEmail(e.target.value)} - onFocus={() => setEmailError("")} - className={cn(emailError && "border-[1.5px] border-[var(--color-danger-600)]")} - /> + {showOtpUi ? ( +
+
+ + + {otpMsg ? {otpMsg} : null} +
+ +
+ { + const onlyNum = e.target.value.replace(/\D/g, "").slice(0, 6); + setOtp(onlyNum); + setOtpMsg(""); + setIsVerified(null); + }} + className={cn("w-full pr-[92px]", otpBorderClass)} + /> + + +
+ + +
+ ) : null} + + {serverMsg &&

{serverMsg}

} {/* 3. 버튼 (세로 정렬) */} diff --git a/src/components/auth/signup/SignupTermsStep.tsx b/src/components/auth/signup/SignupTermsStep.tsx index d537e0e..4d74a30 100644 --- a/src/components/auth/signup/SignupTermsStep.tsx +++ b/src/components/auth/signup/SignupTermsStep.tsx @@ -1,20 +1,26 @@ "use client"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +import { supabaseAuth } from "@/api/client"; import { useAuthFormSubmit } from "@/hooks/useAuthFormSubmit"; import { Button } from "@/shared/button"; import { useSignupFormStore } from "@/stores/signupFormStore"; import { useSignupStepStore } from "@/stores/signupStepStore"; -import { StepFieldMeta } from "@/types/auth"; -import { useState } from "react"; +import type { StepFieldMeta } from "@/types/auth"; export function SignupTermsStep({ fieldId, fieldName }: StepFieldMeta) { - const { email, name, password, agreeToTerms, setAgreeToTerms, reset } = useSignupFormStore(); + const router = useRouter(); + + const { email, password, agreeToTerms, setAgreeToTerms, reset } = useSignupFormStore(); const { goPrev } = useSignupStepStore(); const [serviceChecked, setServiceChecked] = useState(false); const [privacyChecked, setPrivacyChecked] = useState(false); const [termsError, setTermsError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); const syncTerms = (nextService: boolean, nextPrivacy: boolean) => { setServiceChecked(nextService); @@ -28,22 +34,35 @@ export function SignupTermsStep({ fieldId, fieldName }: StepFieldMeta) { } }; - const handleSubmit = useAuthFormSubmit(() => { + const handleSubmit = useAuthFormSubmit(async () => { if (!agreeToTerms) { setTermsError("필수 약관에 모두 동의해야 합니다."); return; } - const signupPayload = { - email, - name, - password, - agreeToTerms, - }; + if (isSubmitting) return; - console.log("🎉 Signup completed:", signupPayload); + try { + setIsSubmitting(true); + setTermsError(""); - reset(); + const { error } = await supabaseAuth.auth.signInWithPassword({ + email: email.trim().toLowerCase(), + password, + }); + + if (error) { + setTermsError("회원가입 완료 처리 중 오류가 발생했습니다."); + return; + } + + reset(); + router.replace("/"); + } catch { + setTermsError("회원가입 완료 처리 중 오류가 발생했습니다."); + } finally { + setIsSubmitting(false); + } }); return ( @@ -101,8 +120,9 @@ export function SignupTermsStep({ fieldId, fieldName }: StepFieldMeta) { {/* 버튼: 회원가입 완료 / 이전 (세로 정렬) */}
+ diff --git a/src/stores/signupFormStore.ts b/src/stores/signupFormStore.ts index bdbb659..704e2e1 100644 --- a/src/stores/signupFormStore.ts +++ b/src/stores/signupFormStore.ts @@ -8,6 +8,7 @@ const initialSignupValues: SignupFormValues = { name: "", password: "", agreeToTerms: false, + accessToken: "", }; export const useSignupFormStore = create((set) => ({ @@ -33,6 +34,11 @@ export const useSignupFormStore = create((set) => ({ agreeToTerms: value, })), + setAccessToken: (value: string) => + set(() => ({ + accessToken: value, + })), + reset: () => set(() => ({ ...initialSignupValues, diff --git a/src/types/auth.ts b/src/types/auth.ts index 026f856..e04d3f4 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -75,6 +75,7 @@ export interface SignupFormValues { name: string; password: string; agreeToTerms: boolean; + accessToken: string; } /** (회원가입용) Signup 폼 Zustand 상태 + 액션 */ @@ -83,6 +84,7 @@ export interface SignupFormState extends SignupFormValues { setName: (value: string) => void; setPassword: (value: string) => void; setAgreeToTerms: (value: boolean) => void; + setAccessToken: (value: string) => void; reset: () => void; } @@ -119,6 +121,7 @@ export interface VerifyOtpResponse { ok: boolean; verified: boolean; message: string; + accessToken?: string; } /* ========================================================= From d055eb07ddc09a78f532512ccc4273b0ba6e28d9 Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Tue, 27 Jan 2026 16:40:41 +0900 Subject: [PATCH 4/5] =?UTF-8?q?CDP-222=20fix=F0=9F=90=9B=20(ci):=20ci=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/frontend-ci.yml | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index 45249a8..a8a7848 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -68,47 +68,81 @@ jobs: runs-on: ubuntu-latest needs: lint timeout-minutes: 15 + + # ✅ 테스트 단계에서도 env 필요한 경우가 많아서 같이 주입 + env: + NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} + NEXT_PUBLIC_API_TIMEOUT: "10000" + steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} + - uses: pnpm/action-setup@v4 with: run_install: false + - name: Get pnpm store directory id: pnpm-cache run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - uses: actions/cache@v4 with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: ${{ runner.os }}-pnpm- + - run: pnpm install --frozen-lockfile + - run: pnpm run --if-present test -- --ci build: runs-on: ubuntu-latest needs: test timeout-minutes: 15 + + # ✅ 여기서 supabaseUrl required 터지던 원인 해결: build에 env 주입 + env: + NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} + NEXT_PUBLIC_API_TIMEOUT: "10000" + steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} + - uses: pnpm/action-setup@v4 with: run_install: false + - name: Get pnpm store directory id: pnpm-cache run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - uses: actions/cache@v4 with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: ${{ runner.os }}-pnpm- + - run: pnpm install --frozen-lockfile + # ✅ (선택) env 누락이면 여기서 바로 죽게 해서 원인 빨리 찾기 + - name: Check env exists + run: | + test -n "$NEXT_PUBLIC_SUPABASE_URL" || (echo "NEXT_PUBLIC_SUPABASE_URL missing" && exit 1) + test -n "$NEXT_PUBLIC_SUPABASE_ANON_KEY" || (echo "NEXT_PUBLIC_SUPABASE_ANON_KEY missing" && exit 1) + test -n "$SUPABASE_SERVICE_ROLE_KEY" || (echo "SUPABASE_SERVICE_ROLE_KEY missing" && exit 1) + # (선택) Next.js 빌드 캐시 # - uses: actions/cache@v4 # with: From c7b66b77846ee7dd01cfb864fe3693cc91c2de44 Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Tue, 27 Jan 2026 17:15:06 +0900 Subject: [PATCH 5/5] =?UTF-8?q?CDP-222=20fix=F0=9F=90=9B=20(ci):=20ci=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/frontend-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index a8a7848..490629b 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -136,7 +136,6 @@ jobs: - run: pnpm install --frozen-lockfile - # ✅ (선택) env 누락이면 여기서 바로 죽게 해서 원인 빨리 찾기 - name: Check env exists run: | test -n "$NEXT_PUBLIC_SUPABASE_URL" || (echo "NEXT_PUBLIC_SUPABASE_URL missing" && exit 1)