Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const SettingsNavContainer = () => {
const settingsNavItems = [
{ label: tSettings("nav.account"), tab: "account" as const },
{ label: tSettings("nav.policy"), tab: "policy" as const },
{ label: tSettings("nav.privacy"), tab: "privacy" as const },
{ label: tSettings("nav.withdrawal"), tab: "withdrawal" as const },
];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
"use client";

import { SettingsProfileContainer } from "@/app/[locale]/(main)/settings/_containers/SettingsProfileContainer";
import { SettingsTermsContainer } from "@/app/[locale]/(main)/settings/_containers/SettingsTermsContainer";
import { useSettingsTab } from "@/app/[locale]/(main)/settings/_hooks/useSettingsTab";
import { SettingsPolicyContainer } from "@/app/[locale]/(main)/settings/policy/_containers/SettingsPolicyContainer";
import { SettingsWithdrawalContainer } from "@/app/[locale]/(main)/settings/withdrawal/_containers/SettingsWithdrawalContainer";
import { AsyncBoundary } from "@/components/boundary/AsyncBoundary";

export const SettingsTabsContainer = () => {
const tab = useSettingsTab();

if (tab === "policy") return <SettingsPolicyContainer />;
if (tab === "policy" || tab === "privacy") {
return (
<AsyncBoundary>
<SettingsTermsContainer
type={tab === "policy" ? "SERVICE" : "PRIVACY"}
/>
</AsyncBoundary>
);
}
if (tab === "withdrawal") return <SettingsWithdrawalContainer />;
return <SettingsProfileContainer />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { useTranslations } from "next-intl";

import type { TermsType } from "@/types/terms-type";

import { PolicyDocument } from "@/components/policy/PolicyDocument";
import { useTerms } from "@/queries/use-terms";

export interface SettingsTermsContainerProps {
type: TermsType;
}

export const SettingsTermsContainer = ({
type,
}: SettingsTermsContainerProps) => {
const t = useTranslations("Policy");
const { data: terms } = useTerms(type);
const term = terms[0];

return (
<div className="px-15 pt-7.5">
{term ? (
<PolicyDocument title={term.title} content={term.content} />
) : (
<p className="typo-body-m-12 text-timo-gray-700">{t("empty")}</p>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import { useSearchParams } from "next/navigation";

export type SettingsTab = "account" | "policy" | "withdrawal";
export type SettingsTab = "account" | "policy" | "privacy" | "withdrawal";

const SETTINGS_TAB_PARAM = "tab";

const isSettingsTab = (
value: string | null,
): value is Exclude<SettingsTab, "account"> =>
value === "policy" || value === "withdrawal";
value === "policy" || value === "privacy" || value === "withdrawal";

export const useSettingsTab = (): SettingsTab => {
const searchParams = useSearchParams();
Expand Down
Empty file.

This file was deleted.

Empty file.
24 changes: 16 additions & 8 deletions apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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 { Link } from "@/i18n/navigation";

export const LoginContainer = () => {
const t = useTranslations("Login");
Expand Down Expand Up @@ -51,19 +53,25 @@ export const LoginContainer = () => {

<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">
<Link
href={`${ROUTES.POLICY}?type=SERVICE`}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{chunks}
</a>
</Link>
),
// TODO: 실제 개인정보 처리방침 페이지 URL 나오면 href 교체
privacy: (chunks) => (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a href="#" className="underline">
<Link
href={`${ROUTES.POLICY}?type=PRIVACY`}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{chunks}
</a>
</Link>
),
})}
</p>
Expand Down
24 changes: 24 additions & 0 deletions apps/timo-web/app/[locale]/policy/_containers/PolicyContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { useTranslations } from "next-intl";

import type { TermsType } from "@/types/terms-type";

import { PolicyDocument } from "@/components/policy/PolicyDocument";
import { useTerms } from "@/queries/use-terms";

export interface PolicyContainerProps {
type: TermsType;
}

export const PolicyContainer = ({ type }: PolicyContainerProps) => {
const t = useTranslations("Policy");
const { data: terms } = useTerms(type);
const term = terms.find((item) => item.type === type);

if (!term) {
return <p className="typo-body-m-12 text-timo-gray-700">{t("empty")}</p>;
}

return <PolicyDocument title={term.title} content={term.content} />;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
35 changes: 35 additions & 0 deletions apps/timo-web/app/[locale]/policy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import timoTextLogo from "@repo/timo-design-system/assets/images/logo/timo-text-logo.svg";
import Image from "next/image";

import { PolicyContainer } from "@/app/[locale]/policy/_containers/PolicyContainer";
import { AsyncBoundary } from "@/components/boundary/AsyncBoundary";
import { ROUTES } from "@/constants/routes";
import { Link } from "@/i18n/navigation";
import { termsTypeSchema } from "@/types/terms-type";

interface PolicyPageProps {
searchParams: Promise<{ type?: string }>;
}

export default async function PolicyPage({ searchParams }: PolicyPageProps) {
const { type } = await searchParams;
const parsedType = termsTypeSchema.safeParse(type);

return (
<div className="flex min-h-screen w-full flex-col items-center gap-12.5 bg-white">
<div className="flex w-full items-center justify-between px-37 py-6.75">
<Link href={ROUTES.LOGIN}>
<Image src={timoTextLogo} alt="Timo" width={92} height={35} />
</Link>
</div>

<div className="flex w-full justify-center px-8 pb-20">
<AsyncBoundary>
<PolicyContainer
type={parsedType.success ? parsedType.data : "SERVICE"}
/>
</AsyncBoundary>
</div>
</div>
);
}
27 changes: 27 additions & 0 deletions apps/timo-web/components/policy/PolicyDocument.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface PolicyDocumentProps {
title: string;
content: string;
headingLevel?: "h1" | "h2";
}

export const PolicyDocument = ({
title,
content,
headingLevel = "h1",
}: PolicyDocumentProps) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

헤딩 레벨을 props로 전달한 이유가 따로 있나요?

const Heading = headingLevel;

return (
<div className="flex w-full max-w-[1147px] flex-col gap-7.5">
<div className="flex flex-col gap-5">
<Heading className="typo-headline-b-16 text-timo-gray-900 w-full">
{title}
</Heading>
<hr className="border-timo-gray-500 w-full" />
</div>
<p className="typo-body-m-12 text-timo-gray-900 whitespace-pre-wrap">
{content}
</p>
</div>
);
};
1 change: 1 addition & 0 deletions apps/timo-web/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const ROUTES = {
FOCUS: "/focus",
STATISTICS: "/statistics",
SETTINGS: "/settings",
POLICY: "/policy",
} as const;
6 changes: 5 additions & 1 deletion apps/timo-web/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"Settings": {
"nav": {
"account": "Account",
"policy": "Terms & Policies",
"policy": "Terms of Service",
"privacy": "Privacy Policy",
"withdrawal": "Delete account"
},
"profile": {
Expand Down Expand Up @@ -226,5 +227,8 @@
"hourUnit": "h",
"minuteUnit": "m"
}
},
"Policy": {
"empty": "This policy content isn't available yet."
}
}
4 changes: 4 additions & 0 deletions apps/timo-web/messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"nav": {
"account": "개인 정보",
"policy": "약관",
"privacy": "개인 정보 처리 방침",
"withdrawal": "회원 탈퇴"
},
"profile": {
Expand Down Expand Up @@ -223,5 +224,8 @@
"hourUnit": "시간",
"minuteUnit": "분"
}
},
"Policy": {
"empty": "약관 내용을 아직 준비 중입니다."
}
}
19 changes: 19 additions & 0 deletions apps/timo-web/queries/use-terms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { useSuspenseQuery } from "@tanstack/react-query";

import {
getGetTermsQueryKey,
getTerms,
} from "@/api/generated/endpoints/terms/terms";
import { termsListSchema, type TermsType } from "@/types/terms-type";

export const useTerms = (type?: TermsType) => {
const params = type ? { type } : undefined;

return useSuspenseQuery({
queryKey: getGetTermsQueryKey(params),
queryFn: ({ signal }) => getTerms(params, undefined, signal),
select: ({ data }) => termsListSchema.parse(data).terms,
});
};
17 changes: 17 additions & 0 deletions apps/timo-web/types/terms-type.ts

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.

해당 스키마가 api/generated/endpoints/terms/terms.zod.ts의 GetTermsResponse랑 내용이 겹치는 것 같아서 새로 만들 필요 없이 가져다 쓰면 좋을 것 같아요! 확인 부탁드립니다

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.

terms.zod.ts의 GetTermsResponse는 type을 그냥 zod.string()으로 생성해서 "SERVICE" | "PRIVACY" 리터럴 타입을 잃게 돼요. 이 리터럴 타입은 PolicyContainer/SettingsTermsContainer의 type prop, useTerms(type?: TermsType), page.tsx의 termsTypeSchema.safeParse(type) 쿼리 파라미터 검증까지 앱 전반에서 실제로 쓰이고 있어서, 이 타입 좁히기 자체가 terms-type.ts가 존재하는 이유입니다.

또한 GetTermsResponse는 status/message/data 래퍼 안에 terms 배열이 인라인으로 정의되어 있고, 아이템 단위 스키마가 별도로 export되어 있지 않아요. 재사용하려면 GetTermsResponse.shape.data.unwrap().shape.terms.element 같은 방식으로 생성 파일 내부 구조에 직접 접근해야 하는데, 이 파일은 "Do not edit manually" 주석이 붙은 orval 자동 생성 파일이라 재생성 시 구조가 바뀌면 조용히 깨질 수 있습니다.

그래서 termsItemSchema/termsListSchema 를 통째로 제거하기보다는, termsTypeSchema(리터럴 enum)는 그대로 유지하는 게 맞다고 생각이들어요.

혜원님은 어떻게 생각하시나요?

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.

아하 설명 감사합니다! ✌🏻
생성된 스키마를 재사용하는 쪽만 생각했는데 TermsType의 리터럴 타입과 orval 내부 구조 의존성까지 생각하면 지금처럼 별도 스키마를 유지하는 게 맞는 것 같습니다!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TermsTypeSchema 부분만 별도로 사용하면 될 것 같아요~

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from "zod";

export const termsTypeSchema = z.enum(["SERVICE", "PRIVACY"]);

export const termsItemSchema = z.object({
termsId: z.number(),
type: termsTypeSchema,
title: z.string(),
content: z.string(),
});

export const termsListSchema = z.object({
terms: z.array(termsItemSchema),
});

export type TermsType = z.infer<typeof termsTypeSchema>;
export type TermsItem = z.infer<typeof termsItemSchema>;
Loading