Skip to content

[authpage] authpage 구현#26

Closed
kevin123753 wants to merge 25 commits intodevelopfrom
feature/auth-pages
Closed

[authpage] authpage 구현#26
kevin123753 wants to merge 25 commits intodevelopfrom
feature/auth-pages

Conversation

@kevin123753
Copy link
Collaborator

@kevin123753 kevin123753 commented Sep 3, 2025

#️⃣ Related Issue

#25

📝 Problem

제가 맡은 파트 구현.
(헤더 드롭아웃 기본 구현)

✅ Solving

📌 프로젝트 구조 및 인증 흐름 정리

1. middleware


역할: 보호된 경로 접근 시 인증 여부 확인

매칭 경로: /mydashboard, /mypage, /dashboard/*

로직

요청 pathname이 보호 경로인지 확인

access_token 쿠키가 없으면 /login?next=...로 302 리다이렉트

있으면 NextResponse.next()로 통과

확장 가능

보호 경로 추가 시: protectedPaths와 config.matcher 모두 수정

JWT 검증 로직 추가 가능(현재는 단순 쿠키 존재 여부만 확인)

2. public/auth

역할: 인증/랜딩 관련 정적 에셋 보관

구조

/public/auth/image/*: 랜딩·로그인·회원가입 메인 이미지

/public/auth/icon/*: 비밀번호 토글, 소셜 아이콘 등

주의: next/image 사용 시 /public은 루트(/)로 접근 → src="/auth/image/..."

3. components

📂 components/auth

공통 컨벤션

디자인 토큰: auth-variables.module.css

접근성: aria-*, label, aria-live 등 필수 적용

AuthButton.tsx

인증 페이지 전용 버튼 (로딩/비활성 상태 지원)

Props: type, disabled, isLoading, loadingText, children, className

동작: isLoading 시 로딩 텍스트 표시

AuthHero.tsx

상단 로고 + 타이틀

로고 클릭 → 루트 이동

EmailInput.tsx

이메일 입력 + 에러 메시지

에러 시 테두리 색상 & aria-invalid 속성 적용

PasswordInput.tsx

비밀번호 입력 + 보기/숨기기 토글

Props: showPassword, onTogglePassword

type="password" ↔ "text" 전환

UnifiedModal.tsx

인증 페이지 공통 모달 (성공/에러)

Props: isOpen, onClose, message, type

오버레이 클릭 또는 확인 버튼으로 닫힘

BackButton

뒤로가기 또는 특정 경로 이동

<BackButton /> 또는 <BackButton href="/mydashboard" />

📂 components/home

Header.tsx: 상단 네비게이션 (로고 + 로그인/회원가입 링크)

Hero.tsx: 메인 비주얼 + CTA 버튼 (로그인으로 이동)

FeatureOne/Two.tsx: 기능 소개 섹션 (배경 일러스트 + 절대 위치 텍스트)

SettingsGrid.tsx: 기능 3개 카드 반복 렌더

Footer.tsx: 링크/소셜/카피라이트 (접근성 속성 포함)

4. hooks

useFormValidation.ts

범용 폼 검증 훅

규칙 기반: rules = { validator, errorMessage }

반환: errors, validateField, validateAllFields, clearError, isFormValid

특징: "빈 값은 검증하지 않음" (로그인/회원가입 훅에서 오버라이드)

5. lib

📂 lib/auth

api.ts

login(): POST /auth/login

changePassword(): PUT /auth/password

type.ts

userSchema, loginSchema, changePasswordSchema

📂 lib/users

api.ts

signup(): POST /users

getMyInfo(): GET /users/me

updateMyInfo(): PUT /users/me

uploadProfileImage(): POST /users/me/image

type.ts

UserType, SignupType, UpdateMyInfoType, UploadProfileImageType

📂 lib/validation

rules.ts: 이메일, 비밀번호, 닉네임 검증

useLoginValidation: 빈 값 허용 안 함

useSignupValidation: confirmPassword 교차 검증

6. pages

📂 /api

session.ts: POST { accessToken } → HttpOnly 쿠키 생성

me.ts: 쿠키 유무로 인증 확인 (더미 응답)

logout.ts: access_token 쿠키 즉시 만료

📂 /login

상태: email, password, showModal, showPassword 등

흐름

폼 유효성 검사

로그인 API 요청

성공 → /api/session 쿠키 저장 → 원래 경로 또는 /mydashboard 이동

실패 → 모달에 에러 표시

UX

모달 열리면 Enter로 폼 제출 차단

비밀번호 토글 가능

📂 /signup

상태: nickname, email, password, confirmPassword 등

흐름

유효성 검사 + 약관 동의

성공 → 성공 모달 → 닫기 시 /login 이동

실패 → 에러 메시지 분기 처리

📂 /mypage

레이아웃: DashboardLayout

기능

프로필 이미지 업로드

닉네임 수정

비밀번호 변경

성공/에러 → UnifiedModal 표시

SSR 인증: getServerSideProps에서 access_token 확인 → 없으면 /login

📂 /index

역할: 랜딩 페이지

구성: Header → Hero → FeatureOne → FeatureTwo → SettingsGrid → Footer

빌드: getStaticProps (정적 생성)

7. styles

auth-variables.module.css

인증 전용 CSS 변수

--auth-bg, --auth-primary, --auth-text-strong, --auth-border, --auth-placeholder, --auth-error

landing.css

랜딩 전용 토큰 + 유틸리티

예: .bg-brand, .text-brand, .bg-inverse, .text-muted-on-inverse

🔑 최종 인증 흐름

로그인 성공

/api/session → HttpOnly 쿠키 저장

middleware → 보호 경로 접근 허용

로그아웃

/api/logout → 쿠키 만료

폼 검증

useFormValidation + 로그인/회원가입 특화 훅

UI/UX

재사용 컴포넌트 활용 (입력, 버튼, 모달, 히어로)

접근성 준수 (aria 속성, 포커스 제어)

📚 Attachment

📋내 파트 외 추가된 코드 정리

1. src/pages/mydashboard/index.tsx

✅ 추가된 코드:

// 인증 상태를 받기 위한 props 타입 정의
interface MydashboardProps {
  /**
   * 서버에서 전달받은 로그인 상태
   */
  isLoggedIn: boolean;
}

/**
 * 서버 사이드에서 실행되는 함수 - 페이지 렌더링 전에 로그인 상태 확인
 */
export const getServerSideProps: GetServerSideProps = async (context) => {
  const { req } = context;

  // HttpOnly 쿠키에서 access_token 확인
  const accessToken = req.cookies.access_token;

  if (!accessToken) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }

  return {
    props: {
      isLoggedIn: true,
    },
  };
};

Mydashboard.getLayout = function getLayout(page: ReactNode) {
  return <DashboardLayout>{page}</DashboardLayout>;
};

2. src/pages/dashboard/[dashboardId].tsx

✅ 추가된 코드:

/**
 * 이재준 작성 - 인증되지 않은 사용자가 대시보드 페이지에 접근할 때 로그인 페이지로 리다이렉트하기 위해 추가
 */
export const getServerSideProps: GetServerSideProps = async (context) => {
  const { req } = context;

  const accessToken = req.cookies.access_token;

  if (!accessToken) {
    return {
      redirect: {
        destination: `/login`,
        permanent: false,
      },
    };
  }

  return { props: {} };
};

3. src/pages/dashboard/[dashboardId]/edit.tsx

✅ 추가된 코드:

/**
 * 이재준 작성 - 인증되지 않은 사용자가 대시보드 편집 페이지에 접근할 때 로그인 페이지로 리다이렉트하기 위해 추가
 */
export const getServerSideProps: GetServerSideProps = async (context) => {
  const { req } = context;

  const accessToken = req.cookies.access_token;

  if (!accessToken) {
    return {
      redirect: {
        destination: `/login`,
        permanent: false,
      },
    };
  }

  return { props: {} };
};

4. src/components/ui/dashboard-header/index.tsx

✅ 추가된 코드:

// 이재준 작성 - 내 정보 페이지로 이동하는 기능 추가
const goMyPage = useCallback(() => {
  close();
  router.push('/mypage');
}, [close, router]);

// 이재준 작성 - 내 대시보드 페이지로 이동하는 기능 추가
const goMyDashboard = useCallback(() => {
  close();
  router.push('/mydashboard');
}, [close, router]);

// 이재준 작성 - 로그아웃 기능 추가
const doLogout = useCallback(async () => {
  try {
    await fetch('/api/logout', { method: 'POST' });
  } catch {
    // 로그아웃 실패 시 무시
  }
  close();
  router.push('/');
}, [close, router]);

✅ 추가된 코드:

{/* 이재준 작성 - 프로필 드롭다운 메뉴 추가 (로그아웃, 내 정보, 내 대시보드) */}
{open && (
  <div
    role='menu'
    aria-label='사용자 메뉴'
    className='shadow-1 absolute top-full right-6 mt-2 w-44 overflow-hidden rounded-md border border-gray-200 bg-white shadow'
  >
    <button
      role='menuitem'
      className='w-full px-4 py-2 text-left text-sm hover:bg-gray-100'
      onClick={doLogout}
    >
      로그아웃
    </button>
    {(() => {
      const path = router.pathname || '';
      const onMyPage = path.startsWith('/mypage');
      const onMyDashboard = path.startsWith('/mydashboard');

      return (
        <>
          {!onMyPage && (
            <button
              role='menuitem'
              className='w-full px-4 py-2 text-left text-sm hover:bg-gray-100'
              onClick={goMyPage}
            >
              내 정보
            </button>
          )}
          {!onMyDashboard && (
            <button
              role='menuitem'
              className='w-full px-4 py-2 text-left text-sm hover:bg-gray-100'
              onClick={goMyDashboard}
            >
              내 대시보드
            </button>
          )}
        </>
      );
    })()}
  </div>
)}

Pn Rule

Note

  • P1: 꼭 반영해 주세요 (Request changes)
  • P2: 적극적으로 고려해 주세요 (Request changes)
  • P3: 웬만하면 반영해 주세요 (Comment)
  • P4: 반영해도 좋고 넘어가도 좋습니다 (Approve)
  • P5: 그냥 사소한 의견입니다 (Approve)

@vercel
Copy link

vercel bot commented Sep 3, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
taskify Error Error Sep 3, 2025 11:59am

@kevin123753 kevin123753 changed the base branch from main to develop September 3, 2025 11:59
Copy link
Collaborator

@Ospac Ospac left a comment

Choose a reason for hiding this comment

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

몇자 남겼습니다 😹 고생 많으셨어요!

Comment on lines +299 to +316
* 이재준 작성 - 인증되지 않은 사용자가 대시보드 편집 페이지에 접근할 때 로그인 페이지로 리다이렉트하기 위해 추가
*/
export const getServerSideProps: GetServerSideProps = async (context) => {
const { req } = context;

const accessToken = req.cookies.access_token;

if (!accessToken) {
return {
redirect: {
destination: `/login`,
permanent: false,
},
};
}

return { props: {} };
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분이 여러번 반복되고 있는 것 같습니다! middleware.ts로 대체해도 좋을 것 같아요

// middleware.ts 예제
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  // 요청에 따른 조건부 로직 처리
  if (request.nextUrl.pathname.startsWith('/admin')) {
    if (!request.cookies.get('auth')) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }
  return response;
}

https://reactnext-central.xyz/blog/nextjs/middleware

/**
* 서버 사이드에서 실행되는 함수 - 페이지 렌더링 전에 로그인 상태 확인
*/
export const getServerSideProps: GetServerSideProps = async (context) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

getServerSideProps()를 대부분 메인 컴포넌트 밑에 정의를 해주셨는데, 위에 정의할지 아래에 정의할지 이야기 해보는 것도 좋을 것 같아요! 저는 개인적으로 props가 전달되는 관계라서 위에서 아래로 읽는게 편한 것 같습니다.

Comment on lines +637 to +639
MyPage.getLayout = function getLayout(page: ReactNode) {
return page;
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

위 return문 에서 DashboardLayout 를 그대로 감싸는 방식을 사용하셨는데,

return (
    <DashboardLayout>
      <div
    ....

공통된 코드유지를 위해서 해당 패턴을 사용하는게 어떨까요.?>

Suggested change
MyPage.getLayout = function getLayout(page: ReactNode) {
return page;
};
MyPage.getLayout = function getLayout(page: ReactNode) {
return <DashboardLayout>{page}</DashboardLayout>
};

Copy link
Collaborator

@Ospac Ospac Sep 3, 2025

Choose a reason for hiding this comment

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

이 파일은 뭔지 모르겠습니다 ㅋㅋ

Comment on lines +118 to +124
{/* 이재준 작성 - 프로필 드롭다운 메뉴 추가 (로그아웃, 내 정보, 내 대시보드) */}
{open && (
<div
role='menu'
aria-label='사용자 메뉴'
className='shadow-1 absolute top-full right-6 mt-2 w-44 overflow-hidden rounded-md border border-gray-200 bg-white shadow'
>
Copy link
Collaborator

Choose a reason for hiding this comment

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

헤더에 드롭다운 부분은 빼서 공통컴포넌트로 다시 만들게요!

Comment on lines +3 to +23
export default function handler(
req: NextApiRequest,
res: NextApiResponse
): void {
if (req.method !== 'GET') {
res.status(405).json({ message: 'Method not allowed' });

return;
}

// HttpOnly 쿠키에서 access_token 확인
const accessToken = req.cookies.access_token;

if (!accessToken) {
res.status(401).json({ message: 'Unauthorized' });

return;
}

// TODO: 실제로는 accessToken을 검증하고 사용자 정보를 가져와야 함
// 현재는 임시로 성공 응답만 반환
Copy link
Collaborator

Choose a reason for hiding this comment

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

accessToken을 검증하는 로직이 Taskify 서버에 이미 있을텐데 Next api를 정의해서 쿠키를 발급하는 방식이 맞는건지 조금 의아합니다, 이렇게 발급한 쿠키로 다른 API 호출도 잘 되는 것일까요?

Comment on lines +72 to +131
try {
// 로그인 API 호출
const loginParams: LoginParams = {
email,
password,
};

const response = await login(loginParams);

// accessToken을 HttpOnly 쿠키로 설정
try {
const sessionResponse = await fetch('/api/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
accessToken: response.accessToken,
}),
});

if (!sessionResponse.ok) {
throw new Error('Session creation failed');
}

// 리다이렉트 경로 결정: 대시보드 경로로 향하던 경우에도 디폴트는 mydashboard
const nextParam = router.query.next as string | undefined;
const nextPath =
nextParam && !nextParam.startsWith('/dashboard')
? nextParam
: '/mydashboard';

router.push(nextPath);
} catch {
setModalMessage(
'로그인 처리 중 오류가 발생했습니다. 다시 시도해주세요.'
);
setShowModal(true);
}
} catch (error: unknown) {
// 에러 메시지 처리
const errorMessage =
error instanceof Error ? error.message : '알 수 없는 오류';

if (errorMessage.includes('[400]')) {
setModalMessage('비밀번호가 일치하지 않습니다.');
setShowModal(true);
} else if (errorMessage.includes('[404]')) {
setModalMessage('존재하지 않는 유저입니다.');
setShowModal(true);
} else {
setModalMessage('로그인에 실패했습니다. 다시 시도해주세요.');
setShowModal(true);
}
} finally {
setIsLoading(false);
}
},
[email, password, router]
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

로그인을 처리하는 로직이 굉장히 길고 try-catch도 이중으로 있어서 이후에 정리해봐도 좋을 것 같아요!
fetch하는 부분과 에러처리를 한 파일로 분리하고 에러 메시지를 throw하는 방식으로 구현하면 어떨까요?

API에러 처리에 대한 부분은 저도 구현을 해야해서, 나중에 같이 이야기해봐도 좋을것 같네요~

Comment on lines +180 to +191
const baseValid = baseValidation.isFormValid(values);

// skipConfirmPassword가 true이거나 비밀번호가 비어있으면 확인 비밀번호 검증을 건너뛰기
const confirmValid =
skipConfirmPassword || !values.password || !values.confirmPassword
? true
: validateConfirmPasswordField(
values.password,
values.confirmPassword
);

return baseValid && confirmValid;
Copy link
Collaborator

Choose a reason for hiding this comment

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

boolean으로 평가되는 값은 'is', 'has'를 접두사로 붙이는 것을 권장드립니다
더불어 복잡한 조건은 변수로 한번 감싸주면 읽기 쉬운 코드가 될 것같아요.

Suggested change
const baseValid = baseValidation.isFormValid(values);
// skipConfirmPassword가 true이거나 비밀번호가 비어있으면 확인 비밀번호 검증을 건너뛰기
const confirmValid =
skipConfirmPassword || !values.password || !values.confirmPassword
? true
: validateConfirmPasswordField(
values.password,
values.confirmPassword
);
return baseValid && confirmValid;
const isFormValid = baseValidation.isFormValid(values);
// skipConfirmPassword가 true이거나 비밀번호가 비어있으면 확인 비밀번호 검증을 건너뛰기
const isPasswordValid =
skipConfirmPassword || !values.password || !values.confirmPassword;
const isConfirmValid = isPasswordValid ? true : validateConfirmPasswordField(
values.password,
values.confirmPassword
);
return isFormValid && isConfirmValid;

Copy link
Collaborator

Choose a reason for hiding this comment

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

(아마도 나중에 알아서 잘 분리하실 것 같지만👾) 이 파일도 fetch 하는 부분들, 긴 핸들러등 이후 리팩토링 해보시면 좋을 것 같아요~~

@Ospac Ospac added the ✨ 기능 기능 구현 label Sep 4, 2025
@Ospac Ospac added this to the 기능 구현 및 1차 마무리 milestone Sep 4, 2025
@Ospac Ospac closed this Sep 4, 2025
@Ospac Ospac reopened this Sep 4, 2025
Ospac added a commit that referenced this pull request Sep 4, 2025
Squashed commit of the following:

commit 931bfcc
Author: jaejoonLee <dlfhdlwm12@naver.com>
Date:   Wed Sep 3 20:49:18 2025 +0900

    fix: eslint, 컴포넌트 분해

commit 53786a0
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Wed Sep 3 11:49:21 2025 +0900

    fix: 무한렌더링,회원가입페이지 오류 정정

commit 16960a8
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Wed Sep 3 04:13:50 2025 +0900

    feat:계정관리 페이지 ui및 기능 일부 구현

commit 90a2c39
Merge: b40e377 b773c2c
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Tue Sep 2 23:51:02 2025 +0900

    merge: common-components와 merge

commit b40e377
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Tue Sep 2 23:31:10 2025 +0900

    feat: 미들웨어 쿠기 기능 구현

commit b773c2c
Author: geha <ospac111@gmail.com>
Date:   Tue Sep 2 23:05:59 2025 +0900

    fix: side-bar padding 변경(#12)

commit f321d3e
Author: geha <ospac111@gmail.com>
Date:   Tue Sep 2 23:00:07 2025 +0900

    feat: dashbaord-layout 추가 + 모든 button active, hover 효과 추가 (#12)

commit 5d83855
Author: geha <ospac111@gmail.com>
Date:   Tue Sep 2 20:05:48 2025 +0900

    fix: webpack/svgr 삭제(svg 컴포넌트화 형식으로 변경), 버그 수정 (#12)

commit a8d1cec
Author: geha <ospac111@gmail.com>
Date:   Tue Sep 2 18:31:18 2025 +0900

    feat: side-menu ui 완료, story 추가 (#12)

commit 99e2aa6
Author: geha <ospac111@gmail.com>
Date:   Tue Sep 2 16:27:46 2025 +0900

    feat: dashboard-header story 추가, font 및 color 수정 (#12)

commit 82f0949
Author: geha <ospac111@gmail.com>
Date:   Tue Sep 2 16:02:02 2025 +0900

    feat: dashboard-header ui 완료 + svgr 세팅 (#12)

commit e79437f
Author: geha <ospac111@gmail.com>
Date:   Tue Sep 2 13:38:52 2025 +0900

    fix: eslint error 수정 및 eslint.config 업데이트 (#12)

commit fe7d5df
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Tue Sep 2 05:02:39 2025 +0900

    feat: api 및 토큰 기능 구현

commit e70b025
Merge: ac55efe ac3492a
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Tue Sep 2 03:24:33 2025 +0900

    chore: develop 브랜치 최신 이력 반영

commit ac55efe
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Tue Sep 2 02:42:50 2025 +0900

    feat: auth페이지 모달창 구현

commit 0129508
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Tue Sep 2 02:23:26 2025 +0900

    chore: visibility변경 및 auth페이지 폴더 및 파일 정리

commit 3fc8bf1
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Tue Sep 2 01:58:00 2025 +0900

    feat: 로그인페이지 및 회원가입페이지 반응형 구현

commit 54d6b2a
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Tue Sep 2 01:08:12 2025 +0900

    feat:랜딩페이지 반응형 구현

commit 6dccaf3
Author: jaejoon <dlfhdlwm12@naver.com>
Date:   Mon Sep 1 18:54:22 2025 +0900

    feat:랜딩페이지 ui 구현

commit 6374029
Author: geha <ospac111@gmail.com>
Date:   Mon Sep 1 17:12:41 2025 +0900

    feat(공통): chip-profile, tag, state 완료 (#12)

commit 4e0e93d
Author: geha <ospac111@gmail.com>
Date:   Mon Sep 1 15:55:06 2025 +0900

    feat(공통): chip-state, chip-tag 완료 (#12)

commit 63d6f83
Author: geha <ospac111@gmail.com>
Date:   Mon Sep 1 14:56:43 2025 +0900

    docs: storybook 설치 설정(#12)

commit b06fdc6
Author: geha <ospac111@gmail.com>
Date:   Mon Sep 1 14:55:24 2025 +0900

    feat: button 완료(#12)

commit 26d0d1f
Author: geha <ospac111@gmail.com>
Date:   Sun Aug 31 14:29:22 2025 +0900

    feat: twMerge, clsx를 쓰기위한 cn util 함수 추가 (#12)

commit d8cd312
Author: geha <ospac111@gmail.com>
Date:   Sun Aug 31 14:20:44 2025 +0900

    feat: storybook 설치 및 설정 (#12)
@Ospac Ospac closed this Sep 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ 기능 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants