-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 온보딩 퍼널 및 로그인 페이지 구현 #139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d6e6c28
a0c0d4a
63871ff
bd2ae23
467246e
b4d162e
33e3392
d0142fb
955854a
51bf3a4
4d94be6
63281e6
626f3c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,74 @@ | ||||||||||||||||||||
| "use client"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import timoTextLogo from "@repo/timo-design-system/assets/images/logo/timo-text-logo.svg"; | ||||||||||||||||||||
| import Image from "next/image"; | ||||||||||||||||||||
| import { useTranslations } from "next-intl"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { OnboardingGoogleButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingGoogleButtonContainer"; | ||||||||||||||||||||
| import { LottiePlayer } from "@/components/lottie/LottiePlayer"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export const LoginContainer = () => { | ||||||||||||||||||||
| const t = useTranslations("Login"); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return ( | ||||||||||||||||||||
| <section className="flex min-h-screen items-center justify-center gap-10 bg-white px-8 lg:gap-16 xl:gap-36 2xl:gap-[225px]"> | ||||||||||||||||||||
| <LottiePlayer | ||||||||||||||||||||
| src="/lottie/onboarding.json" | ||||||||||||||||||||
| className="hidden shrink-0 lg:block lg:size-[350px] xl:size-[430px] 2xl:size-[500px]" | ||||||||||||||||||||
| ariaLabel={t("animationLabel")} | ||||||||||||||||||||
| /> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| <div className="border-timo-gray-500 shadow-timo flex h-110 w-101 flex-col items-center justify-center gap-16 rounded-[4px] border bg-white px-12.5 py-13"> | ||||||||||||||||||||
| <div className="flex w-76 flex-col gap-6"> | ||||||||||||||||||||
| <div className="flex flex-col items-center gap-4"> | ||||||||||||||||||||
| <Image src={timoTextLogo} alt="Timo" width={92} height={35} /> | ||||||||||||||||||||
| <div className="flex w-full flex-col items-center gap-0.5"> | ||||||||||||||||||||
| <p className="typo-headline-m-16 text-timo-blue-300"> | ||||||||||||||||||||
| Less Chaos More Focus | ||||||||||||||||||||
| </p> | ||||||||||||||||||||
| <span className="typo-headline-b-18 text-timo-black"> | ||||||||||||||||||||
| {t("headline")} | ||||||||||||||||||||
| </span> | ||||||||||||||||||||
|
Comment on lines
+29
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 페이지
♿ 제안 수정 <div className="flex w-full flex-col items-center gap-0.5">
<p className="typo-headline-m-16 text-timo-blue-300">
Less Chaos More Focus
</p>
- <span className="typo-headline-b-18 text-timo-black">
+ <h1 className="typo-headline-b-18 text-timo-black">
{t("headline")}
- </span>
+ </h1>
</div>As per path instructions, "헤딩 레벨 점프 금지, 페이지당 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||
| </div> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| <p className="typo-headline-m-14 text-timo-gray-700 text-center"> | ||||||||||||||||||||
| {t("description")} | ||||||||||||||||||||
| </p> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| <div className="flex w-76.25 flex-col gap-5.5"> | ||||||||||||||||||||
| <div className="flex flex-col gap-2"> | ||||||||||||||||||||
| <p className="typo-body-r-12 text-timo-gray-700"> | ||||||||||||||||||||
| {t("connectLabel")} | ||||||||||||||||||||
| </p> | ||||||||||||||||||||
| <OnboardingGoogleButtonContainer | ||||||||||||||||||||
| variant="login" | ||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||
| // TODO: 백엔드 OAuth 로그인 URL 확정 후 리다이렉트 | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| /> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| <p className="typo-body-r-12 text-timo-gray-700 text-center"> | ||||||||||||||||||||
| {t.rich("termsNotice", { | ||||||||||||||||||||
| // TODO: 실제 이용약관 페이지 URL 나오면 href 교체 | ||||||||||||||||||||
| terms: (chunks) => ( | ||||||||||||||||||||
| // eslint-disable-next-line jsx-a11y/anchor-is-valid | ||||||||||||||||||||
| <a href="#" className="underline"> | ||||||||||||||||||||
| {chunks} | ||||||||||||||||||||
| </a> | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| // TODO: 실제 개인정보 처리방침 페이지 URL 나오면 href 교체 | ||||||||||||||||||||
| privacy: (chunks) => ( | ||||||||||||||||||||
| // eslint-disable-next-line jsx-a11y/anchor-is-valid | ||||||||||||||||||||
| <a href="#" className="underline"> | ||||||||||||||||||||
| {chunks} | ||||||||||||||||||||
| </a> | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| })} | ||||||||||||||||||||
| </p> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| </section> | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { LoginContainer } from "@/app/[locale]/login/_containers/LoginContainer"; | ||
|
|
||
| export default function LoginPage() { | ||
| return <LoginContainer />; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| "use client"; | ||
|
|
||
| import { useTranslations } from "next-intl"; | ||
| import { useState } from "react"; | ||
|
|
||
| import { OnboardingButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingButtonContainer"; | ||
| import { OnboardingGoogleButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingGoogleButtonContainer"; | ||
|
|
||
| interface CalendarConnectStepContainerProps { | ||
| onPrev: () => void; | ||
| onStart: () => void; | ||
| } | ||
|
|
||
| export const CalendarConnectStepContainer = ({ | ||
| onPrev, | ||
| onStart, | ||
| }: CalendarConnectStepContainerProps) => { | ||
| const t = useTranslations("Onboarding"); | ||
| const [isCalendarConnected, setIsCalendarConnected] = useState(false); | ||
|
|
||
| return ( | ||
| <> | ||
| <div className="flex flex-col gap-10"> | ||
| <div className="flex flex-col gap-4"> | ||
| <h1 className="typo-headline-b-24 text-timo-black whitespace-pre-line"> | ||
| {t("calendarConnect.title")} | ||
| </h1> | ||
| <p className="typo-headline-m-14 text-timo-gray-700"> | ||
| {t("calendarConnect.description")} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-4"> | ||
| <div className="flex flex-col gap-2"> | ||
| <p className="typo-body-r-12 text-timo-gray-700"> | ||
| {t("calendarConnect.connectLabel")} | ||
| </p> | ||
| <OnboardingGoogleButtonContainer | ||
| variant="connectCalendar" | ||
| isSelected={isCalendarConnected} | ||
| onClick={() => { | ||
| // TODO: 실제 구글 캘린더 OAuth 연동 (백엔드 API 확정 후) | ||
| setIsCalendarConnected(true); | ||
| }} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-2"> | ||
| <p className="typo-body-r-12 text-timo-gray-700"> | ||
| {t("calendarConnect.consentNotice")} | ||
| </p> | ||
| <p className="typo-body-r-12 text-timo-gray-700"> | ||
| {t("calendarConnect.permissionNotice")} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="mt-auto flex justify-between"> | ||
| <OnboardingButtonContainer variant="prev" onClick={onPrev} /> | ||
| <OnboardingButtonContainer variant="start" onClick={onStart} /> | ||
| </div> | ||
| </> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| "use client"; | ||
|
|
||
| import { useTranslations } from "next-intl"; | ||
|
|
||
| import { OnboardingSelectCard } from "@/app/[locale]/onboarding/_components/OnboardingSelectCard"; | ||
| import { OnboardingButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingButtonContainer"; | ||
|
|
||
| interface LanguageStepContainerProps { | ||
| language?: "ko" | "en"; | ||
| onSelect: (language: "ko" | "en" | undefined) => void; | ||
| onNext: () => void; | ||
| } | ||
|
|
||
| export const LanguageStepContainer = ({ | ||
| language, | ||
| onSelect, | ||
| onNext, | ||
| }: LanguageStepContainerProps) => { | ||
| const t = useTranslations("Onboarding"); | ||
|
|
||
| return ( | ||
| <> | ||
| <div className="flex flex-col gap-10"> | ||
| <div className="flex flex-col gap-4"> | ||
| <h1 className="typo-headline-b-24 text-timo-black"> | ||
| {t("language.title")} | ||
| </h1> | ||
| <p className="typo-headline-m-14 text-timo-gray-700 whitespace-pre-line"> | ||
| {t("language.description")} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="flex items-center gap-1"> | ||
| <OnboardingSelectCard | ||
| size="sm" | ||
| label="English" | ||
| sublabel="영어" | ||
| selected={language === "en"} | ||
| onClick={() => onSelect(language === "en" ? undefined : "en")} | ||
| /> | ||
| <OnboardingSelectCard | ||
| size="sm" | ||
| label="한국어" | ||
| sublabel="Korean" | ||
| selected={language === "ko"} | ||
| onClick={() => onSelect(language === "ko" ? undefined : "ko")} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="mt-auto flex justify-end"> | ||
| <OnboardingButtonContainer | ||
| variant="next" | ||
| isActive={language !== undefined} | ||
| disabled={language === undefined} | ||
| onClick={onNext} | ||
| /> | ||
| </div> | ||
| </> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| "use client"; | ||
|
|
||
| import { AlertIcon, MoonIcon, SunIcon } from "@repo/timo-design-system/icons"; | ||
| import { useTranslations } from "next-intl"; | ||
|
|
||
| import { OnboardingTimeDropdown } from "@/app/[locale]/onboarding/_components/OnboardingTimeDropdown"; | ||
| import { OnboardingButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingButtonContainer"; | ||
|
|
||
| interface LifePatternStepContainerProps { | ||
| wakeUpTime?: string; | ||
| bedTime?: string; | ||
| onSelectWakeUpTime: (time: string) => void; | ||
| onSelectBedTime: (time: string) => void; | ||
| onPrev: () => void; | ||
| onNext: () => void; | ||
| } | ||
|
|
||
| export const LifePatternStepContainer = ({ | ||
| wakeUpTime, | ||
| bedTime, | ||
| onSelectWakeUpTime, | ||
| onSelectBedTime, | ||
| onPrev, | ||
| onNext, | ||
| }: LifePatternStepContainerProps) => { | ||
| const t = useTranslations("Onboarding"); | ||
|
|
||
| const isBedTimeInvalid = Boolean( | ||
| wakeUpTime && bedTime && bedTime <= wakeUpTime, | ||
| ); | ||
| const canProceed = Boolean(wakeUpTime && bedTime && !isBedTimeInvalid); | ||
|
Comment on lines
+28
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 자정을 넘나드는 수면 패턴 검증 누락
🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <> | ||
| <div className="flex flex-col gap-10"> | ||
| <div className="flex flex-col gap-4"> | ||
| <h1 className="typo-headline-b-24 text-timo-black"> | ||
| {t("lifePattern.title")} | ||
| </h1> | ||
| <p className="typo-headline-m-14 text-timo-gray-700"> | ||
| {t("lifePattern.description")} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-2"> | ||
| <div className="flex items-center gap-1"> | ||
| <div className="flex w-37.5 flex-col gap-3"> | ||
| <div className="flex items-center gap-2"> | ||
| <SunIcon /> | ||
| <span className="typo-body-r-12 text-timo-blue-300"> | ||
| {t("lifePattern.wakeUpTime")} | ||
| </span> | ||
| </div> | ||
| <OnboardingTimeDropdown | ||
| value={wakeUpTime ?? ""} | ||
| placeholder="01:00" | ||
| onChange={onSelectWakeUpTime} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="flex w-37.5 flex-col gap-3"> | ||
| <div className="flex items-center gap-2"> | ||
| <MoonIcon /> | ||
| <span className="typo-body-r-12 text-timo-blue-300"> | ||
| {t("lifePattern.bedTime")} | ||
| </span> | ||
| </div> | ||
| <OnboardingTimeDropdown | ||
| value={bedTime ?? ""} | ||
| placeholder="23:00" | ||
| onChange={onSelectBedTime} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| {isBedTimeInvalid && ( | ||
| <div className="flex items-center gap-1"> | ||
| <AlertIcon /> | ||
| <span className="typo-body-sb-12 text-timo-red whitespace-nowrap"> | ||
| {t("lifePattern.bedTimeError")} | ||
| </span> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="mt-auto flex justify-between"> | ||
| <OnboardingButtonContainer variant="prev" onClick={onPrev} /> | ||
| <OnboardingButtonContainer | ||
| variant="next" | ||
| isActive={canProceed} | ||
| disabled={!canProceed} | ||
| onClick={onNext} | ||
| /> | ||
| </div> | ||
| </> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
도메인 간 직접 import 위반 — 공유 컴포넌트로 추출 필요
login도메인이onboarding도메인의OnboardingGoogleButtonContainer를 직접 import하고 있습니다. 아키텍처 규칙에서 "도메인 간 직접 import 금지 — 공유 로직은 반드시lib/또는packages/로 추출"을 명시하고 있습니다. 컴포넌트 이름 자체에 "Onboarding"이 포함되어 있어 도메인 종속성이 명확히 드러납니다.OnboardingGoogleButtonContainer와 하위OnboardingGoogleButton를@/components/google-button/같은 공유 위치로 추출하고,variant기반 라벨 조회 대신labelprop을 주입받도록 리팩터링을 권장합니다. 번역 키(onboardingGoogleButton.*)도 공유 네임스페이스로 이동하거나 호출侧에서 전달하는 구조가 적합합니다.As per path instructions, "도메인 간 직접 import 금지 — 공유 로직은 반드시
lib/또는packages/로 추출" 규칙을 따라야 합니다.🤖 Prompt for AI Agents
Source: Path instructions