Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2117dc5
refactor(web): 캘린더 연동 origin을 상수로 분리하고 사용 후 삭제하도록 수정 (#299)
ehye1 Sep 29, 2026
9c003a8
refactor(web): 로그인·캘린더 연동 버튼을 전역 AuthButton으로 이동하고 variant 정리 (#299)
ehye1 Sep 29, 2026
d6dd978
feat(web): 온보딩 캘린더 연동 단계 재활성화 (#299)
ehye1 Sep 29, 2026
212f033
fix(web): 온보딩 카드 레이아웃을 피그마 간격에 맞게 수정 (#299)
ehye1 Sep 29, 2026
b45c03e
feat(web): 캘린더 연동 실패 시 토스트 표시 (#299)
ehye1 Sep 29, 2026
233a9e7
feat(web): Apple 캘린더 연동 UI 추가 및 로고 이미지를 svg로 교체 (#299)
ehye1 Sep 29, 2026
3b18e87
fix(web): 로고 이미지를 png로 되돌리고 선명하게 표시 (#299)
ehye1 Sep 29, 2026
b0f7826
refactor(web): 설정 캘린더 줄을 SettingsCalendar 컴포넌트로 분리 (#299)
ehye1 Sep 29, 2026
642d9a5
refactor(web): 온보딩 4단계 시작 시 불필요한 답변 검사 제거 (#299)
ehye1 Sep 29, 2026
8983e45
fix(web): 캘린더 연동 예외 상황 처리 (#299)
ehye1 Sep 29, 2026
fb11565
fix(web): 온보딩 단계 숫자 중앙 정렬 수정 (#299)
ehye1 Sep 29, 2026
a00ed4f
fix(web): 설정 캘린더 연동 박스 너비 통일 (#299)
ehye1 Sep 29, 2026
ae8e90c
refactor(web): 캘린더 연동 출발지를 sessionStorage 유틸로 분리 (#299)
ehye1 Oct 5, 2026
ad92ec9
fix(web): 온보딩 답변·연동 실패 기록의 저장소 접근 예외 처리 (#299)
ehye1 Oct 5, 2026
47f94ec
chore(web): 캘린더 authorize API에 redirectOrigin 파라미터 반영 (#299)
ehye1 Oct 7, 2026
c08dca2
feat(web): 캘린더 연동 시 현재 origin을 redirectOrigin으로 전달 (#299)
ehye1 Oct 7, 2026
9e79d87
feat(ui): PillButton이 버튼 HTML 속성을 받도록 확장 (#299)
ehye1 Oct 7, 2026
57ca316
fix(web): 설정 캘린더 연동 버튼에 캘린더 이름을 포함한 aria-label 추가 (#299)
ehye1 Oct 7, 2026
dc4c790
fix(web): 설정 캘린더 연동 버튼 aria-label 되돌리기 (#299)
ehye1 Oct 7, 2026
22c14f6
refactor(ui): PillButton HTML 속성 확장 되돌리기 (#299)
ehye1 Oct 7, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PillButton } from "@repo/timo-design-system/ui";
import Image from "next/image";

interface SettingsCalendarProps {
iconSrc: string;
name: string;
isConnected: boolean;
connectLabel: string;
disconnectLabel: string;
onClick: () => void;
}

export const SettingsCalendar = ({
iconSrc,
name,
isConnected,
connectLabel,
disconnectLabel,
onClick,
}: SettingsCalendarProps) => {
return (
<div className="bg-timo-gray-300 flex h-10.25 w-[243px] items-center justify-between gap-4 self-start rounded-lg px-2.5 py-1.5">
<div className="flex items-center gap-1.5">
<Image src={iconSrc} alt="" width={20} height={20} unoptimized />
<span className="typo-headline-m-16 text-timo-gray-700 whitespace-nowrap">
{name}
</span>
</div>

<PillButton

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

선택적인 접근성 개선 의견입니다! 버튼 구분을 위해 aria-label에 Google·Apple 이름도 넣어주면 어떨까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

현재 PillButton이 aria 속성을 받지 않아서 공용 Button으로 교체할 때 aria-label에 구글/애플 이름을 함께 넣도록 반영하겠습니다! 👍

variant={isConnected ? "gray-dark" : "blue"}
onClick={onClick}
>
{isConnected ? disconnectLabel : connectLabel}
</PillButton>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { PillButton } from "@repo/timo-design-system/ui";
import Image from "next/image";

import type {
Expand All @@ -7,6 +6,7 @@ import type {
SettingsTagItem,
} from "@/app/[locale]/(main)/settings/_types/account/profile-type";

import { SettingsCalendar } from "@/app/[locale]/(main)/settings/_components/account/SettingsCalendar";
import { SettingsLanguageSectionContainer } from "@/app/[locale]/(main)/settings/_containers/account/SettingsLanguageSectionContainer";
import { SettingsLogoutModalContainer } from "@/app/[locale]/(main)/settings/_containers/account/SettingsLogoutModalContainer";
import { SettingsTagsSectionContainer } from "@/app/[locale]/(main)/settings/_containers/account/SettingsTagsSectionContainer";
Expand Down Expand Up @@ -73,6 +73,7 @@ export const SettingsProfileView = ({
alt="Google"
width={20}
height={20}
unoptimized
/>
<span className="typo-headline-m-16 text-timo-gray-900">
{googleEmail}
Expand All @@ -88,26 +89,23 @@ export const SettingsProfileView = ({
<h2 className="typo-headline-b-16 text-timo-gray-900">
{labels.calendarSection}
</h2>
<div className="bg-timo-gray-300 flex h-10.25 w-fit items-center gap-4 self-start rounded-lg px-2.5 py-1.5">
<div className="flex items-center gap-1.5">
<Image
src="/images/google-calendar.png"
alt=""
width={20}
height={20}
/>
<span className="typo-headline-m-16 text-timo-gray-700 whitespace-nowrap">
Google Calendar
</span>
</div>

<PillButton
variant={isCalendarConnected ? "gray-dark" : "blue"}
onClick={onConnectCalendar}
>
{isCalendarConnected ? labels.disconnect : labels.connect}
</PillButton>
</div>
<SettingsCalendar
iconSrc="/images/google-calendar.png"
name="Google Calendar"
isConnected={isCalendarConnected}
connectLabel={labels.connect}
disconnectLabel={labels.disconnect}
onClick={onConnectCalendar}
/>
{/* TODO: Apple 캘린더 연동 API 연결 후 연결 상태·onClick 연동 */}
<SettingsCalendar
iconSrc="/images/apple-calendar.png"
name="Apple Calendar"
isConnected={false}
connectLabel={labels.connect}
disconnectLabel={labels.disconnect}
onClick={() => {}}
/>
Comment thread
ehye1 marked this conversation as resolved.
</section>

<hr className="border-timo-gray-500" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useTranslations } from "next-intl";
import { overlay } from "overlay-kit";
import { useState } from "react";
import { useEffect, useState } from "react";

import { TagLimitToastContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/toast/TagLimitToastContainer";
import { SettingsCalendarDisconnectModal } from "@/app/[locale]/(main)/settings/_components/account/SettingsCalendarDisconnectModal";
Expand All @@ -12,6 +12,10 @@ import { useSettingsProfileLabels } from "@/app/[locale]/(main)/settings/_hooks/
import { CreateTagModalContainer } from "@/components/tag/CreateTagModalContainer";
import { AnimatedToast } from "@/components/toast/AnimatedToast";
import { MAX_CUSTOM_TAG_COUNT } from "@/schemas/tag/tag-schema";
import {
clearCalendarConnectFailed,
hasCalendarConnectFailed,
} from "@/utils/calendar/calendar-connect-failed";

export const SettingsProfileContainer = () => {
const tToast = useTranslations("Toast");
Expand All @@ -23,7 +27,7 @@ export const SettingsProfileContainer = () => {
profileState.calendarConnected,
);
const [isCalendarConnectErrorToastOpen, setIsCalendarConnectErrorToastOpen] =
useState(false);
useState(hasCalendarConnectFailed);
const [
isCalendarDisconnectErrorToastOpen,
setIsCalendarDisconnectErrorToastOpen,
Expand All @@ -33,6 +37,10 @@ export const SettingsProfileContainer = () => {
const [isLanguageErrorToastOpen, setIsLanguageErrorToastOpen] =
useState(false);

useEffect(() => {
clearCalendarConnectFailed();
}, []);

const handleConnectCalendar = () => {
if (isCalendarConnected) {
overlay.open(({ isOpen, close, unmount }) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { SettingsLanguage } from "@/app/[locale]/(main)/settings/_types/acc

import { useSettingsLanguageParam } from "@/app/[locale]/(main)/settings/_hooks/account/use-settings-language-param";
import { useLogoutMutation } from "@/app/[locale]/(main)/settings/_queries/account/use-logout-mutation";
import { CALENDAR_CONNECT_ORIGIN } from "@/constants/calendar";
import {
authorize,
useDisconnectCalendar,
Expand All @@ -25,6 +26,7 @@ import {
MAX_CUSTOM_TAG_COUNT,
tagCreateDataSchema,
} from "@/schemas/tag/tag-schema";
import { setCalendarConnectOrigin } from "@/utils/calendar/calendar-connect-origin";
import { getDefaultTagLabelKey, isDefaultTagId } from "@/utils/todo/tag-label";

const LANGUAGE_REQUEST_MAP: Record<
Expand Down Expand Up @@ -99,13 +101,15 @@ export const useSettingsProfile = () => {
}

try {
const response = await authorize();
const response = await authorize({
redirectOrigin: window.location.origin,
});
const url = response.data?.authorizationUrl;
if (!url) {
handlers.onConnectError?.();
return;
}
localStorage.setItem("calendarConnectOrigin", "settings");
setCalendarConnectOrigin(CALENDAR_CONNECT_ORIGIN.SETTINGS);
window.location.assign(url);
} catch {
handlers.onConnectError?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import { LogoTimoIcon } from "@repo/timo-design-system/icons";
import { useTranslations } from "next-intl";

import { OnboardingGoogleButtonContainer } from "@/app/[locale]/onboarding/_containers/OnboardingGoogleButtonContainer";
import { LottiePlayer } from "@/components/lottie/LottiePlayer";
import { ROUTES } from "@/constants/routes";
import { AuthButtonContainer } from "@/containers/auth/AuthButtonContainer";
import { Link } from "@/i18n/navigation";

export const LoginContainer = () => {
Expand Down Expand Up @@ -42,8 +42,8 @@ export const LoginContainer = () => {
<p className="typo-body-r-12 text-timo-gray-700">
{t("connectLabel")}
</p>
<OnboardingGoogleButtonContainer
variant="login"
<AuthButtonContainer
variant="googleLogin"
onClick={() => {
const redirectOrigin = window.location.origin;
window.location.href = `${process.env.NEXT_PUBLIC_API_BASE_URL}/oauth2/authorization/google?redirect_origin=${encodeURIComponent(redirectOrigin)}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,24 @@ import { useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "next/navigation";
import { useEffect, useRef } from "react";

import type { CalendarConnectOrigin } from "@/constants/calendar";

import { CALENDAR_CONNECT_ORIGIN } from "@/constants/calendar";
import { ROUTES } from "@/constants/routes";
import { useConnectCalendar } from "@/generated/endpoints/calendar/calendar";
import { getGetMyProfileQueryKey } from "@/generated/endpoints/user/user";
import { useRouter } from "@/i18n/navigation";
import { markCalendarConnectFailed } from "@/utils/calendar/calendar-connect-failed";
import { consumeCalendarConnectOrigin } from "@/utils/calendar/calendar-connect-origin";

type Route = (typeof ROUTES)[keyof typeof ROUTES];

const ACCESS_DENIED_ERROR = "access_denied";

const CALENDAR_CONNECT_REDIRECT: Record<CalendarConnectOrigin, Route> = {
[CALENDAR_CONNECT_ORIGIN.ONBOARDING]: ROUTES.ONBOARDING,
[CALENDAR_CONNECT_ORIGIN.SETTINGS]: ROUTES.SETTINGS,
};

export const CalendarCallbackContainer = () => {
const searchParams = useSearchParams();
Expand All @@ -21,16 +35,25 @@ export const CalendarCallbackContainer = () => {
const hasRequested = useRef(false);

useEffect(() => {
const origin = localStorage.getItem("calendarConnectOrigin");
const redirectTarget =
origin === "settings" ? ROUTES.SETTINGS : ROUTES.HOME;
if (hasRequested.current) return;
hasRequested.current = true;

const origin = consumeCalendarConnectOrigin();
const hasOrigin = origin !== null;
const redirectTarget = hasOrigin
? CALENDAR_CONNECT_REDIRECT[origin]
: ROUTES.HOME;

const notifyConnectFailed = () => {
if (hasOrigin) markCalendarConnectFailed();
};
Comment thread
ehye1 marked this conversation as resolved.

if (error || !code || !state) {
// 사용자가 권한 동의를 취소한 경우(access_denied)는 실패로 보지 않습니다.
if (error !== ACCESS_DENIED_ERROR) notifyConnectFailed();
router.replace(redirectTarget);
return;
}
if (hasRequested.current) return;
hasRequested.current = true;

connectCalendar(
{ data: { authorizationCode: code, state } },
Expand All @@ -39,10 +62,10 @@ export const CalendarCallbackContainer = () => {
await queryClient.invalidateQueries({
queryKey: getGetMyProfileQueryKey(),
});
localStorage.removeItem("calendarConnectOrigin");
router.replace(redirectTarget);
},
onError: () => {
notifyConnectFailed();
router.replace(redirectTarget);
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,14 @@ export const OnboardingButton = ({
}

if (variant === "start") {
// TODO: 원래 start 버튼 (항상 활성 스타일) — CalendarConnect 단계 재활성화 시 아래 주석 해제 후 isActive 분기 제거
// return (
// <button
// type="button"
// onClick={onClick}
// className="bg-timo-blue-300 flex items-center justify-center gap-2 rounded-[4px] px-4 py-2"
// >
// <span className="typo-headline-m-16 text-white">{label}</span>
// </button>
// );
return (
<button
type="button"
onClick={onClick}
disabled={disabled || !isActive}
className={cn(
"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",
)}
disabled={disabled}
className="bg-timo-blue-300 flex items-center justify-center gap-2 rounded-[4px] px-4 py-2"
>
<span
className={cn(
"typo-headline-m-16",
isActive ? "text-white" : "text-timo-gray-700",
)}
>
{label}
</span>
<span className="typo-headline-m-16 text-white">{label}</span>
</button>
Comment thread
ehye1 marked this conversation as resolved.
);
}
Expand Down
Original file line number Diff line number Diff line change
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 transition-colors duration-200 ease-in-out",
"flex rounded-[4px] border px-[15px] py-[7px] 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 @@ -4,8 +4,7 @@ interface OnboardingStepButtonProps {
step: 1 | 2 | 3 | 4;
}

// TODO: 구글 캘린더 연동 단계 재활성화 시 4 추가
const STEPS = [1, 2, 3] as const;
const STEPS = [1, 2, 3, 4] as const;

export const OnboardingStepButton = ({ step }: OnboardingStepButtonProps) => {
return (
Expand All @@ -20,7 +19,7 @@ export const OnboardingStepButton = ({ step }: OnboardingStepButtonProps) => {
>
<span
className={cn(
"typo-caption-r-10 translate-y-px leading-none",
"typo-caption-r-10 tracking-normal",
s === step ? "text-white" : "text-timo-blue-100",
)}
>
Expand Down
Loading
Loading