Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx
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";

Copy link
Copy Markdown

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 기반 라벨 조회 대신 label prop을 주입받도록 리팩터링을 권장합니다. 번역 키(onboardingGoogleButton.*)도 공유 네임스페이스로 이동하거나 호출侧에서 전달하는 구조가 적합합니다.

As per path instructions, "도메인 간 직접 import 금지 — 공유 로직은 반드시 lib/ 또는 packages/로 추출" 규칙을 따라야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/app/`[locale]/login/_containers/LoginContainer.tsx at line 7,
login이 onboarding 도메인의 OnboardingGoogleButtonContainer를 직접 import하지 않도록 공유 컴포넌트로
추출하세요. OnboardingGoogleButtonContainer와 OnboardingGoogleButton을 공용 위치로 이동하고,
onboarding 전용 variant·번역 키 의존성을 제거해 호출 측에서 label prop을 주입하도록 리팩터링한 뒤 모든 import와
사용처를 갱신하세요.

Source: Path instructions

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

페이지 <h1> 부재 — 접근성 위반

{t("headline")}<span>으로 렌더링되고 있으며, 페이지 전체에 <h1> 요소가 없습니다. 경로 규칙에서 "헤딩 레벨 점프 금지, 페이지당 <h1> 하나"를 요구하고 있습니다. 화면 리더기 사용자는 헤딩 기반 탐색이 불가능합니다.

typo-headline-b-18 스타일이 적용된 headline을 <h1>로 변경하세요. 타이포그래피 클래스는 그대로 유지할 수 있습니다.

♿ 제안 수정
             <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, "헤딩 레벨 점프 금지, 페이지당 <h1> 하나" 규칙을 따라야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span className="typo-headline-b-18 text-timo-black">
{t("headline")}
</span>
<span className="typo-headline-m-16 text-timo-blue-300">
Less Chaos More Focus
</span>
<h1 className="typo-headline-b-18 text-timo-black">
{t("headline")}
</h1>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/app/`[locale]/login/_containers/LoginContainer.tsx around lines
29 - 31, Replace the span rendering t("headline") in LoginContainer with an h1
element, preserving the existing typo-headline-b-18 and text-timo-black classes,
and verify the page contains exactly one h1 without introducing heading-level
jumps.

Source: 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>
);
};
5 changes: 5 additions & 0 deletions apps/timo-web/app/[locale]/login/page.tsx
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
Expand Up @@ -5,7 +5,7 @@ import {
} from "@repo/timo-design-system/icons";
import { cn } from "@repo/timo-design-system/utils";

export interface OnboardingButtonProps {
interface OnboardingButtonProps {
variant: "next" | "prev" | "start";
label: string;
isActive?: boolean;
Expand Down Expand Up @@ -53,7 +53,7 @@ export const OnboardingButton = ({
onClick={onClick}
disabled={isDisabled}
className={cn(
"flex items-center justify-center gap-2 rounded-[4px] px-4 py-2",
"flex items-center justify-center gap-2 rounded-[4px] px-4 py-2 transition-colors duration-200 ease-in-out",
isActive ? "bg-timo-blue-300" : "bg-timo-gray-200",
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cn } from "@repo/timo-design-system/utils";

export interface OnboardingSelectCardProps {
interface OnboardingSelectCardProps {
size: "sm" | "lg";
selected: boolean;
label: string;
Expand Down Expand Up @@ -31,7 +31,7 @@ export const OnboardingSelectCard = ({
type="button"
onClick={onClick}
className={cn(
"flex rounded-[4px] border px-4 py-2 text-left",
"flex rounded-[4px] border px-4 py-2 text-left transition-colors duration-200 ease-in-out",
selected
? "border-timo-blue-300 bg-timo-blue-50"
: "border-timo-gray-500 bg-white",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const OnboardingStepButton = ({ step }: OnboardingStepButtonProps) => {
>
<span
className={cn(
"typo-caption-r-10",
"typo-caption-r-10 translate-y-px leading-none",
s === step ? "text-white" : "text-timo-blue-100",
)}
>
Expand Down
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

자정을 넘나드는 수면 패턴 검증 누락

bedTime <= wakeUpTime 문자열 비교는 "HH:MM" 형식에서 같은 날 내 패턴만 처리합니다. 예를 들어 23:00 기상·07:00 취침 같은 교대근무자 패턴은 bedTime("07:00") <= wakeUpTime("23:00")true가 되어 유효한 입력이 차단됩니다. 대상 사용자층이 일반적인 주간 생활 패턴으로 한정되어 있다면 의도된 동작일 수 있으나, 그렇지 않다면 자정을 넘는 case를 고려해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/onboarding/_containers/LifePatternStepContainer.tsx
around lines 28 - 31, Update the validation around isBedTimeInvalid and
canProceed to support sleep intervals that cross midnight, such as a 23:00
wake-up and 07:00 bedtime, instead of relying on direct HH:MM string ordering.
Convert the times to comparable numeric values and distinguish valid overnight
schedules from genuinely invalid same-day schedules while preserving rejection
of identical times.


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>
</>
);
};
Loading
Loading