[FEAT] 약관 페이지 및 설정 약관 탭 구현 / 약관 API 연결 - #167
Conversation
- 약관 조회 API(GET /api/v1/terms)를 연동해 독립 /policy 페이지와 설정 > 약관 탭을 구현했습니다 - 로그인 페이지의 이용약관·개인정보 처리방침 링크를 /policy?type=SERVICE, /policy?type=PRIVACY로 연결하고, 약관 로고 클릭 시 로그인 페이지로 이동하도록 했습니다 - 약관 데이터가 없을 때(현재 백엔드 미시딩 상태) 보여줄 빈 상태 문구를 추가했습니다
- 최신 화면설계서에 맞춰 설정의 "약관" 탭을 서비스 이용약관(SERVICE)/개인정보 처리방침(PRIVACY) 두 개의 별도 탭으로 분리했습니다 - 두 탭이 공유하는 레이아웃 컴포넌트(PolicyDocument)를 도메인 간 공유 위치(components/policy)로 옮겼습니다 - 영문 "약관" 탭 라벨을 Figma 영문 레퍼런스에 맞춰 "Terms & Policies" → "Terms of Service"로 수정했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Walkthrough약관 타입과 API 응답 검증, 약관 조회 훅, 독립 정책 페이지, 설정의 서비스·개인정보 탭, 로그인 화면의 정책 링크가 추가되었습니다. 약관이 없을 때 표시할 다국어 메시지와 공통 문서 렌더러도 구현되었습니다. Changes약관 정책 흐름
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant LoginContainer
participant PolicyPage
participant PolicyContainer
participant useTerms
participant PolicyDocument
LoginContainer->>PolicyPage: SERVICE 또는 PRIVACY 링크 열기
PolicyPage->>PolicyContainer: 검증된 type 전달
PolicyContainer->>useTerms: type별 약관 조회
useTerms-->>PolicyContainer: 검증된 terms 반환
PolicyContainer->>PolicyDocument: title과 content 전달
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with 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.
Inline comments:
In
`@apps/timo-web/app/`[locale]/(main)/settings/_containers/SettingsTabsContainer.tsx:
- Around line 13-26: Refactor the tab handling in the settings tabs container to
map the “policy” and “privacy” tab values to their respective
SettingsPolicyContainer and SettingsPrivacyContainer components, then render the
selected component once inside a shared AsyncBoundary. Preserve the existing
behavior for both tabs while removing the duplicated conditional rendering.
In
`@apps/timo-web/app/`[locale]/(main)/settings/policy/_containers/SettingsPolicyContainer.tsx:
- Around line 8-19: Extract the shared terms-rendering logic from
SettingsPolicyContainer and SettingsPrivacyContainer into a single
SettingsTermsContainer component that accepts a TermsType prop, then use that
component from SettingsTabsContainer with “SERVICE” for policy and “PRIVACY” for
privacy. Preserve the existing translation, layout, PolicyDocument, and
empty-state rendering behavior.
In
`@apps/timo-web/app/`[locale]/(main)/settings/privacy/_containers/SettingsPrivacyContainer.tsx:
- Around line 8-22: Extract the duplicated useTerms, translation,
term-selection, and empty-state rendering from SettingsPrivacyContainer and
PolicyContainer into a shared PolicyContent component. Make PolicyContent accept
the terms type and heading level as props, preserve the existing PolicyDocument
rendering and Policy empty-state translation, then update both containers to
delegate to it.
In `@apps/timo-web/app/`[locale]/policy/_containers/PolicyContainer.tsx:
- Around line 14-24: Remove the terms[0] fallback in PolicyContainer so only a
term whose type matches the requested type is rendered. Preserve the existing
empty-state response when no matching term is found, and keep the PolicyDocument
rendering unchanged for a valid match.
In `@apps/timo-web/app/`[locale]/policy/page.tsx:
- Around line 11-12: Move the isTermsType type guard out of page.tsx into the
existing terms-type.ts module, or replace its usage with
termsTypeSchema.safeParse. Update page.tsx to import and reuse the shared
validation logic, keeping routing and layout composition free of hardcoded
terms-type business logic and synchronized with the schema.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9a09ceb5-cf1a-47cd-9f8c-0e7854a54ea9
📒 Files selected for processing (14)
apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsNavContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTabsContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_hooks/useSettingsTab.tsapps/timo-web/app/[locale]/(main)/settings/policy/_containers/SettingsPolicyContainer.tsxapps/timo-web/app/[locale]/(main)/settings/privacy/_containers/SettingsPrivacyContainer.tsxapps/timo-web/app/[locale]/login/_containers/LoginContainer.tsxapps/timo-web/app/[locale]/policy/_containers/PolicyContainer.tsxapps/timo-web/app/[locale]/policy/page.tsxapps/timo-web/components/policy/PolicyDocument.tsxapps/timo-web/constants/routes.tsapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonapps/timo-web/queries/use-terms.tsapps/timo-web/types/terms-type.ts
| export const SettingsPrivacyContainer = () => { | ||
| const t = useTranslations("Policy"); | ||
| const { data: terms } = useTerms("PRIVACY"); | ||
| 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> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
PolicyContainer와 로직 중복 — 공통 추출을 제안합니다
SettingsPrivacyContainer는 PolicyContainer와 동일한 패턴을 따릅니다: useTerms 호출 → term 선택 → PolicyDocument 또는 빈 상태 렌더링. 두 컨테이너 모두 useTranslations("Policy")와 t("empty")를 사용합니다.
이 패턴을 공통 컴포넌트로 추출하면 유지보수성이 향상됩니다. 예를 들어 PolicyContent 컴포넌트를 만들어 type과 headingLevel을 prop으로 받도록 하면, 두 컨테이너에서 각각 재사용할 수 있습니다.
♻️ 공통 PolicyContent 컴포넌트 추출 제안
// components/policy/PolicyContent.tsx
"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 PolicyContentProps {
type: TermsType;
headingLevel?: "h1" | "h2";
}
export const PolicyContent = ({ type, headingLevel = "h1" }: PolicyContentProps) => {
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} headingLevel={headingLevel} />;
};그 후 SettingsPrivacyContainer는 단순화:
"use client";
-import { useTranslations } from "next-intl";
-
-import { PolicyDocument } from "`@/components/policy/PolicyDocument`";
-import { useTerms } from "`@/queries/use-terms`";
+import { PolicyContent } from "`@/components/policy/PolicyContent`";
export const SettingsPrivacyContainer = () => {
- const t = useTranslations("Policy");
- const { data: terms } = useTerms("PRIVACY");
- 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>
- )}
+ <PolicyContent type="PRIVACY" headingLevel="h2" />
</div>
);
};🤖 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]/(main)/settings/privacy/_containers/SettingsPrivacyContainer.tsx
around lines 8 - 22, Extract the duplicated useTerms, translation,
term-selection, and empty-state rendering from SettingsPrivacyContainer and
PolicyContainer into a shared PolicyContent component. Make PolicyContent accept
the terms type and heading level as props, preserve the existing PolicyDocument
rendering and Policy empty-state translation, then update both containers to
delegate to it.
There was a problem hiding this comment.
해당 스키마가 api/generated/endpoints/terms/terms.zod.ts의 GetTermsResponse랑 내용이 겹치는 것 같아서 새로 만들 필요 없이 가져다 쓰면 좋을 것 같아요! 확인 부탁드립니다
There was a problem hiding this comment.
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)는 그대로 유지하는 게 맞다고 생각이들어요.
혜원님은 어떻게 생각하시나요?
There was a problem hiding this comment.
아하 설명 감사합니다! ✌🏻
생성된 스키마를 재사용하는 쪽만 생각했는데 TermsType의 리터럴 타입과 orval 내부 구조 의존성까지 생각하면 지금처럼 별도 스키마를 유지하는 게 맞는 것 같습니다!
There was a problem hiding this comment.
TermsTypeSchema 부분만 별도로 사용하면 될 것 같아요~
- 설정 탭의 서비스 이용약관/개인정보 처리방침 컨테이너를 SettingsTermsContainer로 통합해 중복된 조회·번역·빈 상태 로직을 제거했습니다 - PolicyContainer에서 타입이 일치하지 않는 약관을 렌더링하던 폴백을 제거했습니다 - policy 페이지의 약관 타입 검증을 termsTypeSchema.safeParse로 교체해 스키마와 동기화했습니다 - PolicyDocument에 headingLevel prop을 추가해 설정 탭에서 제목 레벨을 조정할 수 있도록 했습니다
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTermsContainer.tsx (1)
14-30: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
terms[0]대신terms.find()를 사용해PolicyContainer와 일관성을 맞추세요
PolicyContainer는 과거 리뷰 피드백을 반영해terms.find((item) => item.type === type)를 사용합니다. 반면SettingsTermsContainer는terms[0]를 직접 사용합니다. API가 요청한 타입과 다른 약관을 반환할 경우,terms[0]는 잘못된 타입의 약관을 렌더링하게 됩니다. 두 컨테이너가 동일한useTerms훅을 공유하므로 조회 로직도 일관되게 유지하는 것이 안전합니다.🔄 Proposed fix: `terms.find()`로 일관성 확보
export const SettingsTermsContainer = ({ type, }: SettingsTermsContainerProps) => { const t = useTranslations("Policy"); const { data: terms } = useTerms(type); - const term = terms[0]; + const term = terms.find((item) => item.type === type);🤖 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]/(main)/settings/_containers/SettingsTermsContainer.tsx around lines 14 - 30, Update SettingsTermsContainer to select the term with terms.find((item) => item.type === type) instead of terms[0], matching PolicyContainer and ensuring only the requested policy type is rendered.
🤖 Prompt for all review comments with 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.
Outside diff comments:
In
`@apps/timo-web/app/`[locale]/(main)/settings/_containers/SettingsTermsContainer.tsx:
- Around line 14-30: Update SettingsTermsContainer to select the term with
terms.find((item) => item.type === type) instead of terms[0], matching
PolicyContainer and ensuring only the requested policy type is rendered.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a8eb0f14-df74-4620-a94a-2a55ce23e6c4
📒 Files selected for processing (7)
apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTabsContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTermsContainer.tsxapps/timo-web/app/[locale]/(main)/settings/policy/_components/.gitkeepapps/timo-web/app/[locale]/(main)/settings/policy/_queries/.gitkeepapps/timo-web/app/[locale]/policy/_containers/PolicyContainer.tsxapps/timo-web/app/[locale]/policy/page.tsxapps/timo-web/components/policy/PolicyDocument.tsx
There was a problem hiding this comment.
TermsTypeSchema 부분만 별도로 사용하면 될 것 같아요~
| headingLevel = "h1", | ||
| }: PolicyDocumentProps) => { |
- 설정 탭·독립 정책 페이지 모두 h1을 그대로 쓰고 있어 사용되지 않던 headingLevel prop을 제거했습니다 - 설정 탭들은 각자 독립적인 h1을 갖는 게 이 프로젝트의 기존 패턴이라 heading 레벨을 조정할 필요가 없었습니다
…to feat/web/163-terms-policy-pages # Conflicts: # apps/timo-web/messages/en.json # apps/timo-web/messages/ko.json
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@apps/timo-web/messages/ko.json`:
- Around line 206-207: Update the switchButton translation in the Korean
messages to a label describing the switch action, such as “전환하기,” instead of
duplicating the completeButton translation “완료하기.”
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: be00372d-08cf-44c5-a0b2-c0f0a1ec992e
📒 Files selected for processing (3)
apps/timo-web/components/policy/PolicyDocument.tsxapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.json
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@apps/timo-web/messages/ko.json`:
- Around line 206-207: Update the switchButton translation in the Korean
messages to a label describing the switch action, such as “전환하기,” instead of
duplicating the completeButton translation “완료하기.”
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: be00372d-08cf-44c5-a0b2-c0f0a1ec992e
📒 Files selected for processing (3)
apps/timo-web/components/policy/PolicyDocument.tsxapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.json
🛑 Comments failed to post (1)
apps/timo-web/messages/ko.json (1)
206-207: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
switchButton번역을 전환 동작에 맞게 수정해 주세요.
switchButton이"완료하기"로 번역되어completeButton과 동일합니다. 사용자가 작업 전환과 완료를 구분할 수 없으므로"전환하기"등 실제 동작에 맞는 라벨로 변경해야 합니다. 메시지 키별 locale 번역을 관리하는 방식은 next-intl 공식 문서도 참고할 수 있습니다. (learn.next-intl.dev)🤖 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/messages/ko.json` around lines 206 - 207, Update the switchButton translation in the Korean messages to a label describing the switch action, such as “전환하기,” instead of duplicating the completeButton translation “완료하기.”
ISSUE 🔗
close #163
What is this PR? 🔍
로그인 페이지의 "이용약관"/"개인정보 처리방침" 링크와 설정 > 약관 탭을 실제 약관 조회 API(
GET /api/v1/terms)로 연동하고, 최신 화면설계서에 맞춰 설정 쪽 탭을 서비스 이용약관/개인정보 처리방침 두 개로 분리했습니다.배경
href="#"placeholder였고, 설정 > 약관 탭도 제목만 있는 빈 스텁이었습니다.GET /api/v1/terms,type=SERVICE|PRIVACY)가 준비돼 있었는데 연동이 안 돼 있었고, 작업 중간에 화면설계서가 갱신되어 설정의 "약관" 탭이 서비스 이용약관/개인정보 처리방침 두 개의 별도 탭으로 나뉘는 것으로 확인됐습니다.useTerms)과 로컬 zod 검증 스키마를 만들어 독립/policy페이지와 설정의 두 탭이 같은 데이터 소스를 쓰도록 하고, 레이아웃 컴포넌트(PolicyDocument)는 도메인 간 공유 위치로 옮겨 중복 없이 재사용했습니다.독립
/policy페이지/policy라우트를 추가하고,?type=SERVICE/?type=PRIVACY쿼리로 각각 다른 약관만 표시하도록 했습니다.page.tsx(서버 컴포넌트)가searchParams.type을 읽어 유효하지 않으면SERVICE로 기본값 처리하고, 로고만 있는 정적 헤더를 먼저 렌더링한 뒤AsyncBoundary로 감싼PolicyContainer(클라이언트,useTerms(type))가 본문을 스트리밍합니다. 로고 클릭 시 로그인 페이지로 돌아갑니다.Policy.empty)를 추가했습니다.공유 데이터 레이어
types/terms-type.ts(zod 스키마)와queries/use-terms.ts(useSuspenseQuery훅)를 앱 전역 공유 위치에 추가했습니다./policy페이지와 설정 두 탭이 동일한 API 데이터를 쓰는데, 도메인 간 직접 import는 금지돼 있어 공유 로직을 도메인 밖으로 뽑아야 했습니다.getGetTermsQueryKey+getTerms(생성 코드)로queryKey/queryFn을 직접 조립하고,select에서termsListSchema.parse()로 응답을 검증해 백엔드 스펙이 조용히 바뀌어도 파싱 실패로 드러나게 했습니다.type을 생략하면 전체 약관을 조회합니다.설정 약관 탭 분리
useSettingsTab에"privacy"를 추가하고, 설정 사이드바를 개인 정보 / 약관 / 개인 정보 처리 방침 / 회원 탈퇴 4개 탭으로 늘렸습니다.SettingsPolicyContainer는SERVICE약관만, 신규SettingsPrivacyContainer는PRIVACY약관만PolicyDocument로 표시합니다.PolicyDocument(제목+구분선+본문)를app/[locale]/policy/_components에서components/policy/로 옮겨/policy페이지와 설정 두 탭이 함께 씁니다. 영문 "약관" 탭 라벨은 Figma 영문 레퍼런스를 근거로 "Terms & Policies" → "Terms of Service"로 수정했습니다(순서는 한국어 디자인 기준을 따라 약관이 개인정보 처리방침보다 먼저 오도록 유지).To Reviewers
/settings,/focus처럼 로그인 가드가 걸린 라우트라 헤드리스 브라우저로 설정 탭 클릭까지는 직접 확인하지 못했고,/policy독립 페이지와 로그인 링크 연결만 실제로 확인했습니다. 로그인 가능한 환경에서 설정 탭 전환도 한 번 확인해 주시면 좋겠습니다.Screenshot 📷
/policy빈 상태Test Checklist ✔
pnpm check-types통과pnpm lint통과/ko/policy?type=SERVICE,/ko/policy?type=PRIVACY실제 API(https://api.timo.kr/api/v1/terms) 호출 및 렌더링 확인/policy?type=...로 연결되는지 확인/policy로고 클릭 시 로그인 페이지로 이동하는지 확인