diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bbf838..6f42725 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium + - name: Lint run: pnpm lint diff --git a/.gitignore b/.gitignore index ef313fc..ca6c905 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ pnpm-debug.log* lerna-debug.log* node_modules +.pnpm-store/ dist dist-ssr .next diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 27ab729..67ed411 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -92,7 +92,7 @@ - `LibraryBookCard`는 도서관/레주메북 진입용 160×220 책 커버 카드입니다. 현재 커버는 `public/assets/library-book-cover.svg` 고정 asset을 사용하며, 이 파일명/경로는 issue #14 초기 카드 계약입니다. 카드 종류가 늘어나면 `cover` 또는 `variant` 계약을 별도 결정합니다. - `PortfolioResumeSheet`는 학생 포트폴리오 첫 자기소개/이력서 시트입니다. 사용자 지정 기준에 따라 이름은 `Title Small`, 전공 상태는 `Body Tiny`, 학번/학과/email 메타는 `Body very Tiny`를 사용합니다. `resume small` / `resume major` token은 PDF/고밀도 출력 기준이 확정될 때 별도 적용 여부를 결정합니다. -- 공개 route와 `ComponentPreview`에서 쓰는 예시 데이터는 실제 API mock이 아니라 화면 확인용 fixture이므로 `src/shared/fixtures/examples/publicExamples.ts`에 둡니다. +- 공개 포트폴리오 route에서 쓰는 예시 데이터는 실제 API mock이 아니라 화면 확인용 fixture이므로 `src/shared/fixtures/examples/publicExamples.ts`에 둡니다. ### Spacing diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..b928c23 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next' + +import { AuthLoginPage } from '@/features/auth/ui' + +export const metadata: Metadata = { + title: '로그인', +} + +export default function LoginPage() { + return +} diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 6ae6edb..eca846e 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -1,49 +1,5 @@ -import Link from 'next/link' - -import { samplePortfolioHref, sampleResumeBookHref } from '@/shared/fixtures/examples/publicExamples' -import { LibraryBookCard } from '@/shared/ui' - -import styles from './publicRoute.module.css' +import { redirect } from 'next/navigation' export default function HomePage() { - return ( -
-
-
- Repo -

- 대덕소프트마이스터고 학생을 위한 포트폴리오 플랫폼 -

-

- Repo는 학생의 이력서와 포트폴리오를 정리하고, 공개 URL을 통해 성장 기록을 보여주기 위한 서비스입니다. -

-
- - 공개 포트폴리오 예시 - - - 레주메북 예시 - -
-
- -
-
- Examples -

- 바로 확인하는 포트폴리오와 레주메북 예시 -

-

- 실제 데이터 연동 전에도 공개 포트폴리오와 레주메북의 진입 카드를 같은 컴포넌트 패턴으로 확인합니다. -

-
- -
- - -
-
-
-
- ) + redirect('/login') } diff --git a/src/app/(public)/publicRoute.module.css b/src/app/(public)/publicRoute.module.css index 59e4c35..060d88e 100644 --- a/src/app/(public)/publicRoute.module.css +++ b/src/app/(public)/publicRoute.module.css @@ -42,100 +42,13 @@ line-height: var(--repo-font-body-large-line-height); } -.actionRow { - display: flex; - flex-wrap: wrap; - gap: 12px; -} - -.actionLink { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 48px; - padding: 0 18px; - border: 1px solid var(--border); - border-radius: 14px; - background: var(--surface-muted); - font-size: var(--repo-font-title-tiny-size); - font-weight: var(--repo-font-title-tiny-weight); - line-height: var(--repo-font-title-tiny-line-height); -} - -.primaryActionLink { - color: var(--repo-text-black); - border-color: var(--brand); - background: var(--brand); -} - -.exampleSection { - display: grid; - grid-template-columns: minmax(0, 1fr) max-content; - gap: 48px; - align-items: center; - margin-top: 24px; - padding: 40px; - border: 1px solid var(--border); - border-radius: 32px; - background: var(--surface); -} - -.exampleSectionHeader { - display: grid; - gap: 16px; - max-width: 560px; -} - -.exampleTitle { - margin: 0; - font-size: var(--repo-font-title-medium-size); - font-weight: var(--repo-font-title-medium-weight); - line-height: var(--repo-font-title-medium-line-height); - letter-spacing: -0.04em; -} - -.exampleDescription { - margin: 0; - color: var(--text-secondary); - font-size: var(--repo-font-body-medium-size); - font-weight: var(--repo-font-body-medium-weight); - line-height: var(--repo-font-body-medium-line-height); -} - -.libraryBookGrid { - display: grid; - grid-template-columns: repeat(2, max-content); - gap: 20px; - padding: 24px; - border-radius: 28px; - background: var(--repo-gray-100); -} - -@media (max-width: 820px) { - .exampleSection { - grid-template-columns: 1fr; - } - - .libraryBookGrid { - justify-content: start; - width: fit-content; - } -} - -@media (max-width: 560px) { - .libraryBookGrid { - grid-template-columns: 1fr; - } -} - @media (max-width: 480px) { .pageShell { width: min(100% - 32px, 1120px); padding: 48px 0; } - .heroCard, - .exampleSection { + .heroCard { padding: 28px; border-radius: 24px; } diff --git a/src/app/(public)/resume-books/[bookId]/page.tsx b/src/app/(public)/resume-books/[bookId]/page.tsx index a724695..b11b3da 100644 --- a/src/app/(public)/resume-books/[bookId]/page.tsx +++ b/src/app/(public)/resume-books/[bookId]/page.tsx @@ -18,7 +18,7 @@ export default async function ResumeBookPage({ params }: ResumeBookPageProps) { {bookId} 레주메북

- 학년/전공/주제별 학생 포트폴리오를 모아보는 레주메북 화면의 Next.js route placeholder입니다. + 학년/전공/주제별 학생 포트폴리오를 모아보는 레주메북 화면입니다.

diff --git a/src/app/dev/component-preview/page.tsx b/src/app/dev/component-preview/page.tsx deleted file mode 100644 index 8dc2452..0000000 --- a/src/app/dev/component-preview/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { notFound } from 'next/navigation' - -import { ComponentPreview } from '@/dev/ComponentPreview' - -export default function ComponentPreviewPage() { - if (process.env.NODE_ENV !== 'development') { - notFound() - } - - return -} diff --git a/src/dev/ComponentPreview.module.css b/src/dev/ComponentPreview.module.css deleted file mode 100644 index 45216d0..0000000 --- a/src/dev/ComponentPreview.module.css +++ /dev/null @@ -1,236 +0,0 @@ -.preview { - display: grid; - gap: 72px; - min-height: 100vh; - padding: 32px 36px 80px; - color: var(--repo-text-black); - background: var(--repo-gray-600); -} - -.section { - display: grid; - gap: 28px; - width: fit-content; -} - -.sectionTitle { - margin: 0; - color: var(--repo-text-black); - font-size: var(--repo-font-title-s-medium-size); - font-weight: var(--repo-font-title-s-medium-weight); - line-height: var(--repo-font-title-s-medium-line-height); -} - -.buttonFrameWrap { - display: grid; - grid-template-columns: max-content 1fr; - gap: 28px; - align-items: center; -} - -.stateLabels { - display: grid; - gap: 56px; - padding-top: 75px; -} - -.stateLabel { - margin: 0; - color: var(--repo-text-black); - font-size: var(--repo-font-title-small-size); - font-weight: var(--repo-font-title-small-weight); - line-height: var(--repo-font-title-small-line-height); -} - -.buttonFrame, -.inputFrame { - display: grid; - width: fit-content; - border: 2px dashed #9747FF; - border-radius: 8px; -} - -.buttonFrame { - gap: 28px; - padding: 32px 38px 28px; -} - -.buttonGrid { - display: grid; - grid-template-columns: repeat(4, max-content); - gap: 38px 52px; - align-items: center; -} - -.column { - display: grid; - gap: 28px; - justify-items: center; -} - -.columnTitle { - margin: 0; - color: var(--repo-text-black); - font-size: var(--repo-font-title-tiny-size); - font-weight: var(--repo-font-title-tiny-weight); - line-height: var(--repo-font-title-tiny-line-height); -} - -.buttonStack { - display: grid; - gap: 28px; -} - -.buttonPreviewHover.buttonPreviewHover { - background: var(--repo-bg-main-lighter); -} - -.groupLabels { - display: grid; - grid-template-columns: 1fr 1fr; - margin-top: -4px; - color: var(--repo-text-black); - font-size: var(--repo-font-title-tiny-size); - font-weight: var(--repo-font-title-tiny-weight); - line-height: var(--repo-font-title-tiny-line-height); - text-align: center; -} - -.inputFrame { - gap: 38px; - width: 353px; - padding: 52px 56px; -} - -.logoFrame { - display: grid; - gap: 48px; - width: fit-content; - padding: 64px 96px; - border: 2px dashed #9747FF; - border-radius: 8px; -} - -.checkboxFrame { - display: grid; - grid-template-columns: repeat(2, max-content); - gap: 56px; - width: fit-content; - padding: 48px 56px; - border: 2px dashed #9747FF; - border-radius: 8px; -} - -.checkboxGroup { - display: grid; - gap: 20px; - align-content: start; -} - -.previewLabel { - margin: 0; - color: var(--repo-text-black); - font-size: var(--repo-font-title-tiny-size); - font-weight: var(--repo-font-title-tiny-weight); - line-height: var(--repo-font-title-tiny-line-height); -} - -.inlineControl { - display: inline-flex; - align-items: center; - gap: 10px; - color: var(--repo-gray-50); - font-size: var(--repo-font-body-small-size); - font-weight: var(--repo-font-body-small-weight); - line-height: var(--repo-font-body-small-line-height); -} - -.switchFrame { - display: grid; - gap: 48px; - justify-items: center; - width: 320px; - padding: 56px 0; - border: 2px dashed #9747FF; - border-radius: 32px; -} - -.switchInteractiveRow { - display: inline-flex; - align-items: center; - gap: 16px; -} - -.switchStateText { - min-width: 24px; - color: var(--repo-text-black); - font-size: var(--repo-font-body-small-size); - font-weight: var(--repo-font-body-small-weight); - line-height: var(--repo-font-body-small-line-height); -} - -.feedbackFrame { - display: grid; - gap: 32px; - width: fit-content; - padding: 40px 48px; - border: 2px dashed #9747FF; - border-radius: 8px; -} - -.feedbackBalloonFrame { - width: 560px; - min-height: 156px; - padding: 46px 96px; - border: 2px dashed #9747FF; - border-radius: 20px; -} - -.toastFrame { - display: grid; - gap: 56px; - width: fit-content; - padding: 56px 84px; - border: 2px dashed #9747FF; - border-radius: 20px; -} - -.dropdownFrame { - display: grid; - gap: 42px; - justify-items: center; - width: fit-content; - padding: 56px 76px; - border: 2px dashed #9747FF; - border-radius: 20px; -} - -.tagFrame { - display: grid; - gap: 32px; - justify-items: center; - width: fit-content; - padding: 42px 64px; - border: 2px dashed #9747FF; - border-radius: 20px; -} - -.libraryBookFrame { - display: grid; - grid-template-columns: repeat(2, max-content); - gap: 32px; - width: fit-content; - padding: 48px 56px; - border: 2px dashed #9747FF; - border-radius: 20px; - background: var(--repo-gray-600); -} - - -.portfolioResumeSheetFrame { - width: fit-content; - padding: 32px; - border: 2px dashed #9747FF; - border-radius: 20px; - background: var(--repo-gray-600); -} diff --git a/src/dev/ComponentPreview.tsx b/src/dev/ComponentPreview.tsx deleted file mode 100644 index 7930c1b..0000000 --- a/src/dev/ComponentPreview.tsx +++ /dev/null @@ -1,333 +0,0 @@ -'use client' - -import type { SVGProps, ReactNode } from 'react' -import { useState } from 'react' - -import { samplePortfolioResume, samplePortfolioHref, sampleResumeBookHref } from '@/shared/fixtures/examples/publicExamples' - -import { - Button, - Checkbox, - CheckboxOption, - Dropdown, - Feedback, - FeedbackBalloon, - Input, - Logo, - LibraryBookCard, - PortfolioResumeSheet, - Switch, - Tag, - Toast, -} from '@/shared/ui' - -import styles from './ComponentPreview.module.css' - -export function ComponentPreview() { - const [interactiveCheckboxChecked, setInteractiveCheckboxChecked] = useState(false) - const [interactiveOptionChecked, setInteractiveOptionChecked] = useState(false) - const [interactiveSwitchChecked, setInteractiveSwitchChecked] = useState(false) - const [feedbackExpanded, setFeedbackExpanded] = useState(false) - const [selectedGrade, setSelectedGrade] = useState('all') - - return ( -
-
-

- Button -

- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
-
- -
- 아이콘 off - 아이콘 on -
-
-
-
- -
-

- Checkbox -

- -
-
-

Primitive

- undefined} aria-label="unchecked checkbox" /> - undefined} aria-label="checked checkbox" /> - - undefined} aria-label="disabled unchecked checkbox" /> - undefined} aria-label="disabled checked checkbox" /> -
- -
-

Checkbox Option

- undefined}> - 선택지 - - undefined}> - 선택지 - - - 클릭 가능한 선택지 - - undefined}> - 비활성 선택지 - - undefined}> - 비활성 선택지 - -
-
-
- -
-

- Switch -

- -
- undefined} aria-label="off 상태 스위치" /> - undefined} aria-label="on 상태 스위치" /> -
- - {interactiveSwitchChecked ? 'on' : 'off'} -
- undefined} aria-label="disabled off 상태 스위치" /> - undefined} aria-label="disabled on 상태 스위치" /> -
-
- -
-

- Feedback -

- -
- undefined} - title="피드백 제목" - /> - undefined} - title="피드백 제목" - /> - -
-
- -
-

- Feedback Balloon -

- -
- -
-
- - -
-

- Portfolio Resume Sheet -

- -
- -
-
- -
-

- Library Book Card -

- -
- - -
-
- -
-

- Tag -

- -
- undefined}>태그 - undefined} tone="light"> - 태그 - - 태그 - 태그 -
-
- -
-

- Dropdown -

- -
- - -
-
- -
-

- Toast -

- -
- 오류 메시지 - 확인 메시지 -
-
- -
-

- Logo -

- -
- - -
-
- -
-

- Input -

- -
- } /> - - } - /> - - -
-
-
- ) -} - -const gradeOptions = [ - { label: '전체', value: 'all' }, - { label: '1학년', value: 'grade-1' }, - { label: '2학년', value: 'grade-2' }, - { label: '3학년', value: 'grade-3' }, -] - -type PreviewColumnProps = { - children: ReactNode - title: string -} - -function PreviewColumn({ children, title }: PreviewColumnProps) { - return ( -
-

{title}

-
{children}
-
- ) -} - -function EyeIcon(props: SVGProps) { - return ( - - - - - ) -} diff --git a/src/features/auth/ui/AuthLoginPage.module.css b/src/features/auth/ui/AuthLoginPage.module.css new file mode 100644 index 0000000..98ed2de --- /dev/null +++ b/src/features/auth/ui/AuthLoginPage.module.css @@ -0,0 +1,331 @@ +.page { + position: relative; + display: grid; + min-height: 100vh; + place-items: center; + overflow: hidden; + padding: 96px 32px 64px; + color: var(--repo-gray-50); + background: linear-gradient(180deg, var(--repo-bg-main-lighter) 0%, var(--repo-gray-950) 100%); +} + +.page::before { + position: absolute; + inset: 0; + content: ''; + background: + radial-gradient(circle at 50% 42%, rgb(255 255 255 / 18%) 0 1px, transparent 1px 7px), + radial-gradient(ellipse at 50% 42%, rgb(255 255 255 / 16%) 0, transparent 35%); + background-size: 7px 7px, 100% 100%; + mask-image: radial-gradient(ellipse 44% 35% at 50% 42%, black 0 52%, transparent 76%); + pointer-events: none; +} + +.header { + position: absolute; + top: 18px; + left: 68px; + right: 68px; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; +} + +.logoLink { + display: inline-flex; + border-radius: 8px; +} + +.loginPill { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-width: 106px; + min-height: 36px; + padding: 8px 18px; + border-radius: 999px; + color: var(--repo-gray-50); + background: rgb(17 17 17 / 28%); + font-size: var(--repo-font-body-tiny-size); + font-weight: 600; + line-height: var(--repo-font-body-tiny-line-height); + letter-spacing: 0; +} + +.loginIcon { + width: 16px; + height: 16px; + color: currentColor; +} + +.authCard { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(0, 324px) minmax(0, 450px); + width: min(774px, 100%); + min-height: 448px; + border-radius: 11px; + overflow: hidden; + box-shadow: 0 18px 34px rgb(0 0 0 / 34%); + transition: + grid-template-columns 220ms ease, + width 220ms ease; +} + +.teacherLayout { + grid-template-columns: minmax(0, 432px) minmax(0, 310px); + width: min(742px, 100%); +} + +.brandPanel { + display: flex; + flex-direction: column; + justify-content: space-between; + grid-column: 1; + grid-row: 1; + min-height: 448px; + padding: 33px 27px 42px; + background: var(--repo-bg-main-lighter); + transition: + opacity 180ms ease, + transform 220ms ease; +} + +.brandIntro { + display: grid; + align-content: start; + justify-items: start; +} + +.divider { + width: 41px; + height: 1px; + margin: 18px 0 22px; + background: var(--repo-bg-main-point); +} + +.welcome { + margin: 0; + color: var(--repo-gray-50); + font-size: var(--repo-font-body-tiny-size); + font-weight: 600; + line-height: var(--repo-font-body-tiny-line-height); + letter-spacing: 0; +} + +.description { + margin: 9px 0 0; + color: var(--repo-gray-400); + font-size: var(--repo-font-body-very-tiny-size); + font-weight: var(--repo-font-body-very-tiny-weight); + line-height: var(--repo-font-body-very-tiny-line-height); + letter-spacing: 0; +} + +.switchButton { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: 28px; + padding: 0; + border: 0; + color: var(--repo-gray-400); + background: transparent; + font-size: var(--repo-font-body-tiny-size); + font-weight: var(--repo-font-body-tiny-weight); + line-height: var(--repo-font-body-tiny-line-height); + letter-spacing: 0; + cursor: pointer; +} + +.switchButton:hover { + color: var(--repo-gray-50); +} + +.switchIcon { + width: 16px; + height: 16px; + color: currentColor; +} + +.formPanel { + display: grid; + grid-column: 2; + grid-row: 1; + place-items: center; + min-height: 448px; + padding: 64px 64px 70px; + color: var(--repo-text-black); + background: var(--repo-gray-50); + transition: + opacity 180ms ease, + transform 220ms ease; +} + +.teacherLayout .brandPanel { + grid-column: 2; + animation: panelFadeInFromRight 220ms ease both; +} + +.teacherLayout .formPanel { + grid-column: 1; + animation: panelFadeInFromLeft 220ms ease both; +} + +.studentLayout .brandPanel { + animation: panelFadeInFromLeft 220ms ease both; +} + +.studentLayout .formPanel { + animation: panelFadeInFromRight 220ms ease both; +} + +.form { + display: grid; + width: min(322px, 100%); + align-content: start; +} + +.title { + margin: 0 0 31px; + text-align: center; + color: var(--repo-text-black); + font-size: var(--repo-font-title-small-size); + font-weight: var(--repo-font-title-small-weight); + line-height: var(--repo-font-title-small-line-height); + letter-spacing: 0; +} + +.fieldGroup { + display: grid; + gap: 8px; + width: 100%; + margin: 0 0 14px; +} + +.label { + color: var(--repo-text-black); + font-size: var(--repo-font-body-tiny-size); + font-weight: var(--repo-font-body-tiny-weight); + line-height: var(--repo-font-body-tiny-line-height); + letter-spacing: 0; +} + +.passwordIcon { + width: 16px; + height: 16px; +} + +.submitButton { + width: 100%; + margin-top: 58px; +} + +@media (max-width: 820px) { + .page { + min-height: 100svh; + padding: 88px 20px 40px; + } + + .header { + top: 20px; + left: 24px; + right: 24px; + } + + .authCard, + .teacherLayout { + grid-template-columns: 1fr; + width: min(430px, 100%); + } + + .brandPanel { + min-height: 252px; + padding: 28px 24px; + } + + .formPanel { + min-height: 430px; + padding: 44px 28px 52px; + } + + .studentLayout .brandPanel { + grid-column: 1; + order: 2; + } + + .studentLayout .formPanel { + grid-column: 1; + order: 1; + } + + .teacherLayout .brandPanel { + grid-column: 1; + order: 2; + } + + .teacherLayout .formPanel { + grid-column: 1; + order: 1; + } +} + +@media (max-width: 420px) { + .page { + padding-inline: 16px; + } + + .loginPill { + min-width: auto; + padding-inline: 14px; + } + + .formPanel { + padding-inline: 22px; + } +} + +@keyframes panelFadeInFromLeft { + from { + opacity: 0.82; + transform: translateX(-12px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes panelFadeInFromRight { + from { + opacity: 0.82; + transform: translateX(12px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .authCard, + .brandPanel, + .formPanel { + transition: none; + } + + .brandPanel, + .formPanel, + .studentLayout .brandPanel, + .studentLayout .formPanel, + .teacherLayout .brandPanel, + .teacherLayout .formPanel { + animation: none; + } +} diff --git a/src/features/auth/ui/AuthLoginPage.tsx b/src/features/auth/ui/AuthLoginPage.tsx new file mode 100644 index 0000000..92d7b4c --- /dev/null +++ b/src/features/auth/ui/AuthLoginPage.tsx @@ -0,0 +1,116 @@ +'use client' + +import Link from 'next/link' +import { useState } from 'react' + +import { Button, Icon, Input, Logo } from '@/shared/ui' + +import styles from './AuthLoginPage.module.css' + +type LoginRole = 'student' | 'teacher' + +type LoginContent = { + description: string + switchLabel: string + title: string +} + +const LOGIN_CONTENT: Record = { + student: { + description: 'DSM학생들을 위한 이력서 관리 플랫폼, Repo', + switchLabel: '선생님으로 전환하기', + title: '학생 로그인', + }, + teacher: { + description: 'DSM선생님을 위한 이력서 관리 플랫폼, Repo', + switchLabel: '학생으로 전환하기', + title: '선생님 로그인', + }, +} + +const oppositeRole: Record = { + student: 'teacher', + teacher: 'student', +} + +export function AuthLoginPage() { + const [role, setRole] = useState('student') + const [showPassword, setShowPassword] = useState(false) + const content = LOGIN_CONTENT[role] + const nextRole = oppositeRole[role] + const layoutClassName = role === 'student' ? styles.studentLayout : styles.teacherLayout + const passwordType = showPassword ? 'text' : 'password' + const passwordToggleLabel = showPassword ? '비밀번호 숨기기' : '비밀번호 보이기' + const passwordIconName = showPassword ? 'eye' : 'eye-off' + + const formPanel = ( +
+
+

+ {content.title} +

+ +
+ + +
+ +
+ + setShowPassword((currentValue) => !currentValue)} + placeholder="비밀번호를 입력해주세요." + rightIcon={} + rightIconAriaLabel={passwordToggleLabel} + type={passwordType} + /> +
+ + +
+
+ ) + + const brandPanel = ( + + ) + + return ( +
+
+ + + + + + 로그인 + +
+ +
+ {brandPanel} + {formPanel} +
+
+ ) +} diff --git a/src/features/auth/ui/index.ts b/src/features/auth/ui/index.ts new file mode 100644 index 0000000..04e3eb0 --- /dev/null +++ b/src/features/auth/ui/index.ts @@ -0,0 +1 @@ +export { AuthLoginPage } from './AuthLoginPage' diff --git a/src/shared/ui/Icon/Icon.tsx b/src/shared/ui/Icon/Icon.tsx index 27a4654..7ef246b 100644 --- a/src/shared/ui/Icon/Icon.tsx +++ b/src/shared/ui/Icon/Icon.tsx @@ -1,21 +1,94 @@ -import type { SVGProps } from 'react'; +import type { SVGProps } from 'react' -export type IconName = 'plus' | 'right-arrow'; +export type IconName = 'chevron-right' | 'eye' | 'eye-off' | 'login' | 'plus' | 'right-arrow' type IconProps = Omit, 'children'> & { - name: IconName; -}; + name: IconName +} export function Icon({ name, 'aria-hidden': ariaHidden = true, focusable = false, ...props }: IconProps) { + if (name === 'chevron-right') { + return + } + + if (name === 'eye') { + return + } + + if (name === 'eye-off') { + return + } + + if (name === 'login') { + return + } + if (name === 'plus') { - return ; + return } if (name === 'right-arrow') { - return ; + return } - return null; + return null +} + +function ChevronRightIcon(props: Omit, 'children'>) { + return ( + + + + ) +} + +function EyeIcon(props: Omit, 'children'>) { + return ( + + + + + ) +} + +function EyeOffIcon(props: Omit, 'children'>) { + return ( + + + + ) +} + +function LoginIcon(props: Omit, 'children'>) { + return ( + + + + ) } function PlusIcon(props: Omit, 'children'>) { @@ -23,7 +96,7 @@ function PlusIcon(props: Omit, 'children'>) { - ); + ) } function RightArrowIcon(props: Omit, 'children'>) { @@ -31,5 +104,5 @@ function RightArrowIcon(props: Omit, 'children'>) { - ); + ) } diff --git a/src/shared/ui/Input/Input.tsx b/src/shared/ui/Input/Input.tsx index 0a1447f..1a83470 100644 --- a/src/shared/ui/Input/Input.tsx +++ b/src/shared/ui/Input/Input.tsx @@ -7,10 +7,19 @@ type InputProps = Omit, 'className'> & { error?: boolean errorMessage?: string onRightIconClick?: () => void + rightIconAriaLabel?: string rightIcon?: ReactNode } -export function Input({ error = false, errorMessage, id, onRightIconClick, rightIcon, ...props }: InputProps) { +export function Input({ + error = false, + errorMessage, + id, + onRightIconClick, + rightIcon, + rightIconAriaLabel = '입력창 오른쪽 아이콘', + ...props +}: InputProps) { const generatedId = useId() const inputId = id ?? generatedId const hasError = error || Boolean(errorMessage) @@ -32,7 +41,7 @@ export function Input({ error = false, errorMessage, id, onRightIconClick, right {rightIcon ? ( onRightIconClick ? (